簡単な対戦ゲーム「MagicalMaze」



【更新履歴】

 ・2026/5/18 バージョン1.0公開。
 ・2026/5/18 バージョン1.1公開。
 ・2026/5/18 バージョン1.2公開。
 ・2026/5/18 バージョン1.3公開。(マルチプレイモード追加)
 ・2026/5/20 バージョン1.4公開。(マップチップを刷新し拡大)


画像

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

【操作説明】


・方向キーで移動する。
・Aキーで取得した魔法で攻撃。
・制限時間内に、より多くのスコアを得たほうが勝ち。
・あるいは、相手のHPを、先にゼロにしたほうが勝ち。
・魔法が当たった障害物が同じ属性なら破壊される。
・同じ属性の魔法を続けて取得すると、攻撃範囲が広くなる。(最大3回)
・相手の攻撃が当たっても、同じ魔法を持っていたらノーダメージ。
・または、同じ属性の障害物の跡に乗っている時は、
 1/2の確率でノーダメージ。

【マルチプレイ時の操作】

・プレイヤー2は、テンキーで移動。スペースキーで魔法で攻撃。

・このゲームで遊ぶには、いつものテスト用ブラウザが必要です。↓

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

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Magical Maze 1.4</title>
    <style>
        body {
            margin: 0;
            padding: 0;
            background-color: #000;
            color: #fff;
            overflow: hidden;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        }
        #gameContainer {
            position: absolute;
            top: 0; left: 0;
            width: 100vw;
            height: 100vh;
        }
        canvas {
            display: block;
            width: 100%;
            height: 100%;
            background-color: #222;
            outline: none;
        }
    </style>
</head>
<body>
    <div id="gameContainer">
        <canvas id="gameCanvas" tabindex="1"></canvas>
    </div>
    <script>
        /**
         * Magical Maze - Main Game Logic (Integrated)
         */

        const canvas = document.getElementById('gameCanvas');
        const ctx = canvas.getContext('2d');
        canvas.focus();

        // --- Game Constants ---
        let TS = 40; 
        const COLS = 15; // マップサイズを半分に
        const ROWS = 10;
        const TIME_LIMIT = 180;
        const MAX_STAGES = 8;
        const UI_HEIGHT = 160; 

        function resizeCanvas() {
            canvas.width = window.innerWidth;
            canvas.height = window.innerHeight;
            // UI領域を除いた画面にマップ全体が収まるようにTSを動的計算(COLS/ROWSが半分になったのでTSは2倍になる)
            TS = Math.floor(Math.min(canvas.width / COLS, (canvas.height - UI_HEIGHT) / ROWS));
            if (TS < 10) TS = 10;
        }
        window.addEventListener('resize', resizeCanvas);
        resizeCanvas();

        // --- Asset Management ---
        const ASSETS = { images: {}, audio: {} };
        const AUDIO_CTX = new (window.AudioContext || window.webkitAudioContext)();
        let currentBgmAudio = null;

        function loadImg(folder, name) {
            const path = `${folder}/${name}.png`;
            if (!ASSETS.images[path]) {
                const img = new Image();
                img.src = path;
                ASSETS.images[path] = { img, loaded: false, error: false };
                img.onload = () => ASSETS.images[path].loaded = true;
                img.onerror = () => ASSETS.images[path].error = true;
            }
        }

        function playBGM(folder, name) {
            if (currentBgmAudio) {
                currentBgmAudio.pause();
                currentBgmAudio = null;
            }
            const path = `${folder}/${name}.mp3`;
            if (!ASSETS.audio[path]) {
                const audio = new Audio(path);
                audio.loop = true;
                audio.volume = 0.4;
                ASSETS.audio[path] = { audio: audio, loaded: false, error: false };
                audio.oncanplaythrough = () => { ASSETS.audio[path].loaded = true; };
                audio.onerror = () => { ASSETS.audio[path].error = true; };
            }
            if (!ASSETS.audio[path].error) {
                currentBgmAudio = ASSETS.audio[path].audio;
                currentBgmAudio.currentTime = 0;
                currentBgmAudio.play().catch(() => {});
            }
        }

        function playSound(folder, name, type = 'beep') {
            const path = `${folder}/${name}.mp3`;
            if (ASSETS.audio[path] && ASSETS.audio[path].loaded && !ASSETS.audio[path].error) {
                const clone = ASSETS.audio[path].audio.cloneNode();
                clone.volume = 0.6;
                clone.play().catch(() => playFallbackSound(type));
                return;
            }
            if (!ASSETS.audio[path]) {
                const audio = new Audio(path);
                ASSETS.audio[path] = { audio: audio, loaded: false, error: false };
                audio.oncanplaythrough = () => { ASSETS.audio[path].loaded = true; };
                audio.onerror = () => {
                    ASSETS.audio[path].error = true;
                    playFallbackSound(type);
                };
                audio.volume = 0.6;
                audio.play().catch(() => {
                    ASSETS.audio[path].error = true;
                    playFallbackSound(type);
                });
                return;
            }
            if (ASSETS.audio[path].error) {
                playFallbackSound(type);
            }
        }

        function playFallbackSound(type) {
            if (AUDIO_CTX.state === 'suspended') {
                AUDIO_CTX.resume().catch(() => {});
            }
            const gain = AUDIO_CTX.createGain();
            gain.connect(AUDIO_CTX.destination);

            if (type === 'break') {
                const bufferSize = Math.floor(AUDIO_CTX.sampleRate * 0.2);
                const buffer = AUDIO_CTX.createBuffer(1, bufferSize, AUDIO_CTX.sampleRate);
                const data = buffer.getChannelData(0);
                for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1;
                const noise = AUDIO_CTX.createBufferSource();
                noise.buffer = buffer;
                const filter = AUDIO_CTX.createBiquadFilter();
                filter.type = 'lowpass'; filter.frequency.value = 800;
                noise.connect(filter); filter.connect(gain);
                gain.gain.setValueAtTime(0.3, AUDIO_CTX.currentTime);
                gain.gain.exponentialRampToValueAtTime(0.01, AUDIO_CTX.currentTime + 0.2);
                noise.start();
                return;
            }

            const osc = AUDIO_CTX.createOscillator();
            osc.connect(gain);

            if (type === 'magic_cast') {
                osc.type = 'sine'; osc.frequency.setValueAtTime(400, AUDIO_CTX.currentTime);
                osc.frequency.exponentialRampToValueAtTime(800, AUDIO_CTX.currentTime + 0.1);
                gain.gain.setValueAtTime(0.1, AUDIO_CTX.currentTime);
            } else if (type === 'damage') {
                osc.type = 'sawtooth'; osc.frequency.setValueAtTime(150, AUDIO_CTX.currentTime);
                osc.frequency.exponentialRampToValueAtTime(50, AUDIO_CTX.currentTime + 0.2);
                gain.gain.setValueAtTime(0.1, AUDIO_CTX.currentTime);
            } else if (type === 'get_food' || type === 'heal' || type === 'speedup') {
                osc.type = 'square';
                let freq = type === 'heal' ? 1000 : (type === 'speedup' ? 1200 : 800);
                osc.frequency.setValueAtTime(freq, AUDIO_CTX.currentTime);
                osc.frequency.setValueAtTime(freq + 400, AUDIO_CTX.currentTime + 0.05);
                gain.gain.setValueAtTime(0.05, AUDIO_CTX.currentTime);
            } else if (type === 'move') {
                osc.type = 'triangle'; osc.frequency.setValueAtTime(300, AUDIO_CTX.currentTime);
                gain.gain.setValueAtTime(0.02, AUDIO_CTX.currentTime);
                osc.start(); osc.stop(AUDIO_CTX.currentTime + 0.05); return;
            } else if (type === 'dont_move') {
                osc.type = 'square'; osc.frequency.setValueAtTime(100, AUDIO_CTX.currentTime);
                gain.gain.setValueAtTime(0.05, AUDIO_CTX.currentTime);
                osc.start(); osc.stop(AUDIO_CTX.currentTime + 0.1); return;
            } else if (type === 'pageup') {
                osc.type = 'sine'; osc.frequency.setValueAtTime(880, AUDIO_CTX.currentTime);
                gain.gain.setValueAtTime(0.1, AUDIO_CTX.currentTime);
            } else {
                osc.type = 'triangle'; osc.frequency.setValueAtTime(440, AUDIO_CTX.currentTime);
                gain.gain.setValueAtTime(0.05, AUDIO_CTX.currentTime);
            }

            osc.start(); osc.stop(AUDIO_CTX.currentTime + 0.2);
        }

        // --- Pre-load Assets ---
        const dirs = ['left', 'right', 'up', 'down'];
        dirs.forEach(d => { loadImg('Player', d); loadImg('Enemy', d); });
        loadImg('Player', 'Player'); loadImg('Enemy', 'Enemy');
        loadImg('Road', 'default');
        
        ['title', 'stage_start', 'stage_clear', 'game_clear', 'game_over'].forEach(s => {
            loadImg('Screen', s);
        });

        const VALID_ITEM_PATHS = { food: [], heal: [], speed: [] };

        function loadItemImages() {
            const types = ['food', 'heal', 'speed'];
            types.forEach(type => {
                const folder = type === 'food' ? 'Food' : type === 'heal' ? 'Heal' : 'SpeedUp';
                const namesToCheck = [type, type.charAt(0).toUpperCase() + type.slice(1)]; 
                if (type === 'speed') { namesToCheck.push('speedup', 'SpeedUp'); }

                for (let i = 1; i <= 20; i++) {
                    namesToCheck.push(`${i}`);
                    namesToCheck.push(`${type}${i}`);
                    namesToCheck.push(`${type.charAt(0).toUpperCase() + type.slice(1)}${i}`);
                }

                namesToCheck.forEach(name => {
                    const path = `${folder}/${name}.png`;
                    if (!ASSETS.images[path]) {
                        const img = new Image();
                        img.src = path;
                        ASSETS.images[path] = { img, loaded: false, error: false };
                        img.onload = () => {
                            ASSETS.images[path].loaded = true;
                            if (!VALID_ITEM_PATHS[type].includes(path)) {
                                VALID_ITEM_PATHS[type].push(path);
                            }
                        };
                        img.onerror = () => { ASSETS.images[path].error = true; };
                    }
                });
            });
        }
        loadItemImages();

        // --- Input Handling ---
        const keys = {};
        document.addEventListener('keydown', e => {
            if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', ' '].includes(e.key)) {
                e.preventDefault();
            }
            keys[e.key] = true;
            if (e.key === 'Escape') togglePause();
            if (gameState === 'TITLE') {
                if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
                    mode = mode === 1 ? 2 : 1;
                    playSound('Sound', 'move', 'move');
                }
            }
        });

        document.addEventListener('keyup', e => {
            keys[e.key] = false;
            if (gameState !== 'PLAY' && gameState !== 'PAUSE') {
                if (gameState === 'TITLE' && (e.key === 'ArrowUp' || e.key === 'ArrowDown' || e.key.toLowerCase() === 'm')) {
                    return;
                }
                handleScreenTransition();
            }
        });

        canvas.addEventListener('mouseup', () => {
            if (gameState !== 'PLAY' && gameState !== 'PAUSE') {
                handleScreenTransition();
            }
        });

        // --- State Variables ---
        let gameState = 'STARTUP';
        let mode = 1;
        let stage = 1;
        let timeLeft = TIME_LIMIT;
        let lastTime = 0;
        let map = [];
        let items = [];
        let players = [];
        let particles = [];
        let meteors = [];
        let fallingItems = [];

        let p1Wins = 0;
        let p2Wins = 0;
        let lastStageWinner = 0;

        const MAGIC_TYPES = [
            { name: 'water', color: 'cyan' },
            { name: 'fire', color: 'red' },
            { name: 'earth', color: 'saddlebrown' },
            { name: 'ice', color: 'blue' },
            { name: 'wind', color: 'green' },
            { name: 'thunder', color: 'yellow' }
        ];
        const HURDLE_TYPES = [...MAGIC_TYPES];

        // --- Classes ---
        class FallingItem {
            constructor(x, y, type, data) {
                this.targetX = x; this.targetY = y;
                this.yPos = y - (ROWS / 2); // グリッドベース
                this.speed = 12; // grids per second
                this.type = type; this.data = data; this.hit = false;
            }
            update(dt) {
                if (this.hit) return;
                this.yPos += this.speed * dt;
                if (this.yPos >= this.targetY) {
                    this.yPos = this.targetY; this.hit = true;
                    items.push({ x: this.targetX, y: this.targetY, type: this.type, magicData: this.data, rndIdx: Math.floor(Math.random() * 10000) });
                }
            }
            draw(ctx) {
                if (this.hit) return;
                let px = this.targetX * TS + TS / 2;
                let py = this.yPos * TS + TS / 2;
                if (this.type === 'magic') {
                    const path = `Magic/${this.data.name}.png`;
                    if (ASSETS.images[path] && ASSETS.images[path].loaded) {
                        ctx.drawImage(ASSETS.images[path].img, px - TS/2, py - TS/2, TS, TS);
                    } else {
                        ctx.beginPath(); ctx.arc(px, py, TS/4, 0, Math.PI*2);
                        ctx.fillStyle = this.data.color; ctx.fill(); ctx.closePath();
                    }
                }
            }
        }

        class Particle {
            constructor(x, y, color) {
                this.x = x; this.y = y; this.color = color;
                this.baseTS = TS; 
                const angle = Math.random() * Math.PI * 2;
                const speed = Math.random() * (TS * 3) + TS; 
                this.vx = Math.cos(angle) * speed; this.vy = Math.sin(angle) * speed;
                this.life = 0.5 + Math.random() * 0.3; this.maxLife = this.life;
                this.size = Math.random() * (TS * 0.2) + (TS * 0.1); 
            }
            update(dt) {
                this.vy += (this.baseTS * 12) * dt; 
                this.x += this.vx * dt; this.y += this.vy * dt;
                this.life -= dt;
            }
            draw(ctx) {
                const alpha = Math.max(0, this.life / this.maxLife);
                ctx.globalAlpha = alpha; ctx.fillStyle = this.color;
                ctx.fillRect(this.x - this.size/2, this.y - this.size/2, this.size, this.size);
                ctx.globalAlpha = 1.0;
            }
        }

        class Meteor {
            constructor(ax, ay, magic, owner) {
                this.targetX = ax; this.targetY = ay;
                this.yPos = ay - (ROWS / 2);
                this.speed = 25 + Math.random() * 8; 
                this.magic = magic; this.owner = owner; this.hit = false;
            }
            update(dt) {
                if (this.hit) return;
                this.yPos += this.speed * dt;
                if (this.yPos >= this.targetY) {
                    this.yPos = this.targetY; this.hit = true;
                    handleMeteorHit(this);
                }
            }
            draw(ctx) {
                if (this.hit) return;
                let px = this.targetX * TS + TS / 2;
                let py = this.yPos * TS + TS / 2;
                const path = `Magic/${this.magic.name}.png`;
                if (ASSETS.images[path] && ASSETS.images[path].loaded) {
                    ctx.drawImage(ASSETS.images[path].img, px - TS/2, py - TS/2, TS, TS);
                } else {
                    ctx.beginPath(); ctx.arc(px, py, TS/4, 0, Math.PI*2);
                    ctx.fillStyle = this.magic.color; ctx.fill(); ctx.closePath();
                }
                ctx.beginPath(); ctx.moveTo(px, py - TS/4);
                ctx.lineTo(px, py - TS*1.5);
                ctx.strokeStyle = this.magic.color; ctx.lineWidth = 4;
                ctx.stroke(); ctx.closePath();
            }
        }

        class Entity {
            constructor(x, y, isPlayer1) {
                this.x = x; this.y = y; this.targetX = x; this.targetY = y;
                this.isPlayer1 = isPlayer1;
                this.hp = 100; this.maxHp = 100; this.score = 0;
                this.magic = null; this.magicLevel = 1; 
                this.attackArea = [];
                this.steps = 0; this.speedTimer = 0; this.hesitateTimer = 0;
                this.moving = false; this.moveTimer = 0; this.dir = 'down';
                this.isAiming = false; // CPU専用フラグ
            }
            update(dt) {
                if (this.speedTimer > 0) this.speedTimer -= dt;
                if (this.moving) {
                    this.moveTimer += dt;
                    let speedThreshold = this.speedTimer > 0 ? 0.1 : 0.2; 
                    if (this.moveTimer >= speedThreshold) {
                        this.x = this.targetX; this.y = this.targetY;
                        this.moving = false; this.moveTimer = 0; this.steps++;
                        checkItemPickup(this);
                    }
                }
            }
            move(dx, dy) {
                if (this.moving) return; 
                const nx = this.x + dx; const ny = this.y + dy;
                if (dx > 0) this.dir = 'right'; if (dx < 0) this.dir = 'left';
                if (dy > 0) this.dir = 'down'; if (dy < 0) this.dir = 'up';

                if (nx >= 0 && nx < COLS && ny >= 0 && ny < ROWS) {
                    const cell = map[ny][nx];
                    if (cell.type === 'road' || cell.type === 'start') {
                        const other = players.find(p => p !== this && p.targetX === nx && p.targetY === ny);
                        if (!other) {
                            this.targetX = nx; this.targetY = ny;
                            this.moving = true;
                            playSound('Sound', 'move', 'move');
                        } else playSound('Sound', 'dont_move', 'dont_move');
                    } else playSound('Sound', 'dont_move', 'dont_move');
                } else playSound('Sound', 'dont_move', 'dont_move');
            }
            draw(ctx) {
                let drawX = this.x * TS; let drawY = this.y * TS; let jumpOffset = 0;
                if (this.moving) {
                    let speedThreshold = this.speedTimer > 0 ? 0.1 : 0.2;
                    const progress = this.moveTimer / speedThreshold;
                    drawX += (this.targetX - this.x) * TS * progress;
                    drawY += (this.targetY - this.y) * TS * progress;
                    jumpOffset = Math.sin(progress * Math.PI) * (TS / 2.6);
                }
                const folder = this.isPlayer1 ? 'Player' : 'Enemy';
                const color = this.isPlayer1 ? (mode === 2 ? 'blue' : 'cyan') : (mode === 2 ? 'red' : 'purple');
                const dirPath = `${folder}/${this.dir}.png`; const defPath = `${folder}/${folder}.png`;
                
                let imgToDraw = null;
                if (ASSETS.images[dirPath] && ASSETS.images[dirPath].loaded) imgToDraw = ASSETS.images[dirPath].img;
                else if (ASSETS.images[defPath] && ASSETS.images[defPath].loaded) imgToDraw = ASSETS.images[defPath].img;

                drawY -= jumpOffset;
                if (imgToDraw) ctx.drawImage(imgToDraw, drawX, drawY, TS, TS);
                else {
                    ctx.beginPath(); ctx.arc(drawX + TS/2, drawY + TS/2, TS/2.2, 0, Math.PI*2);
                    ctx.fillStyle = color; ctx.fill(); ctx.closePath();
                }

                if (this.magic) {
                    ctx.beginPath(); ctx.arc(drawX + TS/2, drawY - TS/4, TS/5, 0, Math.PI*2);
                    ctx.fillStyle = this.magic.color; ctx.fill(); ctx.closePath();
                }
            }
        }

        // --- Map & Logic ---
        function initGame() {
            map = []; items = []; particles = []; meteors = []; fallingItems = [];
            players = [new Entity(0, 0, true), new Entity(COLS - 1, ROWS - 1, false)];
            timeLeft = TIME_LIMIT;

            for (let y = 0; y < ROWS; y++) {
                let row = [];
                for (let x = 0; x < COLS; x++) row.push({ type: 'road', name: 'normal', color: 'lightgreen' });
                map.push(row);
            }

            for (let y = 0; y < ROWS; y++) {
                for (let x = 0; x < COLS; x++) {
                    if (Math.random() < 0.25) {
                        const h = HURDLE_TYPES[Math.floor(Math.random() * HURDLE_TYPES.length)];
                        map[y][x] = { type: 'hurdle', name: h.name, color: h.color };
                        loadImg('Hurdle', h.name);
                    }
                }
            }

            map[0][0] = { type: 'start', name: 'normal', color: 'lightgreen' };
            map[0][1] = { type: 'road', name: 'normal', color: 'lightgreen' };
            map[1][0] = { type: 'road', name: 'normal', color: 'lightgreen' };
            map[ROWS-1][COLS-1] = { type: 'start', name: 'normal', color: 'lightgreen' };
            map[ROWS-1][COLS-2] = { type: 'road', name: 'normal', color: 'lightgreen' };
            map[ROWS-2][COLS-1] = { type: 'road', name: 'normal', color: 'lightgreen' };

            let cx = 0, cy = 0;
            while (cx < COLS - 1 || cy < ROWS - 1) {
                if (!(cx === 0 && cy === 0)) map[cy][cx] = { type: 'road', name: 'normal', color: 'lightgreen' };
                if (cx === COLS - 1) cy++; else if (cy === ROWS - 1) cx++;
                else Math.random() < 0.5 ? cx++ : cy++;
            }

            spawnItems();
        }

        function spawnItems() {
            items = [];
            // マップ縮小に伴い生成数を調整
            for(let i=0; i<4; i++) spawnRandomItem('food');
            for(let i=0; i<3; i++) spawnRandomItem('magic');
            for(let i=0; i<2; i++) spawnRandomItem('heal');
            for(let i=0; i<2; i++) spawnRandomItem('speed');
        }

        function spawnRandomItem(type) {
            let rx, ry, attempts = 0;
            do {
                rx = Math.floor(Math.random() * COLS); ry = Math.floor(Math.random() * ROWS);
                attempts++; if (attempts > 500) break;
            } while (map[ry][rx].type === 'hurdle' || items.some(i => i.x === rx && i.y === ry) || map[ry][rx].type === 'start');

            if (attempts > 500) return;

            if (['food', 'heal', 'speed'].includes(type)) {
                items.push({ x: rx, y: ry, type: type, rndIdx: Math.floor(Math.random() * 10000) });
            } else if (type === 'magic') {
                const m = MAGIC_TYPES[Math.floor(Math.random() * MAGIC_TYPES.length)];
                items.push({ x: rx, y: ry, type: 'magic', magicData: m, rndIdx: Math.floor(Math.random() * 10000) });
                loadImg('Magic', m.name);
            }
        }

        function spawnSpecificMagic(magicData) {
            if (items.some(i => i.type === 'magic' && i.magicData.name === magicData.name)) return;
            let rx, ry, attempts = 0;
            do {
                rx = Math.floor(Math.random() * COLS); ry = Math.floor(Math.random() * ROWS);
                attempts++; if (attempts > 500) break;
            } while (map[ry][rx].type === 'hurdle' || items.some(i => i.x === rx && i.y === ry) || map[ry][rx].type === 'start');
            if (attempts <= 500) {
                items.push({ x: rx, y: ry, type: 'magic', magicData: magicData, rndIdx: Math.floor(Math.random() * 10000) });
                loadImg('Magic', magicData.name);
            }
        }

        function checkItemPickup(player) {
            const idx = items.findIndex(i => i.x === player.x && i.y === player.y);
            if (idx !== -1) {
                const item = items[idx];
                if (item.type === 'food') {
                    player.score += 100 + (player.steps * 10); player.steps = 0;
                    playSound('Sound', 'get_food', 'get_food');
                    items.splice(idx, 1); spawnRandomItem('food');
                } else if (item.type === 'magic') {
                    if (player.magic && player.magic.name === item.magicData.name) {
                        player.magicLevel = Math.min(3, player.magicLevel + 1);
                    } else {
                        player.magic = item.magicData;
                        player.magicLevel = 1;
                    }
                    // 魔法入手時に常時レンジを展開する
                    player.attackArea = generateAttackArea(player.magicLevel);
                    playSound('Sound', 'magic_get', 'get_food');
                    items.splice(idx, 1); spawnSpecificMagic(item.magicData); spawnRandomItem('magic');
                } else if (item.type === 'heal') {
                    player.hp = Math.min(player.maxHp, player.hp + 30);
                    playSound('Sound', 'heal', 'heal');
                    items.splice(idx, 1); spawnRandomItem('heal');
                } else if (item.type === 'speed') {
                    player.speedTimer = 10; 
                    playSound('Sound', 'speedup', 'speedup');
                    items.splice(idx, 1); spawnRandomItem('speed');
                }
            }
        }

        function generateAttackArea(level) {
            const area = [];
            for (let dy = -level; dy <= level; dy++) {
                for (let dx = -level; dx <= level; dx++) {
                    if (Math.abs(dx) + Math.abs(dy) <= level && Math.random() < 0.7) area.push({ dx: dx, dy: dy });
                }
            }
            return area;
        }

        function executeMagic(player) {
            playSound('Sound', 'magic_cast', 'magic_cast'); 
            const magic = player.magic;
            player.attackArea.forEach(cell => {
                const ax = player.targetX + cell.dx; const ay = player.targetY + cell.dy;
                if (ax >= 0 && ax < COLS && ay >= 0 && ay < ROWS) meteors.push(new Meteor(ax, ay, magic, player));
            });
            player.magic = null; player.magicLevel = 1; player.attackArea = [];
        }

        function setStageClear() {
            if (stage >= MAX_STAGES) {
                gameState = 'GAME_CLEAR'; playBGM('ScreenBgm', 'game_clear');
            } else {
                gameState = 'STAGE_CLEAR'; playBGM('ScreenBgm', 'stage_clear');
            }
        }

        function setGameOver() {
            gameState = 'GAME_OVER'; playBGM('ScreenBgm', 'game_over');
        }

        function handleMeteorHit(m) {
            const ax = m.targetX; const ay = m.targetY;
            const magic = m.magic; const player = m.owner;

            for (let i = 0; i < 15; i++) particles.push(new Particle(ax * TS + TS/2, ay * TS + TS/2, magic.color));
            
            let mapCell = map[ay][ax];
            let hitSomething = false;
            
            if (mapCell.type === 'hurdle') {
                hitSomething = true;
                let isDestructible = (mapCell.name === 'water' && magic.name === 'ice') || mapCell.name === magic.name;
                if (isDestructible) {
                    map[ay][ax] = { type: 'road', name: magic.name, color: magic.color };
                    loadImg('Road', magic.name);
                    playSound('Sound', 'break', 'break');
                    for (let i = 0; i < 15; i++) particles.push(new Particle(ax * TS + TS/2, ay * TS + TS/2, mapCell.color));
                } else playSound('Sound', 'damage', 'damage'); 
            }
            
            players.forEach(target => {
                const tx1 = Math.round(target.x); const ty1 = Math.round(target.y);
                const tx2 = target.targetX; const ty2 = target.targetY;

                if (target !== player && ((tx1 === ax && ty1 === ay) || (tx2 === ax && ty2 === ay))) {
                    hitSomething = true;
                    let tCell1 = map[ty1][tx1]; let tCell2 = map[ty2][tx2];
                    let onSameAttribute = (tCell1.type === 'road' && tCell1.name === magic.name) || 
                                          (tCell2.type === 'road' && tCell2.name === magic.name);

                    if (onSameAttribute && Math.random() < 0.5) {
                        // Dodge
                    } else if (!target.magic || target.magic.name !== magic.name) {
                        target.hp -= 30;
                        playSound('Sound', 'damage', 'damage');
                        for (let i = 0; i < 15; i++) {
                            const pColor = target.isPlayer1 ? (mode === 2 ? 'blue' : 'cyan') : (mode === 2 ? 'red' : 'purple');
                            particles.push(new Particle(ax * TS + TS/2, ay * TS + TS/2, pColor));
                        }
                        
                        if (target.hp <= 0 && gameState === 'PLAY') {
                            if (mode === 1) {
                                if (target.isPlayer1) {
                                    setGameOver(); 
                                } else {
                                    lastStageWinner = 1; p1Wins++;
                                    setStageClear();
                                }
                            } else {
                                if (target.isPlayer1) {
                                    lastStageWinner = 2; p2Wins++;
                                } else {
                                    lastStageWinner = 1; p1Wins++;
                                }
                                setStageClear();
                            }
                        }
                    }
                }
            });

            if(!hitSomething) playSound('Sound', 'damage', 'damage');
        }

        function findPath(startX, startY, targetType, targetEntity) {
            let queue = [{x: startX, y: startY, path: []}];
            let visited = Array.from({length: ROWS}, () => Array(COLS).fill(false));
            visited[startY][startX] = true;

            while(queue.length > 0) {
                let curr = queue.shift();
                let found = false;
                if (['magic', 'food', 'heal', 'speed'].includes(targetType)) {
                    if (items.some(i => i.type === targetType && i.x === curr.x && i.y === curr.y)) found = true;
                } else if (targetType === 'player') {
                    if (curr.x === targetEntity.targetX && curr.y === targetEntity.targetY) found = true;
                }

                if (found) return curr.path.length > 0 ? curr.path[0] : null;

                const dirs = [{dx: 0, dy: -1}, {dx: 0, dy: 1}, {dx: -1, dy: 0}, {dx: 1, dy: 0}];
                for (let d of dirs) {
                    let nx = curr.x + d.dx; let ny = curr.y + d.dy;
                    if (nx >= 0 && nx < COLS && ny >= 0 && ny < ROWS) {
                        if (!visited[ny][nx] && (map[ny][nx].type === 'road' || map[ny][nx].type === 'start')) {
                            visited[ny][nx] = true;
                            queue.push({x: nx, y: ny, path: [...curr.path, d]});
                        }
                    }
                }
            }
            return null;
        }

        function checkTrapped(player) {
            if (player.moving) return;
            const dirs = [{dx: 0, dy: -1}, {dx: 0, dy: 1}, {dx: -1, dy: 0}, {dx: 1, dy: 0}];
            let trapped = true; let adjacentHurdles = [];
            
            for (let d of dirs) {
                let nx = player.targetX + d.dx; let ny = player.targetY + d.dy;
                if (nx >= 0 && nx < COLS && ny >= 0 && ny < ROWS) {
                    let cell = map[ny][nx];
                    if (cell.type !== 'hurdle') { trapped = false; break; } 
                    else adjacentHurdles.push(cell);
                }
            }

            if (trapped && adjacentHurdles.length > 0) {
                if (player.magic) {
                    let canBreak = adjacentHurdles.some(h => (h.name === 'water' && player.magic.name === 'ice') || h.name === player.magic.name);
                    if (canBreak) return; 
                }
                if (items.some(i => i.x === player.targetX && i.y === player.targetY && i.type === 'magic')) return;
                if (fallingItems.some(f => f.targetX === player.targetX && f.targetY === player.targetY)) return;

                let targetHurdle = adjacentHurdles[Math.floor(Math.random() * adjacentHurdles.length)];
                let requiredMagicName = targetHurdle.name === 'water' ? 'ice' : targetHurdle.name;
                let mData = MAGIC_TYPES.find(m => m.name === requiredMagicName);
                
                if (mData) {
                    fallingItems.push(new FallingItem(player.targetX, player.targetY, 'magic', mData));
                    loadImg('Magic', mData.name);
                }
            }
        }

        // --- Main Loop ---
        function update(dt) {
            if (gameState === 'STARTUP') {
                gameState = 'TITLE'; playBGM('ScreenBgm', 'title'); return;
            }
            if (gameState !== 'PLAY') return;

            timeLeft -= dt;
            if (timeLeft <= 0) {
                if (gameState === 'PLAY') {
                    if (mode === 1) {
                        if (players[0].score > players[1].score) { 
                            lastStageWinner = 1; p1Wins++; setStageClear(); 
                        } else { 
                            setGameOver(); 
                        }
                    } else {
                        if (players[0].score > players[1].score) { lastStageWinner = 1; p1Wins++; }
                        else if (players[1].score > players[0].score) { lastStageWinner = 2; p2Wins++; }
                        else { lastStageWinner = 0; }
                        setStageClear();
                    }
                }
                return;
            }

            let p1 = players[0];
            if (keys['ArrowUp']) p1.move(0, -1);
            else if (keys['ArrowDown']) p1.move(0, 1);
            else if (keys['ArrowLeft']) p1.move(-1, 0);
            else if (keys['ArrowRight']) p1.move(1, 0);

            // Aキーで即時攻撃
            if (keys['a'] || keys['A']) {
                if (!keys['a_handled']) {
                    if (p1.magic) executeMagic(p1);
                    keys['a_handled'] = true;
                }
            } else keys['a_handled'] = false;

            let p2 = players[1];
            if (mode === 2) {
                if (keys['8'] || keys['Numpad8']) p2.move(0, -1);
                else if (keys['2'] || keys['Numpad2']) p2.move(0, 1);
                else if (keys['4'] || keys['Numpad4']) p2.move(-1, 0);
                else if (keys['6'] || keys['Numpad6']) p2.move(1, 0);

                // Spaceキーで即時攻撃
                if (keys[' '] || keys['Spacebar']) {
                    if (!keys['space_handled']) {
                        if (p2.magic) executeMagic(p2);
                        keys['space_handled'] = true;
                    }
                } else keys['space_handled'] = false;
            } else {
                // CPUの行動ロジック
                if (p2.isAiming) {
                    if (p2.hesitateTimer > 0) p2.hesitateTimer -= dt;
                    else {
                        executeMagic(p2);
                        p2.isAiming = false;
                    }
                } else if (!p2.moving && Math.random() < 0.15 + (stage * 0.05)) {
                    let actionTaken = false;
                    if (p2.magic) {
                        // 既にレンジは展開されているため、その中にP1がいるかチェック
                        let inRange = p2.attackArea.some(cell => {
                            let ax = p2.targetX + cell.dx; let ay = p2.targetY + cell.dy;
                            return (ax === p1.targetX && ay === p1.targetY) || (ax === p1.x && ay === p1.y);
                        });
                        if (inRange) {
                            p2.isAiming = true;
                            playSound('Sound', 'magic_cast', 'magic_cast'); // 威嚇音として鳴らす
                            if (Math.random() < 0.5) p2.hesitateTimer = 1.0 + Math.random();
                            else p2.hesitateTimer = 0.1;
                            actionTaken = true;
                        }
                    }
                    if (!actionTaken && !p2.isAiming) {
                        let nextMove = null;
                        if (!p2.magic) nextMove = findPath(p2.targetX, p2.targetY, 'magic', null);
                        if (!nextMove && p2.hp < 70) nextMove = findPath(p2.targetX, p2.targetY, 'heal', null);
                        if (!nextMove) nextMove = findPath(p2.targetX, p2.targetY, 'food', null);
                        if (!nextMove) nextMove = findPath(p2.targetX, p2.targetY, 'player', p1);

                        if (nextMove) p2.move(nextMove.dx, nextMove.dy);
                        else {
                            const dirs = [{dx: 0, dy: -1}, {dx: 0, dy: 1}, {dx: -1, dy: 0}, {dx: 1, dy: 0}];
                            let rm = dirs[Math.floor(Math.random()*dirs.length)];
                            p2.move(rm.dx, rm.dy);
                        }
                    }
                }
            }

            players.forEach(p => p.update(dt));
            players.forEach(p => checkTrapped(p));
            meteors.forEach(m => m.update(dt));
            meteors = meteors.filter(m => !m.hit); 
            fallingItems.forEach(f => f.update(dt));
            fallingItems = fallingItems.filter(f => !f.hit);
            particles.forEach(p => p.update(dt));
            particles = particles.filter(p => p.life > 0);
        }

        function renderGameElements() {
            // Map
            for (let y = 0; y < ROWS; y++) {
                for (let x = 0; x < COLS; x++) {
                    const cell = map[y][x];
                    if (cell.type === 'road' || cell.type === 'start') {
                        const path = (cell.name === 'normal') ? 'Road/default.png' : `Road/${cell.name}.png`;
                        if (ASSETS.images[path] && ASSETS.images[path].loaded) {
                            ctx.drawImage(ASSETS.images[path].img, x*TS, y*TS, TS, TS);
                        } else {
                            ctx.fillStyle = 'lightgreen'; ctx.fillRect(x*TS, y*TS, TS, TS);
                        }
                    } else if (cell.type === 'hurdle') {
                        const path = `Hurdle/${cell.name}.png`;
                        if (ASSETS.images[path] && ASSETS.images[path].loaded) {
                            ctx.drawImage(ASSETS.images[path].img, x*TS, y*TS, TS, TS);
                            ctx.lineWidth = 4; ctx.strokeStyle = cell.color;
                            ctx.strokeRect(x * TS + 2, y * TS + 2, TS - 4, TS - 4);
                        } else {
                            ctx.fillStyle = cell.color; ctx.fillRect(x*TS, y*TS, TS, TS);
                        }
                    }
                }
            }

            // Items
            items.forEach(item => {
                if (['food', 'heal', 'speed'].includes(item.type)) {
                    const validPaths = VALID_ITEM_PATHS[item.type];
                    if (validPaths.length > 0) {
                        const path = validPaths[item.rndIdx % validPaths.length];
                        ctx.drawImage(ASSETS.images[path].img, item.x * TS, item.y * TS, TS, TS);
                    } else {
                        if (item.type === 'food') {
                            ctx.fillStyle = 'yellow'; ctx.fillRect(item.x * TS + TS/4, item.y * TS + TS/4, TS/2, TS/2);
                        } else if (item.type === 'heal') {
                            ctx.fillStyle = 'yellowgreen'; ctx.fillRect(item.x * TS + TS/3, item.y * TS + TS/3, TS/3, TS/3);
                        } else {
                            ctx.fillStyle = 'cyan'; ctx.fillRect(item.x * TS + TS/3, item.y * TS + TS/3, TS/3, TS/3);
                        }
                    }
                } else if (item.type === 'magic') {
                    const path = `Magic/${item.magicData.name}.png`;
                    if (ASSETS.images[path] && ASSETS.images[path].loaded) {
                        ctx.drawImage(ASSETS.images[path].img, item.x * TS, item.y * TS, TS, TS);
                    } else {
                        ctx.beginPath(); ctx.arc(item.x * TS + TS/2, item.y * TS + TS/2, TS/4, 0, Math.PI*2);
                        ctx.fillStyle = item.magicData.color; ctx.fill(); ctx.closePath();
                    }
                }
            });

            // Falling Items
            fallingItems.forEach(f => f.draw(ctx));

            // Attack Areas (常時表示)
            players.forEach(p => {
                if (p.attackArea.length > 0 && p.magic) {
                    p.attackArea.forEach(cell => {
                        const ax = p.targetX + cell.dx; const ay = p.targetY + cell.dy;
                        if(ax >= 0 && ax < COLS && ay >= 0 && ay < ROWS) {
                            let mapCell = map[ay][ax];
                            let isDestructible = mapCell.type === 'hurdle' && 
                                ((mapCell.name === 'water' && p.magic.name === 'ice') || mapCell.name === p.magic.name);

                            if (isDestructible) {
                                ctx.setLineDash([]); 
                                ctx.globalAlpha = 0.5 + 0.5 * Math.sin(performance.now() / 150); 
                                ctx.strokeStyle = 'white'; 
                            } else {
                                ctx.setLineDash([5, 5]); 
                                ctx.lineDashOffset = -performance.now() / 50; 
                                ctx.globalAlpha = 1.0;
                                ctx.strokeStyle = p.magic.color; 
                            }
                            ctx.lineWidth = 4;
                            ctx.strokeRect(ax * TS + 2, ay * TS + 2, TS - 4, TS - 4);
                        }
                    });
                    ctx.globalAlpha = 1.0; ctx.setLineDash([]);
                }
            });

            // Entities
            players.forEach(p => p.draw(ctx));
            meteors.forEach(m => m.draw(ctx));
            particles.forEach(p => p.draw(ctx));
        }

        function drawHpBar(x, y, hp, maxHp, color, bgColor, w, h) {
            ctx.fillStyle = bgColor; ctx.fillRect(x, y, w, h);
            ctx.fillStyle = color;
            const hpWidth = Math.max(0, (hp / maxHp) * w);
            ctx.fillRect(x, y, hpWidth, h);
            ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; ctx.strokeRect(x, y, w, h);
        }

        function drawPlayerUI(player, isP1, startX, startY) {
            ctx.fillStyle = 'rgba(0,0,0,0.7)';
            ctx.fillRect(startX, startY, 320, 150);
            ctx.strokeStyle = isP1 ? 'cyan' : 'pink';
            ctx.lineWidth = 3;
            ctx.strokeRect(startX, startY, 320, 150);
            
            ctx.fillStyle = '#fff';
            ctx.font = '22px sans-serif';
            ctx.textAlign = 'left';
            
            const px = startX + 15;
            let py = startY + 30;
            const lh = 30; 
            
            // 行1: 名前と勝ち点分の星
            const name = isP1 ? "Player1" : (mode === 1 ? "CPU" : "Player2");
            const wins = isP1 ? p1Wins : p2Wins;
            let stars = "";
            for (let i = 0; i < wins; i++) stars += "★";
            ctx.fillText(`${name}   ${stars}`, px, py);
            py += lh;
            
            // 行2: スコア
            ctx.fillText(`Score ${player.score}`, px, py);
            py += lh;
            
            // 行3: HPとバー
            ctx.fillText(`HP ${Math.floor(player.hp)}`, px, py);
            drawHpBar(px + 80, py - 18, player.hp, player.maxHp, isP1 ? 'cyan' : 'pink', 'navy', 200, 20);
            py += lh;
            
            // 行4: 魔法アイコンをレベル分並べ、属性名
            if (player.magic) {
                const m = player.magic;
                const path = `Magic/${m.name}.png`;
                let iconX = px;
                for(let i=0; i<player.magicLevel; i++) {
                    if (ASSETS.images[path] && ASSETS.images[path].loaded) {
                        ctx.drawImage(ASSETS.images[path].img, iconX, py - 24, 30, 30);
                    } else {
                        ctx.beginPath();
                        ctx.arc(iconX + 15, py - 9, 12, 0, Math.PI * 2);
                        ctx.fillStyle = m.color;
                        ctx.fill();
                        ctx.closePath();
                    }
                    iconX += 35;
                }
                ctx.fillStyle = '#fff';
                ctx.fillText(`${m.name.toUpperCase()}`, iconX, py);
            } else {
                ctx.fillText(`NONE`, px, py);
            }
        }

        function drawCenterUI() {
            const cx = canvas.width / 2;
            const cy = canvas.height - UI_HEIGHT + 40;
            ctx.fillStyle = 'rgba(0,0,0,0.7)';
            ctx.fillRect(cx - 120, cy, 240, 80);
            ctx.strokeStyle = '#fff';
            ctx.lineWidth = 2;
            ctx.strokeRect(cx - 120, cy, 240, 80);
            
            ctx.fillStyle = '#fff';
            ctx.textAlign = 'center';
            ctx.font = 'bold 24px sans-serif';
            ctx.fillText(`Stage: ${stage} / ${MAX_STAGES}`, cx, cy + 30);
            
            const min = Math.floor(timeLeft / 60); 
            const sec = Math.floor(timeLeft % 60).toString().padStart(2, '0');
            ctx.fillText(`Time: ${min}:${sec}`, cx, cy + 65);
        }

        function draw() {
            ctx.fillStyle = '#000'; ctx.fillRect(0, 0, canvas.width, canvas.height);

            if (gameState === 'TITLE') {
                const path = `Screen/title.png`;
                if (ASSETS.images[path] && ASSETS.images[path].loaded) {
                    ctx.drawImage(ASSETS.images[path].img, 0, 0, canvas.width, canvas.height);
                } else {
                    ctx.fillStyle = '#111'; ctx.fillRect(0, 0, canvas.width, canvas.height);
                    ctx.fillStyle = '#fff'; ctx.textAlign = 'center';
                    ctx.font = '50px sans-serif'; ctx.fillText('Magical Maze', canvas.width/2, canvas.height/2 - 100);
                }
                
                let boxW = 500, boxH = 120;
                let boxX = (canvas.width - boxW) / 2, boxY = canvas.height/2 + 50;
                ctx.fillStyle = 'rgba(0,0,0,0.8)'; ctx.strokeStyle = '#fff'; ctx.lineWidth = 2;
                ctx.fillRect(boxX, boxY, boxW, boxH); ctx.strokeRect(boxX, boxY, boxW, boxH);
                
                ctx.fillStyle = '#fff'; ctx.textAlign = 'left'; ctx.font = '24px sans-serif';
                ctx.fillText("Select Mode:", boxX + 20, boxY + 35);
                ctx.fillText(mode === 1 ? "▶ SINGLE MODE (CPU対戦)" : "   SINGLE MODE (CPU対戦)", boxX + 40, boxY + 70);
                ctx.fillText(mode === 2 ? "▶ MULTI MODE (人間同士で対戦)" : "   MULTI MODE (人間同士で対戦)", boxX + 40, boxY + 100);
                return;
            }

            if (gameState === 'STAGE_START' || gameState === 'STAGE_CLEAR' || gameState === 'GAME_OVER' || gameState === 'GAME_CLEAR') {
                let bgName = gameState === 'STAGE_START' ? 'stage_start' : 
                             gameState === 'STAGE_CLEAR' ? 'stage_clear' :
                             gameState === 'GAME_CLEAR' ? 'game_clear' : 'game_over';

                const path = `Screen/${bgName}.png`;
                if (ASSETS.images[path] && ASSETS.images[path].loaded) {
                    ctx.drawImage(ASSETS.images[path].img, 0, 0, canvas.width, canvas.height);
                    ctx.fillStyle = 'rgba(0,0,0,0.6)'; ctx.fillRect(0, 0, canvas.width, canvas.height);
                } else {
                    ctx.fillStyle = '#111'; ctx.fillRect(0, 0, canvas.width, canvas.height);
                }

                ctx.fillStyle = '#fff'; ctx.textAlign = 'center'; 
                
                if (gameState === 'GAME_CLEAR') {
                    ctx.font = 'bold 40px sans-serif';
                    let winnerTxt = "MATCH DRAW!";
                    if (p1Wins > p2Wins) winnerTxt = mode === 2 ? "P1 WINS THE MATCH!" : "YOU WIN THE MATCH!";
                    else if (p2Wins > p1Wins) winnerTxt = (mode === 1 ? "CPU" : "P2") + " WINS THE MATCH!";
                    
                    ctx.fillStyle = p1Wins > p2Wins ? '#00e5ff' : (p2Wins > p1Wins ? '#ff00ff' : '#ffeb3b');
                    ctx.fillText(winnerTxt, canvas.width / 2, canvas.height / 2 - 40);

                    ctx.font = 'bold 30px sans-serif';
                    ctx.fillStyle = '#fff';
                    ctx.fillText(`FINAL SCORE: P1 [ ${p1Wins} ] - [ ${p2Wins} ] ${mode === 1 ? 'CPU' : 'P2'}`, canvas.width / 2, canvas.height / 2 + 20);
                    
                    ctx.font = '20px sans-serif';
                    ctx.fillStyle = '#aaa';
                    ctx.fillText('Press Any Key...', canvas.width / 2, canvas.height / 2 + 80);
                } else {
                    ctx.font = 'bold 40px sans-serif';
                    let txt = gameState === 'STAGE_START' ? `Stage ${stage} Start` :
                              gameState === 'STAGE_CLEAR' ? `Stage ${stage} Clear!` :
                              `Game Over`;
                    ctx.fillText(txt, canvas.width / 2, canvas.height / 2 - 20);

                    if (gameState === 'STAGE_CLEAR') {
                        ctx.font = 'bold 24px sans-serif';
                        ctx.fillStyle = '#00e5ff';
                        let stgWinner = "DRAW!";
                        if (lastStageWinner === 1) stgWinner = "P1 WINS THE STAGE!";
                        else if (lastStageWinner === 2) {
                            stgWinner = (mode === 1 ? "CPU" : "P2") + " WINS THE STAGE!";
                            ctx.fillStyle = '#ff00ff';
                        } else {
                            ctx.fillStyle = '#ffeb3b';
                        }
                        ctx.fillText(stgWinner, canvas.width / 2, canvas.height / 2 + 30);
                        ctx.fillStyle = '#fff';
                    }

                    ctx.font = '20px sans-serif'; 
                    ctx.fillStyle = '#aaa';
                    ctx.fillText('Press Any Key...', canvas.width / 2, canvas.height / 2 + (gameState === 'STAGE_CLEAR' ? 80 : 40));
                }
                return;
            }

            if (gameState === 'PLAY') {
                // スクロールなし・分割なしでマップ全体を描画
                const mapOffsetX = (canvas.width - COLS * TS) / 2;
                const mapOffsetY = Math.max(0, (canvas.height - UI_HEIGHT - ROWS * TS) / 2);

                ctx.save();
                ctx.translate(mapOffsetX, mapOffsetY);
                renderGameElements();
                ctx.restore();

                // UIオーバーレイ描画を下部に配置
                const uiY = canvas.height - UI_HEIGHT + 5;
                drawPlayerUI(players[0], true, 20, uiY);
                drawPlayerUI(players[1], false, canvas.width - 340, uiY);
                drawCenterUI();
            }

            if (gameState === 'PAUSE') {
                ctx.fillStyle = 'rgba(0,0,0,0.5)'; ctx.fillRect(0, 0, canvas.width, canvas.height);
                ctx.fillStyle = '#fff'; ctx.textAlign = 'center'; ctx.font = '50px sans-serif';
                ctx.fillText('PAUSE', canvas.width / 2, canvas.height / 2);
            }
        }

        function handleScreenTransition() {
            playSound('Sound', 'pageup', 'pageup'); 

            if (gameState === 'TITLE') {
                stage = 1; gameState = 'STAGE_START';
                p1Wins = 0; p2Wins = 0; lastStageWinner = 0;
                playBGM('ScreenBgm', 'stage_state'); 
            } else if (gameState === 'STAGE_START') {
                initGame(); gameState = 'PLAY';
                playBGM('StageBgm', 'bgm' + (Math.floor(Math.random() * 3) + 1));
            } else if (gameState === 'STAGE_CLEAR') {
                stage++; gameState = 'STAGE_START'; playBGM('ScreenBgm', 'stage_state');
            } else if (gameState === 'GAME_OVER') {
                gameState = 'STAGE_START'; playBGM('ScreenBgm', 'stage_state');
            } else if (gameState === 'GAME_CLEAR') {
                gameState = 'TITLE'; playBGM('ScreenBgm', 'title');
            }
        }

        function togglePause() {
            if (gameState === 'PLAY') gameState = 'PAUSE';
            else if (gameState === 'PAUSE') gameState = 'PLAY';
        }

        function loop(timestamp) {
            if (!lastTime) lastTime = timestamp;
            const dt = (timestamp - lastTime) / 1000;
            lastTime = timestamp;

            update(dt);
            draw();

            requestAnimationFrame(loop);
        }

        requestAnimationFrame(loop);
    </script>
</body>
</html>

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

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

気楽に遊べるゲーム

  • 23本

コメント

コメントするには、 ログイン または 会員登録 をお願いします。
簡単な対戦ゲーム「MagicalMaze」|古井和雄
word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word 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