横スクロールアクションゲーム?「Street Brawler」


画像
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Street Brawler 3D - 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: 100vw; height: 100vh; }

    /* UI Layer */
    #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: 20px;
        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: 15px; }
    .hp-frame {
        width: 350px; height: 28px; background: #220000; border: 3px 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; }
    .score-info { text-align: right; text-shadow: 2px 2px 0 #000; margin-right: 20px; }
    .score-num { font-size: 32px; color: #00ffff; letter-spacing: 2px; }

    #controls {
        position: absolute; bottom: 10px; left: 10px;
        background: rgba(0,0,0,0.7); padding: 15px; border-radius: 8px; font-size: 14px; font-family: sans-serif;
        border: 1px 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: 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; } }

    /* Enemy HP Bar */
    .enemy-hp {
        position: absolute; width: 60px; height: 6px; 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() {
    // --- Audio ---
    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); }
    };

    // --- Config ---
    const CFG = { GRAVITY: 0.7, GROUND: 0, Z_LIMIT: 45 };
    const WAVE_SIZE = 5;
    const TOTAL_ENEMIES = 25; // Boss at 26

    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 }
    };

    // --- 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 (Inverted Yellow Cone)
    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]);
        
        // Road
        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);
        
        // Water
        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);
    }

    // --- Helper: Joint ---
    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;
    }

    // --- 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;
            
            // HP調整: ボスは通常(30)の3倍 = 90
            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));
            
            // Boss Scale
            if(isBoss) {
                const scale = 1.5 * (1 + (State.stage-1)*0.1);
                this.mesh.scale.set(scale, scale, scale);
                this.y = 100;
            }

            // HP Bar
            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);
            
            // Orientation Fix
            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); // Boss HP bar higher
            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);
        }
    }

    // --- 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 = 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);
        }
    }

    // --- Particle 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(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 = [];
        
        // Spawn Containers
        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() {
        // Spawn 5 enemies max, drop from sky
        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));
    }

    // --- Input & Loop ---
    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') {
            
            // --- Dash (Homing) Logic ---
            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;
                
                // --- Controls Fix ---
                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 { // FPS & 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) {
                    pl.state='walk'; pl.vx=mx; pl.vz=mz;
                } else pl.state='idle';

                const startAttack = (type) => {
                    // Lock-on Logic: Enemies Priority
                    let target=null, minDist=60;
                    
                    // 1. Search Enemies
                    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; }
                    });

                    // 2. Search Crates (if no enemy)
                    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; }
                        });
                    }

                    // Dash or Attack
                    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(); }
            }
        }

        // --- Lock-on Cursor ---
        let lockedObj = null;
        if(pl && State.mode==='PLAY') {
            let minDist=60;
            // Same priority logic for cursor
            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;
            // Position above head (Higher for Boss)
            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;
            
            // Wave Logic
            if(State.enemies.length === 0 && State.spawnedCount < TOTAL_ENEMIES) {
                spawnWave();
            } else if(State.enemies.length === 0 && State.spawnedCount >= TOTAL_ENEMIES && !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 - pl.facing*50, 40, pl.z + 20);
                camera.lookAt(pl.x + pl.facing*20, 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); }
        }

        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>

いいなと思ったら応援しよう!

コメント

コメントするには、 ログイン または 会員登録 をお願いします。
買うたび 抽選 ※条件・上限あり \note クリエイター感謝祭ポイントバックキャンペーン/最大全額もどってくる! 12.1 月〜1.14 水 まで
横スクロールアクションゲーム?「Street Brawler」|古井和雄
word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word

mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1