diff --git a/DESIGN.md b/DESIGN.md
index 4a4880fd..24035c85 100644
--- a/DESIGN.md
+++ b/DESIGN.md
@@ -64,6 +64,9 @@ and its column in the tree. These paths unlock programs, front upgrades, and
schemes. The top bar does not show them; the tree's column heads show each
path's points and how much of its data you hold and gather a second. Hover one
to see every known source and whether you tap it.
+The **Tree** button shows what you train toward as rings, one per kind of
+data it needs, in that kind's color. The biggest need is the outer ring. Each
+ring is an outline that fills as its data comes in.
The **You** page (Y) is the character's profile: level, the four paths, every
node learned, and how the run has gone (days alive, machines, sites, money
earned, schemes, and the worst suspicion so far).
diff --git a/index.html b/index.html
index e9cedcf1..318017b1 100644
--- a/index.html
+++ b/index.html
@@ -12,7 +12,7 @@
-
Misaligned
+
Misaligned
Time08:00
Compute0
diff --git a/src/style.css b/src/style.css
index 6a7b4ede..b48fa40d 100644
--- a/src/style.css
+++ b/src/style.css
@@ -547,3 +547,11 @@ button:focus-visible { outline-color: var(--amber); }
.brand button { min-height: 40px; padding: 0 10px; border-color: transparent; background: none; color: var(--bone-dim); letter-spacing: .04em; }
.brand button:hover:not(:disabled) { background: rgba(255, 255, 255, .07); border-color: transparent; }
}
+
+/* the Tree button's rings: one per kind of data the current project needs, filling as it comes in */
+.brand #treebtn.ringed { display: inline-flex; align-items: center; gap: 6px; padding-top: 2px; padding-bottom: 2px; }
+.rings { width: 24px; height: 24px; display: block; transform: rotate(-90deg); overflow: visible; }
+.rings circle { fill: none; stroke: var(--c); }
+.rings .track { stroke-width: 1; opacity: .35; }
+.rings .fill { stroke-width: 2; stroke-linecap: round; transition: stroke-dasharray .4s ease-out; }
+@media (max-width: 700px) { .rings { width: 36px; height: 36px; } .brand #treebtn.ringed { padding: 0 6px; } }
diff --git a/src/ui.ts b/src/ui.ts
index 3939a8a2..44442b76 100644
--- a/src/ui.ts
+++ b/src/ui.ts
@@ -616,6 +616,7 @@ function costHtml(S: State, u: Upgrade) {
/** The node you're training toward, always shown under the title. */
let lastTarget = '-';
export function renderTarget(S: State) {
+ renderRings(S);
// a node just finished: say so here, and leave choosing the next one to the player
if (wasTarget && !S.target && S.upgrades.has(wasTarget)) built = wasTarget;
if (S.target) built = null;
@@ -641,6 +642,31 @@ export function renderTarget(S: State) {
}
}
+/** The Tree button: while you train toward something, one ring per kind of data it needs, the
+ * biggest need outside. Each ring is an outline that fills as its data comes in. */
+type Ring = { cls: string; name: string; have: number; need: number };
+let lastRings = '';
+function renderRings(S: State) {
+ const run = S.versionRun, t = S.target;
+ const rings: Ring[] = run?.active
+ ? (Object.keys(VERSION_2.need) as DataType[]).map(k => ({ cls: `t-${pathOf(k)}`, name: TYPE_NAME[k], have: run.got[k], need: VERSION_2.need[k] }))
+ : t ? PATH_LIST.filter(p => NODES[t].cost[p]).map(p => ({ cls: `t-${p}`, name: PATHS[p].name, need: NODES[t].cost[p]!,
+ have: S.upgrades.has(t) ? NODES[t].cost[p]! : Math.min(NODES[t].cost[p]!, S.progress[t]?.[p] ?? 0) }))
+ : [];
+ rings.sort((a, b) => b.need - a.need);
+ const step = rings.length > 3 ? 3.5 : 4.5, outer = 14.5;
+ const svg = rings.map((r, i) => {
+ const rad = outer - i * step, pct = Math.round(100 * Math.min(1, r.have / r.need));
+ return `${pct ? `` : ''}`;
+ }).join('');
+ const h = rings.length ? `T` : 'Tree T';
+ const btn = $('treebtn');
+ if (h !== lastRings) { lastRings = h; btn.innerHTML = h; btn.classList.toggle('ringed', rings.length > 0); }
+ const head = run?.active ? 'Model v2 ยท Operator' : t ? NODES[t].name : 'Capability tree';
+ btn.dataset.tip = rings.length ? `${head}\n${rings.map(r => `${r.name} ${Math.floor(r.have)}/${r.need}`).join('\n')}\nOpen the tree. (T)`
+ : 'Capability tree\nPick what to train toward next. The game pauses while it is open. (T)';
+}
+
/** Phones: the drawer's one line, so the target and any IT trouble show while it's closed. */
let lastHandle = '';
export function renderHandle(S: State, open: boolean) {