348 lines
21 KiB
JavaScript
348 lines
21 KiB
JavaScript
const assert = require("assert");
|
|
const fs = require("fs");
|
|
const vm = require("vm");
|
|
const path = require("path");
|
|
const crypto = require("crypto");
|
|
|
|
const source = fs.readFileSync(path.join(__dirname, "..", "runtime", "build", "index.js"), "utf8");
|
|
const context = { console, Math, Date, JSON, isFinite };
|
|
vm.createContext(context);
|
|
vm.runInContext(source, context);
|
|
|
|
assert.strictEqual(context.ENEMIES.length, 9);
|
|
assert.strictEqual(context.SNAPSHOT_TICKS, 1, "Online snapshots should be sent at the full 10 Hz simulation rate.");
|
|
assert.strictEqual(context.ENEMIES[0].category, "air");
|
|
assert.strictEqual(context.ENEMIES[6].category, "ground");
|
|
assert.strictEqual(context.TOWERS.length, 7);
|
|
assert.strictEqual(context.TOWERS[0].targetClass, "ground");
|
|
assert.strictEqual(context.TOWERS[1].targetClass, "both");
|
|
assert.ok(context.TOWERS[1].bothScale < 1, "The all-purpose laser should trade damage for shield-breaking ground coverage.");
|
|
assert.strictEqual(context.TOWERS[3].targetClass, "both");
|
|
assert.ok(context.TOWERS[3].bothScale < 1, "Universal towers should trade damage for flexibility.");
|
|
assert.strictEqual(context.TOWERS[4].damage, 20, "EMP damage should be tuned down to about 20.");
|
|
assert.strictEqual(context.TOWERS[4].targetClass, "both", "EMP should cover both air and ground units.");
|
|
assert.strictEqual(context.TOWERS[6].targetClass, "air", "The Flak tower must only target air units.");
|
|
const hostId = "11111111-1111-1111-1111-111111111111";
|
|
const friendId = "22222222-2222-2222-2222-222222222222";
|
|
const sentInvites = [];
|
|
let inviteLimitRecord = null;
|
|
const inviteNk = {
|
|
storageRead(requests) {
|
|
return requests[0].collection === "private_rooms"
|
|
? [{ value: { host_user_id: hostId, match_id: "private-match" } }]
|
|
: requests[0].collection === "friend_invite_limits" && inviteLimitRecord ? [inviteLimitRecord] : [];
|
|
},
|
|
storageWrite(items) {
|
|
if (items[0].collection === "friend_invite_limits") inviteLimitRecord = { value: items[0].value };
|
|
},
|
|
usersGetId() { return [{ displayName: "Commander" }]; },
|
|
friendsList() { return { friends: [{ state: 0, user: { userId: friendId } }], cursor: "" }; },
|
|
notificationsSend(items) { sentInvites.push(...items); }
|
|
};
|
|
const inviteCtx = { userId: hostId, username: "pilot_host" };
|
|
assert.strictEqual(JSON.parse(context.rpcInviteFriend(inviteCtx, {}, inviteNk, JSON.stringify({ friend_id: friendId, room_code: "ABC123" }))).accepted, true);
|
|
assert.strictEqual(sentInvites.length, 1);
|
|
assert.strictEqual(sentInvites[0].content.room_code, "ABC123");
|
|
assert.strictEqual(sentInvites[0].content.from, "Commander");
|
|
assert.strictEqual(sentInvites[0].persistent, false, "Game invitations must not remain as stale offline messages.");
|
|
assert.strictEqual(JSON.parse(context.rpcInviteFriend(inviteCtx, {}, inviteNk, JSON.stringify({ friend_id: friendId, room_code: "ABC123" }))).error, "invite_cooldown", "Repeated invitations must be rate limited.");
|
|
assert.strictEqual(sentInvites.length, 1, "A rate-limited invitation must not send another notification.");
|
|
assert.strictEqual(JSON.parse(context.rpcInviteFriend({ userId: friendId }, {}, inviteNk, JSON.stringify({ friend_id: hostId, room_code: "ABC123" }))).accepted, false, "Only the private room host may invite.");
|
|
assert.strictEqual(JSON.parse(context.rpcInviteFriend(inviteCtx, {}, { ...inviteNk, friendsList: () => ({ friends: [] }) }, JSON.stringify({ friend_id: friendId, room_code: "ABC123" }))).accepted, false, "Only mutual friends may be invited.");
|
|
const flakState = {
|
|
towers: [{ id: 20, type: 6, x: 800, y: 200, owner: 0, level: 1, cooldown: 0 }],
|
|
enemies: [
|
|
{ id: 401, type: 0, x: 850, y: 200, route_id: 0, path_index: 1, hp: 100, shield: false },
|
|
{ id: 402, type: 6, x: 850, y: 210, route_id: 0, path_index: 1, hp: 100, shield: false }
|
|
],
|
|
credits: [0, 0]
|
|
};
|
|
context.updateTowers(flakState, 1);
|
|
assert.ok(flakState.enemies.find((enemy) => enemy.id === 401).hp < 100, "Flak should damage air units.");
|
|
assert.strictEqual(flakState.enemies.find((enemy) => enemy.id === 402).hp, 100, "Flak must ignore ground units.");
|
|
assert.strictEqual(flakState.shots.length, 1, "Firing towers should publish a visual shot event.");
|
|
assert.strictEqual(flakState.shots[0].tower_id, 20);
|
|
assert.strictEqual(flakState.shots[0].type, 6);
|
|
const shieldTest = { hp: 100, shield: true, shield_hp: 30, max_shield_hp: 30, shield_resistance: 0.28 };
|
|
context.applyDamageToEnemy(shieldTest, 20, 1);
|
|
assert.strictEqual(shieldTest.hp, 100, "Laser damage should drain shield before hull.");
|
|
assert.strictEqual(shieldTest.shield_hp, 10);
|
|
context.applyDamageToEnemy(shieldTest, 20, 1);
|
|
assert.strictEqual(shieldTest.shield, false);
|
|
assert.strictEqual(shieldTest.hp, 90, "Laser damage beyond shield capacity should spill into hull.");
|
|
const priorityState = {
|
|
towers: [{ id: 1, type: 1, x: 800, y: 120, owner: 0, level: 1, cooldown: 0 }],
|
|
enemies: [
|
|
{ id: 201, type: 6, x: 850, y: 120, path_index: 1, hp: 100, shield: true, shield_hp: 50, max_shield_hp: 50, shield_resistance: 0.28 },
|
|
{ id: 202, type: 0, x: 950, y: 120, path_index: 1, hp: 100, shield: false }
|
|
],
|
|
credits: [0, 0], wave: 1
|
|
};
|
|
context.updateTowers(priorityState, 1);
|
|
assert.ok(priorityState.enemies[0].shield_hp < 50, "Online lasers should drain a shield before firing at unshielded units.");
|
|
assert.strictEqual(priorityState.enemies[1].hp, 100);
|
|
|
|
const railgunState = {
|
|
towers: [{ id: 10, type: 5, x: 300, y: 120, owner: 0, level: 1, cooldown: 0 }],
|
|
enemies: [
|
|
{ id: 301, type: 0, x: 400, y: 120, path_index: 1, hp: 1000, shield: false },
|
|
{ id: 302, type: 6, x: 500, y: 120, path_index: 1, hp: 1000, shield: false },
|
|
{ id: 303, type: 0, x: 450, y: 150, path_index: 1, hp: 1000, shield: false }
|
|
],
|
|
credits: [0, 0], wave: 1
|
|
};
|
|
context.updateTowers(railgunState, 0.01);
|
|
assert.ok(railgunState.enemies[0].hp < 1000 && railgunState.enemies[1].hp < 1000, "Online railgun should pierce enemies behind its first hit on the same line.");
|
|
assert.strictEqual(railgunState.enemies[2].hp, 1000, "Online railgun should not hit units away from its beam line.");
|
|
|
|
const empState = {
|
|
towers: [{ id: 11, type: 4, x: 300, y: 120, owner: 0, level: 1, cooldown: 0 }],
|
|
enemies: [
|
|
{ id: 401, type: 0, x: 400, y: 120, path_index: 1, hp: 1000, shield: false },
|
|
{ id: 402, type: 6, x: 450, y: 120, path_index: 1, hp: 1000, shield: false },
|
|
{ id: 403, type: 6, x: 570, y: 120, path_index: 1, hp: 1000, shield: false }
|
|
],
|
|
credits: [0, 0], wave: 1
|
|
};
|
|
context.updateTowers(empState, 0.01);
|
|
assert.strictEqual(empState.enemies[0].hp, 980, "EMP should deal 20 damage to its primary target.");
|
|
assert.strictEqual(empState.enemies[1].hp, 980, "EMP should deal its reduced 20 damage to nearby ground units too.");
|
|
assert.strictEqual(empState.enemies[2].hp, 1000, "EMP should not damage units outside its area radius.");
|
|
|
|
const sent = [];
|
|
const dispatcher = {
|
|
broadcastMessage(opCode, data) {
|
|
sent.push({ opCode, data: JSON.parse(data) });
|
|
},
|
|
matchLabelUpdate(label) {
|
|
let data = label;
|
|
try { data = JSON.parse(label); } catch (error) {}
|
|
sent.push({ opCode: "label", data });
|
|
}
|
|
};
|
|
const nk = { binaryToString: data => Buffer.from(data).toString("utf8") };
|
|
const logger = { info() {} };
|
|
const presenceA = { userId: "pilot-a", sessionId: "a", username: "Pilot A" };
|
|
const presenceB = { userId: "pilot-b", sessionId: "b", username: "Pilot B" };
|
|
|
|
let updatedAccount = null;
|
|
let deletedAccount = null;
|
|
const accountNk = {
|
|
storageRead() { return []; },
|
|
accountUpdateId(...args) { updatedAccount = args; },
|
|
accountDeleteId(...args) { deletedAccount = args; }
|
|
};
|
|
assert.deepStrictEqual(JSON.parse(context.rpcUpdateDisplayName({ userId: "pilot-a" }, logger, accountNk, JSON.stringify({ display_name: " Kommandant Ägir " }))), { accepted: true, display_name: "Kommandant Ägir" });
|
|
assert.strictEqual(updatedAccount[0], "pilot-a");
|
|
assert.strictEqual(updatedAccount[2], "Kommandant Ägir");
|
|
assert.strictEqual(JSON.parse(context.rpcUpdateDisplayName({ userId: "pilot-a" }, logger, accountNk, JSON.stringify({ display_name: "ADMIN" }))).accepted, false);
|
|
assert.strictEqual(JSON.parse(context.rpcUpdateDisplayName({ userId: "pilot-a" }, logger, accountNk, JSON.stringify({ display_name: "A!" }))).accepted, false);
|
|
assert.deepStrictEqual(JSON.parse(context.rpcDeleteAccount({ userId: "pilot-a" }, logger, accountNk, "{}")), { accepted: true });
|
|
assert.deepStrictEqual(deletedAccount, ["pilot-a", false]);
|
|
|
|
const metricStorage = new Map();
|
|
let metricVersion = 0;
|
|
const metricNk = {
|
|
storageRead(ops) {
|
|
return ops.map(op => metricStorage.get(`${op.collection}:${op.userId}:${op.key}`)).filter(Boolean);
|
|
},
|
|
storageWrite(ops) {
|
|
for (const op of ops) {
|
|
const storageKey = `${op.collection}:${op.userId}:${op.key}`;
|
|
const previous = metricStorage.get(storageKey);
|
|
if (op.version === "*" && previous) throw new Error("version conflict");
|
|
if (op.version && op.version !== "*" && (!previous || previous.version !== op.version)) throw new Error("version conflict");
|
|
metricVersion += 1;
|
|
metricStorage.set(storageKey, { value: JSON.parse(JSON.stringify(op.value)), version: String(metricVersion) });
|
|
}
|
|
}
|
|
};
|
|
assert.deepStrictEqual(JSON.parse(context.rpcRecordActivity({ userId: "pilot-metric-a" }, logger, metricNk, JSON.stringify({ platform: "Windows", version: "0.1.0-alpha.2" }))), { accepted: true });
|
|
assert.deepStrictEqual(JSON.parse(context.rpcRecordActivity({ userId: "pilot-metric-a" }, logger, metricNk, JSON.stringify({ platform: "Windows", version: "0.1.0-alpha.2" }))), { accepted: true });
|
|
assert.deepStrictEqual(JSON.parse(context.rpcRecordActivity({ userId: "pilot-metric-b" }, logger, metricNk, JSON.stringify({ platform: "Android", version: "0.1.0-alpha.2" }))), { accepted: true });
|
|
const privateStats = JSON.parse(context.rpcPrivateStats({}, logger, metricNk, "{}"));
|
|
assert.strictEqual(privateStats.total_players, 2);
|
|
assert.strictEqual(privateStats.platforms.windows, 1);
|
|
assert.strictEqual(privateStats.platforms.android, 1);
|
|
assert.strictEqual(privateStats.versions["0.1.0-alpha.2"], 2);
|
|
assert.strictEqual(privateStats.daily_active[context.utcDay(Date.now())], 2);
|
|
assert.throws(() => context.rpcPrivateStats({ userId: "pilot-a" }, logger, metricNk, "{}"), /server-to-server/);
|
|
|
|
const promoStorage = new Map();
|
|
let promoVersion = 0;
|
|
let promoCodeSequence = 0;
|
|
let beforePromoDelete = null;
|
|
const promoNk = {
|
|
uuidv4() {
|
|
promoCodeSequence += 1;
|
|
return `${String(promoCodeSequence).padStart(8, "0")}-0000-4000-8000-000000000001`;
|
|
},
|
|
sha256Hash(value) { return crypto.createHash("sha256").update(value).digest("hex"); },
|
|
storageRead(ops) {
|
|
return ops.map(op => promoStorage.get(`${op.collection}:${op.userId}:${op.key}`)).filter(Boolean);
|
|
},
|
|
storageWrite(ops) {
|
|
for (const op of ops) {
|
|
const key = `${op.collection}:${op.userId}:${op.key}`;
|
|
const previous = promoStorage.get(key);
|
|
if (op.version === "*" && previous) throw new Error("version conflict");
|
|
if (op.version && op.version !== "*" && (!previous || previous.version !== op.version)) throw new Error("version conflict");
|
|
promoVersion += 1;
|
|
promoStorage.set(key, { collection: op.collection, userId: op.userId, key: op.key, value: JSON.parse(JSON.stringify(op.value)), version: String(promoVersion) });
|
|
}
|
|
},
|
|
storageList(userId, collection, limit, cursor) {
|
|
const objects = Array.from(promoStorage.values())
|
|
.filter(record => record.userId === userId && record.collection === collection)
|
|
.slice(0, limit)
|
|
.map(record => ({ key: record.key, value: JSON.parse(JSON.stringify(record.value)) }));
|
|
return { objects, cursor: "" };
|
|
},
|
|
storageDelete(ops) {
|
|
for (const op of ops) {
|
|
const key = `${op.collection}:${op.userId}:${op.key}`;
|
|
if (beforePromoDelete) beforePromoDelete(key);
|
|
const previous = promoStorage.get(key);
|
|
if (!previous || op.version !== previous.version) throw new Error("version conflict");
|
|
promoStorage.delete(key);
|
|
}
|
|
}
|
|
};
|
|
assert.throws(() => context.rpcCreateUnlockCode({ userId: "pilot-a" }, logger, promoNk, JSON.stringify({ reward: "all_maps", max_uses: 1 })), /server-to-server/);
|
|
assert.throws(() => context.rpcCreateUnlockCode({}, logger, promoNk, JSON.stringify({ reward: "all_maps", note: "x".repeat(161) })), /160 characters/);
|
|
const createdPromo = JSON.parse(context.rpcCreateUnlockCode({}, logger, promoNk, JSON.stringify({ reward: "all_maps", max_uses: 2, note: "Für den Multiplayer-Test" })));
|
|
assert.strictEqual(createdPromo.accepted, true);
|
|
assert.strictEqual(createdPromo.code.length, 24);
|
|
assert.strictEqual(createdPromo.note, "Für den Multiplayer-Test");
|
|
assert.strictEqual(JSON.parse(context.rpcListUnlockCodes({}, logger, promoNk, "{}")).codes[0].status, "open");
|
|
assert.throws(() => context.rpcListUnlockCodes({ userId: "pilot-a" }, logger, promoNk, "{}"), /server-to-server/);
|
|
const redeemedPromo = JSON.parse(context.rpcRedeemUnlockCode({ userId: "pilot-promo-a" }, logger, promoNk, JSON.stringify({ code: createdPromo.code })));
|
|
assert.strictEqual(redeemedPromo.accepted, true);
|
|
assert.strictEqual(redeemedPromo.all_maps, true);
|
|
assert.strictEqual(redeemedPromo.all_towers, false);
|
|
const partiallyRedeemed = JSON.parse(context.rpcListUnlockCodes({}, logger, promoNk, "{}")).codes[0];
|
|
assert.strictEqual(partiallyRedeemed.status, "partial");
|
|
assert.strictEqual(partiallyRedeemed.uses, 1);
|
|
assert.strictEqual(partiallyRedeemed.code, createdPromo.code);
|
|
assert.strictEqual(partiallyRedeemed.note, "Für den Multiplayer-Test");
|
|
assert.strictEqual(JSON.parse(context.rpcGetPromoUnlocks({ userId: "pilot-promo-a" }, logger, promoNk, "{}")).all_maps, true);
|
|
assert.strictEqual(JSON.parse(context.rpcRedeemUnlockCode({ userId: "pilot-promo-a" }, logger, promoNk, JSON.stringify({ code: createdPromo.code }))).already_redeemed, true);
|
|
assert.strictEqual(JSON.parse(context.rpcRedeemUnlockCode({ userId: "pilot-promo-b" }, logger, promoNk, JSON.stringify({ code: createdPromo.code }))).accepted, true);
|
|
assert.strictEqual(JSON.parse(context.rpcListUnlockCodes({}, logger, promoNk, "{}")).codes[0].status, "redeemed");
|
|
assert.strictEqual(JSON.parse(context.rpcRedeemUnlockCode({ userId: "pilot-promo-c" }, logger, promoNk, JSON.stringify({ code: createdPromo.code }))).error, "exhausted");
|
|
const usedCodeKey = promoNk.sha256Hash(createdPromo.code);
|
|
assert.throws(() => context.rpcDeleteUnlockCode({ userId: "pilot-a" }, logger, promoNk, JSON.stringify({ code_key: usedCodeKey })), /server-to-server/);
|
|
assert.strictEqual(JSON.parse(context.rpcDeleteUnlockCode({}, logger, promoNk, JSON.stringify({ code_key: usedCodeKey }))).error, "already_used");
|
|
assert.strictEqual(JSON.parse(context.rpcDeleteUnlockCode({}, logger, promoNk, JSON.stringify({ code_key: "bad-key" }))).error, "invalid");
|
|
const unusedPromo = JSON.parse(context.rpcCreateUnlockCode({}, logger, promoNk, JSON.stringify({ reward: "all_towers" })));
|
|
const unusedCodeKey = promoNk.sha256Hash(unusedPromo.code);
|
|
assert.strictEqual(JSON.parse(context.rpcListUnlockCodes({}, logger, promoNk, "{}")).codes.find(entry => entry.code === unusedPromo.code).code_key, unusedCodeKey);
|
|
assert.strictEqual(JSON.parse(context.rpcDeleteUnlockCode({}, logger, promoNk, JSON.stringify({ code_key: unusedCodeKey }))).accepted, true);
|
|
assert.strictEqual(JSON.parse(context.rpcDeleteUnlockCode({}, logger, promoNk, JSON.stringify({ code_key: unusedCodeKey }))).error, "not_found");
|
|
assert.strictEqual(JSON.parse(context.rpcRedeemUnlockCode({ userId: "pilot-promo-d" }, logger, promoNk, JSON.stringify({ code: unusedPromo.code }))).error, "invalid");
|
|
const racingPromo = JSON.parse(context.rpcCreateUnlockCode({}, logger, promoNk, JSON.stringify({ reward: "all_maps" })));
|
|
const racingCodeKey = promoNk.sha256Hash(racingPromo.code);
|
|
beforePromoDelete = key => {
|
|
beforePromoDelete = null;
|
|
const previous = promoStorage.get(key);
|
|
promoVersion += 1;
|
|
promoStorage.set(key, { ...previous, version: String(promoVersion), value: { ...previous.value, uses: 1, redeemed_users: ["pilot-race"] } });
|
|
};
|
|
assert.strictEqual(JSON.parse(context.rpcDeleteUnlockCode({}, logger, promoNk, JSON.stringify({ code_key: racingCodeKey }))).error, "already_used");
|
|
assert.ok(promoStorage.has(`unlock_codes:${context.SYSTEM_USER_ID}:${racingCodeKey}`));
|
|
|
|
let leaderboardWrite = null;
|
|
const leaderboardNk = {
|
|
accountGetId() { return { user: { displayName: "Kommandant Ägir" } }; },
|
|
leaderboardRecordWrite(...args) {
|
|
leaderboardWrite = args;
|
|
return { rank: 1, score: args[3] };
|
|
}
|
|
};
|
|
const submittedRun = JSON.parse(context.rpcSubmitRun(
|
|
{ userId: "pilot-a", username: "technical_guest_id" },
|
|
{ warn() {} },
|
|
leaderboardNk,
|
|
JSON.stringify({ map: 0, difficulty: 1, mode: "mission", wave: 4, score: 4700, client_version: "test" })
|
|
));
|
|
assert.strictEqual(submittedRun.accepted, true);
|
|
assert.strictEqual(leaderboardWrite[2], "Kommandant Ägir");
|
|
assert.strictEqual(leaderboardWrite[5].displayName, "Kommandant Ägir");
|
|
|
|
let state = context.matchInit({}, logger, nk, { difficulty: 1 }).state;
|
|
state = context.matchJoin({}, logger, nk, dispatcher, 1, state, [presenceA]).state;
|
|
state = context.matchJoin({}, logger, nk, dispatcher, 2, state, [presenceB]).state;
|
|
assert.strictEqual(state.started, false);
|
|
assert.strictEqual(state.slots["pilot-a"], 0);
|
|
assert.strictEqual(state.slots["pilot-b"], 1);
|
|
assert.ok(sent.some(entry => entry.opCode === 1 && entry.data.players[0].name === "Pilot A"));
|
|
assert.strictEqual(context.normalizeRoomCode(" ab-c12! "), "ABC12");
|
|
|
|
const privateMatch = context.matchInit({}, logger, nk, { difficulty: 2, mode: "private", roomCode: "AB23CD", hostUserId: "pilot-a" });
|
|
assert.strictEqual(privateMatch.label, "AB23CD");
|
|
assert.strictEqual(privateMatch.state.difficulty, 2);
|
|
|
|
const roomStorage = new Map();
|
|
const nkRooms = {
|
|
storageRead(ops) { return ops.map(op => roomStorage.get(op.key)).filter(Boolean); },
|
|
storageWrite(ops) { for (const op of ops) roomStorage.set(op.key, { value: op.value }); },
|
|
storageDelete(ops) { for (const op of ops) roomStorage.delete(op.key); },
|
|
matchCreate() { return "private-match-id"; }
|
|
};
|
|
const createdRoom = JSON.parse(context.rpcCreatePrivateRoom({ userId: "pilot-a" }, logger, nkRooms, JSON.stringify({ difficulty: 2 })));
|
|
assert.strictEqual(createdRoom.room_code.length, 6);
|
|
const joinedRoom = JSON.parse(context.rpcJoinPrivateRoom({ userId: "pilot-b" }, logger, nkRooms, JSON.stringify({ room_code: createdRoom.room_code.toLowerCase() })));
|
|
assert.strictEqual(joinedRoom.match_id, "private-match-id");
|
|
|
|
function message(sender, opCode, payload) {
|
|
return { sender, opCode, data: Buffer.from(JSON.stringify(payload)) };
|
|
}
|
|
|
|
state = context.matchLoop({}, logger, nk, dispatcher, 3, state, [message(presenceA, 14, { ready: true, name: "Kommandant A" })]).state;
|
|
assert.strictEqual(state.started, false);
|
|
state = context.matchLoop({}, logger, nk, dispatcher, 4, state, [message(presenceB, 14, { ready: true, name: "Kommandant B" })]).state;
|
|
assert.strictEqual(state.started, true);
|
|
assert.strictEqual(state.players["pilot-a"].name, "Kommandant A");
|
|
assert.ok(sent.some(entry => entry.opCode === "label" && entry.data.open === false));
|
|
|
|
state = context.matchLoop({}, logger, nk, dispatcher, 5, state, [
|
|
message(presenceA, 10, { tower_type: 0, x: 200, y: 200 }),
|
|
message(presenceB, 10, { tower_type: 1, x: 2760, y: 200 })
|
|
]).state;
|
|
assert.strictEqual(state.towers.length, 2);
|
|
assert.strictEqual(state.credits[0], 310);
|
|
assert.strictEqual(state.credits[1], 250);
|
|
|
|
state = context.matchLoop({}, logger, nk, dispatcher, 6, state, [message(presenceA, 13, {})]).state;
|
|
assert.strictEqual(state.wave, 1);
|
|
assert.strictEqual(state.waveActive, true);
|
|
assert.strictEqual(context.snapshot(state, 6).wave_types_remaining.length, state.spawnRemaining);
|
|
const groundSchedule = context.buildWaveSchedule(state, 3, context.wavePopulation(state, 3));
|
|
assert.ok(groundSchedule.some(entry => context.ENEMIES[entry.type].category === "ground"), "Ground units should enter the wave schedule from wave 3.");
|
|
for (let tick = 7; tick < 30; tick++) state = context.matchLoop({}, logger, nk, dispatcher, tick, state, []).state;
|
|
assert.ok(state.enemies.length > 0 || state.spawnRemaining > 0);
|
|
assert.ok(sent.some(entry => entry.opCode === 1 && entry.data.started === true));
|
|
const compactSnapshot = context.snapshot(state, 29);
|
|
const untrimmedSnapshot = { ...compactSnapshot, towers: state.towers, enemies: state.enemies };
|
|
assert.ok(JSON.stringify(compactSnapshot).length < JSON.stringify(untrimmedSnapshot).length, "Frequent snapshots should omit simulation-only properties.");
|
|
assert.ok(compactSnapshot.towers.every(tower => !Object.hasOwn(tower, "cooldown")));
|
|
assert.ok(compactSnapshot.enemies.every(enemy => !Object.hasOwn(enemy, "shield_resistance") && !Object.hasOwn(enemy, "reward")));
|
|
assert.ok(state.towers.every(tower => Object.hasOwn(tower, "cooldown")), "Network serialization must not alter simulation state.");
|
|
|
|
const towerA = state.towers.find(tower => tower.owner === 0);
|
|
state = context.matchLoop({}, logger, nk, dispatcher, 30, state, [message(presenceA, 11, { tower_id: towerA.id })]).state;
|
|
assert.strictEqual(state.towers.find(tower => tower.id === towerA.id).level, 2);
|
|
state = context.matchLoop({}, logger, nk, dispatcher, 31, state, [message(presenceA, 12, { tower_id: towerA.id })]).state;
|
|
assert.ok(!state.towers.some(tower => tower.id === towerA.id));
|
|
|
|
state = context.matchLeave({}, logger, nk, dispatcher, 32, state, [presenceB]).state;
|
|
assert.strictEqual(state.started, true);
|
|
assert.strictEqual(state.pausedForReconnect, true);
|
|
assert.strictEqual(context.snapshot(state, 32).players[1].connected, false);
|
|
assert.strictEqual(context.matchJoinAttempt({}, logger, nk, dispatcher, 33, state, presenceB, {}).accept, true);
|
|
state = context.matchJoin({}, logger, nk, dispatcher, 33, state, [presenceB]).state;
|
|
assert.strictEqual(state.pausedForReconnect, false);
|
|
assert.strictEqual(state.slots["pilot-b"], 1);
|
|
|
|
console.log("VOID_BASTION_SERVER_SIM_OK");
|