@linxin666/dsh-pet 0.3.17 → 0.3.19

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 (55) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +16 -1
  3. package/README.zh.md +16 -1
  4. package/assets/blue-throated-bee-eater/pet.json +204 -0
  5. package/assets/blue-throated-bee-eater/previews/failed.gif +0 -0
  6. package/assets/blue-throated-bee-eater/previews/idle.gif +0 -0
  7. package/assets/blue-throated-bee-eater/previews/jumping.gif +0 -0
  8. package/assets/blue-throated-bee-eater/previews/review.gif +0 -0
  9. package/assets/blue-throated-bee-eater/previews/running-left.gif +0 -0
  10. package/assets/blue-throated-bee-eater/previews/running-right.gif +0 -0
  11. package/assets/blue-throated-bee-eater/previews/running.gif +0 -0
  12. package/assets/blue-throated-bee-eater/previews/waiting.gif +0 -0
  13. package/assets/blue-throated-bee-eater/previews/waving.gif +0 -0
  14. package/assets/blue-throated-bee-eater/spritesheet.webp +0 -0
  15. package/assets/blue-throated-bee-eater/voice.json +13 -0
  16. package/contracts/pet-manifest-v2.schema.json +84 -0
  17. package/lib/client.js +176 -27
  18. package/lib/client.js.map +1 -1
  19. package/lib/index.js +170 -6
  20. package/lib/types/client/gameplay-hud.d.ts +2 -0
  21. package/lib/types/client/gameplay-hud.d.ts.map +1 -1
  22. package/lib/types/client/gameplay-hud.js +65 -12
  23. package/lib/types/client/index.d.ts +0 -6
  24. package/lib/types/client/index.d.ts.map +1 -1
  25. package/lib/types/client/index.js +25 -2
  26. package/lib/types/client/locales.d.ts +7 -3
  27. package/lib/types/client/locales.d.ts.map +1 -1
  28. package/lib/types/client/locales.js +7 -3
  29. package/lib/types/client/renderers/Frames2dVisualMount.d.ts.map +1 -1
  30. package/lib/types/client/renderers/Frames2dVisualMount.js +5 -1
  31. package/lib/types/client/renderers/frames2d.d.ts +25 -0
  32. package/lib/types/client/renderers/frames2d.d.ts.map +1 -1
  33. package/lib/types/client/renderers/frames2d.js +84 -7
  34. package/lib/types/manifest-v2.d.ts +45 -0
  35. package/lib/types/manifest-v2.d.ts.map +1 -1
  36. package/lib/types/manifest-v2.js +139 -1
  37. package/lib/types/persist.d.ts +1 -1
  38. package/lib/types/persist.d.ts.map +1 -1
  39. package/lib/types/persist.js +1 -1
  40. package/lib/types/registry.d.ts +22 -0
  41. package/lib/types/registry.d.ts.map +1 -1
  42. package/lib/types/registry.js +45 -1
  43. package/package.json +2 -1
  44. package/src/client/gameplay-hud.test.tsx +64 -0
  45. package/src/client/gameplay-hud.tsx +109 -21
  46. package/src/client/index.ts +26 -2
  47. package/src/client/locales.ts +7 -3
  48. package/src/client/pet.module.css +31 -1
  49. package/src/client/renderers/Frames2dVisualMount.tsx +5 -1
  50. package/src/client/renderers/frames2d.test.ts +43 -0
  51. package/src/client/renderers/frames2d.ts +100 -7
  52. package/src/manifest-v2.ts +167 -1
  53. package/src/persist.ts +1 -1
  54. package/src/registry.test.ts +19 -5
  55. package/src/registry.ts +66 -1
package/lib/index.js CHANGED
@@ -1356,6 +1356,7 @@ const defaultDisplayConfig = {
1356
1356
  right: 24,
1357
1357
  bottom: 120
1358
1358
  };
1359
+ const DISPLAY_SIZE_MAX = 1024;
1359
1360
  const DISPLAY_INSET_MAX = 1e4;
1360
1361
  /** Name constraints. */
1361
1362
  const PET_NAME_MAX_LENGTH = 20;
@@ -1452,7 +1453,7 @@ function loadPetPersist(dir = petHomeDir()) {
1452
1453
  const rawDisplay = parsed.display ?? {};
1453
1454
  const display = {
1454
1455
  visible: typeof rawDisplay.visible === "boolean" ? rawDisplay.visible : base.display.visible,
1455
- size: Math.round(Math.min(512, Math.max(32, finiteNum(rawDisplay.size, base.display.size)))),
1456
+ size: Math.round(Math.min(DISPLAY_SIZE_MAX, Math.max(32, finiteNum(rawDisplay.size, base.display.size)))),
1456
1457
  right: Math.round(clamp(finiteNum(rawDisplay.right, base.display.right), DISPLAY_INSET_MAX)),
1457
1458
  bottom: Math.round(clamp(finiteNum(rawDisplay.bottom, base.display.bottom), DISPLAY_INSET_MAX))
1458
1459
  };
@@ -2442,7 +2443,8 @@ const KNOWN_FRAMES2D = /* @__PURE__ */ new Set([
2442
2443
  "dir",
2443
2444
  "defaultFrameMs",
2444
2445
  "tracks",
2445
- "phases"
2446
+ "phases",
2447
+ "skins"
2446
2448
  ]);
2447
2449
  /** frames2d track field allow-list (drift-locked to the schema file). */
2448
2450
  const KNOWN_FRAMES2D_TRACK = /* @__PURE__ */ new Set([
@@ -2451,6 +2453,33 @@ const KNOWN_FRAMES2D_TRACK = /* @__PURE__ */ new Set([
2451
2453
  "loop",
2452
2454
  "fallback"
2453
2455
  ]);
2456
+ /** frames2d skin entry field allow-list (drift-locked to the schema file). */
2457
+ const KNOWN_SKIN = /* @__PURE__ */ new Set([
2458
+ "id",
2459
+ "label",
2460
+ "idleTrack",
2461
+ "clickActions",
2462
+ "gameplayTracks"
2463
+ ]);
2464
+ /** frames2d skin click-action field allow-list (drift-locked to the schema file). */
2465
+ const KNOWN_SKIN_CLICK = /* @__PURE__ */ new Set([
2466
+ "track",
2467
+ "probability",
2468
+ "phrases"
2469
+ ]);
2470
+ /** Max spoken lines one skin click action may declare. */
2471
+ const SKIN_CLICK_MAX_PHRASES = 5;
2472
+ /** Max length of one spoken line. */
2473
+ const SKIN_CLICK_PHRASE_MAX_LENGTH = 120;
2474
+ /** Validate a skin click action's optional phrase pool (same spirit as gameplay phrases). */
2475
+ function parseSkinPhrases(raw, field, diag) {
2476
+ if (raw === void 0) return void 0;
2477
+ if (!Array.isArray(raw) || raw.length === 0 || raw.length > SKIN_CLICK_MAX_PHRASES || raw.some((line) => typeof line !== "string" || line.trim() === "" || line.length > SKIN_CLICK_PHRASE_MAX_LENGTH)) {
2478
+ diag.error(field + " must be 1..5 non-empty lines of at most 120 chars");
2479
+ return;
2480
+ }
2481
+ return raw;
2482
+ }
2454
2483
  var Diagnostics$1 = class {
2455
2484
  list = [];
2456
2485
  source;
@@ -2695,6 +2724,100 @@ function parseFrames2dBlock(raw, diag) {
2695
2724
  for (const [phase, target] of Object.entries(block.phases)) if (target !== void 0 && tracks[target] === void 0) diag.error("frames2d.phases." + phase + " references unknown track " + JSON.stringify(target));
2696
2725
  }
2697
2726
  for (const [name, track] of Object.entries(tracks)) if (track.fallback !== void 0 && tracks[track.fallback] === void 0) diag.error("frames2d.tracks." + name + ".fallback references unknown track " + JSON.stringify(track.fallback));
2727
+ if (raw.skins !== void 0) if (!Array.isArray(raw.skins) || raw.skins.length === 0 || raw.skins.length > 16) diag.error("frames2d.skins must be an array of 1..16 skins");
2728
+ else {
2729
+ const skins = [];
2730
+ const seen = /* @__PURE__ */ new Set();
2731
+ for (const [index, entry] of raw.skins.entries()) {
2732
+ const field = "frames2d.skins[" + index + "]";
2733
+ if (!isRecord$1(entry)) {
2734
+ diag.error(field + " must be an object");
2735
+ continue;
2736
+ }
2737
+ const extra = unknownKeys$1(entry, KNOWN_SKIN);
2738
+ if (extra.length > 0) diag.error(field + ": unknown field(s) " + extra.map((k) => JSON.stringify(k)).join(", "));
2739
+ const id = typeof entry.id === "string" ? entry.id.trim() : "";
2740
+ if (id === "" || id.length > 24 || !FRAMES2D_TRACK_NAME.test(id)) {
2741
+ diag.error(field + ".id must be a lowercase kebab id of at most 24 chars");
2742
+ continue;
2743
+ }
2744
+ if (seen.has(id)) {
2745
+ diag.error(field + ": duplicate skin id " + JSON.stringify(id));
2746
+ continue;
2747
+ }
2748
+ seen.add(id);
2749
+ const label = typeof entry.label === "string" ? entry.label.trim() : "";
2750
+ if (label === "" || label.length > 40) {
2751
+ diag.error(field + ".label must be a non-empty string of at most 40 chars");
2752
+ continue;
2753
+ }
2754
+ const idleTrack = typeof entry.idleTrack === "string" ? entry.idleTrack : "";
2755
+ if (!validTrackName(idleTrack) || tracks[idleTrack] === void 0) {
2756
+ diag.error(field + ".idleTrack must name a declared frames2d track");
2757
+ continue;
2758
+ }
2759
+ let clickActions;
2760
+ if (entry.clickActions !== void 0) if (!Array.isArray(entry.clickActions) || entry.clickActions.length === 0 || entry.clickActions.length > 8) diag.error(field + ".clickActions must be an array of 1..8 actions");
2761
+ else {
2762
+ const resolved = [];
2763
+ for (const [cIndex, action] of entry.clickActions.entries()) {
2764
+ const cField = field + ".clickActions[" + cIndex + "]";
2765
+ if (!isRecord$1(action)) {
2766
+ diag.error(cField + " must be an object");
2767
+ continue;
2768
+ }
2769
+ const cExtra = unknownKeys$1(action, KNOWN_SKIN_CLICK);
2770
+ if (cExtra.length > 0) diag.error(cField + ": unknown field(s) " + cExtra.map((k) => JSON.stringify(k)).join(", "));
2771
+ const trackName = typeof action.track === "string" ? action.track : "";
2772
+ if (!validTrackName(trackName) || tracks[trackName] === void 0) {
2773
+ diag.error(cField + ".track must name a declared frames2d track");
2774
+ continue;
2775
+ }
2776
+ if (typeof action.probability !== "number" || !Number.isFinite(action.probability) || action.probability <= 0 || action.probability > 1) {
2777
+ diag.error(cField + ".probability must be a number in (0, 1]");
2778
+ continue;
2779
+ }
2780
+ const phrases = parseSkinPhrases(action.phrases, cField + ".phrases", diag);
2781
+ resolved.push({
2782
+ track: trackName,
2783
+ probability: action.probability,
2784
+ ...phrases === void 0 ? {} : { phrases }
2785
+ });
2786
+ }
2787
+ if (resolved.length > 0) clickActions = resolved;
2788
+ }
2789
+ let gameplayTracks;
2790
+ if (entry.gameplayTracks !== void 0) if (!isRecord$1(entry.gameplayTracks)) diag.error(field + ".gameplayTracks must be an object mapping gameplay state names to tracks");
2791
+ else {
2792
+ const states = Object.keys(entry.gameplayTracks);
2793
+ if (states.length === 0 || states.length > 8) diag.error(field + ".gameplayTracks must map 1..8 gameplay states");
2794
+ else {
2795
+ const resolvedTracks = {};
2796
+ for (const state of states) {
2797
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(state)) {
2798
+ diag.error(field + ".gameplayTracks key " + JSON.stringify(state) + " must be a lowercase kebab state name");
2799
+ continue;
2800
+ }
2801
+ const gTrack = entry.gameplayTracks[state];
2802
+ if (typeof gTrack !== "string" || !validTrackName(gTrack) || tracks[gTrack] === void 0) {
2803
+ diag.error(field + ".gameplayTracks[" + JSON.stringify(state) + "] must name a declared frames2d track");
2804
+ continue;
2805
+ }
2806
+ resolvedTracks[state] = gTrack;
2807
+ }
2808
+ if (Object.keys(resolvedTracks).length > 0) gameplayTracks = resolvedTracks;
2809
+ }
2810
+ }
2811
+ skins.push({
2812
+ id,
2813
+ label,
2814
+ idleTrack,
2815
+ ...clickActions === void 0 ? {} : { clickActions },
2816
+ ...gameplayTracks === void 0 ? {} : { gameplayTracks }
2817
+ });
2818
+ }
2819
+ if (skins.length > 0) block.skins = skins;
2820
+ }
2698
2821
  return diag.hasErrors ? void 0 : block;
2699
2822
  }
2700
2823
  /** v1 compat read: map the legacy flat manifest onto the v2 sprite2d shape. */
@@ -3685,6 +3808,46 @@ function resolveFrames2dEntry(manifest, dir, options) {
3685
3808
  record("warning", "pet " + manifest.id + ": frames2d phase " + phase + " maps to dropped track " + JSON.stringify(target) + "; using idle");
3686
3809
  phases[phase] = idleTrack;
3687
3810
  }
3811
+ let skins;
3812
+ if (block.skins !== void 0) {
3813
+ const resolved = [];
3814
+ for (const skin of block.skins) {
3815
+ if (tracks[skin.idleTrack] === void 0) {
3816
+ record("warning", "pet " + manifest.id + ": frames2d skin " + JSON.stringify(skin.id) + " idleTrack dropped; skipping");
3817
+ continue;
3818
+ }
3819
+ let clickActions;
3820
+ if (skin.clickActions !== void 0) {
3821
+ const kept = [];
3822
+ for (const action of skin.clickActions) {
3823
+ if (tracks[action.track] === void 0) {
3824
+ record("warning", "pet " + manifest.id + ": frames2d skin " + JSON.stringify(skin.id) + " click action track " + JSON.stringify(action.track) + " dropped");
3825
+ continue;
3826
+ }
3827
+ kept.push(action);
3828
+ }
3829
+ if (kept.length > 0) clickActions = kept;
3830
+ }
3831
+ let gameplayTracks;
3832
+ if (skin.gameplayTracks !== void 0) {
3833
+ const kept = {};
3834
+ for (const [state, gTrack] of Object.entries(skin.gameplayTracks)) {
3835
+ if (tracks[gTrack] === void 0) {
3836
+ record("warning", "pet " + manifest.id + ": frames2d skin " + JSON.stringify(skin.id) + " gameplay track override " + JSON.stringify(state) + " -> " + JSON.stringify(gTrack) + " dropped");
3837
+ continue;
3838
+ }
3839
+ kept[state] = gTrack;
3840
+ }
3841
+ if (Object.keys(kept).length > 0) gameplayTracks = kept;
3842
+ }
3843
+ resolved.push({
3844
+ ...skin,
3845
+ ...clickActions === void 0 ? {} : { clickActions },
3846
+ ...gameplayTracks === void 0 ? {} : { gameplayTracks }
3847
+ });
3848
+ }
3849
+ if (resolved.length > 0) skins = resolved;
3850
+ }
3688
3851
  const remarks = normalizePetRemarks(manifest.remarks, (message) => record("warning", "pet " + manifest.id + ": " + message));
3689
3852
  let gameplay;
3690
3853
  if (manifest.gameplay !== void 0) {
@@ -3729,7 +3892,8 @@ function resolveFrames2dEntry(manifest, dir, options) {
3729
3892
  renderer: "frames2d",
3730
3893
  frames2d: {
3731
3894
  tracks,
3732
- phases
3895
+ phases,
3896
+ ...skins === void 0 ? {} : { skins }
3733
3897
  },
3734
3898
  ...gameplay === void 0 ? {} : { gameplay },
3735
3899
  cell,
@@ -4618,7 +4782,7 @@ var PetService = class extends Service {
4618
4782
  ...this.ledger.snapshot.display,
4619
4783
  ...patch
4620
4784
  };
4621
- next.size = Math.round(Math.min(512, Math.max(32, next.size)));
4785
+ next.size = Math.round(Math.min(DISPLAY_SIZE_MAX, Math.max(32, next.size)));
4622
4786
  next.right = Math.round(Math.min(DISPLAY_INSET_MAX, Math.max(0, next.right)));
4623
4787
  next.bottom = Math.round(Math.min(DISPLAY_INSET_MAX, Math.max(0, next.bottom)));
4624
4788
  this.ledger.setDisplay(next);
@@ -4662,7 +4826,7 @@ var PetService = class extends Service {
4662
4826
  } else if (section.petId !== void 0) this.syncSettingsFromPet();
4663
4827
  const next = { ...this.ledger.snapshot.display };
4664
4828
  next.visible = section.visible && (section.enabled ?? true);
4665
- next.size = Math.round(Math.min(512, Math.max(32, section.size)));
4829
+ next.size = Math.round(Math.min(DISPLAY_SIZE_MAX, Math.max(32, section.size)));
4666
4830
  next.right = Math.round(Math.min(DISPLAY_INSET_MAX, Math.max(0, section.right)));
4667
4831
  next.bottom = Math.round(Math.min(DISPLAY_INSET_MAX, Math.max(0, section.bottom)));
4668
4832
  this.ledger.setDisplay(next);
@@ -5528,7 +5692,7 @@ const inject = ["webServer"];
5528
5692
  function makePetSettingsSchema(fallbackPetId) {
5529
5693
  return z.object({
5530
5694
  visible: z.boolean().default(true),
5531
- size: z.number().step(1).min(32).max(512).default(160),
5695
+ size: z.number().step(1).min(32).max(DISPLAY_SIZE_MAX).default(160),
5532
5696
  right: z.number().step(1).min(0).max(DISPLAY_INSET_MAX).default(24),
5533
5697
  bottom: z.number().step(1).min(0).max(DISPLAY_INSET_MAX).default(20),
5534
5698
  petId: z.string().default(fallbackPetId),
@@ -30,6 +30,8 @@ export interface GameplayApi {
30
30
  */
31
31
  export interface GameplayBus {
32
32
  setTrack?: (track?: string) => void;
33
+ /** Swap the pet's base idle track (skin switch); undefined restores default. */
34
+ setIdleTrack?: (track?: string) => void;
33
35
  tap?: (fx: number, fy: number) => void;
34
36
  /**
35
37
  * Card open/close request from the chrome (the hover panel's 玩法 action):
@@ -1 +1 @@
1
- {"version":3,"file":"gameplay-hud.d.ts","sourceRoot":"","sources":["../../../src/client/gameplay-hud.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAsE,KAAK,YAAY,EAAE,MAAM,OAAO,CAAA;AAC7G,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AACnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AACnD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAE1D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AACtD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAClD,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,cAAc,CAAA;AAGtC,gFAAgF;AAChF,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAA;IACxD,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAA;IAC1E,QAAQ,EAAE,MAAM,OAAO,CAAC,qBAAqB,CAAC,CAAA;IAC9C,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAA;CACtD;AAED;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;IACnC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;IACtC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;CACpC;AAYD,0EAA0E;AAC1E,wBAAgB,WAAW,CAAC,KAAK,EAAE;IACjC,UAAU,EAAE,aAAa,CAAA;IACzB,KAAK,EAAE,gBAAgB,CAAA;IACvB,GAAG,EAAE,WAAW,CAAA;IAChB,GAAG,EAAE,WAAW,CAAA;IAChB,IAAI,EAAE,UAAU,CAAA;IAChB,CAAC,EAAE,WAAW,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,CAAA;CAC/B,GAAG,YAAY,GAAG,IAAI,CAkVtB"}
1
+ {"version":3,"file":"gameplay-hud.d.ts","sourceRoot":"","sources":["../../../src/client/gameplay-hud.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAsE,KAAK,YAAY,EAAE,MAAM,OAAO,CAAA;AAC7G,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AACnE,OAAO,KAAK,EAAE,aAAa,EAAqB,MAAM,gBAAgB,CAAA;AACtE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAE1D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AACtD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAClD,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,cAAc,CAAA;AAGtC,gFAAgF;AAChF,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAA;IACxD,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAA;IAC1E,QAAQ,EAAE,MAAM,OAAO,CAAC,qBAAqB,CAAC,CAAA;IAC9C,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAA;CACtD;AAED;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;IACnC,gFAAgF;IAChF,YAAY,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;IACvC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;IACtC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;CACpC;AAYD,0EAA0E;AAC1E,wBAAgB,WAAW,CAAC,KAAK,EAAE;IACjC,UAAU,EAAE,aAAa,CAAA;IACzB,KAAK,EAAE,gBAAgB,CAAA;IACvB,GAAG,EAAE,WAAW,CAAA;IAChB,GAAG,EAAE,WAAW,CAAA;IAChB,IAAI,EAAE,UAAU,CAAA;IAChB,CAAC,EAAE,WAAW,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,CAAA;CAC/B,GAAG,YAAY,GAAG,IAAI,CAwatB"}
@@ -22,6 +22,10 @@ export function GameplayHud(props) {
22
22
  const phase = ui.snapshot?.phase ?? 'idle';
23
23
  const [open, setOpen] = useState(false);
24
24
  const [page, setPage] = useState('root');
25
+ // Currently selected skin id (base idle swap); undefined = default look.
26
+ const [skinId, setSkinId] = useState(undefined);
27
+ const skinIdRef = useRef(undefined);
28
+ skinIdRef.current = skinId;
25
29
  const hudRef = useRef(null);
26
30
  const cardRef = useRef(null);
27
31
  const [floats, setFloats] = useState([]);
@@ -52,9 +56,33 @@ export function GameplayHud(props) {
52
56
  // Tap handling (registered on the bus; PetSprite reports sprite-box
53
57
  // fractions). Sleep wakes on tap; work blocks taps; a held touch
54
58
  // animation turns taps into the plain-click boost.
59
+ // Skin contract: while a skin is selected, taps only ever play that
60
+ // skin's own click actions — a miss resolves to the plain click boost,
61
+ // never the default touch zones (so a skinned pet cannot trigger the
62
+ // default pet's shy/work reactions).
55
63
  useEffect(() => {
56
64
  if (def === undefined)
57
65
  return undefined;
66
+ // Play a one-shot override track and lock tap input for its duration so
67
+ // consecutive taps cannot retrigger mid-play (skin click actions and
68
+ // default touch-zone reactions share this path).
69
+ const holdTrack = (track, holdMs) => {
70
+ bus.setTrack?.(track);
71
+ touchLockUntilRef.current = Date.now() + holdMs;
72
+ window.setTimeout(() => {
73
+ if (Date.now() >= touchLockUntilRef.current)
74
+ bus.setTrack?.(undefined);
75
+ }, holdMs);
76
+ };
77
+ const speak = (phrases) => {
78
+ if (phrases !== undefined && phrases.length > 0) {
79
+ const phrase = phrases[Math.floor(Math.random() * phrases.length)];
80
+ store.actions.setFeedback({ text: phrase, kind: 'none', at: Date.now() });
81
+ }
82
+ };
83
+ // Total play time of a track in ms (its fallback lands back in the skin
84
+ // base idle, so holding the lock for the full loop is unnecessary).
85
+ const trackDuration = (track) => definition.frames2d?.tracks[track]?.durations.reduce((sum, ms) => sum + ms, 0) ?? 0;
58
86
  bus.tap = (fx, fy) => {
59
87
  if (modeRef.current === 'sleep') {
60
88
  void api.setMode(null).then(applyResult, () => undefined);
@@ -71,6 +99,29 @@ export function GameplayHud(props) {
71
99
  void api.touch().then(applyResult, () => undefined);
72
100
  return;
73
101
  }
102
+ const activeSkin = definition.frames2d?.skins?.find(skin => skin.id === skinIdRef.current);
103
+ if (activeSkin !== undefined) {
104
+ // Skin click actions roll first (declared order, cumulative
105
+ // probabilities); on a hit play the action's track once. A miss
106
+ // stays on the plain click boost — never the default touch zones.
107
+ const actions = activeSkin.clickActions ?? [];
108
+ if (actions.length > 0) {
109
+ let roll = Math.random();
110
+ const fired = actions.find(action => {
111
+ if (roll < action.probability)
112
+ return true;
113
+ roll -= action.probability;
114
+ return false;
115
+ });
116
+ if (fired !== undefined) {
117
+ holdTrack(fired.track, trackDuration(fired.track) || 3000);
118
+ speak(fired.phrases);
119
+ return;
120
+ }
121
+ }
122
+ void api.touch().then(applyResult, () => undefined);
123
+ return;
124
+ }
74
125
  const zone = def.touch === undefined ? undefined : touchZoneAt(def.touch, hy);
75
126
  if (zone === undefined)
76
127
  return;
@@ -78,15 +129,8 @@ export function GameplayHud(props) {
78
129
  applyResult(result);
79
130
  if (result.hit !== true)
80
131
  return;
81
- if (result.state !== undefined) {
82
- bus.setTrack?.(result.state);
83
- const holdMs = result.stateMs ?? 3000;
84
- touchLockUntilRef.current = Date.now() + holdMs;
85
- window.setTimeout(() => {
86
- if (Date.now() >= touchLockUntilRef.current)
87
- bus.setTrack?.(undefined);
88
- }, holdMs);
89
- }
132
+ if (result.state !== undefined)
133
+ holdTrack(result.state, result.stateMs ?? 3000);
90
134
  if (result.phrase !== undefined) {
91
135
  store.actions.setFeedback({ text: result.phrase, kind: 'none', at: Date.now() });
92
136
  }
@@ -236,14 +280,18 @@ export function GameplayHud(props) {
236
280
  // eslint-disable-next-line react-hooks/exhaustive-deps -- the loop keys on the mode value
237
281
  }, [definition.id, def, view?.mode]);
238
282
  // Sleep loop: hold the sleep track; restore is host-side (lazy settle).
283
+ // While a skin with a gameplayTracks.sleep override is selected, the skin's
284
+ // own track replaces the default sleep track (e.g. a skin-specific doze).
239
285
  useEffect(() => {
240
286
  const sleep = def?.sleep;
241
287
  if (def === undefined || sleep === undefined || view?.mode !== 'sleep')
242
288
  return undefined;
243
- bus.setTrack?.(sleep.state);
289
+ const skinGameplay = definition.frames2d?.skins?.find(skin => skin.id === skinIdRef.current)?.gameplayTracks;
290
+ const hold = skinGameplay?.['sleep'] ?? sleep.state;
291
+ bus.setTrack?.(hold);
244
292
  return () => bus.setTrack?.(undefined);
245
293
  // eslint-disable-next-line react-hooks/exhaustive-deps -- the loop keys on the mode value
246
- }, [definition.id, def, view?.mode]);
294
+ }, [definition.id, def, view?.mode, skinId]);
247
295
  if (def === undefined || view === undefined)
248
296
  return null;
249
297
  const mode = view.mode;
@@ -267,8 +315,13 @@ export function GameplayHud(props) {
267
315
  const setMode = (next) => {
268
316
  void api.setMode(next).then(applyResult, () => undefined);
269
317
  };
318
+ const skins = definition.frames2d?.skins;
319
+ const selectSkin = (skin) => {
320
+ setSkinId(skin?.id);
321
+ bus.setIdleTrack?.(skin?.idleTrack);
322
+ };
270
323
  return (_jsxs("div", { ref: hudRef, className: styles.gameplayHud, "data-dsh-pet-gameplay": definition.id, children: [floats.map(entry => (_jsx("div", { className: styles.gameplayFloat, children: entry.text }, entry.id))), mode !== null && (_jsx("div", { className: styles.gameplayModeChip, children: tr(mode === 'work' ? 'pet.gameplay.working' : 'pet.gameplay.sleeping') })), open && (_jsxs("div", { ref: cardRef, className: styles.gameplayCard, "data-page": page, children: [page === 'root' && (_jsxs(_Fragment, { children: [_jsx("div", { className: styles.gameplayBars, children: Object.entries(stats).map(([name, stat]) => {
271
324
  const value = view.stats[name] ?? 0;
272
325
  return (_jsxs("div", { className: styles.gameplayBarRow, title: statLabel(name) + ' ' + String(value) + '/' + String(stat.max), children: [_jsx("span", { className: styles.gameplayBarLabel, children: statLabel(name) }), _jsx("span", { className: styles.gameplayBarTrack, children: _jsx("span", { className: styles.gameplayBarFill, style: { width: Math.round((value / stat.max) * 100) + '%' } }) })] }, name));
273
- }) }), _jsxs("div", { className: styles.gameplayActions, children: [def.work !== undefined && (_jsx("button", { type: "button", className: styles.action, onClick: () => setMode(mode === 'work' ? null : 'work'), children: tr(mode === 'work' ? 'pet.gameplay.stopWork' : 'pet.gameplay.work') })), def.sleep !== undefined && (_jsx("button", { type: "button", className: styles.action, onClick: () => setMode(mode === 'sleep' ? null : 'sleep'), children: tr(mode === 'sleep' ? 'pet.gameplay.wake' : 'pet.gameplay.sleep') })), shop !== undefined && (_jsx("button", { type: "button", className: styles.action, onClick: () => setPage('shop'), children: tr('pet.gameplay.shop') }))] })] })), page === 'shop' && shop !== undefined && (_jsxs(_Fragment, { children: [_jsx("div", { className: styles.gameplayShopItems, children: shop.items.map(item => (_jsxs("button", { type: "button", className: styles.gameplayShopItem, onClick: () => buy(item.id), title: item.label + ' — ' + String(item.price) + ' ' + currencyLabel(item.currency), children: [item.image !== undefined && (_jsx("img", { className: styles.gameplayShopItemImage, src: item.image, alt: "", draggable: false })), _jsx("span", { className: styles.gameplayShopItemLabel, children: item.label }), _jsxs("span", { className: styles.gameplayShopItemPrice, children: [item.price, " ", currencyLabel(item.currency)] })] }, item.id))) }), _jsx("div", { className: styles.gameplayActions, children: _jsx("button", { type: "button", className: styles.action, onClick: () => setPage('root'), children: tr('pet.gameplay.back') }) })] })), _jsx("button", { type: "button", className: styles.gameplayClose, "aria-label": tr('pet.gameplay.back'), onClick: () => setOpen(false), children: "\u00D7" })] }))] }));
326
+ }) }), _jsxs("div", { className: styles.gameplayActions, children: [def.sleep !== undefined && (_jsx("button", { type: "button", className: styles.action, onClick: () => setMode(mode === 'sleep' ? null : 'sleep'), children: tr(mode === 'sleep' ? 'pet.gameplay.wake' : 'pet.gameplay.sleep') })), shop !== undefined && (_jsx("button", { type: "button", className: styles.action, onClick: () => setPage('shop'), children: tr('pet.gameplay.shop') })), skins !== undefined && skins.length > 0 && (_jsx("button", { type: "button", className: styles.action, onClick: () => setPage('skins'), children: tr('pet.gameplay.skin') })), def.work !== undefined && (_jsx("button", { type: "button", className: styles.action, onClick: () => setMode(mode === 'work' ? null : 'work'), children: tr(mode === 'work' ? 'pet.gameplay.stopWork' : 'pet.gameplay.work') }))] })] })), page === 'shop' && shop !== undefined && (_jsxs(_Fragment, { children: [_jsx("div", { className: styles.gameplayShopItems, children: shop.items.map(item => (_jsxs("button", { type: "button", className: styles.gameplayShopItem, onClick: () => buy(item.id), title: item.label + ' — ' + String(item.price) + ' ' + currencyLabel(item.currency), children: [item.image !== undefined && (_jsx("img", { className: styles.gameplayShopItemImage, src: item.image, alt: "", draggable: false })), _jsx("span", { className: styles.gameplayShopItemLabel, children: item.label }), _jsxs("span", { className: styles.gameplayShopItemPrice, children: [item.price, " ", currencyLabel(item.currency)] })] }, item.id))) }), _jsx("div", { className: styles.gameplayActions, children: _jsx("button", { type: "button", className: styles.action, onClick: () => setPage('root'), children: tr('pet.gameplay.back') }) })] })), page === 'skins' && skins !== undefined && (_jsxs(_Fragment, { children: [_jsxs("div", { className: styles.gameplaySkinItems, children: [_jsx("button", { type: "button", className: skinId === undefined ? styles.gameplaySkinItem + ' ' + styles.gameplaySkinItemActive : styles.gameplaySkinItem, onClick: () => selectSkin(undefined), children: tr('pet.gameplay.skinDefault') }), skins.map(skin => (_jsx("button", { type: "button", className: skinId === skin.id ? styles.gameplaySkinItem + ' ' + styles.gameplaySkinItemActive : styles.gameplaySkinItem, onClick: () => selectSkin(skin), children: skin.label }, skin.id)))] }), _jsx("div", { className: styles.gameplayActions, children: _jsx("button", { type: "button", className: styles.action, onClick: () => setPage('root'), children: tr('pet.gameplay.back') }) })] })), _jsx("button", { type: "button", className: styles.gameplayClose, "aria-label": tr('pet.gameplay.back'), onClick: () => setOpen(false), children: "\u00D7" })] }))] }));
274
327
  }
@@ -35,11 +35,5 @@ declare module '@deepseek-ai/cordis' {
35
35
  };
36
36
  }
37
37
  }
38
- /**
39
- * Client plugin body: register dictionaries, mount the global pet entry and
40
- * poll loop while the plugin is enabled, and seat the settings card as a
41
- * first-level settings section.
42
- * @param ctx - client root context.
43
- */
44
38
  export declare function apply(ctx: ClientContext): void;
45
39
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAGnE,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,4CAA4C,CAAA;AA8ElG,wEAAwE;AACxE,eAAO,MAAM,MAAM,UAA2E,CAAA;AAE9F,qEAAqE;AACrE,YAAY,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AACxE,YAAY,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AACrD,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC7D,YAAY,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AACtF,YAAY,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAA;AACpE,YAAY,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf;;;;WAIG;QACH,aAAa,CAAC,EAAE;YAAE,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAAA;SAAE,CAAA;KAC1E;CACF;AAED;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAqT9C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAGnE,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,4CAA4C,CAAA;AA8ElG,wEAAwE;AACxE,eAAO,MAAM,MAAM,UAA2E,CAAA;AAE9F,qEAAqE;AACrE,YAAY,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AACxE,YAAY,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AACrD,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC7D,YAAY,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AACtF,YAAY,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAA;AACpE,YAAY,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf;;;;WAIG;QACH,aAAa,CAAC,EAAE;YAAE,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAAA;SAAE,CAAA;KAC1E;CACF;AAoBD,wBAAgB,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAiU9C"}
@@ -64,6 +64,16 @@ export const inject = ['slots', 'locale', 'connection', 'settingsScope', 'remote
64
64
  * first-level settings section.
65
65
  * @param ctx - client root context.
66
66
  */
67
+ /**
68
+ * Module-wide work-tick throttle (ms). Hot reloads can leave several
69
+ * GameplayHud instances alive, each running its own 10s work interval;
70
+ * without a shared gate every interval would call workTick and each stale
71
+ * call re-rolls, re-grants treats and re-plays the success/fail track, so
72
+ * the outcome appears to play several times per window. This shared marker
73
+ * accepts the first adjudication of a window and silently suppresses the
74
+ * duplicates that follow. Reset when (re-)entering work mode.
75
+ */
76
+ let lastWorkTickAt = 0;
67
77
  export function apply(ctx) {
68
78
  // Anonymous install heartbeat (docs/telemetry.md): one beat per browser per
69
79
  // UTC day, package name only, silent failure.
@@ -299,8 +309,21 @@ export function apply(ctx) {
299
309
  },
300
310
  gameplay: {
301
311
  touch: (zone) => petApi.gameplayTouch(zone),
302
- setMode: (mode) => petApi.gameplaySetMode(mode),
303
- workTick: () => petApi.gameplayWorkTick(),
312
+ setMode: async (mode) => {
313
+ if (mode === 'work')
314
+ lastWorkTickAt = 0;
315
+ return petApi.gameplaySetMode(mode);
316
+ },
317
+ workTick: async () => {
318
+ // One adjudication per tick window, page-wide: suppress stale
319
+ // duplicate intervals (HMR) re-playing the result track.
320
+ const now = Date.now();
321
+ if (now - lastWorkTickAt < 8500) {
322
+ return { ok: true };
323
+ }
324
+ lastWorkTickAt = now;
325
+ return petApi.gameplayWorkTick();
326
+ },
304
327
  buy: (item) => petApi.gameplayBuy(item),
305
328
  },
306
329
  });
@@ -34,9 +34,11 @@ export declare const zh: {
34
34
  readonly 'pet.gameplay.menu': "玩法";
35
35
  readonly 'pet.gameplay.work': "打工";
36
36
  readonly 'pet.gameplay.stopWork': "收工";
37
- readonly 'pet.gameplay.sleep': "睡觉";
37
+ readonly 'pet.gameplay.sleep': "休息";
38
38
  readonly 'pet.gameplay.wake': "起床";
39
39
  readonly 'pet.gameplay.shop': "商店";
40
+ readonly 'pet.gameplay.skin': "皮肤";
41
+ readonly 'pet.gameplay.skinDefault': "默认";
40
42
  readonly 'pet.gameplay.back': "返回";
41
43
  readonly 'pet.gameplay.buy': "购买";
42
44
  readonly 'pet.gameplay.insufficient': "{currency}不足";
@@ -62,7 +64,7 @@ export declare const zh: {
62
64
  readonly 'settings.visible': "显示宠物";
63
65
  readonly 'settings.visibleHint': "关闭后宠物隐藏,可从聊天输入区重新召唤。";
64
66
  readonly 'settings.size': "大小(px)";
65
- readonly 'settings.sizeHint': "精灵单元高度,范围 32–512。";
67
+ readonly 'settings.sizeHint': "精灵单元高度,范围 32–1024。";
66
68
  readonly 'settings.right': "距右侧(px)";
67
69
  readonly 'settings.rightHint': "距视口右边缘的水平内缩距离。";
68
70
  readonly 'settings.bottom': "距底部(px)";
@@ -116,6 +118,8 @@ export declare const en: {
116
118
  readonly 'pet.gameplay.sleep': "Sleep";
117
119
  readonly 'pet.gameplay.wake': "Wake up";
118
120
  readonly 'pet.gameplay.shop': "Shop";
121
+ readonly 'pet.gameplay.skin': "Skin";
122
+ readonly 'pet.gameplay.skinDefault': "Default";
119
123
  readonly 'pet.gameplay.back': "Back";
120
124
  readonly 'pet.gameplay.buy': "Buy";
121
125
  readonly 'pet.gameplay.insufficient': "Not enough {currency}";
@@ -141,7 +145,7 @@ export declare const en: {
141
145
  readonly 'settings.visible': "Show the pet";
142
146
  readonly 'settings.visibleHint': "When off, the pet hides; summon it again from the input row.";
143
147
  readonly 'settings.size': "Size (px)";
144
- readonly 'settings.sizeHint': "Sprite cell height, 32–512.";
148
+ readonly 'settings.sizeHint': "Sprite cell height, 32–1024.";
145
149
  readonly 'settings.right': "Right inset (px)";
146
150
  readonly 'settings.rightHint': "Horizontal inset from the viewport right edge.";
147
151
  readonly 'settings.bottom': "Bottom inset (px)";
@@ -1 +1 @@
1
- {"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,mDAAmD;AACnD,eAAO,MAAM,EAAE,QAAQ,CAAA;AAEvB,oBAAoB;AACpB,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+EL,CAAA;AAEV,oBAAoB;AACpB,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+EL,CAAA;AAEV,oCAAoC;AACpC,MAAM,MAAM,MAAM,GAAG,MAAM,OAAO,EAAE,CAAA;AAEpC,qDAAqD;AACrD,MAAM,MAAM,eAAe,GAAG,MAAM,CAAA;AAEpC;;;;;GAKG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAGnD;AAED;;;;;;;GAOG;AACH,wBAAgB,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAQvE;AAED,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,kBAAkB;QAC1B,uBAAuB;QACvB,GAAG,EAAE,MAAM,CAAA;KACZ;CACF"}
1
+ {"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,mDAAmD;AACnD,eAAO,MAAM,EAAE,QAAQ,CAAA;AAEvB,oBAAoB;AACpB,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiFL,CAAA;AAEV,oBAAoB;AACpB,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiFL,CAAA;AAEV,oCAAoC;AACpC,MAAM,MAAM,MAAM,GAAG,MAAM,OAAO,EAAE,CAAA;AAEpC,qDAAqD;AACrD,MAAM,MAAM,eAAe,GAAG,MAAM,CAAA;AAEpC;;;;;GAKG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAGnD;AAED;;;;;;;GAOG;AACH,wBAAgB,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAQvE;AAED,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,kBAAkB;QAC1B,uBAAuB;QACvB,GAAG,EAAE,MAAM,CAAA;KACZ;CACF"}
@@ -35,9 +35,11 @@ export const zh = {
35
35
  'pet.gameplay.menu': '玩法',
36
36
  'pet.gameplay.work': '打工',
37
37
  'pet.gameplay.stopWork': '收工',
38
- 'pet.gameplay.sleep': '睡觉',
38
+ 'pet.gameplay.sleep': '休息',
39
39
  'pet.gameplay.wake': '起床',
40
40
  'pet.gameplay.shop': '商店',
41
+ 'pet.gameplay.skin': '皮肤',
42
+ 'pet.gameplay.skinDefault': '默认',
41
43
  'pet.gameplay.back': '返回',
42
44
  'pet.gameplay.buy': '购买',
43
45
  'pet.gameplay.insufficient': '{currency}不足',
@@ -64,7 +66,7 @@ export const zh = {
64
66
  'settings.visible': '显示宠物',
65
67
  'settings.visibleHint': '关闭后宠物隐藏,可从聊天输入区重新召唤。',
66
68
  'settings.size': '大小(px)',
67
- 'settings.sizeHint': '精灵单元高度,范围 32–512。',
69
+ 'settings.sizeHint': '精灵单元高度,范围 32–1024。',
68
70
  'settings.right': '距右侧(px)',
69
71
  'settings.rightHint': '距视口右边缘的水平内缩距离。',
70
72
  'settings.bottom': '距底部(px)',
@@ -119,6 +121,8 @@ export const en = {
119
121
  'pet.gameplay.sleep': 'Sleep',
120
122
  'pet.gameplay.wake': 'Wake up',
121
123
  'pet.gameplay.shop': 'Shop',
124
+ 'pet.gameplay.skin': 'Skin',
125
+ 'pet.gameplay.skinDefault': 'Default',
122
126
  'pet.gameplay.back': 'Back',
123
127
  'pet.gameplay.buy': 'Buy',
124
128
  'pet.gameplay.insufficient': 'Not enough {currency}',
@@ -145,7 +149,7 @@ export const en = {
145
149
  'settings.visible': 'Show the pet',
146
150
  'settings.visibleHint': 'When off, the pet hides; summon it again from the input row.',
147
151
  'settings.size': 'Size (px)',
148
- 'settings.sizeHint': 'Sprite cell height, 32\u2013512.',
152
+ 'settings.sizeHint': 'Sprite cell height, 32\u20131024.',
149
153
  'settings.right': 'Right inset (px)',
150
154
  'settings.rightHint': 'Horizontal inset from the viewport right edge.',
151
155
  'settings.bottom': 'Bottom inset (px)',
@@ -1 +1 @@
1
- {"version":3,"file":"Frames2dVisualMount.d.ts","sourceRoot":"","sources":["../../../../src/client/renderers/Frames2dVisualMount.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAA+B,KAAK,YAAY,EAAE,MAAM,OAAO,CAAA;AACtE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AACnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAInD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACtD,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,eAAe,CAAA;AAEvC,8EAA8E;AAC9E,wBAAgB,mBAAmB,CAAC,KAAK,EAAE;IACzC,UAAU,EAAE,aAAa,CAAA;IACzB,KAAK,EAAE,aAAa,CAAA;IACpB,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,gEAAgE;IAChE,IAAI,EAAE,UAAU,CAAA;IAChB,yEAAyE;IACzE,GAAG,CAAC,EAAE,WAAW,CAAA;IACjB,CAAC,EAAE,WAAW,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,CAAA;CAC/B,GAAG,YAAY,CA6Ef"}
1
+ {"version":3,"file":"Frames2dVisualMount.d.ts","sourceRoot":"","sources":["../../../../src/client/renderers/Frames2dVisualMount.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAA+B,KAAK,YAAY,EAAE,MAAM,OAAO,CAAA;AACtE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AACnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAInD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACtD,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,eAAe,CAAA;AAEvC,8EAA8E;AAC9E,wBAAgB,mBAAmB,CAAC,KAAK,EAAE;IACzC,UAAU,EAAE,aAAa,CAAA;IACzB,KAAK,EAAE,aAAa,CAAA;IACpB,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,gEAAgE;IAChE,IAAI,EAAE,UAAU,CAAA;IAChB,yEAAyE;IACzE,GAAG,CAAC,EAAE,WAAW,CAAA;IACjB,CAAC,EAAE,WAAW,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,CAAA;CAC/B,GAAG,YAAY,CAiFf"}
@@ -51,7 +51,11 @@ export function Frames2dVisualMount(props) {
51
51
  if (props.bus !== undefined) {
52
52
  const gameplayBus = props.bus;
53
53
  gameplayBus.setTrack = (track) => { handleRef.current?.setState(track); };
54
- cleanups.push(() => { gameplayBus.setTrack = undefined; });
54
+ gameplayBus.setIdleTrack = (track) => { handleRef.current?.setIdleTrack(track); };
55
+ cleanups.push(() => {
56
+ gameplayBus.setTrack = undefined;
57
+ gameplayBus.setIdleTrack = undefined;
58
+ });
55
59
  }
56
60
  // The drag gesture drives the conventional 'drag' track when declared.
57
61
  // On release, a declared gameplay.dragEndState (miku: standup) plays
@@ -37,10 +37,35 @@ export interface PetFrames2dConfig {
37
37
  phases: Partial<Record<ActivityPhase, string>> & {
38
38
  idle: string;
39
39
  };
40
+ /** Selectable skins; each swaps the base idle target while selected. */
41
+ skins?: Frames2dSkinConfig[];
42
+ }
43
+ /** One selectable skins entry as served inside the pet definition. */
44
+ export interface Frames2dSkinConfig {
45
+ id: string;
46
+ label: string;
47
+ /** A declared looping track that becomes the base idle while selected. */
48
+ idleTrack: string;
49
+ /** Click actions exclusive to this skin (roll by probability; miss → touch zones). */
50
+ clickActions?: Frames2dSkinClickActionConfig[];
51
+ /** Gameplay-state overrides (state -> track) swapped in while selected. */
52
+ gameplayTracks?: Record<string, string>;
53
+ }
54
+ /** One probability-rolled tap action a skin may declare. */
55
+ export interface Frames2dSkinClickActionConfig {
56
+ track: string;
57
+ probability: number;
58
+ phrases?: string[];
40
59
  }
41
60
  export interface Frames2dRendererHandle extends PetRendererHandle {
42
61
  /** Force a track id (gameplay override); undefined returns to phase mapping. */
43
62
  setState(track: string | undefined): void;
63
+ /**
64
+ * Swap the base idle target (idle phase, unmapped phases and every
65
+ * fallback back to idle) to a declared looping track — a skin. undefined
66
+ * restores the manifest idle track.
67
+ */
68
+ setIdleTrack(track: string | undefined): void;
44
69
  /** The track currently playing (diagnostics and tests). */
45
70
  currentTrack(): string;
46
71
  }
@@ -1 +1 @@
1
- {"version":3,"file":"frames2d.d.ts","sourceRoot":"","sources":["../../../../src/client/renderers/frames2d.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAA4B,KAAK,WAAW,EAA2B,KAAK,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AACzI,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,oEAAoE;AACpE,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,SAAS,EAAE,MAAM,EAAE,CAAA;IACnB,IAAI,EAAE,OAAO,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,8DAA8D;AAC9D,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAA;IAC3C,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC,GAAG;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;CAClE;AAED,MAAM,WAAW,sBAAuB,SAAQ,iBAAiB;IAC/D,gFAAgF;IAChF,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAAA;IACzC,2DAA2D;IAC3D,YAAY,IAAI,MAAM,CAAA;CACvB;AA0CD,eAAO,MAAM,gBAAgB,EAAE,WAAW,CAAC,iBAAiB,CAsP3D,CAAA"}
1
+ {"version":3,"file":"frames2d.d.ts","sourceRoot":"","sources":["../../../../src/client/renderers/frames2d.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAA4B,KAAK,WAAW,EAA2B,KAAK,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AACzI,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,oEAAoE;AACpE,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,SAAS,EAAE,MAAM,EAAE,CAAA;IACnB,IAAI,EAAE,OAAO,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,8DAA8D;AAC9D,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAA;IAC3C,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC,GAAG;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;IACjE,wEAAwE;IACxE,KAAK,CAAC,EAAE,kBAAkB,EAAE,CAAA;CAC7B;AAED,sEAAsE;AACtE,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,0EAA0E;IAC1E,SAAS,EAAE,MAAM,CAAA;IACjB,sFAAsF;IACtF,YAAY,CAAC,EAAE,6BAA6B,EAAE,CAAA;IAC9C,2EAA2E;IAC3E,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACxC;AAED,4DAA4D;AAC5D,MAAM,WAAW,6BAA6B;IAC5C,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;CACnB;AAED,MAAM,WAAW,sBAAuB,SAAQ,iBAAiB;IAC/D,gFAAgF;IAChF,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAAA;IACzC;;;;OAIG;IACH,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAAA;IAC7C,2DAA2D;IAC3D,YAAY,IAAI,MAAM,CAAA;CACvB;AAuFD,eAAO,MAAM,gBAAgB,EAAE,WAAW,CAAC,iBAAiB,CA2Q3D,CAAA"}