uesp-eso-build-wrapper 0.2.0 → 0.4.0

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.
Files changed (52) hide show
  1. package/README.md +77 -123
  2. package/README.pt-BR.md +122 -0
  3. package/dist/lib/eso-engine/calculator.d.ts +33 -22
  4. package/dist/lib/eso-engine/calculator.d.ts.map +1 -1
  5. package/dist/lib/eso-engine/calculator.js +283 -219
  6. package/dist/lib/eso-engine/calculator.js.map +1 -1
  7. package/dist/lib/eso-engine/debug.d.ts +63 -0
  8. package/dist/lib/eso-engine/debug.d.ts.map +1 -0
  9. package/dist/lib/eso-engine/debug.js +98 -0
  10. package/dist/lib/eso-engine/debug.js.map +1 -0
  11. package/dist/lib/eso-engine/engine-globals.d.ts +171 -0
  12. package/dist/lib/eso-engine/engine-globals.d.ts.map +1 -0
  13. package/dist/lib/eso-engine/engine-globals.js +24 -0
  14. package/dist/lib/eso-engine/engine-globals.js.map +1 -0
  15. package/dist/lib/eso-engine/env-setup.d.ts +15 -15
  16. package/dist/lib/eso-engine/env-setup.d.ts.map +1 -1
  17. package/dist/lib/eso-engine/env-setup.js +42 -58
  18. package/dist/lib/eso-engine/env-setup.js.map +1 -1
  19. package/dist/lib/eso-engine/index.d.ts +29 -23
  20. package/dist/lib/eso-engine/index.d.ts.map +1 -1
  21. package/dist/lib/eso-engine/index.js +33 -85
  22. package/dist/lib/eso-engine/index.js.map +1 -1
  23. package/dist/lib/eso-engine/input-stats.d.ts.map +1 -1
  24. package/dist/lib/eso-engine/input-stats.js +450 -129
  25. package/dist/lib/eso-engine/input-stats.js.map +1 -1
  26. package/dist/lib/eso-engine/loader.d.ts +15 -15
  27. package/dist/lib/eso-engine/loader.d.ts.map +1 -1
  28. package/dist/lib/eso-engine/loader.js +98 -130
  29. package/dist/lib/eso-engine/loader.js.map +1 -1
  30. package/dist/lib/eso-engine/types.d.ts +133 -90
  31. package/dist/lib/eso-engine/types.d.ts.map +1 -1
  32. package/dist/lib/eso-engine/types.js +2 -3
  33. package/dist/lib/eso-engine/types.js.map +1 -1
  34. package/dist/lib/uesp-data/extract.d.ts +29 -0
  35. package/dist/lib/uesp-data/extract.d.ts.map +1 -0
  36. package/dist/lib/uesp-data/extract.js +304 -0
  37. package/dist/lib/uesp-data/extract.js.map +1 -0
  38. package/dist/lib/uesp-data/index.d.ts +14 -0
  39. package/dist/lib/uesp-data/index.d.ts.map +1 -0
  40. package/dist/lib/uesp-data/index.js +11 -0
  41. package/dist/lib/uesp-data/index.js.map +1 -0
  42. package/dist/lib/uesp-data/mariadb-dump.d.ts +32 -0
  43. package/dist/lib/uesp-data/mariadb-dump.d.ts.map +1 -0
  44. package/dist/lib/uesp-data/mariadb-dump.js +222 -0
  45. package/dist/lib/uesp-data/mariadb-dump.js.map +1 -0
  46. package/dist/lib/uesp-data/seed.d.ts +43 -0
  47. package/dist/lib/uesp-data/seed.d.ts.map +1 -0
  48. package/dist/lib/uesp-data/seed.js +413 -0
  49. package/dist/lib/uesp-data/seed.js.map +1 -0
  50. package/package.json +29 -12
  51. package/vendor/uesp-esochardata/resources/esoEditBuild.js +3 -3
  52. package/dist/browser-stub.js +0 -1
@@ -1,32 +1,33 @@
1
- "use strict";
2
1
  /**
3
- * Motor de cálculo de builds do ESO.
2
+ * ESO build calculation engine.
4
3
  *
5
- * Implementa a ideia central da arquitetura:
4
+ * Implements the core architecture idea:
6
5
  *
7
- * 1. Injetar dados do personagem nos elementos mock do DOM
8
- * (jQuery vai ler via $("#esotbRace").val(), etc.)
6
+ * 1. Inject character data into the mock DOM elements
7
+ * (jQuery will read via $("#esotbRace").val(), etc.)
9
8
  *
10
- * 2. Injetar dados dos itens DIRETAMENTE em g_EsoBuildItemData[slot]
11
- * — sem precisar mockar jQuery para cada campo de item.
12
- * Os campos vêm exatamente no formato da API pública da UESP:
9
+ * 2. Inject item data DIRECTLY into g_EsoBuildItemData[slot]
10
+ * — no need to mock jQuery for each item field.
11
+ * Fields come exactly in the UESP public API format:
13
12
  * GET https://esolog.uesp.net/exportJson.php?table=minedItem&id=<id>&level=<lv>&quality=<q>
14
13
  *
15
- * 3. Chamar UpdateEsoComputedStatsList_Real(null, true)
16
- * — o parâmetro `noUpdate=true` faz o motor calcular tudo mas pular
17
- * as atualizações de DOM (DisplayEsoAllComputedStats, UpdateReadOnlyStats, etc.)
14
+ * 3. Call UpdateEsoComputedStatsList_Real(null, true)
15
+ * — the `noUpdate=true` parameter makes the engine compute everything but skip
16
+ * the DOM updates (DisplayEsoAllComputedStats, UpdateReadOnlyStats, etc.)
18
17
  *
19
- * 4. Ler os resultados de g_EsoComputedStats[statId].value
18
+ * 4. Read the results from g_EsoComputedStats[statId].value
20
19
  */
21
- Object.defineProperty(exports, "__esModule", { value: true });
22
- exports.calculateBuild = calculateBuild;
23
- exports.listAvailableBuffs = listAvailableBuffs;
24
- exports.listRacialPassives = listRacialPassives;
25
- exports.listClassPassives = listClassPassives;
26
- exports.listPassivesBySkillLine = listPassivesBySkillLine;
27
- exports.listAvailableSkillLines = listAvailableSkillLines;
28
- exports.listAvailableToggleSkills = listAvailableToggleSkills;
29
- const env_setup_1 = require("./env-setup");
20
+ import { resetDomValues, setDomAttr, setDomTextContent, setDomValue } from './env-setup.js';
21
+ import { engineGlobals } from './engine-globals.js';
22
+ // Stat object cache — populated once after initEsoEngineFromData.
23
+ // Avoids Object.keys/values on every calculateBuild; ~200 objects.
24
+ let _statObjects = null;
25
+ export function cacheStatObjects() {
26
+ const stats = engineGlobals().g_EsoComputedStats;
27
+ if (stats && typeof stats === 'object') {
28
+ _statObjects = Object.values(stats);
29
+ }
30
+ }
30
31
  const ALL_SLOTS = [
31
32
  'Head',
32
33
  'Shoulders',
@@ -48,9 +49,9 @@ const ALL_SLOTS = [
48
49
  'Potion',
49
50
  ];
50
51
  /**
51
- * Normaliza os dados de um item garantindo que todos os campos opcionais
52
- * existam com defaults seguros. Sem isso, o motor lança TypeError ao tentar
53
- * chamar .includes() em campos de set bonus undefined (setBonusDesc5..12).
52
+ * Normalizes an item's data ensuring all optional fields
53
+ * exist with safe defaults. Without this, the engine throws a TypeError when trying
54
+ * to call .includes() on undefined set bonus fields (setBonusDesc5..12).
54
55
  */
55
56
  function normalizeItemData(item) {
56
57
  const defaults = {};
@@ -61,102 +62,169 @@ function normalizeItemData(item) {
61
62
  return { ...defaults, ...item };
62
63
  }
63
64
  /**
64
- * Calcula os Computed Character Statistics para a build fornecida.
65
+ * Calculates the Computed Character Statistics for the given build.
66
+ *
67
+ * Each call starts from a clean engine state (previous items, buffs, CP nodes
68
+ * and skill bars are reset), so builds never bleed into each other.
69
+ *
70
+ * @param input - The build to calculate: character sheet, items, champion
71
+ * point nodes, buffs, toggle skills, skill bars and passives.
72
+ * @returns All computed stats — named keys (Health, Magicka, SpellDamage, ...)
73
+ * plus `raw` with the full 221-stat `g_EsoComputedStats` record.
74
+ * @throws If the engine has not been initialized with
75
+ * `initEsoEngineFromData()` first.
65
76
  *
66
77
  * @example
67
78
  * ```ts
68
- * import { calculateBuild } from './src/lib/eso-engine';
79
+ * import { calculateBuild } from 'uesp-eso-build-wrapper';
69
80
  *
70
81
  * const stats = calculateBuild({
71
82
  * character: { race: 'High Elf', class: 'Sorcerer', level: 50,
72
83
  * attributes: { health: 0, magicka: 64, stamina: 0 } },
73
84
  * items: {
74
- * Chest: chestItemFromUespApi, // objeto retornado por esolog.uesp.net/exportJson.php
85
+ * Chest: chestItemFromUespApi, // object returned by esolog.uesp.net/exportJson.php
75
86
  * },
76
87
  * });
77
- * console.log(stats.MaxMagicka, stats.SpellDamage);
88
+ * console.log(stats.Magicka, stats.SpellDamage);
78
89
  * ```
79
90
  */
80
- function calculateBuild(input) {
91
+ export function calculateBuild(input) {
92
+ const { character, items, championPointNodes, activeBuffs, toggleSkills, skillBars, activeWeaponBar, passiveSkills, autoPassives, enchantOverrides, toggledSetBonuses, } = input;
93
+ // ─── GLOBAL STATE RESET ───────────────────────────────────────────────────
94
+ // All resets in one place — makes leaks between calls easier to spot.
95
+ // g_EsoComputedStats: zero value/preCapValue before computing.
96
+ // In the browser they all start at 0 on every load; in the wrapper the object persists
97
+ // across calls and stats from a previous build contaminate the next one. The deferred loop
98
+ // (esoEditBuild.js:4397) copies g_EsoComputedStats[name].value back into
99
+ // inputValues[name] at each step j, so a leftover value corrupts early-computed
100
+ // stats (e.g. BashDamage). _statObjects is populated once at init,
101
+ // avoiding Object.keys on every call.
102
+ if (_statObjects) {
103
+ for (const stat of _statObjects) {
104
+ stat.value = 0;
105
+ stat.preCapValue = 0;
106
+ }
107
+ }
108
+ const g = engineGlobals();
109
+ const itemData = g.g_EsoBuildItemData;
110
+ const enchantData = g.g_EsoBuildEnchantData;
111
+ const buffData = g.g_EsoBuildBuffData;
112
+ const toggleSkillData = g.g_EsoBuildToggledSkillData;
113
+ const emptyBar = () => Array.from({ length: 6 }, (_, i) => ({
114
+ skillId: 0,
115
+ origSkillId: 0,
116
+ morphIndex: 0,
117
+ slotIndex: i,
118
+ }));
119
+ // Items and enchantments
120
+ for (const slot of ALL_SLOTS) {
121
+ itemData[slot] = {};
122
+ if (enchantData)
123
+ enchantData[slot] = {};
124
+ }
125
+ // Champion Points
126
+ g.g_EsoCpData = {};
127
+ // Buffs: zero activation flags and counters (avoids stack bleed like Arcanist Crux)
128
+ if (buffData && typeof buffData === 'object') {
129
+ for (const key of Object.keys(buffData)) {
130
+ const b = buffData[key];
131
+ if (b && typeof b === 'object') {
132
+ b.enabled = false;
133
+ b.skillEnabled = false;
134
+ b.buffEnabled = false;
135
+ b.combatEnabled = false;
136
+ if (b.maxTimes != null && b.count == null)
137
+ b.count = 0;
138
+ }
139
+ }
140
+ }
141
+ // Toggle skills: zero enabled AND valid (valid stayed true from previous calls)
142
+ if (toggleSkillData && typeof toggleSkillData === 'object') {
143
+ for (const key of Object.keys(toggleSkillData)) {
144
+ const s = toggleSkillData[key];
145
+ if (s && typeof s === 'object') {
146
+ s.enabled = false;
147
+ s.combatEnabled = false;
148
+ s.valid = false;
149
+ }
150
+ }
151
+ }
152
+ // Skill bars and passives/actives
153
+ g.g_EsoSkillBarData = [emptyBar(), emptyBar()];
154
+ g.g_EsoSkillPassiveData = {};
155
+ g.g_EsoSkillActiveData = {};
156
+ // ─── DATA INJECTION ───────────────────────────────────────────────────────
81
157
  // -------------------------------------------------------------------------
82
- // PASSO 1: Injeta stats do personagem nos elementos mock do DOM.
83
- // O motor lê esses valores via jQuery: $("#esotbRace").val(), etc.
158
+ // STEP 1: Inject character stats into the mock DOM elements.
159
+ // The engine reads these values via jQuery: $("#esotbRace").val(), etc.
84
160
  // -------------------------------------------------------------------------
85
- (0, env_setup_1.resetDomValues)();
86
- const { character, items, championPointNodes, activeBuffs, toggleSkills, skillBars, activeWeaponBar, passiveSkills, autoPassives, enchantOverrides, } = input;
87
- (0, env_setup_1.setDomValue)('esotbRace', character.race);
88
- (0, env_setup_1.setDomValue)('esotbClass', character.class);
89
- (0, env_setup_1.setDomValue)('esotbLevel', String(Math.min(character.level, 50)));
90
- (0, env_setup_1.setDomValue)('esotbAttrHea', String(character.attributes.health ?? 0));
91
- (0, env_setup_1.setDomValue)('esotbAttrMag', String(character.attributes.magicka ?? 0));
92
- (0, env_setup_1.setDomValue)('esotbAttrSta', String(character.attributes.stamina ?? 0));
93
- // Mundus Stone (pedra de Mundus)
161
+ resetDomValues();
162
+ setDomValue('esotbRace', character.race);
163
+ setDomValue('esotbClass', character.class);
164
+ setDomValue('esotbLevel', String(Math.min(character.level, 50)));
165
+ setDomValue('esotbAttrHea', String(character.attributes.health ?? 0));
166
+ setDomValue('esotbAttrMag', String(character.attributes.magicka ?? 0));
167
+ setDomValue('esotbAttrSta', String(character.attributes.stamina ?? 0));
168
+ // Mundus Stone
94
169
  if (character.mundusStone) {
95
- (0, env_setup_1.setDomValue)('esotbMundus', character.mundusStone);
170
+ setDomValue('esotbMundus', character.mundusStone);
96
171
  }
97
- // Segunda Pedra de Mundus (requer set Twice-Born Star ou será ativada diretamente).
98
- // IsTwiceBornStarEnabled() lê a flag _esoWrapperTwiceBornOverride (patch em loader.ts)
99
- // quando o set não está equipado mas mundusStone2 é fornecido explicitamente.
100
- global._esoWrapperTwiceBornOverride = !!character.mundusStone2;
172
+ // Second Mundus Stone (requires the Twice-Born Star set or it is enabled directly).
173
+ // IsTwiceBornStarEnabled() reads the _esoWrapperTwiceBornOverride flag (patch in loader.ts)
174
+ // when the set is not equipped but mundusStone2 is provided explicitly.
175
+ g._esoWrapperTwiceBornOverride = !!character.mundusStone2;
101
176
  if (character.mundusStone2) {
102
- (0, env_setup_1.setDomValue)('esotbMundus2', character.mundusStone2);
177
+ setDomValue('esotbMundus2', character.mundusStone2);
103
178
  }
104
179
  // PvP Cyrodiil (Battle Spirit)
105
180
  if (character.cyrodiil) {
106
- (0, env_setup_1.setDomValue)('esotbCyrodiil', 'true'); // prop("checked") retorna true quando valor é "true"
181
+ setDomValue('esotbCyrodiil', 'true'); // prop("checked") returns true when the value is "true"
107
182
  }
108
- // Vampiro / Lobisomem
183
+ // Vampire / Werewolf
109
184
  if (character.vampireStage != null)
110
- (0, env_setup_1.setDomValue)('esotbVampireStage', String(character.vampireStage));
185
+ setDomValue('esotbVampireStage', String(character.vampireStage));
111
186
  if (character.werewolfStage != null)
112
- (0, env_setup_1.setDomValue)('esotbWerewolfStage', String(character.werewolfStage));
113
- // Champion Points (default 0 — garante que SpellCrit e WeaponCrit sejam numéricos)
114
- (0, env_setup_1.setDomValue)('esotbCPTotalPoints', String(character.championPoints ?? 0));
115
- // Versão das regras (padrão: Live)
116
- (0, env_setup_1.setDomValue)('esotbRulesVersion', character.rulesVersion ?? 'Live');
117
- // Campos obrigatórios com defaults seguros
118
- (0, env_setup_1.setDomValue)('esotbMountSpeedBonus', '0');
119
- (0, env_setup_1.setDomValue)('esotbBaseWalkSpeed', '3.0');
120
- (0, env_setup_1.setDomValue)('esotbBuildDescription', '');
121
- (0, env_setup_1.setDomValue)('esotbUsePtsRules', 'false');
122
- (0, env_setup_1.setDomValue)('esotbEnableRaceAutoPurchase', 'false');
123
- // Configuração do alvo (target) — necessária para AttackSpellMitigation e EffectivePower.
124
- // Target.EffectiveLevel = 0 causa divisão por zero na fórmula; padrão 50 é o nível máximo.
125
- (0, env_setup_1.setDomValue)('esotbTargetResistance', '18200'); // UESP default: CP160 enemy base resistance
126
- (0, env_setup_1.setDomValue)('esotbTargetEffectiveLevel', '66'); // UESP default: 66 = CP160 (endgame content)
127
- (0, env_setup_1.setDomValue)('esotbTargetCritResistFlat', '0');
128
- (0, env_setup_1.setDomValue)('esotbTargetPenetrationFlat', '0');
129
- (0, env_setup_1.setDomValue)('esotbTargetPenetrationFactor', '0');
130
- (0, env_setup_1.setDomValue)('esotbTargetDefenseBonus', '0');
131
- (0, env_setup_1.setDomValue)('esotbTargetAttackBonus', '0');
132
- (0, env_setup_1.setDomValue)('esotbTargetCritDamage', '0');
133
- (0, env_setup_1.setDomValue)('esotbTargetCritChance', '0');
134
- (0, env_setup_1.setDomValue)('esotbTargetPercentHealth', '100');
187
+ setDomValue('esotbWerewolfStage', String(character.werewolfStage));
188
+ // Champion Points (default 0 — ensures SpellCrit and WeaponCrit stay numeric)
189
+ setDomValue('esotbCPTotalPoints', String(character.championPoints ?? 0));
190
+ // Rules version (default: Live)
191
+ setDomValue('esotbRulesVersion', character.rulesVersion ?? 'Live');
192
+ // Required fields with safe defaults
193
+ setDomValue('esotbMountSpeedBonus', '0');
194
+ setDomValue('esotbBaseWalkSpeed', '3.0');
195
+ setDomValue('esotbBuildDescription', '');
196
+ setDomValue('esotbUsePtsRules', 'false');
197
+ setDomValue('esotbEnableRaceAutoPurchase', 'false');
198
+ // Target configuration — required for AttackSpellMitigation and EffectivePower.
199
+ // Target.EffectiveLevel = 0 causes a division by zero in the formula; 50 is the max level default.
200
+ setDomValue('esotbTargetResistance', '18200'); // UESP default: CP160 enemy base resistance
201
+ setDomValue('esotbTargetEffectiveLevel', '66'); // UESP default: 66 = CP160 (endgame content)
202
+ setDomValue('esotbTargetCritResistFlat', '0');
203
+ setDomValue('esotbTargetPenetrationFlat', '0');
204
+ setDomValue('esotbTargetPenetrationFactor', '0');
205
+ setDomValue('esotbTargetDefenseBonus', '0');
206
+ setDomValue('esotbTargetAttackBonus', '0');
207
+ setDomValue('esotbTargetCritDamage', '0');
208
+ setDomValue('esotbTargetCritChance', '0');
209
+ setDomValue('esotbTargetPercentHealth', '100');
135
210
  // -------------------------------------------------------------------------
136
- // PASSO 2: Injeta dados de itens DIRETAMENTE em g_EsoBuildItemData[slot].
211
+ // STEP 2: Inject item data DIRECTLY into g_EsoBuildItemData[slot].
137
212
  //
138
- // Esta é a ideia central: em vez de mockar jQuery para cada campo de item,
139
- // populamos a variável global que o motor lê nativamente.
140
- // Os dados vêm exatamente no formato da API pública da UESP — sem adaptação.
213
+ // This is the core idea: instead of mocking jQuery for each item field,
214
+ // we populate the global variable the engine reads natively.
215
+ // Data comes exactly in the UESP public API format — no adaptation.
141
216
  // -------------------------------------------------------------------------
142
- const itemData = global.g_EsoBuildItemData;
143
- const enchantData = global.g_EsoBuildEnchantData;
144
- // Reseta todos os slots (evita dados de chamada anterior)
145
- for (const slot of ALL_SLOTS) {
146
- itemData[slot] = {};
147
- if (enchantData) {
148
- // Se há override customizado, popula g_EsoBuildEnchantData[slot] com os dados do glyph.
149
- // Isso faz GetEsoEnchantData() usar o caminho customizado (isDefaultEnchant=false),
150
- // e o engine aplica o fator de escala 0.4044 para slots pequenos (Hands/Waist/Feet/Shoulders).
151
- // Clone raso para evitar mutação entre chamadas.
152
- const override = enchantOverrides?.[slot];
153
- enchantData[slot] = override ? { ...override } : {};
217
+ // Custom enchantments: make GetEsoEnchantData() take the isDefaultEnchant=false path,
218
+ // applying the 0.4044 factor for small slots (Hands/Waist/Feet/Shoulders).
219
+ if (enchantData && enchantOverrides) {
220
+ for (const slot of ALL_SLOTS) {
221
+ const override = enchantOverrides[slot];
222
+ if (override)
223
+ enchantData[slot] = { ...override };
154
224
  }
155
225
  }
156
- // Reseta g_EsoCpData para evitar bleed-through entre chamadas
157
- global.g_EsoCpData = {};
158
- const cpDataGlobal = global.g_EsoCpData;
159
- // Injeta os itens fornecidos (normalizados com defaults seguros)
226
+ const cpDataGlobal = g.g_EsoCpData;
227
+ // Inject the provided items (normalized with safe defaults)
160
228
  if (items) {
161
229
  for (const [slot, item] of Object.entries(items)) {
162
230
  if (item && item.itemId) {
@@ -165,22 +233,22 @@ function calculateBuild(input) {
165
233
  }
166
234
  }
167
235
  // -------------------------------------------------------------------------
168
- // PASSO 3a: Injeta nodes do Champion Points 2.
236
+ // STEP 3a: Inject Champion Points 2 nodes.
169
237
  //
170
- // Dois caminhos dependendo se buildRules.cp está carregado:
238
+ // Two paths depending on whether buildRules.cp is loaded:
171
239
  //
172
- // Novo (preferido): quando g_EsoBuildRules['cp'] existe, o motor usa o caminho
173
- // GetEsoBuildCpRuleValues que lê cpData.description e faz match contra
174
- // ESO_CPEFFECT_MATCHES. Populamos g_EsoCpData[nodeId] com a description completa.
240
+ // New (preferred): when g_EsoBuildRules['cp'] exists, the engine uses the
241
+ // GetEsoBuildCpRuleValues path which reads cpData.description and matches against
242
+ // ESO_CPEFFECT_MATCHES. We populate g_EsoCpData[nodeId] with the full description.
175
243
  //
176
- // Legado: sem cp rules, o motor usa ParseEsoCP2Value com injeção DOM via
177
- // $("#skill_<id>").attr("unlocked") e $("#descskill_<id>").text().
244
+ // Legacy: without cp rules, the engine uses ParseEsoCP2Value with DOM injection via
245
+ // $("#skill_<id>").attr("unlocked") and $("#descskill_<id>").text().
178
246
  // -------------------------------------------------------------------------
179
247
  if (championPointNodes && Object.keys(championPointNodes).length > 0) {
180
- (0, env_setup_1.setDomValue)('esotbEnableCP', 'true');
181
- const hasCpRules = !!global.g_EsoBuildRules?.cp;
182
- const cpSkills = global.g_EsoCpSkills ?? {};
183
- const cpSkillDesc = global.g_EsoCpSkillDesc ?? {};
248
+ setDomValue('esotbEnableCP', 'true');
249
+ const hasCpRules = !!g.g_EsoBuildRules?.cp;
250
+ const cpSkills = g.g_EsoCpSkills ?? {};
251
+ const cpSkillDesc = g.g_EsoCpSkillDesc ?? {};
184
252
  // Build reverse map: name → numeric nodeId (for named-string keys in the input)
185
253
  const cpNameToId = {};
186
254
  for (const [id, meta] of Object.entries(cpSkills)) {
@@ -201,13 +269,13 @@ function calculateBuild(input) {
201
269
  }
202
270
  for (const [nodeId, nodeData] of resolvedNodes) {
203
271
  if (hasCpRules) {
204
- // Resolve o nome a partir dos metadados capturados do browser.
272
+ // Resolve the name from the metadata captured from the browser.
205
273
  const name = cpSkills[nodeId]?.name ?? `CP_${nodeId}`;
206
- // Resolve a descrição dinamicamente:
207
- // 1. Override explícito do chamador
208
- // 2. Lookup exato por nodeData.points em g_EsoCpSkillDesc[nodeId]
209
- // 3. Floor lookup: maior chave disponível ≤ points
210
- // 4. Fallback: chave 0 ou primeira disponível
274
+ // Resolve the description dynamically:
275
+ // 1. Explicit caller override
276
+ // 2. Exact lookup by nodeData.points in g_EsoCpSkillDesc[nodeId]
277
+ // 3. Floor lookup: largest available key ≤ points
278
+ // 4. Fallback: key 0 or first available
211
279
  let desc = nodeData.description;
212
280
  if (!desc && cpSkillDesc[nodeId]) {
213
281
  const nodeDescMap = cpSkillDesc[nodeId];
@@ -231,41 +299,31 @@ function calculateBuild(input) {
231
299
  if (desc) {
232
300
  // Strip HTML tags and ESO color codes (|cHHHHHH...|r) so the engine's regex matching works on plain text
233
301
  const plainDesc = desc.replace(/<[^>]+>/g, '').replace(/\|c[0-9a-fA-F]{6}|\|r/g, '');
234
- cpDataGlobal[nodeId] = { type: 'skill', isUnlocked: true, description: plainDesc, name };
302
+ // isUnlocked: use explicit value from fixture when present; default true for old fixtures without it
303
+ const isUnlocked = nodeData.isUnlocked !== undefined ? nodeData.isUnlocked : true;
304
+ cpDataGlobal[nodeId] = { type: 'skill', isUnlocked, description: plainDesc, name };
235
305
  }
236
- // sem descrição resolvível → node ignorado (sem efeito no motor)
306
+ // no resolvable description → node ignored (no engine effect)
237
307
  }
238
308
  else {
239
- // caminho legado: injeção DOM para ParseEsoCP2Value
309
+ // legacy path: DOM injection for ParseEsoCP2Value
240
310
  const bonus = nodeData.currentBonus;
241
311
  const bonusStr = typeof bonus === 'string' && bonus.endsWith('%')
242
312
  ? `Current value: ${bonus}`
243
313
  : `Current bonus: ${bonus}`;
244
- (0, env_setup_1.setDomAttr)(`skill_${nodeId}`, 'unlocked', '1');
245
- (0, env_setup_1.setDomTextContent)(`descskill_${nodeId}`, bonusStr);
246
- (0, env_setup_1.setDomTextContent)(`descskill_${nodeId}_prev`, `CP Node ${nodeId}`);
314
+ setDomAttr(`skill_${nodeId}`, 'unlocked', '1');
315
+ setDomTextContent(`descskill_${nodeId}`, bonusStr);
316
+ setDomTextContent(`descskill_${nodeId}_prev`, `CP Node ${nodeId}`);
247
317
  }
248
318
  }
249
319
  }
250
320
  // -------------------------------------------------------------------------
251
- // PASSO 3b: Habilita buffs ativos.
321
+ // STEP 3b: Enable active buffs.
252
322
  //
253
- // g_EsoBuildBuffData é um Proxy que auto-cria entradas.
254
- // Setar .enabled = true é suficiente para IsEsoBuffEnabled() retornar true.
255
- // Reseta todos os buffs antes para evitar bleed-through entre chamadas.
323
+ // g_EsoBuildBuffData is a Proxy that auto-creates entries.
324
+ // Setting .enabled = true is enough for IsEsoBuffEnabled() to return true.
325
+ // (reset already done in the global reset block above)
256
326
  // -------------------------------------------------------------------------
257
- const buffData = global.g_EsoBuildBuffData;
258
- if (buffData && typeof buffData === 'object') {
259
- for (const key of Object.keys(buffData)) {
260
- const b = buffData[key];
261
- if (b && typeof b === 'object') {
262
- b.enabled = false;
263
- b.skillEnabled = false;
264
- b.buffEnabled = false;
265
- b.combatEnabled = false;
266
- }
267
- }
268
- }
269
327
  if (activeBuffs) {
270
328
  for (const buffName of activeBuffs) {
271
329
  if (buffData) {
@@ -279,21 +337,11 @@ function calculateBuild(input) {
279
337
  }
280
338
  }
281
339
  // -------------------------------------------------------------------------
282
- // PASSO 3c: Habilita toggle skills.
340
+ // STEP 3c: Enable toggle skills.
283
341
  //
284
- // IsEsoBuildToggledSkillEnabled() verifica: skillData.valid && skillData.enabled
285
- // Reseta todos os toggles antes para evitar bleed-through entre chamadas.
342
+ // IsEsoBuildToggledSkillEnabled() checks: skillData.valid && skillData.enabled
343
+ // (reset of enabled, combatEnabled and valid already done in the global reset block above)
286
344
  // -------------------------------------------------------------------------
287
- const toggleSkillData = global.g_EsoBuildToggledSkillData;
288
- if (toggleSkillData && typeof toggleSkillData === 'object') {
289
- for (const key of Object.keys(toggleSkillData)) {
290
- const s = toggleSkillData[key];
291
- if (s && typeof s === 'object') {
292
- s.enabled = false;
293
- s.combatEnabled = false;
294
- }
295
- }
296
- }
297
345
  if (toggleSkills) {
298
346
  for (const skillName of toggleSkills) {
299
347
  if (toggleSkillData) {
@@ -305,23 +353,17 @@ function calculateBuild(input) {
305
353
  }
306
354
  }
307
355
  // -------------------------------------------------------------------------
308
- // PASSO 3d: Injeta skill bars em g_EsoSkillBarData.
356
+ // STEP 3d: Inject skill bars into g_EsoSkillBarData.
309
357
  //
310
- // O motor usa g_EsoSkillBarData para detectar skill lines ativas e aplicar
311
- // passivos condicionais (ex: passivos de Destruction Staff, set bonuses que
312
- // afetam "Class abilities", etc.).
358
+ // The engine uses g_EsoSkillBarData to detect active skill lines and apply
359
+ // conditional passives (e.g. Destruction Staff passives, set bonuses affecting
360
+ // "Class abilities", etc.).
313
361
  //
314
- // LIMITAÇÃO ATUAL: sem g_SkillsData no uesp-init-data.json, os skill IDs são
315
- // injetados mas os passivos de skill line não geram stats. Será funcional
316
- // quando g_SkillsData for adicionado ao JSON de extração.
362
+ // CURRENT LIMITATION: without g_SkillsData in uesp-init-data.json, skill IDs are
363
+ // injected but skill line passives produce no stats. It becomes functional
364
+ // once g_SkillsData is added to the extraction JSON.
365
+ // (g_EsoSkillBarData already initialized with empty bars in the reset block above)
317
366
  // -------------------------------------------------------------------------
318
- const emptyBar = () => Array.from({ length: 6 }, (_, i) => ({
319
- skillId: 0,
320
- origSkillId: 0,
321
- morphIndex: 0,
322
- slotIndex: i,
323
- }));
324
- global.g_EsoSkillBarData = [emptyBar(), emptyBar()];
325
367
  if (skillBars) {
326
368
  const barMap = [
327
369
  [skillBars.bar1, 0],
@@ -331,7 +373,7 @@ function calculateBuild(input) {
331
373
  if (!slots)
332
374
  continue;
333
375
  slots.slice(0, 6).forEach((slot, slotIndex) => {
334
- global.g_EsoSkillBarData[barIndex][slotIndex] = {
376
+ g.g_EsoSkillBarData[barIndex][slotIndex] = {
335
377
  skillId: slot.skillId,
336
378
  origSkillId: slot.skillId,
337
379
  morphIndex: slot.morphIndex ?? 0,
@@ -341,26 +383,25 @@ function calculateBuild(input) {
341
383
  }
342
384
  }
343
385
  // -------------------------------------------------------------------------
344
- // PASSO 3d-2: Popula g_EsoSkillPassiveData com os passivos do personagem.
386
+ // STEP 3d-2: Populate g_EsoSkillPassiveData with the character's passives.
345
387
  //
346
- // O engine itera g_EsoSkillPassiveData em GetEsoInputSkillPassives e aplica
347
- // cada passivo via regex no texto da descrição (ESO_PASSIVEEFFECT_MATCHES).
348
- // Requer g_SkillsData + GetEsoSkillDescription (de esoskills.js).
388
+ // The engine iterates g_EsoSkillPassiveData in GetEsoInputSkillPassives and applies
389
+ // each passive via regex on the description text (ESO_PASSIVEEFFECT_MATCHES).
390
+ // Requires g_SkillsData + GetEsoSkillDescription (from esoskills.js).
349
391
  //
350
- // Cada entrada: { abilityId } — o engine busca g_SkillsData[abilityId] para
351
- // obter coeficientes e gerar a descrição do passivo.
392
+ // Each entry: { abilityId } — the engine looks up g_SkillsData[abilityId] to
393
+ // get coefficients and generate the passive description.
394
+ // (g_EsoSkillPassiveData already zeroed in the reset block above)
352
395
  // -------------------------------------------------------------------------
353
- global.g_EsoSkillPassiveData = {};
354
396
  const allPassiveIds = new Set(passiveSkills ?? []);
355
397
  if (autoPassives) {
356
398
  // Use the loader snapshot (written before any calculations) to avoid raceType mutation.
357
- const snapshot = global.g_EsoPassiveSkillSnapshot;
399
+ const snapshot = g.g_EsoPassiveSkillSnapshot;
358
400
  if (snapshot) {
359
401
  for (const v of Object.values(snapshot)) {
360
402
  if (!v)
361
403
  continue;
362
- if ((v.raceType === character.race || v.classType === character.class) &&
363
- (v.nextSkill === -1 || String(v.nextSkill) === '-1')) {
404
+ if (v.raceType === character.race && (v.nextSkill === -1 || String(v.nextSkill) === '-1')) {
364
405
  allPassiveIds.add(Number(v.abilityId));
365
406
  }
366
407
  }
@@ -371,16 +412,16 @@ function calculateBuild(input) {
371
412
  for (const abilityId of allPassiveIds) {
372
413
  passiveData[String(abilityId)] = { abilityId };
373
414
  }
374
- global.g_EsoSkillPassiveData = passiveData;
415
+ g.g_EsoSkillPassiveData = passiveData;
375
416
  }
376
417
  // -------------------------------------------------------------------------
377
- // PASSO 3d-3: Popula g_EsoSkillActiveData a partir das skill bars.
418
+ // STEP 3d-3: Populate g_EsoSkillActiveData from the skill bars.
378
419
  //
379
- // GetEsoInputSkillActiveBar lê g_EsoSkillBarData[barra][slot].origSkillId e
380
- // busca g_EsoSkillActiveData[origSkillId].abilityId para obter a descrição.
381
- // Populamos automaticamente para todos os slots não-vazios das barras.
420
+ // GetEsoInputSkillActiveBar reads g_EsoSkillBarData[bar][slot].origSkillId and
421
+ // looks up g_EsoSkillActiveData[origSkillId].abilityId for the description.
422
+ // We populate automatically for every non-empty bar slot.
423
+ // (g_EsoSkillActiveData already zeroed in the reset block above)
382
424
  // -------------------------------------------------------------------------
383
- global.g_EsoSkillActiveData = {};
384
425
  if (skillBars) {
385
426
  const activeData = {};
386
427
  const bars = [skillBars.bar1, skillBars.bar2];
@@ -395,37 +436,60 @@ function calculateBuild(input) {
395
436
  }
396
437
  }
397
438
  }
398
- global.g_EsoSkillActiveData = activeData;
439
+ g.g_EsoSkillActiveData = activeData;
399
440
  }
400
441
  // -------------------------------------------------------------------------
401
- // PASSO 3e: Define qual barra de armas/skills está ativa.
442
+ // STEP 3e: Set which weapon/skill bar is active.
402
443
  //
403
- // g_EsoBuildActiveWeapon — slots MainHand/OffHand ativos para set bonuses.
404
- // g_EsoBuildActiveAbilityBar — índice 1-based lido por CountEsoBarSkillsWithSkillLine/Type
405
- // para passivos que escalam com skills na barra (Expert Mage, Magicka Controller, etc.).
406
- // Ambos precisam ser sincronizados; usar valores distintos resulta em passivos errados.
444
+ // g_EsoBuildActiveWeapon — active MainHand/OffHand slots for set bonuses.
445
+ // g_EsoBuildActiveAbilityBar — 1-based index read by CountEsoBarSkillsWithSkillLine/Type
446
+ // for passives scaling with slotted skills (Expert Mage, Magicka Controller, etc.).
447
+ // Both must be kept in sync; distinct values produce wrong passives.
407
448
  // -------------------------------------------------------------------------
408
449
  const activeBar = activeWeaponBar ?? 1;
409
- global.g_EsoBuildActiveWeapon = activeBar;
410
- global.g_EsoBuildActiveAbilityBar = activeBar;
450
+ g.g_EsoBuildActiveWeapon = activeBar;
451
+ g.g_EsoBuildActiveAbilityBar = activeBar;
411
452
  // -------------------------------------------------------------------------
412
- // PASSO 4: Executa o cálculo.
453
+ // STEP 4: Run the calculation.
413
454
  //
414
455
  // UpdateEsoComputedStatsList_Real(keepSaveResults, noUpdate)
415
- // - keepSaveResults = null → reseta os resultados salvos (comportamento padrão)
416
- // - noUpdate = true → pula DisplayEsoAllComputedStats e UpdateReadOnlyStats
417
- // (operações de DOM que não precisamos)
456
+ // - keepSaveResults = null → resets saved results (default behavior)
457
+ // - noUpdate = true → skips DisplayEsoAllComputedStats and UpdateReadOnlyStats
458
+ // (DOM operations we don't need)
418
459
  // -------------------------------------------------------------------------
419
- const updateFn = global.UpdateEsoComputedStatsList_Real;
460
+ const updateFn = g.UpdateEsoComputedStatsList_Real;
420
461
  if (typeof updateFn !== 'function') {
421
- throw new Error('[eso-engine] UpdateEsoComputedStatsList_Real não está disponível. ' +
422
- 'Certifique-se de chamar initEsoEngine() antes de calculateBuild().');
462
+ throw new Error('[eso-engine] UpdateEsoComputedStatsList_Real is not available. ' +
463
+ 'Make sure to call initEsoEngineFromData() before calculateBuild().');
464
+ }
465
+ // Patch IsEsoBuildToggledSetEnabled to honour toggledSetBonuses.
466
+ // The UESP engine reads this inside GetEsoInputSetValues (called after UpdateEsoItemSets
467
+ // sets valid=true for equipped sets). Without this patch, toggle set bonuses are never
468
+ // enabled in Node because the DOM checkboxes used by UpdateEsoBuildToggledSetData are empty.
469
+ const toggledSetIds = new Set(toggledSetBonuses ?? []);
470
+ const origIsEnabled = g.IsEsoBuildToggledSetEnabled;
471
+ if (toggledSetIds.size > 0) {
472
+ g.IsEsoBuildToggledSetEnabled = function (setId) {
473
+ if (toggledSetIds.has(String(setId))) {
474
+ const td = g.g_EsoBuildToggledSetData?.[String(setId)];
475
+ if (td?.valid)
476
+ return true;
477
+ }
478
+ return origIsEnabled.call(this, setId);
479
+ };
480
+ }
481
+ try {
482
+ updateFn(null, true);
483
+ }
484
+ finally {
485
+ if (toggledSetIds.size > 0) {
486
+ g.IsEsoBuildToggledSetEnabled = origIsEnabled;
487
+ }
423
488
  }
424
- updateFn(null, true);
425
489
  // -------------------------------------------------------------------------
426
- // PASSO 5: Lê os resultados de g_EsoComputedStats[statId].value
490
+ // STEP 5: Read the results from g_EsoComputedStats[statId].value
427
491
  // -------------------------------------------------------------------------
428
- const computedStats = global.g_EsoComputedStats ?? {};
492
+ const computedStats = g.g_EsoComputedStats ?? {};
429
493
  const raw = {};
430
494
  for (const statId of Object.keys(computedStats)) {
431
495
  const stat = computedStats[statId];
@@ -434,40 +498,40 @@ function calculateBuild(input) {
434
498
  }
435
499
  }
436
500
  return {
437
- // Atributos máximos
501
+ // Max attributes
438
502
  Health: raw['Health'] ?? 0,
439
503
  Magicka: raw['Magicka'] ?? 0,
440
504
  Stamina: raw['Stamina'] ?? 0,
441
- // Regeneração
505
+ // Regeneration
442
506
  HealthRegen: raw['HealthRegen'] ?? 0,
443
507
  MagickaRegen: raw['MagickaRegen'] ?? 0,
444
508
  StaminaRegen: raw['StaminaRegen'] ?? 0,
445
- // Dano
509
+ // Damage
446
510
  WeaponDamage: raw['WeaponDamage'] ?? 0,
447
511
  SpellDamage: raw['SpellDamage'] ?? 0,
448
- // Crítico
512
+ // Critical
449
513
  WeaponCrit: raw['WeaponCrit'] ?? 0,
450
514
  SpellCrit: raw['SpellCrit'] ?? 0,
451
515
  SpellCritDamage: raw['SpellCritDamage'] ?? 0,
452
516
  WeaponCritDamage: raw['WeaponCritDamage'] ?? 0,
453
- // Resistências
517
+ // Resistances
454
518
  PhysicalResist: raw['PhysicalResist'] ?? 0,
455
519
  SpellResist: raw['SpellResist'] ?? 0,
456
520
  CritResist: raw['CritResist'] ?? 0,
457
- // Penetração
521
+ // Penetration
458
522
  PhysicalPenetration: raw['PhysicalPenetration'] ?? 0,
459
523
  SpellPenetration: raw['SpellPenetration'] ?? 0,
460
- // Poder efetivo
524
+ // Effective power
461
525
  EffectiveSpellPower: raw['EffectiveSpellPower'] ?? 0,
462
526
  EffectiveWeaponPower: raw['EffectiveWeaponPower'] ?? 0,
463
527
  EffectivePower: raw['EffectivePower'] ?? 0,
464
- // Cura
528
+ // Healing
465
529
  HealingDone: raw['HealingDone'] ?? 0,
466
530
  HealingTaken: raw['HealingTaken'] ?? 0,
467
- // Velocidade
531
+ // Speed
468
532
  RunSpeed: raw['RunSpeed'] ?? 0,
469
533
  SprintSpeed: raw['SprintSpeed'] ?? 0,
470
- // Mitigação
534
+ // Mitigation
471
535
  AttackSpellMitigation: raw['AttackSpellMitigation'] ?? 0,
472
536
  AttackPhysicalMitigation: raw['AttackPhysicalMitigation'] ?? 0,
473
537
  DefenseSpellMitigation: raw['DefenseSpellMitigation'] ?? 0,
@@ -481,14 +545,14 @@ function calculateBuild(input) {
481
545
  * Each entry maps to one row in the UESP buff tab. Pass `entry.name` to
482
546
  * `BuildInput.activeBuffs` to enable that buff in a calculation.
483
547
  *
484
- * Must be called after `initEsoEngine()`.
548
+ * Must be called after `initEsoEngineFromData()`.
485
549
  *
486
550
  * @param group - Optional filter. Ex: "Major", "Minor", "Set", "Target",
487
551
  * "Skill", "Potion", "Poison", "Cyrodiil", "Other".
488
552
  * Omit to return all 164 buffs.
489
553
  */
490
- function listAvailableBuffs(group) {
491
- const buffData = global.g_EsoBuildBuffData;
554
+ export function listAvailableBuffs(group) {
555
+ const buffData = engineGlobals().g_EsoBuildBuffData;
492
556
  if (!buffData || typeof buffData !== 'object')
493
557
  return [];
494
558
  const result = [];
@@ -518,7 +582,7 @@ function listAvailableBuffs(group) {
518
582
  // Uses g_EsoPassiveSkillSnapshot (written by loader.ts at engine init time) to avoid
519
583
  // the engine's mutation of g_SkillsData.raceType during calculations.
520
584
  function buildPassiveSkillInfos(filter, sortKey) {
521
- const snapshot = global.g_EsoPassiveSkillSnapshot;
585
+ const snapshot = engineGlobals().g_EsoPassiveSkillSnapshot;
522
586
  if (!snapshot || typeof snapshot !== 'object')
523
587
  return [];
524
588
  return Object.values(snapshot)
@@ -540,12 +604,12 @@ function buildPassiveSkillInfos(filter, sortKey) {
540
604
  * Each passive may appear in multiple ranks (rank 1, 2, 3).
541
605
  * Pass the abilityId of the desired rank to BuildInput.passiveSkills.
542
606
  *
543
- * Must be called after initEsoEngine().
607
+ * Must be called after initEsoEngineFromData().
544
608
  *
545
609
  * @param race - Race name as passed to BuildInput.character.race.
546
610
  * Ex: "High Elf", "Nord", "Khajiit"
547
611
  */
548
- function listRacialPassives(race) {
612
+ export function listRacialPassives(race) {
549
613
  return buildPassiveSkillInfos((v) => v.raceType === race, (v) => v.baseName);
550
614
  }
551
615
  /**
@@ -553,12 +617,12 @@ function listRacialPassives(race) {
553
617
  * Each passive may appear in multiple ranks (rank 1, 2, 3).
554
618
  * Pass the abilityId of the desired rank to BuildInput.passiveSkills.
555
619
  *
556
- * Must be called after initEsoEngine().
620
+ * Must be called after initEsoEngineFromData().
557
621
  *
558
622
  * @param className - Class name as passed to BuildInput.character.class.
559
623
  * Ex: "Sorcerer", "Nightblade", "Dragonknight"
560
624
  */
561
- function listClassPassives(className) {
625
+ export function listClassPassives(className) {
562
626
  return buildPassiveSkillInfos((v) => v.classType === className, (v) => (v.skillLine ?? '') + v.baseName);
563
627
  }
564
628
  /**
@@ -566,7 +630,7 @@ function listClassPassives(className) {
566
630
  * Each passive may appear in multiple ranks (rank 1, 2, 3).
567
631
  * Pass the abilityId of the desired rank to BuildInput.passiveSkills.
568
632
  *
569
- * Must be called after initEsoEngine().
633
+ * Must be called after initEsoEngineFromData().
570
634
  *
571
635
  * @param skillLine - Skill line name as it appears in g_SkillsData.
572
636
  * Use listAvailableSkillLines() to discover valid names.
@@ -574,17 +638,17 @@ function listClassPassives(className) {
574
638
  * "Fighters Guild", "Mages Guild", "Psijic Order", "Assault", "Support",
575
639
  * "Vampire", "Werewolf"
576
640
  */
577
- function listPassivesBySkillLine(skillLine) {
641
+ export function listPassivesBySkillLine(skillLine) {
578
642
  return buildPassiveSkillInfos((v) => v.skillLine === skillLine, (v) => v.baseName);
579
643
  }
580
644
  /**
581
645
  * Returns all skill line names that have passive skills available.
582
646
  * Use the returned names with listPassivesBySkillLine().
583
647
  *
584
- * Must be called after initEsoEngine().
648
+ * Must be called after initEsoEngineFromData().
585
649
  */
586
- function listAvailableSkillLines() {
587
- const snapshot = global.g_EsoPassiveSkillSnapshot;
650
+ export function listAvailableSkillLines() {
651
+ const snapshot = engineGlobals().g_EsoPassiveSkillSnapshot;
588
652
  if (!snapshot || typeof snapshot !== 'object')
589
653
  return [];
590
654
  const lines = new Set();
@@ -602,10 +666,10 @@ function listAvailableSkillLines() {
602
666
  * Toggle skills backed by a passive (isPassive=true) need the associated skill
603
667
  * in passiveSkills/skillBars for the engine to process the description match.
604
668
  *
605
- * Must be called after initEsoEngine().
669
+ * Must be called after initEsoEngineFromData().
606
670
  */
607
- function listAvailableToggleSkills() {
608
- const toggleData = global.g_EsoBuildToggledSkillData;
671
+ export function listAvailableToggleSkills() {
672
+ const toggleData = engineGlobals().g_EsoBuildToggledSkillData;
609
673
  if (!toggleData || typeof toggleData !== 'object')
610
674
  return [];
611
675
  const result = [];