<!DOCTYPE html>
<html lang="ja" >
<head>
<meta charset="UTF-8" >
<title>Street Brawler 3 D - Adjustments</title>
<style>
body { margin: 0 ; overflow: hidden; background: #000; font-family: 'Arial Black', sans-serif; user-select: none; color: white; }
canvas { display: block; width: 100 vw; height: 100 vh; }
#ui { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 10; }
#hud-top {
position: absolute; top: 0 ; left: 0 ; width: 100 %; padding: 20 px;
display: flex; justify-content: space-between; box-sizing: border-box;
background: linear-gradient(to bottom, rgba(0 ,0 ,0 ,0.9 ), transparent);
}
.bar-group { display: flex; align-items: center; gap: 15 px; }
.hp-frame {
width: 350 px; height: 28 px; background: #220000; border: 3px solid #fff;
transform: skewX(-20 deg); overflow: hidden; box-shadow: 2 px 2 px 5 px rgba (0 ,0 ,0 ,0.8 ) ;
}
.hp-fill { width: 100 %; height: 100 %; background: linear-gradient(90 deg, #ff0000, #ffff00, #00ff00); transition: width 0.1s; }
.score-info { text-align: right; text-shadow: 2 px 2 px 0 #000; margin-right: 20px; }
.score-num { font-size: 32 px; color: #00ffff; letter-spacing: 2px; }
#controls {
position: absolute; bottom: 10 px; left: 10 px;
background: rgba(0 ,0 ,0 ,0.7 ); padding: 15 px; border-radius: 8 px; font-size: 14 px; font-family: sans-serif;
border: 1 px solid #555;
}
#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: 80 px; color: #ffcc00; text-shadow: 5px 5px 0 #ff0000; margin: 0; font-style: italic; }
.blink { animation: blink 0.8 s infinite; font-size: 30 px; margin-top: 20 px; }
@keyframes blink { 50 % { opacity: 0 ; } }
.enemy-hp {
position: absolute; width: 60 px; height: 6 px; background: #300; border: 1px solid #fff;
display: none; pointer-events: none; z-index: 5 ;
}
.enemy-hp-fill { width: 100 %; height: 100 %; background: #f00; }
</style>
</head>
<body>
<canvas id="gameCanvas" ></canvas>
<div id="hp-pool" ></div>
<div id="ui" >
<div id="hud-top" >
<div class ="bar-group" >
<span style="font-size:30px; color:#fc0; text-shadow:3px 3px 0 #000;" >PLAYER</span>
<div class ="hp-frame" ><div id="hp-bar" class ="hp-fill" ></div></div>
</div>
<div class ="score-info" >
SCORE: <span id="score" class ="score-num" >0 </span><br>
<span style="font-size:20px; color:#ccc;" >STAGE <span id="stage" >1 </span></span>
</div>
</div>
<div id="controls" >
<strong>[F2] Side View</strong> | [F1] FPS | [F3] TPS<br>
Move: Arrows | Punch: A | Kick: Z | Jump: X
</div>
</div>
<div id="overlay" >
<h1 id="msg-main" >STREET BRAWLER</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() {
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 ); },
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 ); },
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 ); }
};
const CFG = { GRAVITY: 0.7 , GROUND: 0 , Z_LIMIT: 45 };
const WAVE_SIZE = 5 ;
const TOTAL_ENEMIES = 25 ;
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 }
};
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 ;
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);
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);
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);
}
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;
}
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 .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 : 0xcc3333 ));
if (isBoss) {
const scale = 1.5 * (1 + (State.stage-1 )*0.1 );
this .mesh.scale.set (scale, scale, scale);
this .y = 100 ;
}
if (!isPlayer) {
this .hpEl = document.createElement('div' );
this .hpEl.className = 'enemy-hp' ;
this .hpEl.innerHTML = '<div class="enemy-hp-fill"></div>' ;
document.getElementById('hp-pool' ).appendChild(this .hpEl);
}
}
buildBody(col) {
const skin = 0xffccaa ;
this .root = new THREE.Group(); this .root.position.y=10 ; this .mesh.add (this .root);
this .root.add (new THREE.Mesh(new THREE.BoxGeometry(3.5 ,3 ,2.5 ), new THREE.MeshLambertMaterial({color:0x222222 })));
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: col}));
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 hair = new THREE.Mesh(new THREE.BoxGeometry(3.2 ,1.5 ,3.2 ), new THREE.MeshLambertMaterial({color:0x111111 }));
hair.position.y=3 ; this .headGroup.add (hair);
this .armL = createLimb(1.3 ,4.5 ,1.3 , col, 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 , col, 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);
this .thighL = createLimb(1.6 ,5.5 ,1.6 , 0x222222 , this .root, 1.2 , -1.5 , 0 );
this .shinL = createLimb(1.4 ,5.5 ,1.4 , 0x111111 , this .thighL, 0 , -5.5 , 0 );
this .thighR = createLimb(1.6 ,5.5 ,1.6 , 0x222222 , this .root, -1.2 , -1.5 , 0 );
this .shinR = createLimb(1.4 ,5.5 ,1.4 , 0x111111 , 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 => c.visible=false );
} else {
this .headMesh.visible = true ;
this .headGroup.children.forEach(c => c.visible=true );
}
}
update() {
if (this .state==='dead' ) {
this .updateHpBar(false );
this .deadTimer++;
this .mesh.visible = Math.floor(this .deadTimer/4 )%2 ===0 ;
if (this .deadTimer > 40 ) this .remove ();
return ;
}
this .vy -= CFG.GRAVITY;
this .x += this .vx; this .y += this .vy; this .z += this .vz;
if (this .y<=CFG.GROUND) { this .y=CFG.GROUND; this .vy=0 ; this .grounded=true ; this .vx*=0.8 ; this .vz*=0.8 ; }
else this .grounded=false ;
this .z = Math.max(-CFG.Z_LIMIT, Math.min(CFG.Z_LIMIT, this .z));
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 ;
this .updateHpBar(true );
}
updateHpBar(visible) {
if (!this .hpEl) return ;
if (!visible) { this .hpEl.style.display='none' ; return ; }
const pos = this .mesh.position.clone();
pos.y += (this .isBoss ? 40 : 20 );
pos.project(camera);
const w = window.innerWidth, h = window.innerHeight;
const x = (pos.x*.5 +.5 )*w, y = (-(pos.y*.5 )+.5 )*h;
if (pos.z<1 && x>0 && x<w && y>0 && y<h) {
this .hpEl.style.display='block' ; this .hpEl.style.left=(x-30 )+'px' ; this .hpEl.style.top=y+'px' ;
this .hpEl.children[0 ].style.width = (this .hp/this .maxHp)*100 +'%' ;
} else this .hpEl.style.display='none' ;
}
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.0 ; 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 .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 * 5 ; this .vy = 5 ;
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 .hpEl) this .hpEl.remove ();
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 = 5 + Math.random()*2 ;
geo = new THREE.BoxGeometry(s,s,s);
mat = new THREE.MeshLambertMaterial({color: Math.random()*0xffffff });
this .color = mat.color;
} 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)<8 ) {
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) < 8 ) {
if (this .type==='score' ) { State.score+=100 ; AudioSys.score(); this .remove (); }
else if (this .type==='health' ) { player.hp=Math.min(100 , player.hp+50 ); AudioSys.pickup(); 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<8 ; i++) createDebris(this .mesh.position, this .color);
const r = Math.random();
if (r<0.4 ) 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' );
else new GameItem(this .mesh.position.x, this .mesh.position.z, 'score' );
this .remove ();
}
}
remove () {
scene.remove (this .mesh);
State.items = State.items.filter(i=>i!==this );
}
}
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(1.5 ,1.5 ,1.5 ), new THREE.MeshBasicMaterial({color: col}));
m.position.copy(pos); m.vx = (Math.random()-0.5 )*2 ; m.vy = Math.random()*2 +1 ; m.vz = (Math.random()-0.5 )*2 ;
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.spawnedCount = 0 ;
State.bossSpawned = false ;
State.player.x = 0 ; State.player.z = 0 ;
State.enemies.forEach(e => { scene.remove (e.mesh); if (e.hpEl) e.hpEl.remove (); });
State.items.forEach(i => { scene.remove (i.mesh); });
State.enemies = []; State.items = [];
for (let i=0 ; i<8 ; i++) {
new GameItem(50 +Math.random()*100 , (Math.random()*2 -1 )*(CFG.Z_LIMIT-5 ), 'crate' );
}
showMsg(`STAGE ${State.stage}`, "START!" );
AudioSys.stageStart();
setTimeout(() => { document.getElementById('overlay' ).classList.add ('hidden' ); spawnWave(); }, 2000 );
}
function finishStage ( ) {
State.mode = 'CLEAR' ; AudioSys.clear();
showMsg("STAGE CLEAR!" , "NEXT STAGE..." );
setTimeout(() => {
State.stage++; document.getElementById('stage' ).innerText = State.stage;
startStage();
}, 4000 );
}
function spawnWave ( ) {
for (let i=0 ; i<WAVE_SIZE; i++) {
const x = State.player.x + 60 + Math.random()*40 ;
const z = (Math.random()*2 -1 ) * (CFG.Z_LIMIT-10 );
State.enemies.push(new Character(x, z, false ));
State.spawnedCount++;
}
}
function spawnBoss ( ) {
State.bossSpawned = true ;
const x = State.player.x + 80 ;
State.enemies.push(new Character(x, 0 , false , true ));
}
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' )) {
if (State.mode==='GAME_OVER' ) 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' ;
});
window.addEventListener('keyup' , e => keys[e.code]=false );
function animate ( ) {
requestAnimationFrame(animate);
const pl = State.player;
if (State.mode==='PLAY' && pl && pl.state!=='dead' ) {
if (pl.dashTarget) {
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 > 8 ) {
pl.vx = Math.sign(dx) * 2.5 ;
pl.vz = Math.sign(dz) * 1.5 ;
pl.state = 'dash' ;
} else {
pl.vx = 0 ; pl.vz = 0 ; pl.dashTarget = null ;
pl.state = pl.dashAction; pl.stateTimer = 15 ;
if (pl.dashAction==='punch' ) { AudioSys.punch(); setTimeout(()=>checkHit(pl, pl.heldItem?15 :10 , 10 ), 100 ); }
else { AudioSys.kick(); setTimeout(()=>checkHit(pl, 12 , 10 ), 150 ); }
}
}
else if (pl.grounded && pl.state!=='punch' && pl.state!=='kick' ) {
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 (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 ) {
pl.state='walk' ; pl.vx=mx; pl.vz=mz;
} else pl.state='idle' ;
const startAttack = (type) => {
let target=null , minDist=60 ;
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; }
});
}
if (target && minDist > 10 ) {
pl.dashTarget = target; pl.dashAction = type;
} else {
pl.state=type; pl.stateTimer=15 ;
if (type==='punch' ) { AudioSys.punch(); setTimeout(()=>checkHit(pl, pl.heldItem?15 :10 , 10 ), 100 ); }
else { AudioSys.kick(); setTimeout(()=>checkHit(pl, 12 , 10 ), 150 ); }
}
};
if (keys.KeyA) startAttack('punch' );
if (keys.KeyZ) {
if (pl.heldItem) { pl.dropWeapon(); AudioSys.punch(); }
else startAttack('kick' );
}
if (keys.KeyX) { pl.vy=5 ; pl.state='jump' ; AudioSys.jump(); }
}
}
let lockedObj = null ;
if (pl && State.mode==='PLAY' ) {
let minDist=60 ;
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 yOffset = lockedObj.isBoss ? 40 : 25 ;
lockOnCursor.position.set (pos.x, pos.y + yOffset, 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;
if (State.enemies.length === 0 && State.spawnedCount < TOTAL_ENEMIES) {
spawnWave();
} else if (State.enemies.length === 0 && State.spawnedCount >= TOTAL_ENEMIES && !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 - pl.facing*50 , 40 , pl.z + 20 );
camera.lookAt(pl.x + pl.facing*20 , 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);
}
}
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 ); }
}
renderer.render(scene, camera);
}
function checkHit (atk, range, dmg ) {
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);
}
});
if (atk.isPlayer) {
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 && Math.abs(i.mesh.position.z-atk.z)<8 ) {
i.hit();
}
});
}
}
window.onresize = () => {
camera.aspect = window.innerWidth/window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
};
animate();
};
</script>
</body>
</html>
copy
コメント