import assert from "node:assert/strict"; import test from "node:test"; import { BOT_COUNT, BOT_ID_BASE, Weapon, shooterGame, } from "../dist/index.js"; function runHeadlessMatch(ticks) { const server = shooterGame.createServer(); const totals = { shots: 0, damageEvents: 0, eliminations: 0, respawns: 0, }; for (let tick = 0; tick < ticks; tick += 1) { const result = server.step(); for (const event of result.events) { if (event.type === "shot") totals.shots += 1; if (event.type === "damage") totals.damageEvents += 1; if (event.type === "elimination") totals.eliminations += 1; if (event.type === "respawn") totals.respawns += 1; } } return { ...totals, tick: server.tick, scores: [...server.currentState.players.values()] .map((player) => ({ id: player.id, kills: player.kills, deaths: player.deaths, alive: player.alive, x: Number(player.x.toFixed(5)), z: Number(player.z.toFixed(5)), })) .sort((a, b) => a.id - b.id), }; } test("the complete shooter match simulates headlessly on the backend", () => { const match = runHeadlessMatch(7_200); assert.equal(match.tick, 7_200); assert.equal(match.scores.length, BOT_COUNT); assert.ok(match.shots > 700, `expected active combat, saw ${match.shots} shots`); assert.ok(match.damageEvents > 50, `expected hits, saw ${match.damageEvents}`); assert.ok(match.eliminations >= 10, `expected kills, saw ${match.eliminations}`); assert.ok(match.respawns >= 8, `expected respawns, saw ${match.respawns}`); assert.equal( match.scores.reduce((sum, score) => sum + score.kills, 0), match.eliminations, ); assert.ok(match.scores.every((score) => Number.isFinite(score.x + score.z))); }); test("backend simulation is deterministic", () => { assert.deepEqual(runHeadlessMatch(2_400), runHeadlessMatch(2_400)); }); test("human hitscan damages the historical target without rewinding live positions", () => { const server = shooterGame.createServer(); server.addPlayer(1); const shooter = server.currentState.players.get(1); const target = server.currentState.players.get(BOT_ID_BASE); for (const bot of server.currentState.players.values()) { if (bot.id === BOT_ID_BASE || bot.id === 1) continue; bot.alive = false; bot.respawnTicks = 10_000; } shooter.x = -4; shooter.z = -15; shooter.yaw = Math.PI / 2; shooter.pitch = 0; shooter.spawnProtectionTicks = 0; target.x = 0; target.z = -15; target.velocityX = 0; target.velocityZ = 0; target.inputForward = 0; target.inputStrafe = 0; target.spawnProtectionTicks = 0; target.bot = null; server.step(); target.z = -12.5; const healthBefore = target.health; server.submitInput(1, { sequence: 1, targetTick: 2, observedTick: 1, input: { strafe: 0, forward: 0, yaw: Math.PI / 2, pitch: 0, fire: true, sprint: false, reload: false, weapon: Weapon.PulseRifle, }, }); const result = server.step(); assert.equal(target.z, -12.5, "the live target must not move backward"); assert.ok(target.health < healthBefore, "the rewound hitbox should take damage"); assert.ok( result.events.some( (event) => event.type === "damage" && event.targetId === BOT_ID_BASE, ), ); const shot = result.events.find( (event) => event.type === "shot" && event.sourceId === 1, ); assert.ok(shot); assert.ok(Math.abs(shot.endZ + 15) < 0.01); }); test("shooter recordings seek through checkpoints with privacy intact", () => { const server = shooterGame.createServer({ replaySeed: 99 }); server.addPlayer(1); for (let tick = 0; tick < 420; tick += 1) server.step(); const recording = server.exportRecording(); assert.equal(recording.durationTicks, 420); assert.deepEqual( recording.checkpoints.map(({ tick }) => tick), [0, 120, 240, 360], ); const replay = shooterGame.createReplay(recording); replay.seek(135); const finalFrame = replay.seek(420); assert.deepEqual(finalFrame.state, server.currentState); const playerView = replay.viewAs(1).state; assert.ok(playerView.players.has(1)); assert.equal("nextEventId" in playerView, false); assert.ok( [...playerView.players.values()].every((player) => !("bot" in player)), ); }); test("a human elimination creates a bounded attacker-perspective killcam", () => { const server = shooterGame.createServer(); server.addPlayer(1); let ticket = null; for (let tick = 0; tick < 600 && !ticket; tick += 1) { server.step(); ticket = server.drainReplayTickets(1)[0] ?? null; } assert.ok(ticket, "expected the standing human to receive a bot killcam"); assert.equal(ticket.requesterId, 1); assert.ok(ticket.perspectiveId >= BOT_ID_BASE); assert.equal(ticket.toTick, ticket.issuedAtTick); assert.ok(ticket.frames.length > 60); assert.ok(ticket.frames.length < 300); assert.ok( ticket.frames.every(({ state }) => [...state.players.values()].every( (player) => !("bot" in player) && !("kills" in player), ), ), ); }); test("snapshots and perception events cross the binary protocol", () => { const server = shooterGame.createServer(); server.addPlayer(1); const snapshot = server.createSnapshot(1, 123.5); const snapshotFrame = shooterGame.protocol.encodeServer({ kind: "snapshot", snapshot, }); const decodedSnapshot = shooterGame.protocol.decodeServer(snapshotFrame); assert.equal(decodedSnapshot.kind, "snapshot"); assert.equal(decodedSnapshot.snapshot.state.players.get(1).ammo.length, 3); const event = { id: 99, type: "shot", weapon: Weapon.RailRifle, sourceId: 1, originX: 1, originY: 1.58, originZ: 2, endX: 11, endY: 1.4, endZ: -4, impact: "wall", }; const eventFrame = shooterGame.protocol.encodeServer({ kind: "event", tick: 50, event, }); const decodedEvent = shooterGame.protocol.decodeServer(eventFrame); assert.equal(decodedEvent.kind, "event"); assert.equal(decodedEvent.tick, 50); assert.deepEqual( { ...decodedEvent.event, originY: Number(decodedEvent.event.originY.toFixed(2)), endY: Number(decodedEvent.event.endY.toFixed(2)), }, event, ); }); test("occluded combatants stay private while their gunfire becomes anonymous audio", () => { const server = shooterGame.createServer(); server.addPlayer(1); const authority = server.currentState; const viewer = authority.players.get(1); const hiddenBot = authority.players.get(BOT_ID_BASE); viewer.x = 0; viewer.z = -6; viewer.yaw = 0; hiddenBot.x = 0; hiddenBot.z = 6; const snapshot = server.createSnapshot(1, 0); assert.equal(snapshot.state.players.has(BOT_ID_BASE), false); const perceptions = server.createPerceptions(1, [ { id: 777, type: "shot", sourceId: BOT_ID_BASE, weapon: Weapon.PulseRifle, originX: hiddenBot.x, originY: 1.58, originZ: hiddenBot.z, endX: hiddenBot.x, endY: 1.58, endZ: hiddenBot.z - 10, impact: "wall", }, ]); assert.equal(perceptions.length, 1); assert.equal(perceptions[0].type, "sound"); assert.equal("sourceId" in perceptions[0], false); assert.equal("originX" in perceptions[0], false); }); test("visible opponents expose exact health but redact armor and inventory", () => { const server = shooterGame.createServer(); server.addPlayer(1); const viewer = server.currentState.players.get(1); const opponent = server.currentState.players.get(BOT_ID_BASE); viewer.x = -4; viewer.z = -15; viewer.yaw = Math.PI / 2; opponent.x = 0; opponent.z = -15; opponent.health = 37; opponent.armor = 19; opponent.ammo[Weapon.PulseRifle].magazine = 3; const replicated = server.createSnapshot(1, 0).state.players.get(BOT_ID_BASE); assert.ok(replicated, "opponent should be visible in the clear corridor"); assert.equal(replicated.health, 37); assert.equal(replicated.armor, 0); assert.ok( replicated.ammo.every( (ammo) => ammo.magazine === 0 && ammo.reserve === 0, ), ); assert.equal(replicated.x, opponent.x); assert.equal(replicated.weapon, opponent.weapon); viewer.yaw = -Math.PI / 2; assert.equal( server.createSnapshot(1, 0).state.players.has(BOT_ID_BASE), false, ); });