@linxin666/dsh-pet 0.3.5 → 0.3.9

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 (88) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +4 -0
  3. package/README.zh.md +4 -0
  4. package/contracts/voice-pack-v1.schema.json +30 -0
  5. package/cordis.patch.yml +1 -1
  6. package/lib/client.js +330 -103
  7. package/lib/client.js.map +1 -1
  8. package/lib/index.js +145 -28
  9. package/lib/invariant.js +1 -1
  10. package/lib/live2d-vendor.js +106 -95
  11. package/lib/live2d-vendor.js.map +1 -1
  12. package/lib/{state-DrMX22GL.js → state-NvDEoCln.js} +5 -2
  13. package/lib/types/access.d.ts +5 -4
  14. package/lib/types/access.d.ts.map +1 -1
  15. package/lib/types/access.js +2 -12
  16. package/lib/types/affinity.d.ts +2 -0
  17. package/lib/types/affinity.d.ts.map +1 -1
  18. package/lib/types/affinity.js +12 -0
  19. package/lib/types/announce.d.ts +53 -0
  20. package/lib/types/announce.d.ts.map +1 -0
  21. package/lib/types/announce.js +80 -0
  22. package/lib/types/client/PetDockEntry.d.ts +14 -5
  23. package/lib/types/client/PetDockEntry.d.ts.map +1 -1
  24. package/lib/types/client/PetDockEntry.js +6 -5
  25. package/lib/types/client/PetSettingsCard.d.ts +2 -1
  26. package/lib/types/client/PetSettingsCard.d.ts.map +1 -1
  27. package/lib/types/client/PetSprite.d.ts +8 -0
  28. package/lib/types/client/PetSprite.d.ts.map +1 -1
  29. package/lib/types/client/PetSprite.js +36 -5
  30. package/lib/types/client/index.d.ts +8 -5
  31. package/lib/types/client/index.d.ts.map +1 -1
  32. package/lib/types/client/index.js +11 -5
  33. package/lib/types/client/locales.d.ts +20 -2
  34. package/lib/types/client/locales.d.ts.map +1 -1
  35. package/lib/types/client/locales.js +20 -2
  36. package/lib/types/client/pet-store.d.ts +1 -1
  37. package/lib/types/client/pet-store.d.ts.map +1 -1
  38. package/lib/types/client/pet-store.js +1 -1
  39. package/lib/types/client/renderers/frames2d.d.ts +12 -0
  40. package/lib/types/client/renderers/frames2d.d.ts.map +1 -1
  41. package/lib/types/client/renderers/frames2d.js +143 -18
  42. package/lib/types/client/settings-form.d.ts +35 -43
  43. package/lib/types/client/settings-form.d.ts.map +1 -1
  44. package/lib/types/client/settings-form.js +60 -53
  45. package/lib/types/index.d.ts.map +1 -1
  46. package/lib/types/index.js +10 -9
  47. package/lib/types/ledger.d.ts +4 -2
  48. package/lib/types/ledger.d.ts.map +1 -1
  49. package/lib/types/ledger.js +4 -4
  50. package/lib/types/pair-access.d.ts +35 -0
  51. package/lib/types/pair-access.d.ts.map +1 -0
  52. package/lib/types/pair-access.js +19 -0
  53. package/lib/types/remarks.d.ts +1 -1
  54. package/lib/types/remarks.d.ts.map +1 -1
  55. package/lib/types/remarks.js +11 -2
  56. package/lib/types/service.d.ts +18 -0
  57. package/lib/types/service.d.ts.map +1 -1
  58. package/lib/types/service.js +26 -1
  59. package/lib/types/voice-pack.d.ts +5 -0
  60. package/lib/types/voice-pack.d.ts.map +1 -1
  61. package/lib/types/voice-pack.js +36 -2
  62. package/package.json +17 -13
  63. package/src/access.ts +7 -27
  64. package/src/affinity.ts +13 -0
  65. package/src/announce.ts +103 -0
  66. package/src/client/PetDockEntry.test.tsx +1 -1
  67. package/src/client/PetDockEntry.tsx +15 -4
  68. package/src/client/PetSettingsCard.tsx +2 -1
  69. package/src/client/PetSprite.test.tsx +36 -0
  70. package/src/client/PetSprite.tsx +75 -4
  71. package/src/client/gameplay-hud.test.tsx +1 -1
  72. package/src/client/index.test.tsx +2 -2
  73. package/src/client/index.ts +17 -6
  74. package/src/client/locales.ts +20 -2
  75. package/src/client/pet-store.ts +2 -2
  76. package/src/client/pet.module.css +104 -0
  77. package/src/client/renderers/frames2d.test.ts +101 -0
  78. package/src/client/renderers/frames2d.ts +138 -18
  79. package/src/client/settings-form.ts +78 -92
  80. package/src/dsh-home.ts +1 -1
  81. package/src/index.ts +17 -15
  82. package/src/ledger.ts +6 -4
  83. package/src/pair-access.ts +49 -0
  84. package/src/remarks.test.ts +10 -0
  85. package/src/remarks.ts +9 -2
  86. package/src/service.ts +32 -2
  87. package/src/voice-pack.test.ts +39 -0
  88. package/src/voice-pack.ts +37 -2
package/lib/client.js CHANGED
@@ -6,9 +6,21 @@ window.__ModuleLoader__.load({
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let react = require("react");
8
8
  let react_dom_client = require("react-dom/client");
9
- let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
10
9
  let react_dom = require("react-dom");
11
10
  let react_jsx_runtime = require("react/jsx-runtime");
11
+ //#region \0dsh-store-engine
12
+ const platform = ["@deepseek-ai/dsh-client", "-store"].join("");
13
+ const legacy = ["@deepseek-ai/dsh-client-runtime", "/client"].join("");
14
+ let engine;
15
+ try {
16
+ engine = require(platform);
17
+ } catch {
18
+ engine = require(legacy);
19
+ }
20
+ const createSnapshotStore = engine.createSnapshotStore;
21
+ const defineStore = engine.defineStore;
22
+ engine.shallowEqual;
23
+ //#endregion
12
24
  //#region src/client/pet-store.ts
13
25
  /**
14
26
  * Browser-side pet store: the pet state snapshot plus transient UI feedback
@@ -19,7 +31,7 @@ window.__ModuleLoader__.load({
19
31
  */
20
32
  /** Create the pet store handle (apply world only; never module-level). */
21
33
  function createPetStore() {
22
- return (0, _deepseek_ai_dsh_client_runtime_client.defineStore)({
34
+ return defineStore({
23
35
  init: () => ({
24
36
  snapshot: null,
25
37
  pets: [],
@@ -68,6 +80,12 @@ window.__ModuleLoader__.load({
68
80
  return n;
69
81
  }
70
82
  //#endregion
83
+ //#region src/announce.ts
84
+ /** Whether an announcement is still fresh at `now`. */
85
+ function announcementFresh(announcement, now) {
86
+ return now - announcement.at < announcement.ttlMs;
87
+ }
88
+ //#endregion
71
89
  //#region src/state.ts
72
90
  /**
73
91
  * Map one activity phase onto the animation contract.
@@ -171,7 +189,7 @@ window.__ModuleLoader__.load({
171
189
  }
172
190
  //#endregion
173
191
  //#region \0dsh-css:packages/dsh-pet/src/client/pet.module.css.mjs
174
- const css$2 = ".kz2Bea_float{pointer-events:auto;-webkit-user-select:none;user-select:none;will-change:transform;contain:layout style;flex-direction:column;align-items:center;display:flex;position:fixed}.kz2Bea_sprite{image-rendering:auto;touch-action:none;contain:paint}.kz2Bea_spriteWrap{flex:none;position:relative}.kz2Bea_bubble{white-space:nowrap;color:#f4f7ff;pointer-events:none;border-radius:999px;margin-bottom:6px;padding:4px 10px;font-size:12px;line-height:1.4;animation:2.6s ease-out forwards kz2Bea_pet-bubble-pop;position:absolute;bottom:100%;box-shadow:0 2px 8px #00000040}.kz2Bea_bubblePet{background:#4d6bfef2}.kz2Bea_bubbleFeed{background:#0369a1f2}.kz2Bea_bubbleStatus{text-overflow:ellipsis;backdrop-filter:blur(8px);letter-spacing:.02em;background:linear-gradient(160deg,#131c36e6,#070b1af0);border:1px solid #7e98ff73;max-width:min(280px,100vw - 24px);padding:5px 12px;animation:.24s ease-out kz2Bea_pet-bubble-in;overflow:hidden;box-shadow:0 4px 12px #02061759,inset 0 1px #e2e8ff1a,0 0 12px #4d6bfe2e}.kz2Bea_bubbleWhisper{letter-spacing:.03em;animation:.32s cubic-bezier(.22,1,.36,1) kz2Bea_pet-whisper-in}.kz2Bea_bubbleWhisper:before{content:\"「\";opacity:.7;margin-right:2px}.kz2Bea_bubbleWhisper:after{content:\"」\";opacity:.7;margin-left:2px}.kz2Bea_bubbleStack{pointer-events:auto;flex-direction:column-reverse;align-items:center;gap:4px;display:flex;position:absolute;bottom:100%}.kz2Bea_bubbleAnchor{display:inline-flex;position:relative}.kz2Bea_bubbleMore{z-index:1;color:#eef2ff;cursor:pointer;background:linear-gradient(160deg,#4a68f5f2,#2f44b8f2);border:1px solid #7e98ff8c;border-radius:999px;justify-content:center;align-items:center;min-width:18px;height:18px;padding:0 5px;font-size:10px;font-weight:600;line-height:1;transition:filter .12s;display:inline-flex;position:absolute;top:-9px;right:-10px;box-shadow:0 2px 6px #02061766}.kz2Bea_bubbleMore:hover{filter:brightness(1.15)}.kz2Bea_bubbleMore:focus-visible{outline:none;box-shadow:0 0 0 2px #7e98ffe6}.kz2Bea_bubbleStack .kz2Bea_bubble{pointer-events:auto;margin-bottom:0;position:relative;bottom:auto}.kz2Bea_bubbleClickable{cursor:pointer;font:inherit;text-align:center;appearance:none;transition:filter .12s,box-shadow .12s}.kz2Bea_bubbleClickable:hover{filter:brightness(1.12)}.kz2Bea_bubbleClickable:focus-visible{outline:none;box-shadow:0 0 0 2px #7e98ffe6}@keyframes kz2Bea_pet-bubble-in{0%{opacity:0;transform:translateY(6px)scale(.96)}to{opacity:1;transform:translateY(0)scale(1)}}@keyframes kz2Bea_pet-whisper-in{0%{opacity:0;transform:translateY(8px)scale(.9)}60%{transform:translateY(-1px)scale(1.02)}to{opacity:1;transform:translateY(0)scale(1)}}@keyframes kz2Bea_pet-panel-in{0%{opacity:0;transform:translateY(4px)scale(.97)}to{opacity:1;transform:translateY(0)scale(1)}}@keyframes kz2Bea_pet-bubble-pop{0%{opacity:0;transform:translateY(6px)scale(.85)}15%{opacity:1;transform:translateY(0)scale(1.05)}25%{transform:translateY(0)scale(1)}75%{opacity:1}to{opacity:0;transform:translateY(-8px)scale(.95)}}.kz2Bea_panel{color:#e6ebf8;backdrop-filter:blur(10px);transform-origin:50% 0;background:linear-gradient(165deg,#131c36f2,#070b1af7);border:1px solid #7e98ff4d;border-radius:12px;flex-direction:column;gap:7px;min-width:148px;margin-top:8px;padding:10px 12px;font-size:12px;animation:.2s cubic-bezier(.22,1,.36,1) kz2Bea_pet-panel-in;display:flex;position:absolute;top:100%;box-shadow:0 8px 24px #02061773,0 0 0 1px #4d6bfe1a,inset 0 1px #e2e8ff14}.kz2Bea_panel:after{content:\"\";height:14px;position:absolute;bottom:100%;left:0;right:0}.kz2Bea_panelAbove{transform-origin:50% 100%;margin-top:0;margin-bottom:8px;top:auto;bottom:100%}.kz2Bea_panelAbove:after{top:100%;bottom:auto}.kz2Bea_rankRow{white-space:nowrap;justify-content:space-between;gap:10px;display:flex}.kz2Bea_nameCell{letter-spacing:.02em;background:linear-gradient(90deg,#a9c1ff,#6c8bff 60%,#4d6bfe);color:#0000;-webkit-background-clip:text;background-clip:text;font-weight:600}.kz2Bea_statRank{color:#9db4ff;font-weight:600}.kz2Bea_statTreats{color:#7dd3fc;font-weight:600}.kz2Bea_statPoints{color:#fcd34d;font-weight:600}.kz2Bea_renameRow{align-items:center;gap:6px;display:flex}.kz2Bea_nameInput{color:#e6ebf8;background:#131c36e6;border:1px solid #7e98ff80;border-radius:6px;outline:none;flex:1;min-width:0;padding:3px 6px;font-size:12px}.kz2Bea_nameInput:focus{border-color:#4d6bfe;box-shadow:0 0 0 2px #4d6bfe73}.kz2Bea_actions{gap:6px;display:flex}.kz2Bea_action{cursor:pointer;color:#fff;background:linear-gradient(#4a68f5,#3a55e0);border:none;border-radius:6px;flex:1;padding:4px 8px;font-size:12px;font-weight:600;transition:filter .12s,box-shadow .12s,transform .12s;box-shadow:0 2px 6px #4d6bfe4d}.kz2Bea_action:hover{filter:brightness(1.08);transform:translateY(-1px);box-shadow:0 4px 10px #4d6bfe66}.kz2Bea_action:active{filter:brightness(.94);transform:translateY(0);box-shadow:0 1px 4px #4d6bfe4d}.kz2Bea_action:focus-visible{outline:none;box-shadow:0 0 0 2px #7e98ffe6}.kz2Bea_summon{color:#8ea6ff;cursor:pointer;background:#070b1abf;border:1px dashed #7e98ff99;border-radius:999px;padding:2px 10px;font-size:11px;transition:border-color .12s,color .12s,background .12s,box-shadow .12s}.kz2Bea_summon:hover{color:#c3d3ff;background:#070b1ae6;border-color:#7e98fff2}.kz2Bea_summon:active{color:#8ea6ff;border-color:#7e98ffcc}.kz2Bea_summon:focus-visible{outline:none;box-shadow:0 0 0 2px #7e98ffe6}@media (prefers-reduced-motion:reduce){.kz2Bea_bubble,.kz2Bea_bubbleStatus,.kz2Bea_bubbleWhisper,.kz2Bea_panel{opacity:1;animation:none}.kz2Bea_action,.kz2Bea_summon,.kz2Bea_bubbleMore{transition:none}}.kz2Bea_gameplayHud{width:0;height:0;position:absolute;bottom:0;left:0}.kz2Bea_gameplayModeChip{color:#cdd7ff;white-space:nowrap;pointer-events:none;background:#7e98ff2e;border-radius:999px;padding:1px 8px;font-size:10px;position:absolute;top:0;left:0;transform:translateY(-100%)translateY(-4px)}.kz2Bea_gameplayCard{color:#dfe6ff;background:#070b1aeb;border:1px solid #7e98ff40;border-radius:10px;min-width:180px;max-height:340px;padding:10px 12px;font-size:12px;position:absolute;bottom:0;left:0;overflow-y:auto;transform:none;box-shadow:0 8px 24px #0006}.kz2Bea_gameplayBars{flex-direction:column;gap:4px;margin-bottom:8px;display:flex}.kz2Bea_gameplayBarRow{align-items:center;gap:6px;display:flex}.kz2Bea_gameplayBarLabel{color:#9aa8d8;flex-shrink:0;width:34px}.kz2Bea_gameplayBarTrack{background:#7e98ff26;border-radius:3px;flex:1;height:6px;overflow:hidden}.kz2Bea_gameplayBarFill{background:linear-gradient(90deg,#7e98ff,#9db4ff);border-radius:3px;height:100%;transition:width .3s;display:block}.kz2Bea_gameplayActions{flex-wrap:wrap;gap:6px;display:flex}.kz2Bea_gameplayShopItems{grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin-bottom:8px;display:grid}.kz2Bea_gameplayShopItem{color:inherit;cursor:pointer;background:#7e98ff14;border:1px solid #7e98ff40;border-radius:8px;flex-direction:column;align-items:center;gap:4px;padding:8px 6px;display:flex}.kz2Bea_gameplayShopItem:hover{background:#7e98ff29;border-color:#7e98ffb3}.kz2Bea_gameplayShopItemImage{object-fit:contain;width:36px;height:36px;image-rendering:pixelated;pointer-events:none}.kz2Bea_gameplayShopItemLabel{text-align:center;font-size:11px}.kz2Bea_gameplayShopItemPrice{color:#ffd27e;font-size:10px}.kz2Bea_gameplayClose{color:#9aa8d8;cursor:pointer;background:0 0;border:none;padding:0 2px;font-size:14px;position:absolute;top:4px;right:8px}.kz2Bea_gameplayClose:hover{color:#fff}.kz2Bea_gameplayFloat{color:#3a2a00;white-space:nowrap;pointer-events:none;background:#ffd27eeb;border-radius:999px;padding:2px 10px;font-size:11px;animation:1.1s ease-out forwards kz2Bea_pet-gameplay-float;position:absolute;top:-8px;left:50%;transform:translate(-50%)}@keyframes kz2Bea_pet-gameplay-float{0%{opacity:0;transform:translate(-50%)translateY(6px)}20%{opacity:1}to{opacity:0;transform:translate(-50%)translateY(-18px)}}@media (prefers-reduced-motion:reduce){.kz2Bea_gameplayFloat{opacity:1;animation:none}.kz2Bea_gameplayBarFill{transition:none}}";
192
+ const css$2 = ".kz2Bea_float{pointer-events:auto;-webkit-user-select:none;user-select:none;will-change:transform;contain:layout style;flex-direction:column;align-items:center;display:flex;position:fixed}.kz2Bea_sprite{image-rendering:auto;touch-action:none;contain:paint}.kz2Bea_spriteWrap{flex:none;position:relative}.kz2Bea_bubble{white-space:nowrap;color:#f4f7ff;pointer-events:none;border-radius:999px;margin-bottom:6px;padding:4px 10px;font-size:12px;line-height:1.4;animation:2.6s ease-out forwards kz2Bea_pet-bubble-pop;position:absolute;bottom:100%;box-shadow:0 2px 8px #00000040}.kz2Bea_bubblePet{background:#4d6bfef2}.kz2Bea_bubbleFeed{background:#0369a1f2}.kz2Bea_bubbleStatus{text-overflow:ellipsis;backdrop-filter:blur(8px);letter-spacing:.02em;background:linear-gradient(160deg,#131c36e6,#070b1af0);border:1px solid #7e98ff73;max-width:min(280px,100vw - 24px);padding:5px 12px;animation:.24s ease-out kz2Bea_pet-bubble-in;overflow:hidden;box-shadow:0 4px 12px #02061759,inset 0 1px #e2e8ff1a,0 0 12px #4d6bfe2e}.kz2Bea_bubbleWhisper{letter-spacing:.03em;animation:.32s cubic-bezier(.22,1,.36,1) kz2Bea_pet-whisper-in}.kz2Bea_bubbleWhisper:before{content:\"「\";opacity:.7;margin-right:2px}.kz2Bea_bubbleWhisper:after{content:\"」\";opacity:.7;margin-left:2px}.kz2Bea_bubbleStack{pointer-events:auto;flex-direction:column-reverse;align-items:center;gap:4px;display:flex;position:absolute;bottom:100%}.kz2Bea_bubbleAnchor{display:inline-flex;position:relative}.kz2Bea_bubbleMore{z-index:1;color:#eef2ff;cursor:pointer;background:linear-gradient(160deg,#4a68f5f2,#2f44b8f2);border:1px solid #7e98ff8c;border-radius:999px;justify-content:center;align-items:center;min-width:18px;height:18px;padding:0 5px;font-size:10px;font-weight:600;line-height:1;transition:filter .12s;display:inline-flex;position:absolute;top:-9px;right:-10px;box-shadow:0 2px 6px #02061766}.kz2Bea_bubbleMore:hover{filter:brightness(1.15)}.kz2Bea_bubbleMore:focus-visible{outline:none;box-shadow:0 0 0 2px #7e98ffe6}.kz2Bea_bubbleStack .kz2Bea_bubble{pointer-events:auto;margin-bottom:0;position:relative;bottom:auto}.kz2Bea_bubbleClickable{cursor:pointer;font:inherit;text-align:center;appearance:none;transition:filter .12s,box-shadow .12s}.kz2Bea_bubbleClickable:hover{filter:brightness(1.12)}.kz2Bea_bubbleClickable:focus-visible{outline:none;box-shadow:0 0 0 2px #7e98ffe6}@keyframes kz2Bea_pet-bubble-in{0%{opacity:0;transform:translateY(6px)scale(.96)}to{opacity:1;transform:translateY(0)scale(1)}}@keyframes kz2Bea_pet-whisper-in{0%{opacity:0;transform:translateY(8px)scale(.9)}60%{transform:translateY(-1px)scale(1.02)}to{opacity:1;transform:translateY(0)scale(1)}}@keyframes kz2Bea_pet-panel-in{0%{opacity:0;transform:translateY(4px)scale(.97)}to{opacity:1;transform:translateY(0)scale(1)}}@keyframes kz2Bea_pet-bubble-pop{0%{opacity:0;transform:translateY(6px)scale(.85)}15%{opacity:1;transform:translateY(0)scale(1.05)}25%{transform:translateY(0)scale(1)}75%{opacity:1}to{opacity:0;transform:translateY(-8px)scale(.95)}}.kz2Bea_panel{color:#e6ebf8;backdrop-filter:blur(10px);transform-origin:50% 0;background:linear-gradient(165deg,#131c36f2,#070b1af7);border:1px solid #7e98ff4d;border-radius:12px;flex-direction:column;gap:7px;min-width:148px;margin-top:8px;padding:10px 12px;font-size:12px;animation:.2s cubic-bezier(.22,1,.36,1) kz2Bea_pet-panel-in;display:flex;position:absolute;top:100%;box-shadow:0 8px 24px #02061773,0 0 0 1px #4d6bfe1a,inset 0 1px #e2e8ff14}.kz2Bea_panel:after{content:\"\";height:14px;position:absolute;bottom:100%;left:0;right:0}.kz2Bea_panelAbove{transform-origin:50% 100%;margin-top:0;margin-bottom:8px;top:auto;bottom:100%}.kz2Bea_panelAbove:after{top:100%;bottom:auto}.kz2Bea_rankRow{white-space:nowrap;justify-content:space-between;gap:10px;display:flex}.kz2Bea_nameCell{letter-spacing:.02em;background:linear-gradient(90deg,#a9c1ff,#6c8bff 60%,#4d6bfe);color:#0000;-webkit-background-clip:text;background-clip:text;font-weight:600}.kz2Bea_statRank{color:#9db4ff;font-weight:600}.kz2Bea_statTreats{color:#7dd3fc;font-weight:600}.kz2Bea_statPoints{color:#fcd34d;font-weight:600}.kz2Bea_renameRow{align-items:center;gap:6px;display:flex}.kz2Bea_nameInput{color:#e6ebf8;background:#131c36e6;border:1px solid #7e98ff80;border-radius:6px;outline:none;flex:1;min-width:0;padding:3px 6px;font-size:12px}.kz2Bea_nameInput:focus{border-color:#4d6bfe;box-shadow:0 0 0 2px #4d6bfe73}.kz2Bea_actions{gap:6px;display:flex}.kz2Bea_action{cursor:pointer;color:#fff;background:linear-gradient(#4a68f5,#3a55e0);border:none;border-radius:6px;flex:1;padding:4px 8px;font-size:12px;font-weight:600;transition:filter .12s,box-shadow .12s,transform .12s;box-shadow:0 2px 6px #4d6bfe4d}.kz2Bea_action:hover{filter:brightness(1.08);transform:translateY(-1px);box-shadow:0 4px 10px #4d6bfe66}.kz2Bea_action:active{filter:brightness(.94);transform:translateY(0);box-shadow:0 1px 4px #4d6bfe4d}.kz2Bea_action:focus-visible{outline:none;box-shadow:0 0 0 2px #7e98ffe6}.kz2Bea_summon{color:#8ea6ff;cursor:pointer;background:#070b1abf;border:1px dashed #7e98ff99;border-radius:999px;padding:2px 10px;font-size:11px;transition:border-color .12s,color .12s,background .12s,box-shadow .12s}.kz2Bea_summon:hover{color:#c3d3ff;background:#070b1ae6;border-color:#7e98fff2}.kz2Bea_summon:active{color:#8ea6ff;border-color:#7e98ffcc}.kz2Bea_summon:focus-visible{outline:none;box-shadow:0 0 0 2px #7e98ffe6}@media (prefers-reduced-motion:reduce){.kz2Bea_bubble,.kz2Bea_bubbleStatus,.kz2Bea_bubbleWhisper,.kz2Bea_panel{opacity:1;animation:none}.kz2Bea_action,.kz2Bea_summon,.kz2Bea_bubbleMore{transition:none}}.kz2Bea_gameplayHud{width:0;height:0;position:absolute;bottom:0;left:0}.kz2Bea_gameplayModeChip{color:#cdd7ff;white-space:nowrap;pointer-events:none;background:#7e98ff2e;border-radius:999px;padding:1px 8px;font-size:10px;position:absolute;top:0;left:0;transform:translateY(-100%)translateY(-4px)}.kz2Bea_gameplayCard{color:#dfe6ff;background:#070b1aeb;border:1px solid #7e98ff40;border-radius:10px;min-width:180px;max-height:340px;padding:10px 12px;font-size:12px;position:absolute;bottom:0;left:0;overflow-y:auto;transform:none;box-shadow:0 8px 24px #0006}.kz2Bea_gameplayBars{flex-direction:column;gap:4px;margin-bottom:8px;display:flex}.kz2Bea_gameplayBarRow{align-items:center;gap:6px;display:flex}.kz2Bea_gameplayBarLabel{color:#9aa8d8;flex-shrink:0;width:34px}.kz2Bea_gameplayBarTrack{background:#7e98ff26;border-radius:3px;flex:1;height:6px;overflow:hidden}.kz2Bea_gameplayBarFill{background:linear-gradient(90deg,#7e98ff,#9db4ff);border-radius:3px;height:100%;transition:width .3s;display:block}.kz2Bea_gameplayActions{flex-wrap:wrap;gap:6px;display:flex}.kz2Bea_gameplayShopItems{grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin-bottom:8px;display:grid}.kz2Bea_gameplayShopItem{color:inherit;cursor:pointer;background:#7e98ff14;border:1px solid #7e98ff40;border-radius:8px;flex-direction:column;align-items:center;gap:4px;padding:8px 6px;display:flex}.kz2Bea_gameplayShopItem:hover{background:#7e98ff29;border-color:#7e98ffb3}.kz2Bea_gameplayShopItemImage{object-fit:contain;width:36px;height:36px;image-rendering:pixelated;pointer-events:none}.kz2Bea_gameplayShopItemLabel{text-align:center;font-size:11px}.kz2Bea_gameplayShopItemPrice{color:#ffd27e;font-size:10px}.kz2Bea_gameplayClose{color:#9aa8d8;cursor:pointer;background:0 0;border:none;padding:0 2px;font-size:14px;position:absolute;top:4px;right:8px}.kz2Bea_gameplayClose:hover{color:#fff}.kz2Bea_gameplayFloat{color:#3a2a00;white-space:nowrap;pointer-events:none;background:#ffd27eeb;border-radius:999px;padding:2px 10px;font-size:11px;animation:1.1s ease-out forwards kz2Bea_pet-gameplay-float;position:absolute;top:-8px;left:50%;transform:translate(-50%)}@keyframes kz2Bea_pet-gameplay-float{0%{opacity:0;transform:translate(-50%)translateY(6px)}20%{opacity:1}to{opacity:0;transform:translate(-50%)translateY(-18px)}}@media (prefers-reduced-motion:reduce){.kz2Bea_gameplayFloat{opacity:1;animation:none}.kz2Bea_gameplayBarFill{transition:none}}.kz2Bea_bubbleUsage{white-space:nowrap;backdrop-filter:blur(8px);letter-spacing:.02em;background:linear-gradient(160deg,#131c36eb,#070b1af2);border:1px solid #7e98ff73;flex-direction:column;align-items:stretch;gap:3px;min-width:128px;max-width:min(280px,100vw - 24px);padding:6px 12px;animation:.28s cubic-bezier(.22,1,.36,1) kz2Bea_pet-usage-in;display:flex;box-shadow:0 4px 12px #02061759,inset 0 1px #e2e8ff1a,0 0 12px #4d6bfe2e}.kz2Bea_bubbleUsageHead{justify-content:space-between;align-items:baseline;gap:10px;display:flex}.kz2Bea_bubbleUsageTitle{text-overflow:ellipsis;opacity:.85;overflow:hidden}.kz2Bea_bubbleUsageValue{font-variant-numeric:tabular-nums;flex-shrink:0;font-weight:600}.kz2Bea_bubbleUsageNote{text-overflow:ellipsis;opacity:.55;font-size:10px;overflow:hidden}.kz2Bea_bubbleUsageMeter{background:#e2e8ff24;border-radius:999px;height:4px;display:block;overflow:hidden}.kz2Bea_bubbleUsageMeterFill{background:#6ee7b7;border-radius:999px;height:100%;transition:width .3s;display:block}.kz2Bea_bubbleUsageOk{border-color:#7e98ff73}.kz2Bea_bubbleUsageWarn{border-color:#fbbf248c}.kz2Bea_bubbleUsageWarn .kz2Bea_bubbleUsageMeterFill{background:#fbbf24}.kz2Bea_bubbleUsageLow{border-color:#f8717199}.kz2Bea_bubbleUsageLow .kz2Bea_bubbleUsageMeterFill{background:#f87171}@keyframes kz2Bea_pet-usage-in{0%{opacity:0;transform:translateY(6px)scale(.96)}to{opacity:1;transform:translateY(0)scale(1)}}@media (prefers-reduced-motion:reduce){.kz2Bea_bubbleUsage{animation:none}}";
175
193
  const tagId$2 = "@linxin666/dsh-pet/pet.module.css";
176
194
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
177
195
  const tag = document.createElement("style");
@@ -191,6 +209,16 @@ window.__ModuleLoader__.load({
191
209
  "bubblePet": "kz2Bea_bubblePet",
192
210
  "bubbleStack": "kz2Bea_bubbleStack",
193
211
  "bubbleStatus": "kz2Bea_bubbleStatus",
212
+ "bubbleUsage": "kz2Bea_bubbleUsage",
213
+ "bubbleUsageHead": "kz2Bea_bubbleUsageHead",
214
+ "bubbleUsageLow": "kz2Bea_bubbleUsageLow",
215
+ "bubbleUsageMeter": "kz2Bea_bubbleUsageMeter",
216
+ "bubbleUsageMeterFill": "kz2Bea_bubbleUsageMeterFill",
217
+ "bubbleUsageNote": "kz2Bea_bubbleUsageNote",
218
+ "bubbleUsageOk": "kz2Bea_bubbleUsageOk",
219
+ "bubbleUsageTitle": "kz2Bea_bubbleUsageTitle",
220
+ "bubbleUsageValue": "kz2Bea_bubbleUsageValue",
221
+ "bubbleUsageWarn": "kz2Bea_bubbleUsageWarn",
194
222
  "bubbleWhisper": "kz2Bea_bubbleWhisper",
195
223
  "float": "kz2Bea_float",
196
224
  "gameplayActions": "kz2Bea_gameplayActions",
@@ -217,6 +245,7 @@ window.__ModuleLoader__.load({
217
245
  "pet-bubble-pop": "kz2Bea_pet-bubble-pop",
218
246
  "pet-gameplay-float": "kz2Bea_pet-gameplay-float",
219
247
  "pet-panel-in": "kz2Bea_pet-panel-in",
248
+ "pet-usage-in": "kz2Bea_pet-usage-in",
220
249
  "pet-whisper-in": "kz2Bea_pet-whisper-in",
221
250
  "rankRow": "kz2Bea_rankRow",
222
251
  "renameRow": "kz2Bea_renameRow",
@@ -319,6 +348,54 @@ window.__ModuleLoader__.load({
319
348
  });
320
349
  }
321
350
  /**
351
+ * The announcement bubble (dsh-usage linkage): a dedicated, specially
352
+ * designed surface for sibling-plugin facts — a balance or today-spend pill,
353
+ * or a plan-quota card with a tone-tinted accent, a mini meter for percent
354
+ * windows, and the reset instant. It rides the top of the session bubble
355
+ * stack (column-reverse puts the DOM-last child farthest from the sprite)
356
+ * and persists for its TTL instead of the short feedback pop.
357
+ */
358
+ function UsageAnnouncementBubble(props) {
359
+ const { announcement } = props;
360
+ const tone = announcement.tone === "low" ? pet_module_css_default.bubbleUsageLow : announcement.tone === "warn" ? pet_module_css_default.bubbleUsageWarn : pet_module_css_default.bubbleUsageOk;
361
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
362
+ className: clsx(pet_module_css_default.bubble, pet_module_css_default.bubbleUsage, tone),
363
+ role: "status",
364
+ "aria-live": "polite",
365
+ "data-dsh-pet-announcement": announcement.source,
366
+ children: [
367
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
368
+ className: pet_module_css_default.bubbleUsageHead,
369
+ children: [
370
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
371
+ className: pet_module_css_default.bubbleUsageTitle,
372
+ children: announcement.title
373
+ }),
374
+ (announcement.kind === "balance" || announcement.kind === "cost") && announcement.amount !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
375
+ className: pet_module_css_default.bubbleUsageValue,
376
+ children: announcement.amount
377
+ }),
378
+ announcement.kind === "plan" && announcement.percent !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
379
+ className: pet_module_css_default.bubbleUsageValue,
380
+ children: Math.round(announcement.percent) + "%"
381
+ })
382
+ ]
383
+ }),
384
+ announcement.kind === "plan" && announcement.percent !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
385
+ className: pet_module_css_default.bubbleUsageMeter,
386
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
387
+ className: pet_module_css_default.bubbleUsageMeterFill,
388
+ style: { width: Math.min(100, Math.max(0, announcement.percent)) + "%" }
389
+ })
390
+ }),
391
+ announcement.note !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
392
+ className: pet_module_css_default.bubbleUsageNote,
393
+ children: announcement.note
394
+ })
395
+ ]
396
+ });
397
+ }
398
+ /**
322
399
  * The floating pet. The spritesheet frame advances on requestAnimationFrame
323
400
  * with per-frame durations from the definition's tracks; the atlas image is
324
401
  * loaded once and the background position is written straight to the sprite
@@ -517,7 +594,9 @@ window.__ModuleLoader__.load({
517
594
  const stackOpen = stackPeek || stackPinned;
518
595
  const visibleSessions = !stackOpen && sessionBubbles.length > 1 ? sessionBubbles.slice(0, 1) : sessionBubbles;
519
596
  const statusBubble = feedback === null && sessionBubbles.length === 0 ? snapshot?.bubble : void 0;
520
- const bubblePresent = feedback !== null || sessionBubbles.length > 0 || statusBubble !== void 0;
597
+ const announcement = snapshot?.announcement;
598
+ const usageAnnouncement = feedback === null && announcement !== void 0 && announcementFresh(announcement, Date.now()) ? announcement : void 0;
599
+ const bubblePresent = feedback !== null || sessionBubbles.length > 0 || statusBubble !== void 0 || usageAnnouncement !== void 0;
521
600
  const displayName = snapshot?.name ?? definition.displayName;
522
601
  const decoration = snapshot?.decoration;
523
602
  (0, react.useEffect)(() => {
@@ -614,50 +693,54 @@ window.__ModuleLoader__.load({
614
693
  className: clsx(pet_module_css_default.bubble, feedback.kind === "feed" ? pet_module_css_default.bubbleFeed : pet_module_css_default.bubblePet),
615
694
  children: feedback.text
616
695
  }, feedback.at),
617
- feedback === null && (sessionBubbles.length > 0 || statusBubble !== void 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
696
+ feedback === null && (sessionBubbles.length > 0 || statusBubble !== void 0 || usageAnnouncement !== void 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
618
697
  ref: bubbleRef,
619
698
  className: pet_module_css_default.bubbleStack,
620
699
  onPointerEnter: () => setStackPeek(true),
621
700
  onPointerLeave: () => setStackPeek(false),
622
- children: [visibleSessions.map((session, index) => {
623
- const speaksWhisper = session.whisper !== void 0;
624
- const bubble = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
625
- type: "button",
626
- className: clsx(pet_module_css_default.bubble, pet_module_css_default.bubbleStatus, pet_module_css_default.bubbleClickable, speaksWhisper && pet_module_css_default.bubbleWhisper),
627
- title: props.t("pet.openSessionHint"),
628
- onClick: () => {
629
- props.onOpenSession(session.sessionId);
630
- },
631
- children: [index === 0 && !speaksWhisper && decoration !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusOrnament, {
632
- decoration,
633
- phase
634
- }), session.whisper ?? session.bubble]
635
- }, speaksWhisper ? "whisper:" + session.whisper : session.sessionId);
636
- if (index !== 0 || sessionBubbles.length <= 1) return bubble;
637
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
638
- className: pet_module_css_default.bubbleAnchor,
639
- children: [bubble, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
701
+ children: [
702
+ visibleSessions.map((session, index) => {
703
+ const speaksWhisper = session.whisper !== void 0;
704
+ const bubble = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
640
705
  type: "button",
641
- className: pet_module_css_default.bubbleMore,
642
- title: stackOpen ? props.t("pet.collapseSessions") : props.t("pet.moreSessions", { n: sessionBubbles.length - 1 }),
643
- "aria-label": stackOpen ? props.t("pet.collapseSessions") : props.t("pet.moreSessions", { n: sessionBubbles.length - 1 }),
644
- "aria-expanded": stackOpen,
645
- onClick: (e) => {
646
- e.stopPropagation();
647
- setStackPinned((open) => !open);
706
+ className: clsx(pet_module_css_default.bubble, pet_module_css_default.bubbleStatus, pet_module_css_default.bubbleClickable, speaksWhisper && pet_module_css_default.bubbleWhisper),
707
+ title: props.t("pet.openSessionHint"),
708
+ onClick: () => {
709
+ props.onOpenSession(session.sessionId);
648
710
  },
649
- children: stackOpen ? "×" : "+" + String(sessionBubbles.length - 1)
650
- })]
651
- }, "primary");
652
- }), sessionBubbles.length === 0 && statusBubble !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
653
- className: clsx(pet_module_css_default.bubble, pet_module_css_default.bubbleStatus),
654
- role: "status",
655
- "aria-live": "polite",
656
- children: [decoration !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusOrnament, {
657
- decoration,
658
- phase
659
- }), statusBubble]
660
- }, "status")]
711
+ children: [index === 0 && !speaksWhisper && decoration !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusOrnament, {
712
+ decoration,
713
+ phase
714
+ }), session.whisper ?? session.bubble]
715
+ }, speaksWhisper ? "whisper:" + session.whisper : session.sessionId);
716
+ if (index !== 0 || sessionBubbles.length <= 1) return bubble;
717
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
718
+ className: pet_module_css_default.bubbleAnchor,
719
+ children: [bubble, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
720
+ type: "button",
721
+ className: pet_module_css_default.bubbleMore,
722
+ title: stackOpen ? props.t("pet.collapseSessions") : props.t("pet.moreSessions", { n: sessionBubbles.length - 1 }),
723
+ "aria-label": stackOpen ? props.t("pet.collapseSessions") : props.t("pet.moreSessions", { n: sessionBubbles.length - 1 }),
724
+ "aria-expanded": stackOpen,
725
+ onClick: (e) => {
726
+ e.stopPropagation();
727
+ setStackPinned((open) => !open);
728
+ },
729
+ children: stackOpen ? "×" : "+" + String(sessionBubbles.length - 1)
730
+ })]
731
+ }, "primary");
732
+ }),
733
+ sessionBubbles.length === 0 && statusBubble !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
734
+ className: clsx(pet_module_css_default.bubble, pet_module_css_default.bubbleStatus),
735
+ role: "status",
736
+ "aria-live": "polite",
737
+ children: [decoration !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusOrnament, {
738
+ decoration,
739
+ phase
740
+ }), statusBubble]
741
+ }, "status"),
742
+ usageAnnouncement !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageAnnouncementBubble, { announcement: usageAnnouncement })
743
+ ]
661
744
  }),
662
745
  hovered && dragRef.current === null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
663
746
  ref: panelRef,
@@ -712,7 +795,12 @@ window.__ModuleLoader__.load({
712
795
  children: displayName
713
796
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
714
797
  className: pet_module_css_default.statRank,
715
- children: panelStat("rank", "pet.rank", { rank: snapshot?.affinity.rank ?? "?" })
798
+ children: (() => {
799
+ const rawRank = snapshot?.affinity.rank ?? "?";
800
+ const rankKey = `pet.rank.name.${rawRank}`;
801
+ const localized = props.t(rankKey);
802
+ return panelStat("rank", "pet.rank", { rank: localized !== rankKey ? localized : rawRank });
803
+ })()
716
804
  })]
717
805
  }),
718
806
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -761,7 +849,7 @@ window.__ModuleLoader__.load({
761
849
  ] })
762
850
  })
763
851
  ]
764
- }), document.body);
852
+ }), props.portalTarget ?? document.body);
765
853
  }
766
854
  //#endregion
767
855
  //#region src/client/drag-stream.ts
@@ -1431,10 +1519,11 @@ window.__ModuleLoader__.load({
1431
1519
  * it must not ride a session-scoped slot — on the new-conversation screen no
1432
1520
  * session exists to scope a slot by, and the pet would vanish (issue #48).
1433
1521
  * The client half therefore mounts this entry straight onto 'document.body'
1434
- * (see index.ts): while visible it renders the floating PetSprite (a
1435
- * portal), while hidden it renders a fixed-position summon button. Which
1436
- * sprite renders is decided by the host snapshot's pet id resolved against
1437
- * the registry list — no per-pet component exists.
1522
+ * (see index.ts): while visible it renders the floating PetSprite (a portal
1523
+ * into the plugin root, so the root owns the whole surface), while hidden it
1524
+ * renders a fixed-position summon button. Which sprite renders is decided by
1525
+ * the host snapshot's pet id resolved against the registry list — no per-pet
1526
+ * component exists.
1438
1527
  * @module @linxin666/dsh-pet/client/PetDockEntry
1439
1528
  */
1440
1529
  const DEFAULT_DISPLAY = {
@@ -1492,6 +1581,7 @@ window.__ModuleLoader__.load({
1492
1581
  onRename: props.rename,
1493
1582
  onOpenSession: props.openSession,
1494
1583
  onFeedbackDone: props.feedbackDone,
1584
+ portalTarget: props.portalTarget,
1495
1585
  dragDisabled: snapshot.gameplay?.mode === "work",
1496
1586
  ...gameplay === void 0 || aux === null ? {} : {
1497
1587
  onGameplayTap: (fx, fy) => aux.bus.tap?.(fx, fy),
@@ -1811,6 +1901,18 @@ window.__ModuleLoader__.load({
1811
1901
  * phase-mapped track. Rendering never throws: a broken track list degrades
1812
1902
  * to the first decodable frame, and the 1.2 s stall watchdog re-kicks the
1813
1903
  * playback chain after timer throttling.
1904
+ *
1905
+ * Frame presentation has two modes picked once at mount by capability
1906
+ * probing:
1907
+ * - Canvas bitmap buffer (default where createImageBitmap/fetch/2D context
1908
+ * exist): every frame is decoded exactly once into an ImageBitmap during
1909
+ * the warm pass and drawn onto one <canvas> - steady-state playback issues
1910
+ * zero DOM mutations and zero re-decodes (measured hotspot: swapping
1911
+ * <img>.src per frame drove image decode + invalidation every tick).
1912
+ * - Classic <img> fallback (jsdom/tests or missing APIs): identical to the
1913
+ * historical behavior - cache-warm Image elements plus guarded src swaps,
1914
+ * so environments without modern decoding keep working unchanged.
1915
+ *
1814
1916
  * @module @linxin666/dsh-pet/client/renderers/frames2d
1815
1917
  */
1816
1918
  /** Stall watchdog period: a looping track stuck longer re-kicks its chain. */
@@ -1844,19 +1946,79 @@ window.__ModuleLoader__.load({
1844
1946
  validateConfig: validateFrames2dConfig,
1845
1947
  mount(ctx, config) {
1846
1948
  const reducedMotion = typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1847
- const img = document.createElement("img");
1848
- img.dataset.dshPetFrames2d = ctx.petId;
1849
- img.alt = "";
1850
- img.draggable = false;
1851
- img.style.width = "100%";
1852
- img.style.height = "100%";
1853
- img.style.objectFit = "contain";
1854
- img.style.pointerEvents = "none";
1855
- ctx.container.appendChild(img);
1856
- for (const track of Object.values(config.tracks)) for (const url of track.frames) {
1857
- const pre = new Image();
1858
- pre.src = url;
1949
+ let canvas = null;
1950
+ let context2d = null;
1951
+ let img = null;
1952
+ try {
1953
+ if (typeof createImageBitmap === "function" && typeof fetch === "function") {
1954
+ const probe = document.createElement("canvas");
1955
+ const c2d = probe.getContext("2d");
1956
+ if (c2d !== null) {
1957
+ canvas = probe;
1958
+ context2d = c2d;
1959
+ }
1960
+ }
1961
+ } catch {
1962
+ canvas = null;
1963
+ context2d = null;
1859
1964
  }
1965
+ if (canvas !== null && context2d !== null) {
1966
+ canvas.dataset.dshPetFrames2d = ctx.petId;
1967
+ canvas.draggable = false;
1968
+ canvas.style.width = "100%";
1969
+ canvas.style.height = "100%";
1970
+ canvas.style.objectFit = "contain";
1971
+ canvas.style.pointerEvents = "none";
1972
+ ctx.container.appendChild(canvas);
1973
+ } else {
1974
+ img = document.createElement("img");
1975
+ img.dataset.dshPetFrames2d = ctx.petId;
1976
+ img.alt = "";
1977
+ img.draggable = false;
1978
+ img.style.width = "100%";
1979
+ img.style.height = "100%";
1980
+ img.style.objectFit = "contain";
1981
+ img.style.pointerEvents = "none";
1982
+ ctx.container.appendChild(img);
1983
+ }
1984
+ const decoding = /* @__PURE__ */ new Map();
1985
+ const decodedAll = [];
1986
+ const loadFrame = (url) => {
1987
+ const cached = decoding.get(url);
1988
+ if (cached !== void 0) return cached;
1989
+ const job = (async () => {
1990
+ try {
1991
+ const response = await fetch(url);
1992
+ if (!response.ok) throw new Error("http " + response.status);
1993
+ const bitmap = await createImageBitmap(await response.blob());
1994
+ return {
1995
+ source: bitmap,
1996
+ width: bitmap.width,
1997
+ height: bitmap.height
1998
+ };
1999
+ } catch {
2000
+ return await new Promise((resolve) => {
2001
+ try {
2002
+ const pre = new Image();
2003
+ pre.onload = () => {
2004
+ resolve(pre.naturalWidth > 0 ? {
2005
+ source: pre,
2006
+ width: pre.naturalWidth,
2007
+ height: pre.naturalHeight
2008
+ } : void 0);
2009
+ };
2010
+ pre.onerror = () => resolve(void 0);
2011
+ pre.src = url;
2012
+ } catch {
2013
+ resolve(void 0);
2014
+ }
2015
+ });
2016
+ }
2017
+ })();
2018
+ decoding.set(url, job);
2019
+ decodedAll.push(job.then(() => void 0, () => void 0));
2020
+ return job;
2021
+ };
1860
2022
  let disposed = false;
1861
2023
  let timer;
1862
2024
  let watchdog;
@@ -1864,13 +2026,35 @@ window.__ModuleLoader__.load({
1864
2026
  let frameIndex = 0;
1865
2027
  let lastAdvance = Date.now();
1866
2028
  let override;
2029
+ let drawToken = 0;
2030
+ let lastDrawnUrl;
1867
2031
  const trackForPhase = (phase) => {
1868
2032
  const mapped = config.phases[phase];
1869
2033
  return mapped !== void 0 && config.tracks[mapped] !== void 0 ? mapped : config.phases.idle;
1870
2034
  };
2035
+ /** Canvas path: paints the newest requested frame; stale draws drop out. */
2036
+ const paintCanvas = (url) => {
2037
+ if (context2d === null || canvas === null) return;
2038
+ const myToken = ++drawToken;
2039
+ loadFrame(url).then((frame) => {
2040
+ if (disposed || frame === void 0 || myToken !== drawToken) return;
2041
+ if (lastDrawnUrl === url) return;
2042
+ lastDrawnUrl = url;
2043
+ if (canvas.width !== frame.width || canvas.height !== frame.height) {
2044
+ canvas.width = frame.width;
2045
+ canvas.height = frame.height;
2046
+ } else context2d.clearRect(0, 0, canvas.width, canvas.height);
2047
+ context2d.drawImage(frame.source, 0, 0);
2048
+ }).catch(() => {});
2049
+ };
1871
2050
  const show = (trackId, index) => {
1872
2051
  const url = config.tracks[trackId]?.frames[index];
1873
- if (url !== void 0 && img.getAttribute("src") !== url) img.src = url;
2052
+ if (url === void 0) return;
2053
+ if (img !== null) {
2054
+ if (img.getAttribute("src") !== url) img.src = url;
2055
+ return;
2056
+ }
2057
+ paintCanvas(url);
1874
2058
  };
1875
2059
  const schedule = (ms) => {
1876
2060
  if (disposed) return;
@@ -1922,6 +2106,7 @@ window.__ModuleLoader__.load({
1922
2106
  const expected = (def.durations[frameIndex] ?? 200) + WATCHDOG_MS;
1923
2107
  if (Date.now() - lastAdvance > expected) tick();
1924
2108
  }, WATCHDOG_MS);
2109
+ for (const warmTrack of Object.values(config.tracks)) for (const warmUrl of warmTrack.frames) loadFrame(warmUrl);
1925
2110
  play(track);
1926
2111
  let disposedOnce = false;
1927
2112
  const dispose = () => {
@@ -1931,7 +2116,17 @@ window.__ModuleLoader__.load({
1931
2116
  unsubscribe();
1932
2117
  if (timer !== void 0) clearTimeout(timer);
1933
2118
  if (watchdog !== void 0) clearInterval(watchdog);
1934
- img.remove();
2119
+ Promise.allSettled(decodedAll).then(() => {
2120
+ for (const job of decoding.values()) job.then((frame) => {
2121
+ try {
2122
+ const maybeClose = (frame?.source)?.close;
2123
+ if (typeof maybeClose === "function" && frame !== void 0) maybeClose.call(frame.source);
2124
+ } catch {}
2125
+ }).catch(() => {});
2126
+ decoding.clear();
2127
+ });
2128
+ canvas?.remove();
2129
+ img?.remove();
1935
2130
  };
1936
2131
  ctx.onCleanup(dispose);
1937
2132
  return {
@@ -2584,7 +2779,7 @@ window.__ModuleLoader__.load({
2584
2779
  }
2585
2780
  /** Publish a projection of this form, rebuilt whenever the scope or a draft changes. */
2586
2781
  bind(project) {
2587
- const store = (0, _deepseek_ai_dsh_client_runtime_client.createSnapshotStore)(project());
2782
+ const store = createSnapshotStore(project());
2588
2783
  this.listeners.add(() => {
2589
2784
  store.set(project());
2590
2785
  });
@@ -2599,7 +2794,7 @@ window.__ModuleLoader__.load({
2599
2794
  exposed: snapshot.status === "ready",
2600
2795
  writable: snapshot.writable,
2601
2796
  dirty: plan.length > 0,
2602
- invalid: plan.some((item) => item.run === void 0),
2797
+ invalid: plan.some((item) => item.judge === void 0),
2603
2798
  saving: this.saving,
2604
2799
  failed: this.failed,
2605
2800
  ...this.failedReason === void 0 ? {} : { failedReason: this.failedReason }
@@ -2649,45 +2844,52 @@ window.__ModuleLoader__.load({
2649
2844
  };
2650
2845
  }
2651
2846
  /**
2652
- * Write every staged edit, then re-seed from what the Host accepted.
2847
+ * Write every staged edit in one atomic scope mutation, then re-seed from
2848
+ * what the Host accepted.
2653
2849
  *
2654
- * When the scope carries the optional batch surface (the dsh-web
2655
- * bridge scope), every planned write rides one mutation so cross-field
2656
- * validate hooks (baseURL+model) judge the batch as a unit instead of
2657
- * deadlocking on per-field writes. Otherwise the per-field loop runs.
2658
- * A field lands only when the Host reports it held the staged value; a
2659
- * landed field's draft is dropped, a failed one stays staged for the user.
2660
- * @returns settlement after every write and the read-back.
2850
+ * The whole batch rides one mutate, so cross-field validate hooks
2851
+ * (baseURL+model) judge it as a unit: the Host either applies every write
2852
+ * or refuses the batch. The 0.1.2 scope contract never rejects a refused
2853
+ * mutation the scope recovers with a fresh Host view and resolves — so
2854
+ * resolution alone proves nothing: the outcome is judged by reading the
2855
+ * settled snapshot back, one planned write at a time, and one missed write
2856
+ * fails the whole save. A scope that still rejects on refusal (the dsh-web
2857
+ * bridge scope) reports through the same failure path with its rejection
2858
+ * message. A save that did not land keeps its drafts, so the user can
2859
+ * correct them instead of retyping.
2860
+ * @returns settlement after the mutation and the read-back.
2661
2861
  */
2662
2862
  async save() {
2663
2863
  const plan = this.plan();
2664
- const valid = plan.filter((item) => item.run !== void 0);
2864
+ const valid = plan.filter((item) => item.judge !== void 0);
2665
2865
  if (plan.length === 0 || this.saving || valid.length !== plan.length) return;
2666
- const plannedWrites = valid.map((item) => item.op);
2667
2866
  const pending = /* @__PURE__ */ new Map();
2668
2867
  for (const item of plan) pending.set(item.field, this.staged.get(item.field));
2669
2868
  this.saving = true;
2670
2869
  this.failed = false;
2671
2870
  this.failedReason = void 0;
2672
2871
  this.publish();
2673
- const landed = /* @__PURE__ */ new Set();
2674
- const batch = this.batchedScope();
2675
- if (batch !== void 0) {
2676
- const result = await batch.mutate(plannedWrites);
2677
- if (result.ok) {
2678
- for (const field of result.fields) if (field.landed) landed.add(field.field);
2679
- } else this.failedReason = result.message;
2680
- } else for (const item of valid) if (await item.run()) landed.add(item.field);
2681
- for (const [field, before] of pending) if (landed.has(field) && this.staged.get(field) === before) this.staged.delete(field);
2872
+ const ops = valid.map((item) => item.op.op === "set" ? {
2873
+ op: "set",
2874
+ path: [item.field],
2875
+ value: item.op.value
2876
+ } : {
2877
+ op: "unset",
2878
+ path: [item.field]
2879
+ });
2880
+ let failedReason;
2881
+ try {
2882
+ await this.scope.mutate(ops);
2883
+ } catch (error) {
2884
+ failedReason = error instanceof Error ? error.message : String(error);
2885
+ }
2886
+ const landed = failedReason === void 0 && valid.every((item) => item.judge());
2887
+ for (const [field, before] of pending) if (landed && this.staged.get(field) === before) this.staged.delete(field);
2682
2888
  this.saving = false;
2683
- this.failed = landed.size !== pending.size;
2889
+ this.failed = !landed;
2890
+ this.failedReason = failedReason;
2684
2891
  this.publish();
2685
2892
  }
2686
- /** The scope's batch surface when it supports one; undefined conservatively otherwise. */
2687
- batchedScope() {
2688
- const candidate = this.scope;
2689
- return typeof candidate?.mutate === "function" ? candidate : void 0;
2690
- }
2691
2893
  /**
2692
2894
  * Every staged edit a save would write. An entry whose draft is not a value
2693
2895
  * its field accepts carries no write: the form is still dirty, and the save
@@ -2706,7 +2908,7 @@ window.__ModuleLoader__.load({
2706
2908
  field,
2707
2909
  op: "unset"
2708
2910
  },
2709
- run: () => this.clear(field)
2911
+ judge: () => this.landedUnset(field)
2710
2912
  });
2711
2913
  continue;
2712
2914
  }
@@ -2718,7 +2920,7 @@ window.__ModuleLoader__.load({
2718
2920
  field,
2719
2921
  op: "unset"
2720
2922
  },
2721
- run: void 0
2923
+ judge: void 0
2722
2924
  });
2723
2925
  else if (write.kind === "clear") plan.push({
2724
2926
  field,
@@ -2726,7 +2928,7 @@ window.__ModuleLoader__.load({
2726
2928
  field,
2727
2929
  op: "unset"
2728
2930
  },
2729
- run: () => this.clear(field)
2931
+ judge: () => this.landedUnset(field)
2730
2932
  });
2731
2933
  else plan.push({
2732
2934
  field,
@@ -2735,20 +2937,26 @@ window.__ModuleLoader__.load({
2735
2937
  op: "set",
2736
2938
  value: write.value
2737
2939
  },
2738
- run: () => this.store(field, write.value)
2940
+ judge: () => this.landedSet(field, write.value)
2739
2941
  });
2740
2942
  }
2741
2943
  return plan;
2742
2944
  }
2743
- async clear(field) {
2744
- await this.scope.unset(field);
2745
- return !this.stored(field);
2746
- }
2747
- async store(field, value) {
2748
- await this.scope.set(field, value);
2945
+ /**
2946
+ * Read-back judgment for a planned set: the user layer must hold the
2947
+ * intended value once the mutation has settled.
2948
+ */
2949
+ landedSet(field, value) {
2749
2950
  if (this.specOf(field).secret) return true;
2750
2951
  return this.userLayer()?.[field] === value;
2751
2952
  }
2953
+ /**
2954
+ * Read-back judgment for a planned unset: the field must be gone from the
2955
+ * user layer once the mutation has settled.
2956
+ */
2957
+ landedUnset(field) {
2958
+ return !this.stored(field);
2959
+ }
2752
2960
  stage(field, edit) {
2753
2961
  this.staged.set(field, edit);
2754
2962
  this.failed = false;
@@ -3081,6 +3289,15 @@ window.__ModuleLoader__.load({
3081
3289
  "pet.namePlaceholder": "输入新名字",
3082
3290
  "pet.summon": "召唤{name}",
3083
3291
  "pet.rank": "亲密度 {rank}",
3292
+ "pet.rank.name.幼鲸": "幼鲸",
3293
+ "pet.rank.name.伙伴": "伙伴",
3294
+ "pet.rank.name.挚友": "挚友",
3295
+ "pet.rank.name.深海羁绊": "深海羁绊",
3296
+ "pet.rank.name.心有灵犀": "心有灵犀",
3297
+ "pet.rank.name.传说羁绊": "传说羁绊",
3298
+ "pet.rank.name.神话羁绊": "神话羁绊",
3299
+ "pet.rank.name.永恒之契": "永恒之契",
3300
+ "pet.rank.name.鲸生共渡": "鲸生共渡",
3084
3301
  "pet.points": "{points} 点",
3085
3302
  "pet.treats": "小鱼干 ×{n}",
3086
3303
  "pet.state.loading": "宠物正在赶来…",
@@ -3131,7 +3348,7 @@ window.__ModuleLoader__.load({
3131
3348
  "settings.off": "关",
3132
3349
  "settings.overridden": "已覆盖",
3133
3350
  "settings.reset": "恢复默认",
3134
- "settings.notExposed": "当前 DSH 版本未向设置页暴露本插件的配置命名空间,表单不可用。可编辑 ~/.dsh/settings.yaml 直接配置,或为 dsh-host-apiproxy 的 WEB_SETTINGS_NAMESPACES 白名单补充本命名空间后重启。",
3351
+ "settings.notExposed": "当前 DSH 版本未向设置页暴露本插件的配置命名空间,表单不可用。可编辑 $DSH_HOME/settings.yaml 直接配置,或确认提供该命名空间的插件已挂载其设置域并重启。",
3135
3352
  "settings.readOnly": "当前部署的设置只读。",
3136
3353
  "settings.expand": "展开设置",
3137
3354
  "settings.collapse": "收起设置",
@@ -3151,6 +3368,15 @@ window.__ModuleLoader__.load({
3151
3368
  "pet.namePlaceholder": "Enter a new name",
3152
3369
  "pet.summon": "Summon {name}",
3153
3370
  "pet.rank": "Affinity {rank}",
3371
+ "pet.rank.name.幼鲸": "Baby Whale",
3372
+ "pet.rank.name.伙伴": "Companion",
3373
+ "pet.rank.name.挚友": "Close Friend",
3374
+ "pet.rank.name.深海羁绊": "Deep Sea Bond",
3375
+ "pet.rank.name.心有灵犀": "Kindred Spirit",
3376
+ "pet.rank.name.传说羁绊": "Legendary Bond",
3377
+ "pet.rank.name.神话羁绊": "Mythic Bond",
3378
+ "pet.rank.name.永恒之契": "Eternal Covenant",
3379
+ "pet.rank.name.鲸生共渡": "Lifelong Companion",
3154
3380
  "pet.points": "{points} pts",
3155
3381
  "pet.treats": "Treats ×{n}",
3156
3382
  "pet.state.loading": "The pet is on its way…",
@@ -3201,7 +3427,7 @@ window.__ModuleLoader__.load({
3201
3427
  "settings.off": "Off",
3202
3428
  "settings.overridden": "Overridden",
3203
3429
  "settings.reset": "Reset to default",
3204
- "settings.notExposed": "This DSH version does not expose this plugin's settings namespace to the configuration page, so the form is unavailable. Edit ~/.dsh/settings.yaml directly, or add the namespace to dsh-host-apiproxy's WEB_SETTINGS_NAMESPACES allowlist and restart.",
3430
+ "settings.notExposed": "This DSH version does not expose this plugin's settings namespace to the configuration page, so the form is unavailable. Edit $DSH_HOME/settings.yaml directly, or confirm that the plugin owning the namespace is mounted with its settings domain and restart.",
3205
3431
  "settings.readOnly": "This deployment stores settings read-only.",
3206
3432
  "settings.expand": "Show settings",
3207
3433
  "settings.collapse": "Hide settings",
@@ -3242,7 +3468,7 @@ window.__ModuleLoader__.load({
3242
3468
  /** The building package's version, when the bundle carries it. */
3243
3469
  function bakedVersion() {
3244
3470
  try {
3245
- return "0.3.5";
3471
+ return "0.3.9";
3246
3472
  } catch {
3247
3473
  return;
3248
3474
  }
@@ -3524,7 +3750,8 @@ window.__ModuleLoader__.load({
3524
3750
  const petRoot = (0, react_dom_client.createRoot)(container);
3525
3751
  petRoot.render((0, react.createElement)(PetDockEntry, {
3526
3752
  ...injected(),
3527
- t
3753
+ t,
3754
+ portalTarget: container
3528
3755
  }));
3529
3756
  let uiGone = false;
3530
3757
  disposeUi = () => {