横スクロールアクションゲーム?「KNOCK OUT RUSH」
【更新履歴】
・2026/1/8 バージョン1.0公開
・2026/1/8 バージョン1.1公開
・2026/1/9 他の作品と重複していたタイトルを変更
・2026/1/10 バージョン1.1公開(アクション改善、敵が強くなった)
・ダウンロードされる方はこちら。↓
・ソースコードはこちら。↓
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>KNOCK OUT RUSH</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: #220000; 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: linear-gradient(90deg, #ff0000, #ffff00, #00ff00); transition: width 0.1s; }
.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: linear-gradient(90deg, #550000, #ff0000); transition: width 0.1s; }
#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 (↓Turn) | [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;
}
// Define helper functions before they are used in Classes
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) {
const nextX = char.x + dx;
const nextZ = char.z + dz;
let moveX = dx;
let moveZ = 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 margin = 4;
const overlapX = (nextX > cx - size - margin && nextX < cx + size + margin);
const overlapZ = (nextZ > cz - size - margin && nextZ < cz + size + margin);
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 xOverlapOnly = (char.x > cx - size - margin && char.x < cx + size + margin);
const zOverlapOnly = (char.z > cz - size - margin && char.z < cz + size + margin);
if (zOverlapOnly) moveX = 0; else if (xOverlapOnly) moveZ = 0; else { moveX = 0; moveZ = 0; }
}
}
}
return { x: moveX, z: moveZ, 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;
const nx = dx / dist;
const nz = dz / dist;
char.x += nx * push;
char.z += nz * push;
if(!t.isPlayer || State.mode !== 'PLAY') { t.x -= nx * push; t.z -= nz * 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] } } ] },
'punch1': { loop: false, speed: 0.25, frames: [ { t:0, pose: { ...POSE_ZERO, spine:[0,0.5,0], armR:[-1.0,-0.5,0], forR:[-1.5,0,0] } }, { t:1, pose: { ...POSE_ZERO, root:[0.2, 0.4, 0.2], spine:[0, -0.9, 0], armL:[-0.5, 0.5, 0.5], armR:[-1.6, 0, -0.6], forR:[0,0,0], legL:[0.6,0,0], legR:[-0.4,0,0] } }, { t:2, pose: { ...POSE_ZERO, root:[0.2, 0.4, 0.2], spine:[0, -0.9, 0], armR:[-1.6, 0, -0.6], forR:[0,0,0] } }, { t:3, pose: { ...POSE_ZERO } } ] },
'punch2': { loop: false, speed: 0.25, frames: [ { t:0, pose: { ...POSE_ZERO, spine:[0,-0.5,0], armL:[-1.0,0.5,0.5], forL:[-1.5,0,0] } }, { t:1, pose: { ...POSE_ZERO, root:[0.3, -0.4, -0.2], spine:[0.1, 1.0, 0], armR:[-0.5, -0.5, -0.5], armL:[-1.6, 0.2, 0.8], forL:[0,0,0], legL:[-0.4,0,0], legR:[0.6,0,0] } }, { t:2, pose: { ...POSE_ZERO, root:[0.3, -0.4, -0.2], spine:[0.1, 1.0, 0], armL:[-1.6, 0.2, 0.8], forL:[0,0,0] } }, { t:3, pose: { ...POSE_ZERO } } ] },
'punch3': { loop: false, speed: 0.2, frames: [ { t:0, pose: { ...POSE_ZERO, spine:[0,0.8,0], armR:[-0.5,-0.5,-0.5] } }, { t:1, pose: { ...POSE_ZERO, root:[0, -0.5, 0], spine:[0, -1.0, 0], armR:[-2.5, 0, 0], head:[-0.5,0,0], legL:[0.5,0,0], legR:[-0.5,0,0] } }, { t:2, pose: { ...POSE_ZERO, root:[0, -0.5, 0], spine:[0, -1.0, 0], armR:[-2.5, 0, 0] } }, { t:3, pose: { ...POSE_ZERO } } ] },
'boss_punch': { loop: false, speed: 0.2, frames: [ { t:0, pose: { ...POSE_ZERO, root:[0,-3,0], spine:[0,0.5,0], armR:[-1.0,-0.5,0], forR:[-1.5,0,0], legL:[0.5,0,0], legR:[-0.5,0,0] } }, { t:1, pose: { ...POSE_ZERO, root:[0.2,-3,0.2], spine:[0, -0.9, 0], armL:[-0.5, 0.5, 0.5], armR:[-1.6, 0, -0.6], forR:[0,0,0], legL:[0.6,0,0], legR:[-0.4,0,0] } }, { t:2, pose: { ...POSE_ZERO, root:[0.2,-3,0.2], spine:[0, -0.9, 0], armR:[-1.6, 0, -0.6], forR:[0,0,0] } }, { t:3, pose: { ...POSE_ZERO, root:[0,-3,0] } } ] },
'kick': { loop: false, speed: 0.2, frames: [ { t:0, pose: { ...POSE_ZERO, root:[-0.2,0,0], legR:[-0.8,0,0], shiR:[0,0,0] } }, { t:1, pose: { ...POSE_ZERO, root:[-0.5,0,-0.2], spine:[0,0,0.3], legL:[0,0,0], legR:[-1.9, 0, 0.1], shiR:[0,0,0], armL:[0.5,0,0], armR:[-0.5,0,0] } }, { t:2, pose: { ...POSE_ZERO, root:[-0.5,0,-0.2], legR:[-1.9, 0, 0.1], shiR:[0,0,0] } }, { t:3, pose: { ...POSE_ZERO } } ] },
'kick2': { loop: false, speed: 0.2, frames: [ { t:0, pose: { ...POSE_ZERO, root:[0,-0.5,0], legL:[0.5,0,0] } }, { t:1, pose: { ...POSE_ZERO, root:[0,-0.5,0], spine:[0,3.14,0], legL:[0,0,0], legR:[-1.5,0,1.5], armL:[0,0,1], armR:[0,0,-1] } }, { t:2, pose: { ...POSE_ZERO } } ] },
'kick3': { loop: false, speed: 0.15, frames: [ { t:0, pose: { ...POSE_ZERO, root:[0,-1,0], legR:[-1,0,0] } }, { t:1, pose: { ...POSE_ZERO, root:[0,2,0], spine:[0,3.14*2,0], legR:[-2.0,0,0], armL:[0,0,2], armR:[0,0,-2] } }, { t:2, 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, 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, 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] } } ] }
};
// --- Classes (Defined BEFORE startStage) ---
class Character {
constructor(x, z, isPlayer, isBoss = false) {
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;
this.maxHp = isPlayer ? 100 : (isBoss ? 200 : 60);
this.hp = this.maxHp;
this.facing=1;
this.animName = 'idle';
this.animTime = 0;
this.currentFrameIdx = 0;
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.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;
if(this.y <= move.ground) {
this.y = move.ground; this.vy = 0; this.grounded = true;
this.vx *= 0.8; this.vz *= 0.8;
} else {
this.grounded = false;
}
// コースアウト防止 (Z軸クランプ)
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.mesh.rotation.y = this.facing === 1 ? Math.PI/2 : -Math.PI/2;
}
let nextAnim = 'idle';
if(this.state === 'walk') nextAnim = 'walk';
if(this.state === 'dash') nextAnim = 'dash';
if(this.state === 'jump') nextAnim = 'jump';
// 3段攻撃
if(this.state === 'punch') {
const step = this.comboCount % 3;
if(step === 0) nextAnim = 'punch1';
else if(step === 1) nextAnim = 'punch2';
else nextAnim = 'punch3';
}
if(this.state === 'boss_punch') nextAnim = 'boss_punch';
if(this.state === 'kick') {
const step = this.comboCount % 3;
if(step === 0) nextAnim = 'kick';
else if(step === 1) nextAnim = 'kick2';
else nextAnim = 'kick3';
}
if(this.state === 'mega') nextAnim = 'mega';
if(this.state === 'throw') nextAnim = 'throw';
if(this.state === 'hurt') nextAnim = 'hurt';
if(this.state === 'down') nextAnim = 'down';
if(this.state === 'wakeup') nextAnim = 'wakeup';
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 {
this.root.position.y = 9.0;
}
this.playAnim(nextAnim);
if(this.stateTimer > 0) {
this.stateTimer--;
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; }
const data = ANIM_DATA[name] || ANIM_DATA['idle'];
this.animTime += data.speed;
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);
}
}
takeDamage(dmg, isHeavy) {
if(this.invincible > 0 || this.state === 'dead' || this.state === 'down' || this.state === 'wakeup') return;
this.comboCount = 0;
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 (isHeavy) { 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});
}
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;
State.enemies.forEach(e=>{
if(e.state!=='dead' && this.mesh.position.distanceTo(e.mesh.position)<10) { e.takeDamage(100, true); AudioSys.hit(); 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(100, 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 ? 25 : 15;
if (Math.abs(dx) < 150) {
if (Math.abs(dx) < atkRange && Math.abs(dz) < 10) {
e.vx = 0; e.vz = 0;
if (Math.random() < 0.05) {
if (e.isBoss) {
if (Math.random() < 0.6) { e.state = 'boss_punch'; e.stateTimer = 20; AudioSys.punch(); setTimeout(() => checkHit(e, 15, 10, false), 100); }
else { e.state = 'kick'; e.stateTimer = 20; AudioSys.kick(); setTimeout(() => checkHit(e, 15, 10, false), 150); }
} else if (e.heldItem) { e.state = 'throw'; e.stateTimer = 20; AudioSys.heavyHit(); checkHit(e, 15, 20, true); }
else {
const r = Math.random();
if (r < 0.6) { e.state = 'punch'; e.stateTimer = 15; AudioSys.punch(); setTimeout(() => checkHit(e, 10, 5, false), 100); }
else if (r < 0.9) { e.state = 'kick'; e.stateTimer = 20; AudioSys.kick(); setTimeout(() => checkHit(e, 12, 10, false), 150); }
else { e.state = 'mega'; e.vy = 2; e.vx = e.facing * 8; e.stateTimer = 30; AudioSys.heavyHit(); checkHit(e, 20, 15, true); }
}
} else { e.state = 'idle'; }
} else {
if (Math.random() < 0.01 && Math.abs(dx) > 40) { e.state = 'mega'; e.vy = 2; e.vx = Math.sign(dx) * 8; e.facing = Math.sign(dx); e.stateTimer = 30; AudioSys.heavyHit(); checkHit(e, 20, 15, true); }
else {
e.state = 'walk'; e.vx = Math.sign(dx) * (e.isBoss ? 0.7 : 0.5); e.facing = Math.sign(dx); e.vz = Math.sign(dz) * 0.3;
if (e.collidedItem && e.collidedItem.type === 'crate' && e.grounded) { e.vy = 6; e.state = 'jump'; }
}
}
} else { e.state = 'idle'; e.vx = 0; e.vz = 0; }
}
function checkHit(atk, range, dmg, isHeavy) {
let finalDmg = dmg;
let finalHeavy = isHeavy;
if ((atk.state === 'punch' || atk.state === 'kick') && (atk.comboCount % 3 === 2)) {
finalDmg *= 2; finalHeavy = true;
}
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;
if(Math.sign(dx)===atk.facing && Math.abs(dx)<range && Math.abs(t.z-atk.z)<6) {
const isParry = (t.state === atk.state) && (atk.state === 'punch' || atk.state === '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.takeDamage(finalDmg, finalHeavy);
}
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)<6) {
const isParry = (t.state === atk.state) && (atk.state === 'punch' || atk.state === '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.takeDamage(finalDmg, finalHeavy);
}
});
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;
// Player Reset Logic
if (State.player) { scene.remove(State.player.mesh); State.player = null; }
State.player = new Character(0, 0, true);
State.player.hp = 100;
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) { State.enemies.push(new Character(ex, ez, false)); 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);
if(dist > 10 + (pl.dashTarget.size||0)/2) {
pl.vx = Math.sign(dx) * 2.5; pl.vz = Math.sign(dz) * 1.5; pl.state = 'dash';
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;
pl.state = pl.dashAction; pl.stateTimer = 20;
if(pl.dashAction==='punch') { AudioSys.punch(); pl.comboCount++; pl.lastAttackTime = Date.now(); setTimeout(()=>checkHit(pl, pl.heldItem?15:10, 10, false), 100); }
else if(pl.dashAction==='throw') { AudioSys.heavyHit(); checkHit(pl, 12, 20, true); }
else if(pl.dashAction==='mega') { pl.vy = 2; pl.vx = pl.facing * 8.0; if(State.view !== 'SIDE') pl.vz = (keys.ArrowUp ? -1 : (keys.ArrowDown ? 1 : 0)) * 6.0; pl.stateTimer = 30; AudioSys.heavyHit(); checkHit(pl, 25, 30, true); }
else { AudioSys.kick(); pl.comboCount++; pl.lastAttackTime = Date.now(); setTimeout(()=>checkHit(pl, 12, 10, false), 150); }
}
}
}
else if(!pl.isAttacking && pl.state !== 'mega' && pl.state !== 'down' && pl.state !== 'wakeup') {
let mx=0, mz=0; const spd=1.2;
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; }
else { 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';
const startAttack = (type) => {
let target=null, minDist=60;
State.items.filter(i=>i.type==='crate').forEach(i => { if(pl.mesh.position.distanceTo(i.mesh.position) < 15 + i.size/2) { target=i; minDist=0; } });
if(!target) { 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; target=e; } }); }
if(!target) { 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; target=i; } }); }
const hitRange = 10 + (target ? (target.size||0)/2 : 0);
if(target && minDist > hitRange) { pl.dashTarget = target; pl.dashAction = type; }
else {
pl.state=type; pl.stateTimer=20;
if(type==='punch') { AudioSys.punch(); pl.comboCount++; pl.lastAttackTime = Date.now(); setTimeout(()=>checkHit(pl, pl.heldItem?15:10, 10, false), 100); }
else if(type==='throw') { AudioSys.heavyHit(); checkHit(pl, 12, 20, true); }
else if(type==='mega') { pl.vy = 2; pl.vx = pl.facing * 8.0; if(State.view !== 'SIDE') pl.vz = (keys.ArrowUp ? -1 : (keys.ArrowDown ? 1 : 0)) * 6.0; pl.stateTimer = 30; AudioSys.heavyHit(); checkHit(pl, 25, 30, true); }
else { AudioSys.kick(); pl.comboCount++; pl.lastAttackTime = Date.now(); setTimeout(()=>checkHit(pl, 12, 10, false), 150); }
}
};
if(keys.KeyA) { if((mx!==0||mz!==0)) startAttack('throw'); else startAttack('punch'); }
if(keys.KeyZ) { if(keys.KeyX) startAttack('mega'); else if(pl.heldItem) { pl.dropWeapon(); AudioSys.punch(); } else startAttack('kick'); }
if(keys.KeyX && !keys.KeyZ && pl.grounded) { pl.vy=5; pl.state='jump'; AudioSys.jump(); }
}
}
// Lock-on Update
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) {
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+'%';
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>

コメント