@linxin666/dsh-pet 0.3.22 → 0.3.23
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 +1 -0
- package/README.zh.md +1 -0
- package/lib/client.js +71 -9
- package/lib/client.js.map +1 -1
- package/lib/index.js +10 -3
- package/lib/types/client/PetDockEntry.js +1 -1
- package/lib/types/client/PetSettingsCard.d.ts +4 -0
- package/lib/types/client/PetSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PetSettingsCard.js +3 -1
- package/lib/types/client/PetSprite.d.ts +1 -1
- package/lib/types/client/PetSprite.d.ts.map +1 -1
- package/lib/types/client/PetSprite.js +40 -7
- package/lib/types/client/locales.d.ts +4 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +4 -0
- package/lib/types/index.d.ts +2 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +2 -1
- package/lib/types/persist.d.ts +32 -0
- package/lib/types/persist.d.ts.map +1 -1
- package/lib/types/persist.js +37 -0
- package/lib/types/service.d.ts +3 -1
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js +4 -2
- package/package.json +1 -1
- package/src/client/PetDockEntry.test.tsx +1 -1
- package/src/client/PetDockEntry.tsx +1 -1
- package/src/client/PetSettingsCard.tsx +16 -0
- package/src/client/PetSprite.test.tsx +1 -1
- package/src/client/PetSprite.tsx +40 -7
- package/src/client/gameplay-hud.test.tsx +1 -1
- package/src/client/locales.ts +4 -0
- package/src/client/pet-css.test.ts +14 -0
- package/src/client/pet-store.test.ts +1 -1
- package/src/client/pet.module.css +7 -4
- package/src/index.ts +2 -1
- package/src/persist.test.ts +53 -1
- package/src/persist.ts +46 -0
- package/src/service.ts +7 -1
package/src/client/PetSprite.tsx
CHANGED
|
@@ -14,7 +14,7 @@ import type { CSSProperties, PointerEvent as ReactPointerEvent, ReactElement, Re
|
|
|
14
14
|
import { createPortal } from 'react-dom'
|
|
15
15
|
import clsx from 'clsx'
|
|
16
16
|
import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots'
|
|
17
|
-
import type
|
|
17
|
+
import { bubbleScaleFor, type PetDisplayConfig } from '../persist.ts'
|
|
18
18
|
import type { PetStateView } from '../service.ts'
|
|
19
19
|
import { announcementFresh, type PetAnnouncement } from '../announce.ts'
|
|
20
20
|
import type { PetDefinition } from '../registry.ts'
|
|
@@ -315,15 +315,39 @@ export function PetSprite(props: PetSpriteProps): ReactPortal {
|
|
|
315
315
|
// custom visual (pet-center M3) replaces the atlas entirely.
|
|
316
316
|
useEffect(() => {
|
|
317
317
|
if (props.visual !== undefined) return
|
|
318
|
+
setImageReady(false)
|
|
318
319
|
let cancelled = false
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
320
|
+
let retryTimer: ReturnType<typeof setTimeout> | undefined
|
|
321
|
+
let attempt = 0
|
|
322
|
+
const maxAttempts = 3
|
|
323
|
+
let activeImg: HTMLImageElement | null = null
|
|
324
|
+
|
|
325
|
+
const loadAtlas = () => {
|
|
326
|
+
const img = new Image()
|
|
327
|
+
activeImg = img
|
|
328
|
+
img.onload = () => {
|
|
329
|
+
if (!cancelled) setImageReady(true)
|
|
330
|
+
}
|
|
331
|
+
img.onerror = () => {
|
|
332
|
+
if (cancelled) return
|
|
333
|
+
if (attempt < maxAttempts) {
|
|
334
|
+
attempt += 1
|
|
335
|
+
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 8000)
|
|
336
|
+
retryTimer = setTimeout(loadAtlas, delay)
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
img.src = definition.atlasUrl
|
|
322
340
|
}
|
|
323
|
-
|
|
341
|
+
|
|
342
|
+
loadAtlas()
|
|
343
|
+
|
|
324
344
|
return () => {
|
|
325
345
|
cancelled = true
|
|
326
|
-
|
|
346
|
+
if (retryTimer !== undefined) clearTimeout(retryTimer)
|
|
347
|
+
if (activeImg !== null) {
|
|
348
|
+
activeImg.onload = null
|
|
349
|
+
activeImg.onerror = null
|
|
350
|
+
}
|
|
327
351
|
}
|
|
328
352
|
}, [definition.atlasUrl, props.visual])
|
|
329
353
|
|
|
@@ -488,6 +512,9 @@ export function PetSprite(props: PetSpriteProps): ReactPortal {
|
|
|
488
512
|
const pos = dragPos ?? { right: display.right, bottom: display.bottom }
|
|
489
513
|
const spriteWidth = Math.round(cell.width * spriteScale)
|
|
490
514
|
const spriteHeight = Math.round(cell.height * spriteScale)
|
|
515
|
+
// Bubble typography follows the sprite's own scale (#1549), bounded so a
|
|
516
|
+
// shrunk pet never carries unreadably small text.
|
|
517
|
+
const bubbleScale = bubbleScaleFor(display)
|
|
491
518
|
|
|
492
519
|
// Concurrent sessions share one bubble slot: only the display session
|
|
493
520
|
// speaks by default, and the rest hide behind a '+N' badge until the stack
|
|
@@ -553,7 +580,13 @@ export function PetSprite(props: PetSpriteProps): ReactPortal {
|
|
|
553
580
|
<div
|
|
554
581
|
ref={floatRef}
|
|
555
582
|
className={styles.float}
|
|
556
|
-
style={{
|
|
583
|
+
style={{
|
|
584
|
+
right: pos.right,
|
|
585
|
+
bottom: pos.bottom,
|
|
586
|
+
zIndex: 2147483000,
|
|
587
|
+
// Read by .bubble / .bubbleStatus in pet.module.css.
|
|
588
|
+
...({ '--pet-bubble-scale': String(bubbleScale) } as CSSProperties),
|
|
589
|
+
}}
|
|
557
590
|
onPointerEnter={() => {
|
|
558
591
|
clearHideTimer()
|
|
559
592
|
setHovered(true)
|
|
@@ -117,7 +117,7 @@ function snapshot(view: PetGameplayStateView, skin?: string): PetStateView {
|
|
|
117
117
|
sessionActive: false,
|
|
118
118
|
sessions: [],
|
|
119
119
|
affinity: { points: 0, rank: '幼鲸', rankEmoji: '*', pets: 0, feeds: 0, turns: 0, petCooldown: false, feedCooldown: false },
|
|
120
|
-
display: { visible: true, size: 160, right: 24, bottom: 20 },
|
|
120
|
+
display: { visible: true, size: 160, right: 24, bottom: 20, bubbleScale: 1 },
|
|
121
121
|
pet: { id: 'miku', displayName: 'Miku', description: '' },
|
|
122
122
|
name: 'Miku',
|
|
123
123
|
treats: { stocked: 0, max: 5 },
|
package/src/client/locales.ts
CHANGED
|
@@ -69,6 +69,8 @@ export const zh = {
|
|
|
69
69
|
'settings.visibleHint': '关闭后宠物隐藏,可从聊天输入区重新召唤。',
|
|
70
70
|
'settings.size': '大小(px)',
|
|
71
71
|
'settings.sizeHint': '精灵单元高度,范围 32–1024。',
|
|
72
|
+
'settings.bubbleScale': '气泡字号倍率',
|
|
73
|
+
'settings.bubbleScaleHint': '状态、碎碎念与用量气泡的字号会随宠物大小自动缩放;这一项是在此基础上再乘一个倍率(0.5–2,默认 1)。字号本身不小于 10px、不大于 24px。',
|
|
72
74
|
'settings.right': '距右侧(px)',
|
|
73
75
|
'settings.rightHint': '距视口右边缘的水平内缩距离。',
|
|
74
76
|
'settings.bottom': '距底部(px)',
|
|
@@ -153,6 +155,8 @@ export const en = {
|
|
|
153
155
|
'settings.visibleHint': 'When off, the pet hides; summon it again from the input row.',
|
|
154
156
|
'settings.size': 'Size (px)',
|
|
155
157
|
'settings.sizeHint': 'Sprite cell height, 32\u20131024.',
|
|
158
|
+
'settings.bubbleScale': 'Bubble text multiplier',
|
|
159
|
+
'settings.bubbleScaleHint': 'Status, whisper, and usage bubbles scale with the pet automatically; this multiplies that result (0.5\u20132, default 1). The rendered text stays between 10px and 24px.',
|
|
156
160
|
'settings.right': 'Right inset (px)',
|
|
157
161
|
'settings.rightHint': 'Horizontal inset from the viewport right edge.',
|
|
158
162
|
'settings.bottom': 'Bottom inset (px)',
|
|
@@ -3,6 +3,20 @@ import { describe, expect, it } from 'vitest'
|
|
|
3
3
|
|
|
4
4
|
const css = readFileSync(new URL('./pet.module.css', import.meta.url), 'utf8')
|
|
5
5
|
|
|
6
|
+
describe('pet bubble typography (#1549)', () => {
|
|
7
|
+
it('scales the bubble text and padding from the sprite-supplied variable', () => {
|
|
8
|
+
const bubble = css.match(/\.bubble\s*\{([^}]*)\}/)?.[1] ?? ''
|
|
9
|
+
expect(bubble).toContain('font-size: calc(12px * var(--pet-bubble-scale, 1))')
|
|
10
|
+
expect(bubble).toContain('padding: calc(4px * var(--pet-bubble-scale, 1)) calc(10px * var(--pet-bubble-scale, 1))')
|
|
11
|
+
expect(bubble).not.toContain('font-size: 12px')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('keeps the status bubble width in proportion with its text', () => {
|
|
15
|
+
const status = css.match(/\.bubbleStatus\s*\{([^}]*)\}/)?.[1] ?? ''
|
|
16
|
+
expect(status).toContain('max-width: min(calc(280px * var(--pet-bubble-scale, 1)), calc(100vw - 24px))')
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
|
|
6
20
|
describe('pet hover panel css', () => {
|
|
7
21
|
it('anchors the panel below the pet', () => {
|
|
8
22
|
const panel = css.match(/\.panel\s*\{([^}]*)\}/)?.[1] ?? ''
|
|
@@ -21,7 +21,7 @@ const snapshot = (over: Partial<PetStateView> = {}): PetStateView => ({
|
|
|
21
21
|
petCooldown: false,
|
|
22
22
|
feedCooldown: false,
|
|
23
23
|
},
|
|
24
|
-
display: { visible: true, size: 160, right: 24, bottom: 120 },
|
|
24
|
+
display: { visible: true, size: 160, right: 24, bottom: 120, bubbleScale: 1 },
|
|
25
25
|
pet: { id: 'whale-girl', displayName: 'Whale', description: '' },
|
|
26
26
|
name: 'Whale',
|
|
27
27
|
treats: { stocked: 2, max: 10 },
|
|
@@ -26,9 +26,12 @@
|
|
|
26
26
|
bottom: 100%;
|
|
27
27
|
margin-bottom: 6px;
|
|
28
28
|
white-space: nowrap;
|
|
29
|
-
|
|
29
|
+
/* Bubble typography follows the pet's own scale (#1549): the sprite sets
|
|
30
|
+
--pet-bubble-scale from its display config, and 1 keeps the 12px baseline
|
|
31
|
+
this stylesheet was drawn for. */
|
|
32
|
+
padding: calc(4px * var(--pet-bubble-scale, 1)) calc(10px * var(--pet-bubble-scale, 1));
|
|
30
33
|
border-radius: 999px;
|
|
31
|
-
font-size: 12px;
|
|
34
|
+
font-size: calc(12px * var(--pet-bubble-scale, 1));
|
|
32
35
|
line-height: 1.4;
|
|
33
36
|
color: #f4f7ff;
|
|
34
37
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
|
|
@@ -50,10 +53,10 @@
|
|
|
50
53
|
碎碎念不再单独渲染第二只气泡,也不换色调,而是以 .bubbleWhisper
|
|
51
54
|
接管这同一只气泡的文案,气泡栈内所有气泡共用同一片玻璃。 */
|
|
52
55
|
.bubbleStatus {
|
|
53
|
-
max-width: min(280px, calc(100vw - 24px));
|
|
56
|
+
max-width: min(calc(280px * var(--pet-bubble-scale, 1)), calc(100vw - 24px));
|
|
54
57
|
overflow: hidden;
|
|
55
58
|
text-overflow: ellipsis;
|
|
56
|
-
padding: 5px 12px;
|
|
59
|
+
padding: calc(5px * var(--pet-bubble-scale, 1)) calc(12px * var(--pet-bubble-scale, 1));
|
|
57
60
|
background: linear-gradient(160deg, rgba(19, 28, 54, 0.9), rgba(7, 11, 26, 0.94));
|
|
58
61
|
border: 1px solid rgba(126, 152, 255, 0.45);
|
|
59
62
|
box-shadow:
|
package/src/index.ts
CHANGED
|
@@ -18,7 +18,7 @@ import z from 'schemastery'
|
|
|
18
18
|
import { PetService, PET_SETTINGS_NAMESPACE, type PetConfig, type PetSettingsSection } from './service.ts'
|
|
19
19
|
import { makePetRoutes } from './routes.ts'
|
|
20
20
|
import { loadPetRegistry, petPackageRoot } from './registry.ts'
|
|
21
|
-
import { DISPLAY_INSET_MAX, DISPLAY_SIZE_MAX, DISPLAY_SIZE_MIN } from './persist.ts'
|
|
21
|
+
import { BUBBLE_SCALE_MAX, BUBBLE_SCALE_MIN, DISPLAY_INSET_MAX, DISPLAY_SIZE_MAX, DISPLAY_SIZE_MIN } from './persist.ts'
|
|
22
22
|
import { mountOnce } from './mount-once.ts'
|
|
23
23
|
|
|
24
24
|
export { PetService, MAX_SESSION_BUBBLES } from './service.ts'
|
|
@@ -132,6 +132,7 @@ export function makePetSettingsSchema(fallbackPetId: string) {
|
|
|
132
132
|
size: z.number().step(1).min(DISPLAY_SIZE_MIN).max(DISPLAY_SIZE_MAX).default(160),
|
|
133
133
|
right: z.number().step(1).min(0).max(DISPLAY_INSET_MAX).default(24),
|
|
134
134
|
bottom: z.number().step(1).min(0).max(DISPLAY_INSET_MAX).default(20),
|
|
135
|
+
bubbleScale: z.number().step(0.05).min(BUBBLE_SCALE_MIN).max(BUBBLE_SCALE_MAX).default(1),
|
|
135
136
|
petId: z.string().default(fallbackPetId),
|
|
136
137
|
enabled: z.boolean().default(true),
|
|
137
138
|
decorationEnabled: z.boolean().default(true),
|
package/src/persist.test.ts
CHANGED
|
@@ -5,6 +5,11 @@ import { join } from 'node:path'
|
|
|
5
5
|
import { AFFINITY_MAX, emptyAffinity } from './affinity.ts'
|
|
6
6
|
import { defaultTreatConfig, emptyTreatLedger } from './treats.ts'
|
|
7
7
|
import {
|
|
8
|
+
BUBBLE_FONT_MAX_PX,
|
|
9
|
+
BUBBLE_FONT_MIN_PX,
|
|
10
|
+
BUBBLE_SCALE_MAX,
|
|
11
|
+
BUBBLE_SCALE_MIN,
|
|
12
|
+
bubbleScaleFor,
|
|
8
13
|
DEFAULT_PET_ID,
|
|
9
14
|
DISPLAY_INSET_MAX,
|
|
10
15
|
DISPLAY_SIZE_MAX,
|
|
@@ -52,7 +57,7 @@ describe('loadPetPersist', () => {
|
|
|
52
57
|
skins: { otter: 'lanhainishang' },
|
|
53
58
|
affinity: { ...emptyAffinity(), points: 42, pets: 3, feeds: 1, turns: 10 },
|
|
54
59
|
treats: { ...emptyTreatLedger(), treats: 7, lastTreatGrantAt: 1234, turnsAtLastTreatGrant: 9 },
|
|
55
|
-
display: { visible: false, size: 200, right: 10, bottom: 40 },
|
|
60
|
+
display: { visible: false, size: 200, right: 10, bottom: 40, bubbleScale: 1.25 },
|
|
56
61
|
gameplay: {
|
|
57
62
|
otter: { stats: { hunger: 55.5 }, currencies: { coins: 12 }, mode: 'work' as const, settledAt: 777 },
|
|
58
63
|
},
|
|
@@ -164,6 +169,53 @@ describe('loadPetPersist', () => {
|
|
|
164
169
|
}
|
|
165
170
|
})
|
|
166
171
|
|
|
172
|
+
it('defaults the bubble scale to the untouched baseline (#1549)', () => {
|
|
173
|
+
const dir = tempDir()
|
|
174
|
+
try {
|
|
175
|
+
expect(loadPetPersist(dir).display.bubbleScale).toBe(1)
|
|
176
|
+
} finally {
|
|
177
|
+
rmSync(dir, { recursive: true, force: true })
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('clamps a persisted bubble scale into the supported range (#1549)', () => {
|
|
182
|
+
const dir = tempDir()
|
|
183
|
+
try {
|
|
184
|
+
writeFileSync(join(dir, 'pet.json'), JSON.stringify({ display: { visible: true, size: 160, right: 0, bottom: 0, bubbleScale: 99 } }), 'utf8')
|
|
185
|
+
expect(loadPetPersist(dir).display.bubbleScale).toBe(BUBBLE_SCALE_MAX)
|
|
186
|
+
writeFileSync(join(dir, 'pet.json'), JSON.stringify({ display: { visible: true, size: 160, right: 0, bottom: 0, bubbleScale: 'big' } }), 'utf8')
|
|
187
|
+
expect(loadPetPersist(dir).display.bubbleScale).toBe(defaultDisplayConfig.bubbleScale)
|
|
188
|
+
writeFileSync(join(dir, 'pet.json'), JSON.stringify({ display: { visible: true, size: 160, right: 0, bottom: 0, bubbleScale: -3 } }), 'utf8')
|
|
189
|
+
expect(loadPetPersist(dir).display.bubbleScale).toBe(BUBBLE_SCALE_MIN)
|
|
190
|
+
} finally {
|
|
191
|
+
rmSync(dir, { recursive: true, force: true })
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('follows the pet size and stays inside the readable band (#1549)', () => {
|
|
196
|
+
// The baseline the stylesheet was drawn for: 12px at the default 160px pet.
|
|
197
|
+
expect(bubbleScaleFor({ size: 160, bubbleScale: 1 })).toBe(1)
|
|
198
|
+
// A shrunk pet shrinks the bubble, but never below the floor (the ratio
|
|
199
|
+
// is rounded to two decimals so the same config always paints the same).
|
|
200
|
+
expect(bubbleScaleFor({ size: 100, bubbleScale: 1 })).toBe(0.83)
|
|
201
|
+
expect(bubbleScaleFor({ size: 32, bubbleScale: 1 })).toBe(0.83)
|
|
202
|
+
// An enlarged pet caps the bubble instead of growing it without bound.
|
|
203
|
+
expect(bubbleScaleFor({ size: 1024, bubbleScale: 1 })).toBe(BUBBLE_FONT_MAX_PX / 12)
|
|
204
|
+
// The user's multiplier rides on top of the automatic following.
|
|
205
|
+
expect(bubbleScaleFor({ size: 160, bubbleScale: 2 })).toBe(2)
|
|
206
|
+
expect(bubbleScaleFor({ size: 160, bubbleScale: 0.5 })).toBe(0.83)
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('keeps the baseline when a host omits or corrupts the bubble scale (#1549)', () => {
|
|
210
|
+
// A host that predates the field serves no bubbleScale. NaN would reach
|
|
211
|
+
// --pet-bubble-scale and collapse every bubble's text to zero.
|
|
212
|
+
expect(bubbleScaleFor({ size: 160 })).toBe(1)
|
|
213
|
+
expect(bubbleScaleFor({ size: 160, bubbleScale: Number.NaN })).toBe(1)
|
|
214
|
+
expect(bubbleScaleFor({ size: Number.NaN, bubbleScale: 1 })).toBe(1)
|
|
215
|
+
// A corrupt size must not leak through either; the multiplier still applies.
|
|
216
|
+
expect(bubbleScaleFor({ size: Number.POSITIVE_INFINITY, bubbleScale: 2 })).toBe(2)
|
|
217
|
+
})
|
|
218
|
+
|
|
167
219
|
it('clamps oversized display size to the max', () => {
|
|
168
220
|
const dir = tempDir()
|
|
169
221
|
try {
|
package/src/persist.ts
CHANGED
|
@@ -25,6 +25,13 @@ export interface PetDisplayConfig {
|
|
|
25
25
|
right: number
|
|
26
26
|
/** Vertical inset from the viewport bottom edge, px. */
|
|
27
27
|
bottom: number
|
|
28
|
+
/**
|
|
29
|
+
* Multiplier on the bubble typography's automatic size following (#1549).
|
|
30
|
+
* The rendered bubble scale is `size / 160 * bubbleScale`, bounded by
|
|
31
|
+
* {@link BUBBLE_FONT_MIN_PX}..{@link BUBBLE_FONT_MAX_PX}, so 1 keeps the
|
|
32
|
+
* 12px baseline the stylesheet was drawn for at the default 160px pet.
|
|
33
|
+
*/
|
|
34
|
+
bubbleScale: number
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
export const defaultDisplayConfig: PetDisplayConfig = {
|
|
@@ -32,6 +39,7 @@ export const defaultDisplayConfig: PetDisplayConfig = {
|
|
|
32
39
|
size: 160,
|
|
33
40
|
right: 24,
|
|
34
41
|
bottom: 120,
|
|
42
|
+
bubbleScale: 1,
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
/** Display value bounds (shared by load-time validation and setConfig). */
|
|
@@ -39,6 +47,42 @@ export const DISPLAY_SIZE_MIN = 32
|
|
|
39
47
|
export const DISPLAY_SIZE_MAX = 1024
|
|
40
48
|
export const DISPLAY_INSET_MAX = 10_000
|
|
41
49
|
|
|
50
|
+
/** Bubble typography bounds (issue #1549). */
|
|
51
|
+
export const BUBBLE_SCALE_MIN = 0.5
|
|
52
|
+
export const BUBBLE_SCALE_MAX = 2
|
|
53
|
+
/** Pixel size the bubble stylesheet was drawn for, at the default pet size. */
|
|
54
|
+
export const BUBBLE_BASE_FONT_PX = 12
|
|
55
|
+
/** Pet size that baseline matches; other sizes scale the bubble with them. */
|
|
56
|
+
export const BUBBLE_BASE_SIZE_PX = 160
|
|
57
|
+
/** Readability floor and layout ceiling of the scaled bubble text. */
|
|
58
|
+
export const BUBBLE_FONT_MIN_PX = 10
|
|
59
|
+
export const BUBBLE_FONT_MAX_PX = 24
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Bubble typography scale for one display config (issue #1549): the bubble
|
|
63
|
+
* follows the pet's own size so a shrunk pet does not carry a full-size
|
|
64
|
+
* bubble, and the user's multiplier rides on top. The result is a CSS ratio
|
|
65
|
+
* against {@link BUBBLE_BASE_FONT_PX}, bounded so the text never drops below
|
|
66
|
+
* the readability floor or outgrows the pet. `bubbleScale` is optional at
|
|
67
|
+
* runtime: a host that predates the field (a rolling update, or any snapshot
|
|
68
|
+
* that omits it) falls back to the baseline, because a NaN ratio written into
|
|
69
|
+
* `--pet-bubble-scale` collapses every bubble's text to zero.
|
|
70
|
+
* @param display - display config; `bubbleScale` may be absent on older hosts.
|
|
71
|
+
* @returns the ratio written to `--pet-bubble-scale` (always finite).
|
|
72
|
+
*/
|
|
73
|
+
export function bubbleScaleFor(display: Pick<PetDisplayConfig, 'size'> & { bubbleScale?: number }): number {
|
|
74
|
+
// Guard the arithmetic, not just the clamp: Math.min/max propagate NaN, so a
|
|
75
|
+
// missing or non-finite field would otherwise reach the stylesheet.
|
|
76
|
+
const size = Number.isFinite(display.size) ? display.size : BUBBLE_BASE_SIZE_PX
|
|
77
|
+
const multiplier = typeof display.bubbleScale === 'number' && Number.isFinite(display.bubbleScale)
|
|
78
|
+
? display.bubbleScale
|
|
79
|
+
: 1
|
|
80
|
+
const scaled = (size / BUBBLE_BASE_SIZE_PX) * multiplier
|
|
81
|
+
const min = BUBBLE_FONT_MIN_PX / BUBBLE_BASE_FONT_PX
|
|
82
|
+
const max = BUBBLE_FONT_MAX_PX / BUBBLE_BASE_FONT_PX
|
|
83
|
+
return Math.round(Math.min(max, Math.max(min, scaled)) * 100) / 100
|
|
84
|
+
}
|
|
85
|
+
|
|
42
86
|
/** Everything persisted for the pet. */
|
|
43
87
|
export interface PetPersist {
|
|
44
88
|
/** Selected pet id (a registry entry; clamped at service startup). */
|
|
@@ -198,6 +242,8 @@ export function loadPetPersist(dir: string = petHomeDir()): PetPersist {
|
|
|
198
242
|
size: Math.round(Math.min(DISPLAY_SIZE_MAX, Math.max(DISPLAY_SIZE_MIN, finiteNum(rawDisplay.size, base.display.size)))),
|
|
199
243
|
right: Math.round(clamp(finiteNum(rawDisplay.right, base.display.right), DISPLAY_INSET_MAX)),
|
|
200
244
|
bottom: Math.round(clamp(finiteNum(rawDisplay.bottom, base.display.bottom), DISPLAY_INSET_MAX)),
|
|
245
|
+
// Fractional on purpose: the multiplier is a ratio, not a pixel count.
|
|
246
|
+
bubbleScale: Math.min(BUBBLE_SCALE_MAX, Math.max(BUBBLE_SCALE_MIN, finiteNum(rawDisplay.bubbleScale, base.display.bubbleScale))),
|
|
201
247
|
}
|
|
202
248
|
const petId = typeof parsed.petId === 'string' && parsed.petId.trim() !== ''
|
|
203
249
|
? parsed.petId.trim()
|
package/src/service.ts
CHANGED
|
@@ -31,6 +31,8 @@ import {
|
|
|
31
31
|
DEFAULT_PET_NAME,
|
|
32
32
|
DISPLAY_INSET_MAX,
|
|
33
33
|
DISPLAY_SIZE_MAX,
|
|
34
|
+
BUBBLE_SCALE_MAX,
|
|
35
|
+
BUBBLE_SCALE_MIN,
|
|
34
36
|
DISPLAY_SIZE_MIN,
|
|
35
37
|
PET_NAME_MAX_LENGTH,
|
|
36
38
|
loadPetPersist,
|
|
@@ -111,6 +113,8 @@ export interface PetSettingsSection {
|
|
|
111
113
|
right: number
|
|
112
114
|
/** Vertical inset from the viewport bottom edge, px. */
|
|
113
115
|
bottom: number
|
|
116
|
+
/** Bubble typography multiplier (#1549); see PetDisplayConfig. */
|
|
117
|
+
bubbleScale?: number
|
|
114
118
|
/** Master switch for the plugin (browser half + host routes). */
|
|
115
119
|
enabled?: boolean
|
|
116
120
|
/**
|
|
@@ -756,12 +760,13 @@ export class PetService extends Service {
|
|
|
756
760
|
return { ok: true, skin }
|
|
757
761
|
}
|
|
758
762
|
|
|
759
|
-
/** RPC: update display config (size / position).
|
|
763
|
+
/** RPC: update display config (size / position / bubble scale). Pixel values are clamped to whole pixels. */
|
|
760
764
|
async setConfig(patch: Partial<PetDisplayConfig>): Promise<{ ok: true; display: PetDisplayConfig }> {
|
|
761
765
|
const next = { ...this.ledger.snapshot.display, ...patch }
|
|
762
766
|
next.size = Math.round(Math.min(DISPLAY_SIZE_MAX, Math.max(DISPLAY_SIZE_MIN, next.size)))
|
|
763
767
|
next.right = Math.round(Math.min(DISPLAY_INSET_MAX, Math.max(0, next.right)))
|
|
764
768
|
next.bottom = Math.round(Math.min(DISPLAY_INSET_MAX, Math.max(0, next.bottom)))
|
|
769
|
+
next.bubbleScale = Math.min(BUBBLE_SCALE_MAX, Math.max(BUBBLE_SCALE_MIN, next.bubbleScale))
|
|
765
770
|
this.ledger.setDisplay(next)
|
|
766
771
|
this.flush()
|
|
767
772
|
this.syncSettingsFromPet()
|
|
@@ -800,6 +805,7 @@ export class PetService extends Service {
|
|
|
800
805
|
next.size = Math.round(Math.min(DISPLAY_SIZE_MAX, Math.max(DISPLAY_SIZE_MIN, section.size)))
|
|
801
806
|
next.right = Math.round(Math.min(DISPLAY_INSET_MAX, Math.max(0, section.right)))
|
|
802
807
|
next.bottom = Math.round(Math.min(DISPLAY_INSET_MAX, Math.max(0, section.bottom)))
|
|
808
|
+
next.bubbleScale = Math.min(BUBBLE_SCALE_MAX, Math.max(BUBBLE_SCALE_MIN, section.bubbleScale ?? next.bubbleScale))
|
|
803
809
|
this.ledger.setDisplay(next)
|
|
804
810
|
this.flush()
|
|
805
811
|
}
|