Files
ark-stronghold-protocol/test/render/tiles.test.js
T
sganggs 75202776cc feedback6 R21B: 联防 plays on the round's battlefield again — its terrain, crates, water, devices and runes, as in 0.1.4 (the owner's decision of 2026-10-07, 「官服保留地形,改回去」; GitHub #244). 0.2.0's escaped-level map (8a436249, §25.6.4) is withdrawn: that map is the placeholder grid all 39 wave template levels carry, tile for tile; only the 11 stage levels have terrain. match/unitePhase.js _uniteOpts fields this.stageId in both combat modes, unite.js uniteStageId is removed, the client draws the 联防 on the board's stage again (screens/game.js: 0.1.4's ownStage / baseStage, no map switch, nothing to restore on leaving), the dev mock likewise. Kept from 0.2.0: own-board 突袭 / 乌尔比安 S3 landings (onOwnBoard, raidTile untouched), the bounty payee, the 联防 BGM, the SETTLE result box. data/stages.json keeps the two kind 'unite' records (no field uses them; engine tests use them as a plain two-halves road); tools/build-data.mjs comments only, an offline rebuild reproduces data/ byte for byte. The 联防 battle input is byte-identical to v0.1.4's in 20 cases (5 stages × 1 / 2 helpers × both combat modes); GitHub #41 item 3 (野鬃 pushing walkers back across the gate onto the other half) is back with v0.1.4's numbers, none off the map. Tests: test/match/feedback5-unite-map.test.js rewritten (6, all fail on the parent), test/e2e/coop.e2e.mjs checks the 联防 field's drawn stage; docs: DATA §12, META §4, PLAYING §5, design/geometry-time, I18N, the §25.6.4 pointer; golden: fields unite-1 (escaped_single → act1autochess_m02) and unite-2 (escaped_multi → act1autochess_m07), the stages of 0.1.4, and the co-op matches whose 联防 rounds come out differently on the round's map — coop2-NORMAL-3, coop2-HARD-4, coop3-ABYSS-5, coop4-ABYSS-7, coop2-NORMAL-8-serverrun, coop2-NORMAL-14-standins (now a victory)
2026-10-07 17:40:03 +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 escaped levels' 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();
}
});
});