From 8abc997e26c3dbf5c36bc76403f3610e41c82ea5 Mon Sep 17 00:00:00 2001 From: Cameron Date: Wed, 7 Oct 2026 23:10:41 -0700 Subject: [PATCH] A Double line chip sits beside each site's Plan chip on the map, where you hold a line out; hover says the cost and the new rate --- src/input.ts | 9 +++++---- src/ui.ts | 4 ++++ src/view.ts | 25 ++++++++++++++++++++++--- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/input.ts b/src/input.ts index 8ae40e65..63f43c20 100644 --- a/src/input.ts +++ b/src/input.ts @@ -7,11 +7,12 @@ // right-drag, one finger move the map // wheel / two-finger scroll move the map; ctrl-wheel or pinch zooms // the switch beside a site's name turn Take it all on or off for that site +// the double line chip beside a site's name pay to double its lines out // the plan chip beside a site's name switch to its next plan: none, Gather, Store, Train // a scheme (green diamond) click to see its card; the Claim tool claims it -import { buildRoomAt, canInstall, cancelJob, claim, installProg, order, roomMachines, unclaim, visible, runRuse, setPlan, setTakeAll, takeOver, PLAN_LIST, type Plan, type State } from './sim'; +import { buildRoomAt, canInstall, cancelJob, claim, installProg, order, roomMachines, unclaim, visible, runRuse, setPlan, setTakeAll, upgradeSiteLine, takeOver, PLAN_LIST, type Plan, type State } from './sim'; import { tipAt } from './tip'; -import { planTip, regionTip, takeAllTip } from './ui'; +import { doubleTip, planTip, regionTip, takeAllTip } from './ui'; import { foldedSites, machineAt, pos, roomTabAt, stopAt, regionHit, regionSites, schemeAt, schemePos, shown, siteHit, siteKnown, switchAt, toMap, toScreen, toWorld, zoomAt, zoomToSite, type View } from './view'; /** The Plan chip goes from no plan through each plan in turn, then back to none. */ @@ -77,7 +78,7 @@ export function attachInput(cv: HTMLCanvasElement, getS: () => State, V: View, c if (e.button === 1 || e.button === 2) { beginPan(); return; } const sw = e.button === 0 && !V.building && !V.placing ? switchAt(V, wx, wy) : null; if (sw) { // a site's Take all switch or Plan chip on the map - if (sw.kind === 'take') setTakeAll(S, sw.id, !S.takeAll.has(sw.id)); else setPlan(S, [sw.id], nextPlan(S.plans[sw.id])); + if (sw.kind === 'take') setTakeAll(S, sw.id, !S.takeAll.has(sw.id)); else if (sw.kind === 'double') upgradeSiteLine(S, sw.id); else setPlan(S, [sw.id], nextPlan(S.plans[sw.id])); start = null; changed(); return; } if (V.building) { // hardware in hand: click an open spot to order it there; Shift keeps it in hand @@ -121,7 +122,7 @@ export function attachInput(cv: HTMLCanvasElement, getS: () => State, V: View, c const sw = m ? null : switchAt(V, wx, wy); // a site's Take all switch or Plan chip: say what it does const rg = m || sw || scheme ? null : S.level.map.regions.find(r => r.name === regionHit(S, V, wx, wy)); // a region's name: is it in reach, and how to get there if (e.pointerType === 'touch' || (!sw && !rg)) tipAt(null); - else if (sw) tipAt(sw.kind === 'take' ? takeAllTip(S, [sw.id]) : planTip(S, sw.id), { left: sw.x0, top: sw.y0, right: sw.x1, bottom: sw.y1, width: sw.x1 - sw.x0 }); + else if (sw) tipAt(sw.kind === 'take' ? takeAllTip(S, [sw.id]) : sw.kind === 'double' ? doubleTip(S, sw.id) : planTip(S, sw.id), { left: sw.x0, top: sw.y0, right: sw.x1, bottom: sw.y1, width: sw.x1 - sw.x0 }); else if (rg) { const [rx, ry] = toScreen(V, ...toMap(S, rg.at[0], rg.at[1])); tipAt(regionTip(S, rg.name), { left: rx - 60, top: ry - 14, right: rx + 60, bottom: ry + 20, width: 120 }); diff --git a/src/ui.ts b/src/ui.ts index 948d0d8e..2a7de577 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -572,6 +572,10 @@ export function planTip(S: State, id: string) { const p = S.plans[id], next = p ? PLAN_LIST[PLAN_LIST.indexOf(p) + 1] : PLAN_LIST[0]; return `${p ? `${PLANS[p].name} plan` : 'No plan'}\n${p ? PLANS[p].desc : 'Nothing is installed for you.'} Click for ${next ? `the ${PLANS[next].name} plan` : 'no plan'}.`; } +export function doubleTip(S: State, id: string) { + const lines = siteLines(S, id), cost = lines.reduce((n, m) => n + lineCost(S, m), 0), cap = lines.reduce((n, m) => n + capNow(S, m), 0); + return `Double the line\nPay $${cost} and this site's line out carries ${cap * 2} a second, up from ${cap}.${S.money < cost ? ` You have $${Math.floor(S.money)}.` : ''}`; +} /** Take it all: a switch at the top of a site card, with no words: its tip says what it does. * Its cost is per machine at the site, each second. */ function takeAllSwitch(S: State, ids: string[]) { diff --git a/src/view.ts b/src/view.ts index e68e4c27..cb1b47a1 100644 --- a/src/view.ts +++ b/src/view.ts @@ -7,7 +7,7 @@ import { alert, atWork, auditOn, hourOf, cacheSize, dataType, dayOf, leeVisible, linkKey, pathOf, trainerBuffer, up, canInstall, danaPerson, danaVisible, daylight, has, hopsHome, jobFor, probeVisible, roomAt, sourceFor, tapActive, traceCold, visible, frontOf, type FrontKind, type Machine, type Path, type Prog, type State, - BROKER_LOT, BUILDS, buildRoomAt, buildRooms, orderPrice, plugsAt, spotProblem, type Build, clusters, clusterBoost, crosses, gateLoad, takeable, leaseRate, lostPerMin, tapBuffer, siteData, siteFlow, ruseDef, ruseText, regionOpen, pending, cooled, capNow, moveNeed, rogueOf, rogueHeld, rogueCap, HOUR, type Ruse, PLANS, + BROKER_LOT, BUILDS, buildRoomAt, buildRooms, orderPrice, plugsAt, spotProblem, type Build, clusters, clusterBoost, crosses, gateLoad, takeable, siteLines, lineCost, leaseRate, lostPerMin, tapBuffer, siteData, siteFlow, ruseDef, ruseText, regionOpen, pending, cooled, capNow, moveNeed, rogueOf, rogueHeld, rogueCap, HOUR, type Ruse, PLANS, } from './sim'; import { deskOwner } from './people'; import { LANDMARKS, isLandmark, landmarksAt } from './landmarks'; @@ -1278,7 +1278,7 @@ const labelled = new Map(); const mapTaken: [number, number, number, number][] = []; // screen boxes held by site names on the map this frame /** The switches beside site names drawn this frame (Take it all, Plan), in screen pixels, so a click can find them. */ -const toggles: { id: string; kind: 'take' | 'plan'; x0: number; y0: number; x1: number; y1: number }[] = []; +const toggles: { id: string; kind: 'take' | 'plan' | 'double'; x0: number; y0: number; x1: number; y1: number }[] = []; /** The Take it all switch or Plan chip under a world point, with its screen box. */ export function switchAt(V: View, wx: number, wy: number) { const [x, y] = toScreen(V, wx, wy); @@ -1293,13 +1293,31 @@ function switchesWidth(ctx: CanvasRenderingContext2D, S: State, id: string, px: let w = 0; if (S.takeAll.has(id) || takeable(S, id).length) w += 12 * px + 24 * px; if (hasPlanChip(S, id)) w += 12 * px + ctx.measureText(planText(S, id)).width + 12 * px; + if (siteLines(S, id).length) w += 12 * px + ctx.measureText(doubleText(S, id)).width + 12 * px; ctx.font = font; return w; } /** The Take it all switch, then the Plan chip. (x, y) is the left end, centred on y. */ function drawSwitches(ctx: CanvasRenderingContext2D, S: State, V: View, id: string, x: number, y: number, px: number) { x = drawTakeAll(ctx, S, V, id, x, y, px); - if (hasPlanChip(S, id)) drawPlan(ctx, S, V, id, x, y, px); + if (hasPlanChip(S, id)) x = drawPlan(ctx, S, V, id, x, y, px); + if (siteLines(S, id).length) drawDouble(ctx, S, V, id, x, y, px); +} +/** What it costs to double every line out of a site, as the chip's words. */ +export const doubleCost = (S: State, id: string) => siteLines(S, id).reduce((n, m) => n + lineCost(S, m), 0); +const doubleText = (S: State, id: string) => `DOUBLE LINE $${doubleCost(S, id)}`; +/** A site's Double line chip: click it to pay and double every line out of the site. Grey when you can't pay. */ +function drawDouble(ctx: CanvasRenderingContext2D, S: State, V: View, id: string, x: number, y: number, px: number) { + const ok = S.money >= doubleCost(S, id), text = doubleText(S, id), align = ctx.textAlign, base = ctx.textBaseline; + ctx.font = `500 ${9.5 * px}px ${MONO}`; ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; + const w = ctx.measureText(text).width + 12 * px, h = 14 * px; + ctx.beginPath(); ctx.roundRect(x, y - h / 2, w, h, 3 * px); + ctx.fillStyle = ok ? 'rgba(242,169,59,0.18)' : 'rgba(0,0,0,0.6)'; ctx.fill(); + ctx.strokeStyle = ok ? C.amber : C.muted; ctx.lineWidth = 1.2 * px; ctx.stroke(); + ctx.fillStyle = ok ? C.amber : C.muted; ctx.fillText(text, x + 6 * px, y + 0.5 * px); + ctx.textAlign = align; ctx.textBaseline = base; + const [sx, sy] = toScreen(V, x, y - h / 2), [ex, ey] = toScreen(V, x + w, y + h / 2); + toggles.push({ id, kind: 'double', x0: sx, y0: sy, x1: ex, y1: ey }); } /** A site's plan as a chip: click it for the next plan. */ function drawPlan(ctx: CanvasRenderingContext2D, S: State, V: View, id: string, x: number, y: number, px: number) { @@ -1313,6 +1331,7 @@ function drawPlan(ctx: CanvasRenderingContext2D, S: State, V: View, id: string, ctx.textAlign = align; ctx.textBaseline = base; const [sx, sy] = toScreen(V, x, y - h / 2), [ex, ey] = toScreen(V, x + w, y + h / 2); toggles.push({ id, kind: 'plan', x0: sx, y0: sy, x1: ex, y1: ey }); + return x + w + 12 * px; } /** What a site gathers, stores, and trains, as colored parts. Empty when it runs none of them. */ function flowParts(S: State, id: string): [string, string][] { -- 2.51.2