@skinhub/viewer 0.3.2 → 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 +105 -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 +45 -5
- 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 +105 -7
- 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 +2 -2
- package/src/SkinViewer.tsx +2 -1
- package/src/index.ts +10 -3
- package/src/item.ts +48 -5
- package/src/protocol.ts +28 -2
- package/src/state.ts +97 -6
- package/src/types.ts +139 -7
- package/src/weapons.ts +18 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skinhub/viewer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "React wrapper for the SkinHub CS2 skin viewer embed — props in, postMessage out. No three.js, no renderer, no asset bundle; React is the only peer.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"react": ">=18"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@skinhub/cdn": ">=0.
|
|
57
|
+
"@skinhub/cdn": ">=0.3.0"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@types/bun": "latest",
|
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,
|
|
@@ -84,9 +85,13 @@ export const toSlots = (
|
|
|
84
85
|
slot,
|
|
85
86
|
sticker_id: sticker.id,
|
|
86
87
|
wear: sticker.wear ?? 0,
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
|
|
88
|
+
// `scale` is not on the public surface because nothing in CS2 sets it per sticker, and 0 is
|
|
89
|
+
// how the wire says "no size chosen" - what the protobuf decodes to when the field is
|
|
90
|
+
// ABSENT, which is every item CS2's own sticker UI has ever made. The frame never reads it
|
|
91
|
+
// (its `stickerSlots.ts` takes `uvScale` from the weapon's markup and says so on line 9),
|
|
92
|
+
// and `@skinhub/cdn` >= 0.3.0 omits a non-positive scale from a link rather than refusing
|
|
93
|
+
// to build one - so a link built here now carries exactly what CS2 would carry.
|
|
94
|
+
scale: 0,
|
|
90
95
|
rotation: sticker.rotation ?? 0,
|
|
91
96
|
offset_x: sticker.offsetX ?? 0,
|
|
92
97
|
offset_y: sticker.offsetY ?? 0,
|
|
@@ -103,6 +108,10 @@ export const toSlots = (
|
|
|
103
108
|
offset_x: charm.offset?.[0] ?? 0,
|
|
104
109
|
offset_y: charm.offset?.[1] ?? 0,
|
|
105
110
|
offset_z: charm.offset?.[2] ?? 0,
|
|
111
|
+
// The sticker sealed inside a `Charm | Sticker Slab`. 0 is "nothing sealed", which is every
|
|
112
|
+
// charm this package can be handed: `SkinViewerCharm` has no field for it, because a slab is
|
|
113
|
+
// a distinct econ item rather than a property of a charm someone hangs on a gun.
|
|
114
|
+
wrapped_sticker: 0,
|
|
106
115
|
}
|
|
107
116
|
|
|
108
117
|
return slots
|
|
@@ -207,6 +216,7 @@ export type ResolvedStandalone = {
|
|
|
207
216
|
sticker?: FrameSticker
|
|
208
217
|
charm?: FrameCharm
|
|
209
218
|
collectible?: FrameCollectible
|
|
219
|
+
pet?: FramePet
|
|
210
220
|
agent?: { id: number; pose?: string | null }
|
|
211
221
|
error: SkinViewerError | null
|
|
212
222
|
}
|
|
@@ -243,6 +253,37 @@ export const resolveStandalone = (props: Partial<ViewerSubject>): ResolvedStanda
|
|
|
243
253
|
? { subject: 'agent', agent: dropUndefined({ id: props.operator.id, pose: props.operator.pose }), error: null }
|
|
244
254
|
: bad('agent', 'operator', props.operator.id)
|
|
245
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
|
+
|
|
246
287
|
return null
|
|
247
288
|
}
|
|
248
289
|
|
|
@@ -377,8 +418,10 @@ export const toPublicItem = (item: FrameItem): SkinViewerItem => {
|
|
|
377
418
|
*
|
|
378
419
|
* - `stattrak: 0` is a REAL, freshly-minted counter and `false` is no module. One boolean and one
|
|
379
420
|
* count, not one nullable number.
|
|
380
|
-
* - an unset sticker `scale` is `
|
|
381
|
-
*
|
|
421
|
+
* - an unset sticker `scale` is `0`, meaning "no size chosen", and the encoder OMITS it rather than
|
|
422
|
+
* refusing the link. `g_vStickerNScale` is an inverse uv scale authored 4.63..35.0, so 1 was never
|
|
423
|
+
* an identity - it is a sticker five to thirty-five times too large, on a field the frame and the
|
|
424
|
+
* game both ignore.
|
|
382
425
|
* - a charm's seed rides in `pattern`, because the keychain message is the sticker message reused.
|
|
383
426
|
*
|
|
384
427
|
* That list is the argument for these living here rather than in a docs snippet: they are the same
|
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
|
@@ -58,7 +58,14 @@ export type SkinViewerSticker = {
|
|
|
58
58
|
wear?: number
|
|
59
59
|
/** Degrees, any value; `370` renders as `10`. Default `0`. */
|
|
60
60
|
rotation?: number
|
|
61
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* How far from the slot's authored position to sit, in the game's own offset space. Default `0`.
|
|
63
|
+
*
|
|
64
|
+
* NOT bounded to -0.5..0.5, which this used to say. That range is the material editor's slider,
|
|
65
|
+
* not a limit CS2 enforces - 160 of the 316 authored slots have legal area outside it. The real
|
|
66
|
+
* bound is the weapon's authored `StickerMarkup` region, which is not a square, and the frame
|
|
67
|
+
* applies it to a drag for you.
|
|
68
|
+
*/
|
|
62
69
|
offsetX?: number
|
|
63
70
|
offsetY?: number
|
|
64
71
|
}
|
|
@@ -153,6 +160,8 @@ type ItemConfiguration = {
|
|
|
153
160
|
* `charm` identity `id`, cheap `pattern`
|
|
154
161
|
* `collectible` identity `id`, and NOTHING is cheap - there is no other field to change
|
|
155
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`
|
|
156
165
|
*
|
|
157
166
|
* *** CHANGING WHICH SUBJECT YOU PASS IS ALWAYS AN IDENTITY CHANGE, *** in every direction: a weapon
|
|
158
167
|
* and a pin are drawn by different renderers, so the picture is rebuilt from nothing.
|
|
@@ -294,6 +303,121 @@ export type ViewerOperatorSubject = {
|
|
|
294
303
|
pose?: string | null
|
|
295
304
|
}
|
|
296
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
|
+
|
|
297
421
|
/* ═════════════════════════════════════════════════════════════════════════════════════════════
|
|
298
422
|
* PRESENTATION
|
|
299
423
|
* ═══════════════════════════════════════════════════════════════════════════════════════════ */
|
|
@@ -364,9 +488,17 @@ export type ViewerGloves = {
|
|
|
364
488
|
* upgraded. That is the deliberate trade - the rule is *"no back-compat
|
|
365
489
|
* boilerplate, upgrade when you need to"* - and the failure is legible either way: the frame reports
|
|
366
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.
|
|
367
498
|
*/
|
|
368
499
|
export const MAP_NAMES = [
|
|
369
|
-
'Ancient',
|
|
500
|
+
'Ancient (Day)',
|
|
501
|
+
'Ancient (Night)',
|
|
370
502
|
'Anubis',
|
|
371
503
|
'Baggage',
|
|
372
504
|
'Cache',
|
|
@@ -377,13 +509,12 @@ export const MAP_NAMES = [
|
|
|
377
509
|
'Nuke',
|
|
378
510
|
'Office',
|
|
379
511
|
'Overpass',
|
|
380
|
-
'Train',
|
|
512
|
+
'Train (Night)',
|
|
381
513
|
'Vertigo',
|
|
382
514
|
'Warehouse',
|
|
383
515
|
] as const
|
|
384
516
|
|
|
385
517
|
export type MapName = (typeof MAP_NAMES)[number]
|
|
386
|
-
export type TimeOfDay = 'Day' | 'Night'
|
|
387
518
|
|
|
388
519
|
/**
|
|
389
520
|
* What is BEHIND the item.
|
|
@@ -459,7 +590,7 @@ export type ViewerQualitySettings = {
|
|
|
459
590
|
|
|
460
591
|
export type ViewerEnvironmentSettings = {
|
|
461
592
|
/**
|
|
462
|
-
* WHICH MAP'S LIGHT. Default `'
|
|
593
|
+
* WHICH MAP'S LIGHT. Default `'Dust II'`; `null` is our calibrated reference rig, which is what
|
|
463
594
|
* every fidelity measurement behind this renderer was taken against.
|
|
464
595
|
*
|
|
465
596
|
* *** THIS IS THE LIGHT, {@link background} IS THE PICTURE, AND THEY ARE SEPARATE ON PURPOSE. ***
|
|
@@ -468,8 +599,6 @@ export type ViewerEnvironmentSettings = {
|
|
|
468
599
|
* your page behind it.
|
|
469
600
|
*/
|
|
470
601
|
map?: MapName | null
|
|
471
|
-
/** Default `'Night'`. Falls back on its own for a map that has only one. */
|
|
472
|
-
timeOfDay?: TimeOfDay
|
|
473
602
|
/** Wet surfaces on maps whose own data says it rains. Default `true`. */
|
|
474
603
|
rain?: boolean
|
|
475
604
|
/** See {@link ViewerBackground}. Default `'transparent'`. */
|
|
@@ -709,6 +838,8 @@ type SubjectArms = {
|
|
|
709
838
|
charm: ViewerCharmSubject
|
|
710
839
|
collectible: ViewerCollectibleSubject
|
|
711
840
|
operator: ViewerOperatorSubject
|
|
841
|
+
/** Added in 0.4.1 - a seventh arm on the same rule. See {@link ViewerPetSubject}. */
|
|
842
|
+
pet: ViewerPetSubject
|
|
712
843
|
}
|
|
713
844
|
|
|
714
845
|
/** One arm present, the other five forbidden. Written once so six arms cannot disagree about five. */
|
|
@@ -723,6 +854,7 @@ export type ViewerSubject =
|
|
|
723
854
|
| OnlySubject<'charm'>
|
|
724
855
|
| OnlySubject<'collectible'>
|
|
725
856
|
| OnlySubject<'operator'>
|
|
857
|
+
| OnlySubject<'pet'>
|
|
726
858
|
|
|
727
859
|
export type SkinViewerProps = ViewerSubject & {
|
|
728
860
|
/* ── Presentation ──────────────────────────────────────────────────────────────────────── */
|