@linxin666/dsh-pet 0.3.21 → 0.3.22

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.
@@ -1 +1 @@
1
- {"version":3,"file":"PetSprite.d.ts","sourceRoot":"","sources":["../../../src/client/PetSprite.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAkE,SAAS,EAAE,WAAW,EAAE,MAAM,OAAO,CAAA;AAGnH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AACnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAEjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAIjD,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAA;AAGjC,wEAAwE;AACxE,MAAM,WAAW,cAAc;IAC7B,gDAAgD;IAChD,QAAQ,EAAE,YAAY,GAAG,IAAI,CAAA;IAC7B,8EAA8E;IAC9E,UAAU,EAAE,aAAa,CAAA;IACzB,qDAAqD;IACrD,OAAO,EAAE,gBAAgB,CAAA;IACzB,sCAAsC;IACtC,QAAQ,EAAE,WAAW,GAAG,IAAI,CAAA;IAC5B,8BAA8B;IAC9B,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,sCAAsC;IACtC,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,sCAAsC;IACtC,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,+BAA+B;IAC/B,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;IAClD,6EAA6E;IAC7E,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAA;IAC9C,uDAAuD;IACvD,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAChC,4DAA4D;IAC5D,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAA;IAC1C,2DAA2D;IAC3D,cAAc,EAAE,MAAM,IAAI,CAAA;IAC1B;;;;OAIG;IACH,MAAM,CAAC,EAAE,SAAS,CAAA;IAClB;;;;OAIG;IACH,GAAG,CAAC,EAAE,SAAS,CAAA;IACf;;;;OAIG;IACH,aAAa,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,IAAI,CAAA;IAC9D;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,IAAI,CAAA;IAC3B,qEAAqE;IACrE,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,+CAA+C;IAC/C,CAAC,EAAE,WAAW,CAAC,OAAO,EAAE,CAAC,CAAA;CAC1B;AA8ID;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,WAAW,CA+jB5D"}
1
+ {"version":3,"file":"PetSprite.d.ts","sourceRoot":"","sources":["../../../src/client/PetSprite.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAkE,SAAS,EAAE,WAAW,EAAE,MAAM,OAAO,CAAA;AAGnH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AACnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAEjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAIjD,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAA;AAGjC,wEAAwE;AACxE,MAAM,WAAW,cAAc;IAC7B,gDAAgD;IAChD,QAAQ,EAAE,YAAY,GAAG,IAAI,CAAA;IAC7B,8EAA8E;IAC9E,UAAU,EAAE,aAAa,CAAA;IACzB,qDAAqD;IACrD,OAAO,EAAE,gBAAgB,CAAA;IACzB,sCAAsC;IACtC,QAAQ,EAAE,WAAW,GAAG,IAAI,CAAA;IAC5B,8BAA8B;IAC9B,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,sCAAsC;IACtC,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,sCAAsC;IACtC,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,+BAA+B;IAC/B,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;IAClD,6EAA6E;IAC7E,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAA;IAC9C,uDAAuD;IACvD,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAChC,4DAA4D;IAC5D,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAA;IAC1C,2DAA2D;IAC3D,cAAc,EAAE,MAAM,IAAI,CAAA;IAC1B;;;;OAIG;IACH,MAAM,CAAC,EAAE,SAAS,CAAA;IAClB;;;;OAIG;IACH,GAAG,CAAC,EAAE,SAAS,CAAA;IACf;;;;OAIG;IACH,aAAa,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,IAAI,CAAA;IAC9D;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,IAAI,CAAA;IAC3B,qEAAqE;IACrE,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,+CAA+C;IAC/C,CAAC,EAAE,WAAW,CAAC,OAAO,EAAE,CAAC,CAAA;CAC1B;AAkJD;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,WAAW,CA+jB5D"}
@@ -67,7 +67,7 @@ function StatusOrnament(props) {
67
67
  const delta = now - last;
68
68
  last = now;
69
69
  elapsed += delta;
70
- const duration = decoration.durations[index] ?? 120;
70
+ let duration = decoration.durations[index] ?? 120;
71
71
  // The segment's frame rate (duration ms, typically 90-160) is far
72
72
  // below the rAF cadence, so a 60fps loop would spend ~90% of its
73
73
  // ticks doing nothing. Schedule by the remaining time to the next
@@ -81,6 +81,10 @@ function StatusOrnament(props) {
81
81
  index += 1;
82
82
  else if (decoration.loop)
83
83
  index = segment.from;
84
+ // Durations are per frame: a catch-up that crosses frames must
85
+ // subtract and schedule with the frame it lands on, not the one
86
+ // the tick started from.
87
+ duration = decoration.durations[index] ?? 120;
84
88
  } while (elapsed >= duration);
85
89
  // Only advance the background when the frame actually changes.
86
90
  el.style.backgroundPosition = position(index);
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.21",
4
+ "version": "0.3.22",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
@@ -858,6 +858,68 @@ describe('PetSprite status decoration (pet-center M5, #567)', () => {
858
858
  expect(el.style.backgroundPosition).toBe('0px 0px')
859
859
  })
860
860
 
861
+ it('advances uneven per-frame durations on each frame own clock', () => {
862
+ vi.spyOn(window, 'matchMedia').mockReturnValue({
863
+ matches: false,
864
+ media: '(prefers-reduced-motion: reduce)',
865
+ onchange: null,
866
+ addEventListener: () => {},
867
+ removeEventListener: () => {},
868
+ addListener: () => {},
869
+ removeListener: () => {},
870
+ dispatchEvent: () => false,
871
+ })
872
+ let now = 0
873
+ vi.spyOn(performance, 'now').mockImplementation(() => now)
874
+ const timers: { at: number; callback: () => void }[] = []
875
+ let timerId = 0
876
+ vi.spyOn(window, 'setTimeout').mockImplementation(((callback: () => void, delay = 0) => {
877
+ timers.push({ at: now + delay, callback })
878
+ return ++timerId
879
+ }) as typeof window.setTimeout)
880
+ vi.spyOn(window, 'clearTimeout').mockImplementation(() => {})
881
+ const frames: FrameRequestCallback[] = []
882
+ vi.spyOn(window, 'requestAnimationFrame').mockImplementation(callback => {
883
+ frames.push(callback)
884
+ return frames.length
885
+ })
886
+ vi.spyOn(window, 'cancelAnimationFrame').mockImplementation(() => {})
887
+ const uneven: DecorationView = { ...decoration, durations: [100, 300, 50, 50] }
888
+ renderPet({ snapshot: { ...snapshot, bubble: '正在思考', phase: 'thinking', decoration: uneven } })
889
+ const el = ornament()!
890
+ const step = (ms: number): void => {
891
+ for (const callback of frames.splice(0)) callback(now)
892
+ now += ms
893
+ for (;;) {
894
+ const due = timers.filter(t => t.at <= now)
895
+ if (due.length === 0) break
896
+ for (const t of due) {
897
+ const idx = timers.indexOf(t)
898
+ if (idx >= 0) timers.splice(idx, 1)
899
+ t.callback()
900
+ }
901
+ }
902
+ }
903
+ expect(el.style.backgroundPosition).toBe('0px 0px')
904
+ // One 500 ms segment (100 + 300 + 50 + 50) plus 1 ms: the catch-up loop
905
+ // must subtract each frame's own duration, so it lands one full cycle
906
+ // later on frame 0 with 99 ms of its 100 ms left — not on frame 1.
907
+ act(() => { step(501) })
908
+ expect(el.style.backgroundPosition).toBe('0px 0px')
909
+ act(() => { step(99) })
910
+ expect(el.style.backgroundPosition).toBe('-24px 0px')
911
+ // Frame 1 lasts 300 ms and the next wake uses that duration.
912
+ act(() => { step(299) })
913
+ expect(el.style.backgroundPosition).toBe('-24px 0px')
914
+ act(() => { step(1) })
915
+ expect(el.style.backgroundPosition).toBe('-48px 0px')
916
+ // Frames 2 and 3 are 50 ms each.
917
+ act(() => { step(50) })
918
+ expect(el.style.backgroundPosition).toBe('-72px 0px')
919
+ act(() => { step(50) })
920
+ expect(el.style.backgroundPosition).toBe('0px 0px')
921
+ })
922
+
861
923
  it('holds the segment last frame when the loop is off and stops scheduling', () => {
862
924
  vi.spyOn(window, 'matchMedia').mockReturnValue({
863
925
  matches: false,
@@ -139,7 +139,7 @@ function StatusOrnament(props: { decoration: DecorationView; phase: ActivityPhas
139
139
  const delta = now - last
140
140
  last = now
141
141
  elapsed += delta
142
- const duration = decoration.durations[index] ?? 120
142
+ let duration = decoration.durations[index] ?? 120
143
143
  // The segment's frame rate (duration ms, typically 90-160) is far
144
144
  // below the rAF cadence, so a 60fps loop would spend ~90% of its
145
145
  // ticks doing nothing. Schedule by the remaining time to the next
@@ -151,6 +151,10 @@ function StatusOrnament(props: { decoration: DecorationView; phase: ActivityPhas
151
151
  elapsed -= duration
152
152
  if (index < segment.to) index += 1
153
153
  else if (decoration.loop) index = segment.from
154
+ // Durations are per frame: a catch-up that crosses frames must
155
+ // subtract and schedule with the frame it lands on, not the one
156
+ // the tick started from.
157
+ duration = decoration.durations[index] ?? 120
154
158
  } while (elapsed >= duration)
155
159
  // Only advance the background when the frame actually changes.
156
160
  el.style.backgroundPosition = position(index)