207 lines
8 KiB
JavaScript
207 lines
8 KiB
JavaScript
// Golden test for openpets.virtual-pet.
|
|
import assert from "node:assert/strict";
|
|
import { readFile } from "node:fs/promises";
|
|
import {
|
|
cleanState,
|
|
getMood,
|
|
addXp,
|
|
applyDecay,
|
|
register,
|
|
SCHEDULE_ID,
|
|
} from "./index.js";
|
|
|
|
let createTestHarness;
|
|
try {
|
|
({ createTestHarness } = await import("@open-pets/plugin-sdk/testing"));
|
|
} catch {
|
|
({ createTestHarness } = await import(new URL("../../../packages/sdk/dist/testing.js", import.meta.url)));
|
|
}
|
|
|
|
let activeHarness = null;
|
|
const originalDateNow = Date.now;
|
|
Object.defineProperty(Date, "now", {
|
|
value: () => {
|
|
if (activeHarness && activeHarness.clock) {
|
|
return activeHarness.clock.now();
|
|
}
|
|
return 1000000;
|
|
},
|
|
configurable: true,
|
|
});
|
|
|
|
// 1) Test Pure Helpers
|
|
{
|
|
// cleanState
|
|
const defaultState = cleanState(null);
|
|
assert.equal(defaultState.hunger, 80);
|
|
assert.equal(defaultState.energy, 80);
|
|
assert.equal(defaultState.happiness, 80);
|
|
assert.equal(defaultState.affection, 50);
|
|
assert.equal(defaultState.level, 1);
|
|
assert.equal(defaultState.xp, 0);
|
|
|
|
const customState = cleanState({ hunger: 20, level: 3, careCounts: { fed: 5 } });
|
|
assert.equal(customState.hunger, 20);
|
|
assert.equal(customState.level, 3);
|
|
assert.equal(customState.careCounts.fed, 5);
|
|
|
|
// getMood
|
|
assert.equal(getMood({ hunger: 80, energy: 80, happiness: 80, affection: 80, sleptUntil: 0 }, 1000), "happy");
|
|
assert.equal(getMood({ hunger: 80, energy: 80, happiness: 80, affection: 80, sleptUntil: 5000 }, 1000), "sleeping");
|
|
assert.equal(getMood({ hunger: 20, energy: 80, happiness: 80, affection: 50, sleptUntil: 0 }, 1000), "hungry");
|
|
assert.equal(getMood({ hunger: 80, energy: 10, happiness: 80, affection: 50, sleptUntil: 0 }, 1000), "tired");
|
|
assert.equal(getMood({ hunger: 80, energy: 80, happiness: 15, affection: 50, sleptUntil: 0 }, 1000), "bored");
|
|
|
|
// addXp
|
|
const levelUp = addXp({ xp: 45, level: 1 }, 10);
|
|
assert.equal(levelUp.xp, 5);
|
|
assert.equal(levelUp.level, 2);
|
|
assert.equal(levelUp.leveledUp, true);
|
|
|
|
const normalXp = addXp({ xp: 10, level: 1 }, 10);
|
|
assert.equal(normalXp.xp, 20);
|
|
assert.equal(normalXp.level, 1);
|
|
assert.equal(normalXp.leveledUp, false);
|
|
|
|
// applyDecay
|
|
// 2 hours awake decay: hunger -4, energy -6, happiness -4, affection -2
|
|
const stateDecayed = applyDecay({ hunger: 80, energy: 80, happiness: 80, affection: 50, sleptUntil: 0, lastSeenAt: 1000 }, 2 * 3600_000, 1000 + 2 * 3600_000);
|
|
assert.equal(stateDecayed.hunger, 76);
|
|
assert.equal(stateDecayed.energy, 74);
|
|
assert.equal(stateDecayed.happiness, 76);
|
|
assert.equal(stateDecayed.affection, 48);
|
|
|
|
// 1 hour sleep decay: hunger -2, energy +15, happiness -0.5, affection same
|
|
const stateSlept = applyDecay({ hunger: 80, energy: 50, happiness: 80, affection: 50, sleptUntil: 1000 + 3600_000, lastSeenAt: 1000 }, 3600_000, 1000 + 3600_000);
|
|
assert.equal(stateSlept.hunger, 78);
|
|
assert.equal(stateSlept.energy, 65);
|
|
assert.equal(stateSlept.happiness, 79.5);
|
|
assert.equal(stateSlept.affection, 50);
|
|
}
|
|
|
|
const PERMISSIONS = ["pet:speak", "pet:interact", "pet:pin", "pet:reaction", "schedule", "storage", "commands", "audio", "events"];
|
|
const LOCALES = { en: JSON.parse(await readFile(new URL("./locales/en.json", import.meta.url), "utf8")) };
|
|
|
|
// 2) Start / Reconcile logic
|
|
{
|
|
const h = createTestHarness(register, { permissions: PERMISSIONS, locales: LOCALES, nowMs: 100_000_000_000 });
|
|
activeHarness = h;
|
|
await h.start();
|
|
// Expect state to be initialized is storage
|
|
h.expectStored("state", (s) => s.lastSeenAt === 100_000_000_000 && s.hunger === 80);
|
|
// Expect schedules to contain tick schedule
|
|
assert.ok(h.calls.schedules.has(SCHEDULE_ID));
|
|
// Expect status labels in the dynamic context menu (no on-pet HUD overlay)
|
|
const menu = h.calls.menuItems;
|
|
assert.ok(menu.length > 0, "Should populate the context menu with status labels");
|
|
assert.ok(menu.some((item) => item.id === "stats-row-1" && item.title.includes("80%")), "Menu should show food status");
|
|
assert.ok(menu.some((item) => item.id === "stats-row-2" && item.title.includes("80%")), "Menu should show energy status");
|
|
assert.ok(menu.some((item) => item.id === "stats-row-2" && item.title.includes("80%")), "Menu should show play status");
|
|
assert.ok(menu.some((item) => item.id === "stats-row-3" && item.title.includes("50%")), "Menu should show bond status");
|
|
|
|
h.expectNoErrors();
|
|
}
|
|
|
|
// 3) Reconcile with wall-clock decay catch-up
|
|
{
|
|
const h = createTestHarness(register, { permissions: PERMISSIONS, locales: LOCALES, nowMs: 101_000_000_000 });
|
|
activeHarness = h;
|
|
// Set old state manually in storage. Last seen 2 hours ago.
|
|
const oldState = cleanState({ hunger: 100, energy: 100, happiness: 100, affection: 100, lastSeenAt: 101_000_000_000 - 2 * 3600_000 });
|
|
await h.ctx.storage.set("state", oldState);
|
|
await h.start();
|
|
|
|
// 2 hours elapsed = hunger -4 (96), energy -6 (94), happiness -4 (96), affection -2 (98)
|
|
h.expectStored("state", (s) => s.hunger === 96 && s.energy === 94 && s.happiness === 96 && s.affection === 98);
|
|
h.expectNoErrors();
|
|
}
|
|
|
|
// 4) Commands / actions mutate stats
|
|
{
|
|
const h = createTestHarness(register, { permissions: PERMISSIONS, locales: LOCALES, nowMs: 102_000_000_000 });
|
|
activeHarness = h;
|
|
await h.start();
|
|
|
|
// Feed command: hunger +25, xp +5
|
|
await h.runCommand("feed");
|
|
h.expectStored("state", (s) => s.hunger === 100 && s.xp === 5 && s.careCounts.fed === 1);
|
|
h.expectSpoke(/food|munch|tasty|delicious/);
|
|
h.expectReacted("celebrating");
|
|
|
|
// Play command: happiness +25, energy -15, xp +5
|
|
await h.runCommand("play");
|
|
h.expectStored("state", (s) => s.happiness === 100 && s.energy === 65 && s.xp === 10 && s.careCounts.played === 1);
|
|
h.expectSpoke(/fun|Yay|games|Again/);
|
|
|
|
// Pet command: affection +15, happiness +10, xp +3
|
|
await h.runCommand("pet");
|
|
h.expectStored("state", (s) => s.affection === 65 && s.happiness === 100 && s.xp === 13 && s.careCounts.petted === 1);
|
|
h.expectSpoke(/Purr|nuzzles|Warm|soft/);
|
|
|
|
// Nap command: energy +40, sleptUntil is set
|
|
await h.runCommand("nap");
|
|
h.expectStored("state", (s) => s.energy === 100 && s.sleptUntil === 102_000_000_000 + 15 * 60_000 && s.careCounts.napped === 1);
|
|
h.expectSpoke(/Zzz|curls|Sleepy|Resting/);
|
|
h.expectReacted("waiting");
|
|
|
|
// Show status command
|
|
await h.runCommand("status");
|
|
h.expectSpoke(/resting|hungry|sleepy|play|great|content/);
|
|
|
|
h.expectNoErrors();
|
|
}
|
|
|
|
// 5) Play wakes up pet if sleeping
|
|
{
|
|
const h = createTestHarness(register, { permissions: PERMISSIONS, locales: LOCALES, nowMs: 103_000_000_000 });
|
|
activeHarness = h;
|
|
await h.start();
|
|
await h.runCommand("nap"); // sleeping until 103B + 15M
|
|
h.expectStored("state", (s) => s.sleptUntil > 0);
|
|
|
|
// Running play should wake up pet
|
|
await h.runCommand("play");
|
|
h.expectStored("state", (s) => s.sleptUntil === 0);
|
|
h.expectNoErrors();
|
|
}
|
|
|
|
// 6) Click event triggers petting
|
|
{
|
|
const h = createTestHarness(register, { permissions: PERMISSIONS, locales: LOCALES, nowMs: 104_000_000_000 });
|
|
activeHarness = h;
|
|
await h.start();
|
|
|
|
// Emit click event
|
|
await h.emit("pet:clicked", {});
|
|
h.expectStored("state", (s) => s.affection === 65 && s.careCounts.petted === 1);
|
|
h.expectSpoke(/Purr|nuzzles|Warm|soft/);
|
|
h.expectNoErrors();
|
|
}
|
|
|
|
// 7) Nudge neglected
|
|
{
|
|
const h = createTestHarness(register, { permissions: PERMISSIONS, locales: LOCALES, nowMs: 105_000_000_000 });
|
|
activeHarness = h;
|
|
// Set neglected state
|
|
const neglectedState = cleanState({ hunger: 10, lastSeenAt: 105_000_000_000 - 15 * 60_000 });
|
|
await h.ctx.storage.set("state", neglectedState);
|
|
await h.start();
|
|
|
|
// check if nudge triggered
|
|
h.expectSpoke(/hungry/);
|
|
h.expectStored("state", (s) => s.lastNudgeAt === 105_000_000_000);
|
|
|
|
// If we advance clock by 5 mins, nudge should NOT fire again (cooldown)
|
|
const previousSpeakCount = h.calls.speak.length;
|
|
await h.clock.advance("5m");
|
|
assert.equal(h.calls.speak.length, previousSpeakCount, "nudge should not spam");
|
|
|
|
h.expectNoErrors();
|
|
}
|
|
|
|
Object.defineProperty(Date, "now", {
|
|
value: originalDateNow,
|
|
configurable: true,
|
|
});
|
|
|
|
console.log("openpets.virtual-pet: all checks passed.");
|