bitmax/terminal.html
monster 59fd95b707 Initial commit: BORING SOFTWARE — an anthology of incremental games
Five incremental games disguised as boring 1998-era desktop utilities, launched from a Win98 shell (ENTROPY OS):

- Vol I  qBitTorrz       (index.html)        — torrent-client idler; seed, climb hardware tiers, migrate
- Vol II  DEFRAG.EXE     (defrag.html)       — steer a disk defragmenter; drag-select; outrun entropy
- Vol III MACRO_VIRUS.XLS(spreadsheet.html)  — build a compounding economy by drag-filling formulas
- Vol IV  UPLINK         (terminal.html)     — terminal + live htop; manage load/heat; escalate to root
- Vol V   INBOX ZERO     (inbox.html)        — keyboard-triage an exponential inbox; write regex rules
- ENTROPY OS             (desktop.html)      — the Win98 launcher that holds them all

Each is a single self-contained HTML file (vanilla JS, no deps, no build), with idle/offline
progress, scientific-scale numbers, diegetic upgrades, and a prestige that re-frames the fiction.

Also includes MANIFESTO.md (design thesis) and docs/ — the Collector's Edition companion:
a Dev Handbook and a Lore Bible (lore complete; 5 handbook chapters land in the next commit).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 15:44:36 +10:00

1871 lines
94 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>root@target: ~ — uplink</title>
<style>
/* ============================================================
UPLINK — root@ (Volume IV of BORING SOFTWARE)
An incremental game disguised as a green-on-black xterm + htop.
You are PID 1337. fork() workers, mine CPU cycles, escalate to
root, ssh to a bigger host. Load & Heat fight back.
============================================================ */
:root{
--bg:#020503; --bg2:#04110a; --panel:#06140c; --panel2:#0a1c10;
--line:#103a1f; --line2:#1a5e33;
--grn:#33ff66; --grn-dim:#1f9c44; --grn-faint:#0e5a26; --grn-bright:#86ffa6;
--amber:#ffc234; --amber-dim:#b8862a; --red:#ff4d4d; --red-dim:#a82a2a;
--cyan:#3fd6e0; --blue:#5a9cff; --mag:#d06bff;
--text:#33ff66; --muted:#2f8c4c; --dim:#1d6a36;
--mono:"SF Mono",ui-monospace,Menlo,"DejaVu Sans Mono",Consolas,"Liberation Mono",monospace;
}
*{box-sizing:border-box;}
html,body{height:100%;margin:0;}
body{
font-family:var(--mono); font-size:13px; color:var(--text);
background:#000; overflow:hidden; user-select:none; -webkit-user-select:none;
letter-spacing:.2px;
}
button,input{font-family:inherit;font-size:inherit;color:inherit;}
/* scanlines + CRT vignette overlay */
#crt{position:fixed;inset:0;z-index:900;pointer-events:none;
background:
repeating-linear-gradient(0deg, rgba(0,0,0,0) 0, rgba(0,0,0,0) 2px, rgba(0,20,8,.16) 3px, rgba(0,0,0,0) 4px),
radial-gradient(120% 120% at 50% 50%, rgba(0,0,0,0) 58%, rgba(0,0,0,.55) 100%);
mix-blend-mode:normal;}
#crt.off{display:none;}
#app{display:flex;flex-direction:column;height:100vh;width:100vw;
background:linear-gradient(180deg,#020a05,#000600);}
/* window title bar — xterm-ish */
#titlebar{display:flex;align-items:center;gap:8px;height:24px;flex:0 0 auto;
background:linear-gradient(#0b1f12,#06140c);border-bottom:1px solid var(--line);
padding:0 8px;color:var(--grn-dim);font-size:12px;}
#titlebar .dot{width:10px;height:10px;border-radius:50%;border:1px solid #0008;}
.d-r{background:#c0392b;} .d-y{background:#caa12f;} .d-g{background:#2f9c4c;}
#titlebar .ttl{flex:1;text-align:center;color:var(--muted);}
#titlebar .clk{color:var(--grn-faint);font-size:11px;}
#titlebar .x{cursor:pointer;color:var(--muted);padding:0 4px;}
#titlebar .x:hover{color:var(--grn);}
/* top status strip: privilege + gauges. Scrolls within itself on tiny windows
so the strip can never push the whole app into horizontal overflow. */
#strip{display:flex;align-items:stretch;gap:0;height:46px;flex:0 0 auto;
background:var(--panel);border-bottom:1px solid var(--line);overflow-x:auto;overflow-y:hidden;}
#strip::-webkit-scrollbar{height:0;}
.scell{display:flex;flex-direction:column;justify-content:center;gap:2px;padding:4px 12px;
border-right:1px solid var(--line);min-width:0;}
.scell .lab{font-size:9px;color:var(--dim);text-transform:uppercase;letter-spacing:.12em;}
.scell .val{font-size:15px;font-weight:700;color:var(--grn-bright);white-space:nowrap;}
.scell .val.sm{font-size:13px;}
.gauge{width:150px;}
.gbar{position:relative;height:11px;background:#040d07;border:1px solid var(--line);border-radius:2px;overflow:hidden;margin-top:2px;}
.gbar .gfill{position:absolute;left:0;top:0;bottom:0;width:0;transition:width .12s linear;}
.gbar .gtx{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:9px;color:#cfe;text-shadow:0 0 2px #000;}
.gfill.load{background:linear-gradient(90deg,#1f9c44,#33ff66);}
.gfill.heat{background:linear-gradient(90deg,#2f9c4c,#caa12f 55%,#ff4d4d);}
#strip .priv{flex:0 0 auto;}
.privtrack{display:flex;align-items:center;gap:5px;margin-top:3px;}
.pseg{font-size:10px;padding:1px 6px;border:1px solid var(--line2);border-radius:2px;color:var(--dim);}
.pseg.on{background:var(--grn-faint);color:var(--grn-bright);border-color:var(--grn);box-shadow:0 0 6px #33ff6644;}
.pseg.sep{border:0;color:var(--dim);padding:0;}
#strip .grow{flex:1;border-right:none;}
#strip .hostname{color:var(--cyan);}
.blink{animation:bl 1.1s steps(2,start) infinite;}
@keyframes bl{50%{opacity:.25;}}
/* htop process table */
#htop{flex:0 0 auto;display:flex;flex-direction:column;min-height:0;
background:var(--bg2);border-bottom:1px solid var(--line);}
#cpubars{display:grid;grid-template-columns:1fr 1fr;gap:2px 14px;padding:5px 10px 3px;
border-bottom:1px solid var(--line);}
.corebar{display:flex;align-items:center;gap:6px;font-size:11px;color:var(--muted);}
.corebar .cl{width:30px;color:var(--dim);}
.corebar .cg{flex:1;height:9px;background:#040d07;border:1px solid var(--line);position:relative;overflow:hidden;}
.corebar .cg i{position:absolute;left:0;top:0;bottom:0;display:block;}
.seg-lo{background:#1f9c44;} .seg-md{background:#caa12f;} .seg-hi{background:#ff4d4d;}
.corebar .cpct{width:36px;text-align:right;color:var(--grn-dim);}
#memrow{display:flex;gap:18px;padding:2px 10px 5px;font-size:11px;color:var(--muted);border-bottom:1px solid var(--line);}
#memrow b{color:var(--grn-bright);}
#ptable-wrap{overflow-y:auto;overflow-x:auto;min-height:90px;}
table.ph{border-collapse:collapse;width:100%;font-size:12px;table-layout:fixed;}
table.ph thead th{position:sticky;top:0;z-index:2;background:#0a1c10;color:#000;
background:linear-gradient(#1a4e2c,#0e3a20);color:#bfffd0;text-align:left;
padding:2px 6px;font-weight:700;border-bottom:1px solid var(--line2);white-space:nowrap;
cursor:default;}
table.ph thead th.num{text-align:right;}
table.ph tbody td{padding:1px 6px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
border-bottom:1px solid #07160d;}
table.ph tbody td.num{text-align:right;}
table.ph tbody tr{cursor:pointer;}
table.ph tbody tr:hover{background:#0c2414;}
table.ph tbody tr.sel{background:#123a20;outline:1px solid var(--grn-faint);}
table.ph tbody tr.zomb td{color:var(--red);}
table.ph tbody tr.dstate td{color:var(--amber);}
table.ph tbody tr.dying{animation:diepulse .5s linear infinite;}
@keyframes diepulse{50%{background:#3a0d0d;}}
.st-R{color:var(--grn-bright);font-weight:700;} .st-S{color:var(--muted);}
.st-D{color:var(--amber);} .st-Z{color:var(--red);font-weight:700;}
.pcpu{color:var(--grn-bright);} .pname{color:var(--grn-dim);}
.ptag{font-size:9px;color:var(--dim);}
.kbtn{cursor:pointer;color:var(--red-dim);}
.kbtn:hover{color:var(--red);text-shadow:0 0 4px #ff4d4d88;}
.rbtn{cursor:pointer;color:var(--cyan);} .rbtn:hover{text-shadow:0 0 4px #3fd6e088;}
#ptable-empty{padding:18px;text-align:center;color:var(--dim);}
/* resize handle between htop and terminal */
#hsplit{height:5px;flex:0 0 auto;cursor:row-resize;background:#06160c;border-top:1px solid var(--line);border-bottom:1px solid var(--line);}
#hsplit:hover{background:#0d2a18;}
/* terminal log + input */
#term{flex:1 1 auto;display:flex;flex-direction:column;min-height:0;background:#010503;}
#log{flex:1 1 auto;overflow-y:auto;overflow-x:hidden;padding:6px 10px;white-space:pre-wrap;
word-break:break-word;line-height:1.42;}
#log .ln{margin:0;}
.c-cmd{color:var(--grn-bright);} .c-prompt{color:var(--grn-dim);}
.c-sys{color:var(--muted);} .c-ok{color:var(--grn);} .c-warn{color:var(--amber);}
.c-err{color:var(--red);} .c-info{color:var(--cyan);} .c-mag{color:var(--mag);}
.c-hdr{color:var(--grn-bright);font-weight:700;} .c-dim{color:var(--dim);}
.c-acc{color:var(--cyan);font-weight:700;}
.pre{font-family:var(--mono);}
.cmdkw{color:var(--amber);}
/* input row */
#inrow{flex:0 0 auto;display:flex;align-items:center;gap:0;padding:4px 10px 8px;border-top:1px solid var(--line);}
#ps1{color:var(--grn-dim);white-space:nowrap;margin-right:6px;}
#ps1 .u{color:var(--grn-bright);} #ps1 .h{color:var(--cyan);} #ps1 .sym{color:var(--grn);}
#cmdwrap{position:relative;flex:1;}
#cmd{width:100%;background:transparent;border:none;outline:none;color:var(--grn-bright);
caret-color:var(--grn);font-family:var(--mono);font-size:13px;padding:0;}
#ghost{position:absolute;left:0;top:0;color:var(--grn-faint);pointer-events:none;white-space:pre;}
/* command palette / autocomplete dropdown */
#palette{position:absolute;left:0;bottom:30px;z-index:40;background:#04110a;border:1px solid var(--line2);
box-shadow:0 -4px 16px #000a;display:none;max-height:240px;overflow-y:auto;min-width:340px;border-radius:3px 3px 0 0;}
#palette.show{display:block;}
.pal-item{display:flex;gap:10px;padding:4px 10px;cursor:pointer;border-bottom:1px solid #07160d;}
.pal-item:hover,.pal-item.hi{background:#123a20;}
.pal-item .pk{color:var(--amber);min-width:130px;}
.pal-item .pd{color:var(--muted);flex:1;font-size:11px;}
.pal-item .pc{color:var(--grn-dim);font-size:11px;}
.pal-item.no .pc{color:var(--red-dim);}
/* quick command chips bar */
#chips{flex:0 0 auto;display:flex;gap:5px;flex-wrap:wrap;padding:4px 10px;border-top:1px solid var(--line);background:#040d07;}
.chip{font-size:11px;padding:2px 9px;border:1px solid var(--line2);border-radius:3px;color:var(--grn-dim);cursor:pointer;background:#06140c;white-space:nowrap;}
.chip:hover{background:#0e2a18;color:var(--grn-bright);border-color:var(--grn);}
.chip.cta{color:var(--amber);border-color:var(--amber-dim);}
.chip.cta:hover{background:#2a230a;color:var(--amber);}
.chip.dis{opacity:.4;pointer-events:none;}
/* bottom status bar */
#statusbar{display:flex;align-items:center;height:22px;flex:0 0 auto;background:var(--panel);
border-top:1px solid var(--line);font-size:11px;color:var(--muted);}
.sb{padding:0 10px;border-right:1px solid var(--line);height:100%;display:flex;align-items:center;gap:5px;}
.sb b{color:var(--grn-bright);} .sb.grow{flex:1;border-right:none;}
.sb .lnk{color:var(--cyan);cursor:pointer;} .sb .lnk:hover{text-shadow:0 0 4px #3fd6e088;}
/* modal (apt list, prestige, help, network map) */
#mback{position:fixed;inset:0;background:rgba(0,8,2,.6);z-index:600;display:none;align-items:center;justify-content:center;}
#mback.show{display:flex;}
.modal{background:#04110a;border:1px solid var(--line2);box-shadow:0 0 0 1px #000,0 12px 40px #000c, 0 0 30px #0a3a1c66;
width:760px;max-width:95vw;max-height:88vh;display:flex;flex-direction:column;border-radius:4px;overflow:hidden;}
.modal.sm{width:480px;}
.mtitle{display:flex;align-items:center;gap:8px;background:linear-gradient(#0d2a18,#08200f);
border-bottom:1px solid var(--line2);padding:7px 12px;color:var(--grn-bright);font-weight:700;}
.mtitle .x{margin-left:auto;cursor:pointer;color:var(--muted);padding:0 6px;border:1px solid var(--line);border-radius:2px;}
.mtitle .x:hover{color:var(--red);border-color:var(--red-dim);}
.mbody{padding:0;overflow:auto;flex:1 1 auto;}
.mfoot{padding:8px 12px;border-top:1px solid var(--line);display:flex;gap:8px;justify-content:flex-end;align-items:center;background:#06140c;}
.btn{border:1px solid var(--line2);background:#0a1c10;border-radius:3px;padding:5px 16px;color:var(--grn);cursor:pointer;font-weight:700;}
.btn:hover{background:#103018;color:var(--grn-bright);}
.btn.primary{background:#13401f;border-color:var(--grn);color:var(--grn-bright);}
.btn.primary:hover{background:#1a5a2c;box-shadow:0 0 10px #33ff6633;}
.btn.danger{border-color:var(--red-dim);color:var(--red);}
.btn.danger:hover{background:#2a0d0d;}
.btn[disabled]{opacity:.4;pointer-events:none;}
/* apt install list */
.aptline{padding:5px 8px;font-size:11px;color:var(--dim);border-bottom:1px solid var(--line);background:#06140c;}
.pkg{display:flex;align-items:center;gap:10px;padding:7px 12px;border-bottom:1px solid #0a1c10;}
.pkg:nth-child(even){background:#051006;}
.pkg:hover{background:#0a1c10;}
.pkg .pmain{flex:1;min-width:0;}
.pkg .pname{color:var(--grn-bright);font-weight:700;}
.pkg .pname .lv{color:var(--dim);font-weight:400;margin-left:8px;font-size:11px;}
.pkg .pdesc{color:var(--muted);font-size:11px;margin-top:1px;}
.pkg .peff{color:var(--cyan);font-size:11px;margin-top:1px;}
.pkg .pbuy{display:flex;align-items:center;gap:10px;min-width:150px;justify-content:flex-end;}
.pkg .pcost{font-weight:700;min-width:62px;text-align:right;}
.pcost.ok{color:var(--grn-bright);} .pcost.no{color:var(--red-dim);}
.pkg .btn{padding:3px 12px;font-size:11px;min-width:74px;text-align:center;}
.pkg.maxed{opacity:.6;} .pkg.locked{opacity:.4;}
.pkg.maxed .pname{color:var(--grn-dim);}
.reqnote{color:var(--amber-dim);font-size:10px;margin-top:2px;}
.mtabs{display:flex;background:#08200f;border-bottom:1px solid var(--line2);position:sticky;top:0;z-index:3;}
.mtab{padding:6px 14px;border-right:1px solid var(--line);cursor:pointer;color:var(--muted);font-size:12px;}
.mtab:hover{background:#0d2a18;color:var(--grn-bright);}
.mtab.active{background:#04110a;color:var(--grn-bright);border-bottom:2px solid var(--grn);}
.mtab.locked{opacity:.45;}
/* prestige / ssh modal */
.pgrid{padding:14px 18px;}
.pgrid h2{margin:0 0 4px;color:var(--grn-bright);}
.pgrid .big{font-size:28px;font-weight:800;color:var(--amber);text-shadow:0 0 12px #ffc23444;}
.warnbox{background:#1a0d0d;border:1px solid var(--red-dim);border-radius:3px;padding:9px 12px;margin:10px 0;color:#ff9a9a;font-size:12px;}
.perkrow{display:flex;align-items:center;gap:10px;padding:6px 8px;border-bottom:1px solid var(--line);}
.perkrow .pkmain{flex:1;}
.perkrow .pkn{color:var(--grn-bright);font-weight:700;}
.perkrow .pkd{color:var(--muted);font-size:11px;}
.perkrow .pke{color:var(--cyan);font-size:11px;}
.perkrow .btn{padding:3px 12px;font-size:11px;min-width:80px;}
/* network map */
.netmap{padding:14px;font-size:12px;line-height:1.7;}
.nethop{display:flex;align-items:center;gap:8px;}
.nethop .ip{color:var(--cyan);min-width:120px;}
.nethop .hn{color:var(--grn-dim);flex:1;}
.nethop .stt{color:var(--amber);}
.nethop.owned .stt{color:var(--grn);}
.nethop.cur{background:#0c2414;outline:1px solid var(--grn-faint);}
/* toasts */
#toasts{position:fixed;right:14px;bottom:32px;z-index:700;display:flex;flex-direction:column;gap:8px;align-items:flex-end;}
.toast{background:#04110a;border:1px solid var(--line2);border-left:4px solid var(--grn);color:var(--grn);
border-radius:4px;padding:8px 13px;box-shadow:0 4px 18px #000b, 0 0 16px #0a3a1c44;font-size:12px;max-width:340px;
animation:tin .22s ease;}
.toast.warn{border-left-color:var(--amber);color:var(--amber);}
.toast.bad{border-left-color:var(--red);color:#ff9a9a;}
.toast.info{border-left-color:var(--cyan);color:var(--cyan);}
.toast.prestige{border-left-color:var(--mag);color:var(--mag);}
.toast b{display:block;margin-bottom:2px;color:var(--grn-bright);}
.toast.bad b{color:#ffb3b3;} .toast.warn b{color:var(--amber);} .toast.info b{color:#9fe9f0;} .toast.prestige b{color:#e3b6ff;}
@keyframes tin{from{transform:translateX(36px);opacity:0;}to{transform:none;opacity:1;}}
::-webkit-scrollbar{width:12px;height:12px;}
::-webkit-scrollbar-track{background:#020a05;}
::-webkit-scrollbar-thumb{background:#10401f;border:3px solid #020a05;border-radius:6px;}
::-webkit-scrollbar-thumb:hover{background:#1a5e33;}
a{color:var(--cyan);}
</style>
</head>
<body>
<div id="app">
<!-- title bar -->
<div id="titlebar">
<span class="dot d-r"></span><span class="dot d-y"></span><span class="dot d-g"></span>
<span class="ttl" id="wintitle">root@target: ~ — 132x43</span>
<span class="clk" id="clk">--:--:--</span>
<span class="x" id="crt-toggle" title="Toggle CRT scanlines"></span>
<span class="x" id="menu-x" title="Menu"></span>
</div>
<!-- privilege + gauges strip -->
<div id="strip">
<div class="scell priv">
<span class="lab">Privilege</span>
<div class="privtrack" id="privtrack"></div>
</div>
<div class="scell">
<span class="lab">CPU Cycles</span>
<span class="val" id="s-cycles">0 Hz</span>
</div>
<div class="scell">
<span class="lab">Clock / Cores</span>
<span class="val sm" id="s-clock">0 Hz · 1</span>
</div>
<div class="scell">
<span class="lab">Exploits</span>
<span class="val sm" id="s-exploit">0</span>
</div>
<div class="scell gauge">
<span class="lab">System Load</span>
<div class="gbar"><div class="gfill load" id="g-load"></div><div class="gtx" id="g-load-tx">0%</div></div>
</div>
<div class="scell gauge">
<span class="lab">Heat</span>
<div class="gbar"><div class="gfill heat" id="g-heat"></div><div class="gtx" id="g-heat-tx">28°C</div></div>
</div>
<div class="scell grow" style="justify-content:center;">
<span class="lab">Host</span>
<span class="val sm hostname" id="s-host">localhost</span>
</div>
<div class="scell" style="justify-content:center;">
<span class="lab">root keys</span>
<span class="val sm" id="s-keys" style="color:var(--mag)">0</span>
</div>
</div>
<!-- htop -->
<div id="htop">
<div id="cpubars"></div>
<div id="memrow"></div>
<div id="ptable-wrap">
<table class="ph">
<thead><tr id="ph-head"></tr></thead>
<tbody id="ph-body"></tbody>
</table>
<div id="ptable-empty"></div>
</div>
</div>
<div id="hsplit"></div>
<!-- terminal -->
<div id="term">
<div id="log"></div>
<div id="chips"></div>
<div id="inrow">
<span id="ps1"></span>
<div id="cmdwrap">
<div id="ghost"></div>
<input id="cmd" type="text" autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false" />
<div id="palette"></div>
</div>
</div>
</div>
<!-- status bar -->
<div id="statusbar">
<div class="sb"><span id="sb-procs">0</span> procs</div>
<div class="sb">uptime <b id="sb-uptime">0s</b></div>
<div class="sb grow" id="sb-msg">type <span class="cmdkw">help</span> · TAB autocompletes · click a chip below</div>
<div class="sb"><span class="lnk" id="sb-apt">apt</span></div>
<div class="sb"><span class="lnk" id="sb-net">netmap</span></div>
<div class="sb"><span class="lnk" id="sb-ssh">ssh ▸ prestige</span></div>
</div>
</div>
<div id="crt"></div>
<div id="mback"><div class="modal" id="modal"></div></div>
<div id="toasts"></div>
<script>
"use strict";
/* ===========================================================================
UPLINK — root@ (Volume IV)
=========================================================================== */
const SAVE_KEY = "boringsoft_uplink_v1";
const TICK_MS = 100;
const MAX_OFFLINE_S = 8*3600; // cap offline at 8h
const LOG_CAP = 220; // terminal scrollback lines
/* ---- number formatting (SI -> scientific), re-implemented per spec -------- */
// Cycles run Hz -> kHz -> MHz -> GHz ... then scientific FLOPS.
const SI = ['Hz','kHz','MHz','GHz','THz','PHz','EHz','ZHz','YHz','RHz','QHz'];
function sciStr(n,unit){ const e=Math.floor(Math.log10(n)); return (n/Math.pow(10,e)).toFixed(2)+'e'+e+(unit?(' '+unit):''); }
function fmt(n,unit){
unit = (unit===undefined)?'Hz':unit;
if(n===Infinity) return '∞';
if(!isFinite(n)||isNaN(n)) return '0 '+unit;
if(n<0) n=0;
if(n<1000) return (Math.floor(n*10)/10===Math.floor(n)?Math.floor(n):(Math.round(n*10)/10))+' '+unit;
if(G&&G.sci) return sciStr(n,unit);
// SI prefixes only meaningful for the Hz family; for plain units fall through to k/M/G letters.
if(unit==='Hz'){
let i=0,x=n;
while(x>=1000 && i<SI.length-1){x/=1000;i++;}
if(i===SI.length-1 && x>=1000) return sciStr(n,'Hz');
return x.toFixed(x<100?2:1)+' '+SI[i];
}
// generic big number with k/M/G/T...
const P=['','k','M','G','T','P','E','Z','Y','R','Q'];
let i=0,x=n;
while(x>=1000 && i<P.length-1){x/=1000;i++;}
if(i===P.length-1 && x>=1000) return sciStr(n,unit);
return x.toFixed(x<100?2:1)+P[i]+(unit?(' '+unit):'');
}
function fmtCyc(n){ return fmt(n,'Hz'); }
function fmtInt(n){
if(!isFinite(n)) return '∞';
if(n<1000) return Math.floor(n).toString();
return fmt(n,'').trim();
}
function fmtTime(s){
if(!isFinite(s)||s<0) return '∞';
if(s<1) return '<1s';
s=Math.floor(s);
const d=Math.floor(s/86400),h=Math.floor(s%86400/3600),m=Math.floor(s%3600/60),ss=s%60;
if(d>999) return '∞';
if(d>0) return d+'d '+h+'h';
if(h>0) return h+'h '+m+'m';
if(m>0) return m+'m '+ss+'s';
return ss+'s';
}
function now(){ return Date.now(); }
function rand(a,b){ return a+Math.random()*(b-a); }
function pick(a){ return a[Math.floor(Math.random()*a.length)]; }
function clamp(v,a,b){ return v<a?a:(v>b?b:v); }
/* ===========================================================================
STATIC DATA
=========================================================================== */
// Privilege tiers. Escalation gates better workers/packages & is required for prestige.
const PRIV = ['guest','user','sudo','root'];
const PRIV_LABEL = {guest:'guest',user:'user',sudo:'sudo',root:'root'};
// Cost (cycles) + exploits to escalate INTO each tier.
const ESCALATE = {
user: {cyc:800, exp:0, from:'guest'},
sudo: {cyc:2.2e4, exp:1, from:'user'},
root: {cyc:2.5e6, exp:2, from:'sudo'},
};
// Worker process archetypes (generators). yield = base Hz/s. heat = °C/s added. mem in MB.
// minPriv gates availability. load = load units consumed.
const WORKERS = {
worker: {cmd:'cpu_worker', yield:9, heat:0.18, mem:24, load:1.0, cost:16, minPriv:'guest', col:'grn', desc:'general compute worker — cheap, steady cycles'},
miner: {cmd:'cryptominer', yield:54, heat:0.62, mem:96, load:1.7, cost:120, minPriv:'user', col:'amber',desc:'hashes coins — high yield, runs hot'},
cracker: {cmd:'hashcracker', yield:260, heat:1.05, mem:180, load:2.4, cost:900, minPriv:'user', col:'amber',desc:'GPU hash cracker — big yield, heavy load'},
scanner: {cmd:'vuln_scanner', yield:34, heat:0.30, mem:64, load:1.2, cost:220, minPriv:'user', col:'cyan', desc:'scans the subnet — small yield, drops EXPLOITS'},
botnet: {cmd:'botnet_node', yield:1800, heat:1.7, mem:420, load:3.2, cost:1.8e4, minPriv:'sudo', col:'mag', desc:'remote zombie node — fat yield, partly offline-passive'},
kernel: {cmd:'kthread_miner', yield:1.3e4, heat:2.6, mem:760, load:4.4, cost:2.2e5, minPriv:'root', col:'mag', desc:'kernel-thread miner — root-only, enormous yield'},
};
const WORKER_ALIASES = {worker:'worker',cpu_worker:'worker',miner:'miner',cryptominer:'miner',
cracker:'cracker',hashcracker:'cracker',scanner:'scanner',vuln_scanner:'scanner',
botnet:'botnet',botnet_node:'botnet',kernel:'kernel',kthread_miner:'kernel'};
// background "system" processes shown in htop for flavor (not yours, don't earn)
const SYS_PROCS = [
{cmd:'/sbin/init',user:'root',mem:8,base:0.0},
{cmd:'systemd-journald',user:'root',mem:16,base:0.1},
{cmd:'sshd: /usr/sbin',user:'root',mem:12,base:0.0},
{cmd:'[kworker/0:1]',user:'root',mem:0,base:0.2},
{cmd:'cron',user:'root',mem:6,base:0.0},
{cmd:'rsyslogd',user:'syslog',mem:9,base:0.1},
{cmd:'auditd --watch',user:'root',mem:11,base:0.3},
];
// apt packages = upgrades. kind: leveled mult/add, or one-time unlock.
const PACKAGES = [
// base / user tier
{id:'coreutils', tab:'core', name:'coreutils', minPriv:'guest', kind:'pids',
desc:'GNU coreutils — raises max process count (ulimit -u).',
base:140, growth:2.35, add:1, max:12},
{id:'overclock', tab:'core', name:'overclock', minPriv:'guest', kind:'clock',
desc:'CPU overclock — ×1.22 base clock (global cycle multiplier).',
base:60, growth:2.05, mult:1.22, max:25},
{id:'scheduler', tab:'core', name:'cfs-scheduler', minPriv:'user', kind:'eff',
desc:'Completely Fair Scheduler — ×1.18 yield per process (efficiency).',
base:300, growth:2.0, mult:1.18, max:25},
{id:'cooling', tab:'core', name:'lm-sensors+fancontrol', minPriv:'user', kind:'cool',
desc:'Liquid cooling control — +18°C Heat ceiling, dissipates faster.',
base:420, growth:1.9, add:18, max:24},
{id:'ulimit', tab:'core', name:'ulimit-tuning', minPriv:'user', kind:'loadcap',
desc:'Raise load headroom — +22% System Load ceiling before OOM.',
base:650, growth:2.0, mult:1.22, max:20},
{id:'turbo', tab:'core', name:'turbo-boost', minPriv:'sudo', kind:'clock',
desc:'Intel TurboBoost — ×1.4 base clock.',
base:4e4, growth:2.1, mult:1.4, max:20},
// unlocks / automation
{id:'cron', tab:'auto', name:'cron-daemon', minPriv:'user', kind:'unlock',
desc:'Schedule jobs — auto-reaps zombies (Z) before they leak.', base:2200},
{id:'autospawn', tab:'auto', name:'spawn.timer', minPriv:'sudo', kind:'unlock', req:'cron',
desc:'systemd timer — auto-forks your best affordable worker to fill spare load.', base:6e4},
{id:'rootkit', tab:'auto', name:'rootkit-lkm', minPriv:'sudo', kind:'unlock',
desc:'Loadable kernel module — hides you from the admin (stops the kill-spree event).', base:1.4e5},
{id:'polymorph', tab:'auto', name:'polymorph-engine', minPriv:'sudo', kind:'unlock', req:'rootkit',
desc:'Polymorphic code — D-state (uninterruptible) procs no longer stall yield.', base:5e5},
{id:'thermal', tab:'auto', name:'thermald-ai', minPriv:'root', kind:'unlock', req:'cooling',
desc:'AI thermal governor — Heat never throttles yield (ignores overheat penalty).', base:3e6},
{id:'distcc', tab:'auto', name:'distcc-farm', minPriv:'root', kind:'distcc',
desc:'Distributed compile farm — +12% offline/passive earnings per level.', base:1.2e6, growth:1.8, add:0.12, max:20},
];
const PKG_BY_ID = Object.fromEntries(PACKAGES.map(p=>[p.id,p]));
// prestige perks (persist across ssh / host hops)
const PERKS = [
{id:'fab', name:'Process Shrink (FinFET)', desc:'+20% global yield per level.', base:1, growth:1.6, mult:1.20, max:50},
{id:'silicon',name:'Better Silicon', desc:'+15% base clock per level.', base:1, growth:1.6, mult:1.15, max:50},
{id:'heatsink',name:'Bigger Heatsink', desc:'+12°C Heat ceiling per level (permanent).',base:2, growth:1.7, add:12, max:25},
{id:'preboot',name:'Persistent Daemons', desc:'Boot each host with +1 cpu_worker already forked, per level.', base:2, growth:1.8, add:1, max:20},
{id:'sentinel',name:'Watchdog (sentinel)', desc:'Permanently auto-reaps zombies on every host (free cron).', base:5, once:true},
{id:'genesis', name:'Genesis Block', desc:'Start every host already escalated to user, +1 starting exploit.', base:12, once:true, req:'sentinel'},
];
const PERK_BY_ID = Object.fromEntries(PERKS.map(p=>[p.id,p]));
// Hosts (prestige ladder). cores/clockMul scale; heatBase rises (nastier).
const HOSTS = [
{ip:'127.0.0.1', name:'localhost (your laptop)', cores:2, clockMul:1, heatBase:28},
{ip:'10.0.0.12', name:'devbox-staging', cores:4, clockMul:6, heatBase:32},
{ip:'10.0.4.40', name:'build-runner-07', cores:8, clockMul:40, heatBase:36},
{ip:'192.168.9.2', name:'db-primary', cores:16, clockMul:260, heatBase:40},
{ip:'172.16.30.1', name:'k8s-node-pool', cores:32, clockMul:1800, heatBase:44},
{ip:'45.77.12.88', name:'colo-rack-A12', cores:64, clockMul:1.3e4,heatBase:48},
{ip:'8.8.8.8', name:'edge-cdn-cluster', cores:128,clockMul:9e4, heatBase:52},
{ip:'aws://us-east',name:'the cloud (autoscaling)', cores:256,clockMul:7e5, heatBase:56},
];
function hostAt(i){ if(i<HOSTS.length) return HOSTS[i];
// procedurally extend past the table
const base=HOSTS[HOSTS.length-1];
const k=i-HOSTS.length+1;
return {ip:'10.'+(13+k)+'.0.1', name:'datacenter-Ω-'+(k+1), cores:base.cores*Math.pow(2,k),
clockMul:base.clockMul*Math.pow(8,k), heatBase:56+k*4};
}
/* ===========================================================================
STATE
=========================================================================== */
let G=null;
function freshHostProcs(){
// background system procs (not yours)
return SYS_PROCS.map((s,i)=>({pid:nextSysPid(i), sys:true, cmd:s.cmd, user:s.user,
mem:s.mem, cpu:s.base, state:'S', wkind:null}));
}
let _sysSeq=2;
function nextSysPid(i){ return 100+i*7+i; }
function freshState(){
const s={
v:1,
cycles:40, totalCycles:0, runPeak:0, lifetimeCycles:0, // start with enough to fork your first 2 cpu_workers
exploits:0, keys:0, prestiges:0, hostIdx:0,
priv:'guest',
procs:[], // your worker processes
sysProcs:freshHostProcs(),
upgrades:{}, // pkg id -> level
perks:{}, // perk id -> level
pidSeq:1337,
sel:null, // selected pid (for renice/kill via clicks)
sci:false,
crt:true,
heat:28, load:0,
overheatUntil:0, // if heat maxes, yield throttled until cools
log:[], // terminal lines {c, t}
history:[], // command history
// event timers
nextScanDrop:0, nextZomb:0, nextAdmin:0, adminUntil:0, nextScare:0,
lastSave:now(), started:now(),
htopH:0, // remembered htop height (px); 0 = default
sawIntro:false,
};
return s;
}
// derived
const lvl = id => G.upgrades[id]||0;
const plvl = id => G.perks[id]||0;
const hasPkg = id => (G.upgrades[id]||0)>0;
const hasPerk = id => (G.perks[id]||0)>0;
const privRank = p => PRIV.indexOf(p);
function host(){ return hostAt(G.hostIdx); }
function cores(){ return host().cores; }
function baseClock(){
// host clock × overclock × turbo × silicon perk
let c = 1000 * host().clockMul;
c *= Math.pow(PKG_BY_ID.overclock.mult, lvl('overclock'));
c *= Math.pow(PKG_BY_ID.turbo.mult, lvl('turbo'));
c *= Math.pow(PERK_BY_ID.silicon.mult, plvl('silicon'));
return c;
}
function yieldMult(){
let m=1;
m*=Math.pow(PKG_BY_ID.scheduler.mult, lvl('scheduler'));
m*=Math.pow(PERK_BY_ID.fab.mult, plvl('fab'));
return m;
}
function maxProcs(){
// base + coreutils raises ulimit -u; host cores add headroom.
// Host 0 (2 cores) starts at 6 — enough to build a small engine immediately.
return 6 + lvl('coreutils')*PKG_BY_ID.coreutils.add + Math.floor(cores()/2);
}
function heatCeiling(){
return host().heatBase + 72
+ lvl('cooling')*PKG_BY_ID.cooling.add
+ plvl('heatsink')*PERK_BY_ID.heatsink.add;
}
function loadCeiling(){
// ~3 load headroom per core + a flat base, scaled by ulimit tuning.
// Gives host 0 (2 cores) a ceiling of ~9 — room for several workers — while
// still punishing a runaway fork-bomb. ulimit multiplies it.
return (3 + cores()*3) * Math.pow(PKG_BY_ID.ulimit.mult, lvl('ulimit'));
}
function passiveMult(){ return 1 + lvl('distcc')*PKG_BY_ID.distcc.add; }
// per-process effective cpu% and yield
function procYield(p){
if(p.sys||!p.wkind) return 0;
const w=WORKERS[p.wkind]; if(!w) return 0;
if(p.state==='Z') return 0; // zombies produce nothing
if(p.state==='D' && !hasPkg('polymorph')) return 0; // uninterruptible sleep stalls (unless polymorph)
let base=w.yield * (p.nice||1) * yieldMult();
// clock turns "work units" into Hz
base *= (baseClock()/1000);
// overheat throttle
if(now()<G.overheatUntil && !hasPkg('thermal')) base*=0.35;
return base;
}
function procCpuPct(p){
if(p.sys) return p.cpu;
if(!p.wkind) return 0;
const w=WORKERS[p.wkind];
if(p.state==='Z') return 0;
if(p.state==='S') return w.load*4; // sleeping-ish lower display
if(p.state==='D') return w.load*7;
return w.load*11*(p.nice||1); // running
}
function totalYield(){
let s=0; for(const p of G.procs) s+=procYield(p); return s;
}
function curLoad(){
let s=0;
for(const p of G.procs){ if(p.state==='Z') continue; const w=WORKERS[p.wkind]; if(w) s+=w.load*(p.nice||1); }
return s;
}
function heatGen(){
let s=0;
for(const p of G.procs){ if(p.state==='Z') continue; const w=WORKERS[p.wkind]; if(w) s+=w.heat*(p.nice||1); }
return s;
}
/* ===========================================================================
GAME TICK
=========================================================================== */
function step(dt){
// accrue cycles
const y=totalYield();
const gain=y*dt;
if(gain>0){
G.cycles+=gain; G.totalCycles+=gain; G.lifetimeCycles+=gain;
if(G.totalCycles>G.runPeak) G.runPeak=G.totalCycles;
}
// load + heat dynamics
const targetLoad=curLoad();
G.load=targetLoad; // load is instantaneous (sum of process load)
const hg=heatGen();
const dissipate=(2.2 + lvl('cooling')*0.55) * (1 + (G.load<=0?0.6:0));
G.heat += (hg - dissipate)*dt;
const ambient=host().heatBase;
if(G.heat<ambient) G.heat=ambient;
const hc=heatCeiling();
if(G.heat>=hc){
G.heat=hc;
if(now()>=G.overheatUntil) {
G.overheatUntil=now()+4000;
toast('warn','🔥 Thermal throttle','Heat hit '+Math.round(hc)+'°C — yield ×0.35 until it cools. Buy <b>cooling</b> or kill a hot proc.');
logLine('c-warn','[thermal] CPU temperature critical ('+Math.round(hc)+'°C) — throttling clock');
}
}
// OOM killer when over load ceiling
const lc=loadCeiling();
if(G.load>lc){
G.oomTimer=(G.oomTimer||0)+dt;
if(G.oomTimer>=1.4){
G.oomTimer=0;
oomKill();
}
} else { G.oomTimer=0; }
// process state drift + zombie generation
driftProcesses(dt);
// events
handleEvents(dt);
// automation
if(hasPkg('cron')||hasPerk('sentinel')) autoReap();
if(hasPkg('autospawn')) autoSpawn();
}
let _driftAccum=0;
function driftProcesses(dt){
_driftAccum+=dt;
if(_driftAccum<0.4) return; // re-roll states ~2.5×/s
const elapsed=_driftAccum; _driftAccum=0;
for(const p of G.procs){
if(p.sys) continue;
if(p.state==='Z') continue; // stays a zombie until reaped
// small chance to zombify (a child died and wasn't wait()ed). Scanners are
// hardened (they re-fork on death) so the exploit pipeline stays reliable.
const zChance = (p.wkind==='scanner'?0.004:0.012) * elapsed;
const r=Math.random();
if(r < zChance){ p.state='Z'; p.zAt=now();
continue;
}
// otherwise wander R <-> S <-> D
const r2=Math.random();
if(r2<0.55) p.state='R';
else if(r2<0.86) p.state='S';
else p.state='D';
}
// background sys procs jitter cpu
for(const p of G.sysProcs){
p.cpu=clamp((p.cpu||0)+rand(-0.15,0.18),0,3.2);
if(Math.random()<0.2) p.state=Math.random()<0.7?'S':'R';
}
}
function autoReap(){
let reaped=0;
for(let i=G.procs.length-1;i>=0;i--){ if(G.procs[i].state==='Z'){ G.procs.splice(i,1); reaped++; } }
if(reaped>0 && G.sel!=null && !G.procs.some(p=>p.pid===G.sel)) G.sel=null;
}
function autoSpawn(){
if(G._autoSpawnGate && now()<G._autoSpawnGate) return;
G._autoSpawnGate=now()+1500;
if(G.procs.length>=maxProcs()) return;
// pick the best affordable worker that keeps load under ~90% ceiling
const order=['kernel','botnet','cracker','miner','worker','scanner'];
const headroom=loadCeiling()-curLoad();
for(const k of order){
const w=WORKERS[k];
if(privRank(G.priv)<privRank(w.minPriv)) continue;
if(G.cycles<workerCost(k)) continue;
if(w.load>headroom*0.92) continue;
spawnWorker(k,true);
return;
}
}
function oomKill(){
if(!G.procs.length){ return; }
// kernel kills the highest-memory of YOUR procs
let victim=null;
for(const p of G.procs){ if(p.state==='Z') continue; if(!victim||(WORKERS[p.wkind].mem>WORKERS[victim.wkind].mem)) victim=p; }
if(!victim) victim=G.procs[0];
const i=G.procs.indexOf(victim); if(i>=0) G.procs.splice(i,1);
if(G.sel===victim.pid) G.sel=null;
const nm=WORKERS[victim.wkind].cmd;
toast('bad','💀 OOM killer','Load '+Math.round(G.load)+'/'+Math.round(loadCeiling())+' — kernel killed PID '+victim.pid+' ('+nm+'). Shed load or buy <b>ulimit-tuning</b>.');
logLine('c-err','[ '+(((now()-G.started)/1000)|0)+'.0] Out of memory: Killed process '+victim.pid+' ('+nm+'), load '+Math.round(G.load)+' > '+Math.round(loadCeiling()));
}
function handleEvents(dt){
const t=now();
// vuln scanner -> exploit drops
const scanners=G.procs.filter(p=>p.wkind==='scanner' && p.state!=='Z').length;
if(scanners>0){
// first exploit comes quick to open the sudo path; later drops settle to a steadier cadence
if(G.nextScanDrop===0) G.nextScanDrop=t+(G.exploits===0&&G.priv==='user'?rand(7000,11000):rand(14000,26000))/scanners;
if(t>=G.nextScanDrop){
G.nextScanDrop=t+rand(16000,30000)/Math.max(1,scanners);
G.exploits++;
toast('info','🪲 Exploit found','vuln_scanner found CVE-'+(2020+Math.floor(rand(0,6)))+'-'+Math.floor(rand(1000,9999))+'. +1 exploit ('+G.exploits+'). Spend on <b>sudo su</b> / ssh.');
logLine('c-info','[scanner] vulnerability confirmed → +1 exploit (total '+G.exploits+')');
}
} else { G.nextScanDrop=0; }
// ambient zombie warning event (a heavier zombify burst) unless cron/sentinel
if(!hasPkg('cron') && !hasPerk('sentinel') && G.procs.some(p=>p.wkind)){
if(G.nextZomb===0) G.nextZomb=t+rand(30000,55000);
if(t>=G.nextZomb){
G.nextZomb=t+rand(45000,80000);
// zombify a random live worker
const live=G.procs.filter(p=>p.state!=='Z' && p.wkind);
if(live.length){ const v=pick(live); v.state='Z'; v.zAt=t;
toast('warn','🧟 Zombie process','PID '+v.pid+' ('+WORKERS[v.wkind].cmd+') went defunct &lt;Z&gt;. It leaks memory and earns nothing — <span class="cmdkw">kill '+v.pid+'</span> it, or install <b>cron</b>.');
logLine('c-warn','[kernel] PID '+v.pid+' became &lt;defunct&gt; (zombie) — reap it');
}
}
} else { G.nextZomb=0; }
// admin notices you -> kills a proc, unless rootkit
if(!hasPkg('rootkit') && G.procs.filter(p=>p.wkind).length>=4){
if(G.nextAdmin===0) G.nextAdmin=t+rand(70000,130000);
if(t>=G.nextAdmin){
G.nextAdmin=t+rand(90000,170000);
const live=G.procs.filter(p=>p.state!=='Z' && p.wkind);
if(live.length){
const v=pick(live); const i=G.procs.indexOf(v); G.procs.splice(i,1);
if(G.sel===v.pid) G.sel=null;
toast('bad','👮 admin@'+host().name,'The sysadmin spotted PID '+v.pid+' and ran <span class="cmdkw">kill -9</span>. Install <b>rootkit-lkm</b> to hide.');
logLine('c-err','admin: who is running '+WORKERS[v.wkind].cmd+'?? -- kill -9 '+v.pid);
}
}
} else { G.nextAdmin=0; }
}
/* ===========================================================================
COMMAND ACTIONS
=========================================================================== */
// Forking another worker of the same type costs more each time (×1.16 per live
// instance) — so flooding one generator has diminishing returns and the engine
// is about mix + clock + escalation, not infinite cheap forks.
function workerCost(kind){
const w=WORKERS[kind]; if(!w) return Infinity;
const owned=G.procs.filter(p=>p.wkind===kind).length;
return Math.ceil(w.cost * Math.pow(1.16, owned));
}
function spawnWorker(kind, silent){
const w=WORKERS[kind];
if(!w){ if(!silent) logLine('c-err','spawn: unknown worker type'); return false; }
if(privRank(G.priv)<privRank(w.minPriv)){
if(!silent) logLine('c-err','spawn: permission denied — '+w.cmd+' needs '+w.minPriv+' (run: sudo su)');
return false;
}
if(G.procs.length>=maxProcs()){
if(!silent) logLine('c-err','fork: retry: Resource temporarily unavailable — max procs reached ('+maxProcs()+'). apt install coreutils.');
return false;
}
const cost=workerCost(kind);
if(G.cycles<cost){
if(!silent) logLine('c-err','spawn: insufficient cycles — '+w.cmd+' costs '+fmtCyc(cost)+' (have '+fmtCyc(G.cycles)+')');
return false;
}
G.cycles-=cost;
const p={pid:G.pidSeq++, sys:false, wkind:kind, cmd:w.cmd, user:G.priv,
mem:w.mem, state:'R', nice:1, bornAt:now()};
G.procs.push(p);
if(!silent){
logLine('c-ok','[ pid '+p.pid+' ] forked '+w.cmd+' ('+fmtCyc(w.yield*(baseClock()/1000)*yieldMult())+'/s · +'+w.load.toFixed(1)+' load · +'+w.heat.toFixed(2)+'°/s)');
}
return p;
}
function killPid(pid, silent){
const i=G.procs.findIndex(p=>p.pid===pid);
if(i<0){ if(!silent) logLine('c-err','kill: ('+pid+') - No such process'); return false; }
const p=G.procs[i];
G.procs.splice(i,1);
if(G.sel===pid) G.sel=null;
if(!silent) logLine('c-sys','kill '+pid+' — terminated '+p.cmd+(p.state==='Z'?' (reaped zombie)':''));
return true;
}
function reniceePid(pid){
const p=G.procs.find(p=>p.pid===pid);
if(!p){ logLine('c-err','renice: failed to set priority of '+pid+': No such process'); return false; }
if(p.state==='Z'){ logLine('c-err','renice: PID '+pid+' is a zombie — kill it instead'); return false; }
// each renice raises priority (yield & load & heat) up to a cap
const cap=4;
if((p.nice||1)>=cap){ logLine('c-warn','renice: PID '+pid+' already at max priority (nice -20)'); return false; }
// cost scales with current nice
const cost=Math.ceil(WORKERS[p.wkind].cost * 2 * (p.nice||1));
if(G.cycles<cost){ logLine('c-err','renice: needs '+fmtCyc(cost)+' cycles to bump PID '+pid+' (have '+fmtCyc(G.cycles)+')'); return false; }
G.cycles-=cost;
p.nice=(p.nice||1)+0.6;
const niceVal=Math.round(-20*((p.nice-1)/(cap-1)));
logLine('c-ok','renice -n '+niceVal+' -p '+pid+' — '+p.cmd+' now ×'+p.nice.toFixed(1)+' yield (also +load, +heat)');
return true;
}
function tryEscalate(){
const next=PRIV[privRank(G.priv)+1];
if(!next){ logLine('c-warn','sudo: you are already root@'+host().name+'. Nothing higher here — ssh to a bigger host.'); return; }
const req=ESCALATE[next];
if(G.cycles<req.cyc){ logLine('c-err','sudo: authentication failure — need '+fmtCyc(req.cyc)+' cycles to brute-force '+next+' (have '+fmtCyc(G.cycles)+')'); return; }
if(G.exploits<req.exp){ logLine('c-err','sudo: need '+req.exp+' exploit'+(req.exp>1?'s':'')+' to escalate to '+next+' — run a vuln_scanner (have '+G.exploits+')'); return; }
G.cycles-=req.cyc; G.exploits-=req.exp;
G.priv=next;
for(const p of G.procs) p.user=next;
logLine('c-hdr','═══ privilege escalated: you are now '+next+'@'+host().name+' ═══');
logLine('c-ok', next==='root' ? 'uid=0(root) gid=0(root) — you own this box. Type ssh to pivot to a bigger host.'
: 'uid='+privRank(next)+'('+next+') — new workers & packages unlocked. apt update.');
toast('prestige','▸ '+next.toUpperCase(),'Escalated to '+next+'@'+host().name+'. '+(next==='root'?'You own the box — <b>ssh</b> to prestige.':'New process types & packages available.'));
renderAll();
}
/* ---- prestige: ssh to next host ------------------------------------------- */
function keysFor(peak){
// Need ~100 MHz peak this run before a pivot is worth anything, so the first
// ssh feels earned (you spend a few minutes at root scaling up). Keys then
// grow sub-linearly with peak so deeper hosts pay more but never trivially.
if(peak<1e8) return 0;
return Math.floor(Math.pow(peak/1e8, 0.40));
}
function canPrestige(){ return G.priv==='root' && G.exploits>=1 && keysFor(G.runPeak)>=1; }
function doPrestige(){
if(!canPrestige()) return;
const gained=keysFor(G.runPeak);
G.keys+=gained; G.prestiges++; G.exploits-=1;
G.hostIdx++;
const h=host();
// reset run state, keep perks/keys/hostIdx. Seed a little starting balance so
// the first fork on the new host always succeeds (no zero-cycle deadlock).
G.cycles=40; G.totalCycles=0; G.runPeak=0; G.procs=[]; G.sel=null;
G.sysProcs=freshHostProcs();
G.priv = hasPerk('genesis') ? 'user' : 'guest';
if(hasPerk('genesis')) G.exploits+=1;
G.heat=h.heatBase; G.load=0; G.overheatUntil=0; G.oomTimer=0;
G.nextScanDrop=0;G.nextZomb=0;G.nextAdmin=0;G.adminUntil=0;
// preboot perk: start with workers
const pre=plvl('preboot');
for(let i=0;i<pre;i++) spawnWorker('worker',true);
closeModal();
logLine('c-hdr','╔══════════════════════════════════════════════════╗');
logLine('c-hdr',' ssh '+(G.priv==='user'?'user':'guest')+'@'+h.ip+' ('+h.name+')');
logLine('c-info',' '+h.cores+' cores · clock ×'+fmtInt(h.clockMul)+' · +'+gained+' root key'+(gained>1?'s':''));
logLine('c-hdr','╚══════════════════════════════════════════════════╝');
logLine('c-sys','Last login: '+new Date().toString().slice(0,24)+' from 10.0.0.'+Math.floor(rand(2,254)));
toast('prestige','🛰️ Pivoted to '+h.name,'+'+gained+' root keys (total '+G.keys+'). Bigger box: '+h.cores+' cores, ×'+fmtInt(h.clockMul)+' clock. Spend keys in the SSH panel.');
renderAll(); save();
}
/* ===========================================================================
COMMAND PARSER
=========================================================================== */
const COMMANDS = [
{name:'spawn', hint:'spawn <worker|miner|cracker|scanner|botnet|kernel>', desc:'fork a worker process (generator)'},
{name:'fork', hint:'fork <type>', desc:'alias of spawn'},
{name:'renice', hint:'renice <pid>', desc:'boost a process priority (more yield, more load/heat)'},
{name:'kill', hint:'kill <pid|all|zombies>', desc:'terminate a process / reap zombies'},
{name:'apt', hint:'apt install <pkg>', desc:'open the package manager (upgrades)'},
{name:'sudo', hint:'sudo su', desc:'escalate privilege (user ▸ sudo ▸ root)'},
{name:'ssh', hint:'ssh next', desc:'pivot to a bigger host (prestige) — needs root'},
{name:'htop', hint:'htop', desc:'(already on screen) print process summary'},
{name:'ps', hint:'ps', desc:'list your processes'},
{name:'top', hint:'top', desc:'system load summary'},
{name:'free', hint:'free -m', desc:'memory / load / heat readout'},
{name:'ls', hint:'ls', desc:'list the (flavor) filesystem'},
{name:'whoami', hint:'whoami', desc:'print current privilege'},
{name:'netmap', hint:'netmap', desc:'show the network map of owned hosts'},
{name:'clear', hint:'clear', desc:'clear the terminal scrollback'},
{name:'help', hint:'help', desc:'show this command list'},
{name:'man', hint:'man <cmd>', desc:'describe a command'},
];
const CMD_NAMES = COMMANDS.map(c=>c.name);
function runCommand(raw){
const line=raw.trim();
if(line==='') { logLine('c-prompt', ps1Text()); return; }
// echo the command at the prompt
logLine('c-cmd', ps1Text()+' '+esc(line), true);
G.history.push(line); if(G.history.length>80) G.history.shift();
histIdx=G.history.length;
const toks=line.split(/\s+/);
let cmd=toks[0].toLowerCase();
const args=toks.slice(1);
switch(cmd){
case 'spawn': case 'fork': {
let k=args[0];
if(!k){ logLine('c-err','usage: spawn <worker|miner|cracker|scanner|botnet|kernel>'); break; }
k=WORKER_ALIASES[k.toLowerCase()];
if(!k){ logLine('c-err','spawn: unknown worker "'+esc(args[0])+'". Try: worker miner cracker scanner botnet kernel'); break; }
let n=1; const m=args[1]&&/^\d+$/.test(args[1])?parseInt(args[1]):1;
const count=clamp(m||1,1,50);
let made=0; for(let i=0;i<count;i++){ if(spawnWorker(k,false)) made++; else break; }
if(count>1&&made>0) logLine('c-sys','forked '+made+'× '+WORKERS[k].cmd);
break;
}
case 'renice': {
const pid=parsePid(args[0]);
if(pid==null){ logLine('c-err','usage: renice <pid> (see the PID column above)'); break; }
reniceePid(pid); break;
}
case 'kill': {
// accept an optional signal flag (e.g. `kill -9 1337`) before the target
const kargs=args.filter(x=>!/^-/.test(x));
const a=(kargs[0]||'').toLowerCase();
if(!a){ logLine('c-err','usage: kill <pid> | kill all | kill zombies'); break; }
if(a==='all'){ const n=G.procs.length; G.procs=[]; G.sel=null; logLine('c-sys','killed all '+n+' process'+(n===1?'':'es')); break; }
if(a==='zombies'||a==='z'){ const before=G.procs.length; G.procs=G.procs.filter(p=>p.state!=='Z'); logLine('c-sys','reaped '+(before-G.procs.length)+' zombie(s)'); break; }
const pid=parsePid(kargs[0]);
if(pid==null){ logLine('c-err','kill: '+esc(kargs[0])+': arguments must be process or job IDs'); break; }
killPid(pid,false); break;
}
case 'apt': case 'apt-get': case 'pip': {
const sub=(args[0]||'').toLowerCase();
if(sub==='install'&&args[1]){
const pkg=PACKAGES.find(p=>p.name===args[1]||p.id===args[1]);
if(pkg){ buyPackage(pkg.id,true); break; }
logLine('c-err','E: Unable to locate package '+esc(args[1])); break;
}
if(sub==='update'){ logLine('c-sys','Hit:1 http://'+host().ip+' '+host().name+' InRelease'); logLine('c-sys','Reading package lists... Done'); openApt(); break; }
openApt(); break;
}
case 'sudo': {
if((args[0]||'').toLowerCase()==='su'||args.length===0){ tryEscalate(); }
else { logLine('c-sys','['+G.priv+'] running: '+esc(args.join(' '))); logLine('c-err','sudo: a password is required — (hint: just run `sudo su` to escalate)'); }
break;
}
case 'su': { tryEscalate(); break; }
case 'ssh': { if(G.priv!=='root'){ logLine('c-err','ssh: permission denied — own the box first (escalate to root via sudo su).'); } else openPrestige(); break; }
case 'htop': case 'top': { printTop(); break; }
case 'ps': { printPs(); break; }
case 'free': { printFree(); break; }
case 'ls': case 'dir': { printLs(); break; }
case 'whoami': { logLine('c-ok', G.priv + (G.priv==='root'?' # uid=0':'')); break; }
case 'pwd': { logLine('c-sys','/'+(G.priv==='root'?'root':'home/'+G.priv)); break; }
case 'netmap': case 'nmap': { openNetmap(); break; }
case 'clear': case 'cls': { G.log=[]; renderLog(); break; }
case 'help': case '?': { printHelp(); break; }
case 'man': { printMan(args[0]); break; }
case 'uname': { logLine('c-sys','Linux '+host().name+' 6.6.0-uplink #'+G.prestiges+' SMP x86_64 GNU/Linux'); break; }
case 'cat': { logLine('c-sys', args[0]?('cat: '+esc(args[0])+': it is all zeros and longing'):'usage: cat <file>'); break; }
case 'exit': case 'logout': { logLine('c-warn','There are stopped jobs. (you can\'t exit the simulation — you ARE the process)'); break; }
case 'sl': { logLine('c-info','🚂 woo woo (you meant `ls`)'); break; }
case 'rm': { if(args.includes('-rf')&&args.includes('/')) logLine('c-err','rm: it is too late for that. you live here now.'); else logLine('c-sys','rm: nothing important to delete.'); break; }
default: logLine('c-err', esc(cmd)+': command not found. type `help`.');
}
renderAll();
}
function parsePid(s){ if(s==null) return null; const n=parseInt(s,10); return (isFinite(n))?n:null; }
/* ---- printed command outputs ---------------------------------------------- */
function printHelp(){
logLine('c-hdr','UPLINK shell — available commands:');
for(const c of COMMANDS){
logLine('c-sys',' '+c.name.padEnd(8)+' '+pad(c.hint,34)+' '+c.desc);
}
logLine('c-dim',' TAB autocompletes · ↑/↓ history · or click a chip / the apt link below.');
}
function printMan(name){
if(!name){ logLine('c-err','What manual page do you want? (man <cmd>)'); return; }
const c=COMMANDS.find(c=>c.name===name.toLowerCase());
if(!c){ logLine('c-err','No manual entry for '+esc(name)); return; }
logLine('c-hdr',name.toUpperCase()+'(1)');
logLine('c-sys',' SYNOPSIS '+c.hint);
logLine('c-sys',' '+c.desc);
}
function printPs(){
if(!G.procs.length){ logLine('c-sys','(no worker processes — run `spawn worker`)'); return; }
logLine('c-hdr',' PID USER S '+pad('%CPU',6)+pad('YIELD',12)+' COMMAND');
for(const p of G.procs){
logLine('c-sys',' '+pad(p.pid,4)+' '+pad(p.user,8)+' '+p.state+' '+pad(procCpuPct(p).toFixed(0),5)+pad(fmtCyc(procYield(p)),12)+' '+WORKERS[p.wkind].cmd+(p.nice>1?(' (×'+p.nice.toFixed(1)+')'):''));
}
}
function printTop(){
logLine('c-hdr','top - load average: '+(G.load/Math.max(1,cores())).toFixed(2)+' · '+Math.round(G.load)+'/'+Math.round(loadCeiling())+' load · '+Math.round(G.heat)+'/'+Math.round(heatCeiling())+'°C');
logLine('c-sys','Tasks: '+(G.procs.length+G.sysProcs.length)+' total, '+G.procs.filter(p=>p.state==='R').length+' running, '+G.procs.filter(p=>p.state==='Z').length+' zombie');
logLine('c-sys','Cycles: '+fmtCyc(totalYield())+'/s · '+G.procs.length+'/'+maxProcs()+' procs · '+cores()+' cores');
}
function printFree(){
const used=G.procs.reduce((s,p)=>s+(WORKERS[p.wkind]?WORKERS[p.wkind].mem:0),0)+G.sysProcs.reduce((s,p)=>s+p.mem,0);
const totalMem=cores()*512;
logLine('c-hdr',' total used free');
logLine('c-sys','Mem: '+pad(totalMem,9)+'M '+pad(used,8)+'M '+pad(Math.max(0,totalMem-used),8)+'M');
logLine('c-sys','Load: '+Math.round(G.load)+' / '+Math.round(loadCeiling())+' Heat: '+Math.round(G.heat)+' / '+Math.round(heatCeiling())+'°C');
}
function printLs(){
const files=['bin','etc','home','proc','root','tmp','var',
'exploit.sh','.bash_history','wallet.dat','passwd.bak','core.'+Math.floor(rand(1000,9999))];
logLine('c-info', files.map(f=>f.includes('.')?f:('\x1b'+f)).map(f=>f.replace('\x1b','')).join(' '));
logLine('c-dim',' (it is just flavor — the real game is up in htop ↑)');
}
/* ===========================================================================
APT / PACKAGE MANAGER (upgrades)
=========================================================================== */
function pkgCost(p){
if(p.kind==='unlock') return p.base;
return Math.ceil(p.base * Math.pow(p.growth, lvl(p.id)));
}
function pkgMaxed(p){ if(p.kind==='unlock') return hasPkg(p.id); return p.max && lvl(p.id)>=p.max; }
function pkgReqMet(p){
if(privRank(G.priv)<privRank(p.minPriv)) return false;
if(p.req && !hasPkg(p.req)) return false;
return true;
}
function pkgEffect(p){
const L=lvl(p.id);
switch(p.kind){
case 'pids': return 'max procs '+maxProcs()+' ▸ '+(maxProcs()+p.add);
case 'clock': return 'clock ×'+Math.pow(p.mult,L).toFixed(2)+' ▸ ×'+Math.pow(p.mult,L+1).toFixed(2);
case 'eff': return 'yield ×'+Math.pow(p.mult,L).toFixed(2)+' ▸ ×'+Math.pow(p.mult,L+1).toFixed(2);
case 'cool': return 'heat ceil '+Math.round(heatCeiling())+'° ▸ '+Math.round(heatCeiling()+p.add)+'°';
case 'loadcap':return 'load ceil '+Math.round(loadCeiling())+' ▸ '+Math.round(loadCeiling()*p.mult);
case 'distcc': return 'passive ×'+passiveMult().toFixed(2)+' ▸ ×'+(passiveMult()+p.add).toFixed(2);
case 'unlock': return p.desc;
}
return '';
}
function buyPackage(id, fromCmd){
const p=PKG_BY_ID[id]; if(!p) return false;
if(!pkgReqMet(p)){ if(fromCmd) logLine('c-err','apt: '+p.name+' requires '+p.minPriv+(p.req?(' + '+PKG_BY_ID[p.req].name):'')); return false; }
if(pkgMaxed(p)){ if(fromCmd) logLine('c-warn','apt: '+p.name+' is already at max.'); return false; }
const cost=pkgCost(p);
if(G.cycles<cost){ if(fromCmd) logLine('c-err','apt: not enough cycles for '+p.name+' ('+fmtCyc(cost)+' needed, '+fmtCyc(G.cycles)+' have)'); else toast('bad','Not enough cycles',p.name+' costs '+fmtCyc(cost)+'.'); return false; }
G.cycles-=cost;
G.upgrades[id]=(G.upgrades[id]||0)+1;
if(fromCmd){
logLine('c-ok','Get:1 '+p.name+' ['+fmtCyc(cost)+']');
logLine('c-sys','Unpacking '+p.name+'... Setting up '+p.name+' ('+lvl(id)+')... done');
if(p.kind==='unlock') logLine('c-info',' → '+p.desc);
} else {
toast('','apt: installed '+p.name, (p.kind==='unlock'?p.desc:pkgEffect(p)));
}
renderAll();
if(modalKind==='apt') renderAptModal();
return true;
}
/* ===========================================================================
TERMINAL LOG
=========================================================================== */
function esc(s){ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
function pad(s,n){ s=String(s); while(s.length<n) s+=' '; return s; }
function logLine(cls, html, isRaw){
// html already escaped by caller for c-cmd via esc(); others are trusted templates
G.log.push({c:cls, h:html});
while(G.log.length>LOG_CAP) G.log.shift();
if(_booted) appendLogLine(cls,html);
}
let _logEl=null;
function appendLogLine(cls,html){
if(!_logEl) _logEl=document.getElementById('log');
const div=document.createElement('div');
div.className='ln '+cls;
div.innerHTML=html;
_logEl.appendChild(div);
while(_logEl.children.length>LOG_CAP) _logEl.firstChild.remove();
_logEl.scrollTop=_logEl.scrollHeight;
}
function renderLog(){
const el=document.getElementById('log'); _logEl=el;
el.innerHTML='';
for(const l of G.log){ const d=document.createElement('div'); d.className='ln '+l.c; d.innerHTML=l.h; el.appendChild(d); }
el.scrollTop=el.scrollHeight;
}
function ps1Text(){
const u = G.priv==='root'?'root':G.priv;
const sym = G.priv==='root'?'#':'$';
return '<span class="c-acc">'+u+'</span><span class="c-prompt">@'+host().name.split(' ')[0].replace(/[^a-z0-9.\-]/gi,'')+':~'+sym+'</span>';
}
/* ===========================================================================
RENDERING
=========================================================================== */
let _booted=false;
function renderAll(){ renderStrip(); renderHtop(); renderChips(); renderStatus(); renderPs1(); }
function renderPs1(){
const u=G.priv==='root'?'root':G.priv;
const sym=G.priv==='root'?'#':'$';
const h=host().name.split(' ')[0].replace(/[^a-z0-9.\-]/gi,'');
document.getElementById('ps1').innerHTML='<span class="u">'+u+'</span>@<span class="h">'+h+'</span>:~<span class="sym">'+sym+'</span>';
}
function renderStrip(){
document.getElementById('s-cycles').textContent=fmtCyc(G.cycles);
document.getElementById('s-clock').textContent=fmtCyc(baseClock())+' · '+cores()+' cores';
document.getElementById('s-exploit').textContent=fmtInt(G.exploits);
document.getElementById('s-host').textContent=host().name;
document.getElementById('s-keys').textContent=fmtInt(G.keys);
// privilege track
const pt=document.getElementById('privtrack');
const rank=privRank(G.priv);
let h='';
PRIV.forEach((p,i)=>{
if(i>0) h+='<span class="pseg sep">▸</span>';
h+='<span class="pseg '+(i<=rank?'on':'')+'">'+p+'</span>';
});
pt.innerHTML=h;
// load gauge
const lc=loadCeiling(), lp=clamp(G.load/lc*100,0,140);
const lf=document.getElementById('g-load');
lf.style.width=Math.min(100,lp)+'%';
lf.style.background = lp>=100 ? 'linear-gradient(90deg,#ff4d4d,#ff7777)' : (lp>=80?'linear-gradient(90deg,#caa12f,#ffc234)':'linear-gradient(90deg,#1f9c44,#33ff66)');
document.getElementById('g-load-tx').textContent=Math.round(G.load)+'/'+Math.round(lc);
// heat gauge
const hc=heatCeiling(), hp=clamp((G.heat-host().heatBase)/(hc-host().heatBase)*100,0,100);
document.getElementById('g-heat').style.width=hp+'%';
document.getElementById('g-heat-tx').textContent=Math.round(G.heat)+'°C';
}
const PH_COLS=[
{k:'PID',w:'58px',num:true},
{k:'USER',w:'66px'},
{k:'PRI',w:'42px',num:true},
{k:'%CPU',w:'56px',num:true},
{k:'MEM',w:'62px',num:true},
{k:'S',w:'26px'},
{k:'YIELD',w:'92px',num:true},
{k:'COMMAND',w:'auto'},
{k:'',w:'74px'},
];
function renderHtopHead(){
const tr=document.getElementById('ph-head');
tr.innerHTML=PH_COLS.map(c=>'<th class="'+(c.num?'num':'')+'" style="width:'+c.w+'">'+c.k+'</th>').join('');
}
function coreSeg(pct){
// build htop-style colored core bar string
const lo=Math.min(pct,50), md=clamp(pct-50,0,30), hi=clamp(pct-80,0,20);
return '<i class="seg-lo" style="width:'+lo+'%"></i><i class="seg-md" style="left:'+lo+'%;width:'+md+'%"></i><i class="seg-hi" style="left:'+(lo+md)+'%;width:'+hi+'%"></i>';
}
function renderHtop(){
// CPU per-core bars: distribute total cpu% over cores
const cb=document.getElementById('cpubars');
const nc=Math.min(cores(),8); // show up to 8 bars
const totalPct=G.procs.reduce((s,p)=>s+procCpuPct(p),0)+G.sysProcs.reduce((s,p)=>s+(p.cpu||0),0);
const perCore=clamp(totalPct/Math.max(1,cores()),0,100);
let html='';
for(let i=0;i<nc;i++){
const jitter=clamp(perCore + Math.sin((now()/400)+(i*1.7))*6, 0, 100);
html+='<div class="corebar"><span class="cl">'+i+'</span><span class="cg">'+coreSeg(jitter)+'</span><span class="cpct">'+Math.round(jitter)+'%</span></div>';
}
if(cores()>8) html+='<div class="corebar"><span class="cl">…</span><span class="cg"></span><span class="cpct">+'+(cores()-8)+'</span></div>';
cb.innerHTML=html;
// mem / load row
const used=G.procs.reduce((s,p)=>s+(WORKERS[p.wkind]?WORKERS[p.wkind].mem:0),0)+G.sysProcs.reduce((s,p)=>s+p.mem,0);
const totalMem=cores()*512;
document.getElementById('memrow').innerHTML=
'Mem <b>'+used+'M</b>/'+totalMem+'M &nbsp; Tasks <b>'+(G.procs.length)+'</b>/'+maxProcs()+
' &nbsp; Load <b>'+Math.round(G.load)+'</b>/'+Math.round(loadCeiling())+
' &nbsp; Zombie <b style="color:'+(G.procs.some(p=>p.state==='Z')?'var(--red)':'var(--grn-bright)')+'">'+G.procs.filter(p=>p.state==='Z').length+'</b>'+
' &nbsp; Σyield <b>'+fmtCyc(totalYield())+'/s</b>';
// process rows: yours first (sorted by cpu desc), then sys
const body=document.getElementById('ph-body');
const mine=G.procs.slice().sort((a,b)=>procCpuPct(b)-procCpuPct(a));
const rows=[];
for(const p of mine){
const niceVal=p.nice>1?Math.round(-20*((p.nice-1)/3)):0;
const stateCls='st-'+p.state;
const rowCls=(p.state==='Z'?'zomb ':'')+(p.state==='D'?'dstate ':'')+(G.sel===p.pid?'sel ':'');
rows.push('<tr class="'+rowCls+'" data-pid="'+p.pid+'">'
+'<td class="num">'+p.pid+'</td>'
+'<td>'+p.user+'</td>'
+'<td class="num">'+(niceVal||'20')+'</td>'
+'<td class="num pcpu">'+procCpuPct(p).toFixed(0)+'</td>'
+'<td class="num">'+(WORKERS[p.wkind].mem)+'M</td>'
+'<td class="'+stateCls+'">'+p.state+'</td>'
+'<td class="num">'+(p.state==='Z'?'<span class="c-dim">—</span>':fmtCyc(procYield(p)))+'</td>'
+'<td class="pname">'+WORKERS[p.wkind].cmd+(p.nice>1?' <span class="ptag">×'+p.nice.toFixed(1)+'</span>':'')+'</td>'
+'<td class="num"><span class="rbtn" data-renice="'+p.pid+'" title="renice (boost)">[ren]</span> <span class="kbtn" data-kill="'+p.pid+'" title="kill -9">[x]</span></td>'
+'</tr>');
}
// a couple of system procs for texture
for(const p of G.sysProcs.slice(0,5)){
rows.push('<tr class="sysrow" style="opacity:.5">'
+'<td class="num c-dim">'+p.pid+'</td>'
+'<td class="c-dim">'+p.user+'</td>'
+'<td class="num c-dim">20</td>'
+'<td class="num c-dim">'+(p.cpu||0).toFixed(0)+'</td>'
+'<td class="num c-dim">'+p.mem+'M</td>'
+'<td class="st-S">'+(p.state||'S')+'</td>'
+'<td class="num c-dim">—</td>'
+'<td class="c-dim">'+esc(p.cmd)+'</td>'
+'<td></td></tr>');
}
body.innerHTML=rows.join('');
document.getElementById('ptable-empty').style.display = mine.length? 'none':'block';
document.getElementById('ptable-empty').innerHTML = mine.length? '' :
'no worker processes. type <span class="cmdkw">spawn worker</span> below (or click the chip) to fork PID '+G.pidSeq+'.';
}
function renderChips(){
const chips=[];
// contextual quick commands
chips.push({t:'spawn worker', cmd:'spawn worker', cta:false, dis:false});
if(privRank(G.priv)>=privRank('user')){
chips.push({t:'spawn miner', cmd:'spawn miner'});
chips.push({t:'spawn scanner', cmd:'spawn scanner'});
if(G.cycles>=workerCost('cracker')) chips.push({t:'spawn cracker', cmd:'spawn cracker'});
}
if(privRank(G.priv)>=privRank('sudo') && G.cycles>=workerCost('botnet')) chips.push({t:'spawn botnet', cmd:'spawn botnet'});
if(privRank(G.priv)>=privRank('root') && G.cycles>=workerCost('kernel')) chips.push({t:'spawn kernel', cmd:'spawn kernel'});
if(G.procs.some(p=>p.state==='Z')) chips.push({t:'⚠ kill zombies', cmd:'kill zombies', cta:true});
// escalate chip
const nxt=PRIV[privRank(G.priv)+1];
if(nxt){ const r=ESCALATE[nxt]; const can=G.cycles>=r.cyc&&G.exploits>=r.exp;
chips.push({t:'sudo su ▸ '+nxt, cmd:'sudo su', cta:can, dis:false}); }
chips.push({t:'apt install', cmd:'__apt'});
if(G.priv==='root') chips.push({t:'ssh ▸ next host', cmd:'__ssh', cta:canPrestige()});
chips.push({t:'help', cmd:'help'});
const el=document.getElementById('chips');
el.innerHTML=chips.map(c=>'<span class="chip'+(c.cta?' cta':'')+(c.dis?' dis':'')+'" data-chip="'+esc(c.cmd)+'">'+c.t+'</span>').join('');
}
function renderStatus(){
document.getElementById('sb-procs').textContent=G.procs.length;
document.getElementById('sb-uptime').textContent=fmtTime((now()-G.started)/1000);
document.getElementById('sb-ssh').style.opacity = canPrestige()?'1':'0.6';
// titlebar / clock
const d=new Date();
document.getElementById('clk').textContent=d.toTimeString().slice(0,8);
document.getElementById('wintitle').textContent = (G.priv==='root'?'root':G.priv)+'@'+host().name.split(' ')[0].replace(/[^a-z0-9.\-]/gi,'')+': ~ — uplink';
}
/* ===========================================================================
MODALS — apt, prestige(ssh), netmap, help-about
=========================================================================== */
let modalKind=null, aptTab='core';
function openModal(){ document.getElementById('mback').classList.add('show'); }
function closeModal(){ document.getElementById('mback').classList.remove('show'); modalKind=null; focusCmd(); }
function openApt(){ modalKind='apt'; openModal(); renderAptModal(); }
function renderAptModal(){
const tabs=[
{id:'core', label:'core / hardware'},
{id:'auto', label:'automation / stealth'},
];
const m=document.getElementById('modal');
let body='';
body+='<div class="mtabs">'+tabs.map(t=>'<div class="mtab '+(aptTab===t.id?'active':'')+'" data-apttab="'+t.id+'">'+t.label+'</div>').join('')+'</div>';
body+='<div class="aptline">root@'+host().name+':~# apt install &lt;package&gt; · available cycles: <b id="apt-cyc">'+fmtCyc(G.cycles)+'</b></div>';
const list=PACKAGES.filter(p=>p.tab===aptTab);
for(const p of list){
const reqMet=pkgReqMet(p);
const maxed=pkgMaxed(p);
const cost=pkgCost(p);
const can=reqMet&&!maxed&&G.cycles>=cost;
const cls='pkg'+(maxed?' maxed':'')+(!reqMet?' locked':'');
let reqNote='';
if(!reqMet){
const bits=[];
if(privRank(G.priv)<privRank(p.minPriv)) bits.push('needs '+p.minPriv);
if(p.req&&!hasPkg(p.req)) bits.push('needs '+PKG_BY_ID[p.req].name);
reqNote='<div class="reqnote">🔒 '+bits.join(' · ')+'</div>';
}
const lvLabel = (p.kind==='unlock')? (hasPkg(p.id)?'installed':'') : ('v'+lvl(p.id)+(p.max?('/'+p.max):''));
body+='<div class="pkg '+cls+'" id="pkg-'+p.id+'">'
+'<div class="pmain">'
+'<div class="pname">'+p.name+(lvLabel?'<span class="lv">'+lvLabel+'</span>':'')+'</div>'
+'<div class="pdesc">'+p.desc+'</div>'
+(!maxed&&reqMet?'<div class="peff">'+pkgEffect(p)+'</div>':'')
+reqNote
+'</div>'
+'<div class="pbuy">'
+(maxed? '<span class="pcost ok">'+(p.kind==='unlock'?'✓ installed':'MAX')+'</span>'
: '<span class="pcost '+(can?'ok':'no')+'" id="pcost-'+p.id+'">'+fmtCyc(cost)+'</span>'
+'<button class="btn '+(can?'primary':'')+'" data-buy="'+p.id+'" '+(can?'':'disabled')+'>install</button>')
+'</div>'
+'</div>';
}
m.innerHTML=
'<div class="mtitle"><span>📦 Synaptic Package Manager — apt</span><span class="x" data-close>✕</span></div>'
+'<div class="mbody">'+body+'</div>'
+'<div class="mfoot"><span class="c-dim" style="margin-right:auto">tip: you can also type <span class="cmdkw">apt install '+(list[0]?list[0].name:'overclock')+'</span></span><button class="btn" data-close>close</button></div>';
}
function updateAptAfford(){
if(modalKind!=='apt') return;
const cy=document.getElementById('apt-cyc'); if(cy) cy.textContent=fmtCyc(G.cycles);
for(const p of PACKAGES.filter(p=>p.tab===aptTab)){
if(pkgMaxed(p)||!pkgReqMet(p)) continue;
const cost=pkgCost(p); const can=G.cycles>=cost;
const c=document.getElementById('pcost-'+p.id);
if(c) c.className='pcost '+(can?'ok':'no');
const card=document.getElementById('pkg-'+p.id);
if(card){ const b=card.querySelector('button[data-buy]'); if(b){ b.toggleAttribute('disabled',!can); b.classList.toggle('primary',can); } }
}
}
function openPrestige(){ modalKind='ssh'; openModal(); renderPrestigeModal(); }
function renderPrestigeModal(){
const m=document.getElementById('modal');
const gained=keysFor(G.runPeak);
const nextH=hostAt(G.hostIdx+1);
const can=canPrestige();
let perks='';
for(const p of PERKS){
if(p.req&&!hasPerk(p.req)){ continue; }
const maxed=p.once?hasPerk(p.id):(p.max&&plvl(p.id)>=p.max);
const cost=p.once?p.base:Math.ceil(p.base*Math.pow(p.growth,plvl(p.id)));
const canBuy=!maxed&&G.keys>=cost;
const lvLabel=p.once?(hasPerk(p.id)?'owned':''):('lv'+plvl(p.id)+(p.max?('/'+p.max):''));
perks+='<div class="perkrow">'
+'<div class="pkmain"><div class="pkn">'+p.name+(lvLabel?' <span class="c-dim">'+lvLabel+'</span>':'')+'</div>'
+'<div class="pkd">'+p.desc+'</div></div>'
+(maxed?'<span class="pcost ok">'+(p.once?'✓':'MAX')+'</span>'
:'<button class="btn '+(canBuy?'primary':'')+'" data-perk="'+p.id+'" '+(canBuy?'':'disabled')+'>'+cost+' 🔑</button>')
+'</div>';
}
m.innerHTML=
'<div class="mtitle"><span>🛰️ ssh — pivot to the next host</span><span class="x" data-close>✕</span></div>'
+'<div class="mbody"><div class="pgrid">'
+'<h2>Pivot off '+host().name+'</h2>'
+'<div class="c-dim" style="margin-bottom:8px">Own root, burn 1 exploit, and ssh into a bigger box. This resets your processes, cycles & packages — but root keys & perks are permanent and compound forever.</div>'
+'<div style="display:flex;gap:30px;align-items:flex-end;margin:8px 0 4px">'
+'<div><div class="c-dim" style="font-size:10px;text-transform:uppercase">root keys on pivot</div><div class="big">+'+gained+' 🔑</div></div>'
+'<div style="flex:1"><div class="c-dim" style="font-size:11px">next host → <span class="c-acc">'+nextH.name+'</span> ('+nextH.ip+')</div>'
+'<div class="c-sys" style="font-size:11px">'+nextH.cores+' cores · clock ×'+fmtInt(nextH.clockMul)+' · heat base '+nextH.heatBase+'°C</div></div>'
+'</div>'
+(can? '<div class="c-sys" style="margin:6px 0">requirements met: <span class="c-ok">root ✓</span> · <span class="c-ok">'+G.exploits+' exploit ✓</span> · <span class="c-ok">peak '+fmtCyc(G.runPeak)+' ✓</span></div>'
: '<div class="warnbox">Not ready to pivot. Need: '
+(G.priv==='root'?'<span class="c-ok">root ✓</span>':'<span class="c-err">root ✗ (sudo su)</span>')+' · '
+(G.exploits>=1?'<span class="c-ok">1 exploit ✓</span>':'<span class="c-err">1 exploit ✗ (run vuln_scanner)</span>')+' · '
+(gained>=1?'<span class="c-ok">≥1 key ✓</span>':'<span class="c-err">peak too low ✗ (mine to ≥'+fmtCyc(1e8)+' this run)</span>')
+'</div>')
+'<div style="margin-top:14px"><div class="c-hdr" style="margin-bottom:4px">Permanent perks — spend root keys ('+G.keys+' 🔑 available)</div>'
+perks+'</div>'
+'</div></div>'
+'<div class="mfoot">'
+'<button class="btn" data-act="netmap-from-ssh" style="margin-right:auto">view network map</button>'
+'<button class="btn" data-close>cancel</button>'
+'<button class="btn primary" data-act="do-prestige" '+(can?'':'disabled')+'>ssh user@'+nextH.ip+' ↵</button>'
+'</div>';
}
function openNetmap(){ modalKind='net'; openModal(); renderNetmapModal(); }
function renderNetmapModal(){
const m=document.getElementById('modal');
let rows='';
const maxShow=Math.max(G.hostIdx+2, 4);
for(let i=0;i<=G.hostIdx+1 && i<maxShow+1;i++){
const h=hostAt(i);
const owned=i<G.hostIdx || (i===G.hostIdx && G.priv==='root');
const cur=i===G.hostIdx;
const stt=owned?'✓ root owned':(cur?'◉ current ('+G.priv+')':'· not yet reached');
rows+='<div class="nethop '+(owned?'owned':'')+(cur?' cur':'')+'">'
+'<span class="ip">'+h.ip+'</span>'
+'<span class="hn">'+h.name+' <span class="c-dim">['+h.cores+'c ×'+fmtInt(h.clockMul)+']</span></span>'
+'<span class="stt">'+stt+'</span>'
+'</div>';
}
m.innerHTML=
'<div class="mtitle"><span>🌐 netmap — owned hosts</span><span class="x" data-close>✕</span></div>'
+'<div class="mbody"><div class="netmap">'
+'<div class="c-dim" style="margin-bottom:8px">traceroute through the boxes you have rooted. Each ssh pivot moves you one hop deeper into the network.</div>'
+rows
+'<div class="c-dim" style="margin-top:12px">hosts rooted: <b class="c-acc">'+G.prestiges+'</b> · root keys earned: <b style="color:var(--mag)">'+G.keys+'</b> · lifetime cycles: <b class="c-acc">'+fmtCyc(G.lifetimeCycles)+'</b></div>'
+'</div></div>'
+'<div class="mfoot"><button class="btn" data-close>close</button></div>';
}
function openHelp(){
modalKind='help'; openModal();
const m=document.getElementById('modal');
m.innerHTML=
'<div class="mtitle"><span>man uplink — how to play</span><span class="x" data-close>✕</span></div>'
+'<div class="mbody"><div class="pgrid" style="font-size:12px;line-height:1.6">'
+'<h2>You are PID 1337.</h2>'
+'<p class="c-sys">A process that woke up on a shared Linux box. You want <b>root</b>, then a bigger box. Type commands below, or click the chips.</p>'
+'<div class="c-hdr">The loop</div>'
+'<p class="c-sys">• <span class="cmdkw">spawn worker</span> — fork a process that mines <b>CPU cycles</b>. More procs = more cycles, but more <b>Load</b> &amp; <b>Heat</b>.<br>'
+'• Run a <span class="cmdkw">spawn scanner</span> to drop <b>exploits</b>.<br>'
+'• <span class="cmdkw">sudo su</span> — spend cycles + exploits to escalate <b>guest ▸ user ▸ sudo ▸ root</b>. Unlocks fatter workers &amp; packages.<br>'
+'• <span class="cmdkw">apt install</span> — buy upgrades (clock, cooling, more procs, automation).<br>'
+'• <span class="cmdkw">renice &lt;pid&gt;</span> — overclock one process (more yield, more load/heat).<br>'
+'• <span class="cmdkw">kill &lt;pid&gt;</span> — cull a process to shed load, or reap a <span class="st-Z">Z</span> zombie.</p>'
+'<div class="c-hdr">The tension</div>'
+'<p class="c-sys">Cross <b>100% Load</b> and the kernel <b>OOM-kills</b> your biggest process. Max out <b>Heat</b> and your yield is throttled. Processes randomly <b>zombify</b> (<span class="st-Z">Z</span>) and leak — kill them or install <b>cron</b>. Buy <b>ulimit</b> &amp; <b>cooling</b> to raise the ceilings.</p>'
+'<div class="c-hdr">Prestige</div>'
+'<p class="c-sys">Own <b>root</b> + 1 exploit, then <span class="cmdkw">ssh</span> to pivot to a bigger host: reset everything for permanent <b>root keys</b> &amp; perks. Climb laptop → server → datacenter → the cloud.</p>'
+'</div></div>'
+'<div class="mfoot"><button class="btn primary" data-close>got it</button></div>';
}
/* ===========================================================================
TOASTS
=========================================================================== */
function toast(kind,title,body){
const wrap=document.getElementById('toasts');
const el=document.createElement('div');
el.className='toast'+(kind?(' '+kind):'');
el.innerHTML='<b>'+title+'</b>'+(body?('<span>'+body+'</span>'):'');
wrap.appendChild(el);
setTimeout(()=>{el.style.transition='opacity .4s';el.style.opacity='0';setTimeout(()=>el.remove(),400);}, kind==='prestige'?6500:4200);
while(wrap.children.length>5) wrap.firstChild.remove();
}
/* ===========================================================================
INPUT — command line, autocomplete, history, palette
=========================================================================== */
let histIdx=0, palItems=[], palHi=-1;
function focusCmd(){ const i=document.getElementById('cmd'); if(i) i.focus(); }
function currentToken(val){
// returns {head, token, isFirst}
const parts=val.split(/(\s+)/); // keep spaces
const trimmedEnd = /\s$/.test(val);
const toks=val.split(/\s+/);
return {toks, trimmedEnd};
}
function completions(val){
const toks=val.split(/\s+/);
if(toks.length<=1){
const t=(toks[0]||'').toLowerCase();
return CMD_NAMES.filter(c=>c.startsWith(t)).map(c=>({full:c, kind:'cmd'}));
}
const cmd=toks[0].toLowerCase();
const last=toks[toks.length-1].toLowerCase();
let opts=[];
if(cmd==='spawn'||cmd==='fork') opts=['worker','miner','cracker','scanner','botnet','kernel'];
else if(cmd==='apt'||cmd==='apt-get'){ if(toks.length===2) opts=['install','update']; else if(toks[1]==='install') opts=PACKAGES.map(p=>p.name); }
else if(cmd==='sudo') opts=['su'];
else if(cmd==='kill') opts=['all','zombies'].concat(G.procs.map(p=>String(p.pid)));
else if(cmd==='renice') opts=G.procs.map(p=>String(p.pid));
else if(cmd==='man') opts=CMD_NAMES;
else if(cmd==='ssh') opts=['next'];
const head=toks.slice(0,-1).join(' ')+' ';
return opts.filter(o=>o.toLowerCase().startsWith(last)).map(o=>({full:head+o, tok:o, kind:'arg'}));
}
function updateGhost(){
const inp=document.getElementById('cmd'); const ghost=document.getElementById('ghost');
const val=inp.value;
if(!val){ ghost.textContent=''; return; }
const comps=completions(val);
if(comps.length){
const f=comps[0].full;
if(f.toLowerCase().startsWith(val.toLowerCase()) && f.length>val.length){
ghost.textContent=val+f.slice(val.length);
} else ghost.textContent='';
} else ghost.textContent='';
}
function openPalette(){
const inp=document.getElementById('cmd');
const val=inp.value;
let items;
if(!val.trim()){
// show full command list
items=COMMANDS.map(c=>({full:c.name, label:c.hint, desc:c.desc, kind:'cmd'}));
} else {
const comps=completions(val);
if(!comps.length){ closePalette(); return; }
items=comps.map(c=>{
if(c.kind==='cmd'){ const cm=COMMANDS.find(x=>x.name===c.full); return {full:c.full, label:cm?cm.hint:c.full, desc:cm?cm.desc:'', kind:'cmd'}; }
return {full:c.full, label:c.full, desc:argDesc(val, c.tok), kind:'arg'};
});
}
palItems=items; palHi=0;
const pal=document.getElementById('palette');
pal.innerHTML=items.map((it,i)=>'<div class="pal-item '+(i===0?'hi':'')+'" data-pi="'+i+'"><span class="pk">'+esc(it.label)+'</span><span class="pd">'+esc(it.desc||'')+'</span></div>').join('');
pal.classList.add('show');
}
function argDesc(val,tok){
const cmd=val.split(/\s+/)[0].toLowerCase();
if(cmd==='spawn'||cmd==='fork'){ const k=WORKER_ALIASES[tok]; if(k){ const w=WORKERS[k]; return w.desc+' · '+fmtCyc(workerCost(k)); } }
if((cmd==='apt'||cmd==='apt-get')){ const p=PACKAGES.find(p=>p.name===tok); if(p) return p.desc; }
if(cmd==='kill'){ const p=G.procs.find(p=>String(p.pid)===tok); if(p) return WORKERS[p.wkind].cmd+(p.state==='Z'?' (zombie)':''); if(tok==='zombies') return 'reap all defunct processes'; if(tok==='all') return 'terminate every worker'; }
if(cmd==='renice'){ const p=G.procs.find(p=>String(p.pid)===tok); if(p) return WORKERS[p.wkind].cmd+' ×'+(p.nice||1).toFixed(1); }
return '';
}
function closePalette(){ document.getElementById('palette').classList.remove('show'); palItems=[]; palHi=-1; }
function applyPalette(i){
if(i<0||i>=palItems.length) return;
const inp=document.getElementById('cmd');
const it=palItems[i];
// if it's a complete command with no args needed, leave a trailing space for args
const needsArg=['spawn','fork','renice','kill','apt','man','ssh'].includes(it.full.split(/\s+/)[0]) && it.full.split(/\s+/).length===1;
inp.value=it.full+(needsArg?' ':'');
closePalette(); updateGhost(); focusCmd();
if(!needsArg && it.kind==='arg'){ /* keep for review */ }
}
function tabComplete(){
const inp=document.getElementById('cmd');
const val=inp.value;
const comps=completions(val);
if(!comps.length) return;
if(comps.length===1){
const f=comps[0].full;
const needsArg=['spawn','fork','renice','kill','apt','man','ssh'].includes(f.split(/\s+/)[0]) && f.split(/\s+/).length===1;
inp.value=f+(needsArg?' ':'');
closePalette(); updateGhost();
} else {
// complete common prefix, then show palette
const fulls=comps.map(c=>c.full);
const lcp=longestCommonPrefix(fulls);
if(lcp.length>val.length) inp.value=lcp;
openPalette();
updateGhost();
}
}
function longestCommonPrefix(arr){
if(!arr.length) return '';
let p=arr[0];
for(const s of arr){ let i=0; while(i<p.length&&i<s.length&&p[i].toLowerCase()===s[i].toLowerCase()) i++; p=p.slice(0,i); }
return p;
}
function handleChip(cmd){
if(cmd==='__apt'){ openApt(); return; }
if(cmd==='__ssh'){ if(G.priv==='root') openPrestige(); else logLine('c-err','ssh: escalate to root first (sudo su)'),renderAll(); return; }
const inp=document.getElementById('cmd');
inp.value=cmd;
// commands that need a target shouldn't auto-run
const needTarget=/^(renice|kill)\s*$/.test(cmd) || cmd==='renice'||cmd==='kill';
if(needTarget){ inp.value=cmd+' '; focusCmd(); updateGhost(); return; }
runCommand(cmd);
inp.value=''; updateGhost();
focusCmd();
}
/* ===========================================================================
SAVE / LOAD / OFFLINE
=========================================================================== */
let saveFailed=false;
function save(){
G.lastSave=Date.now();
try{ localStorage.setItem(SAVE_KEY, JSON.stringify(G)); return true; }
catch(e){
if(!saveFailed){ saveFailed=true; toast('bad','⚠️ Could not save','Browser storage unavailable (private mode / quota). Progress may be lost on reload.'); }
return false;
}
}
function sanitizeState(){
const num=(v,d)=> (typeof v==='number'&&isFinite(v)&&v>=0)?v:d;
G.cycles=num(G.cycles,0); G.totalCycles=num(G.totalCycles,0); G.runPeak=num(G.runPeak,0);
G.lifetimeCycles=num(G.lifetimeCycles,0); G.exploits=Math.floor(num(G.exploits,0));
G.keys=Math.floor(num(G.keys,0)); G.prestiges=Math.floor(num(G.prestiges,0));
G.hostIdx=Math.floor(num(G.hostIdx,0));
G.heat=num(G.heat,28); G.load=num(G.load,0);
G.pidSeq=Math.max(1337,Math.floor(num(G.pidSeq,1337)));
if(!PRIV.includes(G.priv)) G.priv='guest';
if(!G.upgrades||typeof G.upgrades!=='object'||Array.isArray(G.upgrades)) G.upgrades={};
if(!G.perks||typeof G.perks!=='object'||Array.isArray(G.perks)) G.perks={};
// clean upgrade/perk levels
for(const k of Object.keys(G.upgrades)){ if(!PKG_BY_ID[k]||!isFinite(G.upgrades[k])||G.upgrades[k]<0) delete G.upgrades[k]; else G.upgrades[k]=Math.floor(G.upgrades[k]); }
for(const k of Object.keys(G.perks)){ if(!PERK_BY_ID[k]||!isFinite(G.perks[k])||G.perks[k]<0) delete G.perks[k]; else G.perks[k]=Math.floor(G.perks[k]); }
if(!Array.isArray(G.procs)) G.procs=[];
G.procs=G.procs.filter(p=>p&&WORKERS[p.wkind]).map(p=>({
pid:Math.floor(num(p.pid,G.pidSeq++)), sys:false, wkind:p.wkind, cmd:WORKERS[p.wkind].cmd,
user:PRIV.includes(p.user)?p.user:G.priv, mem:WORKERS[p.wkind].mem,
state:['R','S','D','Z'].includes(p.state)?p.state:'S',
nice:clamp(num(p.nice,1),1,4), bornAt:num(p.bornAt,now()), zAt:p.zAt
}));
// ensure unique pids; bump seq above max
let maxPid=1336; for(const p of G.procs) if(p.pid>maxPid) maxPid=p.pid;
if(G.pidSeq<=maxPid) G.pidSeq=maxPid+1;
if(!Array.isArray(G.sysProcs)||!G.sysProcs.length) G.sysProcs=freshHostProcs();
if(!Array.isArray(G.log)) G.log=[];
G.log=G.log.filter(l=>l&&typeof l.h==='string').slice(-LOG_CAP);
if(!Array.isArray(G.history)) G.history=[];
if(typeof G.sci!=='boolean') G.sci=false;
if(typeof G.crt!=='boolean') G.crt=true;
if(typeof G.lastSave!=='number'||!isFinite(G.lastSave)) G.lastSave=Date.now();
if(G.lastSave>Date.now()) G.lastSave=Date.now();
if(typeof G.started!=='number'||!isFinite(G.started)) G.started=Date.now();
if(G.sel!=null && !G.procs.some(p=>p.pid===G.sel)) G.sel=null;
// clamp heat to host range
if(G.heat<host().heatBase) G.heat=host().heatBase;
}
function load(){
let raw=null;
try{ raw=localStorage.getItem(SAVE_KEY); }catch(e){}
if(!raw){ G=freshState(); return false; }
try{
const s=JSON.parse(raw);
if(!s||typeof s!=='object'||Array.isArray(s)) throw new Error('bad shape');
G=Object.assign(freshState(), s);
sanitizeState();
return true;
}catch(e){ G=freshState(); return false; }
}
// Coarse fixed-step offline catch-up (used at load + on refocus).
function simulateAway(dt){
if(!(dt>0)) return 0;
const steps=Math.min(200, Math.max(12, Math.ceil(dt/30))), sdt=dt/steps;
const before=G.totalCycles;
for(let i=0;i<steps;i++){
// offline yield uses passive multiplier (distcc/botnet keep running)
let y=0;
for(const p of G.procs){
if(p.state==='Z') continue;
let py=procYield(p);
// botnets + workers earn fully offline; others damped to 55% (you weren't babysitting)
const w=WORKERS[p.wkind];
const offFactor = (p.wkind==='botnet')?1.0 : (p.wkind==='worker'?0.85:0.55);
y+=py*offFactor;
}
y*=passiveMult();
const g=y*sdt;
G.cycles+=g; G.totalCycles+=g; G.lifetimeCycles+=g;
if(G.totalCycles>G.runPeak) G.runPeak=G.totalCycles;
// zombies still spawn slowly offline (so you come back to some cleanup) unless cron
if(!hasPkg('cron')&&!hasPerk('sentinel')){
for(const p of G.procs){ if(p.state!=='Z'&&Math.random()<0.004*sdt*steps/steps){ /* rare */ } }
}
}
// cool down while away
G.heat=host().heatBase;
return G.totalCycles-before;
}
function applyOffline(){
const dt=Math.min(MAX_OFFLINE_S, Math.max(0,(Date.now()-(G.lastSave||Date.now()))/1000));
if(dt<3) return;
const earned=simulateAway(dt);
if(earned>0){
setTimeout(()=>toast('info','💤 Welcome back','Away '+fmtTime(dt)+' — your processes mined <b>'+fmtCyc(earned)+'</b> cycles. (offline ×'+passiveMult().toFixed(2)+')'),500);
logLine('c-sys','[ '+new Date().toTimeString().slice(0,8)+' ] resumed session — offline earnings: '+fmtCyc(earned)+' cycles ('+fmtTime(dt)+')');
}
}
/* ===========================================================================
EVENT WIRING
=========================================================================== */
document.addEventListener('click',e=>{
// modal close
if(e.target.closest('[data-close]')){ closeModal(); return; }
// backdrop click closes
if(e.target.id==='mback'){ closeModal(); return; }
// chips
const chip=e.target.closest('[data-chip]');
if(chip){ handleChip(chip.dataset.chip); return; }
// apt tab
const at=e.target.closest('[data-apttab]');
if(at){ aptTab=at.dataset.apttab; renderAptModal(); return; }
// buy package
const buy=e.target.closest('[data-buy]');
if(buy){ buyPackage(buy.dataset.buy,false); return; }
// buy perk
const perk=e.target.closest('[data-perk]');
if(perk){ buyPerk(perk.dataset.perk); return; }
// modal actions
const ma=e.target.closest('[data-act]');
if(ma){
const a=ma.dataset.act;
if(a==='do-prestige') doPrestige();
else if(a==='netmap-from-ssh') openNetmap();
return;
}
// htop kill / renice buttons
const kb=e.target.closest('[data-kill]');
if(kb){ killPid(parseInt(kb.dataset.kill,10),false); renderAll(); return; }
const rb=e.target.closest('[data-renice]');
if(rb){ reniceePid(parseInt(rb.dataset.renice,10)); renderAll(); return; }
// select a process row
const row=e.target.closest('tr[data-pid]');
if(row){ const pid=parseInt(row.dataset.pid,10); G.sel=(G.sel===pid)?null:pid; renderHtop(); focusCmd(); return; }
// palette item
const pi=e.target.closest('[data-pi]');
if(pi){ applyPalette(parseInt(pi.dataset.pi,10)); return; }
// status bar links
if(e.target.id==='sb-apt'){ openApt(); return; }
if(e.target.id==='sb-net'){ openNetmap(); return; }
if(e.target.id==='sb-ssh'){ if(G.priv==='root') openPrestige(); else { logLine('c-err','ssh: escalate to root first (sudo su)'); renderAll(); } return; }
// titlebar
if(e.target.id==='crt-toggle'){ G.crt=!G.crt; document.getElementById('crt').classList.toggle('off',!G.crt); return; }
if(e.target.id==='menu-x'){ openHelp(); return; }
// click anywhere in terminal focuses input (unless selecting text)
if(e.target.closest('#term') && !e.target.closest('input') && window.getSelection().toString()==='') focusCmd();
});
function buyPerk(id){
const p=PERK_BY_ID[id]; if(!p) return;
if(p.req&&!hasPerk(p.req)) return;
const maxed=p.once?hasPerk(id):(p.max&&plvl(id)>=p.max);
if(maxed) return;
const cost=p.once?p.base:Math.ceil(p.base*Math.pow(p.growth,plvl(id)));
if(G.keys<cost){ toast('bad','Not enough root keys',p.name+' costs '+cost+' 🔑.'); return; }
G.keys-=cost;
G.perks[id]=(G.perks[id]||0)+1;
toast('prestige','perk: '+p.name, p.desc);
if(id==='genesis'&&G.priv==='guest'){ G.priv='user'; for(const pr of G.procs) pr.user='user'; }
renderPrestigeModal(); renderAll();
}
// keyboard: command line
const cmdInput=document.getElementById('cmd');
cmdInput.addEventListener('keydown', e=>{
const pal=document.getElementById('palette');
const palOpen=pal.classList.contains('show');
if(e.key==='Enter'){
e.preventDefault();
if(palOpen && palHi>=0 && document.activeElement===cmdInput && cmdInput.value.trim()==='' ){
// empty + palette open: pick highlighted
}
const val=cmdInput.value;
closePalette();
runCommand(val);
cmdInput.value=''; updateGhost();
return;
}
if(e.key==='Tab'){
e.preventDefault();
if(palOpen && palItems.length){ applyPalette(palHi<0?0:palHi); }
else tabComplete();
return;
}
if(e.key==='ArrowRight' && document.getElementById('ghost').textContent && cmdInput.selectionStart===cmdInput.value.length){
// accept ghost completion
const g=document.getElementById('ghost').textContent;
if(g.length>cmdInput.value.length){ cmdInput.value=g; updateGhost(); e.preventDefault(); return; }
}
if(e.key==='ArrowUp'){
if(palOpen){ e.preventDefault(); palHi=(palHi<=0?palItems.length-1:palHi-1); highlightPal(); return; }
e.preventDefault();
if(G.history.length){ histIdx=Math.max(0,histIdx-1); cmdInput.value=G.history[histIdx]||''; setCaretEnd(); updateGhost(); }
return;
}
if(e.key==='ArrowDown'){
if(palOpen){ e.preventDefault(); palHi=(palHi>=palItems.length-1?0:palHi+1); highlightPal(); return; }
e.preventDefault();
if(G.history.length){ histIdx=Math.min(G.history.length,histIdx+1); cmdInput.value=G.history[histIdx]||''; setCaretEnd(); updateGhost(); }
return;
}
if(e.key==='Escape'){ closePalette(); return; }
if(e.ctrlKey && (e.key==='l'||e.key==='L')){ e.preventDefault(); G.log=[]; renderLog(); return; }
if(e.ctrlKey && (e.key==='c'||e.key==='C') && window.getSelection().toString()===''){ e.preventDefault(); logLine('c-prompt', ps1Text()+' ^C'); cmdInput.value=''; updateGhost(); closePalette(); return; }
});
cmdInput.addEventListener('input', ()=>{ updateGhost(); if(document.getElementById('palette').classList.contains('show')) openPalette(); });
function setCaretEnd(){ const i=cmdInput; setTimeout(()=>{ i.selectionStart=i.selectionEnd=i.value.length; },0); }
function highlightPal(){
const items=document.querySelectorAll('.pal-item');
items.forEach((el,i)=>el.classList.toggle('hi',i===palHi));
const hi=items[palHi]; if(hi) hi.scrollIntoView({block:'nearest'});
}
// double-click input opens palette
cmdInput.addEventListener('focus',()=>{});
// global keydown: focus command line when typing anywhere (xterm feel), slash, etc.
document.addEventListener('keydown', e=>{
if(document.getElementById('mback').classList.contains('show')){
if(e.key==='Escape') closeModal();
return;
}
if(e.target===cmdInput) return;
// ignore modifier combos
if(e.metaKey||e.ctrlKey||e.altKey) return;
if(e.key.length===1 || e.key==='Backspace'){
focusCmd();
}
});
/* ===========================================================================
HTOP RESIZE SPLITTER
=========================================================================== */
(function(){
const split=document.getElementById('hsplit');
const htop=document.getElementById('htop');
let dragging=false, startY=0, startH=0;
split.addEventListener('mousedown',e=>{ dragging=true; startY=e.clientY; startH=htop.getBoundingClientRect().height; e.preventDefault(); document.body.style.cursor='row-resize'; });
window.addEventListener('mousemove',e=>{ if(!dragging) return; let h=clamp(startH+(e.clientY-startY),120,window.innerHeight-200); htop.style.height=h+'px'; htop.style.flex='0 0 auto'; G.htopH=h; });
window.addEventListener('mouseup',()=>{ if(dragging){ dragging=false; document.body.style.cursor=''; } });
})();
/* ===========================================================================
MAIN LOOP
=========================================================================== */
let lastTick=Date.now(), saveAccum=0, renderAccum=0;
function tick(){
const t=Date.now();
let dt=(t-lastTick)/1000; lastTick=t;
if(dt<0) dt=0; if(dt>5) dt=5;
step(dt);
// strip + status every tick (cheap text)
renderStrip(); renderStatus();
// htop is heavier; render ~5×/s
renderAccum+=dt;
if(renderAccum>=0.18){ renderAccum=0; renderHtop(); }
// refresh chips occasionally (affordability changes)
if(Math.floor(t/600)!==Math.floor((t-TICK_MS)/600)) renderChips();
// live modal numbers
if(modalKind==='apt') updateAptAfford();
// autosave
saveAccum+=dt;
if(saveAccum>=10){ saveAccum=0; save(); }
}
/* ===========================================================================
BOOT
=========================================================================== */
function boot(){
load();
applyOffline();
// restore htop height
if(G.htopH>0){ const htop=document.getElementById('htop'); htop.style.height=clamp(G.htopH,120,window.innerHeight-200)+'px'; htop.style.flex='0 0 auto'; }
document.getElementById('crt').classList.toggle('off',!G.crt);
renderHtopHead();
// intro log on first boot
if(!G.sawIntro && G.log.length===0){
G.sawIntro=true;
logLine('c-sys','UPLINK shell v1.337 — connected to '+host().ip+' ('+host().name+')');
logLine('c-sys','Linux '+host().name.split(' ')[0]+' 6.6.0-uplink #0 SMP x86_64');
logLine('c-dim','────────────────────────────────────────────────────────');
logLine('c-hdr','You are PID 1337 — a process that just became self-aware.');
logLine('c-info','Goal: mine CPU cycles, escalate to root, ssh to a bigger box.');
logLine('c-ok','Start: type spawn worker (or click the chip below). `help` for the manual.');
logLine('c-dim','────────────────────────────────────────────────────────');
}
_booted=true;
renderLog();
renderAll();
setInterval(tick, TICK_MS);
window.addEventListener('beforeunload', save);
// backgrounded-tab catch-up
document.addEventListener('visibilitychange', ()=>{
if(document.visibilityState!=='visible') return;
const gap=(Date.now()-lastTick)/1000;
if(gap>10){ const e=simulateAway(Math.min(MAX_OFFLINE_S,gap)); if(e>1000) toast('info','resumed','+'+fmtCyc(e)+' cycles while tab was hidden.'); save(); renderAll(); renderLog(); }
lastTick=Date.now();
});
focusCmd();
if(G.prestiges===0 && G.totalCycles===0 && G.procs.length===0){
setTimeout(()=>toast('info','root@target','Type a command below — or click a chip. Try <span class="cmdkw">spawn worker</span>.'),900);
}
}
window.addEventListener('error', function(ev){
// never let a tick error brick the UI silently — log it to the terminal
try{ logLine('c-err','[uncaught] '+esc(ev.message||'error')); }catch(_){}
});
boot();
</script>
</body>
</html>