@linxin666/dsh-pet 0.1.18 → 0.1.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.
- package/README.i18n.yaml +2 -2
- package/README.md +12 -5
- package/README.zh.md +12 -5
- package/lib/client.js +48 -11
- package/lib/client.js.map +1 -1
- package/lib/index.js +100 -30
- package/lib/invariant.js +2 -2
- package/lib/state-Diiq8vjk.js +394 -0
- package/lib/types/affinity.d.ts +31 -3
- package/lib/types/affinity.d.ts.map +1 -1
- package/lib/types/affinity.js +21 -7
- package/lib/types/client/PetSettingsCard.d.ts +5 -0
- package/lib/types/client/PetSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PetSettingsCard.js +7 -0
- package/lib/types/client/PetSprite.d.ts.map +1 -1
- package/lib/types/client/PetSprite.js +8 -2
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +14 -8
- package/lib/types/client/settings-form.d.ts +8 -0
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +15 -1
- package/lib/types/event-projection.d.ts.map +1 -1
- package/lib/types/event-projection.js +3 -1
- package/lib/types/index.d.ts +4 -2
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +2 -1
- package/lib/types/ledger.d.ts +16 -1
- package/lib/types/ledger.d.ts.map +1 -1
- package/lib/types/ledger.js +29 -4
- package/lib/types/registry.d.ts +10 -0
- package/lib/types/registry.d.ts.map +1 -1
- package/lib/types/registry.js +3 -0
- package/lib/types/remarks.d.ts +56 -0
- package/lib/types/remarks.d.ts.map +1 -0
- package/lib/types/remarks.js +157 -0
- package/lib/types/service.d.ts +40 -3
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js +90 -18
- package/lib/types/state.js +5 -5
- package/lib/types/treats.d.ts +3 -0
- package/lib/types/treats.d.ts.map +1 -1
- package/lib/types/treats.js +5 -2
- package/package.json +1 -1
- package/src/affinity.test.ts +19 -0
- package/src/affinity.ts +22 -7
- package/src/client/PetSettingsCard.tsx +8 -0
- package/src/client/PetSprite.test.tsx +33 -0
- package/src/client/PetSprite.tsx +16 -1
- package/src/client/index.ts +14 -8
- package/src/client/pet.module.css +19 -0
- package/src/client/settings-form.ts +15 -1
- package/src/event-projection.ts +3 -1
- package/src/index.ts +12 -1
- package/src/ledger.test.ts +40 -9
- package/src/ledger.ts +33 -4
- package/src/persist.test.ts +1 -1
- package/src/registry.test.ts +27 -0
- package/src/registry.ts +12 -0
- package/src/remarks.test.ts +89 -0
- package/src/remarks.ts +177 -0
- package/src/service.ts +116 -18
- package/src/state.test.ts +9 -0
- package/src/state.ts +5 -5
- package/src/treats.test.ts +13 -11
- package/src/treats.ts +5 -2
- package/lib/state-CFyJv0sQ.js +0 -213
package/src/service.ts
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
* persistence through `persist`. The API gateway maps these methods onto
|
|
6
6
|
* `pet.state` / `pet.pets` / `pet.interact` / `pet.setVisible` /
|
|
7
7
|
* `pet.setConfig` / `pet.setName` / `pet.setPet` for browser consumers.
|
|
8
|
+
*
|
|
9
|
+
* Concurrent sessions each keep their own machine: the sprite animation
|
|
10
|
+
* follows the most recent meaningful event (the display session) while the
|
|
11
|
+
* state view carries one bubble per active session.
|
|
8
12
|
* @module @linxin666/dsh-pet/service
|
|
9
13
|
*/
|
|
10
14
|
|
|
@@ -91,12 +95,37 @@ export interface PetSettingsSection {
|
|
|
91
95
|
/** Settings namespace of the pet capability. Spelled here rather than imported: the browser half spells the same value. */
|
|
92
96
|
export const PET_SETTINGS_NAMESPACE = 'pet'
|
|
93
97
|
|
|
98
|
+
/**
|
|
99
|
+
* One active session as the pet displays it. Sessions run in parallel, so
|
|
100
|
+
* each gets its own bubble while the sprite itself follows the most recent
|
|
101
|
+
* meaningful event (the display session).
|
|
102
|
+
*/
|
|
103
|
+
export interface PetSessionView {
|
|
104
|
+
/** Session identity (stringified for the wire; never exposed as a key). */
|
|
105
|
+
sessionId: string
|
|
106
|
+
/** The animation this session's activity maps onto. */
|
|
107
|
+
animation: PetStateSnapshot['animation']
|
|
108
|
+
/** This session's status bubble copy. */
|
|
109
|
+
bubble: string
|
|
110
|
+
/** This session's raw activity phase. */
|
|
111
|
+
phase: PetStateSnapshot['phase']
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Hard cap on simultaneously displayed session bubbles (most recent first). */
|
|
115
|
+
export const MAX_SESSION_BUBBLES = 12
|
|
116
|
+
|
|
94
117
|
/** Snapshot returned by `pet.state`. */
|
|
95
118
|
export interface PetStateView {
|
|
96
119
|
animation: PetStateSnapshot['animation']
|
|
97
120
|
bubble?: string
|
|
98
121
|
phase: PetStateSnapshot['phase']
|
|
99
122
|
sessionActive: boolean
|
|
123
|
+
/**
|
|
124
|
+
* Per-session bubbles for every concurrently active session, most recent
|
|
125
|
+
* first; optional so older hosts without the multi-session view stay
|
|
126
|
+
* consumable. The single 'bubble' above mirrors the display session.
|
|
127
|
+
*/
|
|
128
|
+
sessions?: PetSessionView[]
|
|
100
129
|
/** Affinity ledger snapshot. */
|
|
101
130
|
affinity: PetAffinityView
|
|
102
131
|
/** Display configuration. */
|
|
@@ -130,6 +159,14 @@ declare module '@deepseek-ai/cordis' {
|
|
|
130
159
|
}
|
|
131
160
|
}
|
|
132
161
|
|
|
162
|
+
/** Per-session pet activity: projection runtime plus the session's own machine. */
|
|
163
|
+
interface SessionActivity {
|
|
164
|
+
runtime: ProjectionRuntime
|
|
165
|
+
machine: PetStateMachine
|
|
166
|
+
/** The session's most recent meaningful input (for display fallback). */
|
|
167
|
+
lastInput?: PetStateInput
|
|
168
|
+
}
|
|
169
|
+
|
|
133
170
|
/**
|
|
134
171
|
* Cordis service exposing the pet RPC domain. Lazy: nothing is scanned or
|
|
135
172
|
* written until an economic event or interaction arrives; event listeners
|
|
@@ -140,6 +177,7 @@ export class PetService extends Service {
|
|
|
140
177
|
static inject: string[] = []
|
|
141
178
|
|
|
142
179
|
private readonly machine: PetStateMachine
|
|
180
|
+
private readonly stateConfig: PetStateConfig
|
|
143
181
|
private readonly ledger: PetLedger
|
|
144
182
|
private readonly registry: PetRegistry
|
|
145
183
|
private readonly persistDir: string
|
|
@@ -147,7 +185,12 @@ export class PetService extends Service {
|
|
|
147
185
|
private disposeActivity: (() => void) | undefined
|
|
148
186
|
/** Session whose most recent meaningful event currently drives the global pet. */
|
|
149
187
|
private displaySession: Session | undefined
|
|
150
|
-
|
|
188
|
+
/**
|
|
189
|
+
* Per-session activity, most recent last (Map insertion order). Bounded by
|
|
190
|
+
* MAX_SESSION_BUBBLES so a burst of sessions cannot grow it without bound;
|
|
191
|
+
* disposed sessions are removed by the 'session/disposed' listener.
|
|
192
|
+
*/
|
|
193
|
+
private readonly sessionActivity = new Map<Session, SessionActivity>()
|
|
151
194
|
|
|
152
195
|
constructor(ctx: Context, config: PetConfig = {}) {
|
|
153
196
|
super(ctx, 'pet')
|
|
@@ -166,12 +209,15 @@ export class PetService extends Service {
|
|
|
166
209
|
// copied pet.json): fall back to the registry default.
|
|
167
210
|
persist = { ...persist, petId: this.registry.defaultEntry().id }
|
|
168
211
|
}
|
|
169
|
-
const
|
|
212
|
+
const selected = this.registry.byId(persist.petId) ?? this.registry.defaultEntry()
|
|
213
|
+
const ledgerConfig: LedgerConfig = {
|
|
214
|
+
affinity: config.affinity,
|
|
215
|
+
treats: config.treats,
|
|
216
|
+
remarks: selected.remarks,
|
|
217
|
+
}
|
|
170
218
|
this.ledger = new PetLedger(persist, ledgerConfig)
|
|
171
|
-
this.
|
|
172
|
-
|
|
173
|
-
...(config.state ?? {}),
|
|
174
|
-
})
|
|
219
|
+
this.stateConfig = { ...defaultPetStateConfig, ...(config.state ?? {}) }
|
|
220
|
+
this.machine = new PetStateMachine(this.stateConfig)
|
|
175
221
|
this.enabled = config.enabled ?? true
|
|
176
222
|
|
|
177
223
|
this.syncActivity()
|
|
@@ -224,6 +270,7 @@ export class PetService extends Service {
|
|
|
224
270
|
const entry = this.registry.byId(petId)
|
|
225
271
|
if (entry === undefined) return { ok: false, error: 'unknown-pet' }
|
|
226
272
|
this.ledger.setPetId(entry.id)
|
|
273
|
+
this.ledger.setRemarks(entry.remarks)
|
|
227
274
|
this.flush()
|
|
228
275
|
this.syncSettingsFromPet()
|
|
229
276
|
return { ok: true, petId: entry.id }
|
|
@@ -244,7 +291,7 @@ export class PetService extends Service {
|
|
|
244
291
|
this.disposeActivity = (() => {
|
|
245
292
|
const disposers = [
|
|
246
293
|
this.ctx.on('session/event', (session: Session, event: SessionEvent) => {
|
|
247
|
-
const runtime = this.
|
|
294
|
+
const runtime = this.activityOf(session).runtime
|
|
248
295
|
// `activity/status` is an optional compatibility input. It is not
|
|
249
296
|
// declared as a durable event type by this package because current
|
|
250
297
|
// Harness installations publish the official session vocabulary.
|
|
@@ -274,27 +321,60 @@ export class PetService extends Service {
|
|
|
274
321
|
}
|
|
275
322
|
}),
|
|
276
323
|
this.ctx.on('session/disposed', (session: Session) => {
|
|
324
|
+
this.ledger.forgetSession(String(session.id))
|
|
325
|
+
this.sessionActivity.delete(session)
|
|
277
326
|
if (session !== this.displaySession) return
|
|
327
|
+
// The display session is gone: fall back to the most recent
|
|
328
|
+
// remaining session's last input, or settle to idle when none.
|
|
278
329
|
this.displaySession = undefined
|
|
279
|
-
this.
|
|
330
|
+
const remaining = [...this.sessionActivity.entries()].at(-1)
|
|
331
|
+
if (remaining !== undefined) {
|
|
332
|
+
const [nextSession, activity] = remaining
|
|
333
|
+
this.displaySession = nextSession
|
|
334
|
+
if (activity.lastInput !== undefined) this.machine.onActivityStatus(activity.lastInput)
|
|
335
|
+
this.machine.onSessionActive()
|
|
336
|
+
} else {
|
|
337
|
+
this.machine.onSessionDisposed()
|
|
338
|
+
}
|
|
280
339
|
}),
|
|
281
340
|
]
|
|
282
341
|
return () => { for (const dispose of disposers) dispose() }
|
|
283
342
|
})()
|
|
284
343
|
}
|
|
285
344
|
|
|
286
|
-
/** Return the
|
|
287
|
-
private
|
|
288
|
-
let
|
|
289
|
-
if (
|
|
290
|
-
|
|
291
|
-
|
|
345
|
+
/** Return the per-session activity record, creating it on first sight. */
|
|
346
|
+
private activityOf(session: Session): SessionActivity {
|
|
347
|
+
let activity = this.sessionActivity.get(session)
|
|
348
|
+
if (activity === undefined) {
|
|
349
|
+
activity = {
|
|
350
|
+
runtime: emptyProjectionRuntime(),
|
|
351
|
+
machine: new PetStateMachine(this.stateConfig),
|
|
352
|
+
}
|
|
353
|
+
this.sessionActivity.set(session, activity)
|
|
292
354
|
}
|
|
293
|
-
return
|
|
355
|
+
return activity
|
|
294
356
|
}
|
|
295
357
|
|
|
296
|
-
/**
|
|
358
|
+
/**
|
|
359
|
+
* Commit one activity: the session's own machine renders its bubble, and
|
|
360
|
+
* the session becomes the host-global display session (most recent
|
|
361
|
+
* meaningful event wins the sprite animation).
|
|
362
|
+
*/
|
|
297
363
|
private applyActivity(session: Session, input: PetStateInput): void {
|
|
364
|
+
const activity = this.activityOf(session)
|
|
365
|
+
activity.lastInput = input
|
|
366
|
+
activity.machine.onActivityStatus(input)
|
|
367
|
+
activity.machine.onSessionActive()
|
|
368
|
+
// Move to the tail so map order reads most-recent-last, then trim the
|
|
369
|
+
// oldest session states beyond the bubble cap. The display session is
|
|
370
|
+
// reassigned below, so trimming its stale predecessor is safe.
|
|
371
|
+
this.sessionActivity.delete(session)
|
|
372
|
+
this.sessionActivity.set(session, activity)
|
|
373
|
+
while (this.sessionActivity.size > MAX_SESSION_BUBBLES) {
|
|
374
|
+
const oldest = this.sessionActivity.keys().next().value
|
|
375
|
+
if (oldest === undefined) break
|
|
376
|
+
this.sessionActivity.delete(oldest)
|
|
377
|
+
}
|
|
298
378
|
this.displaySession = session
|
|
299
379
|
this.machine.onActivityStatus(input)
|
|
300
380
|
this.machine.onSessionActive()
|
|
@@ -345,8 +425,10 @@ export class PetService extends Service {
|
|
|
345
425
|
* @param section - the resolved settings section.
|
|
346
426
|
*/
|
|
347
427
|
applySettingsSection(section: PetSettingsSection): void {
|
|
348
|
-
|
|
349
|
-
|
|
428
|
+
const selected = typeof section.petId === 'string' ? this.registry.byId(section.petId) : undefined
|
|
429
|
+
if (selected !== undefined) {
|
|
430
|
+
this.ledger.setPetId(selected.id)
|
|
431
|
+
this.ledger.setRemarks(selected.remarks)
|
|
350
432
|
} else if (section.petId !== undefined) {
|
|
351
433
|
// The stored selection names a pet the registry no longer has: keep the
|
|
352
434
|
// current selection and repair the settings document.
|
|
@@ -390,6 +472,21 @@ export class PetService extends Service {
|
|
|
390
472
|
private view(): PetStateView {
|
|
391
473
|
const snapshot = this.machine.render()
|
|
392
474
|
const entry = this.activeEntry()
|
|
475
|
+
// One bubble per concurrently active session, most recent first.
|
|
476
|
+
// Sessions whose own machine has settled (no bubble copy) drop out, so a
|
|
477
|
+
// finished turn does not leave a stale bubble behind.
|
|
478
|
+
const sessions: PetSessionView[] = []
|
|
479
|
+
for (const [session, activity] of [...this.sessionActivity.entries()].reverse()) {
|
|
480
|
+
if (sessions.length >= MAX_SESSION_BUBBLES) break
|
|
481
|
+
const perSession = activity.machine.render()
|
|
482
|
+
if (perSession.bubble === undefined) continue
|
|
483
|
+
sessions.push({
|
|
484
|
+
sessionId: String(session.id),
|
|
485
|
+
animation: perSession.animation,
|
|
486
|
+
bubble: perSession.bubble,
|
|
487
|
+
phase: perSession.phase,
|
|
488
|
+
})
|
|
489
|
+
}
|
|
393
490
|
// Read-only: the ledger settles on economic events only, never on a read,
|
|
394
491
|
// so polling the state cannot trigger pet.json writes.
|
|
395
492
|
return {
|
|
@@ -397,6 +494,7 @@ export class PetService extends Service {
|
|
|
397
494
|
...(snapshot.bubble === undefined ? {} : { bubble: snapshot.bubble }),
|
|
398
495
|
phase: snapshot.phase,
|
|
399
496
|
sessionActive: snapshot.sessionActive,
|
|
497
|
+
sessions,
|
|
400
498
|
affinity: this.ledger.affinityView(Date.now()),
|
|
401
499
|
display: { ...this.ledger.snapshot.display },
|
|
402
500
|
pet: {
|
package/src/state.test.ts
CHANGED
|
@@ -43,6 +43,15 @@ describe('PetStateMachine', () => {
|
|
|
43
43
|
expect(machine.render().bubble).toBeUndefined()
|
|
44
44
|
})
|
|
45
45
|
|
|
46
|
+
it('never bubbles a settled idle session, even with a projected line', () => {
|
|
47
|
+
// An aborted turn projects idle + '已停止'; a stopped session must not
|
|
48
|
+
// linger in the bubble stack.
|
|
49
|
+
const machine = new PetStateMachine(defaultPetStateConfig, () => 1_000)
|
|
50
|
+
machine.onActivityStatus({ phase: 'idle', line: '已停止' })
|
|
51
|
+
expect(machine.render().bubble).toBeUndefined()
|
|
52
|
+
expect(machine.render()).toMatchObject({ animation: 'idle', phase: 'idle' })
|
|
53
|
+
})
|
|
54
|
+
|
|
46
55
|
it('resets on session dispose', () => {
|
|
47
56
|
const machine = new PetStateMachine(defaultPetStateConfig, () => 1_000)
|
|
48
57
|
machine.onSessionActive()
|
package/src/state.ts
CHANGED
|
@@ -144,11 +144,11 @@ export class PetStateMachine {
|
|
|
144
144
|
animation = 'idle'
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
147
|
+
// Settled sessions never bubble: idle (e.g. an aborted/stopped turn)
|
|
148
|
+
// and done past its celebration window both fall silent.
|
|
149
|
+
const settled = this.phase === 'idle'
|
|
150
|
+
|| (this.phase === 'done' && this.doneAt !== undefined && nowMs - this.doneAt >= this.config.celebrateMs)
|
|
151
|
+
const bubble = settled ? undefined : this.phrase ?? this.line
|
|
152
152
|
return {
|
|
153
153
|
animation,
|
|
154
154
|
...(bubble === undefined ? {} : { bubble }),
|
package/src/treats.test.ts
CHANGED
|
@@ -7,20 +7,22 @@ import {
|
|
|
7
7
|
} from './treats.ts'
|
|
8
8
|
|
|
9
9
|
describe('settleTreatGrants', () => {
|
|
10
|
-
it('grants one treat per
|
|
10
|
+
it('grants one treat per 30 completed turns (10x difficulty)', () => {
|
|
11
11
|
const ledger = { ...emptyTreatLedger(), turnsAtLastTreatGrant: 0 }
|
|
12
|
-
const s = settleTreatGrants(ledger,
|
|
12
|
+
const s = settleTreatGrants(ledger, 60, 1_000, defaultTreatConfig)
|
|
13
13
|
expect(s.gained).toBe(2)
|
|
14
14
|
expect(s.ledger.treats).toBe(2)
|
|
15
|
-
expect(s.ledger.turnsAtLastTreatGrant).toBe(
|
|
15
|
+
expect(s.ledger.turnsAtLastTreatGrant).toBe(60)
|
|
16
|
+
// 29 turns are no longer enough for a treat.
|
|
17
|
+
expect(settleTreatGrants(emptyTreatLedger(), 29, 1_000, defaultTreatConfig).gained).toBe(0)
|
|
16
18
|
})
|
|
17
19
|
|
|
18
|
-
it('grants time-output treats per
|
|
20
|
+
it('grants time-output treats per 300-minute period (10x difficulty)', () => {
|
|
19
21
|
const ledger = { ...emptyTreatLedger(), lastTreatGrantAt: 1_000 }
|
|
20
|
-
const s = settleTreatGrants(ledger, 0, 1_000 +
|
|
22
|
+
const s = settleTreatGrants(ledger, 0, 1_000 + 900 * 60_000, defaultTreatConfig)
|
|
21
23
|
expect(s.gained).toBe(3)
|
|
22
24
|
expect(s.ledger.treats).toBe(3)
|
|
23
|
-
expect(s.ledger.lastTreatGrantAt).toBe(1_000 +
|
|
25
|
+
expect(s.ledger.lastTreatGrantAt).toBe(1_000 + 900 * 60_000)
|
|
24
26
|
})
|
|
25
27
|
|
|
26
28
|
it('does not backfill time output before the first settlement, but starts the clock', () => {
|
|
@@ -55,7 +57,7 @@ describe('settleTreatGrants', () => {
|
|
|
55
57
|
|
|
56
58
|
it('caps stocked treats at maxTreats', () => {
|
|
57
59
|
const ledger = { ...emptyTreatLedger(), treats: 19, lastTreatGrantAt: 1_000, turnsAtLastTreatGrant: 0 }
|
|
58
|
-
const s = settleTreatGrants(ledger,
|
|
60
|
+
const s = settleTreatGrants(ledger, 60, 1_000 + 30 * 60_000, defaultTreatConfig)
|
|
59
61
|
expect(s.ledger.treats).toBe(defaultTreatConfig.maxTreats)
|
|
60
62
|
})
|
|
61
63
|
|
|
@@ -73,17 +75,17 @@ describe('settleTreatGrants', () => {
|
|
|
73
75
|
})
|
|
74
76
|
|
|
75
77
|
it('a continuously working user still earns time treats (work does not reset the time anchor)', () => {
|
|
76
|
-
// First settlement:
|
|
78
|
+
// First settlement: 30 completed turns grant one work treat and start the
|
|
77
79
|
// time clock without touching the work anchor's independence.
|
|
78
80
|
let ledger = emptyTreatLedger()
|
|
79
|
-
const first = settleTreatGrants(ledger,
|
|
81
|
+
const first = settleTreatGrants(ledger, 30, 1_000, defaultTreatConfig)
|
|
80
82
|
expect(first.gained).toBe(1)
|
|
81
83
|
ledger = first.ledger
|
|
82
|
-
// Keep working in
|
|
84
|
+
// Keep working in 30-turn steps well under one time period: every work
|
|
83
85
|
// settlement must advance only the turn anchor, never the time anchor.
|
|
84
86
|
const stepMs = defaultTreatConfig.timeTreatMs / 100
|
|
85
87
|
for (let i = 0; i < 10; i++) {
|
|
86
|
-
const s = settleTreatGrants(ledger,
|
|
88
|
+
const s = settleTreatGrants(ledger, 30 + ((i + 1) * 30), 1_000 + ((i + 1) * stepMs), defaultTreatConfig)
|
|
87
89
|
expect(s.gained).toBe(1)
|
|
88
90
|
expect(s.ledger.lastTreatGrantAt).toBe(1_000)
|
|
89
91
|
ledger = s.ledger
|
package/src/treats.ts
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
* two sources, both tied to companionship:
|
|
4
4
|
* - work output: every N completed turns grant one treat;
|
|
5
5
|
* - time output: every T minutes of wall-clock time grant one treat.
|
|
6
|
+
* Difficulty tuned deliberately high (10x the original cadence): treats are
|
|
7
|
+
* a rare delicacy the pet earns through sustained companionship, not a
|
|
8
|
+
* routine drop.
|
|
6
9
|
* Feeding consumes one treat. Settlement is lazy: it runs whenever the host
|
|
7
10
|
* serves a state snapshot or an interaction, so there is no timer and no
|
|
8
11
|
* drift — elapsed periods are computed from the persisted last-grant marks.
|
|
@@ -20,8 +23,8 @@ export interface TreatConfig {
|
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
export const defaultTreatConfig: TreatConfig = {
|
|
23
|
-
turnsPerTreat:
|
|
24
|
-
timeTreatMs:
|
|
26
|
+
turnsPerTreat: 30,
|
|
27
|
+
timeTreatMs: 300 * 60_000,
|
|
25
28
|
maxTreats: 20,
|
|
26
29
|
}
|
|
27
30
|
|
package/lib/state-CFyJv0sQ.js
DELETED
|
@@ -1,213 +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
|
-
* Marker glyphs are plain ASCII (the repo bans all emoji characters);
|
|
5
|
-
* they read as a growing star trail alongside the rank name. */
|
|
6
|
-
const AFFINITY_RANKS = [
|
|
7
|
-
{
|
|
8
|
-
min: 0,
|
|
9
|
-
name: "幼鲸",
|
|
10
|
-
emoji: "*"
|
|
11
|
-
},
|
|
12
|
-
{
|
|
13
|
-
min: 25,
|
|
14
|
-
name: "伙伴",
|
|
15
|
-
emoji: "**"
|
|
16
|
-
},
|
|
17
|
-
{
|
|
18
|
-
min: 50,
|
|
19
|
-
name: "挚友",
|
|
20
|
-
emoji: "***"
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
min: 80,
|
|
24
|
-
name: "深海羁绊",
|
|
25
|
-
emoji: "****"
|
|
26
|
-
}
|
|
27
|
-
];
|
|
28
|
-
const defaultAffinityConfig = {
|
|
29
|
-
turnReward: 1,
|
|
30
|
-
petReward: 1,
|
|
31
|
-
petCooldownMs: 1e4,
|
|
32
|
-
feedReward: 5,
|
|
33
|
-
feedCooldownMs: 3e4
|
|
34
|
-
};
|
|
35
|
-
function emptyAffinity() {
|
|
36
|
-
return {
|
|
37
|
-
points: 0,
|
|
38
|
-
lastPetAt: 0,
|
|
39
|
-
lastFeedAt: 0,
|
|
40
|
-
pets: 0,
|
|
41
|
-
feeds: 0,
|
|
42
|
-
turns: 0
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
/** Rank for a point total. */
|
|
46
|
-
function rankOf(points) {
|
|
47
|
-
let rank = AFFINITY_RANKS[0];
|
|
48
|
-
for (const candidate of AFFINITY_RANKS) if (points >= candidate.min) rank = candidate;
|
|
49
|
-
return rank;
|
|
50
|
-
}
|
|
51
|
-
/** Derive the read-only view of one affinity state at a wall-clock instant. */
|
|
52
|
-
function affinityViewOf(state, nowMs, config = defaultAffinityConfig) {
|
|
53
|
-
const rank = rankOf(state.points);
|
|
54
|
-
return {
|
|
55
|
-
points: state.points,
|
|
56
|
-
rank: rank.name,
|
|
57
|
-
rankEmoji: rank.emoji,
|
|
58
|
-
pets: state.pets,
|
|
59
|
-
feeds: state.feeds,
|
|
60
|
-
turns: state.turns,
|
|
61
|
-
petCooldown: nowMs - state.lastPetAt < config.petCooldownMs,
|
|
62
|
-
feedCooldown: nowMs - state.lastFeedAt < config.feedCooldownMs
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
function clamp(points) {
|
|
66
|
-
return Math.min(100, Math.max(0, points));
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Apply one interaction to a copy of the state (immutable style: returns a
|
|
70
|
-
* new object; the caller replaces the persisted state). Cooldowns only
|
|
71
|
-
* apply once the pet has been interacted with at least once (last*At === 0
|
|
72
|
-
* means "never", so the first pet/feed always lands).
|
|
73
|
-
*/
|
|
74
|
-
function applyInteraction(state, kind, nowMs, config = defaultAffinityConfig) {
|
|
75
|
-
const next = { ...state };
|
|
76
|
-
if (kind === "pet") {
|
|
77
|
-
if (state.lastPetAt !== 0 && nowMs - state.lastPetAt < config.petCooldownMs) return {
|
|
78
|
-
affinity: state,
|
|
79
|
-
delta: 0,
|
|
80
|
-
reaction: "摸过头啦,让鲸鱼娘歇口气~",
|
|
81
|
-
accepted: false
|
|
82
|
-
};
|
|
83
|
-
next.lastPetAt = nowMs;
|
|
84
|
-
next.pets += 1;
|
|
85
|
-
next.points = clamp(state.points + config.petReward);
|
|
86
|
-
return {
|
|
87
|
-
affinity: next,
|
|
88
|
-
delta: config.petReward,
|
|
89
|
-
reaction: "咕噜咕噜~被摸摸好舒服!",
|
|
90
|
-
accepted: true
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
if (kind === "feed") {
|
|
94
|
-
if (state.lastFeedAt !== 0 && nowMs - state.lastFeedAt < config.feedCooldownMs) return {
|
|
95
|
-
affinity: state,
|
|
96
|
-
delta: 0,
|
|
97
|
-
reaction: "吃饱啦,晚点再喂~",
|
|
98
|
-
accepted: false
|
|
99
|
-
};
|
|
100
|
-
next.lastFeedAt = nowMs;
|
|
101
|
-
next.feeds += 1;
|
|
102
|
-
next.points = clamp(state.points + config.feedReward);
|
|
103
|
-
return {
|
|
104
|
-
affinity: next,
|
|
105
|
-
delta: config.feedReward,
|
|
106
|
-
reaction: "呜哇!小鱼干好好吃!",
|
|
107
|
-
accepted: true
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
return {
|
|
111
|
-
affinity: state,
|
|
112
|
-
delta: 0,
|
|
113
|
-
reaction: "",
|
|
114
|
-
accepted: false
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
/** Reward one completed turn (called by the host on `done`). */
|
|
118
|
-
function applyTurnReward(state, config = defaultAffinityConfig) {
|
|
119
|
-
const next = { ...state };
|
|
120
|
-
next.turns += 1;
|
|
121
|
-
next.points = clamp(state.points + config.turnReward);
|
|
122
|
-
return next;
|
|
123
|
-
}
|
|
124
|
-
//#endregion
|
|
125
|
-
//#region src/state.ts
|
|
126
|
-
const defaultPetStateConfig = { celebrateMs: 2400 };
|
|
127
|
-
/**
|
|
128
|
-
* Map one activity phase onto the animation contract.
|
|
129
|
-
* - thinking → `running` and tool → `running-right` (focused work).
|
|
130
|
-
* - review → `review` while answer text is streaming.
|
|
131
|
-
* - waiting → `waiting` (expectant pose, needs user input).
|
|
132
|
-
* - done → `jumping` (celebration), then back to `idle` after the window.
|
|
133
|
-
* - failed → `failed` until another activity event arrives.
|
|
134
|
-
* - idle → `idle` (calm breathing loop).
|
|
135
|
-
*/
|
|
136
|
-
function animationForPhase(phase) {
|
|
137
|
-
switch (phase) {
|
|
138
|
-
case "thinking": return "running";
|
|
139
|
-
case "tool": return "running-right";
|
|
140
|
-
case "review": return "review";
|
|
141
|
-
case "waiting": return "waiting";
|
|
142
|
-
case "done": return "jumping";
|
|
143
|
-
case "failed": return "failed";
|
|
144
|
-
case "idle": return "idle";
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
/** The spritesheet row index for one animation track. */
|
|
148
|
-
function rowOf(animation) {
|
|
149
|
-
return {
|
|
150
|
-
"idle": 0,
|
|
151
|
-
"running-right": 1,
|
|
152
|
-
"running-left": 2,
|
|
153
|
-
"waving": 3,
|
|
154
|
-
"jumping": 4,
|
|
155
|
-
"failed": 5,
|
|
156
|
-
"waiting": 6,
|
|
157
|
-
"running": 7,
|
|
158
|
-
"review": 8
|
|
159
|
-
}[animation];
|
|
160
|
-
}
|
|
161
|
-
/**
|
|
162
|
-
* PetStateMachine — one instance per host process. Holds only the latest
|
|
163
|
-
* input snapshot and the celebration timing; no storage, no side effects.
|
|
164
|
-
*/
|
|
165
|
-
var PetStateMachine = class {
|
|
166
|
-
config;
|
|
167
|
-
now;
|
|
168
|
-
phase = "idle";
|
|
169
|
-
line;
|
|
170
|
-
phrase;
|
|
171
|
-
sessionActive = false;
|
|
172
|
-
doneAt;
|
|
173
|
-
constructor(config = defaultPetStateConfig, now = Date.now) {
|
|
174
|
-
this.config = config;
|
|
175
|
-
this.now = now;
|
|
176
|
-
}
|
|
177
|
-
/** Consume one projected activity update. */
|
|
178
|
-
onActivityStatus(input) {
|
|
179
|
-
this.phase = input.phase;
|
|
180
|
-
this.line = input.line;
|
|
181
|
-
this.phrase = input.phrase;
|
|
182
|
-
if (input.phase === "done") this.doneAt = this.now();
|
|
183
|
-
}
|
|
184
|
-
/** A session became the active one (or a fresh session started). */
|
|
185
|
-
onSessionActive() {
|
|
186
|
-
this.sessionActive = true;
|
|
187
|
-
}
|
|
188
|
-
/** The active session was disposed (or none left). */
|
|
189
|
-
onSessionDisposed() {
|
|
190
|
-
this.sessionActive = false;
|
|
191
|
-
this.phase = "idle";
|
|
192
|
-
this.line = void 0;
|
|
193
|
-
this.phrase = void 0;
|
|
194
|
-
this.doneAt = void 0;
|
|
195
|
-
}
|
|
196
|
-
/** Render the current animation decision. */
|
|
197
|
-
render() {
|
|
198
|
-
const nowMs = this.now();
|
|
199
|
-
let animation = animationForPhase(this.phase);
|
|
200
|
-
if (this.phase === "done" && this.doneAt !== void 0) if (nowMs - this.doneAt < this.config.celebrateMs) animation = "jumping";
|
|
201
|
-
else animation = "idle";
|
|
202
|
-
const bubble = this.phase === "done" && this.doneAt !== void 0 && nowMs - this.doneAt >= this.config.celebrateMs ? void 0 : this.phrase ?? this.line;
|
|
203
|
-
return {
|
|
204
|
-
animation,
|
|
205
|
-
...bubble === void 0 ? {} : { bubble },
|
|
206
|
-
animationStartedAt: nowMs,
|
|
207
|
-
phase: this.phase,
|
|
208
|
-
sessionActive: this.sessionActive
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
};
|
|
212
|
-
//#endregion
|
|
213
|
-
export { AFFINITY_MAX as a, applyInteraction as c, emptyAffinity as d, rankOf as f, rowOf as i, applyTurnReward as l, animationForPhase as n, AFFINITY_RANKS as o, defaultPetStateConfig as r, affinityViewOf as s, PetStateMachine as t, defaultAffinityConfig as u };
|