From f31d1508da38961e79f2cc0592448d12c6876c86 Mon Sep 17 00:00:00 2001 From: VoidKitten <20509618+VoidKitten@users.noreply.github.com> Date: Mon, 14 Sep 2026 22:44:28 -0500 Subject: [PATCH] feat: add resource node world bulk actions Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../account/masterclasses/ResourceNodesTab.js | 55 ++++++++++++++++++- src/ui/styles/_masterclasses.css | 12 ++++ 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/src/ui/components/views/account/masterclasses/ResourceNodesTab.js b/src/ui/components/views/account/masterclasses/ResourceNodesTab.js index 7375bc0d..acf61a97 100644 --- a/src/ui/components/views/account/masterclasses/ResourceNodesTab.js +++ b/src/ui/components/views/account/masterclasses/ResourceNodesTab.js @@ -17,6 +17,7 @@ import { toInt, toNum, useWriteStatus, + writeManyVerified, writeVerified, } from "../accountShared.js"; @@ -162,6 +163,8 @@ const NodeRow = ({ node, gradeState, depletedState }) => { export const ResourceNodesTab = () => { const { loading, error, run: runLoad } = useAccountLoad({ label: "Resource Nodes" }); const activeWorld = van.state(1); + const bulkStatus = useWriteStatus(); + const bulkMessage = van.state(""); const nodes = van.state([]); const gradeStates = new Map(); const depletedStates = new Map(); @@ -212,6 +215,32 @@ export const ResourceNodesTab = () => { nodes.val = nextNodes; }); + const writeWorldDepleted = async (value) => { + if (loading.val || bulkStatus.status.val === "loading") return; + const world = activeWorld.val; + const targets = nodes.val.filter((node) => node.world === world); + if (!targets.length) return; + + bulkMessage.val = ""; + await bulkStatus.run( + async () => { + await writeManyVerified(targets.map((node) => ({ path: `RoyalG[4][${node.nodeIndex}]`, value }))); + targets.forEach((node) => { + depletedStates.get(node.nodeIndex).val = value; + }); + bulkMessage.val = `W${world}: ${targets.length} nodes ${value === 0 ? "refreshed" : "depleted"}.`; + }, + { + onError: (error) => { + bulkMessage.val = `W${world} bulk action failed: ${error.message}. Re-reading node state.`; + void load(); + }, + } + ); + }; + + const bulkDisabled = () => loading.val || !nodes.val.some((node) => node.world === activeWorld.val); + load(); return PersistentAccountListPage({ @@ -222,7 +251,7 @@ export const ResourceNodesTab = () => { refresh: { onClick: load, tooltip: "Re-read Royal resource nodes from the running game.", - disabled: () => loading.val, + disabled: () => loading.val || bulkStatus.status.val === "loading", }, }), state: { loading, error }, @@ -235,7 +264,7 @@ export const ResourceNodesTab = () => { title: "NODE WORLDS", body: [ div( - { class: "masterclass-category-tabs" }, + { class: "masterclass-category-tabs resource-node-toolbar" }, ...Array.from({ length: WORLD_COUNT }, (_, index) => { const world = index + 1; return button( @@ -245,14 +274,34 @@ export const ResourceNodesTab = () => { `masterclass-category-tabs__button${ activeWorld.val === world ? " is-active" : "" }`, + disabled: () => bulkStatus.status.val === "loading", onclick: () => { activeWorld.val = world; }, }, `W${world}` ); - }) + }), + div( + { class: "resource-node-toolbar__actions" }, + ActionButton({ + label: "REFRESH ALL", + status: bulkStatus.status, + disabled: bulkDisabled, + tooltip: "Set every node in the selected world to fresh (collected = 0).", + onClick: () => void writeWorldDepleted(0), + }), + ActionButton({ + label: "DEPLETE ALL", + status: bulkStatus.status, + disabled: bulkDisabled, + variant: "danger", + tooltip: "Deplete every node in the selected world (collected = -1).", + onClick: () => void writeWorldDepleted(-1), + }) + ) ), + div({ role: "status", "aria-live": "polite" }, () => bulkMessage.val), bodyContent, ], }), diff --git a/src/ui/styles/_masterclasses.css b/src/ui/styles/_masterclasses.css index c2edf3e4..a3697e58 100644 --- a/src/ui/styles/_masterclasses.css +++ b/src/ui/styles/_masterclasses.css @@ -474,3 +474,15 @@ justify-self: start; } } + +/* Resource node world actions */ +.resource-node-toolbar { + align-items: center; +} + +.resource-node-toolbar__actions { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-left: auto; +}