230 lines
13 KiB
JavaScript
230 lines
13 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, loadModule } = require('./new-player-ab-test-support.cjs');
|
|
|
|
const level = JSON.parse(fs.readFileSync(path.join(__dirname, '../assets/custom/Json/level9001.json'), 'utf8'));
|
|
const info = level.LEVEL_INFO[0];
|
|
const { nearestLaserHit } = loadModule('assets/Script/LaserRules.ts');
|
|
const directions = { right: [1, 0], left: [-1, 0], up: [0, 1], down: [0, -1] };
|
|
const cc = { v2: (x, y) => ({ x, y }), instantiate: () => {
|
|
const component = { special: 0, longAndShort: [], init(_data, x, y, direction) {
|
|
Object.assign(this, { x, y, direction });
|
|
} };
|
|
const wall = { getComponent: () => component };
|
|
return { children: [null, wall], getChildByName: () => wall, setPosition() {} };
|
|
} };
|
|
const MapController = loadMembers('assets/Script/Map.ts', ['getWllDiraction', 'createWall', 'shrinkVine'], { cc });
|
|
const Block = loadMembers('assets/Script/Block.ts', ['initBlocks'], { cc });
|
|
|
|
// Execute the game's wall direction and prefab creation methods, in createMap's x/y order.
|
|
function gameSlots() {
|
|
const slots = [];
|
|
const map = Object.assign(new MapController(), {
|
|
mapWidth: info.map[0], mapHeight: info.map[1], wallNum: 0,
|
|
Wall_Prefab: Array.from({ length: 12 }), node: {},
|
|
leftDoors: [], rightDoors: [], topDoors: [], bottomDoors: [],
|
|
openWall: [], freezeWall: [], lockWall: [], longAndShortWall: [],
|
|
setDoorInfo(wall) { slots.push({ num: this.wallNum, ...wall.getComponent('Wall') }); },
|
|
});
|
|
map.mapBlocksWall = Array.from({ length: map.mapWidth }, (_, x) =>
|
|
Array.from({ length: map.mapHeight }, (_, y) => {
|
|
const boundary = x === 0 || y === 0 || x === map.mapWidth - 1 || y === map.mapHeight - 1
|
|
|| info.gap.some(point => point.x === x && point.y === y);
|
|
const component = { posX: x, posY: y, setDiraction(direction) { this.direction = direction; } };
|
|
return { x, y, opacity: boundary ? 254 : 255, getComponent: () => component };
|
|
}));
|
|
for (const column of map.mapBlocksWall) for (const cell of column) {
|
|
if (cell.opacity !== 254) continue;
|
|
const direction = map.getWllDiraction('wall', { x: cell.x, y: cell.y });
|
|
if (direction != null) map.createWall(direction, cell);
|
|
}
|
|
return slots;
|
|
}
|
|
const slots = gameSlots();
|
|
function cells(piece) {
|
|
const block = Object.assign(new Block(), { block_Info: piece });
|
|
block.initBlocks();
|
|
const offsets = piece.block === 23 ? [{ x: 0, y: 0 }] : block.allBlocks;
|
|
const x = piece.position.x / 120 + (info.map[0] - 2) / 2;
|
|
const y = piece.position.y / 120 + info.map[1] / 2;
|
|
return offsets.map(offset => ({ x: x + offset.x, y: y + offset.y }));
|
|
}
|
|
function rect(point) {
|
|
return { type: 'rect', minX: point.x - 0.5, maxX: point.x + 0.5,
|
|
minY: point.y - 0.5, maxY: point.y + 0.5 };
|
|
}
|
|
function ray(laser) {
|
|
return ['emitter', 'receiver'].map(role => {
|
|
const slot = slots[laser[role].num], direction = directions[slot.direction];
|
|
return { x: slot.x + direction[0] / 2, y: slot.y + direction[1] / 2 };
|
|
});
|
|
}
|
|
// Grid fixture for the authored layout; runtime collider collection has separate tests.
|
|
function blockers(pieces = level.BLOCK_INFO[0], terrain = info.risefall) {
|
|
return [
|
|
...pieces.filter(piece => piece.type !== 22).flatMap(piece =>
|
|
cells(piece).map(cell => ({ owner: piece.id, ...rect(cell) }))),
|
|
...terrain.filter(cell => cell.vine > 0).map(cell => ({ owner: `vine:${cell.x},${cell.y}`, ...rect(cell) })),
|
|
];
|
|
}
|
|
function firstHit(laser, objects = blockers()) {
|
|
const hit = nearestLaserHit(...ray(laser), objects);
|
|
return hit ? objects[hit.colliderIndex].owner : null;
|
|
}
|
|
|
|
test('level9001 uses actual complete straight-wall slots and independent 1/2/3-cell gates', () => {
|
|
assert.equal(info.id, '9001');
|
|
assert.equal(slots.length, 32);
|
|
assert.equal(new Set(info.lasers.map(laser => laser.id)).size, 3);
|
|
assert.deepEqual(info.lasers.map(laser => laser.color), [7, 7, 6]);
|
|
const used = new Set();
|
|
const expectedHeads = [[0, 3, 9, 3], [0, 6, 9, 6], [4, 0, 4, 9]];
|
|
info.lasers.forEach((laser, index) => {
|
|
const emitter = slots[laser.emitter.num], receiver = slots[laser.receiver.num];
|
|
assert.deepEqual([emitter.x, emitter.y, receiver.x, receiver.y], expectedHeads[index]);
|
|
const forward = directions[emitter.direction], back = directions[receiver.direction];
|
|
assert.ok(forward && back, 'heads must use complete straight walls');
|
|
assert.deepEqual(forward.map((value, axis) => value + back[axis]), [0, 0]);
|
|
const [start, end] = ray(laser);
|
|
assert.equal((end.x - start.x) * forward[1] - (end.y - start.y) * forward[0], 0);
|
|
assert.ok((end.x - start.x) * forward[0] + (end.y - start.y) * forward[1] > 0);
|
|
const gate = level.WALL_INFO[0].filter(wall => wall.laserId === laser.id);
|
|
assert.equal(gate.length, index + 1);
|
|
assert.equal(gate[0].length, gate.length);
|
|
gate.forEach((wall, offset) => {
|
|
assert.equal(wall.length, offset ? 0 : gate.length);
|
|
assert.equal(wall.special, 0);
|
|
assert.equal(wall.color, gate[0].color);
|
|
assert.equal(wall.id, gate[0].id);
|
|
const first = slots[gate[0].num], slot = slots[wall.num];
|
|
assert.equal(slot.direction, first.direction);
|
|
assert.ok(directions[slot.direction], 'gate segments must use complete straight walls');
|
|
assert.equal(slot.x, first.x + (['up', 'down'].includes(slot.direction) ? offset : 0));
|
|
assert.equal(slot.y, first.y + (['left', 'right'].includes(slot.direction) ? offset : 0));
|
|
});
|
|
for (const num of [laser.emitter.num, laser.receiver.num, ...gate.map(wall => wall.num)]) {
|
|
assert.ok(!used.has(num), `wall slot ${num} overlaps`);
|
|
used.add(num);
|
|
}
|
|
});
|
|
assert.deepEqual(level.WALL_INFO[0].filter(wall => wall.length).map(wall => wall.color), [10, 8, 5]);
|
|
});
|
|
|
|
test('all blocks and terrain are inside the board without overlapping, and target widths fit their gates', () => {
|
|
const occupied = new Set(), ids = new Set();
|
|
for (const piece of level.BLOCK_INFO[0]) {
|
|
assert.ok(!ids.has(piece.id)); ids.add(piece.id);
|
|
for (const point of cells(piece)) {
|
|
assert.ok(Number.isInteger(point.x) && Number.isInteger(point.y));
|
|
assert.ok(point.x >= 1 && point.x <= 8 && point.y >= 1 && point.y <= 8);
|
|
const key = `${point.x},${point.y}`;
|
|
assert.ok(!occupied.has(key), `overlapping block at ${key}`); occupied.add(key);
|
|
}
|
|
if (piece.type === 22 || piece.block === 23) continue;
|
|
const gate = level.WALL_INFO[0].find(wall => wall.length && wall.color === piece.color);
|
|
assert.ok(gate, `target ${piece.id} has no color-matched gate`);
|
|
const axis = ['up', 'down'].includes(slots[gate.num].direction) ? 'x' : 'y';
|
|
const values = cells(piece).map(point => point[axis]);
|
|
assert.ok(Math.max(...values) - Math.min(...values) + 1 <= gate.length);
|
|
}
|
|
for (const point of info.risefall) {
|
|
const key = `${point.x},${point.y}`;
|
|
assert.ok(!occupied.has(key), `terrain overlaps at ${key}`); occupied.add(key);
|
|
}
|
|
assert.deepEqual(info.risefall.filter(point => point.vine).map(point => point.vine), [2, 1]);
|
|
});
|
|
|
|
test('initial green beams hit the draggable block and vine; the crossing cyan beam passes both single-color tiles', () => {
|
|
assert.deepEqual(info.lasers.map(laser => firstHit(laser)), [90010, 'vine:6,6', null]);
|
|
const cyan = info.lasers[2];
|
|
const mobile = cells(level.BLOCK_INFO[0].find(piece => piece.type === 22))[0];
|
|
const fixed = info.risefall.find(point => !point.vine);
|
|
// Both transparent objects really lie on the beam, rather than accidentally missing it.
|
|
for (const point of [mobile, fixed]) assert.ok(nearestLaserHit(...ray(cyan), [rect(point)]));
|
|
const [greenStart, greenEnd] = ray(info.lasers[0]), [cyanStart, cyanEnd] = ray(cyan);
|
|
assert.ok(cyanStart.x > greenStart.x && cyanStart.x < greenEnd.x);
|
|
assert.ok(greenStart.y > cyanStart.y && greenStart.y < cyanEnd.y);
|
|
});
|
|
|
|
test('moving the red blocker clears only its own green group; a fixed obstacle still blocks a ray', () => {
|
|
const pieces = structuredClone(level.BLOCK_INFO[0]);
|
|
pieces.find(piece => piece.id === 90010).position.y += 120;
|
|
assert.deepEqual(info.lasers.map(laser => firstHit(laser, blockers(pieces))), [null, 'vine:6,6', null]);
|
|
const fixed = blockers().find(object => object.owner === 90050);
|
|
assert.ok(nearestLaserHit({ x: 0.5, y: 2 }, { x: 8.5, y: 2 }, [fixed]));
|
|
});
|
|
|
|
test('a simple red exit route stays clear and the actual vine shrink releases the second green beam', () => {
|
|
const pieces = structuredClone(level.BLOCK_INFO[0]);
|
|
const red = pieces.find(piece => piece.id === 90010);
|
|
const obstacles = pieces.filter(piece => piece.id !== red.id).flatMap(cells).concat(info.risefall);
|
|
for (const point of [{ x: 2, y: 4 }, { x: 2, y: 5 }, { x: 2, y: 6 }, { x: 2, y: 7 }, { x: 1, y: 7 }]) {
|
|
assert.ok(!obstacles.some(other => other.x === point.x && other.y === point.y));
|
|
red.position.x = (point.x - 4) * 120; red.position.y = (point.y - 5) * 120;
|
|
assert.equal(firstHit(info.lasers[0], blockers(pieces)), null);
|
|
}
|
|
const remaining = pieces.filter(piece => piece.id !== red.id);
|
|
const terrain = structuredClone(info.risefall);
|
|
const removed = [];
|
|
const map = Object.assign(new MapController(), {
|
|
vineBlock: [{ segments: terrain.filter(point => point.vine) }],
|
|
releaseVineSegment(segment) { removed.push(segment); }, queueVineShrinkAnimation() {},
|
|
});
|
|
map.shrinkVine();
|
|
assert.equal(removed.length, 2);
|
|
assert.deepEqual(info.lasers.map(laser => firstHit(laser, blockers(remaining, terrain.filter(point => !removed.includes(point))))),
|
|
[null, null, null]);
|
|
// The remaining yellow block can move down one cell; the cyan block can move up one cell.
|
|
for (const [id, dy] of [[90020, -120], [90030, 120]]) {
|
|
const piece = remaining.find(item => item.id === id);
|
|
piece.position.y += dy;
|
|
const others = remaining.filter(item => item.id !== id).flatMap(cells);
|
|
assert.ok(cells(piece).every(point => !others.some(other => other.x === point.x && other.y === point.y)));
|
|
}
|
|
});
|
|
|
|
test('the authored JSON passes runtime validation and runtime collection preserves initial reception', () => {
|
|
const system = { collide_arr: [] };
|
|
const Runtime = loadMembers('assets/Script/LaserSystem.ts', ['validate', 'collectColliders', 'colliderShape'], {
|
|
cc: { ...cc, isValid: node => !!node }, LQCollideSystem: system,
|
|
LQCollideShape: { Rect: 1, Circle: 2, Polygon: 3 },
|
|
DIRECTIONS: {
|
|
right: { x: 1, y: 0, opposite: 'left' }, left: { x: -1, y: 0, opposite: 'right' },
|
|
up: { x: 0, y: 1, opposite: 'down' }, down: { x: 0, y: -1, opposite: 'up' },
|
|
},
|
|
});
|
|
const map = { node: {}, riseFallBlock: [], mapBlocksWall: Array.from({ length: 10 }, () =>
|
|
Array.from({ length: 10 }, () => ({ opacity: 250 }))) };
|
|
const runtime = Object.assign(new Runtime(), { map });
|
|
const runtimeSlots = slots.map(slot => ({ ...slot, posX: slot.x, posY: slot.y,
|
|
wall_Info: level.WALL_INFO[0].find(wall => wall.num === slot.num),
|
|
node: { parent: { getChildByName: name => name === 'laser' ? {} : null } },
|
|
}));
|
|
assert.equal(runtime.validate(runtimeSlots, info.lasers).length, 3);
|
|
const parent = block => ({ parent: map.node,
|
|
getComponent: name => name === 'Block' ? block : null, getChildByName: () => null });
|
|
const collider = (point, owner, name = 'icon') => {
|
|
system.collide_arr.push({ enabled: true, is_enable: true, data_string: '', collide_shape: 1,
|
|
offset: { x: 0, y: 0 }, size: { width: 1, height: 1 },
|
|
node: { parent: owner, name, activeInHierarchy: true,
|
|
getComponent: () => null, getChildByName: () => null,
|
|
convertToWorldSpaceAR: offset => ({ x: point.x + offset.x, y: point.y + offset.y }),
|
|
},
|
|
});
|
|
};
|
|
for (const piece of level.BLOCK_INFO[0]) {
|
|
const owner = parent({ type: piece.type, over: false });
|
|
for (const point of cells(piece)) collider(point, owner);
|
|
}
|
|
for (const point of info.risefall) {
|
|
const owner = parent(null);
|
|
if (!point.vine) map.riseFallBlock.push(owner);
|
|
collider(point, owner, 'rise');
|
|
}
|
|
const shapes = runtime.collectColliders().map(item => item.shape);
|
|
assert.equal(shapes.length, 9, 'single-color mobile and fixed tiles must be excluded; vine and fixed obstacle remain');
|
|
assert.deepEqual(info.lasers.map(laser => !!nearestLaserHit(...ray(laser), shapes)), [true, true, false]);
|
|
});
|