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

+12 -21
View File
@@ -5,9 +5,9 @@
// const board = buildBoard(stage, { uv: resolveUvTable(tiles), crate?: objMesh, area?: AREAS.normal })
// → { grid, buckets: { board, decal, pipe }, gates: [{ r, c, kind: 'start'|'end' }], devices: [...],
// terrain: { water, mire, smog, infection: [[r,c]…] }, edges: [...cyan field edge strips], bounds, stats }
// `devices` = the active stage devices standing on a built tile — the separator row 13 included (act2 m01's blowers at
// the top edge of the normal / 联防 field); the boss field's (rows ≤ 6: its row-6 blowers) only when the boss field is
// built — user playtest #5 item 6 (stageDevices).
// `devices` = the active stage devices standing on a built tile — separator walls included (act2 m01's blowers: the
// row-13 ones at the top edge of the normal / 联防 field, the row-6 ones at its bottom edge and at the top edge of the
// Final Assault field — user playtest #5 item 6 and its follow-up).
//
// World space = render/projection.js: x = col, y = row, z = height (up); tile (r,c) covers [c±½]×[r±½].
// Heights follow render/style.js TILE_H (the Pixi layers stand units on the same tops: tiles.js heightAt).
@@ -52,8 +52,7 @@ const CONTENT_OF = (g) => g !== '#' && g !== 'X';
* boss field): the devices standing on it act on the field — act2 m01's 源石流发生装置 #001/#002 (#101/#102) at (13, 5 /
* 9 / 13 / 17) blow DOWN into rows 12–10 — and the official normal rounds show those machines at the field's top edge
* (user playtest #5 item 6: the wind lanes worked but the blowers were missing; the 2D board always drew row 13). The
* row-6 wall under the bench is built in these views too (in leftNormal), without the boss field's devices on it
* (stageDevices).
* row-6 wall under the bench is built in these views too (in leftNormal), with the boss field's blowers standing on it.
*/
export const AREAS = Object.freeze({
normal: Object.freeze([Object.freeze({ r0: 6, r1: 13, c0: 0, c1: 10 }), Object.freeze({ r0: 13, r1: 18, c0: 6, c1: 14 })]),
@@ -440,7 +439,7 @@ export function buildBoard(stage, opts = {}) {
}
}
const devices = opts.devices === false ? [] : stageDevices(stage, grid, opts.area || null);
const devices = opts.devices === false ? [] : stageDevices(stage, grid);
for (const d of devices) {
if (d.kind === 'platform' || d.kind === 'sealedFloor' || d.kind === 'waterPlatform') slabDevice(board, decal, UVT, d, grid);
}
@@ -487,23 +486,16 @@ export function tube(g, a, b, rad, seg = 8, col = [1, 1, 1]) {
const DEVICE_KINDS = new Set(['crate', 'platform', 'mound', 'blower', 'turret', 'waterPlatform', 'bush', 'sealedFloor']);
/** The boss field's top wall — the row under the normal field's bench (rows ≤ this one are the boss field's). */
export const BOSS_WALL_ROW = 6;
/**
* Active stage devices standing on drawn tiles: `{ kind, r, c, dir, z0, key, alias, rangeTiles }` — every visible
* predefined token of the level is a scene object, a device on the separator row 13 included (act2 m01's 源石流发生装置
* #001 / #002 blow DOWN into the normal field: the official normal rounds show them at its top edge although row 13
* lies outside every configBlackBoard area rect, user playtest #5 item 6). The boss field's devices (rows ≤ 6, its top
* wall under the bench included: act2 m01's #201 / #202 blow DOWN into rows 5–3) are drawn only when `area` builds the
* boss field (a rect below row 6): the official normal cameras never show that wall — it lies under the shop bar in
* prep and off-screen in battle and in the shop-collapsed prep view — while the remake's prep view showed the boss
* round's machines under the bench ("反而在关底吹风机的贴图又出现了"). `area` null = the whole map. The 2D board
* (render/tiles.js _stageDevices) follows the same rule.
* Active stage devices standing on drawn tiles: `{ kind, r, c, dir, z0, key, alias, rangeTiles }`. Every visible
* predefined token of the level is a scene object; a device on a separator wall is drawn wherever that wall is built,
* whichever field it blows into (act2 m01: row 6 belongs to both leftNormal and leftBoss, row 13 tops the normal field).
* The official client hides none of them by the way they face: the row-13 machines show in the official normal rounds
* although row 13 lies outside every configBlackBoard area rect (user playtest #5 item 6), and so do the boss field's
* row-6 machines under the bench (user playtest #5 follow-up: "吹风机原版道中也该有").
*/
export function stageDevices(stage, grid, area = null) {
export function stageDevices(stage, grid) {
const list = Array.isArray(stage?.devices) ? stage.devices : [];
const bossShown = !area || area.some((a) => a.r0 < BOSS_WALL_ROW);
const out = [];
for (const d of list) {
if (!d || !Array.isArray(d.pos) || !Number.isInteger(d.pos[0]) || !Number.isInteger(d.pos[1])) continue;
@@ -511,7 +503,6 @@ export function stageDevices(stage, grid, area = null) {
const active = typeof d.active === 'boolean' ? d.active : !d.hidden;
if (!active) continue;
const [r, c] = d.pos;
if (r <= BOSS_WALL_ROW && !bossShown) continue;
const t = grid?.[r]?.[c];
if (!t || !t.drawn) continue;
out.push({ kind: d.role, r, c, dir: ['UP', 'DOWN', 'LEFT', 'RIGHT'].includes(d.dir) ? d.dir : 'UP', z0: t.h, key: d.key || null, alias: d.alias || null, rangeTiles: Array.isArray(d.rangeTiles) ? d.rangeTiles : null });
+4 -7
View File
@@ -45,8 +45,6 @@ export const ROW_KEY = Object.freeze({ blocks: 0, devices: 0.01, surface: 0.02 }
export const boxDepthKey = (cam, row) => -cam.depthOf(0, row + 0.45, 0) * 100;
const ROWS = 19, COLS = 21;
/** The boss field's top wall under the normal field's bench (= board3d/layout.js BOSS_WALL_ROW). */
const BOSS_WALL_ROW = 6;
const PLAYABLE_DIST = 2; // margin tiles farther than this from a playable tile are not drawn
const CLIFF_DEPTH = 0.62; // how far the island's edge faces go down to the ground plane (tiles)
/** Brightness of the board meshes (vertex colour above 1 lifts the mid-grey board art a little). */
@@ -364,9 +362,6 @@ export class TileField {
_stageDevices() {
const list = Array.isArray(this.stage?.devices) ? this.stage.devices : [];
// the boss field's devices (rows ≤ 6, its top wall under the bench included) only in the views of the boss field
// (board3d/layout.js stageDevices: the official normal cameras never show that wall; user playtest #5 item 6)
const bossShown = this.field[0] < BOSS_WALL_ROW;
const out = [];
for (const d of list) {
if (!d || !Array.isArray(d.pos) || !Number.isInteger(d.pos[0]) || !Number.isInteger(d.pos[1])) continue;
@@ -374,10 +369,12 @@ export class TileField {
const active = typeof d.active === 'boolean' ? d.active : !d.hidden;
if (!active) continue;
if (d.pos[0] < 0 || d.pos[0] >= ROWS || d.pos[1] < 0 || d.pos[1] >= COLS || d.pos[0] >= 14) continue;
if (d.pos[0] <= BOSS_WALL_ROW && !bossShown) continue;
const gt = this.grid[d.pos[0]] && this.grid[d.pos[0]][d.pos[1]];
// devices stand on the island (playable tiles + the ring around them, dist ≤ 1 — the 3D board's drawn tiles), not
// on the faded margin; the separator row 13 above the field included (act2 m01's blowers)
// on the faded margin. A device on the field's edge wall is drawn like any other (board3d/layout.js stageDevices):
// act2 m01's row-13 blowers top the field and its row-6 ones stand under the bench in the normal views, as in the
// official normal rounds (user playtest #5 item 6 and its follow-up); the airflow (_drawFlow) only covers drawn
// tiles, so the row-6 machines' shows in the Final Assault view alone
if (gt && (!gt.drawn || gt.scenery || gt.dist > 1)) continue;
out.push(d);
}