@axiapps/gw2-data 0.1.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 (56) hide show
  1. package/data/overrides.json +25 -0
  2. package/package.json +32 -0
  3. package/scripts/generate-fixtures.js +242 -0
  4. package/src/api/client.js +117 -0
  5. package/src/api/types.js +80 -0
  6. package/src/engine/attributes.js +525 -0
  7. package/src/engine/boons.js +156 -0
  8. package/src/engine/combos.js +103 -0
  9. package/src/engine/constants.js +298 -0
  10. package/src/engine/graph.js +24 -0
  11. package/src/engine/index.js +82 -0
  12. package/src/engine/modifiers.js +204 -0
  13. package/src/engine/overrides.js +13 -0
  14. package/src/engine/tooltips.js +59 -0
  15. package/src/facts/match.js +134 -0
  16. package/src/facts/merge.js +45 -0
  17. package/src/facts/normalize.js +27 -0
  18. package/src/index.js +60 -0
  19. package/src/wiki/cache.js +103 -0
  20. package/src/wiki/client.js +230 -0
  21. package/src/wiki/parser.js +599 -0
  22. package/src/wiki/relations.js +55 -0
  23. package/src/wiki/resolver.js +352 -0
  24. package/tests/api-client.test.js +138 -0
  25. package/tests/cache.test.js +108 -0
  26. package/tests/engine/attributes.test.js +252 -0
  27. package/tests/engine/boons.test.js +129 -0
  28. package/tests/engine/combos.test.js +76 -0
  29. package/tests/engine/constants.test.js +576 -0
  30. package/tests/engine/fixtures/berserker-thief.json +61 -0
  31. package/tests/engine/fixtures/berserker-warrior.json +113 -0
  32. package/tests/engine/fixtures/celestial-firebrand-wvw.json +94 -0
  33. package/tests/engine/fixtures/harrier-druid.json +119 -0
  34. package/tests/engine/fixtures/viper-mirage.json +104 -0
  35. package/tests/engine/graph.test.js +30 -0
  36. package/tests/engine/integration.test.js +111 -0
  37. package/tests/engine/modifiers.test.js +473 -0
  38. package/tests/engine/overrides.test.js +70 -0
  39. package/tests/engine/snapshot.test.js +53 -0
  40. package/tests/engine/test-utils.js +20 -0
  41. package/tests/engine/tooltips.test.js +62 -0
  42. package/tests/fixtures/capture.js +160 -0
  43. package/tests/fixtures/fixtures.json +839 -0
  44. package/tests/integration.test.js +100 -0
  45. package/tests/match.test.js +176 -0
  46. package/tests/merge.test.js +128 -0
  47. package/tests/normalize.test.js +78 -0
  48. package/tests/parser.test.js +506 -0
  49. package/tests/real-data.test.js +296 -0
  50. package/tests/relations.test.js +80 -0
  51. package/tests/resolver.test.js +721 -0
  52. package/tests/validate-live.js +191 -0
  53. package/tests/wiki-client.test.js +468 -0
  54. package/tests/wiki-integration.test.js +177 -0
  55. package/tests/wiki-live-validation.test.js +61 -0
  56. package/tests/wiki-snapshots.test.js +166 -0
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Extract combo field facts from an entity.
5
+ * Pulls Duration and Radius from adjacent facts for metadata.
6
+ */
7
+ function extractComboFields(entity, sourceType) {
8
+ const results = [];
9
+ const facts = entity.facts || [];
10
+ let duration = 0;
11
+ let radius = 0;
12
+
13
+ for (const fact of facts) {
14
+ if ((fact.type === "Duration" || fact.type === "Time") && fact.duration) {
15
+ duration = fact.duration;
16
+ }
17
+ if (fact.type === "Radius" && fact.distance) {
18
+ radius = fact.distance;
19
+ }
20
+ }
21
+
22
+ for (const fact of facts) {
23
+ if (fact.type !== "ComboField" || !fact.field_type) continue;
24
+ results.push({
25
+ fieldType: fact.field_type,
26
+ sourceType,
27
+ sourceName: entity.name || "",
28
+ duration,
29
+ radius,
30
+ });
31
+ }
32
+ return results;
33
+ }
34
+
35
+ /**
36
+ * Extract combo finisher facts from an entity.
37
+ * Groups by finisher type, counts hits.
38
+ */
39
+ function extractComboFinishers(entity, sourceType) {
40
+ const results = [];
41
+ const facts = entity.facts || [];
42
+
43
+ const byType = new Map();
44
+ for (const fact of facts) {
45
+ if (fact.type !== "ComboFinisher" || !fact.finisher_type) continue;
46
+ const ft = fact.finisher_type;
47
+ if (!byType.has(ft)) byType.set(ft, { count: 0, percent: 100 });
48
+ const entry = byType.get(ft);
49
+ entry.count++;
50
+ if (fact.percent != null && fact.percent < 100) {
51
+ entry.percent = fact.percent;
52
+ }
53
+ }
54
+
55
+ for (const [finisherType, data] of byType) {
56
+ results.push({
57
+ finisherType,
58
+ sourceType,
59
+ sourceName: entity.name || "",
60
+ hitCount: data.count,
61
+ percent: data.percent,
62
+ });
63
+ }
64
+ return results;
65
+ }
66
+
67
+ /**
68
+ * Analyze combo fields and finishers from resolved skills and traits.
69
+ *
70
+ * @param {Object[]} skills - Resolved skill objects with facts
71
+ * @param {Object[]} traits - Resolved trait objects with facts
72
+ * @returns {{ fields: Object[], finishers: Object[] }}
73
+ */
74
+ function analyzeCombos(skills, traits) {
75
+ const allFields = [];
76
+ const allFinishers = [];
77
+
78
+ for (const skill of skills) {
79
+ if (!skill) continue;
80
+ allFields.push(...extractComboFields(skill, "skill"));
81
+ allFinishers.push(...extractComboFinishers(skill, "skill"));
82
+ }
83
+
84
+ for (const trait of traits) {
85
+ if (!trait) continue;
86
+ allFields.push(...extractComboFields(trait, "trait"));
87
+ allFinishers.push(...extractComboFinishers(trait, "trait"));
88
+ }
89
+
90
+ // Deduplicate fields by (fieldType, sourceName)
91
+ const seen = new Set();
92
+ const fields = [];
93
+ for (const field of allFields) {
94
+ const key = `${field.fieldType}::${field.sourceName}`;
95
+ if (seen.has(key)) continue;
96
+ seen.add(key);
97
+ fields.push(field);
98
+ }
99
+
100
+ return { fields, finishers: allFinishers };
101
+ }
102
+
103
+ module.exports = { analyzeCombos };
@@ -0,0 +1,298 @@
1
+ "use strict";
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // GW2 game constants extracted from src/renderer/modules/constants.js
5
+ // Pure data module — no renderer imports, no DOM deps.
6
+ // ---------------------------------------------------------------------------
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // STAT_COMBOS — array of { label, stats } objects
10
+ // For 4-stat combos: first 2 stats are major (0.3 multiplier), last 2 are minor (0.165 multiplier)
11
+ // ---------------------------------------------------------------------------
12
+ const STAT_COMBOS = [
13
+ { label: "Berserker's", stats: ["Power", "Precision", "Ferocity"] },
14
+ { label: "Marauder's", stats: ["Power", "Precision", "Vitality", "Ferocity"] },
15
+ { label: "Assassin's", stats: ["Precision", "Power", "Ferocity"] },
16
+ { label: "Valkyrie", stats: ["Power", "Vitality", "Ferocity"] },
17
+ { label: "Dragon's", stats: ["Power", "Ferocity", "Vitality", "Precision"] },
18
+ { label: "Viper's", stats: ["Power", "ConditionDamage", "Precision", "Expertise"] },
19
+ { label: "Grieving", stats: ["Power", "ConditionDamage", "Ferocity", "Precision"] },
20
+ { label: "Sinister", stats: ["ConditionDamage", "Power", "Precision"] },
21
+ { label: "Dire", stats: ["ConditionDamage", "Toughness", "Vitality"] },
22
+ { label: "Rabid", stats: ["ConditionDamage", "Toughness", "Precision"] },
23
+ { label: "Carrion", stats: ["ConditionDamage", "Power", "Vitality"] },
24
+ { label: "Trailblazer's", stats: ["Toughness", "ConditionDamage", "Vitality", "Expertise"] },
25
+ { label: "Knight's", stats: ["Toughness", "Power", "Precision"] },
26
+ { label: "Soldier's", stats: ["Power", "Toughness", "Vitality"] },
27
+ { label: "Sentinel's", stats: ["Vitality", "Power", "Toughness"] },
28
+ { label: "Wanderer's", stats: ["Power", "Vitality", "Toughness", "Concentration"] },
29
+ { label: "Diviner's", stats: ["Power", "Concentration", "Ferocity", "Precision"] },
30
+ { label: "Cleric's", stats: ["HealingPower", "Toughness", "Power"] },
31
+ { label: "Minstrel's", stats: ["Toughness", "HealingPower", "Vitality", "Concentration"] },
32
+ { label: "Harrier's", stats: ["Power", "HealingPower", "Concentration"] },
33
+ { label: "Ritualist's", stats: ["Vitality", "ConditionDamage", "Expertise", "Concentration"] },
34
+ { label: "Seraph", stats: ["Precision", "ConditionDamage", "HealingPower", "Concentration"] },
35
+ { label: "Crusader", stats: ["Power", "Toughness", "Ferocity", "HealingPower"] },
36
+ { label: "Zealot's", stats: ["Power", "Precision", "HealingPower"] },
37
+ { label: "Giver's", stats: ["Toughness", "HealingPower", "Concentration"] },
38
+ { label: "Celestial", stats: ["Power", "Precision", "Toughness", "Vitality", "ConditionDamage", "Ferocity", "HealingPower", "Expertise", "Concentration"] },
39
+ // Added in issue #133 — missing PvE / WvW stat sets
40
+ { label: "Apothecary's", stats: ["HealingPower", "Toughness", "ConditionDamage"] },
41
+ { label: "Magi's", stats: ["HealingPower", "Precision", "Vitality"] },
42
+ { label: "Shaman's", stats: ["Vitality", "ConditionDamage", "HealingPower"] },
43
+ { label: "Rampager's", stats: ["Precision", "Power", "ConditionDamage"] },
44
+ { label: "Cavalier's", stats: ["Toughness", "Power", "Ferocity"] },
45
+ { label: "Nomad's", stats: ["Toughness", "Vitality", "HealingPower"] },
46
+ { label: "Settler's", stats: ["Toughness", "ConditionDamage", "HealingPower"] },
47
+ { label: "Captain's", stats: ["Toughness", "Power", "HealingPower"] },
48
+ { label: "Vigilant", stats: ["Power", "Toughness", "Concentration"] },
49
+ { label: "Apostate's", stats: ["ConditionDamage", "Toughness", "HealingPower"] },
50
+ { label: "Plaguedoctor's", stats: ["ConditionDamage", "Vitality", "HealingPower", "Concentration"] },
51
+ { label: "Marshal's", stats: ["Power", "HealingPower", "Precision", "ConditionDamage"] },
52
+ { label: "Demolisher", stats: ["Power", "Precision", "Toughness", "Ferocity"] },
53
+ { label: "Commander's", stats: ["Power", "Precision", "Toughness", "Concentration"] },
54
+ ];
55
+
56
+ // Map from label → combo object (includes alias without "'s" suffix)
57
+ const STAT_COMBOS_BY_LABEL = new Map(
58
+ STAT_COMBOS.flatMap((c) => {
59
+ const entries = [[c.label, c]];
60
+ // Add alias without "'s" so imported builds (e.g. "Wanderer") resolve correctly
61
+ if (c.label.endsWith("'s")) entries.push([c.label.slice(0, -2), c]);
62
+ return entries;
63
+ })
64
+ );
65
+
66
+ /**
67
+ * Look up a stat combo by label (or alias without "'s").
68
+ * Returns the combo object or undefined.
69
+ */
70
+ function getStatCombo(label) {
71
+ return STAT_COMBOS_BY_LABEL.get(label);
72
+ }
73
+
74
+ // In WvW, Celestial gear does not grant Expertise or Concentration.
75
+ const WVW_CELESTIAL_EXCLUDED = new Set(["Expertise", "Concentration"]);
76
+
77
+ /**
78
+ * Return the effective stats array for a combo, accounting for game-mode restrictions.
79
+ * In WvW, Celestial excludes Expertise and Concentration.
80
+ * @param {{ label: string, stats: string[] }} combo
81
+ * @param {string} gameMode - "pve" | "wvw" | "pvp"
82
+ * @returns {string[]}
83
+ */
84
+ function getEffectiveStats(combo, gameMode) {
85
+ if (!combo) return [];
86
+ if (gameMode === "wvw" && combo.label === "Celestial") {
87
+ return combo.stats.filter((s) => !WVW_CELESTIAL_EXCLUDED.has(s));
88
+ }
89
+ return combo.stats;
90
+ }
91
+
92
+ // ---------------------------------------------------------------------------
93
+ // SLOT_WEIGHTS — ascended/legendary stat weights per slot
94
+ // p/s = 3-stat major/minor; p4/s4 = 4-stat major/minor; c = Celestial per-stat
95
+ // ---------------------------------------------------------------------------
96
+ const SLOT_WEIGHTS = {
97
+ head: { p: 63, s: 45, p4: 54, s4: 30, c: 30 },
98
+ shoulders: { p: 47, s: 34, p4: 40, s4: 22, c: 22 },
99
+ chest: { p: 141, s: 101, p4: 121, s4: 66, c: 66 },
100
+ hands: { p: 47, s: 34, p4: 40, s4: 22, c: 22 },
101
+ legs: { p: 94, s: 67, p4: 81, s4: 44, c: 44 },
102
+ feet: { p: 47, s: 34, p4: 40, s4: 22, c: 22 },
103
+ mainhand1: { p: 125, s: 90, p4: 107, s4: 59, c: 59 },
104
+ offhand1: { p: 125, s: 90, p4: 107, s4: 59, c: 59 },
105
+ mainhand2: { p: 125, s: 90, p4: 107, s4: 59, c: 59 },
106
+ offhand2: { p: 125, s: 90, p4: 107, s4: 59, c: 59 },
107
+ back: { p: 63, s: 40, p4: 51, s4: 27, c: 28 },
108
+ amulet: { p: 157, s: 108, p4: 132, s4: 71, c: 72 },
109
+ ring1: { p: 126, s: 85, p4: 105, s4: 56, c: 57 },
110
+ ring2: { p: 126, s: 85, p4: 105, s4: 56, c: 57 },
111
+ accessory1: { p: 110, s: 74, p4: 92, s4: 49, c: 50 },
112
+ accessory2: { p: 110, s: 74, p4: 92, s4: 49, c: 50 },
113
+ breather: { p: 63, s: 45, p4: 54, s4: 30, c: 30 },
114
+ aquatic1: { p: 251, s: 179, p4: 215, s4: 118, c: 118 },
115
+ aquatic2: { p: 251, s: 179, p4: 215, s4: 118, c: 118 },
116
+ };
117
+
118
+ // Two-handed land weapon stat weights (same as aquatic — attribute_adjustment 716.8).
119
+ const TWO_HAND_WEIGHTS = { p: 251, s: 179, p4: 215, s4: 118, c: 118 };
120
+
121
+ // ---------------------------------------------------------------------------
122
+ // Slot classification sets
123
+ // ---------------------------------------------------------------------------
124
+ const LAND_ONLY_SLOTS = new Set(["head", "mainhand1", "offhand1", "mainhand2", "offhand2"]);
125
+ const AQUATIC_SLOTS = new Set(["breather", "aquatic1", "aquatic2"]);
126
+
127
+ // ---------------------------------------------------------------------------
128
+ // Profession constants
129
+ // ---------------------------------------------------------------------------
130
+ const PROFESSION_WEIGHT = {
131
+ Elementalist: "light", Mesmer: "light", Necromancer: "light",
132
+ Engineer: "medium", Ranger: "medium", Thief: "medium",
133
+ Guardian: "heavy", Warrior: "heavy", Revenant: "heavy",
134
+ };
135
+
136
+ // Total defense from a full set of level 80 Ascended/Legendary armor (6 pieces).
137
+ const ARMOR_DEFENSE_BY_WEIGHT = { light: 967, medium: 1118, heavy: 1271 };
138
+
139
+ // Base HP at level 80 EXCLUDING base vitality contribution.
140
+ // Formula: totalHP = baseHP + (Vitality * 10), where base Vitality = 1000.
141
+ // High (9212): Warrior, Necromancer
142
+ // Medium (5922): Revenant, Engineer, Ranger, Mesmer
143
+ // Low (1645): Guardian, Thief, Elementalist
144
+ const PROFESSION_BASE_HP = {
145
+ Warrior: 9212, Berserker: 9212, Spellbreaker: 9212, Bladesworn: 9212, Paragon: 9212,
146
+ Necromancer: 9212, Reaper: 9212, Scourge: 9212, Harbinger: 9212,
147
+ Revenant: 5922, Herald: 5922, Renegade: 5922, Vindicator: 5922,
148
+ Engineer: 5922, Scrapper: 5922, Holosmith: 5922, Mechanist: 5922,
149
+ Ranger: 5922, Druid: 5922, Soulbeast: 5922, Untamed: 5922,
150
+ Mesmer: 5922, Chronomancer: 5922, Mirage: 5922, Virtuoso: 5922,
151
+ Guardian: 1645, Dragonhunter: 1645, Firebrand: 1645, Willbender: 1645,
152
+ Thief: 1645, Daredevil: 1645, Deadeye: 1645, Specter: 1645, Antiquary: 1645,
153
+ Elementalist: 1645, Tempest: 1645, Weaver: 1645, Catalyst: 1645,
154
+ };
155
+
156
+ // ---------------------------------------------------------------------------
157
+ // Weapon strength midpoints
158
+ // Exotic level 80 weapon strength midpoints (avg of min/max per wiki.guildwars2.com/wiki/Weapon_strength)
159
+ // ---------------------------------------------------------------------------
160
+ const WEAPON_STRENGTH_MIDPOINT = {
161
+ axe: 952.5, dagger: 952.5, mace: 952.5, pistol: 952.5, sword: 952.5, scepter: 952.5,
162
+ focus: 857.5, shield: 857.5, torch: 857.5, warhorn: 857,
163
+ greatsword: 1047.5, hammer: 1048, longbow: 1000, rifle: 1095.5, shortbow: 952.5, staff: 1048,
164
+ spear: 952.5, trident: 952.5, harpoon: 952.5,
165
+ };
166
+
167
+ // ---------------------------------------------------------------------------
168
+ // Boon/condition constants
169
+ // ---------------------------------------------------------------------------
170
+
171
+ // Fact types where the icon represents the boon/condition being applied.
172
+ const BUFF_FACT_TYPES = new Set(["Buff", "ApplyBuffCondition", "PrefixedBuff"]);
173
+
174
+ // Assumed boon stat effects (per GW2 wiki, level 80)
175
+ const MIGHT_POWER_PER_STACK = 30;
176
+ const MIGHT_CONDI_PER_STACK = 30;
177
+ const FURY_CRIT_CHANCE = 25; // percentage points (PvE)
178
+ const FURY_CRIT_CHANCE_WVW = 20; // percentage points (WvW)
179
+
180
+ // ---------------------------------------------------------------------------
181
+ // Stacking sigils
182
+ // ---------------------------------------------------------------------------
183
+ const _ALL_STATS = ["Power", "Precision", "Toughness", "Vitality", "Ferocity", "ConditionDamage", "Expertise", "Concentration", "HealingPower"];
184
+ const STACKING_SIGIL_DEFS = [
185
+ { id: 24575, key: "sigilBloodlust", label: "Bloodlust", stat: "Power", perStack: 10, maxStacks: 25 },
186
+ { id: 81045, key: "sigilBounty", label: "Bounty", stat: "Concentration", perStack: 9, maxStacks: 25 },
187
+ { id: 24578, key: "sigilCorruption", label: "Corruption", stat: "ConditionDamage", perStack: 10, maxStacks: 25 },
188
+ { id: 67341, key: "sigilCruelty", label: "Cruelty", stat: "Ferocity", perStack: 10, maxStacks: 25 },
189
+ { id: 24584, key: "sigilBenevolence", label: "Benevolence", modifier: "Outgoing Healing", perStack: 0.5, maxStacks: 25 },
190
+ { id: 24582, key: "sigilLife", label: "Life", stat: "HealingPower", perStack: 10, maxStacks: 25 },
191
+ { id: 49457, key: "sigilMomentum", label: "Momentum", stat: "Toughness", perStack: 5, maxStacks: 25 },
192
+ { id: 24580, key: "sigilPerception", label: "Perception", stat: "Precision", perStack: 10, maxStacks: 25 },
193
+ { id: 86170, key: "sigilStars", label: "Stars", allStats: _ALL_STATS, perStack: 2, maxStacks: 25 },
194
+ ];
195
+
196
+ // ---------------------------------------------------------------------------
197
+ // Signet passive buffs
198
+ // ---------------------------------------------------------------------------
199
+ // The GW2 API does not expose these values; maintained as a static map.
200
+ // Key = skill ID, value = { stat, value }.
201
+ // Source: https://wiki.guildwars2.com/wiki/Signet (PvE values, all 180 as of 2025-06).
202
+ const SIGNET_PASSIVE_BUFFS = new Map([
203
+ // Guardian
204
+ [9093, { stat: "Power", value: 180 }], // Bane Signet
205
+ [9151, { stat: "ConditionDamage", value: 180 }], // Signet of Wrath
206
+ [9163, { stat: "Concentration", value: 180 }], // Signet of Mercy
207
+ // Warrior
208
+ [14404, { stat: "Power", value: 180 }], // Signet of Might
209
+ [14410, { stat: "Precision", value: 180 }], // Signet of Fury
210
+ // Ranger
211
+ [12500, { stat: "Toughness", value: 180 }], // Signet of Stone
212
+ [12491, { stat: "Ferocity", value: 180 }], // Signet of the Wild
213
+ // Thief
214
+ [13046, { stat: "Power", value: 180 }], // Assassin's Signet
215
+ [13062, { stat: "Precision", value: 180 }], // Signet of Agility
216
+ // Elementalist
217
+ [5542, { stat: "Precision", value: 180 }], // Signet of Fire
218
+ // Mesmer
219
+ [10232, { stat: "ConditionDamage", value: 180 }], // Signet of Domination
220
+ [10234, { stat: "Expertise", value: 180 }], // Signet of Midnight
221
+ // Necromancer
222
+ [10622, { stat: "Power", value: 180 }], // Signet of Spite
223
+ ]);
224
+
225
+ // ---------------------------------------------------------------------------
226
+ // Boon / condition name sets
227
+ // ---------------------------------------------------------------------------
228
+ const BOON_NAMES = new Set([
229
+ "Aegis", "Alacrity", "Fury", "Might", "Protection", "Quickness",
230
+ "Regeneration", "Resistance", "Resolution", "Stability", "Swiftness", "Vigor",
231
+ ]);
232
+
233
+ const CONDITION_NAMES = new Set([
234
+ "Bleeding", "Blind", "Blinded", "Burning", "Chill", "Chilled",
235
+ "Confusion", "Cripple", "Crippled", "Fear", "Immobile", "Immobilize", "Immobilized",
236
+ "Poison", "Poisoned", "Slow", "Taunt", "Torment",
237
+ "Vulnerability", "Weakness",
238
+ ]);
239
+
240
+ const CONDITION_NAME_NORMALIZE = {
241
+ Blind: "Blinded", Chill: "Chilled", Cripple: "Crippled",
242
+ Immobilize: "Immobile", Immobilized: "Immobile", Poison: "Poisoned",
243
+ };
244
+
245
+ const BOON_DISPLAY_ORDER = [
246
+ "Aegis", "Alacrity", "Fury", "Might", "Protection", "Quickness",
247
+ "Regeneration", "Resistance", "Resolution", "Stability", "Swiftness", "Vigor",
248
+ ];
249
+
250
+ // ---------------------------------------------------------------------------
251
+ // Stat key lists
252
+ // ---------------------------------------------------------------------------
253
+ const ALL_STAT_KEYS = [
254
+ "Power", "Precision", "Toughness", "Vitality", "Ferocity",
255
+ "ConditionDamage", "HealingPower", "Expertise", "Concentration",
256
+ ];
257
+
258
+ // ---------------------------------------------------------------------------
259
+ // CONVERSION_TARGET_MAP — GW2 API AttributeConversion target names → our stat keys
260
+ // Source: src/renderer/modules/stats.js
261
+ // ---------------------------------------------------------------------------
262
+ const CONVERSION_TARGET_MAP = {
263
+ BoonDuration: "Concentration",
264
+ ConditionDuration: "Expertise",
265
+ CritDamage: "Ferocity",
266
+ Healing: "HealingPower",
267
+ };
268
+
269
+ // ---------------------------------------------------------------------------
270
+ // Exports
271
+ // ---------------------------------------------------------------------------
272
+ module.exports = {
273
+ STAT_COMBOS,
274
+ STAT_COMBOS_BY_LABEL,
275
+ getStatCombo,
276
+ getEffectiveStats,
277
+ SLOT_WEIGHTS,
278
+ TWO_HAND_WEIGHTS,
279
+ LAND_ONLY_SLOTS,
280
+ AQUATIC_SLOTS,
281
+ PROFESSION_WEIGHT,
282
+ ARMOR_DEFENSE_BY_WEIGHT,
283
+ PROFESSION_BASE_HP,
284
+ WEAPON_STRENGTH_MIDPOINT,
285
+ BUFF_FACT_TYPES,
286
+ MIGHT_POWER_PER_STACK,
287
+ MIGHT_CONDI_PER_STACK,
288
+ FURY_CRIT_CHANCE,
289
+ FURY_CRIT_CHANCE_WVW,
290
+ STACKING_SIGIL_DEFS,
291
+ SIGNET_PASSIVE_BUFFS,
292
+ BOON_NAMES,
293
+ CONDITION_NAMES,
294
+ CONDITION_NAME_NORMALIZE,
295
+ BOON_DISPLAY_ORDER,
296
+ ALL_STAT_KEYS,
297
+ CONVERSION_TARGET_MAP,
298
+ };
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Build a trait/skill interaction graph from wiki relations data.
5
+ *
6
+ * @param {Set<number>} activeTraitIds - Currently active trait IDs
7
+ * @param {Map<number, { skills: number[], traits: number[] }>} relations - Relations data per trait
8
+ * @returns {Map<number, { relatedSkills: Set<number>, relatedTraits: Set<number> }>}
9
+ */
10
+ function buildInteractionGraph(activeTraitIds, relations) {
11
+ const graph = new Map();
12
+
13
+ for (const traitId of activeTraitIds) {
14
+ const rel = relations.get(traitId);
15
+ graph.set(traitId, {
16
+ relatedSkills: new Set(rel?.skills || []),
17
+ relatedTraits: new Set(rel?.traits || []),
18
+ });
19
+ }
20
+
21
+ return graph;
22
+ }
23
+
24
+ module.exports = { buildInteractionGraph };
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+
3
+ const { computeAttributes, computeSlotStats, getExcludedSlots } = require("./attributes");
4
+ const { collectModifiers, collectActiveTraitIds, isFuryTrait } = require("./modifiers");
5
+ const { computeTooltip } = require("./tooltips");
6
+ const { buildInteractionGraph } = require("./graph");
7
+ const { analyzeBoons, isAllyTargeted, normalizeName } = require("./boons");
8
+ const { analyzeCombos } = require("./combos");
9
+ const { loadOverrides, getOverride } = require("./overrides");
10
+ const {
11
+ STAT_COMBOS, STAT_COMBOS_BY_LABEL, getStatCombo, getEffectiveStats,
12
+ SLOT_WEIGHTS, TWO_HAND_WEIGHTS, LAND_ONLY_SLOTS, AQUATIC_SLOTS,
13
+ PROFESSION_WEIGHT, ARMOR_DEFENSE_BY_WEIGHT, PROFESSION_BASE_HP,
14
+ WEAPON_STRENGTH_MIDPOINT, BUFF_FACT_TYPES,
15
+ MIGHT_POWER_PER_STACK, MIGHT_CONDI_PER_STACK,
16
+ FURY_CRIT_CHANCE, FURY_CRIT_CHANCE_WVW,
17
+ STACKING_SIGIL_DEFS, SIGNET_PASSIVE_BUFFS,
18
+ BOON_NAMES, CONDITION_NAMES, CONDITION_NAME_NORMALIZE,
19
+ BOON_DISPLAY_ORDER, ALL_STAT_KEYS, CONVERSION_TARGET_MAP,
20
+ } = require("./constants");
21
+
22
+ class StatEngine {
23
+ /**
24
+ * @param {Object} catalogs - GW2 API catalog data
25
+ * @param {Map} [overrides] - Override map (auto-loaded if not provided)
26
+ */
27
+ constructor(catalogs, overrides) {
28
+ this._catalogs = catalogs;
29
+ this._overrides = overrides || loadOverrides();
30
+ }
31
+
32
+ computeAttributes(ctx) {
33
+ return computeAttributes(ctx, this._catalogs);
34
+ }
35
+
36
+ collectModifiers(ctx) {
37
+ return collectModifiers(ctx, this._catalogs, this._overrides);
38
+ }
39
+
40
+ computeTooltip(ctx, skill, weaponType) {
41
+ const attrs = this.computeAttributes(ctx);
42
+ const mods = this.collectModifiers(ctx);
43
+ return computeTooltip(attrs, skill, weaponType, mods);
44
+ }
45
+
46
+ analyzeBoons(skills, traits, activeTraitIds) {
47
+ return analyzeBoons(skills, traits, this._overrides, activeTraitIds);
48
+ }
49
+
50
+ analyzeCombos(skills, traits) {
51
+ return analyzeCombos(skills, traits);
52
+ }
53
+ }
54
+
55
+ module.exports = {
56
+ StatEngine,
57
+ // Re-export individual modules for direct use
58
+ computeAttributes,
59
+ computeSlotStats,
60
+ getExcludedSlots,
61
+ collectModifiers,
62
+ collectActiveTraitIds,
63
+ isFuryTrait,
64
+ computeTooltip,
65
+ buildInteractionGraph,
66
+ analyzeBoons,
67
+ isAllyTargeted,
68
+ normalizeName,
69
+ analyzeCombos,
70
+ loadOverrides,
71
+ getOverride,
72
+ // Constants
73
+ STAT_COMBOS, STAT_COMBOS_BY_LABEL, getStatCombo, getEffectiveStats,
74
+ SLOT_WEIGHTS, TWO_HAND_WEIGHTS, LAND_ONLY_SLOTS, AQUATIC_SLOTS,
75
+ PROFESSION_WEIGHT, ARMOR_DEFENSE_BY_WEIGHT, PROFESSION_BASE_HP,
76
+ WEAPON_STRENGTH_MIDPOINT, BUFF_FACT_TYPES,
77
+ MIGHT_POWER_PER_STACK, MIGHT_CONDI_PER_STACK,
78
+ FURY_CRIT_CHANCE, FURY_CRIT_CHANCE_WVW,
79
+ STACKING_SIGIL_DEFS, SIGNET_PASSIVE_BUFFS,
80
+ BOON_NAMES, CONDITION_NAMES, CONDITION_NAME_NORMALIZE,
81
+ BOON_DISPLAY_ORDER, ALL_STAT_KEYS, CONVERSION_TARGET_MAP,
82
+ };