横スクロールアクションゲーム?「KNOCK OUT RUSH」
【更新履歴】
・2026/1/8 バージョン1.0公開
・2026/1/8 バージョン1.1公開
・2026/1/9 他の作品と重複していたタイトルを変更
・2026/1/10 バージョン1.1公開(アクション改善、敵が強くなった)
・2026/1/10 バージョン1.2公開(アクション改善、敵が強くなった)
・ダウンロードされる方はこちら。↓
【操作説明】
・Aキー … パンチ(3発目で敵が吹っ飛ぶ)
・Zキー … キック(3発目で敵が吹っ飛ぶ)
・Xキー … ジャンプ
・Aキーと左キーor右キー … 投げ飛ばし。(敵が吹っ飛ぶ。)
・ZキーとXキー … 飛び蹴り。(敵が吹っ飛ぶ)
・F1キー … 一人称視点モードに切り替え。
・F1キー … 横スクロール視点モードに切り替え。
・F1キー … 三人称視点モードに切り替え。
・相手と同じ攻撃を出すと、回避することができる。
・ソースコードはこちら。↓
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>KNOCK OUT RUSH 1.2</title>
<style>
body { margin: 0; overflow: hidden; background: #000; font-family: 'Arial Black', sans-serif; user-select: none; color: white; }
canvas { display: block; width: 100vw; height: 100vh; }
#ui {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
pointer-events: none; z-index: 10;
display: flex; flex-direction: column; justify-content: space-between;
}
#hud-top {
width: 100%; padding: 20px;
display: flex; justify-content: flex-start; align-items: flex-start;
background: linear-gradient(to bottom, rgba(0,0,0,0.8), transparent);
box-sizing: border-box;
}
.hud-section-left {
display: flex; flex-direction: column;
width: 320px; margin-right: 20px;
text-shadow: 2px 2px 0 #000; flex-shrink: 0;
}
.stage-text { font-size: 24px; color: #ffcc00; line-height: 1.2; }
.score-text { font-size: 24px; color: #00ffff; line-height: 1.2; }
.enemy-count { font-size: 20px; color: #aaa; margin-top: 5px; font-family: monospace; }
.player-hp-box {
display: flex; flex-direction: column; width: 300px; flex-shrink: 0;
}
.hp-label { font-size: 20px; color: #ffcc00; text-shadow: 2px 2px 0 #000; margin-bottom: 2px; }
.hp-frame {
width: 100%; height: 24px; background: #440000; border: 2px solid #fff;
transform: skewX(-20deg); overflow: hidden; box-shadow: 2px 2px 5px rgba(0,0,0,0.8);
}
.hp-fill {
width: 100%; height: 100%;
background-color: #00ff00;
transition: width 0.1s linear, background-color 0.2s;
}
.enemy-hp-box {
display: flex; flex-direction: column; align-items: flex-end;
width: 300px; margin-left: auto; opacity: 0; transition: opacity 0.2s;
}
.enemy-hp-frame {
width: 100%; height: 24px; background: #220000; border: 2px solid #ffaaaa;
transform: skewX(-20deg); overflow: hidden; box-shadow: 2px 2px 5px rgba(0,0,0,0.8);
}
.enemy-hp-fill {
width: 100%; height: 100%;
background-color: #ff0000;
transition: width 0.1s linear;
}
#controls {
padding: 20px;
background: linear-gradient(to top, rgba(0,0,0,0.8), transparent);
font-size: 14px; font-family: sans-serif; text-shadow: 1px 1px 0 #000;
}
#overlay {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.85); display: flex; flex-direction: column;
justify-content: center; align-items: center; z-index: 100;
}
#overlay.hidden { display: none; }
h1 { font-size: 80px; color: #ffcc00; text-shadow: 5px 5px 0 #ff0000; margin: 0; font-style: italic; }
.blink { animation: blink 0.8s infinite; font-size: 30px; margin-top: 20px; }
@keyframes blink { 50% { opacity: 0; } }
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<div id="ui">
<div id="hud-top">
<div class="hud-section-left">
<div class="stage-text">STAGE <span id="stage">1</span></div>
<div class="score-text">SCORE <span id="score">0</span></div>
<div class="enemy-count">KILLS: <span id="kill-count">0</span> / <span id="kill-target">20</span></div>
</div>
<div class="player-hp-box">
<div class="hp-label">PLAYER</div>
<div class="hp-frame"><div id="hp-bar" class="hp-fill"></div></div>
</div>
<div class="enemy-hp-box" id="enemy-hud">
<div class="hp-label" style="color:#ff5555;">ENEMY</div>
<div class="enemy-hp-frame"><div id="enemy-hp-bar" class="enemy-hp-fill"></div></div>
</div>
</div>
<div id="controls">
<strong>[F2] Side View</strong> | [F1] FPS | [F3] TPS<br>
Move: Arrows | Punch: A (Combo) | Kick: Z (Combo) | Jump: X | Throw: Move+A | Mega: Z+X
</div>
</div>
<div id="overlay">
<h1 id="msg-main">KNOCK OUT RUSH</h1>
<div id="msg-sub" class="blink">PRESS 'A' KEY TO START</div>
</div>
<script src="https://unpkg.com/three@0.128.0/build/three.min.js"></script>
<script>
window.onload = function() {
let frameCount = 0;
// --- Audio System ---
const AudioSys = {
ctx: null,
init() { const AC = window.AudioContext||window.webkitAudioContext; if(AC) this.ctx = new AC(); },
play(freq, type, dur, vol, slide=0) {
if(!this.ctx) return; if(this.ctx.state==='suspended') this.ctx.resume();
const o=this.ctx.createOscillator(), g=this.ctx.createGain();
o.type=type; o.frequency.setValueAtTime(freq, this.ctx.currentTime);
if(slide!==0) o.frequency.linearRampToValueAtTime(freq+slide, this.ctx.currentTime+dur);
g.gain.setValueAtTime(vol, this.ctx.currentTime);
g.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime+dur);
o.connect(g); g.connect(this.ctx.destination); o.start(); o.stop(this.ctx.currentTime+dur);
},
noise(dur, vol) {
if(!this.ctx) return;
const b=this.ctx.createBuffer(1,44100*dur,44100), d=b.getChannelData(0);
for(let i=0;i<d.length;i++) d[i]=Math.random()*2-1;
const s=this.ctx.createBufferSource(); s.buffer=b;
const g=this.ctx.createGain(); g.gain.setValueAtTime(vol, this.ctx.currentTime);
g.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime+dur);
s.connect(g); g.connect(this.ctx.destination); s.start();
},
punch() { this.noise(0.1, 0.5); this.play(150,'square',0.1,0.2,-50); },
kick() { this.noise(0.15, 0.5); this.play(100,'square',0.15,0.2,-30); },
hit() { this.noise(0.2, 0.8); this.play(80,'sawtooth',0.2,0.4); },
heavyHit() { this.noise(0.3, 1.0); this.play(60,'sawtooth',0.3,0.6); },
jump() { this.play(300,'sine',0.3,0.2,200); },
explosion() { this.noise(0.8, 1.0); this.play(40,'sawtooth',1.0,0.8,-20); },
pickup() { this.play(600,'sine',0.1,0.3,200); },
heal() { this.play(400,'sine',0.2,0.3,100); this.play(600,'sine',0.4,0.3,100); },
stageStart() { this.play(400,'triangle',1.0,0.5); },
clear() { this.play(800,'sine',0.2,0.4,100); setTimeout(()=>this.play(1200,'sine',0.4,0.4),200); },
boss() { this.play(100,'sawtooth',2.0,0.5); },
allClear() { this.play(600,'sine',0.2,0.5); setTimeout(()=>this.play(800,'sine',0.2,0.5), 200); setTimeout(()=>this.play(1200,'square',1.0,0.5), 400); }
};
// --- Config & State ---
const CFG = { GRAVITY: 0.7, GROUND: 0, Z_LIMIT: 45 };
const WAVE_SIZE = 5;
const FINAL_STAGE = 8;
const State = {
mode: 'START', view: 'SIDE',
score: 0, stage: 1, kills: 0,
spawnedCount: 0, bossSpawned: false,
player: null, enemies: [], items: [], particles: [],
colors: { bg: 0x000000, road: 0x333333 },
activeEnemy: null, activeEnemyTimer: 0,
slowMotion: false, bossDeathTimer: 0
};
// --- Three.js ---
const canvas = document.getElementById('gameCanvas');
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(50, window.innerWidth/window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({canvas, antialias: true});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
// Environment
const amb = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(amb);
const sun = new THREE.DirectionalLight(0xffffff, 0.8);
sun.position.set(50, 100, 50);
sun.castShadow = true;
sun.shadow.mapSize.set(2048, 2048);
scene.add(sun);
// Stars
const starGeo = new THREE.BufferGeometry();
const starCount = 1000;
const starPos = new Float32Array(starCount * 3);
for(let i=0; i<starCount*3; i++) starPos[i] = (Math.random()-0.5)*1000;
starGeo.setAttribute('position', new THREE.BufferAttribute(starPos, 3));
const starMat = new THREE.PointsMaterial({color:0xffffff, size:2, transparent:true, opacity:0});
const starField = new THREE.Points(starGeo, starMat);
scene.add(starField);
// Lock-on Cursor
const cursorGeo = new THREE.ConeGeometry(2, 4, 4);
const cursorMat = new THREE.MeshBasicMaterial({color: 0xffff00, wireframe: false});
const lockOnCursor = new THREE.Mesh(cursorGeo, cursorMat);
lockOnCursor.rotation.x = Math.PI;
lockOnCursor.visible = false;
scene.add(lockOnCursor);
// Stage
const stageObj = new THREE.Group();
scene.add(stageObj);
// --- Functions (Pre-defined) ---
function updateSky(stage) {
const progress = Math.min(1, (stage-1) / 7);
const morning = new THREE.Color(0x87CEEB);
const evening = new THREE.Color(0xFF4500);
const night = new THREE.Color(0x000022);
let bgCol;
if(progress < 0.5) {
bgCol = morning.clone().lerp(evening, progress * 2);
starMat.opacity = 0; sun.intensity = 0.8;
} else {
bgCol = evening.clone().lerp(night, (progress - 0.5) * 2);
starMat.opacity = (progress - 0.5) * 2; sun.intensity = 0.8 - (progress - 0.5);
}
scene.background = bgCol; scene.fog = new THREE.Fog(bgCol, 100, 600);
}
function createLimb(w, h, d, col, parent, x, y, z) {
const grp = new THREE.Group(); grp.position.set(x, y, z);
const m = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), new THREE.MeshLambertMaterial({color: col}));
m.position.y = -h/2; m.castShadow = true; grp.add(m);
if(parent) parent.add(grp);
return grp;
}
function randBrightColor() { return new THREE.Color().setHSL(Math.random(), 0.8, 0.5); }
function isPositionClear(x, z, radius) {
for(let it of State.items) {
if(it.type==='crate') {
const s = it.size/2 + radius;
if(Math.abs(it.mesh.position.x - x) < s && Math.abs(it.mesh.position.z - z) < s) return false;
}
}
return true;
}
function resolveCollision(char, dx, dz) {
let nextX = char.x + dx;
let nextZ = char.z + dz;
let onGroundY = CFG.GROUND;
let hitItem = null;
for (let item of State.items) {
if (item.type !== 'crate') continue;
const size = item.size / 2;
const cx = item.mesh.position.x;
const cz = item.mesh.position.z;
const topY = item.mesh.position.y + size;
const range = size + 5;
const overlapX = (nextX > cx - range && nextX < cx + range);
const overlapZ = (nextZ > cz - range && nextZ < cz + range);
const currentlyAbove = (char.y >= topY - 2.0);
if (overlapX && overlapZ) {
if (currentlyAbove && char.vy <= 0) {
onGroundY = Math.max(onGroundY, topY);
} else if (!currentlyAbove) {
hitItem = item;
const penLeft = nextX - (cx - range);
const penRight = (cx + range) - nextX;
const penBack = nextZ - (cz - range);
const penFront = (cz + range) - nextZ;
const min = Math.min(penLeft, penRight, penBack, penFront);
if (min === penLeft) nextX = cx - range - 0.1;
else if (min === penRight) nextX = cx + range + 0.1;
else if (min === penBack) nextZ = cz - range - 0.1;
else if (min === penFront) nextZ = cz + range + 0.1;
}
}
}
if (nextZ < -CFG.Z_LIMIT) nextZ = -CFG.Z_LIMIT;
if (nextZ > CFG.Z_LIMIT) nextZ = CFG.Z_LIMIT;
return { x: nextX, z: nextZ, ground: onGroundY, hit: hitItem };
}
function resolveCharOverlap(char) {
if(char.state === 'dead' || char.state === 'down') return;
const targets = char.isPlayer ? State.enemies : [State.player, ...State.enemies.filter(e => e !== char)];
targets.forEach(t => {
if(!t || t.state === 'dead') return;
const dx = char.x - t.x;
const dz = char.z - t.z;
const dist = Math.sqrt(dx*dx + dz*dz);
const minDist = 8;
if (dist < minDist && dist > 0.1) {
const push = (minDist - dist) / 2;
char.x += (dx/dist) * push;
char.z += (dz/dist) * push;
if(!t.isPlayer || State.mode !== 'PLAY') { t.x -= (dx/dist) * push; t.z -= (dz/dist) * push; }
}
});
}
function resetStageEnv() {
updateSky(State.stage);
while(stageObj.children.length) stageObj.remove(stageObj.children[0]);
const road = new THREE.Mesh(new THREE.BoxGeometry(10000, 50, 100), new THREE.MeshLambertMaterial({color: 0x333333}));
road.position.y = -25; road.receiveShadow = true;
stageObj.add(road);
const water = new THREE.Mesh(new THREE.PlaneGeometry(10000, 1000), new THREE.MeshBasicMaterial({color: 0x001133, transparent:true, opacity:0.8}));
water.rotation.x = -Math.PI/2; water.position.y = -30;
stageObj.add(water);
}
// --- Animation Data ---
const POSE_ZERO = { root:[0,0,0], spine:[0,0,0], head:[0,0,0], armL:[0,0,1], armR:[0,0,-1], forL:[-0.2,0,0], forR:[-0.2,0,0], legL:[0,0,0], legR:[0,0,0], shiL:[0,0,0], shiR:[0,0,0] };
const ANIM_DATA = {
'idle': { loop: true, speed: 0.05, frames: [ { t:0, pose: { ...POSE_ZERO, spine:[0.1,0,0], armL:[0,0,0.5], armR:[0,0,-0.5], forL:[-0.5,0.5,0], forR:[-0.5,-0.5,0], legL:[-0.1,0,0], legR:[0.1,0,0] } }, { t:1, pose: { ...POSE_ZERO, spine:[0.0,0,0], armL:[0,0,0.4], armR:[0,0,-0.4], forL:[-0.4,0.4,0], forR:[-0.4,-0.4,0], legL:[-0.1,0,0], legR:[0.1,0,0] } } ] },
'walk': { loop: true, speed: 0.15, frames: [ { t:0, pose: { ...POSE_ZERO, root:[0.1,0,0], armL:[-0.5,0,0], armR:[0.5,0,0], forL:[-0.5,0,0], forR:[-0.5,0,0], legL:[0.5,0,0], legR:[-0.5,0,0], shiL:[0.2,0,0], shiR:[0.2,0,0] } }, { t:1, pose: { ...POSE_ZERO, root:[0,0,0], armL:[0.5,0,0], armR:[-0.5,0,0], forL:[-0.5,0,0], forR:[-0.5,0,0], legL:[-0.5,0,0], legR:[0.5,0,0], shiL:[0.2,0,0], shiR:[0.2,0,0] } } ] },
'dash': { loop: true, speed: 0.25, frames: [ { t:0, pose: { ...POSE_ZERO, root:[0.4,0,0], armL:[-1.2,0,0], armR:[0.8,0,0], forL:[-1.5,0,0], forR:[-0.5,0,0], legL:[0.8,0,0], legR:[-0.5,0,0], shiL:[1.0,0,0], shiR:[0.1,0,0] } }, { t:1, pose: { ...POSE_ZERO, root:[0.4,0,0], armL:[0.8,0,0], armR:[-1.2,0,0], forL:[-0.5,0,0], forR:[-1.5,0,0], legL:[-0.5,0,0], legR:[0.8,0,0], shiL:[0.1,0,0], shiR:[1.0,0,0] } } ] },
// Punch 1
'punch1': { loop: false, speed: 0.5, hitFrame: 1, frames: [
{ t:0, pose: { ...POSE_ZERO, spine:[0,0.2,0], armR:[-0.5, 0, 0.5], forR:[-1.8, 0, 0] } },
{ t:1, pose: { ...POSE_ZERO, spine:[0,-0.2,0], armR:[-1.6, 0, -0.6], forR:[0,0,0] } },
{ t:2, pose: { ...POSE_ZERO, spine:[0,0.2,0], armR:[-0.5, 0, 0.5], forR:[-2.0, 0, 0] } },
{ t:3, pose: { ...POSE_ZERO } }
] },
'punch2': { loop: false, speed: 0.5, hitFrame: 1, frames: [
{ t:0, pose: { ...POSE_ZERO, spine:[0,-0.2,0], armL:[-0.5, 0, 0.5], forL:[-1.8, 0, 0] } },
{ t:1, pose: { ...POSE_ZERO, spine:[0,0.2,0], armL:[-1.6, 0.2, 0.8], forL:[0,0,0] } },
{ t:2, pose: { ...POSE_ZERO, spine:[0,-0.2,0], armL:[-0.5, 0, 0.5], forL:[-2.0, 0, 0] } },
{ t:3, pose: { ...POSE_ZERO } }
] },
// Punch 3: Shoryuken
'punch3': { loop: false, speed: 0.25, hitFrame: 2, frames: [
{ t:0, pose: { ...POSE_ZERO, root:[0,-2,0], spine:[0,0.5,0], armR:[-0.5,-0.5,-0.5] } }, // Crouch
{ t:1, pose: { ...POSE_ZERO, root:[0,15,0], spine:[0,-0.5,0], armR:[-3.14,0,0], legL:[0.2,0,0], legR:[-0.5,0,0] } }, // Jump Upper
{ t:2, pose: { ...POSE_ZERO, root:[0,25,0], spine:[0,-0.5,0], armR:[-3.14,0,0] } }, // Peak
{ t:3, pose: { ...POSE_ZERO, root:[0,5,0] } }, // Fall
{ t:4, pose: { ...POSE_ZERO } } // Land
] },
// Boss Punch: Deep Forward Lean
'boss_punch': { loop: false, speed: 0.2, hitFrame: 1, frames: [
{ t:0, pose: { ...POSE_ZERO, root:[0,-3,0], spine:[0.5,0.5,0], armR:[-0.5,0,0.5], forR:[-1.5,0,0] } },
{ t:1, pose: { ...POSE_ZERO, root:[5.0,-10,5.0], spine:[1.4, -0.5, 0], armR:[-1.8, 0, -0.2], forR:[0,0,0] } }, // Super Deep Lean
{ t:2, pose: { ...POSE_ZERO, root:[5.0,-10,5.0], spine:[1.4, -0.5, 0], armR:[-1.8, 0, -0.2] } },
{ t:3, pose: { ...POSE_ZERO, root:[0,-3,0] } }
] },
// Kick
'kick': { loop: false, speed: 0.5, hitFrame: 1, frames: [
{ t:0, pose: { ...POSE_ZERO, root:[0,0,0], legR:[-0.5,0,0] } },
{ t:1, pose: { ...POSE_ZERO, root:[0,0,0], spine:[0,0.2,0], legR:[-2.0, 0, 0.1] } },
{ t:2, pose: { ...POSE_ZERO, root:[0,0,0], legR:[-0.5,0,0] } },
{ t:3, pose: { ...POSE_ZERO } }
] },
'kick2': { loop: false, speed: 0.5, hitFrame: 1, frames: [
{ t:0, pose: { ...POSE_ZERO, root:[0,0,0], legL:[-0.5,0,0] } },
{ t:1, pose: { ...POSE_ZERO, root:[0,0,0], spine:[0,-0.2,0], legL:[-2.0, 0, 0.1] } },
{ t:2, pose: { ...POSE_ZERO, root:[0,0,0], legL:[-0.5,0,0] } },
{ t:3, pose: { ...POSE_ZERO } }
] },
// Kick 3: Tatsumaki (Hurricane Kick) - Horizontal Spin
'kick3': { loop: false, speed: 0.25, hitFrame: 1, frames: [
{ t:0, pose: { ...POSE_ZERO, root:[0,-2,0], spine:[0,-1.0,0], armL:[0,0,1.5], armR:[0,0,-1.5] } }, // Windup
{ t:1, pose: { ...POSE_ZERO, root:[0,5,0], spine:[0,3.14,0], legR:[-1.6,0,1.0], legL:[0,0,0], armL:[0,0,2], armR:[0,0,-2] } }, // Spin 1
{ t:2, pose: { ...POSE_ZERO, root:[0,5,0], spine:[0,6.28,0], legR:[-1.6,0,1.0], legL:[0,0,0], armL:[0,0,2], armR:[0,0,-2] } }, // Spin 2
{ t:3, pose: { ...POSE_ZERO } }
] },
'jump': { loop: false, speed: 0.1, frames: [ { t:0, pose: { ...POSE_ZERO, root:[0.2,0,0], armL:[0,0,2.5], armR:[0,0,-2.5], legL:[-0.5,0,0], legR:[0.2,0,0], shiL:[1.0,0,0], shiR:[0.5,0,0] } } ] },
'mega': { loop: false, speed: 0.1, hitFrame: 0, frames: [ { t:0, pose: { ...POSE_ZERO, root:[0.2, -0.5, -0.4], spine:[0, 0, 0], legR:[-1.6, 0.5, 0], shiR:[0, 0, 0], legL:[-1.8, 0, 0], shiL:[2.2, 0, 0], armL:[-1.0, 0.5, 0.5], forL:[-1.5, 0.5, 0], armR:[0.5, 0, -0.5], forR:[-0.5, 0, 0] }} ] },
'throw': { loop: false, speed: 0.2, hitFrame: 1, frames: [ { t:0, pose: { ...POSE_ZERO, spine:[-0.5,0,0], armL:[-2.8,0,-0.5], armR:[-2.8,0,0.5], forL:[-0.5,0,0], forR:[-0.5,0,0] } }, { t:1, pose: { ...POSE_ZERO, spine:[0.5,0,0], armL:[-1.0,0,-0.5], armR:[-1.0,0,0.5], forL:[-1.5,0,0], forR:[-1.5,0,0], root:[0.2,0,0] } }, { t:2, pose: { ...POSE_ZERO, spine:[0.5,0,0], armL:[-1.0,0,-0.5], armR:[-1.0,0,0.5] } }, { t:3, pose: { ...POSE_ZERO } } ] },
'hurt': { loop: false, speed: 0.15, frames: [ { t:0, pose: { ...POSE_ZERO, root: [-0.6, 0, 0], spine: [-0.6, 0, 0], head: [-0.8, 0, 0], armL: [-0.5, 0, 1.2], armR: [-0.5, 0, -1.2], legL: [0.6, 0, 0], legR: [0.2, 0, 0] }}, { t:1, pose: { ...POSE_ZERO, root: [-0.3, 0, 0], spine: [0.6, 0, 0], head: [0.5, 0, 0], armL: [-1.0, 0.5, 0.8], forL: [-1.8, 0, 0], armR: [-1.0, -0.5, -0.8], forR: [-1.8, 0, 0], legL: [0.4, 0, 0], legR: [0.1, 0, 0] }}, { t:2, pose: { ...POSE_ZERO, spine:[0.6,0,0], head:[0.5,0,0] } } ] },
'down': { loop: false, speed: 0.1, frames: [ { t:0, pose: { ...POSE_ZERO, root:[-1.5, 0, 0], legL:[0.2,0,0], legR:[0.2,0,0], armL:[0,0,1], armR:[0,0,-1], head:[0.2,0,0] }} ] },
'wakeup': { loop: false, speed: 0.08, frames: [ { t:0, pose: { ...POSE_ZERO, root: [-1.5, 0, -1.5], spine: [0.5, 0, 0], legL: [-1.0, 0, 0], legR: [-1.0, 0, 0], armL: [-0.5, 0, 0.5], armR: [-0.5, 0, 0.5]}}, { t:1, pose: { ...POSE_ZERO, root: [-0.5, 0, 0], spine: [0.3, 0, 0], legL: [0.8, 0, 0], shiL: [1.2, 0, 0], armL: [0.5, 0, 0], armR: [0.5, 0, 0] }}, { t:2, pose: { ...POSE_ZERO, root: [-0.2, 0, 0], spine:[0.1,0,0] } } ] },
'dead': { loop: false, speed: 0.1, frames: [ { t:0, pose: { ...POSE_ZERO, root:[-1.5,0,0], armL:[0,0,1.0], armR:[0,0,-1.0], legL:[0.2,0,0.2], legR:[0.2,0,-0.2] } } ] },
// Tackle: Shoulder Charge
'tackle': { loop: false, speed: 0.3, hitFrame: 1, frames: [
{ t:0, pose: { ...POSE_ZERO, root:[0,-2,0], spine:[0.3,0,0], armR:[0.5,0,0], armL:[0.5,0,0] } },
{ t:1, pose: { ...POSE_ZERO, root:[0,-2,0], spine:[0.4,0,0.5], armR:[-1.0,0,0.5], armL:[1.0,0,0] } },
{ t:2, pose: { ...POSE_ZERO, root:[0,-2,0], spine:[0.3,0,0] } }
] }
};
// --- Classes (Defined BEFORE startStage) ---
class Character {
constructor(x, z, isPlayer, isBoss = false, aiSlot = 0) {
this.x=x; this.y=isPlayer?0:50; this.z=z;
this.vx=0; this.vy=0; this.vz=0;
this.isPlayer=isPlayer; this.isBoss=isBoss;
// FIX: Set Base HP correctly so the HUD bar starts full
const baseHp = isBoss ? 200 : (isPlayer ? 100 : 30);
const multiplier = 1 + (State.stage - 1) * 0.1;
this.maxHp = Math.floor(baseHp * multiplier);
this.hp = this.maxHp;
this.facing=1;
this.aiSlot = aiSlot; // 0=Front, 1=Back (Flank)
this.animName = 'idle';
this.animTime = 0;
this.currentFrameIdx = 0;
this.hasHit = false;
this.state='idle';
this.stateTimer=0;
this.dashTarget=null; this.dashAction=null;
this.collidedItem = null;
this.grounded=false; this.invincible=0;
this.heldItem=null;
this.comboCount = 0;
this.lastAttackTime = 0;
this.inputQueue = null;
this.isSpinning = false;
this.mesh = new THREE.Group();
scene.add(this.mesh);
this.buildBody(isPlayer ? 0x3366cc : (isBoss ? 0x660000 : randBrightColor()));
if(isBoss) {
const scale = 1.5 * (1 + (State.stage-1)*0.1);
this.mesh.scale.set(scale, scale, scale);
this.y=100;
}
}
buildBody(shirtCol) {
const skin = 0xffccaa;
const pantsCol = this.isPlayer ? 0x222222 : randBrightColor();
const bootsCol = this.isPlayer ? 0x111111 : randBrightColor();
this.root = new THREE.Group(); this.root.position.y = 9.0; this.mesh.add(this.root);
const pelvis = new THREE.Mesh(new THREE.BoxGeometry(3.6, 2.5, 2.8), new THREE.MeshLambertMaterial({color: pantsCol})); this.root.add(pelvis);
this.spine = new THREE.Group(); this.spine.position.y = 1.25; this.root.add(this.spine);
const chest = new THREE.Mesh(new THREE.BoxGeometry(4.8, 6.0, 3.2), new THREE.MeshLambertMaterial({color: shirtCol})); chest.position.y = 3.0; chest.castShadow = true; this.spine.add(chest);
this.headGroup = new THREE.Group(); this.headGroup.position.y = 6.0; this.spine.add(this.headGroup);
this.headMesh = new THREE.Mesh(new THREE.BoxGeometry(3.0, 3.5, 3.0), new THREE.MeshLambertMaterial({color: skin})); this.headMesh.position.y = 1.75; this.headGroup.add(this.headMesh);
const nose = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.5, 0.5), new THREE.MeshLambertMaterial({color: 0xffaaaa})); nose.position.set(0, 1.5, 1.6); this.headGroup.add(nose);
const hairCol = randBrightColor();
for(let i=0; i<3; i++) {
const h = new THREE.Mesh(new THREE.BoxGeometry(3.2, 1.5, 3.2), new THREE.MeshLambertMaterial({color: hairCol}));
h.position.set((Math.random()-0.5), 3 + i*0.8, (Math.random()-0.5));
h.rotation.y = Math.random();
this.headGroup.add(h);
}
this.armL = createLimb(1.4, 4.5, 1.4, shirtCol, this.spine, 2.9, 5.0, 0);
this.foreL = createLimb(1.2, 4.5, 1.2, skin, this.armL, 0, -4.5, 0);
this.armR = createLimb(1.4, 4.5, 1.4, shirtCol, this.spine, -2.9, 5.0, 0);
this.foreR = createLimb(1.2, 4.5, 1.2, skin, this.armR, 0, -4.5, 0);
this.handR = new THREE.Group(); this.handR.position.y=-4.5; this.foreR.add(this.handR);
this.thighL = createLimb(1.7, 5.5, 1.7, pantsCol, this.root, 1.3, -1.2, 0);
this.shinL = createLimb(1.5, 5.5, 1.5, bootsCol, this.thighL, 0, -5.5, 0);
this.thighR = createLimb(1.7, 5.5, 1.7, pantsCol, this.root, -1.3, -1.2, 0);
this.shinR = createLimb(1.5, 5.5, 1.5, bootsCol, this.thighR, 0, -5.5, 0);
this.joints = { root: this.root, spine: this.spine, head: this.headGroup, armL: this.armL, armR: this.armR, forL: this.foreL, forR: this.foreR, legL: this.thighL, legR: this.thighR, shiL: this.shinL, shiR: this.shinR };
}
setVisibleParts(mode) {
const vis = (mode !== 'FPS');
this.headMesh.visible = vis;
this.headGroup.children.forEach(c => { if(c.geometry && c.geometry.type==='BoxGeometry') c.visible=vis; });
}
update() {
if(this.state === 'dead') {
this.deadTimer = (this.deadTimer||0) + 1;
this.vy -= CFG.GRAVITY; this.x += this.vx; this.y += this.vy;
if(this.y <= CFG.GROUND) { this.y = CFG.GROUND; this.vx *= 0.8; }
this.mesh.position.set(this.x, this.y, this.z);
this.root.position.y = 3.0;
this.playAnim('dead');
if(this.deadTimer > 100) this.remove();
return;
}
this.vy -= CFG.GRAVITY;
const move = resolveCollision(this, this.vx, this.vz);
this.x = move.x;
this.z = move.z;
this.y += this.vy;
this.collidedItem = move.hit;
// Dash Stop
if (this.state === 'dash' && (this.collidedItem || (Math.abs(move.x - (this.x - this.vx)) < 0.1 && Math.abs(move.z - (this.z - this.vz)) < 0.1))) {
this.vx = 0; this.vz = 0; this.dashTarget = null;
if (this.dashAction) {
this.triggerAttack(this.dashAction === 'kick' ? 'kick' : 'punch');
this.dashAction = null;
} else {
this.state = 'idle';
}
}
// Tackle Move & Hit
if (this.state === 'tackle') {
this.vx = this.facing * 8;
// Tackle Hit Check
const t = State.player;
if (t && t.state!=='dead' && t.state!=='down' && t.invincible<=0) {
const dx = t.x - this.x; const dz = t.z - this.z;
if(Math.abs(dx) < 25 && Math.abs(dz) < 15) { // Hit range
t.takeDamage(20, 'blowout'); // Double Damage & Blowout
AudioSys.explosion();
this.state = 'idle'; this.vx = 0; // Stop
}
}
}
if(this.y <= move.ground) {
this.y = move.ground; this.vy = 0; this.grounded = true;
this.vx *= 0.8; this.vz *= 0.8;
this.isSpinning = false;
this.mesh.rotation.x = 0;
if (this.state === 'jump') {
this.state = 'idle';
}
} else {
this.grounded = false;
}
this.z = Math.max(-CFG.Z_LIMIT, Math.min(CFG.Z_LIMIT, this.z));
resolveCharOverlap(this);
this.mesh.position.set(this.x, this.y, this.z);
if(this.state !== 'mega' && this.state !== 'down' && this.state !== 'dead' && !this.isSpinning) {
this.mesh.rotation.y = this.facing === 1 ? Math.PI/2 : -Math.PI/2;
}
if (this.state === 'tackle') {
this.mesh.children.forEach(c => { if(c.material) c.material.emissive = new THREE.Color(Math.random(),0,0); });
} else {
this.mesh.children.forEach(c => { if(c.material) c.material.emissive = new THREE.Color(0,0,0); });
}
if (this.isSpinning) { this.mesh.rotation.x += 0.5; }
let nextAnim = this.state;
if(this.state === 'punch') {
const step = this.comboCount % 3;
nextAnim = (step === 0) ? 'punch1' : (step === 1 ? 'punch2' : 'punch3');
} else if(this.state === 'kick') {
const step = this.comboCount % 3;
nextAnim = (step === 0) ? 'kick' : (step === 1 ? 'kick2' : 'kick3');
}
if (this.state === 'down') {
this.root.position.y = 3.0;
} else if (this.state === 'wakeup') {
const maxTime = 30;
const progress = 1.0 - (Math.max(0, this.stateTimer) / maxTime);
this.root.position.y = 3.0 + (9.0 - 3.0) * Math.pow(progress, 2);
} else if (this.state !== 'kick2' && this.state !== 'boss_punch') {
this.root.position.y = 9.0;
}
this.playAnim(nextAnim);
if(this.stateTimer > 0) {
this.stateTimer--;
if(this.stateTimer < 5 && this.inputQueue) {
this.triggerAttack(this.inputQueue);
}
if(this.stateTimer <= 0) {
if (this.state === 'down') { this.state = 'wakeup'; this.stateTimer = 30; }
else if (this.state === 'wakeup') { this.state = 'idle'; }
else {
this.state = 'idle'; this.vx = 0; this.vz = 0;
}
}
}
if(Date.now() - this.lastAttackTime > 1000 && this.state === 'idle') { this.comboCount = 0; }
if(this.invincible > 0) {
this.invincible--;
this.mesh.visible = (Math.floor(this.invincible/4)%2 === 0);
} else { this.mesh.visible = true; }
}
playAnim(name) {
if(this.animName !== name) {
this.animName = name;
this.animTime = 0;
this.currentFrameIdx = 0;
this.hasHit = false;
}
const data = ANIM_DATA[name] || ANIM_DATA['idle'];
this.animTime += data.speed;
if (data.hitFrame !== undefined && !this.hasHit && Math.floor(this.animTime) >= data.hitFrame) {
this.performHitCheck(name);
this.hasHit = true;
}
let idx = Math.floor(this.animTime);
let alpha = this.animTime - idx;
if(idx >= data.frames.length - 1) {
if(data.loop) { this.animTime = 0; idx = 0; alpha = 0; }
else { idx = Math.max(0, data.frames.length - 2); alpha = 1; }
}
const frameData = data.frames[idx];
if (!frameData) return;
const frameA = frameData.pose;
const frameB = data.frames[idx+1] ? data.frames[idx+1].pose : frameA;
for(const [key, joint] of Object.entries(this.joints)) {
const rotA = frameA[key] || POSE_ZERO[key] || [0,0,0];
const rotB = frameB[key] || POSE_ZERO[key] || [0,0,0];
const rX = rotA[0] + (rotB[0] - rotA[0]) * alpha;
const rY = rotA[1] + (rotB[1] - rotA[1]) * alpha;
const rZ = rotA[2] + (rotB[2] - rotA[2]) * alpha;
joint.rotation.set(rX, rY, rZ);
if (name === 'punch3' && idx >= 1 && idx <= 3) {
this.mesh.position.y += 2.0;
this.mesh.position.x += this.facing * 0.5;
}
if (name === 'mega' && idx >= 1 && idx <= 3) {
this.mesh.position.x += this.facing * 4.0;
}
if (name === 'kick3' && idx >= 1 && idx <= 3) {
// Tatsumaki movement
this.mesh.position.x += this.facing * 2.5;
this.mesh.rotation.y += 2.0; // Spin visual
}
}
}
triggerAttack(type) {
this.inputQueue = null;
this.state = type;
let animName = type;
if(type==='punch') animName = (this.comboCount%3===0)?'punch1':(this.comboCount%3===1)?'punch2':'punch3';
if(type==='kick') animName = (this.comboCount%3===0)?'kick':(this.comboCount%3===1)?'kick2':'kick3';
const data = ANIM_DATA[animName] || ANIM_DATA['punch1'];
this.stateTimer = Math.floor(data.frames.length / data.speed);
if (type === 'punch') AudioSys.punch();
else if (type === 'kick') AudioSys.kick();
if (type === 'punch' || type === 'kick') {
this.comboCount++;
}
this.lastAttackTime = Date.now();
}
performHitCheck(animName) {
let dmg = 10;
let type = 'normal';
let range = 15;
if (animName.includes('3')) {
dmg = 20; type = 'heavy'; range = 20; AudioSys.heavyHit();
} else if (animName.includes('2')) {
dmg = 12;
} else if (this.state === 'mega') {
dmg = 25; type = 'heavy'; range = 30;
} else if (this.state === 'throw') {
dmg = 15; type = 'throw'; range = 20;
} else if (this.state === 'boss_punch') {
range = 50; dmg = 10;
}
if (this.heldItem) { dmg += 10; range += 10; }
checkHit(this, range, dmg, type);
}
takeDamage(dmg, type) {
if(this.invincible > 0 || this.state === 'dead' || this.state === 'down' || this.state === 'wakeup') return;
this.comboCount = 0;
this.inputQueue = null;
this.hp -= dmg;
AudioSys.hit();
createBlood(this.mesh.position);
if (this.isPlayer) {
// HP Bar update logic handled in animate loop
} else {
State.activeEnemy = this;
State.activeEnemyTimer = 60;
}
if(this.heldItem) this.dropWeapon(true);
if(this.hp <= 0) {
this.state = 'dead'; this.deadTimer = 0; this.vx = -this.facing * 5; this.vy = 10;
if(!this.isPlayer) {
State.score += this.isBoss ? 1000 : 10; State.kills++;
if(this.isBoss) { State.slowMotion = true; State.bossDeathTimer = 30; }
} else { showMsg("GAME OVER", "PRESS 'A' TO RETRY"); State.mode = 'GAME_OVER'; }
} else {
if (type === 'blowout') {
this.state = 'down'; this.stateTimer = 90; this.invincible = 100;
this.vx = -this.facing * 10; this.vy = 8;
} else if (type === 'throw') {
this.state = 'hurt'; this.stateTimer = 60; this.invincible = 60;
this.vx = -this.facing * 8; this.vy = 12; this.isSpinning = true;
} else if (type === 'heavy') {
this.state = 'down'; this.stateTimer = 60; this.invincible = 100; this.vx = -this.facing * 4; this.vy = 6;
} else {
this.state = 'hurt'; this.stateTimer = 30; this.invincible = 30; this.vx = -this.facing * 2;
}
this.dashTarget = null;
}
}
dropWeapon(vanish=false) {
if(!this.heldItem) return;
const it = this.heldItem; it.holder = null; this.heldItem = null;
if(vanish) it.remove();
else { scene.add(it.mesh); it.mesh.position.set(this.x, 10, this.z); it.vx = this.facing * 10; it.vy = 2; it.isProjectile = true; }
}
remove() {
scene.remove(this.mesh);
if(this === State.player) return;
State.enemies = State.enemies.filter(e => e !== this);
}
}
class GameItem {
constructor(x, z, type) {
this.type = type; this.hp = type==='crate' ? 5 : 999; this.holder = null;
this.mesh = new THREE.Group(); this.mesh.position.set(x, type==='crate'?5:2, z); scene.add(this.mesh);
let geo, mat;
if(type==='crate') {
const s = 10 + Math.random()*20;
geo = new THREE.BoxGeometry(s,s,s); mat = new THREE.MeshLambertMaterial({color: Math.random()*0xffffff});
this.color = mat.color; this.size = s; this.topY = s;
} else if(type==='weapon') {
geo = new THREE.CylinderGeometry(0.5,0.5,14,6); mat = new THREE.MeshLambertMaterial({color: 0x8b5a2b}); this.mesh.rotation.z = Math.PI/2;
} else if(type==='health') {
geo = new THREE.BoxGeometry(3,3,3); mat = new THREE.MeshLambertMaterial({color: 0x00ff00});
} else if(type==='score') {
geo = new THREE.BoxGeometry(3,1,2); mat = new THREE.MeshLambertMaterial({color: 0xffd700});
}
// Ironball removed per request
this.body = new THREE.Mesh(geo, mat); this.body.castShadow = true; this.mesh.add(this.body);
State.items.push(this);
}
update(player) {
if(this.holder) return;
if(this.isProjectile) {
this.mesh.position.x += this.vx; this.mesh.position.y += this.vy; this.vy -= CFG.GRAVITY;
this.mesh.rotation.z += 0.5;
// Hit check
if(!this.hitProcessed && player && this.mesh.position.distanceTo(player.mesh.position)<20) {
player.takeDamage(10, 'heavy'); AudioSys.explosion(); this.hitProcessed = true; this.remove();
}
if(this.mesh.position.y<0) this.remove();
return;
}
if(player && this.type!=='crate' && !this.isProjectile) {
if(player.mesh.position.distanceTo(this.mesh.position) < 10) {
if(this.type==='score') { State.score+=100; AudioSys.score(); this.remove(); }
else if(this.type==='health') { player.hp=Math.min(player.maxHp, player.hp+100); AudioSys.heal(); this.remove(); }
else if(this.type==='weapon' && !player.heldItem) {
player.heldItem = this; this.holder = player;
scene.remove(this.mesh); player.handR.add(this.mesh);
this.mesh.position.set(0,0,0); this.mesh.rotation.set(Math.PI/2,0,0);
AudioSys.pickup();
}
}
}
}
hit(dmg=1) {
if(this.type!=='crate') return;
this.hp -= dmg; AudioSys.hit(); createSpark(this.mesh.position);
if(this.hp<=0) {
AudioSys.explosion();
for(let i=0; i<10; i++) createDebris(this.mesh.position, this.color);
const r = Math.random();
if(r<0.3) new GameItem(this.mesh.position.x, this.mesh.position.z, 'weapon'); else if(r<0.6) new GameItem(this.mesh.position.x, this.mesh.position.z, 'health');
this.remove();
}
}
remove() { scene.remove(this.mesh); State.items = State.items.filter(i=>i!==this); }
}
// --- Logic ---
function updateEnemyAI(e) {
if(e.state==='dead'||e.state==='down'||e.state==='wakeup'||e.state==='hurt') return;
const pl = State.player;
if (!pl || pl.state === 'dead') { e.vx=0; e.vz=0; e.state='idle'; return; }
const dx = pl.x - e.x; const dz = pl.z - e.z;
if (!e.heldItem) {
let nearestWeapon = null, minDist = 200;
State.items.forEach(i => {
if (i.type === 'weapon' && !i.holder) {
const d = e.mesh.position.distanceTo(i.mesh.position);
if (d < minDist) { minDist = d; nearestWeapon = i; }
}
});
if (nearestWeapon) {
const wdx = nearestWeapon.mesh.position.x - e.x; const wdz = nearestWeapon.mesh.position.z - e.z;
if (Math.abs(wdx) > 5 || Math.abs(wdz) > 5) { e.state = 'walk'; e.vx = Math.sign(wdx) * 0.6; e.vz = Math.sign(wdz) * 0.6; e.facing = Math.sign(wdx) || 1; return; }
}
}
const atkRange = e.isBoss ? 20 : 15;
// --- IMPROVED AI ---
// Calculate Target Position
let targetX = pl.x;
let targetZ = pl.z;
// Flanking Logic
const dist = Math.abs(dx);
if (dist > atkRange && !e.isBoss) {
if (e.aiSlot === 1) { // Flanker (Go Behind)
// Attempt to go to the opposite side of where player is facing
targetX = pl.x - (pl.facing * 30);
} else {
// Attacker (Front)
targetX = pl.x + (pl.facing * 20);
}
} else if (dist <= atkRange) {
targetX = pl.x; // Close enough to engage
}
const tDx = targetX - e.x;
const tDz = targetZ - e.z;
// Force Movement logic (No more idle waiting)
if (Math.abs(tDx) > atkRange || Math.abs(tDz) > 2.0) {
e.state = 'walk';
e.vx = Math.sign(tDx) * (e.isBoss ? 0.8 : 0.6 + Math.random()*0.2); // Rush speed
// If very far, move faster
if (Math.abs(tDx) > 100) e.vx *= 1.5;
if (Math.abs(tDz) > 2.0) {
e.vz = Math.sign(tDz) * 1.5;
} else {
e.vz = 0; e.z = targetZ;
}
e.facing = Math.sign(dx) || 1; // Always face player real position
} else {
// Close Range Combat
e.vx = 0; e.vz = 0;
e.facing = Math.sign(dx) || 1;
// Instant attack logic (less hesitation)
if (Math.random() < 0.15) {
if (e.isBoss) {
const r = Math.random();
if (r < 0.5) { e.triggerAttack('boss_punch'); }
else if (r < 0.8) { e.triggerAttack('tackle'); }
else { e.triggerAttack('kick'); }
} else if (e.heldItem) { e.triggerAttack('throw'); }
else {
const r = Math.random();
if (r < 0.6) { e.triggerAttack('punch'); }
else if (r < 0.9) { e.triggerAttack('kick'); }
else { e.triggerAttack('mega'); e.vy = 2; e.vx = e.facing * 8; }
}
} else {
e.state = 'idle';
}
}
}
function checkHit(atk, range, dmg, type) {
let finalDmg = atk.isPlayer ? dmg : Math.ceil(dmg / 2);
const push = 3.0;
if (!atk.isPlayer) {
const t = State.player;
if(t.state==='dead'||t.state==='down'||t.state==='wakeup'||t.invincible>0) return;
const dx = t.x - atk.x;
const dz = t.z - atk.z;
// 敵からの攻撃ヒット判定 (Z軸許容範囲を50pxに拡大)
if((Math.abs(dx) < 10 || Math.sign(dx)===atk.facing) && Math.abs(dx)<range && Math.abs(dz) < 50) {
const isParry = (t.state.includes('punch') || t.state.includes('kick')) && (atk.state.includes('punch') || atk.state.includes('kick'));
if (isParry) { createSpark(new THREE.Vector3((atk.x+t.x)/2, 10, (atk.z+t.z)/2)); AudioSys.noise(0.05, 0.3); return; }
t.x += Math.sign(dx) * push;
t.takeDamage(finalDmg, type);
}
return;
}
const targets = State.enemies;
targets.forEach(t => {
const dx = t.x - atk.x;
if(Math.sign(dx)===atk.facing && Math.abs(dx)<range && Math.abs(t.z-atk.z)<15) {
const isParry = (t.state.includes('punch') || t.state.includes('kick')) && (atk.state.includes('punch') || atk.state.includes('kick'));
if (isParry) { createSpark(new THREE.Vector3((atk.x+t.x)/2, 10, (atk.z+t.z)/2)); AudioSys.noise(0.05, 0.3); return; }
t.x += Math.sign(dx) * push;
t.takeDamage(finalDmg, type);
}
});
State.items.forEach(i => {
const dx = i.mesh.position.x - atk.x;
if(i.type==='crate' && Math.sign(dx)===atk.facing && Math.abs(dx)<range + i.size/2 + 5 && Math.abs(i.mesh.position.z-atk.z)<10) {
if(atk.state==='mega') i.hit(999); else i.hit(1);
}
});
}
function createBlood(pos) { const m = new THREE.Mesh(new THREE.BoxGeometry(0.8,0.8,0.8), new THREE.MeshBasicMaterial({color: 0xff0000})); m.position.copy(pos); m.vx = (Math.random()-0.5)*3; m.vy = Math.random()*3+2; m.vz = (Math.random()-0.5)*3; scene.add(m); State.particles.push(m); }
function createSpark(pos) { const m = new THREE.Mesh(new THREE.BoxGeometry(1,1,1), new THREE.MeshBasicMaterial({color: 0xffff00})); m.position.copy(pos); m.vx = (Math.random()-0.5)*4; m.vy = Math.random()*4+2; m.vz = (Math.random()-0.5)*4; scene.add(m); State.particles.push(m); }
function createDebris(pos, col) { const m = new THREE.Mesh(new THREE.BoxGeometry(2,2,2), new THREE.MeshBasicMaterial({color: col})); m.position.copy(pos); m.vx = (Math.random()-0.5)*4; m.vy = Math.random()*3+2; m.vz = (Math.random()-0.5)*4; scene.add(m); State.particles.push(m); }
function showMsg(main, sub) {
const el = document.getElementById('overlay'); el.classList.remove('hidden');
document.getElementById('msg-main').innerText = main; document.getElementById('msg-sub').innerText = sub;
}
function startStage() {
resetStageEnv();
State.mode = 'INTRO'; State.kills = 0; State.stageX = 0;
State.spawnedCount = 0; State.bossSpawned = false;
State.slowMotion = false; State.bossDeathTimer = 0;
if (State.player) { scene.remove(State.player.mesh); State.player = null; }
State.player = new Character(0, 0, true);
// FIX: Use MaxHP determined by constructor
State.enemies.forEach(e => { scene.remove(e.mesh); }); State.items.forEach(i => { scene.remove(i.mesh); });
State.enemies = []; State.items = [];
for(let i=0; i<8; i++) {
let cx, cz, ok=false;
for(let t=0; t<10; t++) { cx = 50+Math.random()*200; cz = (Math.random()*2-1)*(CFG.Z_LIMIT-10); if(isPositionClear(cx, cz, 10)) { ok=true; break; } }
if(ok) new GameItem(cx, cz, 'crate');
}
showMsg(`STAGE ${State.stage}`, "START!"); AudioSys.stageStart();
setTimeout(() => { document.getElementById('overlay').classList.add('hidden'); State.mode = 'PLAY'; spawnWave(); }, 2000);
}
function finishStage() {
State.slowMotion = false;
if(State.stage >= FINAL_STAGE) { State.mode = 'GAME_CLEAR'; AudioSys.allClear(); showMsg("ALL CLEAR!!", "YOU ARE THE CHAMPION!"); }
else { State.mode = 'CLEAR'; AudioSys.clear(); showMsg("STAGE CLEAR!", "FULL RECOVERY!"); State.player.hp = State.player.maxHp; setTimeout(() => { State.stage++; document.getElementById('stage').innerText = State.stage; startStage(); }, 4000); }
}
function spawnWave() {
const maxEnemies = 20 + (State.stage - 1) * 10;
if (State.spawnedCount >= maxEnemies) return;
const remaining = maxEnemies - State.spawnedCount;
const count = Math.min(WAVE_SIZE, remaining);
for(let i=0; i<count; i++) {
let ex, ez, ok=false;
for(let t=0; t<10; t++) { ex = State.player.x + 60 + Math.random()*40; ez = (Math.random()*2-1) * (CFG.Z_LIMIT-5); if(isPositionClear(ex, ez, 5)) { ok=true; break; } }
if(ok) {
// Assign slot for flanking (Alternating)
const aiSlot = i % 2;
State.enemies.push(new Character(ex, ez, false, false, aiSlot));
State.spawnedCount++;
}
}
}
function spawnBoss() {
State.bossSpawned = true; showMsg("WARNING", "BOSS INCOMING");
setTimeout(()=> { document.getElementById('overlay').classList.add('hidden'); AudioSys.boss(); State.enemies.push(new Character(State.player.x+80, 0, false, true)); }, 2000);
}
const keys={};
window.addEventListener('keydown', e => {
if(e.key.startsWith('F')) e.preventDefault();
keys[e.code]=true;
if(e.code==='KeyA' && (State.mode==='START' || State.mode==='GAME_OVER' || State.mode==='GAME_CLEAR')) {
if(State.mode==='GAME_OVER' || State.mode==='GAME_CLEAR') location.reload();
else { AudioSys.init(); State.player = new Character(0,0,true); startStage(); }
}
if(e.code==='F1') State.view='FPS';
if(e.code==='F2') State.view='SIDE';
if(e.code==='F3') State.view='TPS';
if(e.code==='ArrowDown' && State.view==='FPS' && State.mode==='PLAY' && State.player) { State.player.facing = -State.player.facing; }
});
window.addEventListener('keyup', e => keys[e.code]=false);
function animate() {
requestAnimationFrame(animate);
if (State.mode === 'START') {
const t = Date.now()*0.0005;
camera.position.set(Math.sin(t)*100, 50, Math.cos(t)*100);
camera.lookAt(0, 0, 0);
renderer.render(scene, camera);
return;
}
if (State.mode === 'INTRO') {
if(State.player) {
const pl = State.player;
if(State.view==='SIDE') { camera.position.set(pl.x, 25, 120); camera.lookAt(pl.x, 10, 0); }
else { camera.position.set(pl.x, 30, pl.z+60); camera.lookAt(pl.x, 10, pl.z); }
}
renderer.render(scene, camera);
return;
}
if (State.slowMotion) {
frameCount++;
if (frameCount % 4 !== 0) return;
if (State.bossDeathTimer > 0) { State.bossDeathTimer--; if(State.bossDeathTimer <= 0) finishStage(); }
}
const pl = State.player;
if(State.mode==='PLAY' && pl && pl.state!=='dead') {
if(pl.dashTarget) {
if(pl.collidedItem && pl.collidedItem.type==='crate' && pl.collidedItem !== pl.dashTarget) { pl.vx = -pl.vx * 0.5; pl.vz = -pl.vz * 0.5; pl.dashTarget = null; }
else {
const tPos = pl.dashTarget.mesh ? pl.dashTarget.mesh.position : pl.dashTarget;
const dx = tPos.x - pl.x; const dz = tPos.z - pl.z;
const dist = Math.sqrt(dx*dx + dz*dz);
const tRadius = (pl.dashTarget.size ? pl.dashTarget.size : 10) / 2;
const targetDist = tRadius + 2;
const dz_diff = tPos.z - pl.z;
const dx_diff = tPos.x - pl.x;
const distToSurfaceX = Math.abs(dx_diff) - tRadius;
if (Math.abs(dz_diff) > 2.0 || distToSurfaceX > 2.0) {
pl.state = 'dash';
if (Math.abs(dz_diff) > 1.0) {
pl.vz = Math.sign(dz_diff) * 4.0;
pl.vx = Math.sign(dx_diff) * (Math.abs(dx_diff) > 50 ? 3.5 : 1.0);
} else {
pl.vz = 0; pl.z = tPos.z;
if (distToSurfaceX > 2.0) {
pl.vx = Math.sign(dx_diff) * 4.0;
} else {
pl.vx = 0;
}
}
pl.facing = Math.sign(dx_diff) || 1;
if(keys.KeyX && pl.grounded) { pl.vy=5; pl.state='jump'; AudioSys.jump(); pl.dashTarget = null; }
} else {
pl.vx = 0; pl.vz = 0; pl.dashTarget = null;
if(pl.state !== 'dash') {
if (keys.KeyA) pl.triggerAttack('punch');
else if (keys.KeyZ) pl.triggerAttack('kick');
}
}
}
}
else if(pl.state === 'idle' || pl.state === 'walk' || pl.state === 'jump' || (pl.stateTimer < 10 && pl.comboCount > 0)) {
let mx=0, mz=0; const spd=1.2;
if(pl.state !== 'punch' && pl.state !== 'kick' && pl.state !== 'mega' && pl.state !== 'throw' && pl.state !== 'hurt' && pl.state !== 'dash') {
if(State.view==='SIDE') {
if(keys.ArrowRight) { mx=spd; pl.facing=1; }
if(keys.ArrowLeft) { mx=-spd; pl.facing=-1; }
if(keys.ArrowUp) mz=-spd;
if(keys.ArrowDown) mz=spd;
}
else if(State.view==='FPS') {
if(keys.ArrowUp) mx = spd * pl.facing;
if(keys.ArrowLeft) mz = -spd * pl.facing;
if(keys.ArrowRight) mz = spd * pl.facing;
if(keys.ArrowDown) mx = -spd * pl.facing;
}
else { // TPS
if(keys.ArrowUp) { mx=spd; pl.facing=1; }
if(keys.ArrowDown) { mx=-spd; pl.facing=-1; }
if(keys.ArrowRight) mz=spd;
if(keys.ArrowLeft) mz=-spd;
}
if(mx!==0 || mz!==0) {
if(pl.grounded) pl.state='walk'; pl.vx=mx; pl.vz=mz;
} else if(pl.grounded) pl.state='idle';
}
if(keys.KeyA) { if((mx!==0||mz!==0)) pl.triggerAttack('throw'); else pl.triggerAttack('punch'); }
else if(keys.KeyZ) { if(keys.KeyX) pl.triggerAttack('mega'); else if(pl.heldItem) { pl.dropWeapon(); AudioSys.punch(); } else pl.triggerAttack('kick'); }
else if(keys.KeyX && !keys.KeyZ && pl.grounded) { pl.vy=5; pl.state='jump'; AudioSys.jump(); }
}
}
let lockedObj = null;
if(pl && State.mode==='PLAY') {
let minDist=60;
State.items.filter(i=>i.type==='crate').forEach(i => { if(pl.mesh.position.distanceTo(i.mesh.position) < 15 + i.size/2) lockedObj=i; });
if(!lockedObj) { State.enemies.forEach(e => { if(e.state==='dead') return; const d = pl.mesh.position.distanceTo(e.mesh.position); const dx = e.x - pl.x; if(Math.sign(dx)===pl.facing && d < minDist) { minDist=d; lockedObj=e; } }); }
if(!lockedObj) { State.items.filter(i=>i.type==='crate').forEach(i => { const d = pl.mesh.position.distanceTo(i.mesh.position); const dx = i.mesh.position.x - pl.x; if(Math.sign(dx)===pl.facing && d < minDist) { minDist=d; lockedObj=i; } }); }
if (lockedObj && !pl.dashTarget && pl.grounded && (keys.KeyA || keys.KeyZ)) {
const tPos = lockedObj.mesh ? lockedObj.mesh.position : lockedObj;
const dist = pl.mesh.position.distanceTo(tPos);
if (dist > 30) {
pl.dashTarget = lockedObj;
pl.dashAction = keys.KeyA ? 'punch' : 'kick';
}
}
}
if(lockedObj) {
lockOnCursor.visible = true;
const pos = lockedObj.mesh ? lockedObj.mesh.position : lockedObj;
const h = lockedObj.isBoss?40 : (lockedObj.size ? lockedObj.size + 15 : 25);
lockOnCursor.position.set(pos.x, pos.y + h, pos.z);
} else { lockOnCursor.visible = false; }
State.enemies.forEach(e => { updateEnemyAI(e); e.update(); });
State.items.forEach(i => i.update(State.mode==='PLAY'?pl:null));
for(let i=State.particles.length-1; i>=0; i--) {
const p = State.particles[i]; p.position.add(new THREE.Vector3(p.vx, p.vy, p.vz)); p.vy -= 0.1; p.rotation.x+=0.1;
if(p.position.y<0) { scene.remove(p); State.particles.splice(i,1); }
}
const ehud = document.getElementById('enemy-hud');
if(State.activeEnemy && State.activeEnemyTimer > 0 && State.activeEnemy.state!=='dead') {
State.activeEnemyTimer--; ehud.style.opacity = 1;
document.getElementById('enemy-hp-bar').style.width = (State.activeEnemy.hp / State.activeEnemy.maxHp)*100 + '%';
} else { ehud.style.opacity = 0; }
if(pl) {
pl.setVisibleParts(State.view); pl.update();
document.getElementById('hp-bar').style.width = (pl.hp/pl.maxHp)*100 +'%';
document.getElementById('score').innerText = State.score;
document.getElementById('kill-count').innerText = State.kills;
const maxEnemies = 20 + (State.stage - 1) * 10;
document.getElementById('kill-target').innerText = maxEnemies;
const livingEnemies = State.enemies.filter(e => e.state !== 'dead').length;
if(livingEnemies === 0) {
if (State.spawnedCount < maxEnemies) { spawnWave(); }
else if (!State.bossSpawned) { spawnBoss(); }
}
if(State.view==='SIDE') { camera.position.set(pl.x, 25, 120); camera.lookAt(pl.x, 10, 0); }
else if(State.view==='TPS') { camera.position.set(pl.x - 50, 40, pl.z); camera.lookAt(pl.x + 100, 10, pl.z); }
else { camera.position.set(pl.x + pl.facing*2, 17, pl.z); camera.lookAt(pl.x + pl.facing*100, 15, pl.z); }
}
renderer.render(scene, camera);
}
window.onresize = () => { camera.aspect = window.innerWidth/window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); };
animate();
};
</script>
</body>
</html>

コメント