<!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 => ({ "&":"&","<":"<",">":">",'"':""","'":"'" }[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>