Playtest #5 follow-up: boss-field blowers show in the normal rounds; 联防 timer restart confirmed

- act2 m01: the boss field's row-6 blower machines stand under the bench in the normal and 联防 views again
  (the user: "吹风机原版道中也该有"); the integration-QA filter (BOSS_WALL_ROW) is removed from the 3D and 2D
  boards, so every device on a built tile is drawn in every view, as the workstream had it
- 联防: a knocked-out operator's redeploy timer restarting in full is confirmed by the user ("进联防复活时间确实是
  重新算"); the [ASSUMED] tags in DESIGN §5.5 / §6.1 / §19.3, META §4 and unite.js say so
- DESIGN §19 intro / §19.7 and the docs-consistency assertions updated; 2595 unit tests, all render browser suites green
This commit is contained in:
sganggs committed 2026-09-30 20:32:47 +08:00
1 parent 15291c2293
commit e6628f0cfe
7 files changed
+53 -60

No files matched your search

+17 -20
View File
@@ -5,19 +5,18 @@
// were never built (their airflow streaks — Pixi — still showed), while the boss field's were (the Final Assault view
// and — facing away, under the bench on the row-6 wall — the normal view).
// Now: the normal / 联防 areas take the separator row 13, and every active device standing on a built tile is drawn
// (render/board3d/layout.js stageDevices; the 2D board render/tiles.js _stageDevices follows the same rule) — except
// the boss field's (rows ≤ 6, its top wall under the bench included), drawn with the boss field only: the official
// normal cameras never show that wall (under the shop bar in prep, off-screen in battle), and the remake's prep view
// showed the boss round's machines under the bench ("反而在关底吹风机的贴图又出现了", QA residual of the integration).
// (render/board3d/layout.js stageDevices; the 2D board render/tiles.js _stageDevices follows the same rule — it used to
// drop an edge-wall device blowing away from the field, so the row-6 machines under the bench now show there too, as
// the user confirmed for the official normal rounds: "吹风机原版道中也该有", playtest #5 follow-up).
// Audit: for every stage and view the 3D board and the 2D board draw the same devices, and every active device is
// drawn exactly in the views whose area holds its tile (the boss field's only in the boss views).
// drawn exactly in the views whose area holds its tile.
import { test, describe } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { buildBoard, stageDevices, classifyStage, AREAS, BOSS_WALL_ROW } from '../../public/js/render/board3d/layout.js';
import { buildBoard, stageDevices, classifyStage, AREAS } from '../../public/js/render/board3d/layout.js';
import { boardArea, bandFor, fieldRows, viewKind } from '../../public/js/render/app.js';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
@@ -63,12 +62,14 @@ async function devicesIn2d(st, vk) {
const VIEWS = ['prep', 'normal', 'unite', 'bossPrep', 'boss', 'pen'];
describe('#6 act2 m01 blowers in every view', () => {
test('repro: the normal / prep view builds the own field\'s row-13 blowers (cols 5, 9) above the wind lanes — and not the boss field\'s row-6 machines under the bench', () => {
for (const vk of ['prep', 'normal']) assert.deepEqual(blowersIn3d(M01, vk), ['13,5', '13,9'], vk);
test('repro: the normal / prep view builds the own field\'s row-13 blowers (cols 5, 9) above the wind lanes', () => {
// (plus the boss field's row-6 machines on the wall under the bench, as in v2.3 — they blow away from this field)
for (const vk of ['prep', 'normal']) assert.deepEqual(blowersIn3d(M01, vk), ['13,5', '13,9', '6,5', '6,9'], vk);
});
test('联防 builds both halves\' row-13 blowers; the Final Assault builds the four row-6 blowers', () => {
assert.deepEqual(blowersIn3d(M01, 'unite'), ['13,13', '13,17', '13,5', '13,9']);
// (the partner's row-6 machines (6,11) / (6,15) stand off the island: row 7–8 are '#' right of col 9 in 联防)
assert.deepEqual(blowersIn3d(M01, 'unite'), ['13,13', '13,17', '13,5', '13,9', '6,5', '6,9']);
for (const vk of ['boss', 'bossPrep', viewKind('hidden'), viewKind('prep', { rect: { r0: 0, r1: 5, c0: 0, c1: 10 } })]) {
assert.deepEqual(blowersIn3d(M01, vk), ['6,11', '6,15', '6,5', '6,9'], vk);
}
@@ -81,10 +82,9 @@ describe('#6 act2 m01 blowers in every view', () => {
assert.equal(t.glyph, 'X');
assert.ok(t.drawn && d.z0 === t.h && t.h > 0, 'on top of the drawn separator block');
assert.equal(d.dir, 'DOWN');
// row 13: onto the built field's lanes (rows 12–10)
assert.equal(d.r, 13, `${d.r},${d.c}: only the field's own machines`);
// row 13: onto the built field's lanes (rows 12–10); row 6: into the boss field, which this view does not build
const onto = d.rangeTiles.slice(1).map(([r, c]) => !!b.grid[r][c].content);
assert.deepEqual(onto, [true, true, true], `${d.r},${d.c}`);
assert.deepEqual(onto, d.r === 13 ? [true, true, true] : [false, false, false], `${d.r},${d.c}`);
}
// the separator row is built along the field's top edge (the pen rows 14–18 are still not)
const rows = new Set(b.grid.flat().filter((t) => t.drawn).map((t) => t.r));
@@ -113,8 +113,7 @@ describe('#6 audit: every device of every stage is drawn in the views of the fie
test('every active device standing on the built island of a view is drawn — every field device and every device acting on the field among them', () => {
const KINDS = new Set(['crate', 'platform', 'mound', 'blower', 'turret', 'waterPlatform', 'bush', 'sealedFloor']);
const inRect = (r, c, R) => r >= R.r0 && r <= R.r1 && c >= R.c0 && c <= R.c1;
// the fields inside their separator walls (normal / 联防: rows 7–12; boss: rows 0–5); the boss field's devices
// (rows ≤ 6: its top wall under the bench too) are drawn in the boss views only
// the fields inside their separator walls (normal / 联防: rows 7–12; boss: rows 0–5)
const INNER = { prep: { r0: 7, r1: 12, c0: 0, c1: 10 }, unite: { r0: 7, r1: 12, c0: 0, c1: 20 }, boss: { r0: 0, r1: 5, c0: 0, c1: 20 } };
for (const [id, st] of Object.entries(stages)) {
for (const [vk, F] of Object.entries(INNER)) {
@@ -124,7 +123,7 @@ describe('#6 audit: every device of every stage is drawn in the views of the fie
if (!KINDS.has(d.role) || !(typeof d.active === 'boolean' ? d.active : !d.hidden)) continue;
const [r, c] = d.pos;
const key = `${d.role}@${r},${c}`;
const onIsland = boardArea(vk).some((a) => inRect(r, c, a)) && !!b.grid[r]?.[c]?.drawn && (vk === 'boss' || r > BOSS_WALL_ROW);
const onIsland = boardArea(vk).some((a) => inRect(r, c, a)) && !!b.grid[r]?.[c]?.drawn;
const acts = (d.rangeTiles || []).some(([rr, cc]) => (rr !== r || cc !== c) && inRect(rr, cc, F) && b.grid[rr]?.[cc]?.content);
assert.equal(built.has(key), onIsland, `${id} ${vk}: ${key} (${d.alias || d.key}) drawn iff on the built island`);
if (inRect(r, c, F) || acts) assert.ok(built.has(key), `${id} ${vk}: ${key} of this field drawn`);
@@ -133,12 +132,10 @@ describe('#6 audit: every device of every stage is drawn in the views of the fie
}
});
test('stageDevices keeps every active device on a drawn tile — the boss field\'s only when the area builds the boss field', () => {
const blowers = (area) => stageDevices(M01, classifyStage(M01, area), area).filter((d) => d.kind === 'blower').map((d) => `${d.r},${d.c}`).sort();
test('stageDevices keeps every active device on a drawn tile', () => {
const blowers = (area) => stageDevices(M01, classifyStage(M01, area)).filter((d) => d.kind === 'blower').map((d) => `${d.r},${d.c}`).sort();
assert.deepEqual(blowers(AREAS.boss), ['6,11', '6,15', '6,5', '6,9']);
assert.deepEqual(blowers(AREAS.normal), ['13,13', '13,5', '13,9'], 'the pen block\'s front wall holds the partner\'s (13,13); the row-6 wall under the bench none');
assert.deepEqual(blowers([...AREAS.boss, ...AREAS.normal]), ['13,13', '13,5', '13,9', '6,11', '6,15', '6,5', '6,9'], 'a camera flight between the fields: both');
assert.deepEqual(blowers(AREAS.normal), ['13,13', '13,5', '13,9', '6,5', '6,9'], 'the pen block\'s front wall holds the partner\'s (13,13)');
assert.equal(blowers(AREAS.all).length, 8, 'the whole map: all 8');
assert.equal(stageDevices(M01, classifyStage(M01, null)).filter((d) => d.kind === 'blower').length, 8, 'no area: the whole map');
});
});