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); });