@linxin666/dsh-pet 0.3.3 → 0.3.5
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 +30 -13
- package/README.zh.md +30 -13
- package/assets/ouo-neko/pet.json +31 -0
- package/assets/ouo-neko/previews/failed.gif +0 -0
- package/assets/ouo-neko/previews/idle.gif +0 -0
- package/assets/ouo-neko/previews/jumping.gif +0 -0
- package/assets/ouo-neko/previews/review.gif +0 -0
- package/assets/ouo-neko/previews/running-left.gif +0 -0
- package/assets/ouo-neko/previews/running-right.gif +0 -0
- package/assets/ouo-neko/previews/running.gif +0 -0
- package/assets/ouo-neko/previews/waiting.gif +0 -0
- package/assets/ouo-neko/previews/waving.gif +0 -0
- package/assets/ouo-neko/spritesheet.webp +0 -0
- package/contracts/pet-manifest-v2.schema.json +102 -1
- package/contracts/voice-pack-v1.schema.json +23 -28
- package/lib/client.js +781 -40
- package/lib/client.js.map +1 -1
- package/lib/index.js +1567 -482
- package/lib/types/chatter.d.ts +68 -46
- package/lib/types/chatter.d.ts.map +1 -1
- package/lib/types/chatter.js +243 -301
- package/lib/types/client/PetDockEntry.d.ts +3 -0
- package/lib/types/client/PetDockEntry.d.ts.map +1 -1
- package/lib/types/client/PetDockEntry.js +18 -2
- package/lib/types/client/PetSprite.d.ts +23 -0
- package/lib/types/client/PetSprite.d.ts.map +1 -1
- package/lib/types/client/PetSprite.js +49 -45
- package/lib/types/client/PluginSettingsCard.d.ts +2 -0
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +2 -2
- package/lib/types/client/drag-stream.d.ts +19 -0
- package/lib/types/client/drag-stream.d.ts.map +1 -0
- package/lib/types/client/drag-stream.js +26 -0
- package/lib/types/client/gameplay-hud.d.ts +50 -0
- package/lib/types/client/gameplay-hud.d.ts.map +1 -0
- package/lib/types/client/gameplay-hud.js +274 -0
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +38 -9
- package/lib/types/client/locales.d.ts +34 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +36 -0
- package/lib/types/client/pet-store.d.ts +7 -1
- package/lib/types/client/pet-store.d.ts.map +1 -1
- package/lib/types/client/pet-store.js +4 -0
- package/lib/types/client/renderers/Frames2dVisualMount.d.ts +29 -0
- package/lib/types/client/renderers/Frames2dVisualMount.d.ts.map +1 -0
- package/lib/types/client/renderers/Frames2dVisualMount.js +83 -0
- package/lib/types/client/renderers/PetRendererSwitch.d.ts +6 -0
- package/lib/types/client/renderers/PetRendererSwitch.d.ts.map +1 -1
- package/lib/types/client/renderers/PetRendererSwitch.js +14 -1
- package/lib/types/client/renderers/frames2d.d.ts +36 -0
- package/lib/types/client/renderers/frames2d.d.ts.map +1 -0
- package/lib/types/client/renderers/frames2d.js +196 -0
- package/lib/types/defaults.d.ts +6 -0
- package/lib/types/defaults.d.ts.map +1 -0
- package/lib/types/defaults.js +5 -0
- package/lib/types/dsh-home.d.ts.map +1 -1
- package/lib/types/dsh-home.js +9 -3
- package/lib/types/event-projection.d.ts +9 -4
- package/lib/types/event-projection.d.ts.map +1 -1
- package/lib/types/event-projection.js +46 -14
- package/lib/types/gameplay.d.ts +182 -0
- package/lib/types/gameplay.d.ts.map +1 -0
- package/lib/types/gameplay.js +618 -0
- package/lib/types/ledger.d.ts +14 -0
- package/lib/types/ledger.d.ts.map +1 -1
- package/lib/types/ledger.js +30 -0
- package/lib/types/manifest-v2.d.ts +38 -1
- package/lib/types/manifest-v2.d.ts.map +1 -1
- package/lib/types/manifest-v2.js +182 -2
- package/lib/types/persist.d.ts +4 -4
- package/lib/types/persist.d.ts.map +1 -1
- package/lib/types/persist.js +41 -4
- package/lib/types/registry.d.ts +29 -0
- package/lib/types/registry.d.ts.map +1 -1
- package/lib/types/registry.js +170 -1
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +30 -3
- package/lib/types/service.d.ts +63 -11
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js +208 -19
- package/lib/types/voice-pack.d.ts +6 -7
- package/lib/types/voice-pack.d.ts.map +1 -1
- package/lib/types/voice-pack.js +44 -47
- package/package.json +5 -2
- package/src/chatter.test.ts +68 -38
- package/src/chatter.ts +269 -316
- package/src/client/PetDockEntry.test.tsx +6 -0
- package/src/client/PetDockEntry.tsx +32 -1
- package/src/client/PetSprite.test.tsx +68 -21
- package/src/client/PetSprite.tsx +63 -45
- package/src/client/PluginSettingsCard.tsx +4 -2
- package/src/client/drag-stream.ts +35 -0
- package/src/client/gameplay-hud.test.tsx +338 -0
- package/src/client/gameplay-hud.tsx +403 -0
- package/src/client/index.test.tsx +10 -1
- package/src/client/index.ts +46 -11
- package/src/client/locales.ts +36 -0
- package/src/client/pet-store.ts +10 -1
- package/src/client/pet.module.css +192 -29
- package/src/client/renderers/Frames2dVisualMount.tsx +111 -0
- package/src/client/renderers/PetRendererSwitch.tsx +28 -1
- package/src/client/renderers/frames2d.test.ts +137 -0
- package/src/client/renderers/frames2d.ts +216 -0
- package/src/client/settings-card.module.css +5 -0
- package/src/defaults.ts +7 -0
- package/src/dsh-home.ts +9 -3
- package/src/event-projection.ts +57 -16
- package/src/gameplay.test.ts +239 -0
- package/src/gameplay.ts +692 -0
- package/src/ledger.test.ts +16 -0
- package/src/ledger.ts +31 -0
- package/src/manifest-v2.test.ts +130 -1
- package/src/manifest-v2.ts +181 -2
- package/src/persist.test.ts +3 -0
- package/src/persist.ts +43 -6
- package/src/registry.test.ts +168 -1
- package/src/registry.ts +199 -1
- package/src/routes.ts +28 -4
- package/src/service.ts +240 -29
- package/src/voice-pack.test.ts +48 -25
- package/src/voice-pack.ts +50 -47
|
@@ -11,12 +11,14 @@
|
|
|
11
11
|
* @module @linxin666/dsh-pet/client/PetDockEntry
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { useEffect, useSyncExternalStore, type ReactElement } from 'react'
|
|
14
|
+
import { useEffect, useRef, useSyncExternalStore, type ReactElement } from 'react'
|
|
15
15
|
import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'
|
|
16
16
|
import type { PetDisplayConfig } from '../persist.ts'
|
|
17
17
|
import type { PetStoreInstance } from './pet-store.ts'
|
|
18
18
|
import { PetSprite } from './PetSprite.tsx'
|
|
19
19
|
import { PetRendererSwitch } from './renderers/PetRendererSwitch.tsx'
|
|
20
|
+
import { createDragStream, type DragStream } from './drag-stream.ts'
|
|
21
|
+
import { GameplayHud, type GameplayApi, type GameplayBus } from './gameplay-hud.tsx'
|
|
20
22
|
import { NS } from './locales.ts'
|
|
21
23
|
import styles from './pet.module.css'
|
|
22
24
|
|
|
@@ -42,6 +44,8 @@ export interface PetInjected {
|
|
|
42
44
|
openSession: (sessionId: string) => void
|
|
43
45
|
/** Clear the reaction bubble. */
|
|
44
46
|
feedbackDone: () => void
|
|
47
|
+
/** Gameplay verb API (miku-pet generalization); wired but unused for pets without a gameplay block. */
|
|
48
|
+
gameplay: GameplayApi
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
/** Composed props of the global pet entry (locale + injected; no slot runtime share). */
|
|
@@ -70,6 +74,15 @@ export function PetDockEntry(props: PetDockEntryProps): ReactElement {
|
|
|
70
74
|
ensure()
|
|
71
75
|
}, [ensure])
|
|
72
76
|
|
|
77
|
+
// Per-pet gameplay wiring: the coordination bus (HUD taps <-> chrome,
|
|
78
|
+
// HUD track overrides <-> frames2d mount) and the shared drag stream.
|
|
79
|
+
const auxRef = useRef<{ id: string; bus: GameplayBus; drag: DragStream } | null>(null)
|
|
80
|
+
if (definition !== null && (auxRef.current === null || auxRef.current.id !== definition.id)) {
|
|
81
|
+
auxRef.current = { id: definition.id, bus: {}, drag: createDragStream() }
|
|
82
|
+
}
|
|
83
|
+
const aux = auxRef.current
|
|
84
|
+
const gameplay = definition?.gameplay
|
|
85
|
+
|
|
73
86
|
if (visible) {
|
|
74
87
|
return (
|
|
75
88
|
<span data-pet-dock data-testid="pet-dock">
|
|
@@ -80,6 +93,7 @@ export function PetDockEntry(props: PetDockEntryProps): ReactElement {
|
|
|
80
93
|
definition={definition}
|
|
81
94
|
phase={snapshot?.phase ?? 'idle'}
|
|
82
95
|
onPet={props.pet}
|
|
96
|
+
{...(aux === null ? {} : { drag: aux.drag, bus: aux.bus })}
|
|
83
97
|
t={props.t}
|
|
84
98
|
>
|
|
85
99
|
<PetSprite
|
|
@@ -94,6 +108,23 @@ export function PetDockEntry(props: PetDockEntryProps): ReactElement {
|
|
|
94
108
|
onRename={props.rename}
|
|
95
109
|
onOpenSession={props.openSession}
|
|
96
110
|
onFeedbackDone={props.feedbackDone}
|
|
111
|
+
dragDisabled={snapshot.gameplay?.mode === 'work'}
|
|
112
|
+
{...(gameplay === undefined || aux === null
|
|
113
|
+
? {}
|
|
114
|
+
: {
|
|
115
|
+
onGameplayTap: (fx: number, fy: number) => aux.bus.tap?.(fx, fy),
|
|
116
|
+
onGameplayMenu: () => aux.bus.openCard?.(),
|
|
117
|
+
hud: (
|
|
118
|
+
<GameplayHud
|
|
119
|
+
definition={definition}
|
|
120
|
+
store={store}
|
|
121
|
+
api={props.gameplay}
|
|
122
|
+
bus={aux.bus}
|
|
123
|
+
drag={aux.drag}
|
|
124
|
+
t={props.t}
|
|
125
|
+
/>
|
|
126
|
+
),
|
|
127
|
+
})}
|
|
97
128
|
t={props.t}
|
|
98
129
|
/>
|
|
99
130
|
</PetRendererSwitch>
|
|
@@ -159,16 +159,17 @@ describe('PetSprite custom visual (pet-center M3)', () => {
|
|
|
159
159
|
})
|
|
160
160
|
})
|
|
161
161
|
|
|
162
|
-
describe('PetSprite
|
|
163
|
-
it('
|
|
162
|
+
describe('PetSprite hide control (#1126)', () => {
|
|
163
|
+
it('does not render an intrusive corner close button and allows hiding via hover panel', () => {
|
|
164
164
|
const onHide = vi.fn()
|
|
165
165
|
const onPet = vi.fn()
|
|
166
166
|
renderPet({ onHide, onPet })
|
|
167
167
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
168
|
+
expect(screen.queryByTestId('pet-close')).toBeNull()
|
|
169
|
+
|
|
170
|
+
fireEvent.pointerOver(screen.getByRole('button', { name: '鲸鱼娘' }))
|
|
171
|
+
const hideBtn = screen.getByText('隐藏')
|
|
172
|
+
fireEvent.click(hideBtn)
|
|
172
173
|
|
|
173
174
|
expect(onHide).toHaveBeenCalledTimes(1)
|
|
174
175
|
expect(onPet).not.toHaveBeenCalled()
|
|
@@ -423,27 +424,18 @@ describe('PetSprite status bubble', () => {
|
|
|
423
424
|
expect(screen.queryByText('正在使用 grep')).not.toBeNull()
|
|
424
425
|
})
|
|
425
426
|
|
|
426
|
-
it('lets
|
|
427
|
-
renderPet({ snapshot: { ...workingSnapshot, whisper: '哼哧哼哧,大脑转得飞快~' } })
|
|
428
|
-
// The whisper speaks THROUGH the bubble: it replaces (not accompanies)
|
|
429
|
-
// the status copy, so the pet never shows two bubbles at once.
|
|
430
|
-
expect(screen.queryByText('哼哧哼哧,大脑转得飞快~')).not.toBeNull()
|
|
431
|
-
expect(screen.queryByText('正在思考')).toBeNull()
|
|
432
|
-
})
|
|
433
|
-
|
|
434
|
-
it('lets the whisper take over the display session bubble in the stack', () => {
|
|
427
|
+
it('lets a session whisper take over its own bubble while others stay collapsed', () => {
|
|
435
428
|
const { onOpenSession } = renderPet({
|
|
436
429
|
snapshot: {
|
|
437
430
|
...workingSnapshot,
|
|
438
|
-
whisper: '我在这儿陪着你呢,别急别急',
|
|
439
431
|
sessions: [
|
|
440
|
-
{ sessionId: 's-a', animation: 'running', phase: 'thinking', bubble: '正在思考' },
|
|
432
|
+
{ sessionId: 's-a', animation: 'running', phase: 'thinking', bubble: '正在思考', whisper: '我在这儿陪着你呢,别急别急' },
|
|
441
433
|
{ sessionId: 's-b', animation: 'running-right', phase: 'tool', bubble: '正在使用 grep' },
|
|
442
434
|
],
|
|
443
435
|
},
|
|
444
436
|
})
|
|
445
|
-
// The
|
|
446
|
-
//
|
|
437
|
+
// The lead session's bubble speaks ITS whisper instead of its status
|
|
438
|
+
// copy; the collapsed extra session hides behind the badge.
|
|
447
439
|
expect(screen.queryByText('我在这儿陪着你呢,别急别急')).not.toBeNull()
|
|
448
440
|
expect(screen.queryByText('正在思考')).toBeNull()
|
|
449
441
|
expect(screen.queryByText('正在使用 grep')).toBeNull()
|
|
@@ -455,9 +447,28 @@ describe('PetSprite status bubble', () => {
|
|
|
455
447
|
expect(onOpenSession).toHaveBeenCalledWith('s-a')
|
|
456
448
|
})
|
|
457
449
|
|
|
450
|
+
it('renders each session whisper on its own expanded bubble', () => {
|
|
451
|
+
renderPet({
|
|
452
|
+
snapshot: {
|
|
453
|
+
...workingSnapshot,
|
|
454
|
+
sessions: [
|
|
455
|
+
{ sessionId: 's-a', animation: 'running', phase: 'thinking', bubble: '正在思考', whisper: 'A 的碎碎念' },
|
|
456
|
+
{ sessionId: 's-b', animation: 'running-right', phase: 'tool', bubble: '正在使用 grep', whisper: 'B 的碎碎念' },
|
|
457
|
+
],
|
|
458
|
+
},
|
|
459
|
+
})
|
|
460
|
+
fireEvent.pointerOver(screen.getByText('A 的碎碎念').closest('div')!)
|
|
461
|
+
expect(screen.queryByText('A 的碎碎念')).not.toBeNull()
|
|
462
|
+
expect(screen.queryByText('B 的碎碎念')).not.toBeNull()
|
|
463
|
+
expect(screen.queryByText('正在思考')).toBeNull()
|
|
464
|
+
})
|
|
465
|
+
|
|
458
466
|
it('lets transient interaction feedback take over the whisper too', () => {
|
|
459
467
|
renderPet({
|
|
460
|
-
snapshot: {
|
|
468
|
+
snapshot: {
|
|
469
|
+
...workingSnapshot,
|
|
470
|
+
sessions: [{ sessionId: 's-a', animation: 'running', phase: 'thinking', bubble: '正在思考', whisper: '哼哧哼哧,大脑转得飞快~' }],
|
|
471
|
+
},
|
|
461
472
|
feedback: { text: '摸摸成功', kind: 'pet', at: 1 },
|
|
462
473
|
})
|
|
463
474
|
expect(screen.queryByText('摸摸成功')).not.toBeNull()
|
|
@@ -677,6 +688,35 @@ describe('PetSprite panel chrome from the voice pack (pet-center M4)', () => {
|
|
|
677
688
|
expect(screen.getByText('鱼干 3(0 分,幼鲸)')).toBeDefined()
|
|
678
689
|
})
|
|
679
690
|
})
|
|
691
|
+
describe('PetSprite gameplay entry in the hover panel (miku generalization)', () => {
|
|
692
|
+
it('renders a 玩法 action and routes clicks to the HUD channel', () => {
|
|
693
|
+
const onGameplayMenu = vi.fn()
|
|
694
|
+
renderPet({ onGameplayMenu })
|
|
695
|
+
// The entry lives in the hover panel; a closed panel shows no chrome.
|
|
696
|
+
expect(screen.queryByText('玩法')).toBeNull()
|
|
697
|
+
fireEvent.pointerOver(screen.getByRole('button', { name: '鲸鱼娘' }))
|
|
698
|
+
expect(screen.getByText('玩法')).toBeDefined()
|
|
699
|
+
fireEvent.click(screen.getByText('玩法'))
|
|
700
|
+
expect(onGameplayMenu).toHaveBeenCalledTimes(1)
|
|
701
|
+
})
|
|
702
|
+
|
|
703
|
+
it('omits the 玩法 action when the pet carries no gameplay HUD', () => {
|
|
704
|
+
renderPet()
|
|
705
|
+
fireEvent.pointerOver(screen.getByRole('button', { name: '鲸鱼娘' }))
|
|
706
|
+
expect(screen.queryByText('玩法')).toBeNull()
|
|
707
|
+
})
|
|
708
|
+
|
|
709
|
+
it('keeps the 玩法 entry when a voice pack hides every panel action', () => {
|
|
710
|
+
renderPet({
|
|
711
|
+
definition: { ...petDefinition(), panel: { actions: [] } },
|
|
712
|
+
onGameplayMenu: vi.fn(),
|
|
713
|
+
})
|
|
714
|
+
fireEvent.pointerOver(screen.getByRole('button', { name: '鲸鱼娘' }))
|
|
715
|
+
expect(screen.getByText('玩法')).toBeDefined()
|
|
716
|
+
expect(screen.queryByText('喂食')).toBeNull()
|
|
717
|
+
})
|
|
718
|
+
})
|
|
719
|
+
|
|
680
720
|
describe('PetSprite status decoration (pet-center M5, #567)', () => {
|
|
681
721
|
const decoration: DecorationView = {
|
|
682
722
|
apiVersion: 'x-org.linxin666.pet-center/status-decoration-v1',
|
|
@@ -724,7 +764,14 @@ describe('PetSprite status decoration (pet-center M5, #567)', () => {
|
|
|
724
764
|
})
|
|
725
765
|
|
|
726
766
|
it('yields the bubble to the whisper (voice moment hides the ornament)', () => {
|
|
727
|
-
renderPet({
|
|
767
|
+
renderPet({
|
|
768
|
+
snapshot: {
|
|
769
|
+
...snapshot,
|
|
770
|
+
phase: 'thinking',
|
|
771
|
+
decoration,
|
|
772
|
+
sessions: [{ sessionId: 's-a', animation: 'running', phase: 'thinking', bubble: '正在思考', whisper: '冲了冲了' }],
|
|
773
|
+
},
|
|
774
|
+
})
|
|
728
775
|
expect(ornament()).toBeNull()
|
|
729
776
|
expect(document.body.textContent).toContain('冲了冲了')
|
|
730
777
|
})
|
package/src/client/PetSprite.tsx
CHANGED
|
@@ -43,6 +43,8 @@ export interface PetSpriteProps {
|
|
|
43
43
|
onHide: () => void
|
|
44
44
|
/** Persist a drag position. */
|
|
45
45
|
onDragEnd: (right: number, bottom: number) => void
|
|
46
|
+
/** Drag gesture notifications for renderers with a drag track (frames2d). */
|
|
47
|
+
onDraggingChange?: (dragging: boolean) => void
|
|
46
48
|
/** Rename the selected pet (persisted by the host). */
|
|
47
49
|
onRename: (name: string) => void
|
|
48
50
|
/** Navigate to the session one status bubble reports on. */
|
|
@@ -55,6 +57,27 @@ export interface PetSpriteProps {
|
|
|
55
57
|
* renders inside the sprite box, and the atlas load + frame loop skip.
|
|
56
58
|
*/
|
|
57
59
|
visual?: ReactNode
|
|
60
|
+
/**
|
|
61
|
+
* Gameplay overlay (miku-pet generalization): rendered inside the float
|
|
62
|
+
* container so hover containment and stacking work unchanged. Absent for
|
|
63
|
+
* pets without a gameplay block.
|
|
64
|
+
*/
|
|
65
|
+
hud?: ReactNode
|
|
66
|
+
/**
|
|
67
|
+
* Gameplay tap sink: receives the tap point as sprite-box fractions
|
|
68
|
+
* (0..1). When present the chrome reports the tap IN ADDITION to the
|
|
69
|
+
* affinity pet; the HUD decides zones/no-ops.
|
|
70
|
+
*/
|
|
71
|
+
onGameplayTap?: (fractionX: number, fractionY: number) => void
|
|
72
|
+
/**
|
|
73
|
+
* Gameplay entry (miku-pet generalization): when present the hover panel
|
|
74
|
+
* renders a 玩法 action that opens/closes the gameplay card. The chrome
|
|
75
|
+
* wires it to the HUD through the per-pet bus (openCard), mirroring the
|
|
76
|
+
* onGameplayTap sink. Absent for pets without a gameplay block.
|
|
77
|
+
*/
|
|
78
|
+
onGameplayMenu?: () => void
|
|
79
|
+
/** Disable the drag gesture (gameplay work mode blocks dragging). */
|
|
80
|
+
dragDisabled?: boolean
|
|
58
81
|
/** Locale translate seat (namespace-bound). */
|
|
59
82
|
t: TranslateNS<typeof NS>
|
|
60
83
|
}
|
|
@@ -365,6 +388,7 @@ export function PetSprite(props: PetSpriteProps): ReactPortal {
|
|
|
365
388
|
useEffect(() => () => clearHideTimer(), [])
|
|
366
389
|
|
|
367
390
|
const onPointerDown = (e: ReactPointerEvent<HTMLDivElement>): void => {
|
|
391
|
+
if (props.dragDisabled === true) return
|
|
368
392
|
e.preventDefault()
|
|
369
393
|
;(e.target as HTMLElement).setPointerCapture?.(e.pointerId)
|
|
370
394
|
const current = dragPos ?? { right: display.right, bottom: display.bottom }
|
|
@@ -377,7 +401,10 @@ export function PetSprite(props: PetSpriteProps): ReactPortal {
|
|
|
377
401
|
if (drag === null) return
|
|
378
402
|
const dx = e.clientX - drag.startX
|
|
379
403
|
const dy = e.clientY - drag.startY
|
|
380
|
-
if (Math.abs(dx) > 4 || Math.abs(dy) > 4)
|
|
404
|
+
if (Math.abs(dx) > 4 || Math.abs(dy) > 4) {
|
|
405
|
+
if (!draggedRef.current) props.onDraggingChange?.(true)
|
|
406
|
+
draggedRef.current = true
|
|
407
|
+
}
|
|
381
408
|
const right = clampOffset(drag.right - dx, window.innerWidth - 40)
|
|
382
409
|
const bottom = clampOffset(drag.bottom - dy, window.innerHeight - 40)
|
|
383
410
|
setDragPos({ right, bottom })
|
|
@@ -385,6 +412,7 @@ export function PetSprite(props: PetSpriteProps): ReactPortal {
|
|
|
385
412
|
const onPointerUp = (): void => {
|
|
386
413
|
if (dragRef.current === null) return
|
|
387
414
|
dragRef.current = null
|
|
415
|
+
if (draggedRef.current) props.onDraggingChange?.(false)
|
|
388
416
|
if (dragPos !== null) props.onDragEnd(dragPos.right, dragPos.bottom)
|
|
389
417
|
}
|
|
390
418
|
|
|
@@ -404,14 +432,14 @@ export function PetSprite(props: PetSpriteProps): ReactPortal {
|
|
|
404
432
|
const statusBubble = feedback === null && sessionBubbles.length === 0
|
|
405
433
|
? snapshot?.bubble
|
|
406
434
|
: undefined
|
|
407
|
-
//
|
|
408
|
-
// woken by
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
// voices at once. Interaction feedback takes over the
|
|
412
|
-
// while it plays, so whispers yield to it like status
|
|
413
|
-
|
|
414
|
-
const bubblePresent = feedback !== null || sessionBubbles.length > 0 || statusBubble !== undefined
|
|
435
|
+
// Each session's inner whisper (碎碎念) rides its own bubble — short
|
|
436
|
+
// inner-voice copy woken by that session's activity, never the model's or
|
|
437
|
+
// another session's. Instead of a second bubble of its own, a fresh
|
|
438
|
+
// whisper takes over its session's bubble and re-tints it, so the pet
|
|
439
|
+
// never wears two voices at once. Interaction feedback takes over the
|
|
440
|
+
// whole bubble area while it plays, so whispers yield to it like status
|
|
441
|
+
// copy.
|
|
442
|
+
const bubblePresent = feedback !== null || sessionBubbles.length > 0 || statusBubble !== undefined
|
|
415
443
|
const displayName = snapshot?.name ?? definition.displayName
|
|
416
444
|
// The host-served status decoration (M5, #567); absent = text-only bubbles.
|
|
417
445
|
const decoration = snapshot?.decoration
|
|
@@ -497,10 +525,16 @@ export function PetSprite(props: PetSpriteProps): ReactPortal {
|
|
|
497
525
|
onPointerDown={onPointerDown}
|
|
498
526
|
onPointerMove={onPointerMove}
|
|
499
527
|
onPointerUp={onPointerUp}
|
|
500
|
-
onClick={() => {
|
|
528
|
+
onClick={(e) => {
|
|
501
529
|
// A pointer sequence that moved (dragged) still fires a trailing
|
|
502
530
|
// click; skip the pet when that happened.
|
|
503
531
|
if (draggedRef.current) return
|
|
532
|
+
if (props.onGameplayTap !== undefined && spriteRef.current !== null) {
|
|
533
|
+
const rect = spriteRef.current.getBoundingClientRect()
|
|
534
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
535
|
+
props.onGameplayTap((e.clientX - rect.left) / rect.width, (e.clientY - rect.top) / rect.height)
|
|
536
|
+
}
|
|
537
|
+
}
|
|
504
538
|
props.onPet()
|
|
505
539
|
}}
|
|
506
540
|
role="button"
|
|
@@ -508,32 +542,14 @@ export function PetSprite(props: PetSpriteProps): ReactPortal {
|
|
|
508
542
|
>
|
|
509
543
|
{props.visual}
|
|
510
544
|
</div>
|
|
511
|
-
<button
|
|
512
|
-
type="button"
|
|
513
|
-
className={styles.closeButton}
|
|
514
|
-
aria-label={panelLabel('hide', props.t('pet.hide'))}
|
|
515
|
-
title={panelLabel('hide', props.t('pet.hide'))}
|
|
516
|
-
data-testid="pet-close"
|
|
517
|
-
onPointerDown={(e) => {
|
|
518
|
-
// Keep the close control from starting a drag on the sprite.
|
|
519
|
-
e.stopPropagation()
|
|
520
|
-
}}
|
|
521
|
-
onClick={(e) => {
|
|
522
|
-
// The close control sits beside the pet button; do not pet as a
|
|
523
|
-
// side effect of closing the overlay.
|
|
524
|
-
e.stopPropagation()
|
|
525
|
-
props.onHide()
|
|
526
|
-
}}
|
|
527
|
-
>
|
|
528
|
-
×
|
|
529
|
-
</button>
|
|
530
545
|
</div>
|
|
546
|
+
{props.hud}
|
|
531
547
|
{feedback !== null && (
|
|
532
548
|
<div key={feedback.at} ref={bubbleRef} className={clsx(styles.bubble, feedback.kind === 'feed' ? styles.bubbleFeed : styles.bubblePet)}>
|
|
533
549
|
{feedback.text}
|
|
534
550
|
</div>
|
|
535
551
|
)}
|
|
536
|
-
{feedback === null && (sessionBubbles.length > 0 || statusBubble !== undefined
|
|
552
|
+
{feedback === null && (sessionBubbles.length > 0 || statusBubble !== undefined) && (
|
|
537
553
|
<div
|
|
538
554
|
ref={bubbleRef}
|
|
539
555
|
className={styles.bubbleStack}
|
|
@@ -541,15 +557,14 @@ export function PetSprite(props: PetSpriteProps): ReactPortal {
|
|
|
541
557
|
onPointerLeave={() => setStackPeek(false)}
|
|
542
558
|
>
|
|
543
559
|
{visibleSessions.map((session, index) => {
|
|
544
|
-
//
|
|
545
|
-
//
|
|
546
|
-
//
|
|
547
|
-
//
|
|
548
|
-
|
|
549
|
-
const speaksWhisper = index === 0 && whisper !== undefined
|
|
560
|
+
// A session's whisper rides ITS OWN bubble (the stack lead when
|
|
561
|
+
// the current session has one, any bubble when the stack is
|
|
562
|
+
// expanded). The key swap restarts the entrance animation so the
|
|
563
|
+
// mood change reads as the bubble re-speaking.
|
|
564
|
+
const speaksWhisper = session.whisper !== undefined
|
|
550
565
|
const bubble = (
|
|
551
566
|
<button
|
|
552
|
-
key={speaksWhisper ? 'whisper:' + whisper : session.sessionId}
|
|
567
|
+
key={speaksWhisper ? 'whisper:' + session.whisper : session.sessionId}
|
|
553
568
|
type="button"
|
|
554
569
|
className={clsx(
|
|
555
570
|
styles.bubble,
|
|
@@ -563,7 +578,7 @@ export function PetSprite(props: PetSpriteProps): ReactPortal {
|
|
|
563
578
|
{index === 0 && !speaksWhisper && decoration !== undefined && (
|
|
564
579
|
<StatusOrnament decoration={decoration} phase={phase} />
|
|
565
580
|
)}
|
|
566
|
-
{
|
|
581
|
+
{session.whisper ?? session.bubble}
|
|
567
582
|
</button>
|
|
568
583
|
)
|
|
569
584
|
// The primary bubble carries the '+N' badge while other sessions
|
|
@@ -592,19 +607,17 @@ export function PetSprite(props: PetSpriteProps): ReactPortal {
|
|
|
592
607
|
</span>
|
|
593
608
|
)
|
|
594
609
|
})}
|
|
595
|
-
{sessionBubbles.length === 0 &&
|
|
596
|
-
// The key swap (status copy <-> whisper) restarts the entrance
|
|
597
|
-
// animation on every mood change.
|
|
610
|
+
{sessionBubbles.length === 0 && statusBubble !== undefined && (
|
|
598
611
|
<div
|
|
599
|
-
key=
|
|
600
|
-
className={clsx(styles.bubble, styles.bubbleStatus
|
|
612
|
+
key="status"
|
|
613
|
+
className={clsx(styles.bubble, styles.bubbleStatus)}
|
|
601
614
|
role="status"
|
|
602
615
|
aria-live="polite"
|
|
603
616
|
>
|
|
604
|
-
{
|
|
617
|
+
{decoration !== undefined && (
|
|
605
618
|
<StatusOrnament decoration={decoration} phase={phase} />
|
|
606
619
|
)}
|
|
607
|
-
{
|
|
620
|
+
{statusBubble}
|
|
608
621
|
</div>
|
|
609
622
|
)}
|
|
610
623
|
</div>
|
|
@@ -704,6 +717,11 @@ export function PetSprite(props: PetSpriteProps): ReactPortal {
|
|
|
704
717
|
{panelLabel('hide', props.t('pet.hide'))}
|
|
705
718
|
</button>
|
|
706
719
|
)}
|
|
720
|
+
{props.onGameplayMenu !== undefined && (
|
|
721
|
+
<button type="button" className={styles.action} onClick={props.onGameplayMenu}>
|
|
722
|
+
{props.t('pet.gameplay.menu')}
|
|
723
|
+
</button>
|
|
724
|
+
)}
|
|
707
725
|
</div>
|
|
708
726
|
</>
|
|
709
727
|
)}
|
|
@@ -39,6 +39,8 @@ export interface PluginSettingsCardProps<TKey extends string = string> {
|
|
|
39
39
|
titleKey: TKey
|
|
40
40
|
/** Locale key of the line describing what this plugin's settings govern. */
|
|
41
41
|
descriptionKey: TKey
|
|
42
|
+
/** Optional rich header description rendered instead of the plain t(descriptionKey) text (the plain text stays as the tooltip). */
|
|
43
|
+
descriptionNode?: ReactNode
|
|
42
44
|
/** The card's form state: availability, writability, and what a save would do. */
|
|
43
45
|
state: CardShell
|
|
44
46
|
/** Write every staged edit. */
|
|
@@ -87,7 +89,7 @@ export function PluginSettingsCard<TKey extends string = string>(props: PluginSe
|
|
|
87
89
|
<div className={css.headerStatic}>
|
|
88
90
|
<span className={css.headText}>
|
|
89
91
|
<span className={css.name} title={title}>{title}</span>
|
|
90
|
-
<span className={css.description} title={description}>{description}</span>
|
|
92
|
+
<span className={css.description} title={description}>{props.descriptionNode ?? description}</span>
|
|
91
93
|
</span>
|
|
92
94
|
{state.dirty ? <span className={css.pending} title={props.t('settings.unsaved')}>{props.t('settings.unsaved')}</span> : null}
|
|
93
95
|
</div>
|
|
@@ -102,7 +104,7 @@ export function PluginSettingsCard<TKey extends string = string>(props: PluginSe
|
|
|
102
104
|
>
|
|
103
105
|
<span className={css.headText}>
|
|
104
106
|
<span className={css.name} title={title}>{title}</span>
|
|
105
|
-
<span className={css.description} title={description}>{description}</span>
|
|
107
|
+
<span className={css.description} title={description}>{props.descriptionNode ?? description}</span>
|
|
106
108
|
</span>
|
|
107
109
|
{state.dirty ? <span className={css.pending} title={props.t('settings.unsaved')}>{props.t('settings.unsaved')}</span> : null}
|
|
108
110
|
<svg
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drag stream — bridges the chrome's pointer drag state onto the renderer
|
|
3
|
+
* contract's { get, subscribe } shape, so renderers with a dedicated drag
|
|
4
|
+
* track (frames2d) can follow the chrome-owned drag gesture. Dispatch is
|
|
5
|
+
* change-only, mirroring the phase stream.
|
|
6
|
+
* @module @linxin666/dsh-pet/client/drag-stream
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** The renderer-facing drag stream. */
|
|
10
|
+
export interface DragStream {
|
|
11
|
+
/** Whether the chrome reports an active drag gesture. */
|
|
12
|
+
get(): boolean
|
|
13
|
+
/** Subscribe to drag state changes; returns the unsubscribe. */
|
|
14
|
+
subscribe(listener: (dragging: boolean) => void): () => void
|
|
15
|
+
/** Feed a fresh drag state; no-op when unchanged. */
|
|
16
|
+
push(dragging: boolean): void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Create the stream (one per pet activation, owned by the renderer switch). */
|
|
20
|
+
export function createDragStream(): DragStream {
|
|
21
|
+
let current = false
|
|
22
|
+
const listeners = new Set<(dragging: boolean) => void>()
|
|
23
|
+
return {
|
|
24
|
+
get: () => current,
|
|
25
|
+
subscribe(listener) {
|
|
26
|
+
listeners.add(listener)
|
|
27
|
+
return () => { listeners.delete(listener) }
|
|
28
|
+
},
|
|
29
|
+
push(dragging) {
|
|
30
|
+
if (dragging === current) return
|
|
31
|
+
current = dragging
|
|
32
|
+
for (const listener of [...listeners]) listener(dragging)
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
}
|