見出し画像

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

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

画像
殴っている様子

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

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


<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>KNOCK OUT RUSH</title>
<style>
    body { margin: 0; overflow: hidden; background: #000; font-family: 'Arial Black', sans-serif; user-select: none; color: white; }
    canvas { display: block; width: 100vw; height: 100vh; }

    #ui { 
        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: #220000; border: 2px solid #fff;
        transform: skewX(-20deg); overflow: hidden; box-shadow: 2px 2px 5px rgba(0,0,0,0.8);
    }
    .hp-fill { width: 100%; height: 100%; background: linear-gradient(90deg, #ff0000, #ffff00, #00ff00); transition: width 0.1s; }

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

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

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

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

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

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

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

    <div id="controls">
        <strong>[F2] Side View</strong> | [F1] FPS (↓Turn) | [F3] TPS<br>
        Move: Arrows | Punch: A (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;
    }
    
    // Define helper functions before they are used in Classes
    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) {
        const nextX = char.x + dx;
        const nextZ = char.z + dz;
        let moveX = dx;
        let moveZ = dz;
        let onGroundY = CFG.GROUND; 
        let hitItem = null;

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

            if (overlapX && overlapZ) {
                if (currentlyAbove && char.vy <= 0) { 
                    onGroundY = Math.max(onGroundY, topY);
                } else if (!currentlyAbove) {
                    hitItem = item;
                    const xOverlapOnly = (char.x > cx - size - margin && char.x < cx + size + margin);
                    const zOverlapOnly = (char.z > cz - size - margin && char.z < cz + size + margin);
                    if (zOverlapOnly) moveX = 0; else if (xOverlapOnly) moveZ = 0; else { moveX = 0; moveZ = 0; }
                }
            }
        }
        return { x: moveX, z: moveZ, 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;
                const nx = dx / dist;
                const nz = dz / dist;
                char.x += nx * push;
                char.z += nz * push;
                if(!t.isPlayer || State.mode !== 'PLAY') { t.x -= nx * push; t.z -= nz * 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] } } ] },
        'punch1': { loop: false, speed: 0.25, frames: [ { t:0, pose: { ...POSE_ZERO, spine:[0,0.5,0], armR:[-1.0,-0.5,0], forR:[-1.5,0,0] } }, { t:1, pose: { ...POSE_ZERO, root:[0.2, 0.4, 0.2], spine:[0, -0.9, 0], armL:[-0.5, 0.5, 0.5], armR:[-1.6, 0, -0.6], forR:[0,0,0], legL:[0.6,0,0], legR:[-0.4,0,0] } }, { t:2, pose: { ...POSE_ZERO, root:[0.2, 0.4, 0.2], spine:[0, -0.9, 0], armR:[-1.6, 0, -0.6], forR:[0,0,0] } }, { t:3, pose: { ...POSE_ZERO } } ] },
        'punch2': { loop: false, speed: 0.25, frames: [ { t:0, pose: { ...POSE_ZERO, spine:[0,-0.5,0], armL:[-1.0,0.5,0.5], forL:[-1.5,0,0] } }, { t:1, pose: { ...POSE_ZERO, root:[0.3, -0.4, -0.2], spine:[0.1, 1.0, 0], armR:[-0.5, -0.5, -0.5], armL:[-1.6, 0.2, 0.8], forL:[0,0,0], legL:[-0.4,0,0], legR:[0.6,0,0] } }, { t:2, pose: { ...POSE_ZERO, root:[0.3, -0.4, -0.2], spine:[0.1, 1.0, 0], armL:[-1.6, 0.2, 0.8], forL:[0,0,0] } }, { t:3, pose: { ...POSE_ZERO } } ] },
        'punch3': { loop: false, speed: 0.2, frames: [ { t:0, pose: { ...POSE_ZERO, spine:[0,0.8,0], armR:[-0.5,-0.5,-0.5] } }, { t:1, pose: { ...POSE_ZERO, root:[0, -0.5, 0], spine:[0, -1.0, 0], armR:[-2.5, 0, 0], head:[-0.5,0,0], legL:[0.5,0,0], legR:[-0.5,0,0] } }, { t:2, pose: { ...POSE_ZERO, root:[0, -0.5, 0], spine:[0, -1.0, 0], armR:[-2.5, 0, 0] } }, { t:3, pose: { ...POSE_ZERO } } ] },
        'boss_punch': { loop: false, speed: 0.2, frames: [ { t:0, pose: { ...POSE_ZERO, root:[0,-3,0], spine:[0,0.5,0], armR:[-1.0,-0.5,0], forR:[-1.5,0,0], legL:[0.5,0,0], legR:[-0.5,0,0] } }, { t:1, pose: { ...POSE_ZERO, root:[0.2,-3,0.2], spine:[0, -0.9, 0], armL:[-0.5, 0.5, 0.5], armR:[-1.6, 0, -0.6], forR:[0,0,0], legL:[0.6,0,0], legR:[-0.4,0,0] } }, { t:2, pose: { ...POSE_ZERO, root:[0.2,-3,0.2], spine:[0, -0.9, 0], armR:[-1.6, 0, -0.6], forR:[0,0,0] } }, { t:3, pose: { ...POSE_ZERO, root:[0,-3,0] } } ] },
        'kick': { loop: false, speed: 0.2, frames: [ { t:0, pose: { ...POSE_ZERO, root:[-0.2,0,0], legR:[-0.8,0,0], shiR:[0,0,0] } }, { t:1, pose: { ...POSE_ZERO, root:[-0.5,0,-0.2], spine:[0,0,0.3], legL:[0,0,0], legR:[-1.9, 0, 0.1], shiR:[0,0,0], armL:[0.5,0,0], armR:[-0.5,0,0] } }, { t:2, pose: { ...POSE_ZERO, root:[-0.5,0,-0.2], legR:[-1.9, 0, 0.1], shiR:[0,0,0] } }, { t:3, pose: { ...POSE_ZERO } } ] },
        'kick2': { loop: false, speed: 0.2, frames: [ { t:0, pose: { ...POSE_ZERO, root:[0,-0.5,0], legL:[0.5,0,0] } }, { t:1, pose: { ...POSE_ZERO, root:[0,-0.5,0], spine:[0,3.14,0], legL:[0,0,0], legR:[-1.5,0,1.5], armL:[0,0,1], armR:[0,0,-1] } }, { t:2, pose: { ...POSE_ZERO } } ] },
        'kick3': { loop: false, speed: 0.15, frames: [ { t:0, pose: { ...POSE_ZERO, root:[0,-1,0], legR:[-1,0,0] } }, { t:1, pose: { ...POSE_ZERO, root:[0,2,0], spine:[0,3.14*2,0], legR:[-2.0,0,0], armL:[0,0,2], armR:[0,0,-2] } }, { t:2, 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, 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, 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] } } ] }
    };

    // --- Classes (Defined BEFORE startStage) ---
    class Character {
        constructor(x, z, isPlayer, isBoss = false) {
            this.x=x; this.y=isPlayer?0:50; this.z=z;
            this.vx=0; this.vy=0; this.vz=0;
            this.isPlayer=isPlayer; this.isBoss=isBoss;
            this.maxHp = isPlayer ? 100 : (isBoss ? 200 : 60); 
            this.hp = this.maxHp;
            this.facing=1; 
            
            this.animName = 'idle';
            this.animTime = 0;
            this.currentFrameIdx = 0;
            
            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.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;

            if(this.y <= move.ground) {
                this.y = move.ground; this.vy = 0; this.grounded = true;
                this.vx *= 0.8; this.vz *= 0.8;
            } else {
                this.grounded = false;
            }

            // コースアウト防止 (Z軸クランプ)
            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.mesh.rotation.y = this.facing === 1 ? Math.PI/2 : -Math.PI/2;
            }

            let nextAnim = 'idle';
            if(this.state === 'walk') nextAnim = 'walk';
            if(this.state === 'dash') nextAnim = 'dash';
            if(this.state === 'jump') nextAnim = 'jump';
            // 3段攻撃
            if(this.state === 'punch') {
                const step = this.comboCount % 3;
                if(step === 0) nextAnim = 'punch1';
                else if(step === 1) nextAnim = 'punch2';
                else nextAnim = 'punch3';
            }
            if(this.state === 'boss_punch') nextAnim = 'boss_punch';
            if(this.state === 'kick') {
                const step = this.comboCount % 3;
                if(step === 0) nextAnim = 'kick';
                else if(step === 1) nextAnim = 'kick2';
                else nextAnim = 'kick3';
            }
            if(this.state === 'mega') nextAnim = 'mega';
            if(this.state === 'throw') nextAnim = 'throw';
            if(this.state === 'hurt') nextAnim = 'hurt';
            if(this.state === 'down') nextAnim = 'down';
            if(this.state === 'wakeup') nextAnim = 'wakeup';

            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 {
                this.root.position.y = 9.0; 
            }

            this.playAnim(nextAnim);

            if(this.stateTimer > 0) {
                this.stateTimer--;
                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; }
            const data = ANIM_DATA[name] || ANIM_DATA['idle'];
            this.animTime += data.speed;
            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);
            }
        }

        takeDamage(dmg, isHeavy) {
            if(this.invincible > 0 || this.state === 'dead' || this.state === 'down' || this.state === 'wakeup') return;
            this.comboCount = 0;
            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 (isHeavy) { 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});
            }
            this.body = new THREE.Mesh(geo, mat); this.body.castShadow = true; this.mesh.add(this.body);
            State.items.push(this);
        }

        update(player) {
            if(this.holder) return;
            if(this.isProjectile) {
                this.mesh.position.x += this.vx; this.mesh.position.y += this.vy; this.vy -= CFG.GRAVITY;
                this.mesh.rotation.z += 0.5;
                State.enemies.forEach(e=>{
                    if(e.state!=='dead' && this.mesh.position.distanceTo(e.mesh.position)<10) { e.takeDamage(100, true); AudioSys.hit(); this.remove(); }
                });
                if(this.mesh.position.y<0) this.remove();
                return;
            }
            if(player && this.type!=='crate' && !this.isProjectile) {
                if(player.mesh.position.distanceTo(this.mesh.position) < 10) {
                    if(this.type==='score') { State.score+=100; AudioSys.score(); this.remove(); }
                    else if(this.type==='health') { player.hp=Math.min(100, player.hp+100); AudioSys.heal(); this.remove(); }
                    else if(this.type==='weapon' && !player.heldItem) {
                        player.heldItem = this; this.holder = player;
                        scene.remove(this.mesh); player.handR.add(this.mesh);
                        this.mesh.position.set(0,0,0); this.mesh.rotation.set(Math.PI/2,0,0);
                        AudioSys.pickup();
                    }
                }
            }
        }

        hit(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 ? 25 : 15;
        if (Math.abs(dx) < 150) { 
            if (Math.abs(dx) < atkRange && Math.abs(dz) < 10) {
                e.vx = 0; e.vz = 0;
                if (Math.random() < 0.05) { 
                    if (e.isBoss) {
                        if (Math.random() < 0.6) { e.state = 'boss_punch'; e.stateTimer = 20; AudioSys.punch(); setTimeout(() => checkHit(e, 15, 10, false), 100); } 
                        else { e.state = 'kick'; e.stateTimer = 20; AudioSys.kick(); setTimeout(() => checkHit(e, 15, 10, false), 150); }
                    } else if (e.heldItem) { e.state = 'throw'; e.stateTimer = 20; AudioSys.heavyHit(); checkHit(e, 15, 20, true); } 
                    else {
                        const r = Math.random();
                        if (r < 0.6) { e.state = 'punch'; e.stateTimer = 15; AudioSys.punch(); setTimeout(() => checkHit(e, 10, 5, false), 100); } 
                        else if (r < 0.9) { e.state = 'kick'; e.stateTimer = 20; AudioSys.kick(); setTimeout(() => checkHit(e, 12, 10, false), 150); } 
                        else { e.state = 'mega'; e.vy = 2; e.vx = e.facing * 8; e.stateTimer = 30; AudioSys.heavyHit(); checkHit(e, 20, 15, true); }
                    }
                } else { e.state = 'idle'; }
            } else {
                if (Math.random() < 0.01 && Math.abs(dx) > 40) { e.state = 'mega'; e.vy = 2; e.vx = Math.sign(dx) * 8; e.facing = Math.sign(dx); e.stateTimer = 30; AudioSys.heavyHit(); checkHit(e, 20, 15, true); } 
                else {
                    e.state = 'walk'; e.vx = Math.sign(dx) * (e.isBoss ? 0.7 : 0.5); e.facing = Math.sign(dx); e.vz = Math.sign(dz) * 0.3; 
                    if (e.collidedItem && e.collidedItem.type === 'crate' && e.grounded) { e.vy = 6; e.state = 'jump'; }
                }
            }
        } else { e.state = 'idle'; e.vx = 0; e.vz = 0; }
    }

    function checkHit(atk, range, dmg, isHeavy) {
        let finalDmg = dmg;
        let finalHeavy = isHeavy;
        if ((atk.state === 'punch' || atk.state === 'kick') && (atk.comboCount % 3 === 2)) {
            finalDmg *= 2; finalHeavy = true;
        }

        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;
            if(Math.sign(dx)===atk.facing && Math.abs(dx)<range && Math.abs(t.z-atk.z)<6) {
                const isParry = (t.state === atk.state) && (atk.state === 'punch' || atk.state === '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.takeDamage(finalDmg, finalHeavy);
            }
            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)<6) {
                const isParry = (t.state === atk.state) && (atk.state === 'punch' || atk.state === '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.takeDamage(finalDmg, finalHeavy);
            }
        });
        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;
        
        // Player Reset Logic
        if (State.player) { scene.remove(State.player.mesh); State.player = null; }
        State.player = new Character(0, 0, true);
        State.player.hp = 100;
        
        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) { State.enemies.push(new Character(ex, ez, false)); State.spawnedCount++; }
        }
    }

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

    const keys={};
    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);
                    if(dist > 10 + (pl.dashTarget.size||0)/2) { 
                        pl.vx = Math.sign(dx) * 2.5; pl.vz = Math.sign(dz) * 1.5; pl.state = 'dash';
                        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;
                        pl.state = pl.dashAction; pl.stateTimer = 20;
                        if(pl.dashAction==='punch') { AudioSys.punch(); pl.comboCount++; pl.lastAttackTime = Date.now(); setTimeout(()=>checkHit(pl, pl.heldItem?15:10, 10, false), 100); }
                        else if(pl.dashAction==='throw') { AudioSys.heavyHit(); checkHit(pl, 12, 20, true); }
                        else if(pl.dashAction==='mega') { pl.vy = 2; pl.vx = pl.facing * 8.0; if(State.view !== 'SIDE') pl.vz = (keys.ArrowUp ? -1 : (keys.ArrowDown ? 1 : 0)) * 6.0; pl.stateTimer = 30; AudioSys.heavyHit(); checkHit(pl, 25, 30, true); }
                        else { AudioSys.kick(); pl.comboCount++; pl.lastAttackTime = Date.now(); setTimeout(()=>checkHit(pl, 12, 10, false), 150); }
                    }
                }
            } 
            else if(!pl.isAttacking && pl.state !== 'mega' && pl.state !== 'down' && pl.state !== 'wakeup') { 
                let mx=0, mz=0; const spd=1.2;
                if(State.view==='SIDE') { if(keys.ArrowRight) { mx=spd; pl.facing=1; } if(keys.ArrowLeft) { mx=-spd; pl.facing=-1; } if(keys.ArrowUp) mz=-spd; if(keys.ArrowDown) mz=spd; } 
                else if(State.view==='FPS') { if(keys.ArrowUp) mx = spd * pl.facing; if(keys.ArrowLeft) mz = -spd * pl.facing; if(keys.ArrowRight) mz = spd * pl.facing; } 
                else { if(keys.ArrowUp) { mx=spd; pl.facing=1; } if(keys.ArrowDown) { mx=-spd; pl.facing=-1; } if(keys.ArrowRight) mz=spd; if(keys.ArrowLeft) mz=-spd; }

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

                const startAttack = (type) => {
                    let target=null, minDist=60;
                    State.items.filter(i=>i.type==='crate').forEach(i => { if(pl.mesh.position.distanceTo(i.mesh.position) < 15 + i.size/2) { target=i; minDist=0; } });
                    if(!target) { State.enemies.forEach(e => { if(e.state==='dead') return; const d = pl.mesh.position.distanceTo(e.mesh.position); const dx = e.x - pl.x; if(Math.sign(dx)===pl.facing && d < minDist) { minDist=d; target=e; } }); }
                    if(!target) { State.items.filter(i=>i.type==='crate').forEach(i => { const d = pl.mesh.position.distanceTo(i.mesh.position); const dx = i.mesh.position.x - pl.x; if(Math.sign(dx)===pl.facing && d < minDist) { minDist=d; target=i; } }); }

                    const hitRange = 10 + (target ? (target.size||0)/2 : 0);
                    if(target && minDist > hitRange) { pl.dashTarget = target; pl.dashAction = type; } 
                    else {
                        pl.state=type; pl.stateTimer=20;
                        if(type==='punch') { AudioSys.punch(); pl.comboCount++; pl.lastAttackTime = Date.now(); setTimeout(()=>checkHit(pl, pl.heldItem?15:10, 10, false), 100); }
                        else if(type==='throw') { AudioSys.heavyHit(); checkHit(pl, 12, 20, true); }
                        else if(type==='mega') { pl.vy = 2; pl.vx = pl.facing * 8.0; if(State.view !== 'SIDE') pl.vz = (keys.ArrowUp ? -1 : (keys.ArrowDown ? 1 : 0)) * 6.0; pl.stateTimer = 30; AudioSys.heavyHit(); checkHit(pl, 25, 30, true); }
                        else { AudioSys.kick(); pl.comboCount++; pl.lastAttackTime = Date.now(); setTimeout(()=>checkHit(pl, 12, 10, false), 150); }
                    }
                };

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

        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+'%';
            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