@poe2-toolkit/tree-core 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 (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +451 -0
  3. package/dist/geometry/centre.d.ts +17 -0
  4. package/dist/geometry/centre.d.ts.map +1 -0
  5. package/dist/geometry/centre.js +59 -0
  6. package/dist/geometry/centre.js.map +1 -0
  7. package/dist/geometry/framing.d.ts +27 -0
  8. package/dist/geometry/framing.d.ts.map +1 -0
  9. package/dist/geometry/framing.js +101 -0
  10. package/dist/geometry/framing.js.map +1 -0
  11. package/dist/geometry/orbit.d.ts +23 -0
  12. package/dist/geometry/orbit.d.ts.map +1 -0
  13. package/dist/geometry/orbit.js +28 -0
  14. package/dist/geometry/orbit.js.map +1 -0
  15. package/dist/geometry/project.d.ts +20 -0
  16. package/dist/geometry/project.d.ts.map +1 -0
  17. package/dist/geometry/project.js +149 -0
  18. package/dist/geometry/project.js.map +1 -0
  19. package/dist/ggg/normalize.d.ts +109 -0
  20. package/dist/ggg/normalize.d.ts.map +1 -0
  21. package/dist/ggg/normalize.js +279 -0
  22. package/dist/ggg/normalize.js.map +1 -0
  23. package/dist/index.d.ts +25 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +24 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/scene/allocate.d.ts +61 -0
  28. package/dist/scene/allocate.d.ts.map +1 -0
  29. package/dist/scene/allocate.js +239 -0
  30. package/dist/scene/allocate.js.map +1 -0
  31. package/dist/scene/buildScene.d.ts +21 -0
  32. package/dist/scene/buildScene.d.ts.map +1 -0
  33. package/dist/scene/buildScene.js +212 -0
  34. package/dist/scene/buildScene.js.map +1 -0
  35. package/dist/scene/connections.d.ts +13 -0
  36. package/dist/scene/connections.d.ts.map +1 -0
  37. package/dist/scene/connections.js +75 -0
  38. package/dist/scene/connections.js.map +1 -0
  39. package/dist/scene/nodeSize.d.ts +26 -0
  40. package/dist/scene/nodeSize.d.ts.map +1 -0
  41. package/dist/scene/nodeSize.js +72 -0
  42. package/dist/scene/nodeSize.js.map +1 -0
  43. package/dist/types.d.ts +426 -0
  44. package/dist/types.d.ts.map +1 -0
  45. package/dist/types.js +16 -0
  46. package/dist/types.js.map +1 -0
  47. package/package.json +50 -0
@@ -0,0 +1,279 @@
1
+ /**
2
+ * Normalise GGG's official skill-tree export (`data.json`) into the engine's
3
+ * {@link TreeData}.
4
+ *
5
+ * This is the only place that knows GGG's field names and quirks. Everything
6
+ * downstream works against the clean {@link TreeData} contract. The transform is
7
+ * deliberately tolerant: optional fields come and go across patches and missing
8
+ * data should never throw.
9
+ *
10
+ * The GGG quirks handled here:
11
+ * - node positions are **baked** (`node.x`/`node.y`); the export ships none of
12
+ * the orbit-radius constants needed to recompute them, so they are read as-is;
13
+ * - edges are split across `in` + `out` id lists, merged into one set here;
14
+ * - arc geometry lives in a separate top-level `edges` table, not on the node;
15
+ * - attribute choices live in a global `skillOverrides` table, not per node;
16
+ * - ascendancy ids are internal (`Ranger1`) and mapped to display names here;
17
+ * - centre/ring dimensions are absent — filled from stable atlas frame sizes.
18
+ */
19
+ // --- centre/ring sizes (GGG atlas frame dimensions, stable across classes) --
20
+ /** Class portrait atlas frame (`background-<class>` Class0..3), 1500². */
21
+ const PORTRAIT_SIZE = 1500;
22
+ /** Ornate + rotating ring atlas frames (`group-background`), 2000². */
23
+ const RING_SIZE = 2000;
24
+ /** Ascendancy disc portrait, 1500². */
25
+ const ASCENDANCY_DISC_SIZE = 1500;
26
+ /** Central opening radius — the game's `PSSCentreInnerRadius` constant. */
27
+ const CENTRE_INNER_RADIUS = 130;
28
+ /**
29
+ * @param raw parsed GGG `data.json`
30
+ * @param version tree/patch version (e.g. "0_5") — GGG does not store it inside
31
+ * the file, so it is supplied here
32
+ */
33
+ export function normalizeGggTree(raw, version) {
34
+ const ascendancyNames = mapAscendancyNames(raw.classes);
35
+ const attributeOptions = mapAttributeOptions(raw.skillOverrides);
36
+ const classNames = raw.classes.map((cls) => cls.name);
37
+ const edgeArcs = mapEdgeArcs(raw.edges);
38
+ const nodes = normalizeNodes(raw, ascendancyNames, attributeOptions, classNames, edgeArcs);
39
+ const ascendancyStarts = mapAscendancyStarts(raw.nodes);
40
+ const startNodeByClass = mapClassStartNodes(raw.nodes);
41
+ return {
42
+ version,
43
+ constants: { centreInnerRadius: CENTRE_INNER_RADIUS },
44
+ groups: normalizeGroups(raw.groups),
45
+ nodes,
46
+ classes: raw.classes.map((cls, index) => normalizeClass(cls, index, startNodeByClass, ascendancyStarts)),
47
+ jewelSlots: (raw.jewelSlots ?? []).map(Number),
48
+ bounds: { minX: raw.min_x, minY: raw.min_y, maxX: raw.max_x, maxY: raw.max_y },
49
+ };
50
+ }
51
+ /** GGG `groups` is keyed by group id (1-based numeric strings) with no holes. */
52
+ function normalizeGroups(raw) {
53
+ const groups = {};
54
+ for (const [key, group] of Object.entries(raw)) {
55
+ groups[Number(key)] = {
56
+ x: group.x,
57
+ y: group.y,
58
+ orbits: group.orbits ?? [],
59
+ nodes: (group.nodes ?? []).map(Number),
60
+ };
61
+ }
62
+ return groups;
63
+ }
64
+ /** Undirected key for an edge between two skill ids. */
65
+ function edgeKey(a, b) {
66
+ return a < b ? `${a}|${b}` : `${b}|${a}`;
67
+ }
68
+ /**
69
+ * GGG's top-level `edges` table marks arcs and gives each arc's world centre
70
+ * directly (`orbitX`/`orbitY`). Map it by undirected edge key; only entries with
71
+ * a centre are kept (arcs without one fall back to the geometric rule).
72
+ */
73
+ function mapEdgeArcs(edges) {
74
+ const arcs = new Map();
75
+ for (const edge of edges ?? []) {
76
+ if (edge.orbitX !== undefined && edge.orbitY !== undefined) {
77
+ arcs.set(edgeKey(Number(edge.from), Number(edge.to)), { x: edge.orbitX, y: edge.orbitY });
78
+ }
79
+ }
80
+ return arcs;
81
+ }
82
+ function normalizeNodes(raw, ascendancyNames, attributeOptions, classNames, edgeArcs) {
83
+ const nodes = {};
84
+ for (const [key, node] of Object.entries(raw.nodes)) {
85
+ const skill = node.skill ?? Number(key);
86
+ nodes[skill] = normalizeNode(skill, node, ascendancyNames, attributeOptions, classNames, edgeArcs);
87
+ }
88
+ return nodes;
89
+ }
90
+ function normalizeNode(skill, raw, ascendancyNames, attributeOptions, classNames, edgeArcs) {
91
+ const name = raw.name ?? '';
92
+ // GGG ships edges split across `in` and `out`; merge + dedupe into one set.
93
+ const neighbours = new Set([
94
+ ...(raw.in ?? []).map(Number),
95
+ ...(raw.out ?? []).map(Number),
96
+ ]);
97
+ const ascendancyName = raw.ascendancyId
98
+ ? ascendancyNames.get(raw.ascendancyId) ?? raw.ascendancyId
99
+ : undefined;
100
+ return {
101
+ skill,
102
+ group: raw.group ?? -1,
103
+ orbit: raw.orbit ?? 0,
104
+ orbitIndex: raw.orbitIndex ?? 0,
105
+ x: raw.x ?? 0,
106
+ y: raw.y ?? 0,
107
+ connections: [...neighbours].map((id) => {
108
+ const arcCentre = edgeArcs.get(edgeKey(skill, id));
109
+ return arcCentre ? { id, arcCentre } : { id };
110
+ }),
111
+ name,
112
+ icon: raw.icon ?? '',
113
+ stats: cleanStats(raw.stats),
114
+ // Kind flags and metadata are added only when present, so absence stays
115
+ // absent (exactOptionalPropertyTypes-friendly).
116
+ ...(raw.isNotable ? { isNotable: true } : {}),
117
+ ...(raw.isKeystone ? { isKeystone: true } : {}),
118
+ ...(raw.isJewelSocket ? { isJewelSocket: true } : {}),
119
+ // GGG has no `noRadius`; the hidden Sinister sockets are identified by name.
120
+ ...(raw.isJewelSocket && name.includes('SinisterJewelSocket') ? { noRadius: true } : {}),
121
+ ...(raw.isMastery ? { isMastery: true } : {}),
122
+ ...(raw.isAscendancyStart ? { isAscendancyStart: true } : {}),
123
+ ...(raw.isGenericAttribute ? { isAttribute: true } : {}),
124
+ ...(raw.activeEffectImage !== undefined ? { activeEffectImage: raw.activeEffectImage } : {}),
125
+ ...(ascendancyName !== undefined ? { ascendancyName } : {}),
126
+ ...(raw.flavourText !== undefined ? { flavourText: joinFlavour(raw.flavourText) } : {}),
127
+ ...(raw.recipe !== undefined ? { recipe: raw.recipe } : {}),
128
+ // Generic +attribute nodes share the three global Str/Dex/Int choices.
129
+ ...(raw.isGenericAttribute && attributeOptions.length > 0 ? { options: attributeOptions } : {}),
130
+ ...(raw.unlockConstraint ? { conditional: true } : {}),
131
+ ...(raw.unlockConstraint?.ascendancy !== undefined ? { unlockAscendancy: raw.unlockConstraint.ascendancy } : {}),
132
+ ...(raw.classStartIndex && raw.classStartIndex.length > 0
133
+ ? { classesStart: raw.classStartIndex.map((i) => classNames[i] ?? String(i)) }
134
+ : {}),
135
+ };
136
+ }
137
+ /** GGG ships keystone flavour text as an array of lines; join into one string. */
138
+ function joinFlavour(flavour) {
139
+ return Array.isArray(flavour) ? flavour.join('\n') : flavour;
140
+ }
141
+ /**
142
+ * GGG stat lines carry inline reference tags: `[ref|display]` renders as
143
+ * `display`, `[ref]` as `ref` (e.g. `[Critical|Critical Hit Chance]` ->
144
+ * "Critical Hit Chance", `[Curse|Curses]` -> "Curses", `[Shock]` -> "Shock").
145
+ * The `display` side is the player-facing text, including the correct plural —
146
+ * strip the tags down to it.
147
+ */
148
+ function cleanStatText(line) {
149
+ return line.replace(/\[(?:[^|\]]*\|)?([^\]]+)\]/g, '$1');
150
+ }
151
+ /** Apply {@link cleanStatText} across a list of stat lines. */
152
+ function cleanStats(stats) {
153
+ return (stats ?? []).map(cleanStatText);
154
+ }
155
+ /**
156
+ * Map GGG's internal ascendancy ids ("Ranger1") to display names ("Deadeye").
157
+ * The display name is what a build's chosen ascendancy is keyed by, so node and
158
+ * allocation agree. Unreleased ascendancies carry no name and are skipped.
159
+ */
160
+ function mapAscendancyNames(classes) {
161
+ const names = new Map();
162
+ for (const cls of classes) {
163
+ for (const asc of cls.ascendancies ?? []) {
164
+ if (asc.name) {
165
+ names.set(asc.id, asc.name);
166
+ }
167
+ }
168
+ }
169
+ return names;
170
+ }
171
+ /** Preferred display order of attribute choices (GGG keys them by numeric id). */
172
+ const ATTRIBUTE_ORDER = ['Strength', 'Dexterity', 'Intelligence'];
173
+ /**
174
+ * The three shared attribute choices (Str / Dex / Int), read from GGG's global
175
+ * `skillOverrides` table. That table also holds unrelated overrides (Pathfinder
176
+ * alternates, per-class node swaps), so it is filtered to the `generic_attribute`
177
+ * entries. Every generic-attribute node reuses these, ordered Str/Dex/Int.
178
+ */
179
+ function mapAttributeOptions(overrides) {
180
+ if (!overrides) {
181
+ return [];
182
+ }
183
+ return Object.entries(overrides)
184
+ .filter(([, override]) => override.id?.startsWith('generic_attribute'))
185
+ .map(([key, override]) => ({
186
+ id: override.skill ?? Number(key),
187
+ name: override.name ?? '',
188
+ stats: cleanStats(override.stats),
189
+ icon: override.icon ?? '',
190
+ }))
191
+ .sort((a, b) => orderIndex(a.name) - orderIndex(b.name));
192
+ }
193
+ function orderIndex(name) {
194
+ const index = ATTRIBUTE_ORDER.indexOf(name);
195
+ return index === -1 ? ATTRIBUTE_ORDER.length : index;
196
+ }
197
+ /**
198
+ * The world position of each ascendancy's start node (the diamond), keyed by
199
+ * GGG ascendancy id. GGG bakes every ascendancy at a far-flung cluster; the
200
+ * start node is the anchor the renderer relocates onto the hub. Ascendancies
201
+ * with no nodes (e.g. unreleased "Abyssal Lich") are simply absent.
202
+ */
203
+ function mapAscendancyStarts(nodes) {
204
+ const starts = new Map();
205
+ for (const node of Object.values(nodes)) {
206
+ if (node.isAscendancyStart && node.ascendancyId && node.x !== undefined && node.y !== undefined) {
207
+ starts.set(node.ascendancyId, { x: node.x, y: node.y });
208
+ }
209
+ }
210
+ return starts;
211
+ }
212
+ /**
213
+ * Build a class-index -> start-node-skill-id map. A class-start node lists the
214
+ * class indices that start there in `classStartIndex` (often two); the same node
215
+ * can serve several classes.
216
+ */
217
+ function mapClassStartNodes(nodes) {
218
+ const byClass = new Map();
219
+ for (const [key, node] of Object.entries(nodes)) {
220
+ const skill = node.skill ?? Number(key);
221
+ for (const classIndex of node.classStartIndex ?? []) {
222
+ if (!byClass.has(classIndex)) {
223
+ byClass.set(classIndex, skill);
224
+ }
225
+ }
226
+ }
227
+ return byClass;
228
+ }
229
+ function normalizeClass(raw, index, startNodeByClass, ascendancyStarts) {
230
+ return {
231
+ name: raw.name,
232
+ id: index,
233
+ baseStr: raw.base_str,
234
+ baseDex: raw.base_dex,
235
+ baseInt: raw.base_int,
236
+ startNode: startNodeByClass.get(index) ?? -1,
237
+ centre: {
238
+ image: `Classes${raw.name}`,
239
+ x: 0,
240
+ y: 0,
241
+ art: { width: PORTRAIT_SIZE, height: PORTRAIT_SIZE },
242
+ active: { width: RING_SIZE, height: RING_SIZE },
243
+ frame: { width: RING_SIZE, height: RING_SIZE },
244
+ },
245
+ // Keep only released ascendancies — ones with a name AND a start node (i.e.
246
+ // actual nodes; e.g. "Abyssal Lich" is named but has none yet, so dropped).
247
+ ascendancies: (raw.ascendancies ?? [])
248
+ .filter((asc) => Boolean(asc.name) && ascendancyStarts.has(asc.id))
249
+ .map((asc) => normalizeAscendancy(asc, ascendancyStarts)),
250
+ ...(raw.overridePairs ? { overridePairs: normalizeOverrides(raw.overridePairs) } : {}),
251
+ };
252
+ }
253
+ /** Normalise the override map's string keys to numeric skill ids. */
254
+ function normalizeOverrides(raw) {
255
+ const out = {};
256
+ for (const [base, target] of Object.entries(raw)) {
257
+ out[Number(base)] = target;
258
+ }
259
+ return out;
260
+ }
261
+ function normalizeAscendancy(raw, ascendancyStarts) {
262
+ const start = ascendancyStarts.get(raw.id) ?? { x: 0, y: 0 };
263
+ // The renderer relocates each disc node by `centre − worldAnchor`. GGG's
264
+ // `offsetX/offsetY` is where the start diamond must land in the hub — on the
265
+ // class's quatrefoil axis, 1332u out (verified against the original PoB
266
+ // layout, exact to ~10u). So worldAnchor = startNode + offset places the
267
+ // diamond on the quatrefoil axis and the rest of the cluster around it.
268
+ const offsetX = raw.offsetX ?? 0;
269
+ const offsetY = raw.offsetY ?? 0;
270
+ return {
271
+ id: raw.name,
272
+ name: raw.name,
273
+ internalId: raw.id,
274
+ image: `Classes${raw.name.replace(/\s+/g, '')}`,
275
+ worldAnchor: { x: start.x + offsetX, y: start.y + offsetY },
276
+ size: { width: ASCENDANCY_DISC_SIZE, height: ASCENDANCY_DISC_SIZE },
277
+ };
278
+ }
279
+ //# sourceMappingURL=normalize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalize.js","sourceRoot":"","sources":["../../src/ggg/normalize.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * `@poe2-toolkit/tree-core` — headless geometry engine for the Path of Exile 2 passive
3
+ * tree. Pure TypeScript, zero runtime dependencies, no DOM, no canvas.
4
+ *
5
+ * This entry point is **source-agnostic**: it works against the {@link TreeData}
6
+ * contract and knows nothing about where the data came from. The pipeline is
7
+ * `TreeData` → `buildScene` → `project` / `nodeAt`, plus the helpers a UI needs
8
+ * (interactive allocation, hit-testing, view framing).
9
+ *
10
+ * Producing `TreeData` is a separate, swappable adapter. The one for GGG's
11
+ * official export lives in the `@poe2-toolkit/tree-core/ggg` subpath, so the engine
12
+ * itself carries no dependency on any particular source shape.
13
+ */
14
+ export type { TreeData, TreeConstants, Group, TreeNode, NodeOption, AttributeChoice, JewelInfo, NodeConnection, Size, ClassDef, CentreArt, AscendancyDef, SpriteFrame, SpriteManifest, Point, WorldRect, Scene, PlacedNode, NodeKind, PlacedConnection, PlacedEffect, CentreLayout, ClassAnchor, BuildAllocation, SceneOptions, Viewport, ScreenScene, ScreenNode, ScreenConnection, ScreenEffect, } from './types.js';
15
+ export { nodePosition } from './geometry/orbit.js';
16
+ export { computeCentreLayout } from './geometry/centre.js';
17
+ export { project, projectPoint, screenToWorld, nodeAt } from './geometry/project.js';
18
+ export { allocatedBounds, allocatedBoundsWithCentre, classBounds } from './geometry/framing.js';
19
+ export { buildScene, chosenAttributeOption, classOverrideNode } from './scene/buildScene.js';
20
+ export { ascendancyStartNode, buildAscendancyGraph, buildTreeGraph, pathToNode, reachable, removalSet, toggleAllocation, toggleAscendancyAllocation, } from './scene/allocate.js';
21
+ export type { TreeGraph } from './scene/allocate.js';
22
+ export { placeConnection } from './scene/connections.js';
23
+ export { classifyNode, nodeTargetSize } from './scene/nodeSize.js';
24
+ export type { NodeSize } from './scene/nodeSize.js';
25
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,YAAY,EACV,QAAQ,EACR,aAAa,EACb,KAAK,EACL,QAAQ,EACR,UAAU,EACV,eAAe,EACf,SAAS,EACT,cAAc,EACd,IAAI,EACJ,QAAQ,EACR,SAAS,EACT,aAAa,EACb,WAAW,EACX,cAAc,EACd,KAAK,EACL,SAAS,EACT,KAAK,EACL,UAAU,EACV,QAAQ,EACR,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,eAAe,EACf,YAAY,EACZ,QAAQ,EACR,WAAW,EACX,UAAU,EACV,gBAAgB,EAChB,YAAY,GACb,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAEnD,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAE3D,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAErF,OAAO,EAAE,eAAe,EAAE,yBAAyB,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAEhG,OAAO,EAAE,UAAU,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC7F,OAAO,EACL,mBAAmB,EACnB,oBAAoB,EACpB,cAAc,EACd,UAAU,EACV,SAAS,EACT,UAAU,EACV,gBAAgB,EAChB,0BAA0B,GAC3B,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACnE,YAAY,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * `@poe2-toolkit/tree-core` — headless geometry engine for the Path of Exile 2 passive
3
+ * tree. Pure TypeScript, zero runtime dependencies, no DOM, no canvas.
4
+ *
5
+ * This entry point is **source-agnostic**: it works against the {@link TreeData}
6
+ * contract and knows nothing about where the data came from. The pipeline is
7
+ * `TreeData` → `buildScene` → `project` / `nodeAt`, plus the helpers a UI needs
8
+ * (interactive allocation, hit-testing, view framing).
9
+ *
10
+ * Producing `TreeData` is a separate, swappable adapter. The one for GGG's
11
+ * official export lives in the `@poe2-toolkit/tree-core/ggg` subpath, so the engine
12
+ * itself carries no dependency on any particular source shape.
13
+ */
14
+ export { nodePosition } from './geometry/orbit.js';
15
+ export { computeCentreLayout } from './geometry/centre.js';
16
+ export { project, projectPoint, screenToWorld, nodeAt } from './geometry/project.js';
17
+ export { allocatedBounds, allocatedBoundsWithCentre, classBounds } from './geometry/framing.js';
18
+ export { buildScene, chosenAttributeOption, classOverrideNode } from './scene/buildScene.js';
19
+ export { ascendancyStartNode, buildAscendancyGraph, buildTreeGraph, pathToNode, reachable, removalSet, toggleAllocation, toggleAscendancyAllocation, } from './scene/allocate.js';
20
+ export { placeConnection } from './scene/connections.js';
21
+ export { classifyNode, nodeTargetSize } from './scene/nodeSize.js';
22
+ // Data-source adapters live in their own subpaths (e.g. `@poe2-toolkit/tree-core/ggg`)
23
+ // so this entry point stays source-agnostic — see ./ggg/normalize.ts.
24
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAmCH,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAEnD,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAE3D,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAErF,OAAO,EAAE,eAAe,EAAE,yBAAyB,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAEhG,OAAO,EAAE,UAAU,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC7F,OAAO,EACL,mBAAmB,EACnB,oBAAoB,EACpB,cAAc,EACd,UAAU,EACV,SAAS,EACT,UAAU,EACV,gBAAgB,EAChB,0BAA0B,GAC3B,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAGnE,uFAAuF;AACvF,sEAAsE"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Interactive allocation over the passive tree graph: click a node to allocate
3
+ * the shortest path to it from the class start (and the current allocation), or
4
+ * click an allocated node to remove it and anything it orphaned.
5
+ *
6
+ * Pure graph logic — no rendering. The page owns the allocation state and feeds
7
+ * clicks through {@link toggleAllocation}.
8
+ */
9
+ import type { TreeData } from '../types.js';
10
+ /** Adjacency list of the walkable tree: node id -> connected node ids. */
11
+ export type TreeGraph = Map<number, Set<number>>;
12
+ /**
13
+ * Build the undirected adjacency graph of walkable nodes. Class-start edges are
14
+ * kept (unlike in the drawn scene) so paths can root at the start node.
15
+ */
16
+ export declare function buildTreeGraph(data: TreeData): TreeGraph;
17
+ /**
18
+ * The start node of an ascendancy panel (its pathing root), or undefined when
19
+ * the ascendancy has none in the data.
20
+ */
21
+ export declare function ascendancyStartNode(data: TreeData, ascendancy: string): number | undefined;
22
+ /**
23
+ * Adjacency graph of a single ascendancy's nodes (its start node included as the
24
+ * root). Ascendancy points are separate from the main tree, so the editor paths
25
+ * within this self-contained subgraph — never across the main-tree boundary.
26
+ */
27
+ export declare function buildAscendancyGraph(data: TreeData, ascendancy: string): TreeGraph;
28
+ /**
29
+ * Shortest path (BFS) from any of `sources` to `target`, as the list of nodes
30
+ * to add — excluding the sources, including the target. `[]` when the target is
31
+ * already a source; `null` when unreachable.
32
+ */
33
+ export declare function pathToNode(graph: TreeGraph, sources: ReadonlySet<number>, target: number): number[] | null;
34
+ /**
35
+ * The nodes a click on an allocated `target` removes: everything beyond it (the
36
+ * nodes that lose their connection to the start once it's cut), keeping the
37
+ * target itself — clicking shortens the path *back* to the clicked node. When
38
+ * nothing lies beyond (the target is a tip), the target itself is removed.
39
+ */
40
+ export declare function removalSet(graph: TreeGraph, startNode: number, allocated: ReadonlySet<number>, target: number): Set<number>;
41
+ /** The subset of `allowed` still reachable from `roots` through the graph. */
42
+ export declare function reachable(graph: TreeGraph, roots: Iterable<number>, allowed: ReadonlySet<number>): Set<number>;
43
+ /**
44
+ * Toggle a node in a manual build:
45
+ * - allocated target -> remove it and prune any node it orphaned from the start
46
+ * - unallocated target -> allocate the shortest path to it
47
+ *
48
+ * Returns the new allocated node ids (start node excluded — it's implicit).
49
+ * Pass a prebuilt `graph` to avoid recomputing it per click.
50
+ */
51
+ export declare function toggleAllocation(data: TreeData, startNode: number, allocated: ReadonlySet<number>, target: number, graph?: TreeGraph): number[];
52
+ /**
53
+ * Toggle an ascendancy node, pathing only within that ascendancy's own subgraph
54
+ * (rooted at its start node). The main-tree allocation is carried through
55
+ * untouched, so this is the ascendancy counterpart to {@link toggleAllocation}
56
+ * — clicks allocate the path from the ascendancy start, or remove beyond.
57
+ *
58
+ * Returns the new full allocated set (main tree + this ascendancy's nodes).
59
+ */
60
+ export declare function toggleAscendancyAllocation(data: TreeData, ascendancy: string, allocated: ReadonlySet<number>, target: number, graph?: TreeGraph): number[];
61
+ //# sourceMappingURL=allocate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"allocate.d.ts","sourceRoot":"","sources":["../../src/scene/allocate.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAY,MAAM,aAAa,CAAC;AAEtD,0EAA0E;AAC1E,MAAM,MAAM,SAAS,GAAG,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;AA+BjD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,QAAQ,GAAG,SAAS,CAmCxD;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAQ1F;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,GAAG,SAAS,CAmClF;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,WAAW,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CAqC1G;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,SAAS,EAChB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,WAAW,CAAC,MAAM,CAAC,EAC9B,MAAM,EAAE,MAAM,GACb,GAAG,CAAC,MAAM,CAAC,CAkBb;AAED,8EAA8E;AAC9E,wBAAgB,SAAS,CAAC,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,CAyB9G;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,QAAQ,EACd,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,WAAW,CAAC,MAAM,CAAC,EAC9B,MAAM,EAAE,MAAM,EACd,KAAK,GAAE,SAAgC,GACtC,MAAM,EAAE,CAoBV;AAED;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,QAAQ,EACd,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,WAAW,CAAC,MAAM,CAAC,EAC9B,MAAM,EAAE,MAAM,EACd,KAAK,GAAE,SAAkD,GACxD,MAAM,EAAE,CAsBV"}
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Interactive allocation over the passive tree graph: click a node to allocate
3
+ * the shortest path to it from the class start (and the current allocation), or
4
+ * click an allocated node to remove it and anything it orphaned.
5
+ *
6
+ * Pure graph logic — no rendering. The page owns the allocation state and feeds
7
+ * clicks through {@link toggleAllocation}.
8
+ */
9
+ /**
10
+ * Walkable for pathing: real main-tree nodes plus the class-start roots.
11
+ * Masteries (not pathed through), ascendancy nodes (separate panel), hidden
12
+ * sockets and conditional/unlock nodes are excluded.
13
+ *
14
+ * The GGG tree's synthetic centre node (keyed `root`, so it has no numeric skill
15
+ * id and parses to NaN) wires every class start together through the hub. Left
16
+ * walkable, a path could shortcut across the centre and surface at whichever rim
17
+ * gateway is fewest hops from the target — even another class's — so it looks
18
+ * like it starts at "the nearest point of the circle". Pathing must root at the
19
+ * class start (or the nearest allocated node) and stay in the tree, never cross
20
+ * the hub, so the centre node is excluded.
21
+ */
22
+ function isWalkable(node) {
23
+ if (Number.isNaN(node.skill)) {
24
+ return false;
25
+ }
26
+ if (node.ascendancyName || node.isMastery || node.conditional) {
27
+ return false;
28
+ }
29
+ if (node.noRadius && node.isJewelSocket) {
30
+ return false;
31
+ }
32
+ return true;
33
+ }
34
+ /**
35
+ * Build the undirected adjacency graph of walkable nodes. Class-start edges are
36
+ * kept (unlike in the drawn scene) so paths can root at the start node.
37
+ */
38
+ export function buildTreeGraph(data) {
39
+ const graph = new Map();
40
+ const link = (from, to) => {
41
+ let set = graph.get(from);
42
+ if (!set) {
43
+ set = new Set();
44
+ graph.set(from, set);
45
+ }
46
+ set.add(to);
47
+ };
48
+ for (const node of Object.values(data.nodes)) {
49
+ if (!isWalkable(node)) {
50
+ continue;
51
+ }
52
+ if (!graph.has(node.skill)) {
53
+ graph.set(node.skill, new Set());
54
+ }
55
+ for (const conn of node.connections) {
56
+ const target = data.nodes[conn.id];
57
+ if (!target || !isWalkable(target)) {
58
+ continue;
59
+ }
60
+ link(node.skill, conn.id);
61
+ link(conn.id, node.skill);
62
+ }
63
+ }
64
+ return graph;
65
+ }
66
+ /**
67
+ * The start node of an ascendancy panel (its pathing root), or undefined when
68
+ * the ascendancy has none in the data.
69
+ */
70
+ export function ascendancyStartNode(data, ascendancy) {
71
+ for (const node of Object.values(data.nodes)) {
72
+ if (node.isAscendancyStart && node.ascendancyName === ascendancy) {
73
+ return node.skill;
74
+ }
75
+ }
76
+ return undefined;
77
+ }
78
+ /**
79
+ * Adjacency graph of a single ascendancy's nodes (its start node included as the
80
+ * root). Ascendancy points are separate from the main tree, so the editor paths
81
+ * within this self-contained subgraph — never across the main-tree boundary.
82
+ */
83
+ export function buildAscendancyGraph(data, ascendancy) {
84
+ const graph = new Map();
85
+ const link = (from, to) => {
86
+ let set = graph.get(from);
87
+ if (!set) {
88
+ set = new Set();
89
+ graph.set(from, set);
90
+ }
91
+ set.add(to);
92
+ };
93
+ for (const node of Object.values(data.nodes)) {
94
+ if (node.ascendancyName !== ascendancy) {
95
+ continue;
96
+ }
97
+ if (!graph.has(node.skill)) {
98
+ graph.set(node.skill, new Set());
99
+ }
100
+ for (const conn of node.connections) {
101
+ const target = data.nodes[conn.id];
102
+ if (!target || target.ascendancyName !== ascendancy) {
103
+ continue;
104
+ }
105
+ link(node.skill, conn.id);
106
+ link(conn.id, node.skill);
107
+ }
108
+ }
109
+ return graph;
110
+ }
111
+ /**
112
+ * Shortest path (BFS) from any of `sources` to `target`, as the list of nodes
113
+ * to add — excluding the sources, including the target. `[]` when the target is
114
+ * already a source; `null` when unreachable.
115
+ */
116
+ export function pathToNode(graph, sources, target) {
117
+ if (sources.has(target)) {
118
+ return [];
119
+ }
120
+ const prev = new Map();
121
+ const seen = new Set(sources);
122
+ const queue = [...sources];
123
+ for (let head = 0; head < queue.length; head++) {
124
+ const current = queue[head];
125
+ for (const next of graph.get(current) ?? []) {
126
+ if (seen.has(next)) {
127
+ continue;
128
+ }
129
+ seen.add(next);
130
+ prev.set(next, current);
131
+ if (next === target) {
132
+ const path = [];
133
+ let step = target;
134
+ while (step !== undefined && !sources.has(step)) {
135
+ path.push(step);
136
+ step = prev.get(step);
137
+ }
138
+ return path.reverse();
139
+ }
140
+ queue.push(next);
141
+ }
142
+ }
143
+ return null;
144
+ }
145
+ /**
146
+ * The nodes a click on an allocated `target` removes: everything beyond it (the
147
+ * nodes that lose their connection to the start once it's cut), keeping the
148
+ * target itself — clicking shortens the path *back* to the clicked node. When
149
+ * nothing lies beyond (the target is a tip), the target itself is removed.
150
+ */
151
+ export function removalSet(graph, startNode, allocated, target) {
152
+ if (!allocated.has(target)) {
153
+ return new Set();
154
+ }
155
+ const remaining = new Set(allocated);
156
+ remaining.delete(target);
157
+ const keep = reachable(graph, [startNode], remaining);
158
+ const beyond = new Set();
159
+ for (const id of allocated) {
160
+ if (id !== target && !keep.has(id)) {
161
+ beyond.add(id);
162
+ }
163
+ }
164
+ return beyond.size > 0 ? beyond : new Set([target]);
165
+ }
166
+ /** The subset of `allowed` still reachable from `roots` through the graph. */
167
+ export function reachable(graph, roots, allowed) {
168
+ const kept = new Set();
169
+ const seen = new Set();
170
+ const queue = [];
171
+ for (const root of roots) {
172
+ seen.add(root);
173
+ queue.push(root);
174
+ }
175
+ for (let head = 0; head < queue.length; head++) {
176
+ const current = queue[head];
177
+ for (const next of graph.get(current) ?? []) {
178
+ if (seen.has(next) || !allowed.has(next)) {
179
+ continue;
180
+ }
181
+ seen.add(next);
182
+ kept.add(next);
183
+ queue.push(next);
184
+ }
185
+ }
186
+ return kept;
187
+ }
188
+ /**
189
+ * Toggle a node in a manual build:
190
+ * - allocated target -> remove it and prune any node it orphaned from the start
191
+ * - unallocated target -> allocate the shortest path to it
192
+ *
193
+ * Returns the new allocated node ids (start node excluded — it's implicit).
194
+ * Pass a prebuilt `graph` to avoid recomputing it per click.
195
+ */
196
+ export function toggleAllocation(data, startNode, allocated, target, graph = buildTreeGraph(data)) {
197
+ if (target === startNode) {
198
+ return [...allocated];
199
+ }
200
+ if (allocated.has(target)) {
201
+ const removed = removalSet(graph, startNode, allocated, target);
202
+ return [...allocated].filter((id) => !removed.has(id));
203
+ }
204
+ const sources = new Set(allocated);
205
+ sources.add(startNode);
206
+ const path = pathToNode(graph, sources, target);
207
+ if (!path) {
208
+ return [...allocated];
209
+ }
210
+ return [...new Set([...allocated, ...path])];
211
+ }
212
+ /**
213
+ * Toggle an ascendancy node, pathing only within that ascendancy's own subgraph
214
+ * (rooted at its start node). The main-tree allocation is carried through
215
+ * untouched, so this is the ascendancy counterpart to {@link toggleAllocation}
216
+ * — clicks allocate the path from the ascendancy start, or remove beyond.
217
+ *
218
+ * Returns the new full allocated set (main tree + this ascendancy's nodes).
219
+ */
220
+ export function toggleAscendancyAllocation(data, ascendancy, allocated, target, graph = buildAscendancyGraph(data, ascendancy)) {
221
+ const start = ascendancyStartNode(data, ascendancy);
222
+ if (start === undefined) {
223
+ return [...allocated];
224
+ }
225
+ // Split off this ascendancy's slice; everything else stays exactly as-is.
226
+ const ascAllocated = new Set();
227
+ const rest = [];
228
+ for (const id of allocated) {
229
+ if (graph.has(id)) {
230
+ ascAllocated.add(id);
231
+ }
232
+ else {
233
+ rest.push(id);
234
+ }
235
+ }
236
+ const nextAsc = toggleAllocation(data, start, ascAllocated, target, graph);
237
+ return [...rest, ...nextAsc];
238
+ }
239
+ //# sourceMappingURL=allocate.js.map