C++ 静的メンバ

main.cpp

#include "rat.h"
#include <iostream>

using namespace std;

int main() {
	CRat *r1, *r2, *r3;
	r1 = new CRat();    //  一匹目のネズミ生成
	r1->squeak();
	CRat::showNum();    //  ネズミの数を表示
	r2 = new CRat();    //  二匹目のネズミ生成
	r3 = new CRat();    //  三匹目のネズミ生成
	r2->squeak();
	r3->squeak();
	delete r1;          //  一匹目のネズミ消去
	delete r2;          //  二匹目のネズミ消去
	CRat::showNum();    //  ネズミの数を表示
	delete r3;          //  三匹目のネズミ消去
	CRat::showNum();    //  ネズミの数を表示
	return 0;
}

rat.cpp

#include "rat.h"
#include <iostream>

using namespace std;

//  ネズミの数の初期値を0に設定
int CRat::m_count = 0;

//  コンストラクタ
CRat::CRat() : m_id(0) {
	m_id = m_count;    //  ネズミの数を、IDとする。
	m_count++;        //  ネズミの数を一つ増やす
}
//  デストラクタ
CRat::~CRat() {
	cout << "ネズミ:" << m_id << "消去" << endl;
	m_count--;        //  ネズミの数を一つ減らす
}
//  ネズミの数の出力
void CRat::showNum()
{
	cout << "現在のネズミの数は、" << m_count << " 匹です。" << endl;
}
//  ネズミが鳴く
void CRat::squeak()
{
	cout << m_id << ":" << "チューチュー" << endl;
}

rat.h

#ifndef _RAT_H_
#define _RAT_H_

class CRat {
public:
	//  コンストラクタ
	CRat();
	//  デストラクタ
	~CRat();
	//  ネズミの数の出力
	static void showNum();
	//  ネズミが鳴く
	void squeak();
private:
	//  ネズミの番号
	int m_id;
	//  ネズミの数
	static int m_count;
};

#endif /* _RAT_H_ */

『ゲド戦記(アースシー)』リメイク企画書(案)

『ゲド戦記(アースシー)』リメイク企画書(案)

1. 企画タイトル

『EARTHSEA:影(仮)』
サブタイトル例:「均衡の海」「影の名」「死者の国」(章構成に合わせて変更)

2. 企画概要(ログライン)

“名前”を失った天才少年ゲドが、自ら生んだ“影”と向き合い、世界の均衡と死の意味を学ぶ。
剣と魔法ではなく、言葉・沈黙・選択で進むファンタジー。

3. 企画意図(なぜ今やるか)

  • 既存映像化は評価が割れやすく、原作ファンにとって「本来の核」が届ききっていない(=“取り返す価値”がある)。
    ル=グウィン本人もジブリ版への所感を公式に残している。
  • いま強いのは、派手さより 世界観の一貫性・テーマ性・キャラクターの内面 を最後まで描ける作品。
  • 「多島海世界アースシー」を、海・風・距離で見せられれば差別化できる(既存映像化の弱点を正面から潰せる)。

4. 原作/権利整理(前提)

  • 原作:アーシュラ・K・ル=グウィン『Earthsea(邦題:ゲド戦記)』シリーズ(1968–2001)
  • 既存:スタジオジブリ映画『ゲド戦記』(2006)
  • 2019年にA24とジェニファー・フォックスがTVシリーズ化を開発中と報道(=同一権利が進行中の可能性があるため、着手前に必ず権利クリア)。

結論:本企画は「ジブリ映画の作り直し」ではなく、原作準拠の“再映像化(リブート)” として設計する。

5. ターゲット

  • コア:原作読者/ハイファンタジー好き(20–50代)
  • 拡張:重厚ドラマ好き、ポスト・ジブリ層、海外配信ユーザー
  • 年齢区分:PG-13想定(暴力は抑えず、露悪にはしない)

6. フォーマット(推奨)

配信アニメ・リミテッドシリーズ

  • 1話45〜55分 × 8話(1シーズン=1冊を基本)
  • まずは Season1:『影との戦い(A Wizard of Earthsea)』 を完結させる
  • 成功後に、2冊目以降をシーズン更新(世界を“育てる”)

理由:原作の芯は「出来事」より「変化」。映画尺だと削るしかない。シリーズが正解。

7. コンセプト

キーワード

  • 均衡(Balance)
  • 真の名(True Name)
  • 影(Shadow)
  • 死と境界(The Dry Land)
  • 海(Sea)

ルール(制作の縛り)

  • 魔法は“光学兵器”ではない。言葉と代償として扱う。
  • 竜は「怪獣」ではない。別の位相の知性として出す。
  • 派手なカットより、風・水・沈黙・距離で世界を感じさせる。

8. ストーリー方針(Season1 全8話)

原作1巻の骨格に沿い、ドラマとして強くする(※固有名詞は原作表記準拠で整理)

1話:島の少年、才能、傲慢、破局(影の解放)
2話:師との時間/“名”の意味/恐怖の芽
3話:旅立ち/各島の文化差を見せる(世界紹介回)
4話:影の追跡/逃げるほど濃くなる
5話:海の章(船旅を核にする)
6話:竜/言葉の届かない相手との対峙
7話:死者の国の入口(クライマックス前)
8話:影に“名”を返す/均衡の回復/静かな帰還

9. キャラクター設計(要点だけ)

  • ゲド:才能=正しさではない。傲慢→恐怖→受容の変化を丁寧に。
  • 師(オジオン等):説教しない。“見せる”導き。
  • 同行者:説明要員にしない。各話で「均衡」の別の側面を担う。
  • :怖いが、低俗にしない。“上位存在”の気配を保つ。

10. ビジュアル&音

  • 色:島ごとに空気が違う(湿度・土の色・植生で差を出す)
  • 海:CGに頼り切らず、手描き/2D表現で“水の重さ”を作る
  • 音:BGMは盛り上げすぎない。風、帆、波、沈黙を主役にする
  • 主題歌:強いメロではなく、余韻型(多言語展開しやすい)

11. 差別化ポイント(勝ち筋)

  1. 原作の順序とテーマを守る(混ぜない、急がない)
  2. 海を撮る(アースシーの主役は海。ここで勝つ)
  3. アクションで誤魔化さず、内面の変化で見せ切る
  4. 世界観監修を厚くし、文化・言語・命名を作り込む

12. 制作体制(想定)

  • ショーランナー:原作理解が深い脚本統括(ファンタジー経験者)
  • 監督:空気演出に強い人(静と動の設計ができる)
  • 監修:
    • 原作権利元(エステート/出版社)
    • 翻訳・言語設定(命名ルールの整合)
    • 海洋/帆船の所作(リアリティライン)

13. マーケ/展開

  • ローンチ前:
    • “海”のティザー(30秒)→ 世界の空気だけ見せる
    • “真の名”コピーで刺す(例:「名前を呼べ。影が止まる。」)
  • 連動:
    • 原作新装版/図説、設定資料集
    • サントラ(環境音重視)
    • アートブック(島別の風景)

14. スケジュール(ざっくり)

  • 開発(脚本・設定):6〜10か月
  • プリプロ:4〜6か月
  • 本制作:12〜18か月
  • 仕上げ:3〜5か月
    ※シリーズは最初の設計が全て。脚本に時間を使う(ここを削ると終わる)

15. 予算レンジ(目安)

  • 45〜55分×8話の高品質アニメは、通常TVアニメより上のレンジになる想定
  • 重要:予算を“作画枚数”に振るより、美術・撮影・音・海表現に集中投下する

16. リスクと対策

  • 権利状況が競合する可能性(2019報道案件)
    最初に権利クリア。ダメなら「別企画(オリジナル多島海ファンタジー)」にピボット可能な設計も同時に用意。
  • 原作ファンの警戒
    → “混ぜない・急がない・海を撮る”を明文化し、監修体制を前面に出す。
  • 退屈に見えるリスク
    → 各話に「島の文化の違い」「選択の代償」を置き、静かな密度で引っ張る。

BlueprintをAIで自動化

それ、方向性はめちゃくちゃアリ。**「BlueprintをAIで自動化」**は実際に現実的にできるし、作り方次第で“作業の8割”をAIに投げられる。

ただし結論から言うと、

  • **AIに「ノードを勝手に配置して配線まで完璧に」**を 毎回100% はまだ難しい
  • でも **「設計→雛形生成→反復修正」**の流れにすると、体感はほぼ自動化できる

できる自動化(強い順)

1) 仕様 → ブループリント設計図(超強い)

例:「Eキーで拾う」「APが1分で回復」「HP0でGameOver」みたいな文章から、

  • 使う変数
  • イベントの流れ
  • 分岐条件
  • UI更新タイミング
    設計として吐かせる
    ここが一番効く。人間が迷わなくなる。

2) ブループリント用の“ノード手順書”を生成(強い)

AIが

  • どのノードを置く
  • どこに繋ぐ
  • 変数名は何にする
    箇条書きで出す
    あなたはUEでその通りに組むだけ。ほぼコピペ作業。

3) Blueprintの代わりにC++を生成して置き換える(実務最強)

「BPで組むと大変」なやつ(AP回復、セーブ、データ管理、複雑なステート)は

  • AIにC++を書かせる
  • BPからその関数を呼ぶ
    が最強ルート。BPはUIとイベントだけにする。

さらに攻めるなら(ガチ自動化)

4) Editor Utility(Blutility)で“ノード生成”をスクリプト化

UEにはEditor Utility Blueprint / Editor Utility Widgetがあって、
エディタ上で作業を自動化できる。
ここを使うと「定型BPを大量に作る」系に効く。

※ただ、BPグラフを完全に自動で組むのは、UEのAPIやプラグイン領域が絡むから、
「まずはテンプレ生成」から入るのが現実的。


いちばん現実的な完成形(おすすめ)

AIがやる:

  1. 仕様を読んで、BP構造(イベント→分岐→処理)を設計
  2. 変数表、関数表、必要なWidget、必要なActor/Componentをリスト化
  3. ノード手順書(どのノードをどこに繋ぐか)を出力
  4. バグりやすいところ(BeginPlay順序、Replicate、UI更新漏れ)をチェックリスト化

あなたがやる:

  • UE上で実装(写経)
  • 動いたらスクショ投げる
    → AIが次の修正を指示

このループが回ると、体感「BPが自動で組み上がっていく」になる。


まずは試せるミニ実験(今日から使える)

やりたい機能を1個だけ文章で書く(例):

「Eキーで近くの武器を拾ってインベントリに入れて、UIに表示。すでに持ってたら入れ替え」

これを投げてくれたら、俺がそのまま

  • 変数一覧(型まで)
  • BP構成(Player / Weapon / InventoryComponent / Widget)
  • ノード手順(イベントグラフの繋ぎ)
  • つまずきポイント
    完成手順書として出す。

Vooglebrowser


<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<title>Voogle - Mini Browser</title>
<style>
  :root{
    --bg:#0b0f14;
    --panel: rgba(255,255,255,.06);
    --stroke: rgba(255,255,255,.10);
    --ink:#eaf0ff;
    --muted: rgba(234,240,255,.70);
    --accent:#7cf0ff;
    --accent2:#7ca0ff;
    --danger:#ff6b6b;
    --ok:#79ffa7;
    --shadow: 0 18px 50px rgba(0,0,0,.35);
    --radius:16px;
    --radius2:22px;
    --glass: blur(14px) saturate(1.2);
  }
  [data-theme="light"]{
    --bg:#f6f7fb;
    --panel: rgba(0,0,0,.05);
    --stroke: rgba(0,0,0,.10);
    --ink:#101828;
    --muted: rgba(16,24,40,.68);
    --shadow: 0 18px 50px rgba(16,24,40,.12);
  }
  *{box-sizing:border-box}
  html,body{height:100%}
  body{
    margin:0;
    background: radial-gradient(1200px 800px at 20% 10%, rgba(124,240,255,.15), transparent 60%),
                radial-gradient(1200px 800px at 80% 20%, rgba(124,160,255,.14), transparent 60%),
                var(--bg);
    color:var(--ink);
    font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Noto Sans JP", Arial;
    overflow:hidden;
  }
  .app{
    height:100%;
    display:grid;
    grid-template-columns: 320px 1fr;
    gap: 12px;
    padding: 12px;
  }
  .card{
    background: linear-gradient(180deg, rgba(255,255,255,.08), rgba(255,255,255,.03));
    border: 1px solid var(--stroke);
    border-radius: var(--radius2);
    box-shadow: var(--shadow);
    backdrop-filter: var(--glass);
    overflow:hidden;
    min-height:0;
  }
  .sidebar{display:flex; flex-direction:column; min-height:0;}
  .main{display:flex; flex-direction:column; min-height:0;}

  /* Topbar */
  .topbar{
    display:flex;
    align-items:center;
    gap:10px;
    padding:10px;
    border-bottom:1px solid var(--stroke);
    background: rgba(0,0,0,.08);
  }
  [data-theme="light"] .topbar{ background: rgba(255,255,255,.55); }
  .brand{
    display:flex; align-items:center; gap:10px;
    padding:10px;
    border-bottom:1px solid var(--stroke);
  }
  .logo{
    width:34px; height:34px; border-radius:12px;
    background: radial-gradient(circle at 30% 30%, var(--accent), rgba(124,240,255,.0) 55%),
                radial-gradient(circle at 70% 70%, var(--accent2), rgba(124,160,255,.0) 55%),
                rgba(255,255,255,.06);
    border:1px solid var(--stroke);
    box-shadow: 0 12px 30px rgba(124,240,255,.14);
  }
  .brand h1{
    font-size:14px; margin:0; letter-spacing:.4px;
  }
  .brand p{margin:0; font-size:12px; color:var(--muted)}
  .btn{
    appearance:none;
    border:1px solid var(--stroke);
    background: rgba(255,255,255,.06);
    color:var(--ink);
    padding:8px 10px;
    border-radius: 12px;
    cursor:pointer;
    transition: transform .08s ease, background .15s ease, border-color .15s ease;
    user-select:none;
    white-space:nowrap;
  }
  .btn:hover{ background: rgba(255,255,255,.10); border-color: rgba(124,240,255,.28); }
  .btn:active{ transform: scale(.98); }
  .btn.primary{
    border-color: rgba(124,240,255,.35);
    background: linear-gradient(180deg, rgba(124,240,255,.18), rgba(124,160,255,.10));
  }
  .btn.danger{
    border-color: rgba(255,107,107,.35);
    background: linear-gradient(180deg, rgba(255,107,107,.16), rgba(255,107,107,.08));
  }
  .btn.ok{
    border-color: rgba(121,255,167,.35);
    background: linear-gradient(180deg, rgba(121,255,167,.14), rgba(121,255,167,.07));
  }
  .icon{
    width:18px;height:18px;display:inline-grid;place-items:center;
    font-weight:700; opacity:.9;
  }

  /* Address */
  .addr{
    flex:1;
    display:flex;
    gap:10px;
    align-items:center;
    min-width:0;
  }
  .addr input{
    width:100%;
    min-width:0;
    padding:10px 12px;
    border-radius: 14px;
    border:1px solid var(--stroke);
    background: rgba(0,0,0,.14);
    color:var(--ink);
    outline:none;
  }
  [data-theme="light"] .addr input{ background: rgba(255,255,255,.75); }
  .hint{
    font-size:12px;
    color:var(--muted);
    padding: 0 12px 10px;
  }

  /* Tabs */
  .tabs{
    display:flex;
    gap:8px;
    padding:10px;
    border-bottom:1px solid var(--stroke);
    overflow:auto;
  }
  .tab{
    display:flex; align-items:center; gap:8px;
    padding:8px 10px;
    border-radius: 14px;
    border:1px solid var(--stroke);
    background: rgba(255,255,255,.06);
    cursor:pointer;
    min-width: 160px;
    max-width: 260px;
    flex: 0 0 auto;
  }
  .tab.active{
    border-color: rgba(124,240,255,.45);
    background: linear-gradient(180deg, rgba(124,240,255,.16), rgba(124,160,255,.10));
  }
  .tab .title{
    overflow:hidden;
    text-overflow:ellipsis;
    white-space:nowrap;
    font-size:13px;
    flex:1;
  }
  .pill{
    font-size:11px;
    color:var(--muted);
    border:1px solid var(--stroke);
    padding:2px 8px;
    border-radius:999px;
    background: rgba(0,0,0,.10);
  }
  [data-theme="light"] .pill{ background: rgba(255,255,255,.6); }
  .x{
    width:24px;height:24px; border-radius:10px;
    display:grid; place-items:center;
    border:1px solid var(--stroke);
    background: rgba(0,0,0,.10);
    opacity:.9;
  }
  .x:hover{ border-color: rgba(255,107,107,.5); }
  [data-theme="light"] .x{ background: rgba(255,255,255,.6); }

  /* Viewport */
  .viewport{
    position:relative;
    flex:1;
    min-height:0;
    background: rgba(0,0,0,.10);
  }
  [data-theme="light"] .viewport{ background: rgba(0,0,0,.03); }
  .frame{
    position:absolute; inset:0;
    width:100%; height:100%;
    border:0;
    background: transparent;
  }
  .overlay{
    position:absolute; inset: 14px;
    border-radius: 18px;
    border:1px dashed rgba(124,240,255,.35);
    display:none;
    place-items:center;
    text-align:center;
    padding:18px;
    background: rgba(0,0,0,.35);
    backdrop-filter: blur(10px);
  }
  [data-theme="light"] .overlay{ background: rgba(255,255,255,.78); }
  .overlay.show{ display:grid; }
  .overlay h2{margin:0 0 8px; font-size:16px;}
  .overlay p{margin:0 0 12px; color:var(--muted); font-size:13px;}
  .overlay .row{display:flex; gap:10px; flex-wrap:wrap; justify-content:center}

  /* Sidebar content */
  .section{
    padding:12px;
    border-top:1px solid var(--stroke);
    min-height:0;
    overflow:auto;
  }
  .section h3{
    margin:0 0 10px;
    font-size:12px;
    color:var(--muted);
    letter-spacing:.18em;
  }
  .list{
    display:flex;
    flex-direction:column;
    gap:8px;
  }
  .item{
    display:flex;
    gap:10px;
    align-items:center;
    padding:10px 10px;
    border-radius: 14px;
    border:1px solid var(--stroke);
    background: rgba(255,255,255,.05);
    cursor:pointer;
  }
  .item:hover{ border-color: rgba(124,240,255,.28); background: rgba(255,255,255,.08); }
  .item .meta{flex:1; min-width:0}
  .item .meta .t{
    font-size:13px;
    overflow:hidden; white-space:nowrap; text-overflow:ellipsis;
  }
  .item .meta .s{
    font-size:12px; color:var(--muted);
    overflow:hidden; white-space:nowrap; text-overflow:ellipsis;
  }
  .tag{
    font-size:11px;
    padding:2px 8px;
    border-radius: 999px;
    border:1px solid var(--stroke);
    color: var(--muted);
  }

  .footerbar{
    padding:10px 12px;
    border-top:1px solid var(--stroke);
    font-size:12px;
    color:var(--muted);
    display:flex;
    gap:12px;
    align-items:center;
    justify-content:space-between;
  }
  .kbd{
    border:1px solid var(--stroke);
    border-bottom-width:2px;
    padding:2px 6px;
    border-radius:8px;
    background: rgba(0,0,0,.10);
    font-size:11px;
    color:var(--muted);
    white-space:nowrap;
  }
  [data-theme="light"] .kbd{ background: rgba(255,255,255,.6); }

  .row{
    display:flex; gap:8px; flex-wrap:wrap;
  }
  .mini{
    font-size:12px;
    padding:6px 8px;
    border-radius: 12px;
  }

  .toast{
    position:fixed;
    right:14px; bottom:14px;
    padding:10px 12px;
    border-radius: 14px;
    border:1px solid var(--stroke);
    background: rgba(0,0,0,.50);
    backdrop-filter: blur(10px);
    color:var(--ink);
    box-shadow: var(--shadow);
    transform: translateY(10px);
    opacity:0;
    transition: .22s ease;
    pointer-events:none;
    max-width: min(420px, calc(100vw - 28px));
  }
  [data-theme="light"] .toast{ background: rgba(255,255,255,.86); }
  .toast.show{ transform: translateY(0); opacity:1; }
  .toast .small{ font-size:12px; color:var(--muted); margin-top:2px; }

  @media (max-width: 980px){
    .app{ grid-template-columns: 1fr; }
    .sidebar{ display:none; }
  }
</style>
</head>
<body data-theme="dark">
  <div class="app">
    <!-- Sidebar -->
    <aside class="card sidebar">
      <div class="brand">
        <div class="logo" aria-hidden="true"></div>
        <div>
          <h1>Voogle</h1>
          <p>Mini Browser (1-file)</p>
        </div>
      </div>

      <div class="section" style="border-top:none">
        <div class="row">
          <button class="btn mini primary" id="btnNewTab"><span class="icon">+</span>新規タブ</button>
          <button class="btn mini" id="btnToggleTheme"><span class="icon">☾</span>テーマ</button>
          <button class="btn mini" id="btnExport"><span class="icon">⤓</span>データ出力</button>
          <label class="btn mini" style="display:inline-flex; align-items:center; gap:8px; cursor:pointer;">
            <span class="icon">⤒</span>データ取込
            <input id="importFile" type="file" accept="application/json" style="display:none" />
          </label>
        </div>
        <div class="hint">※多くの外部サイトは埋め込み禁止。開けない時は「新しいタブで開く」。</div>
      </div>

      <div class="section">
        <h3>クイック</h3>
        <div class="list" id="quickList"></div>
      </div>

      <div class="section">
        <h3>ブックマーク</h3>
        <div class="list" id="bmList"></div>
      </div>

      <div class="section">
        <h3>履歴(最新20件)</h3>
        <div class="list" id="histList"></div>
      </div>

      <div class="footerbar">
        <div class="row" style="gap:6px">
          <span class="kbd">Ctrl</span>+<span class="kbd">L</span> アドレス
          <span class="kbd">Ctrl</span>+<span class="kbd">T</span> 新規
          <span class="kbd">Ctrl</span>+<span class="kbd">W</span> 閉じる
        </div>
        <span id="statusText">Ready</span>
      </div>
    </aside>

    <!-- Main -->
    <main class="card main">
      <div class="topbar">
        <button class="btn" id="btnBack" title="戻る"><span class="icon">←</span></button>
        <button class="btn" id="btnForward" title="進む"><span class="icon">→</span></button>
        <button class="btn" id="btnReload" title="更新"><span class="icon">↻</span></button>

        <div class="addr">
          <input id="addrInput" placeholder="URL または 検索ワード(例: https://example.com / openai)" autocomplete="off" />
        </div>

        <button class="btn primary" id="btnGo" title="移動"><span class="icon">⏎</span></button>
        <button class="btn" id="btnBookmark" title="ブックマーク"><span class="icon">☆</span></button>
        <button class="btn" id="btnOpenExternal" title="新しいタブで開く"><span class="icon">↗</span></button>
        <button class="btn" id="btnPip" title="PiP(対応サイトのみ)"><span class="icon">▣</span></button>
      </div>

      <div class="tabs" id="tabs"></div>

      <div class="viewport">
        <iframe id="frame" class="frame" sandbox="allow-forms allow-modals allow-popups allow-same-origin allow-scripts allow-downloads"></iframe>

        <div class="overlay" id="overlay">
          <div>
            <h2>このページは埋め込みを拒否してる</h2>
            <p>
              たいていはサイト側のセキュリティ(X-Frame-Options / CSP)です。<br>
              下のボタンで外部タブとして開け。
            </p>
            <div class="row">
              <button class="btn ok" id="overlayOpenExternal"><span class="icon">↗</span>新しいタブで開く</button>
              <button class="btn" id="overlayTrySearch"><span class="icon">⌕</span>検索で開く</button>
              <button class="btn danger" id="overlayClose"><span class="icon">×</span>閉じる</button>
            </div>
          </div>
        </div>
      </div>

      <div class="footerbar">
        <div class="row" style="gap:10px; align-items:center">
          <span class="tag" id="originTag">—</span>
          <span class="tag" id="secureTag">—</span>
          <span class="tag" id="embedTag">—</span>
        </div>
        <div class="row" style="gap:8px">
          <button class="btn mini" id="btnHome">ホーム</button>
          <button class="btn mini" id="btnClearHistory">履歴クリア</button>
          <button class="btn mini danger" id="btnResetAll">全リセット</button>
        </div>
      </div>
    </main>
  </div>

  <div class="toast" id="toast"></div>

<script>
(() => {
  // =========================
  // Utilities
  // =========================
  const $ = (sel) => document.querySelector(sel);
  const escapeHTML = (s) => (s ?? "").toString()
    .replaceAll("&","&amp;").replaceAll("<","&lt;")
    .replaceAll(">","&gt;").replaceAll('"',"&quot;").replaceAll("'","&#039;");
  const nowISO = () => new Date().toISOString();
  const isUrlLike = (text) => {
    const t = (text || "").trim();
    if (!t) return false;
    if (/^https?:\/\//i.test(t)) return true;
    if (/^[a-z0-9.-]+\.[a-z]{2,}([\/?#].*)?$/i.test(t)) return true;
    if (/^localhost(:\d+)?(\/.*)?$/i.test(t)) return true;
    if (/^\d{1,3}(\.\d{1,3}){3}(:\d+)?(\/.*)?$/.test(t)) return true;
    return false;
  };
  const normalizeToUrl = (text) => {
    let t = (text || "").trim();
    if (!t) return "";
    if (/^https?:\/\//i.test(t)) return t;
    if (isUrlLike(t)) return "https://" + t;
    return "";
  };
  const buildSearchUrl = (q) => "https://www.google.com/search?q=" + encodeURIComponent(q);

  const toast = (title, detail="") => {
    const el = $("#toast");
    el.innerHTML = `<div><b>${escapeHTML(title)}</b></div>${detail ? `<div class="small">${escapeHTML(detail)}</div>` : ""}`;
    el.classList.add("show");
    clearTimeout(toast._t);
    toast._t = setTimeout(() => el.classList.remove("show"), 2400);
  };

  const setStatus = (s) => { $("#statusText").textContent = s; };

  // =========================
  // Storage
  // =========================
  const KEY = "voogle.v1";
  const defaultState = () => ({
    theme: "dark",
    tabs: [
      { id: crypto.randomUUID(), title: "Home", url: "about:home", history: ["about:home"], hIndex: 0, createdAt: nowISO() }
    ],
    activeTabId: null,
    bookmarks: [
      { title:"OpenAI", url:"https://openai.com", addedAt: nowISO() },
      { title:"Wikipedia", url:"https://ja.wikipedia.org", addedAt: nowISO() },
      { title:"GitHub", url:"https://github.com", addedAt: nowISO() },
      { title:"MDN", url:"https://developer.mozilla.org/ja/", addedAt: nowISO() },
      { title:"YouTube", url:"https://www.youtube.com", addedAt: nowISO() },
    ],
    history: [],
    quick: [
      { title:"ニュース", url:"https://news.google.com/?hl=ja&gl=JP&ceid=JP:ja" },
      { title:"X", url:"https://x.com" },
      { title:"Reddit", url:"https://www.reddit.com" },
      { title:"Qiita", url:"https://qiita.com" },
      { title:"Zenn", url:"https://zenn.dev" },
      { title:"Google", url:"https://www.google.com" },
    ],
    settings: {
      homeUrl: "about:home",
      maxHistory: 200,
      sidebarHistoryView: 20
    }
  });

  const loadState = () => {
    try{
      const raw = localStorage.getItem(KEY);
      if(!raw) return defaultState();
      const s = JSON.parse(raw);
      // minimal migrate
      if(!s.tabs?.length) return defaultState();
      return s;
    }catch(e){
      console.warn(e);
      return defaultState();
    }
  };
  const saveState = () => localStorage.setItem(KEY, JSON.stringify(state));

  let state = loadState();

  // =========================
  // Tabs
  // =========================
  const getActiveTab = () => state.tabs.find(t => t.id === state.activeTabId) || state.tabs[0];
  const setActive = (id) => {
    state.activeTabId = id;
    saveState();
    renderAll();
    loadTabToViewport(getActiveTab());
  };

  const newTab = (url = "about:home", title = "New Tab") => {
    const tab = {
      id: crypto.randomUUID(),
      title,
      url,
      history: [url],
      hIndex: 0,
      createdAt: nowISO()
    };
    state.tabs.push(tab);
    state.activeTabId = tab.id;
    saveState();
    renderAll();
    loadTabToViewport(tab);
    toast("新規タブ", title);
  };

  const closeTab = (id) => {
    if(state.tabs.length <= 1){
      toast("これ以上閉じれない", "最低1タブは残る");
      return;
    }
    const idx = state.tabs.findIndex(t => t.id === id);
    if(idx < 0) return;
    const wasActive = state.activeTabId === id;
    const closed = state.tabs[idx];
    state.tabs.splice(idx,1);
    if(wasActive){
      const fallback = state.tabs[Math.max(0, idx-1)];
      state.activeTabId = fallback.id;
    }
    saveState();
    renderAll();
    loadTabToViewport(getActiveTab());
    toast("タブを閉じた", closed.title);
  };

  const setTabTitle = (tab, title) => {
    tab.title = (title || "Untitled").slice(0, 60);
    saveState();
    renderTabs();
  };

  const pushHistory = (tab, url) => {
    if(tab.history[tab.hIndex] === url) return;
    tab.history = tab.history.slice(0, tab.hIndex + 1);
    tab.history.push(url);
    tab.hIndex = tab.history.length - 1;
  };

  // =========================
  // History + Bookmarks
  // =========================
  const addGlobalHistory = (url, title="") => {
    if(!url || url === "about:home") return;
    state.history.unshift({ url, title, at: nowISO() });
    // de-dupe
    const seen = new Set();
    state.history = state.history.filter(h => {
      const k = h.url;
      if(seen.has(k)) return false;
      seen.add(k);
      return true;
    });
    state.history = state.history.slice(0, state.settings.maxHistory || 200);
    saveState();
    renderSidebar();
  };

  const isBookmarked = (url) => state.bookmarks.some(b => b.url === url);
  const toggleBookmark = () => {
    const tab = getActiveTab();
    const url = tab.url;
    if(!url || url === "about:home") return toast("ホームは登録しない");
    if(isBookmarked(url)){
      state.bookmarks = state.bookmarks.filter(b => b.url !== url);
      saveState();
      renderSidebar();
      toast("ブックマーク削除", url);
    }else{
      state.bookmarks.unshift({ title: tab.title || url, url, addedAt: nowISO() });
      saveState();
      renderSidebar();
      toast("ブックマーク追加", tab.title || url);
    }
    renderIndicators();
  };

  // =========================
  // Viewport Loader
  // =========================
  const frame = $("#frame");
  const overlay = $("#overlay");

  let embedBlockedTimer = null;

  const setOverlay = (show) => {
    overlay.classList.toggle("show", !!show);
  };

  const updateAddressBar = (tab) => {
    $("#addrInput").value = tab.url === "about:home" ? "" : tab.url;
  };

  const renderIndicators = () => {
    const tab = getActiveTab();
    const url = tab.url || "";
    const originTag = $("#originTag");
    const secureTag = $("#secureTag");
    const embedTag = $("#embedTag");

    let origin = "—";
    try{
      if(url.startsWith("about:")) origin = "about";
      else origin = (new URL(url)).hostname;
    }catch(e){ origin = "—"; }

    const secure = url.startsWith("https://") ? "HTTPS" : (url.startsWith("http://") ? "HTTP" : "—");
    originTag.textContent = origin;
    secureTag.textContent = secure;

    const bm = isBookmarked(url) ? "Bookmarked" : "Not bookmarked";
    embedTag.textContent = bm;
  };

  const loadTabToViewport = (tab) => {
    setOverlay(false);
    clearTimeout(embedBlockedTimer);
    renderIndicators();
    updateAddressBar(tab);

    const url = tab.url;

    if(url === "about:home"){
      frame.removeAttribute("src");
      frame.srcdoc = homeHTML();
      setStatus("Home");
      return;
    }

    frame.removeAttribute("srcdoc");
    frame.src = url;

    setStatus("Loading…");

    // "埋め込みブロック" は確実に検知できないが、
    // 一定時間で表示されなければ overlay を出して逃げ道を用意する。
    embedBlockedTimer = setTimeout(() => {
      // about:blank だったり、何も表示されないケースを想定
      // ここは「保険」なので強制表示ではなく、状況を見て出す
      // → タブのURLが外部なら基本出す
      if(getActiveTab().url === url){
        setOverlay(true);
        setStatus("Embed blocked (maybe)");
      }
    }, 1400);
  };

  frame.addEventListener("load", () => {
    clearTimeout(embedBlockedTimer);
    const tab = getActiveTab();
    // タイトルの推定は、クロスオリジンだと取れないのでURLから作る
    if(tab.url.startsWith("about:")){
      setStatus("Ready");
      return;
    }
    setOverlay(false);
    setStatus("Ready");
    addGlobalHistory(tab.url, tab.title);

    // タイトル推定
    let title = tab.title;
    try{
      const u = new URL(tab.url);
      title = u.hostname;
      if(u.pathname && u.pathname !== "/") title += u.pathname.slice(0, 14) + (u.pathname.length > 14 ? "…" : "");
    }catch(e){}
    setTabTitle(tab, title);
    renderIndicators();
  });

  frame.addEventListener("error", () => {
    clearTimeout(embedBlockedTimer);
    setOverlay(true);
    setStatus("Load error");
  });

  const navigate = (input) => {
    const tab = getActiveTab();
    const raw = (input ?? $("#addrInput").value).trim();
    if(!raw){
      tab.url = "about:home";
      pushHistory(tab, tab.url);
      saveState();
      loadTabToViewport(tab);
      renderTabs();
      return;
    }

    const url = normalizeToUrl(raw) || buildSearchUrl(raw);
    tab.url = url;
    pushHistory(tab, url);
    saveState();
    renderTabs();
    loadTabToViewport(tab);
    toast("移動", url);
  };

  const back = () => {
    const tab = getActiveTab();
    if(tab.hIndex <= 0) return toast("戻れない");
    tab.hIndex -= 1;
    tab.url = tab.history[tab.hIndex];
    saveState();
    renderTabs();
    loadTabToViewport(tab);
  };

  const forward = () => {
    const tab = getActiveTab();
    if(tab.hIndex >= tab.history.length - 1) return toast("進めない");
    tab.hIndex += 1;
    tab.url = tab.history[tab.hIndex];
    saveState();
    renderTabs();
    loadTabToViewport(tab);
  };

  const reload = () => {
    const tab = getActiveTab();
    if(tab.url === "about:home"){
      loadTabToViewport(tab);
      return;
    }
    try{
      frame.contentWindow.location.reload();
    }catch(e){
      // クロスオリジンは reload 制限があるので src 再設定
      frame.src = tab.url;
    }
    setStatus("Reloading…");
  };

  const openExternal = () => {
    const tab = getActiveTab();
    const url = tab.url === "about:home" ? buildSearchUrl($("#addrInput").value.trim() || "home") : tab.url;
    window.open(url, "_blank", "noopener,noreferrer");
    toast("外部で開いた", url);
  };

  const tryPip = async () => {
    try{
      const doc = frame.contentDocument;
      if(!doc) throw new Error("Cross-origin");
      const video = doc.querySelector("video");
      if(!video) return toast("動画が見つからない", "このページに <video> がない");
      if(document.pictureInPictureElement) await document.exitPictureInPicture();
      await video.requestPictureInPicture();
      toast("PiP", "Picture-in-Picture");
    }catch(e){
      toast("PiP不可", "多くの外部サイトは制限がある");
    }
  };

  // =========================
  // UI Render
  // =========================
  const renderTabs = () => {
    const el = $("#tabs");
    const activeId = state.activeTabId || state.tabs[0]?.id;
    if(!state.activeTabId) state.activeTabId = activeId;

    el.innerHTML = state.tabs.map(t => {
      const active = t.id === activeId ? "active" : "";
      const pill = t.url === "about:home" ? "HOME" : (t.url.startsWith("https://") ? "HTTPS" : (t.url.startsWith("http://") ? "HTTP" : "—"));
      return `
        <div class="tab ${active}" data-tab="${t.id}">
          <div class="title">${escapeHTML(t.title || "Untitled")}</div>
          <span class="pill">${escapeHTML(pill)}</span>
          <div class="x" title="閉じる" data-close="${t.id}">×</div>
        </div>
      `;
    }).join("");

    el.querySelectorAll(".tab").forEach(tabEl => {
      tabEl.addEventListener("click", (ev) => {
        const closeId = ev.target?.getAttribute?.("data-close");
        if(closeId){
          ev.stopPropagation();
          closeTab(closeId);
          return;
        }
        const id = tabEl.getAttribute("data-tab");
        setActive(id);
      });
    });
  };

  const renderSidebar = () => {
    // Quick
    const q = $("#quickList");
    q.innerHTML = state.quick.map(x => `
      <div class="item" data-url="${escapeHTML(x.url)}">
        <div class="icon">⚡</div>
        <div class="meta">
          <div class="t">${escapeHTML(x.title)}</div>
          <div class="s">${escapeHTML(x.url)}</div>
        </div>
        <span class="tag">OPEN</span>
      </div>
    `).join("");

    // Bookmarks
    const b = $("#bmList");
    b.innerHTML = (state.bookmarks.length ? state.bookmarks : [{title:"(なし)", url:""}]).map(x => `
      <div class="item" data-url="${escapeHTML(x.url || "")}" ${x.url ? "" : "style='opacity:.6; cursor:default'"}>
        <div class="icon">☆</div>
        <div class="meta">
          <div class="t">${escapeHTML(x.title)}</div>
          <div class="s">${escapeHTML(x.url)}</div>
        </div>
        ${x.url ? `<span class="tag">OPEN</span>` : `<span class="tag">—</span>`}
      </div>
    `).join("");

    // History
    const h = $("#histList");
    const max = state.settings.sidebarHistoryView || 20;
    const hist = state.history.slice(0, max);
    h.innerHTML = (hist.length ? hist : [{title:"(なし)", url:""}]).map(x => `
      <div class="item" data-url="${escapeHTML(x.url || "")}" ${x.url ? "" : "style='opacity:.6; cursor:default'"}>
        <div class="icon">⟲</div>
        <div class="meta">
          <div class="t">${escapeHTML(x.title || x.url || "(なし)")}</div>
          <div class="s">${escapeHTML(x.url || "")}</div>
        </div>
        ${x.url ? `<span class="tag">OPEN</span>` : `<span class="tag">—</span>`}
      </div>
    `).join("");

    // Handlers
    const bindOpen = (root) => {
      root.querySelectorAll(".item").forEach(it => {
        it.addEventListener("click", () => {
          const url = it.getAttribute("data-url");
          if(!url) return;
          navigate(url);
        });
      });
    };
    bindOpen(q); bindOpen(b); bindOpen(h);
  };

  const renderTheme = () => {
    document.body.setAttribute("data-theme", state.theme || "dark");
    $("#btnToggleTheme").innerHTML = state.theme === "dark"
      ? `<span class="icon">☾</span>テーマ`
      : `<span class="icon">☀</span>テーマ`;
  };

  const renderAll = () => {
    renderTheme();
    renderTabs();
    renderSidebar();
    renderIndicators();
  };

  // =========================
  // Home page (srcdoc)
  // =========================
  const homeHTML = () => {
    const quick = state.quick.slice(0, 6).map(x => `
      <a class="card" href="${x.url}" target="_blank" rel="noopener noreferrer">
        <div class="t">${escapeHTML(x.title)}</div>
        <div class="s">${escapeHTML(x.url)}</div>
      </a>
    `).join("");

    const bm = state.bookmarks.slice(0, 6).map(x => `
      <a class="card" href="${x.url}" target="_blank" rel="noopener noreferrer">
        <div class="t">☆ ${escapeHTML(x.title)}</div>
        <div class="s">${escapeHTML(x.url)}</div>
      </a>
    `).join("");

    return `<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Home</title>
<style>
  :root{ color-scheme: dark; }
  body{
    margin:0;
    font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Noto Sans JP", Arial;
    background: radial-gradient(1000px 600px at 20% 20%, rgba(124,240,255,.18), transparent 60%),
                radial-gradient(1000px 600px at 80% 20%, rgba(124,160,255,.16), transparent 60%),
                #0b0f14;
    color:#eaf0ff;
  }
  .wrap{ padding: 18px; }
  h1{ font-size:18px; margin:0 0 8px; }
  p{ margin:0 0 14px; opacity:.75; font-size:13px; }
  .grid{
    display:grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap:10px;
  }
  .card{
    display:block;
    padding:12px 12px;
    border-radius: 16px;
    border:1px solid rgba(255,255,255,.10);
    background: rgba(255,255,255,.06);
    text-decoration:none;
    color:inherit;
  }
  .card:hover{ border-color: rgba(124,240,255,.35); background: rgba(255,255,255,.09); }
  .t{ font-size:14px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
  .s{ font-size:12px; opacity:.70; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; margin-top:4px; }
  .row{ display:flex; gap:8px; flex-wrap:wrap; margin-top:12px; }
  .pill{
    border:1px solid rgba(255,255,255,.10);
    padding:6px 10px;
    border-radius: 999px;
    background: rgba(255,255,255,.06);
    font-size:12px;
    opacity:.85;
  }
</style>
</head>
<body>
  <div class="wrap">
    <h1>Voogle Home</h1>
    <p>アドレスバーにURLか検索ワードを入れて Enter。埋め込み不可サイトは外部タブで開く。</p>

    <div class="row">
      <span class="pill">Ctrl+L: アドレス</span>
      <span class="pill">Ctrl+T: 新規タブ</span>
      <span class="pill">Ctrl+W: タブ閉じる</span>
      <span class="pill">Ctrl+R: 更新</span>
    </div>

    <h1 style="margin-top:18px">Quick</h1>
    <div class="grid">${quick}</div>

    <h1 style="margin-top:18px">Bookmarks</h1>
    <div class="grid">${bm}</div>
  </div>
</body>
</html>`;
  };

  // =========================
  // Export / Import
  // =========================
  const exportData = () => {
    const data = JSON.stringify(state, null, 2);
    const blob = new Blob([data], {type:"application/json"});
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = "voogle-data.json";
    document.body.appendChild(a);
    a.click();
    a.remove();
    URL.revokeObjectURL(url);
    toast("出力した", "voogle-data.json");
  };

  const importData = async (file) => {
    try{
      const text = await file.text();
      const obj = JSON.parse(text);
      if(!obj || !obj.tabs || !Array.isArray(obj.tabs)) throw new Error("Invalid");
      state = obj;
      saveState();
      renderAll();
      loadTabToViewport(getActiveTab());
      toast("取込完了", "データを復元した");
    }catch(e){
      toast("取込失敗", "JSONが壊れてるか形式が違う");
    }
  };

  const resetAll = () => {
    if(!confirm("全データを初期化する?")) return;
    state = defaultState();
    saveState();
    renderAll();
    loadTabToViewport(getActiveTab());
    toast("初期化した");
  };

  // =========================
  // Bindings
  // =========================
  $("#btnGo").addEventListener("click", () => navigate());
  $("#addrInput").addEventListener("keydown", (e) => {
    if(e.key === "Enter") navigate();
  });

  $("#btnBack").addEventListener("click", back);
  $("#btnForward").addEventListener("click", forward);
  $("#btnReload").addEventListener("click", reload);
  $("#btnBookmark").addEventListener("click", toggleBookmark);
  $("#btnOpenExternal").addEventListener("click", openExternal);
  $("#btnPip").addEventListener("click", tryPip);
  $("#btnNewTab").addEventListener("click", () => newTab("about:home", "Home"));

  $("#btnToggleTheme").addEventListener("click", () => {
    state.theme = (state.theme === "dark") ? "light" : "dark";
    saveState();
    renderTheme();
    toast("テーマ", state.theme);
  });

  $("#btnExport").addEventListener("click", exportData);
  $("#importFile").addEventListener("change", (e) => {
    const f = e.target.files?.[0];
    if(f) importData(f);
    e.target.value = "";
  });

  $("#btnHome").addEventListener("click", () => navigate("about:home"));
  $("#btnClearHistory").addEventListener("click", () => {
    state.history = [];
    saveState();
    renderSidebar();
    toast("履歴クリア");
  });
  $("#btnResetAll").addEventListener("click", resetAll);

  // Overlay buttons
  $("#overlayOpenExternal").addEventListener("click", openExternal);
  $("#overlayTrySearch").addEventListener("click", () => {
    const tab = getActiveTab();
    const q = tab.url && tab.url !== "about:home" ? tab.url : ($("#addrInput").value.trim() || "home");
    newTab(buildSearchUrl(q), "Search");
  });
  $("#overlayClose").addEventListener("click", () => {
    setOverlay(false);
    toast("閉じた");
  });

  // Keyboard shortcuts
  window.addEventListener("keydown", (e) => {
    const ctrl = e.ctrlKey || e.metaKey;
    if(ctrl && e.key.toLowerCase() === "l"){ e.preventDefault(); $("#addrInput").focus(); $("#addrInput").select(); }
    if(ctrl && e.key.toLowerCase() === "t"){ e.preventDefault(); newTab("about:home", "Home"); }
    if(ctrl && e.key.toLowerCase() === "w"){ e.preventDefault(); closeTab(getActiveTab().id); }
    if(ctrl && e.key.toLowerCase() === "r"){ e.preventDefault(); reload(); }
    if(ctrl && e.key === "Enter"){ e.preventDefault(); openExternal(); }
  });

  // Init
  if(!state.activeTabId) state.activeTabId = state.tabs[0].id;
  renderAll();
  loadTabToViewport(getActiveTab());
})();
</script>
</body>
</html>

「ゲド戦記」ゲーム企画書

企画書:Project “EARTHSEA: TRUE NAME”

1. コンセプト

“言葉(真の名)で世界を変えるRPG”
剣で殴るだけのRPGはもう古い。プレイヤーが強くなる理由を、**魔法体系(言霊・均衡・真の名)**で説明できるゲームにする。

2. ジャンル / 体験

  • ジャンル:探索型アクションRPG + 物語選択
  • コア体験:
    • “真の名”を知るほど強くなる
    • 魔法は便利だが、使うほど均衡が崩れ、世界が歪む
    • 影(内なる闇)は倒すものではなく、向き合い統合するもの

3. ターゲット / プラットフォーム

  • ターゲット:物語重視RPG好き(10代後半〜30代)、文学/ファンタジー好き
  • プラットフォーム:PC / PS5 / Switch(最終は最適化で)
  • 規模:AA(インディー上位〜中堅)

4. 世界観(要約)

群島世界。島ごとに文化も価値観も違う。
世界は“言葉”で支えられていて、真の名を知る者は自然・獣・風・火に命令できる。だが、命令は必ず代償を要求する。均衡が崩れると、死者の国の扉が軋み、影が漏れる。


5. 主人公 / 主要キャラ(例)

※名前は開発用。後で差し替え可能。

  • 主人公:レン(若い魔法使い見習い)
    • 能力:真の名を“聞き取る”才能があるが、感情が揺れると暴走する
  • 相棒:ハル(元海賊の少年 / 航海・交渉が得意)
  • 師:オグ(大賢人)
    • 教え:「魔法は力じゃない。責任だ」
  • 敵:名を喰う者(ネームイーター)
    • 真の名を奪い、人を空っぽにする

6. ゲームループ(面白さの核)

  1. 島に上陸(事件発生)
  2. 聞き込み・探索で“真の名”の断片を集める
  3. ダンジョン攻略(戦闘 / 潜入 / 対話)
  4. ボス戦:剣だけでは勝てない
    • 真の名 or 均衡操作 or 影との対峙が必要
  5. 代償処理:魔法の使い方で島の未来が変わる
  6. 船で次の島へ(航海中イベント)

7. 戦闘システム

基本

  • アクション:軽/重攻撃、回避、ガード、短詠唱
  • パーティ:最大2〜3人(少人数で密度)

“真の名”魔法(差別化)

  • 敵/環境には“真の名スロット”がある(最初は伏せられている)
  • 調査で解放すると、戦闘中に使える呪が増える
    • 例:風の名→突風で投擲物を逸らす
    • 火の名→松明だけではなく“恐怖”も燃やせる(状態異常解除)

均衡(Balance)メーター

  • 強力魔法ほど均衡を消費
  • 均衡が崩れるとデメリット
    • 敵が“影化”、世界の色が沈む、NPCが不安定化、航海イベント悪化
  • ただし、崩れた均衡を戻す“儀式”もゲームになる(探索/クエスト)

8. 影(Shadow)システム:このゲームの心臓

  • 影はラスボスじゃない。主人公の一部
  • 物語進行で“影イベント”が発生
    • 逃げる:一時的に楽、だが均衡悪化
    • 向き合う:辛い、だが新スキル解放(統合魔法)
  • 終盤で「影を倒す」ではなく
    **“真の名で呼び、受け入れる”**が解決になる

9. クエスト設計(例)

  • メイン:島ごとの“均衡の歪み”原因を解く
  • サブ:
    • 失われた真の名を取り戻す(名前を奪われたNPC)
    • 竜の島の禁忌(交渉・儀式)
    • 死者の国の門番(戦闘より対話が重要)

10. 進行・成長

  • レベルよりも「知識(名)」が成長軸
  • 成長要素:
    • 名の辞典(収集・推理・解放)
    • 詠唱の短縮(タイミング入力)
    • 儀式スキル(非戦闘の強化)
    • 船の拡張(航海の選択肢が増える)

11. ビジュアル / サウンド方針

  • アート:群島の光、風、海。人は質素、魔法は派手すぎない
  • UI:羊皮紙+青いインク、辞典が気持ちいい
  • 音:静けさが武器。環境音(波・風)を主役に、戦闘BGMは必要最小限

12. 収益モデル(おすすめ)

  • 買い切り(世界観ゲーはこれが正解)
  • DLC:追加の島+追加の“名”体系(竜語・古語など)

13. 開発規模(目安)

  • 期間:18〜24ヶ月
  • 人員:8〜15人(中核)
  • 重要:文章・演出に強い人を最初から入れる(後付けは地獄)

14. リスクと対策

  • リスク:戦闘が地味になる
    • 対策:環境利用と“名”の解除で派手さを出す
  • リスク:物語が難解
    • 対策:辞典UIと短い会話で理解を積み上げる
  • リスク:権利問題
    • 対策:固有名詞・固有設定を避け、思想(均衡/名/影)を中核にしたオリジナル群島世界へ寄せる

必要なら、この企画書を次のどれかに“完成形”まで一気に落とすよ。

  • 1ページ企画書(ピッチ用)
  • 詳細GDD(システム・UI・クエスト50本)
  • 冒頭30分の台本(チュートリアル〜最初の島)