🎮ArcadeLab

迷宫逃生 | Maze Escape | Escape del Laberinto

by SuperPhoenix65
440 lines16.3 KB
▶ Play
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <title>迷宫逃生 | Maze Escape | Escape del Laberinto</title>
    <style>
        *{margin:0;padding:0;box-sizing:border-box}
        body{background:#111;display:flex;justify-content:center;align-items:center;min-height:100vh;color:#fff;font-family:Arial}
        
        #langBtn{
            position:fixed;top:12px;left:12px;z-index:9999;
            padding:6px 10px;font-size:13px;background:#2266cc;color:#fff;
            border:1px solid #fff;border-radius:6px;cursor:pointer;
        }
        #langPanel{
            position:fixed;top:50px;left:12px;background:#222;
            border:1px solid #fff;border-radius:6px;padding:10px;
            z-index:9998;display:none;
        }
        .lang-item{
            display:block;width:100%;margin:6px 0;padding:5px 8px;
            background:#444;border:none;color:#fff;border-radius:4px;
            cursor:pointer;text-align:left;
        }
        .lang-item:hover{background:#666;}

        #startScreen{
            position:fixed;z-index:999;
            top:0;left:0;width:100%;height:100%;
            background:#000;display:flex;flex-direction:column;
            justify-content:center;align-items:center;
        }
        #startBtn{
            padding:16px 40px;font-size:22px;background:#09f;color:#fff;
            border:none;border-radius:10px;cursor:pointer;margin-top:20px;
        }

        #gameOverScreen{
            position:fixed;z-index:990;
            top:0;left:0;width:100%;height:100%;
            background:rgba(0,0,0,0.9);display:none;
            flex-direction:column;justify-content:center;align-items:center;
        }
        #winScreen{
            position:fixed;z-index:990;
            top:0;left:0;width:100%;height:100%;
            background:rgba(0,0,0,0.9);display:none;
            flex-direction:column;justify-content:center;align-items:center;
        }
        .restartBtn{
            padding:14px 32px;font-size:18px;background:#f30;color:#fff;
            border:none;border-radius:8px;margin-top:20px;cursor:pointer;
        }

        #viewBox{
            width:640px;height:420px;
            border:3px solid #fff;
            overflow:hidden;
            position:relative;
            background:#3a3a3a;
            display:none;
        }
        #mazeWorld{position:absolute}
        .wall{background:#000;position:absolute}
        .player{width:20px;height:20px;background:#0cf;position:absolute;border-radius:50%;z-index:2}
        .monster{width:24px;height:24px;background:#f33;position:absolute;border-radius:50%;z-index:2}
        .ball{width:14px;height:14px;background:#ffd700;border-radius:50%;position:absolute;box-shadow:0 0 8px gold}
        .fog{position:absolute;top:0;left:0;right:0;bottom:0;pointer-events:none;z-index:10}
        .info{position:absolute;top:10px;left:10px;font-size:20px;z-index:99}

        #joystick{
            position:fixed;bottom:20px;left:20px;
            width:120px;height:120px;background:rgba(255,255,255,0.2);
            border-radius:50%;z-index:100;display:none;cursor:grab;
        }
        #stick{
            position:absolute;width:50px;height:50px;background:#fff;
            border-radius:50%;top:35px;left:35px;cursor:grabbing;
        }
    </style>
</head>
<body>

<button id="langBtn">语言 / Lang / Idioma</button>
<div id="langPanel">
    <button class="lang-item" data-lang="zh">中文 | Chinese | Chino</button>
    <button class="lang-item" data-lang="en">English | Inglés</button>
    <button class="lang-item" data-lang="es">Español | Español</button>
</div>

<div id="startScreen">
    <h1 id="startTitle">迷宫逃生</h1>
    <p id="startTip">收集金球消灭怪物|电脑WASD/鼠标拖摇杆,手机触屏摇杆</p>
    <button id="startBtn">开始游戏</button>
</div>

<div id="gameOverScreen">
    <h1 id="loseText">💀 被抓住了</h1>
    <button class="restartBtn" id="restartLose">重新开始</button>
</div>

<div id="winScreen">
    <h1 id="winText">🏆 胜利通关</h1>
    <button class="restartBtn" id="restartWin">重新开始</button>
</div>

<div class="info"><span id="hpLabel">怪物血量:</span> <span id="hp">7</span></div>
<div id="viewBox">
    <div id="mazeWorld"></div>
    <div class="fog"></div>
</div>

<div id="joystick">
    <div id="stick"></div>
</div>

<script>
// ============== 音效系统:音量+节奏双动态变化 ==============
let audioContext;
let bgLoop;
let coinSound;
let currentInterval = 800;
let mainGain; // 全局音量控制器

function initAudio() {
    audioContext = new (window.AudioContext || window.webkitAudioContext)();
    mainGain = audioContext.createGain();
    mainGain.gain.value = 0.12; // 基础音量
    mainGain.connect(audioContext.destination);

    // 舒缓低音背景音乐
    function playBgLoop() {
        let lastTime = 0;
        function loop(time) {
            if (time - lastTime > currentInterval) {
                lastTime = time;
                const osc = audioContext.createOscillator();
                const gain = audioContext.createGain();
                osc.type = 'sine';
                osc.frequency.value = 90;
                gain.gain.value = 0.8;
                osc.connect(gain);
                gain.connect(mainGain);
                osc.start();
                osc.stop(time + 0.12);
            }
            bgLoop = requestAnimationFrame(loop);
        }
        loop(0);
    }
    playBgLoop();

    // 吃金币音效
    coinSound = function playCoin() {
        const osc = audioContext.createOscillator();
        const gain = audioContext.createGain();
        osc.type = 'sine';
        osc.frequency.value = 850;
        gain.gain.value = 0.25;
        osc.connect(gain);
        gain.connect(audioContext.destination);
        osc.start();
        osc.stop(audioContext.currentTime + 0.1);
    };

    // 动态控制:距离越近 → 节奏越快 + 音量越大
    return function updateSound(dist) {
        // 控制节奏(平缓加速,不刺耳)
        let newInterval = dist * 1.8;
        currentInterval = Math.max(350, Math.min(900, newInterval));

        // 控制音量(距离越近音量越大,设置上下限防爆音)
        let volume = 0.35 - dist / 800;
        mainGain.gain.value = Math.max(0.12, Math.min(0.32, volume));
    };
}

let updateMusic;
let audioInited = false;

// ============== 多语言 ==============
const langData = {
    zh:{title:"迷宫逃生",tip:"收集金球消灭怪物|电脑WASD/鼠标拖摇杆,手机触屏摇杆",start:"开始游戏",hp:"怪物血量:",lose:"💀 被抓住了",win:"🏆 胜利通关",restart:"重新开始"},
    en:{title:"Maze Escape",tip:"Collect gold balls to defeat monster | WASD/Joystick for PC, Touch joystick for mobile",start:"Start Game",hp:"Monster HP:",lose:"💀 Caught",win:"🏆 Victory",restart:"Restart"},
    es:{title:"Escape del Laberinto",tip:"Recoge oros para derrotar al monstruo | WASD/Mando PC, Mando táctil móvil",start:"Empezar",hp:"HP Monstruo:",lose:"💀 Capturado",win:"🏆 Victoria",restart:"Reiniciar"}
};
let nowLang = "zh";

const langBtn = document.getElementById("langBtn");
const langPanel = document.getElementById("langPanel");
const langItems = document.querySelectorAll(".lang-item");
const startTitle = document.getElementById("startTitle");
const startTip = document.getElementById("startTip");
const startBtn = document.getElementById("startBtn");
const hpLabel = document.getElementById("hpLabel");
const loseText = document.getElementById("loseText");
const winText = document.getElementById("winText");
const restartLose = document.getElementById("restartLose");
const restartWin = document.getElementById("restartWin");

function setLang(type){
    nowLang = type;
    const d = langData[type];
    startTitle.textContent = d.title;
    startTip.textContent = d.tip;
    startBtn.textContent = d.start;
    hpLabel.textContent = d.hp;
    loseText.textContent = d.lose;
    winText.textContent = d.win;
    restartLose.textContent = d.restart;
    restartWin.textContent = d.restart;
    langPanel.style.display = "none";
}

langBtn.onclick = ()=>{
    langPanel.style.display = langPanel.style.display==="none"?"block":"none";
};
document.body.onclick = (e)=>{
    if(!langBtn.contains(e.target) && !langPanel.contains(e.target)) langPanel.style.display = "none";
}
langItems.forEach(item=>{
    item.onclick = ()=>setLang(item.dataset.lang);
})

// ============== 游戏核心 ==============
const gameOverScreen = document.getElementById('gameOverScreen');
const winScreen = document.getElementById('winScreen');
const viewBox = document.getElementById('viewBox');
const joystick = document.getElementById('joystick');
const stick = document.getElementById('stick');
const world = document.getElementById('mazeWorld');
const hpText = document.getElementById('hp');
const fog = document.querySelector('.fog');

const tile = 32;
const mapW = 32, mapH = 24;
let gameStarted = false;
let gameOver = false;
let isMouseDown = false;

const map = [
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[1,0,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,1],
[1,0,0,0,0,1,0,1,1,1,1,0,1,0,1,1,1,1,0,1,0,1,1,1,1,0,1,0,1,1,0,1],
[1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0,0,0,1,0,0,1],
[1,0,1,0,1,1,1,1,0,0,1,1,1,1,1,0,0,1,1,1,1,0,0,1,1,1,1,0,1,0,0,1],
[1,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1],
[1,1,1,0,1,0,1,1,1,1,1,1,0,0,1,1,1,1,1,1,1,1,0,0,1,0,1,1,1,1,1,1],
[1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1],
[1,0,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,0,1],
[1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1],
[1,1,1,1,1,1,1,0,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,0,1],
[1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1],
[1,0,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,0,0,1,1,1,1,1,1,1,1,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1],
[1,1,1,1,1,0,1,1,1,1,1,1,0,1,0,1,1,1,0,1,1,1,1,1,1,0,1,1,1,1,1,1],
[1,0,0,0,1,0,0,0,0,0,0,1,0,0,0,1,0,1,0,0,0,0,0,0,1,0,0,0,0,0,0,1],
[1,0,1,0,1,1,1,1,1,1,0,1,1,1,1,1,0,1,1,1,1,1,1,0,1,1,1,1,1,1,0,1],
[1,0,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1],
[1,0,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,0,0,1,1,1,1,1,1,1,1,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1],
[1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1],
[1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
];

const player = {x: tile*1.5, y: tile*1.5, size:20, speed:3};
const monster = {x:900, y:600, size:24, speed:1.2, hp:7};
const keys = {w:false,a:false,s:false,d:false};
let balls = [];
let p, m;
let stickMove = {x:0,y:0};

// 开始游戏(启动音效)
startBtn.onclick = () => {
    startScreen.style.display = 'none';
    viewBox.style.display = 'block';
    joystick.style.display = 'block';
    gameStarted = true;
    gameOver = false;
    if(!audioInited){
        audioInited = true;
        updateMusic = initAudio();
    }
};

restartLose.onclick = ()=>location.reload();
restartWin.onclick = ()=>location.reload();

function initMap(){
    world.style.width = mapW*tile+'px';
    world.style.height = mapH*tile+'px';
    for(let y=0;y<mapH;y++){
        for(let x=0;x<mapW;x++){
            if(map[y][x]===1){
                const e = document.createElement('div');
                e.className='wall';
                e.style.left=x*tile+'px';
                e.style.top=y*tile+'px';
                e.style.width=tile+'px';
                e.style.height=tile+'px';
                world.appendChild(e);
            }
        }
    }
}

function initPlayer(){
    p = document.createElement('div'); p.className='player'; world.appendChild(p);
    m = document.createElement('div'); m.className='monster'; world.appendChild(m);
}

function makeBalls(n=16){
    const list = [];
    for(let y=0;y<mapH;y++)for(let x=0;x<mapW;x++)if(map[y][x]===0)list.push({x,y});
    for(let i=0;i<n;i++){
        const o = list.splice(Math.random()*list.length|0,1)[0];
        const e = document.createElement('div'); e.className='ball';
        e.style.left = o.x*tile+9+'px';
        e.style.top = o.y*tile+9+'px';
        world.appendChild(e);
        balls.push({e,x:o.x*tile+9,y:o.y*tile+9});
    }
}

function canGo(x,y){
    const t = tile;
    const pts = [
        [x+2,y+2],[x+player.size-2,y+2],
        [x+2,y+player.size-2],[x+player.size-2,y+player.size-2]
    ];
    for(const [px,py] of pts){
        const gx = (px/t)|0, gy = (py/t)|0;
        if(gx<0||gy<0||gx>=mapW||gy>=mapH||map[gy][gx])return false;
    }
    return true;
}

function cam(){
    const vw=viewBox.clientWidth,vh=viewBox.clientHeight;
    world.style.transform = `translate(${vw/2-player.x}px,${vh/2-player.y}px)`;
    fog.style.background = `radial-gradient(circle 110px at ${vw/2}px ${vh/2}px,transparent 0,#000 160px)`;
}

// ============== 摇杆控制(手机+电脑双适配) ==============
const joyCenterX = 60;
const joyCenterY = 60;
const maxStickRange = 45;

joystick.addEventListener('touchstart', (e) => {e.preventDefault();updateJoyPos(e.touches[0]);});
joystick.addEventListener('touchmove', (e) => {e.preventDefault();updateJoyPos(e.touches[0]);});
joystick.addEventListener('touchend', () => {resetJoy();});

joystick.onmousedown = (e)=>{e.preventDefault();isMouseDown = true;updateJoyPos(e);}
document.onmousemove = (e)=>{if(!isMouseDown)return;updateJoyPos(e);}
document.onmouseup = ()=>{isMouseDown = false;resetJoy();}

function updateJoyPos(e){
    const rect = joystick.getBoundingClientRect();
    let dx = e.clientX - (rect.left + joyCenterX);
    let dy = e.clientY - (rect.top + joyCenterY);
    let dist = Math.hypot(dx, dy);
    if(dist > maxStickRange){dx = dx / dist * maxStickRange; dy = dy / dist * maxStickRange;}
    stickMove.x = dx; stickMove.y = dy;
    stick.style.left = joyCenterX + dx + 'px';
    stick.style.top = joyCenterY + dy + 'px';
}
function resetJoy(){
    stickMove.x = 0; stickMove.y = 0;
    stick.style.left = '35px'; stick.style.top = '35px';
}

document.addEventListener('keydown',e=>{const k=e.key.toLowerCase();if(k in keys)keys[k]=true});
document.addEventListener('keyup',e=>{const k=e.key.toLowerCase();if(k in keys)keys[k]=false});

function doGameOver(){
    gameOver = true;
    viewBox.style.filter = 'brightness(0.4) saturate(0)';
    setTimeout(()=>{gameOverScreen.style.display = 'flex';},300);
}
function doWin(){
    gameOver = true;
    winScreen.style.display = 'flex';
}

// ============== 主循环 ==============
function loop(){
    if(!gameStarted || gameOver){requestAnimationFrame(loop);return;}
    
    let nx=player.x, ny=player.y;
    const sx = stickMove.x; const sy = stickMove.y;
    
    if(keys.a || sx < -18) nx-=player.speed;
    if(keys.d || sx > 18) nx+=player.speed;
    if(keys.w || sy < -18) ny-=player.speed;
    if(keys.s || sy > 18) ny+=player.speed;

    if(canGo(nx,player.y))player.x=nx;
    if(canGo(player.x,ny))player.y=ny;

    const dx=player.x-monster.x, dy=player.y-monster.y;
    const dist=Math.hypot(dx,dy);
    
    // 同步更新音量+节奏
    if(updateMusic) updateMusic(dist);

    if(dist>5){
        monster.x += dx/dist*monster.speed;
        monster.y += dy/dist*monster.speed;
    }

    for(let i=balls.length-1;i>=0;i--){
        const b=balls[i];
        if(Math.hypot(player.x-b.x,player.y-b.y)<18){
            coinSound();
            monster.hp--; hpText.textContent=monster.hp;
            b.e.remove(); balls.splice(i,1);
            if(monster.hp<=0){doWin();return;}
        }
    }

    if(dist<18){
        doGameOver();return;
    }

    p.style.left=player.x+'px';p.style.top=player.y+'px';
    m.style.left=monster.x+'px';m.style.top=monster.y+'px';
    cam();
    requestAnimationFrame(loop);
}

initMap();
initPlayer();
makeBalls();
loop();
</script>
</body>
</html>

Game Source: 迷宫逃生 | Maze Escape | Escape del Laberinto

Creator: SuperPhoenix65

Libraries: none

Complexity: complex (440 lines, 16.3 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: maze-escape-escape-del-laberinto-superphoenix65-mq2bfbjq" to link back to the original. Then publish at arcadelab.ai/publish.