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:
1 parent
d8aab0a2db
commit
205f3bb7f7
7 files changed
+99
-8
No files matched your search
@@ -19,6 +19,8 @@
|
||||
- 设置新增「弹药数量」,是可选项、默认关闭(官方界面没有这个数字):打开后弹药类技能的黄色弹药条下方显示剩余弹药 / 总弹药数(如 13/14),格子密到画不出分隔线时也能读出(PR #552,感谢 @IceCodeNew)。
|
||||
- 场上单位多时(画面质量「高」下通常在第 7 回合以后)模型改用缓存画面绘制,此前野鬃待机时的旗帜、浊心斯卡蒂三技能飞起时的头部会被裁掉,超出的部分还会画进相邻单位的缓存,在隐现的枪口等处留下错位的残影。现在每个模型按它会播放的全部动作预留缓存大小,每个单位只画在自己的区域内;少数特别大的动作(多为部署时从天而降)播放期间直接绘制;单位在眩晕中才加载完成的模型也不再显示初始姿势(#542,感谢 @ImPQlc)。
|
||||
|
||||
- 显示官方场景时,射击台、特制水上平台和封印的地面重新画出来:此前它们只画在被场景替换掉的棋盘层里,战场#03 蓝门旁的高台是平的、战场#05(上半) 的水上平台看起来像一片空水面,干员却站在抬高的位置上(#569,感谢 @A1muE4aY)。
|
||||
|
||||
### 工具与开发
|
||||
- Windows 下场景素材测试的仓库路径不再重复盘符(PR #547,感谢 @Xinglan233)。
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ Findings (2026-09-28): 卫戍协议 levels have no scene bundle (`mapId` null; t
|
||||
|
||||
Correction (2026-10-11, GitHub #532; PR #533 by @andrew05060414): the first finding above was made off season and is wrong. While a season runs, the official hot-update list carries **one prebuilt scene per battlefield** — `scenes/activities/act1autochess/level_act1autochess_m01…m07` and `scenes/activities/act2autochess/level_act2autochess_m01…m04` (each with `…/lightingdata.ab` and `…_lighting_settings.ab`; Android versions `25-11-21-15-21-44_ee1197`, `26-03-13-06-11-47_14fb99`, `26-04-13-12-31-42_e9b560`); the lists of 2026-09-13 and 2026-10-08 have none, and no 卫戍协议 level data either. The asset index of those versions (`<hash>.idx`: each bundle's dependencies) gives each scene's art: act1 m01–m07 and act2 m02 / m04 depend on `arts/maps/map_autochesssand` (m05 and act2 m04 also on `map_doss_shore`), act2 m01 on `map_lakegarden` + `map_syracuse_metro`, act2 m03 on `map_kxmr` + `map_lm_center`. **No battlefield scene depends on `arts/maps/map_autochess`** (the metal theme the procedural board below draws; in the current client the 矢量突破 scenes `act3break` use it). A scene holds the logic tiles (`(row,col)#tileKey` at anchor-local (col, row, −height), high ground 0.4), the art under `GraphicHolder` (static parts merged into "Combined Mesh (root: scene)" objects, lightmapped: Shadowmask mode, one directional key light), and three sub-graphics `HIDDEN@1…3` (`MapSubGraphicHolder`) that the game switches on: the left normal field, the right normal field and the boss field — the areas of `layout.js AREAS`.
|
||||
|
||||
Official battlefield scenes (`board3d/load.js loadScenePack`, `scene.js setScenePack`, `materials.js sceneMaterial`): when `data/local-assets.json` lists `scene/<stageId>`, the 3D board draws that scene instead of the procedural tiles, cliffs and background plane; the devices, gate / objective boxes, field edges and terrain overlays stay as below. The base part is always drawn; a `HIDDEN@n` part is drawn when its centre lies in one of the built areas. Chunks of those shown parts that share a material and a lightmap page are one mesh when the area is built (`load.js mergeSceneChunks`); water shares a mesh across pages because its shader has no lightmap. A switched-off part stays out of that mesh. Without the scene (not extracted, a load failure, an unknown `version`) the procedural metal board is drawn as before. Approximations: the official shaders are not reproduced — a lit material is albedo × (the lightmap's baked light + key light · N·L · the baked shadow mask), in linear light; normal, metallic and emission maps, the vertex-colour blend (`_HG_VERTEX_COLOR_BLEND_ON`), the sky box, the map effects (`mapeff_*`) and the post-processing profile are not drawn; `Torappu/Scene/StylizedWater` is the board's water shader in the material's colours; crates cast no shadow on a scene. `SCENE_LIGHT` `direct` / `indirect` are 1, the scene's own key light and baked light (0.2.6, GitHub #549, PR #554 by @xcdoge; 0.2.5 had 0.8 / 0.4 tuned by eye); **[ASSUMED]** `ambient` 0.2 (a mesh without a lightmap page; the official post-processing being unknown) and the RGBM reading of the lightmap pages (`scenes.py decode_rgbm`: Unity's linear-space decode, range 5). Owner decision (2026-10-12): these extracted files belong to the 0.2.5 asset pack and may ship in the full release zip; a lite package or a source install may download and extract them on that machine. Missing extractor dependencies keep the metal board. The repository still never stores the official files.
|
||||
Official battlefield scenes (`board3d/load.js loadScenePack`, `scene.js setScenePack`, `materials.js sceneMaterial`): when `data/local-assets.json` lists `scene/<stageId>`, the 3D board draws that scene instead of the procedural tiles, cliffs and background plane; the devices, gate / objective boxes, field edges and terrain overlays stay as below, and so do the device slabs — 射击台, 特制水上平台, 封印的地面 (`layout.js buckets.raised`, their sides reaching 0.08 under the scene's ground): they are runtime devices the scenes do not contain, while the scenes do model the 'h' high ground (act2 m01 / m02), which is not repeated (0.2.6, GitHub #569). The base part is always drawn; a `HIDDEN@n` part is drawn when its centre lies in one of the built areas. Chunks of those shown parts that share a material and a lightmap page are one mesh when the area is built (`load.js mergeSceneChunks`); water shares a mesh across pages because its shader has no lightmap. A switched-off part stays out of that mesh. Without the scene (not extracted, a load failure, an unknown `version`) the procedural metal board is drawn as before. Approximations: the official shaders are not reproduced — a lit material is albedo × (the lightmap's baked light + key light · N·L · the baked shadow mask), in linear light; normal, metallic and emission maps, the vertex-colour blend (`_HG_VERTEX_COLOR_BLEND_ON`), the sky box, the map effects (`mapeff_*`) and the post-processing profile are not drawn; `Torappu/Scene/StylizedWater` is the board's water shader in the material's colours; crates cast no shadow on a scene. `SCENE_LIGHT` `direct` / `indirect` are 1, the scene's own key light and baked light (0.2.6, GitHub #549, PR #554 by @xcdoge; 0.2.5 had 0.8 / 0.4 tuned by eye); **[ASSUMED]** `ambient` 0.2 (a mesh without a lightmap page; the official post-processing being unknown) and the RGBM reading of the lightmap pages (`scenes.py decode_rgbm`: Unity's linear-space decode, range 5). Owner decision (2026-10-12): these extracted files belong to the 0.2.5 asset pack and may ship in the full release zip; a lite package or a source install may download and extract them on that machine. Missing extractor dependencies keep the metal board. The repository still never stores the official files.
|
||||
|
||||
Target: a **real 3D board layer** rendered with three.js (vendored ESM, no CDN) underneath the existing Pixi layer:
|
||||
- Geometry generated from `data/stages.json` like the official builder: LOW tiles = top quads; HIGH tiles = raised blocks with side walls; forbidden/void, fences, gates (red), objective (blue), hand/temp pads, special terrain; UVs mapped onto the official atlas regions (reuse/extend `public/assets/local/map/autochess/tiles.json`); normal + emission + metallic maps; lighting tuned to match official screenshots (key directional light, ambient/hemisphere, soft contact shadows); official background meshes & texture; device meshes (OBJ exported by `tools/local-extract`) with their textures; animated gate/objective effects.
|
||||
|
||||
@@ -313,3 +313,31 @@ Sources: [PRTS 维什戴尔](https://prts.wiki/w/维什戴尔), the talent's nea
|
||||
combat tile, through the deployment talent and an actual S3 activation), and a solo / pair case — her own free tiles
|
||||
filled, the empty half takes no shadow and the teammate's half does. Normative: DESIGN §3
|
||||
([geometry-time.md](../design/geometry-time.md)); API note: [SIM.md](../SIM.md). Golden: unchanged.
|
||||
|
||||
### 31.11 Device slabs on the official scenes (#569)
|
||||
|
||||
Issue #569 by @A1muE4aY.
|
||||
|
||||
**Seen.** With an official scene on the 3D board, 战场#03's two raised ranged tiles beside the blue gate were drawn flat,
|
||||
while the operators placed on them stood at their raised height. The scene replaced the whole procedural board
|
||||
(`scene.js setStage` skipped its board, glass, decal and pipe buckets), and the raised slabs of the stage devices —
|
||||
射击台 (`platform`, 0.26), 特制水上平台 (`waterPlatform`, 0.2) and 封印的地面 (`sealedFloor`, 0.05, a hazard decal
|
||||
top) — exist only in that board bucket (`layout.js slabDevice`). The scenes do not contain them: they are runtime
|
||||
devices of the stage (`stageDevices`), and the scene geometry has no raised top on those tiles. Affected while a scene
|
||||
is shown: 战场#03 (射击台), 战场#05(上半) (the 16 water platforms of the normal field, 32 on the boss field — the pool read
|
||||
as empty water) and 战场#06(上半) (射击台 and 封印的地面).
|
||||
|
||||
**Now.** `buildBoard` repeats each device slab into `buckets.raised` (tops and sides; a sealed floor's hazard top into
|
||||
`buckets.raisedDecal`), with the side faces reaching `RAISED_FOOT` (0.08) below the tile so no gap shows against the
|
||||
scene's ground (about z 0). With an official scene `setStage` draws only those two buckets of the procedural board, in
|
||||
the board's own materials; without one the board draws the slabs itself as before. The 'h' high ground is not repeated:
|
||||
the scenes model it (act2 m01's plates and act2 m02's sandstone blocks, checked side by side); an earlier draft that
|
||||
also drew our 'h' plates covered them. Units already stood at `tiles.js heightAt`, which includes the device heights.
|
||||
|
||||
**[ASSUMED]** The slabs keep the metal board's plate and side textures: no official prefab of these devices is extracted.
|
||||
|
||||
**Tests.** `test/render/board3d.test.js` (every stage and area: one plate or decal top and four sides per slab device,
|
||||
every vertex on a slab tile at its top or its foot; 战场#03 and #05(上半) have them; act2 m01 / m02 repeat nothing),
|
||||
`test/render/board3d-scene.test.js` (an official scene on 战场#03 keeps the slabs in the board material and nothing else of
|
||||
the procedural board; dropping the scene restores the board; act2 m02 adds no slab mesh). Screenshots of the five
|
||||
affected stages before / after / metal board were compared by hand. Golden: unchanged (client only).
|
||||
@@ -3,8 +3,11 @@
|
||||
// the buckets into merged BufferGeometries).
|
||||
//
|
||||
// const board = buildBoard(stage, { uv: resolveUvTable(tiles), crate?: objMesh, area?: AREAS.normal })
|
||||
// → { grid, buckets: { board, decal, pipe }, gates: [{ r, c, kind: 'start'|'end' }], devices: [...],
|
||||
// → { grid, buckets: { board, decal, pipe, glass, raised, raisedDecal }, gates: [{ r, c, kind: 'start'|'end' }], devices: [...],
|
||||
// terrain: { water, mire, smog, infection: [[r,c]…] }, edges: [...cyan field edge strips], bounds, stats }
|
||||
// `raised` / `raisedDecal` repeat only the device slabs — 射击台, 特制水上平台, 封印的地面 — with their side faces
|
||||
// reaching below the ground: those are runtime devices that an official scene (scene.js) does not contain, so they are
|
||||
// drawn on it (GitHub #569). The 'h' high ground is part of the scenes (act2 m01 / m02 model it) and is not repeated.
|
||||
// `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).
|
||||
@@ -40,6 +43,8 @@ export const CLIFF = 0.75;
|
||||
export const BEVEL = Object.freeze({ low: [0.035, 0.018], block: [0.04, 0.035] });
|
||||
/** Deep-sea basins: floor depth below the ground. */
|
||||
export const BASIN = 0.14;
|
||||
/** How far below the ground the device slabs' side faces reach on an official scene (its ground sits about z 0). */
|
||||
export const RAISED_FOOT = 0.08;
|
||||
|
||||
const CONTENT_OF = (g) => g !== '#' && g !== 'X';
|
||||
|
||||
@@ -321,6 +326,7 @@ export function buildBoard(stage, opts = {}) {
|
||||
const at = (r, c) => grid[r]?.[c] || null;
|
||||
const topZ = (t) => (t.glyph === 'd' ? -BASIN : t.h);
|
||||
const board = new Geom(), decal = new Geom(), pipe = new Geom(), glass = new Geom();
|
||||
const raised = new Geom(), raisedDecal = new Geom();
|
||||
const gates = [], terrain = { water: [], mire: [], smog: [], infection: [] };
|
||||
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
||||
|
||||
@@ -441,12 +447,15 @@ export function buildBoard(stage, opts = {}) {
|
||||
|
||||
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);
|
||||
if (d.kind === 'platform' || d.kind === 'sealedFloor' || d.kind === 'waterPlatform') {
|
||||
slabDevice(board, decal, UVT, d, grid);
|
||||
slabDevice(raised, raisedDecal, UVT, d, grid, RAISED_FOOT);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
grid, gates, devices, terrain, edges,
|
||||
buckets: { board: board.finish(), decal: decal.finish(), pipe: pipe.finish(), glass: glass.finish() },
|
||||
buckets: { board: board.finish(), decal: decal.finish(), pipe: pipe.finish(), glass: glass.finish(), raised: raised.finish(), raisedDecal: raisedDecal.finish() },
|
||||
bounds: { x0: minX, x1: maxX, y0: minY, y1: maxY },
|
||||
stats: { tiles: grid.flat().filter((t) => t.drawn).length, vertices: board.vertexCount + decal.vertexCount + pipe.vertexCount },
|
||||
};
|
||||
@@ -513,17 +522,18 @@ export function stageDevices(stage, grid) {
|
||||
/** Device heights on their tile (platform tops are standing surfaces: tiles.js heightAt adds them). */
|
||||
export const DEVICE_H = Object.freeze({ platform: TILE_H.platform, sealedFloor: 0.05, waterPlatform: 0.2 });
|
||||
|
||||
function slabDevice(board, decal, UVT, d, grid) {
|
||||
/** `foot` lowers the side faces' bottom (the scene-board copy reaches under an official scene's ground). */
|
||||
function slabDevice(board, decal, UVT, d, grid, foot = 0) {
|
||||
const size = d.kind === 'sealedFloor' ? 0.92 : 0.94;
|
||||
const h = DEVICE_H[d.kind] || 0.2;
|
||||
const z0 = grid[d.r][d.c].h, zt = z0 + h;
|
||||
const zt = grid[d.r][d.c].h + h, z0 = grid[d.r][d.c].h - foot;
|
||||
const a = size / 2, x0 = d.c - a, x1 = d.c + a, y0 = d.r - a, y1 = d.r + a;
|
||||
const topSurf = d.kind === 'sealedFloor' ? null : UVT.plateS;
|
||||
if (topSurf) board.quad([[x0, y0, zt], [x1, y0, zt], [x1, y1, zt], [x0, y1, zt]], surfaceUV(topSurf), [0, 0, 1], [1.02, 1.02, 1.02]);
|
||||
else decal.quad([[x0, y0, zt], [x1, y0, zt], [x1, y1, zt], [x0, y1, zt]], surfaceUV(UVT.hazardX), [0, 0, 1], [1, 1, 1]);
|
||||
const side = UVT.graySide;
|
||||
for (const dir of ['S', 'E', 'N', 'W']) {
|
||||
const uv = surfaceUV(side, sideRect(side, size, h));
|
||||
const uv = surfaceUV(side, sideRect(side, size, h + foot));
|
||||
board.quad(sideCorners(dir, x0, x1, y0, y1, z0, zt), uv, NRM[dir], [[0.5, 0.53, 0.56], [0.5, 0.53, 0.56], [0.85, 0.88, 0.9], [0.85, 0.88, 0.9]]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
// static crates (merged, same material),
|
||||
// decals (MT_autochess_common), fence pipes, blowers (merged, unlit), gate boxes (additive + alpha, merged per
|
||||
// material), cyan field edges, terrain overlays (≤ 4), background plane + its shadow catcher — ~12–16 in total.
|
||||
// An official scene replaces the tiles, glass, decals, pipes and background with one mesh per material and lightmap
|
||||
// An official scene replaces the tiles, glass, decals, pipes and background (all but the device slabs, layout.js
|
||||
// buckets.raised — GitHub #569) with one mesh per material and lightmap
|
||||
// page of the parts the built areas show (load.js mergeSceneChunks). Devices, gates, edges and terrain stay.
|
||||
// The key light's shadow map is rendered only when the geometry changes (autoUpdate off).
|
||||
|
||||
@@ -303,6 +304,11 @@ export class BoardScene {
|
||||
M.glass = this._mesh(board.buckets.glass, this.mat.glass);
|
||||
M.decal = this._mesh(board.buckets.decal, this.mat.decal, { cast: false });
|
||||
M.pipe = this._mesh(board.buckets.pipe, this.mat.pipe);
|
||||
} else {
|
||||
// the scene has no device slabs: the 射击台 / 特制水上平台 / 封印的地面 slabs stay drawn on it (units stand
|
||||
// on them at tiles.js heightAt; GitHub #569)
|
||||
M.raised = this._mesh(board.buckets.raised, this.mat.board);
|
||||
M.raisedDecal = this._mesh(board.buckets.raisedDecal, this.mat.decal, { cast: false });
|
||||
}
|
||||
this._buildDevices(board);
|
||||
this._buildGates(board);
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in new issue
Block a user