refactor: 使用flutter对原先的web项目重构,正在进行中

This commit is contained in:
cancerai committed 2026-10-25 00:06:51 +08:00
1 parent 273224f5cf
commit 18017f819a
205 files changed
+13563

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);
}
+519
View File
@@ -0,0 +1,519 @@
// 跳刺(Needle)10 关 —— IWBT 经典密集针刺风格
// 设计语法(已经 tools/solve.mjs 验证):
// 刺毯缺口每 3 格一个(跳 2 连刺 = 64px 单跳节奏);钻石中心 = 地板上方 2~3 格;
// 垂刺离最低落脚点 5 格(单跳安全、二段跳必死);2 格高矮洞可用撞头跳;
// 竖井台阶每级升 2~3 格横移 3 格内;钢丝柱间距 ≤4(单跳)/ ≤6(二段)。
import { Grid } from './kit.js';
import { vgap, hgap, diamond, spikeRun, block, corner, ceil, wall, well } from './needle.js';
// 刺毯:从 x0 起 n 格留落脚缺口。
// 缺口间距统一 3 格(跳 2 连刺 = 64px)。
// 2026-10-23:JUMP 7.5 + GRAV_FALL 0.55 下单跳水平实测 118.8px(tools/jumpdist.mjs),
// 但落地前一帧竖直速度已达 8.9px/帧,3 连刺(96px)的起拍窗被压到 0~1 帧(不可玩);
// 2 连刺留 55px 射程余量,落地窗恢复充裕。井口/左端右行出口处的 3 连刺须显式拆断(见各关 gaps 注释)
const carpet = (m, x0, y, n, gaps = []) => {
const g = [];
let i = 3;
while (i < n) { g.push(i); i += 3; }
return spikeRun(m, x0, y, n, '^', g.concat(gaps));
};
export const JUMP_LEVELS = [
// ============================================================ j1 刺巷
{
id: 'j1', name: 'SPIKE ALLEY', cn: '刺巷', type: 'jump', diff: 2,
hint: '针刺入门:刺毯单跳节奏、钻石缝、矮洞撞头跳、竖井、对刺门。',
width: 104,
grid: (() => {
const W = 104, H = 26, R = 22;
const m = new Grid(W, H);
m.border('#');
m.rect(1, R, W - 2, 1, '#'); // 主地板
// --- x1-9 起步 ---
m.set(3, R - 1, 'S');
m.set(8, R - 1, 'G');
// --- x10-26 刺毯(单跳节奏,垂刺压顶禁二段,两道上下夹缝)---
carpet(m, 10, R - 1, 17);
ceil(m, 10, 26, R - 6);
vgap(m, 15, R - 2); vgap(m, 21, R - 2);
// --- x28-43 钻石门三连(中心 R-2,单跳穿缝)---
carpet(m, 28, R - 1, 16, [0, 4, 8, 12, 15]); // 加密缺口便于站位
ceil(m, 28, 43, R - 6);
vgap(m, 30, R - 2); vgap(m, 34, R - 2); vgap(m, 38, R - 2);
m.set(28, R - 1, 'G');
// --- x44-56 矮洞(顶 row 18,3 格高,撞头跳 + 对刺门)---
m.rect(44, R - 4, 13, 1, '#');
carpet(m, 45, R - 1, 11);
hgap(m, 48, R - 1); hgap(m, 53, R - 1);
m.set(45, R - 1, 'G');
// --- x57-71 刺坑上的钢丝柱 ---
for (let x = 57; x <= 70; x++) { m.set(x, R, '.'); m.set(x, R + 1, '^'); }
const wires = [[58, 20], [61, 19], [64, 20], [67, 18], [70, 20]];
for (const [x, top] of wires) m.vline(x, top, R - 1, '#');
m.set(72, R - 1, 'G');
// --- x74-79 竖井上上层(井底留 3 格入口)---
m.vline(74, R - 10, R - 4, '#');
m.vline(79, R - 12, R - 1, '#');
block(m, 75, R - 3, 2); // 19
block(m, 77, R - 6, 2); // 16
block(m, 75, R - 9, 2); // 13
wall(m, 79, R - 5, R - 5, '<'); wall(m, 74, R - 7, R - 7, '>');
// --- 上层回廊(地板 row 12)x80-97 ---
m.rect(80, R - 10, 18, 1, '#');
carpet(m, 81, R - 11, 15, [0, 8]); // 8=x89:给对刺门留左侧起拍位(新物理下跳台落点偏右,须能走进 x90)
ceil(m, 81, 95, R - 16); // row 6 垂刺
hgap(m, 92, R - 11); // 对刺门
m.set(82, R - 11, 'G'); m.set(94, R - 11, 'G');
// --- x98-102 下落井 + 终点 ---
wall(m, 99, R - 7, R - 3, '>');
wall(m, 102, R - 5, R - 2, '<');
m.set(100, R - 1, 'X');
return m.out();
})(),
},
// ============================================================ j2 垂直地狱
{
id: 'j2', name: 'VERTICAL HELL', cn: '垂直地狱', type: 'jump', diff: 3,
hint: '52 格高塔:四段之字竖井,每层刺毯+上下夹缝,垂刺压顶、掉落刺追身。',
width: 68,
grid: (() => {
const W = 68, H = 52;
const m = new Grid(W, H);
m.border('#');
// ---- L0 地板 row 48(向右:刺毯 + 三道上下夹缝)----
m.rect(1, 48, 66, 1, '#');
m.set(3, 47, 'S');
carpet(m, 5, 47, 50, [1, 18]); // 1=x6 出生旁缺口;18=x23 放宽落点窗(3 连刺落点贴右腰会被 6px 状态桶卡死)
ceil(m, 5, 54, 43);
vgap(m, 16, 46); vgap(m, 30, 46); vgap(m, 46, 46);
m.set(11, 47, 'G'); m.set(39, 47, 'G');
// ---- 右井 x58..65:L0 → L1(井底 3 格敞口,之字每级升 2 格)----
well(m, 58, 65, 40, 48, 'l');
m.set(65, 45, '^'); m.set(58, 43, '^'); // 踏石外沿刺,逼迫精准落点
// ---- L1 地板 row 40(向左;垂刺避开左井 x2..9)----
m.rect(2, 40, 56, 1, '#');
carpet(m, 10, 39, 44);
ceil(m, 10, 54, 35);
vgap(m, 42, 38); vgap(m, 26, 38); vgap(m, 12, 38);
m.set(47, 39, 'G'); m.set(19, 39, 'G');
// ---- 左井 x2..8:L1 → L2 ----
well(m, 2, 8, 32, 40, 'r');
m.set(2, 37, '^'); m.set(8, 35, '^');
// ---- L2 地板 row 32(向右,井底延伸到右井下方)----
m.rect(9, 32, 57, 1, '#');
carpet(m, 11, 31, 46, [1, 11, 31]); // 1=x12 拆井口 3 连刺;11=x22/31=x42 是 hgap 夹缝通道列(必须为空,否则整门变 3 连刺、4 格跳超射程)
ceil(m, 11, 54, 27);
hgap(m, 22, 31); hgap(m, 42, 31);
vgap(m, 32, 30); vgap(m, 50, 30);
m.set(19, 31, 'G'); m.set(47, 31, 'G');
// ---- 右井 x58..65:L2 → L3 ----
well(m, 58, 65, 24, 32, 'l');
m.set(65, 29, '^'); m.set(58, 27, '^');
// ---- L3 地板 row 24(向左,掉落刺;垂刺避开左井 x2..9)----
m.rect(2, 24, 56, 1, '#');
carpet(m, 10, 23, 44, [0]); // 0=x10:拆掉左端 3 连刺(左行跳落点差 0.8px 蹭 x10 左坡;L1 同位置能过是因 vgap(12) 清空了 x10-14,L3 没有)
ceil(m, 10, 54, 19);
[14, 24, 34, 44].forEach(x => m.set(x, 18, 'D'));
vgap(m, 40, 22); vgap(m, 22, 22);
m.set(47, 23, 'G'); m.set(19, 23, 'G');
// ---- 左井 x2..8:L3 → L4 ----
well(m, 2, 8, 16, 24, 'r');
m.set(2, 21, '^'); m.set(8, 19, '^');
// ---- L4 地板 row 16(向右到终点)----
m.rect(9, 16, 57, 1, '#');
carpet(m, 11, 15, 52, [1, 41]); // 1=x12 拆井口 3 连刺;41=x52 是 hgap 夹缝通道列(必须为空)
ceil(m, 11, 60, 11);
hgap(m, 26, 15); vgap(m, 40, 14); hgap(m, 52, 15);
[18, 34, 56].forEach(x => m.set(x, 10, 'D'));
m.set(27, 15, 'G'); m.set(51, 15, 'G');
m.set(62, 15, 'X');
return m.out();
})(),
},
// ============================================================ j3 钢丝
{
id: 'j3', name: 'TIGHTROPE', cn: '钢丝', type: 'jump', diff: 3,
hint: '一格宽的柱子立在刺海上,高矮间距都不同,垂刺压着头顶。',
width: 132,
grid: (() => {
const W = 132, H = 24, R = 20;
const m = new Grid(W, H);
m.border('#');
m.rect(1, R, W - 2, 1, '#'); // 基座
spikeRun(m, 4, R - 1, W - 6, '^'); // 刺海
// 钢丝柱(顶可站 1 格),高矮错落、间距 3~5
const cols = [
[4, 18], [8, 17], [12, 19], [15, 17], [19, 18], [23, 16], [27, 19],
[30, 17], [34, 18], [38, 16], [42, 19], [46, 17], [50, 15], [54, 18],
[58, 17], [62, 19], [66, 16], [70, 18], [74, 17], [78, 19], [82, 15],
[86, 18], [90, 17], [94, 19], [98, 16], [102, 18], [106, 17], [110, 19],
[114, 16], [118, 18], [122, 17],
];
for (const [x, top] of cols) m.vline(x, top, R - 1, '#');
// 起点/终点平台
m.rect(1, R - 1, 3, 1, '#'); m.set(2, R - 2, 'S');
m.rect(126, R - 1, 4, 1, '#'); m.set(128, R - 2, 'X');
// 垂刺段(离最低柱顶 5 格:单跳安全,二段跳死)
ceil(m, 6, 28, 11); // 段内最低顶 16
ceil(m, 32, 56, 10); // 最低顶 15
ceil(m, 60, 88, 10);
ceil(m, 92, 120, 11);
// 存档(柱顶)
m.set(23, 15, 'G'); m.set(58, 16, 'G'); m.set(90, 16, 'G'); m.set(122, 16, 'G');
return m.out();
})(),
},
// ============================================================ j4 冰刺
{
id: 'j4', name: 'ICE NEEDLES', cn: '冰刺', type: 'jump', diff: 4,
hint: '冰面刹不住车:滑坑、冰上钻石、冰柱钢丝,最后还有一段冰顶矮洞。',
width: 132,
grid: (() => {
const W = 132, H = 26, R = 22;
const m = new Grid(W, H);
m.border('#');
m.rect(1, R, W - 2, 1, '%'); // 冰地板
m.set(3, R - 1, 'S');
// --- x6-30 冰面刺坑(3 格宽:新物理单跳 108.8px > 96px;旧 4~5 格坑跳不过)---
for (const [a, b] of [[7, 9], [13, 15], [19, 21], [25, 27]])
for (let x = a; x <= b; x++) { m.set(x, R, '.'); m.set(x, R + 1, '^'); }
ceil(m, 6, 24, R - 5); // 压顶垂刺:冰面上禁二段跳
m.set(12, R - 1, 'G');
// --- x32-52 冰台阶 + 角刺 + 钻石 ---
block(m, 33, R - 2, 2, '%');
block(m, 38, R - 2, 2);
block(m, 43, R - 4, 2, '%');
ceil(m, 31, 52, R - 5);
vgap(m, 48, R - 2);
m.set(41, R - 1, 'G');
// --- x54-78 冰柱钢丝 ---
spikeRun(m, 54, R - 1, 25, '^');
const ice = [[56, 19], [60, 18], [64, 20], [68, 17], [72, 19], [76, 18]];
for (const [x, top] of ice) m.vline(x, top, R - 1, '%');
ceil(m, 55, 77, 12); // 最低柱顶 17 → 12
m.set(64, 19, 'G');
// --- x80-100 冰顶矮洞(2 格高,撞头跳越刺)---
m.rect(80, R - 3, 21, 1, '%'); // 冰顶 row 19
spikeRun(m, 83, R - 1, 15, '^', [0, 4, 8, 12, 14]);
m.set(81, R - 1, 'G'); m.set(99, R - 1, 'G');
// --- x102-124 冰上钻石阵 ---
carpet(m, 102, R - 1, 23, '%'.length ? [] : []); // 冰面刺毯
ceil(m, 102, 124, R - 5);
vgap(m, 106, R - 2); vgap(m, 111, R - 2);
vgap(m, 116, R - 2); vgap(m, 121, R - 2);
m.set(103, R - 1, 'G');
m.set(126, R - 1, 'G');
m.set(129, R - 1, 'X');
return m.out();
})(),
},
// ============================================================ j5 关口
{
id: 'j5', name: 'THE GAUNTLET', cn: '关口', type: 'jump', diff: 4,
hint: '移动平台跨刺海,炮台点名,矮洞与钻石阵穿插。',
width: 122,
grid: (() => {
const W = 122, H = 28, R = 24;
const m = new Grid(W, H);
m.border('#');
m.rect(1, R, W - 2, 1, '#');
m.set(3, R - 1, 'S');
// --- x5-22 热身刺毯(三道上下夹缝)---
carpet(m, 5, R - 1, 17);
ceil(m, 5, 21, R - 6);
vgap(m, 9, R - 2); vgap(m, 15, R - 2); vgap(m, 21, R - 2);
m.set(7, R - 1, 'G');
// --- x23-49 移动平台跨海 ---
for (let x = 23; x <= 49; x++) { m.set(x, R, '.'); m.set(x, R + 1, '^'); }
m.set(50, R - 1, 'G');
// --- x51-72 矮洞 + 炮台 ---
m.rect(51, R - 4, 22, 1, '#'); // 顶 row 20
carpet(m, 52, R - 1, 20, [0]);
hgap(m, 57, R - 1); hgap(m, 64, R - 1); hgap(m, 70, R - 1);
m.set(56, R - 5, 'O');
m.set(52, R - 1, 'G');
// --- x73-97 浮块跨刺海 + 交叉火力 ---
spikeRun(m, 73, R - 1, 24, '^');
block(m, 76, R - 2, 2); block(m, 82, R - 4, 2); block(m, 88, R - 2, 2); block(m, 93, R - 3, 2);
ceil(m, 75, 96, R - 9); // 浮块区压顶:高块上二段跳必死
m.set(79, R - 6, 'O'); m.set(91, R - 6, 'O');
m.set(95, R - 1, 'G');
// --- x98-118 钻石收尾 ---
carpet(m, 98, R - 1, 20, [0, 1, 19]);
ceil(m, 98, 117, R - 6);
vgap(m, 101, R - 2); vgap(m, 106, R - 2); vgap(m, 111, R - 2); vgap(m, 116, R - 2);
m.set(98, R - 1, 'G');
m.set(119, R - 1, 'G');
m.set(120, R - 1, 'X');
return m.out();
})(),
movers: [
{ x: 25, y: 22, w: 2, h: 1, path: [[25, 22], [35, 22]], speed: 1.6 },
{ x: 39, y: 20, w: 2, h: 1, path: [[39, 20], [47, 20]], speed: 1.9 },
],
turrets: [
{ dir: 'left', period: 70, speed: 2.8, offset: 0 },
{ dir: 'left', period: 62, speed: 3.0, offset: 24 },
{ dir: 'left', period: 66, speed: 2.7, offset: 40 },
],
},
// ============================================================ j6 针刺教堂
{
id: 'j6', name: 'NEEDLE CATHEDRAL', cn: '针刺教堂', type: 'jump', diff: 5,
hint: '三层殿堂:底层向右,竖井爬升,中层折返向左,再爬升,顶层冲刺。',
width: 132,
grid: (() => {
const W = 132, H = 36;
const m = new Grid(W, H);
m.border('#');
// ============ L0 底层(地板 row 30)============
m.rect(1, 30, W - 2, 1, '#');
m.set(3, 29, 'S');
carpet(m, 5, 29, 116);
ceil(m, 5, 120, 24);
vgap(m, 18, 28); vgap(m, 46, 28); vgap(m, 74, 28); vgap(m, 102, 28);
block(m, 32, 29, 2); block(m, 60, 29, 2);
block(m, 88, 29, 2); block(m, 112, 29, 2);
m.set(11, 29, 'G'); m.set(55, 29, 'G'); m.set(95, 29, 'G');
// ============ 右井 x122-129 → L1(左墙底部留 3 格入口)============
m.vline(122, 17, 26, '#'); m.vline(129, 16, 29, '#');
block(m, 124, 27, 2); block(m, 126, 24, 2); block(m, 124, 21, 2); block(m, 126, 18, 2);
wall(m, 129, 26, 26, '<'); wall(m, 122, 23, 23, '>'); wall(m, 129, 20, 20, '<');
// ============ L1 中层(地板 row 16,向左折返,延伸到两侧井底)============
m.rect(1, 16, 122, 1, '#');
carpet(m, 9, 15, 112, [86]); // 86=x95:补 hgap(95) 通道列(否则门芯留一根 '^',GRAV_FALL 0.40 下回跳轨迹下降慢正好嵌进去)
ceil(m, 9, 118, 10);
vgap(m, 108, 14); vgap(m, 82, 14); vgap(m, 56, 14); vgap(m, 30, 14);
hgap(m, 95, 15); hgap(m, 43, 15);
m.set(44, 15, '.'); // 拆掉对刺门左刺'<':GRAV 0.33 下从 x45 起跳第 3 帧必被刺尖别住(起拍窗窄到 2px),清掉后留 ^+> 双刺门,整格起拍都能过
block(m, 69, 15, 2); block(m, 17, 15, 2);
m.set(113, 15, 'G'); m.set(63, 15, 'G'); m.set(21, 15, 'G');
// ============ 左井 x2-7 → L2(右墙底部留 3 格入口,桥接块接 L2 地板边)============
m.vline(8, 9, 12, '#');
block(m, 4, 13, 2); block(m, 2, 10, 2); block(m, 4, 7, 2);
block(m, 6, 7, 2); // 桥
wall(m, 8, 11, 11, '<'); wall(m, 2, 10, 10, '>');
// ============ L2 顶层(地板 row 8,向右到终点)============
m.rect(8, 8, 118, 1, '#');
carpet(m, 9, 7, 114);
m.set(8, 7, '.'); m.set(9, 7, '.'); m.set(10, 7, '.'); m.set(11, 7, '.'); // 井口净空
ceil(m, 9, 122, 2);
vgap(m, 24, 6); vgap(m, 60, 6); vgap(m, 96, 6);
hgap(m, 36, 7); hgap(m, 84, 7);
m.set(15, 7, 'G'); m.set(51, 7, 'G'); m.set(87, 7, 'G');
m.set(122, 7, 'X');
return m.out();
})(),
},
// ============================================================ j7 天降刺
{
id: 'j7', name: 'SKY SPIKES', cn: '天降刺', type: 'jump', diff: 4,
hint: '之字爬塔,每层头顶悬着掉落刺。走近就掉。别停。',
width: 72,
grid: (() => {
const W = 72, H = 52;
const m = new Grid(W, H);
m.border('#');
m.rect(1, 48, W - 2, 1, '#');
m.set(3, 47, 'S');
// 6 层之字平台:每层刺毯 + 钻石 + 掉落刺
const tiers = [
{ y: 44, x0: 4, x1: 30 }, { y: 38, x0: 34, x1: 62 },
{ y: 32, x0: 6, x1: 34 }, { y: 26, x0: 38, x1: 64 },
{ y: 20, x0: 8, x1: 36 }, { y: 14, x0: 40, x1: 64 },
];
for (const t of tiers) {
m.rect(t.x0, t.y, t.x1 - t.x0 + 1, 1, '#');
// 3/4 格交替刺毯;两端各留 2 格净空作为上一层跃入 / 下一层起跳的落脚点
const n = t.x1 - t.x0 + 1;
carpet(m, t.x0, t.y - 1, n, [0, 1, n - 2, n - 1]);
for (let x = t.x0 + 5; x <= t.x1 - 3; x += 7) m.set(x, t.y - 8, 'D');
ceil(m, t.x0 + 2, t.x1 - 2, t.y - 5); // 压顶垂刺:层内二段跳必死
vgap(m, Math.floor((t.x0 + t.x1) / 2), t.y - 2);
}
// 层间连接块(升 2 格,落点对准缺口)
block(m, 32, 46, 2);
block(m, 32, 42, 2); block(m, 34, 40, 2);
block(m, 32, 36, 2); block(m, 30, 34, 2);
block(m, 36, 30, 2); block(m, 38, 28, 2);
block(m, 36, 24, 2); block(m, 34, 22, 2);
block(m, 38, 18, 2); block(m, 40, 16, 2);
// 存档(各层缺口上)
m.set(7, 43, 'G'); m.set(37, 37, 'G'); m.set(9, 31, 'G');
m.set(41, 25, 'G'); m.set(11, 19, 'G'); m.set(43, 13, 'G');
m.set(60, 13, 'X');
return m.out();
})(),
},
// ============================================================ j8 三重威胁
{
id: 'j8', name: 'TRIPLE THREAT', cn: '三重威胁', type: 'jump', diff: 4,
hint: '冰面滑行、塌陷浮桥、单向台攀爬三连考,中间不许喘气。',
width: 130,
grid: (() => {
const W = 130, H = 26, R = 22;
const m = new Grid(W, H);
m.border('#');
m.rect(1, R, W - 2, 1, '#');
m.set(3, R - 1, 'S');
// --- x1-34 冰面段 ---
m.rect(1, R, 36, 1, '%');
for (const [a, b] of [[7, 9], [13, 15], [19, 21], [26, 29]])
for (let x = a; x <= b; x++) { m.set(x, R, '.'); m.set(x, R + 1, '^'); }
vgap(m, 11, R - 2); vgap(m, 17, R - 2); vgap(m, 24, R - 2); vgap(m, 30, R - 2);
m.set(11, R - 1, 'G'); m.set(32, R - 1, 'G');
// --- x38-74 塌陷浮桥(桥下刺海)---
for (let x = 38; x <= 74; x++) { m.set(x, R, '.'); m.set(x, R + 1, '^'); }
for (let x = 38; x <= 74; x++) m.set(x, R - 3, 'V');
m.set(46, R - 4, '^'); m.set(50, R - 4, '^'); m.set(54, R - 4, '^');
m.set(58, R - 4, '^'); m.set(62, R - 4, '^'); m.set(66, R - 4, '^'); m.set(70, R - 4, '^');
ceil(m, 40, 72, R - 9); // 垂刺限高
m.set(51, R - 4, 'G');
// --- x76-100 单向台攀爬 ---
for (let x = 76; x <= 99; x++) { m.set(x, R, '.'); m.set(x, R + 1, '^'); }
m.set(78, R - 2, '-'); m.set(79, R - 2, '-');
m.set(83, R - 4, '-'); m.set(84, R - 4, '-');
m.set(87, R - 6, '-'); m.set(88, R - 6, '-'); m.set(89, R - 6, '-');
m.set(93, R - 4, '-'); m.set(94, R - 4, '-');
m.set(97, R - 2, '-'); m.set(98, R - 2, '-');
// x86 只留压顶刺:JUMP 8.0 下 apex 盒底 479 与 '^' 顶点 480 只差 1px,
// 上升航线有 4 帧顶点落在盒内必死(8.5 时 apex 466 可从容擦过),故去掉地刺
m.set(86, R - 9, 'v');
vgap(m, 93, R - 6);
m.set(101, R - 1, 'G');
// --- x102-126 钻石收尾 ---
carpet(m, 102, R - 1, 24, [0, 1, 20, 21]);
ceil(m, 102, 125, R - 5);
vgap(m, 105, R - 2); vgap(m, 110, R - 2);
vgap(m, 115, R - 2); vgap(m, 120, R - 2);
m.set(102, R - 1, 'G');
m.set(126, R - 1, 'X');
return m.out();
})(),
},
// ============================================================ j9 樱花
{
id: 'j9', name: 'CHERRY BLOSSOM', cn: '樱花', type: 'jump', diff: 5,
hint: '双层结构收 3 个苹果:钻石缝中央一个,矮洞里一个,中层对刺门后一个。',
width: 128,
grid: (() => {
const W = 128, H = 34;
const m = new Grid(W, H);
m.border('#');
// ============ L0 下层(地板 row 28)============
m.rect(1, 28, W - 2, 1, '#');
m.set(3, 27, 'S');
carpet(m, 5, 27, 116, [54]); // 54=x59:拆开矮洞入口的四格跳(洞顶 row24 会挡跳的横向漂移)
ceil(m, 5, 120, 22);
vgap(m, 14, 26); vgap(m, 38, 26);
vgap(m, 86, 26); vgap(m, 110, 26);
block(m, 26, 27, 2); block(m, 50, 27, 2);
block(m, 74, 27, 2); block(m, 98, 27, 2);
m.set(11, 27, 'G'); m.set(55, 27, 'G'); m.set(95, 27, 'G');
// 井口前净空(否则刺毯把竖井入口封死)
m.set(117, 27, '.'); m.set(118, 27, '.'); m.set(119, 27, '.'); m.set(120, 27, '.');
// 苹果 1:钻石缝中央
m.set(62, 26, 'F');
// ============ L0 中段矮洞(顶 row 24,3 格高;撞头跳射程≈3 格,洞内缺口加密到 3)============
m.rect(60, 24, 16, 1, '#');
[63, 66, 69, 72, 75].forEach(x => m.set(x, 27, '.'));
// 苹果 2:矮洞里(跳跃途中拾取)
m.set(66, 26, 'F');
// ============ 右井 x120-126 → L1(左墙底部留 3 格入口)============
m.vline(119, 15, 24, '#'); m.vline(126, 14, 27, '#');
block(m, 121, 25, 2); block(m, 124, 22, 2); block(m, 121, 19, 2); block(m, 124, 16, 2);
wall(m, 126, 24, 24, '<'); wall(m, 119, 21, 21, '>'); wall(m, 126, 18, 18, '<');
// ============ L1 上层(地板 row 14,向左折返,延伸到井底)============
m.rect(2, 14, 118, 1, '#');
carpet(m, 9, 13, 108, [27, 59]); // 27=x36 / 59=x68:补对刺门通道列(否则整门是一面 3 连刺,往回跳必被 < 刺别住)
ceil(m, 9, 116, 8);
hgap(m, 100, 13); hgap(m, 68, 13); hgap(m, 36, 13);
vgap(m, 84, 12); vgap(m, 52, 12); vgap(m, 20, 12);
m.set(105, 13, 'G'); m.set(59, 13, 'G');
// 苹果 3:对刺门后
m.set(44, 12, 'F');
// ============ 终点(上层左端)============
m.set(10, 13, 'X');
return m.out();
})(),
},
// ============================================================ j10 毕业
{
id: 'j10', name: 'FINAL NEEDLE', cn: '毕业针刺', type: 'jump', diff: 5,
hint: '四层大考场:刺毯长廊 → 冰面折返 → 矮洞 → 高空钢丝。全程无尿点。',
width: 150,
grid: (() => {
const W = 150, H = 44;
const m = new Grid(W, H);
m.border('#');
// ============ L0 刺毯长廊(地板 row 38,向右)============
m.rect(1, 38, W - 2, 1, '#');
m.set(3, 37, 'S');
carpet(m, 5, 37, 134);
ceil(m, 5, 138, 32);
vgap(m, 20, 36); vgap(m, 48, 36); vgap(m, 76, 36);
vgap(m, 104, 36); vgap(m, 128, 36);
block(m, 34, 37, 2); block(m, 62, 37, 2);
block(m, 90, 37, 2); block(m, 118, 37, 2);
m.set(11, 37, 'G'); m.set(59, 37, 'G'); m.set(107, 37, 'G');
// ============ 右井 A x140-147 → L1(左墙底部留 3 格入口)============
m.vline(139, 29, 34, '#'); m.vline(147, 28, 37, '#');
block(m, 141, 35, 2); block(m, 144, 32, 2); block(m, 141, 29, 2); block(m, 144, 26, 2);
wall(m, 147, 34, 34, '<'); wall(m, 139, 31, 31, '>');
// ============ L1 冰面层(地板 row 28,向左,延伸到井底)============
m.rect(2, 28, 138, 1, '%');
carpet(m, 11, 27, 126);
m.set(136, 27, '.'); m.set(137, 27, '.'); m.set(138, 27, '.'); // 井 A 出口落点净空
ceil(m, 11, 136, 22);
vgap(m, 116, 26); vgap(m, 88, 26); vgap(m, 60, 26); vgap(m, 32, 26);
block(m, 102, 27, 2); block(m, 74, 27, 2); block(m, 46, 27, 2);
m.set(121, 27, 'G'); m.set(69, 27, 'G'); m.set(23, 27, 'G');
// ============ 左井 B x2-8 → L2(右墙底部留 3 格入口)============
m.vline(8, 17, 24, '#');
block(m, 4, 25, 2); block(m, 2, 22, 2); block(m, 4, 19, 2); block(m, 5, 17, 2);
wall(m, 8, 21, 21, '<'); wall(m, 2, 20, 20, '>'); wall(m, 8, 18, 18, '<');
// ============ L2 矮洞层(地板 row 16,向右,延伸到井 C 底)============
m.rect(8, 16, 138, 1, '#');
m.rect(10, 12, 128, 1, '#'); // 矮洞顶(3 格高洞)
carpet(m, 10, 15, 128);
m.set(8, 15, '.'); m.set(9, 15, '.'); // 井 B 出口净空
hgap(m, 72, 15);
m.set(19, 15, 'G'); m.set(67, 15, 'G'); m.set(115, 15, 'G');
// ============ 右井 C x140-147 → L3(左墙底部留 3 格入口,桥接块接 L3)============
m.vline(139, 9, 12, '#'); m.vline(147, 8, 15, '#');
block(m, 141, 13, 2); block(m, 144, 10, 2); block(m, 141, 7, 2);
block(m, 137, 7, 4); // 桥:向左落到 L3 基座
wall(m, 147, 12, 12, '<'); wall(m, 139, 10, 10, '>');
// ============ L3 高空钢丝(基座 row 8 + 刺毯 row 7,开放高空,向左到终点)============
m.rect(8, 8, 130, 1, '#');
spikeRun(m, 9, 7, 128, '^'); // 刺毯基底
for (let x = 133; x <= 137; x++) m.set(x, 7, '.'); // 井口落脚区
// 钢丝柱:间距 3~6 不规则,柱顶 row 5~7 错落
const wires = [];
const steps = [4, 5, 3, 6, 4, 3, 5, 4, 6, 3, 5, 4, 4, 6, 3, 5, 4, 6, 4, 3, 5, 4, 6, 3, 5, 4, 4, 3];
let wx = 132;
wires.push(wx);
for (const s of steps) { wx -= s; if (wx < 14) break; wires.push(wx); }
wires.forEach((x, i) => m.vline(x, [6, 5, 7, 6, 7, 5][i % 6], 7, '#'));
m.vline(55, 5, 7, '#'); // 过渡柱:x58→x52 原为 6 格跳,HSPD 2.6 下"晚二段"落点整体左移 8px 掉过柱顶摔死;x55 拆成 3+3 格两跳(dj=12 轨迹实测两跳都能精确接住)
// 存档放在两根钢丝顶上
const g1 = wires[6], g2 = wires[Math.floor(wires.length / 2) + 4];
const topOf = i => [6, 5, 7, 6, 7, 5][i % 6];
m.set(g1, topOf(6) - 1, 'G'); m.set(g2, topOf(wires.indexOf(g2)) - 1, 'G');
m.rect(8, 7, 4, 1, '#'); // 终点台
m.set(9, 6, 'X');
return m.out();
})(),
},
];
export default JUMP_LEVELS;
+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;
}
+79
View File
@@ -0,0 +1,79 @@
// 经典针刺阵型库(IWBT 风格):所有函数基于 Grid,坐标单位 = 格
import { Grid } from './kit.js';
// 上下对刺夹缝(横向穿越 1 格高的缝):缝在行 y。
// 自动清理两侧地毯刺作起跳/落点(否则有效跳距翻倍,无解)。
export function vgap(m, x, y) {
m.set(x, y - 1, 'v'); m.set(x, y + 1, '^');
m.set(x - 1, y + 1, '.'); m.set(x + 1, y + 1, '.');
m.set(x - 2, y + 1, '.'); m.set(x + 2, y + 1, '.');
}
// 左右对刺夹缝(竖向穿越 1 格宽的缝):缝在列 x,自动清理两侧落点。
export function hgap(m, x, y) {
m.set(x - 1, y, '>'); m.set(x + 1, y, '<');
m.set(x - 2, y, '.'); m.set(x + 2, y, '.');
}
// 钻石阵:中心空格 + 四向刺(经典 diamond jump)
// 注意:矩形判定盒下,完整钻石的横向穿越窗口只有几像素——
// 作为"跳穿门"请用 vgap/hgap;diamond 只用作翻越/绕行的障碍或装饰。
export function diamond(m, x, y) { vgap(m, x, y); hgap(m, x, y); }
// 一排刺,gaps 为留空偏移(相对 x0)
export function spikeRun(m, x0, y, n, ch = '^', gaps = []) {
for (let i = 0; i < n; i++) if (!gaps.includes(i)) m.set(x0 + i, y, ch);
}
// 悬浮块,顶面铺刺(不可站)/ 或纯块
export function block(m, x, y, w = 1, h = 1, ch = '#') { m.rect(x, y, w, h, ch); }
// 悬浮块 + 顶刺
export function spikedBlock(m, x, y, w = 1) { m.rect(x, y, w, 1, '#'); spikeRun(m, x, y - 1, w, '^'); }
// 天花板垂刺
export function ceil(m, x0, x1, y) { m.hline(y, x0, x1, 'v'); }
// 墙刺竖段
export function wall(m, x, y0, y1, ch) { m.vline(x, y0, y1, ch); }
// 角刺块:w×h 实心块,某个外角带 L 形双刺(经典 corner)
// dir: 'tl' 左上角 / 'tr' 右上 / 'bl' 左下 / 'br' 右下
export function corner(m, x, y, w, h, dir) {
m.rect(x, y, w, h, '#');
if (dir === 'tl') { m.set(x, y - 1, '^'); m.set(x - 1, y, '>'); }
if (dir === 'tr') { m.set(x + w - 1, y - 1, '^'); m.set(x + w, y, '<'); }
if (dir === 'bl') { m.set(x, y + h, 'v'); m.set(x - 1, y + h - 1, '>'); }
if (dir === 'br') { m.set(x + w - 1, y + h, 'v'); m.set(x + w, y + h - 1, '<'); }
}
// 之字踏石竖井(可解性已用 tools/solve.mjs 验证)
// xL..xR 为井内列;topRow = 上层地板行,botRow = 下层地板行(差须为偶数且 ≥4)
// floorSide: 'l' 上层地板在井的左侧(井在右侧)/ 'r' 反之
// 规则:
// 1) 踏石每级升 2 格(单跳 90px > 64px),左右交替,宽度 w 使两级有 2 格重叠
// —— overlap 让"上升后横移"只需 1 格左右,避免横向距离超过滞空距离。
// 2) 靠地板一侧的井壁从 topRow+1 起到 botRow-4,底部留 3 格入口。
// 3) 顶台位于 topRow 且与上层地板紧邻(同高相接),不存在"洞口被顶台封死"。
export function well(m, xL, xR, topRow, botRow, floorSide = 'l', opts = {}) {
const w = opts.w || 5;
let i = 0;
for (let r = topRow; r <= botRow - 2; r += 2, i++) {
const onLeft = (i % 2 === 0) === (floorSide === 'l');
m.rect(onLeft ? xL : xR - w + 1, r, w, 1, '#');
}
const floorX = floorSide === 'l' ? xL - 1 : xR + 1;
const backX = floorSide === 'l' ? xR + 1 : xL - 1;
m.vline(backX, topRow, botRow, '#');
m.vline(floorX, topRow + 1, botRow - 4, '#');
return m;
}
// 竖井:在 (x0..x1) 之间开竖直通道,交替放小平台,两侧可布墙刺
// plats: [{y, side:'l'|'r', w}],spikeEvery: 每隔几行在对面墙上放刺
export function shaft(m, x0, x1, yTop, yBot, plats, opts = {}) {
for (const p of plats) {
const px = p.side === 'l' ? x0 : x1 - p.w + 1;
m.rect(px, p.y, p.w, 1, '#');
if (p.spike) spikeRun(m, px, p.y - 1, p.w, '^', p.spikeGaps || []);
}
if (opts.wallSpikes) {
const { side = 'r', every = 3, from = yTop + 1, to = yBot - 1 } = opts.wallSpikes;
for (let y = from; y <= to; y += every) {
if (plats.some(p => Math.abs(p.y - y) <= 1)) continue;
m.set(side === 'r' ? x1 : x0, y, side === 'r' ? '<' : '>');
}
}
}
export { Grid };
+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),
]),
},
];