見出し画像

横スクロールアクションゲーム?「KNOCK OUT RUSH」

【更新履歴】
・2026/1/8 バージョン1.0公開
・2026/1/8 バージョン1.1公開
・2026/1/9 他の作品と重複していたタイトルを変更
・2026/1/10 バージョン1.1公開(アクション改善、敵が強くなった)
・2026/1/10 バージョン1.2公開(アクション改善、敵が強くなった)

画像
殴っている様子

・ダウンロードされる方はこちら。↓

【操作説明】

・Aキー … パンチ(3発目で敵が吹っ飛ぶ)
・Zキー … キック(3発目で敵が吹っ飛ぶ)
・Xキー … ジャンプ

・Aキーと左キーor右キー … 投げ飛ばし。(敵が吹っ飛ぶ。)
・ZキーとXキー … 飛び蹴り。(敵が吹っ飛ぶ)

・F1キー … 一人称視点モードに切り替え。
・F1キー … 横スクロール視点モードに切り替え。
・F1キー … 三人称視点モードに切り替え。

・相手と同じ攻撃を出すと、回避することができる。


・ソースコードはこちら。↓

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>KNOCK OUT RUSH 1.2</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: #440000; border: 2px solid #fff;
        transform: skewX(-20deg); overflow: hidden; box-shadow: 2px 2px 5px rgba(0,0,0,0.8);
    }
    .hp-fill { 
        width: 100%; height: 100%; 
        background-color: #00ff00; 
        transition: width 0.1s linear, background-color 0.2s; 
    }

    .enemy-hp-box {
        display: flex; flex-direction: column; align-items: flex-end;
        width: 300px; margin-left: auto; opacity: 0; transition: opacity 0.2s;
    }
    .enemy-hp-frame {
        width: 100%; height: 24px; background: #220000; border: 2px solid #ffaaaa;
        transform: skewX(-20deg); overflow: hidden; box-shadow: 2px 2px 5px rgba(0,0,0,0.8);
    }
    .enemy-hp-fill { 
        width: 100%; height: 100%; 
        background-color: #ff0000; 
        transition: width 0.1s linear; 
    }

    #controls {
        padding: 20px;
        background: linear-gradient(to top, rgba(0,0,0,0.8), transparent);
        font-size: 14px; font-family: sans-serif; text-shadow: 1px 1px 0 #000;
    }

    #overlay {
        position: absolute; top: 0; left: 0; width: 100%; height: 100%;
        background: rgba(0,0,0,0.85); display: flex; flex-direction: column;
        justify-content: center; align-items: center; z-index: 100;
    }
    #overlay.hidden { display: none; }
    h1 { font-size: 80px; color: #ffcc00; text-shadow: 5px 5px 0 #ff0000; margin: 0; font-style: italic; }
    .blink { animation: blink 0.8s infinite; font-size: 30px; margin-top: 20px; }
    @keyframes blink { 50% { opacity: 0; } }
</style>
</head>
<body>

<canvas id="gameCanvas"></canvas>

<div id="ui">
    <div id="hud-top">
        <div class="hud-section-left">
            <div class="stage-text">STAGE <span id="stage">1</span></div>
            <div class="score-text">SCORE <span id="score">0</span></div>
            <div class="enemy-count">KILLS: <span id="kill-count">0</span> / <span id="kill-target">20</span></div>
        </div>

        <div class="player-hp-box">
            <div class="hp-label">PLAYER</div>
            <div class="hp-frame"><div id="hp-bar" class="hp-fill"></div></div>
        </div>

        <div class="enemy-hp-box" id="enemy-hud">
            <div class="hp-label" style="color:#ff5555;">ENEMY</div>
            <div class="enemy-hp-frame"><div id="enemy-hp-bar" class="enemy-hp-fill"></div></div>
        </div>
    </div>

    <div id="controls">
        <strong>[F2] Side View</strong> | [F1] FPS | [F3] TPS<br>
        Move: Arrows | Punch: A (Combo) | Kick: Z (Combo) | Jump: X | Throw: Move+A | Mega: 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;
    }
    
    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) {
        let nextX = char.x + dx;
        let nextZ = char.z + dz;
        let onGroundY = CFG.GROUND; 
        let hitItem = null;

        for (let item of State.items) {
            if (item.type !== 'crate') continue;
            const size = item.size / 2;
            const cx = item.mesh.position.x;
            const cz = item.mesh.position.z;
            const topY = item.mesh.position.y + size;
            const range = size + 5; 
            
            const overlapX = (nextX > cx - range && nextX < cx + range);
            const overlapZ = (nextZ > cz - range && nextZ < cz + range);
            const currentlyAbove = (char.y >= topY - 2.0);

            if (overlapX && overlapZ) {
                if (currentlyAbove && char.vy <= 0) { 
                    onGroundY = Math.max(onGroundY, topY);
                } else if (!currentlyAbove) {
                    hitItem = item;
                    const penLeft = nextX - (cx - range);
                    const penRight = (cx + range) - nextX;
                    const penBack = nextZ - (cz - range);
                    const penFront = (cz + range) - nextZ;
                    const min = Math.min(penLeft, penRight, penBack, penFront);

                    if (min === penLeft) nextX = cx - range - 0.1;
                    else if (min === penRight) nextX = cx + range + 0.1;
                    else if (min === penBack) nextZ = cz - range - 0.1;
                    else if (min === penFront) nextZ = cz + range + 0.1;
                }
            }
        }
        
        if (nextZ < -CFG.Z_LIMIT) nextZ = -CFG.Z_LIMIT;
        if (nextZ > CFG.Z_LIMIT) nextZ = CFG.Z_LIMIT;

        return { x: nextX, z: nextZ, ground: onGroundY, hit: hitItem };
    }

    function resolveCharOverlap(char) {
        if(char.state === 'dead' || char.state === 'down') return;
        const targets = char.isPlayer ? State.enemies : [State.player, ...State.enemies.filter(e => e !== char)];
        targets.forEach(t => {
            if(!t || t.state === 'dead') return;
            const dx = char.x - t.x;
            const dz = char.z - t.z;
            const dist = Math.sqrt(dx*dx + dz*dz);
            const minDist = 8; 
            if (dist < minDist && dist > 0.1) {
                const push = (minDist - dist) / 2;
                char.x += (dx/dist) * push;
                char.z += (dz/dist) * push;
                if(!t.isPlayer || State.mode !== 'PLAY') { t.x -= (dx/dist) * push; t.z -= (dz/dist) * push; }
            }
        });
    }
    
    function 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] } } ] },
        
        // Punch 1
        'punch1': { loop: false, speed: 0.5, hitFrame: 1, frames: [ 
            { t:0, pose: { ...POSE_ZERO, spine:[0,0.2,0], armR:[-0.5, 0, 0.5], forR:[-1.8, 0, 0] } }, 
            { t:1, pose: { ...POSE_ZERO, spine:[0,-0.2,0], armR:[-1.6, 0, -0.6], forR:[0,0,0] } }, 
            { t:2, pose: { ...POSE_ZERO, spine:[0,0.2,0], armR:[-0.5, 0, 0.5], forR:[-2.0, 0, 0] } },
            { t:3, pose: { ...POSE_ZERO } } 
        ] },
        'punch2': { loop: false, speed: 0.5, hitFrame: 1, frames: [ 
            { t:0, pose: { ...POSE_ZERO, spine:[0,-0.2,0], armL:[-0.5, 0, 0.5], forL:[-1.8, 0, 0] } },
            { t:1, pose: { ...POSE_ZERO, spine:[0,0.2,0], armL:[-1.6, 0.2, 0.8], forL:[0,0,0] } }, 
            { t:2, pose: { ...POSE_ZERO, spine:[0,-0.2,0], armL:[-0.5, 0, 0.5], forL:[-2.0, 0, 0] } }, 
            { t:3, pose: { ...POSE_ZERO } } 
        ] },
        // Punch 3: Shoryuken
        'punch3': { loop: false, speed: 0.25, hitFrame: 2, frames: [ 
            { t:0, pose: { ...POSE_ZERO, root:[0,-2,0], spine:[0,0.5,0], armR:[-0.5,-0.5,-0.5] } }, // Crouch
            { t:1, pose: { ...POSE_ZERO, root:[0,15,0], spine:[0,-0.5,0], armR:[-3.14,0,0], legL:[0.2,0,0], legR:[-0.5,0,0] } }, // Jump Upper
            { t:2, pose: { ...POSE_ZERO, root:[0,25,0], spine:[0,-0.5,0], armR:[-3.14,0,0] } }, // Peak
            { t:3, pose: { ...POSE_ZERO, root:[0,5,0] } }, // Fall
            { t:4, pose: { ...POSE_ZERO } } // Land
        ] },
        
        // Boss Punch: Deep Forward Lean
        'boss_punch': { loop: false, speed: 0.2, hitFrame: 1, frames: [ 
            { t:0, pose: { ...POSE_ZERO, root:[0,-3,0], spine:[0.5,0.5,0], armR:[-0.5,0,0.5], forR:[-1.5,0,0] } }, 
            { t:1, pose: { ...POSE_ZERO, root:[5.0,-10,5.0], spine:[1.4, -0.5, 0], armR:[-1.8, 0, -0.2], forR:[0,0,0] } }, // Super Deep Lean
            { t:2, pose: { ...POSE_ZERO, root:[5.0,-10,5.0], spine:[1.4, -0.5, 0], armR:[-1.8, 0, -0.2] } }, 
            { t:3, pose: { ...POSE_ZERO, root:[0,-3,0] } } 
        ] },
        
        // Kick
        'kick': { loop: false, speed: 0.5, hitFrame: 1, frames: [ 
            { t:0, pose: { ...POSE_ZERO, root:[0,0,0], legR:[-0.5,0,0] } },
            { t:1, pose: { ...POSE_ZERO, root:[0,0,0], spine:[0,0.2,0], legR:[-2.0, 0, 0.1] } }, 
            { t:2, pose: { ...POSE_ZERO, root:[0,0,0], legR:[-0.5,0,0] } }, 
            { t:3, pose: { ...POSE_ZERO } } 
        ] },
        'kick2': { loop: false, speed: 0.5, hitFrame: 1, frames: [ 
            { t:0, pose: { ...POSE_ZERO, root:[0,0,0], legL:[-0.5,0,0] } },
            { t:1, pose: { ...POSE_ZERO, root:[0,0,0], spine:[0,-0.2,0], legL:[-2.0, 0, 0.1] } }, 
            { t:2, pose: { ...POSE_ZERO, root:[0,0,0], legL:[-0.5,0,0] } }, 
            { t:3, pose: { ...POSE_ZERO } } 
        ] },
        // Kick 3: Tatsumaki (Hurricane Kick) - Horizontal Spin
        'kick3': { loop: false, speed: 0.25, hitFrame: 1, frames: [ 
            { t:0, pose: { ...POSE_ZERO, root:[0,-2,0], spine:[0,-1.0,0], armL:[0,0,1.5], armR:[0,0,-1.5] } }, // Windup
            { t:1, pose: { ...POSE_ZERO, root:[0,5,0], spine:[0,3.14,0], legR:[-1.6,0,1.0], legL:[0,0,0], armL:[0,0,2], armR:[0,0,-2] } }, // Spin 1
            { t:2, pose: { ...POSE_ZERO, root:[0,5,0], spine:[0,6.28,0], legR:[-1.6,0,1.0], legL:[0,0,0], armL:[0,0,2], armR:[0,0,-2] } }, // Spin 2
            { t:3, pose: { ...POSE_ZERO } } 
        ] },
        
        'jump': { loop: false, speed: 0.1, frames: [ { t:0, pose: { ...POSE_ZERO, root:[0.2,0,0], armL:[0,0,2.5], armR:[0,0,-2.5], legL:[-0.5,0,0], legR:[0.2,0,0], shiL:[1.0,0,0], shiR:[0.5,0,0] } } ] },
        'mega': { loop: false, speed: 0.1, hitFrame: 0, 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, hitFrame: 1, frames: [ { t:0, pose: { ...POSE_ZERO, spine:[-0.5,0,0], armL:[-2.8,0,-0.5], armR:[-2.8,0,0.5], forL:[-0.5,0,0], forR:[-0.5,0,0] } }, { t:1, pose: { ...POSE_ZERO, spine:[0.5,0,0], armL:[-1.0,0,-0.5], armR:[-1.0,0,0.5], forL:[-1.5,0,0], forR:[-1.5,0,0], root:[0.2,0,0] } }, { t:2, pose: { ...POSE_ZERO, spine:[0.5,0,0], armL:[-1.0,0,-0.5], armR:[-1.0,0,0.5] } }, { t:3, pose: { ...POSE_ZERO } } ] },
        'hurt': { loop: false, speed: 0.15, frames: [ { t:0, pose: { ...POSE_ZERO, root: [-0.6, 0, 0], spine: [-0.6, 0, 0], head: [-0.8, 0, 0], armL: [-0.5, 0, 1.2], armR: [-0.5, 0, -1.2], legL: [0.6, 0, 0], legR: [0.2, 0, 0] }}, { t:1, pose: { ...POSE_ZERO, root: [-0.3, 0, 0], spine: [0.6, 0, 0], head: [0.5, 0, 0], armL: [-1.0, 0.5, 0.8], forL: [-1.8, 0, 0], armR: [-1.0, -0.5, -0.8], forR: [-1.8, 0, 0], legL: [0.4, 0, 0], legR: [0.1, 0, 0] }}, { t:2, pose: { ...POSE_ZERO, spine:[0.6,0,0], head:[0.5,0,0] } } ] },
        'down': { loop: false, speed: 0.1, frames: [ { t:0, pose: { ...POSE_ZERO, root:[-1.5, 0, 0], legL:[0.2,0,0], legR:[0.2,0,0], armL:[0,0,1], armR:[0,0,-1], head:[0.2,0,0] }} ] },
        'wakeup': { loop: false, speed: 0.08, frames: [ { t:0, pose: { ...POSE_ZERO, root: [-1.5, 0, -1.5], spine: [0.5, 0, 0], legL: [-1.0, 0, 0], legR: [-1.0, 0, 0], armL: [-0.5, 0, 0.5], armR: [-0.5, 0, 0.5]}}, { t:1, pose: { ...POSE_ZERO, root: [-0.5, 0, 0], spine: [0.3, 0, 0], legL: [0.8, 0, 0], shiL: [1.2, 0, 0], armL: [0.5, 0, 0], armR: [0.5, 0, 0] }}, { t:2, pose: { ...POSE_ZERO, root: [-0.2, 0, 0], spine:[0.1,0,0] } } ] },
        'dead': { loop: false, speed: 0.1, frames: [ { t:0, pose: { ...POSE_ZERO, root:[-1.5,0,0], armL:[0,0,1.0], armR:[0,0,-1.0], legL:[0.2,0,0.2], legR:[0.2,0,-0.2] } } ] },
        // Tackle: Shoulder Charge
        'tackle': { loop: false, speed: 0.3, hitFrame: 1, frames: [ 
            { t:0, pose: { ...POSE_ZERO, root:[0,-2,0], spine:[0.3,0,0], armR:[0.5,0,0], armL:[0.5,0,0] } }, 
            { t:1, pose: { ...POSE_ZERO, root:[0,-2,0], spine:[0.4,0,0.5], armR:[-1.0,0,0.5], armL:[1.0,0,0] } }, 
            { t:2, pose: { ...POSE_ZERO, root:[0,-2,0], spine:[0.3,0,0] } } 
        ] }
    };

    // --- Classes (Defined BEFORE startStage) ---
    class Character {
        constructor(x, z, isPlayer, isBoss = false, aiSlot = 0) {
            this.x=x; this.y=isPlayer?0:50; this.z=z;
            this.vx=0; this.vy=0; this.vz=0;
            this.isPlayer=isPlayer; this.isBoss=isBoss;
            // FIX: Set Base HP correctly so the HUD bar starts full
            const baseHp = isBoss ? 200 : (isPlayer ? 100 : 30);
            const multiplier = 1 + (State.stage - 1) * 0.1;
            this.maxHp = Math.floor(baseHp * multiplier);
            this.hp = this.maxHp;
            this.facing=1; 
            this.aiSlot = aiSlot; // 0=Front, 1=Back (Flank)
            
            this.animName = 'idle';
            this.animTime = 0;
            this.currentFrameIdx = 0;
            this.hasHit = false; 
            
            this.state='idle'; 
            this.stateTimer=0;
            this.dashTarget=null; this.dashAction=null;
            this.collidedItem = null;
            this.grounded=false; this.invincible=0;
            this.heldItem=null;

            this.comboCount = 0;
            this.lastAttackTime = 0;
            this.inputQueue = null;

            this.isSpinning = false;

            this.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;

            // Dash Stop
            if (this.state === 'dash' && (this.collidedItem || (Math.abs(move.x - (this.x - this.vx)) < 0.1 && Math.abs(move.z - (this.z - this.vz)) < 0.1))) {
                this.vx = 0; this.vz = 0; this.dashTarget = null;
                if (this.dashAction) {
                     this.triggerAttack(this.dashAction === 'kick' ? 'kick' : 'punch');
                     this.dashAction = null;
                } else {
                     this.state = 'idle'; 
                }
            }
            
            // Tackle Move & Hit
            if (this.state === 'tackle') {
                 this.vx = this.facing * 8; 
                 // Tackle Hit Check
                 const t = State.player;
                 if (t && t.state!=='dead' && t.state!=='down' && t.invincible<=0) {
                     const dx = t.x - this.x; const dz = t.z - this.z;
                     if(Math.abs(dx) < 25 && Math.abs(dz) < 15) { // Hit range
                         t.takeDamage(20, 'blowout'); // Double Damage & Blowout
                         AudioSys.explosion();
                         this.state = 'idle'; this.vx = 0; // Stop
                     }
                 }
            }

            if(this.y <= move.ground) {
                this.y = move.ground; this.vy = 0; this.grounded = true;
                this.vx *= 0.8; this.vz *= 0.8;
                this.isSpinning = false;
                this.mesh.rotation.x = 0;
                
                if (this.state === 'jump') {
                    this.state = 'idle';
                }
            } else {
                this.grounded = false;
            }

            this.z = Math.max(-CFG.Z_LIMIT, Math.min(CFG.Z_LIMIT, this.z));
            resolveCharOverlap(this);

            this.mesh.position.set(this.x, this.y, this.z);
            
            if(this.state !== 'mega' && this.state !== 'down' && this.state !== 'dead' && !this.isSpinning) {
                this.mesh.rotation.y = this.facing === 1 ? Math.PI/2 : -Math.PI/2;
            }
            if (this.state === 'tackle') {
                this.mesh.children.forEach(c => { if(c.material) c.material.emissive = new THREE.Color(Math.random(),0,0); });
            } else {
                this.mesh.children.forEach(c => { if(c.material) c.material.emissive = new THREE.Color(0,0,0); });
            }
            if (this.isSpinning) { this.mesh.rotation.x += 0.5; }

            let nextAnim = this.state;
            if(this.state === 'punch') {
                const step = this.comboCount % 3;
                nextAnim = (step === 0) ? 'punch1' : (step === 1 ? 'punch2' : 'punch3');
            } else if(this.state === 'kick') {
                const step = this.comboCount % 3;
                nextAnim = (step === 0) ? 'kick' : (step === 1 ? 'kick2' : 'kick3');
            }

            if (this.state === 'down') {
                this.root.position.y = 3.0; 
            } else if (this.state === 'wakeup') {
                const maxTime = 30; 
                const progress = 1.0 - (Math.max(0, this.stateTimer) / maxTime);
                this.root.position.y = 3.0 + (9.0 - 3.0) * Math.pow(progress, 2); 
            } else if (this.state !== 'kick2' && this.state !== 'boss_punch') { 
                this.root.position.y = 9.0; 
            }

            this.playAnim(nextAnim);

            if(this.stateTimer > 0) {
                this.stateTimer--;
                if(this.stateTimer < 5 && this.inputQueue) {
                    this.triggerAttack(this.inputQueue);
                }

                if(this.stateTimer <= 0) {
                    if (this.state === 'down') { this.state = 'wakeup'; this.stateTimer = 30; } 
                    else if (this.state === 'wakeup') { this.state = 'idle'; } 
                    else { 
                        this.state = 'idle'; this.vx = 0; this.vz = 0; 
                    }
                }
            }

            if(Date.now() - this.lastAttackTime > 1000 && this.state === 'idle') { this.comboCount = 0; }
            if(this.invincible > 0) {
                this.invincible--;
                this.mesh.visible = (Math.floor(this.invincible/4)%2 === 0);
            } else { this.mesh.visible = true; }
        }

        playAnim(name) {
            if(this.animName !== name) { 
                this.animName = name; 
                this.animTime = 0; 
                this.currentFrameIdx = 0; 
                this.hasHit = false; 
            }
            const data = ANIM_DATA[name] || ANIM_DATA['idle'];
            this.animTime += data.speed;
            
            if (data.hitFrame !== undefined && !this.hasHit && Math.floor(this.animTime) >= data.hitFrame) {
                this.performHitCheck(name);
                this.hasHit = true;
            }

            let idx = Math.floor(this.animTime);
            let alpha = this.animTime - idx;
            if(idx >= data.frames.length - 1) {
                if(data.loop) { this.animTime = 0; idx = 0; alpha = 0; } 
                else { idx = Math.max(0, data.frames.length - 2); alpha = 1; }
            }
            const frameData = data.frames[idx];
            if (!frameData) return; 
            const frameA = frameData.pose;
            const frameB = data.frames[idx+1] ? data.frames[idx+1].pose : frameA;
            for(const [key, joint] of Object.entries(this.joints)) {
                const rotA = frameA[key] || POSE_ZERO[key] || [0,0,0];
                const rotB = frameB[key] || POSE_ZERO[key] || [0,0,0];
                const rX = rotA[0] + (rotB[0] - rotA[0]) * alpha;
                const rY = rotA[1] + (rotB[1] - rotA[1]) * alpha;
                const rZ = rotA[2] + (rotB[2] - rotA[2]) * alpha;
                joint.rotation.set(rX, rY, rZ);
                
                if (name === 'punch3' && idx >= 1 && idx <= 3) {
                     this.mesh.position.y += 2.0; 
                     this.mesh.position.x += this.facing * 0.5;
                }
                if (name === 'mega' && idx >= 1 && idx <= 3) {
                     this.mesh.position.x += this.facing * 4.0; 
                }
                if (name === 'kick3' && idx >= 1 && idx <= 3) {
                     // Tatsumaki movement
                     this.mesh.position.x += this.facing * 2.5; 
                     this.mesh.rotation.y += 2.0; // Spin visual
                }
            }
        }

        triggerAttack(type) {
            this.inputQueue = null;
            this.state = type;
            let animName = type;
            if(type==='punch') animName = (this.comboCount%3===0)?'punch1':(this.comboCount%3===1)?'punch2':'punch3';
            if(type==='kick') animName = (this.comboCount%3===0)?'kick':(this.comboCount%3===1)?'kick2':'kick3';

            const data = ANIM_DATA[animName] || ANIM_DATA['punch1'];
            this.stateTimer = Math.floor(data.frames.length / data.speed);
            
            if (type === 'punch') AudioSys.punch();
            else if (type === 'kick') AudioSys.kick();
            
            if (type === 'punch' || type === 'kick') {
                this.comboCount++;
            }
            this.lastAttackTime = Date.now();
        }

        performHitCheck(animName) {
            let dmg = 10;
            let type = 'normal';
            let range = 15; 

            if (animName.includes('3')) { 
                dmg = 20; type = 'heavy'; range = 20; AudioSys.heavyHit();
            } else if (animName.includes('2')) {
                dmg = 12;
            } else if (this.state === 'mega') {
                dmg = 25; type = 'heavy'; range = 30;
            } else if (this.state === 'throw') {
                dmg = 15; type = 'throw'; range = 20;
            } else if (this.state === 'boss_punch') { 
                range = 50; dmg = 10; 
            }

            if (this.heldItem) { dmg += 10; range += 10; }

            checkHit(this, range, dmg, type);
        }

        takeDamage(dmg, type) {
            if(this.invincible > 0 || this.state === 'dead' || this.state === 'down' || this.state === 'wakeup') return;
            
            this.comboCount = 0; 
            this.inputQueue = null;
            this.hp -= dmg;
            AudioSys.hit(); 
            createBlood(this.mesh.position);
            
            if (this.isPlayer) {
                // HP Bar update logic handled in animate loop
            } else {
                State.activeEnemy = this; 
                State.activeEnemyTimer = 60; 
            }

            if(this.heldItem) this.dropWeapon(true);

            if(this.hp <= 0) {
                this.state = 'dead'; this.deadTimer = 0; this.vx = -this.facing * 5; this.vy = 10; 
                if(!this.isPlayer) {
                    State.score += this.isBoss ? 1000 : 10; State.kills++; 
                    if(this.isBoss) { State.slowMotion = true; State.bossDeathTimer = 30; }
                } else { showMsg("GAME OVER", "PRESS 'A' TO RETRY"); State.mode = 'GAME_OVER'; }
            } else {
                if (type === 'blowout') {
                    this.state = 'down'; this.stateTimer = 90; this.invincible = 100;
                    this.vx = -this.facing * 10; this.vy = 8;
                } else if (type === 'throw') {
                    this.state = 'hurt'; this.stateTimer = 60; this.invincible = 60;
                    this.vx = -this.facing * 8; this.vy = 12; this.isSpinning = true;
                } else if (type === 'heavy') {
                    this.state = 'down'; this.stateTimer = 60; this.invincible = 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});
            }
            // Ironball removed per request
            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; 
                
                // Hit check
                if(!this.hitProcessed && player && this.mesh.position.distanceTo(player.mesh.position)<20) { 
                    player.takeDamage(10, 'heavy'); AudioSys.explosion(); this.hitProcessed = true; this.remove(); 
                }
                if(this.mesh.position.y<0) this.remove();
                return;
            }
            if(player && this.type!=='crate' && !this.isProjectile) {
                if(player.mesh.position.distanceTo(this.mesh.position) < 10) {
                    if(this.type==='score') { State.score+=100; AudioSys.score(); this.remove(); }
                    else if(this.type==='health') { player.hp=Math.min(player.maxHp, player.hp+100); AudioSys.heal(); this.remove(); }
                    else if(this.type==='weapon' && !player.heldItem) {
                        player.heldItem = this; this.holder = player;
                        scene.remove(this.mesh); player.handR.add(this.mesh);
                        this.mesh.position.set(0,0,0); this.mesh.rotation.set(Math.PI/2,0,0);
                        AudioSys.pickup();
                    }
                }
            }
        }

        hit(dmg=1) {
            if(this.type!=='crate') return;
            this.hp -= dmg; AudioSys.hit(); createSpark(this.mesh.position);
            if(this.hp<=0) {
                AudioSys.explosion();
                for(let i=0; i<10; i++) createDebris(this.mesh.position, this.color);
                const r = Math.random();
                if(r<0.3) new GameItem(this.mesh.position.x, this.mesh.position.z, 'weapon'); else if(r<0.6) new GameItem(this.mesh.position.x, this.mesh.position.z, 'health');
                this.remove();
            }
        }

        remove() { scene.remove(this.mesh); State.items = State.items.filter(i=>i!==this); }
    }

    // --- Logic ---
    function updateEnemyAI(e) {
        if(e.state==='dead'||e.state==='down'||e.state==='wakeup'||e.state==='hurt') 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 ? 20 : 15; 
        
        // --- IMPROVED AI ---
        // Calculate Target Position
        let targetX = pl.x;
        let targetZ = pl.z;
        
        // Flanking Logic
        const dist = Math.abs(dx);
        if (dist > atkRange && !e.isBoss) {
            if (e.aiSlot === 1) { // Flanker (Go Behind)
                 // Attempt to go to the opposite side of where player is facing
                 targetX = pl.x - (pl.facing * 30);
            } else {
                 // Attacker (Front)
                 targetX = pl.x + (pl.facing * 20); 
            }
        } else if (dist <= atkRange) {
             targetX = pl.x; // Close enough to engage
        }

        const tDx = targetX - e.x;
        const tDz = targetZ - e.z;
        
        // Force Movement logic (No more idle waiting)
        if (Math.abs(tDx) > atkRange || Math.abs(tDz) > 2.0) {
            e.state = 'walk';
            e.vx = Math.sign(tDx) * (e.isBoss ? 0.8 : 0.6 + Math.random()*0.2); // Rush speed
            
            // If very far, move faster
            if (Math.abs(tDx) > 100) e.vx *= 1.5;

            if (Math.abs(tDz) > 2.0) {
                e.vz = Math.sign(tDz) * 1.5; 
            } else {
                e.vz = 0; e.z = targetZ;
            }
            e.facing = Math.sign(dx) || 1; // Always face player real position
        } else {
            // Close Range Combat
            e.vx = 0; e.vz = 0;
            e.facing = Math.sign(dx) || 1;
            
            // Instant attack logic (less hesitation)
            if (Math.random() < 0.15) { 
                if (e.isBoss) {
                    const r = Math.random();
                    if (r < 0.5) { e.triggerAttack('boss_punch'); } 
                    else if (r < 0.8) { e.triggerAttack('tackle'); } 
                    else { e.triggerAttack('kick'); }
                } else if (e.heldItem) { e.triggerAttack('throw'); } 
                else {
                    const r = Math.random();
                    if (r < 0.6) { e.triggerAttack('punch'); } 
                    else if (r < 0.9) { e.triggerAttack('kick'); } 
                    else { e.triggerAttack('mega'); e.vy = 2; e.vx = e.facing * 8; }
                }
            } else { 
                e.state = 'idle'; 
            }
        }
    }

    function checkHit(atk, range, dmg, type) {
        let finalDmg = atk.isPlayer ? dmg : Math.ceil(dmg / 2);
        const push = 3.0;

        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;
            const dz = t.z - atk.z;

            // 敵からの攻撃ヒット判定 (Z軸許容範囲を50pxに拡大)
            if((Math.abs(dx) < 10 || Math.sign(dx)===atk.facing) && Math.abs(dx)<range && Math.abs(dz) < 50) { 
                 const isParry = (t.state.includes('punch') || t.state.includes('kick')) && (atk.state.includes('punch') || atk.state.includes('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.x += Math.sign(dx) * push;
                 t.takeDamage(finalDmg, type);
            }
            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)<15) {
                const isParry = (t.state.includes('punch') || t.state.includes('kick')) && (atk.state.includes('punch') || atk.state.includes('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.x += Math.sign(dx) * push;
                t.takeDamage(finalDmg, type);
            }
        });
        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;
        
        if (State.player) { scene.remove(State.player.mesh); State.player = null; }
        State.player = new Character(0, 0, true);
        // FIX: Use MaxHP determined by constructor
        
        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) { 
                // Assign slot for flanking (Alternating)
                const aiSlot = i % 2; 
                State.enemies.push(new Character(ex, ez, false, false, aiSlot)); 
                State.spawnedCount++; 
            }
        }
    }

    function spawnBoss() {
        State.bossSpawned = true; showMsg("WARNING", "BOSS INCOMING");
        setTimeout(()=> { document.getElementById('overlay').classList.add('hidden'); AudioSys.boss(); State.enemies.push(new Character(State.player.x+80, 0, false, true)); }, 2000);
    }

    const keys={};
    window.addEventListener('keydown', e => {
        if(e.key.startsWith('F')) e.preventDefault();
        keys[e.code]=true;
        if(e.code==='KeyA' && (State.mode==='START' || State.mode==='GAME_OVER' || State.mode==='GAME_CLEAR')) {
            if(State.mode==='GAME_OVER' || State.mode==='GAME_CLEAR') location.reload();
            else { AudioSys.init(); State.player = new Character(0,0,true); startStage(); }
        }
        if(e.code==='F1') State.view='FPS';
        if(e.code==='F2') State.view='SIDE';
        if(e.code==='F3') State.view='TPS';
        if(e.code==='ArrowDown' && State.view==='FPS' && State.mode==='PLAY' && State.player) { State.player.facing = -State.player.facing; }
    });
    window.addEventListener('keyup', e => keys[e.code]=false);

    function animate() {
        requestAnimationFrame(animate);
        
        if (State.mode === 'START') {
            const t = Date.now()*0.0005;
            camera.position.set(Math.sin(t)*100, 50, Math.cos(t)*100);
            camera.lookAt(0, 0, 0);
            renderer.render(scene, camera);
            return;
        }

        if (State.mode === 'INTRO') {
            if(State.player) {
                 const pl = State.player;
                 if(State.view==='SIDE') { camera.position.set(pl.x, 25, 120); camera.lookAt(pl.x, 10, 0); } 
                 else { camera.position.set(pl.x, 30, pl.z+60); camera.lookAt(pl.x, 10, pl.z); }
            }
            renderer.render(scene, camera);
            return;
        }

        if (State.slowMotion) {
            frameCount++;
            if (frameCount % 4 !== 0) return; 
            if (State.bossDeathTimer > 0) { State.bossDeathTimer--; if(State.bossDeathTimer <= 0) finishStage(); }
        }

        const pl = State.player;
        if(State.mode==='PLAY' && pl && pl.state!=='dead') {
            if(pl.dashTarget) {
                if(pl.collidedItem && pl.collidedItem.type==='crate' && pl.collidedItem !== pl.dashTarget) { pl.vx = -pl.vx * 0.5; pl.vz = -pl.vz * 0.5; pl.dashTarget = null; } 
                else {
                    const tPos = pl.dashTarget.mesh ? pl.dashTarget.mesh.position : pl.dashTarget;
                    const dx = tPos.x - pl.x; const dz = tPos.z - pl.z;
                    const dist = Math.sqrt(dx*dx + dz*dz);
                    
                    const tRadius = (pl.dashTarget.size ? pl.dashTarget.size : 10) / 2;
                    const targetDist = tRadius + 2; 

                    const dz_diff = tPos.z - pl.z;
                    const dx_diff = tPos.x - pl.x;
                    const distToSurfaceX = Math.abs(dx_diff) - tRadius;

                    if (Math.abs(dz_diff) > 2.0 || distToSurfaceX > 2.0) { 
                        pl.state = 'dash';
                        if (Math.abs(dz_diff) > 1.0) {
                            pl.vz = Math.sign(dz_diff) * 4.0;
                            pl.vx = Math.sign(dx_diff) * (Math.abs(dx_diff) > 50 ? 3.5 : 1.0); 
                        } else {
                            pl.vz = 0; pl.z = tPos.z; 
                            if (distToSurfaceX > 2.0) {
                                pl.vx = Math.sign(dx_diff) * 4.0;
                            } else {
                                pl.vx = 0; 
                            }
                        }
                        
                        pl.facing = Math.sign(dx_diff) || 1;

                        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;
                        if(pl.state !== 'dash') {
                             if (keys.KeyA) pl.triggerAttack('punch');
                             else if (keys.KeyZ) pl.triggerAttack('kick');
                        }
                    }
                }
            } 
            else if(pl.state === 'idle' || pl.state === 'walk' || pl.state === 'jump' || (pl.stateTimer < 10 && pl.comboCount > 0)) { 
                let mx=0, mz=0; const spd=1.2;
                if(pl.state !== 'punch' && pl.state !== 'kick' && pl.state !== 'mega' && pl.state !== 'throw' && pl.state !== 'hurt' && pl.state !== 'dash') {
                    if(State.view==='SIDE') { 
                        if(keys.ArrowRight) { mx=spd; pl.facing=1; } 
                        if(keys.ArrowLeft) { mx=-spd; pl.facing=-1; } 
                        if(keys.ArrowUp) mz=-spd; 
                        if(keys.ArrowDown) mz=spd; 
                    } 
                    else if(State.view==='FPS') { 
                        if(keys.ArrowUp) mx = spd * pl.facing; 
                        if(keys.ArrowLeft) mz = -spd * pl.facing; 
                        if(keys.ArrowRight) mz = spd * pl.facing; 
                        if(keys.ArrowDown) mx = -spd * pl.facing; 
                    } 
                    else { // TPS
                        if(keys.ArrowUp) { mx=spd; pl.facing=1; } 
                        if(keys.ArrowDown) { mx=-spd; pl.facing=-1; } 
                        if(keys.ArrowRight) mz=spd; 
                        if(keys.ArrowLeft) mz=-spd; 
                    }

                    if(mx!==0 || mz!==0) {
                        if(pl.grounded) pl.state='walk'; pl.vx=mx; pl.vz=mz;
                    } else if(pl.grounded) pl.state='idle';
                }

                if(keys.KeyA) { if((mx!==0||mz!==0)) pl.triggerAttack('throw'); else pl.triggerAttack('punch'); }
                else if(keys.KeyZ) { if(keys.KeyX) pl.triggerAttack('mega'); else if(pl.heldItem) { pl.dropWeapon(); AudioSys.punch(); } else pl.triggerAttack('kick'); }
                else if(keys.KeyX && !keys.KeyZ && pl.grounded) { pl.vy=5; pl.state='jump'; AudioSys.jump(); }
            }
        }
        
        let lockedObj = null;
        if(pl && State.mode==='PLAY') {
            let minDist=60;
            State.items.filter(i=>i.type==='crate').forEach(i => { if(pl.mesh.position.distanceTo(i.mesh.position) < 15 + i.size/2) lockedObj=i; });
            if(!lockedObj) { State.enemies.forEach(e => { if(e.state==='dead') return; const d = pl.mesh.position.distanceTo(e.mesh.position); const dx = e.x - pl.x; if(Math.sign(dx)===pl.facing && d < minDist) { minDist=d; lockedObj=e; } }); }
            if(!lockedObj) { State.items.filter(i=>i.type==='crate').forEach(i => { const d = pl.mesh.position.distanceTo(i.mesh.position); const dx = i.mesh.position.x - pl.x; if(Math.sign(dx)===pl.facing && d < minDist) { minDist=d; lockedObj=i; } }); }
            
             if (lockedObj && !pl.dashTarget && pl.grounded && (keys.KeyA || keys.KeyZ)) {
                 const tPos = lockedObj.mesh ? lockedObj.mesh.position : lockedObj;
                 const dist = pl.mesh.position.distanceTo(tPos);
                 if (dist > 30) { 
                     pl.dashTarget = lockedObj; 
                     pl.dashAction = keys.KeyA ? 'punch' : 'kick';
                 }
             }
        }
        if(lockedObj) {
            lockOnCursor.visible = true;
            const pos = lockedObj.mesh ? lockedObj.mesh.position : lockedObj;
            const h = lockedObj.isBoss?40 : (lockedObj.size ? lockedObj.size + 15 : 25);
            lockOnCursor.position.set(pos.x, pos.y + h, pos.z);
        } else { lockOnCursor.visible = false; }

        State.enemies.forEach(e => { updateEnemyAI(e); e.update(); });
        State.items.forEach(i => i.update(State.mode==='PLAY'?pl:null));

        for(let i=State.particles.length-1; i>=0; i--) {
            const p = State.particles[i]; p.position.add(new THREE.Vector3(p.vx, p.vy, p.vz)); p.vy -= 0.1; p.rotation.x+=0.1;
            if(p.position.y<0) { scene.remove(p); State.particles.splice(i,1); }
        }

        const ehud = document.getElementById('enemy-hud');
        if(State.activeEnemy && State.activeEnemyTimer > 0 && State.activeEnemy.state!=='dead') {
            State.activeEnemyTimer--; ehud.style.opacity = 1;
            document.getElementById('enemy-hp-bar').style.width = (State.activeEnemy.hp / State.activeEnemy.maxHp)*100 + '%';
        } else { ehud.style.opacity = 0; }

        if(pl) {
            pl.setVisibleParts(State.view); pl.update();
            document.getElementById('hp-bar').style.width = (pl.hp/pl.maxHp)*100 +'%';
            document.getElementById('score').innerText = State.score;
            document.getElementById('kill-count').innerText = State.kills;
            const maxEnemies = 20 + (State.stage - 1) * 10;
            document.getElementById('kill-target').innerText = maxEnemies;
            
            const livingEnemies = State.enemies.filter(e => e.state !== 'dead').length;
            if(livingEnemies === 0) { 
                if (State.spawnedCount < maxEnemies) { spawnWave(); } 
                else if (!State.bossSpawned) { spawnBoss(); } 
            }
            
            if(State.view==='SIDE') { camera.position.set(pl.x, 25, 120); camera.lookAt(pl.x, 10, 0); } 
            else if(State.view==='TPS') { camera.position.set(pl.x - 50, 40, pl.z); camera.lookAt(pl.x + 100, 10, pl.z); } 
            else { camera.position.set(pl.x + pl.facing*2, 17, pl.z); camera.lookAt(pl.x + pl.facing*100, 15, pl.z); }
        }

        renderer.render(scene, camera);
    }

    window.onresize = () => { camera.aspect = window.innerWidth/window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); };
    
    animate();
};
</script>
</body>
</html>


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

コメント

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