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