@xivdyetools/core 2.7.0 → 4.0.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 +85 -67
- package/dist/blending/blending.d.ts +22 -0
- package/dist/blending/blending.d.ts.map +1 -0
- package/dist/blending/blending.js +129 -0
- package/dist/blending/blending.js.map +1 -0
- package/dist/blending/conversions.d.ts +53 -0
- package/dist/blending/conversions.d.ts.map +1 -0
- package/dist/blending/conversions.js +256 -0
- package/dist/blending/conversions.js.map +1 -0
- package/dist/blending/index.d.ts +8 -0
- package/dist/blending/index.d.ts.map +1 -0
- package/dist/blending/index.js +3 -0
- package/dist/blending/index.js.map +1 -0
- package/dist/blending/types.d.ts +36 -0
- package/dist/blending/types.d.ts.map +1 -0
- package/dist/blending/types.js +21 -0
- package/dist/blending/types.js.map +1 -0
- package/dist/config/band-calibration.d.ts +68 -0
- package/dist/config/band-calibration.d.ts.map +1 -0
- package/dist/config/band-calibration.js +196 -0
- package/dist/config/band-calibration.js.map +1 -0
- package/dist/config/band-vocabulary.d.ts +82 -0
- package/dist/config/band-vocabulary.d.ts.map +1 -0
- package/dist/config/band-vocabulary.js +117 -0
- package/dist/config/band-vocabulary.js.map +1 -0
- package/dist/config/consolidated-ids.js +2 -2
- package/dist/config/consolidated-ids.js.map +1 -1
- package/dist/config/dye-vocabulary.d.ts +45 -0
- package/dist/config/dye-vocabulary.d.ts.map +1 -0
- package/dist/config/dye-vocabulary.js +58 -0
- package/dist/config/dye-vocabulary.js.map +1 -0
- package/dist/config/facewear.d.ts +24 -0
- package/dist/config/facewear.d.ts.map +1 -0
- package/dist/config/facewear.js +35 -0
- package/dist/config/facewear.js.map +1 -0
- package/dist/config/learn-links.d.ts +70 -0
- package/dist/config/learn-links.d.ts.map +1 -0
- package/dist/config/learn-links.js +171 -0
- package/dist/config/learn-links.js.map +1 -0
- package/dist/config/product-links.d.ts +44 -0
- package/dist/config/product-links.d.ts.map +1 -0
- package/dist/config/product-links.js +44 -0
- package/dist/config/product-links.js.map +1 -0
- package/dist/constants/index.d.ts +14 -1
- package/dist/constants/index.d.ts.map +1 -1
- package/dist/constants/index.js +38 -22
- package/dist/constants/index.js.map +1 -1
- package/dist/data/character_colors/race_specific/hair_colors.json +1 -1
- package/dist/data/character_colors/race_specific/skin_colors.json +1 -1
- package/dist/data/dyes.json +1127 -0
- package/dist/data/facewear_colors.json +57 -0
- package/dist/data/locales/de.json +4 -50
- package/dist/data/locales/en.json +4 -50
- package/dist/data/locales/fr.json +4 -50
- package/dist/data/locales/ja.json +4 -50
- package/dist/data/locales/ko.json +4 -50
- package/dist/data/locales/zh.json +4 -50
- package/dist/data/presets.json +59 -301
- package/dist/index.d.ts +156 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +114 -10
- package/dist/index.js.map +1 -1
- package/dist/services/APIService.d.ts +7 -61
- package/dist/services/APIService.d.ts.map +1 -1
- package/dist/services/APIService.js +43 -154
- package/dist/services/APIService.js.map +1 -1
- package/dist/services/CharacterColorService.d.ts +4 -36
- package/dist/services/CharacterColorService.d.ts.map +1 -1
- package/dist/services/CharacterColorService.js +16 -68
- package/dist/services/CharacterColorService.js.map +1 -1
- package/dist/services/ColorService.d.ts +55 -68
- package/dist/services/ColorService.d.ts.map +1 -1
- package/dist/services/ColorService.js +86 -94
- package/dist/services/ColorService.js.map +1 -1
- package/dist/services/DyeService.d.ts +10 -103
- package/dist/services/DyeService.d.ts.map +1 -1
- package/dist/services/DyeService.js +13 -156
- package/dist/services/DyeService.js.map +1 -1
- package/dist/services/LocalizationService.d.ts +1 -33
- package/dist/services/LocalizationService.d.ts.map +1 -1
- package/dist/services/LocalizationService.js +0 -44
- package/dist/services/LocalizationService.js.map +1 -1
- package/dist/services/PaletteService.d.ts +8 -9
- package/dist/services/PaletteService.d.ts.map +1 -1
- package/dist/services/PaletteService.js +2 -21
- package/dist/services/PaletteService.js.map +1 -1
- package/dist/services/PresetService.d.ts +2 -34
- package/dist/services/PresetService.d.ts.map +1 -1
- package/dist/services/PresetService.js +12 -59
- package/dist/services/PresetService.js.map +1 -1
- package/dist/services/chara/chara-models.d.ts +68 -0
- package/dist/services/chara/chara-models.d.ts.map +1 -0
- package/dist/services/chara/chara-models.js +87 -0
- package/dist/services/chara/chara-models.js.map +1 -0
- package/dist/services/chara/chara-parser.d.ts +95 -0
- package/dist/services/chara/chara-parser.d.ts.map +1 -0
- package/dist/services/chara/chara-parser.js +299 -0
- package/dist/services/chara/chara-parser.js.map +1 -0
- package/dist/services/chara/chara-resolver.d.ts +103 -0
- package/dist/services/chara/chara-resolver.d.ts.map +1 -0
- package/dist/services/chara/chara-resolver.js +233 -0
- package/dist/services/chara/chara-resolver.js.map +1 -0
- package/dist/services/color/ColorConverter.d.ts +107 -35
- package/dist/services/color/ColorConverter.d.ts.map +1 -1
- package/dist/services/color/ColorConverter.js +159 -48
- package/dist/services/color/ColorConverter.js.map +1 -1
- package/dist/services/color/ColorManipulator.d.ts +11 -0
- package/dist/services/color/ColorManipulator.d.ts.map +1 -1
- package/dist/services/color/ColorManipulator.js +14 -0
- package/dist/services/color/ColorManipulator.js.map +1 -1
- package/dist/services/color/ColorblindnessSimulator.d.ts +17 -0
- package/dist/services/color/ColorblindnessSimulator.d.ts.map +1 -1
- package/dist/services/color/ColorblindnessSimulator.js +53 -1
- package/dist/services/color/ColorblindnessSimulator.js.map +1 -1
- package/dist/services/color/SpectralMixer.d.ts +0 -34
- package/dist/services/color/SpectralMixer.d.ts.map +1 -1
- package/dist/services/color/SpectralMixer.js +0 -73
- package/dist/services/color/SpectralMixer.js.map +1 -1
- package/dist/services/dye/DyeDatabase.d.ts +0 -21
- package/dist/services/dye/DyeDatabase.d.ts.map +1 -1
- package/dist/services/dye/DyeDatabase.js +78 -103
- package/dist/services/dye/DyeDatabase.js.map +1 -1
- package/dist/services/dye/DyeSearch.d.ts +17 -31
- package/dist/services/dye/DyeSearch.d.ts.map +1 -1
- package/dist/services/dye/DyeSearch.js +31 -67
- package/dist/services/dye/DyeSearch.js.map +1 -1
- package/dist/services/dye/HarmonyGenerator.d.ts +11 -18
- package/dist/services/dye/HarmonyGenerator.d.ts.map +1 -1
- package/dist/services/dye/HarmonyGenerator.js +24 -35
- package/dist/services/dye/HarmonyGenerator.js.map +1 -1
- package/dist/services/localization/LocaleLoader.d.ts.map +1 -1
- package/dist/services/localization/LocaleLoader.js +0 -1
- package/dist/services/localization/LocaleLoader.js.map +1 -1
- package/dist/services/localization/TranslationProvider.d.ts +1 -42
- package/dist/services/localization/TranslationProvider.d.ts.map +1 -1
- package/dist/services/localization/TranslationProvider.js +4 -82
- package/dist/services/localization/TranslationProvider.js.map +1 -1
- package/dist/types/index.d.ts +39 -52
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +50 -10
- package/dist/types/index.js.map +1 -1
- package/dist/utils/index.d.ts +35 -122
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +45 -422
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/kd-tree.d.ts +0 -4
- package/dist/utils/kd-tree.d.ts.map +1 -1
- package/dist/utils/kd-tree.js +0 -6
- package/dist/utils/kd-tree.js.map +1 -1
- package/package.json +19 -10
- package/dist/data/character_colors/index.json +0 -40
- package/dist/data/colors_xiv.json +0 -3538
- package/dist/version.d.ts +0 -11
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js +0 -11
- package/dist/version.js.map +0 -1
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `.chara` character-file parser — the 5.0 Swatch Matcher import.
|
|
3
|
+
*
|
|
4
|
+
* Rules measured across 112 sample files (design register, Swatch Matcher
|
|
5
|
+
* 10A, confirmed):
|
|
6
|
+
* - **Parse by key presence, never the declared `TypeName`** — producers
|
|
7
|
+
* (Anamnesis versions, Ktisis) write different key sets. `TypeName` is
|
|
8
|
+
* captured only to *show* the producer.
|
|
9
|
+
* - **`REyeColor` holds the LEFT eye and `LEyeColor` the right** — crossed
|
|
10
|
+
* against the extended floats (`LeftEyeColor`/`RightEyeColor`); trust the
|
|
11
|
+
* extended naming. Never "fix" this by swapping — 16% of samples are
|
|
12
|
+
* heterochromia and a swap corrupts them silently.
|
|
13
|
+
* - **Extended floats are linear RGB** — gamma-encode before use or every
|
|
14
|
+
* colour is wrong-but-plausible.
|
|
15
|
+
* - **Flags gate live-looking data**: `EnableHighlights: false` inerts the
|
|
16
|
+
* highlight index (54% of samples, 47 of those with a live-looking index);
|
|
17
|
+
* `FacePaint: 0` inerts `FacePaintColor` (only 0 is load-bearing — values
|
|
18
|
+
* ≥128 exist, do not range-validate `FacePaint` itself).
|
|
19
|
+
* - **`LipsToneFurPattern` is a fur-pattern enum on Hrothgar**, not a colour.
|
|
20
|
+
* - **`MouthColor` alpha is a continuous opacity**: `0` = no lip; an absent
|
|
21
|
+
* key ≠ 0 (Ktisis omits it and the index stays valid).
|
|
22
|
+
* - **`IsExtendedAppearanceValid` is unreliable** (declared in 16 of 96
|
|
23
|
+
* float-bearing files): the floats count as live only when it is present
|
|
24
|
+
* and true — a missing flag means the index wins, and the UI says so.
|
|
25
|
+
* - **Gear dyes are stain IDs** (`DyeId`/`DyeId2` per slot, 0 = undyed; 35%
|
|
26
|
+
* of dyed channels are the second one).
|
|
27
|
+
* - **Gear models are emitted beside the dyes** (`gearModels`): the
|
|
28
|
+
* `ModelBase`/`ModelVariant` pair (weapons add `ModelSet`) of every WORN
|
|
29
|
+
* slot — `ModelBase == 0` (weapons: `ModelSet == 0` too) is an empty slot
|
|
30
|
+
* and is skipped. `null` hand records (one Anamnesis file) are empty.
|
|
31
|
+
* `Glasses` is `{ GlassesId }` or, on Brio-era Ktisis, a bare integer —
|
|
32
|
+
* both are accepted; 0 = no facewear. Resolution to item names happens
|
|
33
|
+
* off-device (api-worker) — the parser only carries the keys.
|
|
34
|
+
* - **Never read `Base64Image`.**
|
|
35
|
+
* - An unrecognised tribe/race/gender fails loudly (name got vs expected),
|
|
36
|
+
* never an empty palette.
|
|
37
|
+
*/
|
|
38
|
+
import { AppError, ErrorCode } from '@xivdyetools/types';
|
|
39
|
+
import { clamp } from '../../utils/index.js';
|
|
40
|
+
import { isWornCharaModel, isCharaWeaponSlot } from './chara-models.js';
|
|
41
|
+
const GEAR_SLOTS = [
|
|
42
|
+
'MainHand',
|
|
43
|
+
'OffHand',
|
|
44
|
+
'HeadGear',
|
|
45
|
+
'Body',
|
|
46
|
+
'Hands',
|
|
47
|
+
'Legs',
|
|
48
|
+
'Feet',
|
|
49
|
+
'Ears',
|
|
50
|
+
'Neck',
|
|
51
|
+
'Wrists',
|
|
52
|
+
'LeftRing',
|
|
53
|
+
'RightRing',
|
|
54
|
+
];
|
|
55
|
+
/** File tribe spellings → SubRace. Files use the game's plural "Helions". */
|
|
56
|
+
const TRIBE_MAP = {
|
|
57
|
+
Midlander: 'Midlander',
|
|
58
|
+
Highlander: 'Highlander',
|
|
59
|
+
Wildwood: 'Wildwood',
|
|
60
|
+
Duskwight: 'Duskwight',
|
|
61
|
+
Plainsfolk: 'Plainsfolk',
|
|
62
|
+
Dunesfolk: 'Dunesfolk',
|
|
63
|
+
SeekerOfTheSun: 'SeekerOfTheSun',
|
|
64
|
+
KeeperOfTheMoon: 'KeeperOfTheMoon',
|
|
65
|
+
SeaWolf: 'SeaWolf',
|
|
66
|
+
Hellsguard: 'Hellsguard',
|
|
67
|
+
Raen: 'Raen',
|
|
68
|
+
Xaela: 'Xaela',
|
|
69
|
+
Helions: 'Helions',
|
|
70
|
+
// Pre-5.0 identifier — accepted on read, stored as the game's plural
|
|
71
|
+
Helion: 'Helions',
|
|
72
|
+
TheLost: 'TheLost',
|
|
73
|
+
Rava: 'Rava',
|
|
74
|
+
Veena: 'Veena',
|
|
75
|
+
};
|
|
76
|
+
/** File race spellings → Race (Anamnesis serializes "Miqote" and "AuRa"). */
|
|
77
|
+
const RACE_MAP = {
|
|
78
|
+
Hyur: 'Hyur',
|
|
79
|
+
Elezen: 'Elezen',
|
|
80
|
+
Lalafell: 'Lalafell',
|
|
81
|
+
Miqote: "Miqo'te",
|
|
82
|
+
"Miqo'te": "Miqo'te",
|
|
83
|
+
Roegadyn: 'Roegadyn',
|
|
84
|
+
AuRa: 'AuRa',
|
|
85
|
+
Hrothgar: 'Hrothgar',
|
|
86
|
+
Viera: 'Viera',
|
|
87
|
+
};
|
|
88
|
+
const GENDER_MAP = {
|
|
89
|
+
Masculine: 'Male',
|
|
90
|
+
Feminine: 'Female',
|
|
91
|
+
Male: 'Male',
|
|
92
|
+
Female: 'Female',
|
|
93
|
+
};
|
|
94
|
+
/** Linear-light → sRGB gamma encoding, clamped, 0-255. */
|
|
95
|
+
function linearToSrgb255(c) {
|
|
96
|
+
const clamped = clamp(c, 0, 1);
|
|
97
|
+
const encoded = clamped <= 0.0031308 ? clamped * 12.92 : 1.055 * Math.pow(clamped, 1 / 2.4) - 0.055;
|
|
98
|
+
return Math.round(clamp(encoded, 0, 1) * 255);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Parse an extended-appearance float string ("r, g, b" or "r, g, b, a",
|
|
102
|
+
* linear RGB). Throws loudly on a malformed value — a wrong-but-plausible
|
|
103
|
+
* colour is worse than a failure that names the field.
|
|
104
|
+
*/
|
|
105
|
+
function parseFloatColor(field, value) {
|
|
106
|
+
if (value === null || value === undefined)
|
|
107
|
+
return null;
|
|
108
|
+
if (typeof value !== 'string') {
|
|
109
|
+
throw new AppError(ErrorCode.INVALID_INPUT, `.chara field ${field}: expected a comma-separated float string, got ${typeof value}`, 'error');
|
|
110
|
+
}
|
|
111
|
+
const parts = value.split(',').map((p) => Number(p.trim()));
|
|
112
|
+
if ((parts.length !== 3 && parts.length !== 4) || parts.some((n) => Number.isNaN(n))) {
|
|
113
|
+
throw new AppError(ErrorCode.INVALID_INPUT, `.chara field ${field}: unparseable float colour "${value}"`, 'error');
|
|
114
|
+
}
|
|
115
|
+
const [r, g, b] = parts;
|
|
116
|
+
return {
|
|
117
|
+
srgb: { r: linearToSrgb255(r), g: linearToSrgb255(g), b: linearToSrgb255(b) },
|
|
118
|
+
linear: [r, g, b],
|
|
119
|
+
alpha: parts.length === 4 ? parts[3] : null,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Every character colour field — the eight palette indices plus the extended
|
|
124
|
+
* appearance floats. Presence of at least one is what makes a JSON document
|
|
125
|
+
* a `.chara` file; an extended-only file (floats, no indices) is legitimate
|
|
126
|
+
* and resolves as `floatOnly`. See the key-presence rule in `parseCharaFile`.
|
|
127
|
+
*/
|
|
128
|
+
const COLOR_KEYS = [
|
|
129
|
+
'REyeColor',
|
|
130
|
+
'LEyeColor',
|
|
131
|
+
'HairTone',
|
|
132
|
+
'Highlights',
|
|
133
|
+
'Skintone',
|
|
134
|
+
'LimbalEyes',
|
|
135
|
+
'LipsToneFurPattern',
|
|
136
|
+
'FacePaintColor',
|
|
137
|
+
'SkinColor',
|
|
138
|
+
'LeftEyeColor',
|
|
139
|
+
'RightEyeColor',
|
|
140
|
+
'LimbalRingColor',
|
|
141
|
+
'HairColor',
|
|
142
|
+
'HairHighlight',
|
|
143
|
+
'MouthColor',
|
|
144
|
+
];
|
|
145
|
+
function readIndex(record, key) {
|
|
146
|
+
const value = record[key];
|
|
147
|
+
if (value === null || value === undefined)
|
|
148
|
+
return null;
|
|
149
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
150
|
+
throw new AppError(ErrorCode.INVALID_INPUT, `.chara field ${key}: expected a numeric palette index, got ${JSON.stringify(value)}`, 'error');
|
|
151
|
+
}
|
|
152
|
+
return value;
|
|
153
|
+
}
|
|
154
|
+
function mapNamed(table, field, value) {
|
|
155
|
+
if (value === null || value === undefined)
|
|
156
|
+
return { mapped: null, raw: null };
|
|
157
|
+
if (typeof value !== 'string') {
|
|
158
|
+
throw new AppError(ErrorCode.INVALID_INPUT, `.chara field ${field}: expected a string, got ${typeof value}`, 'error');
|
|
159
|
+
}
|
|
160
|
+
// FINDING-027 (2026-08-21 audit): own-property lookup only — `table[value]`
|
|
161
|
+
// on a plain object literal let "constructor" / "__proto__" / "toString"
|
|
162
|
+
// pass validation with a Function or Object.prototype as the mapped value
|
|
163
|
+
const mapped = Object.hasOwn(table, value) ? table[value] : undefined;
|
|
164
|
+
if (mapped === undefined) {
|
|
165
|
+
throw new AppError(ErrorCode.INVALID_INPUT, `.chara field ${field}: unrecognised value "${value}" (expected one of: ${Object.keys(table).join(', ')})`, 'error');
|
|
166
|
+
}
|
|
167
|
+
return { mapped, raw: value };
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Parse a `.chara` file's text content. Structural problems (invalid JSON,
|
|
171
|
+
* unrecognised tribe/race/gender, malformed colour values) throw AppError
|
|
172
|
+
* naming field and value; per-slot range problems are left to the resolver,
|
|
173
|
+
* which reports them as loud slot states.
|
|
174
|
+
*/
|
|
175
|
+
export function parseCharaFile(text) {
|
|
176
|
+
let data;
|
|
177
|
+
try {
|
|
178
|
+
data = JSON.parse(text);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
throw new AppError(ErrorCode.INVALID_INPUT, '.chara file is not valid JSON', 'error');
|
|
182
|
+
}
|
|
183
|
+
if (typeof data !== 'object' || data === null || Array.isArray(data)) {
|
|
184
|
+
throw new AppError(ErrorCode.INVALID_INPUT, '.chara file is not a JSON object', 'error');
|
|
185
|
+
}
|
|
186
|
+
const record = data;
|
|
187
|
+
// Identity fields first: a file that names a bad tribe deserves that
|
|
188
|
+
// message, not the generic refusal below.
|
|
189
|
+
const { mapped: race } = mapNamed(RACE_MAP, 'Race', record['Race']);
|
|
190
|
+
const { mapped: tribe, raw: tribeRaw } = mapNamed(TRIBE_MAP, 'Tribe', record['Tribe']);
|
|
191
|
+
const { mapped: gender } = mapNamed(GENDER_MAP, 'Gender', record['Gender']);
|
|
192
|
+
// Parse by key presence: a JSON carrying no colour field at all is some
|
|
193
|
+
// other document that happens to be JSON, not a sparse character. Refuse
|
|
194
|
+
// it rather than resolving eight dashed slots that read as a valid
|
|
195
|
+
// character wearing nothing.
|
|
196
|
+
if (!COLOR_KEYS.some((key) => key in record)) {
|
|
197
|
+
throw new AppError(ErrorCode.INVALID_INPUT, `.chara file carries no character colour fields (expected at least one of: ${COLOR_KEYS.join(', ')})`, 'error');
|
|
198
|
+
}
|
|
199
|
+
const extendedDeclared = 'IsExtendedAppearanceValid' in record;
|
|
200
|
+
const extendedValid = record['IsExtendedAppearanceValid'] === true;
|
|
201
|
+
const isHrothgar = race === 'Hrothgar';
|
|
202
|
+
const enableHighlights = record['EnableHighlights'] !== false;
|
|
203
|
+
const facePaintValue = readIndex(record, 'FacePaint');
|
|
204
|
+
const facePaintNone = facePaintValue === 0;
|
|
205
|
+
const skinFloat = parseFloatColor('SkinColor', record['SkinColor']);
|
|
206
|
+
// Crossed keys: the index REyeColor pairs with the float LeftEyeColor.
|
|
207
|
+
const leftEyeFloat = parseFloatColor('LeftEyeColor', record['LeftEyeColor']);
|
|
208
|
+
const rightEyeFloat = parseFloatColor('RightEyeColor', record['RightEyeColor']);
|
|
209
|
+
const limbalFloat = parseFloatColor('LimbalRingColor', record['LimbalRingColor']);
|
|
210
|
+
const hairFloat = parseFloatColor('HairColor', record['HairColor']);
|
|
211
|
+
const highlightFloat = parseFloatColor('HairHighlight', record['HairHighlight']);
|
|
212
|
+
const mouthFloat = parseFloatColor('MouthColor', record['MouthColor']);
|
|
213
|
+
const slot = (id, index, float, options = {}) => ({
|
|
214
|
+
slot: id,
|
|
215
|
+
index,
|
|
216
|
+
indexActive: options.indexActive ?? true,
|
|
217
|
+
...(options.inertReason !== undefined ? { inertReason: options.inertReason } : {}),
|
|
218
|
+
float: float?.srgb ?? null,
|
|
219
|
+
floatLinear: float?.linear ?? null,
|
|
220
|
+
alpha: options.alpha !== undefined ? options.alpha : null,
|
|
221
|
+
});
|
|
222
|
+
const slots = [
|
|
223
|
+
slot('leftEye', readIndex(record, 'REyeColor'), leftEyeFloat),
|
|
224
|
+
slot('rightEye', readIndex(record, 'LEyeColor'), rightEyeFloat),
|
|
225
|
+
slot('hair', readIndex(record, 'HairTone'), hairFloat),
|
|
226
|
+
slot('highlights', readIndex(record, 'Highlights'), highlightFloat, {
|
|
227
|
+
indexActive: enableHighlights,
|
|
228
|
+
...(enableHighlights ? {} : { inertReason: 'highlightsDisabled' }),
|
|
229
|
+
}),
|
|
230
|
+
slot('skin', readIndex(record, 'Skintone'), skinFloat),
|
|
231
|
+
slot('limbal', readIndex(record, 'LimbalEyes'), limbalFloat),
|
|
232
|
+
slot('lip', readIndex(record, 'LipsToneFurPattern'), mouthFloat, {
|
|
233
|
+
// On Hrothgar the key is a fur-pattern enum, not a colour index.
|
|
234
|
+
indexActive: !isHrothgar,
|
|
235
|
+
...(isHrothgar ? { inertReason: 'furPattern' } : {}),
|
|
236
|
+
alpha: mouthFloat ? mouthFloat.alpha : null,
|
|
237
|
+
}),
|
|
238
|
+
slot('facePaint', readIndex(record, 'FacePaintColor'), null, {
|
|
239
|
+
indexActive: !facePaintNone,
|
|
240
|
+
...(facePaintNone ? { inertReason: 'facePaintNone' } : {}),
|
|
241
|
+
}),
|
|
242
|
+
];
|
|
243
|
+
const gearDyes = [];
|
|
244
|
+
const gearModels = [];
|
|
245
|
+
for (const gearSlot of GEAR_SLOTS) {
|
|
246
|
+
const gear = record[gearSlot];
|
|
247
|
+
if (typeof gear !== 'object' || gear === null)
|
|
248
|
+
continue;
|
|
249
|
+
const gearRecord = gear;
|
|
250
|
+
for (const [channel, key] of [
|
|
251
|
+
[1, 'DyeId'],
|
|
252
|
+
[2, 'DyeId2'],
|
|
253
|
+
]) {
|
|
254
|
+
const value = gearRecord[key];
|
|
255
|
+
if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
|
|
256
|
+
gearDyes.push({ slot: gearSlot, channel, stainId: value });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
const set = readModelLane(gearRecord['ModelSet']);
|
|
260
|
+
const base = readModelLane(gearRecord['ModelBase']);
|
|
261
|
+
const variant = readModelLane(gearRecord['ModelVariant']);
|
|
262
|
+
if (isWornCharaModel(gearSlot, set, base)) {
|
|
263
|
+
gearModels.push(isCharaWeaponSlot(gearSlot)
|
|
264
|
+
? { slot: gearSlot, set, base, variant }
|
|
265
|
+
: { slot: gearSlot, base, variant });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
producer: typeof record['TypeName'] === 'string' ? record['TypeName'] : null,
|
|
270
|
+
race,
|
|
271
|
+
tribe,
|
|
272
|
+
tribeRaw,
|
|
273
|
+
gender,
|
|
274
|
+
nickname: typeof record['Nickname'] === 'string' ? record['Nickname'] : null,
|
|
275
|
+
extendedValid,
|
|
276
|
+
extendedDeclared,
|
|
277
|
+
slots,
|
|
278
|
+
gearDyes,
|
|
279
|
+
gearModels,
|
|
280
|
+
glassesId: readGlassesId(record['Glasses']),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
/** A model lane is a non-negative integer; anything else reads as 0 (empty). */
|
|
284
|
+
function readModelLane(value) {
|
|
285
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0
|
|
286
|
+
? Math.floor(value)
|
|
287
|
+
: 0;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* `Glasses` is `{ GlassesId: n }` (Anamnesis, Ktisis) or a bare integer
|
|
291
|
+
* (Brio-era "Ktisis/Anamnesis Character File"). Row 0 is "none".
|
|
292
|
+
*/
|
|
293
|
+
function readGlassesId(value) {
|
|
294
|
+
const raw = typeof value === 'object' && value !== null
|
|
295
|
+
? value['GlassesId']
|
|
296
|
+
: value;
|
|
297
|
+
return typeof raw === 'number' && Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : null;
|
|
298
|
+
}
|
|
299
|
+
//# sourceMappingURL=chara-parser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chara-parser.js","sourceRoot":"","sources":["../../../src/services/chara/chara-parser.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `.chara` slot resolution — arbitrates palette index vs extended float and
|
|
3
|
+
* resolves everything against the character colour sheets and dye database.
|
|
4
|
+
*
|
|
5
|
+
* Confirmed rules (Swatch Matcher 10A):
|
|
6
|
+
* - **Index wins where index and float agree**; past ~6 ΔE2000 the float wins
|
|
7
|
+
* and the slot is OFF GRID, with both hexes named.
|
|
8
|
+
* - Floats count as live only when `IsExtendedAppearanceValid` is present and
|
|
9
|
+
* true — a missing flag means the index wins, and the UI says so
|
|
10
|
+
* (`indexWinNote: 'extendedMissing'`).
|
|
11
|
+
* - **Dark/light palettes split 0-95 dark / 128-223 light; 96-127 fails
|
|
12
|
+
* loudly, never clamps** — one rule for every dark/light palette.
|
|
13
|
+
* - A Hrothgar lip can hold a colour with no index → the same OFF-GRID path
|
|
14
|
+
* (`floatOnly`).
|
|
15
|
+
* - Lip alpha 0 = no lip; the lip swatch composites over skin — both the raw
|
|
16
|
+
* colour and the blend are provided.
|
|
17
|
+
* - Gear dye stain IDs resolve through the dye database
|
|
18
|
+
* (`stainID`/`legacyItemID`), never derived from ranges.
|
|
19
|
+
* - Shared-index eyes (84% of files) merge into one badge — `eyesShareIndex`.
|
|
20
|
+
*/
|
|
21
|
+
import type { Dye, Gender, SubRace } from '@xivdyetools/types';
|
|
22
|
+
import type { CharacterColorService } from '../CharacterColorService.js';
|
|
23
|
+
import type { CharaGearSlotId, CharaSlotId, CharaSlotInertReason, ParsedCharaFile } from './chara-parser.js';
|
|
24
|
+
import type { CharaGearModel } from './chara-models.js';
|
|
25
|
+
/** ΔE2000 beyond which a live float overrides the palette index (OFF GRID). */
|
|
26
|
+
export declare const OFF_GRID_DELTA_E2000 = 6;
|
|
27
|
+
export type CharaSlotVerdict =
|
|
28
|
+
/** The palette index is the answer (float absent, not live, or agreeing) */
|
|
29
|
+
'index'
|
|
30
|
+
/** Live float disagrees past the threshold — float wins, both hexes named */
|
|
31
|
+
| 'offGrid'
|
|
32
|
+
/** A colour with no index (e.g. Hrothgar lip) — the OFF-GRID path */
|
|
33
|
+
| 'floatOnly'
|
|
34
|
+
/** A flag gates this slot off (reason on the slot) */
|
|
35
|
+
| 'inert'
|
|
36
|
+
/** Loud failure state — code + message name the field and value */
|
|
37
|
+
| 'error';
|
|
38
|
+
export type CharaSlotErrorCode = 'midRangeIndex' | 'indexOutOfRange' | 'noTribe';
|
|
39
|
+
export interface ResolvedCharaSlot {
|
|
40
|
+
slot: CharaSlotId;
|
|
41
|
+
/** The limbal slot is a limbal ring on Au Ra and a tattoo otherwise */
|
|
42
|
+
kind: CharaSlotId | 'tattoo';
|
|
43
|
+
verdict: CharaSlotVerdict;
|
|
44
|
+
inertReason?: CharaSlotInertReason;
|
|
45
|
+
error?: {
|
|
46
|
+
code: CharaSlotErrorCode;
|
|
47
|
+
message: string;
|
|
48
|
+
};
|
|
49
|
+
/** Raw palette index from the file */
|
|
50
|
+
index: number | null;
|
|
51
|
+
/** Index within the resolved sheet (light sheets subtract 128) */
|
|
52
|
+
sheetIndex: number | null;
|
|
53
|
+
/** Sheet variant for dark/light palettes */
|
|
54
|
+
sheetVariant: 'dark' | 'light' | null;
|
|
55
|
+
/** Grid address like "R6·C3" (8 columns), when the index resolved */
|
|
56
|
+
gridAddress: string | null;
|
|
57
|
+
/** Hex from the palette sheet, when the index resolved */
|
|
58
|
+
indexHex: string | null;
|
|
59
|
+
/** Hex from the extended float (gamma-encoded), when present */
|
|
60
|
+
floatHex: string | null;
|
|
61
|
+
/** ΔE2000 between indexHex and floatHex, when both exist */
|
|
62
|
+
deltaE: number | null;
|
|
63
|
+
/** Set when a float existed but was not live — the UI must say so */
|
|
64
|
+
indexWinNote?: 'extendedMissing';
|
|
65
|
+
/** Lip only: continuous opacity (null = not declared, ≠ 0) */
|
|
66
|
+
alpha: number | null;
|
|
67
|
+
/** Lip only: winning colour composited over the winning skin colour */
|
|
68
|
+
blendHex: string | null;
|
|
69
|
+
}
|
|
70
|
+
export interface ResolvedGearDye {
|
|
71
|
+
slot: CharaGearSlotId;
|
|
72
|
+
channel: 1 | 2;
|
|
73
|
+
stainId: number;
|
|
74
|
+
/** Resolved dye, or null when the stain ID is unknown to the database */
|
|
75
|
+
dye: Dye | null;
|
|
76
|
+
}
|
|
77
|
+
export interface ResolvedCharaCharacter {
|
|
78
|
+
producer: string | null;
|
|
79
|
+
race: ParsedCharaFile['race'];
|
|
80
|
+
tribe: SubRace | null;
|
|
81
|
+
gender: Gender | null;
|
|
82
|
+
nickname: string | null;
|
|
83
|
+
extendedValid: boolean;
|
|
84
|
+
extendedDeclared: boolean;
|
|
85
|
+
slots: ResolvedCharaSlot[];
|
|
86
|
+
/** True when both eyes carry the same palette index — merge into one badge */
|
|
87
|
+
eyesShareIndex: boolean;
|
|
88
|
+
gearDyes: ResolvedGearDye[];
|
|
89
|
+
/** Worn pieces' model keys (undyed included) — item names resolve off-device */
|
|
90
|
+
gearModels: CharaGearModel[];
|
|
91
|
+
/** Facewear `Glasses` sheet row; null when none */
|
|
92
|
+
glassesId: number | null;
|
|
93
|
+
}
|
|
94
|
+
/** Minimal dye lookup the resolver needs (DyeService satisfies this). */
|
|
95
|
+
export interface StainIdLookup {
|
|
96
|
+
getByStainId(stainId: number): Dye | null | undefined;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Resolve a parsed `.chara` file against the character colour sheets and
|
|
100
|
+
* (optionally) the dye database for gear dye names.
|
|
101
|
+
*/
|
|
102
|
+
export declare function resolveCharaColors(parsed: ParsedCharaFile, characterColors: CharacterColorService, dyeLookup?: StainIdLookup): Promise<ResolvedCharaCharacter>;
|
|
103
|
+
//# sourceMappingURL=chara-resolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chara-resolver.d.ts","sourceRoot":"","sources":["../../../src/services/chara/chara-resolver.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `.chara` slot resolution — arbitrates palette index vs extended float and
|
|
3
|
+
* resolves everything against the character colour sheets and dye database.
|
|
4
|
+
*
|
|
5
|
+
* Confirmed rules (Swatch Matcher 10A):
|
|
6
|
+
* - **Index wins where index and float agree**; past ~6 ΔE2000 the float wins
|
|
7
|
+
* and the slot is OFF GRID, with both hexes named.
|
|
8
|
+
* - Floats count as live only when `IsExtendedAppearanceValid` is present and
|
|
9
|
+
* true — a missing flag means the index wins, and the UI says so
|
|
10
|
+
* (`indexWinNote: 'extendedMissing'`).
|
|
11
|
+
* - **Dark/light palettes split 0-95 dark / 128-223 light; 96-127 fails
|
|
12
|
+
* loudly, never clamps** — one rule for every dark/light palette.
|
|
13
|
+
* - A Hrothgar lip can hold a colour with no index → the same OFF-GRID path
|
|
14
|
+
* (`floatOnly`).
|
|
15
|
+
* - Lip alpha 0 = no lip; the lip swatch composites over skin — both the raw
|
|
16
|
+
* colour and the blend are provided.
|
|
17
|
+
* - Gear dye stain IDs resolve through the dye database
|
|
18
|
+
* (`stainID`/`legacyItemID`), never derived from ranges.
|
|
19
|
+
* - Shared-index eyes (84% of files) merge into one badge — `eyesShareIndex`.
|
|
20
|
+
*/
|
|
21
|
+
import { ColorConverter } from '../color/ColorConverter.js';
|
|
22
|
+
import { clamp } from '../../utils/index.js';
|
|
23
|
+
/** ΔE2000 beyond which a live float overrides the palette index (OFF GRID). */
|
|
24
|
+
export const OFF_GRID_DELTA_E2000 = 6;
|
|
25
|
+
const GRID_COLUMNS = 8;
|
|
26
|
+
function gridAddress(sheetIndex) {
|
|
27
|
+
const row = Math.floor(sheetIndex / GRID_COLUMNS) + 1;
|
|
28
|
+
const col = (sheetIndex % GRID_COLUMNS) + 1;
|
|
29
|
+
return `R${row}·C${col}`;
|
|
30
|
+
}
|
|
31
|
+
function rgbToHex(rgb) {
|
|
32
|
+
return ColorConverter.rgbToHex(rgb.r, rgb.g, rgb.b);
|
|
33
|
+
}
|
|
34
|
+
function srgbToLinear(c) {
|
|
35
|
+
const n = c / 255;
|
|
36
|
+
return n <= 0.04045 ? n / 12.92 : Math.pow((n + 0.055) / 1.055, 2.4);
|
|
37
|
+
}
|
|
38
|
+
function linearToSrgb255(c) {
|
|
39
|
+
const clamped = clamp(c, 0, 1);
|
|
40
|
+
const encoded = clamped <= 0.0031308 ? clamped * 12.92 : 1.055 * Math.pow(clamped, 1 / 2.4) - 0.055;
|
|
41
|
+
return Math.round(clamp(encoded, 0, 1) * 255);
|
|
42
|
+
}
|
|
43
|
+
/** Composite `top` over `base` at `alpha`, in linear light. */
|
|
44
|
+
function compositeHex(topHex, baseHex, alpha) {
|
|
45
|
+
const top = ColorConverter.hexToRgb(topHex);
|
|
46
|
+
const base = ColorConverter.hexToRgb(baseHex);
|
|
47
|
+
const mix = (t, b) => linearToSrgb255(srgbToLinear(t) * alpha + srgbToLinear(b) * (1 - alpha));
|
|
48
|
+
return ColorConverter.rgbToHex(mix(top.r, base.r), mix(top.g, base.g), mix(top.b, base.b));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Resolve the sheet + in-sheet index for a slot. Dark/light palettes apply
|
|
52
|
+
* the one split rule: 0-95 dark, 128-223 light, 96-127 loud failure.
|
|
53
|
+
*/
|
|
54
|
+
async function resolveSheet(slot, index, parsed, characterColors) {
|
|
55
|
+
const splitRange = (dark, light, field) => {
|
|
56
|
+
if (index >= 0 && index <= 95) {
|
|
57
|
+
return { sheet: dark, variant: 'dark', sheetIndex: index };
|
|
58
|
+
}
|
|
59
|
+
if (index >= 128 && index <= 223) {
|
|
60
|
+
return { sheet: light, variant: 'light', sheetIndex: index - 128 };
|
|
61
|
+
}
|
|
62
|
+
if (index >= 96 && index <= 127) {
|
|
63
|
+
return {
|
|
64
|
+
sheet: null,
|
|
65
|
+
variant: null,
|
|
66
|
+
sheetIndex: null,
|
|
67
|
+
error: {
|
|
68
|
+
code: 'midRangeIndex',
|
|
69
|
+
message: `${field} index ${index} falls in the 96-127 gap between the dark (0-95) and light (128-223) ranges`,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
sheet: null,
|
|
75
|
+
variant: null,
|
|
76
|
+
sheetIndex: null,
|
|
77
|
+
error: {
|
|
78
|
+
code: 'indexOutOfRange',
|
|
79
|
+
message: `${field} index ${index} is outside the dark (0-95) and light (128-223) ranges`,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
switch (slot) {
|
|
84
|
+
case 'leftEye':
|
|
85
|
+
case 'rightEye':
|
|
86
|
+
return { sheet: characterColors.getEyeColors(), variant: null, sheetIndex: index };
|
|
87
|
+
case 'highlights':
|
|
88
|
+
return { sheet: characterColors.getHighlightColors(), variant: null, sheetIndex: index };
|
|
89
|
+
case 'limbal':
|
|
90
|
+
return { sheet: characterColors.getTattooColors(), variant: null, sheetIndex: index };
|
|
91
|
+
case 'lip':
|
|
92
|
+
return splitRange(characterColors.getLipColorsDark(), characterColors.getLipColorsLight(), 'LipsToneFurPattern');
|
|
93
|
+
case 'facePaint':
|
|
94
|
+
return splitRange(characterColors.getFacePaintColorsDark(), characterColors.getFacePaintColorsLight(), 'FacePaintColor');
|
|
95
|
+
case 'hair':
|
|
96
|
+
case 'skin': {
|
|
97
|
+
if (!parsed.tribe || !parsed.gender) {
|
|
98
|
+
return {
|
|
99
|
+
sheet: null,
|
|
100
|
+
variant: null,
|
|
101
|
+
sheetIndex: null,
|
|
102
|
+
error: {
|
|
103
|
+
code: 'noTribe',
|
|
104
|
+
message: `${slot} needs Tribe and Gender to resolve its race-specific sheet`,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const sheet = slot === 'hair'
|
|
109
|
+
? await characterColors.getHairColors(parsed.tribe, parsed.gender)
|
|
110
|
+
: await characterColors.getSkinColors(parsed.tribe, parsed.gender);
|
|
111
|
+
return { sheet, variant: null, sheetIndex: index };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Resolve a parsed `.chara` file against the character colour sheets and
|
|
117
|
+
* (optionally) the dye database for gear dye names.
|
|
118
|
+
*/
|
|
119
|
+
export async function resolveCharaColors(parsed, characterColors, dyeLookup) {
|
|
120
|
+
const slots = [];
|
|
121
|
+
for (const raw of parsed.slots) {
|
|
122
|
+
const kind = raw.slot === 'limbal' ? (parsed.race === 'AuRa' ? 'limbal' : 'tattoo') : raw.slot;
|
|
123
|
+
const floatHex = raw.float ? rgbToHex(raw.float) : null;
|
|
124
|
+
const floatLive = parsed.extendedValid && floatHex !== null;
|
|
125
|
+
const base = {
|
|
126
|
+
slot: raw.slot,
|
|
127
|
+
kind,
|
|
128
|
+
verdict: 'index',
|
|
129
|
+
...(raw.inertReason !== undefined ? { inertReason: raw.inertReason } : {}),
|
|
130
|
+
index: raw.index,
|
|
131
|
+
sheetIndex: null,
|
|
132
|
+
sheetVariant: null,
|
|
133
|
+
gridAddress: null,
|
|
134
|
+
indexHex: null,
|
|
135
|
+
floatHex,
|
|
136
|
+
deltaE: null,
|
|
137
|
+
alpha: raw.alpha,
|
|
138
|
+
blendHex: null,
|
|
139
|
+
};
|
|
140
|
+
// Lip alpha 0 = no lip (absent ≠ 0 — an absent key leaves the index valid)
|
|
141
|
+
if (raw.slot === 'lip' && raw.alpha === 0) {
|
|
142
|
+
slots.push({ ...base, verdict: 'inert', inertReason: 'noLip' });
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (!raw.indexActive) {
|
|
146
|
+
// A fur-pattern lip can still hold a live colour → the OFF-GRID path.
|
|
147
|
+
if (raw.inertReason === 'furPattern' && floatLive) {
|
|
148
|
+
slots.push({ ...base, verdict: 'floatOnly' });
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
slots.push({ ...base, verdict: 'inert' });
|
|
152
|
+
}
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (raw.index === null) {
|
|
156
|
+
slots.push(floatLive ? { ...base, verdict: 'floatOnly' } : { ...base, verdict: 'inert' });
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const resolution = await resolveSheet(raw.slot, raw.index, parsed, characterColors);
|
|
160
|
+
if (resolution.error || !resolution.sheet || resolution.sheetIndex === null) {
|
|
161
|
+
slots.push({
|
|
162
|
+
...base,
|
|
163
|
+
verdict: 'error',
|
|
164
|
+
...(resolution.error ? { error: resolution.error } : {}),
|
|
165
|
+
});
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const entry = resolution.sheet.find((c) => c.index === resolution.sheetIndex);
|
|
169
|
+
if (!entry) {
|
|
170
|
+
slots.push({
|
|
171
|
+
...base,
|
|
172
|
+
verdict: 'error',
|
|
173
|
+
sheetIndex: resolution.sheetIndex,
|
|
174
|
+
sheetVariant: resolution.variant,
|
|
175
|
+
error: {
|
|
176
|
+
code: 'indexOutOfRange',
|
|
177
|
+
message: `${raw.slot} index ${raw.index} has no entry in its ${resolution.sheet.length}-colour sheet`,
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const resolved = {
|
|
183
|
+
...base,
|
|
184
|
+
sheetIndex: resolution.sheetIndex,
|
|
185
|
+
sheetVariant: resolution.variant,
|
|
186
|
+
gridAddress: gridAddress(resolution.sheetIndex),
|
|
187
|
+
indexHex: entry.hex,
|
|
188
|
+
};
|
|
189
|
+
if (floatLive && floatHex) {
|
|
190
|
+
const deltaE = ColorConverter.getDeltaE(entry.hex, floatHex, 'ciede2000');
|
|
191
|
+
resolved.deltaE = deltaE;
|
|
192
|
+
resolved.verdict = deltaE > OFF_GRID_DELTA_E2000 ? 'offGrid' : 'index';
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
resolved.verdict = 'index';
|
|
196
|
+
if (floatHex && !parsed.extendedValid) {
|
|
197
|
+
// Float present but not live — the index wins and the UI says so.
|
|
198
|
+
resolved.indexWinNote = 'extendedMissing';
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
slots.push(resolved);
|
|
202
|
+
}
|
|
203
|
+
// Lip blend: winning lip colour composited over the winning skin colour.
|
|
204
|
+
const lip = slots.find((s) => s.slot === 'lip');
|
|
205
|
+
const skin = slots.find((s) => s.slot === 'skin');
|
|
206
|
+
if (lip && skin && lip.alpha !== null && lip.alpha > 0) {
|
|
207
|
+
const lipHex = lip.verdict === 'offGrid' || lip.verdict === 'floatOnly' ? lip.floatHex : lip.indexHex;
|
|
208
|
+
const skinHex = skin.verdict === 'offGrid' || skin.verdict === 'floatOnly' ? skin.floatHex : skin.indexHex;
|
|
209
|
+
if (lipHex && skinHex) {
|
|
210
|
+
lip.blendHex = compositeHex(lipHex, skinHex, clamp(lip.alpha, 0, 1));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const leftEye = slots.find((s) => s.slot === 'leftEye');
|
|
214
|
+
const rightEye = slots.find((s) => s.slot === 'rightEye');
|
|
215
|
+
return {
|
|
216
|
+
producer: parsed.producer,
|
|
217
|
+
race: parsed.race,
|
|
218
|
+
tribe: parsed.tribe,
|
|
219
|
+
gender: parsed.gender,
|
|
220
|
+
nickname: parsed.nickname,
|
|
221
|
+
extendedValid: parsed.extendedValid,
|
|
222
|
+
extendedDeclared: parsed.extendedDeclared,
|
|
223
|
+
slots,
|
|
224
|
+
eyesShareIndex: leftEye?.index !== null && leftEye?.index !== undefined && leftEye.index === rightEye?.index,
|
|
225
|
+
gearDyes: parsed.gearDyes.map((g) => ({
|
|
226
|
+
...g,
|
|
227
|
+
dye: dyeLookup?.getByStainId(g.stainId) ?? null,
|
|
228
|
+
})),
|
|
229
|
+
gearModels: parsed.gearModels,
|
|
230
|
+
glassesId: parsed.glassesId,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
//# sourceMappingURL=chara-resolver.js.map
|