This commit is contained in:
111
packages/shared/test/flux.test.mjs
Normal file
111
packages/shared/test/flux.test.mjs
Normal file
@@ -0,0 +1,111 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import {
|
||||
FLUX_BOT_IDS,
|
||||
FLUX_SNAPSHOT_RATE,
|
||||
FLUX_TICK_RATE,
|
||||
fluxGame,
|
||||
} from "../dist/index.js";
|
||||
|
||||
function createThrustPacket(sequence, targetTick, thrust = true) {
|
||||
return {
|
||||
sequence,
|
||||
targetTick,
|
||||
observedTick: Math.max(0, targetTick - 1),
|
||||
input: { thrust },
|
||||
};
|
||||
}
|
||||
|
||||
test("Flux Relay is a complete game defined through the public authoring API", () => {
|
||||
assert.equal(fluxGame.tickRateHz, FLUX_TICK_RATE);
|
||||
assert.equal(fluxGame.snapshotRateHz, FLUX_SNAPSHOT_RATE);
|
||||
|
||||
const server = fluxGame.createServer();
|
||||
server.addPlayer(1);
|
||||
assert.deepEqual(server.submitInput(1, createThrustPacket(1, 1)), {
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
const events = [];
|
||||
for (let tick = 0; tick < 180; tick += 1) {
|
||||
events.push(...server.step().events);
|
||||
}
|
||||
|
||||
assert.ok(server.currentState.cyanScore >= 2);
|
||||
assert.equal(server.currentState.orangeScore, 0);
|
||||
assert.ok(
|
||||
events.some((event) => event.type === "round-won" && event.team === "cyan"),
|
||||
);
|
||||
assert.ok(server.currentState.players.some((player) => player.id === FLUX_BOT_IDS[0]));
|
||||
});
|
||||
|
||||
test("Flux snapshots disclose exact energy only to its owner", () => {
|
||||
const server = fluxGame.createServer();
|
||||
server.addPlayer(1);
|
||||
server.addPlayer(2);
|
||||
|
||||
const playerOneView = server.createSnapshot(1, 0).state;
|
||||
const playerTwoView = server.createSnapshot(2, 0).state;
|
||||
|
||||
assert.equal(playerOneView.players.find((player) => player.id === 1)?.energy, 100);
|
||||
assert.equal(playerOneView.players.find((player) => player.id === 2)?.energy, null);
|
||||
assert.equal(playerTwoView.players.find((player) => player.id === 1)?.energy, null);
|
||||
assert.equal(playerTwoView.players.find((player) => player.id === 2)?.energy, 100);
|
||||
assert.ok(
|
||||
playerOneView.players
|
||||
.filter((player) => player.bot)
|
||||
.every((player) => player.energy === null),
|
||||
);
|
||||
assert.equal("nextEventId" in playerOneView, false);
|
||||
});
|
||||
|
||||
test("the same private Flux event becomes a self cue or anonymous field cue", () => {
|
||||
const server = fluxGame.createServer();
|
||||
server.addPlayer(1);
|
||||
server.addPlayer(2);
|
||||
const player = server.currentState.players.find((candidate) => candidate.id === 1);
|
||||
player.energy = 0.2;
|
||||
|
||||
server.submitInput(1, createThrustPacket(1, 1));
|
||||
const result = server.step();
|
||||
const overheat = result.events.find((event) => event.type === "overheated");
|
||||
assert.ok(overheat);
|
||||
|
||||
const ownCue = server.createPerceptions(1, [overheat])[0];
|
||||
const fieldCue = server.createPerceptions(2, [overheat])[0];
|
||||
assert.equal(ownCue.scope, "self");
|
||||
assert.equal(fieldCue.scope, "field");
|
||||
assert.equal("playerId" in ownCue, false);
|
||||
assert.equal("playerId" in fieldCue, false);
|
||||
});
|
||||
|
||||
test("Flux uses the generic protocol, prediction, acknowledgement, and reconciliation", () => {
|
||||
const server = fluxGame.createServer();
|
||||
server.addPlayer(7);
|
||||
const client = fluxGame.createClient();
|
||||
client.initialize(7, server.createSnapshot(7, 10));
|
||||
|
||||
const packet = client.createInput({ thrust: true }, 1);
|
||||
const wirePacket = fluxGame.protocol.decodeClient(
|
||||
fluxGame.protocol.encodeClient({ kind: "input", packet }),
|
||||
);
|
||||
assert.equal(wirePacket.kind, "input");
|
||||
assert.deepEqual(wirePacket.packet, packet);
|
||||
assert.equal(server.submitInput(7, packet).accepted, true);
|
||||
|
||||
client.step();
|
||||
const result = server.step();
|
||||
client.acknowledge(result.acknowledgements[0].sequence);
|
||||
const wireSnapshot = fluxGame.protocol.decodeServer(
|
||||
fluxGame.protocol.encodeServer({
|
||||
kind: "snapshot",
|
||||
snapshot: server.createSnapshot(7, 20),
|
||||
}),
|
||||
);
|
||||
assert.equal(wireSnapshot.kind, "snapshot");
|
||||
client.reconcile(wireSnapshot.snapshot);
|
||||
|
||||
const local = client.currentState.players.find((player) => player.id === 7);
|
||||
assert.equal(local.thrust, true);
|
||||
assert.ok(local.energy < 100);
|
||||
});
|
||||
219
packages/shared/test/royale.test.mjs
Normal file
219
packages/shared/test/royale.test.mjs
Normal file
@@ -0,0 +1,219 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import {
|
||||
ROYALE_BOT_COUNT,
|
||||
ROYALE_BOT_ID_BASE,
|
||||
ROYALE_CHUNK_SIZE,
|
||||
ROYALE_MAP_SEED,
|
||||
collidesWithRoyaleMap,
|
||||
generateRoyaleChunk,
|
||||
hasRoyaleLineOfSight,
|
||||
royaleGame,
|
||||
} from "../dist/index.js";
|
||||
|
||||
test("the public world seed deterministically generates a 2 km chunked island", () => {
|
||||
const first = generateRoyaleChunk(-3, 4);
|
||||
const second = generateRoyaleChunk(-3, 4);
|
||||
const neighbor = generateRoyaleChunk(-2, 4);
|
||||
assert.deepEqual(first, second);
|
||||
assert.notDeepEqual(first, neighbor);
|
||||
assert.equal(ROYAALE_MAP_SEED_FOR_TEST(), ROYALE_MAP_SEED);
|
||||
assert.equal(ROYAALE_CHUNK_SIZE_FOR_TEST(), ROYALE_CHUNK_SIZE);
|
||||
assert.ok(first.obstacles.every((obstacle) => obstacle.id.startsWith("-3:4:")));
|
||||
});
|
||||
|
||||
test("Syncer Royale simulates a complete authority battle headlessly", () => {
|
||||
const server = royaleGame.createServer();
|
||||
const totals = { shots: 0, damage: 0, eliminations: 0, winners: 0 };
|
||||
for (let tick = 0; tick < 6_000; tick += 1) {
|
||||
for (const event of server.step().events) {
|
||||
if (event.type === "shot") totals.shots += 1;
|
||||
if (event.type === "damage") totals.damage += 1;
|
||||
if (event.type === "elimination") totals.eliminations += 1;
|
||||
if (event.type === "winner") totals.winners += 1;
|
||||
}
|
||||
}
|
||||
assert.equal(server.currentState.players.length, ROYALE_BOT_COUNT);
|
||||
assert.ok(totals.shots > 150, `expected firefights, saw ${totals.shots}`);
|
||||
assert.ok(totals.damage > 50, `expected damage, saw ${totals.damage}`);
|
||||
assert.ok(totals.eliminations > 15, `expected eliminations, saw ${totals.eliminations}`);
|
||||
assert.ok(server.currentState.players.every((player) => Number.isFinite(player.x + player.z)));
|
||||
});
|
||||
|
||||
test("Royale projections hide authority secrets and distant combatants", () => {
|
||||
const server = royaleGame.createServer();
|
||||
server.addPlayer(1);
|
||||
const authority = server.currentState;
|
||||
const viewer = authority.players.find((player) => player.id === 1);
|
||||
const hidden = authority.players.find((player) => player.id === ROYALE_BOT_ID_BASE);
|
||||
viewer.altitude = 0;
|
||||
viewer.x = 0;
|
||||
viewer.z = 0;
|
||||
hidden.x = 430;
|
||||
hidden.z = 0;
|
||||
hidden.alive = true;
|
||||
|
||||
const snapshot = server.createSnapshot(1, 0).state;
|
||||
assert.equal(snapshot.mapSeed, ROYALE_MAP_SEED);
|
||||
assert.equal("secretSeed" in snapshot, false);
|
||||
assert.ok(snapshot.players.some((player) => player.id === 1));
|
||||
assert.ok(!snapshot.players.some((player) => player.id === hidden.id));
|
||||
assert.ok(
|
||||
snapshot.players
|
||||
.filter((player) => player.id !== 1)
|
||||
.every((player) => player.health === null && player.armor === null),
|
||||
);
|
||||
|
||||
const [sound] = server.createPerceptions(1, [
|
||||
{ id: 99, type: "shot", sourceId: hidden.id, x: hidden.x, y: 1.55, z: hidden.z, yaw: 0, pitch: 0 },
|
||||
]);
|
||||
assert.equal(sound.type, "gunfire");
|
||||
assert.equal("sourceId" in sound, false);
|
||||
assert.equal("x" in sound, false);
|
||||
|
||||
const [ownShot] = server.createPerceptions(1, [
|
||||
{
|
||||
id: 100,
|
||||
type: "shot",
|
||||
sourceId: viewer.id,
|
||||
x: viewer.x,
|
||||
y: 1.55,
|
||||
z: viewer.z,
|
||||
yaw: viewer.yaw,
|
||||
pitch: viewer.pitch,
|
||||
},
|
||||
]);
|
||||
assert.equal(ownShot.type, "shot");
|
||||
assert.equal(ownShot.sourceId, viewer.id);
|
||||
});
|
||||
|
||||
test("Royale enforces a viewer bandwidth budget after ordinary visibility", () => {
|
||||
const server = royaleGame.createServer();
|
||||
server.addPlayer(1);
|
||||
const authority = server.currentState;
|
||||
const viewer = authority.players.find((player) => player.id === 1);
|
||||
viewer.altitude = 0;
|
||||
viewer.x = 0;
|
||||
viewer.z = 0;
|
||||
for (const bot of authority.players.filter((player) => player.bot)) {
|
||||
bot.x = 800;
|
||||
bot.z = 0;
|
||||
}
|
||||
for (let index = 0; index < 100; index += 1) {
|
||||
const item = authority.loot[index];
|
||||
item.active = true;
|
||||
item.x = (index % 10) * 2 - 9;
|
||||
item.z = Math.floor(index / 10) * 2 - 9;
|
||||
}
|
||||
|
||||
const snapshot = server.createSnapshot(1, 0).state;
|
||||
assert.ok(snapshot.stream.droppedCount > 0);
|
||||
assert.ok(snapshot.stream.usedBytes <= snapshot.stream.budgetBytes);
|
||||
assert.ok(snapshot.loot.length < 100);
|
||||
assert.equal(snapshot.players.filter((player) => player.id === 1).length, 1);
|
||||
});
|
||||
|
||||
test("Royale input and projected state round-trip through the generic protocol", () => {
|
||||
const server = royaleGame.createServer();
|
||||
server.addPlayer(7);
|
||||
const client = royaleGame.createClient();
|
||||
const welcome = server.createSnapshot(7, 10);
|
||||
client.initialize(7, welcome);
|
||||
const packet = client.createInput({
|
||||
forward: 1,
|
||||
strafe: 0.25,
|
||||
yaw: 0.7,
|
||||
pitch: -0.2,
|
||||
fire: false,
|
||||
sprint: true,
|
||||
reload: false,
|
||||
}, 1);
|
||||
const decoded = royaleGame.protocol.decodeClient(
|
||||
royaleGame.protocol.encodeClient({ kind: "input", packet }),
|
||||
);
|
||||
assert.equal(decoded.kind, "input");
|
||||
assert.deepEqual(decoded.packet, packet);
|
||||
assert.equal(server.submitInput(7, decoded.packet).accepted, true);
|
||||
server.step();
|
||||
assert.equal(server.currentState.players.find((player) => player.id === 7).pitch, -0.2);
|
||||
client.step();
|
||||
client.reconcile(server.createSnapshot(7, 20));
|
||||
assert.ok(client.currentState.players.some((player) => player.id === 7));
|
||||
});
|
||||
|
||||
test("Royale pitch drives the authoritative three-dimensional hit ray", () => {
|
||||
const server = royaleGame.createServer();
|
||||
server.addPlayer(7);
|
||||
server.addPlayer(8);
|
||||
const authority = server.currentState;
|
||||
for (const bot of authority.players.filter((player) => player.bot)) bot.alive = false;
|
||||
const shooter = authority.players.find((player) => player.id === 7);
|
||||
const target = authority.players.find((player) => player.id === 8);
|
||||
let origin = null;
|
||||
for (let x = -100; x <= 100 && !origin; x += 10) {
|
||||
for (let z = -100; z <= 60; z += 10) {
|
||||
if (
|
||||
!collidesWithRoyaleMap(x, z) &&
|
||||
!collidesWithRoyaleMap(x, z + 40) &&
|
||||
hasRoyaleLineOfSight(x, z, x, z + 40)
|
||||
) {
|
||||
origin = { x, z };
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.ok(origin, "expected an unobstructed public-map firing lane");
|
||||
Object.assign(shooter, { x: origin.x, z: origin.z, altitude: 0, yaw: 0 });
|
||||
Object.assign(target, { x: origin.x, z: origin.z + 40, altitude: 20, health: 100, armor: 0 });
|
||||
const pitch = Math.atan2(20, 40);
|
||||
assert.equal(server.submitInput(7, {
|
||||
sequence: 1,
|
||||
targetTick: 1,
|
||||
observedTick: 0,
|
||||
input: {
|
||||
forward: 0,
|
||||
strafe: 0,
|
||||
yaw: 0,
|
||||
pitch,
|
||||
fire: true,
|
||||
sprint: false,
|
||||
reload: false,
|
||||
},
|
||||
}).accepted, true);
|
||||
const result = server.step();
|
||||
assert.ok(result.events.some((event) => event.type === "damage" && event.targetId === 8));
|
||||
assert.ok(target.health < 100);
|
||||
});
|
||||
|
||||
test("Royale strafe-right matches the first-person camera's screen right", () => {
|
||||
const server = royaleGame.createServer();
|
||||
server.addPlayer(7);
|
||||
const authority = server.currentState;
|
||||
for (const bot of authority.players.filter((player) => player.bot)) bot.alive = false;
|
||||
const player = authority.players.find((candidate) => candidate.id === 7);
|
||||
Object.assign(player, { x: 0, z: 0, altitude: 20, yaw: 0 });
|
||||
assert.equal(server.submitInput(7, {
|
||||
sequence: 1,
|
||||
targetTick: 1,
|
||||
observedTick: 0,
|
||||
input: {
|
||||
forward: 0,
|
||||
strafe: 1,
|
||||
yaw: 0,
|
||||
pitch: 0,
|
||||
fire: false,
|
||||
sprint: false,
|
||||
reload: false,
|
||||
},
|
||||
}).accepted, true);
|
||||
server.step();
|
||||
assert.ok(player.velocityX < 0, "screen-right is world -X while facing world +Z");
|
||||
});
|
||||
|
||||
function ROYAALE_MAP_SEED_FOR_TEST() {
|
||||
return ROYALE_MAP_SEED;
|
||||
}
|
||||
|
||||
function ROYAALE_CHUNK_SIZE_FOR_TEST() {
|
||||
return ROYALE_CHUNK_SIZE;
|
||||
}
|
||||
282
packages/shared/test/shooter.test.mjs
Normal file
282
packages/shared/test/shooter.test.mjs
Normal file
@@ -0,0 +1,282 @@
|
||||
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,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user