Files
MatchMaster/tools/test-washing-map.cjs

167 lines
8.8 KiB
JavaScript

const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { loadMembers } = require('./new-player-ab-test-support.cjs');
const cc = { v2: (x, y) => ({ x, y }), isValid: node => !!node && !node.destroyed };
const Controller = loadMembers('assets/Script/Map.ts', [
'checkPass', 'passWall', 'predict_End', 'isWashCell', 'getDirtFrames', 'getWashingBubble', 'unlockSpawnGateBatch',
], { cc });
function exitFixture(overrides = {}) {
const events = [];
const block = { isDirty: true, color: 2, type: 0, posX: 1, posY: 2, block_Info: { lock: true }, ...overrides };
const node = { getComponent: () => block };
const wall = { posX: 0, posY: 2, color: 2, colorArray: [2, 3], special: 0, open: true,
toggleOpen: true, wall_Info: { length: 1 }, changeColorWall: () => events.push('door') };
const wallNode = { getComponent: () => wall, parent: { getChildByName: () => ({ active: false }) } };
const map = Object.assign(new Controller(), {
blocks: [node], blockNum: 1, leftDoors: [{ getChildByName: () => wallNode }],
rightDoors: [], topDoors: [], bottomDoors: [],
getMinAndMax: () => ({ minX: 1, maxX: 8, minY: 1, maxY: 8 }),
checkDiraction: () => [false, false, false, false], detectingBlock: () => true,
checkColor: () => events.push('color'), changeBoom: () => events.push('bomb'),
special_Treatment: () => events.push('eliminate'), createParticle: () => events.push('particle'),
});
return { map, block, node, wall, events };
}
test('dirty blocks reject normal exit before gate, bomb, counter or elimination side effects', () => {
const f = exitFixture();
assert.equal(f.map.checkPass(f.node, [{ x: 0, y: 0 }]), -1);
assert.equal(f.map.blockNum, 1);
assert.equal(f.block.block_Info.lock, true);
assert.deepEqual(f.events, []);
assert.equal(f.block.isDirty, true, 'exit checks must not wash dirt');
});
test('a clean member can exit independently of a dirty adhesive partner', () => {
const f = exitFixture({ isDirty: false, type: 9 });
f.block.block_Info.node = { getComponent: () => ({ isDirty: true }) };
assert.equal(f.map.checkPass(f.node, [{ x: 0, y: 0 }]), 2);
assert.equal(f.map.blockNum, 0);
assert.deepEqual(f.events, ['color', 'door', 'bomb', 'eliminate', 'particle']);
});
test('both rainbow variants ignore dirt and color while retaining closed-gate restrictions', () => {
for (const rainbow of [{ oldRainbow: true, color: 5 }, { color: 100 }]) {
const f = exitFixture(rainbow);
assert.equal(f.map.checkPass(f.node, [{ x: 0, y: 0 }]), 2);
assert.equal(f.map.blockNum, 0);
const closed = exitFixture(rainbow);
closed.wall.special = 2;
closed.wall.open = false;
assert.equal(closed.map.checkPass(closed.node, [{ x: 0, y: 0 }]), -1);
assert.equal(closed.map.blockNum, 1);
assert.deepEqual(closed.events, []);
}
});
test('potential-move prediction does not call washing or declare a movable dirty block lost', () => {
const f = exitFixture();
f.wall.node = { parent: { getChildByName: () => ({ active: false }) } };
f.wall.judgeButtonWall = () => true;
f.map.wall_Pass = [f.wall];
f.map.blockCanMove = () => true;
f.map.blockCanPass = () => true;
f.block.tryWashDirt = () => assert.fail('prediction changed dirt');
assert.equal(f.map.predict_End(false), true);
assert.equal(f.block.isDirty, true);
assert.deepEqual(f.events, []);
});
test('wash lookup accepts playable marked cells and rejects wall, gap and out-of-bounds cells', () => {
const cell = (washing, opacity = 255) => ({ opacity, getComponent: () => ({ washing }) });
const map = Object.assign(new Controller(), { mapBlocksWall: [[cell(false), cell(true), cell(true, 254)]] });
assert.equal(map.isWashCell(0, 0), false);
assert.equal(map.isWashCell(0, 1), true);
assert.equal(map.isWashCell(0, 2), false);
assert.equal(map.isWashCell(-1, 1), false);
assert.equal(map.isWashCell(1, 0), false);
assert.equal(map.isWashCell(0, 3), false);
});
test('spawned blocks wash only after spawn locking is released, before bomb timers resume', () => {
const events = [];
const block = { spawnLocked: true, type: 6,
setSpawnLocked(value) { this.spawnLocked = value; events.push('unlock'); },
tryWashDirt() { assert.equal(this.spawnLocked, false); events.push('wash'); } };
const node = { getComponent: () => block,
getChildByName: () => ({ getComponent: () => ({ startBoom: () => events.push('bomb') }) }) };
const map = Object.assign(new Controller(), { gameStart: true, pause: false });
map.unlockSpawnGateBatch([null, { destroyed: true }, { getComponent: () => null }, node]);
assert.deepEqual(events, ['unlock', 'wash', 'bomb']);
});
test('initial washing waits for adhesive members and mobile floor setup, including non-clear targets', () => {
const events = [], frames = [], timers = [];
const infos = [
{ id: 1, block: 1, type: 9, color: 2, adhesiveTime: 2, floorMove: true },
{ id: 2, block: 0, type: 9, color: 3, adhesiveTime: 2 },
{ id: 3, block: 0, type: 0, color: 11 },
{ id: 4, block: 0, type: 22, color: 2 },
].map(info => ({ ...info, position: { x: 0, y: 0 } }));
const engine = { ...cc, fx: { GameConfig: { BLOCK_INFO: [infos] } }, instantiate() {
const component = {
init(info) { this.info = info; events.push('init' + info.id); },
tryWashDirt() {
assert.ok(this.info, 'all async members initialized');
assert.ok(events.includes('floor'), 'mobile floor overlay exists before washing');
events.push('wash' + this.info.id);
},
};
return { set parent(value) { value.children.push(this); }, setPosition() {}, getComponent: () => component };
} };
const Init = loadMembers('assets/Script/Map.ts', ['blockInit'], {
cc: engine, performance: { now: () => 0 }, setTimeout: fn => timers.push(fn),
});
const map = Object.assign(new Init(), {
node: { children: [] }, blocks: [], blocks2: [], boom17Blocks: [], blockNum: 0,
Block_Array: [{}, {}], sortBlock: blocks => blocks,
scheduleOnce: fn => frames.push(fn), addMoveFloor() { events.push('floor'); this.blocks2 = []; },
});
map.blockInit(() => events.push('complete'));
while (frames.length) frames.shift()();
assert.equal(events.some(event => event.startsWith('wash')), false);
while (timers.length) timers.shift()();
assert.deepEqual(events.filter(event => event.startsWith('wash')), ['wash1', 'wash2', 'wash3', 'wash4']);
assert.equal(events.at(-1), 'complete');
assert.equal(map.blocks.length, 2, 'gray and monochrome stay outside clear-target counting');
});
test('MapBlock activation is reset on reuse and legacy init defaults to ordinary terrain', () => {
const Tile = loadMembers('assets/Script/MapBlock.ts', ['init']);
const icon = { active: false };
const tile = Object.assign(new Tile(), { node: { getChildByName: name => name === 'xizao' ? icon : null } });
tile.init(2, 3, true);
assert.equal(tile.washing, true);
assert.equal(icon.active, true);
tile.init(4, 5);
assert.equal(tile.washing, false);
assert.equal(icon.active, false);
assert.deepEqual([tile.posX, tile.posY], [4, 5]);
});
test('terrain prefab binds the supplied washing and four dirt assets without baking dirt into terrain', () => {
const root = path.resolve(__dirname, '..');
const json = file => JSON.parse(fs.readFileSync(path.join(root, file), 'utf8'));
const data = json('assets/prefab/map/MapBlock.prefab');
const icon = data.find(item => item.__type__ === 'cc.Node' && item._name === 'xizao');
assert.ok(icon, 'washing child exists');
assert.equal(icon._active, false, 'ordinary terrain keeps the old appearance');
assert.deepEqual([icon._contentSize.width, icon._contentSize.height], [120, 120]);
const sprite = icon._components.map(ref => data[ref.__id__]).find(item => item.__type__ === 'cc.Sprite');
assert.equal(sprite._spriteFrame.__uuid__, json('assets/UI/prop/niba/xizao.png.meta').subMetas.xizao.uuid);
const component = data.find(item => Array.isArray(item.dirtFrames));
const expected = [1, 2, 3, 4].map(index => {
const meta = json(`assets/UI/prop/niba/niba${index}.png.meta`);
return Object.values(meta.subMetas)[0].uuid;
});
assert.deepEqual(component.dirtFrames.map(ref => ref.__uuid__), expected);
assert.equal(component.washingBubble.__uuid__, json('assets/UI/prop/niba/paopao.png.meta').subMetas.paopao.uuid);
const map = Object.assign(new Controller(), { MapBlockPrefab: { data: { getComponent: () => component } } });
assert.equal(map.getDirtFrames(), component.dirtFrames);
assert.equal(map.getWashingBubble(), component.washingBubble);
});