const test = require('node:test'); const assert = require('node:assert/strict'); const { loadMembers } = require('./new-player-ab-test-support.cjs'); const MapController = loadMembers('assets/Script/Map.ts', [ 'validateSpawnGateConfigs', 'validateCreatedSpawnGateBatch', 'isSpawnGateAreaEmpty', ]); function fixture(width = 12, height = 12) { return Object.assign(new MapController(), { mapWidth: width + 2, mapHeight: height + 2, Block_Array: Array.from({ length: 23 }, () => ({})), mapBlocksWall: Array.from({ length: width + 2 }, () => Array.from({ length: height + 2 }, () => { const cell = { block_Id: '' }; return { getComponent: () => cell }; })), }); } function gate(width, height, x = 1, y = 1) { return { id: 'rectangle', x, y, width, height, spawnQueue: [{ members: [{ block: 0, type: 0, position: { x: 0, y: 0 } }] }], }; } function block(x, y, offsets = [[0, 0]]) { return { getComponent: () => ({ posX: x, posY: y, allBlocks: offsets.map(([dx, dy]) => ({ x: dx, y: dy })), }), }; } test('spawn config accepts old squares and arbitrary rectangles that fit the map', () => { const map = fixture(); for (const [width, height] of [[2, 2], [3, 3], [1, 1], [1, 10], [10, 1], [2, 5], [6, 3], [10, 10], [11, 2]]) { const config = gate(width, height); const before = JSON.stringify(config); assert.doesNotThrow(() => map.validateSpawnGateConfigs([config]), `${width}x${height}`); assert.equal(JSON.stringify(config), before, 'validation must preserve queue and config data'); } }); test('both spawn dimensions must be positive integers', () => { const map = fixture(); for (const dimension of ['width', 'height']) { for (const value of [0, -1, 1.5, NaN, Infinity, '2', undefined]) { const config = gate(2, 5); config[dimension] = value; assert.throws(() => map.validateSpawnGateConfigs([config]), /positive integers/, `${dimension}=${String(value)}`); } } }); test('rectangles may reach the playable edge but cannot cross it', () => { const map = fixture(12, 8); assert.doesNotThrow(() => map.validateSpawnGateConfigs([gate(11, 7, 2, 2)])); for (const config of [ gate(11, 7, 3, 2), gate(11, 7, 2, 3), gate(2, 5, 0, 1), gate(2, 5, 1, 0), gate(2, 5, 1.5, 1), ]) { assert.throws(() => map.validateSpawnGateConfigs([config]), /outside the playable map/); } }); test('rectangular regions may share an edge but may not share a cell', () => { const map = fixture(); const tall = gate(2, 5); assert.doesNotThrow(() => map.validateSpawnGateConfigs([ tall, gate(6, 3, 3, 1), gate(2, 1, 1, 6), ])); assert.throws(() => map.validateSpawnGateConfigs([tall, gate(6, 3, 2, 3)]), /overlap/); }); test('rectangular size support preserves existing batch validation', () => { const map = fixture(); const cases = [ [config => { delete config.spawnQueue; }, /has no spawnQueue/], [config => { config.spawnQueue[0].members = []; }, /empty batch/], [config => { config.spawnQueue[0].members[0].block = 23; }, /invalid block prefab index/], [config => { config.spawnQueue[0].members[0].position.x = NaN; }, /invalid block position/], [config => { config.spawnQueue[0].members[0].type = 1; }, /stacking block has no stacking color/], [config => { config.spawnQueue[0].members[0].type = 9; }, /adhesive blocks must be paired/], ]; for (const [mutate, error] of cases) { const config = gate(2, 5); mutate(config); assert.throws(() => map.validateSpawnGateConfigs([config]), error); } }); test('created members use the complete rectangular region and full shape footprint', () => { const map = fixture(); const tall = gate(2, 5, 2, 3); assert.doesNotThrow(() => map.validateCreatedSpawnGateBatch([ block(2, 3, [[0, 0], [1, 0]]), block(2, 7, [[0, 0], [1, 0]]), ], tall, 0)); const wide = gate(6, 3, 2, 3); assert.doesNotThrow(() => map.validateCreatedSpawnGateBatch([ block(2, 3), block(7, 5, [[0, 0], [-1, 0], [0, -1]]), ], wide, 0)); for (const outside of [ block(2, 3, [[0, 0], [-1, 0]]), block(3, 3, [[0, 0], [1, 0]]), block(2, 3, [[0, 0], [0, -1]]), block(2, 7, [[0, 0], [0, 1]]), ]) { assert.throws(() => map.validateCreatedSpawnGateBatch([outside], tall, 0), /member exceeds gate area/); } assert.throws(() => map.validateCreatedSpawnGateBatch([ block(2, 7, [[0, 0], [1, 0]]), block(3, 7), ], tall, 0), /members overlap/); }); test('a rectangular gate waits for every cell to clear and ignores occupied cells outside', () => { const map = fixture(); const config = gate(6, 3, 2, 4); map.mapBlocksWall[1][4].getComponent().block_Id = 'outside'; map.mapBlocksWall[8][4].getComponent().block_Id = 'outside'; map.mapBlocksWall[2][3].getComponent().block_Id = 'outside'; map.mapBlocksWall[2][7].getComponent().block_Id = 'outside'; assert.equal(map.isSpawnGateAreaEmpty(config), true); for (let x = 2; x < 8; x++) { for (let y = 4; y < 7; y++) { const cell = map.mapBlocksWall[x][y].getComponent(); cell.block_Id = 'moving-block'; assert.equal(map.isSpawnGateAreaEmpty(config), false, `${x},${y} still occupied`); cell.block_Id = ''; assert.equal(map.isSpawnGateAreaEmpty(config), true, `${x},${y} cleared`); } } });