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>
1871 lines
94 KiB
HTML
1871 lines
94 KiB
HTML
<!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 <Z>. 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 <defunct> (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,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
||
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 Tasks <b>'+(G.procs.length)+'</b>/'+maxProcs()+
|
||
' Load <b>'+Math.round(G.load)+'</b>/'+Math.round(loadCeiling())+
|
||
' 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>'+
|
||
' Σ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 <package> · 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> & <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 & packages.<br>'
|
||
+'• <span class="cmdkw">apt install</span> — buy upgrades (clock, cooling, more procs, automation).<br>'
|
||
+'• <span class="cmdkw">renice <pid></span> — overclock one process (more yield, more load/heat).<br>'
|
||
+'• <span class="cmdkw">kill <pid></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> & <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> & 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>
|