@linxin666/dsh-pet 0.3.3 → 0.3.5

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 (123) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +30 -13
  3. package/README.zh.md +30 -13
  4. package/assets/ouo-neko/pet.json +31 -0
  5. package/assets/ouo-neko/previews/failed.gif +0 -0
  6. package/assets/ouo-neko/previews/idle.gif +0 -0
  7. package/assets/ouo-neko/previews/jumping.gif +0 -0
  8. package/assets/ouo-neko/previews/review.gif +0 -0
  9. package/assets/ouo-neko/previews/running-left.gif +0 -0
  10. package/assets/ouo-neko/previews/running-right.gif +0 -0
  11. package/assets/ouo-neko/previews/running.gif +0 -0
  12. package/assets/ouo-neko/previews/waiting.gif +0 -0
  13. package/assets/ouo-neko/previews/waving.gif +0 -0
  14. package/assets/ouo-neko/spritesheet.webp +0 -0
  15. package/contracts/pet-manifest-v2.schema.json +102 -1
  16. package/contracts/voice-pack-v1.schema.json +23 -28
  17. package/lib/client.js +781 -40
  18. package/lib/client.js.map +1 -1
  19. package/lib/index.js +1567 -482
  20. package/lib/types/chatter.d.ts +68 -46
  21. package/lib/types/chatter.d.ts.map +1 -1
  22. package/lib/types/chatter.js +243 -301
  23. package/lib/types/client/PetDockEntry.d.ts +3 -0
  24. package/lib/types/client/PetDockEntry.d.ts.map +1 -1
  25. package/lib/types/client/PetDockEntry.js +18 -2
  26. package/lib/types/client/PetSprite.d.ts +23 -0
  27. package/lib/types/client/PetSprite.d.ts.map +1 -1
  28. package/lib/types/client/PetSprite.js +49 -45
  29. package/lib/types/client/PluginSettingsCard.d.ts +2 -0
  30. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
  31. package/lib/types/client/PluginSettingsCard.js +2 -2
  32. package/lib/types/client/drag-stream.d.ts +19 -0
  33. package/lib/types/client/drag-stream.d.ts.map +1 -0
  34. package/lib/types/client/drag-stream.js +26 -0
  35. package/lib/types/client/gameplay-hud.d.ts +50 -0
  36. package/lib/types/client/gameplay-hud.d.ts.map +1 -0
  37. package/lib/types/client/gameplay-hud.js +274 -0
  38. package/lib/types/client/index.d.ts.map +1 -1
  39. package/lib/types/client/index.js +38 -9
  40. package/lib/types/client/locales.d.ts +34 -0
  41. package/lib/types/client/locales.d.ts.map +1 -1
  42. package/lib/types/client/locales.js +36 -0
  43. package/lib/types/client/pet-store.d.ts +7 -1
  44. package/lib/types/client/pet-store.d.ts.map +1 -1
  45. package/lib/types/client/pet-store.js +4 -0
  46. package/lib/types/client/renderers/Frames2dVisualMount.d.ts +29 -0
  47. package/lib/types/client/renderers/Frames2dVisualMount.d.ts.map +1 -0
  48. package/lib/types/client/renderers/Frames2dVisualMount.js +83 -0
  49. package/lib/types/client/renderers/PetRendererSwitch.d.ts +6 -0
  50. package/lib/types/client/renderers/PetRendererSwitch.d.ts.map +1 -1
  51. package/lib/types/client/renderers/PetRendererSwitch.js +14 -1
  52. package/lib/types/client/renderers/frames2d.d.ts +36 -0
  53. package/lib/types/client/renderers/frames2d.d.ts.map +1 -0
  54. package/lib/types/client/renderers/frames2d.js +196 -0
  55. package/lib/types/defaults.d.ts +6 -0
  56. package/lib/types/defaults.d.ts.map +1 -0
  57. package/lib/types/defaults.js +5 -0
  58. package/lib/types/dsh-home.d.ts.map +1 -1
  59. package/lib/types/dsh-home.js +9 -3
  60. package/lib/types/event-projection.d.ts +9 -4
  61. package/lib/types/event-projection.d.ts.map +1 -1
  62. package/lib/types/event-projection.js +46 -14
  63. package/lib/types/gameplay.d.ts +182 -0
  64. package/lib/types/gameplay.d.ts.map +1 -0
  65. package/lib/types/gameplay.js +618 -0
  66. package/lib/types/ledger.d.ts +14 -0
  67. package/lib/types/ledger.d.ts.map +1 -1
  68. package/lib/types/ledger.js +30 -0
  69. package/lib/types/manifest-v2.d.ts +38 -1
  70. package/lib/types/manifest-v2.d.ts.map +1 -1
  71. package/lib/types/manifest-v2.js +182 -2
  72. package/lib/types/persist.d.ts +4 -4
  73. package/lib/types/persist.d.ts.map +1 -1
  74. package/lib/types/persist.js +41 -4
  75. package/lib/types/registry.d.ts +29 -0
  76. package/lib/types/registry.d.ts.map +1 -1
  77. package/lib/types/registry.js +170 -1
  78. package/lib/types/routes.d.ts.map +1 -1
  79. package/lib/types/routes.js +30 -3
  80. package/lib/types/service.d.ts +63 -11
  81. package/lib/types/service.d.ts.map +1 -1
  82. package/lib/types/service.js +208 -19
  83. package/lib/types/voice-pack.d.ts +6 -7
  84. package/lib/types/voice-pack.d.ts.map +1 -1
  85. package/lib/types/voice-pack.js +44 -47
  86. package/package.json +5 -2
  87. package/src/chatter.test.ts +68 -38
  88. package/src/chatter.ts +269 -316
  89. package/src/client/PetDockEntry.test.tsx +6 -0
  90. package/src/client/PetDockEntry.tsx +32 -1
  91. package/src/client/PetSprite.test.tsx +68 -21
  92. package/src/client/PetSprite.tsx +63 -45
  93. package/src/client/PluginSettingsCard.tsx +4 -2
  94. package/src/client/drag-stream.ts +35 -0
  95. package/src/client/gameplay-hud.test.tsx +338 -0
  96. package/src/client/gameplay-hud.tsx +403 -0
  97. package/src/client/index.test.tsx +10 -1
  98. package/src/client/index.ts +46 -11
  99. package/src/client/locales.ts +36 -0
  100. package/src/client/pet-store.ts +10 -1
  101. package/src/client/pet.module.css +192 -29
  102. package/src/client/renderers/Frames2dVisualMount.tsx +111 -0
  103. package/src/client/renderers/PetRendererSwitch.tsx +28 -1
  104. package/src/client/renderers/frames2d.test.ts +137 -0
  105. package/src/client/renderers/frames2d.ts +216 -0
  106. package/src/client/settings-card.module.css +5 -0
  107. package/src/defaults.ts +7 -0
  108. package/src/dsh-home.ts +9 -3
  109. package/src/event-projection.ts +57 -16
  110. package/src/gameplay.test.ts +239 -0
  111. package/src/gameplay.ts +692 -0
  112. package/src/ledger.test.ts +16 -0
  113. package/src/ledger.ts +31 -0
  114. package/src/manifest-v2.test.ts +130 -1
  115. package/src/manifest-v2.ts +181 -2
  116. package/src/persist.test.ts +3 -0
  117. package/src/persist.ts +43 -6
  118. package/src/registry.test.ts +168 -1
  119. package/src/registry.ts +199 -1
  120. package/src/routes.ts +28 -4
  121. package/src/service.ts +240 -29
  122. package/src/voice-pack.test.ts +48 -25
  123. package/src/voice-pack.ts +50 -47
package/lib/client.js CHANGED
@@ -42,6 +42,12 @@ window.__ModuleLoader__.load({
42
42
  },
43
43
  setFeedback: (draft, feedback) => {
44
44
  draft.feedback = feedback;
45
+ },
46
+ setGameplayView: (draft, view) => {
47
+ if (draft.snapshot !== null) draft.snapshot = {
48
+ ...draft.snapshot,
49
+ gameplay: view
50
+ };
45
51
  }
46
52
  }
47
53
  });
@@ -165,7 +171,7 @@ window.__ModuleLoader__.load({
165
171
  }
166
172
  //#endregion
167
173
  //#region \0dsh-css:packages/dsh-pet/src/client/pet.module.css.mjs
168
- 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_closeButton{z-index:2;color:#f8fafc;cursor:pointer;touch-action:manipulation;background:#070b1ac7;border:1px solid #e2e8ffb8;border-radius:999px;width:24px;height:24px;padding:0;font:600 18px/20px sans-serif;transition:background .12s,box-shadow .12s;position:absolute;top:4px;right:4px}.kz2Bea_closeButton:hover{background:#2c3e7ef2}.kz2Bea_closeButton:focus-visible{outline:none;box-shadow:0 0 0 2px #7e98fff2}.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,.kz2Bea_closeButton{transition:none}}";
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}}";
169
175
  const tagId$2 = "@linxin666/dsh-pet/pet.module.css";
170
176
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
171
177
  const tag = document.createElement("style");
@@ -186,14 +192,30 @@ window.__ModuleLoader__.load({
186
192
  "bubbleStack": "kz2Bea_bubbleStack",
187
193
  "bubbleStatus": "kz2Bea_bubbleStatus",
188
194
  "bubbleWhisper": "kz2Bea_bubbleWhisper",
189
- "closeButton": "kz2Bea_closeButton",
190
195
  "float": "kz2Bea_float",
196
+ "gameplayActions": "kz2Bea_gameplayActions",
197
+ "gameplayBarFill": "kz2Bea_gameplayBarFill",
198
+ "gameplayBarLabel": "kz2Bea_gameplayBarLabel",
199
+ "gameplayBarRow": "kz2Bea_gameplayBarRow",
200
+ "gameplayBarTrack": "kz2Bea_gameplayBarTrack",
201
+ "gameplayBars": "kz2Bea_gameplayBars",
202
+ "gameplayCard": "kz2Bea_gameplayCard",
203
+ "gameplayClose": "kz2Bea_gameplayClose",
204
+ "gameplayFloat": "kz2Bea_gameplayFloat",
205
+ "gameplayHud": "kz2Bea_gameplayHud",
206
+ "gameplayModeChip": "kz2Bea_gameplayModeChip",
207
+ "gameplayShopItem": "kz2Bea_gameplayShopItem",
208
+ "gameplayShopItemImage": "kz2Bea_gameplayShopItemImage",
209
+ "gameplayShopItemLabel": "kz2Bea_gameplayShopItemLabel",
210
+ "gameplayShopItemPrice": "kz2Bea_gameplayShopItemPrice",
211
+ "gameplayShopItems": "kz2Bea_gameplayShopItems",
191
212
  "nameCell": "kz2Bea_nameCell",
192
213
  "nameInput": "kz2Bea_nameInput",
193
214
  "panel": "kz2Bea_panel",
194
215
  "panelAbove": "kz2Bea_panelAbove",
195
216
  "pet-bubble-in": "kz2Bea_pet-bubble-in",
196
217
  "pet-bubble-pop": "kz2Bea_pet-bubble-pop",
218
+ "pet-gameplay-float": "kz2Bea_pet-gameplay-float",
197
219
  "pet-panel-in": "kz2Bea_pet-panel-in",
198
220
  "pet-whisper-in": "kz2Bea_pet-whisper-in",
199
221
  "rankRow": "kz2Bea_rankRow",
@@ -448,6 +470,7 @@ window.__ModuleLoader__.load({
448
470
  };
449
471
  (0, react.useEffect)(() => () => clearHideTimer(), []);
450
472
  const onPointerDown = (e) => {
473
+ if (props.dragDisabled === true) return;
451
474
  e.preventDefault();
452
475
  e.target.setPointerCapture?.(e.pointerId);
453
476
  const current = dragPos ?? {
@@ -467,7 +490,10 @@ window.__ModuleLoader__.load({
467
490
  if (drag === null) return;
468
491
  const dx = e.clientX - drag.startX;
469
492
  const dy = e.clientY - drag.startY;
470
- if (Math.abs(dx) > 4 || Math.abs(dy) > 4) draggedRef.current = true;
493
+ if (Math.abs(dx) > 4 || Math.abs(dy) > 4) {
494
+ if (!draggedRef.current) props.onDraggingChange?.(true);
495
+ draggedRef.current = true;
496
+ }
471
497
  const right = clampOffset(drag.right - dx, window.innerWidth - 40);
472
498
  const bottom = clampOffset(drag.bottom - dy, window.innerHeight - 40);
473
499
  setDragPos({
@@ -478,6 +504,7 @@ window.__ModuleLoader__.load({
478
504
  const onPointerUp = () => {
479
505
  if (dragRef.current === null) return;
480
506
  dragRef.current = null;
507
+ if (draggedRef.current) props.onDraggingChange?.(false);
481
508
  if (dragPos !== null) props.onDragEnd(dragPos.right, dragPos.bottom);
482
509
  };
483
510
  const pos = dragPos ?? {
@@ -490,8 +517,7 @@ window.__ModuleLoader__.load({
490
517
  const stackOpen = stackPeek || stackPinned;
491
518
  const visibleSessions = !stackOpen && sessionBubbles.length > 1 ? sessionBubbles.slice(0, 1) : sessionBubbles;
492
519
  const statusBubble = feedback === null && sessionBubbles.length === 0 ? snapshot?.bubble : void 0;
493
- const whisper = feedback === null ? snapshot?.whisper : void 0;
494
- const bubblePresent = feedback !== null || sessionBubbles.length > 0 || statusBubble !== void 0 || whisper !== void 0;
520
+ const bubblePresent = feedback !== null || sessionBubbles.length > 0 || statusBubble !== void 0;
495
521
  const displayName = snapshot?.name ?? definition.displayName;
496
522
  const decoration = snapshot?.decoration;
497
523
  (0, react.useEffect)(() => {
@@ -546,13 +572,13 @@ window.__ModuleLoader__.load({
546
572
  hideTimerRef.current = window.setTimeout(() => setHovered(false), 300);
547
573
  },
548
574
  children: [
549
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
575
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
550
576
  className: pet_module_css_default.spriteWrap,
551
577
  style: {
552
578
  width: spriteWidth,
553
579
  height: spriteHeight
554
580
  },
555
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
581
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
556
582
  ref: spriteRef,
557
583
  className: pet_module_css_default.sprite,
558
584
  style: {
@@ -569,41 +595,32 @@ window.__ModuleLoader__.load({
569
595
  onPointerDown,
570
596
  onPointerMove,
571
597
  onPointerUp,
572
- onClick: () => {
598
+ onClick: (e) => {
573
599
  if (draggedRef.current) return;
600
+ if (props.onGameplayTap !== void 0 && spriteRef.current !== null) {
601
+ const rect = spriteRef.current.getBoundingClientRect();
602
+ if (rect.width > 0 && rect.height > 0) props.onGameplayTap((e.clientX - rect.left) / rect.width, (e.clientY - rect.top) / rect.height);
603
+ }
574
604
  props.onPet();
575
605
  },
576
606
  role: "button",
577
607
  "aria-label": definition.displayName,
578
608
  children: props.visual
579
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
580
- type: "button",
581
- className: pet_module_css_default.closeButton,
582
- "aria-label": panelLabel("hide", props.t("pet.hide")),
583
- title: panelLabel("hide", props.t("pet.hide")),
584
- "data-testid": "pet-close",
585
- onPointerDown: (e) => {
586
- e.stopPropagation();
587
- },
588
- onClick: (e) => {
589
- e.stopPropagation();
590
- props.onHide();
591
- },
592
- children: "×"
593
- })]
609
+ })
594
610
  }),
611
+ props.hud,
595
612
  feedback !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
596
613
  ref: bubbleRef,
597
614
  className: clsx(pet_module_css_default.bubble, feedback.kind === "feed" ? pet_module_css_default.bubbleFeed : pet_module_css_default.bubblePet),
598
615
  children: feedback.text
599
616
  }, feedback.at),
600
- feedback === null && (sessionBubbles.length > 0 || statusBubble !== void 0 || whisper !== void 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
617
+ feedback === null && (sessionBubbles.length > 0 || statusBubble !== void 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
601
618
  ref: bubbleRef,
602
619
  className: pet_module_css_default.bubbleStack,
603
620
  onPointerEnter: () => setStackPeek(true),
604
621
  onPointerLeave: () => setStackPeek(false),
605
622
  children: [visibleSessions.map((session, index) => {
606
- const speaksWhisper = index === 0 && whisper !== void 0;
623
+ const speaksWhisper = session.whisper !== void 0;
607
624
  const bubble = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
608
625
  type: "button",
609
626
  className: clsx(pet_module_css_default.bubble, pet_module_css_default.bubbleStatus, pet_module_css_default.bubbleClickable, speaksWhisper && pet_module_css_default.bubbleWhisper),
@@ -614,8 +631,8 @@ window.__ModuleLoader__.load({
614
631
  children: [index === 0 && !speaksWhisper && decoration !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusOrnament, {
615
632
  decoration,
616
633
  phase
617
- }), speaksWhisper ? whisper : session.bubble]
618
- }, speaksWhisper ? "whisper:" + whisper : session.sessionId);
634
+ }), session.whisper ?? session.bubble]
635
+ }, speaksWhisper ? "whisper:" + session.whisper : session.sessionId);
619
636
  if (index !== 0 || sessionBubbles.length <= 1) return bubble;
620
637
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
621
638
  className: pet_module_css_default.bubbleAnchor,
@@ -632,15 +649,15 @@ window.__ModuleLoader__.load({
632
649
  children: stackOpen ? "×" : "+" + String(sessionBubbles.length - 1)
633
650
  })]
634
651
  }, "primary");
635
- }), sessionBubbles.length === 0 && (statusBubble !== void 0 || whisper !== void 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
636
- className: clsx(pet_module_css_default.bubble, pet_module_css_default.bubbleStatus, whisper !== void 0 && pet_module_css_default.bubbleWhisper),
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),
637
654
  role: "status",
638
655
  "aria-live": "polite",
639
- children: [whisper === void 0 && decoration !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusOrnament, {
656
+ children: [decoration !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusOrnament, {
640
657
  decoration,
641
658
  phase
642
- }), whisper ?? statusBubble]
643
- }, whisper === void 0 ? "status" : "whisper:" + whisper)]
659
+ }), statusBubble]
660
+ }, "status")]
644
661
  }),
645
662
  hovered && dragRef.current === null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
646
663
  ref: panelRef,
@@ -732,6 +749,12 @@ window.__ModuleLoader__.load({
732
749
  className: pet_module_css_default.action,
733
750
  onClick: props.onHide,
734
751
  children: panelLabel("hide", props.t("pet.hide"))
752
+ }),
753
+ props.onGameplayMenu !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
754
+ type: "button",
755
+ className: pet_module_css_default.action,
756
+ onClick: props.onGameplayMenu,
757
+ children: props.t("pet.gameplay.menu")
735
758
  })
736
759
  ]
737
760
  })
@@ -741,6 +764,27 @@ window.__ModuleLoader__.load({
741
764
  }), document.body);
742
765
  }
743
766
  //#endregion
767
+ //#region src/client/drag-stream.ts
768
+ /** Create the stream (one per pet activation, owned by the renderer switch). */
769
+ function createDragStream() {
770
+ let current = false;
771
+ const listeners = /* @__PURE__ */ new Set();
772
+ return {
773
+ get: () => current,
774
+ subscribe(listener) {
775
+ listeners.add(listener);
776
+ return () => {
777
+ listeners.delete(listener);
778
+ };
779
+ },
780
+ push(dragging) {
781
+ if (dragging === current) return;
782
+ current = dragging;
783
+ for (const listener of [...listeners]) listener(dragging);
784
+ }
785
+ };
786
+ }
787
+ //#endregion
744
788
  //#region src/client/renderers/registry.ts
745
789
  /** Renderer dispatch table. */
746
790
  var RendererRegistry = class {
@@ -888,6 +932,93 @@ window.__ModuleLoader__.load({
888
932
  });
889
933
  }
890
934
  //#endregion
935
+ //#region src/client/renderers/Frames2dVisualMount.tsx
936
+ /**
937
+ * Frames2d visual mount — the React bridge between the pet center chrome
938
+ * and the imperative frames2d renderer, mirroring the live2d mount. The
939
+ * bridge owns the contract context (asset base, phase stream, interaction
940
+ * write-back, activation cleanups), feeds the polled phase into the stream,
941
+ * forwards the chrome's drag gesture onto the conventional 'drag' track
942
+ * (when the pet declares one), and renders the localized fallback card when
943
+ * the served config is invalid.
944
+ * @module @linxin666/dsh-pet/client/renderers/Frames2dVisualMount
945
+ */
946
+ /** Mount the frames2d renderer as the sprite's visual (inside the chrome). */
947
+ function Frames2dVisualMount(props) {
948
+ const containerRef = (0, react.useRef)(null);
949
+ const streamRef = (0, react.useRef)(null);
950
+ const handleRef = (0, react.useRef)(null);
951
+ const [invalid, setInvalid] = (0, react.useState)(false);
952
+ (0, react.useEffect)(() => {
953
+ setInvalid(false);
954
+ const container = containerRef.current;
955
+ const frames2d = props.definition.frames2d;
956
+ if (container === null || frames2d === void 0) return void 0;
957
+ streamRef.current ??= createPhaseStream(props.phase);
958
+ const cleanups = [];
959
+ const ctx = {
960
+ petId: props.definition.id,
961
+ assetBase: "/pet/" + encodeURIComponent(props.definition.id),
962
+ container,
963
+ phase: streamRef.current,
964
+ interact: props.onPet,
965
+ onCleanup: (fn) => {
966
+ cleanups.push(fn);
967
+ }
968
+ };
969
+ let handle;
970
+ try {
971
+ handle = defaultPetRendererRegistry.mount("frames2d", ctx, frames2d);
972
+ } catch {
973
+ setInvalid(true);
974
+ return () => {
975
+ for (const fn of cleanups.splice(0)) fn();
976
+ };
977
+ }
978
+ handleRef.current = handle;
979
+ if (props.bus !== void 0) {
980
+ const gameplayBus = props.bus;
981
+ gameplayBus.setTrack = (track) => {
982
+ handleRef.current?.setState(track);
983
+ };
984
+ cleanups.push(() => {
985
+ gameplayBus.setTrack = void 0;
986
+ });
987
+ }
988
+ const dragTrack = props.definition.gameplay?.dragState ?? (frames2d.tracks.drag === void 0 ? void 0 : "drag");
989
+ const offDrag = props.drag.subscribe((dragging) => {
990
+ if (dragTrack === void 0) return;
991
+ if (dragging) {
992
+ handle.setState(dragTrack);
993
+ return;
994
+ }
995
+ handle.setState(props.definition.gameplay?.dragEndState);
996
+ });
997
+ cleanups.push(offDrag);
998
+ return () => {
999
+ handleRef.current = null;
1000
+ for (const fn of cleanups.splice(0)) fn();
1001
+ handle.dispose();
1002
+ };
1003
+ }, [props.definition]);
1004
+ (0, react.useEffect)(() => {
1005
+ streamRef.current?.push(props.phase);
1006
+ }, [props.phase]);
1007
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1008
+ ref: containerRef,
1009
+ "data-dsh-pet-frames2d": props.definition.id,
1010
+ style: {
1011
+ width: "100%",
1012
+ height: "100%",
1013
+ pointerEvents: "none"
1014
+ },
1015
+ children: invalid && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1016
+ "data-dsh-pet-frames2d-error": "invalid-config",
1017
+ children: props.t("pet.renderer.unavailable", { renderer: "frames2d" })
1018
+ })
1019
+ });
1020
+ }
1021
+ //#endregion
891
1022
  //#region src/client/renderers/PetRendererSwitch.tsx
892
1023
  /**
893
1024
  * Renderer switch — the client dispatch seam of the pet center (issue #623,
@@ -901,7 +1032,27 @@ window.__ModuleLoader__.load({
901
1032
  /** Dispatch one pet definition to its renderer; unknown kinds get a card. */
902
1033
  function PetRendererSwitch(props) {
903
1034
  const renderer = props.definition.renderer ?? "sprite2d";
1035
+ const dragRef = (0, react.useRef)(null);
1036
+ if (dragRef.current === null || dragRef.current.id !== props.definition.id) dragRef.current = {
1037
+ id: props.definition.id,
1038
+ stream: createDragStream()
1039
+ };
1040
+ const drag = props.drag ?? dragRef.current.stream;
904
1041
  if (renderer === "sprite2d") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: props.children });
1042
+ if (renderer === "frames2d" && defaultPetRendererRegistry.has("frames2d") && (0, react.isValidElement)(props.children)) {
1043
+ const visual = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Frames2dVisualMount, {
1044
+ definition: props.definition,
1045
+ phase: props.phase,
1046
+ onPet: props.onPet,
1047
+ drag,
1048
+ ...props.bus === void 0 ? {} : { bus: props.bus },
1049
+ t: props.t
1050
+ });
1051
+ return (0, react.cloneElement)(props.children, {
1052
+ visual,
1053
+ onDraggingChange: (dragging) => drag.push(dragging)
1054
+ });
1055
+ }
905
1056
  if (renderer === "live2d" && defaultPetRendererRegistry.has("live2d") && (0, react.isValidElement)(props.children)) {
906
1057
  const visual = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Live2dVisualMount, {
907
1058
  definition: props.definition,
@@ -917,6 +1068,362 @@ window.__ModuleLoader__.load({
917
1068
  });
918
1069
  }
919
1070
  //#endregion
1071
+ //#region src/gameplay.ts
1072
+ /** The zone one normalized hit-box point lands in, if any. */
1073
+ function touchZoneAt(touch, yFraction) {
1074
+ return touch.zones.find((zone) => yFraction >= zone.y0 && yFraction < zone.y1);
1075
+ }
1076
+ //#endregion
1077
+ //#region src/client/gameplay-hud.tsx
1078
+ /**
1079
+ * Gameplay HUD — the client half of the manifest 'gameplay' block (miku-pet
1080
+ * generalization). One component owns everything the block needs: the stat
1081
+ * bars and shop card (menu / shop pages), the touch-zone tap
1082
+ * handling, the idle director rolls, the work and sleep loops, and the
1083
+ * float-text toasts. It talks to the host through the injected verb API,
1084
+ * writes results straight back into the store (the 2 s poll stays the
1085
+ * backstop), and steers the frames2d renderer through the per-pet bus.
1086
+ * @module @linxin666/dsh-pet/client/GameplayHud
1087
+ */
1088
+ let floatSeq = 0;
1089
+ /** The gameplay overlay for one frames2d pet that declares 'gameplay'. */
1090
+ function GameplayHud(props) {
1091
+ const { definition, store, api, bus } = props;
1092
+ const ui = (0, react.useSyncExternalStore)(store.subscribe, store.getSnapshot);
1093
+ const def = definition.gameplay;
1094
+ const view = ui.snapshot?.gameplay;
1095
+ const phase = ui.snapshot?.phase ?? "idle";
1096
+ const [open, setOpen] = (0, react.useState)(false);
1097
+ const [page, setPage] = (0, react.useState)("root");
1098
+ const hudRef = (0, react.useRef)(null);
1099
+ const cardRef = (0, react.useRef)(null);
1100
+ const [floats, setFloats] = (0, react.useState)([]);
1101
+ const modeRef = (0, react.useRef)(view?.mode ?? null);
1102
+ modeRef.current = view?.mode ?? null;
1103
+ const phaseRef = (0, react.useRef)(phase);
1104
+ phaseRef.current = phase;
1105
+ const draggingRef = (0, react.useRef)(false);
1106
+ const touchLockUntilRef = (0, react.useRef)(0);
1107
+ const missRef = (0, react.useRef)(0);
1108
+ const busyRef = (0, react.useRef)(false);
1109
+ const tr = props.t;
1110
+ const statLabel = (name) => tr("pet.gameplay.stat." + name);
1111
+ const currencyLabel = (name) => tr("pet.gameplay.currency." + name);
1112
+ const pushFloat = (text) => {
1113
+ const id = ++floatSeq;
1114
+ setFloats((list) => [...list.slice(-3), {
1115
+ id,
1116
+ text
1117
+ }]);
1118
+ window.setTimeout(() => {
1119
+ setFloats((list) => list.filter((entry) => entry.id !== id));
1120
+ }, 1100);
1121
+ };
1122
+ const applyResult = (result) => {
1123
+ if (result.view !== void 0) store.actions.setGameplayView(result.view);
1124
+ };
1125
+ (0, react.useEffect)(() => {
1126
+ if (def === void 0) return void 0;
1127
+ bus.tap = (fx, fy) => {
1128
+ if (modeRef.current === "sleep") {
1129
+ api.setMode(null).then(applyResult, () => void 0);
1130
+ return;
1131
+ }
1132
+ if (modeRef.current === "work") return;
1133
+ const box = def.hitBox ?? {
1134
+ x0: 0,
1135
+ y0: 0,
1136
+ x1: 1,
1137
+ y1: 1
1138
+ };
1139
+ const hx = (fx - box.x0) / (box.x1 - box.x0);
1140
+ const hy = (fy - box.y0) / (box.y1 - box.y0);
1141
+ if (hx < 0 || hx > 1 || hy < 0 || hy > 1) return;
1142
+ if (Date.now() < touchLockUntilRef.current) {
1143
+ api.touch().then(applyResult, () => void 0);
1144
+ return;
1145
+ }
1146
+ const zone = def.touch === void 0 ? void 0 : touchZoneAt(def.touch, hy);
1147
+ if (zone === void 0) return;
1148
+ api.touch(zone.name).then((result) => {
1149
+ applyResult(result);
1150
+ if (result.hit !== true) return;
1151
+ if (result.state !== void 0) {
1152
+ bus.setTrack?.(result.state);
1153
+ const holdMs = result.stateMs ?? 3e3;
1154
+ touchLockUntilRef.current = Date.now() + holdMs;
1155
+ window.setTimeout(() => {
1156
+ if (Date.now() >= touchLockUntilRef.current) bus.setTrack?.(void 0);
1157
+ }, holdMs);
1158
+ }
1159
+ if (result.phrase !== void 0) store.actions.setFeedback({
1160
+ text: result.phrase,
1161
+ kind: "none",
1162
+ at: Date.now()
1163
+ });
1164
+ }, () => void 0);
1165
+ };
1166
+ return () => {
1167
+ bus.tap = void 0;
1168
+ };
1169
+ }, [definition.id, def]);
1170
+ (0, react.useEffect)(() => {
1171
+ bus.openCard = (next) => {
1172
+ setOpen((prev) => next ?? !prev);
1173
+ setPage("root");
1174
+ };
1175
+ return () => {
1176
+ bus.openCard = void 0;
1177
+ };
1178
+ }, [bus]);
1179
+ (0, react.useLayoutEffect)(() => {
1180
+ if (!open) return void 0;
1181
+ const hud = hudRef.current;
1182
+ const card = cardRef.current;
1183
+ if (hud === null || card === null) return void 0;
1184
+ const place = () => {
1185
+ const box = hud.parentElement?.getBoundingClientRect();
1186
+ if (box === void 0) return;
1187
+ const gap = 8;
1188
+ const width = card.getBoundingClientRect().width;
1189
+ const x = window.innerWidth - box.right >= width + gap ? box.width + gap : -(width + gap);
1190
+ card.style.transform = "translate(" + Math.round(x) + "px, " + Math.round(-box.height / 2) + "px) translateY(50%)";
1191
+ card.style.maxHeight = Math.round(box.height) + "px";
1192
+ };
1193
+ place();
1194
+ window.addEventListener("resize", place);
1195
+ return () => {
1196
+ window.removeEventListener("resize", place);
1197
+ };
1198
+ }, [open, page]);
1199
+ (0, react.useEffect)(() => {
1200
+ return props.drag.subscribe((dragging) => {
1201
+ draggingRef.current = dragging;
1202
+ if (dragging && modeRef.current === "sleep") api.setMode(null).then(applyResult, () => void 0);
1203
+ });
1204
+ }, [props.drag]);
1205
+ (0, react.useEffect)(() => {
1206
+ const director = def?.idleDirector;
1207
+ if (def === void 0 || director === void 0) return void 0;
1208
+ const total = director.idleWeight + director.acts.reduce((sum, act) => sum + act.weight, 0);
1209
+ if (total <= 0) return void 0;
1210
+ const timer = window.setInterval(() => {
1211
+ if (phaseRef.current !== "idle") return;
1212
+ if (modeRef.current !== null || draggingRef.current) return;
1213
+ if (Date.now() < touchLockUntilRef.current) return;
1214
+ let pickedAct;
1215
+ if (missRef.current >= director.maxMiss) {
1216
+ const actTotal = director.acts.reduce((sum, act) => sum + act.weight, 0);
1217
+ let actRoll = Math.random() * actTotal;
1218
+ for (const act of director.acts) {
1219
+ actRoll -= act.weight;
1220
+ if (actRoll < 0) {
1221
+ pickedAct = act;
1222
+ break;
1223
+ }
1224
+ }
1225
+ } else {
1226
+ let roll = Math.random() * total;
1227
+ for (const act of director.acts) {
1228
+ roll -= act.weight;
1229
+ if (roll < 0) {
1230
+ pickedAct = act;
1231
+ break;
1232
+ }
1233
+ }
1234
+ }
1235
+ if (pickedAct === void 0) {
1236
+ missRef.current += 1;
1237
+ return;
1238
+ }
1239
+ missRef.current = 0;
1240
+ bus.setTrack?.(pickedAct.track);
1241
+ if (pickedAct.phrases !== void 0 && pickedAct.phrases.length > 0) {
1242
+ const phrase = pickedAct.phrases[Math.floor(Math.random() * pickedAct.phrases.length)];
1243
+ store.actions.setFeedback({
1244
+ text: phrase,
1245
+ kind: "none",
1246
+ at: Date.now()
1247
+ });
1248
+ }
1249
+ }, director.intervalMs);
1250
+ return () => window.clearInterval(timer);
1251
+ }, [definition.id, def]);
1252
+ (0, react.useEffect)(() => {
1253
+ const work = def?.work;
1254
+ if (def === void 0 || work === void 0 || view?.mode !== "work") return void 0;
1255
+ bus.setTrack?.(work.state);
1256
+ let resultTimer = 0;
1257
+ const timer = window.setInterval(() => {
1258
+ if (busyRef.current) return;
1259
+ busyRef.current = true;
1260
+ api.workTick().then((result) => {
1261
+ busyRef.current = false;
1262
+ applyResult(result);
1263
+ if (result.ok !== true || result.outcome === void 0) return;
1264
+ const resultTrack = result.outcome === "success" ? work.successState : work.failState;
1265
+ const hold = result.outcome === "success" ? work.resultMs?.success ?? 1300 : work.resultMs?.fail ?? 1900;
1266
+ bus.setTrack?.(resultTrack);
1267
+ resultTimer = window.setTimeout(() => {
1268
+ if (modeRef.current === "work") bus.setTrack?.(work.state);
1269
+ }, hold);
1270
+ }, () => {
1271
+ busyRef.current = false;
1272
+ });
1273
+ }, work.tickMs);
1274
+ return () => {
1275
+ window.clearInterval(timer);
1276
+ window.clearTimeout(resultTimer);
1277
+ bus.setTrack?.(void 0);
1278
+ };
1279
+ }, [
1280
+ definition.id,
1281
+ def,
1282
+ view?.mode
1283
+ ]);
1284
+ (0, react.useEffect)(() => {
1285
+ const sleep = def?.sleep;
1286
+ if (def === void 0 || sleep === void 0 || view?.mode !== "sleep") return void 0;
1287
+ bus.setTrack?.(sleep.state);
1288
+ return () => bus.setTrack?.(void 0);
1289
+ }, [
1290
+ definition.id,
1291
+ def,
1292
+ view?.mode
1293
+ ]);
1294
+ if (def === void 0 || view === void 0) return null;
1295
+ const mode = view.mode;
1296
+ const stats = def.stats ?? {};
1297
+ const shop = def.shop;
1298
+ const buy = (itemId) => {
1299
+ api.buy(itemId).then((result) => {
1300
+ applyResult(result);
1301
+ if (result.ok !== true) {
1302
+ if (result.error === "insufficient-funds") {
1303
+ const item = shop?.items.find((entry) => entry.id === itemId);
1304
+ pushFloat(tr("pet.gameplay.insufficient", { currency: currencyLabel(item?.currency ?? "treats") }));
1305
+ }
1306
+ return;
1307
+ }
1308
+ if (result.prize !== void 0) pushFloat(tr("pet.gameplay.prize", {
1309
+ amount: result.prize.amount,
1310
+ currency: currencyLabel(result.prize.currency)
1311
+ }));
1312
+ }, () => void 0);
1313
+ };
1314
+ const setMode = (next) => {
1315
+ api.setMode(next).then(applyResult, () => void 0);
1316
+ };
1317
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1318
+ ref: hudRef,
1319
+ className: pet_module_css_default.gameplayHud,
1320
+ "data-dsh-pet-gameplay": definition.id,
1321
+ children: [
1322
+ floats.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1323
+ className: pet_module_css_default.gameplayFloat,
1324
+ children: entry.text
1325
+ }, entry.id)),
1326
+ mode !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1327
+ className: pet_module_css_default.gameplayModeChip,
1328
+ children: tr(mode === "work" ? "pet.gameplay.working" : "pet.gameplay.sleeping")
1329
+ }),
1330
+ open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1331
+ ref: cardRef,
1332
+ className: pet_module_css_default.gameplayCard,
1333
+ "data-page": page,
1334
+ children: [
1335
+ page === "root" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1336
+ className: pet_module_css_default.gameplayBars,
1337
+ children: Object.entries(stats).map(([name, stat]) => {
1338
+ const value = view.stats[name] ?? 0;
1339
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1340
+ className: pet_module_css_default.gameplayBarRow,
1341
+ title: statLabel(name) + " " + String(value) + "/" + String(stat.max),
1342
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1343
+ className: pet_module_css_default.gameplayBarLabel,
1344
+ children: statLabel(name)
1345
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1346
+ className: pet_module_css_default.gameplayBarTrack,
1347
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1348
+ className: pet_module_css_default.gameplayBarFill,
1349
+ style: { width: Math.round(value / stat.max * 100) + "%" }
1350
+ })
1351
+ })]
1352
+ }, name);
1353
+ })
1354
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1355
+ className: pet_module_css_default.gameplayActions,
1356
+ children: [
1357
+ def.work !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1358
+ type: "button",
1359
+ className: pet_module_css_default.action,
1360
+ onClick: () => setMode(mode === "work" ? null : "work"),
1361
+ children: tr(mode === "work" ? "pet.gameplay.stopWork" : "pet.gameplay.work")
1362
+ }),
1363
+ def.sleep !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1364
+ type: "button",
1365
+ className: pet_module_css_default.action,
1366
+ onClick: () => setMode(mode === "sleep" ? null : "sleep"),
1367
+ children: tr(mode === "sleep" ? "pet.gameplay.wake" : "pet.gameplay.sleep")
1368
+ }),
1369
+ shop !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1370
+ type: "button",
1371
+ className: pet_module_css_default.action,
1372
+ onClick: () => setPage("shop"),
1373
+ children: tr("pet.gameplay.shop")
1374
+ })
1375
+ ]
1376
+ })] }),
1377
+ page === "shop" && shop !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1378
+ className: pet_module_css_default.gameplayShopItems,
1379
+ children: shop.items.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1380
+ type: "button",
1381
+ className: pet_module_css_default.gameplayShopItem,
1382
+ onClick: () => buy(item.id),
1383
+ title: item.label + " — " + String(item.price) + " " + currencyLabel(item.currency),
1384
+ children: [
1385
+ item.image !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
1386
+ className: pet_module_css_default.gameplayShopItemImage,
1387
+ src: item.image,
1388
+ alt: "",
1389
+ draggable: false
1390
+ }),
1391
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1392
+ className: pet_module_css_default.gameplayShopItemLabel,
1393
+ children: item.label
1394
+ }),
1395
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1396
+ className: pet_module_css_default.gameplayShopItemPrice,
1397
+ children: [
1398
+ item.price,
1399
+ " ",
1400
+ currencyLabel(item.currency)
1401
+ ]
1402
+ })
1403
+ ]
1404
+ }, item.id))
1405
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1406
+ className: pet_module_css_default.gameplayActions,
1407
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1408
+ type: "button",
1409
+ className: pet_module_css_default.action,
1410
+ onClick: () => setPage("root"),
1411
+ children: tr("pet.gameplay.back")
1412
+ })
1413
+ })] }),
1414
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1415
+ type: "button",
1416
+ className: pet_module_css_default.gameplayClose,
1417
+ "aria-label": tr("pet.gameplay.back"),
1418
+ onClick: () => setOpen(false),
1419
+ children: "×"
1420
+ })
1421
+ ]
1422
+ })
1423
+ ]
1424
+ });
1425
+ }
1426
+ //#endregion
920
1427
  //#region src/client/PetDockEntry.tsx
921
1428
  /**
922
1429
  * Global floating pet entry. The pet is host-global (its state, display and
@@ -953,6 +1460,14 @@ window.__ModuleLoader__.load({
953
1460
  (0, react.useEffect)(() => {
954
1461
  ensure();
955
1462
  }, [ensure]);
1463
+ const auxRef = (0, react.useRef)(null);
1464
+ if (definition !== null && (auxRef.current === null || auxRef.current.id !== definition.id)) auxRef.current = {
1465
+ id: definition.id,
1466
+ bus: {},
1467
+ drag: createDragStream()
1468
+ };
1469
+ const aux = auxRef.current;
1470
+ const gameplay = definition?.gameplay;
956
1471
  if (visible) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
957
1472
  "data-pet-dock": true,
958
1473
  "data-testid": "pet-dock",
@@ -960,6 +1475,10 @@ window.__ModuleLoader__.load({
960
1475
  definition,
961
1476
  phase: snapshot?.phase ?? "idle",
962
1477
  onPet: props.pet,
1478
+ ...aux === null ? {} : {
1479
+ drag: aux.drag,
1480
+ bus: aux.bus
1481
+ },
963
1482
  t: props.t,
964
1483
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PetSprite, {
965
1484
  snapshot,
@@ -973,6 +1492,19 @@ window.__ModuleLoader__.load({
973
1492
  onRename: props.rename,
974
1493
  onOpenSession: props.openSession,
975
1494
  onFeedbackDone: props.feedbackDone,
1495
+ dragDisabled: snapshot.gameplay?.mode === "work",
1496
+ ...gameplay === void 0 || aux === null ? {} : {
1497
+ onGameplayTap: (fx, fy) => aux.bus.tap?.(fx, fy),
1498
+ onGameplayMenu: () => aux.bus.openCard?.(),
1499
+ hud: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GameplayHud, {
1500
+ definition,
1501
+ store,
1502
+ api: props.gameplay,
1503
+ bus: aux.bus,
1504
+ drag: aux.drag,
1505
+ t: props.t
1506
+ })
1507
+ },
976
1508
  t: props.t
977
1509
  })
978
1510
  })
@@ -1268,6 +1800,161 @@ window.__ModuleLoader__.load({
1268
1800
  }
1269
1801
  };
1270
1802
  //#endregion
1803
+ //#region src/client/renderers/frames2d.ts
1804
+ /**
1805
+ * frames2d renderer — plays the free-form named frame-sequence tracks of a
1806
+ * frames2d pet (manifest v2 'frames2d' block). The pet center picks the
1807
+ * track from the ActivityPhase stream (phase -> track map, idle fallback);
1808
+ * the gameplay driver may force a track through the handle's setState
1809
+ * override (drag/work/sleep/shop...), and a finished non-loop track settles
1810
+ * into its fallback, releasing the override when the fallback matches the
1811
+ * phase-mapped track. Rendering never throws: a broken track list degrades
1812
+ * to the first decodable frame, and the 1.2 s stall watchdog re-kicks the
1813
+ * playback chain after timer throttling.
1814
+ * @module @linxin666/dsh-pet/client/renderers/frames2d
1815
+ */
1816
+ /** Stall watchdog period: a looping track stuck longer re-kicks its chain. */
1817
+ const WATCHDOG_MS = 1200;
1818
+ function isRecord(value) {
1819
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1820
+ }
1821
+ /** Fail-closed client-side validation of the served frames2d block. */
1822
+ function validateFrames2dConfig(config) {
1823
+ if (!isRecord(config) || !isRecord(config.tracks) || !isRecord(config.phases)) throw new Error("frames2d config requires tracks and phases objects");
1824
+ const tracks = {};
1825
+ for (const [name, raw] of Object.entries(config.tracks)) {
1826
+ if (!isRecord(raw) || !Array.isArray(raw.frames) || raw.frames.length === 0 || raw.frames.some((f) => typeof f !== "string" || f === "") || !Array.isArray(raw.durations) || raw.durations.length !== raw.frames.length || raw.durations.some((d) => typeof d !== "number" || !(d > 0))) throw new Error("frames2d track " + JSON.stringify(name) + " needs same-length frames/durations");
1827
+ tracks[name] = {
1828
+ frames: raw.frames,
1829
+ durations: raw.durations,
1830
+ loop: raw.loop !== false,
1831
+ ...typeof raw.fallback === "string" ? { fallback: raw.fallback } : {}
1832
+ };
1833
+ }
1834
+ const phases = config.phases;
1835
+ if (typeof phases.idle !== "string" || tracks[phases.idle] === void 0) throw new Error("frames2d phases.idle must name an existing track");
1836
+ return {
1837
+ tracks,
1838
+ phases
1839
+ };
1840
+ }
1841
+ const frames2dRenderer = {
1842
+ id: "frames2d",
1843
+ apiVersion: PET_RENDERER_API_VERSION,
1844
+ validateConfig: validateFrames2dConfig,
1845
+ mount(ctx, config) {
1846
+ 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;
1859
+ }
1860
+ let disposed = false;
1861
+ let timer;
1862
+ let watchdog;
1863
+ let track = config.phases.idle;
1864
+ let frameIndex = 0;
1865
+ let lastAdvance = Date.now();
1866
+ let override;
1867
+ const trackForPhase = (phase) => {
1868
+ const mapped = config.phases[phase];
1869
+ return mapped !== void 0 && config.tracks[mapped] !== void 0 ? mapped : config.phases.idle;
1870
+ };
1871
+ const show = (trackId, index) => {
1872
+ const url = config.tracks[trackId]?.frames[index];
1873
+ if (url !== void 0 && img.getAttribute("src") !== url) img.src = url;
1874
+ };
1875
+ const schedule = (ms) => {
1876
+ if (disposed) return;
1877
+ timer = setTimeout(tick, ms);
1878
+ };
1879
+ function tick() {
1880
+ if (disposed) return;
1881
+ const def = config.tracks[track];
1882
+ if (def === void 0 || def.frames.length === 0) return;
1883
+ lastAdvance = Date.now();
1884
+ const next = frameIndex + 1;
1885
+ if (next < def.frames.length) {
1886
+ frameIndex = next;
1887
+ show(track, frameIndex);
1888
+ schedule(def.durations[frameIndex] ?? 200);
1889
+ return;
1890
+ }
1891
+ if (def.loop) {
1892
+ frameIndex = 0;
1893
+ show(track, frameIndex);
1894
+ schedule(def.durations[frameIndex] ?? 200);
1895
+ return;
1896
+ }
1897
+ const target = def.fallback !== void 0 && config.tracks[def.fallback] !== void 0 ? def.fallback : config.phases.idle;
1898
+ if (target === trackForPhase(ctx.phase.get())) override = void 0;
1899
+ play(target);
1900
+ }
1901
+ function play(trackId) {
1902
+ if (disposed) return;
1903
+ if (config.tracks[trackId] === void 0) trackId = config.phases.idle;
1904
+ if (timer !== void 0) clearTimeout(timer);
1905
+ track = trackId;
1906
+ frameIndex = 0;
1907
+ lastAdvance = Date.now();
1908
+ show(track, frameIndex);
1909
+ if (reducedMotion) return;
1910
+ const def = config.tracks[track];
1911
+ schedule(def.durations[0] ?? 200);
1912
+ }
1913
+ const unsubscribe = ctx.phase.subscribe((phase) => {
1914
+ if (override !== void 0) return;
1915
+ const target = trackForPhase(phase);
1916
+ if (target !== track) play(target);
1917
+ });
1918
+ if (!reducedMotion) watchdog = setInterval(() => {
1919
+ if (disposed) return;
1920
+ const def = config.tracks[track];
1921
+ if (def === void 0 || !def.loop) return;
1922
+ const expected = (def.durations[frameIndex] ?? 200) + WATCHDOG_MS;
1923
+ if (Date.now() - lastAdvance > expected) tick();
1924
+ }, WATCHDOG_MS);
1925
+ play(track);
1926
+ let disposedOnce = false;
1927
+ const dispose = () => {
1928
+ if (disposedOnce) return;
1929
+ disposedOnce = true;
1930
+ disposed = true;
1931
+ unsubscribe();
1932
+ if (timer !== void 0) clearTimeout(timer);
1933
+ if (watchdog !== void 0) clearInterval(watchdog);
1934
+ img.remove();
1935
+ };
1936
+ ctx.onCleanup(dispose);
1937
+ return {
1938
+ dispose,
1939
+ setState(next) {
1940
+ if (disposed) return;
1941
+ if (next === void 0) {
1942
+ override = void 0;
1943
+ const target = trackForPhase(ctx.phase.get());
1944
+ if (target !== track) play(target);
1945
+ return;
1946
+ }
1947
+ if (config.tracks[next] === void 0) return;
1948
+ override = next;
1949
+ if (next !== track) play(next);
1950
+ },
1951
+ currentTrack() {
1952
+ return track;
1953
+ }
1954
+ };
1955
+ }
1956
+ };
1957
+ //#endregion
1271
1958
  //#region src/client/ui-teardown.ts
1272
1959
  /**
1273
1960
  * Cross-bundle-instance teardown slot for the page-global pet root
@@ -1311,7 +1998,7 @@ window.__ModuleLoader__.load({
1311
1998
  }
1312
1999
  //#endregion
1313
2000
  //#region \0dsh-css:packages/dsh-pet/src/client/settings-card.module.css.mjs
1314
- const css$1 = ".kKk9aW_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}.kKk9aW_card:hover{border-color:var(--dsw-alias-label-dimmed)}.kKk9aW_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.kKk9aW_header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}.kKk9aW_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.kKk9aW_headerStatic{border-radius:12px;align-items:center;gap:12px;width:100%;padding:14px 16px;display:flex}.kKk9aW_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.kKk9aW_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.kKk9aW_description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.kKk9aW_pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.kKk9aW_chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.kKk9aW_chevronOpen{transform:rotate(180deg)}.kKk9aW_body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}.kKk9aW_readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}.kKk9aW_notExposed{color:var(--dsw-alias-state-warn-primary);margin:12px 0 0;font-size:12px;line-height:1.5}.kKk9aW_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}.kKk9aW_failed{min-width:0;color:var(--dsw-alias-label-error);text-overflow:ellipsis;white-space:nowrap;flex:1;margin:0;font-size:12px;line-height:1.5;overflow:hidden}.kKk9aW_discard,.kKk9aW_save{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}.kKk9aW_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.kKk9aW_discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}.kKk9aW_save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.kKk9aW_discard:disabled,.kKk9aW_save:disabled{opacity:.4;cursor:default}.kKk9aW_discard:focus-visible,.kKk9aW_save:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.kKk9aW_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.kKk9aW_field+.kKk9aW_field{border-top:1px solid var(--dsw-alias-border-l2)}.kKk9aW_head{align-items:center;gap:8px;display:flex}.kKk9aW_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.kKk9aW_badges{align-items:center;gap:8px;display:inline-flex}.kKk9aW_badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.kKk9aW_reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}.kKk9aW_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.kKk9aW_reset:disabled{cursor:default}.kKk9aW_reset:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px;outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.kKk9aW_input,.kKk9aW_select{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.kKk9aW_input:focus-visible,.kKk9aW_select:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.kKk9aW_input:disabled,.kKk9aW_select:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.kKk9aW_inputInvalid{border:1px solid var(--dsw-alias-label-error);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.kKk9aW_inputInvalid:focus-visible{outline:2px solid var(--dsw-alias-label-error);outline-offset:1px;border-color:var(--dsw-alias-label-error)}.kKk9aW_selectWrap{position:relative}.kKk9aW_selectButton{appearance:none;text-align:left;cursor:pointer;justify-content:space-between;align-items:center;gap:8px;width:100%;display:flex}.kKk9aW_selectLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.kKk9aW_selectChevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.kKk9aW_selectChevronOpen{transform:rotate(180deg)}.kKk9aW_selectPopup{z-index:40;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);max-height:240px;box-shadow:0 8px 24px var(--dsw-alias-bg-mask-2);opacity:0;border-radius:8px;flex-direction:column;padding:4px;transition:opacity .1s,transform .1s;display:flex;position:absolute;top:calc(100% + 4px);left:0;right:0;overflow-y:auto;transform:translateY(-4px)}.kKk9aW_selectPopupOpen{opacity:1;transform:none}.kKk9aW_selectPopupClose{opacity:0;pointer-events:none;transform:translateY(-4px)}.kKk9aW_selectOption{color:var(--dsw-alias-label-primary);cursor:pointer;white-space:nowrap;text-overflow:ellipsis;border-radius:6px;flex-shrink:0;padding:6px 10px;font-size:13px;line-height:1.5;overflow:hidden}.kKk9aW_selectOption:hover,.kKk9aW_selectOptionActive{background:var(--dsw-alias-interactive-bg-hover)}.kKk9aW_selectOptionSelected{color:var(--dsw-alias-brand-primary);background:color-mix(in srgb, var(--dsw-alias-brand-primary-new-colorprimary-new-color) 10%, transparent);font-weight:500}.kKk9aW_invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}.kKk9aW_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}@media (prefers-reduced-motion:reduce){.kKk9aW_card,.kKk9aW_header,.kKk9aW_chevron,.kKk9aW_chevronOpen,.kKk9aW_discard,.kKk9aW_save,.kKk9aW_selectChevron,.kKk9aW_selectChevronOpen,.kKk9aW_selectPopup{transition:none}}";
2001
+ const css$1 = ".kKk9aW_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}.kKk9aW_card:hover{border-color:var(--dsw-alias-label-dimmed)}.kKk9aW_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.kKk9aW_header{appearance:none;box-sizing:border-box;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}.kKk9aW_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.kKk9aW_headerStatic{box-sizing:border-box;border-radius:12px;align-items:center;gap:12px;width:100%;padding:14px 16px;display:flex}.kKk9aW_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.kKk9aW_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.kKk9aW_description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.kKk9aW_pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.kKk9aW_chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.kKk9aW_chevronOpen{transform:rotate(180deg)}.kKk9aW_body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}.kKk9aW_readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}.kKk9aW_notExposed{color:var(--dsw-alias-state-warn-primary);margin:12px 0 0;font-size:12px;line-height:1.5}.kKk9aW_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}.kKk9aW_failed{min-width:0;color:var(--dsw-alias-label-error);text-overflow:ellipsis;white-space:nowrap;flex:1;margin:0;font-size:12px;line-height:1.5;overflow:hidden}.kKk9aW_discard,.kKk9aW_save{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}.kKk9aW_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.kKk9aW_discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}.kKk9aW_save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.kKk9aW_discard:disabled,.kKk9aW_save:disabled{opacity:.4;cursor:default}.kKk9aW_discard:focus-visible,.kKk9aW_save:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.kKk9aW_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.kKk9aW_field+.kKk9aW_field{border-top:1px solid var(--dsw-alias-border-l2)}.kKk9aW_head{align-items:center;gap:8px;display:flex}.kKk9aW_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.kKk9aW_badges{align-items:center;gap:8px;display:inline-flex}.kKk9aW_badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.kKk9aW_reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}.kKk9aW_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.kKk9aW_reset:disabled{cursor:default}.kKk9aW_reset:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px;outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.kKk9aW_input,.kKk9aW_select{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.kKk9aW_input:focus-visible,.kKk9aW_select:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.kKk9aW_input:disabled,.kKk9aW_select:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.kKk9aW_inputInvalid{border:1px solid var(--dsw-alias-label-error);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.kKk9aW_inputInvalid:focus-visible{outline:2px solid var(--dsw-alias-label-error);outline-offset:1px;border-color:var(--dsw-alias-label-error)}.kKk9aW_selectWrap{position:relative}.kKk9aW_selectButton{appearance:none;text-align:left;cursor:pointer;justify-content:space-between;align-items:center;gap:8px;width:100%;display:flex}.kKk9aW_selectLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.kKk9aW_selectChevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.kKk9aW_selectChevronOpen{transform:rotate(180deg)}.kKk9aW_selectPopup{z-index:40;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);max-height:240px;box-shadow:0 8px 24px var(--dsw-alias-bg-mask-2);opacity:0;border-radius:8px;flex-direction:column;padding:4px;transition:opacity .1s,transform .1s;display:flex;position:absolute;top:calc(100% + 4px);left:0;right:0;overflow-y:auto;transform:translateY(-4px)}.kKk9aW_selectPopupOpen{opacity:1;transform:none}.kKk9aW_selectPopupClose{opacity:0;pointer-events:none;transform:translateY(-4px)}.kKk9aW_selectOption{color:var(--dsw-alias-label-primary);cursor:pointer;white-space:nowrap;text-overflow:ellipsis;border-radius:6px;flex-shrink:0;padding:6px 10px;font-size:13px;line-height:1.5;overflow:hidden}.kKk9aW_selectOption:hover,.kKk9aW_selectOptionActive{background:var(--dsw-alias-interactive-bg-hover)}.kKk9aW_selectOptionSelected{color:var(--dsw-alias-brand-primary);background:color-mix(in srgb, var(--dsw-alias-brand-primary-new-colorprimary-new-color) 10%, transparent);font-weight:500}.kKk9aW_invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}.kKk9aW_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}@media (prefers-reduced-motion:reduce){.kKk9aW_card,.kKk9aW_header,.kKk9aW_chevron,.kKk9aW_chevronOpen,.kKk9aW_discard,.kKk9aW_save,.kKk9aW_selectChevron,.kKk9aW_selectChevronOpen,.kKk9aW_selectPopup{transition:none}}";
1315
2002
  const tagId$1 = "@linxin666/dsh-pet/settings-card.module.css";
1316
2003
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
1317
2004
  const tag = document.createElement("style");
@@ -1396,7 +2083,7 @@ window.__ModuleLoader__.load({
1396
2083
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1397
2084
  className: settings_card_module_css_default.description,
1398
2085
  title: description,
1399
- children: description
2086
+ children: props.descriptionNode ?? description
1400
2087
  })]
1401
2088
  }), state.dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1402
2089
  className: settings_card_module_css_default.pending,
@@ -1421,7 +2108,7 @@ window.__ModuleLoader__.load({
1421
2108
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1422
2109
  className: settings_card_module_css_default.description,
1423
2110
  title: description,
1424
- children: description
2111
+ children: props.descriptionNode ?? description
1425
2112
  })]
1426
2113
  }),
1427
2114
  state.dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -2403,6 +3090,23 @@ window.__ModuleLoader__.load({
2403
3090
  "pet.live2d.vendor-missing": "Live2D 组件缺失,请升级宠物插件。",
2404
3091
  "pet.live2d.load-failed": "Live2D 模型加载失败,请检查该宠物目录的完整性。",
2405
3092
  "pet.openSessionHint": "点击跳转到对应会话",
3093
+ "pet.gameplay.menu": "玩法",
3094
+ "pet.gameplay.work": "打工",
3095
+ "pet.gameplay.stopWork": "收工",
3096
+ "pet.gameplay.sleep": "睡觉",
3097
+ "pet.gameplay.wake": "起床",
3098
+ "pet.gameplay.shop": "商店",
3099
+ "pet.gameplay.back": "返回",
3100
+ "pet.gameplay.buy": "购买",
3101
+ "pet.gameplay.insufficient": "{currency}不足",
3102
+ "pet.gameplay.prize": "中奖 +{amount} {currency}",
3103
+ "pet.gameplay.working": "打工中",
3104
+ "pet.gameplay.sleeping": "睡觉中",
3105
+ "pet.gameplay.stat.hunger": "饱食",
3106
+ "pet.gameplay.stat.mood": "心情",
3107
+ "pet.gameplay.stat.energy": "精力",
3108
+ "pet.gameplay.stat.affection": "好感",
3109
+ "pet.gameplay.currency.treats": "小鱼干",
2406
3110
  "pet.moreSessions": "展开其余 {n} 个会话的气泡",
2407
3111
  "pet.collapseSessions": "收起会话气泡",
2408
3112
  "settings.title": "宠物",
@@ -2456,6 +3160,23 @@ window.__ModuleLoader__.load({
2456
3160
  "pet.live2d.vendor-missing": "The Live2D component is missing; please update the pet plugin.",
2457
3161
  "pet.live2d.load-failed": "The Live2D model failed to load; check the pet directory is complete.",
2458
3162
  "pet.openSessionHint": "Click to jump to this session",
3163
+ "pet.gameplay.menu": "Play",
3164
+ "pet.gameplay.work": "Work",
3165
+ "pet.gameplay.stopWork": "Stop work",
3166
+ "pet.gameplay.sleep": "Sleep",
3167
+ "pet.gameplay.wake": "Wake up",
3168
+ "pet.gameplay.shop": "Shop",
3169
+ "pet.gameplay.back": "Back",
3170
+ "pet.gameplay.buy": "Buy",
3171
+ "pet.gameplay.insufficient": "Not enough {currency}",
3172
+ "pet.gameplay.prize": "Prize +{amount} {currency}",
3173
+ "pet.gameplay.working": "Working",
3174
+ "pet.gameplay.sleeping": "Sleeping",
3175
+ "pet.gameplay.stat.hunger": "Hunger",
3176
+ "pet.gameplay.stat.mood": "Mood",
3177
+ "pet.gameplay.stat.energy": "Energy",
3178
+ "pet.gameplay.stat.affection": "Affection",
3179
+ "pet.gameplay.currency.treats": "Treats",
2459
3180
  "pet.moreSessions": "Expand {n} more session bubbles",
2460
3181
  "pet.collapseSessions": "Collapse session bubbles",
2461
3182
  "settings.title": "Pet",
@@ -2521,7 +3242,7 @@ window.__ModuleLoader__.load({
2521
3242
  /** The building package's version, when the bundle carries it. */
2522
3243
  function bakedVersion() {
2523
3244
  try {
2524
- return "0.3.3";
3245
+ return "0.3.5";
2525
3246
  } catch {
2526
3247
  return;
2527
3248
  }
@@ -2597,13 +3318,17 @@ window.__ModuleLoader__.load({
2597
3318
  }
2598
3319
  /** The live host API instance (always defined; failures surface per call). */
2599
3320
  const petApi = {
2600
- state: () => petFetch("/api/pet/state"),
3321
+ state: (currentSessionId) => petFetch("/api/pet/state" + (currentSessionId === void 0 ? "" : "?current=" + encodeURIComponent(currentSessionId))),
2601
3322
  pets: () => petFetch("/api/pet/pets"),
2602
3323
  interact: (kind) => petFetch("/api/pet/interact", { kind }),
2603
3324
  setVisible: (visible) => petFetch("/api/pet/set-visible", { visible }),
2604
3325
  setConfig: (patch) => petFetch("/api/pet/set-config", patch),
2605
3326
  setName: (name) => petFetch("/api/pet/set-name", { name }),
2606
- setPet: (petId) => petFetch("/api/pet/set-pet", { petId })
3327
+ setPet: (petId) => petFetch("/api/pet/set-pet", { petId }),
3328
+ gameplayTouch: (zone) => petFetch("/api/pet/gameplay/touch", zone === void 0 ? {} : { zone }),
3329
+ gameplaySetMode: (mode) => petFetch("/api/pet/gameplay/mode", { mode }),
3330
+ gameplayWorkTick: () => petFetch("/api/pet/gameplay/work-tick", {}),
3331
+ gameplayBuy: (item) => petFetch("/api/pet/gameplay/buy", { item })
2607
3332
  };
2608
3333
  /** Poll interval for the host snapshot. */
2609
3334
  const POLL_MS = 2e3;
@@ -2637,6 +3362,7 @@ window.__ModuleLoader__.load({
2637
3362
  }
2638
3363
  }, "pet: dictionaries");
2639
3364
  defaultPetRendererRegistry.register(live2dRenderer);
3365
+ defaultPetRendererRegistry.register(frames2dRenderer);
2640
3366
  const settingsScope = (ctx.get("webUiSettings") ?? ctx.settingsScope).bind({ namespace: PET_SETTINGS_NS });
2641
3367
  const enabled = () => {
2642
3368
  const snapshot = settingsScope.getSnapshot();
@@ -2679,6 +3405,11 @@ window.__ModuleLoader__.load({
2679
3405
  const setPets = petStore.actions.setPets;
2680
3406
  const setState = petStore.actions.setState;
2681
3407
  const setFeedback = petStore.actions.setFeedback;
3408
+ const sessions = ctx.sessions;
3409
+ const currentSessionId = () => {
3410
+ const current = sessions.list.getSnapshot().current;
3411
+ return current === void 0 ? void 0 : String(current);
3412
+ };
2682
3413
  let petsLoaded = false;
2683
3414
  let stateSeq = 0;
2684
3415
  const pollNow = () => {
@@ -2688,7 +3419,7 @@ window.__ModuleLoader__.load({
2688
3419
  }, () => {});
2689
3420
  const seq = stateSeq + 1;
2690
3421
  stateSeq = seq;
2691
- petApi.state().then((snapshot) => {
3422
+ petApi.state(currentSessionId()).then((snapshot) => {
2692
3423
  if (seq !== stateSeq) return;
2693
3424
  setSnapshot(snapshot);
2694
3425
  }, () => {
@@ -2720,7 +3451,11 @@ window.__ModuleLoader__.load({
2720
3451
  document.removeEventListener("visibilitychange", onVisibility);
2721
3452
  };
2722
3453
  }, "pet: poll");
2723
- const sessions = ctx.sessions;
3454
+ ctx.effect(() => {
3455
+ return sessions.list.subscribe(() => {
3456
+ if (document.visibilityState === "visible") pollNow();
3457
+ });
3458
+ }, "pet: current-session watch");
2724
3459
  const openSession = (sessionId) => {
2725
3460
  if (sessions.list.getSnapshot().byId[sessionId] === void 0) return;
2726
3461
  sessions.open(sessionId);
@@ -2772,6 +3507,12 @@ window.__ModuleLoader__.load({
2772
3507
  },
2773
3508
  feedbackDone: () => {
2774
3509
  setFeedback(null);
3510
+ },
3511
+ gameplay: {
3512
+ touch: (zone) => petApi.gameplayTouch(zone),
3513
+ setMode: (mode) => petApi.gameplaySetMode(mode),
3514
+ workTick: () => petApi.gameplayWorkTick(),
3515
+ buy: (item) => petApi.gameplayBuy(item)
2775
3516
  }
2776
3517
  });
2777
3518
  takeoverPetUiTeardown();