横スクロールアクションゲーム?「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公開(アクション改善、敵が強くなった)
・2026/1/10 バージョン1.3公開(アクション改善、敵が弱くなった)
・2026/8/3 バージョン1.4公開
・2026/8/3 バージョン1.5公開
・ダウンロードされる方はこちら。↓
【操作説明】
・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.5</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: 400px; 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 (Tatsumaki): 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); },
score() { this.play(900,'sine',0.12,0.3,400); },
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;
let pierTexture = null;
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.7);
scene.add(amb);
const sun = new THREE.DirectionalLight(0xffffff, 1.1);
sun.position.set(50, 100, 50);
sun.castShadow = true;
sun.shadow.mapSize.set(2048, 2048);
scene.add(sun);
const fill = new THREE.DirectionalLight(0xaaccff, 0.35);
fill.position.set(-60, 40, -30);
scene.add(fill);
// 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 = 1.1;
} else {
bgCol = evening.clone().lerp(night, (progress - 0.5) * 2);
starMat.opacity = (progress - 0.5) * 2; sun.intensity = 1.1 - (progress - 0.5);
}
scene.background = bgCol; scene.fog = new THREE.Fog(bgCol, 100, 600);
}
function randBrightColor() { return new THREE.Color().setHSL(Math.random(), 0.8, 0.5); }
const THUG_CLOTHING_PALETTE = [0x2b2b2b, 0x3a3a3a, 0x4a3728, 0x54602c, 0x5c2626, 0x2f3b4c, 0x1c1c1c, 0x5c4033, 0x6b4a1e, 0x463225, 0x394a3d];
function randThugColor() { return THUG_CLOTHING_PALETTE[Math.floor(Math.random()*THUG_CLOTHING_PALETTE.length)]; }
const SKIN_TONES = [0xe0ac81, 0xc68863, 0x8d5a3c, 0xf0c8a0, 0xa9714f];
function randSkinTone() { return SKIN_TONES[Math.floor(Math.random()*SKIN_TONES.length)]; }
const HAIR_TONES = [0x1a1a1a, 0x2e2018, 0x4a3728, 0x6b6b6b];
function randHairColor() { return HAIR_TONES[Math.floor(Math.random()*HAIR_TONES.length)]; }
const CONTAINER_PALETTE = [0xa5311f, 0x1f5c8b, 0x8a2e1a, 0x1a5c3a, 0x555b60, 0xb5651d, 0x274472];
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 createPierTexture() {
const canvas = document.createElement('canvas');
canvas.width = 256; canvas.height = 256;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#6b5642';
ctx.fillRect(0, 0, 256, 256);
// horizontal plank seams
ctx.strokeStyle = 'rgba(0,0,0,0.4)';
ctx.lineWidth = 3;
for(let i=0; i<8; i++) {
const y = i * 32;
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(256, y); ctx.stroke();
}
// staggered vertical seams between planks
ctx.strokeStyle = 'rgba(0,0,0,0.2)';
ctx.lineWidth = 2;
for(let i=0; i<8; i++) {
const rowY = i * 32;
const offset = (i % 2 === 0) ? 0 : 64;
for(let x = offset; x < 256; x += 128) {
ctx.beginPath(); ctx.moveTo(x, rowY); ctx.lineTo(x, rowY + 32); ctx.stroke();
}
}
// subtle wood grain streaks
ctx.strokeStyle = 'rgba(0,0,0,0.08)';
ctx.lineWidth = 1;
for(let i=0; i<40; i++) {
const y = Math.random()*256;
ctx.beginPath(); ctx.moveTo(Math.random()*256, y); ctx.lineTo(Math.random()*256, y+Math.random()*10-5); ctx.stroke();
}
const tex = new THREE.CanvasTexture(canvas);
tex.wrapS = THREE.RepeatWrapping; tex.wrapT = THREE.RepeatWrapping;
tex.repeat.set(500, 4);
return tex;
}
function addBackgroundContainerStack(x, z) {
const group = new THREE.Group();
const rows = 2 + Math.floor(Math.random()*3);
const w = 26 + Math.random()*10, h = 11, d = 12 + Math.random()*6;
for(let i=0; i<rows; i++) {
const color = CONTAINER_PALETTE[Math.floor(Math.random()*CONTAINER_PALETTE.length)];
const mesh = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), new THREE.MeshStandardMaterial({color, roughness: 0.8, metalness: 0.2}));
mesh.position.set((Math.random()-0.5)*4, h/2 + i*(h+0.4), (Math.random()-0.5)*3);
group.add(mesh);
}
group.position.set(x, 0, z);
stageObj.add(group);
}
function addBackgroundCrane(x, z) {
const group = new THREE.Group();
const steelMat = new THREE.MeshStandardMaterial({color: 0x7d848a, roughness: 0.6, metalness: 0.5});
const towerH = 70;
const tower = new THREE.Mesh(new THREE.BoxGeometry(6, towerH, 6), steelMat);
tower.position.y = towerH/2;
group.add(tower);
const armSign = Math.random() < 0.5 ? 1 : -1;
const arm = new THREE.Mesh(new THREE.BoxGeometry(80, 4, 4), steelMat);
arm.position.set(armSign * 35, towerH, 0);
group.add(arm);
const counterArm = new THREE.Mesh(new THREE.BoxGeometry(22, 4, 4), steelMat);
counterArm.position.set(-armSign * 12, towerH, 0);
group.add(counterArm);
const cable = new THREE.Mesh(new THREE.CylinderGeometry(0.4, 0.4, 30, 6), steelMat);
cable.position.set(armSign * 65, towerH - 15, 0);
group.add(cable);
group.position.set(x, 0, z);
stageObj.add(group);
}
function resetStageEnv() {
updateSky(State.stage);
while(stageObj.children.length) stageObj.remove(stageObj.children[0]);
if (!pierTexture) pierTexture = createPierTexture();
const road = new THREE.Mesh(new THREE.BoxGeometry(10000, 50, 100), new THREE.MeshStandardMaterial({color: 0xffffff, roughness: 0.85, metalness: 0.05, map: pierTexture}));
road.position.y = -25; road.receiveShadow = true;
stageObj.add(road);
const water = new THREE.Mesh(new THREE.PlaneGeometry(10000, 1200), new THREE.MeshBasicMaterial({color: 0x0d3b45, transparent:true, opacity:0.85}));
water.rotation.x = -Math.PI/2; water.position.y = -30;
stageObj.add(water);
// Dockyard background scenery: container stacks & cranes along both sides of the pier
for(let x = -100; x < 3200; x += 170) {
const side = Math.random() < 0.5 ? -1 : 1;
const gz = side * (CFG.Z_LIMIT + 30 + Math.random()*25);
const gx = x + (Math.random()*80 - 40);
if (Math.random() < 0.72) addBackgroundContainerStack(gx, gz);
else addBackgroundCrane(gx, gz);
}
}
// --- 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], spine:[0.6,0,0], armL:[-1.2,0,0], armR:[0.8,0,0], forL:[-1.8,0,0], forR:[-1.8,0,0], legL:[1.0,0,0], legR:[-0.8,0,0], shiL:[1.2,0,0], shiR:[0.5,0,0] } }, { t:1, pose: { ...POSE_ZERO, root:[0.4,0,0], spine:[0.6,0,0], armL:[0.8,0,0], armR:[-1.2,0,0], forL:[-1.8,0,0], forR:[-1.8,0,0], legL:[-0.8,0,0], legR:[1.0,0,0], shiL:[0.5,0,0], shiR:[1.2,0,0] } } ] },
// Punch
'punch1': { loop: false, speed: 0.5, hitFrame: 1, frames: [
{ t:0, pose: { ...POSE_ZERO, spine:[-0.1,0,0], armR:[0, 0, 0.2], forR:[-2.2, 0, 0] } },
{ t:1, pose: { ...POSE_ZERO, spine:[0.1,0,0], armR:[-1.57, 0, 0.1], forR:[0, 0, 0] } },
{ t:2, pose: { ...POSE_ZERO, spine:[0,0,0], armR:[0, 0, 0], forR:[-1.5, 0, 0] } },
{ t:3, pose: { ...POSE_ZERO } }
] },
'punch2': { loop: false, speed: 0.5, hitFrame: 1, frames: [
{ t:0, pose: { ...POSE_ZERO, spine:[0.1,0,0], armL:[0, 0, 0.2], forL:[-2.2, 0, 0] } },
{ t:1, pose: { ...POSE_ZERO, spine:[-0.1,0,0], armL:[-1.57, 0, 0.1], forL:[0, 0, 0] } },
{ t:2, pose: { ...POSE_ZERO, spine:[0,0,0], armL:[0, 0, 0], forL:[-1.5, 0, 0] } },
{ t:3, pose: { ...POSE_ZERO } }
] },
'punch3': { loop: false, speed: 0.25, hitFrame: 2, frames: [
{ t:0, pose: { ...POSE_ZERO, root:[0,-0.1,0], spine:[-0.15,0.3,0], legL:[0.3,0,0], legR:[-0.4,0,0], armR:[-0.3,-0.3,-0.3] } },
{ t:1, pose: { ...POSE_ZERO, root:[0,0.15,0], spine:[0.1,-0.2,0], armR:[-1.6,0,-0.2], forR:[-0.6,0,0], legL:[-0.5,0,0], legR:[0.5,0,0] } },
{ t:2, pose: { ...POSE_ZERO, root:[0,0.3,0], spine:[0.3,-0.3,0], armR:[-2.8,0,0], forR:[0,0,0], legL:[-0.3,0,0], legR:[0.3,0,0] } },
{ t:3, pose: { ...POSE_ZERO, root:[0,0.1,0], spine:[0.1,0,0], armR:[-1.0,0,0] } },
{ t:4, pose: { ...POSE_ZERO } }
] },
'boss_punch': { loop: false, speed: 0.2, hitFrame: 1, frames: [
{ t:0, pose: { ...POSE_ZERO, root:[0,-0.15,0], spine:[0.4,0.3,0], armR:[-0.4,0,0.4], forR:[-1.4,0,0] } },
{ t:1, pose: { ...POSE_ZERO, root:[0,0.1,0], spine:[0.9,-0.3,0], armR:[-2.0,0,-0.15], forR:[0,0,0] } },
{ t:2, pose: { ...POSE_ZERO, root:[0,0.1,0], spine:[0.9,-0.3,0], armR:[-2.0,0,-0.15] } },
{ t:3, pose: { ...POSE_ZERO, root:[0,-0.1,0], spine:[0.2,0,0] } }
] },
'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 } }
] },
'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] } },
{ 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] } },
{ 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] } },
{ 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: Tatsumaki (Spin Kick)
'mega': { loop: true, speed: 0.25, hitFrame: 0, frames: [
{ t:0, pose: { ...POSE_ZERO, root:[0, 8, 0],
spine:[0, 0, 0],
legR:[-1.6, 0, 0.5], shiR:[0, 0, 0], // Right leg straight out
legL:[-0.5, 0, 0], shiL:[2.0, 0, 0], // Left leg tucked
armL:[0, 0, 1.5], armR:[0, 0, -1.5] // Arms spread
}}
] },
'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': { 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] } }
] }
};
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;
const baseHp = isBoss ? 200 : (isPlayer ? 250 : 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;
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.skinTone = isPlayer ? 0xe0ac81 : randSkinTone();
if (isPlayer) this.hairStyle = 'spiky';
else if (isBoss) this.hairStyle = 'bald';
else {
const hr = Math.random();
this.hairStyle = hr < 0.5 ? 'bald' : (hr < 0.8 ? 'mohawk' : 'buzz');
}
this.mesh = new THREE.Group();
scene.add(this.mesh);
this.buildBody(isPlayer ? 0x3a4a63 : (isBoss ? 0x3a1414 : randThugColor()));
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 = this.skinTone;
const jeansCol = 0x1c2438;
const pantsCol = this.isPlayer ? jeansCol : (this.isBoss ? 0x14100d : randThugColor());
const bootsCol = 0x1a1410;
const wristBandCol = 0xd8d0c0;
const padCol = 0x5a5a5a;
this.root = new THREE.Group(); this.root.position.y = 9.0; this.mesh.add(this.root);
const pelvis = new THREE.Mesh(new THREE.BoxGeometry(4.0, 2.6, 3.0), new THREE.MeshStandardMaterial({color: pantsCol, roughness: 0.6, metalness: 0.05}));
this.root.add(pelvis);
this.spine = new THREE.Group(); this.spine.position.y = 1.25; this.root.add(this.spine);
const backPlate = new THREE.Mesh(new THREE.BoxGeometry(4.8, 6.0, 1.0), new THREE.MeshStandardMaterial({color: shirtCol, roughness: 0.6, metalness: 0.05}));
backPlate.position.set(0, 3.0, -1.0); backPlate.castShadow = true; this.spine.add(backPlate);
const absGroup = new THREE.Group(); absGroup.position.set(0, 1.5, 0.4); this.spine.add(absGroup);
const absC = new THREE.Mesh(new THREE.BoxGeometry(1.5, 2.5, 0.8), new THREE.MeshStandardMaterial({color: skin, roughness: 0.6, metalness: 0.05}));
absC.castShadow = true; absGroup.add(absC);
const absL = new THREE.Mesh(new THREE.BoxGeometry(1.0, 2.5, 2.2), new THREE.MeshStandardMaterial({color: shirtCol, roughness: 0.6, metalness: 0.05}));
absL.position.set(1.4, 0, 0); absGroup.add(absL);
const absR = new THREE.Mesh(new THREE.BoxGeometry(1.0, 2.5, 2.2), new THREE.MeshStandardMaterial({color: shirtCol, roughness: 0.6, metalness: 0.05}));
absR.position.set(-1.4, 0, 0); absGroup.add(absR);
const chestGroup = new THREE.Group(); chestGroup.position.set(0, 4.5, 0.6); this.spine.add(chestGroup);
const pecSkinL = new THREE.Mesh(new THREE.BoxGeometry(1.2, 3.0, 0.5), new THREE.MeshStandardMaterial({color: skin, roughness: 0.6, metalness: 0.05}));
pecSkinL.position.set(0.7, 0, 0.2); chestGroup.add(pecSkinL);
const pecSkinR = new THREE.Mesh(new THREE.BoxGeometry(1.2, 3.0, 0.5), new THREE.MeshStandardMaterial({color: skin, roughness: 0.6, metalness: 0.05}));
pecSkinR.position.set(-0.7, 0, 0.2); chestGroup.add(pecSkinR);
const pecL = new THREE.Mesh(new THREE.BoxGeometry(2.0, 4.0, 3.0), new THREE.MeshStandardMaterial({color: shirtCol, roughness: 0.6, metalness: 0.05}));
pecL.position.set(2.1, 0, 0); pecL.castShadow = true; chestGroup.add(pecL);
const pecR = new THREE.Mesh(new THREE.BoxGeometry(2.0, 4.0, 3.0), new THREE.MeshStandardMaterial({color: shirtCol, roughness: 0.6, metalness: 0.05}));
pecR.position.set(-2.1, 0, 0); pecR.castShadow = true; chestGroup.add(pecR);
this.headGroup = new THREE.Group(); this.headGroup.position.y = 6.0; this.spine.add(this.headGroup);
const neck = new THREE.Mesh(new THREE.BoxGeometry(1.5, 1.0, 1.5), new THREE.MeshStandardMaterial({color: skin, roughness: 0.6, metalness: 0.05}));
neck.position.y = 0.5; this.headGroup.add(neck);
// Sphere Head
this.headMesh = new THREE.Mesh(new THREE.SphereGeometry(1.8, 24, 20), new THREE.MeshStandardMaterial({color: skin, roughness: 0.6, metalness: 0.05}));
this.headMesh.position.y = 2.5; this.headGroup.add(this.headMesh);
const nose = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.5, 0.5), new THREE.MeshStandardMaterial({color: 0xffaaaa, roughness: 0.6, metalness: 0.05}));
nose.position.set(0, 2.3, 1.6); this.headGroup.add(nose);
// --- Hair (varies by hairStyle: bald / mohawk / buzz / spiky) ---
const hairCol = randHairColor();
const hairMat = new THREE.MeshStandardMaterial({color: hairCol, roughness: 0.75, metalness: 0.0});
if (this.hairStyle === 'spiky') {
// 1. Center Top
const hair1 = new THREE.Mesh(new THREE.BoxGeometry(2.5, 2.5, 2.5), hairMat);
hair1.position.set(0, 3.8, 0);
hair1.rotation.y = Math.PI / 4;
hair1.rotation.x = -0.2;
this.headGroup.add(hair1);
// 2. Left Side
const hair2 = new THREE.Mesh(new THREE.BoxGeometry(2.0, 2.0, 2.0), hairMat);
hair2.position.set(1.5, 3.2, 0);
hair2.rotation.z = -0.5;
hair2.rotation.y = Math.PI / 6;
this.headGroup.add(hair2);
// 3. Right Side
const hair3 = new THREE.Mesh(new THREE.BoxGeometry(2.0, 2.0, 2.0), hairMat);
hair3.position.set(-1.5, 3.2, 0);
hair3.rotation.z = 0.5;
hair3.rotation.y = -Math.PI / 6;
this.headGroup.add(hair3);
// Back Hair Base (Adjusted Height)
const hairBack = new THREE.Mesh(new THREE.BoxGeometry(3.0, 3.0, 1.5), hairMat);
hairBack.position.set(0, 3.2, -1.2); // Raised Y position
hairBack.rotation.x = -0.2;
this.headGroup.add(hairBack);
} else if (this.hairStyle === 'mohawk') {
const strip = new THREE.Mesh(new THREE.BoxGeometry(0.7, 1.6, 3.6), hairMat);
strip.position.set(0, 4.1, 0);
this.headGroup.add(strip);
const stripFront = new THREE.Mesh(new THREE.BoxGeometry(0.6, 1.1, 1.4), hairMat);
stripFront.position.set(0, 3.6, 1.6);
this.headGroup.add(stripFront);
} else if (this.hairStyle === 'buzz') {
const cap = new THREE.Mesh(new THREE.BoxGeometry(3.2, 1.1, 3.2), hairMat);
cap.position.set(0, 3.85, -0.1);
this.headGroup.add(cap);
}
// 'bald': no hair mesh added at all -- bare scalp like a classic dockyard bruiser.
// Ears
const earL = new THREE.Mesh(new THREE.BoxGeometry(0.5, 1.0, 0.5), new THREE.MeshStandardMaterial({color: skin, roughness: 0.6, metalness: 0.05}));
earL.position.set(1.9, 2.5, 0); this.headGroup.add(earL);
const earR = new THREE.Mesh(new THREE.BoxGeometry(0.5, 1.0, 0.5), new THREE.MeshStandardMaterial({color: skin, roughness: 0.6, metalness: 0.05}));
earR.position.set(-1.9, 2.5, 0); this.headGroup.add(earR);
// --- ARMS ---
this.armL = new THREE.Group(); this.armL.position.set(3.15, 5.0, 0); this.spine.add(this.armL);
const shL = new THREE.Mesh(new THREE.BoxGeometry(2.9, 2.6, 2.9), new THREE.MeshStandardMaterial({color: padCol, roughness: 0.6, metalness: 0.05}));
shL.position.y = -0.5; this.armL.add(shL);
const uaL = new THREE.Mesh(new THREE.CylinderGeometry(0.95, 0.8, 3.6, 8), new THREE.MeshStandardMaterial({color: skin, roughness: 0.6, metalness: 0.05}));
uaL.position.y = -2.5; this.armL.add(uaL);
this.foreL = new THREE.Group(); this.foreL.position.set(0, -4.5, 0); this.armL.add(this.foreL);
const elbowL = new THREE.Mesh(new THREE.SphereGeometry(1.3, 14, 12), new THREE.MeshStandardMaterial({color: skin, roughness: 0.6, metalness: 0.05}));
elbowL.position.set(0, 0.5, 0); this.foreL.add(elbowL);
const bandL = new THREE.Mesh(new THREE.BoxGeometry(1.4, 0.8, 1.4), new THREE.MeshStandardMaterial({color: wristBandCol, roughness: 0.6, metalness: 0.05}));
bandL.position.y = -0.5; this.foreL.add(bandL);
const laL = new THREE.Mesh(new THREE.CylinderGeometry(0.78, 0.62, 3.0, 8), new THREE.MeshStandardMaterial({color: skin, roughness: 0.6, metalness: 0.05}));
laL.position.y = -2.5; this.foreL.add(laL);
const fistL = new THREE.Mesh(new THREE.SphereGeometry(1.4, 14, 12), new THREE.MeshStandardMaterial({color: skin, roughness: 0.6, metalness: 0.05}));
fistL.position.y = -4.5; this.foreL.add(fistL);
this.armR = new THREE.Group(); this.armR.position.set(-3.15, 5.0, 0); this.spine.add(this.armR);
const shR = new THREE.Mesh(new THREE.BoxGeometry(2.9, 2.6, 2.9), new THREE.MeshStandardMaterial({color: padCol, roughness: 0.6, metalness: 0.05}));
shR.position.y = -0.5; this.armR.add(shR);
const uaR = new THREE.Mesh(new THREE.CylinderGeometry(0.95, 0.8, 3.6, 8), new THREE.MeshStandardMaterial({color: skin, roughness: 0.6, metalness: 0.05}));
uaR.position.y = -2.5; this.armR.add(uaR);
this.foreR = new THREE.Group(); this.foreR.position.set(0, -4.5, 0); this.armR.add(this.foreR);
const elbowR = new THREE.Mesh(new THREE.SphereGeometry(1.3, 14, 12), new THREE.MeshStandardMaterial({color: skin, roughness: 0.6, metalness: 0.05}));
elbowR.position.set(0, 0.5, 0); this.foreR.add(elbowR);
const bandR = new THREE.Mesh(new THREE.BoxGeometry(1.4, 0.8, 1.4), new THREE.MeshStandardMaterial({color: wristBandCol, roughness: 0.6, metalness: 0.05}));
bandR.position.y = -0.5; this.foreR.add(bandR);
const laR = new THREE.Mesh(new THREE.CylinderGeometry(0.78, 0.62, 3.0, 8), new THREE.MeshStandardMaterial({color: skin, roughness: 0.6, metalness: 0.05}));
laR.position.y = -2.5; this.foreR.add(laR);
this.handR = new THREE.Group(); this.handR.position.y=-4.5; this.foreR.add(this.handR);
const fistR = new THREE.Mesh(new THREE.SphereGeometry(1.4, 14, 12), new THREE.MeshStandardMaterial({color: skin, roughness: 0.6, metalness: 0.05}));
this.handR.add(fistR);
this.thighL = new THREE.Group(); this.thighL.position.set(1.4, -1.2, 0); this.root.add(this.thighL);
const buttL = new THREE.Mesh(new THREE.BoxGeometry(1.8, 2.0, 2.0), new THREE.MeshStandardMaterial({color: pantsCol, roughness: 0.6, metalness: 0.05}));
buttL.position.set(0, -0.5, -0.5); this.thighL.add(buttL);
const thM_L = new THREE.Mesh(new THREE.CylinderGeometry(1.15, 0.92, 4.0, 8), new THREE.MeshStandardMaterial({color: pantsCol, roughness: 0.6, metalness: 0.05}));
thM_L.position.y = -3.0; this.thighL.add(thM_L);
this.shinL = new THREE.Group(); this.shinL.position.set(0, -5.5, 0); this.thighL.add(this.shinL);
const shinM_L = new THREE.Mesh(new THREE.CylinderGeometry(0.95, 0.75, 4.0, 8), new THREE.MeshStandardMaterial({color: bootsCol, roughness: 0.6, metalness: 0.05}));
shinM_L.position.y = -2.0; this.shinL.add(shinM_L);
const footL = new THREE.Mesh(new THREE.BoxGeometry(1.8, 1.5, 2.5), new THREE.MeshStandardMaterial({color: bootsCol, roughness: 0.6, metalness: 0.05}));
footL.position.set(0, -4.5, 0.5); this.shinL.add(footL);
this.thighR = new THREE.Group(); this.thighR.position.set(-1.4, -1.2, 0); this.root.add(this.thighR);
const buttR = new THREE.Mesh(new THREE.BoxGeometry(1.8, 2.0, 2.0), new THREE.MeshStandardMaterial({color: pantsCol, roughness: 0.6, metalness: 0.05}));
buttR.position.set(0, -0.5, -0.5); this.thighR.add(buttR);
const thM_R = new THREE.Mesh(new THREE.CylinderGeometry(1.15, 0.92, 4.0, 8), new THREE.MeshStandardMaterial({color: pantsCol, roughness: 0.6, metalness: 0.05}));
thM_R.position.y = -3.0; this.thighR.add(thM_R);
this.shinR = new THREE.Group(); this.shinR.position.set(0, -5.5, 0); this.thighR.add(this.shinR);
const shinM_R = new THREE.Mesh(new THREE.CylinderGeometry(0.95, 0.75, 4.0, 8), new THREE.MeshStandardMaterial({color: bootsCol, roughness: 0.6, metalness: 0.05}));
shinM_R.position.y = -2.0; this.shinR.add(shinM_R);
const footR = new THREE.Mesh(new THREE.BoxGeometry(1.8, 1.5, 2.5), new THREE.MeshStandardMaterial({color: bootsCol, roughness: 0.6, metalness: 0.05}));
footR.position.set(0, -4.5, 0.5); this.shinR.add(footR);
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==='SphereGeometry') 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;
}
if (this.state === 'mega') {
this.vy = 0;
if (this.y < 7.5) this.y = 7.5; // Lowered flight height
this.vx = this.facing * 5;
} else {
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;
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';
}
}
if (this.state === 'tackle') {
this.vx = this.facing * 8;
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) {
t.takeDamage(20, 'blowout');
AudioSys.explosion();
this.state = 'idle'; this.vx = 0;
}
}
}
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';
}
if (this.state === 'mega') this.y = 7.5;
} 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 if (this.state === 'mega') {
this.mesh.children.forEach(c => { if(c.material) c.material.emissive = new THREE.Color(0,0.5,1.0); });
} else {
this.mesh.children.forEach(c => { if(c.material) c.material.emissive = new THREE.Color(0,0,0); });
}
if (this.isSpinning || this.state === 'mega') { this.mesh.rotation.y += 0.8; }
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;
this.mesh.children.forEach(c => { if(c.material) c.material.emissive = new THREE.Color(0,0,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);
}
// These offsets must run ONCE per frame, not once per joint.
// (Previously they were inside the loop above, which has 11
// joints -- causing an unintended 11x-per-frame multiplication
// that sent characters flying far off their intended position
// during the punch3 / kick3 finishing moves.)
if (name === 'punch3' && idx >= 1 && idx <= 3) {
this.mesh.position.y += 2.0;
this.mesh.position.x += this.facing * 0.5;
}
if (name === 'kick3' && idx >= 1 && idx <= 3) {
this.mesh.position.x += this.facing * 2.5;
this.mesh.rotation.y += 2.0;
}
if (name === 'boss_punch' && idx >= 1 && idx <= 2) {
this.mesh.position.x += this.facing * 1.2;
}
}
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'];
if (type === 'mega') this.stateTimer = 15; // 竜巻旋風脚の持続時間を短縮
else this.stateTimer = Math.floor(data.frames.length / data.speed);
if (type === 'punch') AudioSys.punch();
else if (type === 'kick') AudioSys.kick();
else if (type === 'mega') AudioSys.jump();
if (type === 'punch' || type === 'kick') {
this.comboCount++;
}
this.lastAttackTime = Date.now();
}
performHitCheck(animName) {
let dmg = 10;
let type = 'normal';
let range = 30;
if (animName.includes('3')) {
dmg = 20; type = 'heavy'; range = 30; AudioSys.heavyHit();
} else if (animName.includes('2')) {
dmg = 12;
} else if (this.state === 'mega') {
dmg = 20; type = 'blowout'; range = 35;
} else if (this.state === 'throw') {
dmg = 15; type = 'throw'; range = 25;
} 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;
if (type === 'blowout') dmg *= 2;
this.hp -= dmg;
AudioSys.hit();
createBlood(this.mesh.position);
if (!this.isPlayer) {
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 = 60;
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 = 60; 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; it.owner = this; }
}
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;
const containerColor = CONTAINER_PALETTE[Math.floor(Math.random()*CONTAINER_PALETTE.length)];
geo = new THREE.BoxGeometry(s, s*0.88, s*0.8);
mat = new THREE.MeshStandardMaterial({color: containerColor, roughness: 0.75, metalness: 0.35});
this.color = mat.color; this.size = s; this.topY = s;
// corrugated ridge bands (visual only, no effect on collision)
const ridgeMat = new THREE.MeshStandardMaterial({color: 0x000000, roughness: 0.8, metalness: 0.1, transparent: true, opacity: 0.22});
for(let ri=-1; ri<=1; ri++) {
const ridge = new THREE.Mesh(new THREE.BoxGeometry(s*1.01, s*0.05, s*0.81), ridgeMat);
ridge.position.y = ri * s*0.26;
this.mesh.add(ridge);
}
const doorSeam = new THREE.Mesh(new THREE.BoxGeometry(s*0.03, s*0.85, s*0.81), new THREE.MeshStandardMaterial({color: 0x1a1a1a, roughness: 0.7, metalness: 0.2}));
doorSeam.position.x = 0;
this.mesh.add(doorSeam);
} else if(type==='weapon') {
geo = new THREE.CylinderGeometry(0.5,0.5,14,6); mat = new THREE.MeshStandardMaterial({color: 0x8b5a2b, roughness: 0.6, metalness: 0.05}); this.mesh.rotation.z = Math.PI/2;
} else if(type==='health') {
geo = new THREE.BoxGeometry(3,3,3); mat = new THREE.MeshStandardMaterial({color: 0x00ff00, roughness: 0.6, metalness: 0.05});
} else if(type==='score') {
geo = new THREE.BoxGeometry(3,1,2); mat = new THREE.MeshStandardMaterial({color: 0xffd700, roughness: 0.6, metalness: 0.05});
}
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;
if(!this.hitProcessed && player && player !== this.owner && 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'
||e.state==='punch'||e.state==='kick'||e.state==='mega'||e.state==='throw'
||e.state==='tackle'||e.state==='boss_punch'||e.state==='jump'||e.state==='dash') 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 engagementDist = e.isBoss ? 20 : 5;
let targetX = pl.x;
let targetZ = pl.z;
const dist = Math.abs(dx);
if (dist > engagementDist + 20 && !e.isBoss) {
if (e.aiSlot === 1) {
targetX = pl.x - (pl.facing * 30);
} else {
targetX = pl.x + (pl.facing * 20);
}
} else if (dist <= engagementDist) {
targetX = pl.x;
}
const tDx = targetX - e.x;
const tDz = targetZ - e.z;
const dToPlayer = Math.abs(dx);
const zToPlayer = Math.abs(dz);
// AIの停止条件(攻撃開始条件)
// 攻撃射程は engagementDist(歩み寄りをやめる距離)より必ず広く取る。
// 以前は固定値12を使っていたため、ボスの engagementDist(20) より
// 攻撃射程の方が狭くなり、「12〜20の間で歩みを止めるが攻撃もしない」
// というデッドゾーンが生まれ、ボスがプレイヤー付近で立ち止まって
// 何もしなくなる原因になっていた。
const attackDistX = e.isBoss ? engagementDist + 6 : 12;
const attackDistZ = e.isBoss ? 4.0 : 2.0;
if (dToPlayer < attackDistX && zToPlayer < attackDistZ) {
e.vx = 0; e.vz = 0;
e.z = pl.z; // 軸合わせを確定
e.facing = Math.sign(dx) || 1;
if (Math.random() < 0.2) {
if (e.isBoss) {
const r = Math.random();
if (r < 0.2) { e.triggerAttack('boss_punch'); }
else if (r < 0.4) { e.triggerAttack('tackle'); }
else if (r < 0.5) { e.triggerAttack('throw'); }
else if (r < 0.7) { e.triggerAttack('punch'); }
else if (r < 0.85) { e.triggerAttack('kick'); }
else { e.triggerAttack('mega'); e.vy = 2; e.vx = e.facing * 8; }
} else {
const r = Math.random();
if (r < 0.5) { e.triggerAttack('punch'); }
else { e.triggerAttack('kick'); }
}
} else {
e.state = 'idle';
}
return;
}
if (Math.abs(tDx) > engagementDist || Math.abs(tDz) > 1.0) {
e.state = 'walk';
e.vx = Math.sign(tDx) * (e.isBoss ? 0.8 : 0.6 + Math.random()*0.2);
if (Math.abs(tDx) > 100) e.vx *= 1.5;
if (Math.abs(tDz) > 1.0) {
e.vz = Math.sign(tDz) * 1.5;
} else {
e.vz = 0; e.z = targetZ;
}
e.facing = Math.sign(dx) || 1;
} else {
e.vx = 0; e.vz = 0;
e.facing = Math.sign(dx) || 1;
e.state = 'idle';
}
}
function checkHit(atk, range, dmg, type) {
let finalDmg = atk.isPlayer ? dmg : Math.ceil(dmg / 2);
const push = 3.0;
// --- 判定の分離 ---
// プレイヤー攻撃時:精密な判定 (Z幅12)
// 敵攻撃時:やや甘い判定 (Z幅20 + リーチ延長)
let zTolerance = 12;
let actualRange = range;
let closeDistLimit = 20;
if (!atk.isPlayer) {
zTolerance = 20; // 敵の攻撃はZ軸判定を広くする
actualRange += 15; // リーチも少し伸ばす
closeDistLimit = 25; // 密着判定も甘くする
}
// 攻撃対象のリスト作成
let targets = [];
if (!atk.isPlayer) {
targets = [State.player];
} else {
targets = State.enemies;
}
targets.forEach(t => {
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;
const dist = Math.abs(dx);
const zDist = Math.abs(dz);
if(dist < actualRange && zDist < zTolerance) {
const isFacingTarget = Math.sign(dx) === atk.facing;
const isVeryClose = dist < closeDistLimit;
if (isVeryClose || isFacingTarget) {
if (t.state.includes('punch') || t.state.includes('kick')) {
if (atk.state.includes('punch') || atk.state.includes('kick')) {
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);
}
}
});
if (atk.isPlayer) {
State.items.forEach(i => {
if(i.type!=='crate') return;
const dx = i.mesh.position.x - atk.x;
const dz = i.mesh.position.z - atk.z;
if(Math.abs(dx) < range + i.size/2 && Math.abs(dz) < 15) {
if(Math.sign(dx)===atk.facing) {
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);
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<24; i++) {
let cx, cz, ok=false;
for(let t=0; t<10; t++) {
cx = 50 + Math.random() * 600;
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) {
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 tSize = (pl.dashTarget.size || 10);
// --- ロックオン・ダッシュの移動先制御 ---
const offsetDist = tSize/2 + 10;
// ターゲットの「手前」または「奥」(画面上の左右)の位置を計算
// プレイヤーがターゲットより左にいるなら、ターゲットの左側(-offset)へ、右なら右側(+offset)へ移動
const targetX = (pl.x < tPos.x) ? tPos.x - offsetDist : tPos.x + offsetDist;
const targetZ = tPos.z;
const dx = targetX - pl.x;
const dz = targetZ - pl.z;
pl.state = 'dash';
// Z軸移動
if (Math.abs(dz) > 1.0) {
pl.vz = Math.sign(dz) * 4.0;
} else {
pl.vz = 0; pl.z = targetZ;
}
// X軸移動
if (Math.abs(dx) > 2.0) {
pl.vx = Math.sign(dx) * 4.0;
pl.facing = Math.sign(tPos.x - pl.x) || 1; // 顔はターゲットの方を向く
} else {
pl.vx = 0;
}
// 指定座標(正面/背面)に到達したら攻撃開始
if (Math.abs(dz) < 2.0 && Math.abs(dx) < 5.0) {
pl.vx = 0; pl.vz = 0; pl.dashTarget = null;
pl.facing = Math.sign(tPos.x - pl.x) || 1;
if (pl.dashAction) {
pl.triggerAttack(pl.dashAction === 'kick' ? 'kick' : 'punch');
pl.dashAction = null;
} else {
pl.state = 'idle';
}
}
// ジャンプキャンセル
if(keys.KeyX && pl.grounded) { pl.vy=5; pl.state='jump'; AudioSys.jump(); pl.dashTarget = null; }
}
}
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) {
// Throw Input Check
let isThrowInput = false;
if (State.view === 'SIDE') {
isThrowInput = keys.ArrowLeft || keys.ArrowRight;
} else {
isThrowInput = keys.ArrowUp || keys.ArrowDown;
}
if(isThrowInput) 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>


コメント