@linxin666/dsh-pet 0.3.3 → 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 (107) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +17 -3
  3. package/README.zh.md +17 -3
  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 +759 -26
  17. package/lib/client.js.map +1 -1
  18. package/lib/index.js +1224 -22
  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.map +1 -1
  35. package/lib/types/client/index.js +12 -0
  36. package/lib/types/client/locales.d.ts +34 -0
  37. package/lib/types/client/locales.d.ts.map +1 -1
  38. package/lib/types/client/locales.js +36 -0
  39. package/lib/types/client/pet-store.d.ts +7 -1
  40. package/lib/types/client/pet-store.d.ts.map +1 -1
  41. package/lib/types/client/pet-store.js +4 -0
  42. package/lib/types/client/renderers/Frames2dVisualMount.d.ts +29 -0
  43. package/lib/types/client/renderers/Frames2dVisualMount.d.ts.map +1 -0
  44. package/lib/types/client/renderers/Frames2dVisualMount.js +83 -0
  45. package/lib/types/client/renderers/PetRendererSwitch.d.ts +6 -0
  46. package/lib/types/client/renderers/PetRendererSwitch.d.ts.map +1 -1
  47. package/lib/types/client/renderers/PetRendererSwitch.js +14 -1
  48. package/lib/types/client/renderers/frames2d.d.ts +36 -0
  49. package/lib/types/client/renderers/frames2d.d.ts.map +1 -0
  50. package/lib/types/client/renderers/frames2d.js +196 -0
  51. package/lib/types/defaults.d.ts +6 -0
  52. package/lib/types/defaults.d.ts.map +1 -0
  53. package/lib/types/defaults.js +5 -0
  54. package/lib/types/dsh-home.d.ts.map +1 -1
  55. package/lib/types/dsh-home.js +9 -3
  56. package/lib/types/gameplay.d.ts +182 -0
  57. package/lib/types/gameplay.d.ts.map +1 -0
  58. package/lib/types/gameplay.js +618 -0
  59. package/lib/types/ledger.d.ts +14 -0
  60. package/lib/types/ledger.d.ts.map +1 -1
  61. package/lib/types/ledger.js +30 -0
  62. package/lib/types/manifest-v2.d.ts +38 -1
  63. package/lib/types/manifest-v2.d.ts.map +1 -1
  64. package/lib/types/manifest-v2.js +182 -2
  65. package/lib/types/persist.d.ts +4 -4
  66. package/lib/types/persist.d.ts.map +1 -1
  67. package/lib/types/persist.js +41 -4
  68. package/lib/types/registry.d.ts +29 -0
  69. package/lib/types/registry.d.ts.map +1 -1
  70. package/lib/types/registry.js +170 -1
  71. package/lib/types/routes.d.ts.map +1 -1
  72. package/lib/types/routes.js +22 -0
  73. package/lib/types/service.d.ts +56 -0
  74. package/lib/types/service.d.ts.map +1 -1
  75. package/lib/types/service.js +181 -0
  76. package/package.json +5 -2
  77. package/src/client/PetDockEntry.test.tsx +6 -0
  78. package/src/client/PetDockEntry.tsx +32 -1
  79. package/src/client/PetSprite.test.tsx +36 -6
  80. package/src/client/PetSprite.tsx +42 -21
  81. package/src/client/PluginSettingsCard.tsx +4 -2
  82. package/src/client/drag-stream.ts +35 -0
  83. package/src/client/gameplay-hud.test.tsx +338 -0
  84. package/src/client/gameplay-hud.tsx +403 -0
  85. package/src/client/index.ts +17 -1
  86. package/src/client/locales.ts +36 -0
  87. package/src/client/pet-store.ts +10 -1
  88. package/src/client/pet.module.css +192 -29
  89. package/src/client/renderers/Frames2dVisualMount.tsx +111 -0
  90. package/src/client/renderers/PetRendererSwitch.tsx +28 -1
  91. package/src/client/renderers/frames2d.test.ts +137 -0
  92. package/src/client/renderers/frames2d.ts +216 -0
  93. package/src/client/settings-card.module.css +5 -0
  94. package/src/defaults.ts +7 -0
  95. package/src/dsh-home.ts +9 -3
  96. package/src/gameplay.test.ts +239 -0
  97. package/src/gameplay.ts +692 -0
  98. package/src/ledger.test.ts +16 -0
  99. package/src/ledger.ts +31 -0
  100. package/src/manifest-v2.test.ts +130 -1
  101. package/src/manifest-v2.ts +181 -2
  102. package/src/persist.test.ts +3 -0
  103. package/src/persist.ts +43 -6
  104. package/src/registry.test.ts +168 -1
  105. package/src/registry.ts +199 -1
  106. package/src/routes.ts +19 -0
  107. package/src/service.ts +208 -0
@@ -38,6 +38,7 @@ import { PET_DECORATION_API_VERSION } from "./contracts/status-decoration.js";
38
38
  import { dshHome } from "./dsh-home.js";
39
39
  import { parsePetManifest } from "./manifest-v2.js";
40
40
  import { collectModel3References } from "./model3.js";
41
+ import { DEFAULT_PET_ID } from "./defaults.js";
41
42
  /** Fixed row order of the 9-state animation contract. */
42
43
  export const PET_ROW_ORDER = [
43
44
  'idle',
@@ -367,6 +368,169 @@ function resolveLive2dEntry(manifest, dir, options) {
367
368
  ...(remarks === undefined ? {} : { remarks }),
368
369
  };
369
370
  }
371
+ /** Filename-encoded frame duration tail ('<base>_<index>_<ms>.webp'). */
372
+ const FRAMES2D_FILENAME_MS = /_(\d+)\.[^.]+$/;
373
+ /** Trailing frame index, allowing an optional '_<ms>' duration tail after it. */
374
+ const FRAMES2D_FRAME_INDEX = /(\d+)(?:_\d+)?\.[^.]+$/;
375
+ /** Default per-frame duration when neither frameMs nor the filename encodes one. */
376
+ const FRAMES2D_DEFAULT_FRAME_MS = 200;
377
+ /** Image extensions a frames2d track directory may list. */
378
+ const FRAMES2D_IMAGE_EXTENSIONS = new Set(['.webp', '.png', '.gif', '.jpg', '.jpeg']);
379
+ /**
380
+ * Resolve a validated frames2d manifest into a renderable entry. Track frame
381
+ * lists come from the manifest's explicit list or from listing
382
+ * '<dir>/<track>/' in filename order; durations resolve frameMs[i] >
383
+ * filename-encoded '_<ms>' tail > defaultFrameMs. Missing frames warn and
384
+ * skip (the live2d closure discipline); a track left with zero frames is
385
+ * dropped, and if the idle-mapped track ends up empty the entry is rejected
386
+ * fail-closed. The sprite fields carry contract defaults: the chrome sizes
387
+ * frames2d pets off 'display.size', not the atlas.
388
+ */
389
+ function resolveFrames2dEntry(manifest, dir, options) {
390
+ const assetPrefix = options.assetPrefix ?? '/pet';
391
+ const record = (level, message) => {
392
+ options.diagnostics?.push({ level, source: dir, message });
393
+ options.warnings?.push(message);
394
+ };
395
+ const block = manifest.frames2d;
396
+ if (block === undefined) {
397
+ record('error', 'pet ' + manifest.id + ': renderer frames2d requires a frames2d block');
398
+ return undefined;
399
+ }
400
+ const root = block.dir ?? '.';
401
+ const defaultMs = block.defaultFrameMs ?? FRAMES2D_DEFAULT_FRAME_MS;
402
+ const idleTrack = block.phases.idle;
403
+ const tracks = {};
404
+ const servable = [];
405
+ const firstFrameRel = {};
406
+ for (const [name, track] of Object.entries(block.tracks)) {
407
+ const trackDir = root === '.' ? name : root + '/' + name;
408
+ let relFrames = [];
409
+ if (track.frames !== undefined) {
410
+ for (const frame of track.frames) {
411
+ const rel = trackDir + '/' + frame;
412
+ if (!existsSync(join(dir, rel))) {
413
+ record('warning', 'pet ' + manifest.id + ': frames2d frame missing: ' + rel);
414
+ continue;
415
+ }
416
+ relFrames.push(rel);
417
+ }
418
+ }
419
+ else {
420
+ let files = [];
421
+ try {
422
+ files = readdirSync(join(dir, trackDir)).filter(file => {
423
+ if (file.startsWith('.'))
424
+ return false;
425
+ const dot = file.lastIndexOf('.');
426
+ return dot > 0 && FRAMES2D_IMAGE_EXTENSIONS.has(file.slice(dot).toLowerCase());
427
+ });
428
+ }
429
+ catch {
430
+ files = [];
431
+ }
432
+ // Natural frame order: the trailing index (before any _<ms> duration
433
+ // suffix) sorts numerically, so 'eat10' comes after 'eat9' — plain
434
+ // lexicographic order would break any track with 10+ frames.
435
+ files.sort((a, b) => {
436
+ const ia = FRAMES2D_FRAME_INDEX.exec(a)?.[1];
437
+ const ib = FRAMES2D_FRAME_INDEX.exec(b)?.[1];
438
+ if (ia !== undefined && ib !== undefined && ia !== ib)
439
+ return Number(ia) - Number(ib);
440
+ return a < b ? -1 : a > b ? 1 : 0;
441
+ });
442
+ relFrames = files.map(file => trackDir + '/' + file);
443
+ }
444
+ if (relFrames.length === 0) {
445
+ record('warning', 'pet ' + manifest.id + ': frames2d track ' + JSON.stringify(name) + ' has no frames on disk; dropped');
446
+ continue;
447
+ }
448
+ const durations = relFrames.map((rel, index) => {
449
+ if (track.frameMs !== undefined)
450
+ return track.frameMs[index] ?? defaultMs;
451
+ const match = FRAMES2D_FILENAME_MS.exec(rel);
452
+ if (match !== null) {
453
+ const ms = Number(match[1]);
454
+ if (Number.isInteger(ms) && ms >= 16 && ms <= 5000)
455
+ return ms;
456
+ }
457
+ return defaultMs;
458
+ });
459
+ const loop = track.loop ?? true;
460
+ const view = {
461
+ frames: relFrames.map(rel => assetUrl(assetPrefix, manifest.id, rel)),
462
+ durations,
463
+ loop,
464
+ ...(loop ? {} : { fallback: track.fallback ?? idleTrack }),
465
+ };
466
+ tracks[name] = view;
467
+ firstFrameRel[name] = relFrames[0];
468
+ servable.push(...relFrames);
469
+ }
470
+ if (tracks[idleTrack] === undefined) {
471
+ record('error', 'pet ' + manifest.id + ': frames2d idle track ' + JSON.stringify(idleTrack) + ' has no frames on disk');
472
+ return undefined;
473
+ }
474
+ const phases = { ...block.phases };
475
+ for (const [phase, target] of Object.entries(phases)) {
476
+ if (target !== undefined && tracks[target] === undefined) {
477
+ record('warning', 'pet ' + manifest.id + ': frames2d phase ' + phase + ' maps to dropped track ' + JSON.stringify(target) + '; using idle');
478
+ phases[phase] = idleTrack;
479
+ }
480
+ }
481
+ const remarks = normalizePetRemarks(manifest.remarks, message => record('warning', 'pet ' + manifest.id + ': ' + message));
482
+ // Gameplay layer: shop item icons are manifest-relative frame paths —
483
+ // verify them, promote them to browser URLs, and add them to the servable
484
+ // allow-list so the asset route can serve them.
485
+ let gameplay;
486
+ if (manifest.gameplay !== undefined) {
487
+ gameplay = manifest.gameplay;
488
+ if (gameplay.shop !== undefined) {
489
+ const items = [];
490
+ for (const item of gameplay.shop.items) {
491
+ if (item.image === undefined) {
492
+ items.push(item);
493
+ continue;
494
+ }
495
+ if (!existsSync(join(dir, item.image))) {
496
+ record('warning', 'pet ' + manifest.id + ': gameplay shop item ' + item.id + ' image missing: ' + item.image);
497
+ const { image, ...rest } = item;
498
+ items.push(rest);
499
+ continue;
500
+ }
501
+ servable.push(item.image);
502
+ items.push({ ...item, image: assetUrl(assetPrefix, manifest.id, item.image) });
503
+ }
504
+ gameplay = { ...gameplay, shop: { ...gameplay.shop, items } };
505
+ }
506
+ }
507
+ const flatTracks = buildTracks(DEFAULT_FRAME_COUNTS, DEFAULT_PET_COLUMNS, {}, message => record('warning', 'pet ' + manifest.id + ': ' + message));
508
+ if (flatTracks === undefined)
509
+ return undefined;
510
+ // The render box comes from the first idle frame when decodable.
511
+ const firstAbs = join(dir, firstFrameRel[idleTrack]);
512
+ const dims = existsSync(firstAbs) ? readImageDimensions(firstAbs) : undefined;
513
+ const cell = dims !== undefined && dims.width >= 1 && dims.height >= 1 ? dims : { ...DEFAULT_PET_CELL };
514
+ return {
515
+ id: manifest.id,
516
+ displayName: manifest.displayName,
517
+ description: manifest.description ?? '',
518
+ renderer: 'frames2d',
519
+ frames2d: { tracks, phases },
520
+ ...(gameplay === undefined ? {} : { gameplay }),
521
+ cell,
522
+ columns: DEFAULT_PET_COLUMNS,
523
+ rows: [...DEFAULT_FRAME_COUNTS],
524
+ atlasRows: DEFAULT_PET_ROW_COUNT,
525
+ tracks: flatTracks,
526
+ atlasUrl: tracks[idleTrack].frames[0],
527
+ manifestUrl: assetUrl(assetPrefix, manifest.id, 'pet.json'),
528
+ dir,
529
+ spritesheetPath: firstFrameRel[idleTrack],
530
+ servable,
531
+ ...(remarks === undefined ? {} : { remarks }),
532
+ };
533
+ }
370
534
  /** Scan one directory of pet folders; entries come back in name order. */
371
535
  function scanPetDir(dir, options) {
372
536
  if (!existsSync(dir))
@@ -399,6 +563,9 @@ function scanPetDir(dir, options) {
399
563
  if (verdict.manifest.renderer === 'live2d') {
400
564
  entry = resolveLive2dEntry(verdict.manifest, entryDir, options);
401
565
  }
566
+ else if (verdict.manifest.renderer === 'frames2d') {
567
+ entry = resolveFrames2dEntry(verdict.manifest, entryDir, options);
568
+ }
402
569
  else {
403
570
  const legacy = flattenV2Sprite2d(verdict.manifest);
404
571
  if (legacy === undefined) {
@@ -695,7 +862,7 @@ export function loadPetRegistry(options) {
695
862
  warnings,
696
863
  diagnostics,
697
864
  byId: (id) => byId.get(id),
698
- defaultEntry: () => entries.find(entry => builtinIds.has(entry.id)) ?? entries[0],
865
+ defaultEntry: () => entries.find(entry => (entry.id === DEFAULT_PET_ID && builtinIds.has(entry.id))) ?? entries.find(entry => builtinIds.has(entry.id)) ?? entries[0],
699
866
  ...(globalVoice === undefined ? {} : { globalVoice }),
700
867
  decorations,
701
868
  decorationById: (id) => decorationById.get(id),
@@ -733,6 +900,8 @@ export function petEntryView(entry, globalVoice) {
733
900
  description: entry.description,
734
901
  renderer: entry.renderer,
735
902
  ...(entry.live2d === undefined ? {} : { live2d: entry.live2d }),
903
+ ...(entry.frames2d === undefined ? {} : { frames2d: entry.frames2d }),
904
+ ...(entry.gameplay === undefined ? {} : { gameplay: entry.gameplay }),
736
905
  cell: entry.cell,
737
906
  columns: entry.columns,
738
907
  rows: entry.rows,
@@ -1 +1 @@
1
- {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAO9C,+CAA+C;AAC/C,eAAO,MAAM,cAAc,aAAa,CAAA;AAExC,0EAA0E;AAC1E,eAAO,MAAM,gBAAgB,SAAS,CAAA;AAMtC;;;;GAIG;AACH,eAAO,MAAM,cAAc;IACzB,yBAAyB;;IAEzB,iDAAiD;;IAEjD,8EAA8E;;CAEtE,CAAA;AAEV,6DAA6D;AAC7D,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAWD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAUrF;AAwND,4EAA4E;AAC5E,eAAO,MAAM,kBAAkB,QAA8B,CAAA;AAe7D,4EAA4E;AAC5E,eAAO,MAAM,eAAe,QAAmB,CAAA;AAE/C,6EAA6E;AAC7E,MAAM,WAAW,eAAe;IAC9B,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAwMD,4EAA4E;AAC5E,wBAAgB,aAAa,CAAC,IAAI,EAAE;IAAE,OAAO,EAAE,UAAU,CAAC;IAAC,GAAG,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,YAAY,CAAA;CAAE,GAAG,eAAe,GAAG,QAAQ,EAAE,CAwDjI;AAGD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA"}
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAO9C,+CAA+C;AAC/C,eAAO,MAAM,cAAc,aAAa,CAAA;AAExC,0EAA0E;AAC1E,eAAO,MAAM,gBAAgB,SAAS,CAAA;AAMtC;;;;GAIG;AACH,eAAO,MAAM,cAAc;IACzB,yBAAyB;;IAEzB,iDAAiD;;IAEjD,8EAA8E;;CAEtE,CAAA;AAEV,6DAA6D;AAC7D,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAWD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAUrF;AAwND,4EAA4E;AAC5E,eAAO,MAAM,kBAAkB,QAA8B,CAAA;AAe7D,4EAA4E;AAC5E,eAAO,MAAM,eAAe,QAAmB,CAAA;AAE/C,6EAA6E;AAC7E,MAAM,WAAW,eAAe;IAC9B,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAwMD,4EAA4E;AAC5E,wBAAgB,aAAa,CAAC,IAAI,EAAE;IAAE,OAAO,EAAE,UAAU,CAAC;IAAC,GAAG,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,YAAY,CAAA;CAAE,GAAG,eAAe,GAAG,QAAQ,EAAE,CA2EjI;AAGD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA"}
@@ -539,6 +539,28 @@ export function makePetRoutes(deps) {
539
539
  return Promise.reject(new Error('invalid-pet'));
540
540
  return service.setPetId(petId);
541
541
  }),
542
+ // Gameplay verbs (miku-pet generalization): touch rolls a named zone's
543
+ // branch; the omitted-zone form is the plain-click boost during a touch
544
+ // animation. Mode/tick/buy drive the work, sleep and shop loops.
545
+ postRoute(ctx, PET_API_PREFIX + '/gameplay/touch', (body) => {
546
+ const zone = body.zone;
547
+ if (zone !== undefined && typeof zone !== 'string')
548
+ return Promise.reject(new Error('invalid-zone'));
549
+ return service.gameplayTouch(zone);
550
+ }),
551
+ postRoute(ctx, PET_API_PREFIX + '/gameplay/mode', (body) => {
552
+ const mode = body.mode;
553
+ if (mode !== null && mode !== 'work' && mode !== 'sleep')
554
+ return Promise.reject(new Error('invalid-mode'));
555
+ return service.gameplaySetMode(mode);
556
+ }),
557
+ postRoute(ctx, PET_API_PREFIX + '/gameplay/work-tick', () => service.gameplayWorkTick()),
558
+ postRoute(ctx, PET_API_PREFIX + '/gameplay/buy', (body) => {
559
+ const item = body.item;
560
+ if (typeof item !== 'string')
561
+ return Promise.reject(new Error('invalid-item'));
562
+ return service.gameplayBuy(item);
563
+ }),
542
564
  ];
543
565
  const assetRoute = {
544
566
  kind: 'prefix',
@@ -133,9 +133,39 @@ export interface PetStateView {
133
133
  * browser half renders no ornament.
134
134
  */
135
135
  decoration?: DecorationView;
136
+ /**
137
+ * The selected pet's gameplay view (miku-pet generalization), present
138
+ * when the pet declares a gameplay block. Read-only projection: polling
139
+ * settles the view in memory but never writes pet.json.
140
+ */
141
+ gameplay?: PetGameplayStateView;
136
142
  }
137
143
  /** Result of `pet.interact`. */
138
144
  export type PetInteractResult = LedgerInteractionResult;
145
+ /** The gameplay slice of the state view (dynamic state only; defs ride the pet definition). */
146
+ export interface PetGameplayStateView {
147
+ /** Stat values rounded for display. */
148
+ stats: Record<string, number>;
149
+ mode: 'work' | 'sleep' | null;
150
+ }
151
+ /** Result of the gameplay verbs (touch / setMode / workTick / buy). */
152
+ export interface PetGameplayVerbResult {
153
+ ok: boolean;
154
+ error?: string;
155
+ /** Touch: whether a branch hit, plus its presentation. */
156
+ hit?: boolean;
157
+ state?: string;
158
+ stateMs?: number;
159
+ phrase?: string;
160
+ /** Work tick outcome. */
161
+ outcome?: 'success' | 'fail';
162
+ /** Shop purchase: the drawn prize, when the item was a lottery. */
163
+ prize?: {
164
+ amount: number;
165
+ currency: string;
166
+ };
167
+ view?: PetGameplayStateView;
168
+ }
139
169
  declare module '@deepseek-ai/cordis' {
140
170
  interface Context {
141
171
  pet: PetService;
@@ -233,6 +263,32 @@ export declare class PetService extends Service {
233
263
  private applyActivity;
234
264
  /** RPC: pet or feed the pet. */
235
265
  interact(kind: PetInteraction): Promise<PetInteractResult>;
266
+ /** The active pet's gameplay block, if it declares one. */
267
+ private gameplayDef;
268
+ /** The persisted (or fresh) gameplay state of the selected pet. */
269
+ private gameplayState;
270
+ /** Display view of one gameplay state (rounded stats; treats ride the shared treat ledger). */
271
+ private gameplayViewOf;
272
+ /**
273
+ * Move gameplay 'treats' currency (the unified post-wallet currency) from
274
+ * the engine's settle work area into the shared treat ledger, capped by
275
+ * the stock cap. The engine keeps its generic currency record for settle
276
+ * math; this drain is the only bridge to the wallet-free economy.
277
+ */
278
+ private drainGameplayTreats;
279
+ /** Persist the mutated gameplay state of one verb call. */
280
+ private commitGameplay;
281
+ /**
282
+ * RPC: a touch on the pet. 'zone' names a touch zone (roll a branch);
283
+ * omitted means a plain click while a touch animation holds (clickBoost).
284
+ */
285
+ gameplayTouch(zone?: string): Promise<PetGameplayVerbResult>;
286
+ /** RPC: enter or leave a gameplay mode ('work' | 'sleep' | null). */
287
+ gameplaySetMode(mode: 'work' | 'sleep' | null): Promise<PetGameplayVerbResult>;
288
+ /** RPC: one work-round adjudication (only while the work mode holds). */
289
+ gameplayWorkTick(): Promise<PetGameplayVerbResult>;
290
+ /** RPC: buy one shop item (effects, currency swap, or a lottery draw). */
291
+ gameplayBuy(itemId: string): Promise<PetGameplayVerbResult>;
236
292
  /** RPC: show or hide the pet. */
237
293
  setVisible(visible: boolean): Promise<{
238
294
  ok: true;
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAEtD,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AACpF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAQ9C,OAAO,EAAgC,KAAK,uBAAuB,EAAE,MAAM,aAAa,CAAA;AACxF,OAAO,EASL,KAAK,gBAAgB,EACtB,MAAM,cAAc,CAAA;AACrB,OAAO,EAML,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,qBAAqB,EAC3B,MAAM,eAAe,CAAA;AAGtB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAA;AACtE,OAAO,EAGL,KAAK,cAAc,EAEnB,KAAK,gBAAgB,EACtB,MAAM,YAAY,CAAA;AAenB,4BAA4B;AAC5B,MAAM,WAAW,SAAS;IACxB,uBAAuB;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAA;IAClC,4BAA4B;IAC5B,KAAK,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAA;IAC/B,4BAA4B;IAC5B,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAA;IAC7B,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,iEAAiE;IACjE,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,6EAA6E;IAC7E,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,WAAW,CAAA;IACtB,oEAAoE;IACpE,IAAI,CAAC,EAAE,SAAS,WAAW,EAAE,CAAA;CAC9B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,kBAAkB;IACjC,2EAA2E;IAC3E,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,qBAAqB;IACrB,OAAO,EAAE,OAAO,CAAA;IAChB,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAA;IACZ,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAA;IACb,wDAAwD;IACxD,MAAM,EAAE,MAAM,CAAA;IACd,iEAAiE;IACjE,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAC5B;AAED,2HAA2H;AAC3H,eAAO,MAAM,sBAAsB,QAAQ,CAAA;AAE3C;;;;;;;GAOG;AACH,MAAM,WAAW,cAAc;IAC7B,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAA;IACjB,uDAAuD;IACvD,SAAS,EAAE,gBAAgB,CAAC,WAAW,CAAC,CAAA;IACxC,yCAAyC;IACzC,MAAM,EAAE,MAAM,CAAA;IACd,yCAAyC;IACzC,KAAK,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAA;CACjC;AAED,gFAAgF;AAChF,eAAO,MAAM,mBAAmB,KAAK,CAAA;AAErC,wCAAwC;AACxC,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,gBAAgB,CAAC,WAAW,CAAC,CAAA;IACxC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAA;IAChC,aAAa,EAAE,OAAO,CAAA;IACtB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,cAAc,EAAE,CAAA;IAC3B,gCAAgC;IAChC,QAAQ,EAAE,eAAe,CAAA;IACzB,6BAA6B;IAC7B,OAAO,EAAE,gBAAgB,CAAA;IACzB,4CAA4C;IAC5C,GAAG,EAAE;QACH,mBAAmB;QACnB,EAAE,EAAE,MAAM,CAAA;QACV,iDAAiD;QACjD,WAAW,EAAE,MAAM,CAAA;QACnB,4BAA4B;QAC5B,WAAW,EAAE,MAAM,CAAA;KACpB,CAAA;IACD,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAA;IACZ,kCAAkC;IAClC,MAAM,EAAE;QACN,0BAA0B;QAC1B,OAAO,EAAE,MAAM,CAAA;QACf,iBAAiB;QACjB,GAAG,EAAE,MAAM,CAAA;KACZ,CAAA;IACD;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;OAIG;IACH,UAAU,CAAC,EAAE,cAAc,CAAA;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,oBAAoB,CAAA;CAChC;AAED,gCAAgC;AAChC,MAAM,MAAM,iBAAiB,GAAG,uBAAuB,CAAA;AAEvD,+FAA+F;AAC/F,MAAM,WAAW,oBAAoB;IACnC,uCAAuC;IACvC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC7B,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAAA;CAC9B;AAED,uEAAuE;AACvE,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,OAAO,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,0DAA0D;IAC1D,GAAG,CAAC,EAAE,OAAO,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,yBAAyB;IACzB,OAAO,CAAC,EAAE,SAAS,GAAG,MAAM,CAAA;IAC5B,mEAAmE;IACnE,KAAK,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAA;IAC5C,IAAI,CAAC,EAAE,oBAAoB,CAAA;CAC5B;AAED,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf,GAAG,EAAE,UAAU,CAAA;KAChB;CACF;AAiBD;;;;;GAKG;AACH,qBAAa,UAAW,SAAQ,OAAO;IACrC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAK;IAE5B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiB;IACzC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAgB;IAC5C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAW;IAClC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAa;IACtC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAQ;IACnC,OAAO,CAAC,OAAO,CAAS;IACxB,0EAA0E;IAC1E,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,eAAe,CAA0B;IACjD,kFAAkF;IAClF,OAAO,CAAC,cAAc,CAAqB;IAC3C;;;;OAIG;IACH,OAAO,CAAC,UAAU,CAA8D;IAChF;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAsC;IACtE;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAyB;gBAEnD,GAAG,EAAE,OAAO,EAAE,MAAM,GAAE,SAAc;IAgChD;;;;OAIG;IACH,OAAO,CAAC,UAAU;IAYlB,uEAAuE;IACvE,SAAS,IAAI,OAAO;IAIpB,uCAAuC;IACjC,KAAK,IAAI,OAAO,CAAC,YAAY,CAAC;IAIpC,yDAAyD;IACzD,OAAO,IAAI,gBAAgB;IAI3B,2EAA2E;IACrE,IAAI,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC;IAItC,gEAAgE;IAChE,gBAAgB,IAAI,WAAW;IAI/B,wEAAwE;IAClE,WAAW,IAAI,OAAO,CAAC;QAAE,WAAW,EAAE,qBAAqB,EAAE,CAAA;KAAE,CAAC;IAItE;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAMxB,yCAAyC;IACzC,WAAW,IAAI,WAAW,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC;IAI1D,6CAA6C;IAC7C,aAAa,IAAI,MAAM;IAIvB,yEAAyE;IACzE,OAAO,CAAC,KAAK,GAAE,MAA6B,GAAG,MAAM;IAMrD,4EAA4E;IACtE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAUlG,uEAAuE;IACvE,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAMlC,OAAO,CAAC,YAAY;IA8DpB,uGAAuG;IACvG,OAAO,CAAC,aAAa;IAMrB,0EAA0E;IAC1E,OAAO,CAAC,UAAU;IAclB;;;;OAIG;IACH,OAAO,CAAC,aAAa;IAqBrB,gCAAgC;IAC1B,QAAQ,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAahE,2DAA2D;IAC3D,OAAO,CAAC,WAAW;IAInB,mEAAmE;IACnE,OAAO,CAAC,aAAa;IAWrB,+FAA+F;IAC/F,OAAO,CAAC,cAAc;IAMtB;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB;IAM3B,2DAA2D;IAC3D,OAAO,CAAC,cAAc;IAKtB;;;OAGG;IACG,aAAa,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC;IAuClE,qEAAqE;IAC/D,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,OAAO,CAAC,qBAAqB,CAAC;IAcpF,yEAAyE;IACnE,gBAAgB,IAAI,OAAO,CAAC,qBAAqB,CAAC;IAexD,0EAA0E;IACpE,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC;IAiCjE,iCAAiC;IAC3B,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,OAAO,EAAE,gBAAgB,CAAA;KAAE,CAAC;IAOpF,wFAAwF;IAClF,SAAS,CAAC,KAAK,EAAE,OAAO,CAAC,gBAAgB,CAAC,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,OAAO,EAAE,gBAAgB,CAAA;KAAE,CAAC;IAWnG,2EAA2E;IACrE,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAS/F;;;;;OAKG;IACH,oBAAoB,CAAC,OAAO,EAAE,kBAAkB,GAAG,IAAI;IAoBvD,oFAAoF;IACpF,OAAO,CAAC,mBAAmB;IAe3B,qFAAqF;IACrF,OAAO,CAAC,UAAU;IAIlB,8EAA8E;IAC9E,OAAO,CAAC,gBAAgB;IAIxB,OAAO,CAAC,IAAI;IAmEZ,OAAO,CAAC,KAAK;CAOd"}
@@ -19,6 +19,9 @@ import { DEFAULT_DECORATION_ID, decorationView, loadPetRegistry, petEntryView, p
19
19
  import { WHISPER_TTL_MS } from "./chatter.js";
20
20
  import { mergeVoicePacks } from "./voice-pack.js";
21
21
  import { defaultPetStateConfig, PetStateMachine, } from "./state.js";
22
+ import { applyGameplayEffects, drawLotteryTier, initialGameplayState, rollTouchBranch, rollWorkOutcome, settleGameplay, } from "./gameplay.js";
23
+ /** The unified gameplay currency: the shared treat (小鱼干) ledger. */
24
+ const GAMEPLAY_TREATS_CURRENCY = 'treats';
22
25
  /** Settings namespace of the pet capability. Spelled here rather than imported: the browser half spells the same value. */
23
26
  export const PET_SETTINGS_NAMESPACE = 'pet';
24
27
  /** Hard cap on simultaneously displayed session bubbles (most recent first). */
@@ -295,6 +298,174 @@ export class PetService extends Service {
295
298
  this.flush();
296
299
  return result;
297
300
  }
301
+ /* ---------------------------------------------------------------- *
302
+ * Gameplay verbs (miku-pet generalization). The manifest block lives
303
+ * on the registry entry; dynamic state persists per pet id. Verbs
304
+ * settle and persist; the state view projects without writing.
305
+ * ---------------------------------------------------------------- */
306
+ /** The active pet's gameplay block, if it declares one. */
307
+ gameplayDef() {
308
+ return this.activeEntry().gameplay;
309
+ }
310
+ /** The persisted (or fresh) gameplay state of the selected pet. */
311
+ gameplayState(def, now) {
312
+ const petId = this.selectedPetId();
313
+ const stored = this.ledger.snapshot.gameplay[petId];
314
+ return {
315
+ petId,
316
+ state: stored === undefined
317
+ ? initialGameplayState(def, now)
318
+ : { stats: { ...stored.stats }, currencies: { ...stored.currencies }, mode: stored.mode, settledAt: stored.settledAt },
319
+ };
320
+ }
321
+ /** Display view of one gameplay state (rounded stats; treats ride the shared treat ledger). */
322
+ gameplayViewOf(state) {
323
+ const stats = {};
324
+ for (const [name, value] of Object.entries(state.stats))
325
+ stats[name] = Math.round(value);
326
+ return { stats, mode: state.mode };
327
+ }
328
+ /**
329
+ * Move gameplay 'treats' currency (the unified post-wallet currency) from
330
+ * the engine's settle work area into the shared treat ledger, capped by
331
+ * the stock cap. The engine keeps its generic currency record for settle
332
+ * math; this drain is the only bridge to the wallet-free economy.
333
+ */
334
+ drainGameplayTreats(state) {
335
+ const pending = Math.floor(state.currencies[GAMEPLAY_TREATS_CURRENCY] ?? 0);
336
+ delete state.currencies[GAMEPLAY_TREATS_CURRENCY];
337
+ if (pending > 0)
338
+ this.ledger.grantTreats(pending);
339
+ }
340
+ /** Persist the mutated gameplay state of one verb call. */
341
+ commitGameplay(petId, state) {
342
+ this.ledger.setGameplay(petId, state);
343
+ if (this.ledger.takeDirty())
344
+ this.flush();
345
+ }
346
+ /**
347
+ * RPC: a touch on the pet. 'zone' names a touch zone (roll a branch);
348
+ * omitted means a plain click while a touch animation holds (clickBoost).
349
+ */
350
+ async gameplayTouch(zone) {
351
+ const def = this.gameplayDef();
352
+ if (def === undefined)
353
+ return { ok: false, error: 'no-gameplay' };
354
+ const now = Date.now();
355
+ const { petId, state } = this.gameplayState(def, now);
356
+ settleGameplay(state, def, now, { sessionActive: this.machine.render().sessionActive });
357
+ if (zone === undefined) {
358
+ const boost = def.touch?.clickBoost;
359
+ if (boost === undefined)
360
+ return { ok: false, error: 'no-touch' };
361
+ const amount = boost.min + Math.floor(Math.random() * (boost.max - boost.min + 1));
362
+ if (amount > 0)
363
+ applyGameplayEffects(state, def, [{ stat: boost.stat, amount }]);
364
+ this.drainGameplayTreats(state);
365
+ this.commitGameplay(petId, state);
366
+ return { ok: true, hit: false, view: this.gameplayViewOf(state) };
367
+ }
368
+ const target = def.touch?.zones.find(entry => entry.name === zone);
369
+ if (target === undefined)
370
+ return { ok: false, error: 'unknown-zone' };
371
+ const branch = rollTouchBranch(target, Math.random);
372
+ if (branch === undefined) {
373
+ this.drainGameplayTreats(state);
374
+ this.commitGameplay(petId, state);
375
+ return { ok: true, hit: false, view: this.gameplayViewOf(state) };
376
+ }
377
+ if (branch.effects !== undefined)
378
+ applyGameplayEffects(state, def, branch.effects);
379
+ const phrase = branch.phrases !== undefined && branch.phrases.length > 0
380
+ ? branch.phrases[Math.floor(Math.random() * branch.phrases.length)]
381
+ : undefined;
382
+ this.drainGameplayTreats(state);
383
+ this.commitGameplay(petId, state);
384
+ return {
385
+ ok: true,
386
+ hit: true,
387
+ ...(branch.state === undefined ? {} : { state: branch.state }),
388
+ ...(branch.stateMs === undefined ? {} : { stateMs: branch.stateMs }),
389
+ ...(phrase === undefined ? {} : { phrase }),
390
+ view: this.gameplayViewOf(state),
391
+ };
392
+ }
393
+ /** RPC: enter or leave a gameplay mode ('work' | 'sleep' | null). */
394
+ async gameplaySetMode(mode) {
395
+ const def = this.gameplayDef();
396
+ if (def === undefined)
397
+ return { ok: false, error: 'no-gameplay' };
398
+ if (mode === 'work' && def.work === undefined)
399
+ return { ok: false, error: 'no-work' };
400
+ if (mode === 'sleep' && def.sleep === undefined)
401
+ return { ok: false, error: 'no-sleep' };
402
+ const now = Date.now();
403
+ const { petId, state } = this.gameplayState(def, now);
404
+ settleGameplay(state, def, now, { sessionActive: this.machine.render().sessionActive });
405
+ state.mode = mode;
406
+ this.drainGameplayTreats(state);
407
+ this.commitGameplay(petId, state);
408
+ return { ok: true, view: this.gameplayViewOf(state) };
409
+ }
410
+ /** RPC: one work-round adjudication (only while the work mode holds). */
411
+ async gameplayWorkTick() {
412
+ const def = this.gameplayDef();
413
+ if (def?.work === undefined)
414
+ return { ok: false, error: 'no-work' };
415
+ const now = Date.now();
416
+ const { petId, state } = this.gameplayState(def, now);
417
+ if (state.mode !== 'work')
418
+ return { ok: false, error: 'not-working' };
419
+ settleGameplay(state, def, now, { sessionActive: this.machine.render().sessionActive });
420
+ const outcome = rollWorkOutcome(def.work, Math.random);
421
+ const effects = outcome === 'success' ? def.work.success?.effects : def.work.fail?.effects;
422
+ if (effects !== undefined)
423
+ applyGameplayEffects(state, def, effects);
424
+ this.drainGameplayTreats(state);
425
+ this.commitGameplay(petId, state);
426
+ return { ok: true, outcome, view: this.gameplayViewOf(state) };
427
+ }
428
+ /** RPC: buy one shop item (effects, currency swap, or a lottery draw). */
429
+ async gameplayBuy(itemId) {
430
+ const def = this.gameplayDef();
431
+ if (def === undefined)
432
+ return { ok: false, error: 'no-gameplay' };
433
+ const item = def.shop?.items.find(entry => entry.id === itemId);
434
+ if (item === undefined)
435
+ return { ok: false, error: 'unknown-item' };
436
+ const now = Date.now();
437
+ const { petId, state } = this.gameplayState(def, now);
438
+ settleGameplay(state, def, now, { sessionActive: this.machine.render().sessionActive });
439
+ // The unified currency is the shared treat ledger (wallet removed): shop
440
+ // prices and prizes ride the same balance the panel shows and feeding
441
+ // consumes, capped by the stock cap.
442
+ const treats = item.currency === GAMEPLAY_TREATS_CURRENCY;
443
+ const balance = treats ? this.ledger.snapshot.treats.treats : (state.currencies[item.currency] ?? 0);
444
+ if (balance < item.price) {
445
+ return { ok: false, error: 'insufficient-funds', view: this.gameplayViewOf(state) };
446
+ }
447
+ if (treats)
448
+ this.ledger.spendTreats(item.price);
449
+ else
450
+ state.currencies[item.currency] = balance - item.price;
451
+ if (item.effects !== undefined)
452
+ applyGameplayEffects(state, def, item.effects);
453
+ let prize;
454
+ if (item.lottery !== undefined) {
455
+ if (item.lottery.effects !== undefined)
456
+ applyGameplayEffects(state, def, item.lottery.effects);
457
+ const tier = drawLotteryTier(item.lottery, Math.random);
458
+ const prizeCurrency = tier.currency ?? item.lottery.currency ?? item.currency;
459
+ if (prizeCurrency === GAMEPLAY_TREATS_CURRENCY)
460
+ this.ledger.grantTreats(tier.prize);
461
+ else
462
+ state.currencies[prizeCurrency] = (state.currencies[prizeCurrency] ?? 0) + tier.prize;
463
+ prize = { amount: tier.prize, currency: prizeCurrency };
464
+ }
465
+ this.drainGameplayTreats(state);
466
+ this.commitGameplay(petId, state);
467
+ return { ok: true, ...(prize === undefined ? {} : { prize }), view: this.gameplayViewOf(state) };
468
+ }
298
469
  /** RPC: show or hide the pet. */
299
470
  async setVisible(visible) {
300
471
  this.ledger.setDisplay({ ...this.ledger.snapshot.display, visible });
@@ -411,6 +582,15 @@ export class PetService extends Service {
411
582
  ? whisper.text
412
583
  : undefined;
413
584
  const decoration = this.activeDecoration();
585
+ // Gameplay view: a read-only projection. The settle runs on a copy, so
586
+ // polling never writes pet.json (verbs settle and persist).
587
+ const gameplayDef = this.gameplayDef();
588
+ let gameplay;
589
+ if (gameplayDef !== undefined) {
590
+ const { state } = this.gameplayState(gameplayDef, Date.now());
591
+ settleGameplay(state, gameplayDef, Date.now(), { sessionActive: snapshot.sessionActive });
592
+ gameplay = this.gameplayViewOf(state);
593
+ }
414
594
  // Read-only: the ledger settles on economic events only, never on a read,
415
595
  // so polling the state cannot trigger pet.json writes.
416
596
  return {
@@ -433,6 +613,7 @@ export class PetService extends Service {
433
613
  stocked: this.ledger.snapshot.treats.treats,
434
614
  max: this.ledger.treatMax,
435
615
  },
616
+ ...(gameplay === undefined ? {} : { gameplay }),
436
617
  };
437
618
  }
438
619
  flush() {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@linxin666/dsh-pet",
3
3
  "description": "Multi-pet companion plugin for the dsh web GUI: a registry-driven floating pet that reacts to model activity, with per-pet naming, petting/feeding interactions and an affinity score",
4
- "version": "0.3.3",
4
+ "version": "0.3.4",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
@@ -31,7 +31,10 @@
31
31
  "lib/**/*.d.ts.map",
32
32
  "src",
33
33
  "cordis.patch.yml",
34
- "assets",
34
+ "assets/whale",
35
+ "assets/whale-refined",
36
+ "assets/ouo-neko",
37
+ "assets/decorations",
35
38
  "contracts"
36
39
  ],
37
40
  "license": "Apache-2.0",
@@ -79,6 +79,12 @@ function injected(): PetInjected {
79
79
  rename: vi.fn(),
80
80
  openSession: vi.fn(),
81
81
  feedbackDone: vi.fn(),
82
+ gameplay: {
83
+ touch: vi.fn(),
84
+ setMode: vi.fn(),
85
+ workTick: vi.fn(),
86
+ buy: vi.fn(),
87
+ },
82
88
  }
83
89
  }
84
90
 
@@ -11,12 +11,14 @@
11
11
  * @module @linxin666/dsh-pet/client/PetDockEntry
12
12
  */
13
13
 
14
- import { useEffect, useSyncExternalStore, type ReactElement } from 'react'
14
+ import { useEffect, useRef, useSyncExternalStore, type ReactElement } from 'react'
15
15
  import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'
16
16
  import type { PetDisplayConfig } from '../persist.ts'
17
17
  import type { PetStoreInstance } from './pet-store.ts'
18
18
  import { PetSprite } from './PetSprite.tsx'
19
19
  import { PetRendererSwitch } from './renderers/PetRendererSwitch.tsx'
20
+ import { createDragStream, type DragStream } from './drag-stream.ts'
21
+ import { GameplayHud, type GameplayApi, type GameplayBus } from './gameplay-hud.tsx'
20
22
  import { NS } from './locales.ts'
21
23
  import styles from './pet.module.css'
22
24
 
@@ -42,6 +44,8 @@ export interface PetInjected {
42
44
  openSession: (sessionId: string) => void
43
45
  /** Clear the reaction bubble. */
44
46
  feedbackDone: () => void
47
+ /** Gameplay verb API (miku-pet generalization); wired but unused for pets without a gameplay block. */
48
+ gameplay: GameplayApi
45
49
  }
46
50
 
47
51
  /** Composed props of the global pet entry (locale + injected; no slot runtime share). */
@@ -70,6 +74,15 @@ export function PetDockEntry(props: PetDockEntryProps): ReactElement {
70
74
  ensure()
71
75
  }, [ensure])
72
76
 
77
+ // Per-pet gameplay wiring: the coordination bus (HUD taps <-> chrome,
78
+ // HUD track overrides <-> frames2d mount) and the shared drag stream.
79
+ const auxRef = useRef<{ id: string; bus: GameplayBus; drag: DragStream } | null>(null)
80
+ if (definition !== null && (auxRef.current === null || auxRef.current.id !== definition.id)) {
81
+ auxRef.current = { id: definition.id, bus: {}, drag: createDragStream() }
82
+ }
83
+ const aux = auxRef.current
84
+ const gameplay = definition?.gameplay
85
+
73
86
  if (visible) {
74
87
  return (
75
88
  <span data-pet-dock data-testid="pet-dock">
@@ -80,6 +93,7 @@ export function PetDockEntry(props: PetDockEntryProps): ReactElement {
80
93
  definition={definition}
81
94
  phase={snapshot?.phase ?? 'idle'}
82
95
  onPet={props.pet}
96
+ {...(aux === null ? {} : { drag: aux.drag, bus: aux.bus })}
83
97
  t={props.t}
84
98
  >
85
99
  <PetSprite
@@ -94,6 +108,23 @@ export function PetDockEntry(props: PetDockEntryProps): ReactElement {
94
108
  onRename={props.rename}
95
109
  onOpenSession={props.openSession}
96
110
  onFeedbackDone={props.feedbackDone}
111
+ dragDisabled={snapshot.gameplay?.mode === 'work'}
112
+ {...(gameplay === undefined || aux === null
113
+ ? {}
114
+ : {
115
+ onGameplayTap: (fx: number, fy: number) => aux.bus.tap?.(fx, fy),
116
+ onGameplayMenu: () => aux.bus.openCard?.(),
117
+ hud: (
118
+ <GameplayHud
119
+ definition={definition}
120
+ store={store}
121
+ api={props.gameplay}
122
+ bus={aux.bus}
123
+ drag={aux.drag}
124
+ t={props.t}
125
+ />
126
+ ),
127
+ })}
97
128
  t={props.t}
98
129
  />
99
130
  </PetRendererSwitch>