@skinhub/viewer 0.3.3 → 0.4.2
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/EMBED.md +85 -8
- package/dist/SkinViewer.d.ts.map +1 -1
- package/dist/SkinViewer.js +2 -1
- package/dist/SkinViewer.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/item.d.ts +2 -1
- package/dist/item.d.ts.map +1 -1
- package/dist/item.js +30 -0
- package/dist/item.js.map +1 -1
- package/dist/protocol.d.ts +29 -2
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js.map +1 -1
- package/dist/state.d.ts +6 -2
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +88 -7
- package/dist/state.js.map +1 -1
- package/dist/types.d.ts +97 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +12 -2
- package/dist/types.js.map +1 -1
- package/dist/weapons.d.ts +7 -4
- package/dist/weapons.d.ts.map +1 -1
- package/dist/weapons.js +17 -2
- package/dist/weapons.js.map +1 -1
- package/package.json +1 -1
- package/src/SkinViewer.tsx +2 -1
- package/src/index.ts +10 -3
- package/src/item.ts +33 -0
- package/src/protocol.ts +28 -2
- package/src/state.ts +97 -6
- package/src/types.ts +131 -6
- package/src/weapons.ts +18 -3
package/src/SkinViewer.tsx
CHANGED
|
@@ -200,8 +200,9 @@ export const SkinViewer = (props: SkinViewerProps) => {
|
|
|
200
200
|
* whose query briefly returned `undefined` - must not record "no item" as the thing the frame is
|
|
201
201
|
* showing, or the next real item would be diffed against a hole and sent in full for no reason.
|
|
202
202
|
*/
|
|
203
|
+
const previous = sent.current
|
|
203
204
|
sent.current = { ...next, item: next.item ?? sent.current.item }
|
|
204
|
-
if (coversCanvas(patch, next)) {
|
|
205
|
+
if (coversCanvas(patch, next, previous)) {
|
|
205
206
|
setStatus('loading')
|
|
206
207
|
// A NEW ITEM IS A NEW CHANCE. A lost GL context or a 404 on one model says nothing about the
|
|
207
208
|
// next one, so the fallback comes down and the frame is allowed to try. A protocol mismatch is
|
package/src/index.ts
CHANGED
|
@@ -6,14 +6,15 @@
|
|
|
6
6
|
* <SkinViewer item={{ weapon: 'weapon_ak47', paintIndex: 1449, float: 0.27 }} style={{ height: 420 }} />
|
|
7
7
|
* <SkinViewer inspectLink={tradeOffer.inspectLink} style={{ height: 420 }} />
|
|
8
8
|
*
|
|
9
|
-
* A weapon (or a glove) is one of
|
|
9
|
+
* A weapon (or a glove) is one of SEVEN ways to name a subject, and each is its own prop:
|
|
10
10
|
*
|
|
11
11
|
* <SkinViewer sticker={{ id: 37, wear: 0.2 }} /> one sticker, the real holo/foil shader
|
|
12
12
|
* <SkinViewer charm={{ id: 5 }} /> one charm, off the gun
|
|
13
13
|
* <SkinViewer collectible={{ id: 874 }} /> one pin, coin, medal or trophy
|
|
14
14
|
* <SkinViewer operator={{ id: 5036 }} /> one agent, alone
|
|
15
|
+
* <SkinViewer pet={{ id: 4, stage: 'hen' }} /> one chicken pet
|
|
15
16
|
*
|
|
16
|
-
* Exactly one of the
|
|
17
|
+
* Exactly one of the seven, enforced in the types - see `ViewerSubject`.
|
|
17
18
|
*
|
|
18
19
|
* The 3D is not in here. It is a page on our origin that this component embeds and drives over
|
|
19
20
|
* `postMessage`, which is why installing this pulls in no `three`, no `@react-three/fiber` and no
|
|
@@ -60,6 +61,12 @@ export { fromInspectLink, toInspectLink, toPlacement } from './item.js'
|
|
|
60
61
|
|
|
61
62
|
export type {
|
|
62
63
|
MapName,
|
|
64
|
+
PetBackdrop,
|
|
65
|
+
PetEffect,
|
|
66
|
+
PetHat,
|
|
67
|
+
PetLook,
|
|
68
|
+
PetNames,
|
|
69
|
+
PetStage,
|
|
63
70
|
SkinViewerCharm,
|
|
64
71
|
SkinViewerError,
|
|
65
72
|
SkinViewerErrorCode,
|
|
@@ -67,7 +74,6 @@ export type {
|
|
|
67
74
|
SkinViewerItem,
|
|
68
75
|
SkinViewerProps,
|
|
69
76
|
SkinViewerSticker,
|
|
70
|
-
TimeOfDay,
|
|
71
77
|
ViewerAgent,
|
|
72
78
|
ViewerBackground,
|
|
73
79
|
ViewerCameraSettings,
|
|
@@ -80,6 +86,7 @@ export type {
|
|
|
80
86
|
ViewerLocaleSettings,
|
|
81
87
|
ViewerOperatorSubject,
|
|
82
88
|
ViewerOverlaySettings,
|
|
89
|
+
ViewerPetSubject,
|
|
83
90
|
ViewerQualitySettings,
|
|
84
91
|
ViewerResize,
|
|
85
92
|
ViewerSettings,
|
package/src/item.ts
CHANGED
|
@@ -37,6 +37,7 @@ import type {
|
|
|
37
37
|
FrameCharm,
|
|
38
38
|
FrameCollectible,
|
|
39
39
|
FrameItem,
|
|
40
|
+
FramePet,
|
|
40
41
|
FrameSticker,
|
|
41
42
|
FrameSubjectKind,
|
|
42
43
|
PlacementSlots,
|
|
@@ -215,6 +216,7 @@ export type ResolvedStandalone = {
|
|
|
215
216
|
sticker?: FrameSticker
|
|
216
217
|
charm?: FrameCharm
|
|
217
218
|
collectible?: FrameCollectible
|
|
219
|
+
pet?: FramePet
|
|
218
220
|
agent?: { id: number; pose?: string | null }
|
|
219
221
|
error: SkinViewerError | null
|
|
220
222
|
}
|
|
@@ -251,6 +253,37 @@ export const resolveStandalone = (props: Partial<ViewerSubject>): ResolvedStanda
|
|
|
251
253
|
? { subject: 'agent', agent: dropUndefined({ id: props.operator.id, pose: props.operator.pose }), error: null }
|
|
252
254
|
: bad('agent', 'operator', props.operator.id)
|
|
253
255
|
|
|
256
|
+
/* A PET CARRIES ITS WHOLE LOOK IN ONE GROUP - stage, colour group, seed, pose and the look sliders -
|
|
257
|
+
and only the id is identity. `look` is copied so a host mutating its own object cannot change what
|
|
258
|
+
the next diff compares against. */
|
|
259
|
+
if (props.pet)
|
|
260
|
+
return ok(props.pet.id)
|
|
261
|
+
? {
|
|
262
|
+
subject: 'pet',
|
|
263
|
+
pet: dropUndefined({
|
|
264
|
+
id: props.pet.id,
|
|
265
|
+
stage: props.pet.stage,
|
|
266
|
+
variant: props.pet.variant,
|
|
267
|
+
petSeed: props.pet.petSeed,
|
|
268
|
+
pose: props.pet.pose,
|
|
269
|
+
look: props.pet.look
|
|
270
|
+
? {
|
|
271
|
+
...(props.pet.look.attributes && { attributes: { ...props.pet.look.attributes } }),
|
|
272
|
+
...(props.pet.look.shape && { shape: { ...props.pet.look.shape } }),
|
|
273
|
+
}
|
|
274
|
+
: props.pet.look,
|
|
275
|
+
// The photo booth and the names (0.4.2). `names` copied for `look`'s reason.
|
|
276
|
+
hat: props.pet.hat,
|
|
277
|
+
backdrop: props.pet.backdrop,
|
|
278
|
+
light: props.pet.light,
|
|
279
|
+
effect: props.pet.effect,
|
|
280
|
+
names: props.pet.names ? { ...props.pet.names } : props.pet.names,
|
|
281
|
+
nameLabel: props.pet.nameLabel,
|
|
282
|
+
}),
|
|
283
|
+
error: null,
|
|
284
|
+
}
|
|
285
|
+
: bad('pet', 'pet', props.pet.id)
|
|
286
|
+
|
|
254
287
|
return null
|
|
255
288
|
}
|
|
256
289
|
|
package/src/protocol.ts
CHANGED
|
@@ -134,7 +134,7 @@ export type FrameLocale = {
|
|
|
134
134
|
export type FrameSettings = {
|
|
135
135
|
camera?: { fov?: number; defaultZoom?: number }
|
|
136
136
|
quality?: { bloom?: number; bloomSpill?: number; renderScale?: number; antialias?: boolean; shadows?: boolean }
|
|
137
|
-
environment?: { map?: string | null;
|
|
137
|
+
environment?: { map?: string | null; rain?: boolean; background?: string }
|
|
138
138
|
overlays?: {
|
|
139
139
|
stickerGizmo?: boolean
|
|
140
140
|
charmGizmo?: boolean
|
|
@@ -158,12 +158,36 @@ export type FrameInteractions = {
|
|
|
158
158
|
* the person an agent, and an integrator holding a weapon-modifier prop already named `agent` needs a
|
|
159
159
|
* different word for the standalone picture. One rename, in one file.
|
|
160
160
|
*/
|
|
161
|
-
export type FrameSubjectKind = 'weapon' | 'sticker' | 'charm' | 'collectible' | 'agent'
|
|
161
|
+
export type FrameSubjectKind = 'weapon' | 'sticker' | 'charm' | 'collectible' | 'agent' | 'pet'
|
|
162
162
|
|
|
163
163
|
/** The three standalone item groups, in the frame's words. An id, and at most one number. */
|
|
164
164
|
export type FrameSticker = { id: number; wear?: number }
|
|
165
165
|
export type FrameCharm = { id: number; pattern?: number }
|
|
166
166
|
export type FrameCollectible = { id: number }
|
|
167
|
+
/**
|
|
168
|
+
* One chicken pet (0.4.1). `id` is the `pet_definitions` row. Identity is `id` and, for a breed, a
|
|
169
|
+
* `stage` move between pullet and hen (the frame re-frames and says `ready` again); the rest update in
|
|
170
|
+
* place. `variant: null` lets the seed pick the colour group, `pose: null` is the idle.
|
|
171
|
+
*/
|
|
172
|
+
export type FramePet = {
|
|
173
|
+
id: number
|
|
174
|
+
stage?: 'egg' | 'chick' | 'pullet' | 'hen'
|
|
175
|
+
variant?: number | null
|
|
176
|
+
petSeed?: number
|
|
177
|
+
pose?: string | null
|
|
178
|
+
look?: { attributes?: Record<string, number>; shape?: Record<string, number> } | null
|
|
179
|
+
/*
|
|
180
|
+
* THE PHOTO BOOTH AND THE NAMES (0.4.2) - all cheap, none of them identity. Plain strings on the wire;
|
|
181
|
+
* the frame checks each against its own table and names a word it does not know (`types.ts` has the
|
|
182
|
+
* unions). `names` REPLACES the held set rather than merging per stage.
|
|
183
|
+
*/
|
|
184
|
+
hat?: string | null
|
|
185
|
+
backdrop?: string | null
|
|
186
|
+
light?: string | null
|
|
187
|
+
effect?: string | null
|
|
188
|
+
names?: { chick?: string; pullet?: string; hen?: string } | null
|
|
189
|
+
nameLabel?: boolean
|
|
190
|
+
}
|
|
167
191
|
|
|
168
192
|
/**
|
|
169
193
|
* Everything `/frame` holds. One field per prop of the renderer that a host can set.
|
|
@@ -179,6 +203,7 @@ export type FrameState = {
|
|
|
179
203
|
sticker: FrameSticker
|
|
180
204
|
charm: FrameCharm
|
|
181
205
|
collectible: FrameCollectible
|
|
206
|
+
pet: FramePet
|
|
182
207
|
view: 'gun' | 'hands' | 'agent'
|
|
183
208
|
agent: { id: number; pose?: string | null }
|
|
184
209
|
gloves: { type: string; paintIndex: number; float?: number; seed?: number } | null
|
|
@@ -217,6 +242,7 @@ export type FramePatch = {
|
|
|
217
242
|
sticker?: Partial<FrameSticker>
|
|
218
243
|
charm?: Partial<FrameCharm>
|
|
219
244
|
collectible?: Partial<FrameCollectible>
|
|
245
|
+
pet?: Partial<FramePet>
|
|
220
246
|
view?: FrameState['view']
|
|
221
247
|
agent?: Partial<FrameState['agent']>
|
|
222
248
|
gloves?: FrameState['gloves']
|
package/src/state.ts
CHANGED
|
@@ -38,6 +38,7 @@ import type {
|
|
|
38
38
|
FrameItem,
|
|
39
39
|
FrameLabels,
|
|
40
40
|
FramePatch,
|
|
41
|
+
FramePet,
|
|
41
42
|
FrameSettings,
|
|
42
43
|
FrameSticker,
|
|
43
44
|
FrameSubjectKind,
|
|
@@ -79,6 +80,7 @@ export type DesiredState = {
|
|
|
79
80
|
sticker?: FrameSticker
|
|
80
81
|
charm?: FrameCharm
|
|
81
82
|
collectible?: FrameCollectible
|
|
83
|
+
pet?: FramePet
|
|
82
84
|
/** The integrator's own inspect link, forwarded verbatim as `?i=`. See `item.ts`. */
|
|
83
85
|
inspectPayload: string | null
|
|
84
86
|
help: HelpReason | null
|
|
@@ -168,6 +170,7 @@ export const resolveState = (props: Partial<SkinViewerProps>): DesiredState => {
|
|
|
168
170
|
...(standalone.sticker && { sticker: standalone.sticker }),
|
|
169
171
|
...(standalone.charm && { charm: standalone.charm }),
|
|
170
172
|
...(standalone.collectible && { collectible: standalone.collectible }),
|
|
173
|
+
...(standalone.pet && { pet: standalone.pet }),
|
|
171
174
|
inspectPayload: null,
|
|
172
175
|
help: standalone.error ? HELP_FOR[standalone.error.code] : null,
|
|
173
176
|
subjectError: standalone.error,
|
|
@@ -290,6 +293,34 @@ export const frameUrl = (origin: string, desired: DesiredState): { src: string;
|
|
|
290
293
|
num(params, 'pattern', desired.charm.pattern)
|
|
291
294
|
}
|
|
292
295
|
if (desired.collectible) params.set('collectible', String(desired.collectible.id))
|
|
296
|
+
/*
|
|
297
|
+
* THE PET, and its `?pose=` is the pet's clip: the frame reads `?pose=` as a pet clip whenever
|
|
298
|
+
* `?pet=` is present, and an operator's pose has nothing to animate on a pet page. `?petseed=` and
|
|
299
|
+
* not `?seed=` for the charm's reason - `seed` is the weapon's paint seed on this URL.
|
|
300
|
+
*/
|
|
301
|
+
if (desired.pet) {
|
|
302
|
+
const pet = desired.pet
|
|
303
|
+
params.set('pet', String(pet.id))
|
|
304
|
+
if (pet.stage) params.set('stage', pet.stage)
|
|
305
|
+
if (pet.variant !== undefined) params.set('variant', pet.variant === null ? '' : String(pet.variant))
|
|
306
|
+
num(params, 'petseed', pet.petSeed)
|
|
307
|
+
if (pet.pose) params.set('pose', pet.pose)
|
|
308
|
+
const look = petLookParam(pet.look)
|
|
309
|
+
if (look) params.set('look', look)
|
|
310
|
+
/* THE PHOTO BOOTH AND THE NAMES (0.4.2). `null` is written as the frame's explicit off (`none`, or an
|
|
311
|
+
empty `?light=`) so a first paint says exactly what the prop says; absent says nothing. The names
|
|
312
|
+
go per stage (`?chickname=`...), never `?name=`, which the frame reads against a stage the host
|
|
313
|
+
may not have named - and only the named ones: a fresh frame's names are already none. */
|
|
314
|
+
if (pet.hat !== undefined) params.set('hat', pet.hat ?? 'none')
|
|
315
|
+
if (pet.backdrop !== undefined) params.set('backdrop', pet.backdrop ?? 'none')
|
|
316
|
+
if (pet.light !== undefined) params.set('light', pet.light?.replace(/^#/, '') ?? '')
|
|
317
|
+
if (pet.effect !== undefined) params.set('fx', pet.effect ?? 'none')
|
|
318
|
+
for (const stage of PET_NAMED_STAGES) {
|
|
319
|
+
const name = pet.names?.[stage]
|
|
320
|
+
if (name) params.set(`${stage}name`, name)
|
|
321
|
+
}
|
|
322
|
+
if (pet.nameLabel !== undefined) flag(params, 'namelabel', pet.nameLabel)
|
|
323
|
+
}
|
|
293
324
|
|
|
294
325
|
if (item) {
|
|
295
326
|
params.set('weapon', item.weaponType)
|
|
@@ -314,7 +345,7 @@ export const frameUrl = (origin: string, desired: DesiredState): { src: string;
|
|
|
314
345
|
|
|
315
346
|
if (desired.view) params.set('view', desired.view)
|
|
316
347
|
if (desired.agent?.id !== undefined) params.set('agent', String(desired.agent.id))
|
|
317
|
-
if (desired.agent?.pose) params.set('pose', desired.agent.pose)
|
|
348
|
+
if (desired.agent?.pose && !desired.pet?.pose) params.set('pose', desired.agent.pose)
|
|
318
349
|
if (desired.gloves !== undefined) params.set('glove', desired.gloves ? gloveParam(desired.gloves) : 'none')
|
|
319
350
|
|
|
320
351
|
const s = desired.settings
|
|
@@ -327,7 +358,6 @@ export const frameUrl = (origin: string, desired: DesiredState): { src: string;
|
|
|
327
358
|
flag(params, 'shadows', s?.quality?.shadows)
|
|
328
359
|
// `?map=none` is the calibrated reference rig, which is what `map: null` means on the prop.
|
|
329
360
|
if (s?.environment?.map !== undefined) params.set('map', s.environment.map ?? 'none')
|
|
330
|
-
if (s?.environment?.timeOfDay) params.set('time', s.environment.timeOfDay)
|
|
331
361
|
flag(params, 'rain', s?.environment?.rain)
|
|
332
362
|
if (s?.environment?.background) params.set('bg', s.environment.background)
|
|
333
363
|
flag(params, 'stickergizmo', s?.overlays?.stickerGizmo)
|
|
@@ -383,6 +413,15 @@ export const frameUrl = (origin: string, desired: DesiredState): { src: string;
|
|
|
383
413
|
return { src: `${origin.replace(/\/+$/, '')}/frame?${params.toString()}`, expressed }
|
|
384
414
|
}
|
|
385
415
|
|
|
416
|
+
/** `$ChickenHue:0.42,fatness:0.7` - the frame's `?look=`, keys sorted so one look is one URL. */
|
|
417
|
+
const petLookParam = (look: FramePet['look']): string | null => {
|
|
418
|
+
const pairs = [...Object.entries(look?.attributes ?? {}), ...Object.entries(look?.shape ?? {})]
|
|
419
|
+
.filter(([, value]) => Number.isFinite(value))
|
|
420
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
421
|
+
.map(([key, value]) => `${key}:${Math.round(Math.min(Math.max(value, 0), 1) * 1000) / 1000}`)
|
|
422
|
+
return pairs.length ? pairs.join(',') : null
|
|
423
|
+
}
|
|
424
|
+
|
|
386
425
|
/** `type:paintIndex[:float[:seed]]` - one param and not four, because they are one item. */
|
|
387
426
|
const gloveParam = (gloves: ViewerGloves) =>
|
|
388
427
|
[gloves.type, gloves.paintIndex, gloves.float, gloves.seed].filter(part => part !== undefined).join(':')
|
|
@@ -496,6 +535,13 @@ export const diffState = (previous: DesiredState, next: DesiredState): FramePatc
|
|
|
496
535
|
patch.collectible = collectible
|
|
497
536
|
changed = true
|
|
498
537
|
}
|
|
538
|
+
/* The pet's `look` is the one nested field in any standalone group, and a host rebuilds it on every
|
|
539
|
+
render - so it is compared by value, or every render would post a `set`. */
|
|
540
|
+
const pet = diffGroup(previous.pet, next.pet, { look: petLookEqual, names: petNamesEqual })
|
|
541
|
+
if (pet) {
|
|
542
|
+
patch.pet = pet
|
|
543
|
+
changed = true
|
|
544
|
+
}
|
|
499
545
|
|
|
500
546
|
if (next.view !== undefined && next.view !== previous.view) {
|
|
501
547
|
patch.view = next.view
|
|
@@ -555,19 +601,34 @@ export const diffState = (previous: DesiredState, next: DesiredState): FramePatc
|
|
|
555
601
|
* wire, and the alternative reading would make a conditional prop destructive. Same rule as
|
|
556
602
|
* {@link diffSettings} one level in.
|
|
557
603
|
*/
|
|
558
|
-
const diffGroup = <T extends object>(
|
|
604
|
+
const diffGroup = <T extends object>(
|
|
605
|
+
previous: T | undefined,
|
|
606
|
+
next: T | undefined,
|
|
607
|
+
/** Per-key equality for the rare nested field; everything else is `Object.is`. */
|
|
608
|
+
equal: { [K in keyof T]?: (a: T[K], b: T[K]) => boolean } = {},
|
|
609
|
+
): Partial<T> | undefined => {
|
|
559
610
|
if (!next) return undefined
|
|
560
611
|
if (!previous) return { ...next }
|
|
561
612
|
const out: Partial<T> = {}
|
|
562
613
|
let changed = false
|
|
563
614
|
for (const key of Object.keys(next) as (keyof T)[])
|
|
564
|
-
if (!Object.is(previous[key], next[key])) {
|
|
615
|
+
if (!(equal[key] ?? Object.is)(previous[key], next[key])) {
|
|
565
616
|
out[key] = next[key]
|
|
566
617
|
changed = true
|
|
567
618
|
}
|
|
568
619
|
return changed ? out : undefined
|
|
569
620
|
}
|
|
570
621
|
|
|
622
|
+
const petLookEqual = (a: FramePet['look'], b: FramePet['look']) =>
|
|
623
|
+
a === b || (!!a && !!b && petLookParam(a) === petLookParam(b))
|
|
624
|
+
|
|
625
|
+
/** The stages a pet can be named at, in order - the egg cannot be. */
|
|
626
|
+
const PET_NAMED_STAGES = ['chick', 'pullet', 'hen'] as const
|
|
627
|
+
|
|
628
|
+
/** `names` is the other nested pet field a host rebuilds every render - compared by value like `look`. */
|
|
629
|
+
const petNamesEqual = (a: FramePet['names'], b: FramePet['names']) =>
|
|
630
|
+
a === b || (!!a && !!b && PET_NAMED_STAGES.every(stage => (a[stage] ?? '') === (b[stage] ?? '')))
|
|
631
|
+
|
|
571
632
|
const glovesEqual = (a: ViewerGloves | null | undefined, b: ViewerGloves | null | undefined) => {
|
|
572
633
|
if (a === b) return true
|
|
573
634
|
if (!a || !b) return false
|
|
@@ -638,18 +699,48 @@ const localeEqual = (a: FrameSettings['locale'], b: FrameSettings['locale']) =>
|
|
|
638
699
|
* IT TAKES THE WHOLE NEXT STATE rather than just the view, because one of the answers depends on which
|
|
639
700
|
* SUBJECT the patch lands on: an operator's id covers under `subject: 'agent'` for the same reason it
|
|
640
701
|
* covers under `view: 'agent'`, and does not under `hands`.
|
|
702
|
+
*
|
|
703
|
+
* AND THE PREVIOUS ONE, OPTIONALLY, for the pet's stage (see {@link drawnPetStage}): a stage patch only
|
|
704
|
+
* says the raw value moved, and whether the DRAWN stage moved depends on where it came from.
|
|
641
705
|
*/
|
|
642
|
-
export const coversCanvas = (
|
|
706
|
+
export const coversCanvas = (
|
|
707
|
+
patch: FramePatch,
|
|
708
|
+
next: Pick<DesiredState, 'subject' | 'view' | 'pet'>,
|
|
709
|
+
previous?: Pick<DesiredState, 'pet'>,
|
|
710
|
+
): boolean => {
|
|
643
711
|
// A different KIND of subject is a different renderer. Always a reload, in every direction.
|
|
644
712
|
if (patch.subject !== undefined) return true
|
|
645
713
|
if (patch.view !== undefined) return true
|
|
646
714
|
if (patch.item && IDENTITY_FIELDS.some(field => patch.item?.[field] !== undefined)) return true
|
|
647
715
|
// An id is identity for all four standalone subjects; their second field (`wear`, `pattern`) is not.
|
|
648
|
-
if (
|
|
716
|
+
if (
|
|
717
|
+
patch.sticker?.id !== undefined ||
|
|
718
|
+
patch.charm?.id !== undefined ||
|
|
719
|
+
patch.collectible?.id !== undefined ||
|
|
720
|
+
patch.pet?.id !== undefined
|
|
721
|
+
)
|
|
649
722
|
return true
|
|
723
|
+
// A breed's stage is identity too: pullet and hen are one model, but the frame re-frames the bird
|
|
724
|
+
// and says `ready` again. Only when the drawn stage moves - anything else never gets that `ready`.
|
|
725
|
+
if (patch.pet?.stage !== undefined && previous && drawnPetStage(next.pet) !== drawnPetStage(previous.pet)) return true
|
|
650
726
|
// The operator is identity when they ARE the subject, and in the `agent` view, where their
|
|
651
727
|
// `<Suspense>` tears the subtree down; cheap in `hands`, where the arms are already mounted. The
|
|
652
728
|
// asymmetry is the renderer's, not ours.
|
|
653
729
|
if (patch.agent?.id !== undefined && (next.subject === 'agent' || next.view === 'agent')) return true
|
|
654
730
|
return false
|
|
655
731
|
}
|
|
732
|
+
|
|
733
|
+
/** The three breeds (`3` Catalana, `4` Silkie, `5` Polish) - the only pets with more than one stage. */
|
|
734
|
+
const PET_BREED_IDS: readonly number[] = [3, 4, 5]
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* *** THE STAGE THE FRAME WILL DRAW, OR `undefined` WHEN THE STAGE CANNOT CHANGE THE PICTURE. ***
|
|
738
|
+
*
|
|
739
|
+
* The frame clamps a stage to what the definition can be: an egg is only an egg, a chick only a chick,
|
|
740
|
+
* and a breed is a `pullet` or (anything else, or nothing) a `hen`. So `{ id: 3 }` -> `{ id: 3, stage:
|
|
741
|
+
* 'hen' }` is a patch that draws the same bird, and raising `loading` for it would wait for a `ready`
|
|
742
|
+
* that never comes. An id this package does not know is answered `undefined` for the same reason: a
|
|
743
|
+
* missed cover costs a few frames, a false one leaves the host's loading slot up for good.
|
|
744
|
+
*/
|
|
745
|
+
const drawnPetStage = (pet: FramePet | undefined) =>
|
|
746
|
+
pet && PET_BREED_IDS.includes(pet.id) ? (pet.stage === 'pullet' ? 'pullet' : 'hen') : undefined
|
package/src/types.ts
CHANGED
|
@@ -160,6 +160,8 @@ type ItemConfiguration = {
|
|
|
160
160
|
* `charm` identity `id`, cheap `pattern`
|
|
161
161
|
* `collectible` identity `id`, and NOTHING is cheap - there is no other field to change
|
|
162
162
|
* `operator` identity `id`, cheap `pose`
|
|
163
|
+
* `pet` identity `id` and a breed's drawn `stage`, cheap `variant`, `petSeed`, `pose`, `look`,
|
|
164
|
+
* and (0.4.2) `hat`, `backdrop`, `light`, `effect`, `names`, `nameLabel`
|
|
163
165
|
*
|
|
164
166
|
* *** CHANGING WHICH SUBJECT YOU PASS IS ALWAYS AN IDENTITY CHANGE, *** in every direction: a weapon
|
|
165
167
|
* and a pin are drawn by different renderers, so the picture is rebuilt from nothing.
|
|
@@ -301,6 +303,121 @@ export type ViewerOperatorSubject = {
|
|
|
301
303
|
pose?: string | null
|
|
302
304
|
}
|
|
303
305
|
|
|
306
|
+
/** A pet's growth stage - the item's `upgrade level` attribute, 0 egg .. 3 hen. */
|
|
307
|
+
export type PetStage = 'egg' | 'chick' | 'pullet' | 'hen'
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* The look sliders: per-key overrides over what the pet seed rolls, every value `0`..`1`. `attributes`
|
|
311
|
+
* is keyed by render attribute (`$ChickenHue`), `shape` by body characteristic (`fatness`). A key you do
|
|
312
|
+
* not pass is the seed's.
|
|
313
|
+
*/
|
|
314
|
+
export type PetLook = { attributes?: Record<string, number>; shape?: Record<string, number> }
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* ONE CHICKEN PET (CS2 1.41.8.2) - what `/pet/:id` shows on our own site.
|
|
318
|
+
*
|
|
319
|
+
* `id` is the `pet_definitions` row, which is the `pet id` attribute on the item: `1` the egg, `2` the
|
|
320
|
+
* chick, `3` Catalana, `4` Silkie, `5` Polish. Everything else is how that pet looks:
|
|
321
|
+
*
|
|
322
|
+
* `stage` `'egg' | 'chick' | 'pullet' | 'hen'`. An egg is only an egg and a chick only a chick;
|
|
323
|
+
* the three breeds are a pullet or a hen (the default). IDENTITY for a breed - see below.
|
|
324
|
+
* `variant` the colour group index, or `null` (default) to let the seed pick it. Updates IN PLACE.
|
|
325
|
+
* `petSeed` the `pet seed` attribute, a uint32. Default `0`. Updates IN PLACE. What a seed looks
|
|
326
|
+
* like is our closest reconstruction of the game's rule and may differ slightly.
|
|
327
|
+
* `pose` a clip name from the pet's own model, or `null` (default) for its idle. IN PLACE. On the
|
|
328
|
+
* egg only, `'chicknegg_hatch01'` / `'chicknegg_hatch02'` play the hatch (the shell breaks
|
|
329
|
+
* and the chick climbs out; 02 is the shorter take). Any other pet idles on those two.
|
|
330
|
+
* `look` per-key overrides of the seed's roll - see {@link PetLook}. IN PLACE.
|
|
331
|
+
*
|
|
332
|
+
* IDENTITY IS `id` AND `stage`. A different id is a different model. A breed's pullet and hen are the
|
|
333
|
+
* same model, but the stage swaps the body proportions, so moving between them re-frames the bird:
|
|
334
|
+
* `loading` goes up and `onReady` fires again, the same as for a new id, only faster. A stage the pet
|
|
335
|
+
* cannot be (a hen for the egg) is drawn as the stage it can, and changes nothing. Everything else
|
|
336
|
+
* re-renders the bird already on screen.
|
|
337
|
+
*/
|
|
338
|
+
export type ViewerPetSubject = {
|
|
339
|
+
id: number
|
|
340
|
+
stage?: PetStage
|
|
341
|
+
variant?: number | null
|
|
342
|
+
petSeed?: number
|
|
343
|
+
pose?: string | null
|
|
344
|
+
look?: PetLook | null
|
|
345
|
+
/* ── The photo booth and the names (0.4.2). All update IN PLACE and none sends `onReady` again; only
|
|
346
|
+
`backdrop` moves the camera (see there). ── */
|
|
347
|
+
/** A photo booth hat, or `null` (default) for none. Ignored on the egg, which has no head. */
|
|
348
|
+
hat?: PetHat | null
|
|
349
|
+
/**
|
|
350
|
+
* The photo studio's paper backdrop behind the bird, by colour, or `null` (default) for none.
|
|
351
|
+
*
|
|
352
|
+
* IT MOVES THE CAMERA: switching one on seats the camera where the game's booth camera stands, frames
|
|
353
|
+
* the bird looser (room for a hat and the effects) and limits a drag to 30 degrees either side of the
|
|
354
|
+
* seat. A view you pinned (`settings.camera`) keeps its seat; the looser framing and the drag limit
|
|
355
|
+
* still apply, centred on it. Switching it off gives the camera back.
|
|
356
|
+
*/
|
|
357
|
+
backdrop?: PetBackdrop | null
|
|
358
|
+
/**
|
|
359
|
+
* The studio key light, six hex digits (`'ffe0c0'`, a leading `#` is accepted), or `null` (default) for
|
|
360
|
+
* the booth's own warm white `fff2e6`. It lights the studio, so it only shows with a {@link backdrop}.
|
|
361
|
+
*/
|
|
362
|
+
light?: string | null
|
|
363
|
+
/**
|
|
364
|
+
* A photo booth effect, replaying until you clear it, or `null` (default) for none. Not drawn on the egg.
|
|
365
|
+
* The game offers `beam`, `lasers` and `sparks` from a pullet on; this draws what you ask for on a chick too.
|
|
366
|
+
*/
|
|
367
|
+
effect?: PetEffect | null
|
|
368
|
+
/**
|
|
369
|
+
* The pet's names, one per growth stage, as the item stores them (the egg cannot be named). Each is cut to
|
|
370
|
+
* 20 characters and loses `{ } < >`, as the game's own box does. A stage without a name goes by the
|
|
371
|
+
* nearest named one - its own, then the younger stages, then the older ones. Passing `names` REPLACES
|
|
372
|
+
* the set: send every stage you want kept.
|
|
373
|
+
*/
|
|
374
|
+
names?: PetNames | null
|
|
375
|
+
/** Draw the shown stage's name above the bird. Default `false`. */
|
|
376
|
+
nameLabel?: boolean
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** The ten photo booth hats, by the game's own names. */
|
|
380
|
+
export type PetHat =
|
|
381
|
+
| 'helmet'
|
|
382
|
+
| 'armor'
|
|
383
|
+
| 'alien'
|
|
384
|
+
| 'banana'
|
|
385
|
+
| 'glasses'
|
|
386
|
+
| 'nose_glasses'
|
|
387
|
+
| 'party'
|
|
388
|
+
| 'sprout'
|
|
389
|
+
| 'top_hat'
|
|
390
|
+
| 'wizard_hat'
|
|
391
|
+
|
|
392
|
+
/** The photo studio's paper colours. `wallpaper` exists in the game files but the booth never offers it. */
|
|
393
|
+
export type PetBackdrop =
|
|
394
|
+
| 'grey'
|
|
395
|
+
| 'blue'
|
|
396
|
+
| 'green'
|
|
397
|
+
| 'purple'
|
|
398
|
+
| 'yellow'
|
|
399
|
+
| 'brown'
|
|
400
|
+
| 'red'
|
|
401
|
+
| 'black'
|
|
402
|
+
| 'sky'
|
|
403
|
+
| 'abstract'
|
|
404
|
+
| 'wallpaper'
|
|
405
|
+
|
|
406
|
+
/** The nine photo booth effects, by the game's particle names (`lasers`, `feathers`). */
|
|
407
|
+
export type PetEffect =
|
|
408
|
+
| 'explosion'
|
|
409
|
+
| 'lightning'
|
|
410
|
+
| 'fire'
|
|
411
|
+
| 'beam'
|
|
412
|
+
| 'lasers'
|
|
413
|
+
| 'sparks'
|
|
414
|
+
| 'confetti'
|
|
415
|
+
| 'bubbles'
|
|
416
|
+
| 'feathers'
|
|
417
|
+
|
|
418
|
+
/** One name per stage - the item's `custom name attr` (chick), `... 2` (pullet) and `... 3` (hen). */
|
|
419
|
+
export type PetNames = { chick?: string; pullet?: string; hen?: string }
|
|
420
|
+
|
|
304
421
|
/* ═════════════════════════════════════════════════════════════════════════════════════════════
|
|
305
422
|
* PRESENTATION
|
|
306
423
|
* ═══════════════════════════════════════════════════════════════════════════════════════════ */
|
|
@@ -371,9 +488,17 @@ export type ViewerGloves = {
|
|
|
371
488
|
* upgraded. That is the deliberate trade - the rule is *"no back-compat
|
|
372
489
|
* boilerplate, upgrade when you need to"* - and the failure is legible either way: the frame reports
|
|
373
490
|
* an unknown map in its `problems` and keeps the lighting it had.
|
|
491
|
+
*
|
|
492
|
+
* *** THREE NAMES MOVED IN 0.4.0 AND THAT IS THE RULE BEING CHARGED. *** There was a `timeOfDay`
|
|
493
|
+
* field beside {@link ViewerEnvironmentSettings.map}, and a map that had two videos was named by the
|
|
494
|
+
* PAIR. It is one field now: `'Ancient'` became `'Ancient (Day)'` and `'Ancient (Night)'`, and
|
|
495
|
+
* `'Train'` became `'Train (Night)'` - a correction as much as a rename, since `de_train` ships one
|
|
496
|
+
* vanity scene and that scene is night. An embed still passing `'Ancient'` reports an unknown map in
|
|
497
|
+
* `problems` and keeps the lighting it had, which is this list's documented failure and not a new one.
|
|
374
498
|
*/
|
|
375
499
|
export const MAP_NAMES = [
|
|
376
|
-
'Ancient',
|
|
500
|
+
'Ancient (Day)',
|
|
501
|
+
'Ancient (Night)',
|
|
377
502
|
'Anubis',
|
|
378
503
|
'Baggage',
|
|
379
504
|
'Cache',
|
|
@@ -384,13 +509,12 @@ export const MAP_NAMES = [
|
|
|
384
509
|
'Nuke',
|
|
385
510
|
'Office',
|
|
386
511
|
'Overpass',
|
|
387
|
-
'Train',
|
|
512
|
+
'Train (Night)',
|
|
388
513
|
'Vertigo',
|
|
389
514
|
'Warehouse',
|
|
390
515
|
] as const
|
|
391
516
|
|
|
392
517
|
export type MapName = (typeof MAP_NAMES)[number]
|
|
393
|
-
export type TimeOfDay = 'Day' | 'Night'
|
|
394
518
|
|
|
395
519
|
/**
|
|
396
520
|
* What is BEHIND the item.
|
|
@@ -466,7 +590,7 @@ export type ViewerQualitySettings = {
|
|
|
466
590
|
|
|
467
591
|
export type ViewerEnvironmentSettings = {
|
|
468
592
|
/**
|
|
469
|
-
* WHICH MAP'S LIGHT. Default `'
|
|
593
|
+
* WHICH MAP'S LIGHT. Default `'Dust II'`; `null` is our calibrated reference rig, which is what
|
|
470
594
|
* every fidelity measurement behind this renderer was taken against.
|
|
471
595
|
*
|
|
472
596
|
* *** THIS IS THE LIGHT, {@link background} IS THE PICTURE, AND THEY ARE SEPARATE ON PURPOSE. ***
|
|
@@ -475,8 +599,6 @@ export type ViewerEnvironmentSettings = {
|
|
|
475
599
|
* your page behind it.
|
|
476
600
|
*/
|
|
477
601
|
map?: MapName | null
|
|
478
|
-
/** Default `'Night'`. Falls back on its own for a map that has only one. */
|
|
479
|
-
timeOfDay?: TimeOfDay
|
|
480
602
|
/** Wet surfaces on maps whose own data says it rains. Default `true`. */
|
|
481
603
|
rain?: boolean
|
|
482
604
|
/** See {@link ViewerBackground}. Default `'transparent'`. */
|
|
@@ -716,6 +838,8 @@ type SubjectArms = {
|
|
|
716
838
|
charm: ViewerCharmSubject
|
|
717
839
|
collectible: ViewerCollectibleSubject
|
|
718
840
|
operator: ViewerOperatorSubject
|
|
841
|
+
/** Added in 0.4.1 - a seventh arm on the same rule. See {@link ViewerPetSubject}. */
|
|
842
|
+
pet: ViewerPetSubject
|
|
719
843
|
}
|
|
720
844
|
|
|
721
845
|
/** One arm present, the other five forbidden. Written once so six arms cannot disagree about five. */
|
|
@@ -730,6 +854,7 @@ export type ViewerSubject =
|
|
|
730
854
|
| OnlySubject<'charm'>
|
|
731
855
|
| OnlySubject<'collectible'>
|
|
732
856
|
| OnlySubject<'operator'>
|
|
857
|
+
| OnlySubject<'pet'>
|
|
733
858
|
|
|
734
859
|
export type SkinViewerProps = ViewerSubject & {
|
|
735
860
|
/* ── Presentation ──────────────────────────────────────────────────────────────────────── */
|
package/src/weapons.ts
CHANGED
|
@@ -16,7 +16,10 @@
|
|
|
16
16
|
* it is derived from the export itself (`data/skins.json`, `weapon.weapon_id` → `weapon.id`,
|
|
17
17
|
* generated 2026-08-15, verified: 63 distinct defindexes, no defindex mapping to two DIFFERENT
|
|
18
18
|
* weapons — the 20 knives each carry a second `sfui_wpnhud_*` alias for their vanilla row, which
|
|
19
|
-
* `getWeaponModelPath` already resolves to the same model).
|
|
19
|
+
* `getWeaponModelPath` already resolves to the same model). Plus one hand-added row, the C4
|
|
20
|
+
* (49 -> `weapon_c4`, since CS2 1.41.8.2 put stickers on it): the 2026-08-15 `skins.json` the other
|
|
21
|
+
* 63 came from predates the 1.41.8.2 export and has no C4. From that export on it carries a vanilla
|
|
22
|
+
* `weapon_c4` row with `weapon_id: 49`, so the row is derivable like the rest - 64 rows in all.
|
|
20
23
|
*
|
|
21
24
|
* The alternative is fetching a 4.4 MB `skins.json` to answer "what is 7", which would make the
|
|
22
25
|
* inspect-link path — the one the product exists for — cost four megabytes before the first frame.
|
|
@@ -33,7 +36,7 @@
|
|
|
33
36
|
*
|
|
34
37
|
* This is `skin.weapon.id` in `@skinhub/cdn`'s `skins.json` rows, verbatim — so
|
|
35
38
|
* `<SkinViewer weapon={row.weapon.id} paintIndex={…} />` typechecks against a catalogue row with no
|
|
36
|
-
* conversion. Type it into an editor and autocomplete lists all
|
|
39
|
+
* conversion. Type it into an editor and autocomplete lists all 72.
|
|
37
40
|
*/
|
|
38
41
|
export const WEAPON_IDS = [
|
|
39
42
|
// Pistols
|
|
@@ -77,6 +80,8 @@ export const WEAPON_IDS = [
|
|
|
77
80
|
'weapon_xm1014',
|
|
78
81
|
// Other
|
|
79
82
|
'weapon_taser',
|
|
83
|
+
// The C4 - no finish, but stickers, a charm and a name tag since CS2 1.41.8.2. See the defindex table.
|
|
84
|
+
'weapon_c4',
|
|
80
85
|
// Knives
|
|
81
86
|
'weapon_bayonet',
|
|
82
87
|
'weapon_knife_butterfly',
|
|
@@ -117,7 +122,7 @@ export type KnownWeaponId = (typeof WEAPON_IDS)[number]
|
|
|
117
122
|
* `'weapon_ak47'`.
|
|
118
123
|
*
|
|
119
124
|
* The `(string & {})` arm is deliberate and is not a widening mistake: it keeps autocomplete listing
|
|
120
|
-
* the
|
|
125
|
+
* the 72 known ids while still ACCEPTING an id this build has never heard of, so a new weapon in a
|
|
121
126
|
* fresh export renders the day it ships rather than the day the package is upgraded. An unknown id
|
|
122
127
|
* that the asset export also does not know resolves to no model, which surfaces as `unknown-weapon`.
|
|
123
128
|
*/
|
|
@@ -161,6 +166,16 @@ export const WEAPON_ID_BY_DEFINDEX: Readonly<Record<number, KnownWeaponId>> = {
|
|
|
161
166
|
38: 'weapon_scar20',
|
|
162
167
|
39: 'weapon_sg556',
|
|
163
168
|
40: 'weapon_ssg08',
|
|
169
|
+
/**
|
|
170
|
+
* THE ONE ROW ADDED BY HAND, because the `skins.json` this table was derived from predates the
|
|
171
|
+
* 1.41.8.2 export. From that export on, `skins.json` carries a vanilla C4 row (`weapon_id: 49`) and
|
|
172
|
+
* this row is derivable like the other 63. CS2 1.41.8.2 (2026-09-22) gave the `c4` prefab
|
|
173
|
+
* `weapon_supports_stickers`, so a stickered C4 is now a real item with an inspect link that says
|
|
174
|
+
* `defindex: 49` - and without this row it resolved to `unknown-weapon`. `weapon_c4` is the item
|
|
175
|
+
* definition's own `name` in `items_game.txt`, and the renderer's model table has carried it since
|
|
176
|
+
* the same update.
|
|
177
|
+
*/
|
|
178
|
+
49: 'weapon_c4',
|
|
164
179
|
60: 'weapon_m4a1_silencer',
|
|
165
180
|
61: 'weapon_usp_silencer',
|
|
166
181
|
63: 'weapon_cz75a',
|