@axiapps/gw2-data 0.1.1 → 0.1.3

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 (43) hide show
  1. package/data/overrides.json +24 -0
  2. package/package.json +4 -1
  3. package/src/engine/attributes.js +102 -23
  4. package/src/engine/boons.js +19 -1
  5. package/src/engine/constants.js +27 -2
  6. package/src/engine/index.js +4 -4
  7. package/src/engine/modifiers.js +57 -21
  8. package/src/index.js +5 -0
  9. package/src/wiki/parser.js +17 -9
  10. package/scripts/generate-fixtures.js +0 -242
  11. package/tests/api-client.test.js +0 -138
  12. package/tests/cache.test.js +0 -108
  13. package/tests/engine/attributes.test.js +0 -252
  14. package/tests/engine/boons.test.js +0 -129
  15. package/tests/engine/combos.test.js +0 -76
  16. package/tests/engine/constants.test.js +0 -576
  17. package/tests/engine/fixtures/berserker-thief.json +0 -61
  18. package/tests/engine/fixtures/berserker-warrior.json +0 -113
  19. package/tests/engine/fixtures/celestial-firebrand-wvw.json +0 -94
  20. package/tests/engine/fixtures/harrier-druid.json +0 -119
  21. package/tests/engine/fixtures/viper-mirage.json +0 -104
  22. package/tests/engine/graph.test.js +0 -30
  23. package/tests/engine/integration.test.js +0 -111
  24. package/tests/engine/modifiers.test.js +0 -473
  25. package/tests/engine/overrides.test.js +0 -70
  26. package/tests/engine/snapshot.test.js +0 -53
  27. package/tests/engine/test-utils.js +0 -20
  28. package/tests/engine/tooltips.test.js +0 -62
  29. package/tests/fixtures/capture.js +0 -160
  30. package/tests/fixtures/fixtures.json +0 -839
  31. package/tests/integration.test.js +0 -100
  32. package/tests/match.test.js +0 -176
  33. package/tests/merge.test.js +0 -128
  34. package/tests/normalize.test.js +0 -78
  35. package/tests/parser.test.js +0 -506
  36. package/tests/real-data.test.js +0 -296
  37. package/tests/relations.test.js +0 -80
  38. package/tests/resolver.test.js +0 -721
  39. package/tests/validate-live.js +0 -191
  40. package/tests/wiki-client.test.js +0 -468
  41. package/tests/wiki-integration.test.js +0 -177
  42. package/tests/wiki-live-validation.test.js +0 -61
  43. package/tests/wiki-snapshots.test.js +0 -166
@@ -1,252 +0,0 @@
1
- "use strict";
2
-
3
- const { computeAttributes, computeSlotStats, getExcludedSlots } = require("../../src/engine/attributes");
4
-
5
- function makeCtx(overrides = {}) {
6
- return {
7
- profession: "Warrior",
8
- specializations: [],
9
- equipment: {
10
- slots: {},
11
- weapons: {},
12
- runes: {},
13
- infusions: {},
14
- enrichment: null,
15
- food: null,
16
- utility: null,
17
- },
18
- gameMode: "pve",
19
- underwaterMode: false,
20
- activeWeaponSet: 1,
21
- skills: {},
22
- assumedBoons: null,
23
- sigilStacks: null,
24
- ...overrides,
25
- };
26
- }
27
-
28
- function makeCatalogs(overrides = {}) {
29
- return {
30
- traitById: new Map(),
31
- skillById: new Map(),
32
- specializationById: new Map(),
33
- runeById: new Map(),
34
- foodById: new Map(),
35
- utilityById: new Map(),
36
- infusionById: new Map(),
37
- enrichmentById: new Map(),
38
- ...overrides,
39
- };
40
- }
41
-
42
- describe("getExcludedSlots", () => {
43
- test("excludes aquatic slots in land mode", () => {
44
- const excluded = getExcludedSlots(false, 1);
45
- expect(excluded.has("breather")).toBe(true);
46
- expect(excluded.has("aquatic1")).toBe(true);
47
- expect(excluded.has("aquatic2")).toBe(true);
48
- expect(excluded.has("head")).toBe(false);
49
- });
50
-
51
- test("excludes land weapon slots in land mode for inactive set", () => {
52
- const excluded = getExcludedSlots(false, 1);
53
- expect(excluded.has("mainhand2")).toBe(true);
54
- expect(excluded.has("offhand2")).toBe(true);
55
- expect(excluded.has("mainhand1")).toBe(false);
56
- });
57
-
58
- test("excludes land-only slots in underwater mode", () => {
59
- const excluded = getExcludedSlots(true, 1);
60
- expect(excluded.has("head")).toBe(true);
61
- expect(excluded.has("mainhand1")).toBe(true);
62
- expect(excluded.has("breather")).toBe(false);
63
- });
64
- });
65
-
66
- describe("computeSlotStats", () => {
67
- test("3-stat combo returns major + minor stats", () => {
68
- const result = computeSlotStats("Berserker's", "chest", {}, "pve");
69
- expect(result).toEqual([
70
- { stat: "Power", value: 141 },
71
- { stat: "Precision", value: 101 },
72
- { stat: "Ferocity", value: 101 },
73
- ]);
74
- });
75
-
76
- test("4-stat combo returns 2 major + 2 minor stats", () => {
77
- const result = computeSlotStats("Marauder's", "chest", {}, "pve");
78
- expect(result).toEqual([
79
- { stat: "Power", value: 121 },
80
- { stat: "Precision", value: 121 },
81
- { stat: "Vitality", value: 66 },
82
- { stat: "Ferocity", value: 66 },
83
- ]);
84
- });
85
-
86
- test("Celestial uses c weight for all stats", () => {
87
- const result = computeSlotStats("Celestial", "chest", {}, "pve");
88
- expect(result.every((r) => r.value === 66)).toBe(true);
89
- expect(result).toHaveLength(9);
90
- });
91
-
92
- test("WvW Celestial excludes Expertise and Concentration", () => {
93
- const result = computeSlotStats("Celestial", "chest", {}, "wvw");
94
- expect(result.find((r) => r.stat === "Expertise")).toBeUndefined();
95
- expect(result.find((r) => r.stat === "Concentration")).toBeUndefined();
96
- expect(result).toHaveLength(7);
97
- });
98
-
99
- test("two-handed weapon uses TWO_HAND_WEIGHTS", () => {
100
- const weapons = { mainhand1: "greatsword" };
101
- const result = computeSlotStats("Berserker's", "mainhand1", weapons, "pve");
102
- expect(result[0]).toEqual({ stat: "Power", value: 251 });
103
- });
104
- });
105
-
106
- describe("computeAttributes", () => {
107
- test("empty build returns base stats only", () => {
108
- const result = computeAttributes(makeCtx(), makeCatalogs());
109
- expect(result.base.Power).toBe(1000);
110
- expect(result.base.Precision).toBe(1000);
111
- expect(result.base.Toughness).toBe(1000);
112
- expect(result.base.Vitality).toBe(1000);
113
- expect(result.base.Ferocity).toBe(0);
114
- expect(result.total.Power).toBe(1000);
115
- });
116
-
117
- test("equipment slots contribute to total", () => {
118
- const ctx = makeCtx({
119
- equipment: { slots: { chest: "Berserker's" }, weapons: {}, runes: {}, infusions: {} },
120
- });
121
- const result = computeAttributes(ctx, makeCatalogs());
122
- expect(result.equipment.Power).toBe(141);
123
- expect(result.total.Power).toBe(1141);
124
- });
125
-
126
- test("food flat bonuses parsed from buff text", () => {
127
- const catalogs = makeCatalogs({
128
- foodById: new Map([[100, { name: "Steak", buff: "+100 Power" }]]),
129
- });
130
- const ctx = makeCtx({ equipment: { slots: {}, weapons: {}, runes: {}, infusions: {}, food: 100 } });
131
- const result = computeAttributes(ctx, catalogs);
132
- expect(result.food.Power).toBe(100);
133
- });
134
-
135
- test("food 'to All Attributes' adds to all stats", () => {
136
- const catalogs = makeCatalogs({
137
- foodById: new Map([[101, { name: "Feast", buff: "+50 to All Attributes" }]]),
138
- });
139
- const ctx = makeCtx({ equipment: { slots: {}, weapons: {}, runes: {}, infusions: {}, food: 101 } });
140
- const result = computeAttributes(ctx, catalogs);
141
- expect(result.food.Power).toBe(50);
142
- expect(result.food.Ferocity).toBe(50);
143
- expect(result.food.HealingPower).toBe(50);
144
- });
145
-
146
- test("rune bonuses are cumulative per piece", () => {
147
- const catalogs = makeCatalogs({
148
- runeById: new Map([[24836, { name: "Scholar", bonuses: ["+25 Power", "+35 Ferocity", "+50 Power"] }]]),
149
- });
150
- const ctx = makeCtx({
151
- equipment: { slots: {}, weapons: {}, runes: { head: 24836, shoulders: 24836, chest: 24836 }, infusions: {} },
152
- });
153
- const result = computeAttributes(ctx, catalogs);
154
- expect(result.runes.Power).toBe(75);
155
- expect(result.runes.Ferocity).toBe(35);
156
- });
157
-
158
- test("infusion attributes added", () => {
159
- const catalogs = makeCatalogs({
160
- infusionById: new Map([[49431, { name: "+5 Power", infixUpgrade: { attributes: [{ attribute: "Power", modifier: 5 }] } }]]),
161
- });
162
- const ctx = makeCtx({
163
- equipment: { slots: {}, weapons: {}, infusions: { chest: [49431, 49431] }, runes: {} },
164
- });
165
- const result = computeAttributes(ctx, catalogs);
166
- expect(result.infusions.Power).toBe(10);
167
- });
168
-
169
- test("assumed Might boons add Power and ConditionDamage", () => {
170
- const ctx = makeCtx({ assumedBoons: { might: 25 } });
171
- const result = computeAttributes(ctx, makeCatalogs());
172
- expect(result.boons.Power).toBe(750);
173
- expect(result.boons.ConditionDamage).toBe(750);
174
- });
175
-
176
- test("derived health uses profession base HP", () => {
177
- const ctx = makeCtx({ profession: "Warrior" });
178
- const result = computeAttributes(ctx, makeCatalogs());
179
- // Warrior base HP = 9212, base Vitality = 1000, health = 9212 + 1000*10 = 19212
180
- expect(result.derived.health).toBe(19212);
181
- });
182
-
183
- test("derived crit chance formula", () => {
184
- const ctx = makeCtx();
185
- const result = computeAttributes(ctx, makeCatalogs());
186
- // Base precision 1000: critChance = (1000 - 895) / 21 = 5%
187
- expect(result.derived.critChance).toBeCloseTo(5, 0);
188
- });
189
-
190
- test("derived armor includes weight class defense", () => {
191
- const ctx = makeCtx({ profession: "Warrior" });
192
- const result = computeAttributes(ctx, makeCatalogs());
193
- // Warrior = heavy = 1271 defense, base toughness 1000
194
- expect(result.derived.armor).toBe(2271);
195
- });
196
-
197
- test("trait conversions applied after base stats", () => {
198
- const catalogs = makeCatalogs({
199
- traitById: new Map([[500, {
200
- id: 500,
201
- facts: [{ type: "BuffConversion", source: "Vitality", target: "Power", percent: 10 }],
202
- }]]),
203
- specializationById: new Map([[4, { id: 4, minorTraits: [] }]]),
204
- });
205
- const ctx = makeCtx({
206
- specializations: [{ id: 4, majorChoices: { 1: 500 } }],
207
- });
208
- const result = computeAttributes(ctx, catalogs);
209
- // base Vitality = 1000, 10% = floor(100) = 100
210
- expect(result.conversions.Power).toBe(100);
211
- expect(result.total.Power).toBe(1100);
212
- });
213
-
214
- test("berserk-conditional trait bonuses only apply when berserkActive", () => {
215
- const catalogs = makeCatalogs({
216
- traitById: new Map([[2046, {
217
- id: 2046,
218
- slot: "Minor",
219
- description: "Fatal Frenzy",
220
- facts: [
221
- { type: "AttributeAdjust", target: "Power", value: 150 },
222
- { type: "AttributeAdjust", target: "ConditionDamage", value: 300 },
223
- ],
224
- }]]),
225
- specializationById: new Map([[18, { id: 18, minorTraits: [2046] }]]),
226
- });
227
- const baseCtx = {
228
- specializations: [{ id: 18, majorChoices: {} }],
229
- };
230
-
231
- // Without berserk: bonuses NOT applied
232
- const resultOff = computeAttributes(makeCtx({ ...baseCtx, berserkActive: false }), catalogs);
233
- expect(resultOff.traits.Power).toBe(0);
234
- expect(resultOff.traits.ConditionDamage).toBe(0);
235
-
236
- // With berserk: bonuses applied
237
- const resultOn = computeAttributes(makeCtx({ ...baseCtx, berserkActive: true }), catalogs);
238
- expect(resultOn.traits.Power).toBe(150);
239
- expect(resultOn.traits.ConditionDamage).toBe(300);
240
- });
241
-
242
- test("signet passive buffs added", () => {
243
- const ctx = makeCtx({
244
- skills: { healId: null, utilityIds: [], eliteId: null },
245
- equipment: { slots: {}, weapons: {}, runes: {}, infusions: {} },
246
- });
247
- // Bane Signet (9093) = +180 Power
248
- ctx.skills.utilityIds = [9093];
249
- const result = computeAttributes(ctx, makeCatalogs());
250
- expect(result.signets.Power).toBe(180);
251
- });
252
- });
@@ -1,129 +0,0 @@
1
- "use strict";
2
-
3
- const { analyzeBoons, isAllyTargeted, normalizeName } = require("../../src/engine/boons");
4
-
5
- describe("normalizeName", () => {
6
- test("normalizes Blind to Blinded", () => {
7
- expect(normalizeName("Blind")).toBe("Blinded");
8
- });
9
-
10
- test("normalizes Cripple to Crippled", () => {
11
- expect(normalizeName("Cripple")).toBe("Crippled");
12
- });
13
-
14
- test("returns unknown names unchanged", () => {
15
- expect(normalizeName("Might")).toBe("Might");
16
- });
17
- });
18
-
19
- describe("isAllyTargeted", () => {
20
- test("returns true when boon name appears in ally sentence", () => {
21
- expect(isAllyTargeted("Grant Might to nearby allies.", "Might", [])).toBe(true);
22
- });
23
-
24
- test("returns false when boon is in description but not with ally word", () => {
25
- expect(isAllyTargeted("Gain Might. Attack enemies.", "Might", [])).toBe(false);
26
- });
27
-
28
- test("returns false with no description", () => {
29
- expect(isAllyTargeted(null, "Might", [])).toBe(false);
30
- });
31
-
32
- test("returns true for generic ally mention when boon not named", () => {
33
- expect(isAllyTargeted("Grant boons to allies.", "Fury", [])).toBe(true);
34
- });
35
-
36
- test("returns false for unnamed boon when specific boons named with allies", () => {
37
- expect(isAllyTargeted("Grant might to allies.", "Fury", ["Might"])).toBe(false);
38
- });
39
- });
40
-
41
- describe("analyzeBoons", () => {
42
- test("extracts boon from skill with Buff fact", () => {
43
- const skills = [{
44
- name: "For Great Justice!",
45
- description: "Grant Might and Fury to yourself and allies.",
46
- icon: "",
47
- facts: [
48
- { type: "Buff", status: "Might", apply_count: 3, duration: 8 },
49
- { type: "Buff", status: "Fury", apply_count: 1, duration: 8 },
50
- ],
51
- }];
52
- const result = analyzeBoons(skills, [], new Map());
53
- expect(result.boons.length).toBe(2);
54
- expect(result.boons.find((b) => b.name === "Might")).toBeDefined();
55
- expect(result.boons.find((b) => b.name === "Fury")).toBeDefined();
56
- });
57
-
58
- test("extracts condition from skill", () => {
59
- const skills = [{
60
- name: "Sword of Justice",
61
- description: "Create a Sword of Justice.",
62
- icon: "",
63
- facts: [{ type: "Buff", status: "Burning", apply_count: 2, duration: 3 }],
64
- }];
65
- const result = analyzeBoons(skills, [], new Map());
66
- expect(result.conditions.length).toBe(1);
67
- expect(result.conditions[0].name).toBe("Burning");
68
- });
69
-
70
- test("deduplicates by source + stacks + duration + context", () => {
71
- const skill = {
72
- name: "Skill A", description: "", icon: "",
73
- facts: [
74
- { type: "Buff", status: "Might", apply_count: 3, duration: 8 },
75
- { type: "Buff", status: "Might", apply_count: 3, duration: 8 },
76
- ],
77
- };
78
- const result = analyzeBoons([skill], [], new Map());
79
- const might = result.boons.find((b) => b.name === "Might");
80
- expect(might.sources).toHaveLength(1);
81
- });
82
-
83
- test("handles NoData section context for conditional facts", () => {
84
- const skill = {
85
- name: "Skill B", description: "", icon: "",
86
- facts: [
87
- { type: "NoData", text: "On Critical Hit" },
88
- { type: "Buff", status: "Fury", apply_count: 1, duration: 4 },
89
- ],
90
- };
91
- const result = analyzeBoons([skill], [], new Map());
92
- const fury = result.boons.find((b) => b.name === "Fury");
93
- expect(fury.sources[0].context).toBe("On Critical Hit");
94
- });
95
-
96
- test("applies Twisted Medicine ally override for Elixir skills", () => {
97
- const overrides = new Map([
98
- ["trait:2220", { allyTargeted: ["elixir"] }],
99
- ]);
100
- const skills = [{
101
- name: "Elixir B", description: "Drink Elixir B.", icon: "",
102
- categories: ["Elixir"],
103
- facts: [{ type: "Buff", status: "Fury", apply_count: 1, duration: 5 }],
104
- }];
105
- const traits = [{ id: 2220, name: "Twisted Medicine", facts: [] }];
106
- const result = analyzeBoons(skills, traits, overrides, new Set([2220]));
107
- const fury = result.boons.find((b) => b.name === "Fury");
108
- expect(fury.sources[0].isAlly).toBe(true);
109
- });
110
-
111
- test("sorts boons by display order, conditions alphabetically", () => {
112
- const skills = [{
113
- name: "Multi", description: "", icon: "",
114
- facts: [
115
- { type: "Buff", status: "Vigor", apply_count: 1, duration: 5 },
116
- { type: "Buff", status: "Aegis", apply_count: 1, duration: 3 },
117
- { type: "Buff", status: "Weakness", apply_count: 1, duration: 3 },
118
- { type: "Buff", status: "Burning", apply_count: 1, duration: 3 },
119
- ],
120
- }];
121
- const result = analyzeBoons(skills, [], new Map());
122
- // Aegis comes before Vigor in GW2 display order
123
- const boonNames = result.boons.map((b) => b.name);
124
- expect(boonNames.indexOf("Aegis")).toBeLessThan(boonNames.indexOf("Vigor"));
125
- // Conditions sorted alphabetically
126
- const condNames = result.conditions.map((c) => c.name);
127
- expect(condNames).toEqual([...condNames].sort());
128
- });
129
- });
@@ -1,76 +0,0 @@
1
- "use strict";
2
-
3
- const { analyzeCombos } = require("../../src/engine/combos");
4
-
5
- describe("analyzeCombos", () => {
6
- test("extracts combo field from skill facts", () => {
7
- const skills = [{
8
- name: "Flame Wall", icon: "", description: "", facts: [
9
- { type: "ComboField", field_type: "Fire" },
10
- { type: "Time", duration: 5 },
11
- { type: "Radius", distance: 240 },
12
- ],
13
- }];
14
- const result = analyzeCombos(skills, []);
15
- expect(result.fields).toHaveLength(1);
16
- expect(result.fields[0]).toMatchObject({
17
- fieldType: "Fire", sourceName: "Flame Wall", duration: 5, radius: 240,
18
- });
19
- });
20
-
21
- test("extracts combo finisher from skill facts", () => {
22
- const skills = [{
23
- name: "Mighty Blow", icon: "", description: "", facts: [
24
- { type: "ComboFinisher", finisher_type: "Blast", percent: 100 },
25
- ],
26
- }];
27
- const result = analyzeCombos(skills, []);
28
- expect(result.finishers).toHaveLength(1);
29
- expect(result.finishers[0]).toMatchObject({
30
- finisherType: "Blast", sourceName: "Mighty Blow", hitCount: 1, percent: 100,
31
- });
32
- });
33
-
34
- test("groups multiple finishers of same type on one skill", () => {
35
- const skills = [{
36
- name: "Whirling Strike", icon: "", description: "", facts: [
37
- { type: "ComboFinisher", finisher_type: "Whirl" },
38
- { type: "ComboFinisher", finisher_type: "Whirl" },
39
- { type: "ComboFinisher", finisher_type: "Whirl" },
40
- ],
41
- }];
42
- const result = analyzeCombos(skills, []);
43
- expect(result.finishers).toHaveLength(1);
44
- expect(result.finishers[0].hitCount).toBe(3);
45
- });
46
-
47
- test("deduplicates fields by (type, sourceName)", () => {
48
- const skills = [
49
- { name: "Flame Wall", icon: "", description: "", facts: [{ type: "ComboField", field_type: "Fire" }] },
50
- { name: "Flame Wall", icon: "", description: "", facts: [{ type: "ComboField", field_type: "Fire" }] },
51
- ];
52
- const result = analyzeCombos(skills, []);
53
- expect(result.fields).toHaveLength(1);
54
- });
55
-
56
- test("extracts fields from traits too", () => {
57
- const traits = [{
58
- name: "Healing Trait", icon: "", description: "", facts: [
59
- { type: "ComboField", field_type: "Water" },
60
- ],
61
- }];
62
- const result = analyzeCombos([], traits);
63
- expect(result.fields).toHaveLength(1);
64
- expect(result.fields[0].fieldType).toBe("Water");
65
- });
66
-
67
- test("tracks finisher percent below 100", () => {
68
- const skills = [{
69
- name: "Leap", icon: "", description: "", facts: [
70
- { type: "ComboFinisher", finisher_type: "Leap", percent: 50 },
71
- ],
72
- }];
73
- const result = analyzeCombos(skills, []);
74
- expect(result.finishers[0].percent).toBe(50);
75
- });
76
- });