@linxin666/dsh-pet 0.1.3 → 0.1.7

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.
@@ -7,6 +7,7 @@
7
7
  * @module @linxin666/dsh-pet
8
8
  */
9
9
  import { Context } from '@deepseek-ai/cordis';
10
+ import z from 'schemastery';
10
11
  import { type PetConfig } from './service.ts';
11
12
  export { PetService } from './service.ts';
12
13
  export type { PetConfig, PetInteractResult, PetStateView, } from './service.ts';
@@ -24,20 +25,20 @@ export declare const name = "pet";
24
25
  /** Services required before the pet can mount its surfaces. */
25
26
  export declare const inject: string[];
26
27
  /** Settings section schema: the display fields and name the web settings surface edits. */
27
- export declare const PET_SETTINGS_SCHEMA: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<{
28
- visible: import("@deepseek-ai/schemastery").default<boolean, boolean>;
29
- size: import("@deepseek-ai/schemastery").default<number, number>;
30
- right: import("@deepseek-ai/schemastery").default<number, number>;
31
- bottom: import("@deepseek-ai/schemastery").default<number, number>;
32
- name: import("@deepseek-ai/schemastery").default<string, string>;
33
- enabled: import("@deepseek-ai/schemastery").default<boolean, boolean>;
28
+ export declare const PET_SETTINGS_SCHEMA: z<Schemastery.ObjectS<{
29
+ visible: z<boolean, boolean>;
30
+ size: z<number, number>;
31
+ right: z<number, number>;
32
+ bottom: z<number, number>;
33
+ name: z<string, string>;
34
+ enabled: z<boolean, boolean>;
34
35
  }>, Schemastery.ObjectT<{
35
- visible: import("@deepseek-ai/schemastery").default<boolean, boolean>;
36
- size: import("@deepseek-ai/schemastery").default<number, number>;
37
- right: import("@deepseek-ai/schemastery").default<number, number>;
38
- bottom: import("@deepseek-ai/schemastery").default<number, number>;
39
- name: import("@deepseek-ai/schemastery").default<string, string>;
40
- enabled: import("@deepseek-ai/schemastery").default<boolean, boolean>;
36
+ visible: z<boolean, boolean>;
37
+ size: z<number, number>;
38
+ right: z<number, number>;
39
+ bottom: z<number, number>;
40
+ name: z<string, string>;
41
+ enabled: z<boolean, boolean>;
41
42
  }>>;
42
43
  /** Register the pet service and its API + asset routes on the context. */
43
44
  export declare function apply(ctx: Context, config?: PetConfig): void;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAI7C,OAAO,EAAsC,KAAK,SAAS,EAA2B,MAAM,cAAc,CAAA;AAU1G,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AACzC,YAAY,EACV,SAAS,EACT,iBAAiB,EACjB,YAAY,GACb,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,YAAY,EACZ,cAAc,EACd,gBAAgB,EAChB,eAAe,EACf,aAAa,EACb,MAAM,GACP,MAAM,eAAe,CAAA;AACtB,YAAY,EACV,cAAc,EACd,aAAa,EACb,kBAAkB,EAClB,cAAc,GACf,MAAM,eAAe,CAAA;AACtB,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,KAAK,GACN,MAAM,YAAY,CAAA;AACnB,YAAY,EACV,aAAa,EACb,YAAY,EACZ,cAAc,EACd,aAAa,EACb,gBAAgB,GACjB,MAAM,YAAY,CAAA;AACnB,OAAO,EACL,YAAY,EACZ,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,aAAa,CAAA;AACpB,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAC5E,OAAO,EACL,oBAAoB,EACpB,YAAY,EACZ,cAAc,EACd,UAAU,EACV,cAAc,GACf,MAAM,cAAc,CAAA;AACrB,YAAY,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAEhE,OAAO,EACL,aAAa,EACb,cAAc,EACd,cAAc,EACd,gBAAgB,GACjB,MAAM,aAAa,CAAA;AAEpB,sEAAsE;AACtE,eAAO,MAAM,IAAI,QAAQ,CAAA;AAEzB,+DAA+D;AAC/D,eAAO,MAAM,MAAM,UAAgB,CAAA;AAEnC,2FAA2F;AAC3F,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;GAO9B,CAAA;AAEF,0EAA0E;AAC1E,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,GAAE,SAAc,GAAG,IAAI,CAoDhE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAG7C,OAAO,CAAC,MAAM,aAAa,CAAA;AAC3B,OAAO,EAAsC,KAAK,SAAS,EAA2B,MAAM,cAAc,CAAA;AAU1G,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AACzC,YAAY,EACV,SAAS,EACT,iBAAiB,EACjB,YAAY,GACb,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,YAAY,EACZ,cAAc,EACd,gBAAgB,EAChB,eAAe,EACf,aAAa,EACb,MAAM,GACP,MAAM,eAAe,CAAA;AACtB,YAAY,EACV,cAAc,EACd,aAAa,EACb,kBAAkB,EAClB,cAAc,GACf,MAAM,eAAe,CAAA;AACtB,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,KAAK,GACN,MAAM,YAAY,CAAA;AACnB,YAAY,EACV,aAAa,EACb,YAAY,EACZ,cAAc,EACd,aAAa,EACb,gBAAgB,GACjB,MAAM,YAAY,CAAA;AACnB,OAAO,EACL,YAAY,EACZ,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,aAAa,CAAA;AACpB,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAC5E,OAAO,EACL,oBAAoB,EACpB,YAAY,EACZ,cAAc,EACd,UAAU,EACV,cAAc,GACf,MAAM,cAAc,CAAA;AACrB,YAAY,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAEhE,OAAO,EACL,aAAa,EACb,cAAc,EACd,cAAc,EACd,gBAAgB,GACjB,MAAM,aAAa,CAAA;AAEpB,sEAAsE;AACtE,eAAO,MAAM,IAAI,QAAQ,CAAA;AAEzB,+DAA+D;AAC/D,eAAO,MAAM,MAAM,UAAgB,CAAA;AAEnC,2FAA2F;AAC3F,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;GAO9B,CAAA;AAEF,0EAA0E;AAC1E,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,GAAE,SAAc,GAAG,IAAI,CAoDhE"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@linxin666/dsh-pet",
3
3
  "description": "鲸鱼娘宠物插件 for the dsh web GUI: a soft healing whale-girl companion that reacts to model activity (idle/waiting/thinking/tool/done), with petting/feeding interactions and an affinity score",
4
- "version": "0.1.3",
4
+ "version": "0.1.7",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
@@ -1,9 +1,8 @@
1
1
  /**
2
- * Dock anchor inside `conversation.input.selector.context`: the input
3
- * selector row mounts in EVERY conversation phase (no-session cold start,
4
- * the blank-session hero, and the active seat), so the floating pet stays on
5
- * screen on the new-conversation screen too. While visible it mounts the
6
- * floating WhalePet (portal); while hidden it renders the summon button.
2
+ * Dock anchor inside the official SDK's `conversation.input.dock` seat
3
+ * (the ambient readout band above the composer card, listed so several
4
+ * additions can stack). While visible it mounts the floating WhalePet
5
+ * (portal); while hidden it renders the summon button.
7
6
  * @module @linxin666/dsh-pet/client/PetDockEntry
8
7
  */
9
8
 
@@ -40,7 +39,7 @@ export interface PetInjected {
40
39
 
41
40
  /** Composed props of the dock entry (runtime + locale + injected). */
42
41
  export type PetDockEntryProps =
43
- PropsRuntime<'conversation.input.selector.context'>
42
+ PropsRuntime<'conversation.input.dock'>
44
43
  & PetInjected
45
44
  & PropsLocale<typeof NS>
46
45
 
@@ -71,6 +71,7 @@ export function WhalePet(props: WhalePetProps): ReactPortal {
71
71
  const [nameDraft, setNameDraft] = useState('')
72
72
  const [dragPos, setDragPos] = useState<{ right: number; bottom: number } | null>(null)
73
73
  const dragRef = useRef<{ startX: number; startY: number; right: number; bottom: number } | null>(null)
74
+ const hideTimerRef = useRef<number | null>(null)
74
75
  const frameRef = useRef<{ track: PetAnimation | null; index: number; elapsed: number }>({
75
76
  track: null,
76
77
  index: 0,
@@ -193,6 +194,13 @@ export function WhalePet(props: WhalePetProps): ReactPortal {
193
194
  // `draggedRef` records whether the pointer actually moved, so the browser's
194
195
  // trailing click (fired after pointerup) does not pet the whale.
195
196
  const draggedRef = useRef(false)
197
+ const clearHideTimer = (): void => {
198
+ if (hideTimerRef.current !== null) {
199
+ window.clearTimeout(hideTimerRef.current)
200
+ hideTimerRef.current = null
201
+ }
202
+ }
203
+
196
204
  const onPointerDown = (e: ReactPointerEvent<HTMLDivElement>): void => {
197
205
  e.preventDefault()
198
206
  ;(e.target as HTMLElement).setPointerCapture?.(e.pointerId)
@@ -226,15 +234,20 @@ export function WhalePet(props: WhalePetProps): ReactPortal {
226
234
  ref={floatRef}
227
235
  className={styles.float}
228
236
  style={{ right: pos.right, bottom: pos.bottom, zIndex: 2147483000 }}
229
- onPointerEnter={() => setHovered(true)}
237
+ onPointerEnter={() => {
238
+ clearHideTimer()
239
+ setHovered(true)
240
+ }}
230
241
  onPointerLeave={(e) => {
231
242
  // The panel and bubble render OUTSIDE the container's box (absolute,
232
243
  // above the sprite), so moving onto them fires pointerleave on the
233
244
  // container. Treat a target still inside the container's DOM (the
234
- // overflowed panel) as "still hovering".
245
+ // overflowed panel) as "still hovering"; otherwise give the pointer a
246
+ // short grace period to reach the panel across the gap above it.
235
247
  const next = e.relatedTarget
236
248
  if (next instanceof Node && floatRef.current?.contains(next)) return
237
- setHovered(false)
249
+ clearHideTimer()
250
+ hideTimerRef.current = window.setTimeout(() => setHovered(false), 200)
238
251
  }}
239
252
  >
240
253
  <div
@@ -233,13 +233,14 @@ export function apply(ctx: ClientContext): void {
233
233
  },
234
234
  })
235
235
 
236
- // The input selector row mounts in EVERY conversation phase (cold
237
- // start, blank-session hero, active seat) the composer dock band
238
- // only renders for an active session, which is why the pet used to
239
- // vanish on the new-conversation screen.
240
- const disposeDock = ctx.slots.inject('conversation.input.selector.context', () =>
236
+ // The legacy selector-context hole (conversation.input.selector.context)
237
+ // is no longer declared by newer shells (rc.6 dropped it), so the pet
238
+ // mounts on the composer dock band instead. The dock is session-scoped,
239
+ // so the pet appears once a session is active (accepted downgrade, see
240
+ // PR #17).
241
+ const disposeDock = ctx.slots.inject('conversation.input.dock', () =>
241
242
  ctx.slots.register({
242
- name: 'conversation.input.selector.context',
243
+ name: 'conversation.input.dock',
243
244
  id: 'pet',
244
245
  order: 110,
245
246
  inject: injected,
@@ -60,7 +60,7 @@
60
60
 
61
61
  .panel {
62
62
  position: absolute;
63
- bottom: calc(100% + 4px);
63
+ bottom: 100%;
64
64
  background: rgba(15, 23, 42, 0.92);
65
65
  border: 1px solid rgba(148, 163, 184, 0.35);
66
66
  border-radius: 10px;
@@ -1,217 +0,0 @@
1
- //#region lib/types/affinity.js
2
- /**
3
- * Affinity score — pure, clock-injected. The pet grows closer the more you
4
- * work together and care for it: every completed turn earns a small reward,
5
- * petting earns a tiny one (cooldown-gated), feeding earns the most.
6
- * Persistence lives in the service; this module only computes transitions.
7
- * @module @deepseek-ai/dsh-pet/affinity
8
- */
9
- const AFFINITY_MAX = 100;
10
- /** Affinity ranks by points; the pet visibly grows with its rank. */
11
- const AFFINITY_RANKS = [
12
- {
13
- min: 0,
14
- name: "幼鲸",
15
- emoji: "🐣"
16
- },
17
- {
18
- min: 25,
19
- name: "伙伴",
20
- emoji: "🐬"
21
- },
22
- {
23
- min: 50,
24
- name: "挚友",
25
- emoji: "🐳"
26
- },
27
- {
28
- min: 80,
29
- name: "深海羁绊",
30
- emoji: "💙"
31
- }
32
- ];
33
- const defaultAffinityConfig = {
34
- turnReward: 1,
35
- petReward: 1,
36
- petCooldownMs: 1e4,
37
- feedReward: 5,
38
- feedCooldownMs: 3e4
39
- };
40
- function emptyAffinity() {
41
- return {
42
- points: 0,
43
- lastPetAt: 0,
44
- lastFeedAt: 0,
45
- pets: 0,
46
- feeds: 0,
47
- turns: 0
48
- };
49
- }
50
- /** Rank for a point total. */
51
- function rankOf(points) {
52
- let rank = AFFINITY_RANKS[0];
53
- for (const candidate of AFFINITY_RANKS) if (points >= candidate.min) rank = candidate;
54
- return rank;
55
- }
56
- function clamp(points) {
57
- return Math.min(100, Math.max(0, points));
58
- }
59
- /**
60
- * Apply one interaction to a copy of the state (immutable style: returns a
61
- * new object; the caller replaces the persisted state). Cooldowns only
62
- * apply once the pet has been interacted with at least once (last*At === 0
63
- * means "never", so the first pet/feed always lands).
64
- */
65
- function applyInteraction(state, kind, nowMs, config = defaultAffinityConfig) {
66
- const next = { ...state };
67
- if (kind === "pet") {
68
- if (state.lastPetAt !== 0 && nowMs - state.lastPetAt < config.petCooldownMs) return {
69
- affinity: state,
70
- delta: 0,
71
- reaction: "摸过头啦,让鲸鱼娘歇口气~",
72
- accepted: false
73
- };
74
- next.lastPetAt = nowMs;
75
- next.pets += 1;
76
- next.points = clamp(state.points + config.petReward);
77
- return {
78
- affinity: next,
79
- delta: config.petReward,
80
- reaction: "咕噜咕噜~被摸摸好舒服!",
81
- accepted: true
82
- };
83
- }
84
- if (kind === "feed") {
85
- if (state.lastFeedAt !== 0 && nowMs - state.lastFeedAt < config.feedCooldownMs) return {
86
- affinity: state,
87
- delta: 0,
88
- reaction: "吃饱啦,晚点再喂~",
89
- accepted: false
90
- };
91
- next.lastFeedAt = nowMs;
92
- next.feeds += 1;
93
- next.points = clamp(state.points + config.feedReward);
94
- return {
95
- affinity: next,
96
- delta: config.feedReward,
97
- reaction: "呜哇!小鱼干好好吃!",
98
- accepted: true
99
- };
100
- }
101
- return {
102
- affinity: state,
103
- delta: 0,
104
- reaction: "",
105
- accepted: false
106
- };
107
- }
108
- /** Reward one completed turn (called by the host on `done`). */
109
- function applyTurnReward(state, config = defaultAffinityConfig) {
110
- const next = { ...state };
111
- next.turns += 1;
112
- next.points = clamp(state.points + config.turnReward);
113
- return next;
114
- }
115
- //#endregion
116
- //#region lib/types/state.js
117
- /**
118
- * Pet state machine — pure, clock-injected. Maps the DSH `activity/status`
119
- * phase vocabulary (session events) onto the 9-state Codex pet
120
- * animation contract, plus the session lifecycle transitions the web UI
121
- * exposes (turn end celebration, no-session idle).
122
- *
123
- * The machine is deliberately dumb: it holds the last input phase, the
124
- * animation decision, and a one-shot "celebration" window after `done` so the
125
- * pet visibly jumps before settling back to idle. Everything here is a pure
126
- * function of (input, nowMs); persistence and RPC live in the service.
127
- * @module @deepseek-ai/dsh-pet/state
128
- */
129
- const defaultPetStateConfig = { celebrateMs: 2400 };
130
- /**
131
- * Map one activity phase onto the animation contract.
132
- * - thinking / tool → `running` (focused work), with `running-right` as the
133
- * side-alternating variant the client may use for tool activity.
134
- * - waiting → `waiting` (expectant pose, needs user input).
135
- * - done → `jumping` (celebration), then back to `idle` after the window.
136
- * - idle → `idle` (calm breathing loop).
137
- * `failed` has no DSH phase source yet; the machine keeps the mapping table
138
- * so a future error event can light it up.
139
- */
140
- function animationForPhase(phase) {
141
- switch (phase) {
142
- case "thinking": return "running";
143
- case "tool": return "running-right";
144
- case "waiting": return "waiting";
145
- case "done": return "jumping";
146
- case "idle": return "idle";
147
- }
148
- }
149
- /** The spritesheet row index for one animation track. */
150
- function rowOf(animation) {
151
- return {
152
- "idle": 0,
153
- "running-right": 1,
154
- "running-left": 2,
155
- "waving": 3,
156
- "jumping": 4,
157
- "failed": 5,
158
- "waiting": 6,
159
- "running": 7,
160
- "review": 8
161
- }[animation];
162
- }
163
- /**
164
- * PetStateMachine — one instance per host process. Holds only the latest
165
- * input snapshot and the celebration timing; no storage, no side effects.
166
- */
167
- var PetStateMachine = class {
168
- config;
169
- now;
170
- phase = "idle";
171
- line;
172
- phrase;
173
- sessionActive = false;
174
- doneAt;
175
- constructor(config = defaultPetStateConfig, now = Date.now) {
176
- this.config = config;
177
- this.now = now;
178
- }
179
- /** Consume one `activity/status` session event. */
180
- onActivityStatus(input) {
181
- this.phase = input.phase;
182
- this.line = input.line;
183
- this.phrase = input.phrase;
184
- if (input.phase === "done") this.doneAt = this.now();
185
- }
186
- /** A session became the active one (or a fresh session started). */
187
- onSessionActive() {
188
- this.sessionActive = true;
189
- }
190
- /** The active session was disposed (or none left). */
191
- onSessionDisposed() {
192
- this.sessionActive = false;
193
- this.phase = "idle";
194
- this.line = void 0;
195
- this.phrase = void 0;
196
- this.doneAt = void 0;
197
- }
198
- /** Render the current animation decision. */
199
- render() {
200
- const nowMs = this.now();
201
- let animation = animationForPhase(this.phase);
202
- if (this.phase === "done" && this.doneAt !== void 0) {
203
- if (nowMs - this.doneAt < this.config.celebrateMs) animation = "jumping";
204
- else animation = "idle";
205
- }
206
- const bubble = this.phrase ?? this.line;
207
- return {
208
- animation,
209
- ...bubble === void 0 ? {} : { bubble },
210
- animationStartedAt: nowMs,
211
- phase: this.phase,
212
- sessionActive: this.sessionActive
213
- };
214
- }
215
- };
216
- //#endregion
217
- export { AFFINITY_MAX as a, applyTurnReward as c, rankOf as d, rowOf as i, defaultAffinityConfig as l, animationForPhase as n, AFFINITY_RANKS as o, defaultPetStateConfig as r, applyInteraction as s, PetStateMachine as t, emptyAffinity as u };
@@ -1,196 +0,0 @@
1
- //#region src/affinity.ts
2
- const AFFINITY_MAX = 100;
3
- /** Affinity ranks by points; the pet visibly grows with its rank. */
4
- const AFFINITY_RANKS = [
5
- {
6
- min: 0,
7
- name: "幼鲸",
8
- emoji: "🐣"
9
- },
10
- {
11
- min: 25,
12
- name: "伙伴",
13
- emoji: "🐬"
14
- },
15
- {
16
- min: 50,
17
- name: "挚友",
18
- emoji: "🐳"
19
- },
20
- {
21
- min: 80,
22
- name: "深海羁绊",
23
- emoji: "💙"
24
- }
25
- ];
26
- const defaultAffinityConfig = {
27
- turnReward: 1,
28
- petReward: 1,
29
- petCooldownMs: 1e4,
30
- feedReward: 5,
31
- feedCooldownMs: 3e4
32
- };
33
- function emptyAffinity() {
34
- return {
35
- points: 0,
36
- lastPetAt: 0,
37
- lastFeedAt: 0,
38
- pets: 0,
39
- feeds: 0,
40
- turns: 0
41
- };
42
- }
43
- /** Rank for a point total. */
44
- function rankOf(points) {
45
- let rank = AFFINITY_RANKS[0];
46
- for (const candidate of AFFINITY_RANKS) if (points >= candidate.min) rank = candidate;
47
- return rank;
48
- }
49
- function clamp(points) {
50
- return Math.min(100, Math.max(0, points));
51
- }
52
- /**
53
- * Apply one interaction to a copy of the state (immutable style: returns a
54
- * new object; the caller replaces the persisted state). Cooldowns only
55
- * apply once the pet has been interacted with at least once (last*At === 0
56
- * means "never", so the first pet/feed always lands).
57
- */
58
- function applyInteraction(state, kind, nowMs, config = defaultAffinityConfig) {
59
- const next = { ...state };
60
- if (kind === "pet") {
61
- if (state.lastPetAt !== 0 && nowMs - state.lastPetAt < config.petCooldownMs) return {
62
- affinity: state,
63
- delta: 0,
64
- reaction: "摸过头啦,让鲸鱼娘歇口气~",
65
- accepted: false
66
- };
67
- next.lastPetAt = nowMs;
68
- next.pets += 1;
69
- next.points = clamp(state.points + config.petReward);
70
- return {
71
- affinity: next,
72
- delta: config.petReward,
73
- reaction: "咕噜咕噜~被摸摸好舒服!",
74
- accepted: true
75
- };
76
- }
77
- if (kind === "feed") {
78
- if (state.lastFeedAt !== 0 && nowMs - state.lastFeedAt < config.feedCooldownMs) return {
79
- affinity: state,
80
- delta: 0,
81
- reaction: "吃饱啦,晚点再喂~",
82
- accepted: false
83
- };
84
- next.lastFeedAt = nowMs;
85
- next.feeds += 1;
86
- next.points = clamp(state.points + config.feedReward);
87
- return {
88
- affinity: next,
89
- delta: config.feedReward,
90
- reaction: "呜哇!小鱼干好好吃!",
91
- accepted: true
92
- };
93
- }
94
- return {
95
- affinity: state,
96
- delta: 0,
97
- reaction: "",
98
- accepted: false
99
- };
100
- }
101
- /** Reward one completed turn (called by the host on `done`). */
102
- function applyTurnReward(state, config = defaultAffinityConfig) {
103
- const next = { ...state };
104
- next.turns += 1;
105
- next.points = clamp(state.points + config.turnReward);
106
- return next;
107
- }
108
- //#endregion
109
- //#region src/state.ts
110
- const defaultPetStateConfig = { celebrateMs: 2400 };
111
- /**
112
- * Map one activity phase onto the animation contract.
113
- * - thinking / tool → `running` (focused work), with `running-right` as the
114
- * side-alternating variant the client may use for tool activity.
115
- * - waiting → `waiting` (expectant pose, needs user input).
116
- * - done → `jumping` (celebration), then back to `idle` after the window.
117
- * - idle → `idle` (calm breathing loop).
118
- * `failed` has no DSH phase source yet; the machine keeps the mapping table
119
- * so a future error event can light it up.
120
- */
121
- function animationForPhase(phase) {
122
- switch (phase) {
123
- case "thinking": return "running";
124
- case "tool": return "running-right";
125
- case "waiting": return "waiting";
126
- case "done": return "jumping";
127
- case "idle": return "idle";
128
- }
129
- }
130
- /** The spritesheet row index for one animation track. */
131
- function rowOf(animation) {
132
- return {
133
- "idle": 0,
134
- "running-right": 1,
135
- "running-left": 2,
136
- "waving": 3,
137
- "jumping": 4,
138
- "failed": 5,
139
- "waiting": 6,
140
- "running": 7,
141
- "review": 8
142
- }[animation];
143
- }
144
- /**
145
- * PetStateMachine — one instance per host process. Holds only the latest
146
- * input snapshot and the celebration timing; no storage, no side effects.
147
- */
148
- var PetStateMachine = class {
149
- config;
150
- now;
151
- phase = "idle";
152
- line;
153
- phrase;
154
- sessionActive = false;
155
- doneAt;
156
- constructor(config = defaultPetStateConfig, now = Date.now) {
157
- this.config = config;
158
- this.now = now;
159
- }
160
- /** Consume one `activity/status` session event. */
161
- onActivityStatus(input) {
162
- this.phase = input.phase;
163
- this.line = input.line;
164
- this.phrase = input.phrase;
165
- if (input.phase === "done") this.doneAt = this.now();
166
- }
167
- /** A session became the active one (or a fresh session started). */
168
- onSessionActive() {
169
- this.sessionActive = true;
170
- }
171
- /** The active session was disposed (or none left). */
172
- onSessionDisposed() {
173
- this.sessionActive = false;
174
- this.phase = "idle";
175
- this.line = void 0;
176
- this.phrase = void 0;
177
- this.doneAt = void 0;
178
- }
179
- /** Render the current animation decision. */
180
- render() {
181
- const nowMs = this.now();
182
- let animation = animationForPhase(this.phase);
183
- if (this.phase === "done" && this.doneAt !== void 0) if (nowMs - this.doneAt < this.config.celebrateMs) animation = "jumping";
184
- else animation = "idle";
185
- const bubble = this.phrase ?? this.line;
186
- return {
187
- animation,
188
- ...bubble === void 0 ? {} : { bubble },
189
- animationStartedAt: nowMs,
190
- phase: this.phase,
191
- sessionActive: this.sessionActive
192
- };
193
- }
194
- };
195
- //#endregion
196
- export { AFFINITY_MAX as a, applyTurnReward as c, rankOf as d, rowOf as i, defaultAffinityConfig as l, animationForPhase as n, AFFINITY_RANKS as o, defaultPetStateConfig as r, applyInteraction as s, PetStateMachine as t, emptyAffinity as u };