0.2.6: the device slabs stay drawn on an official scene (GitHub #569) — 射击台, 特制水上平台 and 封印的地面 lived only in the procedural board's bucket that a scene replaces, so 战场#03's raised tiles by the blue gate were flat and 战场#05(上半)'s water platforms read as open water; buildBoard repeats them into buckets.raised / raisedDecal with sides reaching under the scene's ground and setStage draws just those on a scene (the 'h' high ground is the scenes' own and is not repeated); DESIGN §31.11, client §15; golden: unchanged

This commit is contained in:
sganggs committed 2026-10-13 17:45:04 +08:00
1 parent d8aab0a2db
commit 205f3bb7f7
7 files changed
+99 -8

No files matched your search

+17
View File
@@ -385,6 +385,23 @@ describe('BoardScene with an official scene (stub renderer)', () => {
assert.equal(board.sceneRes.size, 0, 'scene resources disposed');
});
test('#569: the device slabs stay drawn on an official scene (the procedural board is gone); a stage without slabs adds nothing', () => {
const board = new BoardScene(THREE, fakePack(), { renderer: stubRenderer() });
const m03 = stages.act1autochess_m03;
board.setScenePack(m03.id, fakeScenePack());
board.setStage(m03);
assert.ok(board.official && !board.meshes.board, 'the scene replaces the board');
assert.ok(board.meshes.raised, 'the 射击台 slabs beside the blue gate stay');
assert.equal(board.meshes.raised.geometry.getAttribute('position').count, board.board.buckets.raised.vertexCount);
assert.equal(board.meshes.raised.material, board.mat.board, 'drawn like the procedural board');
assert.equal(board.setScenePack(m03.id, null), true);
assert.ok(board.meshes.board && !board.meshes.raised, 'without the scene the board draws them itself');
board.setScenePack(stage.id, fakeScenePack());
board.setStage(stage);
assert.ok(board.official && !board.meshes.raised && !board.meshes.raisedDecal, `${stage.id}: no slab devices`);
board.destroy();
});
test('another stage, or dropping the scene, shows the procedural board again', () => {
const board = new BoardScene(THREE, fakePack(), { renderer: stubRenderer() });
board.setScenePack(stage.id, fakeScenePack());
+28
View File
@@ -13,6 +13,7 @@ import { SURFACES, SOURCES, surfaceUV, resolveUvTable, cleanSurface, sideRect, t
import { parseObj, mapMesh } from '../../public/js/render/board3d/obj.js';
import {
buildBoard, classifyStage, heightOf, AREAS, areaFor, unionAreas, objToBoard, boxProjectUV, tube, Geom, uvAt, ROWS, COLS,
DEVICE_H, RAISED_FOOT,
} from '../../public/js/render/board3d/layout.js';
import { BoardScene, gatePulse, DIR_TURNS, boxData, LIGHTING } from '../../public/js/render/board3d/scene.js';
import { loadBoardPack, loadThree, resetBoardPack, PACK_IMAGES } from '../../public/js/render/board3d/load.js';
@@ -173,6 +174,33 @@ describe('board geometry (layout.js)', () => {
assert.ok(!buildBoard(stages.act1autochess_m02).devices.some((d) => d.kind === 'crate'));
});
test('#569: the device slabs (射击台 / 特制水上平台 / 封印的地面) are repeated for an official scene, their sides reaching under its ground; nothing else is', () => {
const SLAB = new Set(['platform', 'waterPlatform', 'sealedFloor']);
for (const [id, st] of Object.entries(stages)) {
if (!Array.isArray(st?.rows)) continue;
for (const area of [AREAS.normal, AREAS.boss]) {
const b = buildBoard(st, { area });
const slabs = b.devices.filter((d) => SLAB.has(d.kind));
const decals = slabs.filter((d) => d.kind === 'sealedFloor').length;
// per slab four sides, plus its top: a plate (raised) or the hazard decal of a sealed floor (raisedDecal)
assert.equal(b.buckets.raised.vertexCount, (slabs.length * 5 - decals) * 4, `${id}: raised quads`);
assert.equal(b.buckets.raisedDecal.vertexCount, decals * 4, `${id}: raised decal quads`);
const P = b.buckets.raised.position;
for (let i = 0; i < P.length; i += 3) {
const d = slabs.find((x) => Math.abs(P[i] - x.c) <= 0.5 && Math.abs(P[i + 1] - x.r) <= 0.5);
assert.ok(d, `${id}: every raised vertex is on a slab tile`);
const h = b.grid[d.r][d.c].h;
assert.ok(near(P[i + 2], h + DEVICE_H[d.kind]) || near(P[i + 2], h - RAISED_FOOT), `${id}: a slab top or its foot, z ${P[i + 2]}`);
}
}
}
// the report's 射击台 beside the blue gate (战场#03) and the water platforms (战场#05 上半) are there; the 'h' high
// ground of act2 m01 / m02 is the scenes' own geometry and is not repeated
assert.ok(buildBoard(stages.act1autochess_m03, { area: AREAS.normal }).buckets.raised.vertexCount > 0);
assert.equal(buildBoard(stages.act1autochess_m05, { area: AREAS.normal }).devices.filter((d) => d.kind === 'waterPlatform').length, 16);
for (const id of ['act2autochess_m01', 'act2autochess_m02']) assert.equal(buildBoard(stages[id], { area: AREAS.normal }).buckets.raised.vertexCount, 0, id);
});
test('malformed stages never throw and still build an island', () => {
for (const st of [null, {}, { rows: ['xyz'] }, { rows: Array(19).fill('r'.repeat(21)), devices: [{ role: 'crate', pos: [3, 3] }, { role: 'crate', pos: 'x' }] }]) {
const b = buildBoard(st, { area: AREAS.all });