@skinhub/viewer 0.1.1
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 +442 -0
- package/README.md +153 -0
- package/dist/SkinViewer.d.ts +18 -0
- package/dist/SkinViewer.d.ts.map +1 -0
- package/dist/SkinViewer.js +404 -0
- package/dist/SkinViewer.js.map +1 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -0
- package/dist/item.d.ts +118 -0
- package/dist/item.d.ts.map +1 -0
- package/dist/item.js +319 -0
- package/dist/item.js.map +1 -0
- package/dist/link.d.ts +30 -0
- package/dist/link.d.ts.map +1 -0
- package/dist/link.js +18 -0
- package/dist/link.js.map +1 -0
- package/dist/protocol.d.ts +231 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +128 -0
- package/dist/protocol.js.map +1 -0
- package/dist/state.d.ts +107 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +351 -0
- package/dist/state.js.map +1 -0
- package/dist/types.d.ts +573 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +106 -0
- package/dist/types.js.map +1 -0
- package/dist/useSkinViewer.d.ts +3 -0
- package/dist/useSkinViewer.d.ts.map +1 -0
- package/dist/useSkinViewer.js +66 -0
- package/dist/useSkinViewer.js.map +1 -0
- package/dist/weapons.d.ts +109 -0
- package/dist/weapons.d.ts.map +1 -0
- package/dist/weapons.js +260 -0
- package/dist/weapons.js.map +1 -0
- package/package.json +65 -0
- package/src/SkinViewer.tsx +465 -0
- package/src/index.ts +88 -0
- package/src/item.ts +373 -0
- package/src/link.ts +33 -0
- package/src/protocol.ts +241 -0
- package/src/state.ts +389 -0
- package/src/types.ts +672 -0
- package/src/useSkinViewer.ts +80 -0
- package/src/weapons.ts +284 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* *** `useSkinViewer()` - PROPS FOR STATE, A HOOK FOR VERBS. ***
|
|
5
|
+
*
|
|
6
|
+
* The owner floated this shape and it is the right one. Everything the viewer SHOWS is a prop, because
|
|
7
|
+
* that is what React is good at. The two or three things that are not values - "load it again", "what
|
|
8
|
+
* is it doing right now" - cannot be props without inventing a boolean somebody has to toggle back,
|
|
9
|
+
* so they are here.
|
|
10
|
+
*
|
|
11
|
+
* const viewer = useSkinViewer()
|
|
12
|
+
* <SkinViewer item={item} handle={viewer} loading={<Skeleton />} />
|
|
13
|
+
* <button onClick={viewer.reload} disabled={viewer.status === 'connecting'}>Reload</button>
|
|
14
|
+
*
|
|
15
|
+
* *** THE HOOK IS OPTIONAL AND THE COMPONENT IS COMPLETE WITHOUT IT. *** `onReady`, `onError` and
|
|
16
|
+
* `onChange` cover every integration that does not need a verb, and a component that only worked when
|
|
17
|
+
* a hook was also mounted would be a component with a hidden second half.
|
|
18
|
+
*
|
|
19
|
+
* *** `reload` IS STABLE; THE HANDLE OBJECT IS NOT. *** The object is rebuilt when `status`, `error` or
|
|
20
|
+
* `problems` move, because that is how a React value re-renders the tree that reads it. Put
|
|
21
|
+
* `viewer.reload` in a dependency array, not `viewer`.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { useMemo, useRef, useState } from 'react'
|
|
25
|
+
|
|
26
|
+
import { LINK, type ViewerLink, type ViewerSnapshot } from './link.js'
|
|
27
|
+
import type { SkinViewerHandle } from './types.js'
|
|
28
|
+
|
|
29
|
+
const INITIAL: ViewerSnapshot = { status: 'connecting', error: null, problems: [] }
|
|
30
|
+
|
|
31
|
+
export const useSkinViewer = (): SkinViewerHandle => {
|
|
32
|
+
const [snapshot, setSnapshot] = useState<ViewerSnapshot>(INITIAL)
|
|
33
|
+
|
|
34
|
+
/*
|
|
35
|
+
* ONE LINK OBJECT FOR THE LIFE OF THE HOOK. `useRef` with a lazy fill rather than `useMemo`, because
|
|
36
|
+
* a memo is a cache and React is allowed to throw a cache away; this is identity, and the component
|
|
37
|
+
* writes to it.
|
|
38
|
+
*/
|
|
39
|
+
const link = useRef<ViewerLink | null>(null)
|
|
40
|
+
if (link.current === null)
|
|
41
|
+
link.current = {
|
|
42
|
+
reload: () => {},
|
|
43
|
+
/*
|
|
44
|
+
* *** THE EQUALITY BAIL IS LOAD-BEARING, NOT AN OPTIMISATION. *** The component publishes from
|
|
45
|
+
* an effect that runs on every render; a `setState` that always produced a new object would
|
|
46
|
+
* re-render this hook's owner, which re-renders the component, which runs the effect, which
|
|
47
|
+
* publishes again - forever.
|
|
48
|
+
*
|
|
49
|
+
* *** AND THE ERROR IS COMPARED BY VALUE, NOT IDENTITY, WHICH IS THE WHOLE REASON THIS IS NOT
|
|
50
|
+
* A ONE-LINER. *** A subject failure (`no-item`, a link that did not decode) is DERIVED from
|
|
51
|
+
* this render's props, so it is a fresh object every render even when nothing has changed.
|
|
52
|
+
* Comparing it by identity is exactly the loop above.
|
|
53
|
+
*
|
|
54
|
+
* `problems` is compared by identity on purpose: it is the array the frame sent, and a new one
|
|
55
|
+
* means a new `hello`.
|
|
56
|
+
*/
|
|
57
|
+
publish: next =>
|
|
58
|
+
setSnapshot(current =>
|
|
59
|
+
current.status === next.status &&
|
|
60
|
+
current.problems === next.problems &&
|
|
61
|
+
current.error?.code === next.error?.code &&
|
|
62
|
+
current.error?.message === next.error?.message
|
|
63
|
+
? current
|
|
64
|
+
: next,
|
|
65
|
+
),
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const reload = useRef(() => link.current?.reload()).current
|
|
69
|
+
|
|
70
|
+
return useMemo<SkinViewerHandle>(
|
|
71
|
+
() => ({
|
|
72
|
+
reload,
|
|
73
|
+
status: snapshot.status,
|
|
74
|
+
error: snapshot.error,
|
|
75
|
+
problems: snapshot.problems,
|
|
76
|
+
[LINK]: link.current as ViewerLink,
|
|
77
|
+
}),
|
|
78
|
+
[reload, snapshot],
|
|
79
|
+
)
|
|
80
|
+
}
|
package/src/weapons.ts
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHICH WEAPON, said two ways — and the table that makes them the same question.
|
|
3
|
+
*
|
|
4
|
+
* An integrator holds one of two things. Either a row out of `@skinhub/cdn`'s `skins.json`, whose
|
|
5
|
+
* `weapon.id` is `'weapon_ak47'`, or a decoded inspect link, whose `defindex` is `7`. Both are the
|
|
6
|
+
* AK-47. `<SkinViewer>` takes either — {@link WeaponId} on the `weapon` prop, or the number on
|
|
7
|
+
* `defindex` — and this is the only place the two are reconciled.
|
|
8
|
+
*
|
|
9
|
+
* ─────────────────────────────────────────────────────────────────────────────────────────────
|
|
10
|
+
* WHY THIS TABLE IS CHECKED IN AND NOT FETCHED, when the whole premise of `@skinhub/cdn` is that
|
|
11
|
+
* data derived from the CDN has to be derived at runtime.
|
|
12
|
+
*
|
|
13
|
+
* That rule is about data that CHANGES — a new finish, a new sticker capsule, a new agent. This is
|
|
14
|
+
* not that. A defindex is Valve's item definition index: `weapon_ak47` has been 7 since 2013 and
|
|
15
|
+
* cannot become anything else without breaking every inventory in the game. The table is 63 rows and
|
|
16
|
+
* it is derived from the export itself (`data/skins.json`, `weapon.weapon_id` → `weapon.id`,
|
|
17
|
+
* generated 2026-08-15, verified: 63 distinct defindexes, no defindex mapping to two DIFFERENT
|
|
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).
|
|
20
|
+
*
|
|
21
|
+
* The alternative is fetching a 4.4 MB `skins.json` to answer "what is 7", which would make the
|
|
22
|
+
* inspect-link path — the one the product exists for — cost four megabytes before the first frame.
|
|
23
|
+
*
|
|
24
|
+
* WHAT GOING STALE LOOKS LIKE, stated rather than discovered: Valve ships a new weapon, an integrator
|
|
25
|
+
* passes its defindex, and {@link weaponIdForDefindex} returns `undefined`. `<SkinViewer>` then
|
|
26
|
+
* reports `unknown-weapon` through `onError` and renders its error state, naming the number it could
|
|
27
|
+
* not resolve. That is a legible failure with an obvious fix (pass `weapon` instead, or upgrade), not
|
|
28
|
+
* a blank canvas.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Every weapon the viewer can render, as the CS2 econ item id.
|
|
33
|
+
*
|
|
34
|
+
* This is `skin.weapon.id` in `@skinhub/cdn`'s `skins.json` rows, verbatim — so
|
|
35
|
+
* `<SkinViewer weapon={row.weapon.id} paintIndex={…} />` typechecks against a catalogue row with no
|
|
36
|
+
* conversion. Type it into an editor and autocomplete lists all 71.
|
|
37
|
+
*/
|
|
38
|
+
export const WEAPON_IDS = [
|
|
39
|
+
// Pistols
|
|
40
|
+
'weapon_cz75a',
|
|
41
|
+
'weapon_deagle',
|
|
42
|
+
'weapon_elite',
|
|
43
|
+
'weapon_fiveseven',
|
|
44
|
+
'weapon_glock',
|
|
45
|
+
'weapon_hkp2000',
|
|
46
|
+
'weapon_p250',
|
|
47
|
+
'weapon_revolver',
|
|
48
|
+
'weapon_tec9',
|
|
49
|
+
'weapon_usp_silencer',
|
|
50
|
+
// SMGs
|
|
51
|
+
'weapon_bizon',
|
|
52
|
+
'weapon_mac10',
|
|
53
|
+
'weapon_mp5sd',
|
|
54
|
+
'weapon_mp7',
|
|
55
|
+
'weapon_mp9',
|
|
56
|
+
'weapon_p90',
|
|
57
|
+
'weapon_ump45',
|
|
58
|
+
// Rifles
|
|
59
|
+
'weapon_ak47',
|
|
60
|
+
'weapon_aug',
|
|
61
|
+
'weapon_famas',
|
|
62
|
+
'weapon_galilar',
|
|
63
|
+
'weapon_m4a1',
|
|
64
|
+
'weapon_m4a1_silencer',
|
|
65
|
+
'weapon_sg556',
|
|
66
|
+
// Snipers
|
|
67
|
+
'weapon_awp',
|
|
68
|
+
'weapon_g3sg1',
|
|
69
|
+
'weapon_scar20',
|
|
70
|
+
'weapon_ssg08',
|
|
71
|
+
// Heavy
|
|
72
|
+
'weapon_m249',
|
|
73
|
+
'weapon_mag7',
|
|
74
|
+
'weapon_negev',
|
|
75
|
+
'weapon_nova',
|
|
76
|
+
'weapon_sawedoff',
|
|
77
|
+
'weapon_xm1014',
|
|
78
|
+
// Other
|
|
79
|
+
'weapon_taser',
|
|
80
|
+
// Knives
|
|
81
|
+
'weapon_bayonet',
|
|
82
|
+
'weapon_knife_butterfly',
|
|
83
|
+
'weapon_knife_canis',
|
|
84
|
+
'weapon_knife_cord',
|
|
85
|
+
'weapon_knife_css',
|
|
86
|
+
'weapon_knife_falchion',
|
|
87
|
+
'weapon_knife_flip',
|
|
88
|
+
'weapon_knife_gut',
|
|
89
|
+
'weapon_knife_gypsy_jackknife',
|
|
90
|
+
'weapon_knife_karambit',
|
|
91
|
+
'weapon_knife_kukri',
|
|
92
|
+
'weapon_knife_m9_bayonet',
|
|
93
|
+
'weapon_knife_outdoor',
|
|
94
|
+
'weapon_knife_push',
|
|
95
|
+
'weapon_knife_skeleton',
|
|
96
|
+
'weapon_knife_stiletto',
|
|
97
|
+
'weapon_knife_survival_bowie',
|
|
98
|
+
'weapon_knife_tactical',
|
|
99
|
+
'weapon_knife_ursus',
|
|
100
|
+
'weapon_knife_widowmaker',
|
|
101
|
+
// Gloves. A pair of gloves is the SUBJECT here — the thing on screen, framed and orbitable.
|
|
102
|
+
// Putting a pair ON an operator in the `operator`/`firstPerson` views is not supported; see
|
|
103
|
+
// `SkinViewerProps.operator`.
|
|
104
|
+
'leather_handwraps',
|
|
105
|
+
'motorcycle_gloves',
|
|
106
|
+
'slick_gloves',
|
|
107
|
+
'specialist_gloves',
|
|
108
|
+
'sporty_gloves',
|
|
109
|
+
'studded_bloodhound_gloves',
|
|
110
|
+
'studded_brokenfang_gloves',
|
|
111
|
+
'studded_hydra_gloves',
|
|
112
|
+
] as const
|
|
113
|
+
|
|
114
|
+
export type KnownWeaponId = (typeof WEAPON_IDS)[number]
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* `'weapon_ak47'`.
|
|
118
|
+
*
|
|
119
|
+
* The `(string & {})` arm is deliberate and is not a widening mistake: it keeps autocomplete listing
|
|
120
|
+
* the 71 known ids while still ACCEPTING an id this build has never heard of, so a new weapon in a
|
|
121
|
+
* fresh export renders the day it ships rather than the day the package is upgraded. An unknown id
|
|
122
|
+
* that the asset export also does not know resolves to no model, which surfaces as `unknown-weapon`.
|
|
123
|
+
*/
|
|
124
|
+
export type WeaponId = KnownWeaponId | (string & {})
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Item definition index → econ item id. See the file header for why this is checked in.
|
|
128
|
+
*
|
|
129
|
+
* The 20 knives are listed under their `weapon_*` id rather than the `sfui_wpnhud_*` alias that also
|
|
130
|
+
* appears on their vanilla row; both resolve to the same GLB.
|
|
131
|
+
*/
|
|
132
|
+
export const WEAPON_ID_BY_DEFINDEX: Readonly<Record<number, KnownWeaponId>> = {
|
|
133
|
+
1: 'weapon_deagle',
|
|
134
|
+
2: 'weapon_elite',
|
|
135
|
+
3: 'weapon_fiveseven',
|
|
136
|
+
4: 'weapon_glock',
|
|
137
|
+
7: 'weapon_ak47',
|
|
138
|
+
8: 'weapon_aug',
|
|
139
|
+
9: 'weapon_awp',
|
|
140
|
+
10: 'weapon_famas',
|
|
141
|
+
11: 'weapon_g3sg1',
|
|
142
|
+
13: 'weapon_galilar',
|
|
143
|
+
14: 'weapon_m249',
|
|
144
|
+
16: 'weapon_m4a1',
|
|
145
|
+
17: 'weapon_mac10',
|
|
146
|
+
19: 'weapon_p90',
|
|
147
|
+
23: 'weapon_mp5sd',
|
|
148
|
+
24: 'weapon_ump45',
|
|
149
|
+
25: 'weapon_xm1014',
|
|
150
|
+
26: 'weapon_bizon',
|
|
151
|
+
27: 'weapon_mag7',
|
|
152
|
+
28: 'weapon_negev',
|
|
153
|
+
29: 'weapon_sawedoff',
|
|
154
|
+
30: 'weapon_tec9',
|
|
155
|
+
31: 'weapon_taser',
|
|
156
|
+
32: 'weapon_hkp2000',
|
|
157
|
+
33: 'weapon_mp7',
|
|
158
|
+
34: 'weapon_mp9',
|
|
159
|
+
35: 'weapon_nova',
|
|
160
|
+
36: 'weapon_p250',
|
|
161
|
+
38: 'weapon_scar20',
|
|
162
|
+
39: 'weapon_sg556',
|
|
163
|
+
40: 'weapon_ssg08',
|
|
164
|
+
60: 'weapon_m4a1_silencer',
|
|
165
|
+
61: 'weapon_usp_silencer',
|
|
166
|
+
63: 'weapon_cz75a',
|
|
167
|
+
64: 'weapon_revolver',
|
|
168
|
+
500: 'weapon_bayonet',
|
|
169
|
+
503: 'weapon_knife_css',
|
|
170
|
+
505: 'weapon_knife_flip',
|
|
171
|
+
506: 'weapon_knife_gut',
|
|
172
|
+
507: 'weapon_knife_karambit',
|
|
173
|
+
508: 'weapon_knife_m9_bayonet',
|
|
174
|
+
509: 'weapon_knife_tactical',
|
|
175
|
+
512: 'weapon_knife_falchion',
|
|
176
|
+
514: 'weapon_knife_survival_bowie',
|
|
177
|
+
515: 'weapon_knife_butterfly',
|
|
178
|
+
516: 'weapon_knife_push',
|
|
179
|
+
517: 'weapon_knife_cord',
|
|
180
|
+
518: 'weapon_knife_canis',
|
|
181
|
+
519: 'weapon_knife_ursus',
|
|
182
|
+
520: 'weapon_knife_gypsy_jackknife',
|
|
183
|
+
521: 'weapon_knife_outdoor',
|
|
184
|
+
522: 'weapon_knife_stiletto',
|
|
185
|
+
523: 'weapon_knife_widowmaker',
|
|
186
|
+
525: 'weapon_knife_skeleton',
|
|
187
|
+
526: 'weapon_knife_kukri',
|
|
188
|
+
4725: 'studded_brokenfang_gloves',
|
|
189
|
+
5027: 'studded_bloodhound_gloves',
|
|
190
|
+
5030: 'sporty_gloves',
|
|
191
|
+
5031: 'slick_gloves',
|
|
192
|
+
5032: 'leather_handwraps',
|
|
193
|
+
5033: 'motorcycle_gloves',
|
|
194
|
+
5034: 'specialist_gloves',
|
|
195
|
+
5035: 'studded_hydra_gloves',
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* ─────────────────────────────────────────────────────────────────────────────────────────────
|
|
200
|
+
* *** THE HUD ALIASES — the difference between a knife and an empty canvas. ***
|
|
201
|
+
*
|
|
202
|
+
* `@skinhub/cdn`'s `skins.json` carries TWO ids for every knife. The painted rows say
|
|
203
|
+
* `weapon.id === 'weapon_bayonet'`; the ONE vanilla row per knife says
|
|
204
|
+
* `weapon.id === 'sfui_wpnhud_knifebayonet'` — the HUD string, not the item name. Both share
|
|
205
|
+
* `weapon.weapon_id === 500`. There are 83 distinct `weapon.id` values across 63 defindexes, and the
|
|
206
|
+
* 20 extras are exactly this.
|
|
207
|
+
*
|
|
208
|
+
* So an integrator doing the most natural thing there is —
|
|
209
|
+
*
|
|
210
|
+
* const row = findSkin(skins, { defindex, paintindex })
|
|
211
|
+
* <SkinViewer weaponId={row.weapon.id} … />
|
|
212
|
+
*
|
|
213
|
+
* — hands this component a HUD string for all twenty vanilla knives and nothing else.
|
|
214
|
+
*
|
|
215
|
+
* *** THE ALIAS IS ACCEPTED AND FOLDED, NOT REJECTED. *** The mapping is 1:1 and unambiguous, the
|
|
216
|
+
* renderer's own model table already resolves the same twenty strings, and failing on a value the data
|
|
217
|
+
* package hands you from its documented API would only mean the two packages disagree about what a
|
|
218
|
+
* Bayonet is called. Folding it here additionally makes {@link defindexForWeaponId} answer for those
|
|
219
|
+
* rows, so an inspect link built back out of a vanilla knife carries 500 rather than 0.
|
|
220
|
+
*
|
|
221
|
+
* An id that is neither a known weapon nor a known alias still fails VISIBLY — `<SkinViewer>` reports
|
|
222
|
+
* `unknown-weapon` and renders its error state naming the string it could not resolve. Rendering
|
|
223
|
+
* nothing, silently, is the one outcome this table exists to prevent.
|
|
224
|
+
*
|
|
225
|
+
* Deliberately NOT in {@link WEAPON_IDS}: these are accepted, not recommended, and an editor should
|
|
226
|
+
* not offer `sfui_wpnhud_knifekaram` to somebody typing a weapon id.
|
|
227
|
+
*/
|
|
228
|
+
export const WEAPON_ID_ALIASES: Readonly<Record<string, KnownWeaponId>> = {
|
|
229
|
+
sfui_wpnhud_knifebayonet: 'weapon_bayonet',
|
|
230
|
+
sfui_wpnhud_knife_butterfly: 'weapon_knife_butterfly',
|
|
231
|
+
sfui_wpnhud_knife_canis: 'weapon_knife_canis',
|
|
232
|
+
sfui_wpnhud_knife_cord: 'weapon_knife_cord',
|
|
233
|
+
sfui_wpnhud_knifecss: 'weapon_knife_css',
|
|
234
|
+
sfui_wpnhud_knife_falchion_advanced: 'weapon_knife_falchion',
|
|
235
|
+
sfui_wpnhud_knifeflip: 'weapon_knife_flip',
|
|
236
|
+
sfui_wpnhud_knifegut: 'weapon_knife_gut',
|
|
237
|
+
sfui_wpnhud_knife_gypsy_jackknife: 'weapon_knife_gypsy_jackknife',
|
|
238
|
+
sfui_wpnhud_knifekaram: 'weapon_knife_karambit',
|
|
239
|
+
sfui_wpnhud_knife_kukri: 'weapon_knife_kukri',
|
|
240
|
+
sfui_wpnhud_knifem9: 'weapon_knife_m9_bayonet',
|
|
241
|
+
sfui_wpnhud_knife_outdoor: 'weapon_knife_outdoor',
|
|
242
|
+
sfui_wpnhud_knife_push: 'weapon_knife_push',
|
|
243
|
+
sfui_wpnhud_knife_skeleton: 'weapon_knife_skeleton',
|
|
244
|
+
sfui_wpnhud_knife_stiletto: 'weapon_knife_stiletto',
|
|
245
|
+
sfui_wpnhud_knife_survival_bowie: 'weapon_knife_survival_bowie',
|
|
246
|
+
sfui_wpnhud_knifetactical: 'weapon_knife_tactical',
|
|
247
|
+
sfui_wpnhud_knife_ursus: 'weapon_knife_ursus',
|
|
248
|
+
sfui_wpnhud_knife_widowmaker: 'weapon_knife_widowmaker',
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const KNOWN_WEAPON_IDS: ReadonlySet<string> = new Set<string>(WEAPON_IDS)
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Fold a HUD alias onto the item id. Everything else passes through untouched, including ids this
|
|
255
|
+
* build has never heard of — see {@link WeaponId} for why an unknown id is not an error here.
|
|
256
|
+
*/
|
|
257
|
+
export const normalizeWeaponId = (weapon: WeaponId): WeaponId => WEAPON_ID_ALIASES[weapon] ?? weapon
|
|
258
|
+
|
|
259
|
+
/** True for an id this build can name. `false` is what makes `<SkinViewer>` report `unknown-weapon`. */
|
|
260
|
+
export const isKnownWeaponId = (weapon: WeaponId): boolean => KNOWN_WEAPON_IDS.has(normalizeWeaponId(weapon))
|
|
261
|
+
|
|
262
|
+
const DEFINDEX_BY_WEAPON_ID: Readonly<Record<string, number>> = Object.fromEntries(
|
|
263
|
+
Object.entries(WEAPON_ID_BY_DEFINDEX).map(([defindex, id]) => [id, Number(defindex)]),
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* `7` → `'weapon_ak47'`, or `undefined` for a defindex this build does not know.
|
|
268
|
+
*
|
|
269
|
+
* NEVER RETURNS AN ALIAS — the table is keyed on `weapon_id`, which is the whole reason to build it
|
|
270
|
+
* that way rather than off the first `weapon.id` a scan happens to hit.
|
|
271
|
+
*/
|
|
272
|
+
export const weaponIdForDefindex = (defindex: number): KnownWeaponId | undefined => WEAPON_ID_BY_DEFINDEX[defindex]
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* `'weapon_ak47'` → `7`, or `undefined`.
|
|
276
|
+
*
|
|
277
|
+
* The inverse, for building an inspect link back out of what the viewer is showing — see
|
|
278
|
+
* {@link toPlacement}, exported from the package root. Resolves HUD aliases, so a vanilla Bayonet round-trips to 500 rather than to 0.
|
|
279
|
+
*/
|
|
280
|
+
export const defindexForWeaponId = (weapon: WeaponId): number | undefined =>
|
|
281
|
+
DEFINDEX_BY_WEAPON_ID[normalizeWeaponId(weapon)]
|
|
282
|
+
|
|
283
|
+
/** Gloves take a different renderer and carry no stickers, charm, counter or name plate. */
|
|
284
|
+
export const isGloveId = (weapon: WeaponId): boolean => weapon.endsWith('_gloves') || weapon === 'leather_handwraps'
|