@axiapps/gw2-data 0.1.1 → 0.1.2

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 (42) hide show
  1. package/data/overrides.json +24 -0
  2. package/package.json +3 -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/wiki/parser.js +17 -9
  9. package/scripts/generate-fixtures.js +0 -242
  10. package/tests/api-client.test.js +0 -138
  11. package/tests/cache.test.js +0 -108
  12. package/tests/engine/attributes.test.js +0 -252
  13. package/tests/engine/boons.test.js +0 -129
  14. package/tests/engine/combos.test.js +0 -76
  15. package/tests/engine/constants.test.js +0 -576
  16. package/tests/engine/fixtures/berserker-thief.json +0 -61
  17. package/tests/engine/fixtures/berserker-warrior.json +0 -113
  18. package/tests/engine/fixtures/celestial-firebrand-wvw.json +0 -94
  19. package/tests/engine/fixtures/harrier-druid.json +0 -119
  20. package/tests/engine/fixtures/viper-mirage.json +0 -104
  21. package/tests/engine/graph.test.js +0 -30
  22. package/tests/engine/integration.test.js +0 -111
  23. package/tests/engine/modifiers.test.js +0 -473
  24. package/tests/engine/overrides.test.js +0 -70
  25. package/tests/engine/snapshot.test.js +0 -53
  26. package/tests/engine/test-utils.js +0 -20
  27. package/tests/engine/tooltips.test.js +0 -62
  28. package/tests/fixtures/capture.js +0 -160
  29. package/tests/fixtures/fixtures.json +0 -839
  30. package/tests/integration.test.js +0 -100
  31. package/tests/match.test.js +0 -176
  32. package/tests/merge.test.js +0 -128
  33. package/tests/normalize.test.js +0 -78
  34. package/tests/parser.test.js +0 -506
  35. package/tests/real-data.test.js +0 -296
  36. package/tests/relations.test.js +0 -80
  37. package/tests/resolver.test.js +0 -721
  38. package/tests/validate-live.js +0 -191
  39. package/tests/wiki-client.test.js +0 -468
  40. package/tests/wiki-integration.test.js +0 -177
  41. package/tests/wiki-live-validation.test.js +0 -61
  42. package/tests/wiki-snapshots.test.js +0 -166
@@ -1,242 +0,0 @@
1
- "use strict";
2
-
3
- const fs = require("fs");
4
- const path = require("path");
5
- const { computeAttributes } = require("../src/engine/attributes");
6
- const { hydrateCatalogs } = require("../tests/engine/test-utils");
7
-
8
- const FIXTURE_DIR = path.join(__dirname, "../tests/engine/fixtures");
9
-
10
- const fixtures = [
11
- {
12
- name: "Berserker Warrior",
13
- description: "Heavy armor, 3-stat, full ascended, signets, Might+Fury assumed",
14
- ctx: {
15
- profession: "Warrior",
16
- specializations: [
17
- { id: 4, majorChoices: { 1: 1444, 2: 1449, 3: 1437 } },
18
- ],
19
- equipment: {
20
- slots: {
21
- head: "Berserker's", shoulders: "Berserker's", chest: "Berserker's",
22
- gloves: "Berserker's", legs: "Berserker's", boots: "Berserker's",
23
- mainhand1: "Berserker's", offhand1: "Berserker's",
24
- back: "Berserker's", accessory1: "Berserker's", accessory2: "Berserker's",
25
- amulet: "Berserker's", ring1: "Berserker's", ring2: "Berserker's",
26
- },
27
- weapons: { mainhand1: "greatsword" },
28
- runes: {},
29
- infusions: {},
30
- enrichment: null,
31
- food: null,
32
- utility: null,
33
- },
34
- gameMode: "pve",
35
- underwaterMode: false,
36
- activeWeaponSet: 1,
37
- skills: { healId: null, utilityIds: [9093], eliteId: null },
38
- assumedBoons: { might: 25, fury: true },
39
- sigilStacks: null,
40
- },
41
- catalogs: {
42
- traits: [
43
- { id: 1444, facts: [{ type: "AttributeAdjust", target: "Power", value: 120 }] },
44
- { id: 1449, facts: [] },
45
- { id: 1437, facts: [] },
46
- ],
47
- specializations: [{ id: 4, minorTraits: [] }],
48
- skills: [],
49
- runes: [],
50
- foods: [],
51
- utilities: [],
52
- infusions: [],
53
- enrichments: [],
54
- },
55
- },
56
- {
57
- name: "Viper Mirage",
58
- description: "Medium armor, 4-stat, trait conversions, food + utility",
59
- ctx: {
60
- profession: "Mesmer",
61
- specializations: [
62
- { id: 24, majorChoices: { 1: 700 } },
63
- ],
64
- equipment: {
65
- slots: {
66
- head: "Viper's", shoulders: "Viper's", chest: "Viper's",
67
- gloves: "Viper's", legs: "Viper's", boots: "Viper's",
68
- mainhand1: "Viper's",
69
- back: "Viper's", accessory1: "Viper's", accessory2: "Viper's",
70
- amulet: "Viper's", ring1: "Viper's", ring2: "Viper's",
71
- },
72
- weapons: { mainhand1: "axe" },
73
- runes: {},
74
- infusions: {},
75
- enrichment: null,
76
- food: 91805,
77
- utility: null,
78
- },
79
- gameMode: "pve",
80
- underwaterMode: false,
81
- activeWeaponSet: 1,
82
- skills: { healId: null, utilityIds: [], eliteId: null },
83
- assumedBoons: null,
84
- sigilStacks: null,
85
- },
86
- catalogs: {
87
- traits: [
88
- { id: 700, facts: [{ type: "BuffConversion", source: "Vitality", target: "ConditionDamage", percent: 10 }] },
89
- ],
90
- specializations: [{ id: 24, minorTraits: [] }],
91
- skills: [],
92
- runes: [],
93
- foods: [{ id: 91805, name: "Plate of Beef Rendang", buff: "+100 Expertise\n+70 Condition Damage" }],
94
- utilities: [],
95
- infusions: [],
96
- enrichments: [],
97
- },
98
- },
99
- {
100
- name: "Celestial Firebrand WvW",
101
- description: "Heavy armor, 9-stat, WvW Celestial exclusion, rune bonuses",
102
- ctx: {
103
- profession: "Guardian",
104
- specializations: [],
105
- equipment: {
106
- slots: {
107
- head: "Celestial", shoulders: "Celestial", chest: "Celestial",
108
- gloves: "Celestial", legs: "Celestial", boots: "Celestial",
109
- mainhand1: "Celestial",
110
- back: "Celestial", accessory1: "Celestial", accessory2: "Celestial",
111
- amulet: "Celestial", ring1: "Celestial", ring2: "Celestial",
112
- },
113
- weapons: { mainhand1: "axe" },
114
- runes: {
115
- head: 24836, shoulders: 24836, chest: 24836,
116
- gloves: 24836, legs: 24836, boots: 24836,
117
- },
118
- infusions: {},
119
- enrichment: null,
120
- food: null,
121
- utility: null,
122
- },
123
- gameMode: "wvw",
124
- underwaterMode: false,
125
- activeWeaponSet: 1,
126
- skills: { healId: null, utilityIds: [], eliteId: null },
127
- assumedBoons: null,
128
- sigilStacks: null,
129
- },
130
- catalogs: {
131
- traits: [],
132
- specializations: [],
133
- skills: [],
134
- runes: [{ id: 24836, name: "Superior Rune of the Scholar", bonuses: ["+25 Power", "+35 Ferocity", "+50 Power", "+65 Ferocity", "+100 Power", "+125 Ferocity"] }],
135
- foods: [],
136
- utilities: [],
137
- infusions: [],
138
- enrichments: [],
139
- },
140
- },
141
- {
142
- name: "Harrier Druid",
143
- description: "Medium armor, 3-stat healing, enrichment, infusions",
144
- ctx: {
145
- profession: "Ranger",
146
- specializations: [],
147
- equipment: {
148
- slots: {
149
- head: "Harrier's", shoulders: "Harrier's", chest: "Harrier's",
150
- gloves: "Harrier's", legs: "Harrier's", boots: "Harrier's",
151
- mainhand1: "Harrier's",
152
- back: "Harrier's", accessory1: "Harrier's", accessory2: "Harrier's",
153
- amulet: "Harrier's", ring1: "Harrier's", ring2: "Harrier's",
154
- },
155
- weapons: { mainhand1: "staff" },
156
- runes: {},
157
- infusions: {
158
- head: [49432], shoulders: [49432], chest: [49432],
159
- gloves: [49432], legs: [49432], boots: [49432],
160
- },
161
- enrichment: 78061,
162
- food: null,
163
- utility: null,
164
- },
165
- gameMode: "pve",
166
- underwaterMode: false,
167
- activeWeaponSet: 1,
168
- skills: { healId: null, utilityIds: [], eliteId: null },
169
- assumedBoons: null,
170
- sigilStacks: null,
171
- },
172
- catalogs: {
173
- traits: [],
174
- specializations: [],
175
- skills: [],
176
- runes: [],
177
- foods: [],
178
- utilities: [],
179
- infusions: [{ id: 49432, name: "+5 Healing Power Infusion", infixUpgrade: { attributes: [{ attribute: "Healing", modifier: 5 }] } }],
180
- enrichments: [{ id: 78061, name: "+10 Concentration Enrichment", infixUpgrade: { attributes: [{ attribute: "BoonDuration", modifier: 10 }] } }],
181
- },
182
- },
183
- {
184
- name: "Berserker Thief",
185
- description: "Medium armor, sparse gear (testing empty slots gracefully)",
186
- ctx: {
187
- profession: "Thief",
188
- specializations: [],
189
- equipment: {
190
- slots: { chest: "Berserker's", legs: "Berserker's" },
191
- weapons: {},
192
- runes: {},
193
- infusions: {},
194
- enrichment: null,
195
- food: null,
196
- utility: null,
197
- },
198
- gameMode: "pve",
199
- underwaterMode: false,
200
- activeWeaponSet: 1,
201
- skills: { healId: null, utilityIds: [], eliteId: null },
202
- assumedBoons: null,
203
- sigilStacks: null,
204
- },
205
- catalogs: {
206
- traits: [],
207
- specializations: [],
208
- skills: [],
209
- runes: [],
210
- foods: [],
211
- utilities: [],
212
- infusions: [],
213
- enrichments: [],
214
- },
215
- },
216
- ];
217
-
218
- // Generate fixtures
219
- if (!fs.existsSync(FIXTURE_DIR)) {
220
- fs.mkdirSync(FIXTURE_DIR, { recursive: true });
221
- }
222
-
223
- for (const fixture of fixtures) {
224
- const catalogs = hydrateCatalogs(fixture.catalogs);
225
- const result = computeAttributes(fixture.ctx, catalogs);
226
- const output = {
227
- name: fixture.name,
228
- description: fixture.description,
229
- ctx: fixture.ctx,
230
- catalogs: fixture.catalogs,
231
- expected: {
232
- total: result.total,
233
- derived: result.derived,
234
- },
235
- };
236
- const filename = fixture.name.toLowerCase().replace(/\s+/g, "-") + ".json";
237
- const filepath = path.join(FIXTURE_DIR, filename);
238
- fs.writeFileSync(filepath, JSON.stringify(output, null, 2) + "\n");
239
- console.log(`Generated: ${filename}`);
240
- }
241
-
242
- console.log("Done!");
@@ -1,138 +0,0 @@
1
- "use strict";
2
-
3
- const { Gw2ApiClient } = require("../src/api/client");
4
- const { MemoryCache } = require("../src/wiki/cache");
5
-
6
- describe("Gw2ApiClient", () => {
7
- let client;
8
- let mockFetch;
9
-
10
- beforeEach(() => {
11
- mockFetch = jest.fn();
12
- client = new Gw2ApiClient({
13
- cache: new MemoryCache(),
14
- fetch: mockFetch,
15
- });
16
- });
17
-
18
- describe("fetchJson", () => {
19
- test("returns parsed JSON on success", async () => {
20
- mockFetch.mockResolvedValueOnce({
21
- ok: true,
22
- json: async () => ({ name: "Fireball" }),
23
- });
24
- const result = await client.fetchJson("https://api.guildwars2.com/v2/skills/5489");
25
- expect(result).toEqual({ name: "Fireball" });
26
- });
27
-
28
- test("retries on 429 with delay", async () => {
29
- mockFetch
30
- .mockResolvedValueOnce({ ok: false, status: 429, statusText: "Too Many Requests" })
31
- .mockResolvedValueOnce({
32
- ok: true,
33
- json: async () => ({ name: "Fireball" }),
34
- });
35
- const result = await client.fetchJson("https://api.guildwars2.com/v2/skills/5489");
36
- expect(result).toEqual({ name: "Fireball" });
37
- expect(mockFetch).toHaveBeenCalledTimes(2);
38
- });
39
-
40
- test("throws after max retries", async () => {
41
- mockFetch.mockResolvedValue({ ok: false, status: 500, statusText: "Server Error" });
42
- await expect(
43
- client.fetchJson("https://api.guildwars2.com/v2/skills/5489")
44
- ).rejects.toThrow("500");
45
- });
46
-
47
- test("throws immediately on 4xx (non-429) without retrying", async () => {
48
- mockFetch.mockResolvedValueOnce({ ok: false, status: 404, statusText: "Not Found" });
49
- await expect(
50
- client.fetchJson("https://api.guildwars2.com/v2/skills/99999")
51
- ).rejects.toThrow("404");
52
- expect(mockFetch).toHaveBeenCalledTimes(1);
53
- });
54
- });
55
-
56
- describe("fetchByIds", () => {
57
- test("fetches single chunk of IDs", async () => {
58
- mockFetch.mockResolvedValueOnce({
59
- ok: true,
60
- json: async () => [
61
- { id: 1, name: "Skill A" },
62
- { id: 2, name: "Skill B" },
63
- ],
64
- });
65
- const result = await client.fetchByIds("/v2/skills", [1, 2]);
66
- expect(result).toHaveLength(2);
67
- expect(result[0].name).toBe("Skill A");
68
- });
69
-
70
- test("chunks large ID lists into batches of 180", async () => {
71
- const ids = Array.from({ length: 200 }, (_, i) => i + 1);
72
- mockFetch
73
- .mockResolvedValueOnce({
74
- ok: true,
75
- json: async () => ids.slice(0, 180).map((id) => ({ id })),
76
- })
77
- .mockResolvedValueOnce({
78
- ok: true,
79
- json: async () => ids.slice(180).map((id) => ({ id })),
80
- });
81
- const result = await client.fetchByIds("/v2/skills", ids);
82
- expect(result).toHaveLength(200);
83
- expect(mockFetch).toHaveBeenCalledTimes(2);
84
- });
85
-
86
- test("deduplicates IDs before fetching", async () => {
87
- mockFetch.mockResolvedValueOnce({
88
- ok: true,
89
- json: async () => [{ id: 1, name: "Skill A" }],
90
- });
91
- const result = await client.fetchByIds("/v2/skills", [1, 1, 1]);
92
- expect(result).toHaveLength(1);
93
- expect(mockFetch).toHaveBeenCalledTimes(1);
94
- expect(mockFetch.mock.calls[0][0]).toContain("ids=1");
95
- expect(mockFetch.mock.calls[0][0]).not.toContain("ids=1,1");
96
- });
97
- });
98
-
99
- describe("fetchCached", () => {
100
- test("returns cached value on hit", async () => {
101
- const cache = new MemoryCache();
102
- cache.set("test-key", { cached: true }, 60000);
103
- client = new Gw2ApiClient({ cache, fetch: mockFetch });
104
-
105
- const result = await client.fetchCached("test-key", "https://example.com", 60000);
106
- expect(result).toEqual({ cached: true });
107
- expect(mockFetch).not.toHaveBeenCalled();
108
- });
109
-
110
- test("fetches and caches on miss", async () => {
111
- mockFetch.mockResolvedValueOnce({
112
- ok: true,
113
- json: async () => ({ fresh: true }),
114
- });
115
- const result = await client.fetchCached("test-key", "https://example.com", 60000);
116
- expect(result).toEqual({ fresh: true });
117
- const result2 = await client.fetchCached("test-key", "https://example.com", 60000);
118
- expect(result2).toEqual({ fresh: true });
119
- expect(mockFetch).toHaveBeenCalledTimes(1);
120
- });
121
- });
122
-
123
- describe("constructor defaults", () => {
124
- test("defaults to MemoryCache when no cache provided", async () => {
125
- const defaultClient = new Gw2ApiClient({ fetch: mockFetch });
126
- mockFetch.mockResolvedValueOnce({
127
- ok: true,
128
- json: async () => ({ id: 1 }),
129
- });
130
- const result = await defaultClient.fetchCached("k", "https://example.com", 60000);
131
- expect(result).toEqual({ id: 1 });
132
- // Second call should hit cache
133
- const result2 = await defaultClient.fetchCached("k", "https://example.com", 60000);
134
- expect(result2).toEqual({ id: 1 });
135
- expect(mockFetch).toHaveBeenCalledTimes(1);
136
- });
137
- });
138
- });
@@ -1,108 +0,0 @@
1
- "use strict";
2
-
3
- const { MemoryCache } = require("../src/wiki/cache");
4
-
5
- describe("MemoryCache", () => {
6
- let cache;
7
-
8
- beforeEach(() => {
9
- cache = new MemoryCache();
10
- });
11
-
12
- test("get returns null for missing key", () => {
13
- expect(cache.get("missing")).toBeNull();
14
- });
15
-
16
- test("set and get round-trips a value", () => {
17
- cache.set("key1", { data: "hello" }, 60000);
18
- expect(cache.get("key1")).toEqual({ data: "hello" });
19
- });
20
-
21
- test("get returns null for expired entry", () => {
22
- cache.set("key1", "value", 1); // 1ms TTL
23
- // Advance past TTL
24
- jest.useFakeTimers();
25
- jest.advanceTimersByTime(10);
26
- expect(cache.get("key1")).toBeNull();
27
- jest.useRealTimers();
28
- });
29
-
30
- test("invalidate removes a specific key", () => {
31
- cache.set("key1", "value1", 60000);
32
- cache.set("key2", "value2", 60000);
33
- cache.invalidate("key1");
34
- expect(cache.get("key1")).toBeNull();
35
- expect(cache.get("key2")).toBe("value2");
36
- });
37
-
38
- test("clear removes all entries", () => {
39
- cache.set("key1", "value1", 60000);
40
- cache.set("key2", "value2", 60000);
41
- cache.clear();
42
- expect(cache.get("key1")).toBeNull();
43
- expect(cache.get("key2")).toBeNull();
44
- });
45
-
46
- test("has returns true for valid entry, false for missing/expired", () => {
47
- cache.set("key1", "value", 60000);
48
- expect(cache.has("key1")).toBe(true);
49
- expect(cache.has("missing")).toBe(false);
50
- });
51
- });
52
-
53
- const fs = require("node:fs/promises");
54
- const path = require("node:path");
55
- const os = require("node:os");
56
- const { DiskCache } = require("../src/wiki/cache");
57
-
58
- describe("DiskCache", () => {
59
- let cache;
60
- let tmpDir;
61
-
62
- beforeEach(async () => {
63
- tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "gw2-data-cache-"));
64
- cache = new DiskCache(tmpDir);
65
- });
66
-
67
- afterEach(async () => {
68
- await fs.rm(tmpDir, { recursive: true, force: true });
69
- });
70
-
71
- test("get returns null for missing key", async () => {
72
- expect(await cache.get("missing")).toBeNull();
73
- });
74
-
75
- test("set and get round-trips a value", async () => {
76
- await cache.set("key1", { data: "hello" }, 60000);
77
- expect(await cache.get("key1")).toEqual({ data: "hello" });
78
- });
79
-
80
- test("get returns null for expired entry", async () => {
81
- await cache.set("key1", "value", 1);
82
- // Wait for TTL to expire
83
- await new Promise((r) => setTimeout(r, 10));
84
- expect(await cache.get("key1")).toBeNull();
85
- });
86
-
87
- test("invalidate removes a specific key", async () => {
88
- await cache.set("key1", "value1", 60000);
89
- await cache.set("key2", "value2", 60000);
90
- await cache.invalidate("key1");
91
- expect(await cache.get("key1")).toBeNull();
92
- expect(await cache.get("key2")).toBe("value2");
93
- });
94
-
95
- test("clear removes all entries", async () => {
96
- await cache.set("key1", "value1", 60000);
97
- await cache.set("key2", "value2", 60000);
98
- await cache.clear();
99
- expect(await cache.get("key1")).toBeNull();
100
- expect(await cache.get("key2")).toBeNull();
101
- });
102
-
103
- test("persists across instances", async () => {
104
- await cache.set("key1", "value1", 60000);
105
- const cache2 = new DiskCache(tmpDir);
106
- expect(await cache2.get("key1")).toBe("value1");
107
- });
108
- });