Files
ark-stronghold-protocol/test/render/tiles.test.js
T
sganggs 8a43624935 feedback5 WE: 联防 is played on its own map, the escaped template level (GitHub #41, item 3) — act2autochess constData escapedBattleTemplateMapSinglePlayer / MultiPlayer = level_act1autochess_escaped_single / _multi name the level of the 联防 battle, and its map is the field: two road halves (cols 3–9 and 11–17, rows 9–12) joined at col 10, no water, crates or devices (until 0.1.3 the round's stage, opened to both halves). tools/build-data.mjs builds both into data/stages.json after the 11 battle stages (kind 'unite', helpers 1 / 2, weight 0, inactive, no modes, the name 联防阵地(N名玩家) the remake's own label [ASSUMED]; validation ties config.unite.templates to them); regenerated offline, a JSON compare shows only the two added records. unite.js uniteStageId picks the map of the helper count (escaped_single's routes enter at col 10, escaped_multi's at col 18 through (9,10)); match/unitePhase.js fields the 联防 battle on it (the round's stage only with data that lacks it); each helper on its prep tiles, the first of two shifted 8 columns onto the right half as before (the official halves; player_map_lr_offset 8). Client: screens/game.js draws the shown field's own map and restores the board's on leaving it (a 2-line change in a file of the R5 split; render/app.js enterBattle already loads m.field stageId); the dev mock's 联防 follows. The 联防 BGM (PR #110) is unchanged. Tests: test/match/feedback5-unite-map.test.js (both combat modes, 1 and 2 helpers, degraded data), test/data.test.js, test/render/tiles.test.js. Docs: DATA §12, META §4, DESIGN §3, PLAYING §5. Golden: fields unite-1 / unite-2 (the battle's stage → the 联防 map) and the 6 co-op match scenarios with a 联防 round (coop2-NORMAL-3, coop2-HARD-4, coop3-ABYSS-5, coop4-FUNNY-6, coop4-ABYSS-7, coop2-NORMAL-8-serverrun); roster / bonds unchanged (tools/golden.mjs leaves the 联防 maps out of its stage list)
2026-10-05 18:17:08 +08:00

141 lines
7.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// test/render/tiles.test.js — pure parts of the tile renderer: stage parsing (glyph legend, heights, bands,
// margin fading, scenery rows), style helpers. Importing render/tiles.js must not need PIXI.
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 { parseStage, mixColor } from '../../public/js/render/tiles.js';
import { GLYPH, TILE_H, dmgStyleKey, STATUS_ICON, PROJ } from '../../public/js/render/style.js';
import { STATUS_KEYS } from '../../public/js/render/textures.js';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
const stages = JSON.parse(readFileSync(path.join(ROOT, 'data/stages.json'), 'utf8'));
describe('parseStage', () => {
test('every data stage parses into a 19×21 grid with known materials and sane heights', () => {
for (const [id, st] of Object.entries(stages)) {
const g = parseStage(st);
assert.equal(g.length, 19, id);
for (const row of g) {
assert.equal(row.length, 21, id);
for (const t of row) {
assert.ok(typeof t.mat === 'string' && t.mat, `${id} ${t.r},${t.c} mat`);
assert.ok(Number.isFinite(t.h) && t.h >= 0 && t.h <= 1, `${id} height`);
assert.ok(t.alpha > 0 && t.alpha <= 1);
}
}
// the own board is always drawn and playable; hand slots are raised benches (the two 联防 maps, kind 'unite', have
// no bench: nobody prepares on them)
for (let r = 9; r <= 12; r++) for (let c = 3; c <= 8; c++) assert.ok(g[r][c].drawn, `${id} board ${r},${c}`);
for (let c = 0; c < 10; c++) {
if (st.kind === 'unite') { assert.equal(g[7][c].glyph, '#', `${id} no hand ${c}`); continue; }
assert.equal(g[7][c].glyph, 'a', `${id} hand ${c}`);
assert.equal(g[7][c].h, TILE_H.bench);
}
assert.equal(g[9][2].glyph, 'E', `${id} objective`);
}
});
test('glyph legend: heights and materials follow DATA.md §12', () => {
const st = stages.act2autochess_m02;
const g = parseStage(st);
const find = (glyph) => g.flat().find((t) => t.glyph === glyph);
assert.equal(find('h').h, TILE_H.wall);
assert.equal(find('h').mat, 'wall');
assert.equal(find('X').h, TILE_H.sep);
assert.equal(find('m').mat, 'mire');
assert.equal(find('m').h, 0);
assert.equal(find('S').mat, 'start');
assert.equal(find('E').mat, 'end');
for (const [glyph, def] of Object.entries(GLYPH)) assert.ok(def.mat, glyph);
});
test('bands: rows outside the band are not drawn; pen rows are scenery; far margins skipped', () => {
const st = stages.act2autochess_m01;
const field = parseStage(st, [6, 18]);
for (let c = 0; c < 21; c++) for (let r = 0; r <= 5; r++) assert.equal(field[r][c].drawn, false, `boss rows hidden ${r},${c}`);
assert.ok(field[15][7].scenery && field[15][7].drawn, 'pen gate drawn as scenery');
assert.equal(field[15][7].playable, false);
const boss = parseStage(st, [0, 6]);
for (let r = 7; r <= 18; r++) for (let c = 0; c < 21; c++) assert.equal(boss[r][c].drawn, false);
assert.ok(boss[2][2].drawn && boss[2][2].glyph === 'E');
// margin '#' far from anything playable fades then disappears
const full = parseStage(st);
const margin = full.flat().filter((t) => t.glyph === '#' && t.drawn && t.dist === 2);
assert.ok(margin.length > 0 && margin.every((t) => t.alpha < 1 && t.mat === 'margin'));
});
test('junk / missing stage data never throws', () => {
for (const bad of [null, undefined, {}, { rows: 'x' }, { rows: [null, 42, 'zz??'] }, { rows: Array(19).fill('Q'.repeat(21)), tiles: { Q: { tileKey: 'tile_road', height: 'LOW' } } }]) {
const g = parseStage(bad);
assert.equal(g.length, 19);
assert.equal(g[0].length, 21);
}
const q = parseStage({ rows: Array(19).fill('Q'.repeat(21)), tiles: { Q: { tileKey: 'tile_road', height: 'LOW' } } });
assert.equal(q[10][5].glyph, 'r', 'unknown glyph mapped through its tileKey');
});
});
describe('style helpers', () => {
test('damage style keys and status icons', () => {
assert.equal(dmgStyleKey('phys'), 'phys');
assert.equal(dmgStyleKey('arts'), 'arts');
assert.equal(dmgStyleKey('true'), 'true');
assert.equal(dmgStyleKey('burn'), 'elem');
assert.equal(dmgStyleKey('necrosis'), 'elem');
assert.equal(dmgStyleKey('???'), 'phys');
for (const k of Object.values(STATUS_ICON)) assert.ok(STATUS_KEYS.includes(k), `status icon ${k} drawn in the atlas`);
for (const p of Object.values(PROJ)) assert.ok(p.speed > 0 && p.width > 0);
});
test('mixColor', () => {
assert.equal(mixColor(0x000000, 0xffffff, 0), 0x000000);
assert.equal(mixColor(0x000000, 0xffffff, 1), 0xffffff);
assert.equal(mixColor(0xff0000, 0x0000ff, 0.5), 0x800080);
assert.equal(mixColor(0x123456, 0x654321, -5), 0x123456);
});
});
describe('TileField external board (the 3D board layer draws the tiles)', () => {
test('external mode builds no Pixi board, keeps heights / levels / highlights; switching back rebuilds the 2D board', async () => {
const { installFakePixi } = await import('./fakepixi.js');
const fake = installFakePixi();
try {
const P = globalThis.PIXI;
// minimal mesh stack for the 2D board (not part of the shared fake)
P.Buffer = class { constructor(data) { this.data = data; } update() {} };
P.Geometry = class { addAttribute() { return this; } addIndex() { return this; } };
P.Shader = { from: (v, f, u) => ({ uniforms: { ...u } }) };
P.Mesh = class extends P.Container { constructor(g, s) { super(); this.geometry = g; this.shader = s; } };
P.BLEND_MODES = { ADD: 1, NORMAL: 0 };
const { TileField } = await import('../../public/js/render/tiles.js');
const layers = { ground: new P.Container(), overlay: new P.Container(), props: new P.Container(), anim: new P.Container() };
for (const l of Object.values(layers)) l.addChild = function (...kids) { for (const k of kids) { k.parent = this; this.children.push(k); } return kids[0]; };
const f = new TileField(layers);
f.setView([6, 18], { r0: 7, r1: 12, c0: 0, c1: 10 }, [6, 13]);
f.setExternal(true);
f.setStage(stages.act2autochess_m01);
assert.equal(f.meshes.length, 0, 'no board meshes');
assert.equal(f.rowMeshes.size, 0);
assert.equal(f.animSprites.length, 0);
assert.equal(f.boxes.length, 0);
assert.equal(f.surfaceLayer(10), null, 'units on raised tops use their ground layer');
assert.equal(f.heightAt(10, 4), TILE_H.wall, 'high ground height still known (units / picking)');
assert.ok(f.levels.includes(TILE_H.wall) && f.levels.includes(0));
assert.ok(f.blowers.length > 0, 'blower airflow streaks stay in Pixi');
f.setHighlights([[10, 5]], 'legal');
assert.equal(f.highlights.size, 1);
f.setExternal(false);
assert.ok(f.meshes.length > 0 && f.rowMeshes.size > 0, '2D board rebuilt');
assert.ok(f.boxes.length > 0, 'gate boxes back');
f.setExternal(true);
assert.equal(f.meshes.length, 0);
f.destroy();
} finally {
fake.restore();
}
});
});