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.
- package/README.md +77 -123
- package/README.pt-BR.md +122 -0
- package/dist/lib/eso-engine/calculator.d.ts +33 -22
- package/dist/lib/eso-engine/calculator.d.ts.map +1 -1
- package/dist/lib/eso-engine/calculator.js +283 -219
- package/dist/lib/eso-engine/calculator.js.map +1 -1
- package/dist/lib/eso-engine/debug.d.ts +63 -0
- package/dist/lib/eso-engine/debug.d.ts.map +1 -0
- package/dist/lib/eso-engine/debug.js +98 -0
- package/dist/lib/eso-engine/debug.js.map +1 -0
- package/dist/lib/eso-engine/engine-globals.d.ts +171 -0
- package/dist/lib/eso-engine/engine-globals.d.ts.map +1 -0
- package/dist/lib/eso-engine/engine-globals.js +24 -0
- package/dist/lib/eso-engine/engine-globals.js.map +1 -0
- package/dist/lib/eso-engine/env-setup.d.ts +15 -15
- package/dist/lib/eso-engine/env-setup.d.ts.map +1 -1
- package/dist/lib/eso-engine/env-setup.js +42 -58
- package/dist/lib/eso-engine/env-setup.js.map +1 -1
- package/dist/lib/eso-engine/index.d.ts +29 -23
- package/dist/lib/eso-engine/index.d.ts.map +1 -1
- package/dist/lib/eso-engine/index.js +33 -85
- package/dist/lib/eso-engine/index.js.map +1 -1
- package/dist/lib/eso-engine/input-stats.d.ts.map +1 -1
- package/dist/lib/eso-engine/input-stats.js +450 -129
- package/dist/lib/eso-engine/input-stats.js.map +1 -1
- package/dist/lib/eso-engine/loader.d.ts +15 -15
- package/dist/lib/eso-engine/loader.d.ts.map +1 -1
- package/dist/lib/eso-engine/loader.js +98 -130
- package/dist/lib/eso-engine/loader.js.map +1 -1
- package/dist/lib/eso-engine/types.d.ts +133 -90
- package/dist/lib/eso-engine/types.d.ts.map +1 -1
- package/dist/lib/eso-engine/types.js +2 -3
- package/dist/lib/eso-engine/types.js.map +1 -1
- package/dist/lib/uesp-data/extract.d.ts +29 -0
- package/dist/lib/uesp-data/extract.d.ts.map +1 -0
- package/dist/lib/uesp-data/extract.js +304 -0
- package/dist/lib/uesp-data/extract.js.map +1 -0
- package/dist/lib/uesp-data/index.d.ts +14 -0
- package/dist/lib/uesp-data/index.d.ts.map +1 -0
- package/dist/lib/uesp-data/index.js +11 -0
- package/dist/lib/uesp-data/index.js.map +1 -0
- package/dist/lib/uesp-data/mariadb-dump.d.ts +32 -0
- package/dist/lib/uesp-data/mariadb-dump.d.ts.map +1 -0
- package/dist/lib/uesp-data/mariadb-dump.js +222 -0
- package/dist/lib/uesp-data/mariadb-dump.js.map +1 -0
- package/dist/lib/uesp-data/seed.d.ts +43 -0
- package/dist/lib/uesp-data/seed.d.ts.map +1 -0
- package/dist/lib/uesp-data/seed.js +413 -0
- package/dist/lib/uesp-data/seed.js.map +1 -0
- package/package.json +29 -12
- package/vendor/uesp-esochardata/resources/esoEditBuild.js +3 -3
- package/dist/browser-stub.js +0 -1
|
@@ -1,32 +1,33 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
|
-
*
|
|
2
|
+
* ESO build calculation engine.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
4
|
+
* Implements the core architecture idea:
|
|
6
5
|
*
|
|
7
|
-
* 1.
|
|
8
|
-
* (jQuery
|
|
6
|
+
* 1. Inject character data into the mock DOM elements
|
|
7
|
+
* (jQuery will read via $("#esotbRace").val(), etc.)
|
|
9
8
|
*
|
|
10
|
-
* 2.
|
|
11
|
-
* —
|
|
12
|
-
*
|
|
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.
|
|
16
|
-
* —
|
|
17
|
-
*
|
|
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.
|
|
18
|
+
* 4. Read the results from g_EsoComputedStats[statId].value
|
|
20
19
|
*/
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
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
|
-
*
|
|
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 '
|
|
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, //
|
|
85
|
+
* Chest: chestItemFromUespApi, // object returned by esolog.uesp.net/exportJson.php
|
|
75
86
|
* },
|
|
76
87
|
* });
|
|
77
|
-
* console.log(stats.
|
|
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
|
-
//
|
|
83
|
-
//
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
170
|
+
setDomValue('esotbMundus', character.mundusStone);
|
|
96
171
|
}
|
|
97
|
-
//
|
|
98
|
-
// IsTwiceBornStarEnabled()
|
|
99
|
-
//
|
|
100
|
-
|
|
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
|
-
|
|
177
|
+
setDomValue('esotbMundus2', character.mundusStone2);
|
|
103
178
|
}
|
|
104
179
|
// PvP Cyrodiil (Battle Spirit)
|
|
105
180
|
if (character.cyrodiil) {
|
|
106
|
-
|
|
181
|
+
setDomValue('esotbCyrodiil', 'true'); // prop("checked") returns true when the value is "true"
|
|
107
182
|
}
|
|
108
|
-
//
|
|
183
|
+
// Vampire / Werewolf
|
|
109
184
|
if (character.vampireStage != null)
|
|
110
|
-
|
|
185
|
+
setDomValue('esotbVampireStage', String(character.vampireStage));
|
|
111
186
|
if (character.werewolfStage != null)
|
|
112
|
-
|
|
113
|
-
// Champion Points (default 0 —
|
|
114
|
-
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
//
|
|
124
|
-
// Target.EffectiveLevel = 0
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
//
|
|
211
|
+
// STEP 2: Inject item data DIRECTLY into g_EsoBuildItemData[slot].
|
|
137
212
|
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
157
|
-
|
|
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
|
-
//
|
|
236
|
+
// STEP 3a: Inject Champion Points 2 nodes.
|
|
169
237
|
//
|
|
170
|
-
//
|
|
238
|
+
// Two paths depending on whether buildRules.cp is loaded:
|
|
171
239
|
//
|
|
172
|
-
//
|
|
173
|
-
// GetEsoBuildCpRuleValues
|
|
174
|
-
// ESO_CPEFFECT_MATCHES.
|
|
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
|
-
//
|
|
177
|
-
// $("#skill_<id>").attr("unlocked")
|
|
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
|
-
|
|
181
|
-
const hasCpRules = !!
|
|
182
|
-
const cpSkills =
|
|
183
|
-
const cpSkillDesc =
|
|
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
|
|
272
|
+
// Resolve the name from the metadata captured from the browser.
|
|
205
273
|
const name = cpSkills[nodeId]?.name ?? `CP_${nodeId}`;
|
|
206
|
-
// Resolve
|
|
207
|
-
// 1.
|
|
208
|
-
// 2.
|
|
209
|
-
// 3. Floor lookup:
|
|
210
|
-
// 4. Fallback:
|
|
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
|
-
|
|
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
|
-
//
|
|
306
|
+
// no resolvable description → node ignored (no engine effect)
|
|
237
307
|
}
|
|
238
308
|
else {
|
|
239
|
-
//
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
//
|
|
321
|
+
// STEP 3b: Enable active buffs.
|
|
252
322
|
//
|
|
253
|
-
// g_EsoBuildBuffData
|
|
254
|
-
//
|
|
255
|
-
//
|
|
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
|
-
//
|
|
340
|
+
// STEP 3c: Enable toggle skills.
|
|
283
341
|
//
|
|
284
|
-
// IsEsoBuildToggledSkillEnabled()
|
|
285
|
-
//
|
|
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
|
-
//
|
|
356
|
+
// STEP 3d: Inject skill bars into g_EsoSkillBarData.
|
|
309
357
|
//
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
//
|
|
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
|
-
//
|
|
315
|
-
//
|
|
316
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
386
|
+
// STEP 3d-2: Populate g_EsoSkillPassiveData with the character's passives.
|
|
345
387
|
//
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
//
|
|
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
|
-
//
|
|
351
|
-
//
|
|
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 =
|
|
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 (
|
|
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
|
-
|
|
415
|
+
g.g_EsoSkillPassiveData = passiveData;
|
|
375
416
|
}
|
|
376
417
|
// -------------------------------------------------------------------------
|
|
377
|
-
//
|
|
418
|
+
// STEP 3d-3: Populate g_EsoSkillActiveData from the skill bars.
|
|
378
419
|
//
|
|
379
|
-
// GetEsoInputSkillActiveBar
|
|
380
|
-
//
|
|
381
|
-
//
|
|
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
|
-
|
|
439
|
+
g.g_EsoSkillActiveData = activeData;
|
|
399
440
|
}
|
|
400
441
|
// -------------------------------------------------------------------------
|
|
401
|
-
//
|
|
442
|
+
// STEP 3e: Set which weapon/skill bar is active.
|
|
402
443
|
//
|
|
403
|
-
// g_EsoBuildActiveWeapon —
|
|
404
|
-
// g_EsoBuildActiveAbilityBar —
|
|
405
|
-
//
|
|
406
|
-
//
|
|
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
|
-
|
|
410
|
-
|
|
450
|
+
g.g_EsoBuildActiveWeapon = activeBar;
|
|
451
|
+
g.g_EsoBuildActiveAbilityBar = activeBar;
|
|
411
452
|
// -------------------------------------------------------------------------
|
|
412
|
-
//
|
|
453
|
+
// STEP 4: Run the calculation.
|
|
413
454
|
//
|
|
414
455
|
// UpdateEsoComputedStatsList_Real(keepSaveResults, noUpdate)
|
|
415
|
-
// - keepSaveResults = null →
|
|
416
|
-
// - noUpdate = true →
|
|
417
|
-
// (
|
|
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 =
|
|
460
|
+
const updateFn = g.UpdateEsoComputedStatsList_Real;
|
|
420
461
|
if (typeof updateFn !== 'function') {
|
|
421
|
-
throw new Error('[eso-engine] UpdateEsoComputedStatsList_Real
|
|
422
|
-
'
|
|
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
|
-
//
|
|
490
|
+
// STEP 5: Read the results from g_EsoComputedStats[statId].value
|
|
427
491
|
// -------------------------------------------------------------------------
|
|
428
|
-
const computedStats =
|
|
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
|
-
//
|
|
501
|
+
// Max attributes
|
|
438
502
|
Health: raw['Health'] ?? 0,
|
|
439
503
|
Magicka: raw['Magicka'] ?? 0,
|
|
440
504
|
Stamina: raw['Stamina'] ?? 0,
|
|
441
|
-
//
|
|
505
|
+
// Regeneration
|
|
442
506
|
HealthRegen: raw['HealthRegen'] ?? 0,
|
|
443
507
|
MagickaRegen: raw['MagickaRegen'] ?? 0,
|
|
444
508
|
StaminaRegen: raw['StaminaRegen'] ?? 0,
|
|
445
|
-
//
|
|
509
|
+
// Damage
|
|
446
510
|
WeaponDamage: raw['WeaponDamage'] ?? 0,
|
|
447
511
|
SpellDamage: raw['SpellDamage'] ?? 0,
|
|
448
|
-
//
|
|
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
|
-
//
|
|
517
|
+
// Resistances
|
|
454
518
|
PhysicalResist: raw['PhysicalResist'] ?? 0,
|
|
455
519
|
SpellResist: raw['SpellResist'] ?? 0,
|
|
456
520
|
CritResist: raw['CritResist'] ?? 0,
|
|
457
|
-
//
|
|
521
|
+
// Penetration
|
|
458
522
|
PhysicalPenetration: raw['PhysicalPenetration'] ?? 0,
|
|
459
523
|
SpellPenetration: raw['SpellPenetration'] ?? 0,
|
|
460
|
-
//
|
|
524
|
+
// Effective power
|
|
461
525
|
EffectiveSpellPower: raw['EffectiveSpellPower'] ?? 0,
|
|
462
526
|
EffectiveWeaponPower: raw['EffectiveWeaponPower'] ?? 0,
|
|
463
527
|
EffectivePower: raw['EffectivePower'] ?? 0,
|
|
464
|
-
//
|
|
528
|
+
// Healing
|
|
465
529
|
HealingDone: raw['HealingDone'] ?? 0,
|
|
466
530
|
HealingTaken: raw['HealingTaken'] ?? 0,
|
|
467
|
-
//
|
|
531
|
+
// Speed
|
|
468
532
|
RunSpeed: raw['RunSpeed'] ?? 0,
|
|
469
533
|
SprintSpeed: raw['SprintSpeed'] ?? 0,
|
|
470
|
-
//
|
|
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 `
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
648
|
+
* Must be called after initEsoEngineFromData().
|
|
585
649
|
*/
|
|
586
|
-
function listAvailableSkillLines() {
|
|
587
|
-
const snapshot =
|
|
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
|
|
669
|
+
* Must be called after initEsoEngineFromData().
|
|
606
670
|
*/
|
|
607
|
-
function listAvailableToggleSkills() {
|
|
608
|
-
const toggleData =
|
|
671
|
+
export function listAvailableToggleSkills() {
|
|
672
|
+
const toggleData = engineGlobals().g_EsoBuildToggledSkillData;
|
|
609
673
|
if (!toggleData || typeof toggleData !== 'object')
|
|
610
674
|
return [];
|
|
611
675
|
const result = [];
|