見出し画像

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

【更新履歴】
・2026/1/8 バージョン1.0公開
・2026/1/8 バージョン1.1公開
・2026/1/9 他の作品と重複していたタイトルを変更

画像
殴っている様子

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

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


<!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 Layer */
    #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 Container */
    #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;
    }

    /* Stats (Left) */
    .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 (Center-Left) */
    .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 (Right Edge) */
    .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 | Kick: Z | 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://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>

<script>
window.onload = function() {
    // --- 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 ---
    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
    };

    // --- Three.js ---
    const canvas = document.getElementById('gameCanvas');
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(50, window.innerWidth/window.innerHeight, 0.1, 1000);
    const renderer = new THREE.WebGLRenderer({canvas, antialias: true});
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.shadowMap.enabled = true;

    // Lights
    const amb = new THREE.AmbientLight(0xffffff, 0.6);
    scene.add(amb);
    const sun = new THREE.DirectionalLight(0xffffff, 0.8);
    sun.position.set(50, 100, 50);
    sun.castShadow = true;
    sun.shadow.mapSize.set(2048, 2048);
    scene.add(sun);

    // Lock-on Cursor
    const cursorGeo = new THREE.ConeGeometry(2, 4, 4);
    const cursorMat = new THREE.MeshBasicMaterial({color: 0xffff00, wireframe: false});
    const lockOnCursor = new THREE.Mesh(cursorGeo, cursorMat);
    lockOnCursor.rotation.x = Math.PI; 
    lockOnCursor.visible = false;
    scene.add(lockOnCursor);

    // Stage
    const stageObj = new THREE.Group();
    scene.add(stageObj);

    function resetStageEnv() {
        const h = Math.random();
        State.colors.bg = new THREE.Color().setHSL(h, 0.3, 0.1);
        State.colors.road = new THREE.Color().setHSL((h+0.5)%1, 0.1, 0.2);
        scene.background = State.colors.bg;
        scene.fog = new THREE.Fog(State.colors.bg, 100, 600);
        while(stageObj.children.length) stageObj.remove(stageObj.children[0]);
        const road = new THREE.Mesh(new THREE.BoxGeometry(10000, 50, 100), new THREE.MeshLambertMaterial({color: State.colors.road}));
        road.position.y = -25; road.receiveShadow = true;
        stageObj.add(road);
        const water = new THREE.Mesh(new THREE.PlaneGeometry(10000, 1000), new THREE.MeshBasicMaterial({color: 0x001133}));
        water.rotation.x = -Math.PI/2; water.position.y = -30;
        stageObj.add(water);
    }

    // --- Helpers ---
    function createLimb(w, h, d, col, parent, x, y, z) {
        const grp = new THREE.Group(); grp.position.set(x, y, z);
        const m = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), new THREE.MeshLambertMaterial({color: col}));
        m.position.y = -h/2; m.castShadow = true; grp.add(m);
        if(parent) parent.add(grp);
        return grp;
    }
    function randBrightColor() { return new THREE.Color().setHSL(Math.random(), 0.8, 0.5); }

    // --- Physics & Collision ---
    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; }
                }
            }
        }
        if (char.z + moveZ < -CFG.Z_LIMIT || char.z + moveZ > CFG.Z_LIMIT) moveZ = 0;
        return { x: moveX, z: moveZ, ground: onGroundY, hit: hitItem };
    }

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

    // --- Character ---
    class Character {
        constructor(x, z, isPlayer, isBoss = false) {
            this.x=x; this.y=isPlayer?0:50; this.z=z;
            this.vx=0; this.vy=0; this.vz=0;
            this.isPlayer=isPlayer; this.isBoss=isBoss;
            this.maxHp = isPlayer ? 100 : (isBoss ? 90 : 30);
            this.hp = this.maxHp;
            this.facing=1; 
            this.state='idle'; this.stateTimer=0;
            this.dashTarget=null; this.dashAction=null;
            this.collidedItem = null;
            this.deadTimer=0;
            this.grounded=false; this.invincible=0;
            this.heldItem=null;

            this.mesh = new THREE.Group();
            scene.add(this.mesh);
            this.buildBody(isPlayer ? 0x3366cc : (isBoss ? 0x660000 : 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;
            this.root = new THREE.Group(); this.root.position.y=10; this.mesh.add(this.root);
            const pantsCol = this.isPlayer ? 0x222222 : randBrightColor();
            this.root.add(new THREE.Mesh(new THREE.BoxGeometry(3.5,3,2.5), new THREE.MeshLambertMaterial({color: pantsCol})));
            this.spine = new THREE.Group(); this.spine.position.y=1.5; this.root.add(this.spine);
            const chest = new THREE.Mesh(new THREE.BoxGeometry(4.5,6,3), new THREE.MeshLambertMaterial({color: shirtCol}));
            chest.position.y=3; chest.castShadow=true; this.spine.add(chest);
            this.headGroup = new THREE.Group(); this.headGroup.position.y=6.5; this.spine.add(this.headGroup);
            this.headMesh = new THREE.Mesh(new THREE.BoxGeometry(3,3.5,3), new THREE.MeshLambertMaterial({color: skin}));
            this.headMesh.position.y=1.75; this.headGroup.add(this.headMesh);
            const 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.3,4.5,1.3, shirtCol, this.spine, 2.8, 5.5, 0);
            this.foreL = createLimb(1.1,4.5,1.1, skin, this.armL, 0, -4.5, 0);
            this.armR = createLimb(1.3,4.5,1.3, shirtCol, this.spine, -2.8, 5.5, 0);
            this.foreR = createLimb(1.1,4.5,1.1, skin, this.armR, 0, -4.5, 0);
            this.handR = new THREE.Group(); this.handR.position.y=-4.5; this.foreR.add(this.handR);
            const bootsCol = this.isPlayer ? 0x111111 : randBrightColor();
            this.thighL = createLimb(1.6,5.5,1.6, pantsCol, this.root, 1.2, -1.5, 0);
            this.shinL = createLimb(1.4,5.5,1.4, bootsCol, this.thighL, 0, -5.5, 0);
            this.thighR = createLimb(1.6,5.5,1.6, pantsCol, this.root, -1.2, -1.5, 0);
            this.shinR = createLimb(1.4,5.5,1.4, bootsCol, this.thighR, 0, -5.5, 0);
            this.limbs = [this.armL, this.armR, this.foreL, this.foreR, this.thighL, this.thighR, this.shinL, this.shinR, this.spine, this.headGroup];
        }

        setVisibleParts(mode) {
            if(mode==='FPS') {
                this.headMesh.visible = false;
                this.headGroup.children.forEach(c => { if(c!==this.headMesh && c.geometry.type==='BoxGeometry') c.visible=false; }); 
            } else {
                this.headMesh.visible = true;
                this.headGroup.children.forEach(c => c.visible=true);
            }
        }

        update() {
            if(this.state==='dead') {
                this.deadTimer++;
                this.mesh.visible = Math.floor(this.deadTimer/4)%2===0;
                this.vy -= CFG.GRAVITY; this.x+=this.vx; this.y+=this.vy;
                if(this.y<=CFG.GROUND) this.y=CFG.GROUND;
                this.mesh.position.set(this.x,this.y,this.z);
                if(this.deadTimer > 40) 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;
            }

            this.mesh.position.set(this.x, this.y, this.z);
            this.mesh.rotation.y = this.facing === 1 ? Math.PI/2 : -Math.PI/2;

            this.animate();
            if(this.stateTimer>0) { this.stateTimer--; if(this.stateTimer<=0) this.state='idle'; }
            if(this.invincible>0) { this.invincible--; this.mesh.visible = (Math.floor(this.invincible/4)%2===0); }
            else this.mesh.visible = true;
        }

        animate() {
            const t = Date.now()*0.01;
            this.limbs.forEach(o=>o.rotation.set(0,0,0));
            this.root.position.set(0,10,0); this.root.rotation.x=0;

            const s = this.state;
            if(s==='idle') {
                this.spine.rotation.x = Math.sin(t*0.2)*0.05;
                this.armL.rotation.z=0.1; this.armR.rotation.z=-0.1;
                this.armL.rotation.x=Math.sin(t*0.5)*0.1; this.armR.rotation.x=Math.sin(t*0.5+1)*0.1;
            } else if(s==='walk') {
                const w = Math.sin(t*1.5);
                this.thighL.rotation.x=w*0.8; this.shinL.rotation.x=w>0?0.8:0.1;
                this.thighR.rotation.x=-w*0.8; this.shinR.rotation.x=w<0?0.8:0.1;
                this.armL.rotation.x=-w*0.8; this.foreL.rotation.x=-0.5;
                this.armR.rotation.x=w*0.8; this.foreR.rotation.x=-0.5;
            } else if(s==='dash') { 
                this.root.rotation.x = 0.5; 
                const w = Math.sin(t*2.0);
                this.thighL.rotation.x=w*1.2; this.thighR.rotation.x=-w*1.2;
                this.armL.rotation.x=-1.5; this.armR.rotation.x=-1.5;
            } else if(s==='punch') {
                this.spine.rotation.y = -0.8;
                this.root.position.z = 2.0; 
                if(this.heldItem) {
                    this.armR.rotation.x = -2.5; this.foreR.rotation.x = 0;
                } else {
                    this.armR.rotation.x = -1.5; this.armR.rotation.z = -0.5;
                    this.foreR.rotation.x = 0;
                }
                this.armL.rotation.x=-1.0; this.foreL.rotation.x=-2.0;
            } else if(s==='kick') {
                this.root.rotation.x = -0.5;
                this.thighR.rotation.x = -1.8; this.shinR.rotation.x = 0.2;
                this.thighL.rotation.x = 0.5; this.shinL.rotation.x = 1.0;
                this.armL.rotation.x = 1.5;
            } else if(s==='jump') {
                this.root.position.y = 13;
                this.thighL.rotation.x=-0.5; this.shinL.rotation.x=1.5;
                this.thighR.rotation.x=0.5; this.shinR.rotation.x=1.0;
                this.armL.rotation.z=2.5; this.armR.rotation.z=-2.5;
            } else if(s==='hurt') {
                this.spine.rotation.x = -0.5;
                this.armL.rotation.x=-2.5; this.armR.rotation.x=-2.5;
                this.root.position.z = -1.5;
            }
        }

        takeDamage(dmg, knockback=3) {
            if(this.invincible>0 || this.state==='dead') return;
            this.hp-=dmg; this.state='hurt'; this.stateTimer=12; this.invincible=20;
            AudioSys.hit(); createBlood(this.mesh.position);
            this.vx = -this.facing * knockback;
            this.dashTarget = null; 

            if(!this.isPlayer) {
                State.activeEnemy = this; State.activeEnemyTimer = 60;
            }

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

            if(this.hp<=0) {
                this.state='dead';
                this.mesh.rotation.x = -Math.PI/2; this.mesh.position.y = 2;
                this.vx = -this.facing * 8; this.vy = 10; 
                if(!this.isPlayer) {
                    State.score+=this.isBoss?1000:10; 
                    State.kills++; 
                    if(this.isBoss) { finishStage(); }
                } else {
                    showMsg("GAME OVER", "PRESS 'A' TO RETRY");
                    State.mode='GAME_OVER';
                }
            }
        }

        dropWeapon(vanish=false) {
            if(!this.heldItem) return;
            const it = this.heldItem;
            it.holder = null; this.heldItem = null;
            if(vanish) it.remove();
            else {
                scene.add(it.mesh); it.mesh.position.set(this.x, 10, this.z);
                it.vx = this.facing * 10; it.vy = 2; it.isProjectile = true;
            }
        }

        remove() {
            scene.remove(this.mesh);
            if(this === State.player) return;
            State.enemies = State.enemies.filter(e => e!==this);
        }
    }

    // --- Items ---
    class GameItem {
        constructor(x, z, type) {
            this.type = type;
            this.hp = type==='crate' ? 5 : 999;
            this.holder = null;
            this.mesh = new THREE.Group();
            this.mesh.position.set(x, type==='crate'?5:2, z);
            scene.add(this.mesh);

            let geo, mat;
            if(type==='crate') {
                const s = 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, 5); AudioSys.hit(); this.remove();
                    }
                });
                if(this.mesh.position.y<0) this.remove();
                return;
            }

            if(player && this.type!=='crate' && !this.isProjectile) {
                if(player.mesh.position.distanceTo(this.mesh.position) < 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() {
            if(this.type!=='crate') return;
            this.hp--; 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);
        }
    }

    // --- Effects ---
    function createBlood(pos) {
        const m = new THREE.Mesh(new THREE.BoxGeometry(0.8,0.8,0.8), new THREE.MeshBasicMaterial({color: 0xff0000}));
        m.position.copy(pos); m.vx = (Math.random()-0.5)*3; m.vy = Math.random()*3+2; m.vz = (Math.random()-0.5)*3;
        scene.add(m); State.particles.push(m);
    }
    function createSpark(pos) { 
        const m = new THREE.Mesh(new THREE.BoxGeometry(1,1,1), new THREE.MeshBasicMaterial({color: 0xffff00}));
        m.position.copy(pos); m.vx = (Math.random()-0.5)*4; m.vy = Math.random()*4+2; m.vz = (Math.random()-0.5)*4;
        scene.add(m); State.particles.push(m);
    }
    function createDebris(pos, col) {
        const m = new THREE.Mesh(new THREE.BoxGeometry(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 = 'PLAY'; State.kills = 0; State.stageX = 0; 
        State.spawnedCount = 0; State.bossSpawned = false;
        State.player.x = 0; State.player.z = 0;
        State.player.hp = Math.min(100, State.player.hp+20);
        
        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'); spawnWave(); }, 2000);
    }

    function finishStage() {
        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={};
    const prevKeys={}; // For tracking key up events logic if needed
    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';
        
        // F1 Toggle
        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);
        const pl = State.player;

        if(State.mode==='PLAY' && pl && pl.state!=='dead') {
            
            // --- Dash Logic ---
            if(pl.dashTarget) {
                // FIXED: Safety stop if dashing into ANY crate
                if(pl.collidedItem && pl.collidedItem.type==='crate' && pl.collidedItem !== pl.dashTarget) {
                    pl.vx = -pl.vx * 0.5; // Bounce back
                    pl.vz = -pl.vz * 0.5;
                    pl.dashTarget = null; // Cancel
                } 
                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';
                        
                        // Jump Interrupt
                        if(keys.KeyX && pl.grounded) {
                            pl.vy=5; pl.state='jump'; AudioSys.jump(); 
                            pl.dashTarget = null; // Cancel dash on jump
                        }
                    } else {
                        pl.vx = 0; pl.vz = 0; pl.dashTarget = null;
                        pl.state = pl.dashAction; pl.stateTimer = 20;
                        if(pl.dashAction==='punch') { AudioSys.punch(); setTimeout(()=>checkHit(pl, pl.heldItem?15:10, 10), 100); }
                        else if(pl.dashAction==='throw') { AudioSys.heavyHit(); checkHit(pl, 12, 20, true); }
                        else if(pl.dashAction==='mega') { AudioSys.heavyHit(); checkHit(pl, 20, 30, true); }
                        else { AudioSys.kick(); setTimeout(()=>checkHit(pl, 12, 10), 150); }
                    }
                }
            } 
            else if(!pl.isAttacking) { 
                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;
                    // Down is handled in keydown event for toggle
                    if(keys.ArrowLeft) mz = -spd * pl.facing;
                    if(keys.ArrowRight) mz = 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';

                const startAttack = (type) => {
                    let target=null, minDist=60;
                    // Priority 1: Contact Range Crate
                    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;
                        }
                    });
                    // Priority 2: Enemies
                    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; }
                        });
                    }
                    // Priority 3: Far Crates
                    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(); setTimeout(()=>checkHit(pl, pl.heldItem?15:10, 10), 100); }
                        else if(type==='throw') { AudioSys.heavyHit(); checkHit(pl, 12, 20, true); }
                        else if(type==='mega') { AudioSys.heavyHit(); checkHit(pl, 20, 30, true); }
                        else { AudioSys.kick(); setTimeout(()=>checkHit(pl, 12, 10), 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 Cursor
        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;
        }

        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;
            
            // Boss Check
            const maxEnemies = 20 + (State.stage - 1) * 10;
            document.getElementById('kill-target').innerText = maxEnemies;

            if(State.enemies.length === 0) {
                if (State.spawnedCount < maxEnemies) {
                    spawnWave();
                } else if (!State.bossSpawned) {
                    spawnBoss();
                }
            }
            
            // Camera
            if(State.view==='SIDE') {
                camera.position.set(pl.x, 25, 120);
                camera.lookAt(pl.x, 10, 0);
            } else if(State.view==='TPS') {
                camera.position.set(pl.x - 50, 40, pl.z);
                camera.lookAt(pl.x + 100, 10, pl.z);
            } else { // FPS
                camera.position.set(pl.x + pl.facing*2, 17, pl.z);
                camera.lookAt(pl.x + pl.facing*100, 15, pl.z);
            }
        }
        
        State.enemies.forEach(e => {
            if(e.state!=='dead' && State.mode==='PLAY') {
                const dx = pl.x - e.x;
                if(Math.abs(dx)<60) {
                    if(e.state!=='punch' && e.state!=='kick') {
                        if(Math.abs(dx)>8) { 
                            e.state='walk'; e.vx=Math.sign(dx)*0.4; e.facing=Math.sign(dx); 
                            e.vz = (pl.z - e.z) * 0.05;
                        } else if(Math.random()<0.02) { 
                            e.vx=0; e.state='punch'; e.stateTimer=15; AudioSys.punch();
                            setTimeout(() => checkHit(e, 10, 5), 150);
                        } else { e.vx=0; e.state='idle'; }
                    } else { e.vx=0; e.vz=0; }
                }
            }
            e.update();
        });

        State.items.forEach(i => i.update(State.mode==='PLAY'?pl:null));

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

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

        renderer.render(scene, camera);
    }

    function checkHit(atk, range, dmg, blowAway=false) {
        const targets = atk.isPlayer ? State.enemies : [State.player];
        targets.forEach(t => {
            const dx = t.x - atk.x;
            if(Math.sign(dx)===atk.facing && Math.abs(dx)<range && Math.abs(t.z-atk.z)<6) {
                t.takeDamage(dmg, blowAway ? 15 : 3);
            }
        });
        if(atk.isPlayer) {
            State.items.forEach(i => {
                const dx = i.mesh.position.x - atk.x;
                // Add Crate Radius to range
                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) {
                    i.hit();
                }
            });
        }
    }

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


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

コメント

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