This commit is contained in:
cancerai committed 2026-10-20 22:21:26 +08:00
commit 234e769f4d
34 files changed
+4667

No files matched your search

+63
View File
@@ -0,0 +1,63 @@
// BOSS 战关卡(击败 BOSS 即通关)
import { G, rep, E, plat, ent } from './kit.js';
export default [
{
id: 'b1', name: 'THE GUY', type: 'boss', diff: 3, bg: 'boss', width: 48,
hint: 'X 键射击。他放扇形弹+砸地冲击波,站在平台边缘躲开再输出。',
startGun: true,
bosses: [{ kind: 'guy', hp: 72 }],
grid: G(48, [
rep('#', 48), // 0
E(48), E(48), E(48), E(48), E(48), E(48), E(48),// 1-7
'#' + rep('.', 5) + '######' + rep('.', 24) + '######', // 8
E(48), E(48), E(48), // 9-11
'#' + rep('.', 15) + '######' + rep('.', 8) + '######', // 12
E(48), E(48), E(48), // 13-15
'#' + rep('.', 5) + '######' + rep('.', 24) + '######', // 16
E(48), E(48), E(48), // 17-19
ent(48, [2, 'S'], [24, 'B']), // 20
rep('#', 48), // 21
]),
},
{
id: 'b2', name: 'MECHA KID', type: 'boss', diff: 4, bg: 'boss', width: 48,
hint: '冲刺时贴地必死。跳上高台躲,等他停下再打。',
startGun: true,
bosses: [{ kind: 'mecha', hp: 96 }],
grid: G(48, [
rep('#', 48), // 0
E(48), E(48), E(48), E(48), // 1-4
'#' + rep('.', 12) + '######' + rep('.', 10) + '######', // 5
E(48), E(48), E(48), // 6-8
'#' + rep('.', 3) + '######' + rep('.', 28) + '######', // 9
E(48), E(48), E(48), // 10-12
'#' + rep('.', 15) + '######' + rep('.', 8) + '######', // 13
E(48), E(48), E(48), // 14-16
'#' + rep('.', 3) + '######' + rep('.', 28) + '######', // 17
E(48), E(48), E(48), // 18-19
ent(48, [2, 'S'], [24, 'B']), // 20
rep('#', 48), // 21
]),
},
{
id: 'b3', name: 'THE DEVIL', type: 'boss', diff: 5, bg: 'boss', width: 48,
hint: '三阶段:追踪弹 → 激光横扫 → 环形弹幕。地上还有刺。活下来。',
startGun: true,
bosses: [{ kind: 'devil', hp: 128 }],
grid: G(48, [
rep('#', 48), // 0
E(48), E(48), E(48), // 1-3
'#' + rep('.', 5) + '######' + rep('.', 24) + '######', // 4
E(48), E(48), E(48), // 5-7
'#' + rep('.', 15) + '######' + rep('.', 8) + '######', // 8
E(48), E(48), E(48), // 9-11
'#' + rep('.', 3) + '######' + rep('.', 28) + '######', // 12
E(48), E(48), E(48), // 13-15
'#' + rep('.', 15) + '######' + rep('.', 8) + '######', // 16
E(48), E(48), E(48), // 17-19
ent(48, [2, 'S'], [24, 'B'], [12, '^'], [13, '^'], [34, '^'], [35, '^']), // 20
rep('#', 48), // 21
]),
},
];
+131
View File
@@ -0,0 +1,131 @@
// 耐久(Endurance)关卡:长距离连续考验,存档点稀疏
import { G, rep, E, plat, ent, flr } from './kit.js';
export default [
// ---------------- 1 ----------------
{
id: 'e1', name: 'LONG ROAD', type: 'endur', diff: 3, bg: 'endur', width: 100,
hint: '100 格长路,8 个刺坑,只有 5 个存档点。别急。',
messages: ['才刚刚开始'],
grid: G(100, [
rep('#', 100), // 0
E(100), E(100), E(100), E(100), E(100), // 1-5
E(100), E(100), E(100), E(100), E(100), // 6-10
E(100), // 11
ent(100, [15, 'v'], [17, 'v'], [30, 'v'], [32, 'v'], [55, 'v'], [57, 'v'], [75, 'v'], [77, 'v']), // 12
ent(100, [43, 'F'], [90, 'F']), // 13
'#' + rep('.', 40) + '######' + rep('.', 41) + '######', // 14
E(100), E(100), // 15-16
ent(100, [2, 'S'], [16, 'G'], [30, 'G'], [52, 'G'], [76, 'G'], [98, 'X'], // 17
[26, '^'], [27, '^'], [40, '^'], [41, '^'], [63, '^'], [64, '^'], [78, '^'], [79, '^']),
flr(['#', 9], ['^', 3], ['#', 7], ['^', 4], ['#', 11], ['^', 3], ['#', 6], ['^', 5],
['#', 9], ['^', 3], ['#', 8], ['^', 4], ['#', 10], ['^', 3], ['#', 6], ['^', 5], ['#', 3]), // 18
]),
},
// ---------------- 2 ----------------
{
id: 'e2', name: 'TURRET HELL', type: 'endur', diff: 4, bg: 'endur', width: 88,
hint: '炮台沿路扫射。踩空就是刺海,别站在开阔地。',
turrets: [
{ dir: 'right', period: 78, speed: 2.8, offset: 0 },
{ dir: 'left', period: 92, speed: 2.6, offset: 25 },
{ dir: 'right', period: 70, speed: 3.0, offset: 10 },
{ dir: 'left', period: 86, speed: 2.7, offset: 40 },
{ dir: 'right', period: 74, speed: 2.9, offset: 20 },
{ dir: 'aim', period: 110, speed: 2.4, offset: 55 },
],
grid: G(88, [
rep('#', 88), // 0
E(88), E(88), E(88), E(88), E(88), E(88), // 1-6
E(88), E(88), E(88), // 7-9
E(88), // 10
ent(88, [83, 'X']), // 11
plat(88, 82, 5), // 12
E(88), // 13
plat(88, 78, 5), // 14
E(88), // 15
ent(88, [83, 'G']), // 16
plat(88, 82, 5), // 17
E(88), // 18
plat(88, 78, 5), // 19
E(88), // 20
ent(88, [2, 'S'], [4, 'O'], [10, 'G'], [20, 'O'], [30, 'G'], [36, 'O'],
[50, 'G'], [52, 'O'], [68, 'O'], [70, 'G'], [84, 'O']), // 21
'#' + '.' + rep('#####...', 10) + '#####', // 22
'#' + rep('^', 87), // 23
rep('#', 88), // 24
]),
},
// ---------------- 3 ----------------
{
id: 'e3', name: 'THE PIT', type: 'endur', diff: 4, bg: 'endur', width: 64,
hint: '先从左侧竖井下到底,游过水下刺池,再从右边一路爬回顶端。',
messages: ['下面有东西在等你'],
grid: G(64, [
rep('#', 64), // 0
E(64), E(64), // 1-2
ent(64, [57, 'X']), // 3
'#' + '..' + 'S' + rep('.', 52) + rep('#', 7), // 4 出生 + 右·顶台
'#.' + rep('#', 7), // 5 左
'#' + rep('.', 50) + 'G', // 6
'#' + rep('.', 48) + rep('#', 7), // 7 右
'#' + rep('.', 10) + rep('#', 7), // 8 左
'#' + rep('.', 58) + 'G', // 9
'#' + '...' + 'G' + rep('.', 51) + rep('#', 7), // 10 右
'#.' + rep('#', 7), // 11 左
'#' + rep('.', 50) + 'G', // 12
'#' + rep('.', 48) + rep('#', 7), // 13 右
'#' + rep('.', 10) + rep('#', 7), // 14 左
E(64), // 15
'#' + '...' + 'G' + rep('.', 51) + rep('#', 7), // 16 右
'#.' + rep('#', 7), // 17 左
E(64), // 18
'#' + rep('.', 48) + rep('#', 7), // 19 右
'#' + rep('.', 10) + rep('#', 7), // 20 左
E(64), // 21
'#' + rep('.', 12) + 'G' + rep('.', 42) + rep('#', 7), // 22 右
'#' + rep('.', 10) + rep('#', 7), // 23 左·井底台
'#' + rep('.', 50) + 'G', // 24 出水台
'#' + rep('.', 17) + rep('~', 31) + rep('#', 7) + rep('.', 8), // 25
'#' + rep('.', 17) + rep('~', 31) + rep('.', 14), // 26
'#' + rep('.', 17) + rep('~', 31) + rep('.', 14), // 27
'#' + rep('^', 63), // 28
rep('#', 64), // 29
]),
},
// ---------------- 4 ----------------
{
id: 'e4', name: 'MARATHON', type: 'endur', diff: 5, bg: 'endur', width: 120,
hint: '120 格:冰面、炮台、刺坑、移动平台全都有。5 个存档,自求多福。',
turrets: [
{ dir: 'right', period: 76, speed: 2.9, offset: 0 },
{ dir: 'left', period: 88, speed: 2.7, offset: 30 },
{ dir: 'right', period: 68, speed: 3.1, offset: 15 },
{ dir: 'aim', period: 120, speed: 2.3, offset: 50 },
{ dir: 'left', period: 80, speed: 2.8, offset: 25 },
],
grid: G(120, [
rep('#', 120), // 0
E(120), E(120), E(120), E(120), E(120), // 1-5
E(120), E(120), E(120), E(120), E(120), // 6-10
E(120), E(120), E(120), E(120), E(120), // 11-15
E(120), // 16
ent(120, [20, 'v'], [22, 'v'], [36, 'v'], [38, 'v'], [52, 'v'], [54, 'v'],
[70, 'v'], [72, 'v'], [88, 'v'], [90, 'v'], [104, 'v'], [106, 'v']), // 17
E(120), E(120), E(120), E(120), // 18-21
ent(120, [2, 'S'], [20, 'O'], [24, 'G'], [36, 'O'], [40, 'G'], [52, 'O'],
[66, 'G'], [70, 'O'], [88, 'G'], [100, 'O'], [104, 'G'], [116, 'X'],
[17, '^'], [18, '^'], [54, '^'], [55, '^'], [72, '^'], [73, '^'], [105, '^'], [106, '^']), // 22
flr(['%', 10], ['^', 4], ['#', 12], ['^', 5], ['%', 12], ['^', 4],
['#', 10], ['^', 5], ['#', 15], ['^', 4], ['%', 10], ['^', 5],
['#', 12], ['^', 4], ['#', 7]), // 23
rep('#', 120), // 24
]),
movers: [
{ x: 57, y: 22, w: 3, h: 1, path: [[57, 22], [63, 22]], speed: 1.3 },
],
},
];
+26
View File
@@ -0,0 +1,26 @@
import tutorial from './tutorial.js';
import jump from './jump.js';
import endurance from './endurance.js';
import boss from './boss.js';
import troll from './troll.js';
const raw = [...tutorial, ...jump, ...endurance, ...boss, ...troll];
raw.forEach((l, i) => { l.index = i; l.builtin = true; });
export const LEVELS = raw;
export const TYPE_LABEL = {
jump: '跳刺', endur: '耐久', boss: 'BOSS', troll: '坑向', custom: '自制',
};
export const TYPE_CLASS = {
jump: 't-jump', endur: 't-endur', boss: 't-boss', troll: 't-troll', custom: 't-custom',
};
let uid = 0;
export function makeCustomLevel(data) {
return { ...data, id: 'u' + (Date.now() % 100000) + (uid++), type: 'custom', builtin: false, index: -1 };
}
export function findLevel(id) {
return LEVELS.find(l => l.id === id);
}
+368
View File
@@ -0,0 +1,368 @@
// 跳刺(Needle)关卡 —— 10 关
// 设计基调:地图尽可能大、落脚点尽可能小、存档点尽可能稀疏。
// 物理上限(改动前务必记住):
// 平跳水平 ≈ 4 格(127px),二段跳 ≈ 6 格(206px)
// 单跳上升 ≈ 2.8 格(90px),二段跳上升 ≈ 5 格(162px)
// 天花板刺放在「平台行 -5」:允许完整单跳、二段跳必死 → 只能走 4 格以内的坑
import { Grid } from './kit.js';
/** 落脚台序列:run(起始列, 起始宽, [[间隙, 台宽], ...]) => [{x,w}, ...] */
function run(x0, w0, specs) {
const out = [{ x: x0, w: w0 }];
let x = x0 + w0;
for (const [g, w] of specs) { x += g; out.push({ x, w }); x += w; }
return out;
}
const putAll = (m, row, pads, ch = '#') => pads.forEach(p => m.rect(p.x, row, p.w, 1, ch));
/** 竖井:在 [x0,x1] 列范围内交错上行,每级上升 2 格、横移 step 列 */
function shaft(m, rowBottom, rowTop, x0, x1, step = 6, w = 2) {
const out = [];
let row = rowBottom, x = x0, dir = 1;
while (row >= rowTop) {
m.rect(x, row, w, 1, '#');
out.push({ x, row });
row -= 2;
const nx = x + dir * step;
if (nx < x0 || nx + w - 1 > x1) dir = -dir;
else x = nx;
}
return out;
}
export default [
// ================= 1 =================
{
id: 'j1', name: 'SPIKE ALLEY', type: 'jump', diff: 2, bg: 'jump',
width: 104, hint: '刺海长廊。落脚点越来越小,坑越来越宽。104 格,只有三个存档点。',
grid: (() => {
const W = 104, H = 20, R = 17;
const m = new Grid(W, H);
m.border();
m.hline(18, 1, W - 2, '^'); // 刺海
const pads = run(2, 6, [
[4, 2], [4, 2], [4, 2], [5, 2], [5, 1],
[4, 1], [5, 1], [6, 1], [5, 2], [4, 1],
]);
putAll(m, R, pads);
// 尽头接竖井,终点在半空
const sp = shaft(m, 15, 5, 72, 96, 6, 2);
for (let r = 7; r < 16; r += 3) { m.set(70, r, '>'); m.set(98, r, '<'); }
const top = sp[sp.length - 1];
m.set(3, R - 1, 'S');
m.set(pads[3].x, R - 1, 'G');
m.set(pads[7].x, R - 1, 'G');
m.set(sp[1].x, sp[1].row - 1, 'G');
m.set(sp[3].x, sp[3].row - 1, 'G');
m.set(top.x, top.row - 1, 'X');
return m.out();
})(),
},
// ================= 2 =================
{
id: 'j2', name: 'VERTICAL HELL', type: 'jump', diff: 3, bg: 'jump',
width: 68, hint: '68×52 的高塔。每级上升 2 格、横移 6 格,头顶还悬着掉刺。',
grid: (() => {
const W = 68, H = 52;
const m = new Grid(W, H);
m.border();
m.hline(H - 2, 1, W - 2, '^');
let row = H - 3, x = 4, dir = 1;
const pads = [];
while (row >= 5) {
m.rect(x, row, 2, 1, '#');
pads.push({ x, row });
row -= 2;
const nx = x + dir * 6;
if (nx + 1 > W - 8) dir = -1; else if (nx < 4) dir = 1; else x = nx;
}
for (let r = 10; r < H - 4; r += 3) { m.set(1, r, '>'); m.set(W - 2, r, '<'); }
pads.forEach((p, i) => { if (i % 3 === 1 && p.row - 6 > 2) m.set(p.x, p.row - 6, 'D'); });
pads.forEach((p, i) => { if (i > 0 && i % 6 === 0) m.set(p.x, p.row - 1, 'G'); });
m.set(5, H - 4, 'S');
const top = pads[pads.length - 1];
m.set(top.x, top.row - 1, 'X');
return m.out();
})(),
},
// ================= 3 =================
{
id: 'j3', name: 'TIGHTROPE', type: 'jump', diff: 4, bg: 'jump',
width: 140, hint: '140 格的一格宽钢丝,头顶全是刺:只能单跳,二段跳必死。三个存档点。',
grid: (() => {
const W = 140, H = 22, R = 17;
const m = new Grid(W, H);
m.border();
m.hline(18, 1, W - 2, '^');
const pads = run(2, 5, Array.from({ length: 14 }, () => [4, 1]));
putAll(m, R, pads);
m.hline(R - 5, 8, 78, 'v'); // 天花板刺
const sp = shaft(m, 15, 5, 80, 108, 6, 2);
[7, 10].forEach(r => m.set(78, r, '>'));
[7, 10, 13].forEach(r => m.set(110, r, '<'));
const top = sp[sp.length - 1];
m.set(3, R - 1, 'S');
m.set(pads[4].x, R - 1, 'G');
m.set(pads[10].x, R - 1, 'G');
m.set(sp[1].x, sp[1].row - 1, 'G');
m.set(sp[3].x, sp[3].row - 1, 'G');
m.set(top.x, top.row - 1, 'X');
return m.out();
})(),
},
// ================= 4 =================
{
id: 'j4', name: 'ICE NEEDLES', type: 'jump', diff: 4, bg: 'jump',
width: 130, hint: '前半段冰面打滑、坑有五格宽;后半段一格宽冰柱悬在刺海上——头顶有刺,只能单跳。',
grid: (() => {
const W = 130, H = 24, R = 19;
const m = new Grid(W, H);
m.border();
m.hline(20, 1, W - 2, '^');
m.rect(2, R, 6, 1, '%');
[{ x: 13, w: 6 }, { x: 24, w: 6 }, { x: 35, w: 6 }, { x: 46, w: 5 }]
.forEach(s => m.rect(s.x, R, s.w, 1, '%'));
const pads = run(56, 4, Array.from({ length: 8 }, () => [4, 1]));
putAll(m, R, pads);
m.hline(R - 5, 56, 100, 'v');
const sp = shaft(m, 17, 5, 103, 126, 6, 2);
[8, 11].forEach(r => m.set(101, r, '>'));
[8, 11, 14, 17].forEach(r => m.set(128, r, '<'));
const top = sp[sp.length - 1];
m.set(4, R - 1, 'S');
m.set(14, R - 1, 'G');
m.set(36, R - 1, 'G');
m.set(pads[4].x, R - 1, 'G');
m.set(sp[2].x, sp[2].row - 1, 'G');
m.set(top.x, top.row - 1, 'X');
return m.out();
})(),
},
// ================= 5 =================
{
id: 'j5', name: 'THE GAUNTLET', type: 'jump', diff: 5, bg: 'jump',
width: 120, hint: '两格宽的移动平台横跨刺海,旁边还有炮台点名。等它靠岸,别抢。',
grid: (() => {
const W = 120, H = 24, R = 19;
const m = new Grid(W, H);
m.border();
m.hline(20, 1, W - 2, '^');
[{ x: 2, w: 6 }, { x: 24, w: 3 }, { x: 46, w: 3 }, { x: 68, w: 3 }, { x: 90, w: 3 }]
.forEach(s => m.rect(s.x, R, s.w, 1, '#'));
[35, 57, 79].forEach(x => m.rect(x, R, 1, 1, '#'));
[30, 52, 74, 86].forEach(x => m.set(x, R - 3, 'O'));
const sp = shaft(m, 17, 5, 96, 116, 6, 2);
for (let r = 9; r < 18; r += 3) { m.set(94, r, '>'); m.set(118, r, '<'); }
const top = sp[sp.length - 1];
m.set(4, R - 1, 'S');
m.set(25, R - 1, 'G');
m.set(47, R - 1, 'G');
m.set(69, R - 1, 'G');
m.set(sp[2].x, sp[2].row - 1, 'G');
m.set(top.x, top.row - 1, 'X');
return m.out();
})(),
movers: [
{ x: 10, y: 19, w: 2, h: 1, path: [[10, 19], [22, 19]], speed: 1.5 },
{ x: 29, y: 19, w: 2, h: 1, path: [[29, 19], [44, 19]], speed: 1.7 },
{ x: 51, y: 19, w: 2, h: 1, path: [[51, 19], [66, 19]], speed: 1.9 },
{ x: 73, y: 19, w: 2, h: 1, path: [[73, 19], [88, 19]], speed: 2.1 },
],
turrets: [
{ dir: 'left', period: 70, speed: 2.6, offset: 0 },
{ dir: 'left', period: 62, speed: 2.8, offset: 20 },
{ dir: 'left', period: 58, speed: 3.0, offset: 40 },
{ dir: 'left', period: 54, speed: 3.2, offset: 10 },
],
},
// ================= 6 =================
{
id: 'j6', name: 'NEEDLE CATHEDRAL', type: 'jump', diff: 5, bg: 'jump',
width: 132, hint: '下层长廊 → 竖井爬 9 层 → 上层长廊横穿回去。132×34,五个存档点。',
grid: (() => {
const W = 132, H = 34;
const m = new Grid(W, H);
m.border();
m.hline(H - 2, 1, W - 2, '^');
// --- 下层长廊 row 29(列 2..86)---
const low = run(2, 6, [
[4, 2], [4, 2], [4, 1], [5, 1], [4, 2], [5, 1],
[4, 1], [5, 2], [4, 1], [5, 1], [4, 2], [5, 1],
[4, 3],
]);
putAll(m, 29, low);
m.hline(30, 1, W - 2, '^');
// --- 竖井:列 88..120,row 27 → 11 ---
const sp = shaft(m, 27, 11, 88, 120, 6, 2);
for (let r = 12; r < 28; r += 4) { m.set(86, r, '>'); m.set(122, r, '<'); }
// --- 上层长廊 row 11,从竖井顶往左横穿 ---
const up = [];
let x = sp[sp.length - 1].x;
while (x > 6) { m.rect(x, 11, 2, 1, '#'); up.push(x); x -= 6; }
m.hline(12, 6, 84, '^'); // 上层地板刺(避开竖井列)
m.hline(6, 6, 84, 'v'); // 上层天花板刺
m.rect(2, 11, 6, 1, '#');
m.set(4, 10, 'X');
m.set(4, 28, 'S');
m.set(low[3].x, 28, 'G');
m.set(low[7].x, 28, 'G');
m.set(low[11].x, 28, 'G');
m.set(sp[3].x, sp[3].row - 1, 'G');
m.set(up[3], 10, 'G');
m.set(up[8], 10, 'G');
return m.out();
})(),
},
// ================= 7 =================
{
id: 'j7', name: 'SKY SPIKES', type: 'jump', diff: 5, bg: 'jump',
width: 72, hint: '72×52。两格宽的平台、六格横移,每两级就有一个平台只有一格宽,两边墙上全是刺。',
grid: (() => {
const W = 72, H = 52;
const m = new Grid(W, H);
m.border();
m.hline(H - 2, 1, W - 2, '^');
let row = H - 3, x = 4, dir = 1;
const pads = [];
while (row >= 5) {
const w = (pads.length % 4 === 3) ? 1 : 2;
m.rect(x, row, w, 1, '#');
pads.push({ x, row });
row -= 2;
const nx = x + dir * 6;
if (nx + 1 > W - 8) dir = -1; else if (nx < 4) dir = 1; else x = nx;
}
for (let r = 8; r < H - 4; r += 2) { m.set(1, r, '>'); m.set(W - 2, r, '<'); }
pads.forEach((p, i) => { if (i % 2 === 0 && p.row - 5 > 2) m.set(p.x, p.row - 5, 'D'); });
pads.forEach((p, i) => { if (i > 0 && i % 5 === 0) m.set(p.x, p.row - 1, 'G'); });
m.set(5, H - 4, 'S');
const top = pads[pads.length - 1];
m.set(top.x, top.row - 1, 'X');
return m.out();
})(),
},
// ================= 8 =================
{
id: 'j8', name: 'TRIPLE THREAT', type: 'jump', diff: 5, bg: 'jump',
width: 130, hint: '冰 → 24 格长的塌陷浮桥 → 一格宽钢丝 → 单向平台直上。四连击。',
grid: (() => {
const W = 130, H = 26, R = 21;
const m = new Grid(W, H);
m.border();
m.hline(22, 1, W - 2, '^');
// 1) 冰面
m.rect(2, R, 6, 1, '%');
[{ x: 13, w: 6 }, { x: 24, w: 6 }, { x: 35, w: 5 }].forEach(s => m.rect(s.x, R, s.w, 1, '%'));
// 2) 消失浮桥
m.rect(41, R, 24, 1, 'V');
// 3) 一格宽钢丝(顶刺,只能单跳)
const pads = run(69, 3, [[4, 1], [4, 1], [4, 1], [4, 1], [4, 1]]);
putAll(m, R, pads);
m.hline(R - 5, 69, 92, 'v');
// 4) 单向平台直上
const sp = shaft(m, 19, 7, 96, 124, 6, 2);
const top = sp[sp.length - 1];
m.set(4, R - 1, 'S');
m.set(14, R - 1, 'G');
m.set(36, R - 1, 'G');
m.set(pads[2].x, R - 1, 'G');
m.set(sp[2].x, sp[2].row - 1, 'G');
m.set(top.x, top.row - 1, 'X');
return m.out();
})(),
},
// ================= 9 =================
{
id: 'j9', name: 'CHERRY BLOSSOM', type: 'jump', diff: 5, bg: 'jump',
width: 128, hint: '苹果挂在钢丝之间的空中。不拿能通关,拿了才算完整——你会为它死很多次。',
grid: (() => {
const W = 128, H = 34;
const m = new Grid(W, H);
m.border();
m.hline(H - 2, 1, W - 2, '^');
// 下层长廊 row 29(列 2..84)
const low = run(2, 6, [
[4, 2], [5, 1], [4, 1], [5, 2], [4, 1], [5, 1], [4, 2], [5, 1], [4, 1],
[5, 2], [4, 1], [5, 2], [4, 1],
]);
putAll(m, 29, low);
m.hline(30, 1, W - 2, '^');
// 竖井:列 88..118,row 27 → 13
const sp = shaft(m, 27, 13, 88, 118, 6, 2);
for (let r = 14; r < 28; r += 4) { m.set(86, r, '>'); m.set(120, r, '<'); }
// 上层钢丝 row 11,从竖井顶往左
const up = [];
let x = sp[sp.length - 1].x;
while (x > 8) { m.rect(x, 11, 1, 1, '#'); up.push(x); x -= 4; }
m.hline(12, 8, 84, '^');
m.hline(6, 8, 84, 'v');
m.rect(2, 11, 6, 1, '#');
// 苹果挂在钢丝之间的空中(要跳过去才拿得到)
[3, 7, 11].forEach(i => { if (up[i]) m.set(up[i] - 2, 8, 'F'); });
m.set(4, 10, 'X');
m.set(4, 28, 'S');
m.set(low[3].x, 28, 'G');
m.set(low[6].x, 28, 'G');
m.set(sp[3].x, sp[3].row - 1, 'G');
m.set(up[4], 10, 'G');
m.set(up[9], 10, 'G');
return m.out();
})(),
},
// ================= 10 =================
{
id: 'j10', name: 'FINAL NEEDLE', type: 'jump', diff: 5, bg: 'jump',
width: 150, hint: '150×40。冰 → 钢丝 → 移动平台 → 竖井 → 钢丝。跳刺的毕业考,祝你活着出去。',
grid: (() => {
const W = 150, H = 40;
const m = new Grid(W, H);
m.border();
m.hline(H - 2, 1, W - 2, '^'); // row 38 刺海
// 1) 冰面 row 35
m.rect(2, 35, 6, 1, '%');
[{ x: 13, w: 6 }, { x: 24, w: 6 }, { x: 35, w: 5 }, { x: 45, w: 5 }]
.forEach(s => m.rect(s.x, 35, s.w, 1, '%'));
m.hline(36, 1, W - 2, '^');
// 2) 钢丝 row 35(顶刺,只能单跳)
const w1 = run(54, 4, [[4, 1], [4, 1], [4, 1], [4, 1], [4, 1], [4, 1], [4, 1]]);
putAll(m, 35, w1);
m.hline(30, 54, 92, 'v');
// 3) 移动平台跨刺海
m.rect(97, 35, 4, 1, '#');
m.rect(118, 35, 4, 1, '#');
// 4) 竖井 列 118..144,row 33 → 9
const sp = shaft(m, 33, 9, 118, 144, 6, 2);
for (let r = 10; r < 34; r += 4) { m.set(116, r, '>'); m.set(146, r, '<'); }
// 5) 上层钢丝 row 7,往左到终点
const w2 = [];
let x = sp[sp.length - 1].x - 2;
while (x > 8) { m.rect(x, 7, 1, 1, '#'); w2.push(x); x -= 4; }
m.hline(8, 6, 114, '^');
m.hline(2, 6, 114, 'v');
m.rect(2, 7, 6, 1, '#');
m.set(4, 6, 'X');
m.set(4, 34, 'S');
m.set(14, 34, 'G');
m.set(36, 34, 'G');
m.set(w1[3].x, 34, 'G');
m.set(98, 34, 'G');
m.set(sp[4].x, sp[4].row - 1, 'G');
m.set(w2[5], 6, 'G');
m.set(w2[12], 6, 'G');
return m.out();
})(),
movers: [
{ x: 104, y: 35, w: 2, h: 1, path: [[104, 35], [116, 35]], speed: 2.0 },
],
},
];
+72
View File
@@ -0,0 +1,72 @@
// 关卡地图工具:让手工 ASCII 不必逐格数点
// G(w, rows) —— 每行不足宽度自动补 '.',最后一列强制为 '#'(外墙)
export function G(w, rows) {
return rows.map(r => {
const s = r.length >= w ? r.slice(0, w) : r + '.'.repeat(w - r.length);
return s.slice(0, w - 1) + '#';
});
}
// 重复字符:rep('%', 20)
export const rep = (ch, n) => ch.repeat(Math.max(0, n | 0));
// 空格行
export const E = w => '#' + rep('.', w - 2);
// 实心跳台:plat(w, 起列, 长度) => 一行带 '#####' 的字符串
export function plat(w, x0, len, ch = '#') {
if (x0 < 1) x0 = 1;
return '#' + rep('.', x0 - 1) + rep(ch, len);
}
// 实体行:ent(w, [列, 字符], ...) => 一行只在指定列放实体字符
export function ent(w, ...items) {
const a = new Array(w).fill('.');
a[0] = '#';
for (const [x, ch] of items) if (x > 0 && x < w - 1) a[x] = ch;
return a.join('');
}
// 地板行:从 col1 开始按 [字符, 长度] 铺满,最后一列由 G 补 '#'
export function flr(...parts) {
let s = '#';
for (const [ch, n] of parts) s += rep(ch, n);
return s;
}
// ---------------- 2D 网格构建器 ----------------
// 大地图用拼字符串极易出错(宽度对不齐 / 数错列),改用网格对象:
// const m = new Map(120, 30); m.rect(x,y,w,h,'#'); ... m.out()
export class Grid {
constructor(w, h, fill = '.') {
this.w = w; this.h = h;
this.g = Array.from({ length: h }, () => new Array(w).fill(fill));
}
set(x, y, ch) {
if (x >= 0 && x < this.w && y >= 0 && y < this.h) this.g[y][x] = ch;
return this;
}
rect(x, y, w, h, ch) {
for (let iy = y; iy < y + h; iy++)
for (let ix = x; ix < x + w; ix++) this.set(ix, iy, ch);
return this;
}
hline(y, x0, x1, ch) { for (let x = x0; x <= x1; x++) this.set(x, y, ch); return this; }
vline(x, y0, y1, ch) { for (let y = y0; y <= y1; y++) this.set(x, y, ch); return this; }
border(ch = '#') {
this.hline(0, 0, this.w - 1, ch);
this.hline(this.h - 1, 0, this.w - 1, ch);
this.vline(0, 0, this.h - 1, ch);
this.vline(this.w - 1, 0, this.h - 1, ch);
return this;
}
out() { return this.g.map(r => r.join('')); }
}
/** 落脚台序列:chain(起始列, 起始宽, [间隙...], 台宽) => [{x,w}, ...] */
export function chain(x0, w0, gaps, padW = 2) {
const out = [{ x: x0, w: w0 }];
let x = x0 + w0;
for (const g of gaps) { x += g; out.push({ x, w: padW }); x += padW; }
return out;
}
+91
View File
@@ -0,0 +1,91 @@
// 坑向(Troll / Trap)关卡:背叛玩家的一切直觉
import { G, rep, E, plat, ent, flr } from './kit.js';
export default [
// ---------------- 1 ----------------
{
id: 'r1', name: 'TRUST NO ONE', type: 'troll', diff: 3, bg: 'troll', width: 56,
hint: '路牌在骗你。那两颗"补给"也不怀好意。地板会塌。',
messages: ['← 地板很结实,放心走 →', '这颗苹果看起来很好吃'],
grid: G(56, [
rep('#', 56), // 0
E(56), E(56), E(56), E(56), E(56), E(56), // 1-6
E(56), E(56), E(56), E(56), E(56), E(56), // 7-12
E(56), // 13
ent(56, [20, 'D'], [38, 'D']), // 14
E(56), // 15
E(56), // 16
ent(56, [2, 'S'], [5, 'T'], [9, '*'], [22, 'G'], [36, '*'], [48, 'T'], [52, 'X']), // 17
flr(['#', 10], ['V', 8], ['#', 8], ['^', 5], ['#', 9], ['V', 6], ['#', 9]), // 18
]),
},
// ---------------- 2 ----------------
{
id: 'r2', name: 'FALSE GOAL', type: 'troll', diff: 4, bg: 'troll', width: 56,
hint: '地上那扇门是假的——走过去就死。真正的终点在右上角。',
messages: ['终点就在前面!→', '真的在上面。'],
grid: G(56, [
rep('#', 56), // 0
E(56), E(56), E(56), E(56), E(56), E(56), // 1-6
ent(56, [50, 'X']), // 7
plat(56, 48, 5), // 8
E(56), // 9
plat(56, 44, 5), // 10
E(56), // 11
plat(56, 38, 5), // 12
E(56), // 13
plat(56, 44, 5), // 14
E(56), // 15
plat(56, 38, 5), // 16
ent(56, [2, 'S'], [8, 'T'], [15, 'G'], [24, '^'], [25, '^'],
[30, 'X'], [36, 'G']), // 17
rep('#', 56), // 18
]),
events: [
{ x: 28, y: 14, w: 6, h: 5, once: false, actions: [{ type: 'kill' }] },
],
},
// ---------------- 3 ----------------
{
id: 'r3', name: 'BLIND', type: 'troll', diff: 4, bg: 'troll', width: 56,
hint: '摄像头接管了视野:你只能看见身边一小块地方。天上下刺也看不见。',
messages: ['摄像头已启动 ★'],
grid: G(56, [
rep('#', 56), // 0
E(56), E(56), E(56), E(56), E(56), E(56), // 1-6
E(56), E(56), E(56), E(56), E(56), E(56), // 7-12
E(56), // 13
ent(56, [18, 'D'], [30, 'D'], [44, 'D']), // 14
E(56), // 15
E(56), // 16
ent(56, [2, 'S'], [6, 'T'], [15, 'G'], [32, 'G'], [45, 'X']), // 17
flr(['#', 8], ['^', 3], ['#', 10], ['^', 4], ['#', 12], ['^', 3], ['#', 9], ['^', 4], ['#', 2]), // 18
]),
camzones: [{ x: 1, y: 14, w: 54, h: 5, mode: 'zoom', zoom: 2.6 }],
},
// ---------------- 4 ----------------
{
id: 'r4', name: 'TROLL PARADISE', type: 'troll', diff: 5, bg: 'troll', width: 64,
hint: '漩涡送你回起点,"补给"会杀你,地板会塌,天上还会下刺雨。',
messages: ['这是最后的考验(笑)', '前面的门是假的'],
grid: G(64, [
rep('#', 64), // 0
E(64), E(64), E(64), E(64), E(64), E(64), // 1-6
E(64), E(64), E(64), E(64), E(64), E(64), // 7-12
E(64), // 13
ent(64, [12, 'D'], [25, 'D'], [38, 'D'], [55, 'D']), // 14
E(64), // 15
E(64), // 16
ent(64, [2, 'S'], [5, 'T'], [7, '*'], [19, 'G'], [30, '*'],
[44, 'W'], [47, 'T'], [58, 'X']), // 17
flr(['#', 8], ['V', 6], ['#', 8], ['^', 4], ['#', 8], ['V', 6], ['#', 8], ['^', 3], ['#', 12]), // 18
]),
warps: [{ to: 'spawn' }],
events: [
{ x: 28, y: 14, w: 5, h: 5, once: false, actions: [{ type: 'spikes', x: 26, y: 0, count: 8, gap: 2 }] },
],
},
];
+47
View File
@@ -0,0 +1,47 @@
// 教学关(短,用来熟悉手感)
import { G, rep, E, plat, ent } from './kit.js';
export default [
{
id: 't1', name: 'FIRST STEPS', type: 'jump', diff: 1, bg: 'jump', width: 36,
hint: '← → 移动,Shift / Z / 空格跳跃。刺碰到就死,R 键重来。',
messages: ['← → 移动 Shift 跳跃 R 重来'],
grid: G(36, [
rep('#', 36),
E(36), E(36), E(36), E(36), E(36), E(36),
ent(36, [30, 'X']),
plat(36, 29, 5),
E(36),
plat(36, 26, 5),
E(36),
plat(36, 22, 5),
E(36),
plat(36, 18, 5),
E(36),
plat(36, 13, 5),
ent(36, [2, 'S'], [4, 'T'], [7, '^'], [8, '^'], [9, '^'], [22, 'G']),
rep('#', 36),
]),
},
{
id: 't2', name: 'DOUBLE JUMP', type: 'jump', diff: 1, bg: 'custom', width: 40,
hint: '空中再按一次跳跃=二段跳。5 格宽的刺坑只能靠二段跳过去。',
messages: ['空中再按一次 Shift = 二段跳'],
grid: G(40, [
rep('#', 40),
E(40), E(40), E(40), E(40), E(40), E(40), E(40),
ent(40, [19, 'X']),
plat(40, 18, 4),
E(40),
plat(40, 26, 4),
E(40),
plat(40, 18, 4),
E(40),
plat(40, 26, 4),
E(40),
plat(40, 18, 4),
ent(40, [2, 'S'], [4, 'T'], [11, '^'], [12, '^'], [13, '^'], [14, '^'], [15, '^'], [19, 'G']),
rep('#', 40),
]),
},
];