見出し画像

横スクロールアクションゲーム?「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公開(アクション改善、敵が強くなった)
・2026/1/10 バージョン1.3公開(アクション改善、敵が弱くなった)
・2026/8/3 バージョン1.4公開

画像
ゲーム画面

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

【操作説明】

・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.4 (Enemy Hit Fix)</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: 400px; 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 (Tatsumaki): 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); },
        score() { this.play(900,'sine',0.12,0.3,400); },
        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.7);
    scene.add(amb);
    const sun = new THREE.DirectionalLight(0xffffff, 1.1);
    sun.position.set(50, 100, 50);
    sun.castShadow = true;
    sun.shadow.mapSize.set(2048, 2048);
    scene.add(sun);
    const fill = new THREE.DirectionalLight(0xaaccff, 0.35);
    fill.position.set(-60, 40, -30);
    scene.add(fill);

    // 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 = 1.1;
        } else {
            bgCol = evening.clone().lerp(night, (progress - 0.5) * 2);
            starMat.opacity = (progress - 0.5) * 2; sun.intensity = 1.1 - (progress - 0.5);
        }
        scene.background = bgCol; scene.fog = new THREE.Fog(bgCol, 100, 600);
    }
    
    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.MeshStandardMaterial({color: 0x333333, roughness: 0.6, metalness: 0.05}));
        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], spine:[0.6,0,0], armL:[-1.2,0,0], armR:[0.8,0,0], forL:[-1.8,0,0], forR:[-1.8,0,0], legL:[1.0,0,0], legR:[-0.8,0,0], shiL:[1.2,0,0], shiR:[0.5,0,0] } }, { t:1, pose: { ...POSE_ZERO, root:[0.4,0,0], spine:[0.6,0,0], armL:[0.8,0,0], armR:[-1.2,0,0], forL:[-1.8,0,0], forR:[-1.8,0,0], legL:[-0.8,0,0], legR:[1.0,0,0], shiL:[0.5,0,0], shiR:[1.2,0,0] } } ] },
        
        // Punch
        'punch1': { loop: false, speed: 0.5, hitFrame: 1, frames: [ 
            { t:0, pose: { ...POSE_ZERO, spine:[-0.1,0,0], armR:[0, 0, 0.2], forR:[-2.2, 0, 0] } }, 
            { t:1, pose: { ...POSE_ZERO, spine:[0.1,0,0], armR:[-1.57, 0, 0.1], forR:[0, 0, 0] } }, 
            { t:2, pose: { ...POSE_ZERO, spine:[0,0,0], armR:[0, 0, 0], forR:[-1.5, 0, 0] } }, 
            { t:3, pose: { ...POSE_ZERO } } 
        ] },
        'punch2': { loop: false, speed: 0.5, hitFrame: 1, frames: [ 
            { t:0, pose: { ...POSE_ZERO, spine:[0.1,0,0], armL:[0, 0, 0.2], forL:[-2.2, 0, 0] } }, 
            { t:1, pose: { ...POSE_ZERO, spine:[-0.1,0,0], armL:[-1.57, 0, 0.1], forL:[0, 0, 0] } }, 
            { t:2, pose: { ...POSE_ZERO, spine:[0,0,0], armL:[0, 0, 0], forL:[-1.5, 0, 0] } }, 
            { t:3, pose: { ...POSE_ZERO } } 
        ] },
        'punch3': { loop: false, speed: 0.25, hitFrame: 2, frames: [ 
            { t:0, pose: { ...POSE_ZERO, root:[0,-0.1,0], spine:[-0.15,0.3,0], legL:[0.3,0,0], legR:[-0.4,0,0], armR:[-0.3,-0.3,-0.3] } }, 
            { t:1, pose: { ...POSE_ZERO, root:[0,0.15,0], spine:[0.1,-0.2,0], armR:[-1.6,0,-0.2], forR:[-0.6,0,0], legL:[-0.5,0,0], legR:[0.5,0,0] } }, 
            { t:2, pose: { ...POSE_ZERO, root:[0,0.3,0], spine:[0.3,-0.3,0], armR:[-2.8,0,0], forR:[0,0,0], legL:[-0.3,0,0], legR:[0.3,0,0] } }, 
            { t:3, pose: { ...POSE_ZERO, root:[0,0.1,0], spine:[0.1,0,0], armR:[-1.0,0,0] } }, 
            { t:4, pose: { ...POSE_ZERO } } 
        ] },
        
        'boss_punch': { loop: false, speed: 0.2, hitFrame: 1, frames: [ 
            { t:0, pose: { ...POSE_ZERO, root:[0,-0.15,0], spine:[0.4,0.3,0], armR:[-0.4,0,0.4], forR:[-1.4,0,0] } }, 
            { t:1, pose: { ...POSE_ZERO, root:[0,0.1,0], spine:[0.9,-0.3,0], armR:[-2.0,0,-0.15], forR:[0,0,0] } }, 
            { t:2, pose: { ...POSE_ZERO, root:[0,0.1,0], spine:[0.9,-0.3,0], armR:[-2.0,0,-0.15] } }, 
            { t:3, pose: { ...POSE_ZERO, root:[0,-0.1,0], spine:[0.2,0,0] } } 
        ] },
        
        '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 } } 
        ] },
        '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] } }, 
            { 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] } }, 
            { 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] } }, 
            { 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: Tatsumaki (Spin Kick)
        'mega': { loop: true, speed: 0.25, hitFrame: 0, frames: [ 
            { t:0, pose: { ...POSE_ZERO, root:[0, 8, 0], 
                           spine:[0, 0, 0], 
                           legR:[-1.6, 0, 0.5], shiR:[0, 0, 0], // Right leg straight out
                           legL:[-0.5, 0, 0], shiL:[2.0, 0, 0], // Left leg tucked
                           armL:[0, 0, 1.5], armR:[0, 0, -1.5] // Arms spread
            }} 
        ] },
        
        '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': { 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] } } 
        ] }
    };

    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;
            const baseHp = isBoss ? 200 : (isPlayer ? 250 : 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; 
            
            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 jeansCol = 0x000055; 
            const pantsCol = this.isPlayer ? jeansCol : (this.isBoss ? 0x111111 : randBrightColor());
            const bootsCol = 0x221100; 
            const wristBandCol = 0xffffff;
            const padCol = 0x888888; 

            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.MeshStandardMaterial({color: pantsCol, roughness: 0.6, metalness: 0.05})); 
            this.root.add(pelvis);
            
            this.spine = new THREE.Group(); this.spine.position.y = 1.25; this.root.add(this.spine);
            
            const backPlate = new THREE.Mesh(new THREE.BoxGeometry(4.8, 6.0, 1.0), new THREE.MeshStandardMaterial({color: shirtCol, roughness: 0.6, metalness: 0.05}));
            backPlate.position.set(0, 3.0, -1.0); backPlate.castShadow = true; this.spine.add(backPlate);

            const absGroup = new THREE.Group(); absGroup.position.set(0, 1.5, 0.4); this.spine.add(absGroup);
            const absC = new THREE.Mesh(new THREE.BoxGeometry(1.5, 2.5, 0.8), new THREE.MeshStandardMaterial({color: skin, roughness: 0.6, metalness: 0.05}));
            absC.castShadow = true; absGroup.add(absC);
            const absL = new THREE.Mesh(new THREE.BoxGeometry(1.0, 2.5, 2.2), new THREE.MeshStandardMaterial({color: shirtCol, roughness: 0.6, metalness: 0.05}));
            absL.position.set(1.4, 0, 0); absGroup.add(absL);
            const absR = new THREE.Mesh(new THREE.BoxGeometry(1.0, 2.5, 2.2), new THREE.MeshStandardMaterial({color: shirtCol, roughness: 0.6, metalness: 0.05}));
            absR.position.set(-1.4, 0, 0); absGroup.add(absR);

            const chestGroup = new THREE.Group(); chestGroup.position.set(0, 4.5, 0.6); this.spine.add(chestGroup);
            const pecSkinL = new THREE.Mesh(new THREE.BoxGeometry(1.2, 3.0, 0.5), new THREE.MeshStandardMaterial({color: skin, roughness: 0.6, metalness: 0.05}));
            pecSkinL.position.set(0.7, 0, 0.2); chestGroup.add(pecSkinL);
            const pecSkinR = new THREE.Mesh(new THREE.BoxGeometry(1.2, 3.0, 0.5), new THREE.MeshStandardMaterial({color: skin, roughness: 0.6, metalness: 0.05}));
            pecSkinR.position.set(-0.7, 0, 0.2); chestGroup.add(pecSkinR);
            const pecL = new THREE.Mesh(new THREE.BoxGeometry(1.6, 3.5, 2.6), new THREE.MeshStandardMaterial({color: shirtCol, roughness: 0.6, metalness: 0.05}));
            pecL.position.set(1.9, 0, 0); pecL.castShadow = true; chestGroup.add(pecL);
            const pecR = new THREE.Mesh(new THREE.BoxGeometry(1.6, 3.5, 2.6), new THREE.MeshStandardMaterial({color: shirtCol, roughness: 0.6, metalness: 0.05}));
            pecR.position.set(-1.9, 0, 0); pecR.castShadow = true; chestGroup.add(pecR);

            this.headGroup = new THREE.Group(); this.headGroup.position.y = 6.0; this.spine.add(this.headGroup);
            const neck = new THREE.Mesh(new THREE.BoxGeometry(1.5, 1.0, 1.5), new THREE.MeshStandardMaterial({color: skin, roughness: 0.6, metalness: 0.05}));
            neck.position.y = 0.5; this.headGroup.add(neck);
            
            // Sphere Head
            this.headMesh = new THREE.Mesh(new THREE.SphereGeometry(1.8, 24, 20), new THREE.MeshStandardMaterial({color: skin, roughness: 0.6, metalness: 0.05})); 
            this.headMesh.position.y = 2.5; this.headGroup.add(this.headMesh);
            
            const nose = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.5, 0.5), new THREE.MeshStandardMaterial({color: 0xffaaaa, roughness: 0.6, metalness: 0.05})); 
            nose.position.set(0, 2.3, 1.6); this.headGroup.add(nose);

            // --- Hair (3 Cubes Tilted + Back base) ---
            const hairCol = randBrightColor();
            const hairMat = new THREE.MeshStandardMaterial({color: hairCol, roughness: 0.6, metalness: 0.05});

            // 1. Center Top
            const hair1 = new THREE.Mesh(new THREE.BoxGeometry(2.5, 2.5, 2.5), hairMat);
            hair1.position.set(0, 3.8, 0);
            hair1.rotation.y = Math.PI / 4; 
            hair1.rotation.x = -0.2;
            this.headGroup.add(hair1);

            // 2. Left Side
            const hair2 = new THREE.Mesh(new THREE.BoxGeometry(2.0, 2.0, 2.0), hairMat);
            hair2.position.set(1.5, 3.2, 0);
            hair2.rotation.z = -0.5; 
            hair2.rotation.y = Math.PI / 6;
            this.headGroup.add(hair2);

            // 3. Right Side
            const hair3 = new THREE.Mesh(new THREE.BoxGeometry(2.0, 2.0, 2.0), hairMat);
            hair3.position.set(-1.5, 3.2, 0);
            hair3.rotation.z = 0.5; 
            hair3.rotation.y = -Math.PI / 6;
            this.headGroup.add(hair3);
            
            // Back Hair Base (Adjusted Height)
            const hairBack = new THREE.Mesh(new THREE.BoxGeometry(3.0, 3.0, 1.5), hairMat);
            hairBack.position.set(0, 3.2, -1.2); // Raised Y position
            hairBack.rotation.x = -0.2;
            this.headGroup.add(hairBack);

            // Ears
            const earL = new THREE.Mesh(new THREE.BoxGeometry(0.5, 1.0, 0.5), new THREE.MeshStandardMaterial({color: skin, roughness: 0.6, metalness: 0.05}));
            earL.position.set(1.9, 2.5, 0); this.headGroup.add(earL);
            const earR = new THREE.Mesh(new THREE.BoxGeometry(0.5, 1.0, 0.5), new THREE.MeshStandardMaterial({color: skin, roughness: 0.6, metalness: 0.05}));
            earR.position.set(-1.9, 2.5, 0); this.headGroup.add(earR);

            // --- ARMS ---
            this.armL = new THREE.Group(); this.armL.position.set(2.9, 5.0, 0); this.spine.add(this.armL);
            const shL = new THREE.Mesh(new THREE.BoxGeometry(2.4, 2.2, 2.4), new THREE.MeshStandardMaterial({color: padCol, roughness: 0.6, metalness: 0.05}));
            shL.position.y = -0.5; this.armL.add(shL);
            const uaL = new THREE.Mesh(new THREE.CylinderGeometry(0.72, 0.62, 3.5, 8), new THREE.MeshStandardMaterial({color: skin, roughness: 0.6, metalness: 0.05})); 
            uaL.position.y = -2.5; this.armL.add(uaL);
            
            this.foreL = new THREE.Group(); this.foreL.position.set(0, -4.5, 0); this.armL.add(this.foreL);
            const elbowL = new THREE.Mesh(new THREE.SphereGeometry(1.3, 14, 12), new THREE.MeshStandardMaterial({color: skin, roughness: 0.6, metalness: 0.05}));
            elbowL.position.set(0, 0.5, 0); this.foreL.add(elbowL);
            const bandL = new THREE.Mesh(new THREE.BoxGeometry(1.4, 0.8, 1.4), new THREE.MeshStandardMaterial({color: wristBandCol, roughness: 0.6, metalness: 0.05}));
            bandL.position.y = -0.5; this.foreL.add(bandL);
            const laL = new THREE.Mesh(new THREE.CylinderGeometry(0.6, 0.48, 3.0, 8), new THREE.MeshStandardMaterial({color: skin, roughness: 0.6, metalness: 0.05}));
            laL.position.y = -2.5; this.foreL.add(laL);
            const fistL = new THREE.Mesh(new THREE.SphereGeometry(1.4, 14, 12), new THREE.MeshStandardMaterial({color: skin, roughness: 0.6, metalness: 0.05}));
            fistL.position.y = -4.5; this.foreL.add(fistL);

            this.armR = new THREE.Group(); this.armR.position.set(-2.9, 5.0, 0); this.spine.add(this.armR);
            const shR = new THREE.Mesh(new THREE.BoxGeometry(2.4, 2.2, 2.4), new THREE.MeshStandardMaterial({color: padCol, roughness: 0.6, metalness: 0.05}));
            shR.position.y = -0.5; this.armR.add(shR);
            const uaR = new THREE.Mesh(new THREE.CylinderGeometry(0.72, 0.62, 3.5, 8), new THREE.MeshStandardMaterial({color: skin, roughness: 0.6, metalness: 0.05}));
            uaR.position.y = -2.5; this.armR.add(uaR);

            this.foreR = new THREE.Group(); this.foreR.position.set(0, -4.5, 0); this.armR.add(this.foreR);
            const elbowR = new THREE.Mesh(new THREE.SphereGeometry(1.3, 14, 12), new THREE.MeshStandardMaterial({color: skin, roughness: 0.6, metalness: 0.05}));
            elbowR.position.set(0, 0.5, 0); this.foreR.add(elbowR);
            const bandR = new THREE.Mesh(new THREE.BoxGeometry(1.4, 0.8, 1.4), new THREE.MeshStandardMaterial({color: wristBandCol, roughness: 0.6, metalness: 0.05}));
            bandR.position.y = -0.5; this.foreR.add(bandR);
            const laR = new THREE.Mesh(new THREE.CylinderGeometry(0.6, 0.48, 3.0, 8), new THREE.MeshStandardMaterial({color: skin, roughness: 0.6, metalness: 0.05}));
            laR.position.y = -2.5; this.foreR.add(laR);
            this.handR = new THREE.Group(); this.handR.position.y=-4.5; this.foreR.add(this.handR);
            const fistR = new THREE.Mesh(new THREE.SphereGeometry(1.4, 14, 12), new THREE.MeshStandardMaterial({color: skin, roughness: 0.6, metalness: 0.05}));
            this.handR.add(fistR);

            this.thighL = new THREE.Group(); this.thighL.position.set(1.3, -1.2, 0); this.root.add(this.thighL);
            const buttL = new THREE.Mesh(new THREE.BoxGeometry(1.8, 2.0, 2.0), new THREE.MeshStandardMaterial({color: pantsCol, roughness: 0.6, metalness: 0.05}));
            buttL.position.set(0, -0.5, -0.5); this.thighL.add(buttL);
            const thM_L = new THREE.Mesh(new THREE.CylinderGeometry(0.92, 0.75, 4.0, 8), new THREE.MeshStandardMaterial({color: pantsCol, roughness: 0.6, metalness: 0.05}));
            thM_L.position.y = -3.0; this.thighL.add(thM_L);
            this.shinL = new THREE.Group(); this.shinL.position.set(0, -5.5, 0); this.thighL.add(this.shinL);
            const shinM_L = new THREE.Mesh(new THREE.CylinderGeometry(0.78, 0.62, 4.0, 8), new THREE.MeshStandardMaterial({color: bootsCol, roughness: 0.6, metalness: 0.05}));
            shinM_L.position.y = -2.0; this.shinL.add(shinM_L);
            const footL = new THREE.Mesh(new THREE.BoxGeometry(1.8, 1.5, 2.5), new THREE.MeshStandardMaterial({color: bootsCol, roughness: 0.6, metalness: 0.05}));
            footL.position.set(0, -4.5, 0.5); this.shinL.add(footL);

            this.thighR = new THREE.Group(); this.thighR.position.set(-1.3, -1.2, 0); this.root.add(this.thighR);
            const buttR = new THREE.Mesh(new THREE.BoxGeometry(1.8, 2.0, 2.0), new THREE.MeshStandardMaterial({color: pantsCol, roughness: 0.6, metalness: 0.05}));
            buttR.position.set(0, -0.5, -0.5); this.thighR.add(buttR);
            const thM_R = new THREE.Mesh(new THREE.CylinderGeometry(0.92, 0.75, 4.0, 8), new THREE.MeshStandardMaterial({color: pantsCol, roughness: 0.6, metalness: 0.05}));
            thM_R.position.y = -3.0; this.thighR.add(thM_R);
            this.shinR = new THREE.Group(); this.shinR.position.set(0, -5.5, 0); this.thighR.add(this.shinR);
            const shinM_R = new THREE.Mesh(new THREE.CylinderGeometry(0.78, 0.62, 4.0, 8), new THREE.MeshStandardMaterial({color: bootsCol, roughness: 0.6, metalness: 0.05}));
            shinM_R.position.y = -2.0; this.shinR.add(shinM_R);
            const footR = new THREE.Mesh(new THREE.BoxGeometry(1.8, 1.5, 2.5), new THREE.MeshStandardMaterial({color: bootsCol, roughness: 0.6, metalness: 0.05}));
            footR.position.set(0, -4.5, 0.5); this.shinR.add(footR);

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

            if (this.state === 'mega') {
                this.vy = 0; 
                if (this.y < 7.5) this.y = 7.5; // Lowered flight height
                this.vx = this.facing * 5; 
            } else {
                this.vy -= CFG.GRAVITY;
            }

            const move = resolveCollision(this, this.vx, this.vz);
            this.x = move.x;
            this.z = move.z;
            this.y += this.vy;
            this.collidedItem = move.hit;

            if (this.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'; 
                }
            }
            
            if (this.state === 'tackle') {
                 this.vx = this.facing * 8; 
                 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) { 
                         t.takeDamage(20, 'blowout'); 
                         AudioSys.explosion();
                         this.state = 'idle'; this.vx = 0; 
                     }
                 }
            }

            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';
                }
                if (this.state === 'mega') this.y = 7.5; 
            } 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 if (this.state === 'mega') {
                this.mesh.children.forEach(c => { if(c.material) c.material.emissive = new THREE.Color(0,0.5,1.0); });
            } else {
                this.mesh.children.forEach(c => { if(c.material) c.material.emissive = new THREE.Color(0,0,0); });
            }
            if (this.isSpinning || this.state === 'mega') { this.mesh.rotation.y += 0.8; }

            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; 
                        this.mesh.children.forEach(c => { if(c.material) c.material.emissive = new THREE.Color(0,0,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);
            }

            // These offsets must run ONCE per frame, not once per joint.
            // (Previously they were inside the loop above, which has 11
            // joints -- causing an unintended 11x-per-frame multiplication
            // that sent characters flying far off their intended position
            // during the punch3 / kick3 finishing moves.)
            if (name === 'punch3' && idx >= 1 && idx <= 3) {
                 this.mesh.position.y += 2.0; 
                 this.mesh.position.x += this.facing * 0.5;
            }
            if (name === 'kick3' && idx >= 1 && idx <= 3) {
                 this.mesh.position.x += this.facing * 2.5; 
                 this.mesh.rotation.y += 2.0; 
            }
            if (name === 'boss_punch' && idx >= 1 && idx <= 2) {
                 this.mesh.position.x += this.facing * 1.2;
            }
        }

        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'];
            if (type === 'mega') this.stateTimer = 15; // 竜巻旋風脚の持続時間を短縮
            else this.stateTimer = Math.floor(data.frames.length / data.speed);
            
            if (type === 'punch') AudioSys.punch();
            else if (type === 'kick') AudioSys.kick();
            else if (type === 'mega') AudioSys.jump();
            
            if (type === 'punch' || type === 'kick') {
                this.comboCount++;
            }
            this.lastAttackTime = Date.now();
        }

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

            if (animName.includes('3')) { 
                dmg = 20; type = 'heavy'; range = 30; AudioSys.heavyHit();
            } else if (animName.includes('2')) {
                dmg = 12;
            } else if (this.state === 'mega') {
                dmg = 20; type = 'blowout'; range = 35; 
            } else if (this.state === 'throw') {
                dmg = 15; type = 'throw'; range = 25;
            } 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;
            
            if (type === 'blowout') dmg *= 2;

            this.hp -= dmg;
            AudioSys.hit(); 
            createBlood(this.mesh.position);
            
            if (!this.isPlayer) {
                State.activeEnemy = this; 
                State.activeEnemyTimer = 60; 
            }

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

            if(this.hp <= 0) {
                this.state = 'dead'; this.deadTimer = 0; this.vx = -this.facing * 5; this.vy = 10; 
                if(!this.isPlayer) {
                    State.score += this.isBoss ? 1000 : 10; State.kills++; 
                    if(this.isBoss) { State.slowMotion = true; State.bossDeathTimer = 30; }
                } else { showMsg("GAME OVER", "PRESS 'A' TO RETRY"); State.mode = 'GAME_OVER'; }
            } else {
                if (type === 'blowout') {
                    this.state = 'down'; this.stateTimer = 90; this.invincible = 60;
                    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 = 60; 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; it.owner = this; }
        }

        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.MeshStandardMaterial({color: Math.random()*0xffffff, roughness: 0.6, metalness: 0.05});
                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.MeshStandardMaterial({color: 0x8b5a2b, roughness: 0.6, metalness: 0.05}); this.mesh.rotation.z = Math.PI/2;
            } else if(type==='health') {
                geo = new THREE.BoxGeometry(3,3,3); mat = new THREE.MeshStandardMaterial({color: 0x00ff00, roughness: 0.6, metalness: 0.05});
            } else if(type==='score') {
                geo = new THREE.BoxGeometry(3,1,2); mat = new THREE.MeshStandardMaterial({color: 0xffd700, roughness: 0.6, metalness: 0.05});
            }
            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; 
                
                if(!this.hitProcessed && player && player !== this.owner && 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'
           ||e.state==='punch'||e.state==='kick'||e.state==='mega'||e.state==='throw'
           ||e.state==='tackle'||e.state==='boss_punch'||e.state==='jump'||e.state==='dash') 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 engagementDist = e.isBoss ? 20 : 5; 
        
        let targetX = pl.x;
        let targetZ = pl.z;
        
        const dist = Math.abs(dx);
        if (dist > engagementDist + 20 && !e.isBoss) {
            if (e.aiSlot === 1) { 
                 targetX = pl.x - (pl.facing * 30);
            } else {
                 targetX = pl.x + (pl.facing * 20); 
            }
        } else if (dist <= engagementDist) {
             targetX = pl.x; 
        }

        const tDx = targetX - e.x;
        const tDz = targetZ - e.z;
        
        const dToPlayer = Math.abs(dx);
        const zToPlayer = Math.abs(dz);
        
        // AIの停止条件(攻撃開始条件)
        // 攻撃射程は engagementDist(歩み寄りをやめる距離)より必ず広く取る。
        // 以前は固定値12を使っていたため、ボスの engagementDist(20) より
        // 攻撃射程の方が狭くなり、「12〜20の間で歩みを止めるが攻撃もしない」
        // というデッドゾーンが生まれ、ボスがプレイヤー付近で立ち止まって
        // 何もしなくなる原因になっていた。
        const attackDistX = e.isBoss ? engagementDist + 6 : 12;
        const attackDistZ = e.isBoss ? 4.0 : 2.0;
        if (dToPlayer < attackDistX && zToPlayer < attackDistZ) { 
             e.vx = 0; e.vz = 0;
             e.z = pl.z; // 軸合わせを確定
             e.facing = Math.sign(dx) || 1;
             
             if (Math.random() < 0.2) { 
                 if (e.isBoss) {
                    const r = Math.random();
                    if (r < 0.2) { e.triggerAttack('boss_punch'); } 
                    else if (r < 0.4) { e.triggerAttack('tackle'); } 
                    else if (r < 0.5) { e.triggerAttack('throw'); } 
                    else if (r < 0.7) { e.triggerAttack('punch'); } 
                    else if (r < 0.85) { e.triggerAttack('kick'); } 
                    else { e.triggerAttack('mega'); e.vy = 2; e.vx = e.facing * 8; }
                 } else {
                    const r = Math.random();
                    if (r < 0.5) { e.triggerAttack('punch'); } 
                    else { e.triggerAttack('kick'); }
                 }
             } else {
                 e.state = 'idle'; 
             }
             return;
        }

        if (Math.abs(tDx) > engagementDist || Math.abs(tDz) > 1.0) {
            e.state = 'walk';
            e.vx = Math.sign(tDx) * (e.isBoss ? 0.8 : 0.6 + Math.random()*0.2); 
            
            if (Math.abs(tDx) > 100) e.vx *= 1.5;

            if (Math.abs(tDz) > 1.0) {
                e.vz = Math.sign(tDz) * 1.5; 
            } else {
                e.vz = 0; e.z = targetZ;
            }
            e.facing = Math.sign(dx) || 1; 
        } else {
            e.vx = 0; e.vz = 0;
            e.facing = Math.sign(dx) || 1;
            e.state = 'idle';
        }
    }

    function checkHit(atk, range, dmg, type) {
        let finalDmg = atk.isPlayer ? dmg : Math.ceil(dmg / 2);
        const push = 3.0;
        
        // --- 判定の分離 ---
        // プレイヤー攻撃時:精密な判定 (Z幅12)
        // 敵攻撃時:やや甘い判定 (Z幅20 + リーチ延長)
        
        let zTolerance = 12;
        let actualRange = range;
        let closeDistLimit = 20;

        if (!atk.isPlayer) {
             zTolerance = 20; // 敵の攻撃はZ軸判定を広くする
             actualRange += 15; // リーチも少し伸ばす
             closeDistLimit = 25; // 密着判定も甘くする
        }

        // 攻撃対象のリスト作成
        let targets = [];
        if (!atk.isPlayer) {
            targets = [State.player];
        } else {
            targets = State.enemies;
        }

        targets.forEach(t => {
            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;
            const dist = Math.abs(dx);
            const zDist = Math.abs(dz);
            
            if(dist < actualRange && zDist < zTolerance) { 
                 const isFacingTarget = Math.sign(dx) === atk.facing;
                 const isVeryClose = dist < closeDistLimit; 

                 if (isVeryClose || isFacingTarget) {
                     if (t.state.includes('punch') || t.state.includes('kick')) {
                        if (atk.state.includes('punch') || atk.state.includes('kick')) {
                            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); 
                 }
            }
        });

        if (atk.isPlayer) {
            State.items.forEach(i => {
                if(i.type!=='crate') return;
                const dx = i.mesh.position.x - atk.x;
                const dz = i.mesh.position.z - atk.z;
                if(Math.abs(dx) < range + i.size/2 && Math.abs(dz) < 15) {
                    if(Math.sign(dx)===atk.facing) {
                        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);
        
        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<24; i++) {
            let cx, cz, ok=false;
            for(let t=0; t<10; t++) { 
                cx = 50 + Math.random() * 600; 
                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) { 
                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 tSize = (pl.dashTarget.size || 10);
                    
                    // --- ロックオン・ダッシュの移動先制御 ---
                    const offsetDist = tSize/2 + 10; 
                    
                    // ターゲットの「手前」または「奥」(画面上の左右)の位置を計算
                    // プレイヤーがターゲットより左にいるなら、ターゲットの左側(-offset)へ、右なら右側(+offset)へ移動
                    const targetX = (pl.x < tPos.x) ? tPos.x - offsetDist : tPos.x + offsetDist;
                    const targetZ = tPos.z;

                    const dx = targetX - pl.x;
                    const dz = targetZ - pl.z;

                    pl.state = 'dash';
                    
                    // Z軸移動
                    if (Math.abs(dz) > 1.0) {
                        pl.vz = Math.sign(dz) * 4.0;
                    } else {
                        pl.vz = 0; pl.z = targetZ;
                    }

                    // X軸移動
                    if (Math.abs(dx) > 2.0) {
                        pl.vx = Math.sign(dx) * 4.0;
                        pl.facing = Math.sign(tPos.x - pl.x) || 1; // 顔はターゲットの方を向く
                    } else {
                        pl.vx = 0;
                    }

                    // 指定座標(正面/背面)に到達したら攻撃開始
                    if (Math.abs(dz) < 2.0 && Math.abs(dx) < 5.0) {
                         pl.vx = 0; pl.vz = 0; pl.dashTarget = null;
                         pl.facing = Math.sign(tPos.x - pl.x) || 1;

                         if (pl.dashAction) {
                             pl.triggerAttack(pl.dashAction === 'kick' ? 'kick' : 'punch');
                             pl.dashAction = null;
                         } else {
                             pl.state = 'idle';
                         }
                    }
                    
                    // ジャンプキャンセル
                    if(keys.KeyX && pl.grounded) { pl.vy=5; pl.state='jump'; AudioSys.jump(); pl.dashTarget = null; }
                }
            } 
            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) { 
                    // Throw Input Check
                    let isThrowInput = false;
                    if (State.view === 'SIDE') {
                        isThrowInput = keys.ArrowLeft || keys.ArrowRight;
                    } else {
                        isThrowInput = keys.ArrowUp || keys.ArrowDown;
                    }

                    if(isThrowInput) 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>


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

ピックアップされています

気楽に遊べるゲーム

  • 30本

コメント

コメントするには、 ログイン または 会員登録 をお願いします。
小ぶりなプログラムを試しに作っているんですが、 ここではその説明書きをしていこうと思います。 こういう機能をつけてみてほしいだとか要望は、 コメント欄か、Xのリプライ欄に書いてみて下さい。 ひまをみて対応します。 (未管理著作物裁定制度に定められた問い合わせも受付中。)
横スクロールアクションゲーム?「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