console
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>午夜老宅·逃生</title>
<script src="https://ai.builda.game/sdk/latest/builda-runtime.js"></script>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#000;overflow:hidden;touch-action:none}
canvas{display:block}
#respawnBtn{position:fixed;top:12px;right:12px;background:#992222;color:#fff;border:none;padding:8px 12px;border-radius:6px;font-size:14px;z-index:99;display:none}
#skillBtn{position:fixed;bottom:110px;right:20px;width:60px;height:60px;border-radius:50%;border:#fff 2px solid;background:#222;color:#fff;font-size:13px;z-index:99;display:none}
</style>
</head>
<body>
<button id="respawnBtn">重生</button>
<button id="skillBtn"></button>
<canvas id="c"></canvas>
<script>
Builda.runtime.ready();
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
const respawnBtn = document.getElementById('respawnBtn');
const skillBtn = document.getElementById('skillBtn');
let W,H;
function resize(){
W = window.innerWidth;
H = window.innerHeight;
canvas.width = W;
canvas.height = H;
}
resize();
window.addEventListener('resize',resize);
// 关卡,全部配置安全出生点,第三关拓宽通道
const levels = [
{
name:"前厅",
spawn:{x:70,y:70},
walls:[
{x:0,y:0,w:W,h:22},{x:0,y:H-22,w:W,h:22},{x:0,y:0,w:22,h:H},{x:W-22,y:0,w:22,h:H},
{x:220,y:120,w:140,h:22},{x:420,y:240,w:22,h:180}
],
keys:[{x:340,y:160},{x:600,y:300},{x:160,y:420}],
door:{x:W-80,y:H-100,w:50,h:70},
ghosts:[{x:400,y:320,speed:1.1,path:[{x:400,y:320},{x:650,y:320}]}]
},
{
name:"回廊侧屋",
spawn:{x:70,y:70},
walls:[
{x:0,y:0,w:W,h:22},{x:0,y:H-22,w:W,h:22},{x:0,y:0,w:22,h:H},{x:W-22,y:0,w:22,h:H},
{x:200,y:180,w:22,h:260},{x:400,y:80,w:160,h:22},{x:400,y:300,w:22,h:200}
],
keys:[{x:280,y:360},{x:540,y:140},{x:660,y:420}],
door:{x:W-80,y:H-100,w:50,h:70},
ghosts:[
{x:300,y:220,speed:1,path:[{x:300,y:220},{x:300,y:440}]},
{x:520,y:360,speed:1.2,path:[{x:520,y:360},{x:700,y:360}]}
]
},
{
name:"地下地窖【修复版,加宽通道】",
spawn:{x:70,y:70},
walls:[
{x:0,y:0,w:W,h:22},{x:0,y:H-22,w:W,h:22},{x:0,y:0,w:22,h:H},{x:W-22,y:0,w:22,h:H},
{x:180,y:120,w:22,h:280},{x:340,y:22,w:22,h:220},{x:340,y:320,w:22,h:160},
{x:500,y:160,w:22,h:260},{x:500,y:120,w:140,h:22},{x:660,y:240,w:22,h:220}
],
keys:[{x:260,y:380},{x:420,y:140},{x:580,y:420},{x:720,y:140}],
door:{x:W-80,y:H-100,w:50,h:70},
ghosts:[
{x:260,y:220,speed:0.95,path:[{x:260,y:220},{x:260,y:440}]},
{x:580,y:340,speed:1.1,path:[{x:580,y:340},{x:720,y:340}]}
]
},
{
name:"阁楼终章",
spawn:{x:70,y:70},
walls:[
{x:0,y:0,w:W,h:22},{x:0,y:H-22,w:W,h:22},{x:0,y:0,w:22,h:H},{x:W-22,y:0,w:22,h:H},
{x:160,y:100,w:22,h:300},{x:320,y:200,w:140,h:22},{x:320,y:340,w:22,h:140},
{x:480,y:80,w:22,h:240},{x:480,y:340,w:140,h:22},{x:640,y:160,w:22,h:280}
],
keys:[{x:240,y:360},{x:400,y:140},{x:560,y:420},{x:720,y:120},{x:720,y:380}],
door:{x:W-80,y:H-100,w:50,h:70},
ghosts:[
{x:240,y:240,speed:1,path:[{x:240,y:240},{x:240,y:440}]},
{x:400,y:300,speed:1.05,path:[{x:400,y:300},{x:600,y:300}]},
{x:680,y:360,speed:1.15,path:[{x:680,y:360},{x:680,y:120}]}
]
}
];
// 游戏状态
let state = "menu"; // menu, skillselect, playing, win, lose
let curLevel = 0;
let player = {x:0,y:0,r:14};
let keysCollected = 0;
let selectedSkill = null;
let skillCd = 0;
let skillActive = false;
let skillDur = 0;
let ghosts = [];
let keys = [];
let joystick = {show:false,baseX:0,baseY:0,stickX:0,stickY:0,maxR:55};
// 技能定义
const skillList = [
{name:"隐匿",cd:40000,dur:3000,desc:"3秒怪物看不见你"},
{name:"强光",cd:35000,dur:2500,desc:"怪物短暂减速"},
{name:"感知",cd:45000,dur:4000,desc:"高亮附近钥匙"}
];
// 加载关卡【重点:强制重置出生点!】
function loadLevel(idx){
const lv = levels[idx];
player.x = lv.spawn.x;
player.y = lv.spawn.y;
keysCollected = 0;
keys = JSON.parse(JSON.stringify(lv.keys));
ghosts = JSON.parse(JSON.stringify(lv.ghosts));
respawnBtn.style.display = "block";
}
// 碰撞检测 + 自动挤出修复卡墙
function rectCircleOverlap(rect,cir){
let cx = Math.max(rect.x, Math.min(cir.x, rect.x+rect.w));
let cy = Math.max(rect.y, Math.min(cir.y, rect.y+rect.h));
let dx = cir.x - cx;
let dy = cir.y - cy;
return dx*dx + dy*dy < cir.r*cir.r;
}
function tryMovePlayer(mx,my){
const spd = 2.4;
let nx = player.x + mx*spd;
let ny = player.y + my*spd;
let okX = true, okY = true;
for(const w of levels[curLevel].walls){
if(rectCircleOverlap(w,{...player,x:nx})) okX = false;
if(rectCircleOverlap(w,{...player,y:ny})) okY = false;
}
if(okX) player.x = nx;
if(okY) player.y = ny;
// 自动挤出,防止卡墙
for(const w of levels[curLevel].walls){
if(rectCircleOverlap(w,player)){
const midX = w.x + w.w/2;
const midY = w.y + w.h/2;
if(player.x < midX) player.x = w.x - player.r - 2;
else player.x = w.x + w.w + player.r + 2;
if(player.y < midY) player.y = w.y - player.r - 2;
else player.y = w.y + w.h + player.r + 2;
}
}
}
// 重生按钮
respawnBtn.onclick = ()=>{
const lv = levels[curLevel];
player.x = lv.spawn.x;
player.y = lv.spawn.y;
}
skillBtn.onclick = ()=>{
if(skillCd>0 || !selectedSkill) return;
skillCd = selectedSkill.cd;
skillActive = true;
skillDur = selectedSkill.dur;
}
// 触摸摇杆
canvas.addEventListener("touchstart",e=>{
if(state!=="playing") return;
const t = e.touches[0];
joystick.baseX = W - 110;
joystick.baseY = H - 110;
joystick.stickX = joystick.baseX;
joystick.stickY = joystick.baseY;
joystick.show = true;
})
canvas.addEventListener("touchmove",e=>{
if(!joystick.show || state!=="playing") return;
e.preventDefault();
const t = e.touches[0];
let dx = t.clientX - joystick.baseX;
let dy = t.clientY - joystick.baseY;
const dis = Math.hypot(dx,dy);
if(dis>joystick.maxR){
dx = dx/dis*joystick.maxR;
dy = dy/dis*joystick.maxR;
}
joystick.stickX = joystick.baseX + dx;
joystick.stickY = joystick.baseY + dy;
})
canvas.addEventListener("touchend",()=>{
joystick.show = false;
})
// 主循环
let lastT = 0;
function loop(t){
const dt = t-lastT;
lastT = t;
ctx.fillStyle="#050505";
ctx.fillRect(0,0,W,H);
if(state === "menu"){
ctx.fillStyle="#fff";
ctx.font="bold 32px system-ui";
ctx.textAlign="center";
ctx.fillText("午夜老宅·逃生",W/2,H/2 - 80);
ctx.font="18px system-ui";
ctx.fillText("避开黑影,收集钥匙开门逃离",W/2,H/2 - 30);
// 开始按钮
const btn = {x:W/2-90,y:H/2+20,w:180,h:50};
ctx.fillStyle="#334";
ctx.fillRect(btn.x,btn.y,btn.w,btn.h);
ctx.fillStyle="#fff";
ctx.font="22px system-ui";
ctx.fillText("进入老宅",W/2,btn.y+32);
canvas.onclick = (ev)=>{
const x = ev.clientX,y=ev.clientY;
if(x>btn.x && x<btn.x+btn.w && y>btn.y && y<btn.y+btn.h){
state = "skillselect";
}
}
}else if(state === "skillselect"){
ctx.fillStyle="#fff";
ctx.font="bold 26px system-ui";
ctx.textAlign="center";
ctx.fillText("选择本局技能",W/2,70);
skillList.forEach((sk,i)=>{
const bx = W/2 - 110;
const by = 130 + i*90;
ctx.fillStyle="#223";
ctx.fillRect(bx,by,220,70);
ctx.fillStyle="#fff";
ctx.font="20px system-ui";
ctx.fillText(sk.name,W/2,by+28);
ctx.font="14px system-ui";
ctx.fillText(sk.desc,W/2,by+52);
})
canvas.onclick = (ev)=>{
const x = ev.clientX,y=ev.clientY;
skillList.forEach((sk,i)=>{
const bx = W/2 - 110;
const by = 130 + i*90;
if(x>bx && x<bx+220 && y>by && y<by+70){
selectedSkill = sk;
loadLevel(curLevel);
skillBtn.innerText = sk.name;
skillBtn.style.display = "block";
state = "playing";
}
})
}
}else if(state === "playing"){
// 摇杆移动计算
let mx=0,my=0;
if(joystick.show){
let dx = joystick.stickX - joystick.baseX;
let dy = joystick.stickY - joystick.baseY;
const d = Math.hypot(dx,dy);
if(d>5){
mx = dx/d;
my = dy/d;
}
}
tryMovePlayer(mx,my);
// 技能计时
if(skillCd>0) skillCd -= dt;
if(skillActive){
skillDur -= dt;
if(skillDur<=0) skillActive = false;
}
// 怪物更新
const lv = levels[curLevel];
for(const g of ghosts){
let sp = g.speed;
if(skillActive && selectedSkill.name==="强光") sp *=0.4;
let tar = g.path[g.pidx??0];
const gd = Math.hypot(tar.x-g.x,tar.y-g.y);
if(gd<6){
g.pidx = ((g.pidx??0)+1) % g.path.length;
tar = g.path[g.pidx];
}
const gdx = tar.x-g.x, gdy = tar.y-g.y;
const gdis = Math.hypot(gdx,gdy);
g.x += gdx/gdis * sp;
g.y += gdy/gdis * sp;
// 抓到玩家判定,隐匿可以规避
const pdis = Math.hypot(g.x-player.x,g.y-player.y);
if(pdis < 22 && !(skillActive && selectedSkill.name==="隐匿")){
state = "lose";
}
}
// 捡钥匙
for(let i=keys.length-1;i>=0;i--){
const k = keys[i];
if(Math.hypot(k.x-player.x,k.y-player.y) < 24){
keys.splice(i,1);
keysCollected++;
}
}
// 开门通关
const door = lv.door;
if(keysCollected >= lv.keys.length){
if(player.x>door.x && player.x<door.x+door.w && player.y>door.y && player.y<door.y+door.h){
curLevel++;
if(curLevel >= levels.length){
state = "win";
}else{
loadLevel(curLevel);
}
}
}
// 绘制墙
ctx.fillStyle="#222";
for(const w of lv.walls) ctx.fillRect(w.x,w.y,w.w,w.h);
// 钥匙,感知高亮
for(const k of keys){
if(skillActive && selectedSkill.name==="感知") ctx.fillStyle="#ffdd44";
else ctx.fillStyle="#ddaa22";
ctx.beginPath();
ctx.arc(k.x,k.y,10,0,Math.PI*2);
ctx.fill();
}
// 门
if(keysCollected >= lv.keys.length) ctx.fillStyle="#4a6";
else ctx.fillStyle="#533";
ctx.fillRect(door.x,door.y,door.w,door.h);
// 黑影怪物
for(const g of ghosts){
ctx.fillStyle="#111";
ctx.beginPath();
ctx.arc(g.x,g.y,18,0,Math.PI*2);
ctx.fill();
}
// 玩家
ctx.fillStyle="#ddeeff";
ctx.beginPath();
ctx.arc(player.x,player.y,player.r,0,Math.PI*2);
ctx.fill();
// 摇杆绘制
if(joystick.show){
ctx.fillStyle="rgba(255,255,255,0.18)";
ctx.beginPath();
ctx.arc(joystick.baseX,joystick.baseY,joystick.maxR,0,Math.PI*2);
ctx.fill();
ctx.fillStyle="rgba(255,255,255,0.45)";
ctx.beginPath();
ctx.arc(joystick.stickX,joystick.stickY,22,0,Math.PI*2);
ctx.fill();
}
// HUD
ctx.fillStyle="#fff";
ctx.font="16px system-ui";
ctx.textAlign="left";
ctx.fillText(`钥匙:${keysCollected}/${lv.keys.length}`,24,36);
ctx.fillText(`关卡:${lv.name}`,24,62);
if(skillCd>0){
ctx.fillText(`技能冷却:${(skillCd/1000).toFixed(1)}s`,24,88);
}else{
ctx.fillText("技能就绪",24,88);
}
canvas.onclick = null;
}else if(state === "win"){
ctx.fillStyle="#fff";
ctx.font="bold 32px system-ui";
ctx.textAlign="center";
ctx.fillText("恭喜,全部通关!",W/2,H/2-40);
ctx.font="18px system-ui";
ctx.fillText("点击返回主菜单",W/2,H/2+10);
canvas.onclick = ()=>{
curLevel=0;state="menu";respawnBtn.style.display="none";skillBtn.style.display="none";
}
}else if(state === "lose"){
ctx.fillStyle="#fff";
ctx.font="bold 32px system-ui";
ctx.textAlign="center";
ctx.fillText("被黑影找到了",W/2,H/2-40);
ctx.font="18px system-ui";
ctx.fillText("点击返回菜单",W/2,H/2+10);
canvas.onclick = ()=>{
curLevel=0;state="menu";respawnBtn.style.display="none";skillBtn.style.display="none";
}
}
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
</script>
</body>
</html>