@flow-industries/id 0.23.4 → 0.24.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/README.md +76 -0
- package/dist/sdk/browser-contract.d.ts +1 -0
- package/dist/sdk/browser-contract.js +1 -0
- package/dist/sdk/browser-session-route.js +3 -3
- package/dist/sdk/client/atproto.d.ts +5 -0
- package/dist/sdk/client/atproto.js +49 -0
- package/dist/sdk/client/create-flow.js +2 -1
- package/dist/sdk/db/handle.d.ts +11 -0
- package/dist/sdk/db/handle.js +0 -0
- package/dist/sdk/db/schema.d.ts +4626 -0
- package/dist/sdk/db/schema.js +782 -0
- package/dist/sdk/oauth/encryption.d.ts +4 -0
- package/dist/sdk/oauth/encryption.js +29 -0
- package/dist/sdk/oauth/stores.d.ts +16 -0
- package/dist/sdk/oauth/stores.js +159 -0
- package/dist/sdk/settings/appearance.d.ts +3 -0
- package/dist/sdk/settings/appearance.js +167 -0
- package/dist/sdk/settings/cosmetics.d.ts +149 -0
- package/dist/sdk/settings/cosmetics.js +408 -0
- package/dist/sdk/settings/hair-items.json +312 -0
- package/dist/sdk/settings/registry.d.ts +11 -0
- package/dist/sdk/settings/registry.js +136 -0
- package/dist/sdk/settings/surfaces.d.ts +4 -0
- package/dist/sdk/settings/surfaces.js +37 -0
- package/dist/sdk/types/account-data.d.ts +11 -0
- package/dist/sdk/types/atproto-storage.d.ts +9 -0
- package/dist/sdk/types/atproto-storage.js +0 -0
- package/dist/sdk/types/atproto.d.ts +25 -0
- package/dist/sdk/types/atproto.js +0 -0
- package/dist/sdk/types/auth.d.ts +1 -0
- package/dist/sdk/types/cosmetics.d.ts +2 -1
- package/dist/sdk/types/index.d.ts +1 -0
- package/package.json +6 -2
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cosmetic catalog, the space each item occupies, the surfaces it exposes
|
|
3
|
+
* for paint, and the single rule that decides whether two items may be worn at
|
|
4
|
+
* once.
|
|
5
|
+
*
|
|
6
|
+
* Pure — no DB, no I/O — and deliberately the ONLY implementation of that rule.
|
|
7
|
+
* TF2 evaluated it per equip path and its quickswitch path skipped the check
|
|
8
|
+
* entirely, so players equipped conflicting cosmetics that silently unequipped
|
|
9
|
+
* on restart (Source-1-Games#4055). Here the rule hangs off the surface
|
|
10
|
+
* registry and runs inside the one settings write path, which is why no route
|
|
11
|
+
* can forget it: a client may grey out conflicting picks for UX, but the save
|
|
12
|
+
* is the enforcer.
|
|
13
|
+
*
|
|
14
|
+
* The catalog is a code constant rather than a table, matching the appearance
|
|
15
|
+
* palette beside it — adding an item is a one-line edit and no migration. It
|
|
16
|
+
* promotes to a `cosmetic` table when non-engineers must edit it live.
|
|
17
|
+
*/
|
|
18
|
+
import { isString } from "../json";
|
|
19
|
+
import hairItems from "./hair-items.json";
|
|
20
|
+
/**
|
|
21
|
+
* Region pairs that physically overlap, declared once in ONE direction and
|
|
22
|
+
* symmetrised at module load so a pair can never be half-declared. Not
|
|
23
|
+
* transitive: a hat overlaps `whole_head` and `whole_head` overlaps `face`,
|
|
24
|
+
* but a hat and a bandana coexist happily.
|
|
25
|
+
*/
|
|
26
|
+
const REGION_OVERLAPS = [
|
|
27
|
+
["whole_head", "hat"],
|
|
28
|
+
["whole_head", "hair"],
|
|
29
|
+
["whole_head", "face"],
|
|
30
|
+
["whole_head", "glasses"],
|
|
31
|
+
["whole_head", "ears"],
|
|
32
|
+
["glasses", "face"],
|
|
33
|
+
];
|
|
34
|
+
const OVERLAPS = (() => {
|
|
35
|
+
const map = new Map();
|
|
36
|
+
const link = (from, to) => {
|
|
37
|
+
const set = map.get(from) ?? new Set();
|
|
38
|
+
set.add(to);
|
|
39
|
+
map.set(from, set);
|
|
40
|
+
};
|
|
41
|
+
for (const [left, right] of REGION_OVERLAPS) {
|
|
42
|
+
link(left, right);
|
|
43
|
+
link(right, left);
|
|
44
|
+
}
|
|
45
|
+
return map;
|
|
46
|
+
})();
|
|
47
|
+
/**
|
|
48
|
+
* Every slot, in the order the customizer and the resolver walk them, with the
|
|
49
|
+
* name a player reads it by and whether an avatar may go without it filled.
|
|
50
|
+
*
|
|
51
|
+
* `required` is a property of the SLOT and lives here because this is the only
|
|
52
|
+
* place both halves of it can be read from: the schema projects it onto the
|
|
53
|
+
* slot's descriptor so a client stops offering "None", and the surface's
|
|
54
|
+
* cross-field rule refuses an empty value for it inside the write path. A rule
|
|
55
|
+
* written down in auth and mirrored by hand in the customizer is the shape
|
|
56
|
+
* AUTH-210 went silent in and the one AUTH-222 undid.
|
|
57
|
+
*
|
|
58
|
+
* `satisfies` makes a new slot a typecheck error here until someone has said
|
|
59
|
+
* what it is called and whether an avatar may go without it.
|
|
60
|
+
*/
|
|
61
|
+
const SLOTS = {
|
|
62
|
+
hair: { label: "Hair", required: true },
|
|
63
|
+
hat: { label: "Headwear", required: false },
|
|
64
|
+
face: { label: "Face", required: false },
|
|
65
|
+
top: { label: "Top", required: true },
|
|
66
|
+
bottom: { label: "Bottom", required: true },
|
|
67
|
+
feet: { label: "Footwear", required: false },
|
|
68
|
+
};
|
|
69
|
+
function slotNames() {
|
|
70
|
+
// SAFETY: SLOTS satisfies Record<CosmeticSlot, _>, so its keys are exactly the slot names.
|
|
71
|
+
return Object.keys(SLOTS);
|
|
72
|
+
}
|
|
73
|
+
/** Every slot, in the order the customizer and the resolver walk them. */
|
|
74
|
+
export const COSMETIC_SLOTS = slotNames();
|
|
75
|
+
/** What a player reads this slot's row as. */
|
|
76
|
+
export function slotLabel(slot) {
|
|
77
|
+
return SLOTS[slot].label;
|
|
78
|
+
}
|
|
79
|
+
/** Whether an avatar must wear something in this slot. */
|
|
80
|
+
export function slotRequired(slot) {
|
|
81
|
+
return SLOTS[slot].required;
|
|
82
|
+
}
|
|
83
|
+
/** Namespaces the paint keys so they can never collide with a fixed setting. */
|
|
84
|
+
const PAINT_KEY_PREFIX = "paint";
|
|
85
|
+
/** Separates a paint key's segments. Forbidden inside an id or a material name. */
|
|
86
|
+
const PAINT_KEY_SEPARATOR = ".";
|
|
87
|
+
/** A catalog over `items`, refusing a duplicate id. */
|
|
88
|
+
export function catalogOf(items) {
|
|
89
|
+
const byId = new Map(items.map((item) => [item.id, item]));
|
|
90
|
+
// A duplicate id would silently shadow one of the two items — loud beats a
|
|
91
|
+
// cosmetic that cannot be equipped and no error anywhere.
|
|
92
|
+
if (byId.size !== items.length) {
|
|
93
|
+
throw new Error("duplicate cosmetic id in the catalog");
|
|
94
|
+
}
|
|
95
|
+
return byId;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Every item a player may equip. EACH ONE MUST HAVE GEOMETRY: an id here with
|
|
99
|
+
* no mesh behind it renders as an item that vanishes when worn, and the model's
|
|
100
|
+
* side of the pairing is `cosmetic_items` in
|
|
101
|
+
* `game/games/arena/player/luna_bodygroups.tres` — a catalog id absent from
|
|
102
|
+
* there names nothing the renderer can draw.
|
|
103
|
+
*/
|
|
104
|
+
export const COSMETICS = [
|
|
105
|
+
...hairItems.map((item) => ({
|
|
106
|
+
...item,
|
|
107
|
+
slot: "hair",
|
|
108
|
+
regions: item.id === "hair_original" ? [] : ["hair"],
|
|
109
|
+
hides: item.id === "hair_original" ? [] : ["hair"],
|
|
110
|
+
paintable: [],
|
|
111
|
+
})),
|
|
112
|
+
{
|
|
113
|
+
id: "round_glasses",
|
|
114
|
+
slot: "face",
|
|
115
|
+
name: "Round glasses",
|
|
116
|
+
regions: ["glasses"],
|
|
117
|
+
hides: [],
|
|
118
|
+
// `LUNA_glasses.glb` carries exactly one material, and its name is the
|
|
119
|
+
// model's own — recolouring it paints the frames.
|
|
120
|
+
paintable: ["outline"],
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: "hoodie",
|
|
124
|
+
slot: "top",
|
|
125
|
+
name: "Hoodie",
|
|
126
|
+
regions: ["torso", "arms"],
|
|
127
|
+
// Long sleeves, so both arm segments go; the cuff stops at the wrist and
|
|
128
|
+
// the collar sits below the jaw, which is why the hands and `body_neck`
|
|
129
|
+
// stay. `body_pelvis` stays too — where a hoodie hem falls on the model is
|
|
130
|
+
// unknown until the cosmetic is authored, and hiding a region a cosmetic only
|
|
131
|
+
// half covers opens a hole, which reads far worse than the clipping it
|
|
132
|
+
// would have prevented.
|
|
133
|
+
hides: [
|
|
134
|
+
"body_chest",
|
|
135
|
+
"body_arm_upper_l",
|
|
136
|
+
"body_arm_upper_r",
|
|
137
|
+
"body_arm_lower_l",
|
|
138
|
+
"body_arm_lower_r",
|
|
139
|
+
],
|
|
140
|
+
// The model's torso cosmetic is `cosmetic_tshirt`, whose one material is
|
|
141
|
+
// `tshirt`. That material is carried by no other mesh, which is why the
|
|
142
|
+
// body palette no longer names it: a `tshirt_color` row could only ever
|
|
143
|
+
// reach this item, and this row already owns it (AUTH-226).
|
|
144
|
+
paintable: ["tshirt"],
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: "cargo_shorts",
|
|
148
|
+
slot: "bottom",
|
|
149
|
+
name: "Cargo shorts",
|
|
150
|
+
regions: ["legs"],
|
|
151
|
+
// Exactly what the shipped `cosmetic_shorts` hides. The thigh is its own
|
|
152
|
+
// region below the shorts hem, so a longer cargo cut covers only part of
|
|
153
|
+
// it and claiming the whole thing would amputate the leg.
|
|
154
|
+
hides: ["body_pelvis"],
|
|
155
|
+
paintable: ["shorts"],
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
id: "boots",
|
|
159
|
+
slot: "feet",
|
|
160
|
+
name: "Boots",
|
|
161
|
+
// `feet` rather than `legs`, even though the shaft sheathes the calf: the
|
|
162
|
+
// shorts occupy `legs`, and a boot that fought them over it would be
|
|
163
|
+
// unwearable with the only bottom there is.
|
|
164
|
+
regions: ["feet"],
|
|
165
|
+
// The shipped `cosmetic_boots`. NOT `body_foot_*` — the model's toes stand
|
|
166
|
+
// proud of the boot, so hiding the feet cuts a hole in the avatar rather
|
|
167
|
+
// than preventing a clip (`cosmetic_coverage_test.gd` scores that pair as
|
|
168
|
+
// one of its two historical faults).
|
|
169
|
+
hides: ["body_leg_lower_l", "body_leg_lower_r"],
|
|
170
|
+
// `LUNA_boots.glb` names its one material `Shoes`, capital and all.
|
|
171
|
+
paintable: ["Shoes"],
|
|
172
|
+
},
|
|
173
|
+
];
|
|
174
|
+
/** Every item the shipped model can wear. */
|
|
175
|
+
export const CATALOG = catalogOf(COSMETICS);
|
|
176
|
+
/**
|
|
177
|
+
* What a player is wearing before they have dressed themselves — the outfit the
|
|
178
|
+
* model ships in, named in catalog terms.
|
|
179
|
+
*
|
|
180
|
+
* It is a DEFAULT of the appearance surface, resolved server-side exactly the
|
|
181
|
+
* way `randomDefaults` resolves a palette colour, and that is the whole point:
|
|
182
|
+
* a stored loadout is then the entire truth about what an avatar wears, and no
|
|
183
|
+
* renderer has to guess whether an empty slot means "new player" or "wants
|
|
184
|
+
* nothing there". AUTH-223 is what guessing cost — the client synthesised the
|
|
185
|
+
* starting outfit whenever the loadout named nothing at all, so equipping a
|
|
186
|
+
* single item anywhere silently stripped the other two.
|
|
187
|
+
*
|
|
188
|
+
* Every slot is listed, `""` for the ones a player starts bare in, so
|
|
189
|
+
* `satisfies` makes a new slot a typecheck error here until someone has said
|
|
190
|
+
* what a new player wears in it. A slot {@link SLOTS} marks required must name
|
|
191
|
+
* an item: it is the value an unnamed slot falls back to, so a mandatory slot
|
|
192
|
+
* starting at `""` would be a refusal every new player is born holding.
|
|
193
|
+
*/
|
|
194
|
+
export const STARTING_OUTFIT = {
|
|
195
|
+
hair: "hair_original",
|
|
196
|
+
hat: "",
|
|
197
|
+
face: "",
|
|
198
|
+
top: "hoodie",
|
|
199
|
+
bottom: "cargo_shorts",
|
|
200
|
+
feet: "",
|
|
201
|
+
};
|
|
202
|
+
export function cosmeticById(id, catalog = CATALOG) {
|
|
203
|
+
return catalog.get(id);
|
|
204
|
+
}
|
|
205
|
+
export function cosmeticsForSlot(slot, catalog = CATALOG) {
|
|
206
|
+
return [...catalog.values()].filter((item) => item.slot === slot);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Every region an item occupying `regions` cannot share a body with: those
|
|
210
|
+
* regions themselves, plus everything each of them overlaps.
|
|
211
|
+
*
|
|
212
|
+
* Closing the set here is what lets a client grey out an impossible pick
|
|
213
|
+
* without a copy of {@link REGION_OVERLAPS}. The schema projects this per
|
|
214
|
+
* catalog item, so a collision becomes a set intersection: nothing to
|
|
215
|
+
* symmetrise, and no room to assume overlap is transitive (it is not). A
|
|
216
|
+
* mirrored vocabulary is what AUTH-210 cost.
|
|
217
|
+
*/
|
|
218
|
+
export function conflictingRegions(regions) {
|
|
219
|
+
const closed = new Set(regions);
|
|
220
|
+
for (const region of regions) {
|
|
221
|
+
for (const overlap of OVERLAPS.get(region) ?? [])
|
|
222
|
+
closed.add(overlap);
|
|
223
|
+
}
|
|
224
|
+
return [...closed];
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* The first region two items both occupy, or null. A region trivially occupies
|
|
228
|
+
* itself, so "sharing a region" and "occupying overlapping regions" are the
|
|
229
|
+
* same check rather than two rules that can drift apart.
|
|
230
|
+
*
|
|
231
|
+
* Resolved through {@link conflictingRegions}, so the rule the save path
|
|
232
|
+
* enforces and the projection a client greys out from are ONE function rather
|
|
233
|
+
* than two that happen to agree today.
|
|
234
|
+
*/
|
|
235
|
+
export function sharedRegion(a, b) {
|
|
236
|
+
const blocked = new Set(conflictingRegions(b));
|
|
237
|
+
return a.find((region) => blocked.has(region)) ?? null;
|
|
238
|
+
}
|
|
239
|
+
/** The catalog items a blob names, in slot order, skipping empty and unknown ids. */
|
|
240
|
+
function equippedItems(settings, catalog) {
|
|
241
|
+
const worn = [];
|
|
242
|
+
for (const slot of COSMETIC_SLOTS) {
|
|
243
|
+
const id = settings[slot];
|
|
244
|
+
if (!isString(id) || id === "")
|
|
245
|
+
continue;
|
|
246
|
+
const item = cosmeticById(id, catalog);
|
|
247
|
+
// An id filed under the wrong slot is as unusable as an unknown one: the
|
|
248
|
+
// catalog, not the blob, decides where an item is worn.
|
|
249
|
+
if (!item || item.slot !== slot)
|
|
250
|
+
continue;
|
|
251
|
+
worn.push({ slot, item });
|
|
252
|
+
}
|
|
253
|
+
return worn;
|
|
254
|
+
}
|
|
255
|
+
/** The first pair of equipped items whose regions collide, or null. THE rule. */
|
|
256
|
+
export function equipConflict(settings, catalog = CATALOG) {
|
|
257
|
+
const accepted = [];
|
|
258
|
+
for (const entry of equippedItems(settings, catalog)) {
|
|
259
|
+
for (const prior of accepted) {
|
|
260
|
+
const region = sharedRegion(prior.item.regions, entry.item.regions);
|
|
261
|
+
if (region) {
|
|
262
|
+
return {
|
|
263
|
+
region,
|
|
264
|
+
items: [prior.item.id, entry.item.id],
|
|
265
|
+
slots: [prior.slot, entry.slot],
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
accepted.push(entry);
|
|
270
|
+
}
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* The first mandatory slot the loadout explicitly empties, or null.
|
|
275
|
+
*
|
|
276
|
+
* Explicitly is the whole subtlety: `""` is the unequip verb, while a slot the
|
|
277
|
+
* blob never names falls back to its default — the starting outfit, which fills
|
|
278
|
+
* every mandatory slot. So this refuses the request that undresses an avatar
|
|
279
|
+
* and never a partial patch that simply did not mention the slot.
|
|
280
|
+
*
|
|
281
|
+
* A slot the catalog sells nothing for is exempt for the same reason it is not
|
|
282
|
+
* a setting: there is nothing to put in it, so demanding one would lock every
|
|
283
|
+
* save out until content lands.
|
|
284
|
+
*/
|
|
285
|
+
export function bareSlot(settings, catalog = CATALOG) {
|
|
286
|
+
for (const slot of COSMETIC_SLOTS) {
|
|
287
|
+
if (!slotRequired(slot))
|
|
288
|
+
continue;
|
|
289
|
+
if (cosmeticsForSlot(slot, catalog).length === 0)
|
|
290
|
+
continue;
|
|
291
|
+
if (settings[slot] === "")
|
|
292
|
+
return slot;
|
|
293
|
+
}
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* The appearance surface's cross-field rule as the registry consumes it. Wired
|
|
298
|
+
* onto the surface rather than called from a route, so every write path gets
|
|
299
|
+
* it by construction.
|
|
300
|
+
*
|
|
301
|
+
* Two rules, one hook: a pair of items that cannot share a body, and a slot an
|
|
302
|
+
* avatar may not go without. Both are relations over the whole loadout rather
|
|
303
|
+
* than properties of one key, which is why neither can live in `sanitize`.
|
|
304
|
+
*/
|
|
305
|
+
export function appearanceConflict(settings, catalog = CATALOG) {
|
|
306
|
+
const conflict = equipConflict(settings, catalog);
|
|
307
|
+
if (conflict) {
|
|
308
|
+
const [first, second] = conflict.items.map((id) => cosmeticById(id, catalog)?.name ?? id);
|
|
309
|
+
return {
|
|
310
|
+
code: "equip_region_conflict",
|
|
311
|
+
message: `${first} and ${second} both occupy the ${conflict.region} region`,
|
|
312
|
+
keys: [...conflict.slots],
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
const bare = bareSlot(settings, catalog);
|
|
316
|
+
if (bare) {
|
|
317
|
+
return {
|
|
318
|
+
code: "slot_required",
|
|
319
|
+
message: `${slotLabel(bare)} cannot be left empty`,
|
|
320
|
+
keys: [bare],
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* The equipped items that actually render: catalog-resolved, in slot order,
|
|
327
|
+
* with any item colliding with one already accepted dropped.
|
|
328
|
+
*
|
|
329
|
+
* Dropping here is a self-heal, not a second copy of the rule — the save path
|
|
330
|
+
* refuses conflicts, so a stored blob can only become conflicting when a
|
|
331
|
+
* catalog edit changes an item's regions underneath it. Both paths call
|
|
332
|
+
* {@link sharedRegion}, so there is still exactly one definition of collision.
|
|
333
|
+
*
|
|
334
|
+
* Everything downstream of a loadout resolves it through here, which is what
|
|
335
|
+
* keeps the colour pickers a player is offered and the equipped set a game
|
|
336
|
+
* server receives describing the same avatar.
|
|
337
|
+
*/
|
|
338
|
+
function wornItems(settings, catalog) {
|
|
339
|
+
const worn = [];
|
|
340
|
+
for (const entry of equippedItems(settings, catalog)) {
|
|
341
|
+
const collides = worn.some((prior) => sharedRegion(prior.item.regions, entry.item.regions) !== null);
|
|
342
|
+
if (!collides)
|
|
343
|
+
worn.push(entry);
|
|
344
|
+
}
|
|
345
|
+
return worn;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* The `appearance` key holding one item's colour for one of its materials.
|
|
349
|
+
*
|
|
350
|
+
* Segmented rather than opaque so it can GROW without a second key space:
|
|
351
|
+
* GAME-178 paints a masked region of an item instead of its whole surface,
|
|
352
|
+
* which is one more segment here. The separator is legal in a JSON key and
|
|
353
|
+
* illegal in an id and a material name, which `cosmetics.test.ts` enforces —
|
|
354
|
+
* without that a key would be ambiguous the day someone names an item
|
|
355
|
+
* `round.glasses`.
|
|
356
|
+
*/
|
|
357
|
+
export function paintKey(itemId, material) {
|
|
358
|
+
return [PAINT_KEY_PREFIX, itemId, material].join(PAINT_KEY_SEPARATOR);
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Every recolourable surface the equipped set exposes, in slot order: one
|
|
362
|
+
* entry per paintable material on each worn item.
|
|
363
|
+
*
|
|
364
|
+
* This is what makes the appearance surface dynamic — the colour pickers a
|
|
365
|
+
* player sees are a function of what they are wearing, so a colour cannot
|
|
366
|
+
* outlive the item it painted.
|
|
367
|
+
*/
|
|
368
|
+
export function paintSlots(settings, catalog = CATALOG) {
|
|
369
|
+
return wornItems(settings, catalog).flatMap(({ slot, item }) => item.paintable.map((material) => ({
|
|
370
|
+
key: paintKey(item.id, material),
|
|
371
|
+
slot,
|
|
372
|
+
item,
|
|
373
|
+
material,
|
|
374
|
+
})));
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* The colours a blob holds for one item's paintable materials, skipping any it
|
|
378
|
+
* does not name. Absent rather than blank: no colour is an instruction to
|
|
379
|
+
* leave the item as it was authored, which is not the same as painting it
|
|
380
|
+
* nothing.
|
|
381
|
+
*/
|
|
382
|
+
function paintFor(settings, item) {
|
|
383
|
+
const paint = [];
|
|
384
|
+
for (const material of item.paintable) {
|
|
385
|
+
const color = settings[paintKey(item.id, material)];
|
|
386
|
+
if (isString(color) && color !== "")
|
|
387
|
+
paint.push({ material, color });
|
|
388
|
+
}
|
|
389
|
+
return paint;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* The equipped set as `/api/session/verify` hands it to a game server: the
|
|
393
|
+
* items that render, each carrying the regions it occupies, the body it
|
|
394
|
+
* suppresses, and the paint to apply to it.
|
|
395
|
+
*/
|
|
396
|
+
export function equippedFor(settings, catalog = CATALOG) {
|
|
397
|
+
const equipped = {};
|
|
398
|
+
for (const { slot, item } of wornItems(settings, catalog)) {
|
|
399
|
+
equipped[slot] = {
|
|
400
|
+
id: item.id,
|
|
401
|
+
name: item.name,
|
|
402
|
+
regions: item.regions,
|
|
403
|
+
hides: item.hides,
|
|
404
|
+
paint: paintFor(settings, item),
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
return equipped;
|
|
408
|
+
}
|