ロボットミニゲーム「Wire Arms」
【更新履歴】
・2026/6/8 バージョン1.0公開。
・2026/6/8 バージョン1.1公開。(ルート選択を導入)
・2026/6/8 バージョン1.2公開。(コマンド選択を導入)
・2026/6/8 バージョン1.3公開。(コマンド1パス方式に変更)
・2026/6/9 バージョン1.4公開。(見た目を改善)
■タクティカルモード(未来軌道予測)
・敵機に近づくとロックオンされ、攻撃軌道が表示されます。
・Z、X、C、Vのいずれかのキーを押すと、
待機中のアームが、ターゲットに向けて発射されます。
(ゲーム開始時には、ZとXの2つのアームしか有りません。)
■アクティブガード&自動迎撃
・敵のアームが自機に向かって発射され、
自機と衝突する危険がある(距離35以内かつ接近中)の場合は、
「WARNING: [Z/X/C/V] でガード&迎撃」 と警告が表示されます。
インターセプトの瞬時発動:
・この警告中に、待機中のアームキーを押すと即座に発動します。
バリアと自動追尾:
・自機に防御バリアを展開すると同時に、
指定したアームを「迎撃用インターセプター」として射出します。
完全相殺:
・迎撃アームは飛来する敵の弾を空中で自動追尾し、
激突して完全に相殺(相打ち撃墜)します。
・成功時には、画面に 「INTERCEPT!」 と表示されます。
■部位破壊とアーム管理
・各機体は、「本体HP」のほかに、
最大4基のアームそれぞれに独立した「アームHP」を持っています。
・被弾時、一定確率でアームが身代わりとなってダメージを受けます。
ATTACK PREDICT (緑表示):
・アーム待機状態。
・いつでも攻撃予測モードへ移行可能です。
GUARD / INTERCEPT (水色表示):
・敵の攻撃を感知した迎撃可能状態。
・押すことでアクティブガードが発動します。
IN USE (グレー表示):
・現在アームを射出中。
・手元に戻るまで再使用はできません。
CRUSHED (赤・取り消し線表示):
・アームHPが0になり大破した状態。
・資材(MATERIAL)による自動修理が完了するまで
キー入力は受け付けられません。
EMPTY (黒表示):
・拡張マウント枠はあるが、
まだアームが製造されていない空きスロットです。
■ナノマシン・クラフト
・フィールド上のアイテムを回収することで、
機体はDATA(情報)とMATERIAL(資材)を蓄積し、
リアルタイムに自己進化・修復を行います。
【DATA(水色アイテム / 1回収につき +10)】
・敵を凌駕するための戦術情報リソースです。
最大マウント枠の拡張:
・ゲーム開始時の初期アームは2基ですが、
累計DATAが200ポイントに達するごとに
「最大アーム搭載枠」が1つずつ自動解放 されます。
(最大4枠まで拡張可能)
【MATERIAL(黄緑アイテム / 1回収につき +10)】
・機体の物理的構成を書き換えるためのナノマシン資材です。
・蓄積された資材は、戦闘状況に応じて
以下の優先順位で自動的に消費(クラフト)されます。
自己修復(本体HPの再生):
・本体HPが最大値未満のとき、毎秒 MATERIAL を 1 消費し、
本体HPを 10 ずつ継続回復 します。
アーム修理(CRUSHEDからの再起動):
・大破(CRUSHED)状態のアームが存在する場合、
MATERIAL を 10 消費 してアームHPを全回復させ、
即座に再起動・戦線復帰させます。
アーム新造(空き枠への追加武装):
・DATA拡張によってアームの最大搭載枠が増え、
スロットに空き(EMPTY)がある場合、
MATERIAL を 20 消費 して
新たな武装アームを瞬時に製造し、自動でマウントします。
・戦闘中は画面上部のゲージ
(緑:本体HP、水色:DATA進行度、黄緑:MATERIAL貯蔵量)を
常に意識し、リソースの枯渇に注意して立ち回ってください。
・ダウンロードされる方はこちら。↓
・ソースコードはこちら。↓
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>WIRE ARMS - TACTICAL CORE</title>
<style>
* { box-sizing: border-box; }
body { margin: 0; overflow: hidden; background-color: #020810; font-family: 'Courier New', monospace; user-select: none; }
canvas { display: block; position: absolute; top: 0; left: 0; width: 100vw; height: 100vh; z-index: -1; transition: filter 0.2s; }
body.blur canvas { filter: blur(6px) brightness(0.5); }
/* ── スキャンライン オーバーレイ ── */
#scanlines {
position: fixed; top:0; left:0; width:100%; height:100%;
background: repeating-linear-gradient(to bottom, transparent 0px, transparent 3px, rgba(0,0,0,0.07) 3px, rgba(0,0,0,0.07) 4px);
pointer-events: none; z-index: 50; animation: scanMove 8s linear infinite;
}
@keyframes scanMove { from{background-position:0 0} to{background-position:0 100px} }
/* ── グリッチ フラッシュ ── */
#flashOverlay {
position:fixed; top:0; left:0; width:100%; height:100%;
background:rgba(0,255,200,0); pointer-events:none; z-index:60;
transition: background 0.05s;
}
/* ── UI ── */
#ui { position:absolute; top:0; left:0; width:100%; pointer-events:none; z-index:10; opacity:0; transition:opacity 0.4s; padding:12px; }
.bar-container { display:flex; justify-content:space-between; width:92%; margin:0 auto; gap:10px; }
.status-box {
background: linear-gradient(135deg, rgba(0,30,20,0.9) 0%, rgba(0,10,30,0.9) 100%);
border: 1px solid #0f6; padding:10px 14px; width:42%;
color:#fff; position:relative; overflow:hidden;
clip-path: polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 12px 100%, 0 calc(100% - 12px));
}
.status-box::before {
content:''; position:absolute; top:0; left:0; width:100%; height:2px;
background:linear-gradient(90deg,transparent,#0f6,transparent);
animation: scanBar 3s linear infinite;
}
@keyframes scanBar { from{transform:translateX(-100%)} to{transform:translateX(100%)} }
.status-box.enemy { background:linear-gradient(135deg,rgba(30,5,0,0.9) 0%,rgba(20,0,10,0.9) 100%); border-color:#f33; }
.status-box.enemy::before { background:linear-gradient(90deg,transparent,#f33,transparent); }
.entity-name { font-size:13px; font-weight:bold; letter-spacing:3px; color:#0ff; text-shadow:0 0 8px #0ff; margin-bottom:6px; }
.enemy .entity-name { color:#f55; text-shadow:0 0 8px #f55; }
.gauge-row { margin-bottom:5px; }
.gauge-label { font-size:10px; color:#aaa; margin-bottom:2px; display:flex; justify-content:space-between; }
.gauge-bg { width:100%; height:10px; background:rgba(0,0,0,0.6); border:1px solid #333; position:relative; overflow:hidden; }
.gauge-fg { height:100%; transition:width 0.15s; position:relative; }
.gauge-fg::after { content:''; position:absolute; top:0; right:0; width:3px; height:100%; background:rgba(255,255,255,0.8); }
.hp-fg { background:linear-gradient(90deg,#0a6,#0f6); box-shadow:0 0 8px #0f6; }
.enemy .hp-fg { background:linear-gradient(90deg,#a00,#f44); box-shadow:0 0 8px #f44; }
.data-fg { background:linear-gradient(90deg,#006,#0ff); box-shadow:0 0 6px #0ff; }
.enemy .data-fg { background:linear-gradient(90deg,#600,#f0f); box-shadow:0 0 6px #f0f; }
.mat-fg { background:linear-gradient(90deg,#260,#af2); box-shadow:0 0 6px #af2; }
/* ── コマンドメニュー ── */
#commandMenu {
position:absolute; bottom:24px; left:50%; transform:translateX(-50%);
display:flex; flex-direction:row; gap:12px; z-index:10; opacity:0; transition:opacity 0.3s;
}
.cmd-item {
background:rgba(0,15,25,0.9); border:1px solid #0f6; padding:8px 12px;
font-size:14px; font-weight:bold; width:130px; text-align:center;
white-space:pre-line; border-radius:2px;
clip-path:polygon(0 0,calc(100% - 8px) 0,100% 8px,100% 100%,8px 100%,0 calc(100% - 8px));
transition:all 0.15s;
}
.cmd-item.in-use { border-color:#333; color:#555; }
.cmd-item.crushed { border-color:#f00; color:#f55; text-decoration:line-through; }
.cmd-item.empty { border-color:#222; color:#333; }
.cmd-item.selected{ transform:scale(1.1) translateY(-4px); box-shadow:0 0 20px currentColor; animation: cmdBlink 0.5s infinite alternate; }
@keyframes cmdBlink {
from { opacity: 1; }
to { opacity: 0.6; }
}
/* ── ログ ── */
#log { position:absolute; bottom:110px; right:20px; color:#aaa; pointer-events:none; font-size:12px; line-height:1.7; z-index:10; text-align:right; }
/* ── センタープロンプト ── */
#centerPrompt {
position:absolute; top:38%; width:100%; text-align:center;
font-size:20px; font-weight:bold; color:#fff; pointer-events:none; z-index:10;
letter-spacing:2px; text-transform:uppercase;
}
.locked-on { color:#ffe000 !important; text-shadow:0 0 15px #ffe000, 0 0 30px #ff8000 !important; animation:promptPulse 1s ease-in-out infinite; }
.obstructed { color:#ff2200 !important; text-shadow:0 0 15px #ff2200, 0 0 30px #ff0000 !important; animation:promptFlash 0.3s ease-in-out infinite; }
@keyframes promptPulse { 0%,100%{opacity:1} 50%{opacity:0.7} }
@keyframes promptFlash { 0%,100%{opacity:1} 50%{opacity:0.3} }
/* ── ターゲットHUD ── */
.target-hud { position:absolute; color:#fff; font-size:11px; pointer-events:none; text-align:center; white-space:nowrap; transform:translate(-50%,-100%); display:none; background:rgba(0,0,0,0.7); border:1px solid #0ff; padding:4px 8px; z-index:10; animation:hudBlink 2s linear infinite; }
.target-hud.enemy-hud { border-color:#f55; color:#f55; }
@keyframes hudBlink { 0%,90%{opacity:1} 95%{opacity:0.5} 100%{opacity:1} }
/* ── ステージ ── */
#stageIndicator { position:absolute; top:12px; left:50%; transform:translateX(-50%); color:#0ff; font-size:16px; text-shadow:0 0 10px #0ff; pointer-events:none; opacity:0; transition:opacity 0.3s; z-index:10; letter-spacing:3px; }
#stageIndicator.visible { opacity:1; }
/* ── スクリーン ── */
.screen { display:none; position:absolute; top:0; left:0; width:100vw; height:100vh; background:rgba(0,3,10,0.88); flex-direction:column; justify-content:center; align-items:center; z-index:100; color:#fff; text-align:center; }
.screen.active { display:flex; }
.screen h1 { font-size:68px; margin-bottom:16px; letter-spacing:10px; text-shadow:0 0 30px #0ff; animation:titleGlow 2s ease-in-out infinite; }
@keyframes titleGlow { 0%,100%{text-shadow:0 0 30px #0ff,0 0 60px #0ff} 50%{text-shadow:0 0 10px #0ff,0 0 20px #0ff} }
.screen h2 { font-size:32px; margin-bottom:16px; color:#0ff; letter-spacing:4px; }
.screen p { font-size:20px; color:#aaa; max-width:600px; line-height:1.7; }
.blink { animation:blinker 1.2s linear infinite; }
@keyframes blinker { 50%{opacity:0} }
h2,p { margin:0; padding:0; }
</style>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.min.js"></script>
</head>
<body>
<div id="scanlines"></div>
<div id="flashOverlay"></div>
<div id="ui" style="opacity:0;">
<div class="bar-container">
<div class="status-box" id="uiPlayer">
<div class="entity-name">◈ PLAYER UNIT</div>
<div class="gauge-row"><div class="gauge-label"><span>HP</span><span id="hpPlayerTxt">1000</span></div><div class="gauge-bg"><div class="gauge-fg hp-fg" id="hpBarPlayer"></div></div></div>
<div class="gauge-row"><div class="gauge-label"><span>DATA</span><span id="dataPlayerTxt">0</span></div><div class="gauge-bg" style="height:6px;"><div class="gauge-fg data-fg" id="dataBarPlayer"></div></div></div>
<div class="gauge-row"><div class="gauge-label"><span>MATERIAL</span><span id="matPlayerTxt">0</span></div><div class="gauge-bg" style="height:6px;"><div class="gauge-fg mat-fg" id="matBarPlayer"></div></div></div>
</div>
<div class="status-box enemy" id="uiEnemy">
<div class="entity-name">◈ ENEMY UNIT</div>
<div class="gauge-row"><div class="gauge-label"><span>HP</span><span id="hpEnemyTxt">300</span></div><div class="gauge-bg"><div class="gauge-fg hp-fg" id="hpBarEnemy"></div></div></div>
<div class="gauge-row"><div class="gauge-label"><span>DATA</span><span id="dataEnemyTxt">0</span></div><div class="gauge-bg" style="height:6px;"><div class="gauge-fg data-fg" id="dataBarEnemy"></div></div></div>
<div class="gauge-row"><div class="gauge-label"><span>MATERIAL</span><span id="matEnemyTxt">0</span></div><div class="gauge-bg" style="height:6px;"><div class="gauge-fg mat-fg" id="matBarEnemy"></div></div></div>
</div>
</div>
</div>
<div id="commandMenu"></div>
<div id="stageIndicator"></div>
<div id="centerPrompt"></div>
<div id="log"></div>
<div id="targetHud" class="target-hud"></div>
<div id="screenTitle" class="screen active"><h1>WIRE ARMS</h1><p class="blink" style="font-size:18px;letter-spacing:3px;">▶ CLICK TO START</p></div>
<div id="screenOpening" class="screen"><h2>INITIALIZING SYSTEM</h2><p>神経接続パス確立...<br>自律戦闘AI、起動準備完了。</p></div>
<div id="screenStageStart" class="screen"><h1 id="stageText">STAGE 1</h1><p style="letter-spacing:2px;">敵対プログラムを殲滅せよ</p></div>
<div id="screenPause" class="screen"><h1 style="color:#ffa500;text-shadow:0 0 20px #fa0;">PAUSED</h1><p class="blink">[ESC] 戦闘再開</p></div>
<div id="screenStageClear" class="screen"><h1 style="color:#0f6;text-shadow:0 0 30px #0f6;">STAGE CLEAR</h1><p>敵データの残滓を吸収...<br>次フェーズへ移行します。</p></div>
<div id="screenGameOver" class="screen"><h1 style="color:#f44;text-shadow:0 0 30px #f00;">GAME OVER</h1><p>機体大破。接続をロストしました。</p><p class="blink" style="margin-top:20px;font-size:16px;letter-spacing:3px;">▶ CLICK TO RETRY</p></div>
<div id="screenGameClear" class="screen"><h1 style="color:#f0f;text-shadow:0 0 30px #f0f;">ALL CLEARED</h1><p>MISSION ACCOMPLISHED.<br>全脅威の排除を確認しました。</p><p class="blink" style="margin-top:20px;font-size:16px;letter-spacing:3px;">▶ CLICK TO RESTART</p></div>
<script>
// ==========================================
// 0. ゲーム状態管理 & コマンドカラー定義
// ==========================================
let gameState = 'TITLE';
let currentStage = 1;
const maxStages = 5;
let enemies = [];
let accumulatedEnemyData = 0;
let accumulatedEnemyMat = 0;
const CMD_COLORS = [0x00bfff, 0x32cd32, 0xffd700, 0xda70d6];
const CMD_COLORS_CSS = ['#00bfff', '#32cd32', '#ffd700', '#da70d6'];
const playerTactics = {
routes: [], lines: [], target: null, interceptBit: null,
selectedIndex: null, updateTimer: 0
};
function switchState(newState) {
document.querySelectorAll('.screen').forEach(el => el.classList.remove('active'));
gameState = newState;
const ui = document.getElementById('ui');
const cmdMenu = document.getElementById('commandMenu');
const stageInd= document.getElementById('stageIndicator');
const body = document.body;
if (newState === 'PLAYING') {
ui.style.opacity = '1'; cmdMenu.style.opacity = '1';
stageInd.classList.add('visible'); body.classList.remove('blur');
} else {
ui.style.opacity = '0'; cmdMenu.style.opacity = '0';
stageInd.classList.remove('visible');
document.getElementById('targetHud').style.display = 'none';
body.classList.add('blur');
document.getElementById('centerPrompt').innerText = '';
playerTactics.selectedIndex = null;
}
if (newState === 'TITLE') document.getElementById('screenTitle').classList.add('active');
else if (newState === 'OPENING') document.getElementById('screenOpening').classList.add('active');
else if (newState === 'STAGE_START') {
document.getElementById('stageText').innerText = 'STAGE ' + currentStage;
document.getElementById('screenStageStart').classList.add('active');
setTimeout(() => { initStage(); switchState('PLAYING'); }, 2000);
}
else if (newState === 'PAUSE') document.getElementById('screenPause').classList.add('active');
else if (newState === 'STAGE_CLEAR') {
document.getElementById('screenStageClear').classList.add('active');
player.data += accumulatedEnemyData;
player.materials += accumulatedEnemyMat;
game.addMessage(`SYSTEM: DATA +${accumulatedEnemyData} / MAT +${accumulatedEnemyMat}`);
player.checkCrafting();
}
else if (newState === 'GAME_OVER') document.getElementById('screenGameOver').classList.add('active');
else if (newState === 'GAME_CLEAR') document.getElementById('screenGameClear').classList.add('active');
}
window.addEventListener('click', () => {
window.focus();
if (audioCtx.state === 'suspended') audioCtx.resume();
if (gameState === 'TITLE') {
switchState('OPENING');
setTimeout(() => switchState('STAGE_START'), 2500);
} else if (gameState === 'GAME_OVER' || gameState === 'GAME_CLEAR') {
currentStage = 1;
player.data = 0; player.materials = 0;
player.maxArms = 4;
player.arms = [{ id:'ARM1', hp:100, maxHp:100 }, { id:'ARM2', hp:100, maxHp:100 }, { id:'ARM3', hp:100, maxHp:100 }, { id:'ARM4', hp:100, maxHp:100 }];
switchState('TITLE');
}
});
// ==========================================
// 1. サウンド
// ==========================================
const AudioContext = window.AudioContext || window.webkitAudioContext;
const audioCtx = new AudioContext();
function playSound(type) {
if (audioCtx.state === 'suspended') audioCtx.resume();
const now = audioCtx.currentTime;
const mkOsc = (t, freq, endFreq, dur, gainVal, waveType = 'sawtooth') => {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain); gain.connect(audioCtx.destination);
osc.type = waveType;
osc.frequency.setValueAtTime(freq, now);
if (endFreq) osc.frequency.exponentialRampToValueAtTime(endFreq, now + dur);
gain.gain.setValueAtTime(gainVal, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + dur);
osc.start(now); osc.stop(now + dur);
};
if (type === 'attack') { mkOsc(0, 300, 80, 0.15, 0.25); }
else if (type === 'laser') {
mkOsc(0, 1800, 300, 0.22, 0.35, 'sine');
mkOsc(0, 900, 200, 0.22, 0.15, 'sawtooth');
}
else if (type === 'hit') {
mkOsc(0, 120, 20, 0.35, 0.6);
mkOsc(0, 200, 40, 0.2, 0.3, 'square');
}
else if (type === 'bigHit') {
mkOsc(0, 80, 15, 0.5, 0.9);
mkOsc(0, 300, 50, 0.3, 0.4, 'square');
}
else if (type === 'craft') { mkOsc(0, 600, 1200, 0.3, 0.4, 'square'); }
else if (type === 'crush') { mkOsc(0, 50, 10, 0.4, 0.8); }
else if (type === 'intercept') { mkOsc(0, 1500, 500, 0.1, 0.3, 'square'); }
else if (type === 'menu') { mkOsc(0, 800, 1200, 0.05, 0.2, 'sine'); }
else if (type === 'death') {
mkOsc(0, 400, 30, 0.8, 0.6);
mkOsc(0, 200, 15, 1.0, 0.4, 'square');
}
}
// ==========================================
// 2. 画面演出(フラッシュ・カメラシェイク)
// ==========================================
const flashOverlay = document.getElementById('flashOverlay');
let flashTimer = 0;
function triggerFlash(color = 'rgba(0,255,180,0.35)', duration = 0.12) {
flashOverlay.style.background = color;
flashTimer = duration;
}
let cameraShake = { active:false, timer:0, intensity:0 };
const cameraBasePos = new THREE.Vector3(0, 50, 60);
function triggerCameraShake(intensity = 1.5, duration = 0.25) {
cameraShake.active = true;
cameraShake.timer = duration;
cameraShake.intensity = intensity;
}
function updateCameraShake(dt) {
if (!cameraShake.active) return;
cameraShake.timer -= dt;
if (cameraShake.timer <= 0) {
cameraShake.active = false;
camera.position.copy(cameraBasePos);
return;
}
const ratio = cameraShake.timer / 0.25;
const s = cameraShake.intensity * ratio;
camera.position.set(
cameraBasePos.x + (Math.random()-0.5)*s,
cameraBasePos.y + (Math.random()-0.5)*s*0.5,
cameraBasePos.z + (Math.random()-0.5)*s
);
}
// ==========================================
// 3. UI更新(コマンドメニュー含む)
// ==========================================
const game = {
messages: ['SYSTEM: 起動待機中...'],
addMessage(msg) {
this.messages.push(msg);
if (this.messages.length > 5) this.messages.shift();
document.getElementById('log').innerHTML = this.messages.join('<br>');
},
updateUI() {
document.getElementById('hpBarPlayer').style.width = Math.max(0,(player.hp/player.maxHp)*100)+'%';
document.getElementById('hpPlayerTxt').innerText = Math.floor(player.hp);
document.getElementById('dataPlayerTxt').innerText = player.data;
document.getElementById('dataBarPlayer').style.width= Math.min(100,((player.data%200)/200)*100)+'%';
document.getElementById('matPlayerTxt').innerText = player.materials;
document.getElementById('matBarPlayer').style.width = Math.min(100,(player.materials/50)*100)+'%';
const fe = enemies.find(e => e.hp > 0) || enemies[0];
if (fe) {
document.getElementById('hpBarEnemy').style.width = Math.max(0,(fe.hp/fe.maxHp)*100)+'%';
document.getElementById('hpEnemyTxt').innerText = Math.floor(fe.hp);
document.getElementById('dataEnemyTxt').innerText = fe.data;
document.getElementById('dataBarEnemy').style.width = Math.min(100,((fe.data%200)/200)*100)+'%';
document.getElementById('matEnemyTxt').innerText = fe.materials;
document.getElementById('matBarEnemy').style.width = Math.min(100,(fe.materials/50)*100)+'%';
} else {
document.getElementById('hpBarEnemy').style.width = '0%';
}
document.getElementById('stageIndicator').innerText = `STAGE ${currentStage}/${maxStages} ENEMIES:${enemies.filter(e=>e.hp>0).length}`;
this.updateCommandMenu();
},
updateCommandMenu() {
if (gameState !== 'PLAYING') return;
const keyMap = ['Z','X','C','V'];
const container = document.getElementById('commandMenu');
container.innerHTML = '';
for (let i = 0; i < 4; i++) {
const arm = player.arms[i];
const key = keyMap[i];
const div = document.createElement('div');
const colorCss = CMD_COLORS_CSS[i];
div.className = 'cmd-item';
if (!arm) {
div.className += ' empty'; div.innerText = `[${key}]\nEMPTY`;
} else if (arm.hp <= 0) {
div.className += ' crushed'; div.innerText = `[${key}]\nCRUSHED`;
} else if (bits.find(b => b.owner === player && b.armId === arm.id)) {
div.className += ' in-use'; div.innerText = `[${key}]\nIN USE`;
} else {
if (playerTactics.selectedIndex === i) {
div.className += ' selected';
div.style.cssText = `border-color:${colorCss};color:#fff;background-color:${colorCss}20;text-shadow:0 0 10px ${colorCss};box-shadow:0 0 20px ${colorCss};`;
} else {
div.style.cssText = `border-color:${colorCss};color:${colorCss};text-shadow:0 0 5px ${colorCss};`;
}
const modeText = playerTactics.interceptBit ? 'GUARD' : 'ATTACK';
const routeName = playerTactics.routes[i] ? playerTactics.routes[i].name : modeText;
div.innerText = `[${key}]\n${routeName}`;
}
container.appendChild(div);
}
}
};
// ── フローティングテキスト ──
const floatingTexts = [];
function createFloatingText(text, pos, color, size='24px') {
const div = document.createElement('div');
div.innerText = text;
Object.assign(div.style, {
position:'absolute', color, fontWeight:'bold',
textShadow:`0 0 8px ${color}, 0 0 15px ${color}`,
pointerEvents:'none', fontSize:size, zIndex:20,
fontFamily:"'Courier New',monospace", letterSpacing:'2px'
});
document.body.appendChild(div);
floatingTexts.push({ el:div, pos:pos.clone(), life:1.8 });
}
function updateFloatingTexts(dt) {
for (let i = floatingTexts.length-1; i >= 0; i--) {
const ft = floatingTexts[i]; ft.life -= dt;
if (ft.life <= 0) { ft.el.remove(); floatingTexts.splice(i,1); continue; }
ft.pos.y += dt * 12;
const proj = ft.pos.clone().project(camera);
ft.el.style.left = `calc(${(proj.x*0.5+0.5)*window.innerWidth}px - 30px)`;
ft.el.style.top = `${(proj.y*-0.5+0.5)*window.innerHeight}px`;
ft.el.style.opacity = Math.min(1, ft.life);
ft.el.style.transform = `scale(${0.7+ft.life*0.15})`;
}
}
function updateTargetHud() {
const targetHud = document.getElementById('targetHud');
const target = playerTactics.target;
if (target && target.hp > 0 && gameState === 'PLAYING') {
const proj = target.pos.clone().add(new THREE.Vector3(0,10,0)).project(camera);
targetHud.style.left = `${(proj.x*0.5+0.5)*window.innerWidth}px`;
targetHud.style.top = `${(proj.y*-0.5+0.5)*window.innerHeight}px`;
targetHud.style.display = 'block';
targetHud.className = target.isPlayer ? 'target-hud' : 'target-hud enemy-hud';
targetHud.innerHTML = `TARGET HP:${Math.floor(target.hp)}`;
} else {
targetHud.style.display = 'none';
}
}
// ==========================================
// 4. Three.js セットアップ
// ==========================================
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x020810, 0.013);
const camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 1000);
camera.position.copy(cameraBasePos);
camera.lookAt(0,0,0);
const renderer = new THREE.WebGLRenderer({ antialias:true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0x020810, 1.0);
document.body.appendChild(renderer.domElement);
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth/window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
scene.add(new THREE.AmbientLight(0x304060, 2.5));
const dirLight = new THREE.DirectionalLight(0x80c0ff, 1.8);
dirLight.position.set(20,50,20); scene.add(dirLight);
const rimLight = new THREE.DirectionalLight(0x002040, 0.8);
rimLight.position.set(-20,10,-30); scene.add(rimLight);
const gridHelper = new THREE.GridHelper(100, 25, 0x003366, 0x001122);
scene.add(gridHelper);
const groundGeo = new THREE.PlaneGeometry(100,100);
const groundMat = new THREE.MeshBasicMaterial({ color:0x010812, transparent:true, opacity:0.6 });
const ground = new THREE.Mesh(groundGeo, groundMat);
ground.rotation.x = -Math.PI/2; ground.position.y = 0.01; scene.add(ground);
const blocks = [];
const blockGeo = new THREE.BoxGeometry(8, 10, 8);
const fragments = [];
function generateBlocks() {
blocks.forEach(b => scene.remove(b)); blocks.length = 0;
const hue = Math.random();
const blockMat = new THREE.MeshBasicMaterial({ color:new THREE.Color().setHSL(hue, 0.9, 0.45), wireframe:true });
for (let x=-40; x<=40; x+=20) {
for (let z=-40; z<=40; z+=20) {
if (Math.abs(x)<15 && Math.abs(z)<30) continue;
const mesh = new THREE.Mesh(blockGeo, blockMat);
mesh.position.set(x, 5, z); scene.add(mesh); blocks.push(mesh);
}
}
}
function checkCollision(pos, radius=2.5) {
for (const b of blocks) {
const expand = radius;
if (pos.x > b.position.x - 4 - expand &&
pos.x < b.position.x + 4 + expand &&
pos.z > b.position.z - 4 - expand &&
pos.z < b.position.z + 4 + expand) {
return true;
}
}
return false;
}
// ==========================================
// 5. A* & ルート生成
// ==========================================
const PATH_GRID_SIZE = 4;
const PATH_GRID_W = Math.ceil(100/PATH_GRID_SIZE);
function worldToGrid(x,z) { return { c:Math.floor((x+50)/PATH_GRID_SIZE), r:Math.floor((z+50)/PATH_GRID_SIZE) }; }
function gridToWorld(c,r) { return new THREE.Vector3(c*PATH_GRID_SIZE-50+PATH_GRID_SIZE/2, 1, r*PATH_GRID_SIZE-50+PATH_GRID_SIZE/2); }
function findPath(startPos, endPos, radius=1.5) {
let s = worldToGrid(startPos.x, startPos.z);
let e = worldToGrid(endPos.x, endPos.z);
s.c = Math.max(0,Math.min(PATH_GRID_W-1,s.c)); s.r = Math.max(0,Math.min(PATH_GRID_W-1,s.r));
e.c = Math.max(0,Math.min(PATH_GRID_W-1,e.c)); e.r = Math.max(0,Math.min(PATH_GRID_W-1,e.r));
let open = [{c:s.c,r:s.r,g:0,h:Math.hypot(s.c-e.c,s.r-e.r),parent:null}];
let closed = new Set();
const key = (c,r) => `${c},${r}`;
while (open.length > 0) {
open.sort((a,b)=>(a.g+a.h)-(b.g+b.h));
const cur = open.shift();
if (cur.c===e.c && cur.r===e.r) {
const path=[]; let c=cur;
while(c){ path.push(gridToWorld(c.c,c.r)); c=c.parent; }
path.reverse(); path[0]=startPos.clone(); path[path.length-1]=endPos.clone();
return path;
}
closed.add(key(cur.c,cur.r));
for (const d of [[0,1],[1,0],[0,-1],[-1,0],[1,1],[1,-1],[-1,1],[-1,-1]]) {
const nc=cur.c+d[0], nr=cur.r+d[1];
if (nc<0||nc>=PATH_GRID_W||nr<0||nr>=PATH_GRID_W) continue;
if (closed.has(key(nc,nr))) continue;
if (checkCollision(gridToWorld(nc,nr),radius)) { if(nc!==e.c||nr!==e.r) continue; }
const g = cur.g + (d[0]!==0&&d[1]!==0 ? 1.414 : 1);
const ex = open.find(n=>n.c===nc&&n.r===nr);
if (ex) { if(g<ex.g){ex.g=g;ex.parent=cur;} }
else open.push({c:nc,r:nr,g,h:Math.hypot(nc-e.c,nr-e.r),parent:cur});
}
}
return null;
}
function generateMultiPaths(startPos, targetPos, attackerData, defenderData) {
const routes = [];
const start = startPos.clone().setY(5);
const target = targetPos.clone().setY(5);
let direct = findPath(start,target,1.5) || [start.clone(),target.clone()];
routes.push({path:direct, name:'DIRECT'});
const dir = target.clone().sub(start).normalize();
const L = new THREE.Vector3(-dir.z,0,dir.x);
const R = new THREE.Vector3(dir.z,0,-dir.x);
const lMid = start.clone().lerp(target,0.5).add(L.clone().multiplyScalar(25));
const rMid = start.clone().lerp(target,0.5).add(R.clone().multiplyScalar(25));
const wMid = start.clone().lerp(target,0.4).add(L.clone().multiplyScalar(40));
const pL1=findPath(start,lMid,1.5)||[start.clone(),lMid.clone()];
const pL2=findPath(lMid,target,1.5)||[lMid.clone(),target.clone()];
routes.push({path:pL1.concat(pL2.slice(1)),name:'FLANK_L'});
const pR1=findPath(start,rMid,1.5)||[start.clone(),rMid.clone()];
const pR2=findPath(rMid,target,1.5)||[rMid.clone(),target.clone()];
routes.push({path:pR1.concat(pR2.slice(1)),name:'FLANK_R'});
const pW1=findPath(start,wMid,1.5)||[start.clone(),wMid.clone()];
const pW2=findPath(wMid,target,1.5)||[wMid.clone(),target.clone()];
routes.push({path:pW1.concat(pW2.slice(1)),name:'WIDE_ARC'});
return routes;
}
// ==========================================
// 6. 戦術ルート表示
// ==========================================
function updatePlayerTrajectories(interceptTarget, attackTarget) {
playerTactics.routes = [];
const sp = player.pos.clone().setY(5);
if (interceptTarget) {
const dest = interceptTarget.pos.clone().setY(5);
for (let i=0; i<4; i++) playerTactics.routes.push({path:[sp,dest], name:'INTERCEPT'});
playerTactics.target = interceptTarget.owner || null;
playerTactics.interceptBit = interceptTarget;
} else if (attackTarget) {
playerTactics.routes = generateMultiPaths(sp, attackTarget.pos, player.data, attackTarget.data);
playerTactics.target = attackTarget;
playerTactics.interceptBit = null;
} else {
playerTactics.target = null;
playerTactics.interceptBit = null;
}
}
function drawPlayerTrajectories() {
while (playerTactics.lines.length < 4) {
const mat = new THREE.LineDashedMaterial({dashSize:1.5,gapSize:0.5,transparent:true,opacity:0.5});
const line = new THREE.Line(new THREE.BufferGeometry(), mat);
scene.add(line); playerTactics.lines.push(line);
}
for (let i=0;i<4;i++) {
const line = playerTactics.lines[i];
const arm = player.arms[i];
const route = playerTactics.routes[i];
const inUse = bits.find(b=>b.owner===player&&b.armId===(arm?arm.id:''));
if (!arm||arm.hp<=0||inUse||!route) { line.visible=false; continue; }
line.visible=true;
line.geometry.setFromPoints(route.path.map(p=>p.clone().setY(5)));
line.computeLineDistances();
line.material.color.setHex(CMD_COLORS[i]);
const t = performance.now()*0.001;
if (playerTactics.selectedIndex===i) {
line.material.opacity = 0.6 + Math.sin(t*6)*0.4;
} else if (playerTactics.selectedIndex!==null) {
line.material.opacity = 0.12;
} else {
line.material.opacity = 0.45 + Math.sin(t*2+i)*0.1;
}
}
}
function clearRouteLines() {
for (const l of playerTactics.lines) { scene.remove(l); l.geometry.dispose(); l.material.dispose(); }
playerTactics.lines = [];
}
// ==========================================
// 7. パーティクル & 残像
// ==========================================
const particles = [];
function createHitParticles(pos, color, count=22, speed=22) {
for (let i=0;i<count;i++) {
const size = 0.2 + Math.random()*0.3;
const geo = Math.random()<0.5 ? new THREE.OctahedronGeometry(size,0) : new THREE.SphereGeometry(size,4,4);
const mesh = new THREE.Mesh(geo, new THREE.MeshBasicMaterial({color,wireframe:true}));
mesh.position.copy(pos); scene.add(mesh);
const vel = new THREE.Vector3(
(Math.random()-0.5)*speed,
Math.random()*speed*0.8+4,
(Math.random()-0.5)*speed
);
particles.push({mesh, vel, life:0.6+Math.random()*0.6, maxLife:1.2, spin:new THREE.Vector3(Math.random()*8,Math.random()*8,Math.random()*8)});
}
}
function createExplosionRing(pos, color) {
const geo = new THREE.TorusGeometry(0.1, 0.15, 6, 16);
const mat = new THREE.MeshBasicMaterial({color, wireframe:true, transparent:true, opacity:1});
const mesh = new THREE.Mesh(geo, mat);
mesh.position.copy(pos);
mesh.rotation.x = Math.PI/2;
scene.add(mesh);
particles.push({mesh, vel:new THREE.Vector3(0,0,0), life:0.5, maxLife:0.5, spin:new THREE.Vector3(0,0,0), ring:true, ringGrow:20});
}
function updateParticles(dt) {
for (let i=particles.length-1;i>=0;i--) {
const p=particles[i]; p.life-=dt;
if (p.life<=0) { scene.remove(p.mesh); particles.splice(i,1); continue; }
const ratio = p.life/p.maxLife;
if (p.ring) {
const scale = 1 + (1-ratio)*p.ringGrow;
p.mesh.scale.set(scale,scale,scale);
p.mesh.material.opacity = ratio*0.8;
} else {
p.vel.y -= 25*dt;
p.mesh.position.addScaledVector(p.vel, dt);
p.mesh.rotation.x += p.spin.x*dt;
p.mesh.rotation.y += p.spin.y*dt;
p.mesh.rotation.z += p.spin.z*dt;
p.mesh.material.opacity = ratio;
p.mesh.material.transparent = true;
}
}
}
const afterImages = [];
function createAfterImage(robotGroup, color, opacity=0.5) {
const cg = new THREE.Group();
robotGroup.traverse(child => {
if (child.isMesh) {
const clone = new THREE.Mesh(child.geometry, new THREE.MeshBasicMaterial({color, wireframe:true, transparent:true, opacity}));
clone.position.copy(child.getWorldPosition(new THREE.Vector3()));
clone.quaternion.copy(child.getWorldQuaternion(new THREE.Quaternion()));
clone.scale.copy(child.getWorldScale(new THREE.Vector3()));
cg.add(clone);
}
});
scene.add(cg);
afterImages.push({group:cg, life:0.3, maxLife:0.3});
}
function updateAfterImages(dt) {
for (let i=afterImages.length-1;i>=0;i--) {
const ai=afterImages[i]; ai.life-=dt;
if (ai.life<=0) { scene.remove(ai.group); afterImages.splice(i,1); continue; }
const alpha = (ai.life/ai.maxLife)*0.5;
ai.group.traverse(c=>{ if(c.isMesh) c.material.opacity=alpha; });
}
}
// ==========================================
// 8. ビット(飛翔体)
// ==========================================
const bits = [];
class Bit {
constructor(startPos, path, color, owner, targetObj, armId) {
this.owner=owner; this.targetObj=targetObj; this.path=path;
this.pathIndex=0; this.speed=120; this.pos=startPos.clone();
this.color=color; this.armId=armId; this.isInterceptor=false;
this.bitState='OUTBOUND'; this.laserTimer=0; this.laserLine=null; this.life=15.0;
// 背中から発射された腕(ブレード状)を表現
const geo = new THREE.ConeGeometry(0.6, 4.5, 4);
const mat = new THREE.MeshBasicMaterial({color, wireframe:true});
this.mesh = new THREE.Mesh(geo, mat);
this.mesh.position.copy(this.pos); scene.add(this.mesh);
this.light = new THREE.PointLight(color, 3, 10);
this.mesh.add(this.light);
const pts = this.path.map(p=>p.clone().setY(5));
this.trajectoryLine = new THREE.Line(
new THREE.BufferGeometry().setFromPoints(pts),
new THREE.LineDashedMaterial({color, dashSize:1, gapSize:0.5, transparent:true, opacity:0.7})
);
this.trajectoryLine.computeLineDistances(); scene.add(this.trajectoryLine);
}
update(dt) {
this.life -= dt;
if (this.bitState==='OUTBOUND' && Math.random()<0.3) {
const tPos = this.pos.clone().add(new THREE.Vector3((Math.random()-0.5)*0.5,0,(Math.random()-0.5)*0.5));
const tMesh = new THREE.Mesh(new THREE.OctahedronGeometry(0.15,0), new THREE.MeshBasicMaterial({color:this.color,wireframe:true,transparent:true,opacity:0.7}));
tMesh.position.copy(tPos); scene.add(tMesh);
particles.push({mesh:tMesh,vel:new THREE.Vector3(0,1,0),life:0.15,maxLife:0.15,spin:new THREE.Vector3(0,5,0)});
}
if (this.bitState==='OUTBOUND') {
if (this.isInterceptor && this.targetObj && this.targetObj.life>0) {
const dir = new THREE.Vector3().subVectors(this.targetObj.pos, this.pos).normalize();
this.pos.addScaledVector(dir, this.speed*dt);
this.mesh.position.copy(this.pos);
this.mesh.quaternion.slerp(new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0,1,0), dir), 0.3);
if (this.pos.distanceTo(this.targetObj.pos)<3.0) this.fireLaser();
} else if (this.path && this.pathIndex < this.path.length-1) {
const tp = this.path[this.pathIndex+1].clone().setY(5);
const dir = new THREE.Vector3().subVectors(tp, this.pos).normalize();
const md = this.speed*dt;
const rem = this.pos.distanceTo(tp);
if (rem<=md) { this.pos.copy(tp); this.pathIndex++; }
else { this.pos.addScaledVector(dir, md); }
this.mesh.position.copy(this.pos);
this.mesh.quaternion.slerp(new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0,1,0), dir), 0.3);
if (this.targetObj && this.targetObj.hp>0 && this.pos.distanceTo(this.targetObj.pos)<5) this.fireLaser();
} else { this.fireLaser(); }
} else if (this.bitState==='FIRING') {
this.laserTimer -= dt;
if (this.laserTimer<=0) {
if (this.laserLine && this.laserLine.parent) scene.remove(this.laserLine);
this.laserLine=null; this.mesh.visible=true; this.bitState='RETURN';
}
} else if (this.bitState==='RETURN') {
const tp = this.owner.pos.clone().setY(5);
const dir = new THREE.Vector3().subVectors(tp, this.pos).normalize();
const md = this.speed*4.0*dt;
const rem = this.pos.distanceTo(tp);
if (rem<md||rem<2.0) { this.destroy(); return true; }
this.pos.addScaledVector(dir, md);
this.mesh.position.copy(this.pos);
this.mesh.quaternion.slerp(new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0,-1,0), dir), 0.3);
}
return this.life<=0;
}
fireLaser() {
if (this.bitState==='FIRING') return;
this.bitState='FIRING'; playSound('laser');
if (this.trajectoryLine&&this.trajectoryLine.parent) scene.remove(this.trajectoryLine);
this.trajectoryLine=null; this.mesh.visible=false;
if (this.isInterceptor) {
if (this.targetObj&&this.targetObj.life>0) {
const pts=[this.pos.clone(),this.targetObj.pos.clone()];
this.laserLine=new THREE.Line(new THREE.BufferGeometry().setFromPoints(pts),new THREE.LineBasicMaterial({color:this.color}));
scene.add(this.laserLine);
createHitParticles(this.targetObj.pos,0xffaa00,12,15);
createExplosionRing(this.targetObj.pos,0xffaa00);
createFloatingText('INTERCEPTED!',this.targetObj.pos.clone().add(new THREE.Vector3(0,5,0)),'#ffaa00');
triggerFlash('rgba(255,180,0,0.25)',0.1);
this.targetObj.destroy();
}
} else {
if (!this.targetObj||this.targetObj.hp<=0) { this.laserTimer=0.1; return; }
const pts=[this.pos.clone(),this.targetObj.pos.clone().setY(3)];
this.laserLine=new THREE.Line(new THREE.BufferGeometry().setFromPoints(pts),new THREE.LineBasicMaterial({color:this.color,linewidth:2}));
scene.add(this.laserLine);
this.owner.resolveAttack(this.targetObj);
createHitParticles(this.targetObj.pos.clone().setY(3),this.color,22,24);
createExplosionRing(this.targetObj.pos.clone().setY(3),this.color);
}
this.laserTimer=0.35;
}
destroy() {
if (this.mesh.parent) scene.remove(this.mesh);
if (this.laserLine&&this.laserLine.parent) scene.remove(this.laserLine);
if (this.trajectoryLine&&this.trajectoryLine.parent) scene.remove(this.trajectoryLine);
this.life=0;
}
}
function updateBits(dt) {
for (let i=bits.length-1;i>=0;i--) {
if (bits[i].update(dt)) { bits[i].destroy(); bits.splice(i,1); }
}
}
// ==========================================
// 9. 多腕メカモデル生成(ローポリ・鋭角化)
// ==========================================
function createWireframeRobot(color) {
const g = new THREE.Group();
const mat = () => new THREE.MeshBasicMaterial({color, wireframe:true});
const parts = {};
g.userData.parts = parts;
// ── 胴体 ──
const torso = new THREE.Group();
g.add(torso);
parts.torso = torso;
// 鋭角的な胴体
const torsoMesh = new THREE.Mesh(new THREE.CylinderGeometry(1.8, 0.6, 4.5, 4), mat());
torsoMesh.rotation.y = Math.PI / 4;
torso.add(torsoMesh);
// 胸部装甲
const chestArmor = new THREE.Mesh(new THREE.ConeGeometry(1.5, 2.5, 4), mat());
chestArmor.rotation.x = Math.PI / 2;
chestArmor.rotation.y = Math.PI / 4;
chestArmor.position.set(0, 0.5, 1.2);
torso.add(chestArmor);
// ── 頭 ──
const head = new THREE.Group();
head.position.set(0, 3.0, 0);
torso.add(head);
parts.head = head;
const headMesh = new THREE.Mesh(new THREE.ConeGeometry(1.0, 1.8, 4), mat());
headMesh.rotation.y = Math.PI / 4;
head.add(headMesh);
// 角・バイザー
const horn = new THREE.Mesh(new THREE.ConeGeometry(0.3, 1.5, 3), mat());
horn.rotation.x = -Math.PI / 4;
horn.position.set(0, 0.5, 0.8);
head.add(horn);
// ── 脚 ──
parts.lLeg = new THREE.Group(); parts.lLeg.position.set(-1.2, -2.0, 0); torso.add(parts.lLeg);
const lThigh = new THREE.Mesh(new THREE.CylinderGeometry(0.7, 0.4, 2.8, 4), mat());
lThigh.position.set(0, -1.4, 0); lThigh.rotation.y = Math.PI/4; parts.lLeg.add(lThigh);
parts.lKnee = new THREE.Group(); parts.lKnee.position.set(0, -2.8, 0); parts.lLeg.add(parts.lKnee);
const lShin = new THREE.Mesh(new THREE.CylinderGeometry(0.5, 0.2, 3.0, 4), mat());
lShin.position.set(0, -1.5, 0); lShin.rotation.y = Math.PI/4; parts.lKnee.add(lShin);
parts.rLeg = new THREE.Group(); parts.rLeg.position.set(1.2, -2.0, 0); torso.add(parts.rLeg);
const rThigh = new THREE.Mesh(new THREE.CylinderGeometry(0.7, 0.4, 2.8, 4), mat());
rThigh.position.set(0, -1.4, 0); rThigh.rotation.y = Math.PI/4; parts.rLeg.add(rThigh);
parts.rKnee = new THREE.Group(); parts.rKnee.position.set(0, -2.8, 0); parts.rLeg.add(parts.rKnee);
const rShin = new THREE.Mesh(new THREE.CylinderGeometry(0.5, 0.2, 3.0, 4), mat());
rShin.position.set(0, -1.5, 0); rShin.rotation.y = Math.PI/4; parts.rKnee.add(rShin);
// ── 通常腕 ──
parts.lArm = new THREE.Group(); parts.lArm.position.set(-2.2, 1.5, 0); torso.add(parts.lArm);
const lUA = new THREE.Mesh(new THREE.CylinderGeometry(0.6, 0.4, 2.5, 4), mat());
lUA.position.set(0, -1.25, 0); lUA.rotation.y = Math.PI/4; parts.lArm.add(lUA);
const lShoulder = new THREE.Mesh(new THREE.ConeGeometry(1.2, 2.0, 4), mat());
lShoulder.position.set(0, 0.5, 0); parts.lArm.add(lShoulder);
parts.lElbow = new THREE.Group(); parts.lElbow.position.set(0, -2.5, 0); parts.lArm.add(parts.lElbow);
const lFA = new THREE.Mesh(new THREE.CylinderGeometry(0.4, 0.1, 2.5, 4), mat());
lFA.position.set(0, -1.25, 0); lFA.rotation.y = Math.PI/4; parts.lElbow.add(lFA);
parts.rArm = new THREE.Group(); parts.rArm.position.set(2.2, 1.5, 0); torso.add(parts.rArm);
const rUA = new THREE.Mesh(new THREE.CylinderGeometry(0.6, 0.4, 2.5, 4), mat());
rUA.position.set(0, -1.25, 0); rUA.rotation.y = Math.PI/4; parts.rArm.add(rUA);
const rShoulder = new THREE.Mesh(new THREE.ConeGeometry(1.2, 2.0, 4), mat());
rShoulder.position.set(0, 0.5, 0); parts.rArm.add(rShoulder);
parts.rElbow = new THREE.Group(); parts.rElbow.position.set(0, -2.5, 0); parts.rArm.add(parts.rElbow);
const rFA = new THREE.Mesh(new THREE.CylinderGeometry(0.4, 0.1, 2.5, 4), mat());
rFA.position.set(0, -1.25, 0); rFA.rotation.y = Math.PI/4; parts.rElbow.add(rFA);
// ── 背中の4本腕(飛翔ブレード) ──
parts.backArms = [];
const backArmPos = [
{ x: -1.2, y: 1.5, z: -1.5, rx: 0.5, ry: -0.5, rz: 0.6 }, // Top Left
{ x: 1.2, y: 1.5, z: -1.5, rx: 0.5, ry: 0.5, rz:-0.6 }, // Top Right
{ x: -1.0, y: -0.5, z: -1.5, rx: 0.2, ry: -0.4, rz: 0.9 }, // Bottom Left
{ x: 1.0, y: -0.5, z: -1.5, rx: 0.2, ry: 0.4, rz:-0.9 } // Bottom Right
];
for (let i = 0; i < 4; i++) {
const baPiv = new THREE.Group();
baPiv.position.set(backArmPos[i].x, backArmPos[i].y, backArmPos[i].z);
baPiv.rotation.set(backArmPos[i].rx, backArmPos[i].ry, backArmPos[i].rz);
torso.add(baPiv);
// 鋭利なブレード状のアーム
const blade = new THREE.Mesh(new THREE.ConeGeometry(0.6, 5.0, 4), mat());
blade.position.set(0, 2.5, 0);
baPiv.add(blade);
parts.backArms.push(baPiv);
}
scene.add(g);
return g;
}
function createShieldMesh() {
const mesh = new THREE.Mesh(
new THREE.SphereGeometry(6.0,12,12),
new THREE.MeshBasicMaterial({color:0x00ffff, transparent:true, opacity:0.22, wireframe:true})
);
mesh.visible=false; scene.add(mesh); return mesh;
}
// ==========================================
// 10. Entity クラス
// ==========================================
class Entity {
constructor(color, x, z, isPlayer) {
this.isPlayer=isPlayer; this.color=color;
this.pos=new THREE.Vector3(x,0,z); this.lastPos=this.pos.clone(); this.radius=2.5;
this.maxHp=isPlayer?1000:300; this.hp=this.maxHp;
this.data=0; this.materials=0;
// プレイヤーは初期から4本腕
this.maxArms = isPlayer ? 4 : 2;
this.arms = isPlayer
? [{id:'ARM1',hp:100,maxHp:100},{id:'ARM2',hp:100,maxHp:100},{id:'ARM3',hp:100,maxHp:100},{id:'ARM4',hp:100,maxHp:100}]
: [{id:'ARM1',hp:100,maxHp:100},{id:'ARM2',hp:100,maxHp:100}];
this.robotGroup=createWireframeRobot(color); this.robotGroup.position.copy(this.pos);
this.shieldMesh=createShieldMesh();
this.facing=new THREE.Vector3(0,0,isPlayer?-1:1); this.targetFacing=this.facing.clone();
this.state='idle'; this.timer=0; this.regenTickTimer=0;
this.activeArm=null; this.currentAttackPath=null; this.attackTarget=null; this.activeArmColor=null;
this.afterImageTimer=0; this.walkCycle=0;
this.aiActionTimer=isPlayer?0:2.0; this.moveTarget=new THREE.Vector3(x,0,z);
this.staggerTimer = 0;
this.staggerDir = new THREE.Vector3();
this.launchTimer = 0;
this.launchArmId = null;
this.deathTimer = -1;
this.isDeadDone = false;
}
get dataLevel() { return Math.floor(this.data/100); }
isDataSuperior(o) { return this.data>o.data; }
getAvailableArm() { return this.arms.find(a=>a.hp>0&&!bits.find(b=>b.owner===this&&b.armId===a.id)); }
checkCrafting() {
this.maxArms=Math.min(4, (this.isPlayer ? 4 : 2) + Math.floor(this.data/200));
for (const arm of this.arms) {
if (arm.hp<=0&&this.materials>=10) {
this.materials-=10; arm.hp=arm.maxHp;
if (this.isPlayer) game.addMessage(`SYSTEM: [${arm.id}] REPAIRED.`);
playSound('craft');
}
}
while (this.arms.length<this.maxArms&&this.materials>=20) {
this.materials-=20;
const nid='ARM'+(this.arms.length+1);
this.arms.push({id:nid,hp:100,maxHp:100});
if (this.isPlayer) game.addMessage(`SYSTEM: [${nid}] BUILT.`);
playSound('craft');
}
}
fireBit(targetEntity, preCalcPath, armObj, customColor) {
if (!targetEntity||targetEntity.hp<=0||!armObj) return;
const sp = this.pos.clone().add(new THREE.Vector3(0,5,0));
const path= preCalcPath||generateMultiPaths(sp,targetEntity.pos,this.data,targetEntity.data)[0].path;
bits.push(new Bit(sp,path,customColor||this.color,this,targetEntity,armObj.id));
playSound('attack');
}
update(dt, opponents) {
if (this.deathTimer >= 0) {
this.deathTimer += dt;
this._animateDeath(this.deathTimer);
return;
}
this.regenTickTimer += dt;
if (this.regenTickTimer>1.0) {
this.regenTickTimer=0;
if (this.hp<this.maxHp&&this.materials>=1) {
this.hp=Math.min(this.maxHp,this.hp+10); this.materials-=1;
createFloatingText('+10 HP',this.pos.clone().add(new THREE.Vector3(0,4,0)),'#0f6');
}
}
if (this.targetFacing.lengthSq()>0.001) {
this.facing.lerp(this.targetFacing,0.14); this.facing.y=0; this.facing.normalize();
}
this.robotGroup.rotation.y=Math.atan2(this.facing.x,this.facing.z);
this.robotGroup.position.copy(this.pos);
// よろけ移動
if (this.staggerTimer>0) {
this.staggerTimer-=dt;
const sx = this.pos.x + this.staggerDir.x * dt * 15;
const sz = this.pos.z + this.staggerDir.z * dt * 15;
const testX = new THREE.Vector3(sx, this.pos.y, this.pos.z);
const testZ = new THREE.Vector3(this.pos.x, this.pos.y, sz);
if (!checkCollision(testX, this.radius) && sx>=-45 && sx<=45) this.pos.x = sx;
if (!checkCollision(testZ, this.radius) && sz>=-45 && sz<=45) this.pos.z = sz;
}
this.shieldMesh.position.copy(this.pos).add(new THREE.Vector3(0,5,0));
this.shieldMesh.visible=(this.state==='guarding');
if (this.state==='guarding') this.shieldMesh.rotation.y+=dt*4;
if (this.state==='windup'||this.state==='guarding'||this.staggerTimer>0) {
this.afterImageTimer-=dt;
if (this.afterImageTimer<=0) {
createAfterImage(this.robotGroup, this.color, this.staggerTimer>0?0.4:0.5);
this.afterImageTimer=0.035;
}
} else { this.afterImageTimer=0; }
const movedDist = this.pos.distanceTo(this.lastPos);
const actualSpeed = dt>0 ? movedDist/dt : 0;
this.lastPos.copy(this.pos);
this.animateRobot(dt, actualSpeed);
if (this.state==='windup') {
this.timer-=dt;
if (this.timer<=0) {
this.state='attacking'; this.timer=0.3;
this.fireBit(this.attackTarget,this.currentAttackPath,this.activeArm,this.activeArmColor);
this.launchTimer=0.35;
this.launchArmId=this.activeArm ? this.activeArm.id : 'ARM1';
}
} else if (this.state==='attacking'||this.state==='guarding') {
this.timer-=dt;
if (this.timer<=0) this.state='idle';
}
if (this.launchTimer>0) this.launchTimer-=dt;
}
getNearestOpponent(opponents) {
if (!opponents||opponents.length===0) return null;
let nearest=null, minDist=Infinity;
for (const op of opponents) {
if (op.hp<=0) continue;
const d=this.pos.distanceTo(op.pos);
if (d<minDist) { minDist=d; nearest=op; }
}
return nearest;
}
resolveAttack(defender) {
if (!defender||defender.hp<=0) return;
let isHit=true;
if (defender.state==='guarding') {
const gc=Math.max(0.1,Math.min(1.0, 0.5+((defender.data/100)*0.1)-((this.data/100)*0.05)));
if (Math.random()<gc) {
isHit=false; defender.data+=100;
createFloatingText('GUARD!',defender.pos.clone().add(new THREE.Vector3(0,4,0)),'#0ff','28px');
triggerFlash('rgba(0,200,255,0.2)',0.08);
} else {
createFloatingText('GUARD BREAK!',defender.pos.clone().add(new THREE.Vector3(0,4,0)),'#f80','26px');
}
}
if (isHit) {
const dmg=25; this.data+=100;
const aliveArms=defender.arms.filter(a=>a.hp>0);
if (aliveArms.length>0&&Math.random()<0.4) {
const hitArm=aliveArms[Math.floor(Math.random()*aliveArms.length)];
hitArm.hp-=dmg;
createFloatingText(`ARM HIT! -${dmg}`,defender.pos.clone().add(new THREE.Vector3(0,5,0)),'#fa0','28px');
playSound('crush');
if (hitArm.hp<=0) createFloatingText(`[${hitArm.id}] CRUSHED!`,defender.pos.clone().add(new THREE.Vector3(0,6,0)),'#f00','32px');
} else {
defender.hp-=dmg;
playSound('bigHit');
createFloatingText(`-${dmg}`,defender.pos.clone().add(new THREE.Vector3(0,4,0)),'#f44','40px');
const kbDir=new THREE.Vector3().subVectors(defender.pos,this.pos).normalize();
defender.staggerTimer=0.35;
defender.staggerDir.copy(kbDir);
triggerCameraShake(1.8, 0.25);
}
triggerFlash('rgba(255,60,0,0.3)',0.1);
if (defender.hp<=0) {
defender.hp=0;
defender.deathTimer=0;
playSound('death');
triggerCameraShake(3.0, 0.5);
triggerFlash('rgba(255,0,0,0.5)',0.2);
createHitParticles(defender.pos.clone().add(new THREE.Vector3(0,4,0)),0xff4400,40,28);
createExplosionRing(defender.pos.clone().add(new THREE.Vector3(0,2,0)),defender.color);
createExplosionRing(defender.pos.clone().add(new THREE.Vector3(0,5,0)),0xffffff);
}
}
this.checkCrafting(); defender.checkCrafting(); game.updateUI();
}
_animateDeath(t) {
const parts = this.robotGroup.userData.parts;
if (!parts) return;
const fallAngle = Math.min(Math.PI * 0.5, t * 2.5);
parts.torso.rotation.x = fallAngle;
parts.torso.position.y = 6.0 - Math.sin(fallAngle) * 5;
parts.lLeg.rotation.x = -Math.min(1.0, t * 2);
parts.rLeg.rotation.x = -Math.min(1.0, t * 2);
parts.lKnee.rotation.x = Math.min(1.5, t * 3);
parts.rKnee.rotation.x = Math.min(1.5, t * 3);
parts.lArm.rotation.set(-Math.min(1.5, t*2), 0, 0);
parts.rArm.rotation.set(-Math.min(1.5, t*2), 0, 0);
if (t > 0.8 && !this.isDeadDone) {
this.isDeadDone = true;
this.shieldMesh.visible = false;
}
}
animateRobot(dt, speed) {
const parts = this.robotGroup.userData.parts;
if (!parts) return;
const isRunning = speed > 5;
this.walkCycle += dt * (isRunning ? 14 : 2.5);
const w = Math.sin(this.walkCycle);
const wAbs = Math.abs(w);
// 基準高(地面に埋まらないように保持)
let baseHeight = 6.2;
// ── ダメージによるのけぞり ──
if (this.staggerTimer > 0) {
const sRatio = this.staggerTimer / 0.35;
parts.torso.rotation.x = -0.6 * sRatio; // 後ろへ大きくのけぞる
parts.torso.position.y = baseHeight - (1.0 * sRatio);
parts.lArm.rotation.set(-0.5, 0, 0.8);
parts.rArm.rotation.set(-0.5, 0, -0.8);
parts.head.rotation.x = -0.5 * sRatio;
return;
}
// ── ガード姿勢 ──
if (this.state === 'guarding') {
parts.torso.rotation.x = 0.2;
parts.torso.position.y = baseHeight - 0.5;
parts.lArm.rotation.set(0.5, 0, 0.5);
parts.lElbow.rotation.set(-1.0, 0, 0);
parts.rArm.rotation.set(0.5, 0, -0.5);
parts.rElbow.rotation.set(-1.0, 0, 0);
return;
}
// ── 走行・アイドル(胴体・脚・通常腕) ──
const bobY = isRunning ? Math.max(0, Math.sin(this.walkCycle * 2)) * 0.5 : wAbs * 0.05;
parts.torso.position.y = baseHeight + bobY;
parts.torso.rotation.x = isRunning ? 0.35 : 0.05;
parts.torso.rotation.y = isRunning ? w * 0.1 : 0;
parts.head.rotation.x = isRunning ? -0.2 : 0;
parts.head.rotation.y = w * 0.05;
if (isRunning) {
parts.lArm.rotation.set(w * 0.8, 0, 0.2);
parts.rArm.rotation.set(-w * 0.8, 0, -0.2);
parts.lElbow.rotation.set(-0.5, 0, 0);
parts.rElbow.rotation.set(-0.5, 0, 0);
parts.lLeg.rotation.x = -w * 0.9;
parts.rLeg.rotation.x = w * 0.9;
parts.lKnee.rotation.x = w > 0 ? 0 : -w * 1.5;
parts.rKnee.rotation.x = w < 0 ? 0 : w * 1.5;
} else {
parts.lArm.rotation.set(w * 0.05 + 0.1, 0, 0.2);
parts.rArm.rotation.set(-w * 0.05 + 0.1, 0, -0.2);
parts.lElbow.rotation.set(-0.2, 0, 0);
parts.rElbow.rotation.set(-0.2, 0, 0);
parts.lLeg.rotation.x = 0;
parts.rLeg.rotation.x = 0;
parts.lKnee.rotation.x = 0;
parts.rKnee.rotation.x = 0;
}
// ── 背中の4本腕の制御 ──
const baseRot = [
{ rx: 0.5, ry: -0.5, rz: 0.6 },
{ rx: 0.5, ry: 0.5, rz:-0.6 },
{ rx: 0.2, ry: -0.4, rz: 0.9 },
{ rx: 0.2, ry: 0.4, rz:-0.9 }
];
for (let i = 0; i < 4; i++) {
const ba = parts.backArms[i];
const armId = 'ARM' + (i + 1);
const armObj = this.arms.find(a => a.id === armId);
const isDeployed = bits.some(b => b.owner === this && b.armId === armId);
const isDestroyed = !armObj || armObj.hp <= 0;
if (isDeployed || isDestroyed) {
ba.visible = false;
} else {
ba.visible = true;
if (this.state === 'windup' && this.activeArm && this.activeArm.id === armId) {
// 発射直前の構え(前方を向く)
ba.rotation.x = THREE.MathUtils.lerp(ba.rotation.x, -Math.PI/2, 0.3);
ba.rotation.y = THREE.MathUtils.lerp(ba.rotation.y, 0, 0.3);
ba.rotation.z = THREE.MathUtils.lerp(ba.rotation.z, 0, 0.3);
} else {
// 通常時の羽ばたき・揺らぎ
ba.rotation.x = baseRot[i].rx + Math.sin(this.walkCycle + i)*0.08;
ba.rotation.y = baseRot[i].ry;
ba.rotation.z = baseRot[i].rz + Math.cos(this.walkCycle + i)*0.08;
}
}
}
}
startAttack(opponentList) {
if (this.state!=='idle') return false;
const arm=this.getAvailableArm(); if (!arm) return false;
const target=this.getNearestOpponent(opponentList); if (!target) return false;
const sp=this.pos.clone().setY(5);
const routes=generateMultiPaths(sp,target.pos,this.data,target.data);
this.executeAttack(arm,target,routes[0].path); return true;
}
executeAttack(armObj, target, preCalcPath, customColor) {
this.targetFacing.subVectors(target.pos,this.pos).normalize(); this.targetFacing.y=0;
this.state='windup'; this.activeArm=armObj; this.timer=0.2;
this.currentAttackPath=preCalcPath; this.attackTarget=target;
this.activeArmColor=customColor;
}
startGuard(attacker) {
if (this.state!=='idle'&&this.state!=='guarding') return;
this.state='guarding'; this.timer=0.5;
if (attacker) { this.targetFacing.subVectors(attacker.pos,this.pos).normalize(); this.targetFacing.y=0; }
playSound('hit');
}
}
// ==========================================
// 11. AI
// ==========================================
function runEnemyAI(enemy, player, dt) {
if (enemy.hp<=0||enemy.deathTimer>=0) return;
enemy.aiActionTimer-=dt;
const dataLevel=enemy.dataLevel; const isSuperior=enemy.isDataSuperior(player);
if (enemy.state!=='idle') return;
const attackInterval = isSuperior ? Math.max(2.0,4.0-dataLevel*0.1) : Math.max(3.0,5.0-dataLevel*0.1);
if (enemy.aiActionTimer<=0&&Math.random()<0.03+dataLevel*0.005) {
if (enemy.getAvailableArm()) {
if (isSuperior) {
const pp=player.pos.clone().addScaledVector(new THREE.Vector3(player.targetFacing.x,0,player.targetFacing.z),5);
enemy.moveTarget.copy(pp);
}
if (enemy.startAttack([player])) { enemy.aiActionTimer=attackInterval; return; }
}
}
const vf=fragments.filter(f=>f.userData.type!=='data');
if (vf.length>0) {
let cl=vf[0], md=enemy.pos.distanceTo(cl.position);
for (let i=1;i<vf.length;i++) { const d=enemy.pos.distanceTo(vf[i].position); if(d<md){md=d;cl=vf[i];} }
enemy.moveTarget.copy(cl.position);
} else if (enemy.pos.distanceTo(enemy.moveTarget)<5) {
enemy.moveTarget.set((Math.random()-0.5)*80,1,(Math.random()-0.5)*80);
}
const dir=new THREE.Vector3().subVectors(enemy.moveTarget,enemy.pos);
if (dir.lengthSq()>0.1) {
dir.y=0; dir.normalize(); enemy.targetFacing.copy(dir);
const sp=(20+dataLevel*1.5)*dt;
const nx = enemy.pos.x + dir.x * sp;
const nz = enemy.pos.z + dir.z * sp;
const testX = new THREE.Vector3(nx, enemy.pos.y, enemy.pos.z);
const testZ = new THREE.Vector3(enemy.pos.x, enemy.pos.y, nz);
if (!checkCollision(testX, enemy.radius) && nx>=-45 && nx<=45) enemy.pos.x = nx;
if (!checkCollision(testZ, enemy.radius) && nz>=-45 && nz<=45) enemy.pos.z = nz;
}
}
// ==========================================
// 12. 初期化
// ==========================================
const player = new Entity(0x00ffaa, 0, 20, true);
function createEnemyAt(x,z) {
const e=new Entity(0xff2222,x,z,false);
e.data=Math.floor(Math.random()*200)+(currentStage*150);
e.materials=Math.floor(Math.random()*30); e.checkCrafting();
e.moveTarget=new THREE.Vector3(x,1,z); e.aiActionTimer=2.0+Math.random()*2.0;
return e;
}
function initStage() {
player.hp=player.maxHp; player.pos.set(0,0,20); player.lastPos.copy(player.pos);
player.state='idle'; player.facing.set(0,0,-1); player.targetFacing.set(0,0,-1);
player.robotGroup.position.copy(player.pos);
player.deathTimer=-1; player.isDeadDone=false; player.staggerTimer=0;
clearRouteLines(); playerTactics.selectedIndex=null;
for (const e of enemies) { scene.remove(e.robotGroup); scene.remove(e.shieldMesh); }
enemies.length=0;
for (const b of bits) b.destroy(); bits.length=0;
const sp=[[0,-20],[-15,-15],[15,-15],[-20,-25],[20,-25]];
for (let i=0;i<Math.min(currentStage,5);i++) enemies.push(createEnemyAt(sp[i][0],sp[i][1]));
fragments.forEach(f=>scene.remove(f)); fragments.length=0;
generateBlocks();
game.messages=['SYSTEM: STAGE '+currentStage+' START']; game.updateUI();
}
// ==========================================
// 13. 入力管理
// ==========================================
const keys = {};
window.addEventListener('keydown', e => {
if(['Space','ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.code)) e.preventDefault();
if(audioCtx.state==='suspended') audioCtx.resume();
if (e.code==='Escape') {
if(gameState==='PLAYING') switchState('PAUSE');
else if(gameState==='PAUSE') switchState('PLAYING');
return;
}
if (!keys[e.code]) { keys[e.code]=true; handleInput(e.code); }
});
window.addEventListener('keyup', e => { keys[e.code]=false; });
function handleInput(code) {
if (gameState!=='PLAYING') return;
let armIndex=null;
if(code==='KeyZ') armIndex=0; else if(code==='KeyX') armIndex=1;
else if(code==='KeyC') armIndex=2; else if(code==='KeyV') armIndex=3;
if (armIndex===null) return;
const armObj=player.arms[armIndex];
if (!armObj||armObj.hp<=0) { game.addMessage('SYSTEM: 兵装が使用不可です。'); return; }
if (bits.find(b=>b.owner===player&&b.armId===armObj.id)) return;
if (!playerTactics.routes[armIndex]) return;
// ここで2回押しを判定
if (playerTactics.selectedIndex === armIndex) {
executePlayerAction(armIndex);
playerTactics.selectedIndex = null; // 実行したら選択解除
} else {
playerTactics.selectedIndex = armIndex; // 別のキーなら単に選択状態にする
playSound('menu');
}
game.updateUI();
}
function executePlayerAction(armIndex) {
if (player.state!=='idle'&&player.state!=='guarding') return;
const armObj=player.arms[armIndex];
const routeInfo=playerTactics.routes[armIndex];
if (playerTactics.interceptBit) {
player.startGuard(playerTactics.target);
const ib=new Bit(player.pos.clone().add(new THREE.Vector3(0,5,0)),routeInfo.path,CMD_COLORS[armIndex],player,playerTactics.interceptBit,armObj.id);
ib.isInterceptor=true; bits.push(ib);
playSound('intercept');
createFloatingText('INTERCEPT!',player.pos.clone().add(new THREE.Vector3(0,5,0)),CMD_COLORS_CSS[armIndex]);
} else if (playerTactics.target) {
player.executeAttack(armObj,playerTactics.target,routeInfo.path,CMD_COLORS[armIndex]);
}
}
// ==========================================
// 14. アイテム生成
// ==========================================
setInterval(()=>{
if(gameState!=='PLAYING') return;
for(let k=0;k<5;k++){
const mesh=new THREE.Mesh(new THREE.OctahedronGeometry(1.2),new THREE.MeshBasicMaterial({color:0x00ffff,wireframe:true}));
let valid=false,pos=new THREE.Vector3(),tries=0;
while(!valid&&tries<20){pos.set((Math.random()-0.5)*80,1.5,(Math.random()-0.5)*80);if(!checkCollision(pos,2.0))valid=true;tries++;}
if(!valid) continue;
mesh.position.copy(pos);mesh.userData.type='data';scene.add(mesh);fragments.push(mesh);
}
},4000);
setInterval(()=>{
if(gameState!=='PLAYING') return;
for(let k=0;k<3;k++){
const mesh=new THREE.Mesh(new THREE.BoxGeometry(2,2,2),new THREE.MeshBasicMaterial({color:0xadff2f,wireframe:true}));
let valid=false,pos=new THREE.Vector3(),tries=0;
while(!valid&&tries<20){pos.set((Math.random()-0.5)*80,1.5,(Math.random()-0.5)*80);if(!checkCollision(pos,2.0))valid=true;tries++;}
if(!valid) continue;
mesh.position.copy(pos);mesh.userData.type='material';scene.add(mesh);fragments.push(mesh);
}
},4500);
// ==========================================
// 15. 更新ロジック
// ==========================================
function updateLogic(dt) {
const aliveEnemies = enemies.filter(e => e.hp > 0 && e.deathTimer < 0);
const promptEl = document.getElementById('centerPrompt');
flashTimer -= dt;
if (flashTimer <= 0) { flashOverlay.style.background = 'rgba(0,0,0,0)'; }
updateCameraShake(dt);
playerTactics.updateTimer -= dt;
if (playerTactics.updateTimer<=0) {
playerTactics.updateTimer=0.1;
const incoming=bits.filter(b=>b.targetObj===player&&b.bitState==='OUTBOUND');
incoming.sort((a,b)=>a.pos.distanceToSq(player.pos)-b.pos.distanceToSq(player.pos));
const intercept=(incoming.length>0&&incoming[0].pos.distanceTo(player.pos)<35)?incoming[0]:null;
updatePlayerTrajectories(intercept, player.getNearestOpponent(aliveEnemies));
}
drawPlayerTrajectories();
if (playerTactics.selectedIndex!==null) {
promptEl.innerText='COMMAND SELECTED — PRESS AGAIN TO EXECUTE';
promptEl.className='locked-on';
} else if (playerTactics.interceptBit) {
promptEl.innerText='▲ WARNING: ENEMY ATTACK INCOMING ▲';
promptEl.className='obstructed';
} else if (player.state==='idle'&&aliveEnemies.length>0&&player.getAvailableArm()) {
promptEl.innerText='[ Z / X / C / V ] SELECT COMMAND';
promptEl.className='locked-on';
} else {
promptEl.innerText=''; promptEl.className='';
}
let dx=(keys['ArrowRight']?1:0)-(keys['ArrowLeft']?1:0);
let dz=(keys['ArrowDown']?1:0)-(keys['ArrowUp']?1:0);
if (dx!==0||dz!==0) {
const md=new THREE.Vector3(dx,0,dz).normalize();
player.targetFacing.copy(md);
const spd = 40 * dt;
const nx = player.pos.x + md.x * spd;
const testX = new THREE.Vector3(nx, player.pos.y, player.pos.z);
if (!checkCollision(testX, player.radius) &&
nx >= -45 && nx <= 45) {
player.pos.x = nx;
}
const nz = player.pos.z + md.z * spd;
const testZ = new THREE.Vector3(player.pos.x, player.pos.y, nz);
if (!checkCollision(testZ, player.radius) &&
nz >= -45 && nz <= 45) {
player.pos.z = nz;
}
if(player.state==='guarding') player.state='idle';
}
for (const e of enemies) if (e.hp>0&&e.deathTimer<0) runEnemyAI(e,player,dt);
player.update(dt, aliveEnemies);
for (const e of enemies) e.update(dt,[player]);
updateBits(dt); updateParticles(dt); updateAfterImages(dt);
for (let i=fragments.length-1;i>=0;i--) {
fragments[i].rotation.y+=dt*2; fragments[i].rotation.x+=dt;
if (player.pos.distanceTo(fragments[i].position)<5) {
collectItem(player,fragments[i]); scene.remove(fragments[i]); fragments.splice(i,1); continue;
}
for (const e of aliveEnemies) {
if (fragments[i]&&e.pos.distanceTo(fragments[i].position)<5) {
if(fragments[i].userData.type==='data') continue;
collectItem(e,fragments[i]); scene.remove(fragments[i]); fragments.splice(i,1); break;
}
}
}
updateFloatingTexts(dt); updateTargetHud(); game.updateUI();
if (player.deathTimer >= 0.8 && gameState === 'PLAYING') {
game.addMessage('SYSTEM: 機体大破。GAME OVER'); switchState('GAME_OVER');
} else if (enemies.length > 0 && enemies.every(e => e.deathTimer >= 0.8) && gameState === 'PLAYING') {
accumulatedEnemyData = enemies.reduce((s,e) => s + e.data, 0);
accumulatedEnemyMat = enemies.reduce((s,e) => s + e.materials, 0);
game.addMessage('SYSTEM: 全敵AI沈黙。');
switchState('STAGE_CLEAR');
setTimeout(() => {
if (gameState !== 'STAGE_CLEAR') return;
currentStage++;
if (currentStage > maxStages) switchState('GAME_CLEAR');
else switchState('STAGE_START');
}, 3000);
}
}
function collectItem(collector, itemMesh) {
if (itemMesh.userData.type==='data') {
collector.data+=10; createFloatingText('+DATA 10',collector.pos.clone().add(new THREE.Vector3(0,4,0)),'#0ff');
} else {
collector.materials+=10; createFloatingText('+MAT 10',collector.pos.clone().add(new THREE.Vector3(0,4,0)),'#af2');
}
if(collector.isPlayer) collector.checkCrafting();
}
// ==========================================
// 16. メインループ
// ==========================================
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const dt=Math.min(clock.getDelta(), 0.05);
if (gameState==='PLAYING') updateLogic(dt);
else if (gameState!=='PAUSE') { updateFloatingTexts(dt); updateAfterImages(dt); updateParticles(dt); }
renderer.render(scene, camera);
}
animate();
</script>
</body>
</html>


コメント