mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 20:27:02 +01:00
Split kcd2 App.tsx 306 -> 217 + useInventoryState
App.tsx owned the inventory as well as rendering it: resolving a shared link against this browser's storage at mount, deferring persistence until the visitor consents, clearing the URL fragment, and three mutators. Moved all of it to a useInventoryState hook (163) beside the existing useSolver. The two inline handlers that only set state became named keep/replace callbacks, so the view no longer touches setPersist or setSaved; Clear, Discard and onImport all go through replace. Coverage held at 100% -- totals moved 863/375/181/810 -> 864/375/182/811, exactly the one statement and one function that promoting the Keep handler adds. Branches unchanged, so nothing conditional left test reach. That was the metric that would have caught an over-large extraction here, since the hook has no dedicated test and rides on the four App test files. No kcd2_dice_solver file is over the 250-line cap now. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
This commit is contained in:
parent
47996ac017
commit
44b8b79207
20
docs/superpowers/contracts/kcd2-app-split-2026-08-17.json
Normal file
20
docs/superpowers/contracts/kcd2-app-split-2026-08-17.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"title": "Split kcd2_dice_solver src/App.tsx under the 250-line cap",
|
||||
"objective": "Bring kcd2_dice_solver/src/App.tsx (306 lines) under the repo-wide 250-line file cap by extracting the inventory state it owns -- the link-versus-storage resolution at mount, the deferred-persistence rule, and the three mutators -- into a useInventoryState custom hook under src/hooks/, leaving App.tsx as the view, with behaviour proven by the four existing App test files at 100% coverage.",
|
||||
"acceptance_criteria": [
|
||||
"App.tsx and the new hook are each at most 250 lines, and check_file_length.sh --all no longer lists any kcd2_dice_solver file.",
|
||||
"npx vitest run still reports 24 files and 288 tests, all passing -- no test was added, deleted, or had an assertion weakened.",
|
||||
"npx vitest run --coverage still reports 100% statements, 100% branches, 100% functions and 100% lines. Totals may move by at most one statement and one function, since promoting the inline Keep handler to a named callback in the hook is one added function; any larger movement, or any metric below 100%, means extracted code left test reach and the extraction must be made smaller rather than the threshold lowered.",
|
||||
"npm run lint (tsc --noEmit && eslint src) still exits 0 with no output.",
|
||||
"The hook re-exports nothing: App.tsx imports EMPTY from lib/inventoryIo.ts, its real home, rather than through the hook.",
|
||||
"Zero suppressions added: no eslint-disable, no @ts-ignore, no @ts-expect-error, and vite.config.ts is byte-identical (thresholds and exclude untouched)."
|
||||
],
|
||||
"out_of_scope": [
|
||||
"Any change to the inventory rules: a link only wins when it carries dice or badges, persistence stays deferred until Keep or an edit, Discard restores this browser's stored inventory, and any URL fragment is cleared at mount.",
|
||||
"Changing the rendered markup, class names, ARIA roles or copy -- the four App test files query on these.",
|
||||
"Adding a dedicated test file for the new hook; its coverage comes from the existing App tests, and adding one would change the 288-test count this split is verified against.",
|
||||
"Modifying vite.config.ts, including adding the new file to the coverage exclude list.",
|
||||
"Wiring the file-length pre-commit hook -- it must land last, and 51 files elsewhere in the repo are still over."
|
||||
],
|
||||
"verifier": "npx vitest run (expect 24 files / 288 tests passed); npx vitest run --coverage (expect 100% on all four metrics; totals 864/375/182/811, one statement and one function above the 863/375/181/810 baseline); npm run lint (expect exit 0, no output); bash ~/utils/scripts/check_file_length.sh --all (expect no kcd2_dice_solver entry at all); git diff --stat vite.config.ts (expect empty); grep -rn 'eslint-disable\\|ts-ignore\\|ts-expect-error' src/ (expect no hits); pre-commit run --files on every touched file."
|
||||
}
|
||||
61
docs/superpowers/evidence/kcd2-app-split-2026-08-17.json
Normal file
61
docs/superpowers/evidence/kcd2-app-split-2026-08-17.json
Normal file
@ -0,0 +1,61 @@
|
||||
{
|
||||
"intent": "Bring kcd2_dice_solver/src/App.tsx (306 lines) under the repo-wide 250-line cap with no change to what the app renders or how the inventory behaves, completing the four-file kcd2 series.",
|
||||
"scope": [
|
||||
"kcd2_dice_solver/src/App.tsx — reduced to the view: props, the search query, the solve request, and the JSX",
|
||||
"kcd2_dice_solver/src/hooks/useInventoryState.ts — new: InventoryState, useInventoryState",
|
||||
"Non-goal: any change to rendered markup, class names, ARIA roles or copy, which the four App test files query on",
|
||||
"Non-goal: a dedicated test file for the hook, which would change the 288-test count this split is verified against"
|
||||
],
|
||||
"changes": [
|
||||
"Extracted the inventory state into a useInventoryState hook under src/hooks/, following the existing useSolver pattern: the link-versus-storage resolution at mount, the deferred-persistence boolean and its storage effect, the fragment-clearing effect, and the setDieCount/toggleBadge mutators, all with their explanatory comments.",
|
||||
"Promoted the two inline handlers that only set state — Keep, and the Discard/Clear/import replacement — to named `keep` and `replace` callbacks on the hook, so the view no longer touches setPersist or setSaved directly. importInventory collapsed into `replace`, which Clear, Discard and InventoryIo's onImport now all use.",
|
||||
"The hook exposes `stored` as a plain SavedInventory rather than the loadInventory result, since Discard is the only consumer and it wants the inventory, not the parse report.",
|
||||
"Deliberately avoided a re-export: an initial draft had the hook re-export EMPTY, which is the barrel pattern this series is not using. App.tsx imports EMPTY from lib/inventoryIo.ts, its real home."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "npx vitest run",
|
||||
"result": "pass",
|
||||
"evidence": "Test Files 24 passed (24); Tests 288 passed (288). The four App test files — AppStorage, AppShell, AppLinks, AppRecovery — exercise the extracted state through the component and all still pass unmodified."
|
||||
},
|
||||
{
|
||||
"command": "npx vitest run --coverage",
|
||||
"result": "pass",
|
||||
"evidence": "Statements 100% (864/864), Branches 100% (375/375), Functions 100% (182/182), Lines 100% (811/811). Totals rose by exactly one statement and one function against the 863/375/181/810 baseline, which is the Keep handler becoming a named callback; branches are unchanged, so no conditional left test reach. This was the metric the brief warned would drop if the extraction were too large — it did not."
|
||||
},
|
||||
{
|
||||
"command": "npm run lint (tsc --noEmit && eslint src)",
|
||||
"result": "pass",
|
||||
"evidence": "Exit 0, no output."
|
||||
},
|
||||
{
|
||||
"command": "wc -l src/App.tsx src/hooks/useInventoryState.ts",
|
||||
"result": "pass",
|
||||
"evidence": "App.tsx 217, useInventoryState.ts 163 — both under the 250-line cap, down from a single 306-line file."
|
||||
},
|
||||
{
|
||||
"command": "bash ~/utils/scripts/check_file_length.sh --all",
|
||||
"result": "pass",
|
||||
"evidence": "No kcd2_dice_solver file appears in the over-cap listing at all. This is the fourth and last of the four files the series targeted."
|
||||
},
|
||||
{
|
||||
"command": "grep -rn 'eslint-disable|ts-ignore|ts-expect-error' kcd2_dice_solver/src/ ; git diff --stat vite.config.ts",
|
||||
"result": "pass",
|
||||
"evidence": "No hits and an empty diff: zero suppressions across the whole series, and the coverage thresholds and exclude list were never touched."
|
||||
},
|
||||
{
|
||||
"command": "pre-commit run --files <changed files>",
|
||||
"result": "pass",
|
||||
"evidence": "All configured hooks pass on the touched files."
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"The hook has no dedicated test file; its coverage comes entirely from the four App test files exercising it through the component. Coverage is 100% including branches, but a future change to the hook could be under-tested in a way a direct hook test would catch. Adding one was deliberately left out of scope because it would change the 288-test baseline this split is verified against.",
|
||||
"App.tsx and the hook now share a wide interface — eleven fields. That is the cost of extracting state rather than markup from a component with no prop-drilled subtree; a presentational split would have needed comparable prop plumbing.",
|
||||
"kcd2_dice_solver has no CI workflow and is not covered by pre-commit, so the vitest and lint runs above are the only gate on this change."
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit — one edited file and one added file, with no data, config or schema migration.",
|
||||
"After rollback, re-run npx vitest run --coverage and npm run lint; expect 288 tests passing at 100% coverage (totals returning to 863/375/181/810) and lint exit 0."
|
||||
]
|
||||
}
|
||||
@ -2,37 +2,29 @@
|
||||
* KCD2 Dice Solver.
|
||||
*
|
||||
* Enter what dice and badges you own; get the best six to bring and the best
|
||||
* badge for each tier you hold.
|
||||
* badge for each tier you hold. Owning the inventory — where it came from and
|
||||
* when it is written back — is `useInventoryState`'s job; this file is the view.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import type { JSX } from "react";
|
||||
import { BadgePicker } from "./components/BadgePicker.tsx";
|
||||
import { DiceList } from "./components/DiceList.tsx";
|
||||
import { InventoryIo } from "./components/InventoryIo.tsx";
|
||||
import { ResultPanel } from "./components/ResultPanel.tsx";
|
||||
import type { CountUpdater } from "./components/QuantityStepper.tsx";
|
||||
import { SET_SIZE } from "./core/searchGroups.ts";
|
||||
import type { SolveRequest } from "./core/solve.ts";
|
||||
import { useInventoryState } from "./hooks/useInventoryState.ts";
|
||||
import { useSolver } from "./hooks/useSolver.ts";
|
||||
import type { SolverPort } from "./hooks/useSolver.ts";
|
||||
import {
|
||||
EMPTY,
|
||||
STORAGE_KEY,
|
||||
browserClipboard,
|
||||
browserSaveFile,
|
||||
browserUrlHash,
|
||||
decodeInventoryHash,
|
||||
encodeInventoryHash,
|
||||
isEmptyInventory,
|
||||
loadInventory,
|
||||
} from "./lib/inventoryIo.ts";
|
||||
import type {
|
||||
ClipboardPort,
|
||||
SavedInventory,
|
||||
SaveFilePort,
|
||||
UrlHashPort,
|
||||
} from "./lib/inventoryIo.ts";
|
||||
import type { ClipboardPort, SaveFilePort, UrlHashPort } from "./lib/inventoryIo.ts";
|
||||
|
||||
export interface AppProps {
|
||||
/** Overridden in tests to avoid depending on jsdom worker support. */
|
||||
@ -63,95 +55,18 @@ export function App({
|
||||
const store = storage ?? window.localStorage;
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
// What this browser already had. Kept around so a shared link can be turned
|
||||
// down without losing it.
|
||||
const [stored] = useState(() => loadInventory(store));
|
||||
|
||||
// A link beats whatever is in this browser: following one is a deliberate act
|
||||
// in a way that "opening the page again" is not.
|
||||
const [fromLink] = useState(() => decodeInventoryHash(urlHash.read()));
|
||||
|
||||
/*
|
||||
* ...but only a link that actually carried dice or badges. Testing "did the
|
||||
* parser complain?" is not the same question: a link encoding a count of zero
|
||||
* parses perfectly and yields an EMPTY inventory, which would then replace the
|
||||
* visitor's own dice on screen, disable Clear (nothing to clear), and leave
|
||||
* Keep as the sole enabled control — writing that emptiness over their saved
|
||||
* loadout. Requiring the link to actually carry something removes the case.
|
||||
*/
|
||||
const linkApplied = fromLink !== null && !isEmptyInventory(fromLink.inventory);
|
||||
const [saved, setSaved] = useState<SavedInventory>(() =>
|
||||
linkApplied ? fromLink.inventory : stored.inventory,
|
||||
);
|
||||
|
||||
// Whatever was discarded on the way in, from whichever source was used. The
|
||||
// parser sanitises rather than rejects, so without surfacing this a die added
|
||||
// in a later game patch — or a URL a chat client truncated — would vanish in
|
||||
// complete silence.
|
||||
const dropped = linkApplied ? fromLink.dropped : stored.dropped;
|
||||
|
||||
/*
|
||||
* Nothing is written to storage until the visitor accepts it. Silently
|
||||
* overwriting a saved loadout — with somebody else's dice, or with a
|
||||
* sanitised copy of their own that just lost an entry — is unrecoverable, and
|
||||
* a confirm() dialog is both untestable and hostile. Persistence is deferred
|
||||
* until they do something that implies consent: press Keep, or simply edit
|
||||
* anything. One boolean, and the banner clears itself the moment they engage.
|
||||
*/
|
||||
const [persist, setPersist] = useState(!linkApplied && dropped.length === 0);
|
||||
|
||||
useEffect(() => {
|
||||
if (persist) {
|
||||
store.setItem(STORAGE_KEY, JSON.stringify(saved));
|
||||
}
|
||||
}, [saved, store, persist]);
|
||||
|
||||
useEffect(() => {
|
||||
/*
|
||||
* Clear ANY fragment, not just one that decoded. A fragment left in the bar
|
||||
* is a loaded gun: it is read once, at mount, so a link arriving in an
|
||||
* already-open tab is a same-document navigation that changes nothing now —
|
||||
* but survives to be applied over edited state on the next reload.
|
||||
*/
|
||||
if (urlHash.read() !== "") {
|
||||
urlHash.clear();
|
||||
}
|
||||
}, [urlHash]);
|
||||
|
||||
const setDieCount = useCallback((id: string, update: CountUpdater): void => {
|
||||
setPersist(true);
|
||||
setSaved((previous) => {
|
||||
// Applied to the previous state, not to a value captured at render time,
|
||||
// so a burst of clicks or wheel ticks accumulates instead of collapsing.
|
||||
const count = update(previous.diceCounts[id] ?? 0);
|
||||
// Rebuilt rather than mutated so a count of zero simply never makes it
|
||||
// into the saved object.
|
||||
const diceCounts = Object.fromEntries(
|
||||
Object.entries(previous.diceCounts).filter(([key]) => key !== id),
|
||||
);
|
||||
if (count > 0) {
|
||||
diceCounts[id] = count;
|
||||
}
|
||||
return { ...previous, diceCounts };
|
||||
});
|
||||
}, []);
|
||||
|
||||
const toggleBadge = useCallback((id: string, owned: boolean): void => {
|
||||
setPersist(true);
|
||||
setSaved((previous) => ({
|
||||
...previous,
|
||||
badgeIds: owned
|
||||
? [...previous.badgeIds, id]
|
||||
: previous.badgeIds.filter((existing) => existing !== id),
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const importInventory = useCallback((inventory: SavedInventory): void => {
|
||||
setPersist(true);
|
||||
setSaved(inventory);
|
||||
}, []);
|
||||
|
||||
const total = Object.values(saved.diceCounts).reduce((sum, count) => sum + count, 0);
|
||||
const {
|
||||
saved,
|
||||
stored,
|
||||
dropped,
|
||||
linkApplied,
|
||||
persist,
|
||||
total,
|
||||
keep,
|
||||
replace,
|
||||
setDieCount,
|
||||
toggleBadge,
|
||||
} = useInventoryState(store, urlHash);
|
||||
|
||||
const request = useMemo<SolveRequest | null>(
|
||||
() =>
|
||||
@ -205,8 +120,7 @@ export function App({
|
||||
className="clear"
|
||||
disabled={total === 0 && saved.badgeIds.length === 0}
|
||||
onClick={() => {
|
||||
setPersist(true);
|
||||
setSaved(EMPTY);
|
||||
replace(EMPTY);
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
@ -221,9 +135,7 @@ export function App({
|
||||
<button
|
||||
type="button"
|
||||
className="clear"
|
||||
onClick={() => {
|
||||
setPersist(true);
|
||||
}}
|
||||
onClick={keep}
|
||||
>
|
||||
Keep
|
||||
</button>{" "}
|
||||
@ -235,8 +147,7 @@ export function App({
|
||||
type="button"
|
||||
className="clear"
|
||||
onClick={() => {
|
||||
setPersist(true);
|
||||
setSaved(stored.inventory);
|
||||
replace(stored);
|
||||
}}
|
||||
>
|
||||
Discard
|
||||
@ -264,7 +175,7 @@ export function App({
|
||||
|
||||
<InventoryIo
|
||||
inventory={saved}
|
||||
onImport={importInventory}
|
||||
onImport={replace}
|
||||
clipboard={clipboard}
|
||||
saveFile={saveFile}
|
||||
shareUrl={urlHash.base() + encodeInventoryHash(saved)}
|
||||
|
||||
163
kcd2_dice_solver/src/hooks/useInventoryState.ts
Normal file
163
kcd2_dice_solver/src/hooks/useInventoryState.ts
Normal file
@ -0,0 +1,163 @@
|
||||
/**
|
||||
* The inventory the app is editing: where it came from, when it gets written
|
||||
* back, and how it changes.
|
||||
*
|
||||
* Three sources compete at mount — a shared link, this browser's storage, and
|
||||
* nothing at all — and the rules for resolving them are subtle enough that they
|
||||
* are stated here once rather than inline in the view. The comments on each
|
||||
* decision record cases that were live bugs.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import type { CountUpdater } from "../components/QuantityStepper.tsx";
|
||||
import {
|
||||
STORAGE_KEY,
|
||||
decodeInventoryHash,
|
||||
isEmptyInventory,
|
||||
loadInventory,
|
||||
} from "../lib/inventoryIo.ts";
|
||||
import type { SavedInventory, UrlHashPort } from "../lib/inventoryIo.ts";
|
||||
|
||||
/** What the view needs to render and mutate the inventory. */
|
||||
export interface InventoryState {
|
||||
/** The inventory currently on screen. */
|
||||
readonly saved: SavedInventory;
|
||||
/** The inventory this browser had before a link was applied, for Discard. */
|
||||
readonly stored: SavedInventory;
|
||||
/** Entries the parser could not read, surfaced rather than dropped silently. */
|
||||
readonly dropped: readonly string[];
|
||||
/** True when the on-screen inventory came from a shared link. */
|
||||
readonly linkApplied: boolean;
|
||||
/** False while changes are deliberately not being written to storage. */
|
||||
readonly persist: boolean;
|
||||
/** Total dice owned, across every entry. */
|
||||
readonly total: number;
|
||||
/** Accept the current state and resume writing to storage. */
|
||||
readonly keep: () => void;
|
||||
/** Replace the whole inventory, e.g. from an import or Clear. */
|
||||
readonly replace: (inventory: SavedInventory) => void;
|
||||
/** Change how many of one die is owned. */
|
||||
readonly setDieCount: (id: string, update: CountUpdater) => void;
|
||||
/** Add or remove a badge from the owned set. */
|
||||
readonly toggleBadge: (id: string, owned: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Own the inventory: resolve where it came from, persist it once that is
|
||||
* consented to, and expose the mutators the view needs.
|
||||
*
|
||||
* @param store - Where the inventory is persisted between visits.
|
||||
* @param urlHash - Access to the URL fragment a shared link arrives in.
|
||||
* @returns The current inventory and everything needed to change it.
|
||||
*/
|
||||
export function useInventoryState(
|
||||
store: Pick<Storage, "getItem" | "setItem">,
|
||||
urlHash: UrlHashPort,
|
||||
): InventoryState {
|
||||
// What this browser already had. Kept around so a shared link can be turned
|
||||
// down without losing it.
|
||||
const [stored] = useState(() => loadInventory(store));
|
||||
|
||||
// A link beats whatever is in this browser: following one is a deliberate act
|
||||
// in a way that "opening the page again" is not.
|
||||
const [fromLink] = useState(() => decodeInventoryHash(urlHash.read()));
|
||||
|
||||
/*
|
||||
* ...but only a link that actually carried dice or badges. Testing "did the
|
||||
* parser complain?" is not the same question: a link encoding a count of zero
|
||||
* parses perfectly and yields an EMPTY inventory, which would then replace the
|
||||
* visitor's own dice on screen, disable Clear (nothing to clear), and leave
|
||||
* Keep as the sole enabled control — writing that emptiness over their saved
|
||||
* loadout. Requiring the link to actually carry something removes the case.
|
||||
*/
|
||||
const linkApplied = fromLink !== null && !isEmptyInventory(fromLink.inventory);
|
||||
const [saved, setSaved] = useState<SavedInventory>(() =>
|
||||
linkApplied ? fromLink.inventory : stored.inventory,
|
||||
);
|
||||
|
||||
// Whatever was discarded on the way in, from whichever source was used. The
|
||||
// parser sanitises rather than rejects, so without surfacing this a die added
|
||||
// in a later game patch — or a URL a chat client truncated — would vanish in
|
||||
// complete silence.
|
||||
const dropped = linkApplied ? fromLink.dropped : stored.dropped;
|
||||
|
||||
/*
|
||||
* Nothing is written to storage until the visitor accepts it. Silently
|
||||
* overwriting a saved loadout — with somebody else's dice, or with a
|
||||
* sanitised copy of their own that just lost an entry — is unrecoverable, and
|
||||
* a confirm() dialog is both untestable and hostile. Persistence is deferred
|
||||
* until they do something that implies consent: press Keep, or simply edit
|
||||
* anything. One boolean, and the banner clears itself the moment they engage.
|
||||
*/
|
||||
const [persist, setPersist] = useState(!linkApplied && dropped.length === 0);
|
||||
|
||||
useEffect(() => {
|
||||
if (persist) {
|
||||
store.setItem(STORAGE_KEY, JSON.stringify(saved));
|
||||
}
|
||||
}, [saved, store, persist]);
|
||||
|
||||
useEffect(() => {
|
||||
/*
|
||||
* Clear ANY fragment, not just one that decoded. A fragment left in the bar
|
||||
* is a loaded gun: it is read once, at mount, so a link arriving in an
|
||||
* already-open tab is a same-document navigation that changes nothing now —
|
||||
* but survives to be applied over edited state on the next reload.
|
||||
*/
|
||||
if (urlHash.read() !== "") {
|
||||
urlHash.clear();
|
||||
}
|
||||
}, [urlHash]);
|
||||
|
||||
const setDieCount = useCallback((id: string, update: CountUpdater): void => {
|
||||
setPersist(true);
|
||||
setSaved((previous) => {
|
||||
// Applied to the previous state, not to a value captured at render time,
|
||||
// so a burst of clicks or wheel ticks accumulates instead of collapsing.
|
||||
const count = update(previous.diceCounts[id] ?? 0);
|
||||
// Rebuilt rather than mutated so a count of zero simply never makes it
|
||||
// into the saved object.
|
||||
const diceCounts = Object.fromEntries(
|
||||
Object.entries(previous.diceCounts).filter(([key]) => key !== id),
|
||||
);
|
||||
if (count > 0) {
|
||||
diceCounts[id] = count;
|
||||
}
|
||||
return { ...previous, diceCounts };
|
||||
});
|
||||
}, []);
|
||||
|
||||
const toggleBadge = useCallback((id: string, owned: boolean): void => {
|
||||
setPersist(true);
|
||||
setSaved((previous) => ({
|
||||
...previous,
|
||||
badgeIds: owned
|
||||
? [...previous.badgeIds, id]
|
||||
: previous.badgeIds.filter((existing) => existing !== id),
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const replace = useCallback((inventory: SavedInventory): void => {
|
||||
setPersist(true);
|
||||
setSaved(inventory);
|
||||
}, []);
|
||||
|
||||
const keep = useCallback((): void => {
|
||||
setPersist(true);
|
||||
}, []);
|
||||
|
||||
const total = Object.values(saved.diceCounts).reduce((sum, count) => sum + count, 0);
|
||||
|
||||
return {
|
||||
saved,
|
||||
stored: stored.inventory,
|
||||
dropped,
|
||||
linkApplied,
|
||||
persist,
|
||||
total,
|
||||
keep,
|
||||
replace,
|
||||
setDieCount,
|
||||
toggleBadge,
|
||||
};
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user