@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.
- package/data/overrides.json +25 -0
- package/package.json +32 -0
- package/scripts/generate-fixtures.js +242 -0
- package/src/api/client.js +117 -0
- package/src/api/types.js +80 -0
- package/src/engine/attributes.js +525 -0
- package/src/engine/boons.js +156 -0
- package/src/engine/combos.js +103 -0
- package/src/engine/constants.js +298 -0
- package/src/engine/graph.js +24 -0
- package/src/engine/index.js +82 -0
- package/src/engine/modifiers.js +204 -0
- package/src/engine/overrides.js +13 -0
- package/src/engine/tooltips.js +59 -0
- package/src/facts/match.js +134 -0
- package/src/facts/merge.js +45 -0
- package/src/facts/normalize.js +27 -0
- package/src/index.js +60 -0
- package/src/wiki/cache.js +103 -0
- package/src/wiki/client.js +230 -0
- package/src/wiki/parser.js +599 -0
- package/src/wiki/relations.js +55 -0
- package/src/wiki/resolver.js +352 -0
- package/tests/api-client.test.js +138 -0
- package/tests/cache.test.js +108 -0
- package/tests/engine/attributes.test.js +252 -0
- package/tests/engine/boons.test.js +129 -0
- package/tests/engine/combos.test.js +76 -0
- package/tests/engine/constants.test.js +576 -0
- package/tests/engine/fixtures/berserker-thief.json +61 -0
- package/tests/engine/fixtures/berserker-warrior.json +113 -0
- package/tests/engine/fixtures/celestial-firebrand-wvw.json +94 -0
- package/tests/engine/fixtures/harrier-druid.json +119 -0
- package/tests/engine/fixtures/viper-mirage.json +104 -0
- package/tests/engine/graph.test.js +30 -0
- package/tests/engine/integration.test.js +111 -0
- package/tests/engine/modifiers.test.js +473 -0
- package/tests/engine/overrides.test.js +70 -0
- package/tests/engine/snapshot.test.js +53 -0
- package/tests/engine/test-utils.js +20 -0
- package/tests/engine/tooltips.test.js +62 -0
- package/tests/fixtures/capture.js +160 -0
- package/tests/fixtures/fixtures.json +839 -0
- package/tests/integration.test.js +100 -0
- package/tests/match.test.js +176 -0
- package/tests/merge.test.js +128 -0
- package/tests/normalize.test.js +78 -0
- package/tests/parser.test.js +506 -0
- package/tests/real-data.test.js +296 -0
- package/tests/relations.test.js +80 -0
- package/tests/resolver.test.js +721 -0
- package/tests/validate-live.js +191 -0
- package/tests/wiki-client.test.js +468 -0
- package/tests/wiki-integration.test.js +177 -0
- package/tests/wiki-live-validation.test.js +61 -0
- 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
|
+
};
|