From 2b9f4bb0bec7acc618b5179370e3b697b969fc0f Mon Sep 17 00:00:00 2001 From: Cameron Date: Mon, 28 Sep 2026 12:17:56 -0700 Subject: [PATCH] Sites table (L): every known site in one sortable table; 8x and 16x speed while zoomed out to the map Co-Authored-By: Claude Opus 5.5 --- DESIGN.md | 7 ++++++- index.html | 6 +++++- src/main.ts | 19 +++++++++++++++---- src/style.css | 17 +++++++++++++++++ src/ui.ts | 34 ++++++++++++++++++++++++++++++++++ 5 files changed, 77 insertions(+), 6 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index 06a12f9c..5d599ba9 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -109,6 +109,10 @@ applies to its parts until the player changes a smaller part. Moving capacity between these jobs is a central decision, because the same capacity cannot do all three at once. +The **Sites** table (L) lists every site you know: machines held, compute, +data held by kind, plan, front, earnings, and trouble. Click a row to pick +the site; click a column head to sort. + Sites are programmable. A site's **plan** sets up every machine you hold there: **Gather** (taps, and caches to hold), **Store** (caches), or **Train** (trainers on big machines, taps and caches around them). It installs two at a time, and @@ -296,7 +300,8 @@ threats should create building decisions rather than another period of waiting. One clock drives both the office and the map. Local workday changes, rent, and regional events keep running while zoomed out. Faster map speed can help with travel and long jobs, but every important deadline and threat needs a visible -warning and a response. +warning and a response. 8x and 16x run only while zoomed out to +the map; closer in they run at 4x, and anything big drops the game to 1x. ## The arc diff --git a/index.html b/index.html index 68202c0c..8ab0e089 100644 --- a/index.html +++ b/index.html @@ -12,7 +12,7 @@
-

Misaligned

+

Misaligned

Time08:00
Compute0
@@ -45,6 +45,8 @@ + +
@@ -62,6 +64,7 @@
+ @@ -75,6 +78,7 @@
  • Odd machines, trails and takeovers raise suspicion. Hover the meter to see why. At 100, IT calls an emergency audit: get under 80 within three hours or you're caught. Lie low, or run a scheme: send stored data to a machine to calm IT down.
  • Drag from one of your machines to a neighbor to take it over. It adds load to the machine you spread from, so do it while that machine is busy.
  • Each machine runs in Blend in (safe), Max (more compute, looks odd), or Off. Drag a box to set many at once.
  • +
  • Sites (L) lists every site you know in one table. Zoomed out to the map, 8x and 16x speed work too.
  • Click a site on the map to run it as a whole: give it a plan (Gather, Store, Train) and the game installs programs for you, or take the whole site at once.
  • Build a base on the machines you own: Taps gather data, links carry it, Caches hold it, Trainers turn it into capability, and a Broker sells what's left over through the site's front. Install programs from a machine's card or with keys 1–4.
  • Each kind of data trains its own path. Data from people's desks trains Social, from servers Compute, from the network switches and lines out Stealth, and lab data trains Research. Hover a path meter, or tap it on a phone, to see where its data comes from.
  • diff --git a/src/main.ts b/src/main.ts index bda83147..f1bb6f7b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,9 +1,9 @@ import { attachInput } from './input'; import { dropRuse, runRuse, cancelJob, canInstall, claim, closeTicket, installProg, newGame, removeProg, setMode, setTarget, startVersionRun, pauseVersionRun, step, takeOver, unclaim, type Machine, type Mode, type Prog, type State, type Upgrade, orderDesk } from './sim'; -import { 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 { 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 { draw, fit, glideZoom, machineAt, panBy, stepCamera, toWorld, zoomToRoom, type Tool, type View } from './view'; +import { SITE_FOLD, draw, fit, glideZoom, machineAt, panBy, stepCamera, toWorld, zoomToRoom, type Tool, type View } from './view'; import { BUILDING } from './world'; const cv = document.getElementById('cv') as HTMLCanvasElement; @@ -128,6 +128,14 @@ $('goals').addEventListener('click', e => { if ((e.target as HTMLElement).closes $('log').addEventListener('click', e => { if ((e.target as HTMLElement).closest('.logmore')) { toggleLog(); renderLog(S); } }); $('target').addEventListener('click', () => toggleTree()); $('treebtn').addEventListener('click', () => toggleTree()); +$('sitesbtn').addEventListener('click', () => toggleSites()); +// 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]'); + if (t.closest('[data-act="closesites"]')) toggleSites(false); + else if (head) sortSites(head.dataset.sort!); + else if (row) { V.selection.clear(); V.room = null; V.front = null; V.sites = new Set([row.dataset.site!]); refresh(); } +}); // number keys with nothing targeted pick a program up; click machines to drop it function setPlacing(p: Prog | null) { V.placing = p; } @@ -174,6 +182,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 === 'f') { // zoom in on a room: the folded tile you picked, or the selection's room const room = V.room ?? ms[0]?.room; if (room) zoomToRoom(S, V, room); @@ -252,7 +261,8 @@ function frame(now: number) { const dt = Math.min(0.05, (now - last) / 1000); last = now; if (!S.over && speed > 0 && !treeShown()) { // the tree pauses the game while you choose - for (let i = 0; i < speed; i++) { step(S, dt); if (S.over || S.slow) break; } + const steps = speed > 4 && V.cam.z >= SITE_FOLD ? 4 : speed; // 8x and 16x only while zoomed out to the map + for (let i = 0; i < steps; i++) { step(S, dt); if (S.over || S.slow) break; } if (S.slow) { S.slow = false; if (speed > 1) setSpeed(1); } // something big: drop to 1x so you can see it uiTick += dt; if (uiTick > 0.25 || S.over) { uiTick = 0; refresh(); } @@ -264,7 +274,8 @@ function frame(now: number) { panBy(V, (held.has('ArrowRight') ? v : 0) - (held.has('ArrowLeft') ? v : 0), (held.has('ArrowDown') ? v : 0) - (held.has('ArrowUp') ? v : 0)); } stepCamera(S, V, realDt); - renderStats(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); + 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); requestAnimationFrame(frame); } arrange(); resize(); renderHandle(S, false); placeSide(); fit(V, S, true); refresh(); setSpeed(0); diff --git a/src/style.css b/src/style.css index 3e6b5066..5be7b8ce 100644 --- a/src/style.css +++ b/src/style.css @@ -228,6 +228,23 @@ button.on { background: var(--text); color: #000; border-color: var(--text); } .frontlist .add { color: var(--muted); border-style: dashed; justify-content: center; min-height: 32px; } .frontlist .add.ready { color: var(--amber); border-color: var(--amber); border-style: solid; } .frontlist .add.on { color: #000; } +/* the sites table: every site you know, one row each */ +.sitestable { left: 50%; transform: translateX(-50%); top: calc(96px + env(safe-area-inset-top, 0px)); width: min(900px, calc(100vw - 2 * var(--edge))); max-height: 60vh; overflow: auto; padding: 8px 10px; background: #000; z-index: 4; } +.sitestable .head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; } +.sitestable .x { padding: 0 6px; font-size: 12px; } +.sitestable table { width: 100%; border-collapse: collapse; font-size: 11px; font-variant-numeric: tabular-nums; } +.sitestable th { text-align: left; font-weight: 500; font-size: 9.5px; letter-spacing: .1em; text-transform: uppercase; color: var(--muted); padding: 4px 6px; border-bottom: 1px solid var(--panel-line); white-space: nowrap; } +.sitestable th[data-sort] { cursor: pointer; } +.sitestable th.on { color: var(--text); } +.sitestable td { padding: 5px 6px; border-bottom: 1px solid #1c1c1a; vertical-align: top; } +.sitestable td b { font-weight: 500; display: block; } +.sitestable td small { color: var(--muted); font-size: 10px; } +.sitestable td em { font-style: normal; color: var(--danger); } +.sitestable td b[class^="t-"] { display: inline; color: var(--c); margin-right: 6px; } +.sitestable tbody tr { cursor: pointer; } +.sitestable tbody tr:hover td { background: #111; } +.sitestable tbody tr.on td { background: #1c1508; } +[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; } .pick button small { font-family: var(--sans); font-size: 10px; color: var(--muted); } diff --git a/src/ui.ts b/src/ui.ts index 09bf40b9..6d35b4e3 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -394,6 +394,40 @@ function foundCard(S: State) { } return h; } +// ---------- the sites table ---------- +let tableOpen = false, lastTable = '', sortBy = 'makes'; +export const toggleSites = (open = !tableOpen) => { tableOpen = open; lastTable = ''; }; +export const sortSites = (k: string) => { sortBy = k; lastTable = ''; }; +/** Every site you know, one row each: what it holds, makes, and earns, and where there is trouble. Click a row to pick it. */ +export function renderSitesTable(S: State, V: View) { + const el = $('sitestable'); + if (!tableOpen) { el.hidden = true; return; } + const rows = S.level.map.sites.filter(s => siteKnown(S, s.id)).map(s => { + const st = siteStats(S, s.id), held = siteData(S, s.id), sf = S.sites[s.id], fr = sf?.front ? frontOf(S, sf.front) : null; + const lost = st.all.reduce((n, m) => n + lostPerMin(S, m.id), 0), odd = Math.max(0, ...st.mine.map(m => m.odd)); + return { s, st, held, fr, share: sf?.share ?? 0, lost, odd, data: Object.values(held).reduce((a, b) => a + b, 0), earns: fr ? earnsAt(S, [s.id]) : 0 }; + }); + const key: Record number> = { + name: () => 0, yours: r => r.st.mine.length, makes: r => r.st.makes, data: r => r.data, earns: r => r.earns, trouble: r => r.lost * 10 + r.odd, + }; + rows.sort((a, b) => key[sortBy](b) - key[sortBy](a) || a.s.name.localeCompare(b.s.name)); + const th = (k: string, label: string) => `${label}`; + const kinds = Object.keys(TYPE_NAME) as DataType[]; + const body = rows.map(r => ` + ${r.s.name}${S.publicSites.has(r.s.id) ? 'in the open' : 'secret'} + ${r.st.mine.length}/${r.st.all.length} + ${r.st.makes.toFixed(1)}/s + ${kinds.filter(t => r.held[t]).map(t => typeWord(t, `${r.held[t]} ${TYPE_NAME[t]}`)).join(' ') || 'none'} + ${S.plans[r.s.id] ? PLANS[S.plans[r.s.id]].name : 'by hand'} + ${r.fr ? `${r.fr.name} ${Math.round(r.share * 100)}%` : 'none'} + ${r.fr ? `${money(r.earns)}/s` : ''} + ${[r.lost ? `losing ${r.lost}/min` : '', r.odd >= 50 ? `odd ${Math.round(r.odd)}%` : ''].filter(Boolean).join(' ') || 'quiet'}`).join(''); + const h = `
    Sites · ${rows.length} known
    + ${th('name', 'Site')}${th('yours', 'Yours')}${th('makes', 'Makes')}${th('data', 'Data held')}${th('earns', 'Earns')}${th('trouble', 'Trouble')}${body}
    PlanFront
    `; + el.hidden = false; + if (h !== lastTable) { lastTable = 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) { -- 2.51.2