ゼノギアスリメイク企画書

1. プロジェクト概要

  • 企画名(仮):『Xenogears Rebirth(ゼノギアス リバース)(仮)』
  • ジャンル:ドラマティックRPG / ロボット(ギア)バトルRPG
  • 対象ハード
    • PlayStation 5 / PC(Steam) / 次世代Nintendoプラットフォーム
  • プレイ人数:1人
  • 想定プレイ時間:メインストーリー 60〜80時間+サブ要素 20〜40時間
  • 企画区分:既存IPリメイク(※本書は非公式ファン企画書)

2. 企画意図・コンセプト

2-1. 企画意図

  • 1990年代に発売された原作は、重厚なストーリー・哲学的テーマ・ギア(巨大ロボ)×人間ドラマが高く評価されている。
  • 一方で、
    • ハード性能制約による表現不足
    • 終盤の駆け足展開
    • 現代基準では不便なUI・テンポ
      といった課題も指摘されている。
  • 本リメイクでは、
    **「原作の本質と感情の揺さぶりを守りつつ、現代RPGとしての遊びやすさ・映像・テンポを刷新」**することを目標とする。

2-2. メインコンセプト

  1. 原作のドラマ性・テーマ性を忠実に再現
    • キャラクター性・世界観・大きな物語の流れ・名シーンの構造は尊重。
  2. ゲーム体験の再構築
    • 戦闘テンポ、探索、UI、カメラワークを現代基準に最適化。
  3. 後半シナリオの再表現
    • 当時表現しきれなかったシーンや背景を、プレイアブルパートやイベントとして補完。
  4. ギアバトルの“憧れ”を実現
    • 重厚さとスピード感の両立した、アクション性の高いハイブリッドバトル。

3. ターゲットユーザー

  1. 原作ファン層(30〜40代中心)
    • 当時プレイしたコアファン。
    • シナリオ重視・キャラ重視のRPGを好むユーザー。
  2. 現代のJRPGファン(10〜20代)
    • スタイリッシュなビジュアル、手触りの良いアクションRPGに慣れているユーザー。
    • 他社の大型RPG(FF・テイルズ・ペルソナなど)を遊ぶ層。
  3. ロボット・メカファン
    • アニメ調のロボットバトル、巨大兵器のスケール感に魅力を感じる層。

4. 世界観・ストーリーテーマ(高レベル)

※具体的なネタバレの細部ではなく、「リメイク方向性」を示すレベルに留める。

  • 核となるテーマ
    • 「自己との対話」「存在理由」「因果・輪廻」「人と機械・神話の境界」
  • トーン&ムード
    • 一見牧歌的な日常から、徐々に世界の根源的な闇へ降りていく構成。
    • シリアス中心だが、仲間同士の掛け合いや日常シーンで緩急を付ける。
  • リメイクでの方針
    • 後半の出来事を**ナレーション主体ではなく、可能な限り“実際に操作して体験させる”**形へ再構成。
    • 補完要素として、
      • 過去編の短いプレイアブルセクション
      • サブクエストで描かれる脇役の視点
        などを追加し、世界の厚み・説得力を強化。

5. ゲームシステム全体像

5-1. バトルシステム(人間戦 / ギア戦)

共通方針

  • ベースはコマンドRPG+コンボ(連携)システムを継承しつつ、
    入力タイミングや位置取りを取り入れたアクション性のあるハイブリッドにする。
  • 行動順やバフ・デバフなど、戦術RPGとしての深みを維持。

人間戦(オンフット)

  • 三ボタン程度の「弱・中・強」攻撃の組み合わせで、コンボ技を発動。
  • AP(アクションポイント)制で、行動配分・温存の駆け引きを残す。
  • 回避・ガード・距離管理など、ライトなアクション要素を追加。
  • 必殺技発動時はカットイン+カメラワーク強化。

ギア戦

  • 重量感・速度感を感じるカメラとSEで「巨大兵器を操る感覚」を強調。
  • 燃料・装甲・ブースト管理を要素にした中量級“シミュレーションアクション”寄りの手触り。
  • ギアごとにロール(格闘型 / 射撃型 / 支援型)を明確化し、パーティ編成の意味を強化。
  • 合体技・連携技は演出を刷新し、ボス戦のハイライトに。

5-2. フィールド・ダンジョン

  • シームレス3Dフィールド:ローディングを極力隠し、没入感を重視。
  • キャラアクション:
    • ダッシュ・ジャンプ・よじ登り・壁キックなど、原作の立体的マップを現代仕様にアップグレード。
  • ダンジョンデザイン:
    • ただ複雑にするのではなく、「ギミック+物語的意味」のある構造へ再調整。
    • ショートカット・簡易マップなどで迷いすぎストレスを軽減。

5-3. 町・拠点要素

  • 町では
    • NPCとの会話
    • ショップ
    • サブクエスト受注
    • 仲間との会話イベント(好感度イベントではなく、あくまでストーリー深堀り)
      を配置。
  • 宿屋・酒場などで**仲間同士の会話イベント(「キャンプトーク」的なもの)**を追加し、
    心情描写を補強。

6. キャラクターデザイン・ビジュアル

  • ビジュアルスタイル
    • “ハイエンドなアニメ調3D”を目指し、トゥーンシェーダ+物理ベースレンダリングを両立。
    • オリジナルの印象的なデザインを尊重し、細部・質感・装飾を現代的にアップデート。
  • ギアデザイン
    • シルエット・武装は原作のイメージを維持しつつ、関節ディテール・マーキング・発光表現で情報量を増加。
    • バトル中の変形・形態変化を一部ギアに追加し、差別化と盛り上げに利用。

7. サウンド・音楽

  • BGM
    • 原作楽曲をフルオーケストラ・バンド・民族楽器などで再アレンジ。
    • 要所では原曲アレンジを使用し、ファン心理を刺激。
  • ボイス
    • メインストーリーはフルボイスを基本とし、一部サブイベントもボイス対応。
  • SE
    • ギアの駆動音・ブースト音・衝撃音をハイレゾ化し、“質量感”を音でも表現。

8. UI/UX・快適さ

  • 戦闘速度は**3段階調整(標準 / 早い / 非常に早い)**を用意。
  • オートバトル・オートコンボ機能(難易度EASY向け)を搭載。
  • ログ機能、イベントリプレイ機能で、物語の振り返りをサポート。
  • ミニマップ&クエストガイド(オン/オフ可能)で迷子ストレスを軽減。

9. 新規要素(リメイクオリジナル)

  1. サブクエスト強化
    • 主要都市・村ごとに「世界観を深めるサブストーリー」を用意。
    • 一部はギア専用サブクエスト(闘技場・模擬戦・試作機テストなど)。
  2. ギアカスタマイズ拡張
    • 外見は原作イメージを壊さない範囲で、カラー・マーキング・エンブレム変更を許可。
    • パーツ・チップによる能力カスタム要素を拡張。
  3. フォトモード
    • ギア・キャラクターを撮影できるフォトモードを実装し、SNS映えを意識。
  4. ギャラリーモード
    • イラスト・3Dモデル・BGM・過去PVなどを閲覧可能なモードを搭載。
    • リメイク制作のメイキング要素を一部公開し、ファン満足度を高める。

10. 開発体制(想定)

  • 開発期間:36〜42ヶ月
  • チーム規模:メイン開発 80〜120名規模
    • ディレクター:1名
    • プロデューサー:1〜2名
    • シナリオ・演出:数名(原作関係者+新規ライター)
    • プログラマー:15〜25名
    • アーティスト(キャラ・ギア・背景):25〜35名
    • UI/UX:3〜5名
    • サウンド:3〜5名
    • QA:外部含め多数

11. 開発スケジュール(ラフ)

  1. プリプロダクション(〜6ヶ月)
    • リメイク範囲・シナリオ再構成方針の決定
    • 映像コンセプト・バトルプロトタイプ作成
  2. プロダクション前半(〜18ヶ月)
    • メインストーリー実装
    • バトルシステム・ギアシステムの実装と調整
  3. プロダクション後半(〜30ヶ月)
    • サブクエスト・追加要素・UI/UX実装
    • バランス調整・最適化
  4. ポリッシュ・QA(〜36〜42ヶ月)
    • デバッグ・チューニング・ローカライズ
    • マーケティング連動施策準備

12. ビジネス・収益計画(案)

  • 販売形態:フルプライスパッケージ+DL版(DLCは最小限)
  • DLC案
    • サウンドトラック拡張
    • ビジュアルアートブック(デジタル)
    • ギャラリー用メイキング映像など、世界観を壊さない“資料系DLC”
  • 訴求ポイント
    • 「伝説的RPGの決定版リメイク」
    • 「当時の未完感を、現代の技術と表現で再構築」
    • 「ギアバトル×重厚ストーリーの最高峰を、最新ハードで」

13. 注意事項(IP・ブランド)

  • 本企画書は非公式ファンメイドの仮想リメイク案であり、
    実際の権利は原作の権利元(スクウェア・エニックス等)に帰属する。
  • 実際に商業利用を行う場合は、
    • 権利元への企画提案
    • 商標・著作権の正式なライセンス契約
      が必須となる。

光の歩み:パウロの航路

タイトル(仮)

「光の歩み:パウロの航路」
― 初期教会の旅路を体験し、希望・信頼・愛をゲームとして学ぶアドベンチャーRPG

1. 企画概要

  • ジャンル:物語主導アドベンチャーRPG+探索+対話パズル(非殺傷系)
  • 対象:中高生〜大人。信仰の有無を問わず、歴史物語や倫理的選択を楽しみたい人
  • プラットフォーム:PC/コンソール(Switch/PS/Xbox)+後発でモバイル簡易版
  • プレイ時間:本編10〜15時間+周回要素
  • レーティング想定:CERO B(暴力表現なし、差別・扇動なし)

2. 企画意図・学習目標

  • 初期キリスト教の歴史的背景(1世紀ローマ世界・ディアスポラ都市)を偏りなく紹介
  • 価値観(信仰=Faith/希望=Hope/愛=Charity)を、選択と結果で体験的に理解
  • 実在地・史料に基づく考証のもと、**布教=説得ではなく「対話と理解」**を主眼に

3. コア体験(Game Pillars)

  1. 対話と調停:噂・誤解・利害が交差する街で、中立的に聞き取り、誠実に橋渡し
  2. 旅と共同体:船旅で各都市へ。資源(時間・食糧・寄付)と仲間の士気を管理
  3. 徳の選択:状況に応じて「信仰/希望/愛」を強化する行動を選ぶ(暴力回避)
  4. 史的没入:当時の市場、会堂、家屋教会、ローマ法廷、港湾などを探索し資料収集

4. ゲームループ

  • 街に到着 → 情報収集(傾聴) → 対話パズル(調停/説得/証言の整合) → 共同体支援(施し/労働/手紙) → 船旅準備(資源管理) → 次の地へ
  • 章ごとに手紙(Epistle)が解禁され、読了で小さな洞察ブースト(対話オプション解放)

5. メカニクス

  • 徳メーター:Faith / Hope / Charity(数値でなく「状態」重視。偏らず三徳のバランスが良いと特殊イベント)
  • 祈り(クールダウン制):混乱時に視点を整える。NPCの本心“キーワード”が一瞬だけ可視化
  • 証言カード:聞き取った事実・比喩・譬えの断片。論理矛盾がない並べ方で対話パズルを解く
  • 共同体ボード:困窮、病、孤立、迫害の度合いを可視化。資源配分で街の状態が改善しクエストが変化
  • 非殺傷コンバット:逃走・隠密・法廷弁明・保証人立て等で危機回避(QTE最小)

6. 物語構成(例)

  • プロローグ:ダマスコ(出会いと転機)
  • 第1章:アンティオキア(多文化の街での誤解解消)
  • 第2章:ピシディアのアンティオキア〜リストラ(迫害と連帯)
  • 第3章:コリント(商人ギルドと信仰共同体の摩擦を調停)
  • 第4章:エフェソス(職人組合と偶像問題、平和的収束)
  • 最終章:ローマ(法と良心、希望の証言。エンディング分岐=三徳の在り方)

7. 美術・音響

  • アート:温かい筆致のセミリアル。衣装・建築は史料参照、象徴表現はやさしく
  • UI:巻物/羊皮紙モチーフ。徳メーターは三つの小さな灯火
  • 音楽:弦・笛・竪琴+現代的サウンドスケープ。祈り時は環境音がやわらぐ

8. アクセシビリティ

  • 朗読モード(本文読み上げ)、字幕・フォントサイズ調整、色弱配慮配色
  • 難易度:ストーリーフォーカス/スタンダード/考証チャレンジ(史料クイズ追加)

9. リサーチと表現ガイドライン

  • 宗派横断の監修委員会(神学・歴史・考古の3名以上)を設置
  • 固有名詞・儀礼の描写は史料脚注に準拠、論争点は断定せず複数説を匂わせる
  • 勧誘的・攻撃的表現は避ける。他宗教・文化への敬意を必須要件に

10. 進行・成長

  • 徳の熟達で新しい対話フレーズ、比喩(譬え話)カードが解禁
  • 街レベルが上がると共同体ボードの課題が高度化(教育、孤児支援、介護など)

11. マネタイズ

  • 基本買い切り(DLCで「ガリラヤの物語集」「詩編黙想パック」など追加コンテンツ)
  • 収益の一部を現実のチャリティに寄付する連動企画(透明性レポートをゲーム内掲示板で公開)

12. 競合・差別化

  • 既存の宗教題材ゲームは説話紹介が中心。
    → 本作は**“非対立のデザイン”**(調停パズル×徳システム)で能動的体験を提供。

13. 技術スタック

  • Unity(ユーザーの希望に合致):URP、Yamlローカライズ、Ink/Dialogue System、Timeline
  • セーブはJSON+暗号化、PCとSwitchを初期ターゲット
  • 字幕・スクリプトは外部CSVで管理し監修者が直接校正可能

14. 開発体制・期間(目安)

  • チーム:10〜14名(Pd/PM1、リードデザ1、レベルデザ2、プログラマ3、美術3、UI/UX1、サウンド1、QA1、歴史/神学監修2)
  • 期間:プリプロ3ヶ月/プロダクション10ヶ月/QA・ローカライズ3ヶ月(計16ヶ月)
  • 概算:8,000万〜1.4億円規模(外注とキャスト有無で変動)

15. KPI / 品質目標

  • ストーリー完走率60%以上、レビューで「尊重/配慮」に関する肯定的言及比率70%以上
  • 学習要素:章末クイズ正答率の上昇、資料室閲覧率40%以上

16. リスクと対応

  • 神学表現の誤解:監修フローと用語脚注で軽減
  • 暴力の回避が地味化:対話パズルに“緊張の演出”(時間制限・人だかり・法廷)を導入
  • 文化的感受性:外部レビュー(多宗教・無宗教含む)でプレイテスト

サンプルクエスト(第3章:コリント)

  • 状況:商人ギルドが安息日の取引巡りで共同体を批判。街の不満が高まる
  • 目的:両者の誤解を解き、市場規約の妥協案を提案
  • 手順
    1. 市場で聞き取り→証言カード「規約草案」「祭礼日」「巡礼需要」を入手
    2. 祈りでキーワード「家族」「信用」を取得
    3. 対話パズルで並べ替え:信用>祭礼日>需要調整
    4. 結果:ギルドは早朝市を追加、共同体は夕刻会合に変更→街ボード好転

代替案(スピンオフ)

A. 「たとえの箱庭(Parables Builder)」

  • ジャンル:箱庭パズル+教育
  • イエスのたとえ話を抽象化オブジェクトで組み、他者に伝わるかを評価
  • マップ共有・評価でコミュニティ生成、学校向け教材モード有り

B. 「詩編の旋律」

  • ジャンル:没入型音×言葉アドベンチャー
  • 詩編のテキストを手掛かりに、風景・音景を“調律”して心情を可視化
  • メンタルヘルス配慮の瞑想モード、読み上げ+多言語字幕

必要なら、この企画をピッチ用1枚詳細GDD予算&スケジュール表に分解してドキュメント化します。どの案を推したい? あるいは「現代日本舞台」「子ども向け」などの方向性も出せます。

ブラウザゲーム.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
  <title>Endless Dodge ULTRA - Bullet & Boss</title>
  <style>
    :root{
      --bg1:#070816; --bg2:#0f1b38; --accent:#6ee7ff; --accent2:#9bffb7; --danger:#ff6b6b; --panel:rgba(255,255,255,.08);
      --text:#eaf2ff; --muted:#b5c0d0; --gold:#ffd166; --purple:#c4a7ff; --emerald:#86efac;
    }
    *{box-sizing:border-box}
    html,body{height:100%;}
    body{ margin:0; font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans JP";
      color:var(--text);
      background: radial-gradient(1200px 800px at 20% 10%, #1b2444 0%, var(--bg1) 50%), linear-gradient(160deg, var(--bg2), var(--bg1));
      overflow:hidden;}
    .wrap{position:fixed; inset:0; display:grid; grid-template-rows:auto 1fr auto;}
    header, footer{display:flex; gap:.75rem; align-items:center; justify-content:space-between; padding:.6rem .9rem; backdrop-filter: blur(6px); background:linear-gradient( to bottom, rgba(255,255,255,.06), rgba(255,255,255,.02)); border-bottom:1px solid rgba(255,255,255,.08)}
    header h1{font-size:1rem; margin:0; letter-spacing:.05em; font-weight:700}
    header .right{display:flex; gap:.5rem; align-items:center}
    .pill{ pointer-events:auto; border:1px solid rgba(255,255,255,.14); background:var(--panel); padding:.5rem .8rem; border-radius:999px; font-size:.9rem; color:var(--text); cursor:pointer; user-select:none; transition:transform .08s ease}
    .pill:active{ transform:scale(.97)}
    #gamePanel{ position:relative; display:grid; place-items:center;}
    canvas{ width: min(94vw, 800px); aspect-ratio: 9/16; border-radius: 18px; box-shadow: 0 10px 40px rgba(0,0,0,.5), inset 0 0 0 1px rgba(255,255,255,.06);
      background: radial-gradient(600px 500px at 50% 10%, rgba(110,231,255,.12), transparent 60%), linear-gradient(180deg, rgba(255,255,255,.03), rgba(255,255,255,.02));}
    .hud{ position:absolute; inset:0; pointer-events:none;}
    .row{ display:flex; justify-content:space-between; align-items:center; padding:10px;}
    .score{ font-variant-numeric: tabular-nums; font-size: clamp(18px, 3.5vw, 28px); text-shadow:0 1px 0 rgba(0,0,0,.5)}
    .muted{ color: var(--muted)}
    .center{ position:absolute; inset:0; display:grid; place-items:center;}
    .card{ pointer-events:auto; background:rgba(7,8,22,.92); border:1px solid rgba(255,255,255,.14); border-radius:16px; padding:20px; width:min(92vw, 480px); box-shadow:0 20px 60px rgba(0,0,0,.6)}
    .card h2{ margin:0 0 8px; font-size:1.25rem}
    .card p{ margin:.25rem 0; color:var(--muted)}
    .btn{ display:inline-flex; align-items:center; justify-content:center; gap:.5rem; padding:.7rem 1rem; border-radius:12px; border:1px solid rgba(255,255,255,.16); background:linear-gradient(180deg, rgba(255,255,255,.12), rgba(255,255,255,.06)); color:var(--text); cursor:pointer; font-weight:600}
    .btn:hover{ filter:brightness(1.08)}
    .btn.primary{ border-color: rgba(110,231,255,.5); box-shadow: 0 0 30px rgba(110,231,255,.15) inset}
    .grid{ display:grid; grid-template-columns:1fr 1fr; gap:.6rem}
    .touch{ position:absolute; inset:auto 0 10px 0; display:flex; justify-content:center; gap:12px; pointer-events:auto}
    .touch button{ width:clamp(64px, 22vw, 106px); aspect-ratio:1/1; border-radius:16px; border:1px solid rgba(255,255,255,.14); background:var(--panel); color:var(--text); font-weight:700; font-size:clamp(16px, 4.5vw, 22px); text-shadow:0 1px 0 rgba(0,0,0,.35)}
    .badge{border:1px solid rgba(255,255,255,.14); background:var(--panel); padding:.35rem .6rem; border-radius:999px; font-size:.75rem}
    .toast{ position:absolute; left:50%; top:14%; transform:translateX(-50%); pointer-events:none; opacity:0; transition: opacity .2s, transform .2s; background:rgba(0,0,0,.5); border:1px solid rgba(255,255,255,.18); padding:.35rem .7rem; border-radius:10px; font-weight:700}
    .toast.show{ opacity:1; transform:translate(-50%, -6px)}
    footer{ border-top:1px solid rgba(255,255,255,.08); border-bottom:none; justify-content:center}
    a{ color:var(--accent)}
    dialog{ border:none; border-radius:16px; background:rgba(7,8,22,.96); color:var(--text); width:min(92vw,560px); }
    dialog::backdrop{ background:rgba(0,0,0,.6); }
    .field{ display:flex; justify-content:space-between; align-items:center; gap:10px; padding:8px 0; }
    .range{ width:58% }
    .switch{ appearance:none; width:42px; height:24px; border-radius:999px; background:#445; position:relative; outline:none; cursor:pointer; }
    .switch:checked{ background:#2aa }
    .switch::after{ content:""; position:absolute; top:3px; left:3px; width:18px; height:18px; border-radius:50%; background:#fff; transition:left .15s}
    .switch:checked::after{ left:21px }
    .shop-item{ display:grid; grid-template-columns:1fr auto; gap:.4rem; align-items:center; padding:.5rem; border:1px solid rgba(255,255,255,.12); border-radius:12px; margin:.35rem 0; }
    .chip{ padding:.2rem .5rem; border:1px solid rgba(255,255,255,.16); border-radius:999px; font-size:.75rem; }
  </style>
</head>
<body>
  <div class="wrap">
    <header>
      <h1>Endless Dodge <span class="badge">ULTRA</span></h1>
      <div class="right">
        <span class="badge">💎 <span id="wallet">0</span></span>
        <button id="btnShop" class="pill" aria-label="shop">🛒 ショップ</button>
        <button id="btnSkins" class="pill" aria-label="skins">🎨 スキン</button>
        <button id="btnPause" class="pill" aria-label="pause">⏸</button>
        <button id="btnSound" class="pill" aria-label="sound">🔊</button>
        <button id="btnSettings" class="pill" aria-label="settings">⚙</button>
      </div>
    </header>
    <div id="gamePanel">
      <canvas id="game" width="360" height="640" aria-label="game canvas"></canvas>
      <div class="hud">
        <div class="row">
          <div class="score">
            <span id="score">0</span> pts
            · <span class="muted">Best:</span> <span id="best">0</span>
            · <span class="muted">Combo:</span> <span id="combo">x1.0</span>
            · <span class="muted">Stage:</span> <span id="stage">1</span>
          </div>
          <div class="row" style="gap:.5rem">
            <span class="badge" id="badges">⛨ 0 · 🧲 0 · ⏳ 0</span>
          </div>
        </div>
        <div class="center" id="overlayStart">
          <div class="card">
            <h2>避けて、撃って、強化して、ボスを倒せ!</h2>
            <p>← → / A・D で移動。<strong>Spaceでショット</strong>、<kbd>Shift</kbd>でダッシュ(無敵0.4s)。</p>
            <p>パワーアップ:⛨シールド / 🧲マグネット / ⏳スロウ。コンボでスコア倍率UP。</p>
            <p>ステージごとにボス戦。ボスは弾幕を発射。ショットでHPを削ろう。</p>
            <div class="grid" style="margin-top:10px">
              <button class="btn primary" id="btnStart">▶ ゲーム開始</button>
              <button class="btn" id="btnHow">❓ 操作</button>
            </div>
            <div style="margin-top:10px" class="muted" id="missions"></div>
          </div>
        </div>
        <div class="center" id="overlayBoss" style="display:none">
          <div class="card" style="text-align:center">
            <h2>⚠ B O S S ⚠</h2>
            <p>弾幕を避けつつ、Spaceで撃て!Shiftダッシュも活用。</p>
            <button class="btn primary" id="btnBossGo">戦闘開始</button>
          </div>
        </div>
        <div class="center" id="overlayGameOver" style="display:none">
          <div class="card">
            <h2>ゲームオーバー</h2>
            <p>スコア: <strong id="finalScore">0</strong> / ベスト: <strong id="finalBest">0</strong> / 💎<strong id="earned">0</strong></p>
            <p>達成:<span id="finalMissions" class="muted">-</span></p>
            <div class="grid" style="margin-top:10px">
              <button class="btn primary" id="btnRetry">↻ リトライ</button>
              <button class="btn" id="btnHome">⌂ タイトル</button>
            </div>
          </div>
        </div>
        <div class="touch" id="touchControls" aria-hidden="true">
          <button id="leftBtn" aria-label="left">⟵</button>
          <button id="dashBtn" aria-label="dash">⇧</button>
          <button id="rightBtn" aria-label="right">⟶</button>
        </div>
        <div class="toast" id="toast">Ready</div>
      </div>
    </div>
    <footer>
      <small class="muted">© 2025 Endless Dodge ULTRA · 図形のみ · ローカル保存(設定/進行/ウォレット/実績)</small>
    </footer>
  </div>

  <!-- Settings / Shop / Skins (unchanged structure) -->
  <dialog id="dlgSettings">
    <form method="dialog" style="padding:16px">
      <h3 style="margin:0 0 8px">設定</h3>
      <div class="field"><span>難易度(速度倍率)</span><input class="range" id="rangeSpeed" type="range" min="0.8" max="1.6" step="0.05"></div>
      <div class="field"><span>画面シェイク</span><input id="chkShake" class="switch" type="checkbox"></div>
      <div class="field"><span>色弱モード(高コントラスト)</span><input id="chkCB" class="switch" type="checkbox"></div>
      <div class="field"><span>省エネ描画(★数減少)</span><input id="chkEco" class="switch" type="checkbox"></div>
      <div class="field"><span>操作ヒントの表示</span><input id="chkHints" class="switch" type="checkbox"></div>
      <div style="display:flex; gap:.5rem; justify-content:flex-end; margin-top:10px">
        <button class="btn" value="cancel">閉じる</button>
        <button class="btn primary" id="btnSaveSettings" value="default">保存</button>
      </div>
    </form>
  </dialog>

  <dialog id="dlgShop"><form method="dialog" style="padding:16px"><h3 style="margin:0 0 8px">ショップ</h3><p class="muted">💎はプレイ後にスコアから換算(100pts ≒ 1💎)。</p><div id="shopList"></div><div style="display:flex; gap:.5rem; justify-content:flex-end; margin-top:10px"><button class="btn" value="cancel">閉じる</button></div></form></dialog>
  <dialog id="dlgSkins"><form method="dialog" style="padding:16px"><h3 style="margin:0 0 8px">スキン</h3><div id="skinList"></div><div style="display:flex; gap:.5rem; justify-content:flex-end; margin-top:10px"><button class="btn" value="cancel">閉じる</button></div></form></dialog>

  <script>
  // ===== Utilities & Persistence =====
  const clamp=(v,min,max)=>Math.max(min,Math.min(max,v));
  const rand=(a,b)=>Math.random()*(b-a)+a; const choice=a=>a[(Math.random()*a.length)|0];
  const storage={ get(k,def){ try{return JSON.parse(localStorage.getItem(k)) ?? def}catch{ return def }}, set(k,v){ localStorage.setItem(k, JSON.stringify(v)); } };

  const SAVE={ best:'ultra-best', opts:'ultra-opts', stats:'ultra-stats', wallet:'ultra-wallet', upgrades:'ultra-upgrades', missions:'ultra-missions', skin:'ultra-skin' };
  const opts = Object.assign({ speedMul:1.0, shake:true, colorblind:false, eco:false, hints:true }, storage.get(SAVE.opts, {})); storage.set(SAVE.opts, opts);
  const wallet = { gems: storage.get(SAVE.wallet, 0) };
  function addGems(n){ wallet.gems = Math.max(0, Math.floor(wallet.gems + n)); storage.set(SAVE.wallet, wallet.gems); walletEl.textContent = wallet.gems; }

  const upgrades = Object.assign({ startShield:0, magnetDur:0, dashCD:0, scoreMul:0, extraLife:0 }, storage.get(SAVE.upgrades, {}));
  function uLevel(name){ return upgrades[name]||0 } function saveUpgrades(){ storage.set(SAVE.upgrades, upgrades); buildShop(); }

  const skins = [
    {id:'default', name:'Default', cost:0, color:'#eaf2ff'},
    {id:'neon', name:'Neon Blue', cost:50, color:'#7ee0ff'},
    {id:'sun', name:'Sun Gold', cost:80, color:'#ffd166'},
    {id:'void', name:'Void Purple', cost:120, color:'#c4a7ff'},
    {id:'leaf', name:'Leaf Green', cost:120, color:'#86efac'}
  ];
  let currentSkin = storage.get(SAVE.skin, 'default');

  function toast(msg, t=1200){ const el=document.getElementById('toast'); el.textContent=msg; el.classList.add('show'); clearTimeout(el._t); el._t=setTimeout(()=>el.classList.remove('show'), t); }

  // ===== Audio =====
  const AudioKit=(()=>{ let ctx, enabled=false; function ensure(){ if(!ctx){ const C=window.AudioContext||window.webkitAudioContext; if(C){ ctx=new C(); }} return ctx }
    function beep(freq=440, dur=0.08, type='sine', gain=0.02){ if(!enabled) return; const c=ensure(); if(!c) return; const o=c.createOscillator(); const g=c.createGain(); o.type=type; o.frequency.setValueAtTime(freq,c.currentTime); g.gain.setValueAtTime(gain,c.currentTime); o.connect(g).connect(c.destination); const t=c.currentTime; o.start(t); o.stop(t+dur); }
    function arpeggio(){ if(!enabled) return; const c=ensure(); if(!c) return; const base=220; const seq=[0,4,7,12,7,4]; seq.forEach((st,i)=>{ const o=c.createOscillator(); const g=c.createGain(); o.type='triangle'; o.frequency.setValueAtTime(base*Math.pow(2,st/12), c.currentTime + i*0.08); g.gain.setValueAtTime(0.02, c.currentTime + i*0.08); o.connect(g).connect(c.destination); o.start(c.currentTime + i*0.08); o.stop(c.currentTime + i*0.08 + 0.1); }); }
    return{ enable(){ enabled=true; ensure(); }, disable(){ enabled=false; }, toggle(){ enabled=!enabled; if(enabled) ensure(); return enabled; }, hit(){ beep(120,0.18,'square',0.05); }, coin(){ beep(880,0.07,'triangle',0.03); }, tick(){ beep(660,0.02,'sine',0.015); }, power(){ beep(520,0.1,'sawtooth',0.04); }, dash(){ beep(240,0.06,'square',0.05); }, fanfare(){ arpeggio(); }, shoot(){ beep(720,0.04,'square',0.03); } }
  })();

  // ===== Canvas & World =====
  const canvas=document.getElementById('game'); const ctx=canvas.getContext('2d');
  let dpr=1; function resize(){ dpr=Math.max(1, Math.min(2, window.devicePixelRatio||1)); const w=canvas.clientWidth; const h=canvas.clientHeight; canvas.width=Math.round(w*dpr); canvas.height=Math.round(h*dpr); ctx.setTransform(dpr,0,0,dpr,0,0); }
  new ResizeObserver(resize).observe(canvas); window.addEventListener('orientationchange', resize); resize();

  const state={ running:false, over:false, t:0, score:0, best: storage.get(SAVE.best, 0), baseSpeed:120, speed:120, worldW:360, worldH:640, combo:1, comboTime:0, slowed:0, stage:1, boss:false };
  const fx={ shakeTime:0, shakeAmp:0 };
  const starCount = opts.eco? 40 : 90; const stars=[...Array(starCount)].map(()=>({x:rand(0,360), y:rand(0,640), s:rand(0.5,2), sp:rand(10,40)}));

  const player={ x:180, y:560, r:12, vx:0, speed:270, color:'#eaf2ff', alive:true, flash:0, shield:0, magnet:0, dashCD:0, dashT:0, extra:0, fireCD:0 };
  const obstacles=[]; const coins=[]; const lasers=[]; const particles=[]; const powerups=[]; const bullets=[]; // boss bullets
  const pbullets=[]; // player bullets

  // ===== Spawning =====
  let lastSpawn=0, spawnInt=0.9; let lastLaser=0, laserInt=6.0; let stageTime=0, nextBossAt=28; // seconds
  function spawnBlockRow(yOff=-40){ const gap = clamp(140 - state.t*0.02, 70, 150); const blockW = rand(40, 90); const leftW = rand(10, state.worldW - gap - blockW - 10); const rightX = leftW + gap + blockW; const moving = Math.random()<clamp(0.08 + state.t*0.0006, 0.08, 0.4); const speed = moving? rand(30, 90)* (Math.random()<0.5?-1:1) : 0; obstacles.push({x:0, y:yOff, w:leftW, h:16, vx:0}); obstacles.push({x:rightX, y:yOff, w: state.worldW - rightX, h:16, vx:0}); if(moving){ obstacles.push({x:leftW+4, y:yOff-18, w: blockW-8, h:10, vx:speed}); }
    const cx = leftW + gap/2 + rand(-gap*0.35, gap*0.35); const cluster = (Math.random()<0.6) ? 4 : 1; for(let i=0;i<cluster;i++) coins.push({x:cx + (cluster>1?(i-1.5)*10:0), y:yOff-20 - i*8, r:6, vy:0}); if(Math.random()<0.22) powerups.push({x:cx+rand(-gap*0.3,gap*0.3), y:yOff-36, r:8, kind: choice(['shield','magnet','slow'])}); }
  function spawnLaser(){ const side = Math.random()<0.5? 'L':'R'; const x = side==='L'? -40 : state.worldW+40; const dir = side==='L'? 1 : -1; lasers.push({x, y: rand(120, state.worldH-160), w:120, h:10, vx: 170*dir, life: 4}); }

  // ===== Boss & Bullet Hell =====
  let boss=null; let patternT=0, patternId=0, spiralAng=0; // patterns
  function enterBoss(){ state.boss=true; show(bossOverlay); }
  function startBoss(){ hide(bossOverlay); boss = { x: state.worldW/2, y: 160, r: 22, hp: 6 + state.stage*2, vx: 80 }; bullets.length=0; patternT=0; patternId=0; spiralAng=0; }
  function bossShootFan(){ // 扇状(自機狙い)
    const dx = player.x - boss.x; const dy = (player.y - boss.y); const base = Math.atan2(dy, dx); const n=5; const spread=0.6; for(let i=0;i<n;i++){ const a = base + (i-(n-1)/2)*spread/n; bullets.push({x:boss.x, y:boss.y, r:4, vx:Math.cos(a)*160, vy:Math.sin(a)*160}); }
  }
  function bossShootRing(){ // 全方位リング
    const n=14; for(let i=0;i<n;i++){ const a = (i/n)*Math.PI*2; bullets.push({x:boss.x, y:boss.y, r:3.5, vx:Math.cos(a)*120, vy:Math.sin(a)*120}); }
  }
  function bossShootSpiral(){ // 渦巻き
    const a1 = spiralAng; const a2 = spiralAng + Math.PI; spiralAng += 0.35; bullets.push({x:boss.x, y:boss.y, r:3.5, vx:Math.cos(a1)*150, vy:Math.sin(a1)*150}); bullets.push({x:boss.x, y:boss.y, r:3.5, vx:Math.cos(a2)*150, vy:Math.sin(a2)*150}); }

  function updateBoss(dt){ if(!boss) return; boss.x += boss.vx*dt; if(boss.x<40){ boss.x=40; boss.vx=Math.abs(boss.vx);} if(boss.x>state.worldW-40){ boss.x=state.worldW-40; boss.vx=-Math.abs(boss.vx);} // pattern timeline
    patternT += dt; if(patternId===0){ if(patternT>0.6){ bossShootFan(); patternT=0; if(Math.random()<0.25) patternId=1; } }
    else if(patternId===1){ bossShootSpiral(); if(patternT>2.4){ patternT=0; patternId=2; } }
    else if(patternId===2){ if(patternT>1.0){ bossShootRing(); patternT=0; if(Math.random()<0.5) patternId=0; else patternId=1; } }

    // move bullets
    for(const b of bullets){ b.x += b.vx*dt; b.y += b.vy*dt; }
    for(let i=bullets.length-1;i>=0;i--){ const b=bullets[i]; if(b.x<-40||b.x>state.worldW+40||b.y<-40||b.y>state.worldH+60) bullets.splice(i,1); }

    // hit player
    for(const b of bullets){ const dx=player.x-b.x, dy=player.y-b.y; if(dx*dx+dy*dy <= (player.r+b.r)*(player.r+b.r)){ if(player.dashT<=0){ if(player.shield>0){ player.shield-=1; emit(player.x,player.y,12,'#6ee7ff'); } else if(player.extra>0){ player.extra--; toast('Extra Life!'); } else { return gameOver(); } } } }
  }
  function damageBoss(dmg=1){ if(!boss) return; boss.hp-=dmg; emit(boss.x,boss.y,16,'#c4a7ff'); if(boss.hp<=0){ boss=null; state.boss=false; state.stage++; stageTime=0; nextBossAt = clamp(26 - state.stage, 18, 26); addScore(200); toast(`Stage ${state.stage} クリア!`); AudioKit.fanfare(); }
  }

  function emit(x,y, n=8, col='#a8ffce'){ for(let i=0;i<n;i++){ particles.push({x,y, vx:rand(-90,90), vy:rand(-120,-40), life: rand(.3,.75), col}) } }

  // ===== Input =====
  let left=false, right=false, dashReq=false, shootHold=false;
  window.addEventListener('keydown',e=>{
    if(e.key==='ArrowLeft'||e.key==='a'||e.key==='A') left=true;
    if(e.key==='ArrowRight'||e.key==='d'||e.key==='D') right=true;
    if(e.code==='Space'){ shootHold=true; e.preventDefault(); }
    if(e.key==='Shift') dashReq=true;
  });
  window.addEventListener('keyup',e=>{
    if(e.key==='ArrowLeft'||e.key==='a'||e.key==='A') left=false;
    if(e.key==='ArrowRight'||e.key==='d'||e.key==='D') right=false;
    if(e.code==='Space') shootHold=false;
  });

  const leftBtn=document.getElementById('leftBtn'); const rightBtn=document.getElementById('rightBtn'); const dashBtn=document.getElementById('dashBtn');
  const tp=document.getElementById('touchControls'); const isMobile = /Mobi|Android/i.test(navigator.userAgent); tp.style.display = isMobile? 'flex':'none';
  const press=(b)=>{ b.dataset.down='1'; if(b===leftBtn) left=true; else if(b===rightBtn) right=true; else dashReq=true; };
  const release=(b)=>{ b.dataset.down='0'; if(b===leftBtn) left=false; else if(b===rightBtn) right=false; };
  [leftBtn,rightBtn,dashBtn].forEach(b=>{ b.addEventListener('pointerdown',()=>press(b)); b.addEventListener('pointerup',()=>release(b)); b.addEventListener('pointerleave',()=>release(b)); });
  // mobile taps: single tap=shot, two-finger=dash
  canvas.addEventListener('touchstart',e=>{ if(e.touches.length>=2) { dashReq=true; } else { shootOnce(); } }, {passive:true});
  // desktop click to shoot too
  canvas.addEventListener('mousedown', shootOnce);

  // ===== Loop =====
  let last=performance.now(); function loop(t){ const dt=Math.min(0.033,(t-last)/1000); last=t; if(state.running) update(dt); draw(dt); requestAnimationFrame(loop); } requestAnimationFrame(loop);

  // ===== Mechanics =====
  const stats = { coins:0, dash:0, maxCombo:1, shield:0, score:0 };

  function reset(){ state.running=false; state.over=false; state.t=0; state.score=0; state.stage=1; stageTime=0; nextBossAt=28; state.speed=state.baseSpeed*opts.speedMul; state.combo=1; state.comboTime=0; state.slowed=0; state.boss=false; fx.shakeTime=0; fx.shakeAmp=0; boss=null;
    obstacles.length=0; coins.length=0; particles.length=0; lasers.length=0; powerups.length=0; bullets.length=0; pbullets.length=0;
    player.x=state.worldW/2; player.alive=true; player.flash=0; player.shield=0; player.magnet=0; player.dashCD=Math.max(0,2.6 - uLevel('dashCD')*0.4); player.dashT=0; player.extra = uLevel('extraLife'); player.fireCD=0;
    if(uLevel('startShield')>0) player.shield = 0.8 + 0.4*uLevel('startShield');
    spawnBlockRow(0); updateUI(); }

  function start(){ state.running=true; hide(startOverlay); hide(gameoverOverlay); hide(bossOverlay); AudioKit.tick(); }
  function gameOver(){ state.running=false; state.over=true; player.alive=false; AudioKit.hit(); state.best=Math.max(state.best, Math.floor(state.score)); storage.set(SAVE.best, state.best); const earned = Math.floor((state.score * (1 + 0.1*uLevel('scoreMul')))/100); addGems(earned); finalScore.textContent = Math.floor(state.score); finalBest.textContent = state.best; earnedEl.textContent = earned; finalMissions.textContent = summarizeMissions(); show(gameoverOverlay); updateUI(); }

  function updateUI(){ scoreEl.textContent = Math.floor(state.score); bestEl.textContent = state.best; comboEl.textContent = 'x'+state.combo.toFixed(1); badgesEl.textContent = `⛨ ${Math.ceil(player.shield)} · 🧲 ${Math.ceil(player.magnet)} · ⏳ ${Math.ceil(state.slowed)}`; stageEl.textContent = state.stage; walletEl.textContent = wallet.gems; }

  function addScore(v){ state.score += v * (1 + 0.1*uLevel('scoreMul')) * state.combo; stats.score = Math.floor(state.score); }
  function addCombo(dt){ state.combo = clamp(state.combo + dt*0.05, 1, 5); state.comboTime = 1.8; stats.maxCombo = Math.max(stats.maxCombo, state.combo); }

  function doDash(){ if(player.dashT>0 || player.dashCD>0) return; player.dashT=0.4; player.dashCD=Math.max(0.8, 3.0 - uLevel('dashCD')*0.4); stats.dash++; AudioKit.dash(); toast('Dash!'); fx.shakeTime=0.12; fx.shakeAmp=4; }

  function applyPower(kind){ if(kind==='shield'){ player.shield = Math.max(player.shield, 1.5 + 0.2*uLevel('startShield')); stats.shield++; toast('Shield ⛨'); }
    else if(kind==='magnet'){ player.magnet = Math.max(player.magnet, 4.5 + 0.5*uLevel('magnetDur')); toast('Magnet 🧲'); }
    else if(kind==='slow'){ state.slowed = Math.max(state.slowed, 2.5); toast('Slow ⏳'); }
    AudioKit.power(); }

  function collideCircleRect(cx,cy,cr, r){ const tx=clamp(cx, r.x, r.x+r.w); const ty=clamp(cy, r.y, r.y+r.h); const dx=cx-tx, dy=cy-ty; return dx*dx+dy*dy <= cr*cr; }

  function tryFire(){ if(player.fireCD>0) return; // fire 1~3 shots based on combo
    const n = (state.combo>=3.5? 3 : (state.combo>=2.0? 2:1));
    for(let i=0;i<n;i++){
      const off = (n===1)?0:(i-(n-1)/2)*6; pbullets.push({x:player.x+off, y:player.y-player.r-2, r:3, vy:-380});
    }
    player.fireCD = Math.max(0.08, 0.22 - (state.combo-1)*0.02);
    AudioKit.shoot();
  }
  function shootOnce(){ tryFire(); }

  function update(dt){
    state.t += dt; stageTime += dt; const speedMul = opts.speedMul * (state.slowed>0? 0.55:1); state.speed = clamp(120 + state.t*6, 120, 540) * speedMul; spawnInt = clamp(0.9 - state.t*0.02, 0.26, 0.9); laserInt = clamp(6.0 - state.t*0.01, 3.0, 6.0);

    if(!state.boss && stageTime>=nextBossAt){ enterBoss(); }

    lastSpawn += dt; if(lastSpawn>=spawnInt && !state.boss){ lastSpawn=0; spawnBlockRow(-20); }
    lastLaser += dt; if(lastLaser>=laserInt && !state.boss){ lastLaser=0; spawnLaser(); }

    // Player movement & actions
    const dir = (right?1:0) - (left?1:0);
    const skinCol = skins.find(s=>s.id===currentSkin)?.color || '#eaf2ff'; player.color = skinCol;
    player.vx = dir * player.speed * (player.dashT>0? 1.6:1);
    player.x = clamp(player.x + player.vx * dt, player.r+2, state.worldW - player.r-2);
    if(dashReq){ doDash(); dashReq=false; }
    if(player.dashT>0) player.dashT-=dt; if(player.dashCD>0) player.dashCD-=dt;
    if(player.fireCD>0) player.fireCD-=dt; if(shootHold) tryFire();

    // Stars
    for(const s of stars){ s.y += (state.speed*0.2 + s.sp) * dt; if(s.y>state.worldH) { s.y -= state.worldH; s.x = rand(0,state.worldW);} }

    // Entities movement
    for(const o of obstacles){ o.y += state.speed * dt; o.x += (o.vx||0) * dt; if(o.x<0){ o.x=0; o.vx=Math.abs(o.vx||0);} if(o.x+o.w>state.worldW){ o.x=state.worldW-o.w; o.vx = -Math.abs(o.vx||0);} }
    for(const c of coins){ c.y += (state.speed*0.95) * dt; const ax = (player.magnet>0? (player.x - c.x)*1.6 : 0); const ay = (player.magnet>0? (player.y - c.y)*1.6 : 0); c.x += ax*dt; c.y += ay*dt; }
    for(const p of particles){ p.x += p.vx*dt; p.y += p.vy*dt; p.vy += 420*dt; p.life -= dt; }
    for(const l of lasers){ l.x += l.vx*dt; l.life -= dt; }
    for(const pb of pbullets){ pb.y += pb.vy*dt; }

    if(state.boss){ updateBoss(dt); }

    // Clean
    while(obstacles.length && obstacles[0].y>state.worldH+40) obstacles.shift();
    while(coins.length && coins[0].y>state.worldH+40) coins.shift();
    for(let i=particles.length-1;i>=0;i--) if(particles[i].life<=0) particles.splice(i,1);
    for(let i=lasers.length-1;i>=0;i--) if(lasers[i].life<=0 || lasers[i].x<-160 || lasers[i].x>state.worldW+160) lasers.splice(i,1);
    for(let i=pbullets.length-1;i>=0;i--) if(pbullets[i].y<-30) pbullets.splice(i,1);

    for(let i=powerups.length-1;i>=0;i--) if(powerups[i].y>state.worldH+40) powerups.splice(i,1);
    for(const u of powerups){ u.y += state.speed*0.9*dt; }

    // Collisions with hazards
    let hit=false; if(!state.boss){ for(const o of obstacles){ if(collideCircleRect(player.x,player.y,player.r, o)) { hit=true; break; } } for(const l of lasers){ const r={x:l.x-4, y:l.y-2, w:l.w+8, h:l.h+4}; if(collideCircleRect(player.x,player.y,player.r, r)) { hit=true; break; } } }
    if(hit && player.dashT<=0){ if(player.shield>0){ player.shield-=0.9; emit(player.x, player.y, 14, '#6ee7ff'); fx.shakeTime=0.18; fx.shakeAmp=6; } else if(player.extra>0){ player.extra--; toast('Extra Life!'); emit(player.x,player.y,12,'#86efac'); } else { player.flash=0.18; emit(player.x, player.y, 18, '#ff7777'); return gameOver(); } }

    // coins
    for(let i=coins.length-1;i>=0;i--){ const c=coins[i]; const dx=player.x-c.x, dy=player.y-c.y; if(dx*dx+dy*dy < (player.r+c.r)*(player.r+c.r)){ coins.splice(i,1); addScore(10); addCombo(0.25); stats.coins++; AudioKit.coin(); emit(c.x,c.y,6,'#ffd166'); if(state.boss && boss){ damageBoss(0.3); } } }

    // powerups
    for(let i=powerups.length-1;i>=0;i--){ const u=powerups[i]; const dx=player.x-u.x, dy=player.y-u.y; if(dx*dx+dy*dy < (player.r+u.r)*(player.r+u.r)){ powerups.splice(i,1); applyPower(u.kind); addScore(5); } }

    // player bullets vs boss
    if(boss){ for(let i=pbullets.length-1;i>=0;i--){ const pb=pbullets[i]; const dx=boss.x-pb.x, dy=boss.y-pb.y; if(dx*dx+dy*dy <= (boss.r+pb.r)*(boss.r+pb.r)){ pbullets.splice(i,1); damageBoss(1); addScore(2); } } }

    // Effects timers
    if(player.shield>0) player.shield=Math.max(0, player.shield-dt);
    if(player.magnet>0) player.magnet=Math.max(0, player.magnet-dt);
    if(state.slowed>0) state.slowed=Math.max(0, state.slowed-dt);
    if(player.flash>0) player.flash=Math.max(0, player.flash-0.016);
    if(state.comboTime>0){ state.comboTime-=dt; if(state.comboTime<=0) state.combo = Math.max(1, state.combo-0.1); }

    // Score by time
    addScore(dt*3); updateUI();
  }

  // ===== Rendering =====
  function draw(){ const w=canvas.width/dpr, h=canvas.height/dpr; const sx = (fx.shakeTime>0 && opts.shake)? (rand(-fx.shakeAmp,fx.shakeAmp)) : 0; const sy = (fx.shakeTime>0 && opts.shake)? (rand(-fx.shakeAmp,fx.shakeAmp)) : 0; if(fx.shakeTime>0) fx.shakeTime -= 1/60; ctx.save(); ctx.clearRect(0,0,w,h); ctx.translate(sx, sy);
    const obCol = opts.colorblind? 'rgba(255,255,255,.9)': 'rgba(255,255,255,.14)';
    ctx.save(); ctx.globalAlpha=0.9; for(const s of stars){ ctx.fillStyle = `rgba(255,255,255,${0.2 + s.s*0.2})`; ctx.fillRect(s.x, s.y, s.s, s.s); } ctx.restore();
    ctx.save(); ctx.globalAlpha=0.06; ctx.lineWidth=1; const grid=20; ctx.beginPath(); for(let x=0;x<w;x+=grid){ ctx.moveTo(x,0); ctx.lineTo(x,h);} for(let y=0;y<h;y+=grid){ ctx.moveTo(0,y); ctx.lineTo(w,y);} ctx.strokeStyle='white'; ctx.stroke(); ctx.restore();

    // coins
    ctx.save(); for(const c of coins){ ctx.beginPath(); ctx.arc(c.x, c.y, c.r, 0, Math.PI*2); ctx.fillStyle = opts.colorblind? '#ffbf00' : 'var(--gold)'; ctx.fill(); ctx.lineWidth=1; ctx.strokeStyle='rgba(0,0,0,.25)'; ctx.stroke(); } ctx.restore();
    // powerups
    ctx.save(); for(const u of powerups){ ctx.beginPath(); ctx.arc(u.x, u.y, u.r, 0, Math.PI*2); ctx.fillStyle = u.kind==='shield'? '#6ee7ff' : (u.kind==='magnet'? '#9bffb7' : '#c4a7ff'); ctx.fill(); ctx.strokeStyle='rgba(0,0,0,.3)'; ctx.stroke(); ctx.font='10px system-ui'; ctx.fillStyle='#001'; const sym = u.kind==='shield'? '⛨' : (u.kind==='magnet'? '🧲' : '⏳'); ctx.fillText(sym, u.x-6, u.y+3); } ctx.restore();
    // obstacles & lasers (no boss phase)
    if(!state.boss){ ctx.save(); ctx.fillStyle=obCol; for(const o of obstacles){ ctx.fillRect(o.x, o.y, o.w, o.h); } ctx.restore(); ctx.save(); for(const l of lasers){ const grad=ctx.createLinearGradient(l.x, l.y, l.x+l.w, l.y+l.h); grad.addColorStop(0,'rgba(255,90,90,.85)'); grad.addColorStop(1,'rgba(255,160,160,.5)'); ctx.fillStyle=grad; ctx.fillRect(l.x, l.y, l.w, l.h); } ctx.restore(); }
    // boss
    if(state.boss && boss){ ctx.save(); const g=ctx.createRadialGradient(boss.x-6,boss.y-6,4, boss.x,boss.y,boss.r+6); g.addColorStop(0,'#fff'); g.addColorStop(1,'#c4a7ff'); ctx.fillStyle=g; ctx.beginPath(); ctx.arc(boss.x,boss.y,boss.r,0,Math.PI*2); ctx.fill(); ctx.fillStyle='rgba(255,255,255,.8)'; ctx.fillRect(boss.x-24,boss.y-boss.r-16,48,6); ctx.fillStyle='#ff6bcb'; const hpw = clamp((boss.hp/(6+state.stage*2))*48,0,48); ctx.fillRect(boss.x-24,boss.y-boss.r-16,hpw,6); ctx.restore(); ctx.save(); ctx.fillStyle='#ff9d9d'; for(const b of bullets){ ctx.beginPath(); ctx.arc(b.x,b.y,b.r,0,Math.PI*2); ctx.fill(); } ctx.restore(); }
    // player bullets
    ctx.save(); ctx.fillStyle='#aee3ff'; for(const pb of pbullets){ ctx.beginPath(); ctx.arc(pb.x,pb.y,pb.r,0,Math.PI*2); ctx.fill(); } ctx.restore();
    // player
    ctx.save(); if(player.flash>0){ ctx.shadowColor=getCSS('--danger', '#ff6b6b'); ctx.shadowBlur=18; }
    ctx.beginPath(); ctx.arc(player.x, player.y, player.r, 0, Math.PI*2); const grad=ctx.createRadialGradient(player.x-4,player.y-6,4, player.x,player.y, player.r+6); grad.addColorStop(0, '#ffffff'); grad.addColorStop(1, player.color||'#7ee0ff'); ctx.fillStyle=grad; ctx.fill(); if(player.shield>0){ ctx.globalAlpha=0.25+0.15*Math.sin(performance.now()/120); ctx.beginPath(); ctx.arc(player.x, player.y, player.r+6, 0, Math.PI*2); ctx.strokeStyle='#8ae9ff'; ctx.lineWidth=3; ctx.stroke(); ctx.globalAlpha=1; } if(player.dashT>0){ ctx.globalAlpha=0.5; ctx.beginPath(); ctx.arc(player.x - 10, player.y, player.r*0.9, 0, Math.PI*2); ctx.fillStyle='#bde3ff'; ctx.fill(); ctx.globalAlpha=1; } ctx.restore();
    // particles
    ctx.save(); for(const p of particles){ ctx.globalAlpha = clamp(p.life,0,1); ctx.fillStyle=p.col||'#a8ffce'; ctx.fillRect(p.x, p.y, 2,2); } ctx.restore();
    ctx.restore();
  }

  function getCSS(name, fallback){ return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback; }

  // ===== UI wires =====
  const startOverlay=document.getElementById('overlayStart'); const gameoverOverlay=document.getElementById('overlayGameOver'); const bossOverlay=document.getElementById('overlayBoss');
  const scoreEl=document.getElementById('score'); const bestEl=document.getElementById('best'); const comboEl=document.getElementById('combo'); const stageEl=document.getElementById('stage'); const badgesEl=document.getElementById('badges');
  const btnStart=document.getElementById('btnStart'); const btnRetry=document.getElementById('btnRetry'); const btnHome=document.getElementById('btnHome'); const btnPause=document.getElementById('btnPause'); const btnSound=document.getElementById('btnSound'); const btnSettings=document.getElementById('btnSettings'); const btnShop=document.getElementById('btnShop'); const btnSkins=document.getElementById('btnSkins'); const btnBossGo=document.getElementById('btnBossGo');
  const dlgSettings=document.getElementById('dlgSettings'); const rangeSpeed=document.getElementById('rangeSpeed'); const chkShake=document.getElementById('chkShake'); const chkCB=document.getElementById('chkCB'); const chkEco=document.getElementById('chkEco'); const chkHints=document.getElementById('chkHints'); const missionsEl=document.getElementById('missions'); const walletEl=document.getElementById('wallet');
  const finalScore=document.getElementById('finalScore'); const finalBest=document.getElementById('finalBest'); const earnedEl=document.getElementById('earned'); const finalMissions=document.getElementById('finalMissions');

  function show(el){ el.style.display='grid'; } function hide(el){ el.style.display='none'; }
  btnStart.addEventListener('click',()=>{ start(); AudioKit.enable(); }); btnRetry.addEventListener('click',()=>{ reset(); start(); }); btnHome.addEventListener('click',()=>{ reset(); show(startOverlay); });
  btnPause.addEventListener('click',()=>{ if(!state.running) resume(); else togglePause(); }); btnSound.addEventListener('click',()=>{ const on = AudioKit.toggle(); btnSound.textContent = on ? '🔊' : '🔇'; if(on) AudioKit.tick(); });
  btnSettings.addEventListener('click',()=>{ rangeSpeed.value=opts.speedMul; chkShake.checked=opts.shake; chkCB.checked=opts.colorblind; chkEco.checked=opts.eco; chkHints.checked=opts.hints; dlgSettings.showModal(); });
  document.getElementById('btnSaveSettings').addEventListener('click',(e)=>{ e.preventDefault(); opts.speedMul=parseFloat(rangeSpeed.value); opts.shake=chkShake.checked; opts.colorblind=chkCB.checked; opts.eco=chkEco.checked; opts.hints=chkHints.checked; storage.set(SAVE.opts, opts); dlgSettings.close(); toast('設定を保存しました'); });

  // Shop & Skins
  const dlgShop=document.getElementById('dlgShop'); const shopList=document.getElementById('shopList');
  function buildShop(){ shopList.innerHTML=''; const items=[
    {key:'startShield', name:'開始時シールド', desc:'+0.4〜のシールドを付与', base:40, max:3},
    {key:'magnetDur', name:'マグネット延長', desc:'+0.5s/レベル', base:30, max:5},
    {key:'dashCD', name:'ダッシュCD短縮', desc:'-0.4s/レベル', base:45, max:4},
    {key:'scoreMul', name:'スコア倍率', desc:'+10%/レベル', base:60, max:5},
    {key:'extraLife', name:'エクストラライフ', desc:'1回だけミスを無効化', base:120, max:1},
  ]; items.forEach(it=>{ const lv=uLevel(it.key); const cost = Math.floor(it.base * Math.pow(1.6, lv)); const can = lv<it.max && wallet.gems>=cost; const row=document.createElement('div'); row.className='shop-item'; row.innerHTML=`<div><strong>${it.name}</strong> <span class="chip">Lv.${lv}/${it.max}</span><div class="muted" style="font-size:.85rem">${it.desc}</div></div><button class="btn ${can?'primary':''}" ${can?'':'disabled'}>${lv>=it.max?'MAX':`購入 💎${cost}`}</button>`; row.querySelector('button').onclick=()=>{ if(lv>=it.max) return; if(wallet.gems<cost){ toast('💎不足'); return; } addGems(-cost); upgrades[it.key]=(upgrades[it.key]||0)+1; saveUpgrades(); toast(`${it.name} Lv.${upgrades[it.key]}`); }; shopList.appendChild(row); }); }
  const dlgSkins=document.getElementById('dlgSkins'); const skinList=document.getElementById('skinList');
  function buildSkins(){ skinList.innerHTML=''; skins.forEach(s=>{ const owned = (s.cost===0) || storage.get('skin-'+s.id, false); const can = wallet.gems>=s.cost && !owned; const row=document.createElement('div'); row.className='shop-item'; row.innerHTML=`<div><strong>${s.name}</strong> <span class="chip" style="background:${s.color}; color:#000">●</span> ${s.cost?`<span class='muted'>/ 💎${s.cost}</span>`:'<span class="muted">/ Free</span>'}</div><div><button class="btn ${owned?'':'primary'}" data-id="${s.id}">${owned?(currentSkin===s.id?'使用中':'使用'):('購入')}</button></div>`; row.querySelector('button').onclick=()=>{ if(!owned){ if(wallet.gems<s.cost){ toast('💎不足'); return; } addGems(-s.cost); storage.set('skin-'+s.id,true); } currentSkin=s.id; storage.set(SAVE.skin, currentSkin); buildSkins(); toast(`${s.name} を装備`); }; skinList.appendChild(row); }); }
  btnShop.addEventListener('click',()=>{ buildShop(); dlgShop.showModal(); }); btnSkins.addEventListener('click',()=>{ buildSkins(); dlgSkins.showModal(); }); btnBossGo.addEventListener('click',()=>{ startBoss(); });

  function togglePause(){ if(!state.running || state.over) return; state.running=false; btnPause.textContent='▶'; toast('Pause'); }
  function resume(){ if(state.over) return; state.running=true; btnPause.textContent='⏸'; toast('Resume'); }

  // Missions
  function generateMissions(){ const pool=[
    {id:'c80', text:'コインを80枚集める', test: s=>s.coins>=80},
    {id:'dash4', text:'1プレイでダッシュを4回', test: s=>s.dash>=4},
    {id:'combo35', text:'コンボ倍率3.5達成', test: s=>s.maxCombo>=3.5},
    {id:'shield', text:'シールド取得', test: s=>s.shield>0},
    {id:'score1200', text:'スコア1200到達', test: s=>s.score>=1200},
  ]; const chosen=[]; while(chosen.length<3){ const m=choice(pool); if(!chosen.find(c=>c.id===m.id)) chosen.push(m);} return chosen; }
  let missions = storage.get(SAVE.missions, null); if(!missions){ missions=generateMissions(); storage.set(SAVE.missions, missions);} missionsEl.innerHTML = '<strong>本日のミッション</strong><br>• '+missions.map(m=>m.text).join('<br>• ');
  function summarizeMissions(){ const done = missions.filter(m=>m.test(stats)).map(m=>m.text); return (done.length? done.join(' / ') : 'なし'); }

  // Init
  state.best = storage.get(SAVE.best, 0); walletEl.textContent=wallet.gems; updateUI(); reset(); show(startOverlay);

  </script>
</body>
</html>

『杏アフター / Kyou After』企画書 v1.0

『杏アフター / Kyou After』企画書 v1.0

非公式・二次創作企画書(個人/同人向け)


0. 企画概要(Executive Summary)

  • 作品種別:ビジュアルノベル(全年齢・日本語)※将来的なOVA/ドラマCD展開も想定
  • 題材:『CLANNAD』(Key/VisualArt’s)世界観の二次創作。藤林 を主軸とした後日譚。
  • 開発規模:小〜中規模(スクリプト総量 約10〜14万字 / 本編 6〜8章 + 分岐エンディング3種)
  • ターゲット
    • 原作・関連作ファン(特に杏・椋・春原・智代ラインのファン)
    • 青春群像・学園→社会移行期ドラマを好む層
    • ノスタルジックな地方都市×等身大ロマンスを求める層
  • テーマ
    • 「選択のつづき」 —— 卒業後、私たちは何を選び直すのか
    • 「家族になること」 —— 恋人から“家族”へ変わる責任と喜び
    • 「さよならの効用」 —— 過去への区切りが未来をひらく
  • トーン/キーワード
    • ほろ苦い甘さ / 風通しのよい日常 / 初夏の光 / 夕立 / 教室 / 自転車 / 小さな嘘と大きな誠実

1. 権利・ガイドライン

  • 本企画は非公式の二次創作です。公式画像・音源・シナリオの転用は行いません。
  • 名称・地名等は一般名詞化/迂回表現を基本とし、固有資産の誤使用を回避します。
  • 配布・収益化は各権利者の二次創作ガイドラインに準拠(イベント頒布/少数DL販売を想定)。
  • クレジット表記にて、原作権利元へのリスペクトを明記。

※正式頒布前に最新版ガイドライン確認・必要なら問い合わせ。


2. 企画目的

  • 杏メインの“その後”を丁寧に描き、彼女が大人になる過程ふたりが家族になる瞬間をドラマの核に据える。
  • シリアス偏重に寄らず、日常のユーモア(小気味よい台詞)と手触りのある生活感を両立。
  • 将来のメディア展開(朗読・ドラマCD・動画化)に耐える章立て構造演出設計を備える。

3. ストーリーライン

3.1 ログライン

教員を目指す杏と、働き始めた“あなた”(※デフォルト名は設定可能)。 卒業進路がふたりの距離を少しずつ変えていくなか、過去に置き去りの“選ばれなかった想い”と向き合い、 「家族になる」覚悟を選び直すまでの初夏の物語。

3.2 あらすじ(約300字)

地方都市の春。教育実習を控えた杏は、忙しさを言い訳に“将来”の話を避けがちだ。働き始めたあなたは、 不規則な勤務と責任の重さに揉まれ、すれ違う時間が増えていく。そんな折、杏の前に“教室を嫌う”生徒・澪(みお)が現れ、 杏は初めて「先生」としての壁にぶつかる。過去に置いてきた姉妹の距離、友人たちの旅立ち、雨の日のすれ違い—— 積み重ねた日常はやがて、ひとつの問いに収束する。「わたしたち、家族になる?」

3.3 章立て(想定:全8章 + 分岐ED)

  1. 春、選択の延長線:新生活のリズム / 小さな嘘 / 指切り
  2. 教育実習の教室:問題児・澪との出会い / 杏の“先生”としての初試練
  3. 姉妹の距離:椋の近況と“選ばれなかった側”の痛み / 夕立
  4. 働くという現実:疲労と責任 / それでも笑わせてくれる人
  5. 雨の告白:本音の衝突 / 「守りたいのはあなたの強がり」
  6. 卒業式の教壇:澪の決断 / 杏、教師の覚悟
  7. 未来の設計図:家族の輪郭 / 指輪と自転車の坂道
  8. エピローグ:初夏の風 / 新しい鍵(True/Good/Another End

3.4 分岐・エンディング設計

  • True End:澪の背中を押した杏が、自分の背中も押す。夕暮れの坂道で“家族の宣言”。
  • Good End:互いの課題を持ち帰り、約束は「もう少し先へ」。静かな希望の灯り。
  • Another End:椋が自分の道を歩み出すなか、ふたりは“大切な友だちのまま”。痛みと納得の着地。

4. キャラクター

4.1 藤林 杏(ふじばやし きょう)

  • 属性:快活 / 面倒見 / 短気は愛の裏返し / 不器用な優しさ
  • 目標:良い先生になる。恋人を“家族”にする覚悟を固める。
  • 弱点:強がりが過ぎて、頼ることが苦手。
  • 成長:生徒と向き合うことで、頼る・託す勇気を学ぶ。

4.2 あなた(主人公)

  • 属性:働き始めの若手 / 体力と責任の板挟み
  • 目標:仕事を一人前に。杏と“同じ未来”を見られるようになる。
  • 弱点:疲労で言葉が遅れ、誤解を呼びがち。

4.3 藤林 椋(りょう)

  • 属性:優しさと決意 / 自分の幸せを選び直す人
  • 役割:過去の痛みを整理する“鏡”。杏の成長を促す。

4.4 澪(みお・新規)

  • 属性:教室嫌い / 視線恐怖 / 文学好き
  • 役割:杏を“先生”にする存在。終盤、教壇の前で小さく頷く。

4.5 友人たち(春原、智代 ほか)

  • 機能:軽妙な掛け合い / シリアス緩和 / 人生の先輩の言葉

5. 世界観・舞台

  • 地方都市(坂道と並木、川沿いのベンチ、商店街、古い団地)。
  • 教室/職員室の生活音、雨上がりのアスファルト、夕暮れの自転車。
  • 小動物モチーフ(“いきもの”を大切にする象徴。新規マスコット:**子犬『ポタン』**等の遊び心)

6. 演出・美術・音響

  • 美術:透明感のある初夏色。コントラスト低め、空気遠近感重視。
  • UI:手書き風トーン + 余白多め。章扉は季節のアイコン(鈴蘭/紫陽花/入道雲)。
  • 音響:ピアノ+アコギ中心。日常は3和音モチーフ、雨は単音反復で不安を示唆。
  • 演出
    • 雨滴→傘に乗るSE→台詞の“間”。
    • 黒フラッシュではなく白フェードで前向きな余韻。

7. ゲーム設計

  • プレイ時間:本編6〜8時間 + ED分岐リプレイ
  • 選択肢:各章2〜4箇所。好感度(杏/自分の成熟/他者への誠実)3軸で内部判定。
  • 回想/ギャラリー:事件順/章順 切替、サムネ自動生成。
  • ミニ演出
    • 「メッセージカード」コンポーネント(杏の書置きが日替わりで変化)
    • 「雨の日UI」:ガラスの雨筋エフェクト(負荷軽)

8. シナリオ詳細(ダイジェスト)

  • 第1章:新生活の慌ただしさ。約束を“明日に回す”二人。
  • 第2章:澪と教壇。杏、初めて“言葉が届かない”を知る。
  • 第3章:椋の来訪。喪失の痛みは、誠実さでしか癒やせない。
  • 第4章:あなたの疲労と欠勤。杏の“頼れない癖”が噴出。
  • 第5章:雨の口論→本音の露出→“守りたいのは強がりではなく、あなた自身”。
  • 第6章:卒業式。澪の一歩。杏、先生としての第一歩。
  • 第7章:未来の設計図を並べる夜。指輪ではなくを渡す演出案。
  • 最終章:初夏。坂道の自転車二人乗り(想像の演出)。

9. アセット計画

  • 立ち絵
    • 杏:8表情×2衣装(私服/実習用)+ 小物(髪留め、エプロン)
    • 主人公:シルエット/腕のみ演出(能動性を下げない)
    • 椋:6表情×1衣装
    • 澪:6表情×制服/私服
  • 背景:教室/廊下/川沿い/商店街/部屋/バス停/坂道/夕焼け屋上 ほか(計14〜18)
  • CG:計10〜12枚(雨の口論、手を取る、教壇の前、鍵の受け渡し、夕暮れの坂道など)
  • BGM:12〜15曲(テーマ/日常/雨/夜/決意/エピローグ)
  • SE:環境音重視(雨/風/チャイム/制服の擦れ/シャープペン)

10. 技術

  • エンジン:Ren’Py / 吉里吉里Z / Unity(いずれか)
  • 対応:Windows/Mac(日本語フォント同梱ライセンス確認)
  • スクリプト構造:章ごとにモジュール化、フラグ管理テーブルを別定義。
  • ビルド:CIで自動パッケージング(Win/Mac)・バージョン刻印。

11. スケジュール(目安:24週)

  1. プリプロ(4週):世界観確定/章割/フロー/試作UI
  2. シナリオ(8週):初稿→中稿→最終稿(同時進行でボイス想定台本)
  3. アート(8週):立ち絵→背景→CG→色校
  4. 実装(6週):演出/スクリプト/分岐/セーブ/ギャラリー
  5. QA(2週):誤字修正/分岐網羅/体験版切り出し

※並行最適化で全体24週。小規模なら18〜20週圧縮可。


12. 体制・役割

  • 企画・脚本:1
  • アート(原画/背景/彩色):2〜3
  • サウンド:1
  • 実装:1
  • QA:1

13. 予算(概算・同人規模)

  • シナリオ(14万字×@2円)…… 28万円
  • 立ち絵/CG/背景 合計………… 60〜90万円(規模により変動)
  • BGM/SE…………………………… 15〜25万円
  • 実装/演出………………………… 20〜35万円
  • QA/雑費…………………………… 5〜10万円
  • …………………………… 128〜188万円 目安

14. リスク & 回避

  • 権利トラブル:ガイドライン順守、名称/意匠の自作・差し替え徹底。
  • スケジュール遅延:章ごとマイルストーン・毎週デイリースナップショット。
  • 表現の齟齬:ファンレビュー(クローズド)2回実施。

15. プロモーション

  • 体験版(第1章まるごと):頒布1ヶ月前公開。
  • PV:30秒/90秒(テキストアニメ+BGM)
  • SNS運用:制作ノート/ラフ公開/開発ログ(毎週)
  • 頒布:イベント(コミティア等)/ BOOTH / itch.io / DLsite(ガイドライン適合時)

16. 成功指標(KPI)

  • 体験版DL数 1,000 / 本編頒布数 500〜1,500
  • SNSフォロー +1,000 / 口コミ評価★4.5以上

17. 次アクション(To‑Do)


付録A:吉里吉里Z / Ren’Py 想定仕様メモ

  • 既読スキップ/選択肢ジャンプ/バックログ/環境設定(フォント/字幕速度/音量)
  • スクショ配慮:ロゴ透かし・章名オーバーレイ
  • セーブ互換:章頭オートセーブ + 手動10スロット

付録B:CGリスト(案)

  1. 朝の通学路で小走りの杏(序盤)
  2. 教壇に立つ杏(実習日)
  3. 雨の口論(傘の下)
  4. 椋と杏、夕立後の並木道
  5. 澪が黒板に向き合う瞬間
  6. 鍵の受け渡し(夜、部屋の灯り)
  7. 自転車の二人(夕焼けの坂)
  8. エピローグの窓辺(初夏)

VRMMORPGβ版

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>エルダークロニクル:VR超完全版 – ロードなし(拾う/装備修正)</title>
  <script src="https://aframe.io/releases/1.4.2/aframe.min.js"></script>
  <style>
    body, html { margin:0; padding:0; overflow:hidden; font-family:'Yu Gothic',system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; background:#000;}
    #ui {
      position:absolute; top:20px; left:20px; z-index:10; width:400px; color:#fff;
      background:linear-gradient(180deg,rgba(0,0,0,.92),rgba(0,10,30,.85));
      border-radius:16px; padding:14px 16px; box-shadow:0 0 25px #0ff,0 0 18px #33f4; font-size:15px;
    }
    h2 { margin:0 0 8px 0; font-size:22px; letter-spacing:1px;}
    .row { display:flex; gap:8px; align-items:center; flex-wrap:wrap;}
    .row > * { flex: 1 1 auto; }
    input, select { width:100%; padding:6px 8px; border-radius:8px; border:1px solid #024; background:#00131f; color:#cfe9ff; }
    button {
      background:linear-gradient(90deg,#555,#222 80%);
      color:#fff; padding:10px; margin:5px 0; border:none; cursor:pointer; width:100%;
      border-radius:8px; font-weight:bold; letter-spacing:1px; box-shadow:0 1px 8px #0cf5; transition:background .25s;
    }
    button:hover { background:linear-gradient(90deg,#888 40%,#3cf); }
    .section-title { font-weight:bold; font-size:17px; margin-top:10px; border-bottom:2px solid #5ef; letter-spacing:1.2px; }
    .bar { height:14px; background:#13202a; border-radius:7px; overflow:hidden; margin:6px 0; box-shadow:0 1px 6px #0ff6 inset; }
    .bar-inner { height:100%; background:linear-gradient(90deg,#0f0,#3af); transition:width .25s; }
    .mana-bar .bar-inner { background:linear-gradient(90deg,#33d,#6ff); }
    .enemyhp-bar .bar-inner { background:linear-gradient(90deg,#f55,#fdd); }
    #scenario { margin:10px 0 6px 0; background:rgba(0,10,32,0.7); padding:8px 12px; border-radius:8px; min-height:44px; }
    .kbd { display:inline-block; padding:1px 6px; border-radius:6px; background:#0a2636; border:1px solid #124; font-family:monospace; }
    #hint {
      position:absolute; bottom:18px; left:50%; transform:translateX(-50%);
      color:#eaffff; background:rgba(0,20,35,.72); border:1px solid #0af; padding:8px 12px; border-radius:10px;
      box-shadow:0 0 18px #08f5; font-size:14px; z-index:10;
    }
    #pickupPrompt {
      position:absolute; bottom:70px; left:50%; transform:translateX(-50%);
      color:#fff; background:rgba(0,0,0,.7); border:1px solid #38f; padding:8px 12px; border-radius:10px;
      display:none; z-index:10;
    }
    .tag { display:inline-block; padding:2px 6px; border-radius:6px; background:#012233; border:1px solid #1a4e6c; margin-left:6px; font-size:12px; color:#bfe6ff;}
    .on { background:#0b3754; border-color:#3db3ff; color:#fff; }
  </style>
</head>
<body>
  <div id="ui">
    <h2>エルダークロニクルVR</h2>
    <div class="row">
      <span>プレイヤー名:</span>
      <input id="playerName" placeholder="名前を入力">
    </div>

    <div class="row"><span>レベル:</span><span id="level" class="tag on">1</span>
      <span>装備:</span><span id="equipment" class="tag">なし</span>
    </div>

    <div>HP: <span id="hpText">100</span></div>
    <div class="bar"><div id="hpBar" class="bar-inner" style="width:100%"></div></div>
    <div>魔力: <span id="manaText">100</span></div>
    <div class="bar mana-bar"><div id="manaBar" class="bar-inner" style="width:100%"></div></div>

    <div>敵HP: <span id="enemyHpText">120</span></div>
    <div class="bar enemyhp-bar"><div id="enemyHpBar" class="bar-inner" style="width:100%"></div></div>

    <div class="section-title">シナリオ</div>
    <div id="scenario"></div>

    <div class="section-title">行動</div>
    <button onclick="levelUp()">🎉 レベルアップ</button>
    <button onclick="castSpell()">🪄 魔法発動</button>
    <button onclick="choosePath('wizard')">🧙‍♂️ 魔導士に話す</button>
    <button onclick="choosePath('knight')">🛡️ 騎士に話す</button>
    <button onclick="receiveQuest()">📜 クエスト受注</button>
    <button onclick="toggleEnvironment()">🌄 昼夜切替</button>

    <div class="section-title">移動</div>
    <div class="row">
      <button onclick="changeField('town')">🏘️ 街</button>
      <button onclick="changeField('castle')">🏰 城</button>
    </div>
    <div class="row">
      <button onclick="changeField('cave')">🕳️ 洞窟</button>
      <button onclick="changeField('ruins')">🏛️ 遺跡</button>
    </div>
    <div class="row">
      <button onclick="changeField('dungeon')">🧩 ダンジョン</button>
    </div>

    <div class="section-title">操作</div>
    <div style="line-height:1.6">
      <span class="kbd">WASD</span> 移動 
      <span class="kbd">マウス</span> 視点 
      <span class="kbd">E</span> 拾う 
      <span class="kbd">1</span> 剣装備 
      <span class="kbd">2</span> 銃装備 
      <span class="kbd">クリック</span> 攻撃
    </div>
  </div>

  <div id="pickupPrompt">Eで拾う</div>
  <div id="hint">近くの武器に近づいて <span class="kbd">E</span> で拾い、<span class="kbd">1/2</span> で装備、クリックで攻撃!</div>

  <a-scene loading-screen="enabled:false" renderer="colorManagement:true" shadow="true">
    <a-sky id="sky" color="#0e163e"></a-sky>

    <a-entity id="starParticles" position="0 25 -40" visible="false">
      <a-entity geometry="primitive:sphere; radius:0.18" material="color:#fff; opacity:0.6" position="-10 3 0"></a-entity>
      <a-entity geometry="primitive:sphere; radius:0.12" material="color:#fff; opacity:0.8" position="7 2 -2"></a-entity>
      <a-entity geometry="primitive:sphere; radius:0.11" material="color:#eaf6ff; opacity:0.9" position="3 5 2"></a-entity>
      <a-entity geometry="primitive:sphere; radius:0.10" material="color:#ffe; opacity:0.7" position="13 3 5"></a-entity>
    </a-entity>
    <a-entity id="cloudParticles" position="0 30 -35" visible="true">
      <a-sphere radius="5" position="8 2 -8" color="#f6fbff" opacity="0.18"></a-sphere>
      <a-sphere radius="6" position="-7 3 5" color="#eefbff" opacity="0.14"></a-sphere>
    </a-entity>

    <a-light type="ambient" color="#fff" intensity="1"></a-light>
    <a-light id="sunlight" type="directional" intensity="1.6" position="20 25 -8" castShadow="true" shadow-mapWidth="1024" shadow-mapHeight="1024"></a-light>
    <a-light type="point" color="#cff" intensity="2.2" distance="50" position="2 9 -3"></a-light>
    <a-light type="spot" color="#55aaff" position="0 15 -10" intensity="1.2" angle="30" penumbra="0.7"></a-light>

    <a-plane id="ground" position="0 0 0" rotation="-90 0 0" width="120" height="120" color="#375047" shadow="receive:true"></a-plane>

    <a-entity id="field-town" visible="true">
      <a-circle position="0 0 -6" radius="4" color="#7d7d7d" material="roughness:.9; metalness:.05; opacity:.84; transparent:true"></a-circle>
      <a-entity position="-2 0 -7">
        <a-box width="3.6" height="2.6" depth="2.4" color="#c0a47b" material="roughness:0.3; metalness:0.05" position="0 1.3 0"></a-box>
        <a-cone position="0 3 -0.1" radius-bottom="2.1" height="1.4" color="#7b5322"></a-cone>
        <a-text value="街の家" position="0 3.7 0" color="#fff" width="5" align="center"></a-text>
      </a-entity>
      <a-entity position="2 0 -9">
        <a-box width="2.2" height="2.2" depth="2.4" color="#a86f23" position="0 1.1 0"></a-box>
        <a-cone position="0 2.6 0" radius-bottom="1.4" height="1.0" color="#5c3a12"></a-cone>
      </a-entity>
      <a-entity position="5 0 -5">
        <a-cylinder radius="0.25" height="4" color="#3a2a1a" position="0 2 0"></a-cylinder>
        <a-sphere radius="1.2" color="#174d1f" position="0 3.2 0"></a-sphere>
      </a-entity>
    </a-entity>

    <a-entity id="field-castle" visible="false">
      <a-box position="0 2.8 -12" depth="6" height="6" width="10" color="#ccd2df" material="roughness:0.25; metalness:0.12"></a-box>
      <a-cylinder position="-4 1.2 -13" radius="1.1" height="3.8" color="#858ca3"></a-cylinder>
      <a-cylinder position="4 1.2 -13" radius="1.1" height="3.8" color="#858ca3"></a-cylinder>
      <a-cone position="0 6.4 -12" radius-bottom="3.2" height="2" color="#dba"></a-cone>
      <a-entity geometry="primitive:torus; radius:2.5; tube:0.07" material="color:#66aaff; opacity:.25; transparent:true" position="0 4.5 -12"></a-entity>
      <a-text value="王の城" position="0 6.3 -12" color="#0bf" width="6" align="center"></a-text>
    </a-entity>

    <a-entity id="field-cave" visible="false">
      <a-torus position="2 1.1 -10" radius="1.8" tube="0.8" arc="230" color="#363636" rotation="40 0 90"></a-torus>
      <a-sphere position="2 0.4 -10" radius="1.1" color="#151515"></a-sphere>
      <a-entity position="-2 0 -8">
        <a-cone radius-bottom="0.8" height="2.2" color="#2a2a2a"></a-cone>
        <a-cone radius-bottom="0.5" height="1.4" color="#393939" position="0.7 0 0.4"></a-cone>
      </a-entity>
      <a-text value="洞窟の入口" position="2 2.9 -10" color="#fff" width="6" align="center"></a-text>
    </a-entity>

    <a-entity id="field-ruins" visible="false">
      <a-cylinder position="-2 1.1 -9" radius="0.8" height="2.6" color="#babbb2"></a-cylinder>
      <a-box position="-3.1 1.8 -9" width="3.5" height="0.32" depth="0.7" color="#e0dfc7"></a-box>
      <a-torus position="-2 2.3 -9" radius="0.6" tube="0.09" arc="340" color="#fffeee"></a-torus>
      <a-entity geometry="primitive:torusKnot; p:2; q:7; radius:0.7; tube:0.07" position="2 1.8 -8" material="color:#bcd; opacity:.7; transparent:true"></a-entity>
      <a-text value="古代の遺構" position="0 3.3 -8.5" color="#fff" width="6" align="center"></a-text>
    </a-entity>

    <a-entity id="field-dungeon" visible="false">
      <a-box position="2 1.2 -8.8" depth="3.3" height="2.3" width="3.6" color="#161651"></a-box>
      <a-torus-knot position="-1.5 2.7 -8.2" radius="0.9" tube="0.12" p="3" q="7" color="#64eaff"></a-torus-knot>
      <a-cylinder position="-3 0.7 -9.6" radius="0.8" height="1.2" color="#333"></a-cylinder>
      <a-entity geometry="primitive:torus; radius:1.4; tube:0.05" material="color:#aaeeff; opacity:.18; transparent:true" position="0 2.1 -8.6" rotation="90 0 0"></a-entity>
      <a-text value="ダンジョン入口" position="0 3.1 -8.4" color="#fff" width="6" align="center"></a-text>
    </a-entity>

    <!-- 武器ピックアップ -->
    <a-entity id="swordPickup" class="pickup" data-weapon="sword" position="-1 0 -5">
      <a-box width="0.16" height="1.3" depth="0.08" color="#cfe7ff" material="metalness:0.8; roughness:0.15" position="0 0.75 0"></a-box>
      <a-box width="0.5" height="0.08" depth="0.08" color="#333" position="0 0.1 0"></a-box>
      <a-cylinder radius="0.06" height="0.42" color="#7c5a2b" position="0 -0.1 0"></a-cylinder>
      <a-entity geometry="primitive:ring; radiusInner:0.45; radiusOuter:0.5" rotation="-90 0 0" position="0 0 0" material="color:#44d; opacity:.35; transparent:true"></a-entity>
      <a-text value="剣" color="#fff" position="0 1.7 0" align="center"></a-text>
    </a-entity>

    <a-entity id="gunPickup" class="pickup" data-weapon="gun" position="2 0 -5.5">
      <a-box width="0.7" height="0.18" depth="0.18" color="#222" position="0 0.4 0"></a-box>
      <a-box width="0.3" height="0.28" depth="0.18" color="#444" position="-0.2 0.2 0"></a-box>
      <a-box width="0.12" height="0.40" depth="0.16" color="#333" position="0.2 0.15 0"></a-box>
      <a-cylinder radius="0.06" height="0.28" color="#555" position="0.34 0.46 0" rotation="0 0 90"></a-cylinder>
      <a-entity geometry="primitive:ring; radiusInner:0.45; radiusOuter:0.5" rotation="-90 0 0" position="0 0 0" material="color:#0aa; opacity:.35; transparent:true"></a-entity>
      <a-text value="銃" color="#fff" position="0 1.0 0" align="center"></a-text>
    </a-entity>

    <!-- プレイヤー(リグが移動する:★修正) -->
    <a-entity id="rig" position="0 0 0" wasd-controls="acceleration:30">
      <a-entity id="cam" camera look-controls="pointerLockEnabled:true" position="0 1.6 3"></a-entity>
      <a-entity id="player" position="0 0 -1.5" rotation="0 180 0" shadow="cast:true">
        <a-sphere radius="0.18" color="#ffd7f0" position="0 1.58 0"></a-sphere>
        <a-cylinder radius="0.23" height="0.9" color="#9ad" position="0 1.02 0"></a-cylinder>
        <a-cylinder radius="0.08" height="0.55" color="#9ad" position="-0.16 0.74 0" rotation="0 0 18"></a-cylinder>
        <a-cylinder radius="0.08" height="0.55" color="#9ad" position="0.16 0.74 0" rotation="0 0 -18"></a-cylinder>
        <a-cylinder radius="0.09" height="0.7" color="#79b" position="-0.10 0.35 0"></a-cylinder>
        <a-cylinder radius="0.09" height="0.7" color="#79b" position="0.10 0.35 0"></a-cylinder>
      </a-entity>
    </a-entity>

    <!-- 敵 -->
    <a-entity id="enemy" position="0 0 -9" visible="true">
      <a-sphere id="enemyBody" radius="1.25" color="#9b1e1e" material="metalness:0.35; roughness:.25; emissive:#330000"></a-sphere>
      <a-entity geometry="primitive:torus; radius:1.4; tube:0.05" material="color:#ffeeaa; opacity:.18; transparent:true" position="0 0.3 0" rotation="90 0 0"></a-entity>
      <a-entity geometry="primitive:torusKnot; radius:0.45; tube:0.05; p:2; q:5" material="color:#ffa033; opacity:.22; transparent:true" position="0 1.0 0"></a-entity>
      <a-sphere position="-0.38 0.42 0.95" radius="0.14" color="#fff" material="metalness:.9; roughness:.05; emissive:#a0f"></a-sphere>
      <a-sphere position="0.38 0.42 0.95" radius="0.14" color="#fff" material="metalness:.9; roughness:.05; emissive:#a0f"></a-sphere>
      <a-cone position="-0.45 1.35 0.15" radius-bottom="0.16" height="0.62" color="#f5f3ff" rotation="15 0 60"></a-cone>
      <a-cone position="0.45 1.35 0.15" radius-bottom="0.16" height="0.62" color="#f5f3ff" rotation="15 0 -60"></a-cone>
      <a-text value="敵モンスター" position="0 2.15 0" align="center" color="#fff"></a-text>
    </a-entity>

    <a-entity id="spellEffect" geometry="primitive:sphere; radius:0.55"
              material="color:#72f3ff; opacity:.86; emissive:#88f; transparent:true"
              position="0 1.7 -3" visible="false"
              animation__move="property: position; to: 0 3.1 -6; dur:500; dir:alternate; loop:false">
      <a-entity geometry="primitive:torus; radius:0.7; tube:0.08" material="color:#fff; opacity:.3; transparent:true"></a-entity>
    </a-entity>

    <a-entity id="game" game-manager></a-entity>
  </a-scene>

<script>
let isDay=false, hp=100, mana=100, level=1;
let enemyHP=120, enemyHPMax=120;
function updateBars(){
  document.getElementById("hpText").innerText = Math.max(0,Math.floor(hp));
  document.getElementById("manaText").innerText = Math.max(0,Math.floor(mana));
  document.getElementById("hpBar").style.width = Math.max(0,Math.min(100,hp))+"%";
  document.getElementById("manaBar").style.width = Math.max(0,Math.min(100,mana))+"%";
  document.getElementById("level").innerText = level;
  document.getElementById("enemyHpText").innerText = Math.max(0,enemyHP);
  const w = Math.max(0, Math.min(100, enemyHP*100/enemyHPMax));
  document.getElementById("enemyHpBar").style.width = w+"%";
}
function levelUp(){ level++; hp=Math.min(100,hp+20); mana+=30; showScenario("🎉 レベルアップ!新しい力が湧いてくる!"); updateBars(); }
function castSpell(){
  if(mana<20){ showScenario("💤 魔力が足りない!"); return; }
  mana-=20; updateBars();
  const effect=document.getElementById("spellEffect");
  effect.setAttribute("visible","true");
  setTimeout(()=>{ effect.setAttribute("visible","false"); applyDamageToEnemy(30,"魔法ヒット"); },700);
}
function choosePath(choice){
  if(choice==="wizard"){ document.getElementById("equipment").innerText="魔導士のローブ"; mana+=50; showScenario("🧙‍♂️ 魔導士の試練が始まる…"); }
  else{ document.getElementById("equipment").innerText="騎士の剣"; hp+=30; showScenario("⚔️ 騎士と共に魔王城へ向かう!"); }
  updateBars();
}
function toggleEnvironment(){
  const sky=document.getElementById("sky");
  const star=document.getElementById("starParticles");
  const cloud=document.getElementById("cloudParticles");
  const ground=document.getElementById("ground");
  const sunlight=document.getElementById("sunlight");
  if(isDay){
    sky.setAttribute("color","#0e163e"); star.setAttribute("visible","false"); cloud.setAttribute("visible","true");
    ground.setAttribute("color","#375047"); sunlight.setAttribute("intensity","1.6"); showScenario("🌌 夜の世界へ…");
  }else{
    sky.setAttribute("color","#7ddfff"); star.setAttribute("visible","true"); cloud.setAttribute("visible","false");
    ground.setAttribute("color","#b8ffcc"); sunlight.setAttribute("intensity","2.1"); showScenario("🌞 昼の世界へ…");
  }
  isDay=!isDay;
}
function changeField(fieldName){
  const fields=['town','castle','cave','ruins','dungeon'];
  fields.forEach(name=>{
    const el=document.getElementById(`field-${name}`);
    el.setAttribute('visible', name===fieldName);
  });
  showScenario(`📍 ${fieldName} に移動しました`);
}
function receiveQuest(){
  const quests=[
    "魔導士の塔で失われた書を探せ!","騎士団の旗を取り戻せ!","洞窟の奥に眠る魔石を発見せよ!",
    "遺跡に隠された封印を解け!","ダンジョンの魔王を討伐せよ!"
  ];
  const index=Math.floor(Math.random()*quests.length);
  showScenario("📜 クエスト受注: "+quests[index]);
}

const scenarioList=[
  "目覚めたあなたは不思議な世界にいた。","最初のクエストを受注しよう。","フィールド移動で冒険の扉が開く。",
  "行動や魔法でストーリーが動く。","街で情報を集め、仲間と出会おう。","クエストを進め、魔王に立ち向かえ!"
];
let scenarioIndex=0;
function showScenario(text){ document.getElementById('scenario').innerHTML=text; }
function advanceScenario(){ if(scenarioIndex<scenarioList.length){ showScenario(scenarioList[scenarioIndex]); scenarioIndex++; } }
['levelUp','castSpell','choosePath','changeField','receiveQuest'].forEach(fn=>{
  const orig=window[fn]; window[fn]=function(){ orig.apply(this, arguments); advanceScenario(); }
});

/* ---- ここが拾う/装備の中核 ---- */
const rigEl=()=>document.getElementById('rig');
const camEl=()=>document.getElementById('cam');
const enemyEl=()=>document.getElementById('enemy');
const enemyBodyEl=()=>document.getElementById('enemyBody');

let hasSword=false, hasGun=false, equipped='none';
let attackCooldown=false;
const bullets=[];

function setEquipmentLabel(){
  let label="なし";
  if(equipped==='sword') label="剣";
  if(equipped==='gun') label="銃";
  document.getElementById('equipment').innerText=label;
}

function tryPickup(){
  const rpos=rigEl().object3D.position; // ★リグが移動するのでOK
  const items=[document.getElementById('swordPickup'),document.getElementById('gunPickup')];
  for(const it of items){
    if(!it.getAttribute('visible')) continue;
    const wpos=it.object3D.position;
    const d=rpos.distanceTo(wpos);
    if(d<2.0){
      const w=it.getAttribute('data-weapon');
      it.setAttribute('visible','false');
      if(w==='sword'){ hasSword=true; if(equipped==='none') equipped='sword'; showScenario("🗡️ 剣を拾った! 1で装備。"); }
      if(w==='gun'){ hasGun=true; if(equipped==='none') equipped='gun'; showScenario("🔫 銃を拾った! 2で装備。"); }
      setEquipmentLabel();
      return;
    }
  }
  showScenario("近くに拾えるものはない。");
}

function equipSword(){ if(hasSword){ equipped='sword'; setEquipmentLabel(); showScenario("🗡️ 剣を装備した。"); } else { showScenario("剣をまだ拾っていない。"); } }
function equipGun(){ if(hasGun){ equipped='gun'; setEquipmentLabel(); showScenario("🔫 銃を装備した。"); } else { showScenario("銃をまだ拾っていない。"); } }

function attack(){
  if(attackCooldown) return;
  if(equipped==='sword'){ swordSlash(); }
  else if(equipped==='gun'){ shootBullet(); }
  else{ showScenario("装備がありません。剣や銃を拾ってください。"); }
}

function swordSlash(){
  attackCooldown=true;
  const slash=document.createElement('a-entity');
  slash.setAttribute('geometry','primitive: torus; radius:0.9; tube:0.08; arc:200');
  slash.setAttribute('material','color:#fff; opacity:.6; transparent:true');
  const dir=new AFRAME.THREE.Vector3(); camEl().object3D.getWorldDirection(dir); dir.y=0; dir.normalize();
  const base=rigEl().object3D.position.clone().add(new AFRAME.THREE.Vector3(0,1.2,0)).add(dir.clone().multiplyScalar(0.8));
  slash.setAttribute('position',`${base.x} ${base.y} ${base.z}`);
  const rotY=Math.atan2(dir.x,dir.z)*AFRAME.THREE.MathUtils.RAD2DEG;
  slash.setAttribute('rotation',`0 ${rotY} 0`);
  slash.setAttribute('animation__fade','property: material.opacity; to:0; dur:200; easing:easeOutQuad');
  document.querySelector('a-scene').appendChild(slash);
  setTimeout(()=>slash.parentNode && slash.parentNode.removeChild(slash),220);

  const enemyPos=enemyEl().object3D.position.clone();
  const toEnemy=enemyPos.clone().sub(rigEl().object3D.position.clone().add(new AFRAME.THREE.Vector3(0,1.0,0)));
  const dist=toEnemy.length();
  toEnemy.y=0; toEnemy.normalize();
  const forward=dir.clone();
  const angle=forward.dot(toEnemy);
  if(dist<2.6 && angle>0.5 && enemyEl().getAttribute('visible')){
    applyDamageToEnemy(25,"斬撃");
  } else {
    showScenario("空振り…");
  }
  setTimeout(()=>attackCooldown=false,280);
}

function shootBullet(){
  attackCooldown=true;
  const bullet=document.createElement('a-sphere');
  bullet.setAttribute('radius','0.07');
  bullet.setAttribute('color','#e6f7ff');
  bullet.setAttribute('material','emissive:#88d; metalness:.6; roughness:.2');
  const dir=new AFRAME.THREE.Vector3(); camEl().object3D.getWorldDirection(dir); dir.normalize();
  const start=rigEl().object3D.position.clone().add(new AFRAME.THREE.Vector3(0,1.3,0)).add(dir.clone().multiplyScalar(0.9));
  bullet.setAttribute('position',`${start.x} ${start.y} ${start.z}`);
  document.querySelector('a-scene').appendChild(bullet);
  bullets.push({el:bullet, dir:dir.clone(), life:1800, speed:24});
  setTimeout(()=>attackCooldown=false,120);
}

function applyDamageToEnemy(dmg,label){
  if(!enemyEl().getAttribute('visible')) return;
  enemyHP=Math.max(0, enemyHP - dmg);
  updateBars();
  const origColor=enemyBodyEl().getAttribute('color');
  enemyBodyEl().setAttribute('color','#ffffff');
  setTimeout(()=>enemyBodyEl().setAttribute('color', origColor), 80);
  spawnDamageText(dmg,label);
  if(enemyHP<=0){ killEnemy(); }
}

function spawnDamageText(dmg,label){
  const t=document.createElement('a-text');
  t.setAttribute('value',`${label} -${dmg}`);
  t.setAttribute('color','#ffe6e6');
  t.setAttribute('align','center');
  const p=enemyEl().object3D.position.clone();
  t.setAttribute('position',`${p.x} ${p.y+2.4} ${p.z}`);
  t.setAttribute('animation__rise','property: position; to: '+`${p.x} ${p.y+3.2} ${p.z}`+'; dur:650; easing:easeOutQuad');
  t.setAttribute('animation__fade','property: opacity; to:0; dur:650; easing:easeOutQuad');
  document.querySelector('a-scene').appendChild(t);
  setTimeout(()=>t.parentNode && t.parentNode.removeChild(t),700);
}

function killEnemy(){
  enemyEl().setAttribute('visible','false');
  showScenario("✅ 敵を倒した! 5秒後に再出現する…");
  setTimeout(()=>respawnEnemy(), 5000);
}
function respawnEnemy(){
  enemyHP=enemyHPMax; updateBars();
  const e=enemyEl();
  const x = (Math.random()*6 - 3);
  const z = -8.5 + (Math.random()*3 - 1.5);
  e.setAttribute('position',`${x} 0 ${z}`);
  e.setAttribute('visible','true');
  showScenario("⚠️ 新たな敵が現れた!");
}

/* 入力(E/1/2 の互換強化:★修正) */
AFRAME.registerComponent('game-manager',{
  init:function(){
    window.addEventListener('keydown',(e)=>{
      const k=e.key;
      const c=e.code;
      if(c==='KeyE' || k==='e' || k==='E') tryPickup();
      if(c==='Digit1' || c==='Numpad1' || k==='1') equipSword();
      if(c==='Digit2' || c==='Numpad2' || k==='2') equipGun();
    });
    window.addEventListener('mousedown',()=>attack());
    updateBars(); advanceScenario(); setEquipmentLabel();
  },
  tick:function(time,dt){
    const delta=dt/1000;
    const prompt=document.getElementById('pickupPrompt');
    const rpos=rigEl().object3D.position;
    let near=false;
    ['swordPickup','gunPickup'].forEach(id=>{
      const el=document.getElementById(id);
      if(el.getAttribute('visible')){
        const d=rpos.distanceTo(el.object3D.position);
        if(d<2.0) near=true;
      }
    });
    prompt.style.display = near?'block':'none';

    // 弾
    for(let i=bullets.length-1;i>=0;i--){
      const b=bullets[i];
      if(!b.el.parentNode){ bullets.splice(i,1); continue; }
      b.life -= dt;
      const pos=b.el.object3D.position;
      pos.add(b.dir.clone().multiplyScalar(24*delta));
      if(b.life<=0){ b.el.parentNode.removeChild(b.el); bullets.splice(i,1); continue; }
      if(enemyEl().getAttribute('visible')){
        const d=pos.distanceTo(enemyEl().object3D.position);
        if(d<1.35){
          applyDamageToEnemy(15,"射撃");
          b.el.parentNode.removeChild(b.el);
          bullets.splice(i,1);
        }
      }
    }
    enemyEl().object3D.rotation.y += delta*0.3;
  }
});

updateBars();
advanceScenario();
</script>
</body>
</html>

『ファンタシースター2 VR(仮題)』企画書

『ファンタシースター2 VR(仮題)』企画書

1. 概要

タイトル名:ファンタシースター2 VR(仮)
ジャンル:VR対応3DアクションRPG(ダンジョン探索+戦術バトル)
対応機種:Meta Quest 3 / SteamVR / PS VR2(検討)
プレイ人数:1人(将来的にCO-OP対応を検討)
対象年齢:12歳以上


2. 開発コンセプト

名作『ファンタシースターⅡ』を現代VR技術で完全リメイク。
原作の世界観とキャラクター、シナリオを尊重しつつ、
・没入感ある一人称視点探索
・直感的なバトル操作(モーション・アクション)
・拡張された都市・バイオシステム・ダンジョンの再現
を実現する。


3. 特徴とVRならではの要素

A. 没入型探索

  • 一人称視点によるダンジョン探索
  • 全3D再構築された「モタビア」都市、研究所、バイオドーム、地下施設
  • 「パイプ・テレポーター」による瞬間移動ギミック(VR演出あり)

B. 戦闘システム

  • 武器の物理操作(ソード、ガン、テクニック発動)
  • 旧作のターン制をアレンジした**「ハイブリッド・リアルタイム戦闘」**
    (タイムゲージ制+行動入力:手をかざす/武器を振る/ボタンパネル操作)
  • パーティAI(仲間キャラは自動行動、状況に応じた戦略切替)

C. テクニック(魔法)演出

  • VRでの魔法詠唱アクション(ジェスチャー or インタラクション)
  • 代表例:「レスタ」(回復)、「ゾンデ」(雷撃)など

4. ストーリー概要(原作準拠+VR補完)

西暦1284年。モタビア星ではマザーブレインによる管理社会が完成していた。
だが、突如として現れたバイオモンスターの脅威、そして…
主人公ユーシス(VRではプレイヤー自身)は、恋人の死をきっかけに陰謀の核心へと迫る。

※VRでは原作イベントに新たな視点や演出を加え、臨場感を演出(例:ネイの死のシーンなど)


5. 対応予定機能

機能内容
VR移動方式スムーズ移動/テレポート移動(選択可)
UIホログラムUIパネル、音声ナビ
戦闘補助自動ロックオン、ジェスチャーガイド
サウンド原作BGMのVRリミックス+3D空間音響
コーディネート要素装備外観変更、第一人称での確認可

6. 開発スケジュール(想定)

フェーズ期間内容
企画・検証~2ヶ月原作調査、VRプロトタイプ制作
開発前半3ヶ月戦闘・探索システム実装
開発後半6ヶ月マップ・イベント実装、UI・ボイス収録
テスト2ヶ月VRチューニング、デバッグ、最終調整

7. 収益化・販売戦略(案)

  • 基本パッケージ売切型(4,980円想定)
  • DLC追加シナリオ(ネイ専用編、隠しボスなど)
  • コレクターズエディション(原作アートブック、BGM集)

8. ターゲット層

特徴
昔のPSシリーズファン懐かしさ+現代的な再体験
VR RPGプレイヤー没入感ある探索・バトル体験
SF好き/JRPG好き独特の世界観・設定・BGM

ご希望があれば、次の内容も追加できます:

  • マップ構成と画面イメージ図
  • ジェスチャー一覧
  • ストーリー詳細分岐
  • UIデザインのモックアップ

必要に応じて続き作成しますか?

🎮 ゲーム企画書:『カスタムロボ RE:BOOST(仮)』

🎮 ゲーム企画書:『カスタムロボ RE:BOOST(仮)』


1. タイトル案

  • 『カスタムロボ RE:BOOST』
  • 『カスタムロボ R:Reboot』
  • 『カスタムロボ∞(インフィニティ)』

2. コンセプト

「少年の夢が、再び現実になる。」

プレイヤーはカスタムロボのパイロットとして、機体を自由にカスタマイズし、リアルかつスピーディな戦闘を繰り広げる。かつてのロボットバトルの熱狂を、現代のビジュアルとシステムで完全再現し、シリーズ未体験の若年層と当時のファン両方に向けた“進化系リメイク”。


3. ターゲットユーザー

  • 【主】20~40代:旧作ファン層(N64 / GC / DSユーザー)
  • 【副】10~20代:現代のロボットアクションゲームファン(スプラトゥーン、ARMORED CORE、ガンダム系)

4. プラットフォーム

  • Nintendo Switch 2(想定)
  • PC(Steam)
  • PS5(マルチプラットフォーム想定)

5. ゲームジャンル

  • 対戦型ロボットカスタマイズバトルアクション
    (3Dアクション+パーツ収集+オンライン対戦)

6. 主なゲーム要素

要素内容
ロボットのカスタマイズ頭・胴・脚・ガン・ボム・ポッドなど1000種類以上のパーツ。性能とビジュアルに影響。
スピード感重視のバトル従来のダッシュ・ジャンプ・バーストに加え、新アクション「ブーストチェイン」導入。
シングルモードオリジナルストーリー+リメイク要素。「ラウンドダクロン」のようなドラマ展開。
オンライン対戦ランクマッチ・ルームマッチ・トーナメント。観戦モードあり。
ギルド/クラブ要素ユーザー同士でクランを組み、週替わりのイベントに参加。
スキン/アバターキャラやロボの見た目を変更できる。課金なしでも入手可能。

7. 世界観とストーリー(概要)

西暦2089年。AR空間で行われる仮想バトル「ホロロイド」が世界大会に昇格し、ロボット競技は再び脚光を浴びていた。
主人公は「伝説のロボ乗り」の息子として大会に挑む。背後には国家機密とされる人工知能兵器の陰謀が…。


8. ビジュアルイメージ

  • キャラデザイン:アニメ調+SFテイスト(イラスト例:天神英貴・貞本義行風)
  • ロボデザイン:トイ的なかわいさとメカのかっこよさを両立(例:LBX×AC)

9. 技術・開発情報

  • エンジン:Unreal Engine 5
  • ネットワーク:Photon Fusion / Epic Online Services
  • カスタムAI:バディAIとの協力バトルモードあり

10. マネタイズ・展開

方式内容
買い切り型基本価格:6,980円(DLCパックあり)
DLCストーリー拡張・新パーツセット(例:コラボ機体・歴代作品機体)
コラボ展開他ゲーム/玩具/アニメとコラボ(例:メダロット、LBX、ガンダム)
メディア展開アニメ化・コミカライズ・プラモデル販売(3Dプリント連携も)

11. 差別化ポイント

  • パーツごとの「フィジカル挙動」シミュレーションでリアル感UP
  • AIロボ同士のオート戦も可能(観戦専用モード)
  • 「仮想バトル空間」×「現実の世界」の行き来するハイブリッドストーリー

12. 開発スケジュール(案)

フェーズ内容期間
企画・プロトタイププロトタイピング/ビジュアル検証3ヶ月
α開発基本システム構築6ヶ月
β開発全体完成・バグ修正3ヶ月
PR・リリース体験版配布/事前登録/製品版発売2ヶ月

『デビルチルドレン リメイク』企画書

1. タイトル案

『デビルチルドレン:リバースコード』


2. 企画意図

2000年代初頭に登場し、少年少女が悪魔と契約して戦うという世界観で人気を博した『デビルチルドレン』シリーズを、現代の技術とニーズに合わせてリメイク。ペルソナやポケモン、デジモンの人気が再燃する中、悪魔との契約・育成・融合・人間ドラマを融合させたRPGとして、懐かしさと新しさを両立させます。


3. 想定プラットフォーム

  • Nintendo Switch
  • PlayStation 5
  • Steam(PC)
  • スマートフォン(クラウド連携 or 外伝展開)

4. ゲーム概要

ジャンル

  • 悪魔育成RPG+ダークファンタジーADV

主な要素

  • ターン制バトル(3対3)
  • 悪魔の仲間化・進化・合体(従来の悪魔合体に加え、個体ごとの性格やスキル成長分岐あり)
  • 悪魔と絆を深める「契約イベント」や「共鳴システム」
  • 選択によるストーリールート分岐(LAW / CHAOS / NEUTRAL)
  • ジュブナイル・ホラー要素(子供視点で描く社会の闇)

5. ストーリー構成(例)

あらすじ(リメイク用に刷新)

主人公(カイ / アミ)は、異世界「魔界」と現実世界の交差点である「黄昏区(たそがれく)」に迷い込む。悪魔と契約する能力を持つ「契約者」として覚醒し、2つの世界の崩壊を止める旅へ出る。だが、選ぶ道によって人類の運命も変わる──。


6. メインキャラクター(案)

  • カイ(主人公・男性)
  • アミ(主人公・女性)
  • ロキ(主人公に付き添う謎の悪魔)
  • セト(CHAOSルートの導き手)
  • メタトロン(LAWルートの守護天使)
  • ルシフェル(真実を知る者)

7. 主な新要素

項目内容
3Dグラフィック化Unity / Unreal Engineによるリアルタイム3D
悪魔フルボイス化合体時や契約時に専用セリフあり
難易度調整機能EASY〜CHAOSまで選択可能
マルチエンディングルート分岐と「真・人類ルート」追加
オンライン育成交換要素プレイヤー間で悪魔を交換できる

8. 想定スケジュール

フェーズ期間内容
企画・プリプロ3か月シナリオ/仕様書/キャラデザイン
開発12〜18か月モデリング/実装/収録/テスト
テスト・調整3か月バランス調整/デバッグ
発売XX年XX月パッケージ&DL版展開

9. 想定ターゲット

  • 元祖『デビチル』ファン(20〜40代)
  • 現代のRPGファン(ペルソナ、ポケモン、デジモン層)
  • ダークジュブナイルや退廃SFが好きなプレイヤー

10. プロモーション戦略案

  • 初報ティザーPV公開(『デビチル』ロゴを強調)
  • キャラ人気投票キャンペーン
  • 悪魔デザインコンテスト(ユーザー参加型)
  • オリジナルサウンドトラック配信
  • コラボカフェ、限定グッズ展開

 ゲーム企画書ChronoBlade

 ゲーム企画書

タイトル:「ChronoBlade(クロノブレイド)」

ジャンル:アクションRPG(時間操作×剣術バトル)

プラットフォーム:PC / コンシューマ(PS5 / Xbox) / Steam Deck

プレイ人数:1人(後に協力プレイモードを実装予定)

対象年齢:12歳以上


1. コンセプト

「時間を操る剣士」として、時を越えた世界を旅し、運命を断ち切る壮大な物語を描く。本作は、アクション性と戦略性を融合させた“時間干渉型アクションRPG”であり、時間を遅らせたり、停止したり、巻き戻す力を駆使して戦うのが最大の特徴である。


2. 世界観

――時空を壊された世界で、人々は「未来」を失った。
舞台は、時間軸が乱れた崩壊寸前の多元世界「テンペルクロノス」。各時代(中世・近未来・古代・終末世界など)を旅しながら、異なる歴史の分岐を巡り、「正しい未来」を見出していく。


3. 主人公・キャラクター

  • カイ=アルデン(CV案あり)
     失われた王国の騎士。時を断ち切る剣「クロノブレイド」を手に入れ、失った妹を取り戻すために旅に出る。
  • ティナ=リュミエール
     時の研究者。時間魔法を操ることができ、サポートキャラ兼ヒロイン。

4. ゲームシステム

バトル

  • 通常のアクションバトルに加え、「時間停止」「時間巻き戻し」「未来視(予知)」といったシステムを組み合わせる。
  • コンボ攻撃とタイムスキルの組み合わせで“時空コンボ”が発動。

フィールド探索

  • 各時代の世界を自由に探索。時間の干渉によって地形や人間関係が変化。
  • 例:古代で橋を作れば、未来で村が発展している、などの「因果リンク」要素。

成長・スキル

  • 剣術・時術の2つのスキルツリー。
  • 「記憶のかけら」を集めることで過去の真実や新たな能力を開放。

5. グラフィック・演出

  • セミリアルな3Dアニメ調。
  • UE5を使用、ナイアガラでの時間エフェクト演出を多用。
  • スロー演出や巻き戻しエフェクトなど、視覚的にも時間の影響を感じられる作り。

6. 音楽・効果音

  • テーマ曲は時を感じさせる幻想的な旋律。
  • バトル時には緊張感あるストリングスやピアノ、スロー時にはディレイ・リバース音使用。
  • サウンドディレクター案:古代祐三風を想定。

7. マネタイズ・展開案

  • 本編は買い切り型。
  • 追加DLC(過去の歴史エピソード)配信
  • 時代スキン、ボイスパック販売
  • アニメ化・小説・コミカライズメディア展開も視野に入れる

8. 想定スケジュール

フェーズ内容期間
企画・仕様設計ゲームコンセプト固め、仕様書作成1〜2ヶ月
プロトタイプ制作プレイヤー・バトルコアの試作2〜3ヶ月
本開発ストーリー・マップ制作・演出追加6ヶ月〜12ヶ月
テスト・調整QA・デバッグ・バランス調整2ヶ月
リリース国内外展開1ヶ月〜

9. 開発体制(案)

  • ディレクター:1名
  • プログラマー:2〜3名(UE5ブループリント& C++)
  • デザイナー:2D/3D各1名
  • サウンド:外注
  • シナリオ:1名(外注可)
  • デバッグ:クラウドQA導入想定

必要に応じて、企画書のPDF化・プレゼン用スライド化イラスト付き提案資料化なども可能です。
この内容でよければ続編やDLCの企画案も作成できます。
修正点や、ジャンル・世界観・主人公の変更などご希望があればお知らせください!

4o

ポケットモンスター ファイアレッド リ・イマジンド(Pokémon FireRed Re:Imagined)

【ゲーム企画書】

タイトル(仮)

ポケットモンスター ファイアレッド リ・イマジンド(Pokémon FireRed Re:Imagined)


【ジャンル】

RPG(ロールプレイングゲーム)


【企画意図・背景】

2004年に発売された『ポケットモンスター ファイアレッド』は、初代『赤・緑』のリメイクとして多くのファンに支持されました。本作はその精神を継承しつつ、現代のプレイヤーに向けてさらなる進化を遂げた新たなリメイクです。
「懐かしさ」と「革新」を融合させ、老若男女すべてのプレイヤーが楽しめる冒険体験を提供します。


【対応プラットフォーム】

Nintendo Switch 2(仮称)
※将来的にPCやスマートデバイス展開の可能性も視野


【ターゲットユーザー】

  • ポケモン赤・緑/ファイアレッド世代の20~40代
  • 現在のポケモンプレイヤー(全年齢層)
  • 海外のレトロゲームファン

【主なゲーム特徴】

1. グラフィック完全刷新

  • Unreal Engine 5によるHDグラフィック
  • カントー地方をオープンゾーンとして再構築
  • 天候/時間の概念を導入(リアルタイム連動も可能)

2. バトルシステム刷新

  • アクティブタイム制+クラシックターン制のハイブリッド
  • アニメーションと演出強化(例:ダイナミックカメラ、環境連動)
  • ダブルバトル導入、オンライン対戦完全対応

3. ストーリー拡張

  • ロケット団の過去やリーダー「サカキ」の内面に迫る追加エピソード
  • 主人公に「個別エンディング」導入(選択肢と関係性に応じて変化)

4. キャラ/モンスターの再構築

  • 初代151匹+ジョウト・ホウエンから一部登場(全300匹収録予定)
  • キャラクターボイス実装(重要キャラ中心)
  • 一部トレーナーのデザインを現代風にアップデート

5. 追加コンテンツ・DLC

  • 「オレンジ諸島編」や「ジョウト地方の序盤体験」などのDLC
  • 毎月の期間限定レイドバトルイベント開催

【基本仕様】

項目内容
プレイ人数1人(オンライン時最大4人協力・対戦)
セーブ方式オートセーブ+手動セーブ
ボイス一部フルボイス(メインキャラ)
対戦機能ランクマッチ、フリーバトル、カジュアルルーム
通信ローカル・Wi-Fi・Bluetooth対応
周辺機器対応Joy-Con振動、モーション操作(ポケモン捕獲時)

【開発スケジュール(例)】

フェーズ期間内容
企画・プリプロ3ヶ月仕様決定、アートテスト
α版開発6ヶ月システム実装、マップ構築
β版開発6ヶ月UI、ボイス、イベント追加
デバッグ/最終調整3ヶ月テスト・最適化・ローカライズ
合計約18ヶ月

【販売戦略】

  • 初回特典:特別デザインの「リザードン」配信コード
  • 限定版:設定資料集、サウンドトラック付き
  • コラボ展開:アニメ連動、リアルイベント(ポケモンセンター等)

【予想価格】

  • 通常版:6,980円(税込)
  • 限定版:9,980円(税込)