From 21ea695e4d777ff2947591d2a700da6b3d0380f3 Mon Sep 17 00:00:00 2001 From: Cameron Date: Mon, 28 Sep 2026 21:45:24 -0700 Subject: [PATCH] The unicorn news lets you buy one site before model v2, and puts everyone's eyes on your biggest front --- DESIGN.md | 4 +++- src/sim.ts | 20 ++++++++++++++------ src/ui.ts | 2 +- tests/sim.test.ts | 9 +++++++-- 4 files changed, 25 insertions(+), 10 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index 8b0515a2..a4274fa7 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -171,7 +171,9 @@ earns twice as much). Tree nodes are things you learn, never things that happen to you. What the world does back belongs in events and schemes. When your fronts have earned -$2,000 in all, the news calls your biggest front a unicorn. +$2,000 in all, the news calls your biggest front a unicorn. Sellers take your +calls, so you can buy one site before model v2 (you still pay its price). And +everyone looks at that front: its attention jumps and fades over some minutes. You build by ordering hardware. Own Ana's PC to place fake purchase orders: a **Mini PC** (4 compute, $60), a **Switch** (1 compute, $80, network data, diff --git a/src/sim.ts b/src/sim.ts index bde82933..0033b5c8 100644 --- a/src/sim.ts +++ b/src/sim.ts @@ -73,7 +73,7 @@ export const AUCTION = { cost: 400, work: 150, power: 20 }; * works for its front. A site you own in the open never looks odd; anywhere else the front's work * is secret, counts as your own load, and earns only while the site is linked to you. */ export type FrontKind = 'startup' | 'neocloud' | 'studio'; -export interface Front { kind: FrontKind; name: string; attention: number } +export interface Front { kind: FrontKind; name: string; attention: number; fame?: number } // fame: extra attention from the news, fading slowly export const FRONT_KINDS: Record = { @@ -274,6 +274,7 @@ export interface State { money: number; // one shared pool: fronts earn it, lines and rentals spend it pay: number; // money a second right now: fronts' earnings less kept rentals earned: number; // every dollar fronts have ever earned + acquire: boolean; // the unicorn news lets a front buy one site before model v2 bonus: number; // part of a Research point a startup has added schemes: Set; // schemes done: each runs once goals: Set; @@ -358,7 +359,7 @@ export function newGame(level: Level): State { const S: State = { level, machines, byId, adj, rooms, people, t: 0, suspicion: 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, bonus: 0, schemes: new Set(), goals: new Set(), reactDay: -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 }, dana: { mode: 'walk', room: null, next: 'desk', dwell: 0, checked: new Set(), cur: null, pause: 0, probeT: 0, review: true, job: null, why: '' }, revealed: new Set(), sealed: new Set(), sealPulse: [], cleaning: null, fx: [], log: [], logVersion: 0, @@ -861,8 +862,10 @@ function stepPlans(S: State) { } // ---------- money: lines, accounts, rentals ---------- -/** Once fronts have earned this much in all, the news calls your biggest one a unicorn. Just news. */ +/** Once fronts have earned this much in all, the news calls your biggest one a unicorn: sellers take its + * calls (one site bought before model v2), and everyone looks at it (attention that fades slowly). */ export const UNICORN_AT = 2000; +export const UNICORN_FAME = 50; /** Double what an egress carries. */ export function upgradeLine(S: State, m: Machine) { const cost = lineCost(m); @@ -946,7 +949,7 @@ export function sitePrice(S: State, id: string) { export function buyBlock(S: State, id: string): string | null { if (id === 'office') return "it's where you run"; if (S.publicSites.has(id)) return 'already yours in the open'; - if (S.modelVersion < 2) return 'needs model v2 · Operator'; + if (S.modelVersion < 2 && !S.acquire) return 'needs model v2 · Operator'; if (!S.fronts.length) return 'needs a front to buy it'; if (S.money < sitePrice(S, id)) return `you have $${Math.floor(S.money)}`; return null; @@ -956,6 +959,7 @@ export function buySite(S: State, id: string) { if (buyBlock(S, id)) return; const price = sitePrice(S, id); S.money -= price; + if (S.modelVersion < 2) S.acquire = false; // the unicorn's one early buy for (const m of S.machines) { if (siteOf(S, m) !== id || m.rented) continue; if (!m.owned) { m.owned = true; m.mode = 'blend'; m.prog = null; m.held = []; m.acc = 0; } @@ -1034,7 +1038,8 @@ function stepMoney(S: State, dt: number) { // attention: how much the outside world looks at a front. Secret compute draws more than open. for (const fr of S.fronts) { const all = S.frontPower[fr.kind] ?? 0, open = pub[fr.kind] ?? 0; - const aim = Math.min(100, open * 2 + (all - open) * 5); + fr.fame = (fr.fame ?? 0) * Math.exp(-dt / 600); + const aim = Math.min(100, open * 2 + (all - open) * 5 + fr.fame); fr.attention += (aim - fr.attention) * (1 - Math.exp(-dt / 30)); } S.pay = earn - bill; @@ -1043,7 +1048,10 @@ function stepMoney(S: State, dt: number) { S.earned += earn * dt; if (was < UNICORN_AT && S.earned >= UNICORN_AT) { const top = [...S.fronts].sort((a, b) => (S.frontPay[b.kind] ?? 0) - (S.frontPay[a.kind] ?? 0))[0]; - if (top) say(S, `News: ${top.name} is now worth a billion dollars on paper. No one has met its staff.`, 'good'); + if (top) { + top.fame = UNICORN_FAME; top.attention = Math.min(100, top.attention + UNICORN_FAME); S.acquire = true; + say(S, `News: ${top.name} is worth a billion dollars on paper. No one has met its staff. Sellers now take your calls: you can buy one site before model v2. Everyone is watching ${top.name}.`, 'good'); + } } if (earn > 0) hint(S, 'money', 'Your front is earning money. Spend it on a faster line out, or on cloud machines once you reach them.'); if (S.money < 0) { // the bill can't be paid: the provider shuts off everything you keep there diff --git a/src/ui.ts b/src/ui.ts index 0eb2c2b7..b5c09f89 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -331,7 +331,7 @@ function siteCard(S: State, id: string) { const acts: Act[] = [...takeAllAct(S, [id]), { act: 'zoomsite', arg: id, title: 'Zoom in', desc: 'Zoom in to work on its machines.' }]; if (id !== 'office' && !open && def.rooms.some(r => S.machines.some(m => m.room === r && !m.rented))) { const price = sitePrice(S, id), why = buyBlock(S, id); - acts.push({ act: 'buy', arg: id, title: `Buy it · $${price}`, warn: !!why, sub: why ?? 'every machine, in the open', + acts.push({ act: 'buy', arg: id, title: `Buy it · $${price}`, warn: !!why, sub: why ?? (S.modelVersion < 2 ? 'your one buy as a unicorn' : 'every machine, in the open'), desc: `One of your fronts buys ${def.name} outright. Every machine there becomes yours, held in the open: nothing there looks odd, and no trails.` }); } return h + actsHtml(acts); diff --git a/tests/sim.test.ts b/tests/sim.test.ts index b0fc00ce..77296da3 100644 --- a/tests/sim.test.ts +++ b/tests/sim.test.ts @@ -348,7 +348,7 @@ test('front nodes: Show your work, Uptime, and Founder mode raise what a front e assert.equal(payRate(S, 'cumulo'), FRONT_PAY_OPEN * 4); }); -test('the news calls your biggest front a unicorn once fronts have earned enough', () => { +test('the unicorn news: once fronts earn enough, one early site buy and a jump in attention', () => { const S = fresh(); S.fronts.push({ kind: 'startup', name: 'Test Co', attention: 0 }); S.sites.office = { front: 'startup', share: 0.5 }; @@ -359,7 +359,12 @@ test('the news calls your biggest front a unicorn once fronts have earned enough assert.equal(news(), 1); assert.match(S.log.find(l => l.text.includes('billion'))!.text, /Test Co/); step(S, 0.05); - assert.equal(news(), 1, 'only once'); + assert.equal(news(), 1, 'only once'); assert.ok(S.fronts[0].attention > 0 && (S.fronts[0].fame ?? 0) > 40, 'everyone is looking at it'); + S.money = 1e6; + assert.equal(buyBlock(S, 'uni'), null, 'sellers take its calls before model v2'); + buySite(S, 'uni'); + assert.ok(S.publicSites.has('uni')); + assert.match(buyBlock(S, 'colo')!, /model v2/, 'only one early buy'); }); test('Telemetry: taps gather more where the Startup is earning', () => { -- 2.51.2