Compare commits

...
Sign in to create a new pull request.

1 commit

Author SHA1 Message Date
OpenPets Dev
ab62d9c96d refactor(virtual-pet): reorder context menu, rename submenu, use Title Casing
- Rename the context-menu submenu from 'Virtual Pet' to 'Pet Status'.
- Apply Title Casing to all virtual-pet menu labels ('Check On Pet',
  'Play Game', 'Feed Snack', 'Let Rest', 'Pet Status').
- Move care actions (Pet, Play Game, Feed Snack, Let Rest) to the top
  level, ordered by priority.
- Place the 'Pet Status' submenu directly under 'Check On Pet'.
- Reorder stat rows to Level, Bond, Mood, Play, Food, Energy.
- Update the golden plugin test to match the new row grouping.
2026-06-14 04:57:00 +00:00
4 changed files with 55 additions and 32 deletions

View file

@ -155,34 +155,58 @@ function installPetContextMenu(window: BrowserWindow, action: { readonly label:
async function buildPetContextMenuTemplate(action: { readonly label: string; readonly click: () => void; readonly defaultPet?: boolean }): Promise<Electron.MenuItemConstructorOptions[]> {
if (!action.defaultPet) return [{ label: action.label, click: action.click }];
const commands = await getDefaultPetPluginCommands();
const topLevel: Electron.MenuItemConstructorOptions[] = [];
const topLevelStatus: Electron.MenuItemConstructorOptions[] = [];
const topLevelActions: 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; }
if (command.placement === "top" || command.featured) {
if (command.commandId === "status") {
topLevelStatus.push(item);
} else {
topLevelActions.push(item);
}
continue;
}
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) — put care actions first, then status labels.
// Fully dynamic per-plugin menu sections (ui.menu.setItems) — status labels follow the top-level status command.
const menuItems = await getDefaultPetPluginMenuItems();
for (const item of menuItems) {
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) => {
const buildPluginSubmenu = (plugin: { name: string; commands: Electron.MenuItemConstructorOptions[]; menuItems: Electron.MenuItemConstructorOptions[] }): Electron.MenuItemConstructorOptions => {
const submenu: Electron.MenuItemConstructorOptions[] = [];
if (plugin.commands.length > 0) submenu.push(...plugin.commands);
if (plugin.commands.length > 0 && plugin.menuItems.length > 0) submenu.push({ type: "separator" });
if (plugin.menuItems.length > 0) submenu.push(...plugin.menuItems);
return { label: plugin.name, submenu };
}), { type: "separator" });
};
// Place the virtual-pet stats submenu directly under its top-level status command.
const statsPluginId = "openpets.virtual-pet";
const statsGroup = plugins.get(statsPluginId);
const statsSubmenu = statsGroup && statsGroup.menuItems.length > 0 ? buildPluginSubmenu(statsGroup) : undefined;
if (statsGroup) plugins.delete(statsPluginId);
const template: Electron.MenuItemConstructorOptions[] = [];
if (topLevelStatus.length > 0) template.push(...topLevelStatus.slice(0, 8));
if (statsSubmenu) template.push(statsSubmenu);
if (topLevelActions.length > 0) {
if (template.length > 0) template.push({ type: "separator" });
template.push(...topLevelActions.slice(0, 8));
}
if (plugins.size > 0) {
if (template.length > 0) template.push({ type: "separator" });
template.push(...[...plugins.values()].map(buildPluginSubmenu));
}
if (template.length > 0) template.push({ 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;
}

View file

@ -104,16 +104,16 @@ async function playActionSound(ctx) {
function buildStatusMenuItems(ctx, state, now = Date.now()) {
const mood = getMood(state, now);
const sep = " · ";
const levelText = `${ctx.t("menu.level")}: ${state.level}`;
const bondText = `${ctx.t("menu.bond")}: ${Math.round(state.affection)}%`;
const moodText = `${ctx.t("menu.mood")}: ${ctx.t(`mood.${mood}`)}`;
const playText = `${ctx.t("menu.play")}: ${Math.round(state.happiness)}%`;
const foodText = `${ctx.t("menu.food")}: ${Math.round(state.hunger)}%`;
const energyText = `${ctx.t("menu.energy")}: ${Math.round(state.energy)}%`;
const playText = `${ctx.t("menu.play")}: ${Math.round(state.happiness)}%`;
const bondText = `${ctx.t("menu.bond")}: ${Math.round(state.affection)}%`;
const levelText = `${ctx.t("menu.level")}: ${state.level}`;
return [
{ id: "stats-row-1", title: `${moodText}${sep}${foodText}`, enabled: false },
{ id: "stats-row-2", title: `${energyText}${sep}${playText}`, enabled: false },
{ id: "stats-row-3", title: `${bondText}${sep}${levelText}`, enabled: false },
{ id: "stats-row-1", title: `${levelText}${sep}${bondText}`, enabled: false },
{ id: "stats-row-2", title: `${moodText}${sep}${playText}`, enabled: false },
{ id: "stats-row-3", title: `${foodText}${sep}${energyText}`, enabled: false },
];
}
@ -345,11 +345,11 @@ export function register(OpenPetsPlugin) {
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, placement: "top" }, () => showStatus(ctx));
await ctx.commands.register({ id: "status", title: "$t:command.status.title", description: "$t:command.status.description", icon, placement: "top", priority: 100 }, () => showStatus(ctx));
await ctx.commands.register({ id: "pet", title: "$t:command.pet.title", description: "$t:command.pet.description", icon, placement: "top", priority: 80 }, () => pet(ctx));
await ctx.commands.register({ id: "play", title: "$t:command.play.title", description: "$t:command.play.description", icon, placement: "top", priority: 60 }, () => play(ctx));
await ctx.commands.register({ id: "feed", title: "$t:command.feed.title", description: "$t:command.feed.description", icon, placement: "top", priority: 40 }, () => feed(ctx));
await ctx.commands.register({ id: "nap", title: "$t:command.nap.title", description: "$t:command.nap.description", icon, placement: "top", priority: 20 }, () => nap(ctx));
},
async stop() {},
});

View file

@ -1,5 +1,5 @@
{
"plugin.name": "Virtual Pet",
"plugin.name": "Pet Status",
"plugin.description": "Care for a little desktop companion and watch your bond grow through gentle everyday interactions.",
"hud.food": "Food",
"hud.energy": "Energy",
@ -8,15 +8,15 @@
"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.title": "Feed Snack",
"command.feed.description": "Feed your pet a snack.",
"command.play.title": "Play game",
"command.play.title": "Play Game",
"command.play.description": "Play a quick game.",
"command.pet.title": "Pet",
"command.pet.description": "Give your pet some affection.",
"command.nap.title": "Let rest",
"command.nap.title": "Let Rest",
"command.nap.description": "Let your pet rest for 15 minutes.",
"command.status.title": "Check on pet",
"command.status.title": "Check On Pet",
"command.status.description": "Check how your pet is feeling.",
"menu.mood": "Mood",

View file

@ -94,10 +94,9 @@ const LOCALES = { en: JSON.parse(await readFile(new URL("./locales/en.json", imp
// 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");
assert.ok(menu.some((item) => item.id === "stats-row-1" && item.title.includes("Level: 1") && item.title.includes("50%")), "Menu should show level and bond status");
assert.ok(menu.some((item) => item.id === "stats-row-2" && item.title.includes("Mood:") && item.title.includes("80%")), "Menu should show mood and play status");
assert.ok(menu.some((item) => item.id === "stats-row-3" && item.title.includes("80%")), "Menu should show food and energy status");
h.expectNoErrors();
}