@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,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
- });