ToHeart3 – 未来の約束


『ToHeart3 – 未来の約束』完成しました。

『ToHeart』『ToHeart2』の世界とキャラクターたちを受け継ぎながら、新たな主人公、新たなヒロイン、そして新しい物語を描く長編ノベルゲームです。

テーマは、「思い出と未来、そしてもう一度交わす約束」

懐かしい日常の中で再会する人たち。
新しく出会う仲間たち。
変わってしまったものと、それでも変わらなかった想い。

物語には『ToHeart』『ToHeart2』の歴代キャラクターが登場し、神岸あかり、柚原このみをはじめとしたキャラクターたちと、新たに生まれたオリジナルキャラクターが同じ世界で物語を紡いでいきます。

プレイヤーの選択によってキャラクターとの関係や物語が変化し、それぞれに長編の個別ルートを用意。

楽しい学園生活だけではなく、恋愛、友情、家族、別れ、再会、そして未来へ進んでいくことを描いた作品になっています。

主な特徴

  • 『ToHeart』『ToHeart2』歴代キャラクターが登場
  • 本作オリジナルキャラクターを追加
  • キャラクターごとの立ち絵を実装
  • 長編個別ルートを収録
  • 選択肢によって変化するストーリー
  • 複数のエンディング
  • 日常・恋愛・青春・感動を中心とした物語
  • 過去作品を知っている人が楽しめる再会やクロスオーバー要素
  • 初めてこの世界に触れる人でも楽しめる新規ストーリー

昔、大切だった人。

いつの間にか離れてしまった人。

まだ出会っていない人。

そして、忘れたくない約束。

時間が流れても、人の想いは完全には消えない。

これは、過去を懐かしむだけの物語ではありません。

あの日の思い出を抱えたまま、それでも未来へ進んでいく人たちの物語です。

――いつか交わした約束の、その先へ。

『ToHeart3 – 未来の約束』

ここから、新しい物語が始まります。

https://toheart3-mirai-yakusoku.chodomeyuhei.chatgpt.site

アンリアルエンジン6の新機能

大きな新要素はこのあたりです。

  • Verseが本格的なゲームプログラミング言語になる
    • UE6ではゲームプレイのプログラミングモデルをVerseへ移行。
    • PythonやC#に近い感覚を意識した言語です。
    • C++とも連携します。
    • 将来的には巨大なオンラインゲームを複数サーバーへ自動分散する構想まであります。
  • Scene Graphという新しいゲームプレイフレームワーク
    • Verseベースでゼロから設計。
    • オブジェクトやゲーム機能を「再利用できるコンポーネント」として扱いやすくする方向。
    • EpicはこれをUE6の新しいゲーム制作基盤にしようとしています。
  • UE5とUEFNが統合される
    • これがかなり大きいです。
    • UE5のAAAゲーム制作能力+Fortnite/UEFNで培ったライブゲーム開発環境を、1つのUnreal Engine 6にまとめます。
    • 普通のPC/PS5/Xbox向けゲームとして出すことも、Fortnite向けに出すことも、自前のオンライン環境に出すことも想定されています。
  • AI開発支援がエンジン側に入る
    • MCP(Model Context Protocol)対応。
    • EpicはClaude、Geminiなどとの統合を明言しています。
    • コード生成だけではなく、制作パイプラインそのものをAIで効率化する方向です。
    ここは今Codexをゲーム開発に使う感覚にかなり近い方向性ですね。将来的には「UEエディタ+AIエージェント」が普通になる可能性があります。
  • ゲーム間でコンテンツやコードを持ち運べる方向へ
    • glTFやUSDなどのオープン標準を重視。
    • コンテンツ・コード・ゲーム内経済などを、ゲームやエンジンをまたいで相互運用できる仕組みを目指しています。

そして気になるBlueprintですが、ここはかなり重要です。

UE6初期版ではActorとBlueprintは残ります。

いきなりBlueprintが消えるわけではありません。ただしEpicは、Scene Graph+Verseが十分成熟した段階で、Actor/Blueprintを最終的には非推奨にする計画を明言しています。その際は既存プロジェクトを新方式へ移す変換ツールも用意する方針です。

なので今Blueprintを勉強していることが無駄になるわけではありません。UE5プロジェクトについてもEpicはUE6へ「ハードな断絶を強制しない」方針です。

リリース予定

UE6 Early Access:2027年末予定

その後、

正式版:Early Accessから12〜18か月後

なので、順調なら正式版は2028年末〜2029年前半あたりになります。現在のUE5.8は「予定上最後のUE5メジャーリリース」で、必要なら5.9を出す可能性も残されています。

Epic公式「Unreal Engine 6への道」

ゲーム開発という観点では、UE5:Blueprint中心 → UE6:Verse+Scene Graph+AI支援中心という移行が一番大きそうです。特に一人開発ではAI統合の部分がかなり面白いところです。

star_breaker_stg_stage5_power_barrage.html

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>STAR BREAKER - 5 STAGE STG</title>
<style>
:root{--cyan:#55efff;--pink:#ff4fd8;--yellow:#ffe66d;--bg:#02040e}
*{box-sizing:border-box}
html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#02040e;color:#fff;font-family:system-ui,-apple-system,"Yu Gothic UI",sans-serif}
body{display:grid;place-items:center;background:radial-gradient(circle at 50% 20%,#17265e,#050817 50%,#010208)}
#wrap{position:relative;width:min(100vw,900px);height:min(100vh,960px);aspect-ratio:3/4;overflow:hidden;border:1px solid #55efff55;box-shadow:0 0 36px #55efff22;background:#02040e}
canvas{display:block;width:100%;height:100%;touch-action:none;cursor:crosshair}
#hud{position:absolute;inset:0;pointer-events:none;font-weight:900;text-shadow:0 0 8px #55efff}
.top{position:absolute;top:12px;left:16px;right:16px;display:flex;justify-content:space-between;gap:12px;font-size:clamp(12px,2vw,19px)}
#stageLabel{position:absolute;top:45px;left:0;right:0;text-align:center;color:#bff8ff;letter-spacing:.22em}
#bossWrap{display:none;position:absolute;top:73px;left:9%;width:82%}
#bossName{text-align:center;font-size:13px;margin-bottom:5px;color:#ffd4f5}
.bar{height:13px;border:1px solid #ffffffaa;background:#ffffff12}
#bossHp{height:100%;width:100%;background:linear-gradient(90deg,#ff4fd8,#ff9a3d)}
.overlay{position:absolute;inset:0;display:grid;place-items:center;background:linear-gradient(#02040e33,#02040ecc);z-index:5}
.panel{width:min(88%,650px);padding:clamp(22px,5vw,44px);text-align:center;background:#081027e8;border:1px solid #55efff88;box-shadow:0 0 28px #55efff33;backdrop-filter:blur(7px)}
h1{margin:0 0 10px;font-size:clamp(38px,8vw,76px);line-height:.9;color:#fff;text-shadow:0 0 9px #fff,0 0 22px #55efff,4px 4px 0 #ff4fd8}
.sub{color:#8deeff;letter-spacing:.25em;margin-bottom:22px;font-weight:800}
#message{line-height:1.8;color:#ddebff}
button{margin:20px 7px 0;padding:14px 28px;border:0;border-radius:999px;color:#fff;font-weight:900;font-size:17px;cursor:pointer;background:linear-gradient(90deg,#55efff,#7277ff,#ff4fd8);box-shadow:0 0 18px #55efff66}
button.secondary{background:#252c46;box-shadow:none;border:1px solid #ffffff44}
button:hover{filter:brightness(1.15);transform:scale(1.03)}
.small{margin-top:18px;padding-top:14px;border-top:1px solid #ffffff22;color:#b7c9dd;font-size:13px;line-height:1.7}
#mobile{position:absolute;left:0;right:0;bottom:12px;display:none;justify-content:space-between;padding:0 14px;pointer-events:none;z-index:4}
.group{display:flex;gap:8px;pointer-events:auto}.touch{width:58px;height:58px;margin:0;padding:0;border-radius:50%;opacity:.62;font-size:20px}
@media(pointer:coarse){#mobile{display:flex}}
</style>
</head>
<body>
<div id="wrap">
<canvas id="game" width="720" height="960"></canvas>

<div id="hud">
  <div class="top">
    <div>SCORE <span id="score">0000000</span></div>
    <div>HI <span id="high">0000000</span></div>
    <div>POWER <span id="power">1</span></div>
  </div>
  <div id="stageLabel">STAGE 1 / 5</div>
  <div id="bossWrap">
    <div id="bossName">BOSS</div>
    <div class="bar"><div id="bossHp"></div></div>
  </div>
</div>

<div id="overlay" class="overlay">
  <div class="panel">
    <h1 id="title">STAR<br>BREAKER</h1>
    <div class="sub" id="subtitle">5 STAGE MISSION</div>
    <div id="message">全5ステージを突破し、最終要塞を破壊せよ。<br>敵弾に一発でも当たると爆発してゲームオーバー。<br>POWERアイテムでショットを最大5段階まで強化。</div>
    <div id="buttons">
      <button id="startBtn">GAME START</button>
    </div>
    <div class="small">PC:WASD / 矢印キー / マウスドラッグで移動 | P:一時停止<br>スマホ:画面ドラッグ、または下の移動ボタン</div>
  </div>
</div>

<div id="mobile">
  <div class="group"><button class="touch" data-key="left">◀</button><button class="touch" data-key="right">▶</button></div>
  <div class="group"><button class="touch" data-key="up">▲</button><button class="touch" data-key="down">▼</button></div>
</div>
</div>

<script>
(()=>{
"use strict";
const canvas=document.getElementById("game"),ctx=canvas.getContext("2d");
const W=720,H=960;
const scoreEl=document.getElementById("score"),highEl=document.getElementById("high"),powerEl=document.getElementById("power");
const stageLabel=document.getElementById("stageLabel"),bossWrap=document.getElementById("bossWrap"),bossName=document.getElementById("bossName"),bossHp=document.getElementById("bossHp");
const overlay=document.getElementById("overlay"),title=document.getElementById("title"),subtitle=document.getElementById("subtitle"),message=document.getElementById("message"),buttons=document.getElementById("buttons");

let running=false,paused=false,dead=false,last=0,time=0,score=0,high=Number(localStorage.getItem("starBreaker5High")||0);
let stage=1,stageTime=0,waveTimer=0,wavesSpawned=0,bossActive=false,transition=false,shake=0,flash=0;
let pointer=false;
const keys={},bullets=[],enemyBullets=[],enemies=[],particles=[],powerups=[],stars=[];

const stageData=[
 {name:"ORBITAL FRONT",duration:23,boss:"IRON TALON",hp:260,bg:["#07112c","#02040e"]},
 {name:"CRIMSON NEBULA",duration:25,boss:"SCARLET MANTA",hp:380,bg:["#2b071c","#07030b"]},
 {name:"ASTEROID BELT",duration:27,boss:"GRAVITY GOLEM",hp:520,bg:["#241b10","#050403"]},
 {name:"VOID TEMPLE",duration:29,boss:"ABYSS SERAPH",hp:700,bg:["#16072a","#030109"]},
 {name:"FINAL FORTRESS",duration:32,boss:"OMEGA EMPRESS",hp:980,bg:["#051b26","#010408"]}
];

const player={x:W/2,y:H-120,r:11,speed:410,cool:0,rate:.12,power:1,maxPower:5,inv:0,tilt:0};
for(let i=0;i<150;i++)stars.push({x:Math.random()*W,y:Math.random()*H,z:Math.random()*2.5+.5,a:Math.random()*.8+.2});

function clearAll(){bullets.length=enemyBullets.length=enemies.length=particles.length=powerups.length=0}
function updateHUD(){
 scoreEl.textContent=String(Math.floor(score)).padStart(7,"0");
 highEl.textContent=String(Math.floor(high)).padStart(7,"0");
 powerEl.textContent=player.power>=player.maxPower?"MAX":player.power;
 stageLabel.textContent=`STAGE ${stage} / 5 ${stageData[stage-1].name}`;
}
function reset(){
 clearAll();stage=1;stageTime=0;waveTimer=0;wavesSpawned=0;bossActive=false;transition=false;dead=false;paused=false;
 score=0;time=0;shake=0;flash=0;player.x=W/2;player.y=H-120;player.cool=0;player.power=1;player.inv=1;
 bossWrap.style.display="none";updateHUD();
}
function begin(){
 reset();running=true;overlay.style.display="none";last=performance.now();requestAnimationFrame(loop);
}
function addScore(n){
 score+=n;if(score>high){high=score;localStorage.setItem("starBreaker5High",String(Math.floor(high)))}updateHUD();
}
function burst(x,y,color,count=25,speed=260){
 for(let i=0;i<count;i++){const a=Math.random()*Math.PI*2,s=Math.random()*speed+40;
 particles.push({x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s,life:Math.random()*.7+.3,max:1,size:Math.random()*6+2,color});
 }}
function shoot(){
 let shots=[];
 if(player.power===1)shots=[{a:0,x:0,d:1}];
 if(player.power===2)shots=[{a:-.10,x:-7,d:1},{a:.10,x:7,d:1}];
 if(player.power===3)shots=[{a:-.18,x:-10,d:1},{a:0,x:0,d:2},{a:.18,x:10,d:1}];
 if(player.power===4)shots=[{a:-.25,x:-15,d:2},{a:-.08,x:-5,d:2},{a:.08,x:5,d:2},{a:.25,x:15,d:2}];
 if(player.power>=5)shots=[
   {a:-.31,x:-18,d:2},{a:-.16,x:-10,d:2},{a:0,x:0,d:3},
   {a:.16,x:10,d:2},{a:.31,x:18,d:2}
 ];
 shots.forEach(s=>bullets.push({
   x:player.x+s.x,y:player.y-20,
   vx:Math.sin(s.a)*230,vy:-820,
   r:s.d>=3?7:5,dmg:s.d,power:player.power
 }));
 player.rate=Math.max(.065,.13-player.power*.013);
}
function spawnEnemy(type,x,y){
 const s=stage;
 if(type==="scout")enemies.push({type,x,y,r:19,hp:1+s,maxHp:1+s,vx:(Math.random()-.5)*130,vy:105+s*11,age:0,shot:1.2+Math.random()});
 if(type==="zig")enemies.push({type,x,y,r:22,hp:3+s,maxHp:3+s,vx:0,vy:80+s*7,age:Math.random()*5,shot:.9+Math.random()});
 if(type==="tank")enemies.push({type,x,y,r:32,hp:11+s*4,maxHp:11+s*4,vx:0,vy:42+s*3,age:0,shot:.7});
}
function spawnWave(){
 wavesSpawned++;
 const pattern=(wavesSpawned+stage)%4;
 if(pattern===0){for(let i=0;i<7;i++)spawnEnemy("scout",75+i*95,-40-i*34)}
 if(pattern===1){for(let i=0;i<6;i++)spawnEnemy("zig",90+i*108,-45-i*70)}
 if(pattern===2){spawnEnemy("tank",W/2,-70);for(let i=0;i<4;i++)spawnEnemy("scout",120+i*160,-160-i*40)}
 if(pattern===3){for(let i=0;i<3;i++){spawnEnemy("zig",170+i*190,-60-i*80);spawnEnemy("scout",90+i*250,-190-i*50)}}
}
function spawnBoss(){
 bossActive=true;enemyBullets.length=0;
 const d=stageData[stage-1];
 enemies.push({type:"boss",x:W/2,y:-130,r:82,hp:d.hp,maxHp:d.hp,age:0,shot:.6,stage,vy:48});
 bossName.textContent=d.boss;bossHp.style.width="100%";bossWrap.style.display="block";
}
function addEnemyBullet(x,y,a,speed,r=7,color="#ff8ae7"){
 enemyBullets.push({x,y,vx:Math.cos(a)*speed,vy:Math.sin(a)*speed,r,color});
}
function enemyShoot(e){
 const aim=Math.atan2(player.y-e.y,player.x-e.x);
 const base=145+stage*18;

 if(e.type==="scout"){
   if(stage<=2){
     for(let i=-1;i<=1;i++)addEnemyBullet(e.x,e.y,aim+i*.12,base,6,"#ff8ae7");
   }else{
     for(let i=-2;i<=2;i++)addEnemyBullet(e.x,e.y,aim+i*.13,base+12,6,"#ff8ae7");
   }
 }

 if(e.type==="zig"){
   const count=5+stage;
   for(let i=0;i<count;i++){
     const a=(i/(count-1)-.5)*1.15+Math.PI/2;
     addEnemyBullet(e.x,e.y,a,145+stage*12,6,"#9d8cff");
   }
   addEnemyBullet(e.x,e.y,aim,210+stage*8,5,"#ffffff");
 }

 if(e.type==="tank"){
   const n=10+stage*2;
   for(let i=0;i<n;i++){
     const a=i*Math.PI*2/n+e.age*.32;
     addEnemyBullet(e.x,e.y,a,125+stage*10,7,"#ffb25f");
   }
   for(let i=-2;i<=2;i++)addEnemyBullet(e.x,e.y,aim+i*.15,190+stage*10,6,"#fff0a8");
 }

 if(e.type==="boss"){
   const ratio=e.hp/e.maxHp;

   if(e.stage===1){
     for(let ring=0;ring<2;ring++){
       for(let i=-5;i<=5;i++){
         const a=aim+i*.12+(ring?0.055:0);
         addEnemyBullet(e.x,e.y+35,a,195+ring*45,7,ring?"#ffd1f4":"#ff78d8");
       }
     }
   }

   if(e.stage===2){
     const n=22;
     for(let i=0;i<n;i++){
       const a=i*Math.PI*2/n+e.age*.62;
       addEnemyBullet(e.x,e.y,a,155+(i%2)*42,7,i%2?"#ff7ac8":"#a884ff");
     }
     for(let i=-2;i<=2;i++)addEnemyBullet(e.x,e.y+25,aim+i*.13,235,6,"#ffffff");
   }

   if(e.stage===3){
     for(let ring=0;ring<3;ring++){
       const n=12+ring*4;
       for(let i=0;i<n;i++){
         const a=i*Math.PI*2/n+e.age*(ring%2?-.42:.42);
         addEnemyBullet(e.x,e.y,a,115+ring*42,8-ring,"#ffb35f");
       }
     }
   }

   if(e.stage===4){
     for(let side=-1;side<=1;side+=2){
       for(let i=0;i<9;i++){
         const a=aim+side*(.08+i*.085)+Math.sin(e.age*1.8)*.18;
         addEnemyBullet(e.x+side*45,e.y+20,a,205+i*6,6,"#b477ff");
       }
     }
     if(ratio<.5){
       for(let i=0;i<18;i++){
         const a=i*Math.PI/9-e.age*.55;
         addEnemyBullet(e.x,e.y,a,175,7,"#ff70dd");
       }
     }
   }

   if(e.stage===5){
     const n=ratio>.6?28:38;
     for(let i=0;i<n;i++){
       const a=i*Math.PI*2/n+e.age*.78;
       addEnemyBullet(e.x,e.y,a,150+(i%4)*28,7,i%2?"#ff4fd8":"#65eaff");
     }
     for(let i=-5;i<=5;i++)addEnemyBullet(e.x,e.y+42,aim+i*.095,250,6,"#fff2a8");
     if(ratio<.45){
       for(let i=0;i<20;i++){
         const a=i*Math.PI/10-e.age*1.05;
         addEnemyBullet(e.x,e.y,a,225,5,"#ffffff");
       }
     }
   }
 }
}
function collide(a,b){const dx=a.x-b.x,dy=a.y-b.y;return dx*dx+dy*dy<(a.r+b.r)*(a.r+b.r)}
function gameOver(){
 if(dead)return;dead=true;running=false;shake=34;flash=.75;
 burst(player.x,player.y,"#ff4fd8",120,430);burst(player.x,player.y,"#ffe66d",80,360);
 draw();
 setTimeout(()=>{
  title.innerHTML="GAME<br>OVER";subtitle.textContent="SHIP DESTROYED";
  message.innerHTML=`敵弾に命中し、機体が爆発した。<br>SCORE:<b>${Math.floor(score).toLocaleString()}</b> STAGE:<b>${stage}</b>`;
  buttons.innerHTML='<button id="retryBtn">リトライ</button><button id="quitBtn" class="secondary">やめる</button>';
  overlay.style.display="grid";
  document.getElementById("retryBtn").addEventListener("click",begin);
  document.getElementById("quitBtn").addEventListener("click",quitGame);
 },650);
}
function quitGame(){
 clearAll();dead=false;bossWrap.style.display="none";
 title.innerHTML="STAR<br>BREAKER";subtitle.textContent="MISSION ABORTED";
 message.innerHTML="ゲームを終了しました。<br>もう一度遊ぶときはスタートを押してください。";
 buttons.innerHTML='<button id="againBtn">GAME START</button>';
 document.getElementById("againBtn").addEventListener("click",begin);
 draw();
}
function stageClear(){
 transition=true;bossActive=false;bossWrap.style.display="none";enemyBullets.length=0;clearAll();
 addScore(2500*stage);
 if(stage===5){running=false;title.innerHTML="ALL<br>CLEAR";subtitle.textContent="OMEGA FORTRESS DESTROYED";
  message.innerHTML=`全5ステージ制覇!<br>FINAL SCORE:<b>${Math.floor(score).toLocaleString()}</b>`;
  buttons.innerHTML='<button id="retryAll">もう一度遊ぶ</button><button id="quitAll" class="secondary">やめる</button>';
  overlay.style.display="grid";
  document.getElementById("retryAll").addEventListener("click",begin);
  document.getElementById("quitAll").addEventListener("click",quitGame);
  return;
 }
 running=false;title.innerHTML=`STAGE ${stage}<br>CLEAR`;subtitle.textContent=stageData[stage-1].boss+" DESTROYED";
 message.innerHTML=`次は STAGE ${stage+1}「${stageData[stage].name}」`;
 buttons.innerHTML='<button id="nextStage">NEXT STAGE</button>';
 overlay.style.display="grid";
 document.getElementById("nextStage").addEventListener("click",()=>{
   stage++;stageTime=0;waveTimer=0;wavesSpawned=0;transition=false;player.x=W/2;player.y=H-120;player.inv=1.5;
   updateHUD();overlay.style.display="none";running=true;last=performance.now();requestAnimationFrame(loop);
 });
}
function update(dt){
 time+=dt;stageTime+=dt;if(player.inv>0)player.inv-=dt;if(shake>0)shake=Math.max(0,shake-dt*55);if(flash>0)flash=Math.max(0,flash-dt*1.7);
 stars.forEach(s=>{s.y+=(55+s.z*65+stage*8)*dt;if(s.y>H){s.y=-5;s.x=Math.random()*W}});
 let dx=0,dy=0;if(keys.ArrowLeft||keys.a||keys.left)dx--;if(keys.ArrowRight||keys.d||keys.right)dx++;if(keys.ArrowUp||keys.w||keys.up)dy--;if(keys.ArrowDown||keys.s||keys.down)dy++;
 const l=Math.hypot(dx,dy)||1;dx/=l;dy/=l;player.x+=dx*player.speed*dt;player.y+=dy*player.speed*dt;player.x=Math.max(25,Math.min(W-25,player.x));player.y=Math.max(95,Math.min(H-35,player.y));player.tilt+=(dx*.45-player.tilt)*Math.min(1,dt*10);
 player.cool-=dt;if(player.cool<=0){shoot();player.cool=player.rate}
 if(!bossActive){
   waveTimer-=dt;if(waveTimer<=0){spawnWave();waveTimer=Math.max(2.5,4.6-stage*.28)}
   if(stageTime>=stageData[stage-1].duration)spawnBoss();
 }
 for(let i=bullets.length-1;i>=0;i--){const b=bullets[i];b.x+=b.vx*dt;b.y+=b.vy*dt;if(b.y<-30||b.x<-30||b.x>W+30)bullets.splice(i,1)}
 for(let i=enemyBullets.length-1;i>=0;i--){const b=enemyBullets[i];b.x+=b.vx*dt;b.y+=b.vy*dt;if(b.y>H+50||b.y<-60||b.x<-60||b.x>W+60){enemyBullets.splice(i,1);continue}if(player.inv<=0&&collide(b,player)){enemyBullets.splice(i,1);gameOver();return}}
 for(let i=enemies.length-1;i>=0;i--){const e=enemies[i];e.age+=dt;
   if(e.type==="scout"){e.x+=e.vx*dt;e.y+=e.vy*dt;if(e.x<30||e.x>W-30)e.vx*=-1}
   if(e.type==="zig"){e.y+=e.vy*dt;e.x+=Math.sin(e.age*2.5)*125*dt}
   if(e.type==="tank"){e.y+=e.vy*dt;e.x=W/2+Math.sin(e.age*.85)*220}
   if(e.type==="boss"){if(e.y<165)e.y+=e.vy*dt;else e.x=W/2+Math.sin(e.age*(.55+e.stage*.06))*245;bossHp.style.width=Math.max(0,e.hp/e.maxHp*100)+"%"}
   e.shot-=dt;if(e.shot<=0&&e.y>0){enemyShoot(e);e.shot=e.type==="boss"?Math.max(.22,.62-stage*.065):e.type==="tank"?1.08:e.type==="zig"?.92:1.12}
   for(let j=bullets.length-1;j>=0;j--){const b=bullets[j];if(collide(b,e)){e.hp-=b.dmg;bullets.splice(j,1);burst(b.x,b.y,"#ffe66d",4,80);
     if(e.hp<=0){const boss=e.type==="boss";burst(e.x,e.y,boss?"#ff4fd8":"#55efff",boss?130:28,boss?430:230);shake=boss?30:7;
       addScore(boss?5000*stage:e.type==="tank"?550:e.type==="zig"?220:100);
       if(!boss){
         const dropRate=e.type==="tank"?.60:e.type==="zig"?.32:.20;
         if(Math.random()<dropRate)powerups.push({x:e.x,y:e.y,r:15,vy:82,spin:0});
       }
       enemies.splice(i,1);if(boss){enemyBullets.length=0;setTimeout(stageClear,550)}break;
     }}}
   if(i<enemies.length&&e.y>H+100)enemies.splice(i,1);
   else if(i<enemies.length&&player.inv<=0&&collide(e,player)){gameOver();return}
 }
 for(let i=powerups.length-1;i>=0;i--){const p=powerups[i];p.y+=p.vy*dt;p.spin+=dt*4;if(collide(p,player)){player.power=Math.min(player.maxPower,player.power+1);addScore(player.power>=player.maxPower?800:400);burst(p.x,p.y,"#ffe66d",20,190);powerups.splice(i,1);updateHUD()}else if(p.y>H+30)powerups.splice(i,1)}
 for(let i=particles.length-1;i>=0;i--){const p=particles[i];p.x+=p.vx*dt;p.y+=p.vy*dt;p.vx*=.984;p.vy*=.984;p.life-=dt;if(p.life<=0)particles.splice(i,1)}
}
function drawShip(){
 ctx.save();ctx.translate(player.x,player.y);ctx.rotate(player.tilt);if(player.inv>0&&Math.floor(player.inv*13)%2===0)ctx.globalAlpha=.3;
 ctx.shadowBlur=22;ctx.shadowColor="#55efff";ctx.fillStyle="#e9fdff";ctx.beginPath();ctx.moveTo(0,-27);ctx.lineTo(18,19);ctx.lineTo(0,10);ctx.lineTo(-18,19);ctx.closePath();ctx.fill();
 ctx.fillStyle="#ff4fd8";ctx.beginPath();ctx.moveTo(0,-13);ctx.lineTo(7,11);ctx.lineTo(-7,11);ctx.closePath();ctx.fill();
 ctx.shadowColor="#ff9a3d";ctx.fillStyle="#fff";ctx.beginPath();ctx.moveTo(-7,15);ctx.lineTo(0,35+Math.random()*9);ctx.lineTo(7,15);ctx.closePath();ctx.fill();ctx.restore();
}
function drawEnemy(e){
 ctx.save();ctx.translate(e.x,e.y);
 if(e.type==="boss"){ctx.rotate(Math.sin(e.age*.8)*.07);ctx.shadowBlur=28;ctx.shadowColor="#ff4fd8";ctx.fillStyle=`hsl(${305-e.stage*18} 65% 22%)`;ctx.beginPath();ctx.moveTo(0,-72);ctx.lineTo(96,-18);ctx.lineTo(65,60);ctx.lineTo(0,36);ctx.lineTo(-65,60);ctx.lineTo(-96,-18);ctx.closePath();ctx.fill();ctx.strokeStyle="#ff76df";ctx.lineWidth=5;ctx.stroke();ctx.fillStyle="#fff";ctx.beginPath();ctx.arc(0,-4,25,0,Math.PI*2);ctx.fill();ctx.fillStyle="#ff4fd8";ctx.beginPath();ctx.arc(0,-4,11,0,Math.PI*2);ctx.fill()}
 else{const c=e.type==="tank"?"#ff9a3d":e.type==="zig"?"#a873ff":"#55efff";ctx.shadowBlur=16;ctx.shadowColor=c;ctx.fillStyle=c;if(e.type==="tank"){ctx.beginPath();ctx.roundRect(-29,-24,58,48,12);ctx.fill();ctx.fillStyle="#fff";ctx.fillRect(-8,-31,16,24)}else{ctx.rotate(Math.PI);ctx.beginPath();ctx.moveTo(0,-24);ctx.lineTo(22,18);ctx.lineTo(0,8);ctx.lineTo(-22,18);ctx.closePath();ctx.fill();ctx.fillStyle="#fff";ctx.beginPath();ctx.arc(0,2,6,0,Math.PI*2);ctx.fill()}}
 ctx.restore();
}
function draw(){
 const bg=stageData[Math.max(0,stage-1)].bg,g=ctx.createLinearGradient(0,0,0,H);g.addColorStop(0,bg[0]);g.addColorStop(1,bg[1]);ctx.fillStyle=g;ctx.fillRect(0,0,W,H);
 ctx.save();if(shake>0)ctx.translate((Math.random()-.5)*shake,(Math.random()-.5)*shake);
 stars.forEach(s=>{ctx.globalAlpha=s.a;ctx.fillStyle="#fff";ctx.fillRect(s.x,s.y,s.z,s.z*3)});ctx.globalAlpha=1;
 for(const b of bullets){ctx.shadowBlur=15;ctx.shadowColor="#55efff";ctx.fillStyle="#efffff";ctx.fillRect(b.x-3,b.y-13,6,24)}ctx.shadowBlur=0;
 for(const b of enemyBullets){ctx.shadowBlur=14;ctx.shadowColor=b.color||"#ff4fd8";ctx.fillStyle=b.color||"#ff8ae7";ctx.beginPath();ctx.arc(b.x,b.y,b.r,0,Math.PI*2);ctx.fill()}ctx.shadowBlur=0;
 enemies.forEach(drawEnemy);
 powerups.forEach(p=>{ctx.save();ctx.translate(p.x,p.y);ctx.rotate(p.spin);ctx.shadowBlur=18;ctx.shadowColor="#ffe66d";ctx.fillStyle="#ffe66d";ctx.beginPath();for(let i=0;i<8;i++){const a=i*Math.PI/4,r=i%2?7:15;ctx.lineTo(Math.cos(a)*r,Math.sin(a)*r)}ctx.closePath();ctx.fill();ctx.restore()});
 particles.forEach(p=>{ctx.globalAlpha=Math.max(0,p.life/p.max);ctx.fillStyle=p.color;ctx.fillRect(p.x,p.y,p.size,p.size)});ctx.globalAlpha=1;
 if(!dead)drawShip();
 if(paused){ctx.fillStyle="#0009";ctx.fillRect(0,0,W,H);ctx.fillStyle="#fff";ctx.textAlign="center";ctx.font="bold 54px system-ui";ctx.fillText("PAUSE",W/2,H/2)}
 if(flash>0){ctx.globalAlpha=flash;ctx.fillStyle="#fff";ctx.fillRect(0,0,W,H);ctx.globalAlpha=1}
 ctx.restore();
}
function loop(now){if(!running)return;const dt=Math.min(.033,(now-last)/1000);last=now;if(!paused)update(dt);draw();if(running)requestAnimationFrame(loop)}
function setPointer(e){const r=canvas.getBoundingClientRect();player.x=(e.clientX-r.left)/r.width*W;player.y=(e.clientY-r.top)/r.height*H}
addEventListener("keydown",e=>{keys[e.key]=true;keys[e.key.toLowerCase()]=true;if(e.key.toLowerCase()==="p"&&running)paused=!paused;if(["ArrowUp","ArrowDown","ArrowLeft","ArrowRight"," "].includes(e.key))e.preventDefault()});
addEventListener("keyup",e=>{keys[e.key]=false;keys[e.key.toLowerCase()]=false});
canvas.addEventListener("pointerdown",e=>{pointer=true;setPointer(e)});canvas.addEventListener("pointermove",e=>{if(pointer)setPointer(e)});addEventListener("pointerup",()=>pointer=false);
document.querySelectorAll(".touch").forEach(b=>{const k=b.dataset.key;b.addEventListener("pointerdown",e=>{e.preventDefault();keys[k]=true});b.addEventListener("pointerup",()=>keys[k]=false);b.addEventListener("pointercancel",()=>keys[k]=false)});
document.getElementById("startBtn").addEventListener("click",begin);
highEl.textContent=String(high).padStart(7,"0");updateHUD();draw();
})();
</script>
</body>
</html>

「Gジェネレーション クロスブースト」ゲーム企画書

「Gジェネレーション クロスブースト」ゲーム企画書

タイトル

SDガンダム ジージェネレーション クロスブースト


ジャンル

  • ガンダム戦略シミュレーションRPG
  • 育成 × クロスオーバー × リアルタイム演出

コンセプト

歴代ガンダム作品の世界を越え、
異なる時代・勢力・機体・パイロットが交差する――

「限界突破(クロスブースト)」 をテーマにした
新世代Gジェネレーション。

従来のターン制戦略に加え、

  • ブースト覚醒
  • 合体戦術
  • リンク必殺技
  • 巨大ボスレイド
  • AI戦艦支援

などを搭載し、
“育成の気持ちよさ” と “戦闘演出の熱さ” を極限まで強化する。


世界観

宇宙世紀、アナザー、未来世紀、厄祭戦…

全ガンダム世界を繋ぐ謎の次元崩壊
「クロスブレイク」 が発生。

時空が混ざり合い、

  • アムロとキラ
  • 刹那とヒイロ
  • バナージとシーブック

など、本来出会わない戦士たちが共闘する。

しかしその裏では、
全時代を支配しようとする
謎の存在「ゼロ・オリジン」が動き始めていた。


ゲームシステム

1. クロスブーストシステム

本作最大の特徴。

戦闘中にテンションMAXで発動。

機体性能が大幅上昇し、

  • 攻撃回数増加
  • MAP兵器強化
  • EN消費軽減
  • 演出変化
  • 専用BGM

などが発生。


機動戦士ガンダムSEED DESTINY

  • ストライクフリーダムガンダム
    → スーパードラグーン全展開

機動戦士ガンダム00

  • ダブルオーライザー
    → TRANS-AM BURST発動

機動戦士ガンダムUC

  • ユニコーンガンダム
    → 覚醒モード変化

2. クロスリンク必殺技

特定キャラ同士で超演出発動。

アムロ × カミーユ × ジュドー

「宇宙世紀の光」

キラ × 刹那

「未来を切り開く力」

ヒイロ × 刹那

「ゼロとイノベイター」

超高火力+特殊カットイン。


3. 超大型レイドボス

最大8部隊で挑む。

レイド候補

  • デビルガンダム最終形態
  • ネオ・ジオング
  • ELSクアンタ暴走体
  • ターンX完全覚醒
  • グレートジオング
  • オリジナル巨大MA

4. 戦艦AI支援システム

艦長によって能力が変化。

オットー艦長

  • EN回復
  • 命中支援
  • UC系強化

ブライト艦長

  • 全体士気アップ
  • 回避率上昇

マリュー艦長

  • SEED系強化

5. SSP・ULT進化システム

既存のSP化をさらに進化。

SSP

  • 限界突破
  • 新武装追加
  • カットイン追加

ULT

最終覚醒段階。

  • 機体外観変化
  • 専用BGM
  • EX必殺技
  • 特殊台詞追加

ストーリーモード

Chapter 1

「崩壊する宇宙世紀」

Chapter 2

「アナザー世界侵食」

Chapter 3

「鉄血とSEEDの衝突」

Chapter 4

「ELS襲来」

Chapter 5

「∀の黒歴史」

Final

「全てを超えるガンダム」


オリジナル主人公

男主人公

レイン・クロス

  • 次元適応能力を持つ青年

女主人公

セリア・ノヴァ

  • AIと共鳴する少女

プレイヤー選択式。


オリジナル機体

クロスガンダム・ゼロ

特徴:

  • 全作品技術融合
  • フェーズ変形
  • 光の翼
  • GNドライブ
  • サイコミュ
  • 月光蝶

ULT形態:

「クロスガンダム・インフィニティ」


PvP・オンライン

マスターリーグ

  • 全国ランキング
  • AI防衛編成

ギルド戦

  • 20vs20

ワールドレイド

全ユーザー共闘。


ガシャシステム

Ver.シリーズ

Ver.1

宇宙世紀

Ver.2

アナザー系

Ver.3

神話級限定


ハーフアニバーサリー案

目玉機体候補

  • ターンエーガンダム
  • ターンX
  • V2アサルトバスターガンダム
  • クロスボーン・ガンダムX1フルクロス
  • ダブルオークアンタ

UIイメージ

ホーム画面

  • 3D機体表示
  • ライブ背景
  • AIナビキャラ

編成画面

  • ドラッグ編成
  • タグ自動表示
  • 相性可視化

戦闘画面

  • フル3D演出
  • カメラズーム
  • 必殺技シネマ

BGM

  • 原作曲アレンジ
  • オーケストラ版
  • 覚醒専用BGM

ターゲット層

  • Gジェネファン
  • ガンダムファン
  • 育成ゲーム好き
  • 戦略RPG好き
  • ソシャゲユーザー

キャッチコピー

「限界を超えろ、全てのガンダム。」

「時代を越えた覚醒が始まる。」

「クロスしろ、運命を。」

STGGAME.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Bullet Hell STG Game</title>
  <style>
    * { box-sizing: border-box; }

    body {
      margin: 0;
      min-height: 100vh;
      background: radial-gradient(circle at top, #1f2b5c, #070915 70%);
      display: flex;
      align-items: center;
      justify-content: center;
      font-family: system-ui, sans-serif;
      color: white;
      overflow: hidden;
    }

    .wrap { text-align: center; }

    h1 {
      margin: 0 0 10px;
      font-size: 28px;
      letter-spacing: 0.08em;
    }

    canvas {
      background: #050816;
      border: 3px solid #ffffff33;
      border-radius: 16px;
      box-shadow: 0 20px 80px #000a;
      display: block;
    }

    .info {
      margin-top: 10px;
      color: #dce6ff;
      font-size: 14px;
    }

    .panel {
      position: fixed;
      top: 16px;
      left: 50%;
      transform: translateX(-50%);
      display: flex;
      gap: 20px;
      background: #0008;
      border: 1px solid #fff2;
      padding: 8px 16px;
      border-radius: 999px;
      backdrop-filter: blur(8px);
      font-weight: 700;
    }
  </style>
</head>
<body>
  <div class="panel">
    <div>Score: <span id="score">0</span></div>
    <div>HP: <span id="hp">5</span></div>
    <div>Power: <span id="power">1</span></div>
  </div>

  <div class="wrap">
    <h1>BULLET STORM</h1>
    <canvas id="game" width="480" height="640"></canvas>
    <div class="info">移動: WASD / 方向キー ショット: Space パワーアップを取ると弾が強化 リスタート: Enter</div>
  </div>

  <script>
    const canvas = document.getElementById("game");
    const ctx = canvas.getContext("2d");
    const scoreEl = document.getElementById("score");
    const hpEl = document.getElementById("hp");
    const powerEl = document.getElementById("power");

    const keys = {};

    const player = {
      x: canvas.width / 2,
      y: canvas.height - 70,
      w: 34,
      h: 42,
      speed: 5,
      hp: 5,
      power: 1,
      shotCooldown: 0,
      invincible: 0
    };

    let bullets = [];
    let enemyBullets = [];
    let enemies = [];
    let items = [];
    let particles = [];
    let stars = [];
    let score = 0;
    let enemyTimer = 0;
    let itemTimer = 0;
    let gameOver = false;
    let boss = null;
    let bossTimer = 0;
    let frame = 0;

    for (let i = 0; i < 100; i++) {
      stars.push({
        x: Math.random() * canvas.width,
        y: Math.random() * canvas.height,
        r: Math.random() * 2 + 0.5,
        speed: Math.random() * 2 + 1
      });
    }

    window.addEventListener("keydown", (e) => {
      keys[e.key.toLowerCase()] = true;
      if (gameOver && e.key === "Enter") restart();
    });

    window.addEventListener("keyup", (e) => {
      keys[e.key.toLowerCase()] = false;
    });

    function restart() {
      player.x = canvas.width / 2;
      player.y = canvas.height - 70;
      player.hp = 5;
      player.power = 1;
      player.shotCooldown = 0;
      player.invincible = 0;
      bullets = [];
      enemyBullets = [];
      enemies = [];
      items = [];
      particles = [];
      score = 0;
      enemyTimer = 0;
      itemTimer = 0;
      frame = 0;
      gameOver = false;
      updateUI();
    }

    function updateUI() {
      scoreEl.textContent = score;
      hpEl.textContent = player.hp;
      powerEl.textContent = player.power;
    }

    function addPlayerBullet(x, y, vx, vy, power = 1) {
      bullets.push({ x, y, w: 6, h: 16, vx, vy, power });
    }

    function shoot() {
      if (player.power === 1) {
        addPlayerBullet(player.x, player.y - 25, 0, -10);
      } else if (player.power === 2) {
        addPlayerBullet(player.x - 9, player.y - 25, 0, -10);
        addPlayerBullet(player.x + 9, player.y - 25, 0, -10);
      } else if (player.power === 3) {
        addPlayerBullet(player.x, player.y - 28, 0, -11);
        addPlayerBullet(player.x - 16, player.y - 20, -1.2, -10);
        addPlayerBullet(player.x + 16, player.y - 20, 1.2, -10);
      } else {
        addPlayerBullet(player.x, player.y - 30, 0, -12, 2);
        addPlayerBullet(player.x - 14, player.y - 24, -0.8, -11);
        addPlayerBullet(player.x + 14, player.y - 24, 0.8, -11);
        addPlayerBullet(player.x - 24, player.y - 10, -1.7, -10);
        addPlayerBullet(player.x + 24, player.y - 10, 1.7, -10);
      }

      player.shotCooldown = Math.max(4, 10 - player.power);
    }

    function spawnEnemy() {
      if (boss) return;
      const size = Math.random() * 16 + 28;
      enemies.push({
        x: Math.random() * (canvas.width - size) + size / 2,
        y: -size,
        w: size,
        h: size,
        speed: Math.random() * 1.4 + 1.4,
        hp: size > 38 ? 4 : 2,
        shotTimer: Math.floor(Math.random() * 50),
        type: Math.random() < 0.35 ? "spread" : "normal"
      });
    }

    function spawnPowerItem(x = Math.random() * (canvas.width - 40) + 20, y = -20) {
      items.push({
        x,
        y,
        w: 24,
        h: 24,
        speed: 2.2,
        type: "power"
      });
    }

    function enemyShoot(enemy) {
      if (enemy.type === "spread") {
        for (let i = -2; i <= 2; i++) {
          enemyBullets.push({
            x: enemy.x,
            y: enemy.y + enemy.h / 2,
            w: 8,
            h: 8,
            vx: i * 1.1,
            vy: 3.2
          });
        }
      } else {
        const dx = player.x - enemy.x;
        const dy = player.y - enemy.y;
        const len = Math.hypot(dx, dy) || 1;
        enemyBullets.push({
          x: enemy.x,
          y: enemy.y + enemy.h / 2,
          w: 8,
          h: 8,
          vx: dx / len * 3.2,
          vy: dy / len * 3.2
        });
      }
    }

    function createExplosion(x, y) {
      for (let i = 0; i < 16; i++) {
        particles.push({
          x,
          y,
          vx: (Math.random() - 0.5) * 7,
          vy: (Math.random() - 0.5) * 7,
          life: 24,
          r: Math.random() * 4 + 2
        });
      }
    }

    function isHit(a, b) {
      return (
        a.x - a.w / 2 < b.x + b.w / 2 &&
        a.x + a.w / 2 > b.x - b.w / 2 &&
        a.y - a.h / 2 < b.y + b.h / 2 &&
        a.y + a.h / 2 > b.y - b.h / 2
      );
    }

    function damagePlayer() {
      if (player.invincible > 0) return;
      player.hp--;
      player.power = Math.max(1, player.power - 1);
      player.invincible = 80;
      createExplosion(player.x, player.y);
      updateUI();
      if (player.hp <= 0) gameOver = true;
    }

    function update() {
      if (gameOver) return;
      frame++;

      // boss spawn
      bossTimer++;
      if (!boss && bossTimer > 2000) {
        boss = {
          x: canvas.width / 2,
          y: 120,
          w: 120,
          h: 120,
          hp: 200,
          maxHp: 200,
          shotTimer: 0
        };
      }

      if (keys["arrowleft"] || keys["a"]) player.x -= player.speed;
      if (keys["arrowright"] || keys["d"]) player.x += player.speed;
      if (keys["arrowup"] || keys["w"]) player.y -= player.speed;
      if (keys["arrowdown"] || keys["s"]) player.y += player.speed;

      player.x = Math.max(player.w / 2, Math.min(canvas.width - player.w / 2, player.x));
      player.y = Math.max(player.h / 2, Math.min(canvas.height - player.h / 2, player.y));

      if (player.invincible > 0) player.invincible--;
      if (player.shotCooldown > 0) player.shotCooldown--;
      if ((keys[" "] || keys["space"]) && player.shotCooldown <= 0) shoot();

      bullets.forEach((b) => {
        b.x += b.vx;
        b.y += b.vy;
      });
      bullets = bullets.filter((b) => b.y > -30 && b.x > -30 && b.x < canvas.width + 30);

      enemyBullets.forEach((b) => {
        b.x += b.vx;
        b.y += b.vy;
      });
      enemyBullets = enemyBullets.filter((b) => b.y < canvas.height + 30 && b.x > -30 && b.x < canvas.width + 30);

      enemyTimer++;
      if (enemyTimer > Math.max(20, 40 - Math.floor(score / 1000))) {
        spawnEnemy();
        enemyTimer = 0;
      }

      itemTimer++;
      if (itemTimer > 520) {
        spawnPowerItem();
        itemTimer = 0;
      }

      enemies.forEach((e) => {
        e.y += e.speed;
        e.shotTimer++;
        if (e.shotTimer > 70) {
          enemyShoot(e);
          e.shotTimer = 0;
        }
      });

      // boss behavior
      if (boss) {
        boss.shotTimer++;
        if (boss.shotTimer % 40 === 0) {
          for (let i = 0; i < 20; i++) {
            const angle = (Math.PI * 2 / 20) * i + frame * 0.02;
            enemyBullets.push({
              x: boss.x,
              y: boss.y,
              w: 10,
              h: 10,
              vx: Math.cos(angle) * 3,
              vy: Math.sin(angle) * 3
            });
          }
        }
      }

      items.forEach((item) => {
        item.y += item.speed;
        item.x += Math.sin((frame + item.y) * 0.04) * 0.8;
      });
      items = items.filter((item) => item.y < canvas.height + 40);

      for (let i = items.length - 1; i >= 0; i--) {
        if (isHit(player, items[i])) {
          player.power = Math.min(4, player.power + 1);
          score += 300;
          createExplosion(items[i].x, items[i].y);
          items.splice(i, 1);
          updateUI();
        }
      }

      for (let i = enemies.length - 1; i >= 0; i--) {
        const e = enemies[i];
        if (isHit(player, e)) {
          createExplosion(e.x, e.y);
          enemies.splice(i, 1);
          damagePlayer();
          continue;
        }

        if (e.y > canvas.height + 50) {
          enemies.splice(i, 1);
          damagePlayer();
        }
      }

      for (let i = enemyBullets.length - 1; i >= 0; i--) {
        if (isHit(player, enemyBullets[i])) {
          enemyBullets.splice(i, 1);
          damagePlayer();
        }
      }

      for (let i = enemies.length - 1; i >= 0; i--) {
        for (let j = bullets.length - 1; j >= 0; j--) {
          if (isHit(enemies[i], bullets[j])) {
            enemies[i].hp -= bullets[j].power;
            bullets.splice(j, 1);

            if (enemies[i].hp <= 0) {
              const drop = Math.random() < 0.18;
              if (drop) spawnPowerItem(enemies[i].x, enemies[i].y);
              createExplosion(enemies[i].x, enemies[i].y);
              enemies.splice(i, 1);
              score += 100;
              updateUI();
            }
            break;
          }
        }
      }

      particles.forEach((p) => {
        p.x += p.vx;
        p.y += p.vy;
        p.life--;
      });
      particles = particles.filter((p) => p.life > 0);

      stars.forEach((s) => {
        s.y += s.speed;
        if (s.y > canvas.height) {
          s.y = 0;
          s.x = Math.random() * canvas.width;
        }
      });
    }

    function drawPlayer() {
      if (player.invincible > 0 && Math.floor(frame / 5) % 2 === 0) return;

      ctx.save();
      ctx.translate(player.x, player.y);

      // body
      const grad = ctx.createLinearGradient(0, -20, 0, 30);
      grad.addColorStop(0, "#7df9ff");
      grad.addColorStop(1, "#0077ff");
      ctx.fillStyle = grad;
      ctx.beginPath();
      ctx.moveTo(0, -26);
      ctx.lineTo(20, 22);
      ctx.lineTo(0, 12);
      ctx.lineTo(-20, 22);
      ctx.closePath();
      ctx.fill();

      // cockpit
      ctx.fillStyle = "#ffffff";
      ctx.beginPath();
      ctx.arc(0, -6, 6, 0, Math.PI * 2);
      ctx.fill();

      // wings glow
      ctx.fillStyle = "#00eaff";
      ctx.globalAlpha = 0.5;
      ctx.fillRect(-24, 0, 8, 10);
      ctx.fillRect(16, 0, 8, 10);
      ctx.globalAlpha = 1;

      // engine flame
      ctx.fillStyle = "#ffcf5a";
      ctx.beginPath();
      ctx.moveTo(-8, 24);
      ctx.lineTo(0, 40 + Math.random() * 6);
      ctx.lineTo(8, 24);
      ctx.closePath();
      ctx.fill();

      ctx.restore();
    }

    function drawEnemy(e) {
      ctx.save();
      ctx.translate(e.x, e.y);

      // core
      const grad = ctx.createRadialGradient(0, 0, 4, 0, 0, e.w / 2);
      grad.addColorStop(0, "#fff");
      grad.addColorStop(1, e.type === "spread" ? "#ff00cc" : "#ff0000");
      ctx.fillStyle = grad;
      ctx.beginPath();
      ctx.arc(0, 0, e.w / 2, 0, Math.PI * 2);
      ctx.fill();

      // spikes
      ctx.strokeStyle = "#fff";
      ctx.lineWidth = 2;
      for (let i = 0; i < 6; i++) {
        const angle = (Math.PI * 2 / 6) * i + frame * 0.01;
        ctx.beginPath();
        ctx.moveTo(Math.cos(angle) * 6, Math.sin(angle) * 6);
        ctx.lineTo(Math.cos(angle) * (e.w / 2 + 8), Math.sin(angle) * (e.w / 2 + 8));
        ctx.stroke();
      }

      ctx.restore();
    }

    function drawPowerItem(item) {
      ctx.save();
      ctx.translate(item.x, item.y);
      ctx.rotate(frame * 0.05);
      ctx.fillStyle = "#68ff7a";
      ctx.fillRect(-12, -12, 24, 24);
      ctx.fillStyle = "#052";
      ctx.font = "bold 18px system-ui";
      ctx.textAlign = "center";
      ctx.textBaseline = "middle";
      ctx.fillText("P", 0, 1);
      ctx.restore();
    }

    function draw() {
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      ctx.fillStyle = "#050816";
      ctx.fillRect(0, 0, canvas.width, canvas.height);

      ctx.fillStyle = "#ffffff";
      stars.forEach((s) => {
        ctx.globalAlpha = 0.4 + Math.random() * 0.5;
        ctx.beginPath();
        ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
        ctx.fill();
      });
      ctx.globalAlpha = 1;

      bullets.forEach((b) => {
        ctx.fillStyle = b.power >= 2 ? "#fff36a" : "#8ffcff";
        ctx.fillRect(b.x - b.w / 2, b.y - b.h / 2, b.w, b.h);
      });

      enemyBullets.forEach((b) => {
        ctx.fillStyle = "#ff9a3b";
        ctx.beginPath();
        ctx.arc(b.x, b.y, b.w / 2, 0, Math.PI * 2);
        ctx.fill();
      });

      items.forEach(drawPowerItem);
      enemies.forEach(drawEnemy);
      // draw boss
      if (boss) {
        ctx.save();
        ctx.translate(boss.x, boss.y);

        const grad = ctx.createRadialGradient(0, 0, 10, 0, 0, boss.w / 2);
        grad.addColorStop(0, "#fff");
        grad.addColorStop(1, "#ff00aa");
        ctx.fillStyle = grad;
        ctx.beginPath();
        ctx.arc(0, 0, boss.w / 2, 0, Math.PI * 2);
        ctx.fill();
        ctx.restore();

        // HP bar
        ctx.fillStyle = "#000";
        ctx.fillRect(80, 20, 320, 16);
        ctx.fillStyle = "#ff0066";
        ctx.fillRect(80, 20, 320 * (boss.hp / boss.maxHp), 16);
      }

      drawPlayer();

      particles.forEach((p) => {
        ctx.globalAlpha = p.life / 24;
        ctx.fillStyle = "#ffd35a";
        ctx.beginPath();
        ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
        ctx.fill();
      });
      ctx.globalAlpha = 1;

      if (gameOver) {
        ctx.fillStyle = "#000b";
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        ctx.fillStyle = "white";
        ctx.textAlign = "center";
        ctx.font = "bold 42px system-ui";
        ctx.fillText("GAME OVER", canvas.width / 2, canvas.height / 2 - 30);
        ctx.font = "20px system-ui";
        ctx.fillText("Score: " + score, canvas.width / 2, canvas.height / 2 + 10);
        ctx.fillText("Enterでリスタート", canvas.width / 2, canvas.height / 2 + 48);
      }
    }

    function loop() {
      update();
      draw();
      requestAnimationFrame(loop);
    }

    updateUI();
    loop();
  </script>
</body>
</html>

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>