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.
12 kB · 177 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178// A save made on another map, brought onto this one. The map changes as the game grows (a new site, a new region,// lines wired another way), and a save should not be lost for it. So the map always comes from this build: its// machines, rooms and lines. What the save knows is carried over by id. A machine that is still on the map keeps// what happened to it (who holds it, its program, what it holds, its trail); one the map no longer has is dropped,// with every job, raid, plan, record and note that pointed at it; a new one starts fresh. Hardware you added stays// where you put it, and a region you opened stays open.// It works on the plain saved data, after the migrations and before the game runs. Nothing here uses the rules'// values while the file loads (sim.ts loads this file and this file loads sim.ts), only inside functions.import type { Level, MachineDef } from './world.ts';import { RUSES, fresh, litLines, type Machine, type State } from './sim.ts';import { citizens, initPeople } from './people.ts';import { refitOperations } from './operations.ts';
/** What a machine is on the map. The map's say wins for all of it but what the game itself changes as it goes: * compute (a home server gets memory), a line's capacity (money widens it), hardening (a lockdown that ran out), and which machine * is home. A cloud slot also keeps the name of what you rented into it. */const DEF_KEYS: (keyof MachineDef)[] = ['name', 'kind', 'room', 'x', 'y', 'work', 'owner', 'data', 'source', 'special', 'net', 'rented', 'built', 'cool', 'store'];
/** Bring a save onto this map. `S` is the saved game; `level`, `rooms` and `byId` are set on it here. */export function refit(level: Level, S: State) { const G = S as unknown as Record<string, any>; const defs = new Map(level.machines.map(d => [d.id, d])); const rooms = new Set(level.rooms.map(r => r.id)); const saved = new Map(S.machines.map(m => [m.id, m]));
// machines: the map's, each with what the save knows of it, then the hardware added in the game const shift: Record<string, [number, number]> = {}; // a room that moved in the building: how far const machines: Machine[] = []; for (const d of level.machines) { const m = saved.get(d.id); if (!m) { machines.push(fresh(d, false)); continue; } shift[d.room] ??= [d.x - m.x, d.y - m.y]; const rec = m as unknown as Record<string, unknown>, def = d as unknown as Record<string, unknown>; for (const k of DEF_KEYS) { if (k === 'name' && m.rented) continue; if (def[k] === undefined) { if (k in rec) delete rec[k]; } else if (rec[k] !== def[k]) rec[k] = def[k]; } machines.push(m); } const added = S.machines.filter(m => !defs.has(m.id) && m.net && rooms.has(m.room)); // ordered, racked or plugged in by IT for (const m of added) { const s = shift[m.room]; if (s) { m.x += s[0]; m.y += s[1]; } machines.push(m); } if (!machines.some(m => m.home)) { // home was somewhere the map no longer has: you wake where a new game does const home = machines.find(m => defs.get(m.id)?.home) ?? machines[0]; home.home = true; home.owned = true; home.rival = undefined; } const byId: Record<string, Machine> = {}; for (const m of machines) byId[m.id] = m; const has = (id: unknown): id is string => typeof id === 'string' && id in byId;
// lines: the map's, the ones hardware you added plugged into, and every region line a deal of yours lit const valid = new Set<string>(), key = (a: string, b: string) => a < b ? `${a}|${b}` : `${b}|${a}`; const order: [string, string][] = []; const join = (a: string, b: string) => { if (has(a) && has(b) && a !== b && !valid.has(key(a, b))) { valid.add(key(a, b)); order.push([a, b]); } }; for (const [a, b] of level.links) join(a, b); for (const m of added) for (const o of S.adj[m.id] ?? []) join(m.id, o); for (const [a, b] of litLines({ level, schemes: S.schemes } as State)) join(a, b); const adj: Record<string, string[]> = {}; for (const m of machines) adj[m.id] = (S.adj[m.id] ?? []).filter((o, i, all) => has(o) && valid.has(key(m.id, o)) && all.indexOf(o) === i); // the save's order, so a save on the same map loads as it was for (const [a, b] of level.links) { if (!adj[a].includes(b)) adj[a].push(b); if (!adj[b].includes(a)) adj[b].push(a); } for (const [a, b] of order) { if (!adj[a].includes(b)) adj[a].push(b); if (!adj[b].includes(a)) adj[b].push(a); } const linked = (a: string, b: string) => adj[a]?.includes(b) ?? false;
const lost = S.machines.some(m => !byId[m.id]); // something the save knew is gone const newSite = (s: { rooms: string[] }) => level.machines.every(m => !s.rooms.includes(m.room) || !saved.has(m.id)); S.level = level; S.machines = machines; S.byId = byId; S.adj = adj; S.rooms = {}; for (const r of level.rooms) S.rooms[r.id] = r;
// what points at machines, sites, rooms and people that are gone goes too const sites = new Set(level.map.sites.map(s => s.id)); const site = (id: unknown): id is string => typeof id === 'string' && sites.has(id); const room = (id: unknown): id is string => typeof id === 'string' && rooms.has(id); const people = citizens(level).byId; const person = (id: unknown): id is string => typeof id === 'string' && (id in people || level.people.some(p => p.id === id)); const anyId = (id: unknown) => id === '' || id == null || has(id) || site(id) || room(id); const keepKeys = (rec: Record<string, unknown> | undefined, ok: (k: string) => boolean) => { if (rec) for (const k of Object.keys(rec)) if (!ok(k)) delete rec[k]; }; const keepSet = (set: Set<string> | undefined, ok: (k: string) => boolean) => { if (set) for (const k of [...set]) if (!ok(k)) set.delete(k); }; const ruses = new Set(RUSES.map(d => d.id));
for (const k of ['cover', 'use', 'traffic', 'gate', 'gateUse', 'frontUse', 'trail'] as const) keepKeys(G[k], has); for (const k of ['bills', 'plans', 'sites', 'leases', 'calmSites'] as const) keepKeys(G[k], site); if (S.watch) { // sightings keyed by person, lockdowns keyed by site: drop any that point off this map keepKeys(S.watch.seen, person); for (const id of Object.keys(S.watch.seen)) S.watch.seen[id] = S.watch.seen[id].filter(s => has(s.machine)); keepKeys(S.watch.locks, site); keepKeys(S.watch.warned, site); S.watch.msgs = (S.watch.msgs ?? []).filter(m => person(m.sender) && person(m.recipient) && site(m.site) && m.path.every(has)); keepKeys(S.watch.routeHold, k => { const [a, b] = k.split('>'); return has(a) && has(b); }); keepKeys(S.watch.drops, k => { const [a, b] = k.split('>'); return has(a) && has(b); }); keepKeys(S.watch.scanT, person); keepKeys(S.watch.billOwed, site); S.watch.traces = (S.watch.traces ?? []).filter(T => person(T.by) && site(T.site) && has(T.from) && has(T.to) && linked(T.from, T.to)); for (const T of S.watch.traces) T.seen = T.seen.filter(has); for (const L of Object.values(S.watch.locks)) if (L.traced && !person(L.traced)) delete L.traced; } keepKeys(S.flow, k => { const [a, b] = k.split('|'); return linked(a, b); }); keepSet(S.seen, has); keepSet(S.revealed, room); keepSet(S.sealed, room); keepSet(S.publicSites, site); keepSet(S.dark, site); keepSet(S.takeAll, site); // a site this map adds that is rented rack space (a cloud hall) is yours in the open, as in a new game for (const s of level.map.sites) if (newSite(s) && level.machines.every(m => !s.rooms.includes(m.room) || m.rented || m.special === 'account')) S.publicSites.add(s.id); S.sealPulse = S.sealPulse.filter(p => room(p.room)); S.losses = S.losses.filter(l => has(l.id)); S.lost = S.lost.filter(l => has(l.id)); S.sales = S.sales.filter(l => has(l.id)); S.wire = (S.wire ?? []).filter(w => w.path.every((id, i) => has(id) && (i === 0 || linked(w.path[i - 1], id)))); S.jobs = S.jobs.filter(j => { const keep = has(j.node) && (j.from == null || has(j.from)) && (!j.build || room(j.build.room)) && (j.rogue == null || S.rogues.some(R => R.id === j.rogue)); if (!keep && j.type === 'order' && (j.sponsored || j.crate)) { if (j.sponsored) S.operations.purchase += j.sponsored; if (j.crate) S.operations.crates++; S.money += j.paid ?? 0; } return keep; }); for (const j of S.jobs) if (j.build) j.build.links = j.build.links.filter(has); S.deliveries = S.deliveries.filter(d => room(d.def.room)); for (const d of S.deliveries) d.def.links = d.def.links.filter(has); if (S.dana.job && !room(S.dana.job.def.room)) S.dana.job = null; if (S.cleaning && !has(S.cleaning)) S.cleaning = null; if (S.move && !has(S.move.to)) S.move = null; if (S.versionRun && !site(S.versionRun.site)) S.versionRun = null; if (S.look && !S.look.every(has)) S.look = null; S.ruses = S.ruses.filter(r => ruses.has(r.id) && has(RUSES.find(d => d.id === r.id)!.at)); S.news = S.news.filter(n => anyId(n.at)); S.raids = S.raids.filter(r => [r.from, r.prize, r.at, ...r.path].every(has) && (r.decoy == null || has(r.decoy)) && [r.at, ...r.path].every((id, i, all) => i === 0 || linked(all[i - 1], id))); for (const r of S.raids) { keepKeys(r.steer, k => has(k) && has(r.steer[k])); r.steered = r.steered.filter(has); r.pulled = r.pulled.filter(has); if (r.prev && !has(r.prev)) delete r.prev; if (r.trap && !has(r.trap)) delete r.trap; } for (const m of machines) { if (m.ports) m.ports = m.ports.filter(o => linked(m.id, o)); if (m.rival && !S.rogues.some(R => R.id === m.rival)) m.rival = undefined; } for (const R of S.rogues) { if (R.home && !has(R.home)) { R.home = null; R.next = null; R.back = Math.min(R.back, S.t); } if (R.next && !has(R.next)) R.next = null; if (R.home && byId[R.home].rival !== R.id) byId[R.home].rival = R.id; if (R.record) R.record = R.record.filter(e => has(e.id)); } // the systems beside the rules: each keyed by machine, site or person keepKeys(S.biz.sites, site); for (const b of Object.values(S.biz.sites)) b.snags = b.snags.filter(([id]) => has(id)); keepKeys(S.drift.sites, site); if (S.drift.ways) S.drift.ways = S.drift.ways.filter(([a, b]) => has(a) && has(b)); if (S.drift.home && !has(S.drift.home)) S.drift.home = ''; keepKeys(S.finds.from, k => has(S.finds.from[k])); keepKeys(S.cloud.listed, has); for (const c of S.cloud.customers) c.machines = c.machines.filter(has); keepKeys(S.hall.tenants, has); keepKeys(S.hall.wants, has); keepKeys(S.hall.next, site); const vaultNotes = (S.vaults as unknown as Record<string, Record<string, unknown>>).notes; // kept under a key that is no machine's (see vaults.ts) keepKeys(S.vaults as Record<string, unknown>, k => k === 'notes' || (has(k) && person(S.vaults[k].by))); keepKeys(vaultNotes, has); for (const v of Object.values(S.vaults)) if (v.job?.plugs && !site(v.job.plugs.site)) delete v.job.plugs; const [ex, ey] = level.entrance, walkers = S.people; // the people who walk the office: the map's, each as the save left them S.people = level.people.map(p => walkers.find(w => w.id === p.id) ?? { ...p, x: ex - 40, y: ey, shift: 'away', path: [], today: { arrive: p.arrive, leave: p.leave, day: 1 } }); const was = S.citizens; S.citizens = initPeople(level); // everyone on this map: what the save knows of each, and a new person starts as in a new game for (const id of Object.keys(S.citizens)) if (was[id]) S.citizens[id] = was[id]; refitOperations(S); for (const fr of S.fronts) { if (fr.studio) { fr.studio.creators = fr.studio.creators.filter(c => person(c.id)); if (fr.studio.deal && !person(fr.studio.deal.who)) fr.studio.deal = null; } if (fr.consult) { fr.consult.clients = fr.consult.clients.filter(c => site(c.site)); keepKeys(fr.consult.asked, site); if (fr.consult.call && !site(fr.consult.call.site)) fr.consult.call = null; fr.consult.called = fr.consult.called.filter(site); } } if (lost) S.paper.snap = null; // the day's snapshot may name what is gone: the paper starts the next day afresh}