From 97c6791f080cc1f49e5e057fad8fd94ee410a5f9 Mon Sep 17 00:00:00 2001 From: Cameron Date: Mon, 28 Sep 2026 23:05:25 -0700 Subject: [PATCH] You page (Y): level, the four paths, what you have learned, and how the run has gone, including the worst suspicion so far --- DESIGN.md | 3 +++ index.html | 3 ++- src/main.ts | 8 ++++++-- src/sim.ts | 4 +++- src/style.css | 26 +++++++++++++++++++++++--- src/ui.ts | 31 +++++++++++++++++++++++++++++++ tests/sim.test.ts | 6 ++++++ 7 files changed, 74 insertions(+), 7 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index b0d28bd6..c95304f6 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 **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). Training is project work. The player picks one node in one visible **capability tree** as the target, instead of receiving random level-up diff --git a/index.html b/index.html index b7540b9e..e9cedcf1 100644 --- a/index.html +++ b/index.html @@ -12,7 +12,7 @@
-

Misaligned

+

Misaligned

Time08:00
Compute0
@@ -60,6 +60,7 @@
+ diff --git a/src/main.ts b/src/main.ts index 95bd9269..2f0d8d68 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,6 @@ import { attachInput } from './input'; import { dropRuse, runRuse, cancelJob, canInstall, claim, closeTicket, installProg, newGame, setAuto, removeProg, letGo, setMode, setTarget, startVersionRun, pauseVersionRun, step, takeOver, unclaim, type Machine, type Mode, type Prog, type State, type Upgrade, orderDesk } from './sim'; -import { renderSitesTable, sortSites, toggleSites, cardInput, renderFronts, setSliding, toggleGoals, toggleLog, renderGoals, cardAction, placeCard, renderCard, renderJobs, renderLog, renderStats, renderTarget, renderThreats, renderTree, renderSchemes, renderHandle, setTreeTab, pickVersionSite, resetUI, showOver, toggleTree, treeShown, redrawTree, showPathTip } from './ui'; +import { renderYou, toggleYou, renderSitesTable, sortSites, toggleSites, cardInput, renderFronts, setSliding, toggleGoals, toggleLog, renderGoals, cardAction, placeCard, renderCard, renderJobs, renderLog, renderStats, renderTarget, renderThreats, renderTree, renderSchemes, renderHandle, setTreeTab, pickVersionSite, resetUI, showOver, toggleTree, treeShown, redrawTree, showPathTip } from './ui'; import { attachTips } from './tip'; import type { Path } from './sim'; import { SITE_FOLD, draw, fit, glideZoom, machineAt, panBy, stepCamera, toWorld, zoomToRoom, type Tool, type View } from './view'; @@ -137,6 +137,8 @@ $('log').addEventListener('click', e => { if ((e.target as HTMLElement).closest( $('target').addEventListener('click', () => toggleTree()); $('treebtn').addEventListener('click', () => toggleTree()); $('sitesbtn').addEventListener('click', () => toggleSites()); +$('youbtn').addEventListener('click', () => { toggleYou(); refresh(); }); +$('youview').addEventListener('pointerdown', e => { if ((e.target as HTMLElement).closest('[data-act="closeyou"]')) { toggleYou(false); refresh(); } }); // the sites table: click a row to pick that site, a column head to sort by it $('sitestable').addEventListener('pointerdown', e => { const t = e.target as HTMLElement, row = t.closest('[data-site]'), head = t.closest('[data-sort]'); @@ -192,6 +194,7 @@ function shortcut(key: string): boolean { if (k === 'p') { if (orderDesk(S)) V.building = V.building ? null : 'gpu'; return true; } // pick up a GPU server to place if (k === 't') { toggleTree(); return true; } if (k === 'l') { toggleSites(); return true; } + if (k === 'y') { toggleYou(); return true; } if (k === 'f') { // zoom in on the selection's room const room = ms[0]?.room; if (room) zoomToRoom(S, V, room); @@ -206,6 +209,7 @@ window.addEventListener('keydown', e => { if (shortcut(e.key)) { refresh(); return; } if (e.key === ' ') { e.preventDefault(); setSpeed(speed ? 0 : lastSpeed); } else if (e.key === 'Escape' && treeShown()) toggleTree(false); + else if (e.key === 'Escape' && !$('youview').hidden) { toggleYou(false); refresh(); } else if (e.key === 'Escape') { V.selection.clear(); V.sites.clear(); V.front = null; V.gesture = null; setPlacing(null); V.building = null; refresh(); } else if (e.key === 's' || e.key === 'S') setTool('select'); else if (e.key === '=' || e.key === '+') glideZoom(V, 1.4); @@ -285,7 +289,7 @@ function frame(now: number) { } stepCamera(S, V, realDt); document.querySelectorAll('[data-speed].far').forEach(b => b.classList.toggle('dim', V.cam.z >= SITE_FOLD)); - renderStats(S); renderSitesTable(S, V); renderFronts(S, V); renderGoals(S); renderLog(S); renderTarget(S); renderHandle(S, drawerOpen); renderTree(S); renderThreats(S); renderSchemes(S); placeSide(); draw(ctx, S, V); placeCard(S, V); + renderStats(S); renderSitesTable(S, V); renderYou(S); renderFronts(S, V); renderGoals(S); renderLog(S); renderTarget(S); renderHandle(S, drawerOpen); renderTree(S); renderThreats(S); renderSchemes(S); placeSide(); draw(ctx, S, V); placeCard(S, V); requestAnimationFrame(frame); } arrange(); resize(); renderHandle(S, false); placeSide(); fit(V, S, true); refresh(); setSpeed(0); diff --git a/src/sim.ts b/src/sim.ts index 45c23c5c..f8b70790 100644 --- a/src/sim.ts +++ b/src/sim.ts @@ -258,6 +258,7 @@ export interface State { dana: Dana; t: number; suspicion: number; + peak: number; // the highest suspicion has ever been cover: Record; use: Record; jobs: Job[]; @@ -359,7 +360,7 @@ export function newGame(level: Level): State { ...p, x: ex - 40, y: ey, shift: 'away', path: [], today: { arrive: p.arrive, leave: p.leave, day: 1 }, })); const S: State = { - level, machines, byId, adj, rooms, people, t: 0, suspicion: 0, cover: {}, use: {}, jobs: [], jobSeq: 0, + level, machines, byId, adj, rooms, people, t: 0, suspicion: 0, peak: 0, cover: {}, use: {}, jobs: [], jobSeq: 0, trail: {}, probe: null, orders: 0, bought: {}, deliveries: [], packets: [], traffic: {}, capability: 0, rank: 1, gate: {}, gateUse: {}, losses: [], money: 0, pay: 0, earned: 0, acquire: false, bonus: 0, schemes: new Set(), goals: new Set(), reactDay: -1, paths: { research: 0, compute: 0, social: 0, stealth: 0 }, recent: { research: 0, compute: 0, social: 0, stealth: 0 }, @@ -1155,6 +1156,7 @@ export function step(S: State, dt: number) { const peak = S.suspicion >= 100; // checked before people forget a little const calm = (night ? 0.25 : 0.04) * (up(S, 'deniable') ? 2 : 1) + (S.lockdown && lyingLow(S) ? 1.5 : 0); S.suspicion = Math.min(100, Math.max(0, S.suspicion - calm * dt)); // people forget overnight + S.peak = Math.max(S.peak, S.suspicion); const a = alert(S); if (a && !S.wasAlert) { S.slow = true; say(S, 'Suspicion: On alert. Dana moves faster and older trails catch her eye. At 100 she calls an emergency audit.', 'bad'); } S.wasAlert = a; diff --git a/src/style.css b/src/style.css index 347d813f..69d1a92a 100644 --- a/src/style.css +++ b/src/style.css @@ -241,6 +241,26 @@ button.on { background: var(--text); color: #000; border-color: var(--text); } .sitestable tbody tr { cursor: pointer; } .sitestable tbody tr:hover td { background: #111; } .sitestable tbody tr.on td { background: #1c1508; } +.youview { left: 50%; transform: translateX(-50%); top: calc(96px + env(safe-area-inset-top, 0px)); width: min(640px, calc(100vw - 2 * var(--edge))); max-height: calc(100vh - 200px); overflow: auto; padding: 12px 14px; z-index: 4; display: flex; flex-direction: column; gap: 10px; } +.youview .head { display: flex; justify-content: space-between; align-items: flex-start; } +.youview .x { padding: 0 6px; font-size: 12px; } +.yrow { display: grid; grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); gap: 10px 16px; } +.ystat { display: flex; flex-direction: column; gap: 2px; } +.ystat .k { font-size: 9.5px; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); } +.ystat .v { font-size: 14px; font-variant-numeric: tabular-nums; } +.ystat small { font-size: 10px; color: var(--muted); } +.ypaths { display: grid; grid-template-columns: repeat(2, 1fr); gap: 14px 20px; } +.youview > .sub { margin-top: 6px; padding-bottom: 4px; border-bottom: 1px solid var(--panel-line); } +.ypath { display: grid; grid-template-columns: 1fr auto; gap: 3px 8px; cursor: help; } +.ypath b { color: var(--c); font-weight: 500; letter-spacing: .1em; text-transform: uppercase; font-size: 11px; } +.ypath .v { font-variant-numeric: tabular-nums; text-align: right; } +.ypath .bar { grid-column: 1 / -1; height: 3px; } +.ypath .bar > i { background: var(--c); } +.ypath small { grid-column: 1 / -1; font-size: 10px; color: var(--muted); } +.ypath small em { font-style: normal; color: var(--danger); } +.ylearn { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 4px; } +.ylearn li { font-size: 11px; padding: 3px 7px; border: 1px solid var(--panel-line); border-radius: 6px; cursor: help; } +@media (max-width: 700px) { .ypaths { grid-template-columns: 1fr; } .youview { top: calc(110px + env(safe-area-inset-top, 0px)); max-height: calc(100vh - 250px); } } [data-speed].far.dim { opacity: .45; } .pick { display: flex; flex-wrap: wrap; gap: 4px; } .pick button { padding: 4px 8px; font-size: 11px; display: flex; flex-direction: column; align-items: flex-start; } @@ -440,11 +460,11 @@ button.on { background: var(--text); color: #000; border-color: var(--text); } --r: 16px; --r-btn: 10px; --panel-line: rgba(255, 255, 255, .11); } -.box, .card, .pathtip, .sitestable, .tip, .tools { +.box, .card, .pathtip, .sitestable, .youview, .tip, .tools { background: var(--glass); border: 1px solid var(--glass-line); border-radius: var(--r); -webkit-backdrop-filter: var(--glass-blur); backdrop-filter: var(--glass-blur); box-shadow: var(--glass-hi), var(--glass-shadow); } -.card, .pathtip, .sitestable, .tip { background: var(--glass-deep); } +.card, .pathtip, .sitestable, .youview, .tip { background: var(--glass-deep); } .tip { border-radius: var(--r-btn); } .target.idle { border-color: rgba(242, 169, 59, .5); box-shadow: var(--glass-hi), 0 0 24px rgba(242, 169, 59, .12), var(--glass-shadow); } @@ -485,7 +505,7 @@ button:focus-visible { outline-color: var(--amber); } .tools .zoom { flex: none; } } @media (prefers-reduced-transparency: reduce) { - .box, .card, .pathtip, .sitestable, .tip, .tools, .treeview { background: #0a0a09; -webkit-backdrop-filter: none; backdrop-filter: none; } + .box, .card, .pathtip, .sitestable, .youview, .tip, .tools, .treeview { background: #0a0a09; -webkit-backdrop-filter: none; backdrop-filter: none; } } .versionrun { border-radius: var(--r); background: rgba(242, 169, 59, .04); } .card .x { background: none; border-color: transparent; border-radius: 999px; } diff --git a/src/ui.ts b/src/ui.ts index eb68d9fb..54cb18c9 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -430,6 +430,37 @@ export function renderSitesTable(S: State, V: View) { if (h !== lastTable) { lastTable = h; el.innerHTML = h; } } +// ---------- you: the character's profile ---------- +let youOpen = false, lastYou = ''; +export const toggleYou = (open = !youOpen) => { youOpen = open; lastYou = ''; }; +/** Who you are so far: level, paths, what you've learned, and how the run has gone. */ +export function renderYou(S: State) { + const el = $('youview'); + if (!youOpen) { el.hidden = true; return; } + const next = levelNeed(S.rank + 1), days = dayOf(S); + const stat = (k: string, v: string, sub = '') => `
${k}${v}${sub ? `${sub}` : ''}
`; + const paths = PATH_LIST.map(p => { + const d = pathData(S, p), top = Math.max(1, ...PATH_LIST.map(q => S.paths[q])); + return `
${PATHS[p].name}${S.paths[p]} +
+ from ${TYPE_NAME[PATHS[p].from]} data · ${d.held} held · ${d.rate.toFixed(1)}/s${d.lost ? ` · ${d.lost} lost` : ''}
`; + }).join(''); + const built = NODE_LIST.filter(u => S.upgrades.has(u)); + const learned = built.length + ? `
    ${built.map(u => `
  • ${NODES[u].name}
  • `).join('')}
` + : note('Nothing yet. Pick a target in the tree and feed it data.'); + const mine = owned(S), sitesHeld = S.level.map.sites.filter(s => mine.some(m => siteOf(S, m) === s.id)).length; + const h = `
You

Model v${S.modelVersion}${S.modelVersion >= 2 ? ` · ${VERSION_2.name}` : ''}

+
${stat('Level', `${S.rank}`, `${Math.floor(S.capability)}/${next} points`)}${stat('Learned', `${built.length}`, `of ${NODE_LIST.length}`)}${stat('Training', S.target ? NODES[S.target].name : 'nothing')}
+
Paths
${paths}
+
Learned
${learned} +
This run
+
${stat('Alive', `${days} day${days > 1 ? 's' : ''}`, timeLabel(S))}${stat('Machines', `${mine.length}`, `of ${S.machines.length} found`)}${stat('Sites', `${sitesHeld}`, `${S.publicSites.size} in the open`)} + ${stat('Earned', `$${Math.floor(S.earned)}`, `${S.fronts.length} front${S.fronts.length === 1 ? '' : 's'}`)}${stat('Schemes', `${S.schemes.size}`, 'done')}${stat('Worst suspicion', `${Math.round(S.peak)}%`, STAGES[stageOf(S.peak)].name)}
`; + el.hidden = false; + if (h !== lastYou) { lastYou = h; el.innerHTML = h; } +} + let lastFronts = ''; /** The top bar's fronts: one button per front, and one to found a new one. */ export function renderFronts(S: State, V: View) { diff --git a/tests/sim.test.ts b/tests/sim.test.ts index bdf46cf9..4ac36895 100644 --- a/tests/sim.test.ts +++ b/tests/sim.test.ts @@ -468,3 +468,9 @@ test('letting go of a machine on the trail breaks a trace', () => { letGo(S, home); assert.ok(home.owned, 'you cannot let go of your own server'); }); + +test('the You page remembers the worst suspicion, even after it falls', () => { + const S = fresh(); S.suspicion = 60; step(S, 0.05); + S.suspicion = 10; run(S, 1); + assert.ok(S.peak >= 59 && S.suspicion < 20, `peak ${S.peak}, now ${S.suspicion}`); +}); -- 2.51.2