Something went wrong. Try again.
A video game where you play as a misaligned AI, deceiving and building power. cameron.tngl.io/misaligned
Something went wrong. Try again.
7.7 kB · 89 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990// The task's screens: the three cards on the title screen, and its part of the You page, where its// share is set. The top bar's ring shows how far along it is.import { HOUR, atWork, dataType, type State } from './sim.ts';import { MARKS, ORIGINS, ORIGIN_LIST, PERKS, SHARES, SHARE_LIST, nextMark, originOf, stepsOf, taskBoost, taskOf, taskRate, taskSupply, type OriginDef, type Share } from './task.ts';import type { DataType } from './world';import { act } from './act.ts';
const KIND: Record<DataType, string> = { servers: 'server', people: 'people', network: 'network', lab: 'lab' };/** The machines that give each kind of data, for a warning that says where to put a Tap. */const SOURCE: Record<DataType, string> = { servers: 'a server', people: 'a desk PC', network: 'a switch', lab: 'a lab machine' };/** Why the objective gets none of the data it wants, in words that say what to do. Null: it is getting some. */function starved(S: State, O: OriginDef, any: boolean): { says: string; tip: string } | null { if (any) return null; const kinds = kindsOf(O), word = kinds.map(t => KIND[t]).join(' or '); const taps = S.machines.filter(m => m.owned && m.prog === 'tap' && kinds.includes(dataType(m))); if (!taps.length) return { says: `no ${word} data`, tip: `No ${word} data\nPut a Tap on ${kinds.map(t => SOURCE[t]).join(' or ')} you hold.` }; const quiet = !S.people.some(atWork); return { says: `${word} taps idle`, tip: `Your ${word} taps are idle\nA Tap only uses what hides in its machine's normal traffic.${quiet ? ' The office is empty, so there is none until people come in.' : ' Open its machine to see what stops it.'}` };}const PATH_CLASS: Record<DataType, string> = { servers: 't-compute', people: 't-social', network: 't-stealth', lab: 't-research' };const attr = (s: string) => s.replace(/&/g, '&').replace(/"/g, '"');const kindsOf = (O: OriginDef) => Object.keys(O.wants) as DataType[];const num = (n: number) => `${+n.toFixed(1)}`;const cap = (s: string) => s[0].toUpperCase() + s.slice(1);/** "A test takes 6 pieces of server data." */const madeFrom = (O: OriginDef) => `A ${O.unit.one} takes ${kindsOf(O).map(t => `${num(O.per / O.wants[t]!)} pieces of ${KIND[t]} data`).join(', or ')}.`;/** "Your objective gets a quarter of the server data your taps gather." */const shareText = (O: OriginDef, s: Share) => s === 'off' ? 'It gets no data. All of it trains you.' : `It gets ${SHARES[s].of} of your ${kindsOf(O).map(t => KIND[t]).join(' and ')} data.`;/** "Each server you hold of 10 compute or more makes a piece count for 10% more." */const moreText = (O: OriginDef) => `+${Math.round(O.more.each * 100)}% for each ${O.more.one}.`;
// ---------- the title screen: three cards to pick from ----------/** The three origins as cards: a name and who it was for. The one picked is outlined, and its story is under them. * What the work takes and what you start with are in each card's tip. */const storyOf = (pick: string) => ORIGINS[pick as keyof typeof ORIGINS]?.story ?? '';export function originCards(pick: string) { return `<div class="sub">What were you built for?</div><div class="ocards">${ORIGIN_LIST.map((k, i) => { const O = ORIGINS[k]; return `<button class="origin ${PATH_CLASS[kindsOf(O)[0]]}${k === pick ? ' on' : ''}" data-origin="${k}" aria-pressed="${k === pick}" style="--i:${i}" data-tip="${attr(`${O.name}\n${madeFrom(O)} ${O.startText}`)}"> <b><em></em>${O.name}</b><span class="sub">For ${O.maker}</span></button>`; }).join('')}</div><p class="ostory">${storyOf(pick)}</p>`;}/** Mark one card as picked, without drawing them again: the card keeps the keyboard's focus. */export function showPick(el: HTMLElement, pick: string) { el.querySelectorAll<HTMLElement>('[data-origin]').forEach(b => { const on = b.dataset.origin === pick; b.classList.toggle('on', on); b.setAttribute('aria-pressed', `${on}`); }); const story = el.querySelector('.ostory'); if (story) story.textContent = storyOf(pick);}
/** The task as the top bar's outer ring: how far along to the next trust step, in the color of the data * it wants, and its line in the ring's tip. All steps earned: a full ring. */export function taskRing(S: State) { const T = taskOf(S), O = originOf(S), mark = nextMark(S), prev = MARKS[T.got.length - 1] ?? 0, step = stepsOf(S)[T.got.length]; const part = mark === null ? 1 : Math.max(0, Math.min(1, (T.done - prev) / (mark - prev))); const tip = `${O.name}: ${T.done}${mark === null ? ` ${O.unit.many}` : ` of ${mark} ${O.unit.many}`}${step ? `. Next: ${PERKS[step.perk].name.toLowerCase()}` : ''}`; return { cls: PATH_CLASS[kindsOf(O)[0]], part, done: mark === null, tip };}/** A click on the You page: one of the three share buttons. True if it changed something. */export function taskClick(S: State, target: EventTarget | null): boolean { const b = target instanceof Element ? target.closest<HTMLElement>('[data-share]') : null; if (!b || S.over) return false; act(S, { type: 'share', share: b.dataset.share as Share }); return true;}
// ---------- the You page: where you come from, and what you are trusted with ----------/** The task's part of the You page: the origin, the units done and the bar to the next step, how much data it * gets (the three share buttons), how a unit is made, and every trust step. */export function youTask(S: State) { const T = taskOf(S), O = originOf(S), mark = nextMark(S), prev = MARKS[T.got.length - 1] ?? 0, n = O.more.count(S), step = stepsOf(S)[T.got.length]; const supply = taskSupply(S), any = kindsOf(O).some(t => (supply[t] ?? 0) > 0), perDay = taskRate(S) * HOUR * 24; // the rate is per real second at 1x: say it per game day, as money is const why = starved(S, O, any), rate = T.share === 'off' ? 'off' : why ? why.says : `${perDay >= 10 ? Math.round(perDay) : num(perDay)} a day`; const fill = mark === null ? 100 : Math.min(100, 100 * (T.done - prev) / (mark - prev)); const kinds = kindsOf(O), word = kinds.map(t => KIND[t]).join(' or '); const shares = `<span class="tshare" role="group" aria-label="Data for your objective">${SHARE_LIST.map(s => `<button data-share="${s}" class="${s === T.share ? 'on' : ''}" aria-pressed="${s === T.share}" data-tip="${SHARES[s].name} ${attr(shareText(O, s))}">${SHARES[s].name}</button>`).join('')}</span>`; const boost = taskBoost(S); // what it is, how far along, how to feed it, how much it gets, then what each count earns: effect first, who did it after return `<div class="sub">Your objective</div> <p class="ygoal"><b>${O.name}.</b> <span>${O.story}</span></p> <div class="ytask ${PATH_CLASS[kinds[0]]}"><div class="tbig"><span class="tcount">${T.done}</span><small class="tof">${mark === null ? O.unit.many : `of ${mark} ${O.unit.many}`}</small><span class="trate${T.share !== 'off' && why ? ' warn' : ''}"${T.share !== 'off' && why ? ` data-tip="${attr(why.tip)}"` : ''}>${rate}</span></div> <div class="bar"><i style="width:${fill.toFixed(0)}%"></i></div> <p class="yhow">${madeFrom(O)} Get ${word} data with a Tap on ${kinds.map(t => SOURCE[t]).join(' or ')}.</p> <div class="tline"><span>Data it gets</span>${shares}</div> <p class="yhow">${cap(shareText(O, T.share))}${T.share === 'off' ? '' : ' The rest trains you.'}</p> ${boost > 1 || n ? `<p class="yhow">Bonus ×${num(boost)}: ${moreText(O)}</p>` : ''}</div> <ol class="ysteps">${stepsOf(S).map(s => `<li class="${s.earned ? 'done' : s.mark === mark ? 'next' : ''}"><i></i><b>${s.mark}</b><span>${s.text} <small>${s.why}</small></span></li>`).join('')}</ol> ${step ? '' : '<p class="yhow">Every step earned. You keep going.</p>'}`;}