@linxin666/dsh-pet 0.3.23 → 0.3.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +5 -4
  3. package/README.zh.md +5 -4
  4. package/icon.svg +3 -0
  5. package/lib/client.js +522 -194
  6. package/lib/client.js.map +1 -1
  7. package/lib/index.js +710 -426
  8. package/lib/live2d-vendor.js +96 -96
  9. package/lib/live2d-vendor.js.map +1 -1
  10. package/lib/types/announce.d.ts +6 -6
  11. package/lib/types/announce.d.ts.map +1 -1
  12. package/lib/types/announce.js +5 -5
  13. package/lib/types/client/PetDockEntry.d.ts.map +1 -1
  14. package/lib/types/client/PetDockEntry.js +1 -1
  15. package/lib/types/client/PetSettingsCard.d.ts +20 -9
  16. package/lib/types/client/PetSettingsCard.d.ts.map +1 -1
  17. package/lib/types/client/PetSettingsCard.js +116 -5
  18. package/lib/types/client/PetSprite.d.ts +6 -0
  19. package/lib/types/client/PetSprite.d.ts.map +1 -1
  20. package/lib/types/client/PetSprite.js +95 -0
  21. package/lib/types/client/PluginSettingsCard.d.ts +4 -0
  22. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
  23. package/lib/types/client/PluginSettingsCard.js +3 -2
  24. package/lib/types/client/gameplay-hud.d.ts +10 -1
  25. package/lib/types/client/gameplay-hud.d.ts.map +1 -1
  26. package/lib/types/client/gameplay-hud.js +137 -23
  27. package/lib/types/client/index.d.ts +8 -5
  28. package/lib/types/client/index.d.ts.map +1 -1
  29. package/lib/types/client/index.js +61 -27
  30. package/lib/types/client/main-session.d.ts +33 -0
  31. package/lib/types/client/main-session.d.ts.map +1 -0
  32. package/lib/types/client/main-session.js +34 -0
  33. package/lib/types/client/settings-form.d.ts +17 -17
  34. package/lib/types/client/settings-form.d.ts.map +1 -1
  35. package/lib/types/client/settings-form.js +30 -27
  36. package/lib/types/event-projection.d.ts.map +1 -1
  37. package/lib/types/event-projection.js +5 -3
  38. package/lib/types/gameplay.d.ts +79 -5
  39. package/lib/types/gameplay.d.ts.map +1 -1
  40. package/lib/types/gameplay.js +212 -17
  41. package/lib/types/http.d.ts +18 -1
  42. package/lib/types/http.d.ts.map +1 -1
  43. package/lib/types/http.js +22 -1
  44. package/lib/types/index.d.ts +100 -29
  45. package/lib/types/index.d.ts.map +1 -1
  46. package/lib/types/index.js +113 -64
  47. package/lib/types/persist.d.ts.map +1 -1
  48. package/lib/types/persist.js +3 -1
  49. package/lib/types/routes.d.ts.map +1 -1
  50. package/lib/types/routes.js +3 -1
  51. package/lib/types/service.d.ts +8 -3
  52. package/lib/types/service.d.ts.map +1 -1
  53. package/lib/types/service.js +26 -4
  54. package/package.json +46 -26
  55. package/src/announce.ts +6 -6
  56. package/src/client/PetDockEntry.tsx +1 -0
  57. package/src/client/PetSettingsCard.test.tsx +103 -0
  58. package/src/client/PetSettingsCard.tsx +129 -26
  59. package/src/client/PetSprite.tsx +96 -0
  60. package/src/client/PluginSettingsCard.tsx +7 -2
  61. package/src/client/gameplay-hud.test.tsx +106 -8
  62. package/src/client/gameplay-hud.tsx +143 -28
  63. package/src/client/index.test.tsx +37 -4
  64. package/src/client/index.ts +74 -34
  65. package/src/client/main-session.ts +43 -0
  66. package/src/client/settings-form.ts +34 -31
  67. package/src/event-projection.ts +5 -3
  68. package/src/gameplay.test.ts +138 -1
  69. package/src/gameplay.ts +253 -16
  70. package/src/http.ts +23 -1
  71. package/src/index.test.ts +107 -11
  72. package/src/index.ts +162 -73
  73. package/src/persist.ts +3 -1
  74. package/src/registry.test.ts +3 -2
  75. package/src/routes.ts +3 -1
  76. package/src/service.ts +27 -5
package/lib/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { _ as RemarkPicker, a as AFFINITY_MAX, c as applyInteraction, d as emptyAffinity, f as rankOf, g as REMARK_LINE_MAX, h as REMARK_LINES_MAX, i as rowOf, l as applyTurnReward, m as REMARK_KINDS, n as animationForPhase, o as AFFINITY_RANKS, p as BUILTIN_REMARKS, r as defaultPetStateConfig, s as affinityViewOf, t as PetStateMachine, u as defaultAffinityConfig, v as builtinRemark, y as normalizePetRemarks } from "./state-NvDEoCln.js";
2
- import z from "schemastery";
2
+ import z from "@deepseek-ai/schemastery";
3
3
  import { Service } from "@deepseek-ai/cordis";
4
4
  import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, realpathSync, renameSync, statSync, writeFileSync } from "node:fs";
5
5
  import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
@@ -11,10 +11,10 @@ import { readFile } from "node:fs/promises";
11
11
  /** Default freshness window. */
12
12
  const ANNOUNCE_DEFAULT_TTL_MS = 1e4;
13
13
  /**
14
- * Hard TTL ceiling. A repeating announcer (dsh-usage) declares its poll
15
- * cadence as the TTL, so an `always`-mode bubble stays continuous across
16
- * polls; the ceiling means a source that dies unmounts its bubble within at
17
- * most one missed refresh cycle rather than lingering forever.
14
+ * Hard TTL ceiling. A repeating announcer declares its poll cadence as
15
+ * the TTL, so an always-on bubble stays continuous across polls; the
16
+ * ceiling means a source that dies unmounts its bubble within at most one
17
+ * missed refresh cycle rather than lingering forever.
18
18
  */
19
19
  const ANNOUNCE_MAX_TTL_MS = 72e5;
20
20
  /** Hard bounds: lengths and the TTL range (1 s .. 60 s). */
@@ -894,9 +894,9 @@ function projectOfficialEvent(event, runtime, nowMs = Date.now()) {
894
894
  };
895
895
  }
896
896
  case "tool/result": {
897
- const block = event.data.message.content[0];
898
- const callId = String(event.data.message.source.callId);
899
- const failed = event.data.error !== void 0 || block.isError === true;
897
+ const { message } = event.data;
898
+ const callId = String(message.toolCallId);
899
+ const failed = event.data.error !== void 0 || message.isError === true;
900
900
  const wasTest = runtime.testCalls.delete(callId);
901
901
  runtime.activeTools.delete(callId);
902
902
  runtime.stepHadFailure ||= failed;
@@ -1000,7 +1000,7 @@ function projectAssistantStreamFrame(frame, runtime, nowMs = Date.now()) {
1000
1000
  //#region src/treats.ts
1001
1001
  const defaultTreatConfig = {
1002
1002
  turnsPerTreat: 30,
1003
- timeTreatMs: 300 * 6e4,
1003
+ timeTreatMs: 18e6,
1004
1004
  maxTreats: 20
1005
1005
  };
1006
1006
  function emptyTreatLedger() {
@@ -1474,7 +1474,7 @@ function loadGameplay(parsed) {
1474
1474
  const item = {
1475
1475
  stats,
1476
1476
  currencies,
1477
- mode: record.mode === "work" || record.mode === "sleep" ? record.mode : null,
1477
+ mode: typeof record.mode === "string" && record.mode.length > 0 && record.mode.length <= 24 ? record.mode : null,
1478
1478
  settledAt: clamp(finiteNum(record.settledAt, 0), Number.MAX_SAFE_INTEGER)
1479
1479
  };
1480
1480
  if (typeof record.incomeCarryMs === "number" && Number.isFinite(record.incomeCarryMs)) item.incomeCarryMs = Math.max(0, record.incomeCarryMs);
@@ -1664,49 +1664,55 @@ function normalizePanel(raw, onWarning = () => {}) {
1664
1664
  }
1665
1665
  const panel = {};
1666
1666
  const labelsRaw = raw.labels;
1667
- if (labelsRaw !== void 0) if (!isRecord$3(labelsRaw)) onWarning("panel.labels must be an object");
1668
- else {
1669
- const labels = {};
1670
- for (const key of PANEL_LABEL_KEYS) {
1671
- const value = labelsRaw[key];
1672
- if (value === void 0) continue;
1673
- if (typeof value !== "string") {
1674
- onWarning("panel.labels." + key + " must be a string");
1675
- continue;
1667
+ if (labelsRaw !== void 0) {
1668
+ if (!isRecord$3(labelsRaw)) onWarning("panel.labels must be an object");
1669
+ else {
1670
+ const labels = {};
1671
+ for (const key of PANEL_LABEL_KEYS) {
1672
+ const value = labelsRaw[key];
1673
+ if (value === void 0) continue;
1674
+ if (typeof value !== "string") {
1675
+ onWarning("panel.labels." + key + " must be a string");
1676
+ continue;
1677
+ }
1678
+ const line = normalizeLine(value, "label", onWarning);
1679
+ if (line !== void 0) labels[key] = line.slice(0, 40);
1676
1680
  }
1677
- const line = normalizeLine(value, "label", onWarning);
1678
- if (line !== void 0) labels[key] = line.slice(0, 40);
1681
+ if (Object.keys(labels).length > 0) panel.labels = labels;
1679
1682
  }
1680
- if (Object.keys(labels).length > 0) panel.labels = labels;
1681
1683
  }
1682
1684
  const statsRaw = raw.stats;
1683
- if (statsRaw !== void 0) if (!isRecord$3(statsRaw)) onWarning("panel.stats must be an object");
1684
- else {
1685
- const stats = {};
1686
- for (const key of PANEL_STAT_KEYS) {
1687
- const value = statsRaw[key];
1688
- if (value === void 0) continue;
1689
- if (typeof value !== "string") {
1690
- onWarning("panel.stats." + key + " must be a string");
1691
- continue;
1685
+ if (statsRaw !== void 0) {
1686
+ if (!isRecord$3(statsRaw)) onWarning("panel.stats must be an object");
1687
+ else {
1688
+ const stats = {};
1689
+ for (const key of PANEL_STAT_KEYS) {
1690
+ const value = statsRaw[key];
1691
+ if (value === void 0) continue;
1692
+ if (typeof value !== "string") {
1693
+ onWarning("panel.stats." + key + " must be a string");
1694
+ continue;
1695
+ }
1696
+ const line = normalizeLine(value, "stat", onWarning);
1697
+ if (line !== void 0) stats[key] = line.slice(0, 80);
1692
1698
  }
1693
- const line = normalizeLine(value, "stat", onWarning);
1694
- if (line !== void 0) stats[key] = line.slice(0, 80);
1699
+ if (Object.keys(stats).length > 0) panel.stats = stats;
1695
1700
  }
1696
- if (Object.keys(stats).length > 0) panel.stats = stats;
1697
1701
  }
1698
1702
  const actionsRaw = raw.actions;
1699
- if (actionsRaw !== void 0) if (!Array.isArray(actionsRaw)) onWarning("panel.actions must be an array");
1700
- else {
1701
- const seen = /* @__PURE__ */ new Set();
1702
- for (const entry of actionsRaw) {
1703
- if (typeof entry !== "string" || !PANEL_ACTIONS.includes(entry)) {
1704
- onWarning("unknown panel action dropped: " + String(entry));
1705
- continue;
1703
+ if (actionsRaw !== void 0) {
1704
+ if (!Array.isArray(actionsRaw)) onWarning("panel.actions must be an array");
1705
+ else {
1706
+ const seen = /* @__PURE__ */ new Set();
1707
+ for (const entry of actionsRaw) {
1708
+ if (typeof entry !== "string" || !PANEL_ACTIONS.includes(entry)) {
1709
+ onWarning("unknown panel action dropped: " + String(entry));
1710
+ continue;
1711
+ }
1712
+ seen.add(entry);
1706
1713
  }
1707
- seen.add(entry);
1714
+ panel.actions = PANEL_ACTIONS.filter((action) => seen.has(action));
1708
1715
  }
1709
- panel.actions = PANEL_ACTIONS.filter((action) => seen.has(action));
1710
1716
  }
1711
1717
  if (panel.labels === void 0 && panel.stats === void 0 && panel.actions === void 0) return void 0;
1712
1718
  return panel;
@@ -1741,30 +1747,34 @@ function normalizeVoicePack(raw, onWarning = () => {}) {
1741
1747
  if (version !== void 0 && (typeof version !== "number" || version !== 1)) onWarning("voicePackVersion " + String(version) + " is not supported; reading as v1 best-effort");
1742
1748
  const overrides = {};
1743
1749
  const statusRaw = raw.status;
1744
- if (statusRaw !== void 0) if (!isRecord$3(statusRaw)) onWarning("status must be an object");
1745
- else for (const key of Object.keys(statusRaw)) {
1746
- if (!STATUS_SCENES.includes(key)) {
1747
- onWarning("unknown status scene " + key + " ignored");
1748
- continue;
1750
+ if (statusRaw !== void 0) {
1751
+ if (!isRecord$3(statusRaw)) onWarning("status must be an object");
1752
+ else for (const key of Object.keys(statusRaw)) {
1753
+ if (!STATUS_SCENES.includes(key)) {
1754
+ onWarning("unknown status scene " + key + " ignored");
1755
+ continue;
1756
+ }
1757
+ const pool = normalizePool(statusRaw[key], "status", onWarning);
1758
+ if (pool !== void 0 && pool.length > 0) overrides.status = {
1759
+ ...overrides.status,
1760
+ [key]: pool
1761
+ };
1749
1762
  }
1750
- const pool = normalizePool(statusRaw[key], "status", onWarning);
1751
- if (pool !== void 0 && pool.length > 0) overrides.status = {
1752
- ...overrides.status,
1753
- [key]: pool
1754
- };
1755
1763
  }
1756
1764
  const toolsRaw = raw.tools;
1757
- if (toolsRaw !== void 0) if (!isRecord$3(toolsRaw)) onWarning("tools must be an object");
1758
- else for (const key of Object.keys(toolsRaw)) {
1759
- if (!TOOL_CATEGORIES.includes(key)) {
1760
- onWarning("unknown tool family " + key + " ignored");
1761
- continue;
1765
+ if (toolsRaw !== void 0) {
1766
+ if (!isRecord$3(toolsRaw)) onWarning("tools must be an object");
1767
+ else for (const key of Object.keys(toolsRaw)) {
1768
+ if (!TOOL_CATEGORIES.includes(key)) {
1769
+ onWarning("unknown tool family " + key + " ignored");
1770
+ continue;
1771
+ }
1772
+ const pool = normalizePool(toolsRaw[key], "tools", onWarning);
1773
+ if (pool !== void 0 && pool.length > 0) overrides.tools = {
1774
+ ...overrides.tools,
1775
+ [key]: pool
1776
+ };
1762
1777
  }
1763
- const pool = normalizePool(toolsRaw[key], "tools", onWarning);
1764
- if (pool !== void 0 && pool.length > 0) overrides.tools = {
1765
- ...overrides.tools,
1766
- [key]: pool
1767
- };
1768
1778
  }
1769
1779
  const remainingRaw = raw.toolRemaining;
1770
1780
  if (remainingRaw !== void 0) {
@@ -1772,27 +1782,31 @@ function normalizeVoicePack(raw, onWarning = () => {}) {
1772
1782
  if (pool !== void 0 && pool.length > 0) overrides.toolRemaining = pool;
1773
1783
  }
1774
1784
  const whispersRaw = raw.whispers;
1775
- if (whispersRaw !== void 0) if (!isRecord$3(whispersRaw)) onWarning("whispers must be an object");
1776
- else {
1777
- for (const key of Object.keys(whispersRaw)) if (key === "generic" || key === "rules") onWarning("whispers." + key + " is no longer supported and was ignored");
1778
- else if (!WHISPER_KEYS.has(key)) onWarning("unknown whispers field " + key + " ignored");
1779
- const categories = normalizeWhisperCategories(whispersRaw.categories, onWarning);
1780
- const results = normalizeWhisperResults(whispersRaw.results, onWarning);
1781
- if (categories !== void 0 || results !== void 0) overrides.whispers = {
1782
- ...categories === void 0 ? {} : { categories },
1783
- ...results === void 0 ? {} : { results }
1784
- };
1785
+ if (whispersRaw !== void 0) {
1786
+ if (!isRecord$3(whispersRaw)) onWarning("whispers must be an object");
1787
+ else {
1788
+ for (const key of Object.keys(whispersRaw)) if (key === "generic" || key === "rules") onWarning("whispers." + key + " is no longer supported and was ignored");
1789
+ else if (!WHISPER_KEYS.has(key)) onWarning("unknown whispers field " + key + " ignored");
1790
+ const categories = normalizeWhisperCategories(whispersRaw.categories, onWarning);
1791
+ const results = normalizeWhisperResults(whispersRaw.results, onWarning);
1792
+ if (categories !== void 0 || results !== void 0) overrides.whispers = {
1793
+ ...categories === void 0 ? {} : { categories },
1794
+ ...results === void 0 ? {} : { results }
1795
+ };
1796
+ }
1785
1797
  }
1786
1798
  const panel = raw.panel === void 0 ? void 0 : normalizePanel(raw.panel, onWarning);
1787
1799
  const remarks = raw.remarks === void 0 ? void 0 : normalizePetRemarks(raw.remarks, onWarning);
1788
1800
  const ranksRaw = raw.ranks;
1789
1801
  let ranks;
1790
- if (ranksRaw !== void 0) if (!isRecord$3(ranksRaw)) onWarning("ranks must be an object");
1791
- else {
1792
- const out = {};
1793
- for (const [key, value] of Object.entries(ranksRaw)) if (typeof value === "string" && value.trim() !== "") out[key] = value.trim().slice(0, 80);
1794
- else onWarning("invalid rank name for " + key);
1795
- if (Object.keys(out).length > 0) ranks = out;
1802
+ if (ranksRaw !== void 0) {
1803
+ if (!isRecord$3(ranksRaw)) onWarning("ranks must be an object");
1804
+ else {
1805
+ const out = {};
1806
+ for (const [key, value] of Object.entries(ranksRaw)) if (typeof value === "string" && value.trim() !== "") out[key] = value.trim().slice(0, 80);
1807
+ else onWarning("invalid rank name for " + key);
1808
+ if (Object.keys(out).length > 0) ranks = out;
1809
+ }
1796
1810
  }
1797
1811
  if (overrides.status === void 0 && overrides.tools === void 0 && overrides.toolRemaining === void 0 && overrides.whispers === void 0 && panel === void 0 && remarks === void 0 && ranks === void 0) return;
1798
1812
  return {
@@ -1864,6 +1878,13 @@ function mergeVoicePacks(...layers) {
1864
1878
  }
1865
1879
  //#endregion
1866
1880
  //#region src/gameplay.ts
1881
+ /** Every roam direction, in roll order (equal chance each unless restricted). */
1882
+ const PET_ROAM_DIRECTIONS = [
1883
+ "up",
1884
+ "down",
1885
+ "left",
1886
+ "right"
1887
+ ];
1867
1888
  const KEBAB = /^[a-z0-9][a-z0-9-]*$/;
1868
1889
  const MAX_STATS = 16;
1869
1890
  const MAX_ZONES = 8;
@@ -1873,6 +1894,8 @@ const MAX_SHOP_ITEMS = 32;
1873
1894
  const MAX_LOTTERY_TIERS = 16;
1874
1895
  const MAX_PHRASES = 64;
1875
1896
  const PHRASE_MAX_LENGTH = 120;
1897
+ const MAX_MODES = 8;
1898
+ const MODE_LABEL_MAX_LENGTH = 40;
1876
1899
  const STAT_VALUE_MAX = 1e6;
1877
1900
  const CURRENCY_MAX = 9999999;
1878
1901
  const KNOWN_GAMEPLAY = /* @__PURE__ */ new Set([
@@ -1882,6 +1905,8 @@ const KNOWN_GAMEPLAY = /* @__PURE__ */ new Set([
1882
1905
  "touch",
1883
1906
  "work",
1884
1907
  "sleep",
1908
+ "modes",
1909
+ "roam",
1885
1910
  "passiveIncome",
1886
1911
  "shop",
1887
1912
  "dragState",
@@ -1928,6 +1953,26 @@ const KNOWN_SLEEP = /* @__PURE__ */ new Set([
1928
1953
  "wakeState",
1929
1954
  "restore"
1930
1955
  ]);
1956
+ const KNOWN_MODE = /* @__PURE__ */ new Set([
1957
+ "state",
1958
+ "label",
1959
+ "activeLabel",
1960
+ "restore"
1961
+ ]);
1962
+ const KNOWN_ROAM = /* @__PURE__ */ new Set([
1963
+ "state",
1964
+ "intervalMs",
1965
+ "probability",
1966
+ "distanceMin",
1967
+ "distanceMax",
1968
+ "speed",
1969
+ "directions"
1970
+ ]);
1971
+ const KNOWN_RESTORE = /* @__PURE__ */ new Set([
1972
+ "stat",
1973
+ "amount",
1974
+ "intervalMs"
1975
+ ]);
1931
1976
  const KNOWN_SHOP_ITEM = /* @__PURE__ */ new Set([
1932
1977
  "id",
1933
1978
  "label",
@@ -2024,6 +2069,25 @@ function parseStateRef(raw, field, hooks) {
2024
2069
  }
2025
2070
  return raw;
2026
2071
  }
2072
+ /** Parse one mode restore rule "{ stat (declared), amount, intervalMs }". */
2073
+ function parseModeRestore(raw, field, stats, fail) {
2074
+ if (!isRecord$2(raw)) {
2075
+ fail(field + " must be an object { stat, amount, intervalMs }");
2076
+ return;
2077
+ }
2078
+ const extra = unknownKeys$2(raw, KNOWN_RESTORE);
2079
+ if (extra.length > 0) fail(field + ": unknown field(s) " + extra.map((k) => JSON.stringify(k)).join(", "));
2080
+ const stat = typeof raw.stat === "string" && stats[raw.stat] !== void 0 ? raw.stat : void 0;
2081
+ if (stat === void 0) fail(field + ".stat must reference a declared stat");
2082
+ if (!intIn(raw.amount, 1, 1e3)) fail(field + ".amount must be an integer in [1, 1000]");
2083
+ if (!intIn(raw.intervalMs, 1e3, 6e5)) fail(field + ".intervalMs must be an integer in [1000, 600000]");
2084
+ if (stat === void 0 || !intIn(raw.amount, 1, 1e3) || !intIn(raw.intervalMs, 1e3, 6e5)) return void 0;
2085
+ return {
2086
+ stat,
2087
+ amount: raw.amount,
2088
+ intervalMs: raw.intervalMs
2089
+ };
2090
+ }
2027
2091
  /**
2028
2092
  * Validate the manifest 'gameplay' block (fail-closed). Only frames2d pets
2029
2093
  * may declare gameplay today: every state reference checks against the
@@ -2043,72 +2107,80 @@ function parseGameplayManifest(raw, hooks) {
2043
2107
  error(message);
2044
2108
  };
2045
2109
  const stats = {};
2046
- if (raw.stats !== void 0) if (!isRecord$2(raw.stats)) fail("gameplay.stats must be an object keyed by stat id");
2047
- else {
2048
- const entries = Object.entries(raw.stats);
2049
- if (entries.length > MAX_STATS) fail("gameplay.stats declares too many stats (max 16)");
2050
- for (const [name, value] of entries) {
2051
- if (!validName(name, 24)) {
2052
- fail("gameplay.stats: invalid stat id " + JSON.stringify(name));
2053
- continue;
2054
- }
2055
- if (!isRecord$2(value)) {
2056
- fail("gameplay.stats." + name + " must be an object");
2057
- continue;
2058
- }
2059
- const statExtra = unknownKeys$2(value, KNOWN_STAT);
2060
- if (statExtra.length > 0) fail("gameplay.stats." + name + ": unknown field(s) " + statExtra.map((k) => JSON.stringify(k)).join(", "));
2061
- if (!intIn(value.max, 1, STAT_VALUE_MAX)) {
2062
- fail("gameplay.stats." + name + ".max must be an integer in [1, 1000000]");
2063
- continue;
2110
+ if (raw.stats !== void 0) {
2111
+ if (!isRecord$2(raw.stats)) fail("gameplay.stats must be an object keyed by stat id");
2112
+ else {
2113
+ const entries = Object.entries(raw.stats);
2114
+ if (entries.length > MAX_STATS) fail("gameplay.stats declares too many stats (max 16)");
2115
+ for (const [name, value] of entries) {
2116
+ if (!validName(name, 24)) {
2117
+ fail("gameplay.stats: invalid stat id " + JSON.stringify(name));
2118
+ continue;
2119
+ }
2120
+ if (!isRecord$2(value)) {
2121
+ fail("gameplay.stats." + name + " must be an object");
2122
+ continue;
2123
+ }
2124
+ const statExtra = unknownKeys$2(value, KNOWN_STAT);
2125
+ if (statExtra.length > 0) fail("gameplay.stats." + name + ": unknown field(s) " + statExtra.map((k) => JSON.stringify(k)).join(", "));
2126
+ if (!intIn(value.max, 1, STAT_VALUE_MAX)) {
2127
+ fail("gameplay.stats." + name + ".max must be an integer in [1, 1000000]");
2128
+ continue;
2129
+ }
2130
+ const def = { max: value.max };
2131
+ if (value.initial !== void 0) {
2132
+ if (!numIn(value.initial, 0, value.max)) fail("gameplay.stats." + name + ".initial must be within [0, max]");
2133
+ else def.initial = value.initial;
2134
+ }
2135
+ for (const key of [
2136
+ "decayPerMinute",
2137
+ "workingDecayPerMinute",
2138
+ "idleDecayPerMinute"
2139
+ ]) if (value[key] !== void 0) {
2140
+ if (!numIn(value[key], 0, 1e3)) fail("gameplay.stats." + name + "." + key + " must be a number in [0, 1000]");
2141
+ else def[key] = value[key];
2142
+ }
2143
+ stats[name] = def;
2064
2144
  }
2065
- const def = { max: value.max };
2066
- if (value.initial !== void 0) if (!numIn(value.initial, 0, value.max)) fail("gameplay.stats." + name + ".initial must be within [0, max]");
2067
- else def.initial = value.initial;
2068
- for (const key of [
2069
- "decayPerMinute",
2070
- "workingDecayPerMinute",
2071
- "idleDecayPerMinute"
2072
- ]) if (value[key] !== void 0) if (!numIn(value[key], 0, 1e3)) fail("gameplay.stats." + name + "." + key + " must be a number in [0, 1000]");
2073
- else def[key] = value[key];
2074
- stats[name] = def;
2075
2145
  }
2076
2146
  }
2077
2147
  const block = {};
2078
2148
  if (Object.keys(stats).length > 0) block.stats = stats;
2079
- if (raw.idleDirector !== void 0) if (!isRecord$2(raw.idleDirector) || !Array.isArray(raw.idleDirector.acts)) fail("gameplay.idleDirector must be an object with an acts array");
2080
- else {
2081
- const d = raw.idleDirector;
2082
- const dExtra = unknownKeys$2(d, KNOWN_IDLE_DIRECTOR);
2083
- if (dExtra.length > 0) fail("gameplay.idleDirector: unknown field(s) " + dExtra.map((k) => JSON.stringify(k)).join(", "));
2084
- if (d.intervalMs !== void 0 && !intIn(d.intervalMs, 1e3, 6e4)) fail("gameplay.idleDirector.intervalMs must be an integer in [1000, 60000]");
2085
- if (d.maxMiss !== void 0 && !intIn(d.maxMiss, 0, 10)) fail("gameplay.idleDirector.maxMiss must be an integer in [0, 10]");
2086
- if (d.idleWeight !== void 0 && !intIn(d.idleWeight, 0, 1e4)) fail("gameplay.idleDirector.idleWeight must be an integer in [0, 10000]");
2087
- if (d.acts.length === 0 || d.acts.length > MAX_ACTS) fail("gameplay.idleDirector.acts must declare 1..16 acts");
2088
- const acts = [];
2089
- for (const act of d.acts) {
2090
- if (!isRecord$2(act) || !intIn(act.weight, 1, 1e4)) {
2091
- fail("gameplay.idleDirector.acts entries need a weight integer in [1, 10000]");
2092
- continue;
2149
+ if (raw.idleDirector !== void 0) {
2150
+ if (!isRecord$2(raw.idleDirector) || !Array.isArray(raw.idleDirector.acts)) fail("gameplay.idleDirector must be an object with an acts array");
2151
+ else {
2152
+ const d = raw.idleDirector;
2153
+ const dExtra = unknownKeys$2(d, KNOWN_IDLE_DIRECTOR);
2154
+ if (dExtra.length > 0) fail("gameplay.idleDirector: unknown field(s) " + dExtra.map((k) => JSON.stringify(k)).join(", "));
2155
+ if (d.intervalMs !== void 0 && !intIn(d.intervalMs, 1e3, 6e4)) fail("gameplay.idleDirector.intervalMs must be an integer in [1000, 60000]");
2156
+ if (d.maxMiss !== void 0 && !intIn(d.maxMiss, 0, 10)) fail("gameplay.idleDirector.maxMiss must be an integer in [0, 10]");
2157
+ if (d.idleWeight !== void 0 && !intIn(d.idleWeight, 0, 1e4)) fail("gameplay.idleDirector.idleWeight must be an integer in [0, 10000]");
2158
+ if (d.acts.length === 0 || d.acts.length > MAX_ACTS) fail("gameplay.idleDirector.acts must declare 1..16 acts");
2159
+ const acts = [];
2160
+ for (const act of d.acts) {
2161
+ if (!isRecord$2(act) || !intIn(act.weight, 1, 1e4)) {
2162
+ fail("gameplay.idleDirector.acts entries need a weight integer in [1, 10000]");
2163
+ continue;
2164
+ }
2165
+ const aExtra = unknownKeys$2(act, KNOWN_ACT);
2166
+ if (aExtra.length > 0) fail("gameplay.idleDirector.acts: unknown field(s) " + aExtra.map((k) => JSON.stringify(k)).join(", "));
2167
+ const track = parseStateRef(act.track, "gameplay.idleDirector.acts.track", hooks);
2168
+ if (track === void 0) continue;
2169
+ const entry = {
2170
+ track,
2171
+ weight: act.weight
2172
+ };
2173
+ const phrases = parsePhrases(act.phrases, "gameplay.idleDirector.acts.phrases", hooks);
2174
+ if (phrases !== void 0) entry.phrases = phrases;
2175
+ acts.push(entry);
2093
2176
  }
2094
- const aExtra = unknownKeys$2(act, KNOWN_ACT);
2095
- if (aExtra.length > 0) fail("gameplay.idleDirector.acts: unknown field(s) " + aExtra.map((k) => JSON.stringify(k)).join(", "));
2096
- const track = parseStateRef(act.track, "gameplay.idleDirector.acts.track", hooks);
2097
- if (track === void 0) continue;
2098
- const entry = {
2099
- track,
2100
- weight: act.weight
2177
+ if (acts.length > 0) block.idleDirector = {
2178
+ intervalMs: intIn(d.intervalMs, 1e3, 6e4) ? d.intervalMs : 5e3,
2179
+ maxMiss: intIn(d.maxMiss, 0, 10) ? d.maxMiss : 2,
2180
+ idleWeight: intIn(d.idleWeight, 0, 1e4) ? d.idleWeight : 0,
2181
+ acts
2101
2182
  };
2102
- const phrases = parsePhrases(act.phrases, "gameplay.idleDirector.acts.phrases", hooks);
2103
- if (phrases !== void 0) entry.phrases = phrases;
2104
- acts.push(entry);
2105
2183
  }
2106
- if (acts.length > 0) block.idleDirector = {
2107
- intervalMs: intIn(d.intervalMs, 1e3, 6e4) ? d.intervalMs : 5e3,
2108
- maxMiss: intIn(d.maxMiss, 0, 10) ? d.maxMiss : 2,
2109
- idleWeight: intIn(d.idleWeight, 0, 1e4) ? d.idleWeight : 0,
2110
- acts
2111
- };
2112
2184
  }
2113
2185
  if (raw.hitBox !== void 0) {
2114
2186
  const b = raw.hitBox;
@@ -2120,66 +2192,70 @@ function parseGameplayManifest(raw, hooks) {
2120
2192
  y1: b.y1
2121
2193
  };
2122
2194
  }
2123
- if (raw.touch !== void 0) if (!isRecord$2(raw.touch) || !Array.isArray(raw.touch.zones)) fail("gameplay.touch must be an object with a zones array");
2124
- else {
2125
- const tExtra = unknownKeys$2(raw.touch, KNOWN_TOUCH);
2126
- if (tExtra.length > 0) fail("gameplay.touch: unknown field(s) " + tExtra.map((k) => JSON.stringify(k)).join(", "));
2127
- let clickBoost;
2128
- if (raw.touch.clickBoost !== void 0) {
2129
- const cb = raw.touch.clickBoost;
2130
- if (!isRecord$2(cb) || typeof cb.stat !== "string" || stats[cb.stat] === void 0 || !intIn(cb.min, 0, 1e3) || !intIn(cb.max, 0, 1e3) || cb.min > cb.max) fail("gameplay.touch.clickBoost must be { stat (declared), min, max } integers with 0 <= min <= max <= 1000");
2131
- else clickBoost = {
2132
- stat: cb.stat,
2133
- min: cb.min,
2134
- max: cb.max
2135
- };
2136
- }
2137
- const zones = [];
2138
- if (raw.touch.zones.length === 0 || raw.touch.zones.length > MAX_ZONES) fail("gameplay.touch.zones must declare 1..8 zones");
2139
- for (const zoneRaw of raw.touch.zones) {
2140
- if (!isRecord$2(zoneRaw) || !validName(zoneRaw.name) || !numIn(zoneRaw.y0, 0, 1) || !numIn(zoneRaw.y1, 0, 1) || !(zoneRaw.y0 < zoneRaw.y1)) {
2141
- fail("gameplay.touch.zones entries need a kebab name and 0 <= y0 < y1 <= 1");
2142
- continue;
2143
- }
2144
- const zExtra = unknownKeys$2(zoneRaw, KNOWN_ZONE);
2145
- if (zExtra.length > 0) fail("gameplay.touch." + zoneRaw.name + ": unknown field(s) " + zExtra.map((k) => JSON.stringify(k)).join(", "));
2146
- if (!Array.isArray(zoneRaw.branches) || zoneRaw.branches.length === 0 || zoneRaw.branches.length > MAX_BRANCHES) {
2147
- fail("gameplay.touch." + zoneRaw.name + ".branches must declare 1..8 branches");
2148
- continue;
2195
+ if (raw.touch !== void 0) {
2196
+ if (!isRecord$2(raw.touch) || !Array.isArray(raw.touch.zones)) fail("gameplay.touch must be an object with a zones array");
2197
+ else {
2198
+ const tExtra = unknownKeys$2(raw.touch, KNOWN_TOUCH);
2199
+ if (tExtra.length > 0) fail("gameplay.touch: unknown field(s) " + tExtra.map((k) => JSON.stringify(k)).join(", "));
2200
+ let clickBoost;
2201
+ if (raw.touch.clickBoost !== void 0) {
2202
+ const cb = raw.touch.clickBoost;
2203
+ if (!isRecord$2(cb) || typeof cb.stat !== "string" || stats[cb.stat] === void 0 || !intIn(cb.min, 0, 1e3) || !intIn(cb.max, 0, 1e3) || cb.min > cb.max) fail("gameplay.touch.clickBoost must be { stat (declared), min, max } integers with 0 <= min <= max <= 1000");
2204
+ else clickBoost = {
2205
+ stat: cb.stat,
2206
+ min: cb.min,
2207
+ max: cb.max
2208
+ };
2149
2209
  }
2150
- let probabilitySum = 0;
2151
- const branches = [];
2152
- for (const branchRaw of zoneRaw.branches) {
2153
- if (!isRecord$2(branchRaw) || !numIn(branchRaw.probability, 0, 1) || branchRaw.probability === 0) {
2154
- fail("gameplay.touch." + zoneRaw.name + ".branches entries need a probability in (0, 1]");
2210
+ const zones = [];
2211
+ if (raw.touch.zones.length === 0 || raw.touch.zones.length > MAX_ZONES) fail("gameplay.touch.zones must declare 1..8 zones");
2212
+ for (const zoneRaw of raw.touch.zones) {
2213
+ if (!isRecord$2(zoneRaw) || !validName(zoneRaw.name) || !numIn(zoneRaw.y0, 0, 1) || !numIn(zoneRaw.y1, 0, 1) || !(zoneRaw.y0 < zoneRaw.y1)) {
2214
+ fail("gameplay.touch.zones entries need a kebab name and 0 <= y0 < y1 <= 1");
2155
2215
  continue;
2156
2216
  }
2157
- const bExtra = unknownKeys$2(branchRaw, KNOWN_BRANCH);
2158
- if (bExtra.length > 0) fail("gameplay.touch." + zoneRaw.name + ": unknown branch field(s) " + bExtra.map((k) => JSON.stringify(k)).join(", "));
2159
- probabilitySum += branchRaw.probability;
2160
- const branch = { probability: branchRaw.probability };
2161
- const effects = parseEffects(branchRaw.effects, "gameplay.touch." + zoneRaw.name + ".effects", stats, hooks);
2162
- if (effects !== void 0) branch.effects = effects;
2163
- const state = parseStateRef(branchRaw.state, "gameplay.touch." + zoneRaw.name + ".state", hooks);
2164
- if (state !== void 0) branch.state = state;
2165
- if (branchRaw.stateMs !== void 0) if (!intIn(branchRaw.stateMs, 200, 1e4)) fail("gameplay.touch." + zoneRaw.name + ".stateMs must be an integer in [200, 10000]");
2166
- else branch.stateMs = branchRaw.stateMs;
2167
- const phrases = parsePhrases(branchRaw.phrases, "gameplay.touch." + zoneRaw.name + ".phrases", hooks);
2168
- if (phrases !== void 0) branch.phrases = phrases;
2169
- branches.push(branch);
2217
+ const zExtra = unknownKeys$2(zoneRaw, KNOWN_ZONE);
2218
+ if (zExtra.length > 0) fail("gameplay.touch." + zoneRaw.name + ": unknown field(s) " + zExtra.map((k) => JSON.stringify(k)).join(", "));
2219
+ if (!Array.isArray(zoneRaw.branches) || zoneRaw.branches.length === 0 || zoneRaw.branches.length > MAX_BRANCHES) {
2220
+ fail("gameplay.touch." + zoneRaw.name + ".branches must declare 1..8 branches");
2221
+ continue;
2222
+ }
2223
+ let probabilitySum = 0;
2224
+ const branches = [];
2225
+ for (const branchRaw of zoneRaw.branches) {
2226
+ if (!isRecord$2(branchRaw) || !numIn(branchRaw.probability, 0, 1) || branchRaw.probability === 0) {
2227
+ fail("gameplay.touch." + zoneRaw.name + ".branches entries need a probability in (0, 1]");
2228
+ continue;
2229
+ }
2230
+ const bExtra = unknownKeys$2(branchRaw, KNOWN_BRANCH);
2231
+ if (bExtra.length > 0) fail("gameplay.touch." + zoneRaw.name + ": unknown branch field(s) " + bExtra.map((k) => JSON.stringify(k)).join(", "));
2232
+ probabilitySum += branchRaw.probability;
2233
+ const branch = { probability: branchRaw.probability };
2234
+ const effects = parseEffects(branchRaw.effects, "gameplay.touch." + zoneRaw.name + ".effects", stats, hooks);
2235
+ if (effects !== void 0) branch.effects = effects;
2236
+ const state = parseStateRef(branchRaw.state, "gameplay.touch." + zoneRaw.name + ".state", hooks);
2237
+ if (state !== void 0) branch.state = state;
2238
+ if (branchRaw.stateMs !== void 0) {
2239
+ if (!intIn(branchRaw.stateMs, 200, 1e4)) fail("gameplay.touch." + zoneRaw.name + ".stateMs must be an integer in [200, 10000]");
2240
+ else branch.stateMs = branchRaw.stateMs;
2241
+ }
2242
+ const phrases = parsePhrases(branchRaw.phrases, "gameplay.touch." + zoneRaw.name + ".phrases", hooks);
2243
+ if (phrases !== void 0) branch.phrases = phrases;
2244
+ branches.push(branch);
2245
+ }
2246
+ if (probabilitySum > 1 + 1e-9) fail("gameplay.touch." + zoneRaw.name + ": branch probabilities must sum to at most 1");
2247
+ if (branches.length > 0) zones.push({
2248
+ name: zoneRaw.name,
2249
+ y0: zoneRaw.y0,
2250
+ y1: zoneRaw.y1,
2251
+ branches
2252
+ });
2170
2253
  }
2171
- if (probabilitySum > 1.000000001) fail("gameplay.touch." + zoneRaw.name + ": branch probabilities must sum to at most 1");
2172
- if (branches.length > 0) zones.push({
2173
- name: zoneRaw.name,
2174
- y0: zoneRaw.y0,
2175
- y1: zoneRaw.y1,
2176
- branches
2177
- });
2254
+ if (zones.length > 0 || clickBoost !== void 0) block.touch = {
2255
+ zones,
2256
+ ...clickBoost === void 0 ? {} : { clickBoost }
2257
+ };
2178
2258
  }
2179
- if (zones.length > 0 || clickBoost !== void 0) block.touch = {
2180
- zones,
2181
- ...clickBoost === void 0 ? {} : { clickBoost }
2182
- };
2183
2259
  }
2184
2260
  if (raw.work !== void 0) {
2185
2261
  const w = raw.work;
@@ -2200,15 +2276,19 @@ function parseGameplayManifest(raw, hooks) {
2200
2276
  tickMs: w.tickMs,
2201
2277
  successProbability: w.successProbability
2202
2278
  };
2203
- if (w.resultMs !== void 0) if (!isRecord$2(w.resultMs) || !intIn(w.resultMs.success, 200, 1e4) || !intIn(w.resultMs.fail, 200, 1e4)) fail("gameplay.work.resultMs must be { success, fail } integers in [200, 10000]");
2204
- else work.resultMs = {
2205
- success: w.resultMs.success,
2206
- fail: w.resultMs.fail
2207
- };
2208
- for (const key of ["success", "fail"]) if (w[key] !== void 0) if (!isRecord$2(w[key])) fail("gameplay.work." + key + " must be an object { effects }");
2209
- else {
2210
- const effects = parseEffects(w[key].effects, "gameplay.work." + key + ".effects", stats, hooks);
2211
- if (effects !== void 0) work[key] = { effects };
2279
+ if (w.resultMs !== void 0) {
2280
+ if (!isRecord$2(w.resultMs) || !intIn(w.resultMs.success, 200, 1e4) || !intIn(w.resultMs.fail, 200, 1e4)) fail("gameplay.work.resultMs must be { success, fail } integers in [200, 10000]");
2281
+ else work.resultMs = {
2282
+ success: w.resultMs.success,
2283
+ fail: w.resultMs.fail
2284
+ };
2285
+ }
2286
+ for (const key of ["success", "fail"]) if (w[key] !== void 0) {
2287
+ if (!isRecord$2(w[key])) fail("gameplay.work." + key + " must be an object { effects }");
2288
+ else {
2289
+ const effects = parseEffects(w[key].effects, "gameplay.work." + key + ".effects", stats, hooks);
2290
+ if (effects !== void 0) work[key] = { effects };
2291
+ }
2212
2292
  }
2213
2293
  block.work = work;
2214
2294
  }
@@ -2222,18 +2302,101 @@ function parseGameplayManifest(raw, hooks) {
2222
2302
  if (sExtra.length > 0) fail("gameplay.sleep: unknown field(s) " + sExtra.map((k) => JSON.stringify(k)).join(", "));
2223
2303
  const state = parseStateRef(s.state, "gameplay.sleep.state", hooks);
2224
2304
  const wakeState = parseStateRef(s.wakeState, "gameplay.sleep.wakeState", hooks);
2225
- const restoreStat = typeof s.restore.stat === "string" && stats[s.restore.stat] !== void 0 ? s.restore.stat : void 0;
2226
- if (restoreStat === void 0) fail("gameplay.sleep.restore.stat must reference a declared stat");
2227
- if (!intIn(s.restore.amount, 1, 1e3)) fail("gameplay.sleep.restore.amount must be an integer in [1, 1000]");
2228
- if (!intIn(s.restore.intervalMs, 1e3, 6e5)) fail("gameplay.sleep.restore.intervalMs must be an integer in [1000, 600000]");
2229
- if (state !== void 0 && restoreStat !== void 0 && intIn(s.restore.amount, 1, 1e3) && intIn(s.restore.intervalMs, 1e3, 6e5)) block.sleep = {
2305
+ const restore = parseModeRestore(s.restore, "gameplay.sleep.restore", stats, fail);
2306
+ if (state !== void 0 && restore !== void 0) block.sleep = {
2230
2307
  state,
2231
2308
  ...wakeState === void 0 ? {} : { wakeState },
2232
- restore: {
2233
- stat: restoreStat,
2234
- amount: s.restore.amount,
2235
- intervalMs: s.restore.intervalMs
2309
+ restore
2310
+ };
2311
+ }
2312
+ }
2313
+ if (raw.modes !== void 0) {
2314
+ if (!isRecord$2(raw.modes)) fail("gameplay.modes must be an object keyed by mode id");
2315
+ else {
2316
+ const entries = Object.entries(raw.modes);
2317
+ if (entries.length > MAX_MODES) fail("gameplay.modes declares too many modes (max 8)");
2318
+ const modes = {};
2319
+ for (const [name, value] of entries) {
2320
+ if (!validName(name, 24) || name === "work" || name === "sleep") {
2321
+ fail("gameplay.modes: invalid mode id " + JSON.stringify(name) + " (kebab, and not the reserved work/sleep)");
2322
+ continue;
2323
+ }
2324
+ if (!isRecord$2(value)) {
2325
+ fail("gameplay.modes." + name + " must be an object");
2326
+ continue;
2236
2327
  }
2328
+ const mExtra = unknownKeys$2(value, KNOWN_MODE);
2329
+ if (mExtra.length > 0) fail("gameplay.modes." + name + ": unknown field(s) " + mExtra.map((k) => JSON.stringify(k)).join(", "));
2330
+ const state = parseStateRef(value.state, "gameplay.modes." + name + ".state", hooks);
2331
+ let usable = state !== void 0;
2332
+ let label;
2333
+ if (value.label !== void 0) {
2334
+ if (typeof value.label !== "string" || value.label.trim() === "" || value.label.length > MODE_LABEL_MAX_LENGTH) {
2335
+ fail("gameplay.modes." + name + ".label must be a non-empty string of at most 40 chars");
2336
+ usable = false;
2337
+ } else label = value.label.trim();
2338
+ }
2339
+ let activeLabel;
2340
+ if (value.activeLabel !== void 0) {
2341
+ if (typeof value.activeLabel !== "string" || value.activeLabel.trim() === "" || value.activeLabel.length > MODE_LABEL_MAX_LENGTH) {
2342
+ fail("gameplay.modes." + name + ".activeLabel must be a non-empty string of at most 40 chars");
2343
+ usable = false;
2344
+ } else activeLabel = value.activeLabel.trim();
2345
+ }
2346
+ const restore = value.restore === void 0 ? void 0 : parseModeRestore(value.restore, "gameplay.modes." + name + ".restore", stats, fail);
2347
+ if (value.restore !== void 0 && restore === void 0) usable = false;
2348
+ if (usable && state !== void 0) modes[name] = {
2349
+ state,
2350
+ ...label === void 0 ? {} : { label },
2351
+ ...activeLabel === void 0 ? {} : { activeLabel },
2352
+ ...restore === void 0 ? {} : { restore }
2353
+ };
2354
+ }
2355
+ if (Object.keys(modes).length > 0) block.modes = modes;
2356
+ }
2357
+ }
2358
+ if (raw.roam !== void 0) {
2359
+ const rm = raw.roam;
2360
+ if (!isRecord$2(rm)) fail("gameplay.roam must be an object");
2361
+ else {
2362
+ const rExtra = unknownKeys$2(rm, KNOWN_ROAM);
2363
+ if (rExtra.length > 0) fail("gameplay.roam: unknown field(s) " + rExtra.map((k) => JSON.stringify(k)).join(", "));
2364
+ const state = parseStateRef(rm.state, "gameplay.roam.state", hooks);
2365
+ if (!intIn(rm.intervalMs, 1e3, 36e5)) fail("gameplay.roam.intervalMs must be an integer in [1000, 3600000]");
2366
+ if (!numIn(rm.probability, 0, 1) || rm.probability === 0) fail("gameplay.roam.probability must be a number in (0, 1]");
2367
+ if (!intIn(rm.distanceMin, 1, 2e3)) fail("gameplay.roam.distanceMin must be an integer in [1, 2000]");
2368
+ if (!intIn(rm.distanceMax, 1, 2e3)) fail("gameplay.roam.distanceMax must be an integer in [1, 2000]");
2369
+ if (intIn(rm.distanceMin, 1, 2e3) && intIn(rm.distanceMax, 1, 2e3) && rm.distanceMin > rm.distanceMax) fail("gameplay.roam.distanceMin must not exceed distanceMax");
2370
+ if (!intIn(rm.speed, 1, 2e3)) fail("gameplay.roam.speed must be an integer in [1, 2000] (px per second)");
2371
+ let directions;
2372
+ if (rm.directions !== void 0) {
2373
+ if (!Array.isArray(rm.directions) || rm.directions.length === 0) fail("gameplay.roam.directions must be a non-empty array of up, down, left, right");
2374
+ else {
2375
+ const picked = [];
2376
+ for (const entry of rm.directions) {
2377
+ if (typeof entry !== "string" || !PET_ROAM_DIRECTIONS.includes(entry)) {
2378
+ fail("gameplay.roam.directions entries must be up, down, left or right");
2379
+ picked.length = 0;
2380
+ break;
2381
+ }
2382
+ if (picked.includes(entry)) {
2383
+ fail("gameplay.roam.directions must not repeat " + entry);
2384
+ picked.length = 0;
2385
+ break;
2386
+ }
2387
+ picked.push(entry);
2388
+ }
2389
+ if (picked.length > 0) directions = picked;
2390
+ }
2391
+ }
2392
+ if (state !== void 0 && intIn(rm.intervalMs, 1e3, 36e5) && numIn(rm.probability, 0, 1) && rm.probability !== 0 && intIn(rm.distanceMin, 1, 2e3) && intIn(rm.distanceMax, 1, 2e3) && rm.distanceMin <= rm.distanceMax && intIn(rm.speed, 1, 2e3)) block.roam = {
2393
+ state,
2394
+ intervalMs: rm.intervalMs,
2395
+ probability: rm.probability,
2396
+ distanceMin: rm.distanceMin,
2397
+ distanceMax: rm.distanceMax,
2398
+ speed: rm.speed,
2399
+ ...directions === void 0 ? {} : { directions }
2237
2400
  };
2238
2401
  }
2239
2402
  }
@@ -2283,8 +2446,10 @@ function parseGameplayManifest(raw, hooks) {
2283
2446
  price: itemRaw.price,
2284
2447
  currency: itemRaw.currency
2285
2448
  };
2286
- if (itemRaw.image !== void 0) if (typeof itemRaw.image !== "string" || itemRaw.image.includes("..") || itemRaw.image.includes("\\") || itemRaw.image.startsWith("/")) fail("gameplay.shop." + itemRaw.id + ".image must be a safe manifest-relative frame path");
2287
- else item.image = itemRaw.image;
2449
+ if (itemRaw.image !== void 0) {
2450
+ if (typeof itemRaw.image !== "string" || itemRaw.image.includes("..") || itemRaw.image.includes("\\") || itemRaw.image.startsWith("/")) fail("gameplay.shop." + itemRaw.id + ".image must be a safe manifest-relative frame path");
2451
+ else item.image = itemRaw.image;
2452
+ }
2288
2453
  const effects = parseEffects(itemRaw.effects, "gameplay.shop." + itemRaw.id + ".effects", stats, hooks);
2289
2454
  if (effects !== void 0) item.effects = effects;
2290
2455
  if (itemRaw.lottery !== void 0) {
@@ -2306,11 +2471,13 @@ function parseGameplayManifest(raw, hooks) {
2306
2471
  probability: tierRaw.probability,
2307
2472
  prize: tierRaw.prize
2308
2473
  };
2309
- if (tierRaw.currency !== void 0) if (!validName(tierRaw.currency, 24)) fail("gameplay.shop." + itemRaw.id + ".lottery tier currency must be a kebab id");
2310
- else tier.currency = tierRaw.currency;
2474
+ if (tierRaw.currency !== void 0) {
2475
+ if (!validName(tierRaw.currency, 24)) fail("gameplay.shop." + itemRaw.id + ".lottery tier currency must be a kebab id");
2476
+ else tier.currency = tierRaw.currency;
2477
+ }
2311
2478
  tiers.push(tier);
2312
2479
  }
2313
- if (tierSum > 1.000000001) fail("gameplay.shop." + itemRaw.id + ".lottery tier probabilities must sum to at most 1");
2480
+ if (tierSum > 1 + 1e-9) fail("gameplay.shop." + itemRaw.id + ".lottery tier probabilities must sum to at most 1");
2314
2481
  if (tiers.length > 0) {
2315
2482
  const lotteryEffects = parseEffects(l.effects, "gameplay.shop." + itemRaw.id + ".lottery.effects", stats, hooks);
2316
2483
  item.lottery = {
@@ -2343,6 +2510,29 @@ function parseGameplayManifest(raw, hooks) {
2343
2510
  }
2344
2511
  return failed ? void 0 : block;
2345
2512
  }
2513
+ /**
2514
+ * One declared extra mode, by OWN key only. A plain `modes[id]` lookup also
2515
+ * answers for Object.prototype members ('constructor', 'toString', …), which
2516
+ * would let a crafted mode id pass the "is it declared?" test.
2517
+ */
2518
+ function declaredModeOf(manifest, mode) {
2519
+ const modes = manifest.modes;
2520
+ if (modes === void 0 || !Object.prototype.hasOwnProperty.call(modes, mode)) return void 0;
2521
+ return modes[mode];
2522
+ }
2523
+ /** The restore rule of one active mode, when it declares one. */
2524
+ function modeRestoreOf(manifest, mode) {
2525
+ if (mode === null) return void 0;
2526
+ if (mode === "sleep") return manifest.sleep?.restore;
2527
+ if (mode === "work") return void 0;
2528
+ return declaredModeOf(manifest, mode)?.restore;
2529
+ }
2530
+ /** Whether the manifest still declares this gameplay mode id. */
2531
+ function isDeclaredMode(manifest, mode) {
2532
+ if (mode === "work") return manifest.work !== void 0;
2533
+ if (mode === "sleep") return manifest.sleep !== void 0;
2534
+ return declaredModeOf(manifest, mode) !== void 0;
2535
+ }
2346
2536
  /** Fresh state for one pet: stats at their initial (default max), no currency. */
2347
2537
  function initialGameplayState(manifest, now) {
2348
2538
  const stats = {};
@@ -2364,9 +2554,11 @@ function clampGameplay(state, manifest) {
2364
2554
  for (const [name, value] of Object.entries(state.currencies)) state.currencies[name] = Math.min(CURRENCY_MAX, Math.max(0, Math.floor(value)));
2365
2555
  }
2366
2556
  /**
2367
- * Lazy settle: apply stat decay, passive income and sleep restore for the
2368
- * elapsed wall time since the last settle. Mirrors the treats.ts discipline
2369
- * (no host timers; read paths settle). Returns whether anything changed.
2557
+ * Lazy settle: apply stat decay, passive income and the active mode's restore
2558
+ * rule (sleep restores energy, a declared extra mode restores whatever it
2559
+ * declares) for the elapsed wall time since the last settle. Mirrors the
2560
+ * treats.ts discipline (no host timers; read paths settle). Returns whether
2561
+ * anything changed.
2370
2562
  */
2371
2563
  function settleGameplay(state, manifest, now, options) {
2372
2564
  const elapsedMs = now - state.settledAt;
@@ -2397,14 +2589,14 @@ function settleGameplay(state, manifest, now, options) {
2397
2589
  changed = true;
2398
2590
  }
2399
2591
  }
2400
- if (state.mode === "sleep" && manifest.sleep !== void 0) {
2592
+ const restore = modeRestoreOf(manifest, state.mode);
2593
+ if (restore !== void 0) {
2401
2594
  const restoreElapsed = elapsedMs + (state.restoreCarryMs ?? 0);
2402
- const interval = manifest.sleep.restore.intervalMs;
2595
+ const interval = restore.intervalMs;
2403
2596
  const ticks = Math.floor(restoreElapsed / interval);
2404
2597
  state.restoreCarryMs = restoreElapsed % interval;
2405
2598
  if (ticks > 0) {
2406
- const stat = manifest.sleep.restore.stat;
2407
- state.stats[stat] = (state.stats[stat] ?? 0) + ticks * manifest.sleep.restore.amount;
2599
+ state.stats[restore.stat] = (state.stats[restore.stat] ?? 0) + ticks * restore.amount;
2408
2600
  changed = true;
2409
2601
  }
2410
2602
  } else state.restoreCarryMs = 0;
@@ -2649,16 +2841,26 @@ function parseSprite2dBlock(raw, diag) {
2649
2841
  const spritesheetPath = safeManifestPath(raw.spritesheetPath);
2650
2842
  if (spritesheetPath === void 0) diag.error("sprite2d.spritesheetPath must be a safe manifest-relative path");
2651
2843
  const block = { spritesheetPath: spritesheetPath ?? "" };
2652
- if (raw.cell !== void 0) if (!isRecord$1(raw.cell)) diag.error("sprite2d.cell must be an object { width?, height? }");
2653
- else block.cell = raw.cell;
2654
- if (raw.columns !== void 0) if (typeof raw.columns !== "number" || !Number.isInteger(raw.columns) || raw.columns < 1) diag.error("sprite2d.columns must be a positive integer");
2655
- else block.columns = raw.columns;
2656
- if (raw.atlasRows !== void 0) if (typeof raw.atlasRows !== "number" || !Number.isInteger(raw.atlasRows) || raw.atlasRows < 1) diag.error("sprite2d.atlasRows must be a positive integer");
2657
- else block.atlasRows = raw.atlasRows;
2658
- if (raw.frames !== void 0) if (!Array.isArray(raw.frames) || raw.frames.some((v) => typeof v !== "number" || !Number.isInteger(v) || v < 0)) diag.error("sprite2d.frames must be an array of non-negative integers");
2659
- else block.frames = raw.frames;
2660
- if (raw.tracks !== void 0) if (!isRecord$1(raw.tracks)) diag.error("sprite2d.tracks must be an object keyed by animation");
2661
- else block.tracks = raw.tracks;
2844
+ if (raw.cell !== void 0) {
2845
+ if (!isRecord$1(raw.cell)) diag.error("sprite2d.cell must be an object { width?, height? }");
2846
+ else block.cell = raw.cell;
2847
+ }
2848
+ if (raw.columns !== void 0) {
2849
+ if (typeof raw.columns !== "number" || !Number.isInteger(raw.columns) || raw.columns < 1) diag.error("sprite2d.columns must be a positive integer");
2850
+ else block.columns = raw.columns;
2851
+ }
2852
+ if (raw.atlasRows !== void 0) {
2853
+ if (typeof raw.atlasRows !== "number" || !Number.isInteger(raw.atlasRows) || raw.atlasRows < 1) diag.error("sprite2d.atlasRows must be a positive integer");
2854
+ else block.atlasRows = raw.atlasRows;
2855
+ }
2856
+ if (raw.frames !== void 0) {
2857
+ if (!Array.isArray(raw.frames) || raw.frames.some((v) => typeof v !== "number" || !Number.isInteger(v) || v < 0)) diag.error("sprite2d.frames must be an array of non-negative integers");
2858
+ else block.frames = raw.frames;
2859
+ }
2860
+ if (raw.tracks !== void 0) {
2861
+ if (!isRecord$1(raw.tracks)) diag.error("sprite2d.tracks must be an object keyed by animation");
2862
+ else block.tracks = raw.tracks;
2863
+ }
2662
2864
  return diag.hasErrors ? void 0 : block;
2663
2865
  }
2664
2866
  function parseLive2dBlock(raw, diag) {
@@ -2678,16 +2880,24 @@ function parseLive2dBlock(raw, diag) {
2678
2880
  model: model ?? "",
2679
2881
  motions: motions ?? { idle: "" }
2680
2882
  };
2681
- if (raw.scale !== void 0) if (typeof raw.scale !== "number" || !Number.isFinite(raw.scale) || raw.scale <= 0 || raw.scale > 10) diag.error("live2d.scale must be a number in (0, 10]");
2682
- else block.scale = raw.scale;
2683
- if (raw.translate !== void 0) if (!isRecord$1(raw.translate) || raw.translate.x !== void 0 && typeof raw.translate.x !== "number" || raw.translate.y !== void 0 && typeof raw.translate.y !== "number") diag.error("live2d.translate must be an object { x?: number, y?: number }");
2684
- else block.translate = raw.translate;
2883
+ if (raw.scale !== void 0) {
2884
+ if (typeof raw.scale !== "number" || !Number.isFinite(raw.scale) || raw.scale <= 0 || raw.scale > 10) diag.error("live2d.scale must be a number in (0, 10]");
2885
+ else block.scale = raw.scale;
2886
+ }
2887
+ if (raw.translate !== void 0) {
2888
+ if (!isRecord$1(raw.translate) || raw.translate.x !== void 0 && typeof raw.translate.x !== "number" || raw.translate.y !== void 0 && typeof raw.translate.y !== "number") diag.error("live2d.translate must be an object { x?: number, y?: number }");
2889
+ else block.translate = raw.translate;
2890
+ }
2685
2891
  const expressions = parsePhaseStringMap(raw.expressions, "live2d.expressions", diag);
2686
2892
  if (expressions !== void 0) block.expressions = expressions;
2687
- if (raw.hitAreas !== void 0) if (!Array.isArray(raw.hitAreas) || raw.hitAreas.some((v) => typeof v !== "string" || v.trim() === "")) diag.error("live2d.hitAreas must be an array of non-empty strings");
2688
- else block.hitAreas = raw.hitAreas;
2689
- if (raw.lipSync !== void 0) if (typeof raw.lipSync !== "boolean") diag.error("live2d.lipSync must be a boolean");
2690
- else block.lipSync = raw.lipSync;
2893
+ if (raw.hitAreas !== void 0) {
2894
+ if (!Array.isArray(raw.hitAreas) || raw.hitAreas.some((v) => typeof v !== "string" || v.trim() === "")) diag.error("live2d.hitAreas must be an array of non-empty strings");
2895
+ else block.hitAreas = raw.hitAreas;
2896
+ }
2897
+ if (raw.lipSync !== void 0) {
2898
+ if (typeof raw.lipSync !== "boolean") diag.error("live2d.lipSync must be a boolean");
2899
+ else block.lipSync = raw.lipSync;
2900
+ }
2691
2901
  return diag.hasErrors ? void 0 : block;
2692
2902
  }
2693
2903
  const FRAMES2D_TRACK_NAME = /^[a-z0-9][a-z0-9-]*$/;
@@ -2724,25 +2934,33 @@ function parseFrames2dTrack(raw, field, diag) {
2724
2934
  const extra = unknownKeys$1(raw, KNOWN_FRAMES2D_TRACK);
2725
2935
  if (extra.length > 0) diag.error(field + ": unknown field(s) " + extra.map((k) => JSON.stringify(k)).join(", "));
2726
2936
  const track = {};
2727
- if (raw.frames !== void 0) if (!Array.isArray(raw.frames) || raw.frames.length === 0) diag.error(field + ".frames must be a non-empty array of frame file names");
2728
- else if (raw.frames.length > FRAMES2D_MAX_FRAMES) diag.error(field + ".frames declares too many frames (" + raw.frames.length + ", max 64)");
2729
- else {
2730
- const frames = [];
2731
- for (const entry of raw.frames) {
2732
- const safe = safeFrameName(entry);
2733
- if (safe === void 0) diag.error(field + ".frames entry " + JSON.stringify(String(entry)) + " is not a safe image file name");
2734
- else frames.push(safe);
2937
+ if (raw.frames !== void 0) {
2938
+ if (!Array.isArray(raw.frames) || raw.frames.length === 0) diag.error(field + ".frames must be a non-empty array of frame file names");
2939
+ else if (raw.frames.length > FRAMES2D_MAX_FRAMES) diag.error(field + ".frames declares too many frames (" + raw.frames.length + ", max 64)");
2940
+ else {
2941
+ const frames = [];
2942
+ for (const entry of raw.frames) {
2943
+ const safe = safeFrameName(entry);
2944
+ if (safe === void 0) diag.error(field + ".frames entry " + JSON.stringify(String(entry)) + " is not a safe image file name");
2945
+ else frames.push(safe);
2946
+ }
2947
+ if (frames.length === raw.frames.length) track.frames = frames;
2735
2948
  }
2736
- if (frames.length === raw.frames.length) track.frames = frames;
2737
- }
2738
- if (raw.frameMs !== void 0) if (!Array.isArray(raw.frameMs) || raw.frameMs.length === 0 || raw.frameMs.some((v) => typeof v !== "number" || !Number.isInteger(v) || v < FRAMES2D_MIN_FRAME_MS || v > FRAMES2D_MAX_FRAME_MS)) diag.error(field + ".frameMs must be a non-empty array of integer ms in [16, 5000]");
2739
- else if (track.frames === void 0) diag.error(field + ".frameMs requires a valid explicit frames list (directory tracks use filename-encoded ms or defaultFrameMs)");
2740
- else if (raw.frameMs.length !== track.frames.length) diag.error(field + ".frameMs must have the same length as frames");
2741
- else track.frameMs = raw.frameMs;
2742
- if (raw.loop !== void 0) if (typeof raw.loop !== "boolean") diag.error(field + ".loop must be a boolean");
2743
- else track.loop = raw.loop;
2744
- if (raw.fallback !== void 0) if (typeof raw.fallback !== "string" || !validTrackName(raw.fallback)) diag.error(field + ".fallback must be a track name");
2745
- else track.fallback = raw.fallback;
2949
+ }
2950
+ if (raw.frameMs !== void 0) {
2951
+ if (!Array.isArray(raw.frameMs) || raw.frameMs.length === 0 || raw.frameMs.some((v) => typeof v !== "number" || !Number.isInteger(v) || v < FRAMES2D_MIN_FRAME_MS || v > FRAMES2D_MAX_FRAME_MS)) diag.error(field + ".frameMs must be a non-empty array of integer ms in [16, 5000]");
2952
+ else if (track.frames === void 0) diag.error(field + ".frameMs requires a valid explicit frames list (directory tracks use filename-encoded ms or defaultFrameMs)");
2953
+ else if (raw.frameMs.length !== track.frames.length) diag.error(field + ".frameMs must have the same length as frames");
2954
+ else track.frameMs = raw.frameMs;
2955
+ }
2956
+ if (raw.loop !== void 0) {
2957
+ if (typeof raw.loop !== "boolean") diag.error(field + ".loop must be a boolean");
2958
+ else track.loop = raw.loop;
2959
+ }
2960
+ if (raw.fallback !== void 0) {
2961
+ if (typeof raw.fallback !== "string" || !validTrackName(raw.fallback)) diag.error(field + ".fallback must be a track name");
2962
+ else track.fallback = raw.fallback;
2963
+ }
2746
2964
  return diag.hasErrors ? void 0 : track;
2747
2965
  }
2748
2966
  function parseFrames2dBlock(raw, diag) {
@@ -2761,8 +2979,10 @@ function parseFrames2dBlock(raw, diag) {
2761
2979
  if (dir === void 0) diag.error("frames2d.dir must be a safe manifest-relative directory");
2762
2980
  else block.dir = dir;
2763
2981
  }
2764
- if (raw.defaultFrameMs !== void 0) if (typeof raw.defaultFrameMs !== "number" || !Number.isInteger(raw.defaultFrameMs) || raw.defaultFrameMs < FRAMES2D_MIN_FRAME_MS || raw.defaultFrameMs > FRAMES2D_MAX_FRAME_MS) diag.error("frames2d.defaultFrameMs must be an integer in [16, 5000]");
2765
- else block.defaultFrameMs = raw.defaultFrameMs;
2982
+ if (raw.defaultFrameMs !== void 0) {
2983
+ if (typeof raw.defaultFrameMs !== "number" || !Number.isInteger(raw.defaultFrameMs) || raw.defaultFrameMs < FRAMES2D_MIN_FRAME_MS || raw.defaultFrameMs > FRAMES2D_MAX_FRAME_MS) diag.error("frames2d.defaultFrameMs must be an integer in [16, 5000]");
2984
+ else block.defaultFrameMs = raw.defaultFrameMs;
2985
+ }
2766
2986
  const tracks = {};
2767
2987
  if (!isRecord$1(raw.tracks)) diag.error("frames2d.tracks is required and must be an object keyed by track name");
2768
2988
  else {
@@ -2788,99 +3008,105 @@ function parseFrames2dBlock(raw, diag) {
2788
3008
  for (const [phase, target] of Object.entries(block.phases)) if (target !== void 0 && tracks[target] === void 0) diag.error("frames2d.phases." + phase + " references unknown track " + JSON.stringify(target));
2789
3009
  }
2790
3010
  for (const [name, track] of Object.entries(tracks)) if (track.fallback !== void 0 && tracks[track.fallback] === void 0) diag.error("frames2d.tracks." + name + ".fallback references unknown track " + JSON.stringify(track.fallback));
2791
- if (raw.skins !== void 0) if (!Array.isArray(raw.skins) || raw.skins.length === 0 || raw.skins.length > 16) diag.error("frames2d.skins must be an array of 1..16 skins");
2792
- else {
2793
- const skins = [];
2794
- const seen = /* @__PURE__ */ new Set();
2795
- for (const [index, entry] of raw.skins.entries()) {
2796
- const field = "frames2d.skins[" + index + "]";
2797
- if (!isRecord$1(entry)) {
2798
- diag.error(field + " must be an object");
2799
- continue;
2800
- }
2801
- const extra = unknownKeys$1(entry, KNOWN_SKIN);
2802
- if (extra.length > 0) diag.error(field + ": unknown field(s) " + extra.map((k) => JSON.stringify(k)).join(", "));
2803
- const id = typeof entry.id === "string" ? entry.id.trim() : "";
2804
- if (id === "" || id.length > 24 || !FRAMES2D_TRACK_NAME.test(id)) {
2805
- diag.error(field + ".id must be a lowercase kebab id of at most 24 chars");
2806
- continue;
2807
- }
2808
- if (seen.has(id)) {
2809
- diag.error(field + ": duplicate skin id " + JSON.stringify(id));
2810
- continue;
2811
- }
2812
- seen.add(id);
2813
- const label = typeof entry.label === "string" ? entry.label.trim() : "";
2814
- if (label === "" || label.length > 40) {
2815
- diag.error(field + ".label must be a non-empty string of at most 40 chars");
2816
- continue;
2817
- }
2818
- const idleTrack = typeof entry.idleTrack === "string" ? entry.idleTrack : "";
2819
- if (!validTrackName(idleTrack) || tracks[idleTrack] === void 0) {
2820
- diag.error(field + ".idleTrack must name a declared frames2d track");
2821
- continue;
2822
- }
2823
- let clickActions;
2824
- if (entry.clickActions !== void 0) if (!Array.isArray(entry.clickActions) || entry.clickActions.length === 0 || entry.clickActions.length > 8) diag.error(field + ".clickActions must be an array of 1..8 actions");
2825
- else {
2826
- const resolved = [];
2827
- for (const [cIndex, action] of entry.clickActions.entries()) {
2828
- const cField = field + ".clickActions[" + cIndex + "]";
2829
- if (!isRecord$1(action)) {
2830
- diag.error(cField + " must be an object");
2831
- continue;
2832
- }
2833
- const cExtra = unknownKeys$1(action, KNOWN_SKIN_CLICK);
2834
- if (cExtra.length > 0) diag.error(cField + ": unknown field(s) " + cExtra.map((k) => JSON.stringify(k)).join(", "));
2835
- const trackName = typeof action.track === "string" ? action.track : "";
2836
- if (!validTrackName(trackName) || tracks[trackName] === void 0) {
2837
- diag.error(cField + ".track must name a declared frames2d track");
2838
- continue;
2839
- }
2840
- if (typeof action.probability !== "number" || !Number.isFinite(action.probability) || action.probability <= 0 || action.probability > 1) {
2841
- diag.error(cField + ".probability must be a number in (0, 1]");
2842
- continue;
2843
- }
2844
- const phrases = parseSkinPhrases(action.phrases, cField + ".phrases", diag);
2845
- resolved.push({
2846
- track: trackName,
2847
- probability: action.probability,
2848
- ...phrases === void 0 ? {} : { phrases }
2849
- });
3011
+ if (raw.skins !== void 0) {
3012
+ if (!Array.isArray(raw.skins) || raw.skins.length === 0 || raw.skins.length > 16) diag.error("frames2d.skins must be an array of 1..16 skins");
3013
+ else {
3014
+ const skins = [];
3015
+ const seen = /* @__PURE__ */ new Set();
3016
+ for (const [index, entry] of raw.skins.entries()) {
3017
+ const field = "frames2d.skins[" + index + "]";
3018
+ if (!isRecord$1(entry)) {
3019
+ diag.error(field + " must be an object");
3020
+ continue;
2850
3021
  }
2851
- if (resolved.length > 0) clickActions = resolved;
2852
- }
2853
- let gameplayTracks;
2854
- if (entry.gameplayTracks !== void 0) if (!isRecord$1(entry.gameplayTracks)) diag.error(field + ".gameplayTracks must be an object mapping gameplay state names to tracks");
2855
- else {
2856
- const states = Object.keys(entry.gameplayTracks);
2857
- if (states.length === 0 || states.length > 8) diag.error(field + ".gameplayTracks must map 1..8 gameplay states");
2858
- else {
2859
- const resolvedTracks = {};
2860
- for (const state of states) {
2861
- if (!/^[a-z0-9][a-z0-9-]*$/.test(state)) {
2862
- diag.error(field + ".gameplayTracks key " + JSON.stringify(state) + " must be a lowercase kebab state name");
2863
- continue;
3022
+ const extra = unknownKeys$1(entry, KNOWN_SKIN);
3023
+ if (extra.length > 0) diag.error(field + ": unknown field(s) " + extra.map((k) => JSON.stringify(k)).join(", "));
3024
+ const id = typeof entry.id === "string" ? entry.id.trim() : "";
3025
+ if (id === "" || id.length > 24 || !FRAMES2D_TRACK_NAME.test(id)) {
3026
+ diag.error(field + ".id must be a lowercase kebab id of at most 24 chars");
3027
+ continue;
3028
+ }
3029
+ if (seen.has(id)) {
3030
+ diag.error(field + ": duplicate skin id " + JSON.stringify(id));
3031
+ continue;
3032
+ }
3033
+ seen.add(id);
3034
+ const label = typeof entry.label === "string" ? entry.label.trim() : "";
3035
+ if (label === "" || label.length > 40) {
3036
+ diag.error(field + ".label must be a non-empty string of at most 40 chars");
3037
+ continue;
3038
+ }
3039
+ const idleTrack = typeof entry.idleTrack === "string" ? entry.idleTrack : "";
3040
+ if (!validTrackName(idleTrack) || tracks[idleTrack] === void 0) {
3041
+ diag.error(field + ".idleTrack must name a declared frames2d track");
3042
+ continue;
3043
+ }
3044
+ let clickActions;
3045
+ if (entry.clickActions !== void 0) {
3046
+ if (!Array.isArray(entry.clickActions) || entry.clickActions.length === 0 || entry.clickActions.length > 8) diag.error(field + ".clickActions must be an array of 1..8 actions");
3047
+ else {
3048
+ const resolved = [];
3049
+ for (const [cIndex, action] of entry.clickActions.entries()) {
3050
+ const cField = field + ".clickActions[" + cIndex + "]";
3051
+ if (!isRecord$1(action)) {
3052
+ diag.error(cField + " must be an object");
3053
+ continue;
3054
+ }
3055
+ const cExtra = unknownKeys$1(action, KNOWN_SKIN_CLICK);
3056
+ if (cExtra.length > 0) diag.error(cField + ": unknown field(s) " + cExtra.map((k) => JSON.stringify(k)).join(", "));
3057
+ const trackName = typeof action.track === "string" ? action.track : "";
3058
+ if (!validTrackName(trackName) || tracks[trackName] === void 0) {
3059
+ diag.error(cField + ".track must name a declared frames2d track");
3060
+ continue;
3061
+ }
3062
+ if (typeof action.probability !== "number" || !Number.isFinite(action.probability) || action.probability <= 0 || action.probability > 1) {
3063
+ diag.error(cField + ".probability must be a number in (0, 1]");
3064
+ continue;
3065
+ }
3066
+ const phrases = parseSkinPhrases(action.phrases, cField + ".phrases", diag);
3067
+ resolved.push({
3068
+ track: trackName,
3069
+ probability: action.probability,
3070
+ ...phrases === void 0 ? {} : { phrases }
3071
+ });
2864
3072
  }
2865
- const gTrack = entry.gameplayTracks[state];
2866
- if (typeof gTrack !== "string" || !validTrackName(gTrack) || tracks[gTrack] === void 0) {
2867
- diag.error(field + ".gameplayTracks[" + JSON.stringify(state) + "] must name a declared frames2d track");
2868
- continue;
3073
+ if (resolved.length > 0) clickActions = resolved;
3074
+ }
3075
+ }
3076
+ let gameplayTracks;
3077
+ if (entry.gameplayTracks !== void 0) {
3078
+ if (!isRecord$1(entry.gameplayTracks)) diag.error(field + ".gameplayTracks must be an object mapping gameplay state names to tracks");
3079
+ else {
3080
+ const states = Object.keys(entry.gameplayTracks);
3081
+ if (states.length === 0 || states.length > 8) diag.error(field + ".gameplayTracks must map 1..8 gameplay states");
3082
+ else {
3083
+ const resolvedTracks = {};
3084
+ for (const state of states) {
3085
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(state)) {
3086
+ diag.error(field + ".gameplayTracks key " + JSON.stringify(state) + " must be a lowercase kebab state name");
3087
+ continue;
3088
+ }
3089
+ const gTrack = entry.gameplayTracks[state];
3090
+ if (typeof gTrack !== "string" || !validTrackName(gTrack) || tracks[gTrack] === void 0) {
3091
+ diag.error(field + ".gameplayTracks[" + JSON.stringify(state) + "] must name a declared frames2d track");
3092
+ continue;
3093
+ }
3094
+ resolvedTracks[state] = gTrack;
3095
+ }
3096
+ if (Object.keys(resolvedTracks).length > 0) gameplayTracks = resolvedTracks;
2869
3097
  }
2870
- resolvedTracks[state] = gTrack;
2871
3098
  }
2872
- if (Object.keys(resolvedTracks).length > 0) gameplayTracks = resolvedTracks;
2873
3099
  }
3100
+ skins.push({
3101
+ id,
3102
+ label,
3103
+ idleTrack,
3104
+ ...clickActions === void 0 ? {} : { clickActions },
3105
+ ...gameplayTracks === void 0 ? {} : { gameplayTracks }
3106
+ });
2874
3107
  }
2875
- skins.push({
2876
- id,
2877
- label,
2878
- idleTrack,
2879
- ...clickActions === void 0 ? {} : { clickActions },
2880
- ...gameplayTracks === void 0 ? {} : { gameplayTracks }
2881
- });
3108
+ if (skins.length > 0) block.skins = skins;
2882
3109
  }
2883
- if (skins.length > 0) block.skins = skins;
2884
3110
  }
2885
3111
  return diag.hasErrors ? void 0 : block;
2886
3112
  }
@@ -2931,14 +3157,22 @@ function parseV2(source, diag) {
2931
3157
  renderer
2932
3158
  };
2933
3159
  if (license !== void 0) manifest.license = license;
2934
- if (source.description !== void 0) if (typeof source.description !== "string" || source.description.length > 500) diag.error("description must be a string of at most 500 chars");
2935
- else manifest.description = source.description;
2936
- if (source.version !== void 0) if (typeof source.version !== "string" || !SEMVER_PATTERN.test(source.version)) diag.error("version must be a semver string (x.y.z)");
2937
- else manifest.version = source.version;
2938
- if (source.author !== void 0) if (typeof source.author !== "string" || source.author.length > 128) diag.error("author must be a string of at most 128 chars");
2939
- else manifest.author = source.author;
2940
- if (source.homepage !== void 0) if (typeof source.homepage !== "string") diag.error("homepage must be a string URL");
2941
- else manifest.homepage = source.homepage;
3160
+ if (source.description !== void 0) {
3161
+ if (typeof source.description !== "string" || source.description.length > 500) diag.error("description must be a string of at most 500 chars");
3162
+ else manifest.description = source.description;
3163
+ }
3164
+ if (source.version !== void 0) {
3165
+ if (typeof source.version !== "string" || !SEMVER_PATTERN.test(source.version)) diag.error("version must be a semver string (x.y.z)");
3166
+ else manifest.version = source.version;
3167
+ }
3168
+ if (source.author !== void 0) {
3169
+ if (typeof source.author !== "string" || source.author.length > 128) diag.error("author must be a string of at most 128 chars");
3170
+ else manifest.author = source.author;
3171
+ }
3172
+ if (source.homepage !== void 0) {
3173
+ if (typeof source.homepage !== "string") diag.error("homepage must be a string URL");
3174
+ else manifest.homepage = source.homepage;
3175
+ }
2942
3176
  if (renderer === "sprite2d") {
2943
3177
  const block = parseSprite2dBlock(source.sprite2d, diag);
2944
3178
  if (block !== void 0) manifest.sprite2d = block;
@@ -2955,13 +3189,15 @@ function parseV2(source, diag) {
2955
3189
  if (source.sprite2d !== void 0) diag.error("renderer frames2d must not declare a sprite2d block");
2956
3190
  if (source.live2d !== void 0) diag.error("renderer frames2d must not declare a live2d block");
2957
3191
  }
2958
- if (source.gameplay !== void 0) if (renderer !== "frames2d") diag.error("gameplay currently requires renderer frames2d (its state references name frames2d tracks)");
2959
- else {
2960
- const gameplay = parseGameplayManifest(source.gameplay, {
2961
- stateNames: new Set(Object.keys(manifest.frames2d?.tracks ?? {})),
2962
- error: (message) => diag.error("gameplay: " + message)
2963
- });
2964
- if (gameplay !== void 0) manifest.gameplay = gameplay;
3192
+ if (source.gameplay !== void 0) {
3193
+ if (renderer !== "frames2d") diag.error("gameplay currently requires renderer frames2d (its state references name frames2d tracks)");
3194
+ else {
3195
+ const gameplay = parseGameplayManifest(source.gameplay, {
3196
+ stateNames: new Set(Object.keys(manifest.frames2d?.tracks ?? {})),
3197
+ error: (message) => diag.error("gameplay: " + message)
3198
+ });
3199
+ if (gameplay !== void 0) manifest.gameplay = gameplay;
3200
+ }
2965
3201
  }
2966
3202
  const sequences = parseSequences(source.sequences, diag);
2967
3203
  if (sequences !== void 0) manifest.sequences = sequences;
@@ -3411,7 +3647,8 @@ function petPackageRoot(importMetaUrl) {
3411
3647
  /** Resolve the hatch-pet custom pets directory (CODEX_HOME or ~/.codex). */
3412
3648
  function codexPetsDir(env = process.env, home = homedir()) {
3413
3649
  const raw = env.CODEX_HOME !== void 0 && env.CODEX_HOME.trim() !== "" ? env.CODEX_HOME.trim() : join(home, ".codex");
3414
- return join(raw === "~" ? home : raw.startsWith("~/") || raw.startsWith("~\\") ? join(home, raw.slice(2)) : raw, "pets");
3650
+ const expanded = raw === "~" ? home : raw.startsWith("~/") || raw.startsWith("~\\") ? join(home, raw.slice(2)) : raw;
3651
+ return join(expanded, "pets");
3415
3652
  }
3416
3653
  /** Finite non-negative integer guard, else the fallback. */
3417
3654
  function finiteInt(value, fallback, max) {
@@ -4048,7 +4285,7 @@ function readPetJson(file, options) {
4048
4285
  * symlink — must not hang or exhaust the host before the warn-and-drop
4049
4286
  * discipline can apply (review-spd follow-up, pet-center M4/M5).
4050
4287
  */
4051
- const PET_SCAN_JSON_CAP = 64 * 1024;
4288
+ const PET_SCAN_JSON_CAP = 65536;
4052
4289
  /**
4053
4290
  * Stat one scanned JSON descriptor with a regular-file + size guard, so a
4054
4291
  * pathological user file is skipped with a warning instead of stalling or
@@ -4651,10 +4888,9 @@ var PetService = class extends Service {
4651
4888
  return {
4652
4889
  petId,
4653
4890
  state: stored === void 0 ? initialGameplayState(def, now) : {
4891
+ ...stored,
4654
4892
  stats: { ...stored.stats },
4655
- currencies: { ...stored.currencies },
4656
- mode: stored.mode,
4657
- settledAt: stored.settledAt
4893
+ currencies: { ...stored.currencies }
4658
4894
  }
4659
4895
  };
4660
4896
  }
@@ -4662,9 +4898,10 @@ var PetService = class extends Service {
4662
4898
  gameplayViewOf(state) {
4663
4899
  const stats = {};
4664
4900
  for (const [name, value] of Object.entries(state.stats)) stats[name] = Math.round(value);
4901
+ const def = this.gameplayDef();
4665
4902
  return {
4666
4903
  stats,
4667
- mode: state.mode
4904
+ mode: state.mode !== null && def !== void 0 && !isDeclaredMode(def, state.mode) ? null : state.mode
4668
4905
  };
4669
4906
  }
4670
4907
  /**
@@ -4743,7 +4980,11 @@ var PetService = class extends Service {
4743
4980
  view: this.gameplayViewOf(state)
4744
4981
  };
4745
4982
  }
4746
- /** RPC: enter or leave a gameplay mode ('work' | 'sleep' | null). */
4983
+ /**
4984
+ * RPC: enter or leave a gameplay mode (null clears it). Every mode the
4985
+ * manifest declares is accepted: 'work', 'sleep', or one of the extra
4986
+ * 'modes' entries (a bath, a play session…).
4987
+ */
4747
4988
  async gameplaySetMode(mode) {
4748
4989
  const def = this.gameplayDef();
4749
4990
  if (def === void 0) return {
@@ -4758,9 +4999,14 @@ var PetService = class extends Service {
4758
4999
  ok: false,
4759
5000
  error: "no-sleep"
4760
5001
  };
5002
+ if (mode !== null && !isDeclaredMode(def, mode)) return {
5003
+ ok: false,
5004
+ error: "unknown-mode"
5005
+ };
4761
5006
  const now = Date.now();
4762
5007
  const { petId, state } = this.gameplayState(def, now);
4763
5008
  settleGameplay(state, def, now, { sessionActive: this.machine.render().sessionActive });
5009
+ if (state.mode !== mode) state.restoreCarryMs = 0;
4764
5010
  state.mode = mode;
4765
5011
  this.drainGameplayTreats(state);
4766
5012
  this.commitGameplay(petId, state);
@@ -5103,7 +5349,7 @@ function isPetAllowed(ctx, request) {
5103
5349
  //#endregion
5104
5350
  //#region src/http.ts
5105
5351
  /** Default body cap for readJsonBody: 64 KiB. */
5106
- const DEFAULT_JSON_BODY_MAX_BYTES = 64 * 1024;
5352
+ const DEFAULT_JSON_BODY_MAX_BYTES = 65536;
5107
5353
  /** Family-default JSON response headers; callers may append or override. */
5108
5354
  const JSON_HEADERS = {
5109
5355
  "content-type": "application/json; charset=utf-8",
@@ -5186,11 +5432,11 @@ const PREVIEW_PATTERN = /^[A-Za-z0-9._-]+$/;
5186
5432
  */
5187
5433
  const PET_ASSET_CAPS = {
5188
5434
  /** pet.json manifest. */
5189
- manifest: 64 * 1024,
5435
+ manifest: 65536,
5190
5436
  /** Atlas, preview and Live2D texture imagery. */
5191
- image: 20 * 1024 * 1024,
5437
+ image: 20971520,
5192
5438
  /** Live2D model closure files (.moc3, motion/physics/expression JSON; M3). */
5193
- model: 32 * 1024 * 1024
5439
+ model: 33554432
5194
5440
  };
5195
5441
  /** Imagery extensions classify into the image cap; everything else served from a closure is model-class. */
5196
5442
  const IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
@@ -5305,7 +5551,7 @@ function postRoute(ctx, path, run) {
5305
5551
  handler: (req, res) => {
5306
5552
  if (!guard(ctx, req, res)) return Promise.resolve();
5307
5553
  if (!requireMethod(req, res, "POST")) return Promise.resolve();
5308
- return readJsonBody(req, { maxBytes: 64 * 1024 }).then((parsed) => {
5554
+ return readJsonBody(req, { maxBytes: 65536 }).then((parsed) => {
5309
5555
  const payload = parsed ?? {};
5310
5556
  return run(typeof payload === "object" && payload !== null ? payload : {}).then((value) => writeJson(res, 200, value), (error) => {
5311
5557
  writeJson(res, 400, {
@@ -5718,7 +5964,7 @@ function makePetRoutes(deps) {
5718
5964
  }),
5719
5965
  postRoute(ctx, "/api/pet/gameplay/mode", (body) => {
5720
5966
  const mode = body.mode;
5721
- if (mode !== null && mode !== "work" && mode !== "sleep") return Promise.reject(/* @__PURE__ */ new Error("invalid-mode"));
5967
+ if (mode !== null && typeof mode !== "string") return Promise.reject(/* @__PURE__ */ new Error("invalid-mode"));
5722
5968
  return service.gameplaySetMode(mode);
5723
5969
  }),
5724
5970
  postRoute(ctx, "/api/pet/gameplay/work-tick", () => service.gameplayWorkTick()),
@@ -5801,24 +6047,77 @@ const name = "pet";
5801
6047
  /** Services required before the pet can mount its surfaces. */
5802
6048
  const inject = ["webServer"];
5803
6049
  /**
5804
- * Settings section schema: pet selection and display fields the web settings
5805
- * surface edits. petId is a plain string on purpose: the service clamps the
5806
- * resolved value against the registry, so a stored selection that points at
5807
- * a removed pet cannot invalidate the section (a strict union would refuse
5808
- * the whole registration). The settings card renders the actual registry
5809
- * choices itself from '/api/pet/pets'.
6050
+ * Defaults of the fields the pet's settings page edits. They are the schema
6051
+ * defaults of the pet row's own config, i.e. what a field the profile entry
6052
+ * never set resolves to.
5810
6053
  */
5811
- function makePetSettingsSchema(fallbackPetId) {
5812
- return z.object({
5813
- visible: z.boolean().default(true),
5814
- size: z.number().step(1).min(32).max(DISPLAY_SIZE_MAX).default(160),
5815
- right: z.number().step(1).min(0).max(DISPLAY_INSET_MAX).default(24),
5816
- bottom: z.number().step(1).min(0).max(DISPLAY_INSET_MAX).default(20),
5817
- bubbleScale: z.number().step(.05).min(BUBBLE_SCALE_MIN).max(2).default(1),
5818
- petId: z.string().default(fallbackPetId),
5819
- enabled: z.boolean().default(true),
5820
- decorationEnabled: z.boolean().default(true)
5821
- });
6054
+ const PET_FORM_DEFAULTS = {
6055
+ visible: true,
6056
+ size: 160,
6057
+ right: 24,
6058
+ bottom: 20,
6059
+ bubbleScale: 1,
6060
+ petId: DEFAULT_PET_ID,
6061
+ enabled: true,
6062
+ decorationEnabled: true
6063
+ };
6064
+ /**
6065
+ * Plugin configuration. Under the 0.1.7 settings model a plugin's own Cordis
6066
+ * Config IS its settings page: the Host derives one form per profile entry
6067
+ * from this schema, so the fields the pet card edits live here with the
6068
+ * defaults the card inherits, next to the tuning block a profile may still
6069
+ * declare.
6070
+ *
6071
+ * Every page field is `volatile()` on purpose. The Host serves exactly the
6072
+ * volatile fields of a Config and refuses writes to any other path, and a
6073
+ * volatile field is the one it can commit into the RUNNING config: the pet
6074
+ * reads the edited value from the live reference instead of being remounted
6075
+ * for each edit (see `syncSettings` in `apply`). petId stays a plain string
6076
+ * because the service clamps the value against the registry, so a stored
6077
+ * selection naming a removed pet cannot invalidate the entry.
6078
+ */
6079
+ const Config = z.object({
6080
+ visible: z.boolean().default(PET_FORM_DEFAULTS.visible).volatile(),
6081
+ size: z.number().step(1).min(32).max(DISPLAY_SIZE_MAX).default(PET_FORM_DEFAULTS.size).volatile(),
6082
+ right: z.number().step(1).min(0).max(DISPLAY_INSET_MAX).default(PET_FORM_DEFAULTS.right).volatile(),
6083
+ bottom: z.number().step(1).min(0).max(DISPLAY_INSET_MAX).default(PET_FORM_DEFAULTS.bottom).volatile(),
6084
+ bubbleScale: z.number().step(.05).min(BUBBLE_SCALE_MIN).max(2).default(PET_FORM_DEFAULTS.bubbleScale).volatile(),
6085
+ petId: z.string().volatile(),
6086
+ enabled: z.boolean().default(PET_FORM_DEFAULTS.enabled).volatile(),
6087
+ decorationEnabled: z.boolean().default(PET_FORM_DEFAULTS.decorationEnabled).volatile()
6088
+ });
6089
+ /**
6090
+ * Read one live config field.
6091
+ * @param field - the field's live reference, plain value, or nothing at all.
6092
+ * @param fallback - value used when the field is absent.
6093
+ * @returns the current field value.
6094
+ */
6095
+ function readLive(field, fallback) {
6096
+ if (field === void 0) return fallback;
6097
+ const ref = field;
6098
+ return typeof ref.get === "function" ? ref.get() ?? fallback : field;
6099
+ }
6100
+ /**
6101
+ * The settings section the pet runs with: the effective values of the row's
6102
+ * own config. `fallbackPetId` covers a mount whose config names no pet at all
6103
+ * (a direct mount outside a Loader) — under a Loader the schema default is
6104
+ * always present, so the persisted selection stands whenever the config
6105
+ * carries it.
6106
+ * @param config - the effective config of the pet row.
6107
+ * @param fallbackPetId - pet id to use when the config names none.
6108
+ * @returns the resolved settings section.
6109
+ */
6110
+ function petSettingsSection(config, fallbackPetId) {
6111
+ return {
6112
+ visible: readLive(config.visible, PET_FORM_DEFAULTS.visible),
6113
+ size: readLive(config.size, PET_FORM_DEFAULTS.size),
6114
+ right: readLive(config.right, PET_FORM_DEFAULTS.right),
6115
+ bottom: readLive(config.bottom, PET_FORM_DEFAULTS.bottom),
6116
+ bubbleScale: readLive(config.bubbleScale, PET_FORM_DEFAULTS.bubbleScale),
6117
+ petId: readLive(config.petId, fallbackPetId),
6118
+ enabled: readLive(config.enabled, PET_FORM_DEFAULTS.enabled),
6119
+ decorationEnabled: readLive(config.decorationEnabled, PET_FORM_DEFAULTS.decorationEnabled)
6120
+ };
5822
6121
  }
5823
6122
  /** Register the pet service and its API + asset routes on the context. */
5824
6123
  const apply = mountOnce("@linxin666/dsh-pet", applyImpl);
@@ -5829,18 +6128,11 @@ function applyImpl(ctx, config = {}) {
5829
6128
  });
5830
6129
  const service = new PetService(ctx, {
5831
6130
  ...config,
6131
+ enabled: readLive(config.enabled, PET_FORM_DEFAULTS.enabled),
6132
+ decorationEnabled: readLive(config.decorationEnabled, PET_FORM_DEFAULTS.decorationEnabled),
5832
6133
  registry
5833
6134
  });
5834
- let current = () => base;
5835
- const base = {
5836
- visible: service.display().visible,
5837
- size: service.display().size,
5838
- right: service.display().right,
5839
- bottom: service.display().bottom,
5840
- petId: service.selectedPetId(),
5841
- enabled: config.enabled ?? true,
5842
- decorationEnabled: config.decorationEnabled ?? true
5843
- };
6135
+ const current = () => petSettingsSection(config, service.selectedPetId());
5844
6136
  const routes = makePetRoutes({
5845
6137
  service,
5846
6138
  ctx
@@ -5859,33 +6151,25 @@ function applyImpl(ctx, config = {}) {
5859
6151
  disposeRoutes = void 0;
5860
6152
  }
5861
6153
  };
6154
+ const syncSettings = () => {
6155
+ const section = current();
6156
+ service.applySettingsSection(section);
6157
+ service.setEnabled(section.enabled ?? true);
6158
+ syncRoutes();
6159
+ };
6160
+ ctx.on("loader/volatile-update", () => {
6161
+ syncSettings();
6162
+ });
5862
6163
  ctx.inject(["settings"], (settingsCtx) => {
5863
- try {
5864
- const schema = makePetSettingsSchema(service.selectedPetId());
5865
- if (typeof settingsCtx.settings?.installSection === "function") settingsCtx.settings.installSection(ctx, "pet", schema, base, {
5866
- setSource: (source) => {
5867
- current = source;
5868
- },
5869
- onChange: () => {
5870
- const section = current();
5871
- service.applySettingsSection(section);
5872
- service.setEnabled(section.enabled ?? true);
5873
- syncRoutes();
5874
- }
5875
- });
5876
- else if (typeof settingsCtx.settings?.register === "function") {
5877
- const scope = settingsCtx.settings.register("pet", schema, { base });
5878
- current = () => scope?.get?.() ?? base;
5879
- scope?.watch?.(() => {
5880
- const section = current();
5881
- service.applySettingsSection(section);
5882
- service.setEnabled(section.enabled ?? true);
5883
- syncRoutes();
5884
- });
6164
+ settingsCtx.effect(() => {
6165
+ try {
6166
+ return settingsCtx.settings.configure({ auto: false }, ctx.fiber);
6167
+ } catch {
6168
+ return () => {};
5885
6169
  }
5886
- } catch {}
6170
+ }, "pet: settings page policy");
5887
6171
  });
5888
- syncRoutes();
6172
+ syncSettings();
5889
6173
  }
5890
6174
  //#endregion
5891
- export { AFFINITY_MAX, AFFINITY_RANKS, BUILTIN_REMARKS, DEFAULT_FRAME_COUNTS, DEFAULT_PET_CELL, DEFAULT_PET_COLUMNS, DEFAULT_PET_ID, DEFAULT_PET_NAME, DEFAULT_PET_ROW_COUNT, DEFAULT_TRACK_PATTERNS, MAX_SESSION_BUBBLES, PET_API_PREFIX, PET_ASSET_PREFIX, PET_NAME_MAX_LENGTH, PET_ROW_ORDER, PetService, PetStateMachine, REMARK_KINDS, REMARK_LINES_MAX, REMARK_LINE_MAX, RemarkPicker, animationForPhase, apply, applyInteraction, applyTurnReward, builtinRemark, codexPetsDir, consumeTreat, defaultDisplayConfig, defaultTreatConfig, emptyAffinity, emptyPersist, emptyTreatLedger, inject, loadPetPersist, loadPetRegistry, makePetRoutes, makePetSettingsSchema, name, normalizePetRemarks, petEntryView, petHomeDir, petPackageRoot, rankOf, resolvePetManifest, rowOf, savePetPersist, settleTreatGrants };
6175
+ export { AFFINITY_MAX, AFFINITY_RANKS, BUILTIN_REMARKS, Config, DEFAULT_FRAME_COUNTS, DEFAULT_PET_CELL, DEFAULT_PET_COLUMNS, DEFAULT_PET_ID, DEFAULT_PET_NAME, DEFAULT_PET_ROW_COUNT, DEFAULT_TRACK_PATTERNS, MAX_SESSION_BUBBLES, PET_API_PREFIX, PET_ASSET_PREFIX, PET_FORM_DEFAULTS, PET_NAME_MAX_LENGTH, PET_ROW_ORDER, PetService, PetStateMachine, REMARK_KINDS, REMARK_LINES_MAX, REMARK_LINE_MAX, RemarkPicker, animationForPhase, apply, applyInteraction, applyTurnReward, builtinRemark, codexPetsDir, consumeTreat, defaultDisplayConfig, defaultTreatConfig, emptyAffinity, emptyPersist, emptyTreatLedger, inject, loadPetPersist, loadPetRegistry, makePetRoutes, name, normalizePetRemarks, petEntryView, petHomeDir, petPackageRoot, petSettingsSection, rankOf, resolvePetManifest, rowOf, savePetPersist, settleTreatGrants };