横スクロールアクションゲーム?「KNOCK OUT RUSH」
【更新履歴】
・2026/1/8 バージョン1.0公開
・2026/1/8 バージョン1.1公開
・2026/1/9 他の作品と重複していたタイトルを変更
・ダウンロードされる方はこちら。↓
・ソースコードはこちら。↓
<!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 Layer */
#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 Container */
#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;
}
/* Stats (Left) */
.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 (Center-Left) */
.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 (Right Edge) */
.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 | Kick: Z | 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://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
window.onload = function() {
// --- 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 ---
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
};
// --- 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;
// Lights
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);
// 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);
function resetStageEnv() {
const h = Math.random();
State.colors.bg = new THREE.Color().setHSL(h, 0.3, 0.1);
State.colors.road = new THREE.Color().setHSL((h+0.5)%1, 0.1, 0.2);
scene.background = State.colors.bg;
scene.fog = new THREE.Fog(State.colors.bg, 100, 600);
while(stageObj.children.length) stageObj.remove(stageObj.children[0]);
const road = new THREE.Mesh(new THREE.BoxGeometry(10000, 50, 100), new THREE.MeshLambertMaterial({color: State.colors.road}));
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}));
water.rotation.x = -Math.PI/2; water.position.y = -30;
stageObj.add(water);
}
// --- Helpers ---
function createLimb(w, h, d, col, parent, x, y, z) {
const grp = new THREE.Group(); grp.position.set(x, y, z);
const m = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), new THREE.MeshLambertMaterial({color: col}));
m.position.y = -h/2; m.castShadow = true; grp.add(m);
if(parent) parent.add(grp);
return grp;
}
function randBrightColor() { return new THREE.Color().setHSL(Math.random(), 0.8, 0.5); }
// --- Physics & Collision ---
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; }
}
}
}
if (char.z + moveZ < -CFG.Z_LIMIT || char.z + moveZ > CFG.Z_LIMIT) moveZ = 0;
return { x: moveX, z: moveZ, ground: onGroundY, hit: hitItem };
}
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;
}
// --- Character ---
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 ? 90 : 30);
this.hp = this.maxHp;
this.facing=1;
this.state='idle'; this.stateTimer=0;
this.dashTarget=null; this.dashAction=null;
this.collidedItem = null;
this.deadTimer=0;
this.grounded=false; this.invincible=0;
this.heldItem=null;
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;
this.root = new THREE.Group(); this.root.position.y=10; this.mesh.add(this.root);
const pantsCol = this.isPlayer ? 0x222222 : randBrightColor();
this.root.add(new THREE.Mesh(new THREE.BoxGeometry(3.5,3,2.5), new THREE.MeshLambertMaterial({color: pantsCol})));
this.spine = new THREE.Group(); this.spine.position.y=1.5; this.root.add(this.spine);
const chest = new THREE.Mesh(new THREE.BoxGeometry(4.5,6,3), new THREE.MeshLambertMaterial({color: shirtCol}));
chest.position.y=3; chest.castShadow=true; this.spine.add(chest);
this.headGroup = new THREE.Group(); this.headGroup.position.y=6.5; this.spine.add(this.headGroup);
this.headMesh = new THREE.Mesh(new THREE.BoxGeometry(3,3.5,3), 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.3,4.5,1.3, shirtCol, this.spine, 2.8, 5.5, 0);
this.foreL = createLimb(1.1,4.5,1.1, skin, this.armL, 0, -4.5, 0);
this.armR = createLimb(1.3,4.5,1.3, shirtCol, this.spine, -2.8, 5.5, 0);
this.foreR = createLimb(1.1,4.5,1.1, skin, this.armR, 0, -4.5, 0);
this.handR = new THREE.Group(); this.handR.position.y=-4.5; this.foreR.add(this.handR);
const bootsCol = this.isPlayer ? 0x111111 : randBrightColor();
this.thighL = createLimb(1.6,5.5,1.6, pantsCol, this.root, 1.2, -1.5, 0);
this.shinL = createLimb(1.4,5.5,1.4, bootsCol, this.thighL, 0, -5.5, 0);
this.thighR = createLimb(1.6,5.5,1.6, pantsCol, this.root, -1.2, -1.5, 0);
this.shinR = createLimb(1.4,5.5,1.4, bootsCol, this.thighR, 0, -5.5, 0);
this.limbs = [this.armL, this.armR, this.foreL, this.foreR, this.thighL, this.thighR, this.shinL, this.shinR, this.spine, this.headGroup];
}
setVisibleParts(mode) {
if(mode==='FPS') {
this.headMesh.visible = false;
this.headGroup.children.forEach(c => { if(c!==this.headMesh && c.geometry.type==='BoxGeometry') c.visible=false; });
} else {
this.headMesh.visible = true;
this.headGroup.children.forEach(c => c.visible=true);
}
}
update() {
if(this.state==='dead') {
this.deadTimer++;
this.mesh.visible = Math.floor(this.deadTimer/4)%2===0;
this.vy -= CFG.GRAVITY; this.x+=this.vx; this.y+=this.vy;
if(this.y<=CFG.GROUND) this.y=CFG.GROUND;
this.mesh.position.set(this.x,this.y,this.z);
if(this.deadTimer > 40) 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;
}
this.mesh.position.set(this.x, this.y, this.z);
this.mesh.rotation.y = this.facing === 1 ? Math.PI/2 : -Math.PI/2;
this.animate();
if(this.stateTimer>0) { this.stateTimer--; if(this.stateTimer<=0) this.state='idle'; }
if(this.invincible>0) { this.invincible--; this.mesh.visible = (Math.floor(this.invincible/4)%2===0); }
else this.mesh.visible = true;
}
animate() {
const t = Date.now()*0.01;
this.limbs.forEach(o=>o.rotation.set(0,0,0));
this.root.position.set(0,10,0); this.root.rotation.x=0;
const s = this.state;
if(s==='idle') {
this.spine.rotation.x = Math.sin(t*0.2)*0.05;
this.armL.rotation.z=0.1; this.armR.rotation.z=-0.1;
this.armL.rotation.x=Math.sin(t*0.5)*0.1; this.armR.rotation.x=Math.sin(t*0.5+1)*0.1;
} else if(s==='walk') {
const w = Math.sin(t*1.5);
this.thighL.rotation.x=w*0.8; this.shinL.rotation.x=w>0?0.8:0.1;
this.thighR.rotation.x=-w*0.8; this.shinR.rotation.x=w<0?0.8:0.1;
this.armL.rotation.x=-w*0.8; this.foreL.rotation.x=-0.5;
this.armR.rotation.x=w*0.8; this.foreR.rotation.x=-0.5;
} else if(s==='dash') {
this.root.rotation.x = 0.5;
const w = Math.sin(t*2.0);
this.thighL.rotation.x=w*1.2; this.thighR.rotation.x=-w*1.2;
this.armL.rotation.x=-1.5; this.armR.rotation.x=-1.5;
} else if(s==='punch') {
this.spine.rotation.y = -0.8;
this.root.position.z = 2.0;
if(this.heldItem) {
this.armR.rotation.x = -2.5; this.foreR.rotation.x = 0;
} else {
this.armR.rotation.x = -1.5; this.armR.rotation.z = -0.5;
this.foreR.rotation.x = 0;
}
this.armL.rotation.x=-1.0; this.foreL.rotation.x=-2.0;
} else if(s==='kick') {
this.root.rotation.x = -0.5;
this.thighR.rotation.x = -1.8; this.shinR.rotation.x = 0.2;
this.thighL.rotation.x = 0.5; this.shinL.rotation.x = 1.0;
this.armL.rotation.x = 1.5;
} else if(s==='jump') {
this.root.position.y = 13;
this.thighL.rotation.x=-0.5; this.shinL.rotation.x=1.5;
this.thighR.rotation.x=0.5; this.shinR.rotation.x=1.0;
this.armL.rotation.z=2.5; this.armR.rotation.z=-2.5;
} else if(s==='hurt') {
this.spine.rotation.x = -0.5;
this.armL.rotation.x=-2.5; this.armR.rotation.x=-2.5;
this.root.position.z = -1.5;
}
}
takeDamage(dmg, knockback=3) {
if(this.invincible>0 || this.state==='dead') return;
this.hp-=dmg; this.state='hurt'; this.stateTimer=12; this.invincible=20;
AudioSys.hit(); createBlood(this.mesh.position);
this.vx = -this.facing * knockback;
this.dashTarget = null;
if(!this.isPlayer) {
State.activeEnemy = this; State.activeEnemyTimer = 60;
}
if(this.heldItem) this.dropWeapon(true);
if(this.hp<=0) {
this.state='dead';
this.mesh.rotation.x = -Math.PI/2; this.mesh.position.y = 2;
this.vx = -this.facing * 8; this.vy = 10;
if(!this.isPlayer) {
State.score+=this.isBoss?1000:10;
State.kills++;
if(this.isBoss) { finishStage(); }
} else {
showMsg("GAME OVER", "PRESS 'A' TO RETRY");
State.mode='GAME_OVER';
}
}
}
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);
}
}
// --- Items ---
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, 5); 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() {
if(this.type!=='crate') return;
this.hp--; 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);
}
}
// --- Effects ---
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 = 'PLAY'; State.kills = 0; State.stageX = 0;
State.spawnedCount = 0; State.bossSpawned = false;
State.player.x = 0; State.player.z = 0;
State.player.hp = Math.min(100, State.player.hp+20);
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'); spawnWave(); }, 2000);
}
function finishStage() {
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={};
const prevKeys={}; // For tracking key up events logic if needed
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';
// F1 Toggle
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);
const pl = State.player;
if(State.mode==='PLAY' && pl && pl.state!=='dead') {
// --- Dash Logic ---
if(pl.dashTarget) {
// FIXED: Safety stop if dashing into ANY crate
if(pl.collidedItem && pl.collidedItem.type==='crate' && pl.collidedItem !== pl.dashTarget) {
pl.vx = -pl.vx * 0.5; // Bounce back
pl.vz = -pl.vz * 0.5;
pl.dashTarget = null; // Cancel
}
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';
// Jump Interrupt
if(keys.KeyX && pl.grounded) {
pl.vy=5; pl.state='jump'; AudioSys.jump();
pl.dashTarget = null; // Cancel dash on jump
}
} else {
pl.vx = 0; pl.vz = 0; pl.dashTarget = null;
pl.state = pl.dashAction; pl.stateTimer = 20;
if(pl.dashAction==='punch') { AudioSys.punch(); setTimeout(()=>checkHit(pl, pl.heldItem?15:10, 10), 100); }
else if(pl.dashAction==='throw') { AudioSys.heavyHit(); checkHit(pl, 12, 20, true); }
else if(pl.dashAction==='mega') { AudioSys.heavyHit(); checkHit(pl, 20, 30, true); }
else { AudioSys.kick(); setTimeout(()=>checkHit(pl, 12, 10), 150); }
}
}
}
else if(!pl.isAttacking) {
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;
// Down is handled in keydown event for toggle
if(keys.ArrowLeft) mz = -spd * pl.facing;
if(keys.ArrowRight) mz = 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';
const startAttack = (type) => {
let target=null, minDist=60;
// Priority 1: Contact Range Crate
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;
}
});
// Priority 2: Enemies
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; }
});
}
// Priority 3: Far Crates
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(); setTimeout(()=>checkHit(pl, pl.heldItem?15:10, 10), 100); }
else if(type==='throw') { AudioSys.heavyHit(); checkHit(pl, 12, 20, true); }
else if(type==='mega') { AudioSys.heavyHit(); checkHit(pl, 20, 30, true); }
else { AudioSys.kick(); setTimeout(()=>checkHit(pl, 12, 10), 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 Cursor
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;
}
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;
// Boss Check
const maxEnemies = 20 + (State.stage - 1) * 10;
document.getElementById('kill-target').innerText = maxEnemies;
if(State.enemies.length === 0) {
if (State.spawnedCount < maxEnemies) {
spawnWave();
} else if (!State.bossSpawned) {
spawnBoss();
}
}
// Camera
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 { // FPS
camera.position.set(pl.x + pl.facing*2, 17, pl.z);
camera.lookAt(pl.x + pl.facing*100, 15, pl.z);
}
}
State.enemies.forEach(e => {
if(e.state!=='dead' && State.mode==='PLAY') {
const dx = pl.x - e.x;
if(Math.abs(dx)<60) {
if(e.state!=='punch' && e.state!=='kick') {
if(Math.abs(dx)>8) {
e.state='walk'; e.vx=Math.sign(dx)*0.4; e.facing=Math.sign(dx);
e.vz = (pl.z - e.z) * 0.05;
} else if(Math.random()<0.02) {
e.vx=0; e.state='punch'; e.stateTimer=15; AudioSys.punch();
setTimeout(() => checkHit(e, 10, 5), 150);
} else { e.vx=0; e.state='idle'; }
} else { e.vx=0; e.vz=0; }
}
}
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;
}
renderer.render(scene, camera);
}
function checkHit(atk, range, dmg, blowAway=false) {
const targets = atk.isPlayer ? State.enemies : [State.player];
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) {
t.takeDamage(dmg, blowAway ? 15 : 3);
}
});
if(atk.isPlayer) {
State.items.forEach(i => {
const dx = i.mesh.position.x - atk.x;
// Add Crate Radius to range
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) {
i.hit();
}
});
}
}
window.onresize = () => {
camera.aspect = window.innerWidth/window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
};
animate();
};
</script>
</body>
</html>

コメント