Aran Red Fantasy.html


<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  <title>Aran Red Fantasy - Ultimate</title>

  <style>
    /* ===============================
       基本CSSスタイル
    =============================== */
    body {
      font-family: Arial, sans-serif;
      background-color: #f0f0f0;
      margin: 0;
      padding: 0;
    }
    header, footer, nav {
      background-color: #005ce6;
      color: #fff;
      text-align: center;
      padding: 10px;
    }
    header h1, footer .container { margin: 0; }

    nav a {
      color: #fff;
      text-decoration: none;
      margin: 0 8px;
      padding: 5px 8px;
      display: inline-block;
    }
    nav a:hover { background-color: #004bb5; border-radius: 4px; }
    nav a.active { background-color: #003a8c; border-radius: 4px; }

    main { padding: 20px; }
    .container { max-width: 1400px; margin: 0 auto; }

    .button {
      background-color: #4CAF50;
      border: none;
      color: white;
      padding: 8px 16px;
      text-align: center;
      text-decoration: none;
      font-size: 14px;
      margin: 4px 2px;
      cursor: pointer;
      border-radius: 5px;
    }
    .button:hover { background-color: #45a049; }
    .disabled { opacity: 0.6; cursor: default; }

    .muted { color:#667; font-size: 13px; }

    /* カード風 */
    .card {
      background-color: #fff;
      border: 1px solid #ddd;
      border-radius: 5px;
      padding: 16px;
      margin-bottom: 20px;
      box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
    }
    .card h3 { margin-top: 0; }

    /* プログレスバー */
    .progress-bar {
      background-color: #ddd;
      border-radius: 5px;
      height: 20px;
      width: 100%;
      margin-bottom: 10px;
    }
    .progress {
      background-color: #4CAF50;
      height: 100%;
      border-radius: 5px;
      width: 0%;
    }

    /* インベントリアイテム表示 */
    .inventory-item {
      background-color: #fff;
      border: 1px solid #ddd;
      border-radius: 5px;
      display: inline-block;
      margin: 5px;
      padding: 10px;
      min-width: 120px;
      text-align: center;
      cursor: pointer;
      transition: background-color 0.2s;
      user-select: none;
    }
    .inventory-item:hover { background-color: #eef; }

    /* メッセージ表示 */
    .message {
      background-color: #fff8dd;
      border: 1px solid #f5c666;
      padding: 10px;
      margin-bottom: 10px;
      border-radius: 5px;
      white-space: pre-wrap;
    }

    /* モーダル */
    .modal-bg {
      position: fixed;
      top: 0; left: 0;
      width:100%; height:100%;
      background: rgba(0,0,0,.5);
      display: flex;
      justify-content: center;
      align-items: center;
      z-index: 999;
    }
    .modal {
      background: #fff;
      padding: 20px;
      border-radius: 5px;
      text-align: center;
      max-width: 520px;
      width: 92%;
    }
    .modal h2 { margin-top: 0; }
    .modal img { max-width: 100%; height: auto; border-radius: 6px; }

    /* キャラクター表示 */
    #character-image {
      max-width: 420px;
      width: 100%;
      height: auto;
      margin: 20px auto;
      display: block;
      border-radius: 8px;
      border: 1px solid #ddd;
      background: #fff;
    }

    /* バトル用スタイル */
    .battle-container {
      display: flex;
      flex-wrap: wrap;
      gap: 20px;
    }
    .enemy-card {
      background-color: #fff;
      border: 1px solid #ddd;
      border-radius: 5px;
      width: 250px;
      padding: 16px;
      text-align: center;
    }

    /* メッセージログ */
    .log {
      background-color: #eef;
      border: 1px solid #bbe;
      border-radius: 5px;
      padding: 10px;
      max-height: 300px;
      overflow-y: auto;
      margin: 10px 0;
      white-space: pre-wrap;
    }

    /* ロケーションボタン */
    #location-buttons button { margin-right: 10px; }

    /* スキル一覧 */
    .skill-list { list-style: none; padding: 0; }
    .skill-list li { margin: 5px 0; }

    /* クエストログ */
    #quest-log-list { list-style: none; padding: 0; }
    #quest-log-list li { margin: 4px 0; }

    /* 実績一覧 */
    #achievement-list { list-style: none; padding: 0; }
    #achievement-list li { margin: 5px 0; }

    /* ===============================
       アートギャラリー
    =============================== */
    .gallery-grid{
      display:grid;
      grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
      gap: 14px;
    }
    .gallery-item{
      background:#fff;
      border:1px solid #ddd;
      border-radius:8px;
      overflow:hidden;
      box-shadow: 0px 2px 4px rgba(0,0,0,0.08);
      display:flex;
      flex-direction:column;
    }
    .gallery-item img{
      width:100%;
      height:auto;
      display:block;
      background:#fff;
    }
    .gallery-meta{
      padding:10px;
      display:flex;
      align-items:center;
      justify-content:space-between;
      gap:8px;
      flex-wrap:wrap;
    }
    .badge{
      display:inline-block;
      padding:4px 8px;
      border-radius:999px;
      font-size:12px;
      background:#eef;
      border:1px solid #bbe;
      color:#223;
    }
    .badge.owned{
      background:#e9ffe9;
      border-color:#9fd49f;
      color:#1c5a1c;
    }
    .badge.rarity-ur{
      background:#fff2cc;
      border-color:#f3d27a;
      color:#6b4b00;
    }
    .badge.rarity-ssr{
      background:#e8f0ff;
      border-color:#9fb7ff;
      color:#133a7a;
    }
    .gallery-actions{
      display:flex;
      gap:8px;
      flex-wrap:wrap;
      padding: 0 10px 12px;
    }

    /* ガチャUI */
    .gacha-row{
      display:flex;
      align-items:center;
      justify-content:space-between;
      gap:10px;
      flex-wrap:wrap;
    }
    .gacha-result{
      margin-top:10px;
      display:flex;
      gap:10px;
      flex-wrap:wrap;
      align-items:flex-start;
    }
    .gacha-card{
      width: 220px;
      background:#fff;
      border:1px solid #ddd;
      border-radius:10px;
      overflow:hidden;
      box-shadow: 0px 2px 4px rgba(0,0,0,0.08);
    }
    .gacha-card img{ width:100%; display:block; }
    .gacha-card .p{ padding:10px; }
  </style>
</head>

<body>
  <!-- ===============================
       ヘッダー
  =============================== -->
  <header>
    <h1>Aran Red Fantasy - Ultimate</h1>
  </header>

  <!-- ===============================
       ナビゲーション
  =============================== -->
  <nav>
    <div class="container">
      <a href="#" id="home-link" onclick="showPage('home')">Home</a>
      <a href="#" id="quests-link" onclick="showPage('quests')">Quests</a>
      <a href="#" id="items-link" onclick="showPage('items')">Items</a>
      <a href="#" id="friends-link" onclick="showPage('friends')">Companions</a>
      <a href="#" id="character-link" onclick="showPage('character')">Character</a>
      <a href="#" id="art-link" onclick="showPage('art')">Art Gallery</a>
      <a href="#" id="battle-link" onclick="showPage('battle')">Battle</a>
      <a href="#" id="store-link" onclick="showPage('store')">Store</a>
      <a href="#" id="craft-link" onclick="showPage('craft')">Craft</a>
      <a href="#" id="skills-link" onclick="showPage('skills')">Skills</a>
      <a href="#" id="questlog-link" onclick="showPage('questlog')">QuestLog</a>
      <a href="#" id="achievements-link" onclick="showPage('achievements')">Achievements</a>
    </div>
  </nav>

  <!-- ===============================
       メインコンテンツ
  =============================== -->
  <main>
    <div class="container" id="content">

      <!-- ===============================
           Home
      =============================== -->
      <div id="home">
        <h2>Welcome to Aran Red Fantasy!</h2>
        <p>Explore the world, complete quests, craft items, recruit companions, and unlock achievements!</p>

        <div id="home-message"></div>

        <button class="button" onclick="showLoginModal()">Log In / Change User</button>
        <button class="button" onclick="logout()">Logout (Reset All Data)</button>
        <br/><br/>

        <!-- BGM(継続再生対応:audioはページ外に置く) -->
        <div class="card">
          <h3>BGM</h3>
          <p class="muted">※ 最初の1回だけ「Enable BGM」を押してください(ブラウザの自動再生ブロック対策)。以後はページ切替しても継続します。</p>
          <button class="button" id="bgm-enable-btn" onclick="enableBGM()">Enable BGM (First Click)</button>
          <button class="button" onclick="toggleMusic()">Toggle Music</button>
          <div class="muted" id="bgm-status">Status: Off</div>
        </div>

        <!-- ★ガチャ(SSR/UR追加) -->
        <div class="card">
          <h3>Art Gacha</h3>
          <div class="gacha-row">
            <div class="muted">
              Cost: <strong>10 Gold</strong> / pull<br/>
              SSR/URが出ます。引いたアートは自動で所持になり、アートギャラリーに追加されます。
            </div>
            <div>
              <button class="button" onclick="pullArtGacha(1)">Pull x1</button>
              <button class="button" onclick="pullArtGacha(10)">Pull x10</button>
            </div>
          </div>
          <div class="muted" id="gacha-status">—</div>
          <div class="gacha-result" id="gacha-result"></div>
        </div>

        <!-- ランダムイベント/天候表示 -->
        <div id="weather-display"></div>
        <button class="button" onclick="triggerRandomEvent()">Check Random Event</button>

        <!-- 昼夜サイクル -->
        <div id="day-night-display"></div>
        <button class="button" onclick="advanceTime()">Pass Time (+6h)</button>

        <!-- 宿屋で休息 -->
        <h3>Inn</h3>
        <button class="button" onclick="restAtInn()">Rest at Inn (10 Gold)</button>

        <!-- ロケーション移動 -->
        <div id="location-section" class="card">
          <h3>Locations</h3>
          <div id="location-buttons">
            <button class="button" onclick="moveLocation('Town')">Move to Town</button>
            <button class="button" onclick="moveLocation('Forest')">Move to Forest</button>
            <button class="button" onclick="moveLocation('Dungeon')">Move to Dungeon</button>
            <button class="button" onclick="moveLocation('Mountain')">Move to Mountain</button>
          </div>
          <p>Current Location: <span id="current-location">Town</span></p>
          <div class="log" id="location-log"></div>
        </div>
      </div>

      <!-- ===============================
           Quests
      =============================== -->
      <div id="quests" style="display: none;">
        <h2>Quests</h2>

        <h3>Main Quests</h3>
        <div class="card" id="dragon-quest">
          <h4>Defeat the Dragon</h4>
          <p>A fierce dragon has appeared near the village! Defeat it to save the locals.</p>
          <div class="progress-bar">
            <div class="progress" id="dragon-progress"></div>
          </div>
          <p>Reward: 100 Gold, 100 XP, Dragon Scale</p>
          <button class="button" onclick="startQuest('dragon')">Start Quest</button>
        </div>

        <div class="card" id="final-quest" style="display: none;">
          <h4>The Ancient Evil (Final)</h4>
          <p>The final threat emerges after you've proven your strength! Vanquish it!</p>
          <div class="progress-bar">
            <div class="progress" id="final-progress"></div>
          </div>
          <p>Reward: 200 Gold, 200 XP, Legendary Relic</p>
          <button class="button" onclick="startQuest('final')">Start Quest</button>
        </div>

        <h3>Side Quests</h3>
        <div class="card" id="crystal-quest">
          <h4>Collect Magic Crystals</h4>
          <p>Gather magical crystals scattered around the forest. Watch out for monsters!</p>
          <div class="progress-bar">
            <div class="progress" id="crystal-progress"></div>
          </div>
          <p>Reward: 50 Gold, 50 XP, Magic Crystal</p>
          <button class="button" onclick="startQuest('crystal')">Start Quest</button>
        </div>

        <div class="card" id="orc-quest">
          <h4>Eliminate the Orc Bandits</h4>
          <p>A group of orc bandits is attacking travelers. Defeat them to restore peace!</p>
          <div class="progress-bar">
            <div class="progress" id="orc-progress"></div>
          </div>
          <p>Reward: 80 Gold, 70 XP, Orc Tusk</p>
          <button class="button" onclick="startQuest('orc')">Start Quest</button>
        </div>
      </div>

      <!-- ===============================
           Items
      =============================== -->
      <div id="items" style="display: none;">
        <h2>Inventory</h2>
        <p>Click an item to use/equip/sell it (if applicable).</p>
        <div id="inventory"></div>
      </div>

      <!-- ===============================
           Companions
      =============================== -->
      <div id="friends" style="display: none;">
        <h2>Companions</h2>
        <p>Hire companions who fight alongside you!</p>
        <input type="text" id="friendName" placeholder="Companion name" />
        <button class="button" onclick="hireCompanion()">Hire Companion</button>

        <h3>Your Companions</h3>
        <ul id="companion-list"></ul>
        <p class="muted">* Each companion has its own level, HP, and Attack. They also gain XP when you do.</p>
      </div>

      <!-- ===============================
           Character
      =============================== -->
      <div id="character" style="display: none;">
        <h2>Character</h2>
        <img src="a.png" alt="Character" id="character-image"/>
        <p>Name: <span id="character-name"></span></p>
        <p>Level: <span id="character-level"></span></p>
        <p>HP: <span id="character-hp"></span> / <span id="character-maxhp"></span></p>
        <p>XP: <span id="character-xp"></span> / <span id="character-nextLevelXp"></span></p>
        <p>Gold: <span id="character-gold"></span></p>
        <p>Attack: <span id="character-attack"></span></p>
        <p>Defense: <span id="character-defense"></span></p>
        <p>Skill Points: <span id="character-skillpoints"></span></p>
        <p>Active Buffs/Debuffs: <span id="character-buffs">None</span></p>
        <p>Special Items: <span id="character-items">None</span></p>
        <p class="muted">Portrait changes when you buy or pull an art (Store / Gacha).</p>
      </div>

      <!-- ===============================
           Art Gallery
      =============================== -->
      <div id="art" style="display: none;">
        <h2>アートギャラリー</h2>
        <p class="muted">所持済みのアートは「Set as Character Art」でキャラクター画像に設定できます。ガチャでも入手できます。</p>

        <div class="card">
          <h3>Your Art Collection</h3>
          <div id="art-collection-summary" class="muted"></div>
        </div>

        <div class="gallery-grid" id="art-gallery-grid"></div>
      </div>

      <!-- ===============================
           Battle
      =============================== -->
      <div id="battle" style="display: none;">
        <h2>Battle Arena</h2>
        <p>Choose an enemy to fight or wait for random encounters in the wild!</p>

        <div class="battle-container">
          <div class="enemy-card">
            <h3>Slime</h3>
            <p>HP: 30</p>
            <p>Attack: 1-3</p>
            <p>Reward: 10 Gold, 10 XP</p>
            <button class="button" onclick="startBattle('slime')">Fight Slime</button>
          </div>

          <div class="enemy-card">
            <h3>Goblin</h3>
            <p>HP: 50</p>
            <p>Attack: 2-5</p>
            <p>Reward: 20 Gold, 20 XP</p>
            <button class="button" onclick="startBattle('goblin')">Fight Goblin</button>
          </div>

          <div class="enemy-card">
            <h3>Orc Warrior</h3>
            <p>HP: 80</p>
            <p>Attack: 5-8</p>
            <p>Reward: 40 Gold, 40 XP</p>
            <button class="button" onclick="startBattle('orcEnemy')">Fight Orc</button>
          </div>
        </div>

        <div class="log" id="battle-log"></div>
      </div>

      <!-- ===============================
           Store
      =============================== -->
      <div id="store" style="display: none;">
        <h2>Store</h2>
        <p>Use your gold to purchase or sell items!</p>

        <div class="card">
          <h3>Buy Items</h3>
          <div>
            <h4>Minor Health Potion (20 Gold)</h4>
            <button class="button" onclick="buyItem('Minor Health Potion')">Buy</button>
          </div>
          <div>
            <h4>Major Health Potion (50 Gold)</h4>
            <button class="button" onclick="buyItem('Major Health Potion')">Buy</button>
          </div>
          <div>
            <h4>Iron Sword (80 Gold)</h4>
            <button class="button" onclick="buyItem('Iron Sword')">Buy</button>
          </div>
          <div>
            <h4>Steel Armor (100 Gold)</h4>
            <button class="button" onclick="buyItem('Steel Armor')">Buy</button>
          </div>
          <div>
            <h4>Lucky Ring (120 Gold)</h4>
            <button class="button" onclick="buyItem('Lucky Ring')">Buy</button>
          </div>
        </div>

        <!-- アート購入:購入するとキャラクター絵が変わる -->
        <div class="card">
          <h3>Art Shop(購入でキャラクター画像が変わる)</h3>
          <p class="muted">Buy an art → it becomes “Owned” and you can set it anytime. (Gacha also adds Owned.)</p>
          <div id="art-shop-list"></div>
        </div>

        <div class="card">
          <h3>Sell Items</h3>
          <p>Click an item in your inventory to sell it, if possible.</p>
          <p class="muted">(You can't sell special quest items or currently equipped gear.)</p>
        </div>
      </div>

      <!-- ===============================
           Craft
      =============================== -->
      <div id="craft" style="display: none;">
        <h2>Item Crafting</h2>
        <p>Combine items to create something new!</p>
        <div class="card">
          <h3>Example Recipes</h3>
          <ul>
            <li>Dragon Scale + Orc Tusk => Dragon Tusk Lance (Weapon)</li>
            <li>Magic Crystal + Magic Crystal => Greater Crystal (Special)</li>
          </ul>
          <p>Select any two items from your inventory to craft (if a valid recipe exists).</p>
        </div>
        <p>Currently Selected: <span id="craft-selection">None</span></p>
        <button class="button" id="craft-button" onclick="attemptCraft()" disabled>Craft</button>
      </div>

      <!-- ===============================
           Skills
      =============================== -->
      <div id="skills" style="display: none;">
        <h2>Skills</h2>
        <p>Use skill points to learn or upgrade skills!</p>
        <p>You have <span id="skill-point-display"></span> skill points.</p>
        <ul class="skill-list" id="skill-list"></ul>
      </div>

      <!-- ===============================
           QuestLog
      =============================== -->
      <div id="questlog" style="display: none;">
        <h2>Quest Log</h2>
        <ul id="quest-log-list"></ul>
      </div>

      <!-- ===============================
           Achievements
      =============================== -->
      <div id="achievements" style="display: none;">
        <h2>Achievements</h2>
        <ul id="achievement-list"></ul>
      </div>

    </div>
  </main>

  <!-- ===============================
       フッター
  =============================== -->
  <footer>
    <div class="container">
      &copy; 2025 Aran Red Fantasy
    </div>
  </footer>

  <!-- ===============================
       ログインモーダル
  =============================== -->
  <div class="modal-bg" id="login-modal-bg" style="display: none;">
    <div class="modal">
      <h2>Enter Your Name</h2>
      <input type="text" id="loginName" placeholder="Your name" />
      <br/><br/>
      <button class="button" onclick="confirmLogin()">Login</button>
      <button class="button" onclick="closeLoginModal()">Cancel</button>
    </div>
  </div>

  <!-- ===============================
       アイテム使用モーダル
  =============================== -->
  <div class="modal-bg" id="item-modal-bg" style="display: none;">
    <div class="modal">
      <h2 id="item-modal-title">Use/Equip Item</h2>
      <p id="item-modal-description"></p>
      <button class="button" onclick="confirmItemUse()">Use/Equip</button>
      <button class="button" onclick="closeItemModal()">Cancel</button>
    </div>
  </div>

  <!-- ===============================
       アートプレビューモーダル
  =============================== -->
  <div class="modal-bg" id="art-modal-bg" style="display: none;">
    <div class="modal">
      <h2 id="art-modal-title">Art Preview</h2>
      <img id="art-modal-img" alt="Art Preview" />
      <p class="muted" id="art-modal-desc"></p>
      <div style="margin-top:10px;">
        <button class="button" id="art-modal-set-btn" onclick="confirmSetPortrait()">Set as Character Art</button>
        <button class="button" onclick="closeArtModal()">Close</button>
      </div>
    </div>
  </div>

  <!-- ===============================
       BGM本体(継続再生のためページ切替の外に置く)
  =============================== -->
  <audio id="bgm" loop preload="auto" playsinline>
    <source src="http://tyosuke20xx.com/fjordnosundakaze.mp3" type="audio/mpeg">
  </audio>

  <!-- ===============================
       JavaScript
  =============================== -->
  <script>
    // -------------------------------------------
    // ページ切り替え
    // -------------------------------------------
    function showPage(page) {
      const pages = [
        "home", "quests", "items", "friends", "character",
        "art",
        "battle", "store", "craft", "skills", "questlog", "achievements"
      ];
      pages.forEach(p => {
        const pageElement = document.getElementById(p);
        const linkElement = document.getElementById(p + '-link');
        if (!pageElement) return;

        if (p === page) {
          pageElement.style.display = "block";
          if (linkElement) linkElement.classList.add("active");
        } else {
          pageElement.style.display = "none";
          if (linkElement) linkElement.classList.remove("active");
        }
      });

      if (page === "skills") refreshSkillList();
      if (page === "questlog") updateQuestLog();
      if (page === "achievements") updateAchievementList();
      if (page === "art") renderArtGallery();
      if (page === "store") renderArtShop();
    }

    // -------------------------------------------
    // ローカルストレージキー
    // -------------------------------------------
    const LS_KEY_USER        = "ARF_Username_Ultimate";
    const LS_KEY_CHARACTER   = "ARF_Character_Ultimate";
    const LS_KEY_INVENTORY   = "ARF_Inventory_Ultimate";
    const LS_KEY_COMPANIONS  = "ARF_Companions_Ultimate";
    const LS_KEY_QUESTS      = "ARF_Quests_Ultimate";
    const LS_KEY_SKILLS      = "ARF_Skills_Ultimate";
    const LS_KEY_DAYTIME     = "ARF_Daytime_Ultimate";
    const LS_KEY_WEATHER     = "ARF_Weather_Ultimate";
    const LS_KEY_ACHIEVEMENT = "ARF_Achievement_Ultimate";

    // BGM状態
    const LS_KEY_BGM = "ARF_BGM_STATE_Ultimate";

    // -------------------------------------------
    // ★アート定義(SSR1〜SSR3 + UR1〜UR10)
    // -------------------------------------------
    const ART_LIST = [
      // SSR(追加)
      { key:"SSR1", name:"SSR Art 1", url:"http://tyosuke20xx.com/SSR1.png", cost: 20, rarity:"SSR" },
      { key:"SSR2", name:"SSR Art 2", url:"http://tyosuke20xx.com/SSR2.png", cost: 20, rarity:"SSR" },
      { key:"SSR3", name:"SSR Art 3", url:"http://tyosuke20xx.com/SSR3.png", cost: 20, rarity:"SSR" },

      // UR
      { key:"UR1",  name:"UR Art 1",  url:"http://tyosuke20xx.com/UR1.png",  cost: 30, rarity:"UR" },
      { key:"UR2",  name:"UR Art 2",  url:"http://tyosuke20xx.com/UR2.png",  cost: 30, rarity:"UR" },
      { key:"UR3",  name:"UR Art 3",  url:"http://tyosuke20xx.com/UR3.png",  cost: 30, rarity:"UR" },
      { key:"UR4",  name:"UR Art 4",  url:"http://tyosuke20xx.com/UR4.png",  cost: 30, rarity:"UR" },
      { key:"UR5",  name:"UR Art 5",  url:"http://tyosuke20xx.com/UR5.png",  cost: 30, rarity:"UR" },
      { key:"UR6",  name:"UR Art 6",  url:"http://tyosuke20xx.com/UR6.png",  cost: 30, rarity:"UR" },
      { key:"UR7",  name:"UR Art 7",  url:"http://tyosuke20xx.com/UR7.png",  cost: 30, rarity:"UR" },
      { key:"UR8",  name:"UR Art 8",  url:"http://tyosuke20xx.com/UR8.png",  cost: 30, rarity:"UR" },
      { key:"UR9",  name:"UR Art 9",  url:"http://tyosuke20xx.com/UR9.png",  cost: 30, rarity:"UR" },
      { key:"UR10", name:"UR Art 10", url:"http://tyosuke20xx.com/UR10.png", cost: 30, rarity:"UR" }
    ];

    // -------------------------------------------
    // ★ガチャ設定(SSR/UR抽選)
    // -------------------------------------------
    const GACHA_COST = 10;         // 1回10G
    const GACHA_RATE_UR = 10;      // UR 10%
    const GACHA_RATE_SSR = 90;     // SSR 90%(残り)

    // -------------------------------------------
    // キャラクター情報
    // -------------------------------------------
    let character = {
      name: "Adventurer",
      level: 1,
      hp: 50,
      maxHp: 50,
      xp: 0,
      nextLevelXp: 100,
      gold: 0,
      attack: 5,
      defense: 2,
      skillPoints: 0,
      location: "Town",
      specialItems: [],
      buffs: [],

      ownedArtKeys: [],
      portraitUrl: "a.png"
    };

    // -------------------------------------------
    // クエスト情報
    // -------------------------------------------
    let mainQuests = {
      dragon: {
        name: "Defeat the Dragon",
        progress: 0,
        reward: { gold: 100, xp: 100, items: ["Dragon Scale"] },
        isRunning: false,
        isCompleted: false,
        unlockNext: "final",
        locked: false
      },
      final: {
        name: "The Ancient Evil",
        progress: 0,
        reward: { gold: 200, xp: 200, items: ["Legendary Relic"] },
        isRunning: false,
        isCompleted: false,
        unlockNext: null,
        locked: true
      }
    };
    let sideQuests = {
      crystal: {
        name: "Collect Magic Crystals",
        progress: 0,
        reward: { gold: 50, xp: 50, items: ["Magic Crystal"] },
        isRunning: false,
        isCompleted: false,
        unlockNext: null,
        locked: false
      },
      orc: {
        name: "Eliminate the Orc Bandits",
        progress: 0,
        reward: { gold: 80, xp: 70, items: ["Orc Tusk"] },
        isRunning: false,
        isCompleted: false,
        unlockNext: null,
        locked: false
      }
    };

    function getAllQuests() {
      return { ...mainQuests, ...sideQuests };
    }

    // -------------------------------------------
    // インベントリ
    // -------------------------------------------
    let inventory = [];

    // -------------------------------------------
    // 仲間
    // -------------------------------------------
    let companions = [];

    // -------------------------------------------
    // スキル
    // -------------------------------------------
    let skills = {
      Fireball: {
        name: "Fireball",
        level: 0,
        maxLevel: 3,
        cost: 1,
        description: "Deal extra magic damage in battle"
      },
      Heal: {
        name: "Heal",
        level: 0,
        maxLevel: 3,
        cost: 1,
        description: "Restores some HP at the start of battle"
      }
    };

    // -------------------------------------------
    // バトル用エネミー
    // -------------------------------------------
    const enemies = {
      slime: {
        name: "Slime",
        hp: 30,
        attackMin: 1,
        attackMax: 3,
        rewardGold: 10,
        rewardXp: 10
      },
      goblin: {
        name: "Goblin",
        hp: 50,
        attackMin: 2,
        attackMax: 5,
        rewardGold: 20,
        rewardXp: 20
      },
      orcEnemy: {
        name: "Orc Warrior",
        hp: 80,
        attackMin: 5,
        attackMax: 8,
        rewardGold: 40,
        rewardXp: 40
      }
    };

    // -------------------------------------------
    // ストアアイテム
    // -------------------------------------------
    const storeItems = {
      "Minor Health Potion": { name: "Minor Health Potion", type: "potion", heal: 20, cost: 20 },
      "Major Health Potion": { name: "Major Health Potion", type: "potion", heal: 50, cost: 50 },
      "Iron Sword": { name: "Iron Sword", type: "weapon", attack: 5, cost: 80, equipped: false },
      "Steel Armor": { name: "Steel Armor", type: "armor", defense: 5, cost: 100, equipped: false },
      "Lucky Ring": { name: "Lucky Ring", type: "accessory", attack: 1, defense: 1, cost: 120, equipped: false }
    };

    // -------------------------------------------
    // クラフト用レシピ
    // -------------------------------------------
    const craftRecipes = [
      {
        components: ["Dragon Scale", "Orc Tusk"].sort(),
        result: { name: "Dragon Tusk Lance", type: "weapon", attack: 10, equipped: false }
      },
      {
        components: ["Magic Crystal", "Magic Crystal"].sort(),
        result: { name: "Greater Crystal", type: "special" }
      }
    ];

    // -------------------------------------------
    // 昼夜 & 天候
    // -------------------------------------------
    let currentHour = 12;
    let currentWeather = "Sunny";
    const possibleWeathers = ["Sunny","Rainy","Storm","Cloudy"];

    // -------------------------------------------
    // 実績
    // -------------------------------------------
    let achievements = {
      firstKill: { name: "First Blood", description: "Defeat your first enemy.", isUnlocked: false },
      level5:    { name: "Rising Hero", description: "Reach Level 5.", isUnlocked: false },
      quest3:    { name: "Quest Hunter", description: "Complete 3 Quests.", isUnlocked: false }
    };

    // -------------------------------------------
    // onload
    // -------------------------------------------
    window.onload = function() {
      loadLocalData();

      // BGM(継続再生 & 状態保存)
      loadBgmState();
      wireBgmAutoSave();
      updateEnableBtn();
      setBgmStatus(isMusicPlaying ? "On (will resume)" : "Off");
      resumeBgmOnNextUserActionIfNeeded();

      updateCharacterInfo();
      updateQuestVisibility();
      updateInventoryDisplay();
      updateCompanionList();
      updateDayNightDisplay();
      updateWeatherDisplay();
      updateAchievementList();
      renderArtShop();
      renderArtGallery();

      showPage('home');
      document.getElementById("current-location").textContent = character.location;
    };

    // -------------------------------------------
    // ローカルストレージ: 読込/保存/リセット
    // -------------------------------------------
    function loadLocalData() {
      let storedName = localStorage.getItem(LS_KEY_USER);
      if (storedName) character.name = storedName;

      let storedChar = localStorage.getItem(LS_KEY_CHARACTER);
      if (storedChar) {
        try {
          const parsed = JSON.parse(storedChar);
          character = { ...character, ...parsed };
        } catch(e) {}
      }

      if (!Array.isArray(character.ownedArtKeys)) character.ownedArtKeys = [];
      if (!character.portraitUrl) character.portraitUrl = "a.png";

      let storedInv = localStorage.getItem(LS_KEY_INVENTORY);
      if (storedInv) { try { inventory = JSON.parse(storedInv); } catch(e) {} }

      let storedComp = localStorage.getItem(LS_KEY_COMPANIONS);
      if (storedComp) { try { companions = JSON.parse(storedComp); } catch(e) {} }

      let storedMQ = localStorage.getItem(LS_KEY_QUESTS+"_main");
      if (storedMQ) { try { mainQuests = JSON.parse(storedMQ); } catch(e) {} }

      let storedSQ = localStorage.getItem(LS_KEY_QUESTS+"_side");
      if (storedSQ) { try { sideQuests = JSON.parse(storedSQ); } catch(e) {} }

      let storedSkills = localStorage.getItem(LS_KEY_SKILLS);
      if (storedSkills) { try { skills = JSON.parse(storedSkills); } catch(e) {} }

      let storedHour = localStorage.getItem(LS_KEY_DAYTIME+"_hour");
      if (storedHour) currentHour = parseInt(storedHour, 10);

      let storedWeather = localStorage.getItem(LS_KEY_WEATHER);
      if (storedWeather) currentWeather = storedWeather;

      let storedAchv = localStorage.getItem(LS_KEY_ACHIEVEMENT);
      if (storedAchv) { try { achievements = JSON.parse(storedAchv); } catch(e) {} }
    }

    function saveLocalData() {
      localStorage.setItem(LS_KEY_USER, character.name);
      localStorage.setItem(LS_KEY_CHARACTER, JSON.stringify(character));
      localStorage.setItem(LS_KEY_INVENTORY, JSON.stringify(inventory));
      localStorage.setItem(LS_KEY_COMPANIONS, JSON.stringify(companions));
      localStorage.setItem(LS_KEY_QUESTS+"_main", JSON.stringify(mainQuests));
      localStorage.setItem(LS_KEY_QUESTS+"_side", JSON.stringify(sideQuests));
      localStorage.setItem(LS_KEY_SKILLS, JSON.stringify(skills));
      localStorage.setItem(LS_KEY_DAYTIME+"_hour", currentHour.toString());
      localStorage.setItem(LS_KEY_WEATHER, currentWeather);
      localStorage.setItem(LS_KEY_ACHIEVEMENT, JSON.stringify(achievements));
    }

    function logout() {
      if (!confirm("All data will be cleared. Are you sure?")) return;
      localStorage.clear();
      location.reload();
    }

    // -------------------------------------------
    // ログインモーダル
    // -------------------------------------------
    function showLoginModal() {
      document.getElementById("login-modal-bg").style.display = "flex";
    }
    function closeLoginModal() {
      document.getElementById("login-modal-bg").style.display = "none";
    }
    function confirmLogin() {
      const inputName = document.getElementById("loginName").value.trim();
      if (inputName) {
        character.name = inputName;
        saveLocalData();
        updateCharacterInfo();
      }
      closeLoginModal();
    }

    // -------------------------------------------
    // キャラクター情報表示更新
    // -------------------------------------------
    function updateCharacterInfo() {
      document.getElementById("character-name").textContent = character.name;
      document.getElementById("character-level").textContent = character.level;
      document.getElementById("character-hp").textContent = character.hp;
      document.getElementById("character-maxhp").textContent = character.maxHp;
      document.getElementById("character-xp").textContent = character.xp;
      document.getElementById("character-nextLevelXp").textContent = character.nextLevelXp;
      document.getElementById("character-gold").textContent = character.gold;
      document.getElementById("character-attack").textContent = character.attack;
      document.getElementById("character-defense").textContent = character.defense;
      document.getElementById("character-skillpoints").textContent = character.skillPoints;

      const img = document.getElementById("character-image");
      if (img) img.src = character.portraitUrl || "a.png";

      if (character.specialItems.length > 0) {
        document.getElementById("character-items").textContent = character.specialItems.join(", ");
      } else {
        document.getElementById("character-items").textContent = "None";
      }

      if (character.buffs.length > 0) {
        document.getElementById("character-buffs").textContent = character.buffs.map(b => b.name).join(", ");
      } else {
        document.getElementById("character-buffs").textContent = "None";
      }

      saveLocalData();
      checkAchievements();
    }

    // -------------------------------------------
    // レベルアップ
    // -------------------------------------------
    function addXp(amount) {
      character.xp += amount;
      while (character.xp >= character.nextLevelXp) {
        character.level++;
        character.xp -= character.nextLevelXp;
        character.nextLevelXp = character.level * 100;
        character.maxHp += 20;
        character.hp = character.maxHp;
        character.attack += 1;
        character.defense += 1;
        character.skillPoints += 1;
        showHomeMessage(`Level up! Now Level ${character.level} (+1 Skill Point).`);

        for (let c of companions) {
          c.level++;
          c.hp = c.maxHp;
          c.attack++;
        }
      }
      updateCharacterInfo();
    }

    // -------------------------------------------
    // バフ/デバフ
    // -------------------------------------------
    function addBuff(buffObj) {
      character.buffs.push(buffObj);
      updateCharacterInfo();
    }

    function processBuffsEachTurn(logElm) {
      for (let i = character.buffs.length - 1; i >= 0; i--) {
        const b = character.buffs[i];
        if (b.effectType === "dot") {
          character.hp -= b.effectValue;
          if (character.hp < 0) character.hp = 0;
          logMessage(logElm, `[${b.name}] You take ${b.effectValue} damage! (HP: ${character.hp})`);
        }
        b.turns--;
        if (b.turns <= 0) {
          logMessage(logElm, `[${b.name}] effect ended.`);
          character.buffs.splice(i, 1);
        }
      }
    }

    // -------------------------------------------
    // ホームメッセージ
    // -------------------------------------------
    function showHomeMessage(msg) {
      const homeMessage = document.getElementById('home-message');
      homeMessage.innerHTML = `<div class="message">${msg}</div>`;
    }

    // -------------------------------------------
    // 昼夜
    // -------------------------------------------
    function updateDayNightDisplay() {
      let dnElm = document.getElementById("day-night-display");
      let hourStr = (currentHour < 10) ? "0"+currentHour : currentHour;
      let isNight = (currentHour >= 18 || currentHour < 6);
      let dayNight = isNight ? "Night" : "Day";
      dnElm.innerHTML = `<p>Time: ${hourStr}:00 (${dayNight})</p>`;
    }
    function advanceTime() {
      currentHour += 6;
      if (currentHour >= 24) currentHour -= 24;
      saveLocalData();
      updateDayNightDisplay();
      showHomeMessage("Time passes by...");
    }

    // -------------------------------------------
    // 天候
    // -------------------------------------------
    function updateWeatherDisplay() {
      const wElm = document.getElementById("weather-display");
      wElm.innerHTML = `<p>Weather: ${currentWeather}</p>`;
    }
    function changeWeatherRandom() {
      currentWeather = possibleWeathers[Math.floor(Math.random() * possibleWeathers.length)];
      updateWeatherDisplay();
      saveLocalData();
    }

    // -------------------------------------------
    // ランダムイベント
    // -------------------------------------------
    function triggerRandomEvent() {
      const randomRoll = Math.random();
      let msg = "";
      if (randomRoll < 0.2) {
        msg = "A traveling merchant appears, offering rare goods (not yet implemented).";
      } else if (randomRoll < 0.4) {
        changeWeatherRandom();
        msg = `The weather suddenly changes to ${currentWeather}!`;
      } else if (randomRoll < 0.6) {
        addBuff({ name: 'Poison', turns: 3, effectType: 'dot', effectValue: 3 });
        msg = "You stepped on a poisonous trap! You are now poisoned.";
      } else {
        msg = "Nothing special happens.";
      }
      showHomeMessage(msg);
    }

    // -------------------------------------------
    // 宿屋
    // -------------------------------------------
    function restAtInn() {
      if (character.gold < 10) {
        showHomeMessage("Not enough gold to rest at the inn!");
        return;
      }
      character.gold -= 10;
      character.hp = character.maxHp;
      for (let c of companions) c.hp = c.maxHp;
      showHomeMessage("You and your companions rest at the inn and recover full HP.");
      updateCharacterInfo();
    }

    // -------------------------------------------
    // ロケーション移動 + ランダムエンカウント
    // -------------------------------------------
    function moveLocation(newLocation) {
      character.location = newLocation;
      document.getElementById("current-location").textContent = newLocation;
      saveLocalData();

      const logElm = document.getElementById('location-log');
      logElm.textContent = `You moved to ${newLocation}.`;

      let encounterChance = 0;
      if (newLocation === "Town") encounterChance = 0;
      else if (newLocation === "Forest") encounterChance = 40;
      else if (newLocation === "Dungeon") encounterChance = 70;
      else if (newLocation === "Mountain") encounterChance = 50;

      const roll = Math.random() * 100;
      if (roll < encounterChance) {
        const enemyKeys = Object.keys(enemies);
        const randEnemyKey = enemyKeys[Math.floor(Math.random() * enemyKeys.length)];
        logElm.textContent += `\nA wild ${enemies[randEnemyKey].name} appears!`;
        startBattle(randEnemyKey);
      }
    }

    // -------------------------------------------
    // 仲間の雇用
    // -------------------------------------------
    function hireCompanion() {
      const input = document.getElementById('friendName');
      let name = input.value.trim();
      if (!name) return;

      let newCompanion = { name, level: 1, hp: 30, maxHp: 30, attack: 2 };
      companions.push(newCompanion);

      input.value = "";
      updateCompanionList();
      saveLocalData();
      showHomeMessage(`${name} joined your party!`);
    }

    function updateCompanionList() {
      const listElm = document.getElementById('companion-list');
      listElm.innerHTML = "";
      companions.forEach(c => {
        const li = document.createElement("li");
        li.textContent = `${c.name} (Lv ${c.level}, HP ${c.hp}/${c.maxHp}, ATK ${c.attack})`;
        listElm.appendChild(li);
      });
    }

    // -------------------------------------------
    // インベントリ表示
    // -------------------------------------------
    let selectedItemIndex = null;
    let selectedForCraft = [];

    function updateInventoryDisplay() {
      const invElm = document.getElementById('inventory');
      invElm.innerHTML = "";

      if (inventory.length === 0) {
        invElm.innerHTML = "<p>Your inventory is empty.</p>";
        return;
      }

      inventory.forEach((item, index) => {
        const div = document.createElement("div");
        div.className = "inventory-item";
        div.textContent = item.name;

        if (item.equipped) div.style.border = "2px solid #4CAF50";

        div.onclick = () => onInventoryItemClick(index);
        invElm.appendChild(div);
      });
    }

    function onInventoryItemClick(index) {
      if (document.getElementById("craft").style.display === "block") {
        toggleCraftSelection(index);
        return;
      }

      selectedItemIndex = index;
      const item = inventory[index];
      const modalTitle = document.getElementById("item-modal-title");
      const modalDesc = document.getElementById("item-modal-description");

      if (item.type === "potion") {
        modalTitle.textContent = `Use ${item.name}?`;
        modalDesc.textContent = `This potion restores ${item.heal} HP.`;
      } else if (item.type === "weapon") {
        modalTitle.textContent = `Equip ${item.name}?`;
        modalDesc.textContent = `Weapon (+${item.attack} Attack).`;
      } else if (item.type === "armor") {
        modalTitle.textContent = `Equip ${item.name}?`;
        modalDesc.textContent = `Armor (+${item.defense} Defense).`;
      } else if (item.type === "accessory") {
        modalTitle.textContent = `Equip ${item.name}?`;
        modalDesc.textContent = `Accessory (+${item.attack} ATK, +${item.defense} DEF).`;
      } else {
        modalTitle.textContent = item.name;
        modalDesc.textContent = "A special item. No direct use/equip.";
      }

      if (canSellItem(item)) {
        modalDesc.textContent += `\n(Sell price: ${sellPrice(item)} Gold)`;
      }

      document.getElementById("item-modal-bg").style.display = "flex";
    }

    function closeItemModal() {
      document.getElementById("item-modal-bg").style.display = "none";
      selectedItemIndex = null;
    }

    function confirmItemUse() {
      if (selectedItemIndex === null) return;
      const item = inventory[selectedItemIndex];

      if (item.type === "potion") {
        character.hp += item.heal;
        if (character.hp > character.maxHp) character.hp = character.maxHp;
        inventory.splice(selectedItemIndex, 1);
        showHomeMessage(`${item.name} used! You recovered ${item.heal} HP.`);
      }
      else if (item.type === "weapon") {
        unequipItem("weapon");
        item.equipped = true;
        character.attack += item.attack;
        showHomeMessage(`${item.name} equipped. (+${item.attack} Attack)`);
      }
      else if (item.type === "armor") {
        unequipItem("armor");
        item.equipped = true;
        character.defense += item.defense;
        showHomeMessage(`${item.name} equipped. (+${item.defense} Defense)`);
      }
      else if (item.type === "accessory") {
        unequipItem("accessory");
        item.equipped = true;
        character.attack += item.attack;
        character.defense += item.defense;
        showHomeMessage(`${item.name} equipped. (+${item.attack} ATK, +${item.defense} DEF)`);
      }
      else {
        if (canSellItem(item)) {
          let price = sellPrice(item);
          character.gold += price;
          inventory.splice(selectedItemIndex, 1);
          showHomeMessage(`You sold ${item.name} for ${price} Gold.`);
        } else {
          showHomeMessage(`You can't use ${item.name} right now.`);
        }
      }

      updateCharacterInfo();
      updateInventoryDisplay();
      closeItemModal();
    }

    function unequipItem(type) {
      for (let i = 0; i < inventory.length; i++) {
        let it = inventory[i];
        if (it.type === type && it.equipped) {
          it.equipped = false;
          if (type === "weapon") character.attack -= it.attack;
          else if (type === "armor") character.defense -= it.defense;
          else if (type === "accessory") { character.attack -= it.attack; character.defense -= it.defense; }
        }
      }
    }

    function canSellItem(item) {
      if (item.equipped) return false;
      if (item.type === "special") return false;
      return !["weapon","armor","accessory","potion"].includes(item.type) ? true : false;
    }
    function sellPrice(item) { return 30; }

    // -------------------------------------------
    // クエストUI
    // -------------------------------------------
    function updateQuestVisibility() {
      const finalQuestCard = document.getElementById("final-quest");
      finalQuestCard.style.display = (!mainQuests.final.locked) ? "block" : "none";
    }

    function startQuest(questKey) {
      let q = mainQuests[questKey] || sideQuests[questKey];
      if (!q) return;
      if (q.isRunning || q.isCompleted) return;
      if (q.locked) {
        showHomeMessage("This quest is locked. Complete the previous quest first!");
        return;
      }

      q.isRunning = true;
      q.progress = 0;
      updateProgressBar(questKey);

      let progressInterval = setInterval(() => {
        q.progress += 5;
        if (q.progress > 100) q.progress = 100;
        updateProgressBar(questKey);

        if (q.progress === 100) {
          clearInterval(progressInterval);
          completeQuest(questKey);
        }
      }, 400);
    }

    function completeQuest(questKey) {
      let q = mainQuests[questKey] || sideQuests[questKey];
      q.isRunning = false;
      q.isCompleted = true;

      character.gold += q.reward.gold;
      addXp(q.reward.xp);
      q.reward.items.forEach(it => character.specialItems.push(it));

      showHomeMessage(`${q.name} completed! You got ${q.reward.gold} Gold, ${q.reward.xp} XP, and ${q.reward.items.join(", ")}.`);

      if (q.unlockNext) {
        if (mainQuests[q.unlockNext]) mainQuests[q.unlockNext].locked = false;
        else if (sideQuests[q.unlockNext]) sideQuests[q.unlockNext].locked = false;
      }

      updateQuestVisibility();
      updateCharacterInfo();
      saveLocalData();
    }

    function updateProgressBar(questKey) {
      const bar = document.getElementById(questKey + '-progress');
      let q = mainQuests[questKey] || sideQuests[questKey];
      if (bar) bar.style.width = q.progress + '%';
    }

    // -------------------------------------------
    // バトル
    // -------------------------------------------
    function startBattle(enemyKey) {
      const enemyDef = enemies[enemyKey];
      if (!enemyDef) return;
      const logElm = document.getElementById('battle-log');
      logElm.innerHTML = `A wild ${enemyDef.name} appears! (HP: ${enemyDef.hp})`;

      if (skills.Heal.level > 0) {
        const healAmount = skills.Heal.level * 10;
        character.hp += healAmount;
        if (character.hp > character.maxHp) character.hp = character.maxHp;
        logMessage(logElm, `[Skill: Heal Lv${skills.Heal.level}] You healed ${healAmount} HP!`);
        updateCharacterInfo();
      }

      let enemyHp = enemyDef.hp;

      let battleInterval = setInterval(() => {
        processBuffsEachTurn(logElm);

        if (character.hp <= 0) {
          clearInterval(battleInterval);
          logMessage(logElm, "You have been defeated...");
          saveLocalData();
          return;
        }

        let baseDamage = getRandomInt(character.attack - 2, character.attack + 2);
        if (baseDamage < 1) baseDamage = 1;

        if (skills.Fireball.level > 0) {
          let extra = skills.Fireball.level * 2;
          baseDamage += extra;
          logMessage(logElm, `[Fireball Lv${skills.Fireball.level}] Extra ${extra} magic damage!`);
        }

        let totalCompanionDamage = 0;
        companions.forEach(c => { if (c.hp > 0) totalCompanionDamage += c.attack; });

        let totalDamage = baseDamage + totalCompanionDamage;
        enemyHp -= totalDamage;

        logMessage(logElm, `You (and companions) deal ${totalDamage} damage! (Enemy HP: ${Math.max(enemyHp, 0)})`);

        if (enemyHp <= 0) {
          clearInterval(battleInterval);
          logMessage(logElm, `You defeated the ${enemyDef.name}!`);
          character.gold += enemyDef.rewardGold;
          addXp(enemyDef.rewardXp);
          updateCompanionXP(enemyDef.rewardXp);
          updateCharacterInfo();
          saveLocalData();

          achievements.firstKill.isUnlocked = true;
          updateAchievementList();
          return;
        }

        let eAtk = getRandomInt(enemyDef.attackMin, enemyDef.attackMax);
        let dmgToPlayer = eAtk - character.defense;
        if (dmgToPlayer < 1) dmgToPlayer = 1;

        character.hp -= dmgToPlayer;
        if (character.hp < 0) character.hp = 0;

        logMessage(logElm, `The ${enemyDef.name} hits you for ${dmgToPlayer}. (Your HP: ${character.hp})`);
        updateCharacterInfo();

        if (character.hp <= 0) {
          clearInterval(battleInterval);
          logMessage(logElm, "You have been defeated...");
          saveLocalData();
          return;
        }
      }, 800);
    }

    function logMessage(logElm, msg) {
      const p = document.createElement("p");
      p.textContent = msg;
      logElm.appendChild(p);
      logElm.scrollTop = logElm.scrollHeight;
    }

    function updateCompanionXP(amount) {
      for (let c of companions) {
        c.level += Math.floor(amount/50);
        c.maxHp += 5;
        c.hp = c.maxHp;
        c.attack += 1;
      }
      updateCompanionList();
    }

    // -------------------------------------------
    // ストア購入
    // -------------------------------------------
    function buyItem(itemKey) {
      const itemDef = storeItems[itemKey];
      if (!itemDef) return;
      if (character.gold < itemDef.cost) {
        showHomeMessage(`You don't have enough gold to buy ${itemDef.name}.`);
        return;
      }
      character.gold -= itemDef.cost;

      let newItem = JSON.parse(JSON.stringify(itemDef));
      if (["weapon","armor","accessory"].includes(newItem.type)) newItem.equipped = false;

      inventory.push(newItem);

      showHomeMessage(`You bought ${newItem.name}!`);
      updateCharacterInfo();
      updateInventoryDisplay();
    }

    // -------------------------------------------
    // アート所持/購入/設定
    // -------------------------------------------
    function isArtOwned(artKey) {
      return character.ownedArtKeys.includes(artKey);
    }

    function grantArt(artKey, setAsPortrait=false) {
      const art = ART_LIST.find(a => a.key === artKey);
      if (!art) return false;

      if (!isArtOwned(art.key)) {
        character.ownedArtKeys.push(art.key);
      }
      if (setAsPortrait) {
        character.portraitUrl = art.url;
      }
      saveLocalData();
      updateCharacterInfo();
      renderArtShop();
      renderArtGallery();
      return true;
    }

    function buyArt(artKey) {
      const art = ART_LIST.find(a => a.key === artKey);
      if (!art) return;

      if (isArtOwned(art.key)) {
        showHomeMessage(`You already own ${art.name}.`);
        return;
      }
      if (character.gold < art.cost) {
        showHomeMessage(`Not enough gold to buy ${art.name}. Need ${art.cost} Gold.`);
        return;
      }

      character.gold -= art.cost;

      // 購入したら即キャラ絵変更
      grantArt(art.key, true);

      showHomeMessage(`Purchased ${art.name}! Character portrait changed.`);
    }

    function setPortraitFromArt(artKey) {
      const art = ART_LIST.find(a => a.key === artKey);
      if (!art) return;

      if (!isArtOwned(art.key)) {
        showHomeMessage("You don't own this art yet. Buy it in Store or pull it from Gacha.");
        return;
      }

      character.portraitUrl = art.url;
      saveLocalData();
      updateCharacterInfo();
      showHomeMessage(`Character portrait set to ${art.name}.`);
    }

    // Storeのアート一覧描画
    function renderArtShop() {
      const wrap = document.getElementById("art-shop-list");
      if (!wrap) return;

      wrap.innerHTML = "";
      ART_LIST.forEach(a => {
        const owned = isArtOwned(a.key);

        const rarityBadge = a.rarity === "UR"
          ? `<span class="badge rarity-ur">UR</span>`
          : `<span class="badge rarity-ssr">SSR</span>`;

        const row = document.createElement("div");
        row.style.marginBottom = "10px";
        row.innerHTML = `
          <div style="display:flex; align-items:center; justify-content:space-between; gap:10px; flex-wrap:wrap;">
            <div>
              <strong>${a.name}</strong>
              ${rarityBadge}
              <span class="muted">(${a.cost} Gold)</span>
              ${owned ? `<span class="badge owned">Owned</span>` : `<span class="badge">Not Owned</span>`}
            </div>
            <div>
              <button class="button" onclick="buyArt('${a.key}')" ${owned ? 'disabled class="button disabled"' : ''} ${owned ? 'disabled' : ''}>Buy</button>
              <button class="button" onclick="openArtModal('${a.key}')">Preview</button>
            </div>
          </div>
        `;
        wrap.appendChild(row);
      });
    }

    // ギャラリー描画
    function renderArtGallery() {
      const grid = document.getElementById("art-gallery-grid");
      const sum = document.getElementById("art-collection-summary");
      if (!grid || !sum) return;

      const ownedCount = character.ownedArtKeys.length;
      sum.textContent = `Owned: ${ownedCount} / ${ART_LIST.length}`;

      grid.innerHTML = "";
      ART_LIST.forEach(a => {
        const owned = isArtOwned(a.key);

        const item = document.createElement("div");
        item.className = "gallery-item";

        const img = document.createElement("img");
        img.src = a.url;
        img.alt = a.name;
        img.loading = "lazy";
        img.style.cursor = "pointer";
        img.onclick = () => openArtModal(a.key);

        const meta = document.createElement("div");
        meta.className = "gallery-meta";
        meta.innerHTML = `
          <div>
            <strong>${a.name}</strong><br/>
            <span class="muted">${a.cost} Gold</span>
          </div>
          <div style="display:flex; gap:6px; align-items:center;">
            ${a.rarity === "UR"
              ? `<span class="badge rarity-ur">UR</span>`
              : `<span class="badge rarity-ssr">SSR</span>`
            }
            ${owned ? `<span class="badge owned">Owned</span>` : `<span class="badge">Not Owned</span>`}
          </div>
        `;

        const actions = document.createElement("div");
        actions.className = "gallery-actions";

        const btnPreview = document.createElement("button");
        btnPreview.className = "button";
        btnPreview.textContent = "Preview";
        btnPreview.onclick = () => openArtModal(a.key);

        const btnSet = document.createElement("button");
        btnSet.className = "button";
        btnSet.textContent = "Set as Character Art";
        btnSet.disabled = !owned;
        if (!owned) btnSet.classList.add("disabled");
        btnSet.onclick = () => setPortraitFromArt(a.key);

        actions.appendChild(btnPreview);
        actions.appendChild(btnSet);

        item.appendChild(img);
        item.appendChild(meta);
        item.appendChild(actions);
        grid.appendChild(item);
      });
    }

    // -------------------------------------------
    // ★ガチャ(SSR1〜SSR3/UR1〜UR10から抽選)
    // - 引いたアートはOwnedに追加
    // - 1枚目だけは演出的にキャラ絵も即変更(setAsPortrait=true)
    // -------------------------------------------
    function pullArtGacha(times) {
      const totalCost = GACHA_COST * times;
      const status = document.getElementById("gacha-status");
      const resultWrap = document.getElementById("gacha-result");
      resultWrap.innerHTML = "";

      if (character.gold < totalCost) {
        status.textContent = `Not enough gold. Need ${totalCost} Gold.`;
        showHomeMessage(`Not enough gold for gacha. Need ${totalCost} Gold.`);
        return;
      }

      character.gold -= totalCost;

      let pulled = [];
      for (let i=0; i<times; i++) {
        const art = rollOneArt();
        pulled.push(art);
        // 1枚目だけ即ポートレート変更(継続仕様)
        grantArt(art.key, i === 0);
      }

      updateCharacterInfo();

      status.textContent = `Pulled ${times} time(s). Cost ${totalCost} Gold.`;

      // 表示
      pulled.forEach((a, idx) => {
        const card = document.createElement("div");
        card.className = "gacha-card";
        card.innerHTML = `
          <img src="${a.url}" alt="${a.name}">
          <div class="p">
            <strong>${a.name}</strong><br/>
            <span class="muted">${a.rarity}</span>
            ${isArtOwned(a.key) ? `<span class="badge owned" style="margin-left:6px;">Owned</span>` : ``}
          </div>
        `;
        card.onclick = () => openArtModal(a.key);
        resultWrap.appendChild(card);
      });

      showHomeMessage(`Gacha result: ${pulled.map(a => a.rarity + " " + a.key).join(", ")}`);

      saveLocalData();
      renderArtShop();
      renderArtGallery();
    }

    function rollOneArt() {
      const r = Math.random() * 100;
      let rarity = (r < GACHA_RATE_UR) ? "UR" : "SSR";

      const pool = ART_LIST.filter(a => a.rarity === rarity);
      // 念のため
      if (pool.length === 0) return ART_LIST[Math.floor(Math.random() * ART_LIST.length)];

      return pool[Math.floor(Math.random() * pool.length)];
    }

    // -------------------------------------------
    // アートプレビュー・モーダル
    // -------------------------------------------
    let pendingArtKey = null;

    function openArtModal(artKey) {
      const art = ART_LIST.find(a => a.key === artKey);
      if (!art) return;

      pendingArtKey = artKey;

      document.getElementById("art-modal-title").textContent = `${art.name} (${art.rarity})`;
      document.getElementById("art-modal-img").src = art.url;

      const owned = isArtOwned(art.key);
      document.getElementById("art-modal-desc").textContent =
        owned ? "Owned: You can set this as your character art." : "Not owned: Buy it in Store or pull it from Gacha.";

      const setBtn = document.getElementById("art-modal-set-btn");
      setBtn.disabled = !owned;
      if (!owned) setBtn.classList.add("disabled");
      else setBtn.classList.remove("disabled");

      document.getElementById("art-modal-bg").style.display = "flex";
    }

    function closeArtModal() {
      document.getElementById("art-modal-bg").style.display = "none";
      pendingArtKey = null;
    }

    function confirmSetPortrait() {
      if (!pendingArtKey) return;
      setPortraitFromArt(pendingArtKey);
      closeArtModal();
    }

    // -------------------------------------------
    // クラフト関連
    // -------------------------------------------
    function toggleCraftSelection(invIndex) {
      const item = inventory[invIndex];
      if (selectedForCraft.includes(invIndex)) {
        selectedForCraft = selectedForCraft.filter(i => i !== invIndex);
      } else {
        if (selectedForCraft.length >= 2) {
          showHomeMessage("You can only select up to 2 items for crafting.");
          return;
        }
        selectedForCraft.push(invIndex);
      }
      updateCraftSelectionDisplay();
    }

    function updateCraftSelectionDisplay() {
      let names = selectedForCraft.map(i => inventory[i].name);
      if (names.length === 0) names.push("None");
      document.getElementById("craft-selection").textContent = names.join(" & ");
      document.getElementById("craft-button").disabled = (selectedForCraft.length < 2);
    }

    function attemptCraft() {
      if (selectedForCraft.length < 2) return;

      let itemA = inventory[selectedForCraft[0]];
      let itemB = inventory[selectedForCraft[1]];
      let combo = [itemA.name, itemB.name].sort();

      let craftedItem = null;
      for (let r of craftRecipes) {
        if (r.components[0] === combo[0] && r.components[1] === combo[1]) {
          craftedItem = r.result;
          break;
        }
      }

      if (!craftedItem) {
        showHomeMessage("No valid recipe found for these items.");
        selectedForCraft = [];
        updateCraftSelectionDisplay();
        return;
      }

      let idxA = Math.max(selectedForCraft[0], selectedForCraft[1]);
      let idxB = Math.min(selectedForCraft[0], selectedForCraft[1]);
      inventory.splice(idxA, 1);
      inventory.splice(idxB, 1);
      inventory.push(craftedItem);

      showHomeMessage(`You crafted: ${craftedItem.name}!`);
      selectedForCraft = [];
      updateCraftSelectionDisplay();
      updateInventoryDisplay();
      saveLocalData();
    }

    // -------------------------------------------
    // スキル
    // -------------------------------------------
    function refreshSkillList() {
      document.getElementById("skill-point-display").textContent = character.skillPoints;
      const listElm = document.getElementById("skill-list");
      listElm.innerHTML = "";

      for (let sKey in skills) {
        let sk = skills[sKey];
        let li = document.createElement("li");
        li.innerHTML = `
          <strong>${sk.name} (Lv${sk.level}/${sk.maxLevel})</strong>
          - ${sk.description}
          ${
            sk.level < sk.maxLevel
              ? `(<button onclick="learnSkill('${sKey}')">Upgrade (cost ${sk.cost})</button>)`
              : ''
          }
        `;
        listElm.appendChild(li);
      }
    }

    function learnSkill(skillKey) {
      let skill = skills[skillKey];
      if (!skill) return;

      if (skill.level >= skill.maxLevel) {
        showHomeMessage(`${skill.name} is already at max level.`);
        return;
      }
      if (character.skillPoints < skill.cost) {
        showHomeMessage(`Not enough skill points to upgrade ${skill.name}.`);
        return;
      }

      character.skillPoints -= skill.cost;
      skill.level++;
      showHomeMessage(`You upgraded ${skill.name} to level ${skill.level}.`);
      updateCharacterInfo();
      refreshSkillList();
    }

    // -------------------------------------------
    // クエストログ
    // -------------------------------------------
    function updateQuestLog() {
      const logElm = document.getElementById("quest-log-list");
      logElm.innerHTML = "";

      let allQ = getAllQuests();
      for (let key in allQ) {
        let q = allQ[key];
        let status = q.isCompleted ? "Completed" : (q.isRunning ? "In Progress" : "Not Started");
        let li = document.createElement("li");
        li.textContent = `${q.name}: ${status}`;
        logElm.appendChild(li);
      }
    }

    // -------------------------------------------
    // 実績
    // -------------------------------------------
    function checkAchievements() {
      if (character.level >= 5) achievements.level5.isUnlocked = true;

      let completedCount = 0;
      let allQ = getAllQuests();
      for (let key in allQ) if (allQ[key].isCompleted) completedCount++;

      if (completedCount >= 3) achievements.quest3.isUnlocked = true;

      saveLocalData();
      updateAchievementList();
    }

    function updateAchievementList() {
      const listElm = document.getElementById("achievement-list");
      listElm.innerHTML = "";

      for (let aKey in achievements) {
        let a = achievements[aKey];
        let status = a.isUnlocked ? "Unlocked" : "Locked";
        let li = document.createElement("li");
        li.textContent = `${a.name} - ${a.description} [${status}]`;
        listElm.appendChild(li);
      }
    }

    // -------------------------------------------
    // BGM(継続再生 & 状態保存 & 復帰)
    // -------------------------------------------
    let isMusicPlaying = false;
    let bgmUnlocked = false;
    let wantAutoResume = false;

    function setBgmStatus(text) {
      const s = document.getElementById("bgm-status");
      if (s) s.textContent = "Status: " + text;
    }

    function saveBgmState() {
      const audio = document.getElementById("bgm");
      if (!audio) return;

      const state = {
        unlocked: bgmUnlocked,
        playing: isMusicPlaying,
        volume: audio.volume,
        time: audio.currentTime
      };
      localStorage.setItem(LS_KEY_BGM, JSON.stringify(state));
    }

    function loadBgmState() {
      const audio = document.getElementById("bgm");
      if (!audio) return;

      const raw = localStorage.getItem(LS_KEY_BGM);
      if (!raw) return;

      try {
        const st = JSON.parse(raw);
        bgmUnlocked = !!st.unlocked;
        isMusicPlaying = !!st.playing;
        wantAutoResume = isMusicPlaying;

        if (typeof st.volume === "number") audio.volume = st.volume;

        if (typeof st.time === "number") {
          audio.addEventListener("loadedmetadata", () => {
            try { audio.currentTime = Math.max(0, st.time); } catch(e) {}
          }, { once: true });
        }
      } catch(e) {}
    }

    function wireBgmAutoSave() {
      const audio = document.getElementById("bgm");
      if (!audio) return;

      audio.addEventListener("play", () => { isMusicPlaying = true; saveBgmState(); });
      audio.addEventListener("pause", () => { isMusicPlaying = false; saveBgmState(); });
      audio.addEventListener("volumechange", saveBgmState);

      let lastSave = 0;
      audio.addEventListener("timeupdate", () => {
        const now = Date.now();
        if (now - lastSave > 4000) {
          lastSave = now;
          saveBgmState();
        }
      });
    }

    function updateEnableBtn() {
      const btn = document.getElementById("bgm-enable-btn");
      if (!btn) return;

      if (bgmUnlocked) {
        btn.textContent = "BGM Enabled";
        btn.classList.add("disabled");
        btn.disabled = true;
      } else {
        btn.textContent = "Enable BGM (First Click)";
        btn.classList.remove("disabled");
        btn.disabled = false;
      }
    }

    function resumeBgmOnNextUserActionIfNeeded() {
      if (!bgmUnlocked || !wantAutoResume) return;

      const audio = document.getElementById("bgm");
      if (!audio) return;

      const resumeOnce = () => {
        audio.play().then(() => {
          isMusicPlaying = true;
          wantAutoResume = false;
          setBgmStatus("On (Resumed)");
          saveBgmState();
        }).catch(() => {
          setBgmStatus("Blocked (Enable again)");
        });
      };

      document.addEventListener("pointerdown", resumeOnce, { once: true });
      document.addEventListener("keydown", resumeOnce, { once: true });
    }

    function enableBGM() {
      const audio = document.getElementById("bgm");
      if (!audio) return;

      audio.volume = 0.6;

      audio.play().then(() => {
        bgmUnlocked = true;
        isMusicPlaying = true;
        wantAutoResume = false;

        updateEnableBtn();
        setBgmStatus("On");
        showHomeMessage("BGM Enabled & Playing");
        saveBgmState();
      }).catch(() => {
        setBgmStatus("Blocked (Click again)");
        showHomeMessage("BGM blocked by browser. Click Enable BGM again.");
      });
    }

    function toggleMusic() {
      const audio = document.getElementById("bgm");
      if (!audio) return;

      if (!bgmUnlocked) {
        showHomeMessage("First, click 'Enable BGM (First Click)'.");
        setBgmStatus("Locked");
        return;
      }

      if (!isMusicPlaying) {
        audio.play().then(() => {
          isMusicPlaying = true;
          setBgmStatus("On");
          showHomeMessage("Music On");
          saveBgmState();
        }).catch(() => {
          setBgmStatus("Blocked");
          showHomeMessage("Music could not be played (browser block).");
        });
      } else {
        audio.pause();
        isMusicPlaying = false;
        setBgmStatus("Off");
        showHomeMessage("Music Off");
        saveBgmState();
      }
    }

    // -------------------------------------------
    // 汎用ランダム整数
    // -------------------------------------------
    function getRandomInt(min, max) {
      return Math.floor(Math.random() * (max - min + 1) + min);
    }
  </script>
</body>
</html>

ELDER Social VR.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>ELDER Social VR - 超完全版(クライアントのみ)</title>
  <script src="https://aframe.io/releases/1.4.2/aframe.min.js"></script>

  <style>
    :root{
      --bg: rgba(10,12,14,.82);
      --glass: rgba(255,255,255,.06);
      --stroke: rgba(255,255,255,.13);
      --accent: rgba(120,240,255,.95);
      --accent2: rgba(120,160,255,.95);
    }
    html,body{ margin:0; padding:0; height:100%; overflow:hidden; background:#000; font-family: "Yu Gothic", system-ui, -apple-system, Segoe UI, sans-serif; }
    a-scene{ position:fixed; inset:0; z-index:0; }
    canvas{ position:fixed !important; inset:0; z-index:0; }

    /* ===== HUD (DOM UI) ===== */
    #hud{
      position: fixed; inset: 0;
      display:flex; align-items:flex-start; justify-content:center;
      pointer-events:none;
      z-index: 999999;
    }
    #panel{
      margin-top: 18px;
      width: min(560px, calc(100vw - 26px));
      background: linear-gradient(180deg, rgba(255,255,255,.06), rgba(0,0,0,.28));
      border: 1px solid var(--stroke);
      border-radius: 18px;
      box-shadow: 0 18px 60px rgba(0,0,0,.45), 0 0 0 1px rgba(0,0,0,.2) inset;
      backdrop-filter: blur(10px);
      color:#fff;
      padding: 16px;
      pointer-events:auto;
      touch-action: manipulation;
      user-select:none;
      position: relative;
    }
    #panel, #panel *{ pointer-events:auto; }

    /* UI隠すボタン */
    #btnHideUI{
      position:absolute;
      top: 12px;
      right: 12px;
      width: auto;
      padding: 10px 12px;
      border-radius: 12px;
      font-weight: 900;
      font-size: 12px;
      letter-spacing: .2px;
      background: rgba(0,0,0,.35);
      border: 1px solid rgba(255,255,255,.16);
      cursor: pointer;
      display:flex;
      align-items:center;
      gap:8px;
      -webkit-tap-highlight-color: transparent;
    }
    #btnHideUI:hover{ border-color: rgba(120,240,255,.35); }
    #btnHideUI:active{ transform: scale(.99); }

    /* HUDを閉じた後に出す小ボタン */
    #floatingShowUI{
      position: fixed;
      right: 14px;
      bottom: 14px;
      z-index: 9999999;
      display: none;
      pointer-events:auto;
      background: rgba(10,12,14,.72);
      border: 1px solid rgba(255,255,255,.18);
      backdrop-filter: blur(10px);
      color:#fff;
      border-radius: 999px;
      padding: 12px 14px;
      font-weight: 900;
      cursor:pointer;
      box-shadow: 0 10px 40px rgba(0,0,0,.4);
      -webkit-tap-highlight-color: transparent;
    }
    #floatingShowUI:hover{ border-color: rgba(120,240,255,.35); }
    #floatingShowUI:active{ transform: scale(.99); }

    .top{
      display:flex; align-items:center; justify-content:space-between;
      gap: 10px;
      margin-bottom: 12px;
      padding-right: 86px; /* 隠すボタン分スペース */
    }
    .brand{
      font-weight: 900;
      letter-spacing: .5px;
      font-size: 22px;
    }
    .pill{
      display:inline-flex; align-items:center; gap:8px;
      background: rgba(255,255,255,.06);
      border: 1px solid rgba(255,255,255,.14);
      padding: 8px 12px;
      border-radius: 999px;
      font-size: 12px;
      opacity:.95;
    }

    .grid{
      display:grid;
      grid-template-columns: 1fr 1fr;
      gap: 10px;
    }
    .btn{
      display:flex; align-items:center; justify-content:center;
      gap: 10px;
      padding: 14px 12px;
      border-radius: 14px;
      background: linear-gradient(180deg, rgba(255,255,255,.08), rgba(0,0,0,.25));
      border: 1px solid rgba(255,255,255,.14);
      color:#fff;
      font-weight: 900;
      letter-spacing: .4px;
      cursor:pointer;
      transform: translateZ(0);
      transition: transform .08s, border-color .18s, background .18s;
      touch-action: manipulation;
      -webkit-tap-highlight-color: transparent;
    }
    .btn:hover{ border-color: rgba(120,240,255,.35); }
    .btn:active{ transform: scale(.99); }
    .btn.primary{ border-color: rgba(120,240,255,.28); box-shadow: 0 0 0 1px rgba(120,240,255,.10) inset; }
    .btn.danger{ border-color: rgba(255,120,160,.25); }

    .section{
      margin-top: 12px;
      background: rgba(0,0,0,.22);
      border: 1px solid rgba(255,255,255,.10);
      border-radius: 16px;
      padding: 12px;
    }
    .sectionTitle{
      font-weight: 900;
      opacity:.92;
      margin-bottom: 8px;
      display:flex; align-items:center; justify-content:space-between;
      gap: 10px;
    }
    .statGrid{
      display:grid;
      grid-template-columns: 1fr 1fr;
      gap: 10px;
    }
    .card{
      background: rgba(255,255,255,.04);
      border: 1px solid rgba(255,255,255,.10);
      border-radius: 14px;
      padding: 10px;
    }
    .label{ opacity:.82; font-size: 12px; }
    .big{ font-size: 20px; font-weight: 900; margin-top: 4px; }
    .bar{
      margin-top: 8px;
      height: 10px;
      background: rgba(255,255,255,.08);
      border-radius: 999px;
      overflow:hidden;
      border: 1px solid rgba(0,0,0,.22);
    }
    .bar > div{ height:100%; width:50%; background: linear-gradient(90deg, var(--accent), var(--accent2)); }

    .log{
      margin-top: 10px;
      max-height: 140px;
      overflow:auto;
      padding: 10px;
      border-radius: 14px;
      border: 1px solid rgba(255,255,255,.12);
      background: rgba(0,0,0,.28);
      font-size: 12px;
      line-height: 1.35;
    }

    /* モーダル */
    #modalBack{
      position: fixed; inset:0;
      display:none;
      align-items:center; justify-content:center;
      background: rgba(0,0,0,.55);
      z-index: 9999999;
      pointer-events:auto;
    }
    #modal{
      width: min(620px, calc(100vw - 26px));
      background: rgba(10,12,14,.92);
      border: 1px solid rgba(255,255,255,.14);
      border-radius: 18px;
      box-shadow: 0 18px 70px rgba(0,0,0,.55);
      color:#fff;
      padding: 14px;
    }
    #modal h3{
      margin: 0 0 10px 0;
      font-size: 18px;
      letter-spacing: .3px;
    }
    .modalBody{ opacity:.92; font-size: 14px; line-height: 1.5; white-space: pre-wrap; }
    .modalBtns{ display:flex; gap: 10px; margin-top: 12px; flex-wrap:wrap; }
    .modalBtns .btn{ flex: 1 1 140px; }

    /* VR中はDOM HUDを隠す */
    body.vr #hud{ display:none !important; }
    body.vr #floatingShowUI{ display:none !important; }
  </style>
</head>

<body>
  <!-- HUD -->
  <div id="hud">
    <div id="panel">
      <div id="btnHideUI" title="UIを隠す(ESCで戻せる)">✕ UIを隠す</div>

      <div class="top">
        <div class="brand">ELDER Social VR</div>
        <div class="pill">現在地: <b id="fieldTag">街</b></div>
      </div>

      <div class="grid" style="margin-bottom:10px;">
        <div class="btn primary" id="btnTown">🏘️ 街</div>
        <div class="btn primary" id="btnCastle">🏰 城</div>
        <div class="btn primary" id="btnCave">🕳️ 洞窟</div>
        <div class="btn primary" id="btnRuins">🏛️ 遺跡</div>
      </div>

      <div class="grid" style="margin-bottom:10px;">
        <div class="btn" id="btnEnterVR">🕶️ Enter VR</div>
        <div class="btn danger" id="btnExit">⏏ Exit</div>
        <div class="btn" id="btnWave">👋 Wave</div>
        <div class="btn" id="btnCheer">🎉 Cheer</div>
      </div>

      <div class="section">
        <div class="sectionTitle">
          <span>レベル / EXP</span>
          <span class="pill">名前: <b id="playerNameLabel">YOU</b></span>
        </div>
        <div class="statGrid">
          <div class="card">
            <div class="label">Lv / EXP</div>
            <div class="big">Lv.<span id="level">1</span> <span style="opacity:.8;font-size:12px;">EXP</span> <span id="expText">0</span>/<span id="expNeedText">100</span></div>
            <div class="bar"><div id="expBar" style="width:0%"></div></div>
          </div>
          <div class="card">
            <div class="label">ゴールド</div>
            <div class="big"><span id="goldText">0</span> G</div>
            <div class="label" style="margin-top:6px;">街の商人で買い物できる</div>
          </div>
        </div>

        <div class="statGrid" style="margin-top:10px;">
          <div class="card">
            <div class="label">HP</div>
            <div class="big" id="hpText">100</div>
            <div class="bar"><div id="hpBar" style="width:100%"></div></div>
          </div>
          <div class="card">
            <div class="label">魔力</div>
            <div class="big" id="manaText">100</div>
            <div class="bar"><div id="manaBar" style="width:100%"></div></div>
          </div>
        </div>
      </div>

      <div class="grid" style="margin-top:10px;">
        <div class="btn" id="btnTalk">💬 話す</div>
        <div class="btn" id="btnQuest">📜 クエスト</div>
        <div class="btn" id="btnShop">🛒 ショップ</div>
        <div class="btn" id="btnRest">🛏 休憩</div>
        <div class="btn" id="btnSave">💾 セーブ</div>
        <div class="btn" id="btnLoad">📂 ロード</div>
      </div>

      <div class="log" id="log"></div>
      <div style="opacity:.7;font-size:11px;margin-top:8px;line-height:1.35;">
        操作: WASD移動 / Shiftダッシュ / Spaceジャンプ / マウス視点(クリックでポインタロック)<br/>
        VR: Enter VR → コントローラのレーザーで3D UIを押せる(DOM UIは非表示)<br/>
        UI: 右上「UIを隠す」or ESCで切替
      </div>
    </div>
  </div>

  <!-- HUDを隠した後に出すボタン -->
  <div id="floatingShowUI">≡ UIを表示</div>

  <!-- Modal -->
  <div id="modalBack">
    <div id="modal">
      <h3 id="modalTitle">TITLE</h3>
      <div class="modalBody" id="modalBody"></div>
      <div class="modalBtns" id="modalBtns"></div>
    </div>
  </div>

  <!-- A-Frame -->
  <a-scene
    id="scene"
    renderer="colorManagement:true; physicallyCorrectLights:true"
    shadow="type:pcfsoft"
    webxr="optionalFeatures: local-floor, bounded-floor, hand-tracking"
  >
    <a-assets>
      <!-- BGM(エリアで切替) -->
      <audio id="bgmTown"   src="https://www.free-stock-music.com/music/scott-buckley/mp3/scott-buckley-beautiful-oblivion.mp3" crossorigin="anonymous"></audio>
      <audio id="bgmCastle" src="https://www.free-stock-music.com/music/scott-buckley/mp3/scott-buckley-the-endurance.mp3" crossorigin="anonymous"></audio>
      <audio id="bgmCave"   src="https://www.free-stock-music.com/music/scott-buckley/mp3/scott-buckley-in-search-of-solitude.mp3" crossorigin="anonymous"></audio>
      <audio id="bgmRuins"  src="https://www.free-stock-music.com/music/wombat-noises-audio/mp3/wombat-noises-audio-the-ruins-of-atlantis.mp3" crossorigin="anonymous"></audio>
    </a-assets>

    <!-- 空 -->
    <a-sky id="sky" color="#061018"></a-sky>

    <!-- 光 -->
    <a-light type="ambient" intensity="0.9" color="#dff8ff"></a-light>
    <a-light id="sun" type="directional" intensity="1.35" position="30 40 10"
             castShadow="true" shadow-mapWidth="2048" shadow-mapHeight="2048"></a-light>

    <!-- 海 -->
    <a-entity id="ocean" position="0 0 0">
      <a-cylinder position="0 -1.4 0" radius="140" height="2.2" open-ended="true"
                  material="color:#0a2a3a; metalness:0.05; roughness:0.35; opacity:0.95; transparent:true"></a-cylinder>
      <a-ring position="0 -0.2 0" radius-inner="65" radius-outer="140"
              rotation="-90 0 0"
              material="color:#0b3143; opacity:0.88; transparent:true"></a-ring>
    </a-entity>

    <!-- 島(3段) -->
    <a-entity id="island">
      <a-cylinder position="0 -0.1 0" radius="62" height="1"
                  material="color:#cbb48b; roughness:0.95; metalness:0.0"></a-cylinder>
      <a-cylinder position="0 0.05 0" radius="50" height="1"
                  material="color:#2f6a3f; roughness:0.95; metalness:0.0"></a-cylinder>
      <a-cylinder position="0 0.35 0" radius="34" height="1.2"
                  material="color:#2a5f3a; roughness:0.95"></a-cylinder>
      <a-ring position="0 0.42 0" radius-inner="14" radius-outer="17" rotation="-90 0 0"
              material="color:#3b2f23; roughness:1"></a-ring>
      <a-ring position="0 0.42 0" radius-inner="26" radius-outer="29" rotation="-90 0 0"
              material="color:#3b2f23; roughness:1; opacity:0.85; transparent:true"></a-ring>
    </a-entity>

    <!-- BGM -->
    <a-entity id="bgm" sound="src:#bgmTown; autoplay:false; loop:true; volume:0.65; positional:false"></a-entity>

    <!-- プレイヤー(※yは基準0.55で置く。起動時にJSで“地面にスナップ”して埋まりゼロ) -->
    <a-entity id="playerRig" position="0 0.55 18">
      <!-- かっこいい勇者(簡易ハイディテール) -->
      <a-entity id="hero" position="0 0 0"
                animation__idle="property: rotation; dir: alternate; dur: 1800; loop: true; to: 0 1.2 0"
                animation__breath="property: scale; dir: alternate; dur: 1200; loop: true; to: 1.01 1.02 1.01">

        <!-- 影 -->
        <a-circle radius="0.75" rotation="-90 0 0" position="0 0.02 0"
                  material="color:#000; opacity:0.25; transparent:true"></a-circle>

        <!-- マント -->
        <a-entity id="cape" position="0 1.22 0.16" rotation="10 0 0"
                  animation__cape="property: rotation; dir: alternate; dur: 900; loop: true; to: 12 1 0">
          <a-plane width="0.95" height="1.35" position="0 -0.58 -0.20"
                   material="color:#0b1020; opacity:0.96; transparent:true; side:double"></a-plane>
          <a-plane width="0.55" height="1.15" position="-0.28 -0.62 -0.21" rotation="0 8 0"
                   material="color:#0a0f1c; opacity:0.90; transparent:true; side:double"></a-plane>
          <a-plane width="0.55" height="1.15" position="0.28 -0.62 -0.21" rotation="0 -8 0"
                   material="color:#0a0f1c; opacity:0.90; transparent:true; side:double"></a-plane>
        </a-entity>

        <!-- ブーツ -->
        <a-box position="-0.17 0.16 0" width="0.22" height="0.34" depth="0.30"
               material="color:#1a1418; roughness:1"></a-box>
        <a-box position="0.17 0.16 0" width="0.22" height="0.34" depth="0.30"
               material="color:#1a1418; roughness:1"></a-box>
        <a-box position="-0.17 0.06 -0.14" width="0.24" height="0.12" depth="0.22"
               material="color:#0f0c10; roughness:1"></a-box>
        <a-box position="0.17 0.06 -0.14" width="0.24" height="0.12" depth="0.22"
               material="color:#0f0c10; roughness:1"></a-box>

        <!-- 脚(鎧) -->
        <a-box position="-0.17 0.56 0" width="0.26" height="0.54" depth="0.32"
               material="color:#2b3140; metalness:0.25; roughness:0.55"></a-box>
        <a-box position="0.17 0.56 0" width="0.26" height="0.54" depth="0.32"
               material="color:#2b3140; metalness:0.25; roughness:0.55"></a-box>

        <!-- 膝ルーン -->
        <a-box position="-0.17 0.44 -0.18" width="0.26" height="0.14" depth="0.14"
               material="color:#78f0ff; emissive:#2dd; emissiveIntensity:0.20; metalness:0.3; roughness:0.35; opacity:0.55; transparent:true"></a-box>
        <a-box position="0.17 0.44 -0.18" width="0.26" height="0.14" depth="0.14"
               material="color:#78f0ff; emissive:#2dd; emissiveIntensity:0.20; metalness:0.3; roughness:0.35; opacity:0.55; transparent:true"></a-box>

        <!-- 腰 -->
        <a-box position="0 0.92 0.00" width="0.72" height="0.14" depth="0.40"
               material="color:#15151a; roughness:1"></a-box>
        <a-box position="0 0.92 0.23" width="0.16" height="0.12" depth="0.06"
               material="color:#ad7b2e; roughness:0.75; metalness:0.25"></a-box>

        <!-- 腰布 -->
        <a-plane width="0.42" height="0.58" position="0 0.64 0.20"
                 material="color:#152a52; opacity:0.92; transparent:true; side:double"></a-plane>
        <a-plane width="0.35" height="0.36" position="0 0.54 -0.22" rotation="0 180 0"
                 material="color:#0f1733; opacity:0.85; transparent:true; side:double"></a-plane>

        <!-- 胸鎧 -->
        <a-box position="0 1.22 0" width="0.74" height="0.82" depth="0.42"
               material="color:#3b6b8e; metalness:0.38; roughness:0.22"></a-box>

        <!-- コア -->
        <a-box position="0 1.22 0.24" width="0.60" height="0.66" depth="0.06"
               material="color:#78f0ff; emissive:#2dd; emissiveIntensity:0.30; metalness:0.25; roughness:0.18; opacity:0.55; transparent:true"></a-box>
        <a-ring position="0 1.22 0.27" radius-inner="0.12" radius-outer="0.20"
                material="color:#78f0ff; emissive:#2dd; emissiveIntensity:0.35; opacity:0.45; transparent:true"></a-ring>

        <!-- 肩当て -->
        <a-sphere position="-0.50 1.54 0.02" radius="0.22"
                  material="color:#2f4f6a; metalness:0.35; roughness:0.22"></a-sphere>
        <a-sphere position="0.50 1.54 0.02" radius="0.22"
                  material="color:#2f4f6a; metalness:0.35; roughness:0.22"></a-sphere>
        <a-cone position="-0.58 1.58 0.02" radius-bottom="0.10" height="0.22" rotation="0 0 25"
                material="color:#cbd3da; metalness:0.55; roughness:0.25"></a-cone>
        <a-cone position="0.58 1.58 0.02" radius-bottom="0.10" height="0.22" rotation="0 0 -25"
                material="color:#cbd3da; metalness:0.55; roughness:0.25"></a-cone>

        <!-- 腕 -->
        <a-box position="-0.62 1.18 0.02" width="0.20" height="0.68" depth="0.24"
               material="color:#2b3140; metalness:0.25; roughness:0.55"></a-box>
        <a-box position="0.62 1.18 0.02" width="0.20" height="0.68" depth="0.24"
               material="color:#2b3140; metalness:0.25; roughness:0.55"></a-box>
        <a-sphere position="-0.62 0.84 0.02" radius="0.10" material="color:#1a1418; roughness:1"></a-sphere>
        <a-sphere position="0.62 0.84 0.02" radius="0.10" material="color:#1a1418; roughness:1"></a-sphere>

        <!-- 襟 -->
        <a-torus radius="0.24" tube="0.06" position="0 1.58 0.02" rotation="90 0 0"
                 material="color:#0b1020; roughness:1; opacity:0.96; transparent:true"></a-torus>

        <!-- 頭 -->
        <a-entity id="headGroup" position="0 1.78 0">
          <a-sphere id="heroHead" position="0 0 0" radius="0.22" material="color:#f4d7bd; roughness:0.95"></a-sphere>
          <a-sphere position="0 0.05 -0.02" radius="0.24" material="color:#1b1b1f; roughness:0.9; metalness:0.15"></a-sphere>
          <a-box position="0 -0.02 -0.18" width="0.32" height="0.18" depth="0.10"
                 material="color:#0f0f12; roughness:1"></a-box>
          <a-box position="0 0.00 -0.24" width="0.36" height="0.10" depth="0.06"
                 material="color:#78f0ff; emissive:#2dd; emissiveIntensity:0.40; opacity:0.55; transparent:true"></a-box>
          <a-cone position="-0.16 0.20 -0.02" radius-bottom="0.06" height="0.18" rotation="0 0 25"
                  material="color:#cbd3da; metalness:0.55; roughness:0.25"></a-cone>
          <a-cone position="0.16 0.20 -0.02" radius-bottom="0.06" height="0.18" rotation="0 0 -25"
                  material="color:#cbd3da; metalness:0.55; roughness:0.25"></a-cone>
        </a-entity>

        <!-- 剣 -->
        <a-entity id="sword" position="0.40 1.02 0.10" rotation="0 0 18">
          <a-box width="0.05" height="0.86" depth="0.06" position="0 0.42 0"
                 material="color:#cbd3da; metalness:0.78; roughness:0.18"></a-box>
          <a-box width="0.02" height="0.84" depth="0.02" position="0 0.42 -0.03"
                 material="color:#ffffff; opacity:0.18; transparent:true"></a-box>
          <a-box width="0.20" height="0.05" depth="0.12" position="0 0.04 0"
                 material="color:#ad7b2e; metalness:0.35; roughness:0.55"></a-box>
          <a-torus radius="0.09" tube="0.012" position="0 0.04 0.07" rotation="90 0 0"
                   material="color:#78f0ff; emissive:#2dd; emissiveIntensity:0.30; opacity:0.45; transparent:true"></a-torus>
          <a-box width="0.07" height="0.18" depth="0.07" position="0 -0.08 0"
                 material="color:#2b1c12; roughness:1"></a-box>
          <a-sphere radius="0.03" position="0 -0.18 0"
                    material="color:#78f0ff; emissive:#2dd; emissiveIntensity:0.35; opacity:0.6; transparent:true"></a-sphere>
        </a-entity>

        <!-- 盾 -->
        <a-entity id="shield" position="-0.70 1.06 -0.06" rotation="0 0 12">
          <a-cylinder radius="0.24" height="0.09" rotation="90 0 0"
                      material="color:#3a5f7a; metalness:0.28; roughness:0.32"></a-cylinder>
          <a-ring radius-inner="0.13" radius-outer="0.24" rotation="90 0 0"
                  material="color:#78f0ff; emissive:#2dd; emissiveIntensity:0.25; opacity:0.45; transparent:true"></a-ring>
          <a-circle radius="0.07" rotation="90 0 0" position="0 0 0.05"
                    material="color:#ad7b2e; metalness:0.35; roughness:0.55"></a-circle>
        </a-entity>

        <!-- 名前 -->
        <a-text id="name3d" value="YOU" position="0 2.35 0" align="center" width="4" color="#ffffff"
                shader="msdf" font="https://cdn.aframe.io/fonts/Roboto-msdf.json"></a-text>
      </a-entity>

      <a-camera id="cam"
        position="0 1.75 3.4"
        look-controls="pointerLockEnabled: false"
        wasd-controls-enabled="false"
        fov="72"
      ></a-camera>

      <a-entity id="rightHand" laser-controls="hand:right" raycaster="objects: .vrbtn" line="opacity:0.75"></a-entity>
      <a-entity id="leftHand"  laser-controls="hand:left"  raycaster="objects: .vrbtn" line="opacity:0.75"></a-entity>
      <a-entity id="mouseCursor" cursor="rayOrigin: mouse" raycaster="objects: .vrbtn"></a-entity>

      <!-- VR UI -->
      <a-entity id="vrUI" position="0 1.55 -1.25" visible="false">
        <a-plane width="1.55" height="0.92" material="color:#0b0f14; opacity:0.78; transparent:true"></a-plane>
        <a-text value="VRUI" position="0 0.40 0.01" align="center" width="2.6" color="#7ff"
                shader="msdf" font="https://cdn.aframe.io/fonts/Roboto-msdf.json"></a-text>

        <a-entity position="0 0.12 0.02">
          <a-plane class="vrbtn" vr-btn="action:fieldTown"  position="-0.48 0.12 0" width="0.48" height="0.16" material="color:#13202b; opacity:0.95"></a-plane>
          <a-text value="街" position="-0.48 0.12 0.02" align="center" width="1.6" color="#fff" shader="msdf" font="https://cdn.aframe.io/fonts/Roboto-msdf.json"></a-text>

          <a-plane class="vrbtn" vr-btn="action:fieldCastle" position="0.48 0.12 0" width="0.48" height="0.16" material="color:#13202b; opacity:0.95"></a-plane>
          <a-text value="城" position="0.48 0.12 0.02" align="center" width="1.6" color="#fff" shader="msdf" font="https://cdn.aframe.io/fonts/Roboto-msdf.json"></a-text>

          <a-plane class="vrbtn" vr-btn="action:fieldCave" position="-0.48 -0.08 0" width="0.48" height="0.16" material="color:#13202b; opacity:0.95"></a-plane>
          <a-text value="洞窟" position="-0.48 -0.08 0.02" align="center" width="1.6" color="#fff" shader="msdf" font="https://cdn.aframe.io/fonts/Roboto-msdf.json"></a-text>

          <a-plane class="vrbtn" vr-btn="action:fieldRuins" position="0.48 -0.08 0" width="0.48" height="0.16" material="color:#13202b; opacity:0.95"></a-plane>
          <a-text value="遺跡" position="0.48 -0.08 0.02" align="center" width="1.6" color="#fff" shader="msdf" font="https://cdn.aframe.io/fonts/Roboto-msdf.json"></a-text>

          <a-plane class="vrbtn" vr-btn="action:talk" position="-0.48 -0.30 0" width="0.48" height="0.16" material="color:#10261e; opacity:0.95"></a-plane>
          <a-text value="話す" position="-0.48 -0.30 0.02" align="center" width="1.6" color="#fff" shader="msdf" font="https://cdn.aframe.io/fonts/Roboto-msdf.json"></a-text>

          <a-plane class="vrbtn" vr-btn="action:quest" position="0.48 -0.30 0" width="0.48" height="0.16" material="color:#2a2110; opacity:0.95"></a-plane>
          <a-text value="クエスト" position="0.48 -0.30 0.02" align="center" width="1.6" color="#fff" shader="msdf" font="https://cdn.aframe.io/fonts/Roboto-msdf.json"></a-text>
        </a-entity>

        <a-text value="スティック移動 / トリガーで押す" position="0 -0.43 0.01" align="center" width="2.8" color="#bfefff"
                shader="msdf" font="https://cdn.aframe.io/fonts/Roboto-msdf.json"></a-text>
      </a-entity>
    </a-entity>

    <!-- NPC(★snap で“その場所の地面”に自動補正 → 埋まりゼロ) -->
    <a-entity id="npcGroup">
      <a-entity id="npcGuide" class="npc snap" position="-6 1.15 10" rotation="0 25 0">
        <a-cylinder radius="0.35" height="1.2" material="color:#203a4a; roughness:0.9"></a-cylinder>
        <a-sphere radius="0.22" position="0 0.86 0" material="color:#f2d7bf; roughness:0.95"></a-sphere>
        <a-cone radius-bottom="0.28" height="0.35" position="0 1.12 0" material="color:#0a0f18; roughness:1"></a-cone>
        <a-text value="Guide" position="0 1.45 0" align="center" width="4" color="#fff" shader="msdf" font="https://cdn.aframe.io/fonts/Roboto-msdf.json"></a-text>
      </a-entity>

      <a-entity id="npcKnight" class="npc snap" position="10 1.15 0" rotation="0 -120 0" visible="false">
        <a-cylinder radius="0.36" height="1.2" material="color:#3a4652; metalness:0.25; roughness:0.35"></a-cylinder>
        <a-sphere radius="0.22" position="0 0.86 0" material="color:#f2d7bf; roughness:0.95"></a-sphere>
        <a-box width="0.48" height="0.16" depth="0.06" position="0 0.58 0.2" material="color:#78f0ff; opacity:0.5; transparent:true"></a-box>
        <a-text value="Castle Knight" position="0 1.45 0" align="center" width="4" color="#fff" shader="msdf" font="https://cdn.aframe.io/fonts/Roboto-msdf.json"></a-text>
      </a-entity>

      <a-entity id="npcMiner" class="npc snap" position="-10 1.15 -8" rotation="0 60 0" visible="false">
        <a-cylinder radius="0.35" height="1.2" material="color:#3b2f23; roughness:0.95"></a-cylinder>
        <a-sphere radius="0.22" position="0 0.86 0" material="color:#f2d7bf; roughness:0.95"></a-sphere>
        <a-sphere radius="0.18" position="0 1.06 0" material="color:#2d2d2f; roughness:1"></a-sphere>
        <a-text value="Miner" position="0 1.45 0" align="center" width="4" color="#fff" shader="msdf" font="https://cdn.aframe.io/fonts/Roboto-msdf.json"></a-text>
      </a-entity>

      <a-entity id="npcSage" class="npc snap" position="6 1.15 -12" rotation="0 -30 0" visible="false">
        <a-cylinder radius="0.35" height="1.2" material="color:#2a1f2f; roughness:0.95"></a-cylinder>
        <a-sphere radius="0.22" position="0 0.86 0" material="color:#f2d7bf; roughness:0.95"></a-sphere>
        <a-torus radius="0.34" tube="0.05" position="0 0.95 0" rotation="90 0 0" material="color:#7ff; opacity:0.35; transparent:true"></a-torus>
        <a-text value="Ruins Sage" position="0 1.45 0" align="center" width="4" color="#fff" shader="msdf" font="https://cdn.aframe.io/fonts/Roboto-msdf.json"></a-text>
      </a-entity>
    </a-entity>

    <!-- Enemy -->
    <a-entity id="enemy" position="0 1.10 -2" visible="true">
      <a-entity id="enemyModel">
        <a-sphere radius="0.55" material="color:#8a1b2d; metalness:0.15; roughness:0.45; emissive:#200;"></a-sphere>
        <a-sphere radius="0.22" position="0 0.52 0.06" material="color:#2a0b10; roughness:0.9"></a-sphere>
        <a-cone radius-bottom="0.16" height="0.35" position="-0.22 0.78 0.02" rotation="20 0 40" material="color:#ddd; roughness:0.7"></a-cone>
        <a-cone radius-bottom="0.16" height="0.35" position="0.22 0.78 0.02" rotation="20 0 -40" material="color:#ddd; roughness:0.7"></a-cone>
        <a-sphere radius="0.06" position="-0.14 0.55 -0.46" material="color:#fff; emissive:#f0f; emissiveIntensity:0.9"></a-sphere>
        <a-sphere radius="0.06" position="0.14 0.55 -0.46" material="color:#fff; emissive:#f0f; emissiveIntensity:0.9"></a-sphere>
        <a-ring radius-inner="0.62" radius-outer="0.74" rotation="-90 0 0" position="0 -0.35 0"
                material="color:#7ff; opacity:0.14; transparent:true"></a-ring>
      </a-entity>
      <a-text id="enemyName3D" value="Enemy" position="0 1.25 0" align="center" width="4" color="#fff"
              shader="msdf" font="https://cdn.aframe.io/fonts/Roboto-msdf.json"></a-text>
    </a-entity>

    <!-- Fields -->
    <a-entity id="field-town" visible="true">
      <!-- 噴水(snapで地面補正) -->
      <a-entity id="fountain" class="snap" position="0 0 0">
        <a-cylinder radius="2.1" height="0.35" position="0 0.725 0"
                    material="color:#55606a; roughness:0.55; metalness:0.1"></a-cylinder>
        <a-cylinder radius="1.35" height="0.42" position="0 1.11 0"
                    material="color:#3f4a54; roughness:0.55; metalness:0.12"></a-cylinder>
        <a-cylinder radius="0.25" height="1.0" position="0 1.82 0"
                    material="color:#6b7782; roughness:0.5"></a-cylinder>
        <a-sphere radius="0.26" position="0 2.42 0"
                  material="color:#7ff; opacity:0.5; transparent:true; emissive:#2dd; emissiveIntensity:0.25"></a-sphere>
        <a-torus radius="0.95" tube="0.06" position="0 0.90 0" rotation="90 0 0"
                 material="color:#7ff; opacity:0.25; transparent:true"></a-torus>
      </a-entity>

      <!-- 家(各家をsnapで地面補正) -->
      <a-entity id="houses">
        <a-entity class="snap" position="-10 1.70 6" rotation="0 35 0">
          <a-box width="4" height="2.3" depth="3.2" material="color:#bda982; roughness:0.9"></a-box>
          <a-cone radius-bottom="2.8" height="1.4" position="0 1.85 0" material="color:#5a2c1b; roughness:1"></a-cone>
          <a-plane width="1.2" height="0.7" position="0 0.6 1.61" material="color:#1b2a35; opacity:0.55; transparent:true"></a-plane>
        </a-entity>

        <a-entity class="snap" position="10 1.60 8" rotation="0 -20 0">
          <a-box width="3.2" height="2.1" depth="3.0" material="color:#c2b08a; roughness:0.9"></a-box>
          <a-cone radius-bottom="2.2" height="1.3" position="0 1.7 0" material="color:#6a3a22; roughness:1"></a-cone>
          <a-plane width="1.0" height="0.65" position="0.2 0.5 1.51" material="color:#1b2a35; opacity:0.55; transparent:true"></a-plane>
        </a-entity>

        <a-entity class="snap" position="-14 1.55 -6" rotation="0 70 0">
          <a-box width="3.6" height="2.0" depth="2.6" material="color:#b8a27a; roughness:0.9"></a-box>
          <a-cone radius-bottom="2.4" height="1.2" position="0 1.6 0" material="color:#4f2a1a; roughness:1"></a-cone>
        </a-entity>

        <a-entity class="snap" position="13 1.65 -6" rotation="0 -55 0">
          <a-box width="4.2" height="2.2" depth="3.2" material="color:#b5a07a; roughness:0.95"></a-box>
          <a-cone radius-bottom="2.9" height="1.3" position="0 1.75 0" material="color:#2b1c12; roughness:1"></a-cone>
          <a-plane width="2.2" height="0.7" position="0 0.6 1.61" material="color:#0b0f14; opacity:0.65; transparent:true"></a-plane>
          <a-text value="SHOP" position="0 1.1 1.65" align="center" width="4" color="#7ff"
                  shader="msdf" font="https://cdn.aframe.io/fonts/Roboto-msdf.json"></a-text>
        </a-entity>
      </a-entity>

      <!-- 屋台(snapで地面補正) -->
      <a-entity class="snap" position="-6 0.55 -4" rotation="0 15 0">
        <a-box width="2.2" height="0.5" depth="1.2" position="0 0.7 0" material="color:#6a3a22; roughness:1"></a-box>
        <a-plane width="2.4" height="1.2" position="0 1.35 0" rotation="-30 0 0" material="color:#7ff; opacity:0.2; transparent:true"></a-plane>
        <a-cylinder radius="0.05" height="1.4" position="-1.05 0.7 -0.55" material="color:#3b2f23"></a-cylinder>
        <a-cylinder radius="0.05" height="1.4" position="1.05 0.7 -0.55" material="color:#3b2f23"></a-cylinder>
      </a-entity>

      <!-- 木&街灯(snapで地面補正) -->
      <a-entity>
        <a-entity class="snap" position="-18 1.85 2">
          <a-cylinder radius="0.18" height="2.6" material="color:#3b2f23; roughness:1"></a-cylinder>
          <a-sphere radius="1.2" position="0 2.0 0" material="color:#2a7a45; roughness:1"></a-sphere>
        </a-entity>

        <a-entity class="snap" position="18 1.85 4">
          <a-cylinder radius="0.18" height="2.6" material="color:#3b2f23; roughness:1"></a-cylinder>
          <a-sphere radius="1.2" position="0 2.0 0" material="color:#2a7a45; roughness:1"></a-sphere>
        </a-entity>

        <a-entity class="snap" position="6 1.60 14">
          <a-cylinder radius="0.06" height="2.1" material="color:#45515a; roughness:0.6"></a-cylinder>
          <a-sphere radius="0.18" position="0 1.08 0" material="color:#fff; emissive:#7ff; emissiveIntensity:0.65; opacity:0.85; transparent:true"></a-sphere>
        </a-entity>
      </a-entity>
    </a-entity>

    <a-entity id="field-castle" visible="false">
      <a-ring position="0 0.43 0" radius-inner="0" radius-outer="30" rotation="-90 0 0"
              material="color:#636b75; roughness:0.9; opacity:0.9; transparent:true"></a-ring>

      <a-entity position="0 0.75 -18">
        <a-box width="26" height="6" depth="2.8" material="color:#a8b1bb; roughness:0.65; metalness:0.05"></a-box>
        <a-box width="7" height="4" depth="2.2" position="0 -0.4 1.1" material="color:#8a939e; roughness:0.65"></a-box>
        <a-box width="5.2" height="4.2" depth="0.8" position="0 -0.9 1.8" material="color:#2b1c12; roughness:1"></a-box>
        <a-plane width="1.2" height="2.6" position="-4 0.6 1.9" material="color:#7ff; opacity:0.25; transparent:true"></a-plane>
        <a-plane width="1.2" height="2.6" position="4 0.6 1.9" material="color:#7ff; opacity:0.25; transparent:true"></a-plane>
      </a-entity>

      <a-entity>
        <a-entity position="-12 1.2 -18">
          <a-cylinder radius="2.1" height="8.2" material="color:#9aa3ad; roughness:0.55; metalness:0.05"></a-cylinder>
          <a-cone radius-bottom="2.3" height="2.4" position="0 5.2 0" material="color:#6a3a22; roughness:1"></a-cone>
        </a-entity>
        <a-entity position="12 1.2 -18">
          <a-cylinder radius="2.1" height="8.2" material="color:#9aa3ad; roughness:0.55; metalness:0.05"></a-cylinder>
          <a-cone radius-bottom="2.3" height="2.4" position="0 5.2 0" material="color:#6a3a22; roughness:1"></a-cone>
        </a-entity>
      </a-entity>

      <a-ring position="0 0.18 -12" radius-inner="18" radius-outer="28" rotation="-90 0 0"
              material="color:#0b3143; opacity:0.55; transparent:true"></a-ring>

      <a-entity position="0 0.55 -6">
        <a-cylinder radius="1.4" height="0.6" material="color:#4a535c; roughness:0.65"></a-cylinder>
        <a-sphere radius="0.75" position="0 1.0 0" material="color:#a8b1bb; roughness:0.5"></a-sphere>
        <a-torus-knot radius="0.35" tube="0.08" position="0 1.8 0" p="2" q="5"
                      material="color:#7ff; emissive:#2dd; emissiveIntensity:0.25; opacity:0.5; transparent:true"></a-torus-knot>
      </a-entity>
    </a-entity>

    <a-entity id="field-cave" visible="false">
      <a-entity position="0 0.55 -6">
        <a-sphere radius="10" material="color:#0b0f14; opacity:0.22; transparent:true" segments-width="18" segments-height="12"></a-sphere>
      </a-entity>

      <a-entity position="0 0.55 -16">
        <a-torus radius="8" tube="2.2" arc="200" rotation="0 0 90"
                 material="color:#4a3f34; roughness:1; metalness:0"></a-torus>
      </a-entity>

      <a-entity id="rocks">
        <a-sphere radius="5" position="-12 2 -14" material="color:#3a332d; roughness:1"></a-sphere>
        <a-sphere radius="6" position="12 1 -16" material="color:#352f2a; roughness:1"></a-sphere>
        <a-sphere radius="4.5" position="0 3 -22" material="color:#2f2a25; roughness:1"></a-sphere>
      </a-entity>

      <a-entity>
        <a-cone radius-bottom="0.8" height="2.8" position="-4 6 -14" material="color:#2f2a25; roughness:1"></a-cone>
        <a-cone radius-bottom="0.6" height="2.2" position="3 5.7 -16" material="color:#2f2a25; roughness:1"></a-cone>
        <a-cone radius-bottom="0.7" height="2.5" position="8 6.2 -12" material="color:#2f2a25; roughness:1"></a-cone>
      </a-entity>

      <a-entity position="-6 0.55 -10">
        <a-octahedron radius="0.9" material="color:#7ff; opacity:0.55; transparent:true; emissive:#2dd; emissiveIntensity:0.35"></a-octahedron>
        <a-octahedron radius="0.6" position="1.0 0.2 0.3" material="color:#8cf; opacity:0.55; transparent:true; emissive:#2dd; emissiveIntensity:0.25"></a-octahedron>
        <a-light type="point" intensity="0.8" distance="10" color="#7ff"></a-light>
      </a-entity>
    </a-entity>

    <a-entity id="field-ruins" visible="false">
      <a-ring position="0 0.43 0" radius-inner="0" radius-outer="30" rotation="-90 0 0"
              material="color:#6a6555; roughness:0.95; opacity:0.92; transparent:true"></a-ring>

      <a-entity>
        <a-entity position="-12 0.55 -8">
          <a-cylinder radius="0.8" height="3.2" material="color:#c9c2a3; roughness:0.9"></a-cylinder>
          <a-box width="2.2" height="0.35" depth="2.2" position="0 1.85 0" material="color:#bdb493; roughness:0.95"></a-box>
        </a-entity>
        <a-entity position="12 0.55 -8">
          <a-cylinder radius="0.8" height="2.1" material="color:#c9c2a3; roughness:0.9"></a-cylinder>
          <a-box width="2.2" height="0.35" depth="2.2" position="0 1.25 0" material="color:#bdb493; roughness:0.95"></a-box>
        </a-entity>
        <a-entity position="-8 0.55 -18" rotation="0 20 0">
          <a-cylinder radius="0.7" height="2.4" material="color:#bdb493; roughness:0.95"></a-cylinder>
          <a-box width="1.9" height="0.28" depth="1.9" position="0 1.38 0" material="color:#c9c2a3; roughness:0.95"></a-box>
        </a-entity>
        <a-entity position="8 0.55 -18" rotation="0 -20 0">
          <a-cylinder radius="0.7" height="3.0" material="color:#bdb493; roughness:0.95"></a-cylinder>
          <a-box width="1.9" height="0.28" depth="1.9" position="0 1.68 0" material="color:#c9c2a3; roughness:0.95"></a-box>
        </a-entity>
      </a-entity>

      <a-entity position="0 2.0 -16">
        <a-torus radius="4.0" tube="0.55" arc="180" rotation="0 0 90" material="color:#c9c2a3; roughness:0.9"></a-torus>
      </a-entity>

      <a-entity position="0 0.55 -8">
        <a-box width="4.2" height="0.8" depth="2.6" material="color:#5a5648; roughness:0.95"></a-box>
        <a-ring radius-inner="0.9" radius-outer="1.5" rotation="-90 0 0" position="0 0.41 0"
                material="color:#7ff; opacity:0.35; transparent:true; emissive:#2dd; emissiveIntensity:0.25"></a-ring>
        <a-light type="point" intensity="0.9" distance="14" color="#7ff" position="0 1.3 0"></a-light>
      </a-entity>

      <a-entity id="floating" position="0 2.2 -10">
        <a-box width="0.6" height="0.35" depth="0.6" position="-1.2 0.3 0" material="color:#c9c2a3; roughness:0.9"></a-box>
        <a-box width="0.4" height="0.25" depth="0.4" position="1.0 -0.1 0.5" material="color:#bdb493; roughness:0.9"></a-box>
        <a-box width="0.5" height="0.3" depth="0.5" position="0.2 0.5 -0.7" material="color:#c9c2a3; roughness:0.9"></a-box>
      </a-entity>
    </a-entity>

    <a-entity id="confetti" visible="false" position="0 2.4 10">
      <a-ring radius-inner="0.2" radius-outer="0.6" rotation="-90 0 0" material="color:#7ff; opacity:0.35; transparent:true"></a-ring>
      <a-ring radius-inner="0.6" radius-outer="1.0" rotation="-90 0 0" material="color:#fff; opacity:0.18; transparent:true"></a-ring>
    </a-entity>
  </a-scene>

<script>
/* 3D VR UI ボタン */
AFRAME.registerComponent('vr-btn', {
  schema: { action: { type:'string' } },
  init: function(){
    this.el.addEventListener('click', () => {
      const fn = window[this.data.action];
      if(typeof fn === 'function') fn();
    });
  }
});

/* ===== 状態 ===== */
const state = {
  field: "town",
  hp: 100,
  mana: 100,
  level: 1,
  exp: 0,
  expNeed: 100,
  gold: 0,
  quest: null,
  storyStep: 0,
  inVR: false,
  audioUnlocked: false,
  enemy: { name:"影の獣", hp:80, maxHp:80, atk:10, exp:35, gold:15 }
};
const FIELD_JP = { town:"街", castle:"城", cave:"洞窟", ruins:"遺跡" };
const ENEMIES = {
  town:   [{ name:"路地のスライム", hp:60, atk:9,  exp:35,  gold:16 }, { name:"野良ゴブリン", hp:80, atk:12, exp:45, gold:20 }],
  castle: [{ name:"亡霊騎士",     hp:110, atk:16, exp:70,  gold:35 }, { name:"城壁の影",     hp:130, atk:18, exp:85, gold:42 }],
  cave:   [{ name:"洞窟コウモリ", hp:90,  atk:15, exp:65,  gold:30 }, { name:"岩喰い蜥蜴",   hp:140, atk:20, exp:95, gold:55 }],
  ruins:  [{ name:"封印の番人",   hp:170, atk:24, exp:120, gold:70 }, { name:"古代の眼",     hp:150, atk:22, exp:110,gold:62 }]
};

/* ★あなたが“配置に使ってきた基準地面” */
const BASE_GROUND_Y = 0.55;

/* ★島は3段。距離で「今いる地面の高さ」を返す(埋まり防止の本体) */
const GROUND_LAYERS = [
  { r: 34, y: 0.95 }, // 上段(radius=34 height=1.2 posY=0.35 → top=0.95)
  { r: 50, y: 0.55 }, // 中段(radius=50 height=1.0 posY=0.05 → top=0.55)
  { r: 62, y: 0.40 }  // 下段(radius=62 height=1.0 posY=-0.1 → top=0.40)
];

function groundYAt(x, z){
  const r = Math.hypot(x, z);
  for(const layer of GROUND_LAYERS){
    if(r <= layer.r) return layer.y;
  }
  return GROUND_LAYERS[GROUND_LAYERS.length - 1].y;
}
function enemyYAt(x, z){
  return groundYAt(x, z) + 0.55; // 敵球半径0.55
}

/* ★「基準0.55で置いた物」を、実地面に合わせて持ち上げる */
function snapElToGround(el){
  if(!el) return;
  const p = el.getAttribute("position");
  if(!p || typeof p.x!=="number" || typeof p.z!=="number" || typeof p.y!=="number") return;
  const gy = groundYAt(p.x, p.z);
  const dy = gy - BASE_GROUND_Y;
  if(Math.abs(dy) < 0.0001) return;
  el.setAttribute("position", { x:p.x, y:p.y + dy, z:p.z });
}
function snapAll(){
  document.querySelectorAll(".snap").forEach(snapElToGround);
}

/* ===== ユーティリティ ===== */
function escapeHtml(s){ return String(s).replace(/[&<>"']/g, m => ({ "&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#039;" }[m])); }
function log(msg){
  const el = document.getElementById("log");
  const t = new Date().toLocaleTimeString();
  el.innerHTML = `<div>【${t}】${escapeHtml(msg)}</div>` + el.innerHTML;
}
function clamp(v,a,b){ return Math.max(a, Math.min(b, v)); }

/* ===== UI表示/非表示 ===== */
let hudVisible = true;
function setHUDVisible(visible){
  hudVisible = !!visible;
  document.getElementById("hud").style.display = hudVisible ? "flex" : "none";
  document.getElementById("floatingShowUI").style.display = (!hudVisible && !state.inVR) ? "block" : "none";
  localStorage.setItem("elder_ui_hidden", hudVisible ? "0" : "1");
}
function toggleHUD(){ setHUDVisible(!hudVisible); }

/* ===== UI反映 ===== */
function updateUI(){
  document.getElementById("fieldTag").textContent = FIELD_JP[state.field] || state.field;
  document.getElementById("hpText").textContent = Math.floor(state.hp);
  document.getElementById("manaText").textContent = Math.floor(state.mana);
  document.getElementById("level").textContent = state.level;
  document.getElementById("expText").textContent = state.exp;
  document.getElementById("expNeedText").textContent = state.expNeed;
  document.getElementById("goldText").textContent = state.gold;
  document.getElementById("hpBar").style.width = clamp(state.hp,0,100) + "%";
  document.getElementById("manaBar").style.width = clamp(state.mana,0,100) + "%";
  document.getElementById("expBar").style.width = Math.min(100, (state.exp/state.expNeed)*100) + "%";
}

/* ===== DOMボタンを確実に(多重発火を抑える) ===== */
function bindPress(el, fn){
  let last = 0;
  const handler = (e) => {
    const now = performance.now();
    if(now - last < 180) return; // 連打/多重防止
    last = now;
    try{ e.preventDefault(); }catch(_){}
    unlockAudio();
    fn();
  };
  el.addEventListener("pointerup", handler, { passive:false });
  el.addEventListener("touchend", handler, { passive:false });
  el.addEventListener("click", handler, { passive:false });
}

/* ===== オーディオ ===== */
function unlockAudio(){
  if(state.audioUnlocked) return;
  state.audioUnlocked = true;
  const bgm = document.getElementById("bgm");
  try{
    bgm.components.sound.playSound();
    log("🔊 BGM開始(ユーザー操作で解除)");
  }catch(e){}
}
function setBGMByField(){
  const bgm = document.getElementById("bgm");
  const srcMap = { town:"#bgmTown", castle:"#bgmCastle", cave:"#bgmCave", ruins:"#bgmRuins" };
  const src = srcMap[state.field] || "#bgmTown";
  bgm.setAttribute("sound", `src:${src}; autoplay:false; loop:true; volume:0.65; positional:false`);
  try{
    bgm.components.sound.stopSound();
    if(state.audioUnlocked) bgm.components.sound.playSound();
  }catch(e){}
}

/* ===== フィールド切替 ===== */
function setField(field){
  state.field = field;
  ["town","castle","cave","ruins"].forEach(name=>{
    document.getElementById("field-"+name).setAttribute("visible", name===field);
  });
  document.getElementById("npcGuide").setAttribute("visible", field==="town");
  document.getElementById("npcKnight").setAttribute("visible", field==="castle");
  document.getElementById("npcMiner").setAttribute("visible", field==="cave");
  document.getElementById("npcSage").setAttribute("visible", field==="ruins");

  const sky = document.getElementById("sky");
  const sun = document.getElementById("sun");
  if(field==="town"){ sky.setAttribute("color","#061018"); sun.setAttribute("intensity","1.35"); }
  if(field==="castle"){ sky.setAttribute("color","#071321"); sun.setAttribute("intensity","1.45"); }
  if(field==="cave"){ sky.setAttribute("color","#04070b"); sun.setAttribute("intensity","0.85"); }
  if(field==="ruins"){ sky.setAttribute("color","#050b10"); sun.setAttribute("intensity","1.05"); }

  setBGMByField();
  spawnEnemy();
  updateUI();
  log(`📍 ${FIELD_JP[field]} に移動した`);
}

/* ===== 敵 ===== */
function spawnEnemy(){
  const list = ENEMIES[state.field] || ENEMIES.town;
  const e = list[Math.floor(Math.random()*list.length)];
  state.enemy = { name:e.name, hp:e.hp, maxHp:e.hp, atk:e.atk, exp:e.exp, gold:e.gold };
  document.getElementById("enemyName3D").setAttribute("value", e.name);
  const enemy = document.getElementById("enemy");
  enemy.setAttribute("visible","true");
  enemy.setAttribute("position", { x:0, y:enemyYAt(0, -2), z:-2 });
  enemy.setAttribute("animation__pop","property: scale; from: 0.7 0.7 0.7; to: 1 1 1; dur: 220; easing: easeOutBack");
  log(`⚠️ ${e.name} が現れた`);
}
function enemyCounter(){
  if(Math.random() < 0.18){ log(`💨 ${state.enemy.name} の攻撃は外れた`); return; }
  const raw = state.enemy.atk + Math.floor(Math.random()*6) - Math.floor(state.level/4);
  const dmg = Math.max(2, raw);
  state.hp -= dmg;
  log(`🩸 反撃:${state.enemy.name} から ${dmg} ダメージ`);
  if(state.hp <= 0){ state.hp = 1; log("🧊 倒れかけた…(HP1で踏みとどまった)"); }
  updateUI();
}
function gainRewards(exp, gold){
  state.exp += exp;
  state.gold += gold;
  log(`✅ 報酬:EXP +${exp} / ${gold}G`);
  while(state.exp >= state.expNeed){
    state.exp -= state.expNeed;
    state.level++;
    state.expNeed = Math.floor(state.expNeed*1.25 + 25);
    state.hp = clamp(state.hp + 18, 0, 100);
    state.mana = clamp(state.mana + 12, 0, 100);
    log(`🎉 レベルアップ! Lv.${state.level}`);
  }
  updateUI();
}
function enemyDie(){
  const enemy = document.getElementById("enemy");
  enemy.setAttribute("animation__die","property: scale; to: 0.01 0.01 0.01; dur: 250; easing: easeInQuad");
  setTimeout(()=> enemy.setAttribute("visible","false"), 260);
  gainRewards(state.enemy.exp, state.enemy.gold);
  setTimeout(()=> spawnEnemy(), 1200);
}
function damageEnemy(dmg, by="攻撃"){
  state.enemy.hp -= dmg;
  log(`⚔️ ${by}:${state.enemy.name} に ${dmg} ダメージ(残り ${Math.max(0,state.enemy.hp)})`);
  document.getElementById("enemyModel").setAttribute("animation__hit","property: rotation; dir: alternate; dur: 70; loop: 4; to: 0 0 12");
  if(state.enemy.hp <= 0){ enemyDie(); return; }
  enemyCounter();
}

/* ===== 行動 ===== */
function wave(){ document.getElementById("hero").setAttribute("animation__wave","property: rotation; dir: alternate; dur: 180; loop: 6; to: 0 0 8"); log("👋 Wave!"); }
function cheer(){
  const conf = document.getElementById("confetti");
  conf.setAttribute("visible","true");
  conf.setAttribute("animation__up","property: position; from: 0 2.4 10; to: 0 4.2 10; dur: 520; easing: easeOutQuad");
  conf.setAttribute("animation__fade","property: material.opacity; from: 0.35; to: 0; dur: 520; easing: easeOutQuad");
  setTimeout(()=>{ conf.setAttribute("visible","false"); conf.setAttribute("material","opacity:0.35; transparent:true"); }, 560);
  log("🎉 Cheer!");
}
function rest(){
  const bhp=state.hp, bmn=state.mana;
  state.hp = clamp(state.hp + 40, 0, 100);
  state.mana = clamp(state.mana + 40, 0, 100);
  updateUI();
  log(`🛏 休憩:HP ${bhp}→${state.hp} / 魔力 ${bmn}→${state.mana}`);
}
function attack(){ const dmg = (14 + Math.floor(state.level/2)) + Math.floor(Math.random()*8); damageEnemy(dmg, "通常攻撃"); updateUI(); }
function castSpell(){
  if(state.mana < 18){ log("💤 魔力が足りない"); return; }
  state.mana -= 18;
  const dmg = 24 + Math.floor(state.level*1.2) + Math.floor(Math.random()*10);
  damageEnemy(dmg, "魔法");
  updateUI();
}

/* ===== モーダル ===== */
function openModal(title, body, buttons){
  document.getElementById("modalTitle").textContent = title;
  document.getElementById("modalBody").textContent = body;
  const area = document.getElementById("modalBtns");
  area.innerHTML = "";
  buttons.forEach(b=>{
    const div = document.createElement("div");
    div.className = "btn " + (b.type || "");
    div.textContent = b.label;
    bindPress(div, ()=>{ try{ b.onClick(); }catch(e){} });
    area.appendChild(div);
  });
  document.getElementById("modalBack").style.display = "flex";
}
function closeModal(){ document.getElementById("modalBack").style.display = "none"; unlockAudio(); }
document.getElementById("modalBack").addEventListener("click", (e)=>{ if(e.target && e.target.id === "modalBack") closeModal(); });

/* ===== 会話/クエスト/ショップ ===== */
function talk(){
  const npcName = (state.field==="town") ? "Guide"
               : (state.field==="castle") ? "Castle Knight"
               : (state.field==="cave") ? "Miner"
               : "Ruins Sage";
  openModal(`💬 ${npcName}`, `${npcName}:\nここは ${FIELD_JP[state.field]}。\n準備ができたら戦うか、別の場所へ行け。`, [
    { label:"通常攻撃", type:"primary", onClick:()=>{ closeModal(); attack(); } },
    { label:"魔法", type:"primary", onClick:()=>{ closeModal(); castSpell(); } },
    { label:"閉じる", onClick:()=> closeModal() }
  ]);
}
function quest(){
  openModal("📜 クエスト", "今は簡易クエスト(討伐でEXPとGを稼げ)。\n次段階で固定シナリオを増やせる。", [
    { label:"閉じる", onClick:()=> closeModal() }
  ]);
}
function shop(){
  openModal("🛒 ショップ", "(クライアントのみ簡易)\n街でゴールドを稼いで強化できる拡張に対応。", [
    { label:"閉じる", onClick:()=> closeModal() }
  ]);
}

/* ===== セーブ/ロード ===== */
function saveGame(){
  const data = { ...state, audioUnlocked: state.audioUnlocked };
  localStorage.setItem("elder_social_vr_save", JSON.stringify(data));
  log("💾 セーブ完了");
}
function loadGame(){
  const raw = localStorage.getItem("elder_social_vr_save");
  if(!raw){ log("📂 セーブデータがない"); return; }
  try{
    const data = JSON.parse(raw);
    Object.assign(state, data || {});
    setField(state.field || "town");
    updateUI();
    log("📂 ロード完了");
  }catch(e){
    log("❌ ロード失敗:データ破損");
  }
}

/* ===== VR Enter/Exit ===== */
function enterVR(){
  const scene = document.getElementById("scene");
  state.inVR = true;
  document.body.classList.add("vr");
  document.getElementById("vrUI").setAttribute("visible","true");
  document.getElementById("cam").setAttribute("position","0 1.72 0.05");
  document.getElementById("heroHead").setAttribute("material","opacity:0.0; transparent:true; color:#f4d7bd");
  try{ scene.enterVR(); log("🕶️ VRに入った"); }catch(e){ log("⚠️ WebXRに入れない(疑似VRで続行)"); }
}
function exitApp(){
  const scene = document.getElementById("scene");
  state.inVR = false;
  document.body.classList.remove("vr");
  document.getElementById("vrUI").setAttribute("visible","false");
  document.getElementById("cam").setAttribute("position","0 1.75 3.4");
  document.getElementById("heroHead").setAttribute("material","opacity:1.0; transparent:false; color:#f4d7bd");
  try{ scene.exitVR(); }catch(e){}
  setHUDVisible(hudVisible);
  log("⏏ Exit");
}
function fieldTown(){ setField("town"); }
function fieldCastle(){ setField("castle"); }
function fieldCave(){ setField("cave"); }
function fieldRuins(){ setField("ruins"); }

/* ===== 操作 ===== */
const rig = document.getElementById("playerRig");
const hero = document.getElementById("hero");
const cam  = document.getElementById("cam");
const keys = { w:false,a:false,s:false,d:false, shift:false, space:false };
let vy = 0, grounded = true;

function getYaw(){
  const rot = cam.getAttribute("rotation");
  return (rot && typeof rot.y === "number") ? rot.y : 0;
}
function tickMovement(dt){
  const speedBase = keys.shift ? 7.2 : 4.4;
  const step = (speedBase * dt) / 1000;

  let moveX = 0, moveZ = 0;
  if(keys.w) moveZ -= 1;
  if(keys.s) moveZ += 1;
  if(keys.a) moveX -= 1;
  if(keys.d) moveX += 1;

  const len = Math.hypot(moveX, moveZ);
  if(len > 0){ moveX/=len; moveZ/=len; }

  const yaw = (getYaw() * Math.PI) / 180;
  const cos = Math.cos(yaw), sin = Math.sin(yaw);
  const dx = (moveX * cos - moveZ * sin) * step;
  const dz = (moveX * sin + moveZ * cos) * step;

  const pos = rig.getAttribute("position");
  let nx = pos.x + dx, nz = pos.z + dz;

  const r = Math.hypot(nx, nz);
  const limit = 44;
  if(r > limit){ const k = limit / r; nx *= k; nz *= k; }

  if(keys.space && grounded){ vy = 5.2; grounded = false; }
  if(!grounded){ vy -= 12.0 * (dt/1000); }

  let ny = pos.y + vy * (dt/1000);

  /* ★地面は固定じゃない。今いる場所の地面へクランプ(埋まりゼロ) */
  const gy = groundYAt(nx, nz);
  if(ny <= gy){ ny = gy; vy = 0; grounded = true; }

  rig.setAttribute("position", { x:nx, y:ny, z:nz });

  if(len > 0){ hero.setAttribute("rotation", { x:0, y:getYaw(), z:0 }); }

  // 敵の追従(★yも地面追従)
  const enemy = document.getElementById("enemy");
  const epos = enemy.getAttribute("position");
  const dist = Math.hypot((epos.x - nx), (epos.z - nz));
  if(dist > 18){
    const ez = nz - 2.5;
    enemy.setAttribute("position", { x:nx, y:enemyYAt(nx, ez), z:ez });
  }

  const floating = document.getElementById("floating");
  if(floating){
    const t = performance.now() / 1000;
    floating.setAttribute("rotation", { x:0, y:(t*18)%360, z:0 });
    floating.setAttribute("position", { x:0, y:2.2 + Math.sin(t*1.4)*0.12, z:-10 });
  }
}
function hookThumbstick(){
  const RH = document.getElementById("rightHand");
  const LH = document.getElementById("leftHand");
  const onMove = (e)=>{
    if(!e || !e.detail) return;
    const { x, y } = e.detail;
    keys.w = y < -0.2; keys.s = y > 0.2; keys.a = x < -0.2; keys.d = x > 0.2;
  };
  RH.addEventListener("thumbstickmoved", onMove);
  LH.addEventListener("thumbstickmoved", onMove);
}
window.addEventListener("keydown", (e)=>{
  if(e.repeat) return;
  if(e.code==="KeyW") keys.w = true;
  if(e.code==="KeyA") keys.a = true;
  if(e.code==="KeyS") keys.s = true;
  if(e.code==="KeyD") keys.d = true;
  if(e.code==="ShiftLeft" || e.code==="ShiftRight") keys.shift = true;
  if(e.code==="Space") keys.space = true;
  if(e.code==="KeyJ"){ unlockAudio(); attack(); }
  if(e.code==="KeyK"){ unlockAudio(); castSpell(); }
  if(e.code==="Escape"){ if(!state.inVR) toggleHUD(); }
});
window.addEventListener("keyup", (e)=>{
  if(e.code==="KeyW") keys.w = false;
  if(e.code==="KeyA") keys.a = false;
  if(e.code==="KeyS") keys.s = false;
  if(e.code==="KeyD") keys.d = false;
  if(e.code==="ShiftLeft" || e.code==="ShiftRight") keys.shift = false;
  if(e.code==="Space") keys.space = false;
});

/* ===== DOMボタン配線 ===== */
function wireButtons(){
  bindPress(document.getElementById("btnTown"),   ()=> setField("town"));
  bindPress(document.getElementById("btnCastle"), ()=> setField("castle"));
  bindPress(document.getElementById("btnCave"),   ()=> setField("cave"));
  bindPress(document.getElementById("btnRuins"),  ()=> setField("ruins"));

  bindPress(document.getElementById("btnEnterVR"), ()=> enterVR());
  bindPress(document.getElementById("btnExit"),    ()=> exitApp());
  bindPress(document.getElementById("btnWave"),    ()=> wave());
  bindPress(document.getElementById("btnCheer"),   ()=> cheer());

  bindPress(document.getElementById("btnTalk"),  ()=> talk());
  bindPress(document.getElementById("btnQuest"), ()=> quest());
  bindPress(document.getElementById("btnShop"),  ()=> shop());
  bindPress(document.getElementById("btnRest"),  ()=> rest());

  bindPress(document.getElementById("btnSave"), ()=> saveGame());
  bindPress(document.getElementById("btnLoad"), ()=> loadGame());

  bindPress(document.getElementById("btnHideUI"), ()=> setHUDVisible(false));
  bindPress(document.getElementById("floatingShowUI"), ()=> setHUDVisible(true));

  bindPress(document.getElementById("panel"), ()=>{
    try{ cam.components["look-controls"].pointerLockEnabled = true; }catch(e){}
  });
}

/* ===== 初期化 ===== */
(function init(){
  wireButtons();
  hookThumbstick();

  const hidden = localStorage.getItem("elder_ui_hidden") === "1";
  setHUDVisible(!hidden);

  /* ★まず“基準0.55で置いた物”を全てスナップ(埋まり解消) */
  snapAll();

  /* ★プレイヤーは「その場の地面」に強制一致(埋まりゼロ) */
  const p0 = rig.getAttribute("position");
  if(p0 && typeof p0.x==="number" && typeof p0.z==="number"){
    rig.setAttribute("position", { x:p0.x, y:groundYAt(p0.x, p0.z), z:p0.z });
  }

  updateUI();
  setBGMByField();
  spawnEnemy();
  log("起動。島の段差に合わせて player/NPC/建物を自動補正(埋まりゼロ)。UIは右上で閉じられる。ESCでも切替。");

  let last = performance.now();
  function loop(now){
    const dt = now - last;
    last = now;
    tickMovement(dt);
    requestAnimationFrame(loop);
  }
  requestAnimationFrame(loop);

  const scene = document.getElementById("scene");
  scene.addEventListener("enter-vr", ()=>{
    state.inVR = true;
    document.body.classList.add("vr");
    document.getElementById("vrUI").setAttribute("visible","true");
    document.getElementById("cam").setAttribute("position","0 1.72 0.05");
    document.getElementById("heroHead").setAttribute("material","opacity:0.0; transparent:true; color:#f4d7bd");
    log("🕶️ WebXR: enter-vr");
  });
  scene.addEventListener("exit-vr", ()=>{
    state.inVR = false;
    document.body.classList.remove("vr");
    document.getElementById("vrUI").setAttribute("visible","false");
    document.getElementById("cam").setAttribute("position","0 1.75 3.4");
    document.getElementById("heroHead").setAttribute("material","opacity:1.0; transparent:false; color:#f4d7bd");
    setHUDVisible(hudVisible);
    log("⏏ WebXR: exit-vr");
  });
})();
</script>
</body>
</html>

ゲーム投稿サイト.html

<!doctype html>
<html lang="ja">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title>GameWorks — ゲーム作品投稿</title>
  <meta name="description" content="ゲーム作品を投稿・管理できるローカル保存型ポートフォリオサイト" />
  <style>
    :root{
      --bg:#0b0d12; --panel:#111520; --panel2:#0f1320; --card:#0f1524;
      --text:#e9edf7; --muted:#aab3c7; --line:#1f2a44;
      --accent:#7c5cff; --accent2:#22c55e; --warn:#f59e0b; --danger:#ef4444;
      --shadow: 0 12px 30px rgba(0,0,0,.35);
      --r: 18px;
      --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
      --sans: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji";
    }
    *{box-sizing:border-box}
    html,body{height:100%}
    body{
      margin:0;
      background:
        radial-gradient(900px 600px at 20% -10%, rgba(124,92,255,.25), transparent 60%),
        radial-gradient(900px 600px at 85% 0%, rgba(34,197,94,.18), transparent 60%),
        var(--bg);
      color:var(--text);
      font-family:var(--sans);
      letter-spacing:.2px;
    }
    a{color:inherit}
    .app{
      min-height:100%;
      display:grid;
      grid-template-columns: 360px 1fr;
      gap:16px;
      padding:16px;
      max-width:1400px;
      margin:0 auto;
    }
    @media (max-width: 980px){
      .app{grid-template-columns: 1fr; }
    }
    header{
      grid-column: 1 / -1;
      display:flex;
      align-items:center;
      justify-content:space-between;
      gap:12px;
      padding:14px 16px;
      border:1px solid var(--line);
      border-radius: var(--r);
      background: linear-gradient(180deg, rgba(17,21,32,.9), rgba(15,19,32,.75));
      box-shadow: var(--shadow);
      position:sticky;
      top:16px;
      z-index:10;
      backdrop-filter: blur(10px);
    }
    .brand{
      display:flex; align-items:center; gap:12px;
    }
    .logo{
      width:38px; height:38px; border-radius:14px;
      background: conic-gradient(from 180deg, var(--accent), #3b82f6, var(--accent2), var(--accent));
      box-shadow: 0 10px 20px rgba(124,92,255,.25);
    }
    .brand h1{font-size:16px; margin:0}
    .brand p{margin:2px 0 0 0; color:var(--muted); font-size:12px}
    .headerActions{display:flex; align-items:center; gap:10px; flex-wrap:wrap; justify-content:flex-end}
    .chip{
      display:inline-flex; align-items:center; gap:8px;
      padding:8px 10px;
      border:1px solid var(--line);
      border-radius: 999px;
      color:var(--muted);
      background: rgba(10,12,18,.35);
      font-size:12px;
      user-select:none;
    }
    .btn{
      appearance:none; border:1px solid var(--line);
      background: rgba(15,19,32,.85);
      color:var(--text);
      padding:10px 12px;
      border-radius: 12px;
      cursor:pointer;
      font-weight:600;
      transition: transform .08s ease, border-color .2s ease, background .2s ease, opacity .2s ease;
    }
    .btn:hover{border-color: rgba(124,92,255,.55)}
    .btn:active{transform: translateY(1px)}
    .btn.primary{
      border-color: rgba(124,92,255,.6);
      background: linear-gradient(180deg, rgba(124,92,255,.35), rgba(124,92,255,.15));
    }
    .btn.good{
      border-color: rgba(34,197,94,.55);
      background: linear-gradient(180deg, rgba(34,197,94,.25), rgba(34,197,94,.10));
    }
    .btn.danger{
      border-color: rgba(239,68,68,.6);
      background: linear-gradient(180deg, rgba(239,68,68,.25), rgba(239,68,68,.10));
    }
    .btn.ghost{background: transparent}
    .btn.small{padding:8px 10px; border-radius: 10px; font-size:12px}
    .btn:disabled{opacity:.55; cursor:not-allowed}
    .panel{
      border:1px solid var(--line);
      border-radius: var(--r);
      background: linear-gradient(180deg, rgba(17,21,32,.92), rgba(15,19,32,.78));
      box-shadow: var(--shadow);
      overflow:hidden;
    }
    .panelHeader{
      padding:14px 16px;
      border-bottom:1px solid var(--line);
      display:flex; align-items:center; justify-content:space-between; gap:10px;
    }
    .panelHeader h2{font-size:14px; margin:0}
    .panelHeader .hint{font-size:12px; color:var(--muted)}
    .panelBody{padding:14px 16px}
    .field{display:flex; flex-direction:column; gap:6px; margin-bottom:12px}
    .field label{font-size:12px; color:var(--muted)}
    .row{display:grid; grid-template-columns: 1fr 1fr; gap:10px}
    @media (max-width: 980px){ .row{grid-template-columns: 1fr} }
    input, select, textarea{
      width:100%;
      padding:10px 12px;
      border-radius: 12px;
      border:1px solid var(--line);
      background: rgba(10,12,18,.35);
      color:var(--text);
      outline:none;
    }
    textarea{min-height:110px; resize:vertical; line-height:1.5}
    input:focus, select:focus, textarea:focus{border-color: rgba(124,92,255,.55)}
    .help{font-size:11px; color:var(--muted); line-height:1.5}
    .divider{height:1px; background: var(--line); margin:14px 0}
    .toolbar{
      display:flex; gap:10px; align-items:center; flex-wrap:wrap;
      padding:12px 16px;
      border-bottom:1px solid var(--line);
      background: rgba(10,12,18,.18);
    }
    .toolbar input, .toolbar select{
      padding:10px 12px; border-radius: 999px;
      min-width: 200px;
    }
    .toolbar .grow{flex:1}
    .stats{
      display:flex; gap:8px; flex-wrap:wrap;
      padding:0 16px 14px 16px;
      color:var(--muted);
      font-size:12px;
    }
    .grid{
      padding:16px;
      display:grid;
      grid-template-columns: repeat(3, minmax(0, 1fr));
      gap:14px;
    }
    @media (max-width: 1200px){ .grid{grid-template-columns: repeat(2, minmax(0, 1fr));} }
    @media (max-width: 680px){ .grid{grid-template-columns: 1fr;} }
    .card{
      border:1px solid var(--line);
      border-radius: 18px;
      overflow:hidden;
      background: linear-gradient(180deg, rgba(15,21,36,.95), rgba(12,16,28,.88));
      box-shadow: 0 10px 25px rgba(0,0,0,.22);
      display:flex; flex-direction:column;
      min-height: 260px;
    }
    .thumb{
      height:160px;
      background: radial-gradient(1200px 260px at 10% 0%, rgba(124,92,255,.18), transparent 60%),
                  radial-gradient(1200px 260px at 80% 0%, rgba(34,197,94,.12), transparent 60%),
                  rgba(10,12,18,.25);
      border-bottom:1px solid var(--line);
      display:flex; align-items:center; justify-content:center;
      position:relative;
      overflow:hidden;
    }
    .thumb img{
      width:100%; height:100%;
      object-fit:cover;
      display:block;
      filter:saturate(1.02) contrast(1.02);
    }
    .badgeRow{
      position:absolute; left:10px; top:10px;
      display:flex; gap:8px; flex-wrap:wrap;
    }
    .badge{
      font-size:11px; color:var(--text);
      padding:6px 9px;
      border-radius: 999px;
      border:1px solid rgba(255,255,255,.12);
      background: rgba(0,0,0,.35);
      backdrop-filter: blur(8px);
    }
    .badge.good{border-color: rgba(34,197,94,.35)}
    .badge.warn{border-color: rgba(245,158,11,.35)}
    .badge.muted{color:var(--muted)}
    .cardBody{padding:12px 12px 10px 12px; display:flex; flex-direction:column; gap:8px; flex:1}
    .titleRow{display:flex; align-items:flex-start; justify-content:space-between; gap:10px}
    .card h3{margin:0; font-size:15px; line-height:1.25}
    .tagline{margin:0; color:var(--muted); font-size:12px; line-height:1.45}
    .meta{
      display:flex; gap:8px; flex-wrap:wrap;
      color:var(--muted); font-size:12px;
    }
    .pill{
      border:1px solid var(--line);
      background: rgba(10,12,18,.25);
      padding:6px 9px; border-radius:999px;
      font-size:12px;
    }
    .cardFooter{
      padding:10px 12px 12px 12px;
      display:flex; align-items:center; justify-content:space-between; gap:10px;
      border-top:1px solid var(--line);
      background: rgba(10,12,18,.18);
    }
    .actions{display:flex; gap:8px; flex-wrap:wrap}
    .muted{color:var(--muted)}
    .mono{font-family:var(--mono)}
    .empty{
      padding:28px 16px 40px 16px;
      text-align:center;
      color:var(--muted);
    }
    dialog{
      border:none;
      border-radius: 18px;
      padding:0;
      width:min(920px, calc(100vw - 24px));
      background: rgba(15,19,32,.96);
      color:var(--text);
      box-shadow: 0 30px 80px rgba(0,0,0,.55);
    }
    dialog::backdrop{background: rgba(0,0,0,.6)}
    .modalHeader{
      padding:14px 16px;
      border-bottom:1px solid var(--line);
      display:flex; align-items:center; justify-content:space-between; gap:10px;
    }
    .modalHeader h3{margin:0; font-size:14px}
    .modalBody{padding:14px 16px}
    .modalGrid{
      display:grid; grid-template-columns: 1.2fr .8fr; gap:14px;
    }
    @media (max-width: 900px){ .modalGrid{grid-template-columns: 1fr} }
    .gallery{
      display:grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap:10px;
    }
    @media (max-width: 680px){ .gallery{grid-template-columns: repeat(2, minmax(0, 1fr));} }
    .gimg{
      border:1px solid var(--line);
      border-radius: 14px;
      overflow:hidden;
      background: rgba(10,12,18,.25);
      aspect-ratio: 16 / 10;
      display:flex; align-items:center; justify-content:center;
    }
    .gimg img{width:100%; height:100%; object-fit:cover}
    .kvs{display:grid; grid-template-columns: 110px 1fr; gap:8px; align-items:start; font-size:12px; color:var(--muted)}
    .kvs b{color:var(--text); font-weight:700}
    .note{
      padding:10px 12px;
      border:1px solid var(--line);
      border-radius: 14px;
      background: rgba(10,12,18,.25);
      color:var(--muted);
      font-size:12px;
      line-height:1.55;
    }
    .toast{
      position: fixed;
      left:50%;
      bottom:16px;
      transform: translateX(-50%);
      background: rgba(15,19,32,.95);
      border: 1px solid var(--line);
      border-radius: 999px;
      padding: 10px 14px;
      color: var(--text);
      box-shadow: var(--shadow);
      opacity:0;
      pointer-events:none;
      transition: opacity .2s ease, transform .2s ease;
      display:flex; gap:8px; align-items:center;
      z-index:100;
      max-width: calc(100vw - 24px);
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .toast.show{opacity:1; transform: translateX(-50%) translateY(-2px)}
    .smallLink{
      color: var(--muted);
      font-size:12px;
      text-decoration:none;
      border-bottom:1px dashed rgba(170,179,199,.35);
    }
    .smallLink:hover{color: var(--text); border-bottom-color: rgba(233,237,247,.55)}
    .dangerText{color: #ffb4b4}
    .goodText{color: #b5ffd1}
    .warnText{color: #ffe2b0}
  </style>
</head>
<body>
  <div class="app">
    <header>
      <div class="brand">
        <div class="logo" aria-hidden="true"></div>
        <div>
          <h1>GameWorks <span class="muted">— ゲーム作品投稿</span></h1>
          <p>サーバー不要 / ブラウザに保存 / 作品ポートフォリオを即作る</p>
        </div>
      </div>
      <div class="headerActions">
        <span class="chip" id="chipCount">作品: 0</span>
        <button class="btn small" id="btnSeed" type="button">サンプル追加</button>
        <button class="btn small" id="btnExport" type="button">エクスポート</button>
        <button class="btn small" id="btnImport" type="button">インポート</button>
        <button class="btn small danger" id="btnWipe" type="button">全削除</button>
      </div>
    </header>

    <!-- Left: 投稿フォーム -->
    <section class="panel" aria-label="投稿フォーム">
      <div class="panelHeader">
        <div>
          <h2 id="formTitle">新規投稿</h2>
          <div class="hint" id="formHint">作品情報を入力して保存</div>
        </div>
        <button class="btn small ghost" id="btnResetForm" type="button">リセット</button>
      </div>
      <div class="panelBody">
        <div class="field">
          <label for="author">投稿者名(任意)</label>
          <input id="author" type="text" maxlength="40" placeholder="例:Yuhei" />
          <div class="help">同じブラウザ内だけの表示です。</div>
        </div>

        <div class="field">
          <label for="title">作品タイトル *</label>
          <input id="title" type="text" maxlength="60" placeholder="例:Elder Chronicle VR" required />
        </div>

        <div class="field">
          <label for="tagline">ひとこと(キャッチコピー)</label>
          <input id="tagline" type="text" maxlength="80" placeholder="例:探索と戦闘が気持ちいいVRアクションRPG" />
        </div>

        <div class="row">
          <div class="field">
            <label for="engine">使用エンジン</label>
            <select id="engine">
              <option value="">未設定</option>
              <option>Unity</option>
              <option>Unreal Engine</option>
              <option>Godot</option>
              <option>RPGツクール</option>
              <option>自作</option>
              <option>その他</option>
            </select>
          </div>
          <div class="field">
            <label for="status">状態</label>
            <select id="status">
              <option>開発中</option>
              <option>体験版あり</option>
              <option>公開中</option>
              <option>凍結</option>
            </select>
          </div>
        </div>

        <div class="row">
          <div class="field">
            <label for="platform">対応プラットフォーム</label>
            <input id="platform" type="text" maxlength="60" placeholder="例:PC / Web / Quest / Android" />
          </div>
          <div class="field">
            <label for="genre">ジャンル</label>
            <input id="genre" type="text" maxlength="60" placeholder="例:アクション / VR / サバイバル" />
          </div>
        </div>

        <div class="field">
          <label for="tags">タグ(カンマ区切り)</label>
          <input id="tags" type="text" maxlength="120" placeholder="例:VR, 探索, ダンジョン, ボス戦" />
        </div>

        <div class="field">
          <label for="desc">説明 *</label>
          <textarea id="desc" maxlength="2000" placeholder="作品の魅力、遊び方、特徴、今後の予定など"></textarea>
          <div class="help">最大2000文字。長い場合は要点→詳細の順で書くと強い。</div>
        </div>

        <div class="row">
          <div class="field">
            <label for="linkPlay">プレイURL / 公開ページ</label>
            <input id="linkPlay" type="url" placeholder="https://..." />
          </div>
          <div class="field">
            <label for="linkRepo">GitHub / リポジトリ</label>
            <input id="linkRepo" type="url" placeholder="https://github.com/..." />
          </div>
        </div>

        <div class="row">
          <div class="field">
            <label for="linkVideo">動画URL(YouTubeなど)</label>
            <input id="linkVideo" type="url" placeholder="https://www.youtube.com/watch?v=..." />
          </div>
          <div class="field">
            <label for="rating">自己評価(1〜5)</label>
            <select id="rating">
              <option value="0">未設定</option>
              <option value="1">★1</option>
              <option value="2">★2</option>
              <option value="3">★3</option>
              <option value="4">★4</option>
              <option value="5">★5</option>
            </select>
          </div>
        </div>

        <div class="field">
          <label for="shots">スクリーンショット(複数可 / 自動でデータ化して保存)</label>
          <input id="shots" type="file" accept="image/*" multiple />
          <div class="help">
            画像はブラウザ内に保存されます(容量が大きいと重くなります)。<br/>
            目安:1枚 500KB〜1MB程度に圧縮すると快適。
          </div>
        </div>

        <div class="divider"></div>

        <div style="display:flex; gap:10px; flex-wrap:wrap; align-items:center">
          <button class="btn primary" id="btnSave" type="button">保存</button>
          <button class="btn" id="btnDraft" type="button">下書き保存</button>
          <span class="help" id="saveHelp">* 必須:タイトル / 説明</span>
        </div>

        <div class="divider"></div>

        <div class="note">
          <b>保存方式:</b>このページは <span class="mono">localStorage</span> に保存します。<br/>
          つまり「あなたのブラウザ内だけ」に残ります。公開サイトで運用したいなら、次はサーバー保存(PHP/DB)に切り替える。
        </div>
      </div>
    </section>

    <!-- Right: 一覧 -->
    <main class="panel" aria-label="作品一覧">
      <div class="toolbar">
        <input class="grow" id="q" type="search" placeholder="検索:タイトル/説明/タグ/エンジン/ジャンル..." />
        <select id="filterStatus" title="状態">
          <option value="">状態:すべて</option>
          <option>開発中</option>
          <option>体験版あり</option>
          <option>公開中</option>
          <option>凍結</option>
        </select>
        <select id="sort" title="並び替え">
          <option value="new">新しい順</option>
          <option value="old">古い順</option>
          <option value="likes">いいね順</option>
          <option value="rating">評価順</option>
          <option value="title">タイトル順</option>
        </select>
        <button class="btn small good" id="btnNew" type="button">+ 新規投稿</button>
      </div>

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

      <div id="list" class="grid" aria-live="polite"></div>
      <div id="empty" class="empty" hidden>
        まだ作品がありません。左のフォームから投稿して、あなたの作品集を完成させよう。
      </div>
    </main>
  </div>

  <!-- 詳細モーダル -->
  <dialog id="modal">
    <div class="modalHeader">
      <h3 id="mTitle">詳細</h3>
      <div style="display:flex; gap:8px; align-items:center">
        <button class="btn small" id="mEdit" type="button">編集</button>
        <button class="btn small" id="mLike" type="button">いいね</button>
        <button class="btn small danger" id="mDelete" type="button">削除</button>
        <button class="btn small ghost" id="mClose" type="button">閉じる</button>
      </div>
    </div>
    <div class="modalBody">
      <div class="modalGrid">
        <div>
          <div class="note" id="mTagline"></div>
          <div class="divider"></div>

          <div class="gallery" id="mGallery"></div>

          <div class="divider"></div>

          <div class="note" id="mDesc"></div>

          <div class="divider"></div>

          <div class="panel" style="background: rgba(10,12,18,.12); box-shadow:none">
            <div class="panelHeader" style="border-bottom:1px solid var(--line)">
              <div>
                <h2 style="margin:0;font-size:14px">コメント</h2>
                <div class="hint">ローカル保存(このブラウザだけ)</div>
              </div>
            </div>
            <div class="panelBody">
              <div class="field">
                <label for="cName">名前(任意)</label>
                <input id="cName" type="text" maxlength="40" placeholder="例:Anonymous" />
              </div>
              <div class="field">
                <label for="cText">コメント</label>
                <textarea id="cText" maxlength="400" placeholder="感想 / フィードバック"></textarea>
                <div class="help">最大400文字</div>
              </div>
              <div style="display:flex; gap:10px; align-items:center; flex-wrap:wrap">
                <button class="btn good" id="cAdd" type="button">コメント追加</button>
                <span class="help" id="cHint"></span>
              </div>
              <div class="divider"></div>
              <div id="cList" style="display:flex;flex-direction:column;gap:10px"></div>
            </div>
          </div>
        </div>

        <div>
          <div class="note">
            <div class="kvs" id="mMeta"></div>
          </div>
          <div class="divider"></div>

          <div class="note" id="mLinks"></div>

          <div class="divider"></div>

          <div class="note" id="mSystem"></div>
        </div>
      </div>
    </div>
  </dialog>

  <div class="toast" id="toast" role="status" aria-live="polite"></div>

  <input id="importFile" type="file" accept="application/json" hidden />

  <script>
    "use strict";

    // ====== Storage Keys ======
    const KEY = "gameworks_posts_v1";
    const KEY_DRAFT = "gameworks_draft_v1";

    // ====== DOM ======
    const el = (id) => document.getElementById(id);

    const chipCount = el("chipCount");
    const listEl = el("list");
    const emptyEl = el("empty");
    const statsEl = el("stats");

    const qEl = el("q");
    const filterStatusEl = el("filterStatus");
    const sortEl = el("sort");

    const formTitleEl = el("formTitle");
    const formHintEl = el("formHint");

    // form fields
    const authorEl = el("author");
    const titleEl = el("title");
    const taglineEl = el("tagline");
    const engineEl = el("engine");
    const statusEl = el("status");
    const platformEl = el("platform");
    const genreEl = el("genre");
    const tagsEl = el("tags");
    const descEl = el("desc");
    const linkPlayEl = el("linkPlay");
    const linkRepoEl = el("linkRepo");
    const linkVideoEl = el("linkVideo");
    const ratingEl = el("rating");
    const shotsEl = el("shots");

    const btnSave = el("btnSave");
    const btnDraft = el("btnDraft");
    const btnResetForm = el("btnResetForm");

    const btnSeed = el("btnSeed");
    const btnExport = el("btnExport");
    const btnImport = el("btnImport");
    const btnWipe = el("btnWipe");
    const btnNew = el("btnNew");

    const toastEl = el("toast");
    const importFileEl = el("importFile");

    // modal
    const modal = el("modal");
    const mTitle = el("mTitle");
    const mTagline = el("mTagline");
    const mGallery = el("mGallery");
    const mDesc = el("mDesc");
    const mMeta = el("mMeta");
    const mLinks = el("mLinks");
    const mSystem = el("mSystem");

    const mEdit = el("mEdit");
    const mLike = el("mLike");
    const mDelete = el("mDelete");
    const mClose = el("mClose");

    // comments
    const cName = el("cName");
    const cText = el("cText");
    const cAdd = el("cAdd");
    const cHint = el("cHint");
    const cList = el("cList");

    // ====== State ======
    let posts = loadPosts();
    let editingId = null;
    let pendingImages = []; // base64 array for current form
    let currentModalId = null;

    // ====== Utils ======
    const nowISO = () => new Date().toISOString();
    const uid = () => "p_" + Math.random().toString(16).slice(2) + Date.now().toString(16);

    function toast(msg){
      toastEl.textContent = msg;
      toastEl.classList.add("show");
      clearTimeout(toastEl._t);
      toastEl._t = setTimeout(()=> toastEl.classList.remove("show"), 1700);
    }

    function safeText(str){
      // basic escape for text rendering into HTML
      return String(str ?? "")
        .replaceAll("&","&amp;")
        .replaceAll("<","&lt;")
        .replaceAll(">","&gt;")
        .replaceAll('"',"&quot;")
        .replaceAll("'","&#39;");
    }

    function normalizeTags(input){
      return String(input ?? "")
        .split(",")
        .map(s => s.trim())
        .filter(Boolean)
        .slice(0, 20);
    }

    function stars(n){
      const v = Number(n || 0);
      if(!v) return "未設定";
      return "★".repeat(v) + "☆".repeat(5 - v);
    }

    function fmtDate(iso){
      try{
        const d = new Date(iso);
        const y = d.getFullYear();
        const m = String(d.getMonth()+1).padStart(2,"0");
        const dd = String(d.getDate()).padStart(2,"0");
        const hh = String(d.getHours()).padStart(2,"0");
        const mm = String(d.getMinutes()).padStart(2,"0");
        return `${y}/${m}/${dd} ${hh}:${mm}`;
      }catch{
        return String(iso);
      }
    }

    function loadPosts(){
      try{
        const raw = localStorage.getItem(KEY);
        const arr = raw ? JSON.parse(raw) : [];
        return Array.isArray(arr) ? arr : [];
      }catch{
        return [];
      }
    }

    function savePosts(){
      localStorage.setItem(KEY, JSON.stringify(posts));
      chipCount.textContent = `作品: ${posts.length}`;
    }

    function loadDraft(){
      try{
        const raw = localStorage.getItem(KEY_DRAFT);
        return raw ? JSON.parse(raw) : null;
      }catch{
        return null;
      }
    }

    function saveDraft(draft){
      localStorage.setItem(KEY_DRAFT, JSON.stringify(draft));
    }

    function clearDraft(){
      localStorage.removeItem(KEY_DRAFT);
    }

    function bytesApprox(){
      // rough localStorage usage for our key
      const raw = localStorage.getItem(KEY) || "";
      return raw.length;
    }

    function buildStats(filteredCount){
      const likeSum = posts.reduce((a,p)=> a + (p.likes||0), 0);
      const bytes = bytesApprox();
      const mb = (bytes / (1024*1024)).toFixed(2);
      statsEl.innerHTML = `
        <span class="chip">表示: <b>${filteredCount}</b></span>
        <span class="chip">総いいね: <b>${likeSum}</b></span>
        <span class="chip">保存サイズ目安: <b>${mb} MB</b></span>
      `;
    }

    // ====== Image handling (compress to JPEG) ======
    async function fileToDataUrlCompressed(file, maxW=1280, quality=0.82){
      const img = await new Promise((res, rej)=>{
        const i = new Image();
        i.onload = ()=> res(i);
        i.onerror = rej;
        i.src = URL.createObjectURL(file);
      });

      const scale = Math.min(1, maxW / img.width);
      const w = Math.round(img.width * scale);
      const h = Math.round(img.height * scale);

      const canvas = document.createElement("canvas");
      canvas.width = w; canvas.height = h;
      const ctx = canvas.getContext("2d");
      ctx.drawImage(img, 0, 0, w, h);

      URL.revokeObjectURL(img.src);

      return canvas.toDataURL("image/jpeg", quality);
    }

    shotsEl.addEventListener("change", async () => {
      const files = Array.from(shotsEl.files || []);
      if(!files.length) return;

      toast("画像を処理中…");
      const max = 9;
      const slice = files.slice(0, max);

      const out = [];
      for(const f of slice){
        if(!f.type.startsWith("image/")) continue;
        try{
          const d = await fileToDataUrlCompressed(f);
          out.push(d);
        }catch{}
      }
      pendingImages = out;
      toast(`スクショ ${pendingImages.length}枚 準備OK`);
    });

    // ====== Render ======
    function getFiltered(){
      const q = (qEl.value || "").trim().toLowerCase();
      const st = filterStatusEl.value || "";

      let arr = [...posts];

      if(q){
        arr = arr.filter(p=>{
          const blob = [
            p.title, p.tagline, p.desc, p.engine, p.platform, p.genre,
            ...(p.tags||[]),
          ].join(" ").toLowerCase();
          return blob.includes(q);
        });
      }
      if(st){
        arr = arr.filter(p => (p.status || "") === st);
      }

      // sort
      const s = sortEl.value;
      arr.sort((a,b)=>{
        if(s === "new") return (b.createdAt||"").localeCompare(a.createdAt||"");
        if(s === "old") return (a.createdAt||"").localeCompare(b.createdAt||"");
        if(s === "likes") return (b.likes||0) - (a.likes||0);
        if(s === "rating") return (Number(b.rating||0) - Number(a.rating||0)) || (b.likes||0)-(a.likes||0);
        if(s === "title") return (a.title||"").localeCompare(b.title||"", "ja");
        return 0;
      });

      return arr;
    }

    function statusBadgeClass(status){
      if(status === "公開中") return "good";
      if(status === "体験版あり") return "warn";
      return "";
    }

    function render(){
      savePosts(); // also updates chipCount
      const arr = getFiltered();
      buildStats(arr.length);

      listEl.innerHTML = "";
      emptyEl.hidden = arr.length !== 0;

      for(const p of arr){
        const cover = (p.images && p.images[0]) ? `<img alt="cover" src="${p.images[0]}">` : "";
        const tags = (p.tags||[]).slice(0,3).map(t=>`<span class="pill">#${safeText(t)}</span>`).join("");
        const engine = p.engine ? `<span class="pill">${safeText(p.engine)}</span>` : "";
        const genre = p.genre ? `<span class="pill">${safeText(p.genre)}</span>` : "";
        const rating = Number(p.rating||0) ? `<span class="pill">${safeText(stars(p.rating))}</span>` : "";

        const card = document.createElement("article");
        card.className = "card";
        card.innerHTML = `
          <div class="thumb">
            ${cover || `<div class="muted">No Image</div>`}
            <div class="badgeRow">
              <span class="badge ${statusBadgeClass(p.status)}">${safeText(p.status || "未設定")}</span>
              ${p.platform ? `<span class="badge muted">${safeText(p.platform)}</span>` : ""}
            </div>
          </div>
          <div class="cardBody">
            <div class="titleRow">
              <div style="min-width:0">
                <h3 title="${safeText(p.title)}">${safeText(p.title)}</h3>
                <p class="tagline">${safeText(p.tagline || "—")}</p>
              </div>
              <div class="muted" style="font-size:12px; white-space:nowrap">
                ❤ ${p.likes||0}
              </div>
            </div>
            <div class="meta">
              ${engine}
              ${genre}
              ${rating}
              ${tags}
            </div>
            <div class="muted" style="font-size:12px; line-height:1.55; display:-webkit-box; -webkit-line-clamp:3; -webkit-box-orient:vertical; overflow:hidden">
              ${safeText(p.desc || "")}
            </div>
          </div>
          <div class="cardFooter">
            <div class="muted" style="font-size:12px">更新: ${safeText(fmtDate(p.updatedAt || p.createdAt))}</div>
            <div class="actions">
              <button class="btn small" data-act="open" data-id="${p.id}">詳細</button>
              <button class="btn small good" data-act="like" data-id="${p.id}">いいね</button>
            </div>
          </div>
        `;
        listEl.appendChild(card);
      }
    }

    listEl.addEventListener("click", (e)=>{
      const btn = e.target.closest("button");
      if(!btn) return;
      const id = btn.getAttribute("data-id");
      const act = btn.getAttribute("data-act");
      if(!id || !act) return;

      if(act === "open") openModal(id);
      if(act === "like") { likePost(id); render(); }
    });

    // ====== CRUD ======
    function validateForm(){
      const title = titleEl.value.trim();
      const desc = descEl.value.trim();
      if(!title || !desc){
        toast("タイトルと説明は必須");
        return false;
      }
      return true;
    }

    function readFormAsPost(){
      const tags = normalizeTags(tagsEl.value);
      return {
        id: editingId || uid(),
        author: authorEl.value.trim(),
        title: titleEl.value.trim(),
        tagline: taglineEl.value.trim(),
        engine: engineEl.value,
        status: statusEl.value,
        platform: platformEl.value.trim(),
        genre: genreEl.value.trim(),
        tags,
        desc: descEl.value.trim(),
        linkPlay: linkPlayEl.value.trim(),
        linkRepo: linkRepoEl.value.trim(),
        linkVideo: linkVideoEl.value.trim(),
        rating: Number(ratingEl.value || 0),
        images: (pendingImages && pendingImages.length) ? pendingImages : [],
        likes: 0,
        comments: [],
        createdAt: nowISO(),
        updatedAt: nowISO()
      };
    }

    function resetForm(keepDraft=false){
      editingId = null;
      pendingImages = [];
      if(!keepDraft){
        authorEl.value = "";
        titleEl.value = "";
        taglineEl.value = "";
        engineEl.value = "";
        statusEl.value = "開発中";
        platformEl.value = "";
        genreEl.value = "";
        tagsEl.value = "";
        descEl.value = "";
        linkPlayEl.value = "";
        linkRepoEl.value = "";
        linkVideoEl.value = "";
        ratingEl.value = "0";
        shotsEl.value = "";
      }
      formTitleEl.textContent = "新規投稿";
      formHintEl.textContent = "作品情報を入力して保存";
      btnSave.textContent = "保存";
    }

    function setFormFromPost(p){
      editingId = p.id;
      pendingImages = Array.isArray(p.images) ? [...p.images] : [];

      authorEl.value = p.author || "";
      titleEl.value = p.title || "";
      taglineEl.value = p.tagline || "";
      engineEl.value = p.engine || "";
      statusEl.value = p.status || "開発中";
      platformEl.value = p.platform || "";
      genreEl.value = p.genre || "";
      tagsEl.value = (p.tags || []).join(", ");
      descEl.value = p.desc || "";
      linkPlayEl.value = p.linkPlay || "";
      linkRepoEl.value = p.linkRepo || "";
      linkVideoEl.value = p.linkVideo || "";
      ratingEl.value = String(p.rating || 0);
      shotsEl.value = "";

      formTitleEl.textContent = "編集";
      formHintEl.textContent = "内容を更新して保存";
      btnSave.textContent = "更新";
      toast("編集モード");
      window.scrollTo({top:0, behavior:"smooth"});
    }

    function upsertPost(p){
      const idx = posts.findIndex(x=>x.id===p.id);
      if(idx >= 0){
        const prev = posts[idx];
        posts[idx] = {
          ...prev,
          ...p,
          likes: prev.likes || 0,
          comments: Array.isArray(prev.comments) ? prev.comments : [],
          createdAt: prev.createdAt || nowISO(),
          updatedAt: nowISO()
        };
      }else{
        posts.unshift(p);
      }
      savePosts();
    }

    btnSave.addEventListener("click", ()=>{
      if(!validateForm()) return;

      const p = readFormAsPost();
      // if editing, keep previous likes/comments/createdAt
      if(editingId){
        const prev = posts.find(x=>x.id===editingId);
        if(prev){
          p.likes = prev.likes || 0;
          p.comments = Array.isArray(prev.comments) ? prev.comments : [];
          p.createdAt = prev.createdAt || nowISO();
          p.updatedAt = nowISO();
          // if user did not reselect images, keep old images
          if(!pendingImages.length && Array.isArray(prev.images)) p.images = prev.images;
        }
      }

      upsertPost(p);
      clearDraft();
      resetForm();
      render();
      toast(editingId ? "更新した" : "保存した");
    });

    btnDraft.addEventListener("click", ()=>{
      const draft = {
        author: authorEl.value,
        title: titleEl.value,
        tagline: taglineEl.value,
        engine: engineEl.value,
        status: statusEl.value,
        platform: platformEl.value,
        genre: genreEl.value,
        tags: tagsEl.value,
        desc: descEl.value,
        linkPlay: linkPlayEl.value,
        linkRepo: linkRepoEl.value,
        linkVideo: linkVideoEl.value,
        rating: ratingEl.value,
        images: pendingImages
      };
      saveDraft(draft);
      toast("下書きを保存した");
    });

    btnResetForm.addEventListener("click", ()=>{
      resetForm();
      toast("フォームをリセット");
    });

    btnNew.addEventListener("click", ()=>{
      resetForm();
      toast("新規投稿モード");
      window.scrollTo({top:0, behavior:"smooth"});
    });

    // ====== Like / Delete ======
    function likePost(id){
      const p = posts.find(x=>x.id===id);
      if(!p) return;
      p.likes = (p.likes||0) + 1;
      p.updatedAt = nowISO();
      savePosts();
      toast("いいね +1");
      if(currentModalId === id) refreshModal();
    }

    function deletePost(id){
      const idx = posts.findIndex(x=>x.id===id);
      if(idx < 0) return;
      posts.splice(idx, 1);
      savePosts();
      toast("削除した");
    }

    // ====== Modal ======
    function openModal(id){
      const p = posts.find(x=>x.id===id);
      if(!p) return;
      currentModalId = id;
      // count view? (optional)
      modal.showModal();
      refreshModal();
    }

    function refreshModal(){
      const p = posts.find(x=>x.id===currentModalId);
      if(!p) return;

      mTitle.textContent = p.title || "詳細";
      mTagline.innerHTML = `<b>${safeText(p.tagline || "—")}</b><br><span class="muted">❤ ${p.likes||0} / 評価: ${safeText(stars(p.rating))}</span>`;

      // gallery
      mGallery.innerHTML = "";
      const imgs = Array.isArray(p.images) ? p.images : [];
      if(imgs.length){
        for(const src of imgs){
          const div = document.createElement("div");
          div.className = "gimg";
          div.innerHTML = `<img alt="screenshot" src="${src}">`;
          mGallery.appendChild(div);
        }
      }else{
        mGallery.innerHTML = `<div class="note">スクショなし</div>`;
      }

      mDesc.innerHTML = safeText(p.desc || "").replaceAll("\n","<br>");

      // meta
      const t = (p.tags||[]).map(x=>`#${x}`).join(" ");
      mMeta.innerHTML = `
        <b>投稿者</b><div>${safeText(p.author || "—")}</div>
        <b>状態</b><div>${safeText(p.status || "—")}</div>
        <b>エンジン</b><div>${safeText(p.engine || "—")}</div>
        <b>プラットフォーム</b><div>${safeText(p.platform || "—")}</div>
        <b>ジャンル</b><div>${safeText(p.genre || "—")}</div>
        <b>タグ</b><div>${safeText(t || "—")}</div>
        <b>作成</b><div>${safeText(fmtDate(p.createdAt))}</div>
        <b>更新</b><div>${safeText(fmtDate(p.updatedAt || p.createdAt))}</div>
      `;

      // links
      const links = [];
      if(p.linkPlay) links.push(`<a class="smallLink" href="${safeText(p.linkPlay)}" target="_blank" rel="noopener">▶ 公開/プレイページ</a>`);
      if(p.linkRepo) links.push(`<a class="smallLink" href="${safeText(p.linkRepo)}" target="_blank" rel="noopener">⌂ リポジトリ</a>`);
      if(p.linkVideo) links.push(`<a class="smallLink" href="${safeText(p.linkVideo)}" target="_blank" rel="noopener">🎬 動画</a>`);
      mLinks.innerHTML = links.length ? links.join("<br>") : `<span class="muted">リンクなし</span>`;

      mSystem.innerHTML = `
        <b>操作</b><br>
        ・編集で左フォームに読み込み<br>
        ・いいねはローカルカウント<br>
        ・削除は取り消し不可
      `;

      // buttons
      mLike.textContent = `いいね (${p.likes||0})`;

      renderComments(p);
    }

    mClose.addEventListener("click", ()=> modal.close());
    modal.addEventListener("click", (e)=>{
      const rect = modal.getBoundingClientRect();
      const inDialog = (
        rect.top <= e.clientY && e.clientY <= rect.top + rect.height &&
        rect.left <= e.clientX && e.clientX <= rect.left + rect.width
      );
      if(!inDialog) modal.close();
    });

    mLike.addEventListener("click", ()=>{
      if(!currentModalId) return;
      likePost(currentModalId);
      render();
    });

    mDelete.addEventListener("click", ()=>{
      if(!currentModalId) return;
      const p = posts.find(x=>x.id===currentModalId);
      if(!p) return;
      const ok = confirm(`「${p.title}」を削除します。よろしいですか?`);
      if(!ok) return;
      deletePost(currentModalId);
      modal.close();
      currentModalId = null;
      render();
    });

    mEdit.addEventListener("click", ()=>{
      const p = posts.find(x=>x.id===currentModalId);
      if(!p) return;
      setFormFromPost(p);
      modal.close();
    });

    // ====== Comments ======
    function renderComments(p){
      const arr = Array.isArray(p.comments) ? p.comments : [];
      cHint.textContent = `コメント数: ${arr.length}`;

      cList.innerHTML = "";
      if(!arr.length){
        cList.innerHTML = `<div class="note">まだコメントはありません。</div>`;
        return;
      }
      for(const c of arr.slice().reverse()){
        const box = document.createElement("div");
        box.className = "note";
        box.innerHTML = `
          <b>${safeText(c.name || "Anonymous")}</b>
          <span class="muted"> / ${safeText(fmtDate(c.at))}</span><br>
          ${safeText(c.text || "").replaceAll("\n","<br>")}
        `;
        cList.appendChild(box);
      }
    }

    cAdd.addEventListener("click", ()=>{
      const p = posts.find(x=>x.id===currentModalId);
      if(!p) return;

      const text = (cText.value || "").trim();
      if(!text){
        toast("コメントを入力");
        return;
      }
      const name = (cName.value || "").trim();

      p.comments = Array.isArray(p.comments) ? p.comments : [];
      p.comments.push({ name, text, at: nowISO() });
      p.updatedAt = nowISO();
      savePosts();
      cText.value = "";
      toast("コメント追加");
      refreshModal();
      render();
    });

    // ====== Search / filter / sort ======
    [qEl, filterStatusEl, sortEl].forEach(x => x.addEventListener("input", render));

    // ====== Export / Import ======
    btnExport.addEventListener("click", ()=>{
      const payload = {
        version: 1,
        exportedAt: nowISO(),
        posts
      };
      const blob = new Blob([JSON.stringify(payload, null, 2)], {type:"application/json"});
      const url = URL.createObjectURL(blob);
      const a = document.createElement("a");
      a.href = url;
      a.download = `gameworks_export_${new Date().toISOString().slice(0,10)}.json`;
      a.click();
      URL.revokeObjectURL(url);
      toast("エクスポートした");
    });

    btnImport.addEventListener("click", ()=>{
      importFileEl.value = "";
      importFileEl.click();
    });

    importFileEl.addEventListener("change", async ()=>{
      const file = importFileEl.files && importFileEl.files[0];
      if(!file) return;

      try{
        const text = await file.text();
        const data = JSON.parse(text);
        const arr = data && data.posts;
        if(!Array.isArray(arr)) throw new Error("invalid");
        // merge (by id)
        const map = new Map(posts.map(p=>[p.id, p]));
        for(const p of arr){
          if(!p || !p.id) continue;
          const prev = map.get(p.id);
          map.set(p.id, prev ? ({...prev, ...p}) : p);
        }
        posts = Array.from(map.values());
        // normalize
        posts = posts.map(p=>({
          id: p.id || uid(),
          author: p.author || "",
          title: p.title || "Untitled",
          tagline: p.tagline || "",
          engine: p.engine || "",
          status: p.status || "開発中",
          platform: p.platform || "",
          genre: p.genre || "",
          tags: Array.isArray(p.tags) ? p.tags : normalizeTags(p.tags),
          desc: p.desc || "",
          linkPlay: p.linkPlay || "",
          linkRepo: p.linkRepo || "",
          linkVideo: p.linkVideo || "",
          rating: Number(p.rating || 0),
          images: Array.isArray(p.images) ? p.images : [],
          likes: Number(p.likes || 0),
          comments: Array.isArray(p.comments) ? p.comments : [],
          createdAt: p.createdAt || nowISO(),
          updatedAt: p.updatedAt || p.createdAt || nowISO()
        }));

        savePosts();
        render();
        toast("インポート完了");
      }catch{
        toast("インポート失敗(JSON形式を確認)");
      }
    });

    // ====== Wipe ======
    btnWipe.addEventListener("click", ()=>{
      const ok = confirm("全作品データを削除します。取り消しできません。よろしいですか?");
      if(!ok) return;
      posts = [];
      savePosts();
      render();
      resetForm();
      clearDraft();
      toast("全削除した");
    });

    // ====== Seed ======
    btnSeed.addEventListener("click", ()=>{
      const seed = [
        {
          id: uid(),
          author: "Yuhei",
          title: "KnightSurvivors",
          tagline: "短時間で熱くなれるサバイバルアクション",
          engine: "Unity",
          status: "体験版あり",
          platform: "PC / Web",
          genre: "サバイバル / アクション",
          tags: ["サバイバル","爽快","ローグライト"],
          desc: "敵の波をさばき、ビルドを組み替えて最適解を探す。\n短時間でも気持ちよく終われるテンポを意識。",
          linkPlay: "",
          linkRepo: "",
          linkVideo: "",
          rating: 4,
          images: [],
          likes: 12,
          comments: [],
          createdAt: nowISO(),
          updatedAt: nowISO()
        },
        {
          id: uid(),
          author: "Yuhei",
          title: "Elder Chronicle VR",
          tagline: "探索・クエスト・戦闘を1つにまとめたVR世界",
          engine: "A-Frame",
          status: "開発中",
          platform: "Quest / WebXR",
          genre: "VR / RPG",
          tags: ["VR","RPG","ダンジョン","クエスト"],
          desc: "場所移動・クエスト受注・戦闘のループを磨いていく。\nUIと世界観の一体感を最優先。",
          linkPlay: "",
          linkRepo: "",
          linkVideo: "",
          rating: 5,
          images: [],
          likes: 30,
          comments: [],
          createdAt: nowISO(),
          updatedAt: nowISO()
        }
      ];
      posts = [...seed, ...posts];
      savePosts();
      render();
      toast("サンプルを追加した");
    });

    // ====== Load draft on start ======
    (function init(){
      chipCount.textContent = `作品: ${posts.length}`;

      const draft = loadDraft();
      if(draft){
        authorEl.value = draft.author || "";
        titleEl.value = draft.title || "";
        taglineEl.value = draft.tagline || "";
        engineEl.value = draft.engine || "";
        statusEl.value = draft.status || "開発中";
        platformEl.value = draft.platform || "";
        genreEl.value = draft.genre || "";
        tagsEl.value = draft.tags || "";
        descEl.value = draft.desc || "";
        linkPlayEl.value = draft.linkPlay || "";
        linkRepoEl.value = draft.linkRepo || "";
        linkVideoEl.value = draft.linkVideo || "";
        ratingEl.value = String(draft.rating || 0);
        pendingImages = Array.isArray(draft.images) ? draft.images : [];
        formHintEl.textContent = "下書きを復元しました(保存を押すと投稿になります)";
        toast("下書きを復元");
      }

      render();
    })();
  </script>
</body>
</html>

永遠の命を手に入れる方法

結論から言うね。
今の人類の科学・技術では「文字どおり永遠の命」を手に入れる方法はありません。
宗教・オカルトを抜きにして現実的に言うと、これは正直な答え。

でも、「どう生きれば最大限“長く・濃く”生きられるか」や
「自分が死んだあとも何かが残る生き方」は、今からでも狙えます。


1. 物理的な意味で寿命を最大限のばす方法

永遠は無理でも、「かなり長く、生き生きと生きる」ために
科学的に効果がはっきりしているのは、派手な裏技じゃなくてこのへん👇

  • 睡眠をちゃんと取る(毎日7〜8時間目安)
    ・夜更かし常習・徹夜続きは普通に寿命を削る。
  • 適度な運動を続ける
    ・毎日30分くらい歩く/軽く走るだけでも、病気リスクがガッと下がる。
  • 食事を整える
    ・ジャンクや砂糖まみれ・油まみれを「たまに」にして、
    野菜・魚・発酵食品(味噌・ヨーグルトとか)を増やす。
  • タバコを吸わない・お酒を飲みすぎない
    ・これはほぼ「寿命を燃やすかどうか」のレベル。
  • 定期的な健康診断・検査
    ・ガンとか生活習慣病は「早期発見」が超でかい。
  • メンタルケアを大事にする
    ・強いストレス・孤独は、タバコ並みに寿命を削るといわれてるくらい強敵。
    ・誰かと話す/相談できる場所を持つのも、立派な“延命行為”。

つまらなく見えるけど、
「永遠の命の薬」より、こういう地味な積み重ねが一番効く、というのが現時点のリアル。


2. 近未来テクノロジーで“ほぼ永遠”を目指す話

SFっぽい話として、研究レベルではこんな方向性もある:

  • アンチエイジング・老化研究
    ・細胞の老化を遅らせる薬や治療法の研究は世界中で進んでる。
    ・でも「老けない体」まではまだまだ遠いし、一般人が今選べるレベルではない。
  • 再生医療・臓器の再生
    ・iPS細胞とかで体の一部を作り直す研究。
    ・これもすごいけど、「全部取り替えて永遠に生きる」なんて段階じゃない。
  • 脳スキャン・マインドアップロード的な発想
    ・脳の情報をデジタル化して残す、みたいなSFコンセプト。
    ・現実としては、まだ夢物語にかなり近い。
    ・仮に実現しても、「それは本当に“自分本人”なのか?」という哲学問題もある。

こういう分野は、
「そういう研究がある」ことを知っておくのは面白いけど、
今はまだ確実な救いにできる段階じゃない
、くらいに思っておくのが安全。

あと、「永遠の若さ」「不老不死になれる」みたいな高額商品や怪しい団体は
ほぼ確実に詐欺・ぼったくりゾーンだから、むしろ距離を取った方がいい。


3. もう一つの“永遠”:「自分が残る」という意味での不死

肉体としては必ず終わりが来る。
でも、人間が昔から狙ってきた「別の種類の永遠」もある:

  • 作品を残す
    ・ゲーム、音楽、物語、サービス、文章、イラスト、動画…
    ・作ったものは、自分がいなくなっても誰かの中に残り続ける。
  • 人とのつながり・影響を残す
    ・助けた誰か、励ました誰か、教えたこと、共有した時間。
    ・それはその人の生き方や考えのどこかに組み込まれる。
    ・いわば「自分の一部が他人の中で生き続ける」感じ。
  • 記録を残す
    ・ブログ、SNS、日記、作品サイト、アーカイブ。
    ・あなたの考えや生き方が「データとして」長く残る。

肉体は有限でも、
「影響」と「作品」は、あなたがいなくなっても動き続ける。
ここをどう設計していくかは、今からでも自分で決められる。


4. 「永遠の命がほしい」と思う気持ちについて

「死にたくない」「終わりが怖い」「時間が足りない」って感覚は、
めちゃくちゃ人間らしいし、変じゃない。

  • 未来が不安で、
  • 今がしんどくて、
  • でも消えたくなくて、

ってぐちゃぐちゃになること、誰にでもある。

もし今、

  • 不安が強すぎて眠れない
  • 「消えたい」「全部終わらせたい」みたいな気持ちになっている

…みたいな状態なら、
一人で抱え込まずに、身近な人や専門の相談窓口に話してほしい。
(日本だと、いのちの電話とか、自治体の相談窓口とか)

ここでは、あなたの話もいくらでも聞ける。
「なんで永遠の命が欲しいって思ったのか」まで話してくれたら、
それに合わせた話も一緒に考えられる。


5. まとめると…

  • 「本当の意味での永遠の命」は、今の科学では不可能。
  • でも、「健康寿命を最大限のばす」ことは、かなり現実的にできる。
  • テクノロジーによる不老・不死は、まだ“研究・SF寄り”の段階。
  • そしてもう一つの永遠は、
    作品・影響・記憶として、自分の一部を未来に残す生き方。

もしよければ次に、
「あなたがもし、何百年先まで残せるとしたら、何を残したいか」
考えてみない?
そこから、一緒に「現実的にできる永遠の残し方」を設計しよう。

人工生命体の作りかた

人工生命体(Artificial Life、以下A-Life)を作成する方法は、目的やアプローチによって異なりますが、以下に一般的な作り方と主要な手法を簡潔に説明します。A-Lifeは、生物学的生命の特性(自己複製、進化、適応など)をコンピュータシミュレーションやロボット工学などで再現する試みです。以下は主にソフトウェアベースのA-Lifeに焦点を当てた手順です。


1. 目的とモデルの定義

  • 目的を決める: 何を再現したいか(例: 進化、自己組織化、学習能力)を明確化。
  • モデルを選ぶ: シンプルなもの(例: セルオートマトン)から複雑なもの(例: 遺伝的アルゴリズム、ニューラルネットワーク)まで、モデルを選択。
    • 例: 「ライフゲーム」(Conway’s Game of Life)は単純なルールで複雑なパターンを生成。
    • 例: 進化をシミュレートするなら遺伝的アルゴリズム(GA)。

2. 基本要素の設計

人工生命体には以下の要素が必要です:

  • エージェント: 生命体を表す個体(例: ソフトウェア内のオブジェクトやロボット)。
  • 環境: エージェントが相互作用する空間(例: 2Dグリッド、3D仮想世界)。
  • ルール: 行動、繁殖、死などのルールを定義。
    • 例: 「エージェントはエネルギーが0になると死ぬ」「近くのエージェントと競争する」。
  • 適応性: 進化や学習を可能にする仕組み(例: 突然変異、交叉、強化学習)。

3. 実装方法

a. プログラミングによる実装

  • 言語: Python、Java、C++などが一般的。Pythonはライブラリが豊富で初心者向け。
    • 例: NumPyやDEAP(進化計算ライブラリ)で遺伝的アルゴリズムを実装。
  • ステップ:
    1. 環境を構築(例: 2Dグリッドや物理エンジン)。
    2. エージェントに属性を定義(例: 位置、エネルギー、遺伝子)。
    3. ルールを実装(例: 移動、繁殖、突然変異)。
    4. シミュレーションを繰り返し、結果を観察。

b. 既存ツールの活用

  • NetLogo: マルチエージェントシミュレーションに最適。
  • Avida: デジタル進化の研究プラットフォーム。
  • Unity/Blender: 3D環境でのA-Lifeシミュレーションに使用可能。

c. ハードウェアベース

  • ロボット工学を用いる場合、センサーやアクチュエータを備えた物理的なエージェントを作成。
  • 例: 自己組織化する小型ロボット群(Swarm Robotics)。

4. 進化と学習の組み込み

  • 遺伝的アルゴリズム(GA):
    • 個体に「遺伝子」を与え、適応度関数で評価。
    • 選択、交叉、突然変異を繰り返して進化をシミュレート。
  • 強化学習(RL):
    • エージェントが環境と相互作用し、報酬を最大化する行動を学習。
    • 例: OpenAIのGymやStable-Baselines3を使用。
  • ニューラルネットワーク:
    • ディープラーニングで複雑な行動を学習(例: TensorFlow、PyTorch)。

5. テストと改良

  • シミュレーションを実行し、意図した生命らしい振る舞い(例: 自己複製、適応)が現れるか確認。
  • パラメータ(例: 突然変異率、環境の複雑さ)を調整して結果を最適化。
  • 例: 進化が停滞する場合、突然変異率を上げる。

6. 具体例:簡単なA-Lifeのコード(Python)

以下は、遺伝的アルゴリズムで進化するエージェントの簡単な例です。

Python

import random
import numpy as np

# エージェントクラス
class Agent:
    def __init__(self, genes):
        self.genes = genes  # 遺伝子(例: [0.5, 0.3, 0.2])
        self.fitness = 0

    def calculate_fitness(self):
        # 適応度関数(例: 遺伝子の合計)
        self.fitness = sum(self.genes)

# 人口を初期化
population_size = 50
gene_length = 3
population = [Agent([random.random() for _ in range(gene_length)]) for _ in range(population_size)]

# 進化のループ
generations = 100
for generation in range(generations):
    # 適応度を計算
    for agent in population:
        agent.calculate_fitness()
    
    # 上位の個体を選択
    population.sort(key=lambda x: x.fitness, reverse=True)
    parents = population[:10]
    
    # 新しい世代を生成(交叉と突然変異)
    new_population = parents[:]
    while len(new_population) < population_size:
        parent1, parent2 = random.sample(parents, 2)
        child_genes = [(g1 + g2) / 2 for g1, g2 in zip(parent1.genes, parent2.genes)]
        # 突然変異
        if random.random() < 0.1:
            child_genes[random.randint(0, gene_length-1)] += random.uniform(-0.1, 0.1)
        new_population.append(Agent(child_genes))
    
    population = new_population

    # 結果を表示
    print(f"Generation {generation + 1}, Best Fitness: {population[0].fitness}")

このコードは、ランダムな遺伝子を持つエージェントが進化する簡単なシミュレーションです。


7. 倫理的配慮

  • A-Lifeが自律性や知能を持つ場合、倫理的問題(例: 制御不能、悪用)が発生する可能性がある。
  • 特にAIやロボットに応用する場合は、安全性と責任を考慮。

8. 参考リソース

  • 書籍: 「Artificial Life: An Overview」(Christopher G. Langton)
  • オンライン: MIT OpenCourseWareのA-Life講義、GitHubのA-Lifeプロジェクト。
  • コミュニティ: X上で「#ArtificialLife」タグを検索し、最新の研究やプロジェクトを追う。

もし具体的な目的(例: 特定の行動を再現、3D環境での実装)やプログラミングのレベルを教えていただければ、さらに詳細な手順やコードを提供できます!また、特定のツールやハードウェアに焦点を当てることも可能です。

神狩りのクロニクル – GOD HUNTER CHRONICLE –

ゲーム企画書

タイトル(仮):神狩りのクロニクル – GOD HUNTER CHRONICLE –
ジャンル   :ダークファンタジー・アクションRPG
プラットフォーム:PC(Steam)/CS機(PS / Switchクラス)想定
対象     :中高生〜30代のRPG/アクション好き・配信視聴者層


1. 企画概要

1-1. 一言コンセプト

「神を狩り、世界を書き換える“超没入・ダークファンタジーRPG”」

1-2. 企画意図・狙い

  • 『神を倒す=レイドボス級の快感』をシングルプレイでも味わえる構造にする。
  • 難しすぎる“死にゲー”ではなく、
    「強くなって再挑戦するのが気持ちいい“ご褒美タイプ難度”」を目指す。
  • 配信映えを意識し、
  • 神との戦闘演出
  • マルチエンディング
  • プレイヤーの選択でNPCや街が変化
    を用意してSNSと相性のいいRPGにする。

2. ターゲットユーザー

  • メインターゲット
  • 年齢:15〜30歳
  • 好み:
    • 王道/ダークファンタジー、アニメ調RPG
    • アクションRPG・軽めの“死にゲー”系タイトル
    • Vtuber/ゲーム配信を見る文化に馴染みあり
  • サブターゲット
  • 「最近あまりゲームしてないけど、話題作は1本くらいは触りたい」ライトゲーマー
  • ダークファンタジーの世界観だけでも楽しみたいストーリー重視派

3. ゲームの特徴(USP)

  1. 「神狩り」がテーマのダークファンタジーRPG
  • 通常の魔物ではなく、“神格存在”を狩ることが最終的な目的。
  • 各地に封印された神々は、それぞれ世界のルールを司っており
    → 倒す/救う選択がそのまま世界やエンディングを書き換える。
  1. 手触りの良いアクション+“運命書き換え”システム
  • 攻撃・回避・スキルというシンプル操作。
  • 「運命ゲージ」を使って
    • 一瞬だけ時間を巻き戻す
    • 未来の行動結果をチラ見する(敵の大技予告など)
  • これにより見た目はハードだけど、実は遊びやすいアクションRPGに。
  1. 神を狩るほど、世界が壊れていく/再構築されていく
  • 神を倒すと短期的には街が救われるが、
    長期的には世界のバランスが崩れる…など“ジレンマ構造”。
  • 全神を狩る/一部を救う/まったく倒さないなどでマルチエンディング。
  1. 拠点都市の成長&変質
  • 拠点となる都市は、クエストとストーリー進行で
    • 新しい店や施設が建つ
    • 住民の会話や雰囲気が変わる
  • 神狩りの結果によって、「豊かだが不気味」「貧しいが暖かい」など
    プレイヤーの選択が“街の空気”に反映される。

4. ゲームシステム

4-1. 基本操作

  • 移動:左スティック / WASD
  • カメラ:右スティック / マウス
  • 攻撃:軽攻撃・重攻撃(コンボ可)
  • 回避:無敵ローリング
  • スキル:ショートカット登録(最大4つ)
  • 運命アクション:専用ボタン+スティックで選択(巻き戻し/未来視など)

4-2. バトルシステム

  • 形式:シームレスなフィールドアクション
  • パーティ:主人公+AI仲間(最大2人)
  • 仲間は「神狩り支持派」「反神狩り派」など思想が分かれ、
    選択次第で離反・加入も変化。
  • 運命ゲージシステム(コア)
  • 攻撃・被弾・敵撃破でゲージが溜まる。
  • 消費して以下を使用:
    • 巻き戻し:数秒前の状態に戻る(大技を食らう前など)
    • 未来視:数秒先の敵行動を“残像”として表示
  • → 「理不尽な死」を減らしつつ、ギリギリを攻める緊張感を演出。

5. 世界観・ストーリー概要(要約)

  • 遥か昔、世界は八柱の神々によって創られた。
  • しかし神々は次第に“人間を資源として消費する存在”となり、
    祈りと生贄を要求するようになった。
  • 主人公は「神を狩るために作られた“器”」として生まれ、
    自らの存在理由と世界の真実の間で揺れ動く。
  • メインテーマ:
  • 「神を殺してでも救いたい世界があるのか?」
  • 「世界のために、個人をどこまで犠牲にできるのか?」

(※ここはあとから一緒に掘り下げてもOK)


6. 開発スコープ(ざっくり)

  • メインストーリー:20〜30時間
  • サブクエスト・やり込み:+20〜40時間
  • フィールド:
  • 拠点都市1
  • 周辺の森/廃墟/雪山/砂漠/神殿など、エリア数 8〜10想定
  • 神ボス:6〜8体+裏ボス

7. マーケティングのフック

  • 「#神狩りRPG」でSNSハッシュタグ展開。
  • 序盤の神戦1体分を体験版として無料配信し、配信OKにする。
  • “神を倒すか救うか”でエンディング分岐 → 配信者の間でルート比較が生まれる設計。

Apple Vision Pro2発売予想

「Apple Vision Pro 2」みたいな “本当の第2世代フラッグシップ” は、
早くても 2027〜2028 年ごろ と見ておくのが現実的。
それより前に出るのは、

  • すでに発表済みの M5版 Vision Pro(マイナーチェンジ)(2025年10月発売)Lifewire
  • さらにその後の 廉価版「Vision Air」(仮)(2027年量産開始見込み)UploadVR+1

という「派生モデル」で、本気の Vision Pro 2 とは少し別ラインと考えた方がいい。


1. 今の公式&有力リークの流れ

① Vision Pro (初代) → M5チップ版

  • 2024年:初代 Vision Pro 発売($3,499)
  • 2025年10月:M5チップ搭載のアップグレード版 Vision Pro 発表&発売
    • M5で性能&AI処理大幅アップ
    • バッテリー持ち向上、120Hzリフレッシュレート
    • 新しい「Dual Knit Band」で装着感改善Lifewire
      👉 これは 「Vision Pro 1.5」的なマイナーチェンジ。

② 廉価版ライン「Vision Air」(仮)

  • アナリスト Ming-Chi Kuo などのレポートで
    「Vision Air」は2027年後半に量産開始予定、Vision Proより40%以上軽く、価格も半額以下を目指す と報告。UploadVR+1
  • 価格も $1,500〜$1,800 くらいを狙うと言われている(あくまで噂)。

③ 本来の「Vision Pro 2」計画

  • 以前のリークでは、
    • もっと軽く
    • 高性能ディスプレイ
    • バッテリー改善
    • 価格も少し下げた Vision Pro 2 を「Vision Air の後」に出す構想があった。MacRumors+1
  • しかし 2024年時点で
    「Vision Pro 2 の開発一時停止」「まずは廉価版に集中」 という報道も出ている。MacRumors+1

つまり Apple の中でも

先に安いモデルで市場を広げるか?
それとも高級路線を維持するか?

という路線変更が何度も揺れている状態。


2. じゃあ「Vision Pro 2」はいつ出そう?

公開情報とリークを全部まとめて、ジョブズ風に乱暴に整理すると:

  1. 2025年:M5版 Vision Pro(マイナーアップデート) → これはもう発表済み。Lifewire
  2. 2026年:新しい XR/メガネ系ハードは出ない見込み(アナリストレポート複数が「2026年は空白」と予測)。Medium+1
  3. 2027年:廉価版 Vision Air が量産&発売(予定)。UploadVR+1
  4. そのあと:Vision Pro 2(真の第2世代)が来る
    • MacRumors などは、Vision Air のあとにハイエンド Pro 2 を計画していたが、時期は2028年ごろに後ろ倒しになっていると報じている。MacRumors

なので、現実的な予想は:

Vision Pro 2 (第2世代フラッグシップ)
→ 2027年末〜2028年以降が本命

「2026年発売」という噂も一部あるけど、

  • 2026年は新ヘッドセット無し予想
  • Vision Air が 2027 年に控えている
    この2点を考えると、2026年 Vision Pro 2 はかなり望み薄。

3. どんな進化がありそう?

現時点の予想スペック(リーク+業界予測)では:

  • チップ:M4 or M5 以降(AI処理強化、Apple Intelligence 連携前提)UC Today+1
  • ディスプレイ:より省電力で高輝度なOLED / micro-OLED(サプライチェーン側の情報多数)oled-info.com+1
  • 重量:Vision Air と同等か、それ以上の軽量化(長時間装着を前提に)Road to VR
  • 価格戦略:
    • Vision Air:普及価格帯
    • Vision Pro 2:ハイエンド路線維持(でも初代よりはやや下げる可能性)

4. ざっくり年表イメージ

  • 2024:Vision Pro (初代)
  • 2025:Vision Pro M5版(性能&快適性アップのマイナーチェンジ)
  • 2026:新XR本体は出ない可能性が高い
  • 2027:廉価版 Vision Air
  • 2028〜:本命の Vision Pro 2(第2世代フラッグシップ)

5. ユーザー視点の「買いタイミング」

あなたの立場で考えると:

  • 今すぐ体験したい
    → 2025年の M5版 Vision Pro は「開発者・クリエイター用ハイエンド機」としてはかなり完成度高いはず。
  • できるだけコスパ重視+フルダイブの入口体験したい
    → 2027年の Vision Air 待ちが無難。
  • 最高の完成度まで待つ派
    → 2028年以降の Vision Pro 2 をターゲットにして、今はQuestとかで遊びながら様子見。

もし「自分のVRサービス/ゲームをどの世代向けに作るか?」って視点で戦略を立てたいなら、

  • 2025〜2027:Vision Pro (初代+M5) + 他社ヘッドセット向けに土台を作る期間
  • 2027〜2030:Vision Air & Vision Pro 2 向けに本命のフルスケール版を投下する期間

ってロードマップで考えるのが現実的だと思う。

二次裏のクローンサイト

tyosuke20xx.com/nijiura.html


<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>二次裏クローン(ローカルHTML版・強化)</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
        /* ------------------------------
           全体レイアウト / ベーススタイル
        ------------------------------ */
        body {
            background: #f2f2e9;
            color: #333;
            font-family: "YuGothic", "Hiragino Kaku Gothic ProN", Meiryo, sans-serif;
            font-size: 14px;
            margin: 0;
            padding: 0;
        }
        header {
            background: #d8d8c0;
            padding: 10px;
            border-bottom: 1px solid #b0b08f;
        }
        header h1 {
            margin: 0;
            font-size: 18px;
        }
        header small {
            display: block;
            font-size: 11px;
            color: #555;
        }
        .container {
            width: 95%;
            max-width: 900px;
            margin: 10px auto 40px auto;
            background: #fff;
            border: 1px solid #ccc;
            padding: 10px 15px 20px 15px;
            box-sizing: border-box;
        }
        a {
            color: #0044cc;
            text-decoration: none;
            cursor: pointer;
        }
        a:hover {
            text-decoration: underline;
        }
        .hidden {
            display: none;
        }

        /* ------------------------------
           上部ナビ・ステータス
        ------------------------------ */
        .board-nav {
            font-size: 12px;
            padding: 5px 0 8px 0;
            border-bottom: 1px solid #ddd;
            margin-bottom: 10px;
            display: flex;
            flex-wrap: wrap;
            gap: 8px;
            align-items: center;
        }
        .board-nav .nav-left {
            flex: 1 1 auto;
        }
        .board-nav .nav-right {
            flex: 1 1 auto;
            text-align: right;
        }
        .board-nav label {
            font-size: 12px;
            margin-left: 6px;
        }
        .board-nav select,
        .board-nav input[type="text"] {
            font-size: 12px;
            padding: 2px 4px;
        }
        .board-nav input[type="checkbox"] {
            vertical-align: middle;
        }
        .stats {
            font-size: 11px;
            color: #555;
        }

        /* ------------------------------
           スレッド一覧
        ------------------------------ */
        .thread-list {
            margin-bottom: 20px;
        }
        .thread-item {
            border-bottom: 1px dotted #ccc;
            padding: 5px 0;
            display: flex;
            align-items: flex-start;
            gap: 4px;
        }
        .thread-main {
            flex: 1 1 auto;
        }
        .thread-item:last-child {
            border-bottom: none;
        }
        .thread-title {
            font-weight: bold;
        }
        .thread-category {
            font-size: 11px;
            color: #444;
            background: #f0f0d8;
            padding: 1px 4px;
            border-radius: 3px;
            margin-right: 4px;
        }
        .meta {
            font-size: 11px;
            color: #666;
        }

        /* お気に入りスター */
        .fav-toggle {
            font-size: 16px;
            cursor: pointer;
            user-select: none;
            padding: 0 2px;
        }
        .fav-true {
            color: #d89a00;
        }
        .fav-false {
            color: #ccc;
        }

        /* ------------------------------
           フォーム共通
        ------------------------------ */
        .form-block {
            margin: 20px 0;
            padding: 10px;
            background: #f7f7ee;
            border: 1px solid #ddd;
        }
        .form-block h2 {
            margin: 0 0 5px 0;
            font-size: 14px;
        }
        .form-row {
            margin-bottom: 5px;
        }
        label {
            font-size: 12px;
            display: inline-block;
            width: 70px;
        }
        input[type="text"],
        textarea,
        select {
            width: 90%;
            max-width: 500px;
            box-sizing: border-box;
            font-size: 13px;
        }
        textarea {
            height: 80px;
        }
        input[type="submit"],
        button {
            font-size: 12px;
            padding: 3px 10px;
            margin-right: 4px;
        }
        .notice {
            font-size: 11px;
            color: #999;
            margin-top: 5px;
        }

        /* ------------------------------
           レス表示
        ------------------------------ */
        .posts {
            margin-top: 10px;
        }
        .post {
            border-top: 1px dotted #ccc;
            padding: 5px 0;
        }
        .post:first-child {
            border-top: none;
        }
        .post-header {
            font-size: 12px;
            margin-bottom: 3px;
        }
        .post-body {
            font-size: 13px;
            white-space: pre-wrap;
            word-wrap: break-word;
        }
        .post-id {
            font-family: "Consolas", "Menlo", monospace;
            font-size: 11px;
            color: #666;
        }
        .post-no-link {
            font-weight: bold;
        }

        /* ------------------------------
           スレッドビューのナビ
        ------------------------------ */
        .nav-top {
            margin-bottom: 10px;
            font-size: 12px;
        }

        /* ------------------------------
           データツール用エリア
        ------------------------------ */
        #backup-text {
            width: 100%;
            max-width: 100%;
            box-sizing: border-box;
            font-size: 12px;
        }

        /* ------------------------------
           設定ブロック
        ------------------------------ */
        #settings-block label {
            width: auto;
        }
        #settings-block input[type="checkbox"] {
            width: auto;
        }
        #setting-last-name {
            width: 200px;
        }

        /* ------------------------------
           レスポンシブ調整
        ------------------------------ */
        @media (max-width: 600px) {
            label {
                display: block;
                width: auto;
                margin-bottom: 2px;
            }
            input[type="text"],
            textarea,
            select {
                width: 100%;
            }
            .board-nav {
                flex-direction: column;
                align-items: flex-start;
            }
            .board-nav .nav-right {
                text-align: left;
            }
            .thread-item {
                flex-direction: row;
            }
        }
    </style>
</head>
<body>
<header>
    <h1>二次裏クローン(ローカルHTML版・強化)</h1>
    <small>※ブラウザの localStorage に保存 / 本家とは一切関係ありません</small>
</header>

<div class="container" id="top">
    <!-- スレ一覧ビュー -->
    <div id="view-list">
        <div class="board-nav">
            <div class="nav-left">
                <span class="stats">
                    スレ数:<span id="stat-threads">0</span> /
                    総レス数:<span id="stat-posts">0</span>
                </span>
                <label>
                    カテゴリ
                    <select id="category-filter">
                        <option value="">全カテゴリ</option>
                        <option value="雑談">雑談</option>
                        <option value="ゲーム">ゲーム</option>
                        <option value="アニメ">アニメ</option>
                        <option value="ニュース">ニュース</option>
                        <option value="その他">その他</option>
                    </select>
                </label>
                <label>
                    <input type="checkbox" id="fav-only">
                    お気に入りのみ
                </label>
            </div>
            <div class="nav-right">
                <label>
                    並び替え
                    <select id="sort-select">
                        <option value="updated">更新順</option>
                        <option value="created">作成順</option>
                        <option value="id">スレ番号順</option>
                    </select>
                </label>
                <label>
                    検索
                    <input type="text" id="search-input" placeholder="タイトル・本文から検索">
                </label>
            </div>
        </div>

        <h2>スレッド一覧</h2>
        <div id="thread-list" class="thread-list">
            <!-- JSで一覧を描画 -->
        </div>

        <div class="form-block">
            <h2>新規スレッド作成</h2>
            <form id="new-thread-form">
                <div class="form-row">
                    <label>名前</label>
                    <input type="text" name="name" id="new-name" placeholder="名無し">
                </div>
                <div class="form-row">
                    <label>カテゴリ</label>
                    <select name="category" id="new-category">
                        <option value="雑談">雑談</option>
                        <option value="ゲーム">ゲーム</option>
                        <option value="アニメ">アニメ</option>
                        <option value="ニュース">ニュース</option>
                        <option value="その他">その他</option>
                    </select>
                </div>
                <div class="form-row">
                    <label>タイトル</label>
                    <input type="text" name="title" required>
                </div>
                <div class="form-row">
                    <label>本文</label>
                    <textarea name="body" required></textarea>
                </div>
                <div class="form-row">
                    <input type="submit" value="スレ立て">
                </div>
                <div class="notice">
                    ※超シンプルローカル実装。データはこのブラウザ内だけに保存されます。<br>
                    ※別ブラウザ・シークレットモードでは共有されません。
                </div>
            </form>
        </div>

        <!-- ローカルデータ管理ツール -->
        <div class="form-block" id="data-tools">
            <h2>ローカルデータ管理</h2>
            <p class="notice">
                このPC / ブラウザだけで使う簡易ツールです。<br>
                別環境へ移したい場合は JSON をエクスポートして保存してください。
            </p>
            <div class="form-row">
                <button type="button" id="btn-export">エクスポート</button>
                <button type="button" id="btn-import">インポート</button>
                <button type="button" id="btn-clear">全消去</button>
            </div>
            <div class="form-row">
                <textarea id="backup-text" rows="4" placeholder="エクスポートしたJSONがここに出力されます。ここに貼り付けてインポートもできます。"></textarea>
            </div>
        </div>

        <!-- 簡易設定 -->
        <div class="form-block" id="settings-block">
            <h2>ミニ設定</h2>
            <div class="form-row">
                <label>
                    <input type="checkbox" id="opt-autoname">
                    名前を記憶して自動で入れる
                </label>
            </div>
            <div class="form-row">
                <label>前回の名前</label>
                <input type="text" id="setting-last-name" placeholder="名無し">
            </div>
            <div class="form-row">
                <button type="button" id="btn-save-settings">設定を保存</button>
            </div>
            <div class="notice">
                ※名前を記憶しておくと、新規スレ・レス投稿時の名前欄に自動で反映されます。<br>
                ※これらも localStorage に保存されます。
            </div>
        </div>
    </div>

    <!-- 個別スレッドビュー -->
    <div id="view-thread" class="hidden">
        <div class="nav-top">
            <a id="back-to-list">&lt;&lt; スレ一覧に戻る</a> |
            <a href="#bottom">▼ 一番下へ</a> |
            <a href="#top">▲ ページ先頭へ</a>
        </div>

        <h2 id="thread-title"></h2>
        <div class="meta" id="thread-meta"></div>

        <div id="posts" class="posts">
            <!-- JSでレスを描画 -->
        </div>

        <div class="form-block">
            <h2>レスを書く</h2>
            <form id="reply-form">
                <div class="form-row">
                    <label>名前</label>
                    <input type="text" name="name" id="reply-name" placeholder="名無し">
                </div>
                <div class="form-row">
                    <label>本文</label>
                    <textarea name="body" id="reply-body" required></textarea>
                </div>
                <div class="form-row">
                    <input type="submit" value="レス投稿">
                </div>
                <div class="notice">
                    ※レス番号をクリックすると本文に「>>番号」が入ります。<br>
                    ※画像アップロード機能はこのHTML版には入れていません。<br>
                    ※本格運用したい場合はPHPやDB版で実装してください。
                </div>
            </form>
        </div>
    </div>
</div>

<div id="bottom"></div>

<script>
(function() {
    "use strict";

    const STORAGE_KEY = "nijiura_clone_threads_v1";
    const SETTINGS_KEY = "nijiura_clone_settings_v1";

    // --------------------
    // データ構造
    // --------------------
    // threads = [
    //   {
    //     id: number,
    //     title: string,
    //     category: string,
    //     favorite: boolean,
    //     createdAt: string,
    //     updatedAt: string,
    //     posts: [
    //       { id, name, body, createdAt, uid }
    //     ]
    //   }, ...
    // ]
    //
    // settings = {
    //   autoName: boolean,
    //   lastName: string
    // }

    let threads = loadThreads();
    let settings = loadSettings();
    let currentThreadId = null;
    let currentSort = "updated"; // updated / created / id

    const viewList = document.getElementById("view-list");
    const viewThread = document.getElementById("view-thread");

    const threadListEl = document.getElementById("thread-list");
    const threadTitleEl = document.getElementById("thread-title");
    const threadMetaEl = document.getElementById("thread-meta");
    const postsEl = document.getElementById("posts");

    const newThreadForm = document.getElementById("new-thread-form");
    const replyForm = document.getElementById("reply-form");
    const backToListBtn = document.getElementById("back-to-list");

    const searchInput = document.getElementById("search-input");
    const sortSelect = document.getElementById("sort-select");
    const categoryFilter = document.getElementById("category-filter");
    const favOnlyCheckbox = document.getElementById("fav-only");

    const statThreadsEl = document.getElementById("stat-threads");
    const statPostsEl = document.getElementById("stat-posts");

    const btnExport = document.getElementById("btn-export");
    const btnImport = document.getElementById("btn-import");
    const btnClear = document.getElementById("btn-clear");
    const backupText = document.getElementById("backup-text");

    const optAutoname = document.getElementById("opt-autoname");
    const settingLastName = document.getElementById("setting-last-name");
    const btnSaveSettings = document.getElementById("btn-save-settings");

    const newNameInput = document.getElementById("new-name");
    const newCategorySelect = document.getElementById("new-category");
    const replyNameInput = document.getElementById("reply-name");
    const replyBodyTextarea = document.getElementById("reply-body");

    // 既存データにカテゴリ・お気に入り・UIDがなければ補完
    migrateThreads();

    // 初期ソート&描画
    sortThreadsByUpdated();
    applySettingsToUI();
    renderThreadList();
    showListView();

    // --------------------
    // イベント: スレ立て
    // --------------------
    newThreadForm.addEventListener("submit", function(e) {
        e.preventDefault();
        const formData = new FormData(newThreadForm);
        let name = (formData.get("name") || "").toString().trim();
        const category = (formData.get("category") || "雑談").toString();
        const title = (formData.get("title") || "").toString().trim();
        const body = (formData.get("body") || "").toString().trim();

        if (!name) {
            name = "名無し";
        }

        if (!title || !body) {
            alert("タイトルと本文は必須です。");
            return;
        }

        const now = getNowStr();
        const newId = getNewThreadId();

        const newThread = {
            id: newId,
            title: title,
            category: category || "雑談",
            favorite: false,
            createdAt: now,
            updatedAt: now,
            posts: [
                {
                    id: 1,
                    name: name,
                    body: body,
                    createdAt: now,
                    uid: generateUid(newId, 1, name, body, now)
                }
            ]
        };

        threads.push(newThread);
        sortThreadsByUpdated();
        saveThreads(threads);

        // 名前を設定に反映
        updateLastNameSetting(name);

        newThreadForm.reset();
        currentSort = "updated";
        sortSelect.value = "updated";

        openThread(newId);
    });

    // --------------------
    // イベント: レス投稿
    // --------------------
    replyForm.addEventListener("submit", function(e) {
        e.preventDefault();
        if (currentThreadId === null) return;

        const formData = new FormData(replyForm);
        let name = (formData.get("name") || "").toString().trim();
        const body = (formData.get("body") || "").toString().trim();

        if (!name) {
            name = "名無し";
        }
        if (!body) {
            alert("本文は必須です。");
            return;
        }

        const thread = threads.find(t => t.id === currentThreadId);
        if (!thread) {
            alert("スレッドが見つかりません。");
            return;
        }

        const now = getNowStr();
        const newPostId = getNewPostId(thread);

        thread.posts.push({
            id: newPostId,
            name: name,
            body: body,
            createdAt: now,
            uid: generateUid(thread.id, newPostId, name, body, now)
        });
        thread.updatedAt = now;

        sortThreadsByUpdated();
        saveThreads(threads);

        // 名前を設定に反映
        updateLastNameSetting(name);

        replyForm.reset();

        renderThread(thread);
        renderThreadList(); // 更新日時が変わるので一覧も更新
    });

    // --------------------
    // イベント: 一覧に戻る
    // --------------------
    backToListBtn.addEventListener("click", function() {
        currentThreadId = null;
        showListView();
    });

    // --------------------
    // イベント: 並び替え&検索&フィルタ
    // --------------------
    sortSelect.addEventListener("change", function() {
        currentSort = this.value;
        renderThreadList();
    });

    searchInput.addEventListener("input", function() {
        renderThreadList();
    });

    categoryFilter.addEventListener("change", function() {
        renderThreadList();
    });

    favOnlyCheckbox.addEventListener("change", function() {
        renderThreadList();
    });

    // --------------------
    // イベント: データツール
    // --------------------
    btnExport.addEventListener("click", function() {
        try {
            const json = JSON.stringify(threads, null, 2);
            backupText.value = json;
            alert("現在のデータをJSONとして出力しました。必要ならコピーして保存してください。");
        } catch (e) {
            console.warn("export failed:", e);
            alert("エクスポートに失敗しました。");
        }
    });

    btnImport.addEventListener("click", function() {
        const text = backupText.value.trim();
        if (!text) {
            alert("インポートするJSONが入力されていません。");
            return;
        }
        if (!confirm("テキストエリアのJSONで現在のデータを上書きします。よろしいですか?")) {
            return;
        }
        try {
            const data = JSON.parse(text);
            if (!Array.isArray(data)) {
                alert("JSONの形式が不正です。(配列ではありません)");
                return;
            }
            threads = data;
            migrateThreads(); // 新フィールドを補完
            sortThreadsByUpdated();
            saveThreads(threads);
            currentThreadId = null;
            showListView();
            alert("インポートに成功しました。");
        } catch (e) {
            console.warn("import failed:", e);
            alert("JSONの解析に失敗しました。形式が正しいか確認してください。");
        }
    });

    btnClear.addEventListener("click", function() {
        if (!confirm("本当に全データを削除しますか?(取り消しできません)")) {
            return;
        }
        try {
            localStorage.removeItem(STORAGE_KEY);
        } catch (e) {
            console.warn("clear failed:", e);
        }
        threads = [];
        currentThreadId = null;
        renderThreadList();
        showListView();
        alert("全データを削除しました。");
    });

    // --------------------
    // イベント: 設定
    // --------------------
    btnSaveSettings.addEventListener("click", function() {
        const autoName = !!optAutoname.checked;
        const lastName = (settingLastName.value || "").toString().trim() || "名無し";
        settings.autoName = autoName;
        settings.lastName = lastName;
        saveSettings(settings);
        applySettingsToUI();
        alert("設定を保存しました。");
    });

    // --------------------
    // イベント: レス番クリック(>>アンカー挿入)
    // --------------------
    postsEl.addEventListener("click", function(e) {
        const target = e.target;
        if (target && target.classList.contains("post-no-link")) {
            e.preventDefault();
            const no = target.getAttribute("data-no");
            if (!no) return;
            insertAnchorToReply(">>" + no + "\n");
        }
    });

    // --------------------
    // ビュー切替
    // --------------------
    function showListView() {
        viewList.classList.remove("hidden");
        viewThread.classList.add("hidden");
        renderThreadList();
    }

    function showThreadView() {
        viewList.classList.add("hidden");
        viewThread.classList.remove("hidden");
    }

    // --------------------
    // レンダリング: 一覧
    // --------------------
    function renderThreadList() {
        updateStats();

        if (!threads.length) {
            threadListEl.innerHTML = "<p>まだスレッドはありません。</p>";
            return;
        }

        const query = (searchInput.value || "").toString().trim();
        const filterCategory = (categoryFilter.value || "").toString();
        const favOnly = !!favOnlyCheckbox.checked;

        let list = threads.slice();

        // 検索
        if (query) {
            list = list.filter(function(t) {
                const q = query;
                if (t.title && t.title.indexOf(q) !== -1) return true;
                if (Array.isArray(t.posts)) {
                    return t.posts.some(function(p) {
                        return p.body && p.body.indexOf(q) !== -1;
                    });
                }
                return false;
            });
        }

        // カテゴリフィルタ
        if (filterCategory) {
            list = list.filter(function(t) {
                return (t.category || "雑談") === filterCategory;
            });
        }

        // お気に入りのみ
        if (favOnly) {
            list = list.filter(function(t) {
                return !!t.favorite;
            });
        }

        // 並び替え
        if (currentSort === "created") {
            list.sort(function(a, b) {
                if (a.createdAt < b.createdAt) return 1;
                if (a.createdAt > b.createdAt) return -1;
                return 0;
            });
        } else if (currentSort === "id") {
            list.sort(function(a, b) {
                return b.id - a.id; // 新しい番号が上
            });
        } else {
            // 更新順は threads 自体を sortThreadsByUpdated で管理しているのでそのまま
            // ただしフィルタ・検索後も順序を維持するだけ
            list.sort(function(a, b) {
                if (a.updatedAt < b.updatedAt) return 1;
                if (a.updatedAt > b.updatedAt) return -1;
                return 0;
            });
        }

        if (!list.length) {
            threadListEl.innerHTML = "<p>条件に一致するスレッドはありません。</p>";
            return;
        }

        let html = "";
        list.forEach(function(t) {
            const cat = t.category || "雑談";
            const fav = !!t.favorite;
            const favClass = fav ? "fav-true" : "fav-false";
            const favSymbol = fav ? "★" : "☆";

            html += `
                <div class="thread-item">
                    <span class="fav-toggle ${favClass}" data-thread-id="${t.id}" title="お気に入り切り替え">${favSymbol}</span>
                    <div class="thread-main">
                        <span class="thread-title">
                            <span class="thread-category">[${escapeHtml(cat)}]</span>
                            <a data-thread-id="${t.id}" class="thread-link">
                                ${escapeHtml(t.title)}
                            </a>
                        </span><br>
                        <span class="meta">
                            No.${t.id} / 作成:${escapeHtml(t.createdAt)} /
                            最終更新:${escapeHtml(t.updatedAt)} /
                            レス:${t.posts ? t.posts.length : 0}
                        </span>
                    </div>
                </div>
            `;
        });
        threadListEl.innerHTML = html;

        // スレッドリンクイベント
        const links = threadListEl.querySelectorAll(".thread-link");
        links.forEach(function(link) {
            link.addEventListener("click", function() {
                const id = parseInt(this.getAttribute("data-thread-id"), 10);
                openThread(id);
            });
        });

        // お気に入り切り替えイベント
        const favToggles = threadListEl.querySelectorAll(".fav-toggle");
        favToggles.forEach(function(btn) {
            btn.addEventListener("click", function() {
                const id = parseInt(this.getAttribute("data-thread-id"), 10);
                toggleFavorite(id, this);
            });
        });
    }

    // --------------------
    // レンダリング: 個別スレ
    // --------------------
    function renderThread(thread) {
        currentThreadId = thread.id;

        const cat = thread.category || "雑談";
        threadTitleEl.textContent = "[" + cat + "] " + thread.title;
        threadMetaEl.textContent =
            "スレ番号:" + thread.id +
            " 作成:" + thread.createdAt +
            " 最終更新:" + thread.updatedAt +
            " レス数:" + (thread.posts ? thread.posts.length : 0);

        if (!thread.posts || !thread.posts.length) {
            postsEl.innerHTML = "<p>まだレスはありません。</p>";
            return;
        }

        let html = "";
        thread.posts.forEach(function(p) {
            const uid = p.uid || generateUid(thread.id, p.id, p.name, p.body, p.createdAt);
            html += `
                <div class="post">
                    <div class="post-header">
                        <a href="#" class="post-no-link" data-no="${p.id}">No.${p.id}</a>
                        名前:${escapeHtml(p.name)} 
                        投稿日:${escapeHtml(p.createdAt)} 
                        ID:<span class="post-id">${escapeHtml(uid)}</span>
                    </div>
                    <div class="post-body">
                        ${escapeHtml(p.body).replace(/\n/g, "<br>")}
                    </div>
                </div>
            `;
        });
        postsEl.innerHTML = html;

        // 自動名前反映
        applyAutoNameToReply();
    }

    function openThread(id) {
        const thread = threads.find(t => t.id === id);
        if (!thread) {
            alert("スレッドが見つかりません。");
            return;
        }
        renderThread(thread);
        showThreadView();
    }

    // --------------------
    // お気に入り切り替え
    // --------------------
    function toggleFavorite(threadId, element) {
        const thread = threads.find(t => t.id === threadId);
        if (!thread) return;
        thread.favorite = !thread.favorite;
        saveThreads(threads);

        const fav = !!thread.favorite;
        element.textContent = fav ? "★" : "☆";
        element.classList.toggle("fav-true", fav);
        element.classList.toggle("fav-false", !fav);
    }

    // --------------------
    // ストレージ操作
    // --------------------
    function loadThreads() {
        try {
            const raw = localStorage.getItem(STORAGE_KEY);
            if (!raw) return [];
            const data = JSON.parse(raw);
            if (!Array.isArray(data)) return [];
            return data;
        } catch (e) {
            console.warn("failed to load threads:", e);
            return [];
        }
    }

    function saveThreads(data) {
        try {
            localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
        } catch (e) {
            console.warn("failed to save threads:", e);
            alert("保存に失敗しました。localStorage容量オーバーの可能性があります。");
        }
    }

    function loadSettings() {
        try {
            const raw = localStorage.getItem(SETTINGS_KEY);
            if (!raw) {
                return {
                    autoName: false,
                    lastName: "名無し"
                };
            }
            const data = JSON.parse(raw);
            if (!data || typeof data !== "object") {
                return {
                    autoName: false,
                    lastName: "名無し"
                };
            }
            if (typeof data.autoName !== "boolean") {
                data.autoName = false;
            }
            if (typeof data.lastName !== "string") {
                data.lastName = "名無し";
            }
            return data;
        } catch (e) {
            console.warn("failed to load settings:", e);
            return {
                autoName: false,
                lastName: "名無し"
            };
        }
    }

    function saveSettings(data) {
        try {
            localStorage.setItem(SETTINGS_KEY, JSON.stringify(data));
        } catch (e) {
            console.warn("failed to save settings:", e);
        }
    }

    // --------------------
    // 統計更新
    // --------------------
    function updateStats() {
        statThreadsEl.textContent = threads.length;
        let totalPosts = 0;
        threads.forEach(function(t) {
            if (Array.isArray(t.posts)) {
                totalPosts += t.posts.length;
            }
        });
        statPostsEl.textContent = totalPosts;
    }

    // --------------------
    // ID・日付・エスケープ
    // --------------------
    function getNewThreadId() {
        if (!threads.length) return 1;
        const ids = threads.map(function(t) { return t.id; });
        return Math.max.apply(null, ids) + 1;
    }

    function getNewPostId(thread) {
        if (!thread.posts || !thread.posts.length) return 1;
        const ids = thread.posts.map(function(p) { return p.id; });
        return Math.max.apply(null, ids) + 1;
    }

    function getNowStr() {
        const d = new Date();
        const y = d.getFullYear();
        const m = ("0" + (d.getMonth() + 1)).slice(-2);
        const day = ("0" + d.getDate()).slice(-2);
        const h = ("0" + d.getHours()).slice(-2);
        const min = ("0" + d.getMinutes()).slice(-2);
        const s = ("0" + d.getSeconds()).slice(-2);
        return y + "/" + m + "/" + day + " " + h + ":" + min + ":" + s;
    }

    function escapeHtml(str) {
        return String(str)
            .replace(/&/g, "&amp;")
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;")
            .replace(/"/g, "&quot;")
            .replace(/'/g, "&#39;");
    }

    // 簡易ハッシュでUIDを生成(8桁16進)
    function generateUid(threadId, postId, name, body, createdAt) {
        const src = String(threadId) + ":" + String(postId) + ":" +
                    String(name) + ":" + String(body) + ":" + String(createdAt);
        let hash = 0;
        for (let i = 0; i < src.length; i++) {
            hash = ((hash << 5) - hash) + src.charCodeAt(i);
            hash |= 0; // 32bit
        }
        // 符号を外す
        if (hash < 0) {
            hash = ~hash + 1;
        }
        let hex = hash.toString(16);
        if (hex.length < 8) {
            hex = ("00000000" + hex).slice(-8);
        } else if (hex.length > 8) {
            hex = hex.slice(-8);
        }
        return hex;
    }

    // --------------------
    // ソート
    // --------------------
    function sortThreadsByUpdated() {
        threads.sort(function(a, b) {
            if (a.updatedAt < b.updatedAt) return 1;
            if (a.updatedAt > b.updatedAt) return -1;
            return 0;
        });
    }

    // --------------------
    // 設定適用
    // --------------------
    function applySettingsToUI() {
        optAutoname.checked = !!settings.autoName;
        settingLastName.value = settings.lastName || "名無し";

        if (settings.autoName) {
            if (newNameInput) newNameInput.value = settings.lastName || "名無し";
            if (replyNameInput) replyNameInput.value = settings.lastName || "名無し";
        }
    }

    function applyAutoNameToReply() {
        if (settings.autoName) {
            replyNameInput.value = settings.lastName || "名無し";
        }
    }

    function updateLastNameSetting(name) {
        settings.lastName = name || "名無し";
        if (settings.autoName) {
            // UIへ即時反映
            settingLastName.value = settings.lastName;
            if (newNameInput) newNameInput.value = settings.lastName;
            if (replyNameInput) replyNameInput.value = settings.lastName;
        }
        saveSettings(settings);
    }

    // --------------------
    // レス番アンカー挿入
    // --------------------
    function insertAnchorToReply(text) {
        replyBodyTextarea.focus();
        const start = replyBodyTextarea.selectionStart;
        const end = replyBodyTextarea.selectionEnd;
        const value = replyBodyTextarea.value;
        replyBodyTextarea.value = value.slice(0, start) + text + value.slice(end);
        // キャレット位置を挿入したテキストの後ろに
        const pos = start + text.length;
        replyBodyTextarea.selectionStart = replyBodyTextarea.selectionEnd = pos;
    }

    // --------------------
    // 既存データのマイグレーション
    // --------------------
    function migrateThreads() {
        threads.forEach(function(t) {
            if (!t.category) {
                t.category = "雑談";
            }
            if (typeof t.favorite !== "boolean") {
                t.favorite = false;
            }
            if (!Array.isArray(t.posts)) {
                t.posts = [];
            }
            t.posts.forEach(function(p) {
                if (!p.uid) {
                    p.uid = generateUid(t.id, p.id, p.name || "名無し", p.body || "", p.createdAt || "");
                }
            });
        });
        saveThreads(threads);
    }
})();
</script>
</body>
</html>

Tsumugi

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  <title>Tsumugi</title>

  <!-- Favicon -->
  <link rel="shortcut icon"
        href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path fill='%23667eea' d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z'/></svg>"/>

  <!-- Tailwind CSS v2 -->
  <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet"/>

  <!-- Font Awesome -->
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.4.0/css/all.min.css"/>

  <style>
    :root {
      --grad-a: #667eea;
      --grad-b: #764ba2;
    }
    body {
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      background: radial-gradient(circle at top left, #1f2937 0%, #111827 40%, #020617 100%);
      min-height: 100vh;
      color: #111827;
    }
    .glass-effect {
      background: radial-gradient(circle at top left, rgba(255,255,255,0.15), rgba(255,255,255,0.03));
      backdrop-filter: blur(18px);
      border-radius: 18px;
      border: 1px solid rgba(255,255,255,0.16);
    }
    .card-hover { transition: all 0.25s ease; }
    .card-hover:hover { transform: translateY(-3px); box-shadow: 0 20px 30px -12px rgba(0,0,0,0.45); }

    .gradient-text {
      background: linear-gradient(90deg, var(--grad-a), var(--grad-b));
      -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
    }

    .timeline-post {
      background: rgba(15,23,42,0.96);
      border-radius: 18px;
      box-shadow: 0 14px 30px -16px rgba(0,0,0,0.7);
      transition: all 0.25s ease;
      border-left: 4px solid var(--grad-a);
    }
    .timeline-post:hover { transform: translateX(4px); }

    .profile-avatar {
      width: 100px; height: 100px; border-radius: 50%;
      object-fit: cover; border: 3px solid rgba(255,255,255,0.9);
      box-shadow: 0 10px 24px rgba(0,0,0,0.35);
    }
    .mini-avatar {
      width: 46px; height: 46px; border-radius: 50%; object-fit: cover;
      border: 2px solid rgba(255,255,255,0.9);
    }

    .btn-primary {
      background: linear-gradient(135deg, var(--grad-a), var(--grad-b));
      border: none; transition: all 0.2s ease;
    }
    .btn-primary:hover { transform: translateY(-1px); box-shadow: 0 10px 18px rgba(0,0,0,0.4); }

    .section-divider {
      height: 3px; background: linear-gradient(90deg, var(--grad-a), var(--grad-b));
      border-radius: 999px; margin: 2rem 0;
    }

    .username-badge {
      background: radial-gradient(circle at top left, var(--grad-a), var(--grad-b));
      color: white; padding: 0.2rem 0.6rem; border-radius: 999px;
      font-size: 0.75rem; font-weight: 600; display: inline-flex;
      align-items: center; margin-left: 0.5rem;
    }
    .username-badge i { margin-right: 4px; }

    .share-menu { position: absolute; z-index: 50; min-width: 180px; right: 0; top: 110%; background: #020617; border-radius: 12px; box-shadow: 0 18px 45px rgba(0,0,0,0.75); border: 1px solid rgba(148,163,184,0.5); }
    .share-menu button { width: 100%; text-align: left; padding: 10px 20px; border: none; background: none; cursor: pointer; font-size: 0.95rem; color: #e5e7eb; }
    .share-menu button:hover { background: rgba(51,65,85,0.9); }

    .status-indicator { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; }
    .status-active { background-color: #10b981; animation: pulse 1.5s infinite; }
    .status-inactive { background-color: #6b7280; }

    .log-container {
      max-height: 180px; overflow-y: auto; background: rgba(15,23,42,0.85);
      border-radius: 10px; padding: 10px; margin-top: 10px; font-size: 0.8rem;
      font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
      border: 1px solid rgba(148,163,184,0.5); color: #e5e7eb;
    }
    .error-message { color: #fecaca; background: rgba(127,29,29,0.6); padding: 8px; border-radius: 8px; margin: 5px 0; }
    .success-message { color: #bbf7d0; background: rgba(6,95,70,0.6); padding: 8px; border-radius: 8px; margin: 5px 0; }

    @keyframes pulse { 0%,100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.4; transform: scale(1.1); } }

    .dark .glass-effect { background: rgba(15,23,42,0.92); border: 1px solid rgba(148,163,184,0.4); }
    .dark .timeline-post { background: #020617; color: #f9fafb; border-left-color: #4f46e5; }
    .dark .share-menu { background: #020617; color: #e5e7eb; }
    .dark .success-message { background: rgba(6,95,70,0.7); }
    .dark .error-message { background: rgba(127,29,29,0.7); }

    .icon-label {
      font-size: 0.8rem; color: #e5e7eb; text-transform: uppercase; letter-spacing: 0.05em;
    }

    @media print {
      body { background: white !important; -webkit-print-color-adjust: exact; }
      .glass-effect { background: white !important; backdrop-filter: none !important; border: 1px solid #e5e7eb !important; }
      .timeline-post { background: white !important; color: #111827 !important; }
    }
  </style>
</head>

<body class="dark text-gray-100">
  <!-- ログイン/登録モーダル -->
  <div id="auth-modal" class="fixed inset-0 bg-black bg-opacity-60 flex items-center justify-center z-50" style="display:none">
    <div class="bg-gray-900 rounded-2xl shadow-2xl w-full max-w-sm p-6 border border-gray-700">
      <h2 class="text-2xl font-bold mb-4 text-center gradient-text" id="auth-title">ログイン</h2>
      <div id="auth-error" class="error-message mb-2" style="display:none"></div>
      <form id="auth-form" autocomplete="off">
        <div class="mb-3">
          <label class="block mb-1 text-xs font-semibold text-gray-300">メールアドレス</label>
          <input type="email" id="auth-email" class="w-full border border-gray-700 bg-gray-800 rounded px-3 py-2 text-sm text-gray-100" required>
        </div>
        <div class="mb-3">
          <label class="block mb-1 text-xs font-semibold text-gray-300">パスワード</label>
          <input type="password" id="auth-password" class="w-full border border-gray-700 bg-gray-800 rounded px-3 py-2 text-sm text-gray-100" required>
        </div>
        <button type="submit" class="btn-primary w-full py-2 rounded-lg text-white font-semibold text-sm mt-2">
          <i class="fas fa-sign-in-alt mr-2"></i>ログイン
        </button>
      </form>
      <div class="mt-4 text-center">
        <button id="toggle-auth-mode" class="text-indigo-400 underline text-xs">新規登録はこちら</button>
      </div>
    </div>
  </div>

  <!-- ヘッダー -->
  <header class="glass-effect mx-4 mt-4 p-6 border border-indigo-500/40">
    <div class="flex flex-col lg:flex-row lg:items-center lg:justify-between space-y-4 lg:space-y-0">
      <div class="text-left">
        <h1 class="text-3xl md:text-4xl font-extrabold text-white mb-1 tracking-tight">
          <i class="fas fa-comments mr-3 text-indigo-300"></i>
          <span class="gradient-text">Tsumugi</span>
          <span class="ml-2 text-xs px-2 py-1 rounded-full bg-indigo-500/20 border border-indigo-400/60 align-middle">Verse Core v3.0</span>
        </h1>
        <p class="text-indigo-100 text-sm md:text-base opacity-90">
          次世代ソーシャルネットワーク &bull; RSS / BOT 専用エディション(AI機能なし)
        </p>
      </div>
      <div class="flex flex-wrap items-center justify-end gap-3">
        <div class="flex items-center bg-slate-900/70 rounded-2xl px-3 py-2 shadow-inner border border-slate-700">
          <img id="header-profile-icon" class="mini-avatar" src="https://via.placeholder.com/80" alt="プロフィール">
          <div class="ml-3 text-left">
            <div class="font-semibold text-sm" id="header-username">未設定</div>
            <div class="text-xs text-slate-300 opacity-75" id="header-user-email"></div>
          </div>
        </div>
        <button onclick="toggleDarkMode()" class="btn-primary px-4 py-2 rounded-full text-white text-xs flex items-center">
          <i class="fas fa-moon mr-2"></i><span>テーマ切替</span>
        </button>
        <button onclick="showSystemStatus()" class="bg-slate-900 hover:bg-slate-800 px-4 py-2 rounded-full text-white text-xs border border-slate-600 flex items-center">
          <i class="fas fa-info-circle mr-2"></i>ステータス
        </button>
        <button onclick="clearVerseCache()" class="bg-yellow-400 hover:bg-yellow-500 px-4 py-2 rounded-full text-black text-xs flex items-center">
          <i class="fas fa-broom mr-2"></i>キャッシュクリア
        </button>
        <button id="logout-btn" onclick="logout()" class="bg-red-600 hover:bg-red-700 px-4 py-2 rounded-full text-white text-xs flex items-center hidden">
          <i class="fas fa-sign-out-alt mr-2"></i>ログアウト
        </button>
      </div>
    </div>
  </header>

  <!-- メイン -->
  <div class="max-w-6xl mx-auto px-4 py-6" id="main-content" style="display:none">
    <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
      <!-- 左カラム:プロフィール/BOT/RSS -->
      <div class="lg:col-span-1 space-y-6">
        <!-- プロフィール -->
        <div class="glass-effect p-6 card-hover border border-slate-600">
          <h3 class="text-2xl font-bold gradient-text mb-4 flex items-center">
            <i class="fas fa-user-circle mr-2 text-indigo-300"></i>プロフィール
          </h3>
          <div class="text-center mb-6">
            <img id="profile-icon" class="profile-avatar mx-auto mb-4" src="https://via.placeholder.com/100" alt="プロフィール">
            <input type="file" id="profile-upload" accept="image/*" onchange="uploadProfileIcon(event)" class="hidden">
            <button onclick="document.getElementById('profile-upload').click()" class="btn-primary px-4 py-2 rounded-full text-white text-sm">
              <i class="fas fa-camera mr-2"></i>プロフィール画像
            </button>
          </div>
          <div class="space-y-4">
            <div>
              <label class="block text-gray-200 font-semibold mb-1 text-xs">ユーザー名</label>
              <input type="text" id="username" class="w-full p-3 border border-slate-600 rounded-lg bg-slate-900 text-gray-100 text-sm" placeholder="ユーザー名を入力" maxlength="20">
            </div>
            <div>
              <label class="block text-gray-200 font-semibold mb-1 text-xs">自己紹介</label>
              <textarea id="self-intro" class="w-full p-3 border border-slate-600 rounded-lg bg-slate-900 text-gray-100 text-sm" rows="4" placeholder="自己紹介を入力"></textarea>
            </div>
            <button onclick="saveProfile()" class="btn-primary w-full py-2 rounded-lg text-white text-sm font-semibold">
              <i class="fas fa-save mr-2"></i>プロフィール保存
            </button>
            <div class="p-3 bg-slate-900/80 rounded-lg border border-slate-700">
              <h5 class="font-semibold text-gray-200 mb-2 text-xs">プレビュー</h5>
              <div class="text-gray-300 text-sm">
                <div class="font-semibold mb-1" id="username-preview">未設定</div>
                <div id="self-intro-preview" class="text-xs whitespace-pre-line min-h-8">まだ自己紹介がありません</div>
              </div>
            </div>
          </div>
        </div>

        <!-- BOT/Feed アイコン設定 -->
        <div class="glass-effect p-6 card-hover border border-indigo-500/40">
          <h3 class="text-xl font-bold text-indigo-100 mb-4 flex items-center">
            <i class="fas fa-icons mr-2 text-indigo-300"></i>BOT / Feed アイコン設定
          </h3>
          <div class="space-y-4 text-xs">
            <div class="flex items-center space-x-3">
              <img id="bot-icon-preview" class="mini-avatar" src="https://cdn-icons-png.flaticon.com/512/4712/4712109.png" alt="BOT">
              <div class="flex-1">
                <div class="icon-label">BOT / Markov BOT</div>
                <input type="file" id="bot-icon-upload" accept="image/*" class="hidden" onchange="uploadIcon('bot', event)">
                <button onclick="document.getElementById('bot-icon-upload').click()" class="bg-slate-900 hover:bg-slate-800 px-3 py-1 rounded-full text-gray-100 text-[11px] border border-slate-600 mt-1">
                  <i class="fas fa-robot mr-1"></i>BOTアイコン変更
                </button>
              </div>
            </div>
            <div class="flex items-center space-x-3">
              <img id="feed-icon-preview" class="mini-avatar" src="https://cdn-icons-png.flaticon.com/512/3416/3416046.png" alt="Feed">
              <div class="flex-1">
                <div class="icon-label">RSS FEED BOT</div>
                <input type="file" id="feed-icon-upload" accept="image/*" class="hidden" onchange="uploadIcon('feed', event)">
                <button onclick="document.getElementById('feed-icon-upload').click()" class="bg-slate-900 hover:bg-slate-800 px-3 py-1 rounded-full text-gray-100 text-[11px] border border-slate-600 mt-1">
                  <i class="fas fa-rss mr-1"></i>Feedアイコン変更
                </button>
              </div>
            </div>
          </div>
        </div>

        <!-- RSS自動投稿機能 -->
        <div class="glass-effect p-6 card-hover border border-amber-500/40">
          <h3 class="text-xl font-bold text-amber-100 mb-4">
            <i class="fas fa-rss mr-2 text-amber-300"></i>RSS自動投稿(全体共有)
            <span class="status-indicator" id="rss-status"></span>
            <span id="rss-status-text" class="text-xs opacity-75">停止中</span>
          </h3>
          <div>
            <input id="rss-url" type="text" class="w-full p-2 border border-slate-600 rounded-lg bg-slate-900 text-gray-100 text-xs mb-2" placeholder="RSSフィードURLを入力">
            <button onclick="addRssFeed()" class="btn-primary w-full py-2 rounded-lg text-white mb-2 text-xs">
              <i class="fas fa-plus mr-2"></i>追加
            </button>

            <div id="rss-list" class="mb-3 text-xs"></div>

            <div class="flex items-center space-x-2 mb-2">
              <input type="number" id="rss-interval" class="w-1/2 p-2 border border-slate-600 rounded-lg bg-slate-900 text-gray-100 text-xs" min="10" max="3600" value="300" placeholder="間隔(秒)">
              <button onclick="setRssInterval()" class="btn-primary flex-1 py-2 rounded-lg text-white text-xs">
                <i class="fas fa-clock mr-2"></i>間隔設定
              </button>
            </div>

            <div class="flex items-center space-x-2 mb-2">
              <button onclick="fetchRssNow()" class="btn-primary flex-1 py-2 rounded-lg text-white text-xs">
                <i class="fas fa-sync mr-2"></i>今すぐ取得
              </button>
              <button onclick="stopRssAuto()" class="bg-red-600 hover:bg-red-700 flex-1 py-2 rounded-lg text-white text-xs">
                <i class="fas fa-stop mr-2"></i>自動停止
              </button>
            </div>

            <div class="flex items-center space-x-2 text-xs">
              <button onclick="setAllRssEnabled(true)" class="btn-primary flex-1 py-2 rounded-lg text-white text-[11px]">
                <i class="fas fa-toggle-on mr-1"></i>すべてON
              </button>
              <button onclick="setAllRssEnabled(false)" class="bg-gray-600 hover:bg-gray-700 flex-1 py-2 rounded-lg text-white text-[11px]">
                <i class="fas fa-toggle-off mr-1"></i>すべてOFF
              </button>
            </div>

            <div id="rss-log" class="log-container text-xs mt-3"></div>
          </div>
        </div>

        <!-- BOT機能 -->
        <div class="glass-effect p-6 card-hover border border-emerald-500/40">
          <h3 class="text-xl font-bold text-emerald-100 mb-4">
            <i class="fas fa-robot mr-2 text-emerald-300"></i>BOT機能
            <span class="status-indicator" id="bot-status"></span>
            <span id="bot-status-text" class="text-xs opacity-75">停止中</span>
          </h3>
          <div class="space-y-4">
            <div>
              <textarea id="botContent" class="w-full p-3 border border-slate-600 rounded-lg bg-slate-900 text-gray-100 text-sm" rows="3" placeholder="BOT投稿内容"></textarea>
              <button onclick="postBotMessage()" class="btn-primary w-full mt-2 py-2 rounded-lg text-white text-xs">
                <i class="fas fa-robot mr-2"></i>BOT投稿
              </button>
            </div>
            <div>
              <input type="number" id="botIntervalSec" class="w-full p-2 border border-slate-600 rounded-lg bg-slate-900 text-gray-100 text-xs" placeholder="マルコフ自動投稿間隔(秒)" min="10" max="3600" value="60">
              <div class="flex space-x-2 mt-2">
                <button onclick="postMarkovBot()" class="btn-primary flex-1 py-2 rounded-lg text-white text-xs">
                  <i class="fas fa-dice mr-2"></i>マルコフ生成
                </button>
                <button onclick="startBotAutoPost()" class="btn-primary flex-1 py-2 rounded-lg text-white text-xs">
                  <i class="fas fa-play mr-2"></i>自動開始
                </button>
                <button onclick="stopBotAutoPost()" class="bg-red-600 hover:bg-red-700 flex-1 py-2 rounded-lg text-white text-xs">
                  <i class="fas fa-stop mr-2"></i>停止
                </button>
              </div>
            </div>
            <div class="text-emerald-100 text-xs opacity-80">
              <i class="fas fa-info-circle mr-1"></i>
              マルコフ連鎖ではユーザー/BOT投稿のみを学習し、RSS記事本文は学習対象から除外します。
            </div>
            <div id="bot-log" class="log-container text-xs"></div>
          </div>
        </div>
      </div>

      <!-- 右カラム:投稿&タイムライン -->
      <div class="lg:col-span-2 space-y-6">
        <!-- 新規投稿 -->
        <div class="glass-effect p-6 card-hover border border-slate-600">
          <h3 class="text-2xl font-bold gradient-text mb-4 flex items-center">
            <i class="fas fa-edit mr-2 text-indigo-300"></i>新規投稿
          </h3>
          <div>
            <textarea id="postContent" class="w-full p-4 border border-slate-600 rounded-lg bg-slate-900 text-gray-100 text-sm" rows="4" placeholder="今何を考えていますか?(Ctrl+Enter で投稿)" maxlength="500"></textarea>
            <div class="mt-4 flex flex-col md:flex-row md:items-center md:justify-between space-y-3 md:space-y-0">
              <div class="text-indigo-100 text-xs opacity-90 flex items-center space-x-2">
                <i class="fas fa-info-circle"></i>
                <span>あなたの思いを共有しましょう</span>
                <span id="char-count" class="ml-2 px-2 py-1 rounded-full bg-slate-900 border border-slate-600">(0/500)</span>
              </div>
              <div class="flex space-x-2">
                <button onclick="createUserPost()" class="btn-primary px-5 py-2 rounded-lg text-white text-xs font-semibold">
                  <i class="fas fa-paper-plane mr-2"></i>投稿する
                </button>
              </div>
            </div>
          </div>
        </div>

        <div class="section-divider"></div>

        <!-- タイムライン -->
        <div class="glass-effect p-6 border border-slate-600">
          <div class="flex flex-col md:flex-row md:justify-between md:items-center mb-4 space-y-3 md:space-y-0">
            <h3 class="text-2xl font-bold text-indigo-100 flex items-center">
              <i class="fas fa-stream mr-2 text-indigo-300"></i>タイムライン
              <span id="post-count" class="text-sm font-normal opacity-75 ml-2">(0件の投稿)</span>
            </h3>
            <div class="flex space-x-2">
              <button onclick="clearAllPosts()" class="bg-red-600 hover:bg-red-700 px-3 py-1 rounded text-white text-xs flex items-center">
                <i class="fas fa-trash mr-1"></i>全削除
              </button>
              <button onclick="exportData()" class="bg-emerald-600 hover:bg-emerald-700 px-3 py-1 rounded text-white text-xs flex items-center">
                <i class="fas fa-download mr-1"></i>エクスポート
              </button>
            </div>
          </div>

          <!-- フィルター&検索 -->
          <div class="flex flex-col md:flex-row md:items-center md:justify-between mb-4 space-y-3 md:space-y-0">
            <div class="flex flex-wrap gap-2 text-xs">
              <button id="filter-all" onclick="setFilter('all')" class="px-3 py-1 rounded-full border border-slate-600 bg-indigo-600 text-white flex items-center">
                <i class="fas fa-globe mr-1"></i>すべて
              </button>
              <button id="filter-user" onclick="setFilter('user')" class="px-3 py-1 rounded-full border border-slate-600 text-slate-200 flex items-center">
                <i class="fas fa-user mr-1"></i>ユーザー
              </button>
              <button id="filter-bot" onclick="setFilter('bot')" class="px-3 py-1 rounded-full border border-slate-600 text-slate-200 flex items-center">
                <i class="fas fa-robot mr-1"></i>BOT
              </button>
              <button id="filter-feed" onclick="setFilter('feed')" class="px-3 py-1 rounded-full border border-slate-600 text-slate-200 flex items-center">
                <i class="fas fa-rss mr-1"></i>Feed
              </button>
            </div>
            <div class="relative w-full md:w-64">
              <input id="timeline-search" type="text" class="w-full pl-8 pr-3 py-2 rounded-full bg-slate-900 border border-slate-600 text-xs text-slate-100" placeholder="キーワード検索(本文・ユーザー名)">
              <i class="fas fa-search text-slate-400 text-xs absolute left-2.5 top-1/2 transform -translate-y-1/2"></i>
            </div>
          </div>

          <div id="timeline" class="space-y-4"></div>
          <div id="empty-timeline" class="text-center py-12 text-slate-200 opacity-80">
            <i class="fas fa-comments text-4xl mb-4 text-indigo-300"></i>
            <p class="text-lg">まだ投稿がありません</p>
            <p class="text-xs text-slate-300">最初の投稿をして、タイムラインを始めましょう!</p>
          </div>
        </div>
      </div>
    </div>
  </div>

  <footer class="glass-effect mx-4 mb-4 p-4 text-center border border-slate-700">
    <p class="text-slate-200 opacity-80 text-xs">
      <i class="fas fa-copyright mr-1"></i>
      2025 Verse – 次世代ソーシャルネットワーク v3.0
      <span class="ml-4 inline-flex items-center">
        <i class="fas fa-rss mr-1 text-amber-300"></i>共有RSS / 個別ON/OFF / BOT・マルコフ自動投稿
      </span>
    </p>
  </footer>

  <script>
    // ==== 初期RSS ====
    const PRESET_RSS = [
      "http://2ch-2.net/rss/all.xml",
      "http://2ch-ranking.net/rss/livemarket1.rdf",
      "http://2ch-ranking.net/rss/livemarket2.rdf",
      "http://kabumatome.doorblog.jp/index.rdf",
      "http://momoniji.com/feed",
      "http://oekakigakusyuu.blog97.fc2.com/?xml",
      "http://otanews.livedoor.biz/atom.xml",
      "http://otanews.livedoor.biz/index.rdf",
      "http://news4vip.livedoor.biz/index.rdf",
      "http://news.kakaku.com/prdnews/rss.asp",
      "http://www.jma-net.go.jp/rss/jma.rss",
      "http://rss.asahi.com/rss/asahi/newsheadlines.rdf",
      "https://uploadvr.com/feed/",
      "http://www.atmarkit.co.jp/rss/rss2dc.xml",
      "http://liginc.co.jp/feed",
      "http://liginc.co.jp/feed/",
      "http://blog.livedoor.jp/shachiani/index.rdf",
      "http://manga.lemon-s.com/atom.xml",
      "http://b.hatena.ne.jp/search/text?safe=on&q=%E3%82%BB%E3%82%AD%E3%83%A5%E3%83%AA%E3%83%86%E3%82%A3&users=500&mode=rss",
      "http://creator-life.net/feed/",
      "http://feedblog.ameba.jp/rss/ameblo/ca-1pixel/rss20.xml",
      "http://rssblog.ameba.jp/ca-1pixel/rss20.xml",
      "http://weekly.ascii.jp/cate/1/rss.xml",
      "http://blog.livedoor.jp/coleblog/atom.xml",
      "http://2chantena.antenam.biz/rss1s.rss",
      "http://www.4gamer.net/rss/index.xml",
      "http://www.4gamer.net/rss/news_topics.xml",
      "http://nyan.eggtree.net/feed.xml",
      "http://nunnnunn.hatenablog.com/rss",
      "http://www.nikkansports.com/general/atom.xml",
      "http://feeds.afpbb.com/rss/afpbb/access_ranking",
      "http://akiba-pc.watch.impress.co.jp/cda/rss/akiba-pc.rdf",
      "https://area.autodesk.jp/rss.xml",
      "http://av.watch.impress.co.jp/sublink/av.rdf",
      "http://rss.allabout.co.jp/aa/latest/ch/netpc/",
      "http://www.ar-ch.org/atom.xml",
      "http://feeds.arstechnica.com/arstechnica/BAaf",
      "https://feeds.feedburner.com/awwwards-sites-of-the-day",
      "http://news.bbc.co.uk/rss/newsonline_uk_edition/front_page/rss091.xml",
      "http://www.criteo.com/blog/rss/",
      "https://blueskyweb.xyz/rss.xml",
      "http://boingboing.net/rss.xml",
      "http://www.cc2.co.jp/blog/?feed=rss2",
      "http://cgarena.com/cgarena.xml",
      "http://cgtracking.net/feed",
      "http://japan.cnet.com/rss/index.rdf",
      "http://newclassic.jp/feed",
      "https://www.cssmania.com/feed/",
      "http://ceron.jp/top/?type=rss",
      "http://blog.btrax.com/jp/comments/feed/",
      "http://2ch.logpo.jp/1hour.xml",
      "http://menthas.com/javascript/rss",
      "http://www.nhk.or.jp/rss/news/cat0.xml",
      "http://ozpa-h4.com/feed/",
      "https://www.youtube.com/feeds/videos.xml?channel_id=UC1DCedRgGHBdm81E1llLhOQ",
      "http://rass.blog43.fc2.com/?xml",
      "http://stackoverflow.com/feeds",
      "http://www.slideshare.net/rss/latest",
      "http://www.jp.square-enix.com/whatsnew2/whatsnew.rdf",
      "http://www.ituore.com/feed",
      "http://synodos.jp/comments/feed",
      "http://www.shinkigensha.co.jp/feed/",
      "http://e-shuushuu.net/index.rss",
      "http://slashdot.org/index.rss",
      "http://feeds.feedburner.com/TheHackersNews?format=xml",
      "http://googleblog.blogspot.com/atom.xml",
      "http://www.theregister.co.uk/tonys/slashdot.rdf",
      "http://thinkit.co.jp/rss.xml",
      "http://blog.livedoor.jp/news23vip/atom.xml",
      "http://blog.livedoor.jp/news23vip/index.rdf",
      "http://www.webcreatorbox.com/feed/",
      "http://web-d.navigater.info/atom.xml",
      "http://2ch-c.net/?xml_all",
      "http://smhn.info/feed",
      "http://feeds.japan.zdnet.com/rss/zdnet/all.rdf",
      "http://20kaido.com/index.rdf",
      "http://2chnode.com/rss/feed/all",
      "http://akiba-souken.com/feed/all/",
      "http://amaebi.net/index.rdf",
      "http://amakakeru.blog59.fc2.com/?xml",
      "http://artskype.com/rss/feed.xml",
      "http://asitagamienai.blog118.fc2.com/?xml",
      "http://beta.egmnow.com/feed/",
      "http://blog.livedoor.jp/ogenre/index.rdf",
      "http://blog.nicovideo.jp/atom.xml",
      "http://blog.tsubuani.com/feed",
      "http://blogs.adobe.com/flex/atom.xml",
      "http://blogs.adobe.com/index.xml",
      "http://bm.s5-style.com/feed",
      "http://business.nikkeibp.co.jp/rss/all_nbo.rdf",
      "http://createlier.sitemix.jp/feed/",
      "http://crocro.com/news/nc.cgi?action=search&skin=rdf_srch_xml",
      "http://d.hatena.ne.jp/thk/rss",
      "http://damage0.blomaga.jp/index.rdf",
      "http://danbooru.donmai.us/posts.atom",
      "http://danbooru.donmai.us/posts.atom?tags=rss",
      "http://dengekionline.com/cate/11/rss.xml",
      "http://dictionary.reference.com/wordoftheday/wotd.rss",
      "http://doujin-games88.net/feed",
      "http://doujin.sekurosu.com/rss",
      "http://dousyoko.blog.fc2.com/?xml",
      "http://eroaniblog.blog.fc2.com/?xml",
      "http://eroanimedougakan.blog.fc2.com/?xml",
      "http://erogetrailers.com/api?md=latest",
      "http://eronizimage.blog.fc2.com/?xml",
      "http://erosanime.blog121.fc2.com/?xml",
      "http://erotaganime.blog.fc2.com/?xml",
      "http://feed.nikkeibp.co.jp/rss/nikkeibp/index.rdf",
      "http://feed.rssad.jp/rss/gigazine/rss_2.0",
      "http://feed.rssad.jp/rss/jcast/index.xml",
      "http://feed.rssad.jp/rss/klug/fxnews/rss5.xml",
      "http://feedblog.ameba.jp/rss/ameblo/yusayusa0211/rss20.xml",
      "http://feeds.adobe.com/xml/rss.cfm?query=byMostRecent&languages=1",
      "http://feeds.builder.japan.zdnet.com/rss/builder/all.rdf",
      "http://feeds.fc2.com/fc2/xml?host=anrism.blog&format=xml",
      "http://feeds.fc2.com/fc2/xml?host=kahouha2jigen.blog&format=xml",
      "http://feeds.feedburner.com/gekiura",
      "http://feeds.journal.mycom.co.jp/rss/mycom/index",
      "http://feeds.reuters.com/reuters/JPTopNews?format=xml",
      "http://galten705.blog.fc2.com/?xml",
      "http://gamanjiru.net/feed",
      "http://gamanjiru.net/feed/atom",
      "http://gamebiz.jp/?feed=rss",
      "http://gamenode.jp/rss/feed/all",
      "http://ggsoku.com/feed/atom/",
      "http://girlcelly.blog.fc2.com/?xml&trackback",
      "http://hairana.blog.fc2.com/?xml",
      "http://haruka-yumenoato.net/static/rss/index.rss",
      "http://headline.harikonotora.net/rss2.xml",
      "http://hentaidoujinanime.com/?xml",
      "http://homepage1.nifty.com/maname/index.rdf",
      "http://horiemon.com/feed/",
      "http://ideahacker.net/feed/",
      "http://itpro.nikkeibp.co.jp/rss/develop.rdf",
      "http://itpro.nikkeibp.co.jp/rss/news.rss",
      "http://itpro.nikkeibp.co.jp/rss/oss.rdf",
      "http://itpro.nikkeibp.co.jp/rss/win.rdf",
      "http://japan.internet.com/rss/rdf/index.rdf",
      "http://jp.leopard-raws.org/rss.php",
      "http://jp.techcrunch.com/feed/",
      "http://kakaku.com/trendnews/rss.xml",
      "http://kamisoku.blog47.fc2.com/?xml",
      "http://kanesoku.com/index.rdf",
      "http://kibougamotenai.blog.fc2.com/?xml",
      "http://kiisu.jpn.org/rss/now.xml",
      "http://konachan.com/post/piclens?page=1&tags=loli",
      "http://labo.tv/2chnews/index.xml",
      "http://lineblog.me/yamamotoichiro/atom.xml",
      "http://majimougen.blog.fc2.com/?xml",
      "http://mantan-web.jp/rss/mantan.xml",
      "http://matome.naver.jp/feed/hot",
      "http://matome.naver.jp/feed/tech",
      "http://matome.sekurosu.com/rss",
      "http://mizuhonokuni2ch.com/?xml",
      "http://momoiroanime.blog.fc2.com/?xml",
      "http://moroahedoujin.com/?xml",
      "http://nesingazou.blog.fc2.com/?xml",
      "http://newnews-moe.com/index.rdf",
      "http://news.ameba.jp/index.xml",
      "http://news.com.com/2547-1_3-0-5.xml",
      "http://news.nicovideo.jp/?rss=2.0",
      "http://news.nicovideo.jp/ranking/hot?rss=2.0",
      "http://newsbiz.yahoo.co.jp/topnews.rss",
      "http://nijitora.blog.fc2.com/?xml",
      "http://nodvd21ver2.blog.fc2.com/?xml",
      "http://orebibou.com/feed/",
      "http://osu.ppy.sh/feed/ranked/",
      "http://otakomu.jp/feed",
      "http://pcgameconquest.blog.fc2.com/?xml",
      "http://picks.dir.yahoo.co.jp/dailypicks/rss/",
      "http://piknik2ch.blog76.fc2.com/?xml",
      "http://plus.appgiga.jp/feed/user",
      "http://purisoku.com/index.rdf",
      "http://rdsig.yahoo.co.jp/RV=1/RU=aHR0cDovL3NlYXJjaHJhbmtpbmcueWFob28uY28uanAvcnNzL2J1cnN0X3JhbmtpbmctcnNzLnhtbA--;_ylt=A2RhjFhfAi9XEi0A6Glhdu57",
      "http://read2ch.net/rss/",
      "http://rss.dailynews.yahoo.co.jp/fc/computer/rss.xml",
      "http://rss.rssad.jp/rss/akibapc/akiba-pc.rdf",
      "http://rss.rssad.jp/rss/ascii/biz/rss.xml",
      "http://rss.rssad.jp/rss/ascii/hobby/rss.xml",
      "http://rss.rssad.jp/rss/ascii/it/rss.xml",
      "http://rss.rssad.jp/rss/ascii/mac/rss.xml",
      "http://rss.rssad.jp/rss/ascii/pc/rss.xml",
      "http://rss.rssad.jp/rss/ascii/rss.xml",
      "http://rss.rssad.jp/rss/codezine/new/20/index.xml",
      "http://rss.rssad.jp/rss/forest/rss.xml",
      "http://rss.rssad.jp/rss/gihyo/feed/atom",
      "http://rss.rssad.jp/rss/headline/headline.rdf",
      "http://rss.rssad.jp/rss/impresswatch/pcwatch.rdf",
      "http://rss.rssad.jp/rss/itm/1.0/makoto.xml",
      "http://rss.rssad.jp/rss/itm/1.0/netlab.xml",
      "http://rss.rssad.jp/rss/itm/2.0/kw_akiba.xml",
      "http://rss.rssad.jp/rss/itm/2.0/kw_android_appli.xml",
      "http://rss.rssad.jp/rss/itm/2.0/kw_apple.xml",
      "http://rss.rssad.jp/rss/itm/2.0/kw_facebook.xml",
      "http://rss.rssad.jp/rss/itm/2.0/kw_google.xml",
      "http://rss.rssad.jp/rss/itm/2.0/kw_ipad.xml",
      "http://rss.rssad.jp/rss/itm/2.0/kw_iphone.xml",
      "http://rss.rssad.jp/rss/itm/2.0/kw_iphone_appli.xml",
      "http://rss.rssad.jp/rss/itm/2.0/kw_mixi.xml",
      "http://rss.rssad.jp/rss/itm/2.0/kw_smartphone.xml",
      "http://rss.rssad.jp/rss/itm/2.0/kw_twitter.xml",
      "http://rss.rssad.jp/rss/itm/2.0/kw_ustream.xml",
      "http://rss.rssad.jp/rss/itm/2.0/kw_youtube.xml",
      "http://rss.rssad.jp/rss/itmbizid/1.0/bizid.xml",
      "http://rss.rssad.jp/rss/itmnews/2.0/news_bursts.xml",
      "http://rss.rssad.jp/rss/japaninternetcom/index.rdf",
      "http://rss.rssad.jp/rss/oshietekun/atom.xml",
      "http://rss.rssad.jp/rss/slashdot/slashdot.rss",
      "http://rss.rssad.jp/rss/zaikeishimbun/main.xml",
      "http://rssc.dokoda.jp/r/8a1dd8f128047929ba4390dab3c8065e/http/searchranking.yahoo.co.jp/realtime_buzz/",
      "http://sakurabaryo.com/feed/",
      "http://sankei.jp.msn.com/rss/news/points.xml",
      "http://sankei.jp.msn.com/rss/news/west_points.xml",
      "http://search.goo.ne.jp/rss/newkw.rdf",
      "http://sekurosu.com/rss",
      "http://streaming.yahoo.co.jp/rss/newly/anime/",
      "http://sub0000528116.hmk-temp.com/wordpress/?feed=rss2",
      "http://sukebei.nyaa.se/?page=rss&sort=2",
      "http://tenshoku.mynavi.jp/knowhow/rss.xml",
      "http://tensinyakimeshi.blog98.fc2.com/?xml",
      "http://thefreedom12.blog41.fc2.com/?xml",
      "http://togetter.com/rss/hot/culture/62",
      "http://togetter.com/rss/hot/culture/63",
      "http://torimatome.main.jp/blogs/comments/feed",
      "http://torimatome.main.jp/blogs/feed",
      "http://toshinokyouko.com/rss.php",
      "http://tvanimedouga.blog93.fc2.com/?xml",
      "http://uranourainformation.blog21.fc2.com/?xml",
      "http://video.fc2.com/a/feed_popular.php?m=week",
      "http://weather.livedoor.com/forecast/rss/area/400010.xml",
      "http://wotopi.jp/feed",
      "http://www.100shiki.com/feed",
      "http://www.alistapart.com/rss.xml",
      "http://www.anime-sharing.com/forum/external.php?type=RSS2&forumids=36",
      "http://www.anime-sharing.com/forum/external.php?type=RSS2&forumids=38",
      "http://www.anime-sharing.com/forum/external.php?type=RSS2&forumids=47",
      "http://www.blosxom.com/?feed=rss2",
      "http://www.britannica.com/eb/dailycontent/rss",
      "http://www.csmonitor.com/rss/top.rss",
      "http://www.ehackingnews.com/feeds/posts/default",
      "http://www.falcom.co.jp/new.xml",
      "http://www.famitsu.com/rss/category/fcom_game.rdf",
      "http://www.famitsu.com/rss/fcom_all.rdf",
      "http://www.ganganonline.com/rss/index.xml",
      "http://www.ideaxidea.com/feed",
      "http://www.itnews711.com/index.rdf",
      "http://www.jp.playstation.com/whatsnew/whatsnew.rdf",
      "http://www.keyman.or.jp/rss/v1/?rss_type=all",
      "http://www.koubo.co.jp/rss.xml",
      "http://www.nyaa.se/?page=rss&sort=2",
      "http://www.nyaa.se/?page=rss&user=118009",
      "http://www.nytimes.com/services/xml/rss/userland/HomePage.xml",
      "http://www.phianime.tv/feed/",
      "http://www.rebootdevelop.hr/feed/",
      "http://www.rictus.com/muchado/feed/",
      "http://www.sbcr.jp/atom.xml",
      "http://www.slashgear.com/comments/feed/",
      "http://www.torrent-anime.com/feed",
      "http://www.torrent-anime.com/feed/",
      "http://www.webimemo.com/feed/",
      "http://www.wired.com/news_drop/netcenter/netcenter.rdf",
      "http://www.xvideos.com/rss/rss.xml",
      "http://www.youtube.com/rss/user/KADOKAWAanime/videos.rss",
      "http://www.youtube.com/rss/user/demosouko/videos.rss",
      "http://www.yukawanet.com/index.rdf",
      "http://www.zou3.net/php/rss/nikkei2rss.php?head=main",
      "http://xml.ehgt.org/ehtracker.xml",
      "http://xml.metafilter.com/rss.xml",
      "http://xvideos.2jiero.info/feed",
      "http://yaraon.blog109.fc2.com/?xml",
      "http://yusaani.com/home/feed/",
      "http://zipdeyaruo.blog42.fc2.com/?xml",
      "http://www.portalgraphics.net/rss/latest_image_list.xml",
      "http://api.syosetu.com/writernovel/430380.Atom",
      "http://creive.me/feed/",
      "http://gihyo.jp/dev/feed/atom",
      "http://gihyo.jp/feed/rss1",
      "http://hakase255.blog135.fc2.com/?xml",
      "http://2ch-ranking.net/rss/zenban.rdf",
      "http://www.isus.jp/feed/",
      "http://www.jiji.com/rss/ranking.rdf",
      "http://jp.gamesindustry.biz/rss/index.xml",
      "https://www.youtube.com/feeds/videos.xml?channel_id=UCx1nAvtVDIsaGmCMSe8ofsQ",
      "http://zakuzaku911.com/index.rdf",
      "http://ke-tai.org/blog/feed/",
      "http://data.newantenna.net/ero/rss/all.xml",
      "http://developer.mixi.co.jp/feed/atom",
      "http://neoneetch.blog.fc2.com/?xml",
      "http://rss.itmedia.co.jp/rss/1.0/netlab.xml",
      "http://netgeek.biz/feed",
      "http://blog.esuteru.com/index.rdf",
      "http://b.hatena.ne.jp/hotentry/game.rss",
      "http://b.hatena.ne.jp/hotentry.rss",
      "http://mobile.seisyun.net/rss/hot.rdf",
      "http://yomi.mobi/rss/hot.rdf",
      "http://saymygame.com/feed/",
      "http://blog.webcreativepark.net/atom.xml",
      "http://buhidoh.net/?xml",
      "http://www.webcyou.com/?feed=rss2",
      "http://withnews.jp/rss/consumer/new.rdf",
      "https://yande.re/post/atom?tags=loli",
      "http://blog.livedoor.jp/nizigami/atom.xml",
      "http://nvmzaq.blog.fc2.com/?xml",
      "http://keieimanga.net/index.rdf",
      "http://megumi.ldblog.jp/atom.xml",
      "http://kirik.tea-nifty.com/diary/index.rdf",
      "http://sinri.net/comments/feed",
      "http://himasoku.com/atom.xml",
      "http://himasoku.com/index.rdf",
      "http://20kaido.com/index.rdf",
      "http://h723.blog.fc2.com/?xml",
      "http://onecall2ch.com/index.rdf",
      "http://www.forest.impress.co.jp/rss.xml",
      "http://www.zaikei.co.jp/rss/sections/it.xml",
      "http://akiba.keizai.biz/rss.xml",
      "http://agag.tw/feed/2d-popular.rss",
      "http://adult-vr.jp/feed/",
      "http://www.anige-sokuhouvip.com/?xml",
      "http://animeanime.jp/rss/index.rdf",
      "http://alfalfalfa.com/index.rdf",
      "http://feeds.feedburner.com/fc2/GhfA",
      "http://erogetaiken072.blog.fc2.com/?xml",
      "http://otanew.jp/atom.xml",
      "http://jin115.com/index.rdf",
      "http://www.onlinegamer.jp/rss/news.rdf",
      "http://karapaia.livedoor.biz/index.rdf",
      "http://getnews.jp/feed/ext/orig",
      "http://www.gungho.co.jp/news/xml/rss.xml",
      "http://blog.livedoor.jp/kinisoku/index.rdf",
      "http://feeds.gizmodo.jp/rss/gizmodo/index.xml",
      "http://himado.in/?sort=movie_id&rss=1",
      "http://k-tai.impress.co.jp/cda/rss/ktai.rdf",
      "http://gehasoku.com/atom.xml",
      "http://feedblog.ameba.jp/rss/ameblo/principia-ca/rss20.xml",
      "http://zai.diamond.jp/list/feed/rssfxnews",
      "http://capacitor.blog.fc2.com/?xml",
      "http://blog.livedoor.jp/vipsister23/index.rdf",
      "http://vipsister23.com/atom.xml",
      "http://b.hatena.ne.jp/search/tag?safe=on&q=2ch&users=500&mode=rss",
      "http://b.hatena.ne.jp/search/tag?safe=on&q=%E3%83%8D%E3%83%83%E3%83%88%E3%83%AF%E3%83%BC%E3%82%AF&users=500&mode=rss",
      "http://b.hatena.ne.jp/search/tag?safe=off&q=%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0&users=500&mode=rss",
      "http://shikaku2ch.doorblog.jp/atom.xml",
      "http://dotinstall.com/lessons.rss",
      "http://2ch-ranking.net/rss/news4vip.rdf",
      "http://blog.livedoor.jp/insidears/index.rdf",
      "http://2ch-ranking.net/rss/newsplus.rdf",
      "http://2ch-ranking.net/rss/news.rdf",
      "http://nullpoantenna.com/game.rdf",
      "http://workingnews.blog117.fc2.com/?xml",
      "http://bm.s5-style.com/feed",
      "http://2ch-ranking.net/rss/ghard.rdf",
      "http://www.724685.com/blog/rss.xml",
      "http://www.yukawanet.com/index.rdf",
      "http://2ch-ranking.net/rss/bizplus.rdf",
      "http://www.nicovideo.jp/ranking/fav/daily/all?rss=2.0&lang=ja-jp",
      "http://www.tarikin.net/rss0.rdf",
      "http://blog.livedoor.jp/dqnplus/index.rdf",
      "http://www.seojapan.com/blog/feed",
      "http://2ch-ranking.net/rss/morningcoffee.rdf",
      "http://2ch-ranking.net/mt50k.rdf",
      "http://rssblog.ameba.jp/yandereotto/rss20.xml",
      "https://business.nikkei.com/rss/sns/nb.rdf",
      "http://daredemopc.blog51.fc2.com/?xml",
      "http://erogetaikenban.blog65.fc2.com/?xml",
      "http://news.goo.ne.jp/rss/topstories/gootop/index.rdf",
      "http://lanovelien.blog121.fc2.com/?xml",
      "http://news.livedoor.com/topics/rss/eco.xml",
      "http://ragnarokonline.gungho.jp/index.rdf",
      "http://rocketnews24.com/feed/",
      "https://news.denfaminicogamer.jp/feed",
      "http://www.igda.jp/?feed=rss2",
      "http://feeds.cnn.co.jp/cnn/rss"
    ];

    // ==== アプリ状態 ====
    if (!localStorage.getItem('verse_shared_rssFeeds')) {
      localStorage.setItem('verse_shared_rssFeeds', JSON.stringify(PRESET_RSS));
    }

    let users = JSON.parse(localStorage.getItem('verse_users') || '[]');
    let currentUser = JSON.parse(localStorage.getItem('verse_currentUser') || 'null');
    let posts = JSON.parse(localStorage.getItem('verse_posts') || '[]');
    let isDarkMode = localStorage.getItem('verse_darkMode') === 'true';
    let isInitialized = false;

    // BOT / RSS 状態
    let botInterval = null;
    let rssInterval = null;

    // タイムラインフィルタ&検索
    let currentFilter = 'all';
    let currentSearch = '';

    // 共有RSS設定
    let sharedRssFeeds = JSON.parse(localStorage.getItem('verse_shared_rssFeeds') || '[]');
    let sharedRssInterval = Number(localStorage.getItem('verse_shared_rssInterval')) || 300;
    let sharedRssLastIds = JSON.parse(localStorage.getItem('verse_shared_rssLastIds') || '{}');
    let sharedRssEnabled = JSON.parse(localStorage.getItem('verse_shared_rssEnabled') || '{}');

    // アイコン設定(BOT, Feed)
    let verseIcons = JSON.parse(localStorage.getItem('verse_icons') || 'null');
    if (!verseIcons) {
      verseIcons = {
        bot: 'https://cdn-icons-png.flaticon.com/512/4712/4712109.png',
        feed: 'https://cdn-icons-png.flaticon.com/512/3416/3416046.png'
      };
      localStorage.setItem('verse_icons', JSON.stringify(verseIcons));
    }

    function saveIcons() {
      localStorage.setItem('verse_icons', JSON.stringify(verseIcons));
      updateAllUI();
    }

    function uploadIcon(type, e) {
      const f = e.target.files[0];
      if (!f) return;
      if (f.size > 5 * 1024 * 1024) { alert('5MB以下にしてください。'); return; }
      const r = new FileReader();
      r.onload = () => {
        verseIcons[type] = r.result;
        saveIcons();
        alert(type.toUpperCase() + ' アイコンを更新しました');
      };
      r.readAsDataURL(f);
    }

    // ==== キャッシュクリア ====
    function clearVerseCache() {
      if (!confirm('Tsumugi / Verse のローカルキャッシュ(ユーザー, 投稿, RSS設定など)をすべて削除します。よろしいですか?')) return;
      Object.keys(localStorage).forEach(k => {
        if (k.startsWith('verse_')) localStorage.removeItem(k);
      });
      alert('ローカルキャッシュを削除しました。ページを再読み込みします。');
      location.reload();
    }

    // ===== 認証UI =====
    function showAuthModal(mode = 'login', errorMsg = '') {
      document.getElementById('auth-title').textContent = (mode === 'register') ? '新規登録' : 'ログイン';
      document.getElementById('auth-form').authMode = mode;
      document.getElementById('auth-email').value = '';
      document.getElementById('auth-password').value = '';
      document.getElementById('auth-modal').style.display = '';
      document.getElementById('main-content').style.display = 'none';
      document.getElementById('auth-error').textContent = errorMsg || '';
      document.getElementById('auth-error').style.display = errorMsg ? '' : 'none';
      document.getElementById('toggle-auth-mode').textContent = (mode === 'register') ? 'ログインはこちら' : '新規登録はこちら';
    }
    function hideAuthModal() {
      document.getElementById('auth-modal').style.display = 'none';
      document.getElementById('main-content').style.display = '';
    }

    document.addEventListener('DOMContentLoaded', () => {
      document.getElementById('auth-form').onsubmit = function(e) {
        e.preventDefault();
        const email = document.getElementById('auth-email').value.trim().toLowerCase();
        const password = document.getElementById('auth-password').value;
        if (!email || !password) {
          showAuthModal(this.authMode, 'メールアドレスとパスワードを入力してください');
          return;
        }
        if (this.authMode === 'register') {
          if (users.find(u => u.email === email)) {
            showAuthModal('register', 'このメールアドレスは既に登録されています');
            return;
          }
          const newUser = {
            email,
            password,
            profile: { icon: 'https://via.placeholder.com/100', username: email.split('@')[0], selfIntro: '' }
          };
          users.push(newUser);
          localStorage.setItem('verse_users', JSON.stringify(users));
          currentUser = { email };
          localStorage.setItem('verse_currentUser', JSON.stringify(currentUser));
          showAuthModal('login', '登録完了!ログインしてください');
        } else {
          const user = users.find(u => u.email === email && u.password === password);
          if (!user) { showAuthModal('login', 'メールアドレスまたはパスワードが違います'); return; }
          currentUser = { email };
          localStorage.setItem('verse_currentUser', JSON.stringify(currentUser));
          hideAuthModal();
          initializeApp();
        }
      };

      document.getElementById('toggle-auth-mode').onclick = function() {
        const mode = (document.getElementById('auth-title').textContent === '新規登録') ? 'login' : 'register';
        showAuthModal(mode);
      };

      if (!currentUser) showAuthModal('login'); else { hideAuthModal(); initializeApp(); }
    });

    function logout() {
      localStorage.removeItem('verse_currentUser');
      currentUser = null;
      stopRssAuto();
      stopBotAutoPost();
      showAuthModal('login');
    }

    // ===== 初期化 =====
    function initializeApp() {
      if (isInitialized) return;
      if (!currentUser) { showAuthModal('login'); return; }

      users = JSON.parse(localStorage.getItem('verse_users') || '[]');
      posts = JSON.parse(localStorage.getItem('verse_posts') || '[]');
      isDarkMode = localStorage.getItem('verse_darkMode') === 'true';

      const user = users.find(u => u.email === currentUser.email);
      window.profile = user ? user.profile : { icon: 'https://via.placeholder.com/100', username: 'ゲストユーザー', selfIntro: '' };

      updateAllUI();
      updateStatusIndicators();
      updateRssUI();

      if (isDarkMode) {
        document.body.classList.add('dark');
      } else {
        document.body.classList.remove('dark');
      }

      document.getElementById('main-content').style.display = '';
      document.getElementById('logout-btn').classList.remove('hidden');
      isInitialized = true;

      startRssAuto();
      addLog('bot-log', 'BOT機能初期化完了', 'success');
      addLog('rss-log', 'RSS自動投稿(全体共有)初期化完了', 'success');
    }

    // ===== 投稿(ユーザー/BOT/Feed/Markov) =====
    function createUserPost() {
      const ta = document.getElementById('postContent');
      const txt = ta.value.trim();
      if (!txt) return alert('投稿内容を入力してください。');
      if (!currentUser) return alert('ログインが必要です。');
      createPost(txt, 'user', profile.username, profile.icon);
      ta.value = '';
      updateCharCount();
    }

    function createPost(content, type = 'user', username = null, icon = null, extra = {}) {
      if (!content || !content.trim()) return false;

      let finalIcon = icon;
      if (!finalIcon) {
        if (type === 'bot' || type === 'markov') finalIcon = verseIcons.bot;
        else if (type === 'feed') finalIcon = verseIcons.feed;
        else finalIcon = profile.icon;
      }

      const post = {
        id: Date.now() + Math.random(),
        content: content.trim(),
        likes: 0,
        timestamp: new Date().toLocaleString('ja-JP'),
        type,
        username: username || profile.username,
        icon: finalIcon,
        userEmail: currentUser ? currentUser.email : '',
        ...extra
      };
      posts.unshift(post);
      saveData();
      renderTimeline();
      return true;
    }

    function likePost(id) {
      const idx = posts.findIndex(p => p.id === id);
      if (idx >= 0) {
        posts[idx].likes++;
        saveData();
        renderTimeline();
      }
    }

    function deletePost(id) {
      if (!confirm('この投稿を削除しますか?')) return;
      posts = posts.filter(p => p.id !== id);
      saveData();
      renderTimeline();
    }

    function clearAllPosts() {
      if (!confirm('全ての投稿を削除しますか?')) return;
      posts = [];
      saveData();
      renderTimeline();
    }

    function exportData() {
      const blob = new Blob([JSON.stringify(posts, null, 2)], { type: 'application/json' });
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = 'verse_posts.json';
      a.click();
      URL.revokeObjectURL(url);
    }

    // ===== タイムライン描画 =====
    function escapeHtml(s) {
      return (s || '').replace(/[&<>"']/g, m => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;','\'':'&#39;'}[m]));
    }

    function renderTimeline() {
      const tl = document.getElementById('timeline');
      const emp = document.getElementById('empty-timeline');
      const cnt = document.getElementById('post-count');
      if (!tl || !emp || !cnt) return;

      let displayPosts = posts.slice();

      // フィルター
      displayPosts = displayPosts.filter(p => {
        if (currentFilter === 'user' && p.type !== 'user') return false;
        if (currentFilter === 'bot' && !['bot','markov'].includes(p.type)) return false;
        if (currentFilter === 'feed' && p.type !== 'feed') return false;
        return true;
      });

      // 検索
      if (currentSearch && currentSearch.trim() !== '') {
        const q = currentSearch.trim().toLowerCase();
        displayPosts = displayPosts.filter(p => {
          const text = (p.content || '') + ' ' + (p.username || '');
          return text.toLowerCase().includes(q);
        });
      }

      if (displayPosts.length === 0) {
        tl.innerHTML = '';
        emp.style.display = 'block';
        cnt.textContent = '(0件の投稿)';
        return;
      }
      emp.style.display = 'none';
      cnt.textContent = `(${displayPosts.length}件の投稿)`;

      tl.innerHTML = displayPosts.map(p => {
        const info = {
          bot: '<i class="fas fa-robot mr-1"></i>BOT',
          markov: '<i class="fas fa-dice mr-1"></i>MarkovBOT',
          user: '<i class="fas fa-user mr-1"></i>ユーザー',
          feed: '<i class="fas fa-rss mr-1"></i>FEEDBOT'
        }[p.type] || '<i class="fas fa-user mr-1"></i>';

        const main = p.link
          ? `<a href="${p.link}" target="_blank" class="text-sky-400 underline">${escapeHtml(p.content)}</a>`
          : `${escapeHtml(p.content)}`;

        return `
          <div class="timeline-post p-6">
            <div class="flex justify-between items-start mb-4">
              <div class="flex items-center space-x-3">
                <img src="${p.icon}" class="w-10 h-10 rounded-full object-cover border border-slate-500" onerror="this.src='https://via.placeholder.com/40'">
                <div>
                  <div class="flex items-center">
                    <span class="font-semibold text-slate-100">${escapeHtml(p.username)}</span>
                    <span class="username-badge text-[10px]">${info}</span>
                  </div>
                  <div class="text-[11px] text-slate-400">${p.timestamp}</div>
                </div>
              </div>
            </div>
            <div class="text-slate-100 mb-4 leading-relaxed text-sm">${main}</div>
            <div class="flex items-center space-x-4 pt-4 border-t border-slate-700">
              <button onclick="likePost(${p.id})" class="flex items-center space-x-2 text-slate-300 hover:text-red-400 text-xs">
                <i class="fas fa-heart"></i><span>${p.likes}</span>
              </button>
              <div class="relative">
                <button onclick="toggleShareMenu(${p.id})" class="flex items-center space-x-2 text-slate-300 hover:text-sky-400 text-xs">
                  <i class="fas fa-share"></i><span>シェア</span>
                </button>
                <div id="share-menu-${p.id}" class="share-menu hidden">
                  <button onclick="shareToX(${p.id})"><i class="fab fa-x-twitter text-sky-400 mr-2"></i>Xでシェア</button>
                  <button onclick="shareToLine(${p.id})"><i class="fab fa-line text-green-400 mr-2"></i>LINEでシェア</button>
                  <button onclick="copyPost(${p.id})"><i class="fas fa-copy mr-2"></i>コピー</button>
                </div>
              </div>
              <button onclick="deletePost(${p.id})" class="flex items-center space-x-2 text-slate-400 hover:text-red-400 ml-auto text-xs">
                <i class="fas fa-trash"></i><span>削除</span>
              </button>
            </div>
          </div>
        `;
      }).join('');
    }

    function getPostContentText(id) {
      const p = posts.find(x => x.id === id);
      if (!p) return '';
      const tmp = document.createElement('div');
      tmp.innerHTML = p.content;
      return tmp.textContent || tmp.innerText || '';
    }
    function toggleShareMenu(id) {
      document.querySelectorAll('[id^="share-menu-"]').forEach(el => el.classList.add('hidden'));
      const m = document.getElementById('share-menu-' + id);
      if (m) m.classList.toggle('hidden');
    }
    function shareToX(id) {
      const t = encodeURIComponent(getPostContentText(id));
      const u = encodeURIComponent(location.href);
      window.open(`https://twitter.com/intent/tweet?text=${t}&url=${u}`, '_blank');
    }
    function shareToLine(id) {
      const u = encodeURIComponent(location.href);
      window.open(`https://social-plugins.line.me/lineit/share?url=${u}`, '_blank');
    }
    function copyPost(id) {
      const t = getPostContentText(id);
      if (navigator.clipboard) {
        navigator.clipboard.writeText(t).then(() => alert('コピーしました')).catch(() => fallbackCopy(t));
      } else fallbackCopy(t);
    }
    function fallbackCopy(t) {
      const ta = document.createElement('textarea');
      ta.value = t; document.body.appendChild(ta);
      ta.select(); document.execCommand('copy');
      document.body.removeChild(ta);
      alert('コピーしました');
    }

    // ===== タイムライン フィルタ&検索 =====
    function setFilter(f) {
      currentFilter = f;
      ['all','user','bot','feed'].forEach(k => {
        const btn = document.getElementById('filter-' + k);
        if (!btn) return;
        if (k === f) {
          btn.classList.add('bg-indigo-600','text-white');
        } else {
          btn.classList.remove('bg-indigo-600','text-white');
        }
      });
      renderTimeline();
    }

    // ===== RSS UI(個別ON/OFF + 一括ON/OFF) =====
    function updateRssUI() {
      const listDiv = document.getElementById('rss-list');
      if (!listDiv) return;

      if (!sharedRssFeeds || sharedRssFeeds.length === 0) {
        listDiv.innerHTML = '<div class="text-slate-200 text-[11px] opacity-80">RSSフィード未登録</div>';
      } else {
        listDiv.innerHTML = sharedRssFeeds.map((url, i) => {
          const enabled = sharedRssEnabled[url] !== false;
          const enc = encodeURIComponent(url);
          return `
            <div class="flex items-center space-x-2 bg-slate-900 rounded px-2 py-2 mb-1 border border-slate-700 text-[11px]">
              <input type="checkbox" ${enabled ? 'checked' : ''} onchange="toggleRssEnabled('${enc}', this.checked)" title="ON/OFF">
              <div class="truncate flex-1 text-slate-100" title="${escapeHtml(url)}">${escapeHtml(url)}</div>
              <button onclick="delRssFeed(${i})" class="text-red-400 hover:text-red-500" title="削除"><i class="fas fa-trash"></i></button>
            </div>
          `;
        }).join('');
      }
      const iv = document.getElementById('rss-interval');
      if (iv) iv.value = sharedRssInterval || 300;
    }

    function toggleRssEnabled(encUrl, on) {
      const url = decodeURIComponent(encUrl);
      sharedRssEnabled[url] = !!on;
      saveSharedRss();
      addLog('rss-log', `FEED ${on ? 'ON' : 'OFF'}: ${url}`, 'info');
    }
    function setAllRssEnabled(on) {
      (sharedRssFeeds || []).forEach(u => sharedRssEnabled[u] = !!on);
      saveSharedRss();
      updateRssUI();
      addLog('rss-log', `全フィードを${on ? 'ON' : 'OFF'}にしました`, 'success');
    }

    function saveSharedRss() {
      localStorage.setItem('verse_shared_rssFeeds', JSON.stringify(sharedRssFeeds));
      localStorage.setItem('verse_shared_rssInterval', String(sharedRssInterval));
      localStorage.setItem('verse_shared_rssLastIds', JSON.stringify(sharedRssLastIds));
      localStorage.setItem('verse_shared_rssEnabled', JSON.stringify(sharedRssEnabled));
    }

    function addRssFeed() {
      const url = document.getElementById('rss-url').value.trim();
      if (!/^https?:\/\/.+/.test(url)) { addLog('rss-log', '正しいRSSフィードURLを入力してください', 'error'); return; }
      if (!sharedRssFeeds) sharedRssFeeds = [];
      if (sharedRssFeeds.includes(url)) { addLog('rss-log', 'すでに登録済みです', 'error'); return; }
      sharedRssFeeds.push(url);
      sharedRssEnabled[url] = true;
      saveSharedRss();
      updateRssUI();
      addLog('rss-log', `RSS追加: ${url}`, 'success');
      document.getElementById('rss-url').value = '';
    }

    function delRssFeed(i) {
      if (!sharedRssFeeds[i]) return;
      if (!confirm('このフィードを削除しますか?')) return;
      const url = sharedRssFeeds[i];
      sharedRssFeeds.splice(i, 1);
      delete sharedRssEnabled[url];
      delete sharedRssLastIds[url];
      saveSharedRss();
      updateRssUI();
      addLog('rss-log', 'RSS削除', 'info');
    }

    function setRssInterval() {
      const iv = document.getElementById('rss-interval').valueAsNumber || 300;
      if (iv < 10) return alert('間隔は10秒以上で設定してください。');
      sharedRssInterval = iv;
      saveSharedRss();
      updateRssUI();
      startRssAuto();
      addLog('rss-log', `自動投稿間隔を${iv}秒に設定`, 'success');
    }

    function fetchRssNow() { fetchRssFeeds(); }

    function fetchRssFeeds() {
      if (!sharedRssFeeds || sharedRssFeeds.length === 0) return;
      sharedRssFeeds.forEach(feedUrl => {
        if (sharedRssEnabled[feedUrl] === false) {
          addLog('rss-log', `OFFのため取得スキップ: ${feedUrl}`, 'info');
          return;
        }
        fetch('https://api.rss2json.com/v1/api.json?rss_url=' + encodeURIComponent(feedUrl))
          .then(resp => resp.json())
          .then(data => {
            if (!data.items || !data.items.length) return;
            let lastId = sharedRssLastIds[feedUrl] || '';
            let newItems = [];
            for (const item of data.items) {
              const guid = item.guid || item.link || item.pubDate || item.title;
              if (!lastId || String(guid) > String(lastId)) newItems.push(item);
            }
            if (newItems.length === 0) return;

            newItems.reverse().forEach(item => {
              const guid = item.guid || item.link || item.pubDate || item.title;
              if (!posts.some(p => p.type === 'feed' && p.link === item.link)) {
                createPost(item.title, 'feed', 'FEEDBOT', verseIcons.feed, { link: item.link });
                addLog('rss-log', `新しい記事: ${item.title}`, 'success');
              }
              sharedRssLastIds[feedUrl] = guid;
            });
            saveSharedRss();
          })
          .catch(() => addLog('rss-log', 'RSS取得エラー: ' + feedUrl, 'error'));
      });
    }

    function startRssAuto() {
      stopRssAuto();
      fetchRssFeeds();
      rssInterval = setInterval(fetchRssFeeds, (sharedRssInterval || 300) * 1000);
      updateStatusIndicators();
      addLog('rss-log', `RSS自動投稿を開始 (${sharedRssInterval}秒間隔)`, 'success');
    }

    function stopRssAuto() {
      if (rssInterval) clearInterval(rssInterval);
      rssInterval = null;
      updateStatusIndicators();
      addLog('rss-log', 'RSS自動投稿を停止しました', 'info');
    }

    // ===== BOT =====
    function postBotMessage() {
      const ta = document.getElementById('botContent');
      const txt = ta.value.trim();
      if (!txt) return alert('BOT投稿内容を入力してください。');
      if (!currentUser) return alert('ログインが必要です。');
      if (createPost(txt, 'bot', 'BOT', verseIcons.bot)) {
        ta.value = '';
        addLog('bot-log', `BOT投稿: "${txt.substring(0, 30)}..."`, 'success');
      }
    }

    function generateMarkovText() {
      let text = posts
        .filter(p => ['user','bot','markov'].includes(p.type))
        .map(p => {
          const d = document.createElement('div');
          d.innerHTML = p.content;
          return (d.textContent || d.innerText || '')
            .replace(/\s+/g, ' ').replace(/https?:\/\/\S+/g, '').trim();
        }).join(' ');

      if (text.length < 20) {
        const fallbacks = [
          "今日はいい天気ですね!",
          "最近面白いニュースありましたか?",
          "新しいアイデアが浮かんできました。",
          "みんなはどう思いますか?"
        ];
        return fallbacks[Math.floor(Math.random() * fallbacks.length)];
      }

      const tokens = text.match(/[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\w]+|[。、!?\r\n]/g) || [];
      if (tokens.length < 2) return tokens.join('');

      const markov = {};
      for (let i = 0; i < tokens.length - 2; i++) {
        const key = tokens[i] + '|' + tokens[i+1];
        if (!markov[key]) markov[key] = [];
        markov[key].push(tokens[i+2]);
      }

      let idx = Math.floor(Math.random() * (tokens.length - 2));
      let key = tokens[idx] + '|' + tokens[idx+1];
      let result = [tokens[idx], tokens[idx+1]];
      let maxLen = 60 + Math.floor(Math.random() * 40);

      for (let i = 0; i < maxLen; i++) {
        const nexts = markov[key];
        if (!nexts || nexts.length === 0) break;
        const next = nexts[Math.floor(Math.random() * nexts.length)];
        result.push(next);
        if (/[。!?\n]/.test(next)) break;
        key = result[result.length - 2] + '|' + result[result.length - 1];
      }
      return result.join('').replace(/\n/g, '');
    }

    function postMarkovBot() {
      const txt = generateMarkovText();
      if (createPost(txt, 'markov', 'MarkovBOT', verseIcons.bot)) {
        addLog('bot-log', `マルコフ投稿: "${txt.substring(0, 40)}..."`, 'success');
      }
    }

    function startBotAutoPost() {
      const iv = document.getElementById('botIntervalSec').valueAsNumber || 60;
      if (iv < 10) { alert('間隔は10秒以上で設定してください。'); return; }
      stopBotAutoPost();
      setTimeout(postMarkovBot, 3000);
      botInterval = setInterval(postMarkovBot, iv * 1000);
      updateStatusIndicators();
      addLog('bot-log', `マルコフBOT自動投稿開始 (${iv}秒間隔)`, 'success');
    }

    function stopBotAutoPost() {
      if (botInterval) {
        clearInterval(botInterval);
        botInterval = null;
        updateStatusIndicators();
        addLog('bot-log', 'マルコフBOT自動投稿を停止しました', 'info');
      }
    }

    // ===== 共通UI/保存 =====
    function addLog(id, msg, type = 'info') {
      const el = document.getElementById(id);
      const ts = new Date().toLocaleTimeString('ja-JP');
      const div = document.createElement('div');
      const cls = { error: 'error-message', success: 'success-message', info: 'text-slate-100 text-[11px]' }[type] || 'text-slate-100 text-[11px]';
      div.className = cls;
      div.innerHTML = `<span class="opacity-70">[${ts}]</span> ${escapeHtml(msg)}`;
      if (el) {
        el.appendChild(div);
        el.scrollTop = el.scrollHeight;
        while (el.children.length > 100) el.removeChild(el.firstChild);
      }
      try { console.log(`[${ts}] ${msg}`); } catch(_) {}
    }

    function updateStatusIndicators() {
      const botI = document.getElementById('bot-status');
      const botT = document.getElementById('bot-status-text');
      if (botI && botT) {
        const active = botInterval !== null;
        botI.className = `status-indicator ${active ? 'status-active' : 'status-inactive'}`;
        botT.textContent = active ? '動作中' : '停止中';
      }
      const rssI = document.getElementById('rss-status');
      const rssT = document.getElementById('rss-status-text');
      if (rssI && rssT) {
        const active = rssInterval !== null;
        rssI.className = `status-indicator ${active ? 'status-active' : 'status-inactive'}`;
        rssT.textContent = active ? '動作中' : '停止中';
      }
    }

    function showSystemStatus() {
      alert(
`=== Verse システムステータス ===
全体投稿数: ${posts.length}
RSS登録数: ${sharedRssFeeds.length}
BOT投稿数: ${posts.filter(p => ['bot', 'markov'].includes(p.type)).length}

BOT自動投稿: ${botInterval ? '動作中' : '停止中'}
RSS自動投稿: ${rssInterval ? '動作中' : '停止中'}`
      );
    }

    function uploadProfileIcon(e) {
      const f = e.target.files[0];
      if (!f) return;
      if (f.size > 5 * 1024 * 1024) { alert('5MB以下にしてください。'); return; }
      const r = new FileReader();
      r.onload = () => {
        profile.icon = r.result;
        saveProfileNoAlert();
        updateAllUI();
        alert('プロフィール画像更新!');
      };
      r.readAsDataURL(f);
    }

    function saveProfile() {
      const un = document.getElementById('username').value.trim();
      const si = document.getElementById('self-intro').value.trim();
      if (un.length > 20) { alert('ユーザー名は20文字以内で。'); return; }
      profile.username = un || 'ゲストユーザー';
      profile.selfIntro = si;
      saveProfileNoAlert();
      updateAllUI();
      alert('プロフィール保存!');
    }

    function saveProfileNoAlert() {
      users = JSON.parse(localStorage.getItem('verse_users') || '[]');
      const idx = users.findIndex(u => u.email === (currentUser && currentUser.email));
      if (idx >= 0) {
        users[idx].profile = profile;
        localStorage.setItem('verse_users', JSON.stringify(users));
      }
    }

    function updateAllUI() {
      const pi = document.getElementById('profile-icon');
      const hi = document.getElementById('header-profile-icon');
      if (pi) pi.src = profile.icon;
      if (hi) hi.src = profile.icon;

      ['username-preview','header-username'].forEach(id => {
        const el = document.getElementById(id);
        if (el) el.textContent = profile.username;
      });

      const sip = document.getElementById('self-intro-preview');
      if (sip) sip.textContent = profile.selfIntro || 'まだ自己紹介がありません';

      const emailEl = document.getElementById('header-user-email');
      if (emailEl && currentUser) emailEl.textContent = currentUser.email;

      const bi = document.getElementById('bot-icon-preview');
      const fi = document.getElementById('feed-icon-preview');
      if (bi) bi.src = verseIcons.bot;
      if (fi) fi.src = verseIcons.feed;

      renderTimeline();
    }

    function updateCharCount() {
      const pc = document.getElementById('postContent');
      const cc = document.getElementById('char-count');
      if (pc && cc) {
        const l = pc.value.length;
        cc.textContent = `(${l}/500)`;
        cc.style.color = l > 450 ? '#fca5a5' : '';
      }
    }

    function toggleDarkMode() {
      isDarkMode = !isDarkMode;
      if (isDarkMode) {
        document.body.classList.add('dark');
      } else {
        document.body.classList.remove('dark');
      }
      localStorage.setItem('verse_darkMode', isDarkMode.toString());
    }

    function saveData() { localStorage.setItem('verse_posts', JSON.stringify(posts)); }

    // 入力UIフック
    document.addEventListener('DOMContentLoaded', () => {
      const pc = document.getElementById('postContent');
      if (pc) {
        pc.addEventListener('input', updateCharCount);
        pc.addEventListener('keydown', e => { if (e.key === 'Enter' && e.ctrlKey) { e.preventDefault(); createUserPost(); } });
      }
      const ui = document.getElementById('username');
      if (ui) ui.addEventListener('input', () => {
        const v = ui.value.trim() || 'ゲストユーザー';
        ['username-preview','header-username'].forEach(id => {
          const el = document.getElementById(id);
          if (el) el.textContent = v;
        });
      });
      const si = document.getElementById('self-intro');
      if (si) si.addEventListener('input', () => {
        const v = si.value.trim() || 'まだ自己紹介がありません';
        const el = document.getElementById('self-intro-preview');
        if (el) el.textContent = v;
      });

      const ts = document.getElementById('timeline-search');
      if (ts) ts.addEventListener('input', () => {
        currentSearch = ts.value || '';
        renderTimeline();
      });

      setFilter('all');
    });

    window.addEventListener('beforeunload', () => { stopBotAutoPost(); stopRssAuto(); saveData(); });
    window.addEventListener('error', e => { addLog('bot-log', `システムエラー: ${e.message}`, 'error'); });
  </script>
</body>
</html>

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

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・ブランド)

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