Move virtual-pet HUD to the context menu #2
5 changed files with 99 additions and 129 deletions
|
|
@ -154,25 +154,33 @@ async function buildPetContextMenuTemplate(action: { readonly label: string; rea
|
|||
if (!action.defaultPet) return [{ label: action.label, click: action.click }];
|
||||
const commands = await getDefaultPetPluginCommands();
|
||||
const topLevel: Electron.MenuItemConstructorOptions[] = [];
|
||||
const plugins = new Map<string, { name: string; commands: Electron.MenuItemConstructorOptions[] }>();
|
||||
const plugins = new Map<string, { name: string; commands: Electron.MenuItemConstructorOptions[]; menuItems: Electron.MenuItemConstructorOptions[] }>();
|
||||
const sorted = [...commands].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
||||
for (const command of sorted) {
|
||||
const item: Electron.MenuItemConstructorOptions = { label: command.commandTitle, click: () => { if (command.form) openPluginCommandForm(command).catch((error) => logError("pet.window", "plugin command form failed", error)); else executeDefaultPetPluginCommand(command.pluginId, command.commandId).catch((error) => logError("pet.window", "plugin command failed", error)); } };
|
||||
if (command.placement === "top" || command.featured) { topLevel.push(item); continue; }
|
||||
const group = plugins.get(command.pluginId) ?? { name: command.pluginName, commands: [] };
|
||||
const group = plugins.get(command.pluginId) ?? { name: command.pluginName, commands: [], menuItems: [] };
|
||||
group.commands.push(item);
|
||||
plugins.set(command.pluginId, group);
|
||||
}
|
||||
// Fully dynamic per-plugin menu sections (ui.menu.setItems).
|
||||
// Fully dynamic per-plugin menu sections (ui.menu.setItems) — show status labels first.
|
||||
const menuItems = await getDefaultPetPluginMenuItems();
|
||||
for (const item of menuItems) {
|
||||
const group = plugins.get(item.pluginId) ?? { name: item.pluginName, commands: [] };
|
||||
group.commands.push({ label: item.title, enabled: item.enabled !== false, type: item.checked === true ? "checkbox" : "normal", checked: item.checked === true ? true : undefined, click: () => { executeDefaultPetPluginMenuSelect(item.pluginId, item.itemId).catch((error) => logError("pet.window", "plugin menu select failed", error)); } });
|
||||
const group = plugins.get(item.pluginId) ?? { name: item.pluginName, commands: [], menuItems: [] };
|
||||
group.menuItems.push({ label: item.title, enabled: item.enabled !== false, type: item.checked === true ? "checkbox" : "normal", checked: item.checked === true ? true : undefined, click: () => { executeDefaultPetPluginMenuSelect(item.pluginId, item.itemId).catch((error) => logError("pet.window", "plugin menu select failed", error)); } });
|
||||
plugins.set(item.pluginId, group);
|
||||
}
|
||||
const template: Electron.MenuItemConstructorOptions[] = [];
|
||||
if (topLevel.length > 0) template.push(...topLevel.slice(0, 8), { type: "separator" });
|
||||
if (plugins.size > 0) template.push(...[...plugins.values()].map((plugin) => ({ label: plugin.name, submenu: plugin.commands })), { type: "separator" });
|
||||
if (plugins.size > 0) {
|
||||
template.push(...[...plugins.values()].map((plugin) => {
|
||||
const submenu: Electron.MenuItemConstructorOptions[] = [];
|
||||
if (plugin.menuItems.length > 0) submenu.push(...plugin.menuItems);
|
||||
if (plugin.menuItems.length > 0 && plugin.commands.length > 0) submenu.push({ type: "separator" });
|
||||
if (plugin.commands.length > 0) submenu.push(...plugin.commands);
|
||||
return { label: plugin.name, submenu };
|
||||
}), { type: "separator" });
|
||||
}
|
||||
template.push({ label: t("pet.menu.openControlCenter"), click: () => { import("./windows.js").then(({ openControlCenterWindow }) => openControlCenterWindow()).catch((error) => logError("pet.window", "open control center failed", error)); } }, { label: action.label, click: action.click });
|
||||
return template;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,8 +72,8 @@ export type PluginServiceOptions = {
|
|||
readonly capabilities?: PluginHostCapabilities;
|
||||
};
|
||||
|
||||
export const bundledOfficialPluginIds = ["openpets.reminders"] as const;
|
||||
const bundledEnabledByDefault = new Set<string>(["openpets.reminders"]);
|
||||
export const bundledOfficialPluginIds = ["openpets.reminders", "openpets.virtual-pet"] as const;
|
||||
const bundledEnabledByDefault = new Set<string>(["openpets.reminders", "openpets.virtual-pet"]);
|
||||
const staleBundledPluginIds = ["openpets.daily-reminders", "openpets.pomodoro", "openpets.ambient-companion", "openpets.break-buddy", "openpets.focus-buddy", "openpets.github-notifications", "openpets.pet-pal", "openpets.quick-reminders", "openpets.wander-buddy"] as const;
|
||||
|
||||
export class PluginService {
|
||||
|
|
|
|||
|
|
@ -2,17 +2,6 @@
|
|||
|
||||
export const SCHEDULE_ID = "virtual-pet-tick";
|
||||
|
||||
const pinnedBubbles = new WeakMap();
|
||||
|
||||
function getPinnedBubble(ctx) {
|
||||
return pinnedBubbles.get(ctx) ?? null;
|
||||
}
|
||||
|
||||
function setPinnedBubble(ctx, handle) {
|
||||
if (handle) pinnedBubbles.set(ctx, handle);
|
||||
else pinnedBubbles.delete(ctx);
|
||||
}
|
||||
|
||||
export function cleanState(state = {}) {
|
||||
const current = state && typeof state === "object" ? state : {};
|
||||
const careCounts = current.careCounts && typeof current.careCounts === "object" ? current.careCounts : {};
|
||||
|
|
@ -76,24 +65,24 @@ export function addXp(state, amount) {
|
|||
|
||||
export function applyDecay(state, elapsedMs, now) {
|
||||
const lastSeen = state.lastSeenAt || now;
|
||||
|
||||
|
||||
let sleepMs = 0;
|
||||
if (state.sleptUntil > lastSeen) {
|
||||
const sleepEnd = Math.min(state.sleptUntil, now);
|
||||
sleepMs = sleepEnd - lastSeen;
|
||||
}
|
||||
const wakeMs = Math.max(0, elapsedMs - sleepMs);
|
||||
|
||||
|
||||
const sleepHours = sleepMs / 3600000;
|
||||
const wakeHours = wakeMs / 3600000;
|
||||
|
||||
|
||||
// Stats decay per hour: hunger (-2), energy (-3), happiness (-2), affection (-1)
|
||||
// During sleep: hunger (-2), energy reacts (+15), happiness (-0.5), affection stays same
|
||||
const newHunger = state.hunger - wakeHours * 2 - sleepHours * 2;
|
||||
const newEnergy = state.energy - wakeHours * 3 + sleepHours * 15;
|
||||
const newHappiness = state.happiness - wakeHours * 2 - sleepHours * 0.5;
|
||||
const newAffection = state.affection - wakeHours * 1;
|
||||
|
||||
|
||||
return {
|
||||
...state,
|
||||
hunger: Math.max(0, Math.min(100, newHunger)),
|
||||
|
|
@ -112,40 +101,21 @@ async function playActionSound(ctx) {
|
|||
} catch {}
|
||||
}
|
||||
|
||||
export async function updatePinned(ctx, state, now = Date.now()) {
|
||||
const spec = {
|
||||
tone: "info",
|
||||
sticky: true,
|
||||
pin: true,
|
||||
priority: "normal",
|
||||
hud: {
|
||||
items: [
|
||||
{ icon: "food", value: state.hunger, tone: "amber", label: ctx.t("hud.food") },
|
||||
{ icon: "zap", value: state.energy, tone: "blue", label: ctx.t("hud.energy") },
|
||||
{ icon: "play", value: state.happiness, tone: "green", label: ctx.t("hud.play") },
|
||||
{ icon: "heart", value: state.affection, tone: "pink", label: ctx.t("hud.bond") },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const pinnedBubble = getPinnedBubble(ctx);
|
||||
if (pinnedBubble) {
|
||||
try {
|
||||
await pinnedBubble.update(spec);
|
||||
return;
|
||||
} catch {
|
||||
setPinnedBubble(ctx, null);
|
||||
}
|
||||
}
|
||||
|
||||
function buildStatusMenuItems(ctx, state, now = Date.now()) {
|
||||
const mood = getMood(state, now);
|
||||
return [
|
||||
{ id: "mood", title: `${ctx.t("menu.mood")}: ${ctx.t(`mood.${mood}`)}`, enabled: false },
|
||||
{ id: "food", title: `${ctx.t("menu.food")}: ${Math.round(state.hunger)}%`, enabled: false },
|
||||
{ id: "energy", title: `${ctx.t("menu.energy")}: ${Math.round(state.energy)}%`, enabled: false },
|
||||
{ id: "play", title: `${ctx.t("menu.play")}: ${Math.round(state.happiness)}%`, enabled: false },
|
||||
{ id: "bond", title: `${ctx.t("menu.bond")}: ${Math.round(state.affection)}%`, enabled: false },
|
||||
{ id: "level", title: `${ctx.t("menu.level")}: ${state.level}`, enabled: false },
|
||||
];
|
||||
}
|
||||
|
||||
async function updateMenu(ctx, state, now = Date.now()) {
|
||||
try {
|
||||
const nextBubble = await ctx.ui.bubble(spec);
|
||||
nextBubble.onDismiss(() => {
|
||||
if (getPinnedBubble(ctx)?.id === nextBubble.id) {
|
||||
setPinnedBubble(ctx, null);
|
||||
}
|
||||
});
|
||||
setPinnedBubble(ctx, nextBubble);
|
||||
await ctx.ui.menu.setItems(buildStatusMenuItems(ctx, state, now));
|
||||
} catch {}
|
||||
}
|
||||
|
||||
|
|
@ -154,19 +124,19 @@ export async function maybeNudge(ctx, state, now = Date.now()) {
|
|||
const isTired = state.energy < 30;
|
||||
const isBored = state.happiness < 30;
|
||||
const isNeglected = state.affection < 30;
|
||||
|
||||
|
||||
if (isHungry || isTired || isBored || isNeglected) {
|
||||
const nudgeCooldown = 6 * 3600_000;
|
||||
if (state.lastNudgeAt === 0 || now - state.lastNudgeAt >= nudgeCooldown) {
|
||||
state.lastNudgeAt = now;
|
||||
await ctx.storage.set("state", state);
|
||||
|
||||
|
||||
let speechKey = "nudge.neglected";
|
||||
if (isHungry) speechKey = "nudge.hungry";
|
||||
else if (isTired) speechKey = "nudge.tired";
|
||||
else if (isBored) speechKey = "nudge.bored";
|
||||
else if (isNeglected) speechKey = "nudge.neglected";
|
||||
|
||||
|
||||
try {
|
||||
await ctx.pet.speak(ctx.t(speechKey));
|
||||
} catch {}
|
||||
|
|
@ -186,11 +156,11 @@ async function scheduleNextTick(ctx, now = Date.now()) {
|
|||
export async function feed(ctx, now = Date.now()) {
|
||||
const state = cleanState(await ctx.storage.get("state"));
|
||||
const cleanActive = wakeUpIfSleeping(state, now);
|
||||
|
||||
|
||||
const hunger = Math.min(100, cleanActive.hunger + 25);
|
||||
const xpInfo = addXp(cleanActive, 5);
|
||||
const careCounts = { ...cleanActive.careCounts, fed: cleanActive.careCounts.fed + 1 };
|
||||
|
||||
|
||||
const newState = {
|
||||
...cleanActive,
|
||||
hunger,
|
||||
|
|
@ -200,10 +170,10 @@ export async function feed(ctx, now = Date.now()) {
|
|||
lastActionAt: now,
|
||||
lastSeenAt: now,
|
||||
};
|
||||
|
||||
|
||||
await ctx.storage.set("state", newState);
|
||||
await playActionSound(ctx);
|
||||
|
||||
|
||||
try {
|
||||
await ctx.pet.react("celebrating", { showMessage: false });
|
||||
if (xpInfo.leveledUp) {
|
||||
|
|
@ -213,20 +183,20 @@ export async function feed(ctx, now = Date.now()) {
|
|||
await ctx.pet.speak(ctx.t(`speech.feed.${idx}`));
|
||||
}
|
||||
} catch {}
|
||||
|
||||
await updatePinned(ctx, newState, now);
|
||||
|
||||
await updateMenu(ctx, newState, now);
|
||||
return newState;
|
||||
}
|
||||
|
||||
export async function play(ctx, now = Date.now()) {
|
||||
const state = cleanState(await ctx.storage.get("state"));
|
||||
const cleanActive = wakeUpIfSleeping(state, now);
|
||||
|
||||
|
||||
const happiness = Math.min(100, cleanActive.happiness + 25);
|
||||
const energy = Math.max(0, cleanActive.energy - 15);
|
||||
const xpInfo = addXp(cleanActive, 5);
|
||||
const careCounts = { ...cleanActive.careCounts, played: cleanActive.careCounts.played + 1 };
|
||||
|
||||
|
||||
const newState = {
|
||||
...cleanActive,
|
||||
happiness,
|
||||
|
|
@ -237,10 +207,10 @@ export async function play(ctx, now = Date.now()) {
|
|||
lastActionAt: now,
|
||||
lastSeenAt: now,
|
||||
};
|
||||
|
||||
|
||||
await ctx.storage.set("state", newState);
|
||||
await playActionSound(ctx);
|
||||
|
||||
|
||||
try {
|
||||
await ctx.pet.react("celebrating", { showMessage: false });
|
||||
if (xpInfo.leveledUp) {
|
||||
|
|
@ -250,20 +220,20 @@ export async function play(ctx, now = Date.now()) {
|
|||
await ctx.pet.speak(ctx.t(`speech.play.${idx}`));
|
||||
}
|
||||
} catch {}
|
||||
|
||||
await updatePinned(ctx, newState, now);
|
||||
|
||||
await updateMenu(ctx, newState, now);
|
||||
return newState;
|
||||
}
|
||||
|
||||
export async function pet(ctx, now = Date.now()) {
|
||||
const state = cleanState(await ctx.storage.get("state"));
|
||||
const cleanActive = wakeUpIfSleeping(state, now);
|
||||
|
||||
|
||||
const affection = Math.min(100, cleanActive.affection + 15);
|
||||
const happiness = Math.min(100, cleanActive.happiness + 10);
|
||||
const xpInfo = addXp(cleanActive, 3);
|
||||
const careCounts = { ...cleanActive.careCounts, petted: cleanActive.careCounts.petted + 1 };
|
||||
|
||||
|
||||
const newState = {
|
||||
...cleanActive,
|
||||
affection,
|
||||
|
|
@ -274,10 +244,10 @@ export async function pet(ctx, now = Date.now()) {
|
|||
lastActionAt: now,
|
||||
lastSeenAt: now,
|
||||
};
|
||||
|
||||
|
||||
await ctx.storage.set("state", newState);
|
||||
await playActionSound(ctx);
|
||||
|
||||
|
||||
try {
|
||||
await ctx.pet.react("waving", { showMessage: false });
|
||||
if (xpInfo.leveledUp) {
|
||||
|
|
@ -287,19 +257,19 @@ export async function pet(ctx, now = Date.now()) {
|
|||
await ctx.pet.speak(ctx.t(`speech.pet.${idx}`));
|
||||
}
|
||||
} catch {}
|
||||
|
||||
await updatePinned(ctx, newState, now);
|
||||
|
||||
await updateMenu(ctx, newState, now);
|
||||
return newState;
|
||||
}
|
||||
|
||||
export async function nap(ctx, now = Date.now()) {
|
||||
const state = cleanState(await ctx.storage.get("state"));
|
||||
|
||||
|
||||
const energy = Math.min(100, state.energy + 40);
|
||||
const sleptUntil = now + 15 * 60_000;
|
||||
const xpInfo = addXp(state, 5);
|
||||
const careCounts = { ...state.careCounts, napped: state.careCounts.napped + 1 };
|
||||
|
||||
|
||||
const newState = {
|
||||
...state,
|
||||
energy,
|
||||
|
|
@ -310,10 +280,10 @@ export async function nap(ctx, now = Date.now()) {
|
|||
lastActionAt: now,
|
||||
lastSeenAt: now,
|
||||
};
|
||||
|
||||
|
||||
await ctx.storage.set("state", newState);
|
||||
await playActionSound(ctx);
|
||||
|
||||
|
||||
try {
|
||||
await ctx.pet.react("waiting", { showMessage: false });
|
||||
if (xpInfo.leveledUp) {
|
||||
|
|
@ -323,15 +293,15 @@ export async function nap(ctx, now = Date.now()) {
|
|||
await ctx.pet.speak(ctx.t(`speech.nap.${idx}`));
|
||||
}
|
||||
} catch {}
|
||||
|
||||
await updatePinned(ctx, newState, now);
|
||||
|
||||
await updateMenu(ctx, newState, now);
|
||||
return newState;
|
||||
}
|
||||
|
||||
export async function showStatus(ctx, now = Date.now()) {
|
||||
const state = cleanState(await ctx.storage.get("state"));
|
||||
await updatePinned(ctx, state, now);
|
||||
|
||||
await updateMenu(ctx, state, now);
|
||||
|
||||
const mood = getMood(state, now);
|
||||
try {
|
||||
await ctx.pet.speak(ctx.t(`speech.status.${mood}`));
|
||||
|
|
@ -341,7 +311,7 @@ export async function showStatus(ctx, now = Date.now()) {
|
|||
export async function reconcile(ctx, now = Date.now()) {
|
||||
const rawState = await ctx.storage.get("state");
|
||||
const state = cleanState(rawState);
|
||||
|
||||
|
||||
let updatedState;
|
||||
if (state.lastSeenAt > 0) {
|
||||
const elapsedMs = Math.max(0, now - state.lastSeenAt);
|
||||
|
|
@ -349,12 +319,12 @@ export async function reconcile(ctx, now = Date.now()) {
|
|||
} else {
|
||||
updatedState = state;
|
||||
}
|
||||
|
||||
|
||||
updatedState.lastSeenAt = now;
|
||||
const savedState = cleanState(updatedState);
|
||||
await ctx.storage.set("state", savedState);
|
||||
|
||||
await updatePinned(ctx, savedState, now);
|
||||
|
||||
await updateMenu(ctx, savedState, now);
|
||||
await maybeNudge(ctx, savedState, now);
|
||||
await scheduleNextTick(ctx, now);
|
||||
return savedState;
|
||||
|
|
@ -364,18 +334,18 @@ export function register(OpenPetsPlugin) {
|
|||
OpenPetsPlugin.register({
|
||||
async start(ctx) {
|
||||
await reconcile(ctx);
|
||||
|
||||
|
||||
try {
|
||||
ctx.events.on("pet:clicked", () => pet(ctx));
|
||||
} catch {}
|
||||
|
||||
|
||||
const icon = ctx.assets.icon("virtual-pet");
|
||||
|
||||
|
||||
await ctx.commands.register({ id: "feed", title: "$t:command.feed.title", description: "$t:command.feed.description", icon }, () => feed(ctx));
|
||||
await ctx.commands.register({ id: "play", title: "$t:command.play.title", description: "$t:command.play.description", icon }, () => play(ctx));
|
||||
await ctx.commands.register({ id: "pet", title: "$t:command.pet.title", description: "$t:command.pet.description", icon }, () => pet(ctx));
|
||||
await ctx.commands.register({ id: "nap", title: "$t:command.nap.title", description: "$t:command.nap.description", icon }, () => nap(ctx));
|
||||
await ctx.commands.register({ id: "status", title: "$t:command.status.title", description: "$t:command.status.description", icon }, () => showStatus(ctx));
|
||||
await ctx.commands.register({ id: "status", title: "$t:command.status.title", description: "$t:command.status.description", icon, placement: "top" }, () => showStatus(ctx));
|
||||
},
|
||||
async stop() {},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
"hud.bond": "Bond",
|
||||
"config.sound.label": "Action sound",
|
||||
"config.sound.description": "Optionally play a sound for direct care actions like feeding, playing, petting, or resting.",
|
||||
|
||||
|
||||
"command.feed.title": "Feed snack",
|
||||
"command.feed.description": "Feed your pet a snack.",
|
||||
"command.play.title": "Play game",
|
||||
|
|
@ -19,6 +19,20 @@
|
|||
"command.status.title": "Check on pet",
|
||||
"command.status.description": "Check how your pet is feeling.",
|
||||
|
||||
"menu.mood": "Mood",
|
||||
"menu.food": "Food",
|
||||
"menu.energy": "Energy",
|
||||
"menu.play": "Play",
|
||||
"menu.bond": "Bond",
|
||||
"menu.level": "Level",
|
||||
|
||||
"mood.sleeping": "Sleeping",
|
||||
"mood.hungry": "Hungry",
|
||||
"mood.tired": "Tired",
|
||||
"mood.bored": "Bored",
|
||||
"mood.happy": "Happy",
|
||||
"mood.content": "Content",
|
||||
|
||||
"speech.feed.0": "Thanks for the food!",
|
||||
"speech.feed.1": "*munch munch munch*",
|
||||
"speech.feed.2": "Yum, tasty!",
|
||||
|
|
|
|||
|
|
@ -91,35 +91,13 @@ const LOCALES = { en: JSON.parse(await readFile(new URL("./locales/en.json", imp
|
|||
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 pinned status bubble to show HUD
|
||||
h.expectBubble({ sticky: true, pin: true });
|
||||
|
||||
// Verify that the bubble contains the correct HUD items/values rather than text
|
||||
const lastBubble = h.calls.bubbles[h.calls.bubbles.length - 1];
|
||||
assert.ok(lastBubble, "Should have a bubble");
|
||||
assert.ok(lastBubble.spec.hud, "Bubble should have a HUD spec");
|
||||
assert.equal(lastBubble.spec.hud.items.length, 4);
|
||||
|
||||
const [food, energy, play, bond] = lastBubble.spec.hud.items;
|
||||
assert.equal(food.icon, "food");
|
||||
assert.equal(food.value, 80);
|
||||
assert.equal(food.tone, "amber");
|
||||
assert.equal(food.label, "Food");
|
||||
|
||||
assert.equal(energy.icon, "zap");
|
||||
assert.equal(energy.value, 80);
|
||||
assert.equal(energy.tone, "blue");
|
||||
assert.equal(energy.label, "Energy");
|
||||
|
||||
assert.equal(play.icon, "play");
|
||||
assert.equal(play.value, 80);
|
||||
assert.equal(play.tone, "green");
|
||||
assert.equal(play.label, "Play");
|
||||
|
||||
assert.equal(bond.icon, "heart");
|
||||
assert.equal(bond.value, 50);
|
||||
assert.equal(bond.tone, "pink");
|
||||
assert.equal(bond.label, "Bond");
|
||||
// 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 === "food" && item.title.includes("80%")), "Menu should show food status");
|
||||
assert.ok(menu.some((item) => item.id === "energy" && item.title.includes("80%")), "Menu should show energy status");
|
||||
assert.ok(menu.some((item) => item.id === "play" && item.title.includes("80%")), "Menu should show play status");
|
||||
assert.ok(menu.some((item) => item.id === "bond" && item.title.includes("50%")), "Menu should show bond status");
|
||||
|
||||
h.expectNoErrors();
|
||||
}
|
||||
|
|
@ -132,7 +110,7 @@ const LOCALES = { en: JSON.parse(await readFile(new URL("./locales/en.json", imp
|
|||
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();
|
||||
|
|
@ -143,7 +121,7 @@ const LOCALES = { en: JSON.parse(await readFile(new URL("./locales/en.json", imp
|
|||
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);
|
||||
|
|
@ -180,7 +158,7 @@ const LOCALES = { en: JSON.parse(await readFile(new URL("./locales/en.json", imp
|
|||
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);
|
||||
|
|
@ -192,7 +170,7 @@ const LOCALES = { en: JSON.parse(await readFile(new URL("./locales/en.json", imp
|
|||
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);
|
||||
|
|
@ -208,7 +186,7 @@ const LOCALES = { en: JSON.parse(await readFile(new URL("./locales/en.json", imp
|
|||
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);
|
||||
|
|
@ -217,7 +195,7 @@ const LOCALES = { en: JSON.parse(await readFile(new URL("./locales/en.json", imp
|
|||
const previousSpeakCount = h.calls.speak.length;
|
||||
await h.clock.advance("5m");
|
||||
assert.equal(h.calls.speak.length, previousSpeakCount, "nudge should not spam");
|
||||
|
||||
|
||||
h.expectNoErrors();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue