138 lines
5.6 KiB
JavaScript
138 lines
5.6 KiB
JavaScript
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`);
|
|
}
|
|
}
|
|
});
|