feat: audit phase 6 (T4) — node allocation, the Path of Exile payoff

The site borrowed PoE's visual language — passive-tree layout, plasma
connections, an RPG stats panel — without any of its mechanic. Nodes were
purely decorative. This is the interaction the concept was promising.

- New utils/allocation.ts: path-to-root walking, allocate/deallocate, edge
  state, and URL encode/decode. Pure, 17 unit tests.
- Allocation is path-connected like the game: allocating a leaf brings its
  whole path, and deallocating a node removes its descendants so nothing is
  ever orphaned. pathToRoot is cycle-safe, and a test asserts every real node
  reaches the origin.
- New pixi/allocation.ts: the path reads as "lit" by dimming everything else
  rather than brightening the selection — the tree is already near its
  brightness ceiling, so recession is the only headroom. Nodes dim via alpha
  (nothing tweens it after the entrance); connections dim via tint, because
  their alpha is driven by the permanent plasma pulse and writing to it would
  fight the animation every frame.
- Shareable via ?build=. Encoded as node ids, not indices: a bitset would be
  shorter, but any edit to data.ts would silently re-point every previously
  shared link at the wrong nodes. Decoding re-derives each path, so a
  hand-edited or partially-stale link can't produce a broken build, and
  unknown ids are dropped rather than throwing.
- Allocation hangs off a button in the modal rather than the node click,
  which already opens that modal. BuildBar shows the count with share/reset;
  share falls back to rewriting the address bar when the clipboard is blocked.

Two bugs found while verifying:
- Both deep links (?build and ?tour) had ended up nested inside the
  first-visit coach-hint setTimeout, so a shared link did nothing at all for
  a returning visitor and waited out the whole intro for everyone else —
  backwards, since a shared link is precisely the cold-arrival case. Hoisted
  to run immediately, unconditionally. A ?tour link now also skips the intro.
- A restored ?build= was wiped by the entrance, which tweens every node's
  alpha to 1 after the deep link applies. The result was a full-brightness
  tree with a counter claiming 4 allocated. Now re-applied once the entrance
  finishes.

Verified in the production build: allocating TypeScript lights
origin -> Skills -> Languages -> TypeScript and dims the rest; the counter
reads 4; the generated link restores exactly that state on a cold load; Reset
clears it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
TehRiehlDealandClaude Opus 5 committed 2026-08-09 02:01:53 -07:00
1 parent aa1cda8245
commit 85086e31b2
6 files changed
+448 -10

No files matched your search

+40
View File
@@ -0,0 +1,40 @@
// Visual treatment for allocated nodes.
//
// The path reads as "lit" by dimming everything else rather than brightening
// the selection — the tree is already near its brightness ceiling, so
// recession is the only headroom available.
import { isEdgeAllocated } from '$lib/utils/allocation';
import type { SkillTreeController } from './controller';
/** Alpha for nodes outside the current build. */
const DIMMED_NODE_ALPHA = 0.22;
/** Tint applied to connections outside the current build. */
const DIMMED_LINE_TINT = 0x2a2a35;
/**
* Apply (or clear) allocation styling.
*
* Nodes are dimmed via `alpha`, which nothing tweens after the entrance
* finishes. Connections are dimmed via `tint` instead — their alpha is driven
* by the permanent plasma-beam pulse, so writing to it here would fight the
* animation every frame.
*/
export function updateAllocation(ctrl: SkillTreeController, allocated: ReadonlySet<string>) {
const active = allocated.size > 0;
for (const obj of ctrl.nodeObjects) {
const lit = !active || allocated.has(obj.node.id);
const alpha = lit ? 1 : DIMMED_NODE_ALPHA;
obj.graphics.alpha = alpha;
obj.glowGraphics.alpha = lit ? 1 : DIMMED_NODE_ALPHA * 0.5;
if (obj.label) obj.label.alpha = alpha;
}
for (const line of ctrl.lineObjects) {
const lit = !active || isEdgeAllocated(allocated, line.node);
const tint = lit ? 0xffffff : DIMMED_LINE_TINT;
line.graphics.tint = tint;
line.glowGraphics.tint = tint;
line.plasmaBeam.tint = tint;
}
}