@linxin666/dsh-pet 0.1.14 → 0.1.16
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 +73 -38
- package/README.zh.md +91 -55
- package/assets/whale/pet.json +12 -1
- package/cordis.patch.yml +4 -4
- package/lib/client.js +296 -347
- package/lib/client.js.map +1 -1
- package/lib/index.js +975 -317
- package/lib/invariant.js +1 -1
- package/lib/{state-P-wsVJ6O.js → state-CFyJv0sQ.js} +15 -1
- package/lib/types/affinity.d.ts +15 -0
- package/lib/types/affinity.d.ts.map +1 -1
- package/lib/types/affinity.js +14 -0
- package/lib/types/client/PetDockEntry.d.ts +14 -12
- package/lib/types/client/PetDockEntry.d.ts.map +1 -1
- package/lib/types/client/PetDockEntry.js +13 -8
- package/lib/types/client/PetSettingsCard.d.ts +22 -9
- package/lib/types/client/PetSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PetSettingsCard.js +42 -7
- package/lib/types/client/{WhalePet.d.ts → PetSprite.d.ts} +21 -20
- package/lib/types/client/PetSprite.d.ts.map +1 -0
- package/lib/types/client/{WhalePet.js → PetSprite.js} +44 -67
- package/lib/types/client/PluginSettingsCard.d.ts +26 -11
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +30 -8
- package/lib/types/client/index.d.ts +11 -8
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +32 -14
- package/lib/types/client/locales.d.ts +10 -10
- package/lib/types/client/locales.js +10 -10
- package/lib/types/client/pet-store.d.ts +5 -0
- package/lib/types/client/pet-store.d.ts.map +1 -1
- package/lib/types/client/pet-store.js +4 -0
- package/lib/types/client/settings-form.d.ts +76 -5
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +97 -20
- package/lib/types/client/spritesheet.d.ts +14 -48
- package/lib/types/client/spritesheet.d.ts.map +1 -1
- package/lib/types/client/spritesheet.js +13 -81
- package/lib/types/dsh-home.d.ts +17 -0
- package/lib/types/dsh-home.d.ts.map +1 -0
- package/lib/types/dsh-home.js +34 -0
- package/lib/types/event-projection.d.ts +36 -0
- package/lib/types/event-projection.d.ts.map +1 -0
- package/lib/types/event-projection.js +98 -0
- package/lib/types/index.d.ts +24 -11
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +53 -33
- package/lib/types/ledger.d.ts +79 -0
- package/lib/types/ledger.d.ts.map +1 -0
- package/lib/types/ledger.js +146 -0
- package/lib/types/persist.d.ts +15 -4
- package/lib/types/persist.d.ts.map +1 -1
- package/lib/types/persist.js +39 -8
- package/lib/types/registry.d.ts +158 -0
- package/lib/types/registry.d.ts.map +1 -0
- package/lib/types/registry.js +276 -0
- package/lib/types/routes.d.ts +10 -10
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +169 -51
- package/lib/types/service.d.ts +59 -57
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js +111 -211
- package/package.json +2 -2
- package/src/affinity.ts +33 -0
- package/src/client/PetDockEntry.tsx +34 -26
- package/src/client/PetSettingsCard.tsx +67 -22
- package/src/client/{WhalePet.test.tsx → PetSprite.test.tsx} +56 -8
- package/src/client/{WhalePet.tsx → PetSprite.tsx} +55 -73
- package/src/client/PluginSettingsCard.tsx +90 -18
- package/src/client/index.ts +38 -16
- package/src/client/locales.ts +10 -10
- package/src/client/pet-store.ts +9 -0
- package/src/client/settings-card.module.css +9 -0
- package/src/client/settings-form.ts +153 -19
- package/src/client/spritesheet.ts +16 -92
- package/src/dsh-home.test.ts +35 -0
- package/src/dsh-home.ts +36 -0
- package/src/event-projection.ts +128 -0
- package/src/index.test.ts +23 -0
- package/src/index.ts +89 -45
- package/src/ledger.test.ts +69 -0
- package/src/ledger.ts +190 -0
- package/src/persist.test.ts +52 -3
- package/src/persist.ts +49 -11
- package/src/registry.test.ts +132 -0
- package/src/registry.ts +400 -0
- package/src/routes.ts +166 -53
- package/src/service.ts +153 -285
- package/lib/types/client/WhalePet.d.ts.map +0 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-pet",
|
|
3
|
-
"description": "
|
|
4
|
-
"version": "0.1.
|
|
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.1.16",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
package/src/affinity.ts
CHANGED
|
@@ -84,6 +84,39 @@ export function rankOf(points: number): (typeof AFFINITY_RANKS)[number] {
|
|
|
84
84
|
return rank
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
/** Read-only affinity snapshot suited for the RPC view shape. */
|
|
88
|
+
export interface PetAffinityView {
|
|
89
|
+
points: number
|
|
90
|
+
rank: string
|
|
91
|
+
rankEmoji: string
|
|
92
|
+
pets: number
|
|
93
|
+
feeds: number
|
|
94
|
+
turns: number
|
|
95
|
+
/** True while the pet interaction is inside its cooldown. */
|
|
96
|
+
petCooldown: boolean
|
|
97
|
+
/** True while the feed is inside its cooldown. */
|
|
98
|
+
feedCooldown: boolean
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Derive the read-only view of one affinity state at a wall-clock instant. */
|
|
102
|
+
export function affinityViewOf(
|
|
103
|
+
state: AffinityState,
|
|
104
|
+
nowMs: number,
|
|
105
|
+
config: AffinityConfig = defaultAffinityConfig,
|
|
106
|
+
): PetAffinityView {
|
|
107
|
+
const rank = rankOf(state.points)
|
|
108
|
+
return {
|
|
109
|
+
points: state.points,
|
|
110
|
+
rank: rank.name,
|
|
111
|
+
rankEmoji: rank.emoji,
|
|
112
|
+
pets: state.pets,
|
|
113
|
+
feeds: state.feeds,
|
|
114
|
+
turns: state.turns,
|
|
115
|
+
petCooldown: nowMs - state.lastPetAt < config.petCooldownMs,
|
|
116
|
+
feedCooldown: nowMs - state.lastFeedAt < config.feedCooldownMs,
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
87
120
|
function clamp(points: number): number {
|
|
88
121
|
return Math.min(AFFINITY_MAX, Math.max(0, points))
|
|
89
122
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Global floating pet entry. The pet is host-global (its state, display and
|
|
3
|
-
* interactions live on
|
|
3
|
+
* interactions live on '/api/pet/*' endpoints with no session dimension), so
|
|
4
4
|
* it must not ride a session-scoped slot — on the new-conversation screen no
|
|
5
5
|
* session exists to scope a slot by, and the pet would vanish (issue #48).
|
|
6
|
-
* The client half therefore mounts this entry straight onto
|
|
7
|
-
* (see index.ts): while visible it renders the floating
|
|
8
|
-
* while hidden it renders a fixed-position summon button.
|
|
6
|
+
* The client half therefore mounts this entry straight onto 'document.body'
|
|
7
|
+
* (see index.ts): while visible it renders the floating PetSprite (a
|
|
8
|
+
* portal), while hidden it renders a fixed-position summon button. Which
|
|
9
|
+
* sprite renders is decided by the host snapshot's pet id resolved against
|
|
10
|
+
* the registry list — no per-pet component exists.
|
|
9
11
|
* @module @linxin666/dsh-pet/client/PetDockEntry
|
|
10
12
|
*/
|
|
11
13
|
|
|
@@ -13,27 +15,27 @@ import { useEffect, useSyncExternalStore, type ReactElement } from 'react'
|
|
|
13
15
|
import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'
|
|
14
16
|
import type { PetDisplayConfig } from '../persist.ts'
|
|
15
17
|
import type { PetStoreInstance } from './pet-store.ts'
|
|
16
|
-
import {
|
|
18
|
+
import { PetSprite } from './PetSprite.tsx'
|
|
17
19
|
import { NS } from './locales.ts'
|
|
18
20
|
import styles from './pet.module.css'
|
|
19
21
|
|
|
20
22
|
/** Injected actions handed to the dock entry component. */
|
|
21
23
|
export interface PetInjected {
|
|
22
|
-
/** The app-wide pet store instance (snapshot + feedback). */
|
|
24
|
+
/** The app-wide pet store instance (snapshot + registry list + feedback). */
|
|
23
25
|
store: PetStoreInstance
|
|
24
|
-
/** Ensure the first snapshot is fetched (called on mount). */
|
|
26
|
+
/** Ensure the first snapshot (and registry list) is fetched (called on mount). */
|
|
25
27
|
ensure: () => void
|
|
26
|
-
/** Pet the
|
|
28
|
+
/** Pet the sprite (click). */
|
|
27
29
|
pet: () => void
|
|
28
|
-
/** Feed the
|
|
30
|
+
/** Feed the sprite. */
|
|
29
31
|
feed: () => void
|
|
30
|
-
/** Hide the
|
|
32
|
+
/** Hide the sprite. */
|
|
31
33
|
hide: () => void
|
|
32
|
-
/** Summon the hidden
|
|
34
|
+
/** Summon the hidden sprite back. */
|
|
33
35
|
summon: () => void
|
|
34
36
|
/** Persist a drag position. */
|
|
35
37
|
dragEnd: (right: number, bottom: number) => void
|
|
36
|
-
/** Rename the pet (persisted by the host). */
|
|
38
|
+
/** Rename the selected pet (persisted by the host). */
|
|
37
39
|
rename: (name: string) => void
|
|
38
40
|
/** Clear the reaction bubble. */
|
|
39
41
|
feedbackDone: () => void
|
|
@@ -47,7 +49,7 @@ export type PetDockEntryProps =
|
|
|
47
49
|
const DEFAULT_DISPLAY: PetDisplayConfig = { visible: true, size: 160, right: 24, bottom: 20 }
|
|
48
50
|
|
|
49
51
|
/**
|
|
50
|
-
* Dock entry: while the pet is visible, mount the floating
|
|
52
|
+
* Dock entry: while the pet is visible, mount the floating PetSprite (it
|
|
51
53
|
* portals itself onto document.body); while hidden, render the summon
|
|
52
54
|
* button so the pet can always come back. The store is the plugin-owned
|
|
53
55
|
* single instance — the slot system provides none because the pet is
|
|
@@ -58,6 +60,7 @@ export function PetDockEntry(props: PetDockEntryProps): ReactElement {
|
|
|
58
60
|
const ui = useSyncExternalStore(store.subscribe, store.getSnapshot)
|
|
59
61
|
const snapshot = ui.snapshot
|
|
60
62
|
const feedback = ui.feedback
|
|
63
|
+
const definition = ui.pets.find(entry => entry.id === snapshot?.pet.id) ?? null
|
|
61
64
|
const visible = snapshot?.display.visible ?? true
|
|
62
65
|
|
|
63
66
|
useEffect(() => {
|
|
@@ -67,18 +70,23 @@ export function PetDockEntry(props: PetDockEntryProps): ReactElement {
|
|
|
67
70
|
if (visible) {
|
|
68
71
|
return (
|
|
69
72
|
<span data-pet-dock data-testid="pet-dock">
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
73
|
+
{snapshot === null || definition === null
|
|
74
|
+
? null
|
|
75
|
+
: (
|
|
76
|
+
<PetSprite
|
|
77
|
+
snapshot={snapshot}
|
|
78
|
+
definition={definition}
|
|
79
|
+
display={snapshot.display}
|
|
80
|
+
feedback={feedback}
|
|
81
|
+
onPet={props.pet}
|
|
82
|
+
onFeed={props.feed}
|
|
83
|
+
onHide={props.hide}
|
|
84
|
+
onDragEnd={props.dragEnd}
|
|
85
|
+
onRename={props.rename}
|
|
86
|
+
onFeedbackDone={props.feedbackDone}
|
|
87
|
+
t={props.t}
|
|
88
|
+
/>
|
|
89
|
+
)}
|
|
82
90
|
</span>
|
|
83
91
|
)
|
|
84
92
|
}
|
|
@@ -96,7 +104,7 @@ export function PetDockEntry(props: PetDockEntryProps): ReactElement {
|
|
|
96
104
|
onClick={props.summon}
|
|
97
105
|
data-testid="pet-summon"
|
|
98
106
|
>
|
|
99
|
-
{props.t('pet.summon', { name: snapshot?.name ?? '
|
|
107
|
+
{props.t('pet.summon', { name: snapshot?.name ?? '' })}
|
|
100
108
|
</button>
|
|
101
109
|
)
|
|
102
110
|
}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The pet settings card: display layout
|
|
3
|
-
* namespace the host plugin registers. Registered into the
|
|
4
|
-
*
|
|
2
|
+
* The pet settings card: pet selection plus display layout, bound to the
|
|
3
|
+
* 'pet' settings namespace the host plugin registers. Registered into the
|
|
4
|
+
* 'web-ui.plugin.item' slot the plugin-configuration section renders. The
|
|
5
|
+
* petId choices come from the registry endpoint ('/api/pet/pets') — the same
|
|
6
|
+
* list the sprite renders from — so the card carries no per-pet knowledge.
|
|
5
7
|
*/
|
|
6
8
|
|
|
7
9
|
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
|
8
10
|
import type { SettingsScope, SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
|
9
|
-
import { PluginSettingsCard, ValueField, BooleanField } from './PluginSettingsCard.tsx'
|
|
10
|
-
import { CardForm, booleanField,
|
|
11
|
+
import { PluginSettingsCard, ValueField, BooleanField, ChoiceField } from './PluginSettingsCard.tsx'
|
|
12
|
+
import { CardForm, booleanField, choiceField, numberField, type CardActions, type CardShell, type FieldState as CardFieldState } from './settings-form.ts'
|
|
11
13
|
|
|
12
14
|
/** The pet's settings fields this card edits (the namespace's full schema). */
|
|
13
15
|
export interface PetSettings {
|
|
@@ -21,8 +23,8 @@ export interface PetSettings {
|
|
|
21
23
|
right?: number
|
|
22
24
|
/** Vertical inset from the viewport bottom edge, px. */
|
|
23
25
|
bottom?: number
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
+
/** Selected pet id (a registry entry). */
|
|
27
|
+
petId?: string
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
/** What the pet settings card renders. */
|
|
@@ -37,8 +39,10 @@ export interface PetSettingsCardState extends CardShell {
|
|
|
37
39
|
right: CardFieldState
|
|
38
40
|
/** Bottom inset. */
|
|
39
41
|
bottom: CardFieldState
|
|
40
|
-
/**
|
|
41
|
-
|
|
42
|
+
/** Selected pet. */
|
|
43
|
+
petId: CardFieldState
|
|
44
|
+
/** Pet choices (registry ids + display names), loaded from the host. */
|
|
45
|
+
petChoices: readonly { value: string; label: string }[]
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
/** The registration-side face the card's slot entry injects. */
|
|
@@ -49,12 +53,32 @@ export interface PetSettingsCardFace extends CardActions {
|
|
|
49
53
|
}
|
|
50
54
|
}
|
|
51
55
|
|
|
52
|
-
/**
|
|
56
|
+
/** One registry choice as served by '/api/pet/pets'. */
|
|
57
|
+
interface PetChoice {
|
|
58
|
+
id: string
|
|
59
|
+
displayName: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Fetch the registry list (the same data the sprite renders from). */
|
|
63
|
+
async function fetchPetChoices(): Promise<PetChoice[]> {
|
|
64
|
+
const response = await fetch('/api/pet/pets')
|
|
65
|
+
if (!response.ok) throw new Error('pet pets failed: ' + response.status)
|
|
66
|
+
return (await response.json()) as PetChoice[]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Bridges the 'pet' scope onto the card's staged form. */
|
|
53
70
|
export class PetSettingsCardController {
|
|
54
71
|
private readonly form: CardForm<PetSettings>
|
|
55
72
|
private readonly store: SnapshotStore<PetSettingsCardState>
|
|
73
|
+
// The choice list rides a mutable array shared with the choiceField spec,
|
|
74
|
+
// so loading the registry re-validates and re-formats the petId field
|
|
75
|
+
// without rebuilding the form.
|
|
76
|
+
private readonly petChoices: string[] = []
|
|
77
|
+
private readonly petLabels = new Map<string, string>()
|
|
78
|
+
private loaded = false
|
|
79
|
+
private attempts = 0
|
|
56
80
|
|
|
57
|
-
/** @param scope - the bound settings scope for the
|
|
81
|
+
/** @param scope - the bound settings scope for the 'pet' namespace. */
|
|
58
82
|
constructor(scope: SettingsScope<PetSettings>) {
|
|
59
83
|
this.form = new CardForm(scope, [
|
|
60
84
|
booleanField('enabled'),
|
|
@@ -62,9 +86,27 @@ export class PetSettingsCardController {
|
|
|
62
86
|
numberField('size'),
|
|
63
87
|
numberField('right'),
|
|
64
88
|
numberField('bottom'),
|
|
65
|
-
|
|
89
|
+
choiceField('petId', this.petChoices),
|
|
66
90
|
])
|
|
67
91
|
this.store = this.form.bind(() => this.projection())
|
|
92
|
+
void this.loadPets()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Resolve the registry choices once (retried a few times on failure). */
|
|
96
|
+
private async loadPets(): Promise<void> {
|
|
97
|
+
if (this.loaded) return
|
|
98
|
+
try {
|
|
99
|
+
const list = await fetchPetChoices()
|
|
100
|
+
this.petChoices.splice(0, this.petChoices.length, ...list.map(choice => choice.id))
|
|
101
|
+
for (const choice of list) this.petLabels.set(choice.id, choice.displayName)
|
|
102
|
+
this.loaded = true
|
|
103
|
+
this.store.set(this.projection())
|
|
104
|
+
} catch {
|
|
105
|
+
this.attempts += 1
|
|
106
|
+
if (this.attempts < 3) {
|
|
107
|
+
window.setTimeout(() => { void this.loadPets() }, 3000)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
68
110
|
}
|
|
69
111
|
|
|
70
112
|
private projection(): PetSettingsCardState {
|
|
@@ -75,7 +117,8 @@ export class PetSettingsCardController {
|
|
|
75
117
|
size: this.form.field('size'),
|
|
76
118
|
right: this.form.field('right'),
|
|
77
119
|
bottom: this.form.field('bottom'),
|
|
78
|
-
|
|
120
|
+
petId: this.form.field('petId'),
|
|
121
|
+
petChoices: this.petChoices.map(id => ({ value: id, label: this.petLabels.get(id) ?? id })),
|
|
79
122
|
}
|
|
80
123
|
}
|
|
81
124
|
|
|
@@ -130,6 +173,17 @@ export function PetSettingsCard(props: PetSettingsCardProps) {
|
|
|
130
173
|
onEdit={(text) => { props.edit('enabled', text) }}
|
|
131
174
|
onReset={() => { props.resetField('enabled') }}
|
|
132
175
|
/>
|
|
176
|
+
<ChoiceField
|
|
177
|
+
id="settings-pet-pet"
|
|
178
|
+
label={t('settings.pet')}
|
|
179
|
+
hint={t('settings.petHint')}
|
|
180
|
+
inheritLabel={t('settings.inherit')}
|
|
181
|
+
{...fieldProps}
|
|
182
|
+
{...state.petId}
|
|
183
|
+
choices={state.petChoices}
|
|
184
|
+
onEdit={(text) => { props.edit('petId', text) }}
|
|
185
|
+
onReset={() => { props.resetField('petId') }}
|
|
186
|
+
/>
|
|
133
187
|
<BooleanField
|
|
134
188
|
id="settings-pet-visible"
|
|
135
189
|
label={t('settings.visible')}
|
|
@@ -172,15 +226,6 @@ export function PetSettingsCard(props: PetSettingsCardProps) {
|
|
|
172
226
|
onEdit={(text) => { props.edit('bottom', text) }}
|
|
173
227
|
onReset={() => { props.resetField('bottom') }}
|
|
174
228
|
/>
|
|
175
|
-
<ValueField
|
|
176
|
-
id="settings-pet-name"
|
|
177
|
-
label={t('settings.name')}
|
|
178
|
-
hint={t('settings.nameHint')}
|
|
179
|
-
{...fieldProps}
|
|
180
|
-
{...state.name}
|
|
181
|
-
onEdit={(text) => { props.edit('name', text) }}
|
|
182
|
-
onReset={() => { props.resetField('name') }}
|
|
183
|
-
/>
|
|
184
229
|
</PluginSettingsCard>
|
|
185
230
|
)
|
|
186
231
|
}
|
|
@@ -1,14 +1,47 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* PetSprite rename-box keyboard handling. The rename input must treat
|
|
4
4
|
* Enter/Escape keydowns that arrive during IME composition (candidate
|
|
5
5
|
* selection) as composition input, never as submit/cancel (issue #89).
|
|
6
6
|
*/
|
|
7
7
|
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'
|
|
8
8
|
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
|
9
|
-
import {
|
|
9
|
+
import { PetSprite, type PetSpriteProps } from './PetSprite.tsx'
|
|
10
10
|
import { t } from './locales.ts'
|
|
11
11
|
import type { PetStateView } from '../service.ts'
|
|
12
|
+
import type { PetDefinition, PetTrackDef } from '../registry.ts'
|
|
13
|
+
import type { PetAnimation } from '../state.ts'
|
|
14
|
+
|
|
15
|
+
/** A minimal pet definition (geometry + tracks) as served by the host. */
|
|
16
|
+
function petDefinition(): PetDefinition {
|
|
17
|
+
const track = (frames: number[], durations: number[], loop = true, fallback?: PetAnimation): PetTrackDef => ({
|
|
18
|
+
frames,
|
|
19
|
+
durations,
|
|
20
|
+
loop,
|
|
21
|
+
...(fallback === undefined ? {} : { fallback }),
|
|
22
|
+
})
|
|
23
|
+
return {
|
|
24
|
+
id: 'whale-girl',
|
|
25
|
+
displayName: '鲸鱼娘',
|
|
26
|
+
description: '测试用鲸鱼娘',
|
|
27
|
+
cell: { width: 192, height: 208 },
|
|
28
|
+
columns: 8,
|
|
29
|
+
rows: [6, 8, 8, 4, 5, 8, 6, 6, 6],
|
|
30
|
+
tracks: {
|
|
31
|
+
idle: track([0, 1, 2, 3, 4, 5], [400, 400, 400, 400, 400, 400]),
|
|
32
|
+
'running-right': track([0, 1, 2, 3, 4, 5, 6, 7], [225, 225, 225, 225, 225, 225, 225, 225]),
|
|
33
|
+
'running-left': track([0, 1, 2, 3, 4, 5, 6, 7], [225, 225, 225, 225, 225, 225, 225, 225]),
|
|
34
|
+
waving: track([0, 1, 2, 3], [350, 350, 350, 350]),
|
|
35
|
+
jumping: track([0, 1, 2, 3, 4], [300, 300, 300, 300, 300], false, 'idle'),
|
|
36
|
+
failed: track([0, 1, 2, 3, 4, 5, 6, 7], [450, 450, 450, 450, 450, 450, 450, 450], false, 'idle'),
|
|
37
|
+
waiting: track([0, 1, 2, 3, 4, 5], [450, 450, 450, 450, 450, 450]),
|
|
38
|
+
running: track([0, 1, 2, 3, 4, 5], [250, 250, 250, 250, 250, 250]),
|
|
39
|
+
review: track([0, 1, 2, 3, 4, 5], [550, 550, 550, 550, 550, 550]),
|
|
40
|
+
},
|
|
41
|
+
atlasUrl: '/pet/whale-girl/spritesheet.webp',
|
|
42
|
+
manifestUrl: '/pet/whale-girl/pet.json',
|
|
43
|
+
}
|
|
44
|
+
}
|
|
12
45
|
|
|
13
46
|
/** Snapshot fixture: idle whale girl named 泡泡. */
|
|
14
47
|
const snapshot: PetStateView = {
|
|
@@ -26,6 +59,7 @@ const snapshot: PetStateView = {
|
|
|
26
59
|
feedCooldown: false,
|
|
27
60
|
},
|
|
28
61
|
display: { visible: true, size: 160, right: 24, bottom: 20 },
|
|
62
|
+
pet: { id: 'whale-girl', displayName: '鲸鱼娘', description: '测试用鲸鱼娘' },
|
|
29
63
|
name: '泡泡',
|
|
30
64
|
treats: { stocked: 3, max: 5 },
|
|
31
65
|
}
|
|
@@ -55,10 +89,11 @@ afterEach(() => {
|
|
|
55
89
|
})
|
|
56
90
|
|
|
57
91
|
/** Render the pet with mocked callbacks; returns the rename spy. */
|
|
58
|
-
function renderPet(overrides: Partial<
|
|
92
|
+
function renderPet(overrides: Partial<PetSpriteProps> = {}): { onRename: ReturnType<typeof vi.fn> } {
|
|
59
93
|
const onRename = vi.fn()
|
|
60
|
-
const props:
|
|
94
|
+
const props: PetSpriteProps = {
|
|
61
95
|
snapshot,
|
|
96
|
+
definition: petDefinition(),
|
|
62
97
|
display: snapshot.display,
|
|
63
98
|
feedback: null,
|
|
64
99
|
onPet: vi.fn(),
|
|
@@ -70,13 +105,13 @@ function renderPet(overrides: Partial<WhalePetProps> = {}): { onRename: ReturnTy
|
|
|
70
105
|
t,
|
|
71
106
|
...overrides,
|
|
72
107
|
}
|
|
73
|
-
render(<
|
|
108
|
+
render(<PetSprite {...props} />)
|
|
74
109
|
return { onRename }
|
|
75
110
|
}
|
|
76
111
|
|
|
77
112
|
/** Hover the sprite to open the panel, then click the rename button. */
|
|
78
113
|
function openRename(): HTMLInputElement {
|
|
79
|
-
fireEvent.pointerOver(screen.getByRole('button', { name: '
|
|
114
|
+
fireEvent.pointerOver(screen.getByRole('button', { name: '鲸鱼娘' }))
|
|
80
115
|
fireEvent.click(screen.getByText('改名'))
|
|
81
116
|
return screen.getByPlaceholderText('输入新名字') as HTMLInputElement
|
|
82
117
|
}
|
|
@@ -100,7 +135,7 @@ function fireComposingKeydown(target: Element, key: string): void {
|
|
|
100
135
|
fireEvent.compositionEnd(target)
|
|
101
136
|
}
|
|
102
137
|
|
|
103
|
-
describe('
|
|
138
|
+
describe('PetSprite rename input', () => {
|
|
104
139
|
it('submits the draft on Enter outside composition', () => {
|
|
105
140
|
const { onRename } = renderPet()
|
|
106
141
|
const input = openRename()
|
|
@@ -138,7 +173,7 @@ describe('WhalePet rename input', () => {
|
|
|
138
173
|
})
|
|
139
174
|
})
|
|
140
175
|
|
|
141
|
-
describe('
|
|
176
|
+
describe('PetSprite status bubble', () => {
|
|
142
177
|
const workingSnapshot: PetStateView = {
|
|
143
178
|
...snapshot,
|
|
144
179
|
animation: 'running',
|
|
@@ -160,3 +195,16 @@ describe('WhalePet status bubble', () => {
|
|
|
160
195
|
expect(screen.queryByText('正在思考')).toBeNull()
|
|
161
196
|
})
|
|
162
197
|
})
|
|
198
|
+
|
|
199
|
+
describe('PetSprite definition-driven render', () => {
|
|
200
|
+
it('labels the sprite with the pet display name', () => {
|
|
201
|
+
renderPet()
|
|
202
|
+
expect(screen.queryByRole('button', { name: '鲸鱼娘' })).not.toBeNull()
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('shows the renamed snapshot name in the hover panel', () => {
|
|
206
|
+
renderPet()
|
|
207
|
+
fireEvent.pointerOver(screen.getByRole('button', { name: '鲸鱼娘' }))
|
|
208
|
+
expect(screen.queryByText('泡泡')).not.toBeNull()
|
|
209
|
+
})
|
|
210
|
+
})
|