@linxin666/dsh-pet 0.3.2 → 0.3.4

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 (127) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +25 -7
  3. package/README.zh.md +25 -7
  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/lib/client.js +830 -26
  17. package/lib/client.js.map +1 -1
  18. package/lib/index.js +1226 -24
  19. package/lib/types/client/PetDockEntry.d.ts +3 -0
  20. package/lib/types/client/PetDockEntry.d.ts.map +1 -1
  21. package/lib/types/client/PetDockEntry.js +18 -2
  22. package/lib/types/client/PetSprite.d.ts +23 -0
  23. package/lib/types/client/PetSprite.d.ts.map +1 -1
  24. package/lib/types/client/PetSprite.js +34 -29
  25. package/lib/types/client/PluginSettingsCard.d.ts +2 -0
  26. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
  27. package/lib/types/client/PluginSettingsCard.js +2 -2
  28. package/lib/types/client/drag-stream.d.ts +19 -0
  29. package/lib/types/client/drag-stream.d.ts.map +1 -0
  30. package/lib/types/client/drag-stream.js +26 -0
  31. package/lib/types/client/gameplay-hud.d.ts +50 -0
  32. package/lib/types/client/gameplay-hud.d.ts.map +1 -0
  33. package/lib/types/client/gameplay-hud.js +274 -0
  34. package/lib/types/client/index.d.ts +1 -1
  35. package/lib/types/client/index.d.ts.map +1 -1
  36. package/lib/types/client/index.js +16 -0
  37. package/lib/types/client/locales.d.ts +34 -0
  38. package/lib/types/client/locales.d.ts.map +1 -1
  39. package/lib/types/client/locales.js +36 -0
  40. package/lib/types/client/pet-store.d.ts +7 -1
  41. package/lib/types/client/pet-store.d.ts.map +1 -1
  42. package/lib/types/client/pet-store.js +4 -0
  43. package/lib/types/client/renderers/Frames2dVisualMount.d.ts +29 -0
  44. package/lib/types/client/renderers/Frames2dVisualMount.d.ts.map +1 -0
  45. package/lib/types/client/renderers/Frames2dVisualMount.js +83 -0
  46. package/lib/types/client/renderers/PetRendererSwitch.d.ts +6 -0
  47. package/lib/types/client/renderers/PetRendererSwitch.d.ts.map +1 -1
  48. package/lib/types/client/renderers/PetRendererSwitch.js +14 -1
  49. package/lib/types/client/renderers/frames2d.d.ts +36 -0
  50. package/lib/types/client/renderers/frames2d.d.ts.map +1 -0
  51. package/lib/types/client/renderers/frames2d.js +196 -0
  52. package/lib/types/client/settings-form.d.ts +1 -1
  53. package/lib/types/client/settings-form.js +1 -1
  54. package/lib/types/client/telemetry.d.ts +27 -0
  55. package/lib/types/client/telemetry.d.ts.map +1 -0
  56. package/lib/types/client/telemetry.js +97 -0
  57. package/lib/types/defaults.d.ts +6 -0
  58. package/lib/types/defaults.d.ts.map +1 -0
  59. package/lib/types/defaults.js +5 -0
  60. package/lib/types/dsh-home.d.ts.map +1 -1
  61. package/lib/types/dsh-home.js +9 -3
  62. package/lib/types/gameplay.d.ts +182 -0
  63. package/lib/types/gameplay.d.ts.map +1 -0
  64. package/lib/types/gameplay.js +618 -0
  65. package/lib/types/http.d.ts +1 -1
  66. package/lib/types/http.js +1 -1
  67. package/lib/types/index.d.ts +1 -1
  68. package/lib/types/index.js +1 -1
  69. package/lib/types/ledger.d.ts +14 -0
  70. package/lib/types/ledger.d.ts.map +1 -1
  71. package/lib/types/ledger.js +30 -0
  72. package/lib/types/loopback.d.ts +1 -1
  73. package/lib/types/loopback.js +1 -1
  74. package/lib/types/manifest-v2.d.ts +38 -1
  75. package/lib/types/manifest-v2.d.ts.map +1 -1
  76. package/lib/types/manifest-v2.js +182 -2
  77. package/lib/types/mount-once.d.ts +1 -1
  78. package/lib/types/mount-once.js +2 -2
  79. package/lib/types/persist.d.ts +4 -4
  80. package/lib/types/persist.d.ts.map +1 -1
  81. package/lib/types/persist.js +41 -4
  82. package/lib/types/registry.d.ts +29 -0
  83. package/lib/types/registry.d.ts.map +1 -1
  84. package/lib/types/registry.js +170 -1
  85. package/lib/types/routes.d.ts.map +1 -1
  86. package/lib/types/routes.js +22 -0
  87. package/lib/types/service.d.ts +56 -0
  88. package/lib/types/service.d.ts.map +1 -1
  89. package/lib/types/service.js +181 -0
  90. package/package.json +6 -3
  91. package/src/client/PetDockEntry.test.tsx +6 -0
  92. package/src/client/PetDockEntry.tsx +32 -1
  93. package/src/client/PetSprite.test.tsx +36 -6
  94. package/src/client/PetSprite.tsx +42 -21
  95. package/src/client/PluginSettingsCard.tsx +4 -2
  96. package/src/client/drag-stream.ts +35 -0
  97. package/src/client/gameplay-hud.test.tsx +338 -0
  98. package/src/client/gameplay-hud.tsx +403 -0
  99. package/src/client/index.ts +23 -2
  100. package/src/client/locales.ts +36 -0
  101. package/src/client/pet-store.ts +10 -1
  102. package/src/client/pet.module.css +192 -29
  103. package/src/client/renderers/Frames2dVisualMount.tsx +111 -0
  104. package/src/client/renderers/PetRendererSwitch.tsx +28 -1
  105. package/src/client/renderers/frames2d.test.ts +137 -0
  106. package/src/client/renderers/frames2d.ts +216 -0
  107. package/src/client/settings-card.module.css +5 -0
  108. package/src/client/settings-form.ts +1 -1
  109. package/src/client/telemetry.ts +105 -0
  110. package/src/defaults.ts +7 -0
  111. package/src/dsh-home.ts +9 -3
  112. package/src/gameplay.test.ts +239 -0
  113. package/src/gameplay.ts +692 -0
  114. package/src/http.ts +1 -1
  115. package/src/index.ts +1 -1
  116. package/src/ledger.test.ts +16 -0
  117. package/src/ledger.ts +31 -0
  118. package/src/loopback.ts +1 -1
  119. package/src/manifest-v2.test.ts +130 -1
  120. package/src/manifest-v2.ts +181 -2
  121. package/src/mount-once.ts +2 -2
  122. package/src/persist.test.ts +3 -0
  123. package/src/persist.ts +43 -6
  124. package/src/registry.test.ts +168 -1
  125. package/src/registry.ts +199 -1
  126. package/src/routes.ts +19 -0
  127. package/src/service.ts +208 -0
package/src/service.ts CHANGED
@@ -56,6 +56,19 @@ import {
56
56
  type PetStateInput,
57
57
  type PetStateSnapshot,
58
58
  } from './state.ts'
59
+ import {
60
+ applyGameplayEffects,
61
+ drawLotteryTier,
62
+ initialGameplayState,
63
+ rollTouchBranch,
64
+ rollWorkOutcome,
65
+ settleGameplay,
66
+ type PetGameplayManifest,
67
+ type PetGameplayState,
68
+ } from './gameplay.ts'
69
+
70
+ /** The unified gameplay currency: the shared treat (小鱼干) ledger. */
71
+ const GAMEPLAY_TREATS_CURRENCY = 'treats'
59
72
 
60
73
  /** Plugin configuration. */
61
74
  export interface PetConfig {
@@ -176,11 +189,40 @@ export interface PetStateView {
176
189
  * browser half renders no ornament.
177
190
  */
178
191
  decoration?: DecorationView
192
+ /**
193
+ * The selected pet's gameplay view (miku-pet generalization), present
194
+ * when the pet declares a gameplay block. Read-only projection: polling
195
+ * settles the view in memory but never writes pet.json.
196
+ */
197
+ gameplay?: PetGameplayStateView
179
198
  }
180
199
 
181
200
  /** Result of `pet.interact`. */
182
201
  export type PetInteractResult = LedgerInteractionResult
183
202
 
203
+ /** The gameplay slice of the state view (dynamic state only; defs ride the pet definition). */
204
+ export interface PetGameplayStateView {
205
+ /** Stat values rounded for display. */
206
+ stats: Record<string, number>
207
+ mode: 'work' | 'sleep' | null
208
+ }
209
+
210
+ /** Result of the gameplay verbs (touch / setMode / workTick / buy). */
211
+ export interface PetGameplayVerbResult {
212
+ ok: boolean
213
+ error?: string
214
+ /** Touch: whether a branch hit, plus its presentation. */
215
+ hit?: boolean
216
+ state?: string
217
+ stateMs?: number
218
+ phrase?: string
219
+ /** Work tick outcome. */
220
+ outcome?: 'success' | 'fail'
221
+ /** Shop purchase: the drawn prize, when the item was a lottery. */
222
+ prize?: { amount: number; currency: string }
223
+ view?: PetGameplayStateView
224
+ }
225
+
184
226
  declare module '@deepseek-ai/cordis' {
185
227
  interface Context {
186
228
  pet: PetService
@@ -483,6 +525,162 @@ export class PetService extends Service {
483
525
  return result
484
526
  }
485
527
 
528
+ /* ---------------------------------------------------------------- *
529
+ * Gameplay verbs (miku-pet generalization). The manifest block lives
530
+ * on the registry entry; dynamic state persists per pet id. Verbs
531
+ * settle and persist; the state view projects without writing.
532
+ * ---------------------------------------------------------------- */
533
+
534
+ /** The active pet's gameplay block, if it declares one. */
535
+ private gameplayDef(): PetGameplayManifest | undefined {
536
+ return this.activeEntry().gameplay
537
+ }
538
+
539
+ /** The persisted (or fresh) gameplay state of the selected pet. */
540
+ private gameplayState(def: PetGameplayManifest, now: number): { petId: string; state: PetGameplayState } {
541
+ const petId = this.selectedPetId()
542
+ const stored = this.ledger.snapshot.gameplay[petId]
543
+ return {
544
+ petId,
545
+ state: stored === undefined
546
+ ? initialGameplayState(def, now)
547
+ : { stats: { ...stored.stats }, currencies: { ...stored.currencies }, mode: stored.mode, settledAt: stored.settledAt },
548
+ }
549
+ }
550
+
551
+ /** Display view of one gameplay state (rounded stats; treats ride the shared treat ledger). */
552
+ private gameplayViewOf(state: PetGameplayState): PetGameplayStateView {
553
+ const stats: Record<string, number> = {}
554
+ for (const [name, value] of Object.entries(state.stats)) stats[name] = Math.round(value)
555
+ return { stats, mode: state.mode }
556
+ }
557
+
558
+ /**
559
+ * Move gameplay 'treats' currency (the unified post-wallet currency) from
560
+ * the engine's settle work area into the shared treat ledger, capped by
561
+ * the stock cap. The engine keeps its generic currency record for settle
562
+ * math; this drain is the only bridge to the wallet-free economy.
563
+ */
564
+ private drainGameplayTreats(state: PetGameplayState): void {
565
+ const pending = Math.floor(state.currencies[GAMEPLAY_TREATS_CURRENCY] ?? 0)
566
+ delete state.currencies[GAMEPLAY_TREATS_CURRENCY]
567
+ if (pending > 0) this.ledger.grantTreats(pending)
568
+ }
569
+
570
+ /** Persist the mutated gameplay state of one verb call. */
571
+ private commitGameplay(petId: string, state: PetGameplayState): void {
572
+ this.ledger.setGameplay(petId, state)
573
+ if (this.ledger.takeDirty()) this.flush()
574
+ }
575
+
576
+ /**
577
+ * RPC: a touch on the pet. 'zone' names a touch zone (roll a branch);
578
+ * omitted means a plain click while a touch animation holds (clickBoost).
579
+ */
580
+ async gameplayTouch(zone?: string): Promise<PetGameplayVerbResult> {
581
+ const def = this.gameplayDef()
582
+ if (def === undefined) return { ok: false, error: 'no-gameplay' }
583
+ const now = Date.now()
584
+ const { petId, state } = this.gameplayState(def, now)
585
+ settleGameplay(state, def, now, { sessionActive: this.machine.render().sessionActive })
586
+ if (zone === undefined) {
587
+ const boost = def.touch?.clickBoost
588
+ if (boost === undefined) return { ok: false, error: 'no-touch' }
589
+ const amount = boost.min + Math.floor(Math.random() * (boost.max - boost.min + 1))
590
+ if (amount > 0) applyGameplayEffects(state, def, [{ stat: boost.stat, amount }])
591
+ this.drainGameplayTreats(state)
592
+ this.commitGameplay(petId, state)
593
+ return { ok: true, hit: false, view: this.gameplayViewOf(state) }
594
+ }
595
+ const target = def.touch?.zones.find(entry => entry.name === zone)
596
+ if (target === undefined) return { ok: false, error: 'unknown-zone' }
597
+ const branch = rollTouchBranch(target, Math.random)
598
+ if (branch === undefined) {
599
+ this.drainGameplayTreats(state)
600
+ this.commitGameplay(petId, state)
601
+ return { ok: true, hit: false, view: this.gameplayViewOf(state) }
602
+ }
603
+ if (branch.effects !== undefined) applyGameplayEffects(state, def, branch.effects)
604
+ const phrase = branch.phrases !== undefined && branch.phrases.length > 0
605
+ ? branch.phrases[Math.floor(Math.random() * branch.phrases.length)]
606
+ : undefined
607
+ this.drainGameplayTreats(state)
608
+ this.commitGameplay(petId, state)
609
+ return {
610
+ ok: true,
611
+ hit: true,
612
+ ...(branch.state === undefined ? {} : { state: branch.state }),
613
+ ...(branch.stateMs === undefined ? {} : { stateMs: branch.stateMs }),
614
+ ...(phrase === undefined ? {} : { phrase }),
615
+ view: this.gameplayViewOf(state),
616
+ }
617
+ }
618
+
619
+ /** RPC: enter or leave a gameplay mode ('work' | 'sleep' | null). */
620
+ async gameplaySetMode(mode: 'work' | 'sleep' | null): Promise<PetGameplayVerbResult> {
621
+ const def = this.gameplayDef()
622
+ if (def === undefined) return { ok: false, error: 'no-gameplay' }
623
+ if (mode === 'work' && def.work === undefined) return { ok: false, error: 'no-work' }
624
+ if (mode === 'sleep' && def.sleep === undefined) return { ok: false, error: 'no-sleep' }
625
+ const now = Date.now()
626
+ const { petId, state } = this.gameplayState(def, now)
627
+ settleGameplay(state, def, now, { sessionActive: this.machine.render().sessionActive })
628
+ state.mode = mode
629
+ this.drainGameplayTreats(state)
630
+ this.commitGameplay(petId, state)
631
+ return { ok: true, view: this.gameplayViewOf(state) }
632
+ }
633
+
634
+ /** RPC: one work-round adjudication (only while the work mode holds). */
635
+ async gameplayWorkTick(): Promise<PetGameplayVerbResult> {
636
+ const def = this.gameplayDef()
637
+ if (def?.work === undefined) return { ok: false, error: 'no-work' }
638
+ const now = Date.now()
639
+ const { petId, state } = this.gameplayState(def, now)
640
+ if (state.mode !== 'work') return { ok: false, error: 'not-working' }
641
+ settleGameplay(state, def, now, { sessionActive: this.machine.render().sessionActive })
642
+ const outcome = rollWorkOutcome(def.work, Math.random)
643
+ const effects = outcome === 'success' ? def.work.success?.effects : def.work.fail?.effects
644
+ if (effects !== undefined) applyGameplayEffects(state, def, effects)
645
+ this.drainGameplayTreats(state)
646
+ this.commitGameplay(petId, state)
647
+ return { ok: true, outcome, view: this.gameplayViewOf(state) }
648
+ }
649
+
650
+ /** RPC: buy one shop item (effects, currency swap, or a lottery draw). */
651
+ async gameplayBuy(itemId: string): Promise<PetGameplayVerbResult> {
652
+ const def = this.gameplayDef()
653
+ if (def === undefined) return { ok: false, error: 'no-gameplay' }
654
+ const item = def.shop?.items.find(entry => entry.id === itemId)
655
+ if (item === undefined) return { ok: false, error: 'unknown-item' }
656
+ const now = Date.now()
657
+ const { petId, state } = this.gameplayState(def, now)
658
+ settleGameplay(state, def, now, { sessionActive: this.machine.render().sessionActive })
659
+ // The unified currency is the shared treat ledger (wallet removed): shop
660
+ // prices and prizes ride the same balance the panel shows and feeding
661
+ // consumes, capped by the stock cap.
662
+ const treats = item.currency === GAMEPLAY_TREATS_CURRENCY
663
+ const balance = treats ? this.ledger.snapshot.treats.treats : (state.currencies[item.currency] ?? 0)
664
+ if (balance < item.price) {
665
+ return { ok: false, error: 'insufficient-funds', view: this.gameplayViewOf(state) }
666
+ }
667
+ if (treats) this.ledger.spendTreats(item.price)
668
+ else state.currencies[item.currency] = balance - item.price
669
+ if (item.effects !== undefined) applyGameplayEffects(state, def, item.effects)
670
+ let prize: PetGameplayVerbResult['prize']
671
+ if (item.lottery !== undefined) {
672
+ if (item.lottery.effects !== undefined) applyGameplayEffects(state, def, item.lottery.effects)
673
+ const tier = drawLotteryTier(item.lottery, Math.random)
674
+ const prizeCurrency = tier.currency ?? item.lottery.currency ?? item.currency
675
+ if (prizeCurrency === GAMEPLAY_TREATS_CURRENCY) this.ledger.grantTreats(tier.prize)
676
+ else state.currencies[prizeCurrency] = (state.currencies[prizeCurrency] ?? 0) + tier.prize
677
+ prize = { amount: tier.prize, currency: prizeCurrency }
678
+ }
679
+ this.drainGameplayTreats(state)
680
+ this.commitGameplay(petId, state)
681
+ return { ok: true, ...(prize === undefined ? {} : { prize }), view: this.gameplayViewOf(state) }
682
+ }
683
+
486
684
  /** RPC: show or hide the pet. */
487
685
  async setVisible(visible: boolean): Promise<{ ok: true; display: PetDisplayConfig }> {
488
686
  this.ledger.setDisplay({ ...this.ledger.snapshot.display, visible })
@@ -597,6 +795,15 @@ export class PetService extends Service {
597
795
  ? whisper.text
598
796
  : undefined
599
797
  const decoration = this.activeDecoration()
798
+ // Gameplay view: a read-only projection. The settle runs on a copy, so
799
+ // polling never writes pet.json (verbs settle and persist).
800
+ const gameplayDef = this.gameplayDef()
801
+ let gameplay: PetGameplayStateView | undefined
802
+ if (gameplayDef !== undefined) {
803
+ const { state } = this.gameplayState(gameplayDef, Date.now())
804
+ settleGameplay(state, gameplayDef, Date.now(), { sessionActive: snapshot.sessionActive })
805
+ gameplay = this.gameplayViewOf(state)
806
+ }
600
807
  // Read-only: the ledger settles on economic events only, never on a read,
601
808
  // so polling the state cannot trigger pet.json writes.
602
809
  return {
@@ -619,6 +826,7 @@ export class PetService extends Service {
619
826
  stocked: this.ledger.snapshot.treats.treats,
620
827
  max: this.ledger.treatMax,
621
828
  },
829
+ ...(gameplay === undefined ? {} : { gameplay }),
622
830
  }
623
831
  }
624
832