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

+81
View File
@@ -0,0 +1,81 @@
<script lang="ts">
// Controls for the allocated path: how many nodes, share, reset.
const {
count = 0,
onReset,
onShare
}: {
count?: number;
onReset: () => void;
onShare: () => Promise<boolean> | boolean;
} = $props();
let copied = $state(false);
async function share() {
copied = await onShare();
if (copied) setTimeout(() => (copied = false), 2000);
}
</script>
{#if count > 0}
<div class="build-bar" role="status" aria-live="polite">
<span class="count"><strong>{count}</strong> allocated</span>
<button onclick={share}>{copied ? '✓ Link copied' : 'Share build'}</button>
<button class="reset" onclick={onReset}>Reset</button>
</div>
{/if}
<style>
.build-bar {
position: fixed;
top: var(--space-5);
right: var(--space-5);
z-index: var(--z-panel-toggle);
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
border: 1px solid rgba(255, 215, 0, 0.4);
border-radius: var(--radius-pill);
background: rgba(10, 10, 20, 0.85);
font-size: 0.78rem;
}
.count {
color: var(--text-muted);
}
.count strong {
color: var(--color-gold);
font-variant-numeric: tabular-nums;
}
button {
padding: 0.25rem 0.6rem;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: var(--radius-pill);
background: transparent;
color: var(--text-primary);
font: inherit;
font-size: 0.72rem;
cursor: pointer;
}
button:hover {
border-color: var(--color-gold);
}
.reset {
border-color: transparent;
color: var(--text-subtle);
}
@media (max-width: 767px) {
.build-bar {
top: auto;
bottom: calc(var(--space-5) + 3.2rem);
right: var(--space-2);
}
}
</style>