v0.01
This commit is contained in:
commit
234e769f4d
34 files changed
+4667
No files matched your search
+217
@@ -0,0 +1,217 @@
|
||||
// 入口:游戏循环 / 状态机
|
||||
import { LEVELS, findLevel } from './levels/index.js';
|
||||
import { Level } from './level.js';
|
||||
import { Player } from './player.js';
|
||||
import { Camera } from './camera.js';
|
||||
import { Input } from './input.js';
|
||||
import { UI } from './ui.js';
|
||||
import { Save } from './save.js';
|
||||
import { sfx, unlockAudio, Audio2 } from './audio.js';
|
||||
import { Editor } from './editor.js';
|
||||
import { clamp } from './util.js';
|
||||
|
||||
const W = 800, H = 608;
|
||||
const STEP = 1000 / 60;
|
||||
|
||||
const canvas = document.getElementById('game');
|
||||
const ctx = canvas.getContext('2d', { alpha: false });
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
|
||||
const G = {
|
||||
state: 'menu', // menu | playing | paused | result | editor
|
||||
level: null, player: null, cam: null,
|
||||
data: null, idx: -1,
|
||||
deaths: 0, ms: 0, hudTick: 0,
|
||||
time: 0,
|
||||
lastLevelId: null,
|
||||
};
|
||||
|
||||
// ---------------- 关卡流程 ----------------
|
||||
function startLevel(data, idx = -1) {
|
||||
unlockAudio();
|
||||
G.data = data; G.idx = idx; G.lastLevelId = data.id;
|
||||
G.level = new Level(data);
|
||||
G.player = new Player(G.level);
|
||||
G.cam = new Camera(W, H);
|
||||
G.cam.reset(G.level, G.player);
|
||||
G.player.onDeath = () => { G.deaths++; };
|
||||
G.player.onClear = () => finish();
|
||||
G.level.bosses.forEach(b => { b.onDefeat = () => finish(); });
|
||||
G.deaths = 0; G.ms = 0; G.hudTick = 0;
|
||||
G.state = 'playing';
|
||||
UI.showGame();
|
||||
UI.toast('');
|
||||
if (data.hint) UI.toast(data.hint);
|
||||
}
|
||||
|
||||
/** 整关从头再来:保留死亡数,丢弃所有存档点进度 */
|
||||
function restart() {
|
||||
if (!G.data) return;
|
||||
const d = G.deaths;
|
||||
startLevel(G.data, G.idx);
|
||||
G.deaths = d; // 死亡数不重置
|
||||
}
|
||||
|
||||
/** 自杀(IWBT 的 R 键):死后自动回到最近的存档点 */
|
||||
function suicide() {
|
||||
if (!G.player || G.state !== 'playing') return;
|
||||
G.player.kill();
|
||||
}
|
||||
|
||||
function finish() {
|
||||
if (G.state !== 'playing') return;
|
||||
G.state = 'result';
|
||||
sfx.goal();
|
||||
const apples = G.player.apples || 0;
|
||||
const total = G.level.totalApples || 0;
|
||||
const better = Save.clear(G.data.id, G.deaths, G.ms, apples);
|
||||
const hasNext = G.idx >= 0 && G.idx + 1 < LEVELS.length;
|
||||
UI.result({
|
||||
name: G.data.name, ms: G.ms, deaths: G.deaths,
|
||||
apples, totalApples: total, better, hasNext,
|
||||
});
|
||||
}
|
||||
|
||||
function nextLevel() {
|
||||
if (G.idx >= 0 && G.idx + 1 < LEVELS.length) startLevel(LEVELS[G.idx + 1], G.idx + 1);
|
||||
else toLevels();
|
||||
}
|
||||
function toLevels() { G.state = 'menu'; UI.show('scr-levels'); }
|
||||
function toTitle() { G.state = 'menu'; UI.show('scr-title'); }
|
||||
function pause() { if (G.state === 'playing') { G.state = 'paused'; UI.show('scr-pause'); } }
|
||||
function resume() { if (G.state === 'paused') { G.state = 'playing'; UI.showGame(); } }
|
||||
|
||||
// ---------------- 更新 ----------------
|
||||
function update() {
|
||||
const L = G.level, P = G.player;
|
||||
// R = 自杀回存档点;Backspace = 整关重开
|
||||
if (Input.hit('suicide')) { suicide(); return; }
|
||||
if (Input.hit('restart')) { restart(); return; }
|
||||
if (Input.hit('pause')) { pause(); return; }
|
||||
|
||||
const wasDead = P.dead;
|
||||
L.update(1, P, G.cam);
|
||||
for (const b of L.bosses) b.update(1, P);
|
||||
P.update(1);
|
||||
// 刚从死亡中复活:镜头立刻对准重生点,别慢慢飘过去
|
||||
if (wasDead && !P.dead) G.cam.snapTo(P, L);
|
||||
G.cam.update(L, P, 1);
|
||||
G.ms += STEP;
|
||||
G.time++;
|
||||
|
||||
if (++G.hudTick % 5 === 0) {
|
||||
UI.hud({
|
||||
name: G.data.name, deaths: G.deaths, ms: G.ms,
|
||||
apples: P.apples, totalApples: L.totalApples, hasGun: P.hasGun,
|
||||
save: P.saveIdx || 0, totalSaves: L.totalSaves || 0,
|
||||
});
|
||||
const boss = L.bosses.find(b => !b.dead) || L.bosses[0];
|
||||
UI.bossBar(L.bosses.length ? boss : null);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- 渲染 ----------------
|
||||
function render() {
|
||||
const t = G.time;
|
||||
if (G.state === 'playing' || G.state === 'paused' || G.state === 'result') {
|
||||
const L = G.level, P = G.player, C = G.cam;
|
||||
ctx.fillStyle = '#0a0a14';
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
L.drawBg(ctx, C, t);
|
||||
ctx.save();
|
||||
C.apply(ctx);
|
||||
L.draw(ctx, C, t);
|
||||
for (const b of L.bosses) b.draw(ctx, t);
|
||||
P.draw(ctx);
|
||||
ctx.restore();
|
||||
L.drawOverlay(ctx, C, t);
|
||||
UI.toast(L.toast || '');
|
||||
// 死亡时的血色闪烁
|
||||
if (P.dead) {
|
||||
ctx.fillStyle = 'rgba(200,0,40,.18)';
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
}
|
||||
} else {
|
||||
drawTitleBg(t);
|
||||
}
|
||||
}
|
||||
|
||||
function drawTitleBg(t) {
|
||||
const g = ctx.createLinearGradient(0, 0, 0, H);
|
||||
g.addColorStop(0, '#181828'); g.addColorStop(1, '#08080f');
|
||||
ctx.fillStyle = g; ctx.fillRect(0, 0, W, H);
|
||||
// 漂浮的刺
|
||||
ctx.globalAlpha = 0.5;
|
||||
for (let i = 0; i < 26; i++) {
|
||||
const x = ((i * 137 + t * 0.15) % (W + 60)) - 30;
|
||||
const y = 120 + ((i * 71) % (H - 160));
|
||||
ctx.fillStyle = i % 3 === 0 ? '#3a3a4c' : '#2a2a38';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y + 18); ctx.lineTo(x + 11, y); ctx.lineTo(x + 22, y + 18);
|
||||
ctx.closePath(); ctx.fill();
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
// 底部刺海
|
||||
ctx.fillStyle = '#1a1a26';
|
||||
ctx.fillRect(0, H - 30, W, 30);
|
||||
ctx.fillStyle = '#c8c8d8';
|
||||
for (let x = 0; x < W; x += 22) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, H - 4); ctx.lineTo(x + 11, H - 26); ctx.lineTo(x + 22, H - 4);
|
||||
ctx.closePath(); ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- 主循环 ----------------
|
||||
let last = performance.now(), acc = 0;
|
||||
function loop(now) {
|
||||
requestAnimationFrame(loop);
|
||||
let dt = now - last; last = now;
|
||||
if (dt > 200) dt = 200;
|
||||
acc += dt;
|
||||
let guard = 0;
|
||||
while (acc >= STEP && guard++ < 6) {
|
||||
acc -= STEP;
|
||||
if (G.state === 'playing') update();
|
||||
else if (G.state === 'paused' && Input.hit('pause')) resume();
|
||||
Input.endFrame();
|
||||
}
|
||||
if (G.state !== 'playing') G.time++;
|
||||
render();
|
||||
}
|
||||
|
||||
// ---------------- 启动 ----------------
|
||||
Save.load();
|
||||
Audio2.enabled = Save.data.audio !== false;
|
||||
|
||||
UI.init({
|
||||
play: (lv) => startLevel(lv, lv.builtin === false ? -1 : (lv.index ?? -1)),
|
||||
levels: toLevels,
|
||||
help: () => UI.show('scr-help'),
|
||||
editor: () => { G.state = 'editor'; UI.show('scr-editor'); Editor.open(); },
|
||||
back: toTitle,
|
||||
resume,
|
||||
restart: () => { restart(); },
|
||||
quit: toLevels,
|
||||
retry: () => startLevel(G.data, G.idx),
|
||||
next: nextLevel,
|
||||
wipe: () => {
|
||||
if (confirm('确定清除所有通关记录与自制关卡?')) { Save.wipe(); UI.renderLevels(); }
|
||||
},
|
||||
});
|
||||
|
||||
Editor.init({
|
||||
startLevel: (data) => startLevel(data, -1),
|
||||
refresh: () => UI.renderLevels(),
|
||||
});
|
||||
|
||||
// 首次交互解锁音频
|
||||
const unlock = () => { unlockAudio(); removeEventListener('pointerdown', unlock); removeEventListener('keydown', unlock); };
|
||||
addEventListener('pointerdown', unlock);
|
||||
addEventListener('keydown', unlock);
|
||||
|
||||
UI.show('scr-title');
|
||||
requestAnimationFrame(loop);
|
||||
|
||||
// 调试钩子
|
||||
window.__IW = { G, LEVELS, Save, startLevel };
|
||||
Reference in new issue
Block a user