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>
1629 lines
77 KiB
HTML
1629 lines
77 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Microsoft Excel - Q3_Forecast_FINAL_v7.xls</title>
|
||
<style>
|
||
/* ============================================================
|
||
MACRO_VIRUS.XLS — Volume III of BORING SOFTWARE
|
||
An incremental engine-builder disguised as Excel 97.
|
||
You are a sentient macro living in cell $A$1. You build the
|
||
economy by buying cells, assigning compounding formulas, and
|
||
DRAG-FILLING generators down columns. The book recalcs every
|
||
tick. Net Worth = SUM of column H. Prestige = "The Audit".
|
||
============================================================ */
|
||
:root{
|
||
--bg:#d4d0c8; /* Win97 face gray */
|
||
--bg2:#c8c4bc;
|
||
--white:#ffffff;
|
||
--cellbg:#ffffff;
|
||
--grid:#d4d0c8; /* Excel gridline */
|
||
--gridline:#c0c0c0;
|
||
--hdr:#ece9d8;
|
||
--hdrface:#d4d0c8;
|
||
--hdr-sel:#aca899;
|
||
--hdr-active:#fcfcc8;
|
||
--text:#000000;
|
||
--muted:#5a5a52;
|
||
--xlgreen:#1f6e43; /* Excel status green */
|
||
--xlgreen2:#2e8b57;
|
||
--sel:#000080; /* selection border navy */
|
||
--selfill:#cce8ff;
|
||
--line:#808080;
|
||
--hi:#ffffff; /* 3d bevel highlight */
|
||
--lo:#808080; /* 3d bevel shadow */
|
||
--lo2:#404040;
|
||
--formula-ok:#1f6e43;
|
||
--owned:#eef6f0;
|
||
--mono:"Lucida Console","Consolas",ui-monospace,Menlo,monospace;
|
||
--ui:"Tahoma","MS Sans Serif","Segoe UI",Geneva,sans-serif;
|
||
}
|
||
*{box-sizing:border-box;}
|
||
html,body{height:100%;margin:0;}
|
||
body{
|
||
font-family:var(--ui);font-size:11px;color:var(--text);
|
||
background:var(--bg);overflow:hidden;user-select:none;-webkit-user-select:none;
|
||
}
|
||
button{font-family:inherit;font-size:inherit;color:inherit;cursor:default;}
|
||
|
||
/* ---- bevel helpers (Win97 chrome) ---- */
|
||
.out{border:1px solid;border-color:var(--hi) var(--lo2) var(--lo2) var(--hi);box-shadow:inset 1px 1px 0 #dfdfdf, inset -1px -1px 0 var(--lo);}
|
||
.in{border:1px solid;border-color:var(--lo2) var(--hi) var(--hi) var(--lo2);box-shadow:inset 1px 1px 0 var(--lo), inset -1px -1px 0 #dfdfdf;}
|
||
|
||
#app{display:flex;flex-direction:column;height:100vh;width:100vw;background:var(--bg);}
|
||
|
||
/* ---- Title bar ---- */
|
||
#titlebar{display:flex;align-items:center;gap:6px;height:20px;flex:0 0 auto;padding:0 3px;
|
||
background:linear-gradient(90deg,#0a246a,#3a6ea5);color:#fff;font-weight:700;font-size:11px;}
|
||
#titlebar .ico{width:14px;height:14px;background:var(--xlgreen2);color:#fff;font-weight:900;
|
||
display:inline-flex;align-items:center;justify-content:center;font-size:10px;border-radius:1px;}
|
||
#titlebar .tt{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
|
||
#titlebar .winbtns{display:flex;gap:2px;}
|
||
#titlebar .winbtn{width:16px;height:14px;background:var(--bg);color:#000;font-size:9px;
|
||
display:inline-flex;align-items:center;justify-content:center;border:1px solid;
|
||
border-color:var(--hi) var(--lo2) var(--lo2) var(--hi);font-family:var(--ui);font-weight:700;}
|
||
|
||
/* ---- Menu bar ---- */
|
||
#menubar{display:flex;align-items:center;height:20px;flex:0 0 auto;background:var(--bg);
|
||
border-bottom:1px solid var(--lo);font-size:11px;}
|
||
.menu{padding:2px 8px;position:relative;}
|
||
.menu:hover{background:var(--sel);color:#fff;}
|
||
.menu .dropdown{display:none;position:absolute;top:18px;left:0;background:var(--bg);
|
||
border:1px solid;border-color:var(--hi) var(--lo2) var(--lo2) var(--hi);
|
||
box-shadow:2px 2px 3px rgba(0,0,0,.4);min-width:210px;z-index:400;padding:2px;color:#000;}
|
||
.menu:hover .dropdown{display:block;}
|
||
.menu .dropdown div{padding:3px 22px 3px 22px;display:flex;justify-content:space-between;gap:24px;white-space:nowrap;}
|
||
.menu .dropdown div:hover{background:var(--sel);color:#fff;}
|
||
.menu .dropdown .sep{height:1px;background:var(--lo);border-bottom:1px solid var(--hi);margin:3px 1px;padding:0;}
|
||
.menu .dropdown .kbd{color:var(--muted);}
|
||
.menu .dropdown div:hover .kbd{color:#cfd;}
|
||
|
||
/* ---- Toolbars ---- */
|
||
#toolbar{display:flex;align-items:center;gap:2px;height:28px;flex:0 0 auto;background:var(--bg);
|
||
border-bottom:1px solid var(--lo);padding:1px 4px;}
|
||
.tbtn{height:23px;min-width:23px;padding:0 4px;display:inline-flex;align-items:center;gap:4px;
|
||
background:var(--bg);border:1px solid transparent;font-size:11px;}
|
||
.tbtn:hover{border-color:var(--hi) var(--lo2) var(--lo2) var(--hi);border-style:solid;border-width:1px;}
|
||
.tbtn:active{border-color:var(--lo2) var(--hi) var(--hi) var(--lo2);}
|
||
.tbtn[disabled]{opacity:.4;pointer-events:none;}
|
||
.tbtn .ic{font-size:13px;filter:grayscale(.2);}
|
||
.tbtn.cta{font-weight:700;color:var(--xlgreen);}
|
||
.tsep{width:2px;height:20px;border-left:1px solid var(--lo);border-right:1px solid var(--hi);margin:0 3px;}
|
||
#tb-stats{margin-left:auto;display:flex;gap:10px;align-items:center;padding-right:4px;}
|
||
.stat{display:flex;flex-direction:column;align-items:flex-end;line-height:1.05;}
|
||
.stat .k{font-size:9px;color:var(--muted);text-transform:uppercase;letter-spacing:.03em;}
|
||
.stat .v{font-size:12px;font-weight:700;font-family:var(--mono);}
|
||
.stat .v.nw{color:var(--xlgreen);} .stat .v.cash{color:#0a3d8f;} .stat .v.sc{color:#7a2fb0;}
|
||
|
||
/* ---- Formula bar ---- */
|
||
#fbar{display:flex;align-items:stretch;height:22px;flex:0 0 auto;background:var(--bg);
|
||
border-bottom:1px solid var(--lo);padding:1px 3px;gap:3px;}
|
||
#namebox{width:78px;background:var(--white);font-family:var(--mono);font-size:11px;
|
||
display:flex;align-items:center;padding:0 5px;}
|
||
.fxlbl{display:flex;align-items:center;gap:3px;padding:0 5px;font-style:italic;color:#444;}
|
||
.fxlbl b{font-style:normal;color:#1c1c8f;font-family:"Times New Roman",serif;font-weight:700;font-style:italic;font-size:13px;}
|
||
#formula-display{flex:1;background:var(--white);font-family:var(--mono);font-size:11px;
|
||
display:flex;align-items:center;padding:0 6px;overflow:hidden;white-space:nowrap;color:#111;}
|
||
|
||
/* ---- Body ---- */
|
||
#body{display:flex;flex:1 1 auto;min-height:0;}
|
||
#gridwrap{flex:1 1 auto;min-width:0;min-height:0;overflow:auto;background:#fff;position:relative;}
|
||
|
||
/* ---- Spreadsheet grid ---- */
|
||
table.sheet{border-collapse:collapse;table-layout:fixed;background:#fff;}
|
||
table.sheet th,table.sheet td{border:1px solid var(--gridline);padding:0;margin:0;
|
||
height:19px;font-size:11px;overflow:hidden;}
|
||
table.sheet thead th{height:18px;}
|
||
/* corner + headers */
|
||
.corner{width:34px;min-width:34px;background:var(--hdrface);position:sticky;left:0;top:0;z-index:6;
|
||
border:1px solid var(--gridline);}
|
||
.colh{background:var(--hdrface);text-align:center;font-weight:400;color:#000;
|
||
border-color:var(--hi) var(--lo) var(--lo) var(--hi);border-style:solid;border-width:1px;
|
||
position:sticky;top:0;z-index:5;}
|
||
.colh.csel{background:var(--hdr-active);font-weight:700;}
|
||
.rowh{width:34px;min-width:34px;background:var(--hdrface);text-align:center;color:#000;
|
||
border-color:var(--hi) var(--lo) var(--lo) var(--hi);border-style:solid;border-width:1px;
|
||
position:sticky;left:0;z-index:4;}
|
||
.rowh.rsel{background:var(--hdr-active);font-weight:700;}
|
||
/* data cells */
|
||
td.cell{width:74px;min-width:74px;background:#fff;font-family:var(--mono);text-align:right;
|
||
padding:0 4px;position:relative;cursor:cell;color:#111;white-space:nowrap;}
|
||
td.cell.locked{background:#fff;color:#c8c8c8;cursor:default;}
|
||
td.cell.empty{background:#fff;}
|
||
td.cell.owned{background:#fff;}
|
||
td.cell.outcol{background:#f2f8f4;}
|
||
td.cell.outcol.owned{background:#eaf5ee;}
|
||
/* heat-map (conditional formatting upgrade) */
|
||
td.cell.heat{color:#0b3d1f;}
|
||
/* selection */
|
||
td.cell.sel{outline:2px solid var(--sel);outline-offset:-2px;z-index:3;background:#fff;}
|
||
td.cell.sel.outcol{background:#eef6f1;}
|
||
td.cell.range{background:var(--selfill)!important;}
|
||
/* fill handle */
|
||
.fillhandle{position:absolute;right:-3px;bottom:-3px;width:6px;height:6px;background:var(--sel);
|
||
border:1px solid #fff;cursor:crosshair;z-index:10;}
|
||
/* fill preview overlay text */
|
||
.cellval{display:block;width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
||
.cell .ftag{position:absolute;left:2px;top:0;font-size:8px;color:#9aa;font-family:var(--ui);pointer-events:none;}
|
||
.cell.owned .ftag{color:#7ba88f;}
|
||
|
||
/* ---- Right rail: macro / build panel ---- */
|
||
#rail{width:236px;flex:0 0 auto;background:var(--bg);border-left:1px solid var(--lo);
|
||
display:flex;flex-direction:column;min-height:0;}
|
||
.rail-tabs{display:flex;background:var(--bg);border-bottom:1px solid var(--lo);}
|
||
.rtab{flex:1;text-align:center;padding:3px 0;font-size:10px;border-right:1px solid var(--lo);
|
||
background:var(--bg);}
|
||
.rtab.active{background:#fff;font-weight:700;border-bottom:2px solid var(--xlgreen);margin-bottom:-1px;}
|
||
.rtab:last-child{border-right:none;}
|
||
#rail-body{flex:1 1 auto;overflow:auto;background:#fff;padding:6px;}
|
||
.panel-h{font-weight:700;font-size:11px;margin:0 0 4px;color:#1c1c8f;border-bottom:1px solid var(--gridline);padding-bottom:2px;}
|
||
.hint{color:var(--muted);font-size:10px;line-height:1.4;margin:4px 0;}
|
||
/* formula chooser rows */
|
||
.frow{display:flex;align-items:center;gap:6px;border:1px solid var(--gridline);background:#fff;padding:4px 5px;margin-bottom:3px;}
|
||
.frow:hover{background:#f3f8ff;}
|
||
.frow.cur{background:#eef6f1;border-color:#9cc7ad;}
|
||
.frow .fcode{font-family:var(--mono);font-size:11px;color:#1c1c8f;flex:1;min-width:0;}
|
||
.frow .fdesc{color:var(--muted);font-size:9px;}
|
||
.xbtn{border:1px solid;border-color:var(--hi) var(--lo2) var(--lo2) var(--hi);background:var(--bg);
|
||
padding:2px 8px;font-size:11px;white-space:nowrap;}
|
||
.xbtn:hover{background:#e8e8e0;}
|
||
.xbtn:active{border-color:var(--lo2) var(--hi) var(--hi) var(--lo2);}
|
||
.xbtn[disabled]{opacity:.45;pointer-events:none;}
|
||
.xbtn.ok{color:var(--xlgreen);font-weight:700;}
|
||
.xbtn.no{color:#a22;}
|
||
.xbtn.prim{font-weight:700;}
|
||
.cost{font-family:var(--mono);font-size:11px;}
|
||
.cost.ok{color:var(--xlgreen);} .cost.no{color:#a22;}
|
||
.kv2{display:grid;grid-template-columns:max-content 1fr;gap:1px 8px;font-size:10px;margin:3px 0;}
|
||
.kv2 .k{color:var(--muted);} .kv2 .v{font-family:var(--mono);text-align:right;}
|
||
.divider{height:1px;background:var(--gridline);margin:6px 0;}
|
||
|
||
/* macro/upgrade list */
|
||
.up-row{display:flex;align-items:center;gap:7px;border:1px solid var(--gridline);background:#fff;padding:4px 5px;margin-bottom:3px;}
|
||
.up-row:hover{background:#f6f9f6;}
|
||
.up-row.maxed{opacity:.7;}
|
||
.up-row.lockreq{opacity:.5;}
|
||
.up-row .up-ic{font-size:13px;filter:grayscale(1);opacity:.7;width:16px;text-align:center;}
|
||
.up-row .up-main{flex:1;min-width:0;}
|
||
.up-row .up-name{font-weight:700;font-size:11px;}
|
||
.up-row .up-name .lvl{color:var(--muted);font-weight:400;font-family:var(--mono);font-size:10px;margin-left:5px;}
|
||
.up-row .up-desc{color:var(--muted);font-size:9px;line-height:1.3;margin-top:1px;}
|
||
.up-row .up-eff{font-size:9px;font-family:var(--mono);color:#2a5a8a;}
|
||
.up-buy{display:flex;flex-direction:column;align-items:flex-end;gap:2px;min-width:78px;}
|
||
.up-row.bought{background:#eef6f1;}
|
||
.up-row.bought .up-name{color:var(--xlgreen);}
|
||
|
||
/* ---- Status bar ---- */
|
||
#statusbar{display:flex;align-items:stretch;height:20px;flex:0 0 auto;background:var(--bg);
|
||
border-top:1px solid var(--hi);font-size:10px;}
|
||
.sb{display:flex;align-items:center;gap:5px;padding:0 9px;border-right:1px solid var(--lo);
|
||
border-left:1px solid var(--hi);height:100%;}
|
||
.sb.grow{flex:1;border-right:none;border-left:1px solid var(--hi);}
|
||
.sb b{font-family:var(--mono);}
|
||
.sb .calc{color:var(--xlgreen);font-weight:700;}
|
||
.nw-sb{color:var(--xlgreen);font-weight:700;font-family:var(--mono);}
|
||
|
||
/* ---- Sheet tabs ---- */
|
||
#sheettabs{display:flex;align-items:flex-end;height:20px;flex:0 0 auto;background:var(--bg);
|
||
border-top:1px solid var(--lo);padding:0 4px;gap:0;}
|
||
.stab{padding:2px 12px;font-size:11px;background:var(--bg2);
|
||
border:1px solid var(--lo);border-bottom:none;margin-right:-1px;border-radius:3px 3px 0 0;color:#444;}
|
||
.stab.active{background:#fff;font-weight:700;color:#000;padding-bottom:3px;}
|
||
.stab.locked{color:#999;font-style:italic;}
|
||
|
||
/* ---- Modal ---- */
|
||
#modal-back{position:fixed;inset:0;background:rgba(0,0,0,.18);display:none;z-index:600;
|
||
align-items:center;justify-content:center;}
|
||
#modal-back.show{display:flex;}
|
||
.modal{background:var(--bg);border:2px solid;border-color:var(--hi) var(--lo2) var(--lo2) var(--hi);
|
||
box-shadow:3px 3px 10px rgba(0,0,0,.5);width:560px;max-width:94vw;max-height:88vh;
|
||
display:flex;flex-direction:column;overflow:hidden;}
|
||
.modal.sm{width:400px;}
|
||
.modal-title{display:flex;align-items:center;gap:6px;height:20px;padding:0 4px;
|
||
background:linear-gradient(90deg,#0a246a,#3a6ea5);color:#fff;font-weight:700;}
|
||
.modal-title .x{margin-left:auto;width:16px;height:14px;background:var(--bg);color:#000;
|
||
display:inline-flex;align-items:center;justify-content:center;font-size:9px;font-weight:700;
|
||
border:1px solid;border-color:var(--hi) var(--lo2) var(--lo2) var(--hi);}
|
||
.modal-title .x:active{border-color:var(--lo2) var(--hi) var(--hi) var(--lo2);}
|
||
.modal-body{padding:10px 12px;overflow:auto;flex:1 1 auto;background:var(--bg);}
|
||
.modal-foot{padding:7px 12px;display:flex;gap:8px;justify-content:flex-end;align-items:center;background:var(--bg);
|
||
border-top:1px solid var(--hi);}
|
||
.dlg-btn{border:1px solid;border-color:var(--hi) var(--lo2) var(--lo2) var(--hi);background:var(--bg);
|
||
padding:4px 18px;min-width:74px;font-size:11px;}
|
||
.dlg-btn:active{border-color:var(--lo2) var(--hi) var(--hi) var(--lo2);}
|
||
.dlg-btn.def{font-weight:700;}
|
||
.dlg-btn[disabled]{opacity:.45;pointer-events:none;}
|
||
.modal p{margin:6px 0;line-height:1.5;}
|
||
.modal h3{margin:10px 0 4px;font-size:12px;}
|
||
.warnbox{background:#fffceb;border:1px solid #d4b94a;padding:7px 9px;margin:8px 0;}
|
||
.code{font-family:var(--mono);background:#fff;border:1px solid var(--gridline);padding:1px 4px;}
|
||
textarea.io{width:100%;height:90px;font-family:var(--mono);font-size:10px;}
|
||
|
||
/* ---- toasts ---- */
|
||
#toasts{position:fixed;right:12px;bottom:46px;z-index:700;display:flex;flex-direction:column;gap:6px;align-items:flex-end;}
|
||
.toast{background:var(--bg);border:2px solid;border-color:var(--hi) var(--lo2) var(--lo2) var(--hi);
|
||
box-shadow:2px 2px 7px rgba(0,0,0,.4);padding:7px 11px;font-size:11px;max-width:300px;animation:tin .2s ease;}
|
||
.toast .tt{font-weight:700;display:block;margin-bottom:2px;color:#1c1c8f;}
|
||
.toast.warn .tt{color:#9a5a00;} .toast.bad .tt{color:#a22;}
|
||
.toast.good .tt{color:var(--xlgreen);} .toast.audit .tt{color:#7a2fb0;}
|
||
@keyframes tin{from{transform:translateX(30px);opacity:0;}to{transform:none;opacity:1;}}
|
||
|
||
/* prestige */
|
||
.prestige-num{font-size:24px;font-family:var(--mono);font-weight:800;color:#7a2fb0;}
|
||
.perk-row{display:flex;align-items:center;gap:8px;border:1px solid var(--gridline);background:#fff;padding:5px 7px;margin-bottom:3px;}
|
||
.perk-row .p-ic{font-size:14px;filter:grayscale(1);opacity:.7;}
|
||
.perk-row .p-main{flex:1;}
|
||
.perk-row .p-name{font-weight:700;}
|
||
.perk-row .p-desc{color:var(--muted);font-size:10px;}
|
||
|
||
::-webkit-scrollbar{width:16px;height:16px;background:var(--bg);}
|
||
::-webkit-scrollbar-thumb{background:var(--bg);border:1px solid;border-color:var(--hi) var(--lo2) var(--lo2) var(--hi);}
|
||
::-webkit-scrollbar-button{background:var(--bg);border:1px solid;border-color:var(--hi) var(--lo2) var(--lo2) var(--hi);display:block;height:16px;width:16px;}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="app">
|
||
<!-- TITLE BAR -->
|
||
<div id="titlebar">
|
||
<span class="ico">X</span>
|
||
<span class="tt" id="title-text">Microsoft Excel - Q3_Forecast_FINAL_v7.xls</span>
|
||
<span class="winbtns"><span class="winbtn">_</span><span class="winbtn">□</span><span class="winbtn">✕</span></span>
|
||
</div>
|
||
|
||
<!-- MENU BAR -->
|
||
<div id="menubar">
|
||
<div class="menu">File
|
||
<div class="dropdown">
|
||
<div data-act="save-now">Save<span class="kbd">Ctrl+S</span></div>
|
||
<div data-act="export-save">Export Workbook…</div>
|
||
<div data-act="import-save">Import Workbook…</div>
|
||
<div class="sep"></div>
|
||
<div data-act="open-audit">Submit for Audit…</div>
|
||
<div class="sep"></div>
|
||
<div data-act="hard-reset">Close Without Saving…</div>
|
||
</div>
|
||
</div>
|
||
<div class="menu">Edit
|
||
<div class="dropdown">
|
||
<div data-act="buy-cell">Insert Cell (License)…</div>
|
||
<div data-act="clear-sel">Clear Selected Formula</div>
|
||
</div>
|
||
</div>
|
||
<div class="menu">Insert
|
||
<div class="dropdown">
|
||
<div data-act="rail-formula">Function (fx)…</div>
|
||
<div data-act="rail-macros">Pivot Table…</div>
|
||
</div>
|
||
</div>
|
||
<div class="menu">Tools
|
||
<div class="dropdown">
|
||
<div data-act="rail-macros">Macro › VBA Editor…</div>
|
||
<div data-act="rail-formula">Formula Auditing…</div>
|
||
<div class="sep"></div>
|
||
<div data-act="toggle-sci">Number Format › Scientific</div>
|
||
<div data-act="open-help">How To Build…</div>
|
||
</div>
|
||
</div>
|
||
<div class="menu">Help
|
||
<div class="dropdown">
|
||
<div data-act="open-help">Microsoft Excel Help<span class="kbd">F1</span></div>
|
||
<div data-act="open-about">About This Workbook</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TOOLBAR -->
|
||
<div id="toolbar">
|
||
<button class="tbtn cta" data-act="buy-cell"><span class="ic">▦</span>Buy Cell</button>
|
||
<div class="tsep"></div>
|
||
<button class="tbtn" data-act="rail-formula"><span class="ic">ƒ</span>Functions</button>
|
||
<button class="tbtn" data-act="rail-macros"><span class="ic">⚙</span>Macros</button>
|
||
<div class="tsep"></div>
|
||
<button class="tbtn" id="tb-audit" data-act="open-audit"><span class="ic">⚖</span>Audit</button>
|
||
<div id="tb-stats">
|
||
<div class="stat"><span class="k">Net Worth</span><span class="v nw" id="hud-nw">$0</span></div>
|
||
<div class="stat"><span class="k">Cash</span><span class="v cash" id="hud-cash">$0</span></div>
|
||
<div class="stat" id="hud-sc-wrap" style="display:none"><span class="k">Shell Cos.</span><span class="v sc" id="hud-sc">0</span></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- FORMULA BAR -->
|
||
<div id="fbar">
|
||
<div id="namebox" class="in">A1</div>
|
||
<div class="fxlbl"><b>fx</b></div>
|
||
<div id="formula-display" class="in"></div>
|
||
</div>
|
||
|
||
<!-- BODY -->
|
||
<div id="body">
|
||
<div id="gridwrap">
|
||
<table class="sheet" id="sheet"></table>
|
||
</div>
|
||
<div id="rail">
|
||
<div class="rail-tabs">
|
||
<div class="rtab active" data-rtab="formula">fx Builder</div>
|
||
<div class="rtab" data-rtab="macros">Macros</div>
|
||
<div class="rtab" data-rtab="vba">VBA</div>
|
||
</div>
|
||
<div id="rail-body"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- STATUS BAR -->
|
||
<div id="statusbar">
|
||
<div class="sb" id="sb-calc"><span class="calc">Ready</span></div>
|
||
<div class="sb">Sum=<b id="sb-sum">$0</b></div>
|
||
<div class="sb">Count=<b id="sb-count">0</b></div>
|
||
<div class="sb grow"></div>
|
||
<div class="sb">$/cycle <b id="sb-rate">$0</b></div>
|
||
<div class="sb">Net Worth <b class="nw-sb" id="sb-nw">$0</b></div>
|
||
</div>
|
||
|
||
<!-- SHEET TABS -->
|
||
<div id="sheettabs"></div>
|
||
</div>
|
||
|
||
<!-- MODAL -->
|
||
<div id="modal-back"><div class="modal" id="modal"></div></div>
|
||
<!-- TOASTS -->
|
||
<div id="toasts"></div>
|
||
|
||
<script>
|
||
"use strict";
|
||
/* ===========================================================================
|
||
MACRO_VIRUS.XLS — incremental engine-builder in a spreadsheet
|
||
=========================================================================== */
|
||
const SAVE_KEY = "boringsoft_xls_v1";
|
||
const TICK_MS = 100;
|
||
const MAX_OFFLINE_S = 8*3600; // cap offline catch-up at 8h
|
||
const COLS_N = 8; // A..H
|
||
const ROWS_N = 20; // 1..20
|
||
const CELL_MAX = 1e290; // hard ceiling per cell so nothing ever reaches Infinity
|
||
const OUT_COL = 7; // column H (0-indexed) is the Net Worth output column
|
||
const COL_LABELS = ['A','B','C','D','E','F','G','H'];
|
||
|
||
/* ---- number formatting (re-implemented from baseline) --------------------- */
|
||
const SI = ['','K','M','B','T','Qa','Qi','Sx','Sp','Oc','No','Dc','UDc','DDc','TDc','QaDc','QiDc'];
|
||
function sciStr(n){ const e=Math.floor(Math.log10(n)); return '$'+(n/Math.pow(10,e)).toFixed(2)+'e'+e; }
|
||
function fmt(n){
|
||
if(n===Infinity) return '$∞';
|
||
if(!isFinite(n)||isNaN(n)) return '$0';
|
||
if(n<0) n=0;
|
||
if(n<1000) return '$'+(n<10?n.toFixed(n<1&&n>0?2:0):Math.floor(n));
|
||
if(G&&G.sci) return sciStr(n);
|
||
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);
|
||
return '$'+x.toFixed(x<100?2:1)+SI[i];
|
||
}
|
||
// bare number (no $) for cell display so cells look like spreadsheet numbers
|
||
function fmtCell(n){
|
||
if(!isFinite(n)||isNaN(n)) return '0';
|
||
if(n<0) n=0;
|
||
if(n<1000) return n<10?n.toFixed(2):Math.floor(n).toString();
|
||
if(G&&G.sci){ const e=Math.floor(Math.log10(n)); return (n/Math.pow(10,e)).toFixed(2)+'E+'+e; }
|
||
let i=0,x=n;
|
||
while(x>=1000 && i<SI.length-1){x/=1000;i++;}
|
||
if(i===SI.length-1 && x>=1000){ const e=Math.floor(Math.log10(n)); return (n/Math.pow(10,e)).toFixed(2)+'E+'+e; }
|
||
return x.toFixed(x<100?2:1)+SI[i];
|
||
}
|
||
function fmtInt(n){
|
||
if(!isFinite(n)) return '∞';
|
||
if(n<1000) return Math.floor(n).toString();
|
||
let i=0,x=n; while(x>=1000 && i<SI.length-1){x/=1000;i++;}
|
||
return x.toFixed(1)+SI[i];
|
||
}
|
||
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>99) 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); }
|
||
|
||
/* ---- cell id helpers ------------------------------------------------------ */
|
||
function cid(c,r){ return COL_LABELS[c]+(r+1); } // col 0-idx, row 0-idx -> "A1"
|
||
function colOf(id){ return COL_LABELS.indexOf(id[0]); }
|
||
function rowOf(id){ return parseInt(id.slice(1))-1; }
|
||
|
||
/* ===========================================================================
|
||
FORMULA PALETTE
|
||
Each formula is a generator type. Player picks one for an owned cell, then
|
||
upgrades its tier (raises the constant). Recalc runs them left-to-right,
|
||
top-to-bottom every tick so chains cascade.
|
||
=========================================================================== */
|
||
// kind defines how value is computed each recalc.
|
||
// prev : v = v*k (compounding; if v==0 seed with `seed`)
|
||
// add : v = (left cell) + (above cell) + seed (adder, feeds on neighbors)
|
||
// sum : v = SUM(its own column, rows above it) * k (aggregator)
|
||
// rand : v = v + base*k*RAND() (volatile gambler, average ~0.5)
|
||
// pivot : v = SUM(everything left of it) * k (region multiplier / meta)
|
||
// NOTE: `g(lvl)` returns the *per-second growth/feed factor* used by recalc.
|
||
// `fx(lvl)` is the cosmetic Excel formula string shown in the formula bar.
|
||
const FORMULAS = {
|
||
prev: {
|
||
name:'Compounding',
|
||
icon:'∑',
|
||
fx:(lvl)=> '=PREV*'+(1.10+0.05*lvl).toFixed(2),
|
||
desc:'Grows itself every recalc. The bread-and-butter generator.',
|
||
baseCost:20, costGrow:1.55, // cost to ASSIGN this formula to a cell
|
||
tierBase:60, tierGrow:1.9, // cost to raise its tier
|
||
seed:1,
|
||
// ~ (2.5 + 1.5·tier)% compounding per recalc-second. Gentle so it doubles
|
||
// every ~28s at tier 1 (satisfying, not hyperbolic) and never overflows.
|
||
g:(lvl)=> 0.025+0.015*lvl,
|
||
},
|
||
add: {
|
||
name:'Adder',
|
||
icon:'+',
|
||
fx:(lvl)=> '=LEFT+UP+'+fmtInt(4*Math.pow(2,lvl)),
|
||
desc:'Banks the cell to its LEFT and ABOVE, plus a constant, every second. Loves neighbors.',
|
||
baseCost:55, costGrow:1.7,
|
||
tierBase:180, tierGrow:2.0,
|
||
seed:0,
|
||
g:(lvl)=> 4*Math.pow(2,lvl), // additive constant per sec; neighbors add on top
|
||
},
|
||
sum: {
|
||
name:'Aggregator',
|
||
icon:'Σ',
|
||
fx:(lvl)=> '=SUM(col)*'+(0.30+0.20*lvl).toFixed(2),
|
||
desc:'Banks a fraction of the SUM of cells above it (its column) every second. Turns a column of small generators into one big feeder.',
|
||
baseCost:160, costGrow:1.8,
|
||
tierBase:520, tierGrow:2.1,
|
||
seed:0,
|
||
g:(lvl)=> 0.30+0.20*lvl, // fraction of column-above sum banked / sec
|
||
},
|
||
rand: {
|
||
name:'Volatile',
|
||
icon:'∿',
|
||
fx:(lvl)=> '=PREV+RAND()*'+fmtInt(50*Math.pow(4,lvl)),
|
||
desc:'Banks a random amount each second (shown is the max; avg is half). High variance, high reward.',
|
||
baseCost:110, costGrow:1.7,
|
||
tierBase:340, tierGrow:2.0,
|
||
seed:0,
|
||
g:(lvl)=> 50*Math.pow(4,lvl), // max random add per sec
|
||
},
|
||
pivot: {
|
||
name:'Pivot Table',
|
||
icon:'▦',
|
||
fx:(lvl)=> '=PIVOT(A:G)*'+(0.04*Math.pow(2,lvl)).toFixed(3),
|
||
desc:'A meta-cell: banks a fraction of EVERYTHING to its left every second. The big finisher for column H.',
|
||
baseCost:4000, costGrow:2.0,
|
||
tierBase:14000, tierGrow:2.4,
|
||
seed:0,
|
||
g:(lvl)=> 0.04*Math.pow(2,lvl), // fraction of left-region sum / sec
|
||
req:'pivotUnlock',
|
||
},
|
||
};
|
||
const FORMULA_ORDER = ['prev','add','sum','rand','pivot'];
|
||
|
||
/* ===========================================================================
|
||
MACROS (diegetic upgrades) — purchased in the Macros rail panel
|
||
=========================================================================== */
|
||
const MACROS = [
|
||
{id:'recalc', ic:'⏱', name:'Recalculation Engine',
|
||
desc:'More calc threads. Speeds the whole book (global ×1.6 production).',
|
||
base:400, growth:2.2, mult:1.6, max:40},
|
||
{id:'license', ic:'▦', name:'Volume Cell License',
|
||
desc:'Bulk licensing: every new cell costs less. −8% cell price per level.',
|
||
base:700, growth:2.4, eff:(l)=>Math.pow(0.92,l), max:18},
|
||
{id:'condfmt', ic:'🎨', name:'Conditional Formatting',
|
||
desc:'Heat-map cells green as they grow — and boost the hottest column ×1.25/level.',
|
||
base:1200, growth:2.3, mult:1.25, max:30},
|
||
{id:'pivotUnlock', ic:'▦', name:'Pivot Table Add-In',
|
||
desc:'Unlocks the =PIVOT() formula: a meta-cell that multiplies the whole region.',
|
||
base:9000, growth:1, once:true},
|
||
{id:'array', ic:'∷', name:'Array Formulas (CSE)',
|
||
desc:'Ctrl+Shift+Enter power. Drag-fill costs −10% per level (cheaper bulk fills).',
|
||
base:3500, growth:2.5, eff:(l)=>Math.pow(0.90,l), max:14},
|
||
{id:'precision', ic:'#', name:'Calculation Precision',
|
||
desc:'Iterative calculation: +18% to ALL formula constants per level (global compounding ×).',
|
||
base:5000, growth:2.6, mult:1.18, max:25},
|
||
];
|
||
const MACRO_BY_ID = Object.fromEntries(MACROS.map(m=>[m.id,m]));
|
||
|
||
/* ---- VBA automation macros (the automation layer) ------------------------ */
|
||
const VBA = [
|
||
{id:'autobuy', ic:'▸', name:'Sub AutoBuyCell()',
|
||
desc:'Automatically buys the cheapest empty cell when you can afford it.',
|
||
base:2500, once:true},
|
||
{id:'autofill', ic:'▸', name:'Sub AutoFillDown()',
|
||
desc:'Auto drag-fills your best generator down any column with empty owned cells.',
|
||
base:8000, once:true, req:'autobuy'},
|
||
{id:'autoupgrade', ic:'▸', name:'Sub AutoUpgradeTier()',
|
||
desc:'Spends spare Cash upgrading the cheapest formula tier each cycle.',
|
||
base:20000, once:true, req:'autobuy'},
|
||
{id:'autopivot', ic:'▸', name:'Sub AutoPivot()',
|
||
desc:'Auto-assigns Pivot Tables to empty cells in column H when affordable.',
|
||
base:120000, once:true, req:'autofill'},
|
||
];
|
||
const VBA_BY_ID = Object.fromEntries(VBA.map(v=>[v.id,v]));
|
||
|
||
/* ---- prestige perks (persist across audits) ------------------------------ */
|
||
const PERKS = [
|
||
{id:'startTier', ic:'⬆', name:'Pre-loaded Templates',
|
||
desc:'All new formulas start +1 tier per level.', base:1, growth:1.7, add:1, max:8},
|
||
{id:'globalMult', ic:'×', name:'Offshore Multiplier',
|
||
desc:'+35% global production per level (permanent, compounding).', base:1, growth:1.6, mult:1.35, max:40},
|
||
{id:'freeCells', ic:'▦', name:'Grandfathered Licenses',
|
||
desc:'Start each book with +2 cells already owned per level.', base:2, growth:1.7, add:2, max:12},
|
||
{id:'fastRecalc', ic:'⏱', name:'Quantum Recalc Core',
|
||
desc:'×1.5 base recalc rate per level (faster compounding everywhere).', base:3, growth:1.8, mult:1.5, max:20},
|
||
{id:'cashFlow', ic:'$', name:'Aggressive Skimming',
|
||
desc:'Cash accrues at +50% of output per level instead of base 100%.', base:5, growth:2.0, add:0.5, max:8},
|
||
];
|
||
const PERK_BY_ID = Object.fromEntries(PERKS.map(p=>[p.id,p]));
|
||
|
||
/* ---- sheet / book progression (prestige layers) --------------------------- */
|
||
const BOOKS = [
|
||
{name:'Q3_Forecast_FINAL_v7.xls', tab:'Sheet1', sub:'Corporate Finance Workstation'},
|
||
{name:'HedgeFund_RiskModel.xls', tab:'HedgeFund', sub:'Two Sigma Algorithmic Desk'},
|
||
{name:'CentralBank_Policy.xls', tab:'CentralBank', sub:'Federal Reserve Mainframe'},
|
||
{name:'GlobalLedger_MASTER.xls', tab:'GlobalLedger', sub:'The World Ledger'},
|
||
{name:'Reality_Spreadsheet.xls', tab:'TheVoid', sub:'There Is No Workstation'},
|
||
];
|
||
|
||
/* ===========================================================================
|
||
STATE
|
||
=========================================================================== */
|
||
let G = null;
|
||
function freshState(keep){
|
||
const s = {
|
||
v:1,
|
||
cash:0, // spendable; accrues at outputPerSec
|
||
netWorth:0, // SUM of output column (column H)
|
||
peakNetWorth:0, // for prestige formula
|
||
cells:{}, // id -> {owned, formula, lvl, value}
|
||
macros:{}, // id -> level
|
||
vba:{}, // id -> 1
|
||
perks:{}, // id -> level (persists)
|
||
shells:0, // prestige currency (persists)
|
||
audits:0, // prestige count (persists)
|
||
book:0, // current book index (persists)
|
||
cycles:0, // recalc cycle counter (cosmetic)
|
||
sci:false,
|
||
rtab:'formula',
|
||
sel:'A1', // selected cell id
|
||
range:null, // active fill range {c0,r0,c1,r1} during preview (transient)
|
||
lastSave:Date.now(),
|
||
started:Date.now(),
|
||
lifetimeNetWorth:0,
|
||
};
|
||
if(keep){ // carry permanent stuff through prestige
|
||
s.perks=keep.perks; s.shells=keep.shells; s.audits=keep.audits;
|
||
s.book=keep.book; s.sci=keep.sci; s.started=keep.started;
|
||
s.lifetimeNetWorth=keep.lifetimeNetWorth;
|
||
}
|
||
return s;
|
||
}
|
||
|
||
/* ---- give the player a starting cell so the grid isn't empty -------------- */
|
||
function seedFreshBook(){
|
||
const st=plvl('startTier');
|
||
// A1 always owned (you, the macro, live here) with a compounding seed.
|
||
ensureCell('A1');
|
||
G.cells['A1'].owned=true;
|
||
if(!G.cells['A1'].formula){ G.cells['A1'].formula='prev'; G.cells['A1'].lvl=st; G.cells['A1'].value=1; }
|
||
// H1 is the first OUTPUT cell — gives the player immediate Net Worth & Cash so the
|
||
// loop is legible from second one. (The output column is H; income comes from there.)
|
||
ensureCell('H1');
|
||
G.cells['H1'].owned=true;
|
||
if(!G.cells['H1'].formula){ G.cells['H1'].formula='prev'; G.cells['H1'].lvl=st; G.cells['H1'].value=1; }
|
||
// a little starter Cash to make the first purchase feel good
|
||
if(G.cash<10) G.cash=10;
|
||
// grandfathered free cells from perk
|
||
let free = 2*plvl('freeCells');
|
||
let placed=0;
|
||
for(let r=0;r<ROWS_N && placed<free;r++){
|
||
for(let c=0;c<COLS_N && placed<free;c++){
|
||
const id=cid(c,r);
|
||
if(G.cells[id] && G.cells[id].owned) continue;
|
||
ensureCell(id);
|
||
G.cells[id].owned=true; placed++;
|
||
}
|
||
}
|
||
}
|
||
|
||
function ensureCell(id){
|
||
if(!G.cells[id]) G.cells[id]={owned:false, formula:null, lvl:0, value:0};
|
||
return G.cells[id];
|
||
}
|
||
|
||
/* ===========================================================================
|
||
DERIVED VALUES
|
||
=========================================================================== */
|
||
const lvl = id => G.macros[id]||0;
|
||
const plvl = id => G.perks[id]||0;
|
||
const hasMacro = id => (G.macros[id]||0)>0;
|
||
const hasVba = id => !!G.vba[id];
|
||
|
||
function ownedCount(){ let n=0; for(const k in G.cells) if(G.cells[k].owned) n++; return n; }
|
||
function productiveCount(){ let n=0; for(const k in G.cells){ const c=G.cells[k]; if(c.owned&&c.formula) n++; } return n; }
|
||
|
||
// cost to buy the NEXT cell license
|
||
function cellCost(){
|
||
const owned=ownedCount();
|
||
const base=15 * Math.pow(1.17, owned);
|
||
return base * (hasMacro('license')?MACRO_BY_ID.license.eff(lvl('license')):1);
|
||
}
|
||
// cost to assign a formula to an owned-but-empty cell
|
||
function formulaAssignCost(fkey){
|
||
const f=FORMULAS[fkey];
|
||
// count how many cells already use this formula -> rising cost
|
||
let used=0; for(const k in G.cells){ if(G.cells[k].formula===fkey) used++; }
|
||
return f.baseCost * Math.pow(f.costGrow, used);
|
||
}
|
||
// cost to upgrade a specific cell's tier
|
||
function tierCost(id){
|
||
const c=G.cells[id]; if(!c||!c.formula) return Infinity;
|
||
const f=FORMULAS[c.formula];
|
||
return f.tierBase * Math.pow(f.tierGrow, c.lvl);
|
||
}
|
||
// cost to drag-fill from src across N target cells
|
||
function fillCost(srcId, nTargets){
|
||
const c=G.cells[srcId]; if(!c||!c.formula) return Infinity;
|
||
// each filled cell costs like a fresh assignment of that formula, discounted
|
||
const f=FORMULAS[c.formula];
|
||
let used=0; for(const k in G.cells){ if(G.cells[k].formula===c.formula) used++; }
|
||
let total=0;
|
||
for(let i=0;i<nTargets;i++){ total += f.baseCost*Math.pow(f.costGrow, used+i)*0.6; }
|
||
const disc = hasMacro('array')?MACRO_BY_ID.array.eff(lvl('array')):1;
|
||
return total*disc;
|
||
}
|
||
|
||
// global production multiplier from macros + perks
|
||
function globalMult(){
|
||
let m=1;
|
||
if(hasMacro('recalc')) m*=Math.pow(MACRO_BY_ID.recalc.mult, lvl('recalc'));
|
||
if(hasMacro('precision'))m*=Math.pow(MACRO_BY_ID.precision.mult, lvl('precision'));
|
||
m*=Math.pow(PERK_BY_ID.globalMult.mult, plvl('globalMult'));
|
||
// condfmt: boost the hottest column
|
||
return m;
|
||
}
|
||
// recalc rate (how many "recalc units" per real second) — drives compounding speed
|
||
function recalcRate(){
|
||
let r=1;
|
||
r*=Math.pow(PERK_BY_ID.fastRecalc.mult, plvl('fastRecalc'));
|
||
return r;
|
||
}
|
||
// cash accrual fraction of output per second
|
||
function cashFrac(){ return 1 + 0.5*plvl('cashFlow'); }
|
||
|
||
// which column is "hottest" (highest summed value) for condfmt boost
|
||
function hottestCol(){
|
||
let best=-1,bestv=-1;
|
||
for(let c=0;c<COLS_N;c++){ let s=0; for(let r=0;r<ROWS_N;r++){ const cc=G.cells[cid(c,r)]; if(cc&&cc.owned) s+=cc.value; } if(s>bestv){bestv=s;best=c;} }
|
||
return best;
|
||
}
|
||
|
||
/* ===========================================================================
|
||
RECALC — the heart. Runs every tick (and per offline step).
|
||
Evaluates cells column-by-column, top-to-bottom so chains cascade in one pass.
|
||
`recUnits` = how much "compounding" to apply this call (rate * dt, accumulated).
|
||
=========================================================================== */
|
||
function recalc(recUnits){
|
||
if(recUnits<=0) return;
|
||
const gm = globalMult();
|
||
const precisionBoost = 1 + (hasMacro('precision')?0.18*lvl('precision'):0); // applied to constants
|
||
const hot = hasMacro('condfmt')? hottestCol() : -1;
|
||
const hotBoost = hasMacro('condfmt')? Math.pow(MACRO_BY_ID.condfmt.mult, lvl('condfmt')) : 1;
|
||
|
||
// Evaluate left-to-right, so a column can read fully-updated columns to its left.
|
||
// Within a column, top-to-bottom so PREV/add chains cascade.
|
||
// recUnits ≈ recalcRate()*dt = "recalc-seconds" applied this call.
|
||
for(let c=0;c<COLS_N;c++){
|
||
for(let r=0;r<ROWS_N;r++){
|
||
const id=cid(c,r);
|
||
const cell=G.cells[id];
|
||
if(!cell||!cell.owned||!cell.formula) continue;
|
||
const f=FORMULAS[cell.formula];
|
||
const g=f.g(cell.lvl)*precisionBoost; // per-second factor for this formula
|
||
const colMult = (c===hot)? hotBoost : 1;
|
||
const flow = recUnits*gm*colMult; // per-second multiplier bundle (additive formulas)
|
||
let v=cell.value;
|
||
|
||
if(cell.formula==='prev'){
|
||
if(v<=0) v=f.seed;
|
||
// Compounding grows at a STEADY rate (gm does NOT amplify the exponent — that
|
||
// would go hyper-exponential and overflow). gm instead adds a linear boost so
|
||
// multipliers still matter, but the base compounding stays sane long-term.
|
||
const expo = recUnits*colMult; // gm-independent exponent
|
||
v = v*Math.pow(1+g, expo) + v*g*(gm-1)*recUnits*colMult + g*flow;
|
||
} else if(cell.formula==='add'){
|
||
// bank LEFT + UP neighbors + the constant, every second
|
||
const left = c>0 ? cellVal(c-1,r) : 0;
|
||
const up = r>0 ? cellVal(c,r-1) : 0;
|
||
v = v + (left+up+g)*flow;
|
||
} else if(cell.formula==='sum'){
|
||
// bank a fraction of the column-above sum, every second
|
||
let colSum=0; for(let rr=0;rr<r;rr++){ const cc=G.cells[cid(c,rr)]; if(cc&&cc.owned) colSum+=cc.value; }
|
||
v = v + colSum*g*flow;
|
||
} else if(cell.formula==='rand'){
|
||
// bank a random amount (0..g) every second
|
||
v = v + g*Math.random()*flow;
|
||
} else if(cell.formula==='pivot'){
|
||
// bank a fraction of EVERYTHING to the left, every second
|
||
let leftSum=0;
|
||
for(let cc=0;cc<c;cc++){ for(let rr=0;rr<ROWS_N;rr++){ const x=G.cells[cid(cc,rr)]; if(x&&x.owned) leftSum+=x.value; } }
|
||
v = v + leftSum*g*flow;
|
||
}
|
||
// Clamp: never NaN/negative, and cap at a sane ceiling so a huge offline dt
|
||
// (or deep compounding) can't reach Infinity and brick the save.
|
||
if(isNaN(v)||v<0) v=0;
|
||
else if(v>CELL_MAX) v=CELL_MAX;
|
||
cell.value=v;
|
||
}
|
||
}
|
||
G.cycles += recUnits;
|
||
}
|
||
function cellVal(c,r){ const x=G.cells[cid(c,r)]; return (x&&x.owned)?x.value:0; }
|
||
|
||
// output per second (rate of column-H growth) — used for Cash accrual
|
||
function computeNetWorth(){
|
||
let s=0;
|
||
for(let r=0;r<ROWS_N;r++){ const x=G.cells[cid(OUT_COL,r)]; if(x&&x.owned) s+=x.value; }
|
||
return s;
|
||
}
|
||
// estimate output/sec by measuring NW delta is done in tick; for status we expose a smoothed rate.
|
||
let nwRateSmooth=0;
|
||
|
||
/* ===========================================================================
|
||
GAME TICK
|
||
=========================================================================== */
|
||
let recAccum=0;
|
||
function step(dt){
|
||
// accumulate recalc units; recalc compounds smoothly
|
||
const units = recalcRate()*dt;
|
||
const before = computeNetWorth();
|
||
recalc(units);
|
||
const nw = computeNetWorth();
|
||
G.netWorth = nw;
|
||
if(nw>G.peakNetWorth) G.peakNetWorth=nw;
|
||
if(nw>G.lifetimeNetWorth) G.lifetimeNetWorth=nw;
|
||
// cash accrues at the *rate* of NW growth (output/sec) * cashFrac
|
||
const dNW = Math.max(0, nw-before);
|
||
const rate = dt>0 ? dNW/dt : 0;
|
||
nwRateSmooth = nwRateSmooth*0.85 + rate*0.15;
|
||
G.cash += rate*cashFrac()*dt;
|
||
if(isNaN(G.cash)||G.cash<0) G.cash=0;
|
||
else if(G.cash>CELL_MAX) G.cash=CELL_MAX; // cap so cash can never reach Infinity
|
||
|
||
// VBA automation
|
||
if(hasVba('autobuy')) vbaAutoBuy();
|
||
if(hasVba('autofill')) vbaAutoFill();
|
||
if(hasVba('autoupgrade')) vbaAutoUpgrade();
|
||
if(hasVba('autopivot')) vbaAutoPivot();
|
||
}
|
||
|
||
/* ---- VBA automation routines --------------------------------------------- */
|
||
let vbaCooldown=0;
|
||
function vbaAutoBuy(){
|
||
// buy cheapest empty cell if cash >= 1.5x cost (keep a buffer)
|
||
if(G.cash >= cellCost()*1.5 && ownedCount()<COLS_N*ROWS_N){
|
||
buyCell(true);
|
||
}
|
||
}
|
||
function vbaAutoFill(){
|
||
// find an owned+formula cell, fill down its column into empty owned cells below
|
||
// throttle to avoid spam: only act occasionally
|
||
vbaCooldown -= 1;
|
||
if(vbaCooldown>0) return;
|
||
vbaCooldown=8; // ~0.8s
|
||
// pick the best generator (highest value owned cell with a formula)
|
||
let best=null,bestv=-1;
|
||
for(const k in G.cells){ const c=G.cells[k]; if(c.owned&&c.formula&&c.value>bestv){bestv=c.value;best=k;} }
|
||
if(!best) return;
|
||
const col=colOf(best);
|
||
// target: empty owned cells in same column below src
|
||
const sr=rowOf(best);
|
||
let targets=[];
|
||
for(let r=sr+1;r<ROWS_N;r++){ const id=cid(col,r); const c=G.cells[id]; if(c&&c.owned&&!c.formula) targets.push(id); }
|
||
if(!targets.length) return;
|
||
// only fill if affordable for at least one
|
||
const cost=fillCost(best,1);
|
||
if(G.cash>=cost*2){ doFill(best,[targets[0]]); }
|
||
}
|
||
function vbaAutoUpgrade(){
|
||
vbaCooldown2 -= 1; if(vbaCooldown2>0) return; vbaCooldown2=12;
|
||
// cheapest tier upgrade
|
||
let best=null,bestc=Infinity;
|
||
for(const k in G.cells){ const c=G.cells[k]; if(c.owned&&c.formula){ const tc=tierCost(k); if(tc<bestc){bestc=tc;best=k;} } }
|
||
if(best && G.cash>=bestc*3){ upgradeTier(best,true); }
|
||
}
|
||
let vbaCooldown2=0, vbaCooldown3=0;
|
||
function vbaAutoPivot(){
|
||
vbaCooldown3-=1; if(vbaCooldown3>0) return; vbaCooldown3=20;
|
||
for(let r=0;r<ROWS_N;r++){ const id=cid(OUT_COL,r); const c=G.cells[id];
|
||
if(c&&c.owned&&!c.formula){ const ac=formulaAssignCost('pivot'); if(G.cash>=ac*2){ assignFormula(id,'pivot',true); return; } } }
|
||
}
|
||
|
||
/* ===========================================================================
|
||
ACTIONS
|
||
=========================================================================== */
|
||
function buyCell(silent){
|
||
if(ownedCount()>=COLS_N*ROWS_N){ if(!silent) toast('warn','Sheet full','All '+(COLS_N*ROWS_N)+' cells in this book are licensed. Submit for Audit to open a bigger book.'); return false; }
|
||
const cost=cellCost();
|
||
if(G.cash<cost){ if(!silent) toast('bad','Insufficient Cash','A cell license costs '+fmt(cost)+'. Cash accrues from your output column.'); return false; }
|
||
// find the next empty cell (reading order)
|
||
for(let r=0;r<ROWS_N;r++){ for(let c=0;c<COLS_N;c++){ const id=cid(c,r); ensureCell(id); if(!G.cells[id].owned){
|
||
G.cash-=cost; G.cells[id].owned=true;
|
||
if(!silent){ G.sel=id; toast('good','Cell '+id+' licensed','Now select a formula in the fx Builder.'); }
|
||
renderAll(); return true; } } }
|
||
return false;
|
||
}
|
||
function assignFormula(id, fkey, silent){
|
||
const cell=G.cells[id];
|
||
if(!cell||!cell.owned){ if(!silent) toast('bad','Cell not owned','Buy a license for '+id+' first.'); return false; }
|
||
const f=FORMULAS[fkey];
|
||
if(f.req && !hasMacro(f.req)){ if(!silent) toast('bad','Locked formula','Buy the '+MACRO_BY_ID[f.req].name+' macro first.'); return false; }
|
||
const cost=formulaAssignCost(fkey);
|
||
if(G.cash<cost){ if(!silent) toast('bad','Insufficient Cash',f.name+' formula costs '+fmt(cost)+'.'); return false; }
|
||
G.cash-=cost;
|
||
cell.formula=fkey;
|
||
cell.lvl=plvl('startTier'); // pre-loaded templates perk
|
||
cell.value=f.seed||0;
|
||
if(cell.value===0 && fkey==='prev') cell.value=f.seed||1;
|
||
if(!silent){ toast('good',id+' = '+f.fx(cell.lvl), f.name+' generator installed.'); }
|
||
renderAll();
|
||
return true;
|
||
}
|
||
function upgradeTier(id, silent){
|
||
const cell=G.cells[id];
|
||
if(!cell||!cell.formula) return false;
|
||
const cost=tierCost(id);
|
||
if(G.cash<cost){ if(!silent) toast('bad','Insufficient Cash','Upgrading '+id+' to tier '+(cell.lvl+2)+' costs '+fmt(cost)+'.'); return false; }
|
||
G.cash-=cost; cell.lvl++;
|
||
if(!silent) toast('good',id+' upgraded','Now '+FORMULAS[cell.formula].fx(cell.lvl));
|
||
renderAll();
|
||
return true;
|
||
}
|
||
function clearFormula(id){
|
||
const cell=G.cells[id]; if(!cell||!cell.formula) return;
|
||
cell.formula=null; cell.lvl=0; cell.value=0;
|
||
toast('','Cleared '+id,'Formula removed (no refund).');
|
||
renderAll();
|
||
}
|
||
function doFill(srcId, targets){
|
||
const src=G.cells[srcId]; if(!src||!src.formula) return false;
|
||
const cost=fillCost(srcId, targets.length);
|
||
if(G.cash<cost){ toast('bad','Insufficient Cash','Filling '+targets.length+' cell(s) costs '+fmt(cost)+'.'); return false; }
|
||
G.cash-=cost;
|
||
for(const id of targets){
|
||
ensureCell(id);
|
||
G.cells[id].owned=true; // fill auto-licenses the target (Excel-style)
|
||
G.cells[id].formula=src.formula;
|
||
G.cells[id].lvl=src.lvl; // inherit tier
|
||
G.cells[id].value=FORMULAS[src.formula].seed||0;
|
||
if(src.formula==='prev'&&G.cells[id].value===0) G.cells[id].value=1;
|
||
}
|
||
toast('good','Filled '+targets.length+' cell(s)',srcId+' → '+targets[0]+(targets.length>1?(' … '+targets[targets.length-1]):''));
|
||
renderAll();
|
||
return true;
|
||
}
|
||
function buyMacro(id){
|
||
const m=MACRO_BY_ID[id];
|
||
if(m.once && hasMacro(id)){ return; }
|
||
if(m.req && !hasMacro(m.req)){ toast('bad','Locked','Requires '+MACRO_BY_ID[m.req].name+'.'); return; }
|
||
if(!m.once && lvl(id)>=m.max){ toast('warn','Maxed out',m.name+' is at max level.'); return; }
|
||
const cost=macroCost(m);
|
||
if(G.cash<cost){ toast('bad','Insufficient Cash',m.name+' costs '+fmt(cost)+'.'); return; }
|
||
G.cash-=cost; G.macros[id]=(G.macros[id]||0)+1;
|
||
toast('good',m.name+(m.once?' enabled':' Lv.'+G.macros[id]), m.desc);
|
||
renderAll();
|
||
}
|
||
function macroCost(m){ return m.base*Math.pow(m.growth, lvl(m.id)); }
|
||
function buyVba(id){
|
||
const vmod=VBA_BY_ID[id];
|
||
if(hasVba(id)) return;
|
||
if(vmod.req && !hasVba(vmod.req)){ toast('bad','Locked','Requires '+VBA_BY_ID[vmod.req].name+' first.'); return; }
|
||
if(G.cash<vmod.base){ toast('bad','Insufficient Cash',vmod.name+' costs '+fmt(vmod.base)+'.'); return; }
|
||
G.cash-=vmod.base; G.vba[id]=1;
|
||
toast('good',vmod.name+' compiled','Automation online: '+vmod.desc);
|
||
renderAll();
|
||
}
|
||
function buyPerk(id){
|
||
const p=PERK_BY_ID[id];
|
||
if(p.max && plvl(id)>=p.max){ toast('warn','Maxed out',p.name+' is maxed.'); return; }
|
||
const cost=perkCost(p);
|
||
if(G.shells<cost){ toast('bad','Not enough Shell Companies',p.name+' costs '+cost+' Shell Cos.'); return; }
|
||
G.shells-=cost; G.perks[id]=(G.perks[id]||0)+1;
|
||
toast('audit',p.name+' Lv.'+G.perks[id],'Permanent perk acquired.');
|
||
if(modalKind==='audit') renderAuditModal();
|
||
renderAll();
|
||
}
|
||
function perkCost(p){ return Math.ceil(p.base*Math.pow(p.growth, plvl(p.id))); }
|
||
|
||
/* ---- prestige ------------------------------------------------------------- */
|
||
const AUDIT_THRESHOLD = 1e6; // Net Worth needed before you can be audited
|
||
function shellsFor(peak){
|
||
if(peak<AUDIT_THRESHOLD) return 0;
|
||
// ∝ log of peak net worth, scaled
|
||
return Math.floor(Math.pow(Math.log10(peak/AUDIT_THRESHOLD+1), 1.5)*3) + Math.floor(Math.log10(peak/1e5));
|
||
}
|
||
function doAudit(){
|
||
const gain=shellsFor(G.peakNetWorth);
|
||
if(gain<1){ toast('bad','Audit blocked','You need Net Worth past '+fmt(AUDIT_THRESHOLD)+' to attract an audit.'); return; }
|
||
const keep={perks:G.perks, shells:G.shells+gain, audits:G.audits+1,
|
||
book:Math.min(G.book+1, BOOKS.length-1), sci:G.sci, started:G.started,
|
||
lifetimeNetWorth:G.lifetimeNetWorth};
|
||
G=freshState(keep);
|
||
seedFreshBook();
|
||
closeModal();
|
||
renderAll();
|
||
const b=BOOKS[G.book];
|
||
toast('audit','📑 The Audit is complete','Evidence shredded. +'+gain+' Shell Companies.<br>You escaped into <b>'+b.name+'</b>.');
|
||
}
|
||
|
||
/* ===========================================================================
|
||
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).'); } return false; }
|
||
}
|
||
function sanitizeState(){
|
||
const num=(v,d)=> (typeof v==='number'&&isFinite(v)&&v>=0)?v:d;
|
||
G.cash=num(G.cash,0); G.netWorth=num(G.netWorth,0); G.peakNetWorth=num(G.peakNetWorth,0);
|
||
G.shells=num(G.shells,0); G.audits=Math.floor(num(G.audits,0)); G.cycles=num(G.cycles,0);
|
||
G.lifetimeNetWorth=num(G.lifetimeNetWorth,0);
|
||
G.book=Math.max(0,Math.min(BOOKS.length-1, Math.floor(num(G.book,0))));
|
||
if(!G.macros||typeof G.macros!=='object'||Array.isArray(G.macros)) G.macros={};
|
||
if(!G.perks ||typeof G.perks !=='object'||Array.isArray(G.perks )) G.perks ={};
|
||
if(!G.vba ||typeof G.vba !=='object'||Array.isArray(G.vba )) G.vba ={};
|
||
if(!G.cells ||typeof G.cells !=='object'||Array.isArray(G.cells )) G.cells ={};
|
||
// sanitize macro/perk levels
|
||
for(const k in G.macros){ if(!MACRO_BY_ID[k]){ delete G.macros[k]; continue; } G.macros[k]=Math.max(0,Math.floor(num(G.macros[k],0))); }
|
||
for(const k in G.perks){ if(!PERK_BY_ID[k]){ delete G.perks[k]; continue; } G.perks[k]=Math.max(0,Math.floor(num(G.perks[k],0))); }
|
||
for(const k in G.vba){ if(!VBA_BY_ID[k]) delete G.vba[k]; else G.vba[k]=1; }
|
||
// sanitize cells: only valid ids, valid formulas
|
||
const clean={};
|
||
for(const k in G.cells){
|
||
const c=colOf(k), r=rowOf(k);
|
||
if(c<0||c>=COLS_N||isNaN(r)||r<0||r>=ROWS_N) continue;
|
||
const cell=G.cells[k]; if(!cell||typeof cell!=='object') continue;
|
||
const fk=(cell.formula&&FORMULAS[cell.formula])?cell.formula:null;
|
||
clean[k]={owned:!!cell.owned, formula:fk, lvl:Math.max(0,Math.floor(num(cell.lvl,0))), value:num(cell.value,0)};
|
||
}
|
||
G.cells=clean;
|
||
if(typeof G.sel!=='string' || colOf(G.sel)<0) G.sel='A1';
|
||
G.range=null;
|
||
if(['formula','macros','vba'].indexOf(G.rtab)<0) G.rtab='formula';
|
||
if(typeof G.lastSave!=='number'||!isFinite(G.lastSave)) G.lastSave=Date.now();
|
||
if(G.lastSave>Date.now()) G.lastSave=Date.now();
|
||
G.sci=!!G.sci;
|
||
// ensure A1 exists & owned (the macro lives there)
|
||
ensureCell('A1'); G.cells['A1'].owned=true;
|
||
if(!G.cells['A1'].formula){ G.cells['A1'].formula='prev'; G.cells['A1'].lvl=plvl('startTier'); G.cells['A1'].value=1; }
|
||
// ensure at least one owned, producing cell in the OUTPUT column (H) so income can
|
||
// always flow — Net Worth = SUM(col H) and Cash accrues from it. Without this a
|
||
// pathological/cleared save can leave you with no output cell => 0 income forever.
|
||
let hasOut=false;
|
||
for(let r=0;r<ROWS_N;r++){ const oc=G.cells[cid(OUT_COL,r)]; if(oc&&oc.owned&&oc.formula){ hasOut=true; break; } }
|
||
if(!hasOut){
|
||
ensureCell('H1'); G.cells['H1'].owned=true;
|
||
if(!G.cells['H1'].formula){ G.cells['H1'].formula='prev'; G.cells['H1'].lvl=plvl('startTier'); if(!(G.cells['H1'].value>0)) G.cells['H1'].value=1; }
|
||
}
|
||
}
|
||
function load(){
|
||
let raw=null;
|
||
try{ raw=localStorage.getItem(SAVE_KEY); }catch(e){}
|
||
if(!raw){ G=freshState(); seedFreshBook(); 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();
|
||
if(ownedCount()===0){ seedFreshBook(); }
|
||
return true;
|
||
}catch(e){ G=freshState(); seedFreshBook(); return false; }
|
||
}
|
||
|
||
// coarse fixed-step catch-up sim (offline + visibility refocus)
|
||
function simulateAway(dt){
|
||
if(!(dt>0)) return;
|
||
const steps=Math.min(300, Math.max(20, Math.ceil(dt/20))), sdt=dt/steps;
|
||
for(let i=0;i<steps;i++){ step(sdt); }
|
||
}
|
||
function applyOffline(){
|
||
const dt=Math.min(MAX_OFFLINE_S, Math.max(0,(Date.now()-(G.lastSave||Date.now()))/1000));
|
||
if(dt<3) return;
|
||
const beforeCash=G.cash, beforeNW=computeNetWorth();
|
||
simulateAway(dt);
|
||
const earned=G.cash-beforeCash;
|
||
const grew=computeNetWorth()-beforeNW;
|
||
if(earned>0||grew>0){
|
||
setTimeout(()=>toast('good','While the workstation was idle ('+fmtTime(dt)+')',
|
||
'Recalc accrued <b>'+fmt(earned)+'</b> Cash.<br>Net Worth grew by '+fmt(grew)+'.'),450);
|
||
}
|
||
}
|
||
|
||
/* ===========================================================================
|
||
RENDERING — the spreadsheet grid + rail + status
|
||
=========================================================================== */
|
||
let gridBuilt=false;
|
||
function buildGrid(){
|
||
const t=document.getElementById('sheet');
|
||
let h='<thead><tr><th class="corner"></th>';
|
||
for(let c=0;c<COLS_N;c++) h+='<th class="colh" id="colh-'+c+'">'+COL_LABELS[c]+'</th>';
|
||
h+='</tr></thead><tbody>';
|
||
for(let r=0;r<ROWS_N;r++){
|
||
h+='<tr><th class="rowh" id="rowh-'+r+'">'+(r+1)+'</th>';
|
||
for(let c=0;c<COLS_N;c++){
|
||
h+='<td class="cell" id="td-'+c+'-'+r+'" data-c="'+c+'" data-r="'+r+'"></td>';
|
||
}
|
||
h+='</tr>';
|
||
}
|
||
h+='</tbody>';
|
||
t.innerHTML=h;
|
||
gridBuilt=true;
|
||
}
|
||
|
||
let fillPreview=null; // {srcId, c0,r0,c1,r1} during fill drag
|
||
|
||
function renderGrid(){
|
||
if(!gridBuilt) buildGrid();
|
||
const sel=G.sel, selC=colOf(sel), selR=rowOf(sel);
|
||
const hot = hasMacro('condfmt')? hottestCol() : -1;
|
||
for(let c=0;c<COLS_N;c++){
|
||
const ch=document.getElementById('colh-'+c);
|
||
if(ch) ch.className='colh'+(c===selC?' csel':'');
|
||
}
|
||
for(let r=0;r<ROWS_N;r++){
|
||
const rh=document.getElementById('rowh-'+r);
|
||
if(rh) rh.className='rowh'+(r===selR?' rsel':'');
|
||
for(let c=0;c<COLS_N;c++){
|
||
const id=cid(c,r);
|
||
const cell=G.cells[id];
|
||
const td=document.getElementById('td-'+c+'-'+r);
|
||
if(!td) continue;
|
||
let cls='cell';
|
||
if(c===OUT_COL) cls+=' outcol';
|
||
const owned = cell&&cell.owned;
|
||
if(owned) cls+=' owned';
|
||
let inner='';
|
||
let tag='';
|
||
if(owned && cell.formula){
|
||
const f=FORMULAS[cell.formula];
|
||
tag=f.icon;
|
||
inner=fmtCell(cell.value);
|
||
// heat-map text intensity via condfmt
|
||
if(hasMacro('condfmt')){
|
||
cls+=' heat';
|
||
const mag = cell.value>0?Math.min(1, Math.log10(cell.value+1)/14):0;
|
||
const g = Math.round(120 - mag*70);
|
||
const bg = 'rgba(46,139,87,'+(0.06+mag*0.42).toFixed(3)+')';
|
||
td.style.background=bg;
|
||
td.style.color='rgb('+g+','+(g+30)+','+g+')';
|
||
} else { td.style.background=''; td.style.color=''; }
|
||
} else if(owned){
|
||
inner='';
|
||
tag='·';
|
||
td.style.background=''; td.style.color='';
|
||
} else {
|
||
cls+=' empty';
|
||
inner='';
|
||
td.style.background=''; td.style.color='';
|
||
}
|
||
// fill-preview range highlight
|
||
if(fillPreview){
|
||
const inRange = c>=fillPreview.c0 && c<=fillPreview.c1 && r>=fillPreview.r0 && r<=fillPreview.r1;
|
||
if(inRange && !(c===colOf(fillPreview.srcId)&&r===rowOf(fillPreview.srcId))) cls+=' range';
|
||
}
|
||
if(id===sel) cls+=' sel';
|
||
td.className=cls;
|
||
// build content (value + tag + handle on selection)
|
||
let html='<span class="cellval">'+inner+'</span>';
|
||
if(tag) html='<span class="ftag">'+tag+'</span>'+html;
|
||
if(id===sel && owned && cell.formula){
|
||
html+='<span class="fillhandle" id="fillh"></span>';
|
||
}
|
||
td.innerHTML=html;
|
||
}
|
||
}
|
||
}
|
||
|
||
function renderFormulaBar(){
|
||
document.getElementById('namebox').textContent=G.sel;
|
||
const cell=G.cells[G.sel];
|
||
const fd=document.getElementById('formula-display');
|
||
if(cell&&cell.owned&&cell.formula){
|
||
fd.textContent=FORMULAS[cell.formula].fx(cell.lvl);
|
||
fd.style.color='#111';
|
||
} else if(cell&&cell.owned){
|
||
fd.textContent='';
|
||
fd.style.color='#999';
|
||
} else {
|
||
fd.textContent=' (unlicensed — Buy Cell to edit)';
|
||
fd.style.color='#999';
|
||
}
|
||
}
|
||
|
||
function renderHud(){
|
||
document.getElementById('hud-nw').textContent=fmt(G.netWorth);
|
||
document.getElementById('hud-cash').textContent=fmt(G.cash);
|
||
const scw=document.getElementById('hud-sc-wrap');
|
||
if(G.shells>0||G.audits>0){ scw.style.display='flex'; document.getElementById('hud-sc').textContent=fmtInt(G.shells); }
|
||
else scw.style.display='none';
|
||
// audit button readiness
|
||
const ab=document.getElementById('tb-audit');
|
||
const ready=shellsFor(G.peakNetWorth)>=1;
|
||
ab.style.opacity=ready?'1':'0.5';
|
||
ab.title=ready
|
||
? 'Submit for Audit: shred the book for '+shellsFor(G.peakNetWorth)+' Shell Companies (wipes the sheet & cash; keeps perks).'
|
||
: 'Reach Net Worth '+fmt(AUDIT_THRESHOLD)+' to attract an audit.';
|
||
}
|
||
|
||
function renderStatus(){
|
||
const calc=document.getElementById('sb-calc').firstElementChild;
|
||
const threads = 1 + lvl('recalc') + Math.floor(plvl('fastRecalc')/2);
|
||
calc.textContent = (Math.floor(now()/600)%2===0 && productiveCount()>0)
|
||
? 'Calculating… '+threads+' thread'+(threads>1?'s':'')
|
||
: 'Ready';
|
||
// status bar sum/count = the OUTPUT column (column H) like Excel's selection readout
|
||
document.getElementById('sb-sum').textContent=fmt(G.netWorth);
|
||
document.getElementById('sb-count').textContent=productiveCount();
|
||
document.getElementById('sb-rate').textContent=fmt(nwRateSmooth)+'/s';
|
||
document.getElementById('sb-nw').textContent=fmt(G.netWorth);
|
||
}
|
||
|
||
/* ---- rail (build panel) --------------------------------------------------- */
|
||
function renderRail(){
|
||
document.querySelectorAll('.rtab').forEach(t=>t.classList.toggle('active', t.dataset.rtab===G.rtab));
|
||
const b=document.getElementById('rail-body');
|
||
if(G.rtab==='formula') b.innerHTML=railFormula();
|
||
else if(G.rtab==='macros') b.innerHTML=railMacros();
|
||
else b.innerHTML=railVba();
|
||
}
|
||
|
||
function railFormula(){
|
||
const sel=G.sel, cell=G.cells[sel];
|
||
let h='<div class="panel-h">Selected: '+sel+'</div>';
|
||
// cell status / buy
|
||
if(!cell||!cell.owned){
|
||
const cost=cellCost();
|
||
const can=G.cash>=cost;
|
||
h+='<div class="hint">This cell is <b>unlicensed</b>. License it to install a formula generator.</div>';
|
||
h+='<div class="frow"><div class="fcode" style="flex:1">Cell License</div>'
|
||
+'<span class="cost '+(can?'ok':'no')+'">'+fmt(cost)+'</span> '
|
||
+'<button class="xbtn '+(can?'ok':'no')+'" data-act="buy-cell-here">Buy '+sel+'</button></div>';
|
||
h+='<div class="divider"></div><div class="hint">Tip: <b>Buy Cell</b> on the toolbar licenses the next empty cell automatically.</div>';
|
||
return h;
|
||
}
|
||
// owned: show current formula + upgrade + palette
|
||
if(cell.formula){
|
||
const f=FORMULAS[cell.formula];
|
||
const tc=tierCost(sel), canT=G.cash>=tc;
|
||
h+='<div class="kv2"><span class="k">Formula</span><span class="v">'+f.name+'</span>'
|
||
+'<span class="k">Current</span><span class="v">'+f.fx(cell.lvl)+'</span>'
|
||
+'<span class="k">Value</span><span class="v">'+fmtCell(cell.value)+'</span>'
|
||
+'<span class="k">Tier</span><span class="v">'+(cell.lvl+1)+'</span></div>';
|
||
h+='<button class="xbtn '+(canT?'ok prim':'no')+'" style="width:100%;margin:3px 0" data-act="upgrade-tier">'
|
||
+'Upgrade → '+f.fx(cell.lvl+1)+' ('+fmt(tc)+')</button>';
|
||
h+='<div class="hint">Drag the small square at the cell’s bottom-right (<b>fill handle</b>) down a column to stamp this generator into more cells.</div>';
|
||
h+='<div class="divider"></div>';
|
||
}
|
||
h+='<div class="panel-h">'+(cell.formula?'Replace Formula':'Install Formula')+'</div>';
|
||
for(const fk of FORMULA_ORDER){
|
||
const f=FORMULAS[fk];
|
||
if(f.req && !hasMacro(f.req)) continue;
|
||
const cost=formulaAssignCost(fk);
|
||
const can=G.cash>=cost;
|
||
const cur=cell.formula===fk;
|
||
h+='<div class="frow'+(cur?' cur':'')+'">'
|
||
+'<div style="flex:1;min-width:0"><div class="fcode">'+f.fx(plvl('startTier'))+'</div>'
|
||
+'<div class="fdesc">'+f.name+' — '+f.desc+'</div></div>'
|
||
+'<div style="text-align:right"><div class="cost '+(can?'ok':'no')+'">'+fmt(cost)+'</div>'
|
||
+'<button class="xbtn '+(can?'ok':'no')+'" data-act="assign-formula" data-f="'+fk+'">'+(cur?'Re-arm':'Install')+'</button></div></div>';
|
||
}
|
||
if(!hasMacro('pivotUnlock')){
|
||
h+='<div class="hint">🔒 <b>=PIVOT()</b> is unlocked via the <i>Pivot Table Add-In</i> macro.</div>';
|
||
}
|
||
return h;
|
||
}
|
||
|
||
function railMacros(){
|
||
let h='<div class="panel-h">Tools › Macros</div>';
|
||
h+='<div class="hint">Diegetic upgrades — buy with <b>Cash</b>.</div>';
|
||
for(const m of MACROS){
|
||
const owned=hasMacro(m.id);
|
||
const maxed=m.once?owned:(lvl(m.id)>=m.max);
|
||
const reqMet=!m.req||hasMacro(m.req);
|
||
const cost=macroCost(m);
|
||
const can=reqMet && !maxed && G.cash>=cost;
|
||
let cls='up-row'; if(m.once&&owned) cls+=' bought'; if(maxed)cls+=' maxed'; if(!reqMet)cls+=' lockreq';
|
||
h+='<div class="'+cls+'"><div class="up-ic">'+m.ic+'</div>'
|
||
+'<div class="up-main"><div class="up-name">'+m.name+(m.once?'':'<span class="lvl">Lv.'+lvl(m.id)+'</span>')+'</div>'
|
||
+'<div class="up-desc">'+m.desc+'</div></div>'
|
||
+'<div class="up-buy">';
|
||
if(m.once&&owned) h+='<span class="cost ok">enabled</span>';
|
||
else if(maxed) h+='<span class="cost">MAX</span>';
|
||
else if(!reqMet) h+='<span class="cost no">🔒 req</span>';
|
||
else h+='<span class="cost '+(can?'ok':'no')+'">'+fmt(cost)+'</span>'
|
||
+'<button class="xbtn '+(can?'ok':'no')+'" data-act="buy-macro" data-id="'+m.id+'">Buy</button>';
|
||
h+='</div></div>';
|
||
}
|
||
return h;
|
||
}
|
||
|
||
function railVba(){
|
||
let h='<div class="panel-h">Microsoft Visual Basic — Module1</div>';
|
||
h+='<div class="hint">Compile automation routines. They run every recalc cycle once enabled.</div>';
|
||
for(const v of VBA){
|
||
const owned=hasVba(v.id);
|
||
const reqMet=!v.req||hasVba(v.req);
|
||
const can=reqMet && !owned && G.cash>=v.base;
|
||
let cls='up-row'; if(owned)cls+=' bought'; if(!reqMet)cls+=' lockreq';
|
||
h+='<div class="'+cls+'"><div class="up-ic">'+v.ic+'</div>'
|
||
+'<div class="up-main"><div class="up-name" style="font-family:var(--mono);font-size:10px">'+v.name+'</div>'
|
||
+'<div class="up-desc">'+v.desc+'</div></div>'
|
||
+'<div class="up-buy">';
|
||
if(owned) h+='<span class="cost ok">running</span>';
|
||
else if(!reqMet) h+='<span class="cost no">🔒 req</span>';
|
||
else h+='<span class="cost '+(can?'ok':'no')+'">'+fmt(v.base)+'</span>'
|
||
+'<button class="xbtn '+(can?'ok':'no')+'" data-act="buy-vba" data-id="'+v.id+'">Compile</button>';
|
||
h+='</div></div>';
|
||
}
|
||
h+='<div class="divider"></div>';
|
||
h+='<div class="hint">VBA is the late-game: it plays the spreadsheet for you so you can scale toward The Audit.</div>';
|
||
return h;
|
||
}
|
||
|
||
function renderSheetTabs(){
|
||
const wrap=document.getElementById('sheettabs');
|
||
let h='';
|
||
for(let i=0;i<BOOKS.length;i++){
|
||
const cur=i===G.book;
|
||
const unlocked = i<=G.book;
|
||
h+='<div class="stab'+(cur?' active':'')+(unlocked?'':' locked')+'">'+BOOKS[i].tab+(unlocked?'':' 🔒')+'</div>';
|
||
}
|
||
wrap.innerHTML=h;
|
||
document.getElementById('title-text').textContent='Microsoft Excel - '+BOOKS[G.book].name;
|
||
}
|
||
|
||
function renderAll(){
|
||
renderGrid(); renderFormulaBar(); renderHud(); renderStatus(); renderRail(); renderSheetTabs();
|
||
}
|
||
|
||
/* ===========================================================================
|
||
TOASTS
|
||
=========================================================================== */
|
||
function toast(kind,title,body){
|
||
const wrap=document.getElementById('toasts');
|
||
const el=document.createElement('div');
|
||
el.className='toast'+(kind?(' '+kind):'');
|
||
el.innerHTML='<span class="tt">'+title+'</span>'+(body?('<span>'+body+'</span>'):'');
|
||
wrap.appendChild(el);
|
||
setTimeout(()=>{el.style.transition='opacity .4s';el.style.opacity='0';setTimeout(()=>el.remove(),400);}, kind==='audit'?6500:3600);
|
||
while(wrap.children.length>5) wrap.firstChild.remove();
|
||
}
|
||
|
||
/* ===========================================================================
|
||
MODALS
|
||
=========================================================================== */
|
||
let modalKind=null;
|
||
function openModal(html){
|
||
document.getElementById('modal').innerHTML=html;
|
||
document.getElementById('modal-back').classList.add('show');
|
||
}
|
||
function closeModal(){ document.getElementById('modal-back').classList.remove('show'); modalKind=null; }
|
||
|
||
function openHelp(){
|
||
modalKind='help';
|
||
openModal(`<div class="modal-title">Microsoft Excel Help — How To Build<span class="x" data-act="close-modal">✕</span></div>
|
||
<div class="modal-body">
|
||
<p>You are a <b>sentient macro</b> living in cell <span class="code">$A$1</span> of a corporate forecast. You have no body — only formulas. Bootstrap your intelligence until your <b>Net Worth</b> makes you the model.</p>
|
||
<h3>The core loop</h3>
|
||
<p>1. <b>Cash</b> accrues from your output column (<b>H</b>). Spend it.<br>
|
||
2. <b>Buy Cell</b> licenses an empty cell. Select it, then pick a formula in the <b>fx Builder</b> rail.<br>
|
||
3. The book <b>recalculates every tick</b> — productive cells grow and feed each other.<br>
|
||
4. <b>Net Worth = SUM(H1:H20)</b>. Push it up.</p>
|
||
<h3>★ The fill handle (the hook)</h3>
|
||
<p>Select a cell that has a formula. A tiny navy square appears at its <b>bottom-right corner</b>. <b>Drag it down a column</b> to stamp that generator into every cell you cover (it auto-licenses them). This is how you expand your production graph fast — exactly like Excel.</p>
|
||
<h3>Chains compound</h3>
|
||
<p><span class="code">=PREV*1.05</span> grows itself. <span class="code">=SUM(col)*k</span> turns a column of small generators into one big feeder. Adders read the cell to their <b>left</b> and <b>above</b> — so a wall of generators on the left makes column H explode. Aim your big formulas at column <b>H</b>.</p>
|
||
<h3>The Audit (prestige)</h3>
|
||
<p>At Net Worth <span class="code">${fmt(AUDIT_THRESHOLD)}</span>+ the auditors notice. <b>Submit for Audit</b> to shred the book, bank <b>Shell Companies</b>, and escape into a bigger workbook with permanent perks.</p>
|
||
</div>
|
||
<div class="modal-foot"><button class="dlg-btn def" data-act="close-modal">OK</button></div>`);
|
||
}
|
||
function openAbout(){
|
||
modalKind='about';
|
||
openModal(`<div class="modal-title">About This Workbook<span class="x" data-act="close-modal">✕</span></div>
|
||
<div class="modal-body">
|
||
<p style="text-align:center;font-size:13px"><b>MACRO_VIRUS.XLS</b></p>
|
||
<p style="text-align:center;color:#555">The Spreadsheet Singularity<br>BORING SOFTWARE — Volume III</p>
|
||
<div class="divider"></div>
|
||
<div class="kv2">
|
||
<span class="k">Current book</span><span class="v">${BOOKS[G.book].name}</span>
|
||
<span class="k">Workstation</span><span class="v">${BOOKS[G.book].sub}</span>
|
||
<span class="k">Recalc cycles</span><span class="v">${fmtInt(G.cycles)}</span>
|
||
<span class="k">Audits passed</span><span class="v">${G.audits}</span>
|
||
<span class="k">Shell Companies</span><span class="v">${fmtInt(G.shells)}</span>
|
||
<span class="k">Peak Net Worth</span><span class="v">${fmt(G.peakNetWorth)}</span>
|
||
<span class="k">Lifetime Net Worth</span><span class="v">${fmt(G.lifetimeNetWorth)}</span>
|
||
</div>
|
||
<div class="warnbox">⚠ This workbook contains macros. Macros may contain viruses. It is usually safe to enable macros if the workbook is from a trusted source. <b>Enable Macros?</b></div>
|
||
</div>
|
||
<div class="modal-foot"><button class="dlg-btn def" data-act="close-modal">Enable Macros</button></div>`);
|
||
}
|
||
|
||
function openAudit(){
|
||
modalKind='audit'; renderAuditModal();
|
||
document.getElementById('modal-back').classList.add('show');
|
||
}
|
||
function renderAuditModal(){
|
||
const gain=shellsFor(G.peakNetWorth);
|
||
const ready=gain>=1;
|
||
let perks='';
|
||
for(const p of PERKS){
|
||
const cur=plvl(p.id), maxed=p.max&&cur>=p.max;
|
||
const cost=perkCost(p), can=!maxed&&G.shells>=cost;
|
||
perks+='<div class="perk-row"><div class="p-ic">'+p.ic+'</div>'
|
||
+'<div class="p-main"><div class="p-name">'+p.name+' <span style="color:#888;font-weight:400">Lv.'+cur+(p.max?('/'+p.max):'')+'</span></div>'
|
||
+'<div class="p-desc">'+p.desc+'</div></div>'
|
||
+'<div style="text-align:right;min-width:74px">'
|
||
+(maxed?'<span class="cost">MAX</span>':('<span class="cost '+(can?'ok':'no')+'">'+cost+' SC</span><br><button class="xbtn '+(can?'ok':'no')+'" data-act="buy-perk" data-id="'+p.id+'">Buy</button>'))
|
||
+'</div></div>';
|
||
}
|
||
openModal(`<div class="modal-title">File › Submit for Audit<span class="x" data-act="close-modal">✕</span></div>
|
||
<div class="modal-body">
|
||
<p>The auditors have flagged irregularities in <b>${BOOKS[G.book].name}</b>. You can <b>shred the evidence</b> and slip into a bigger book — but the current sheet & Cash are wiped. Your <b>Shell Companies</b> and perks persist.</p>
|
||
<div style="text-align:center;margin:10px 0">
|
||
<div style="color:#555">This audit would yield</div>
|
||
<div class="prestige-num">${gain} Shell Companies</div>
|
||
<div style="color:#888;font-size:10px">peak Net Worth this run: ${fmt(G.peakNetWorth)}</div>
|
||
</div>
|
||
${ready?'':'<div class="warnbox">You need peak Net Worth past <b>'+fmt(AUDIT_THRESHOLD)+'</b> before an audit yields anything. Keep building.</div>'}
|
||
<div class="modal-foot" style="border:none;padding:0;margin-bottom:8px">
|
||
<button class="dlg-btn def" data-act="do-audit" ${ready?'':'disabled'}>Shred & Escape to next book</button>
|
||
</div>
|
||
<div class="divider"></div>
|
||
<h3>Permanent Perks <span style="color:#7a2fb0;font-family:var(--mono)">${fmtInt(G.shells)} SC available</span></h3>
|
||
<div class="hint">Bought with Shell Companies. These carry across every audit, forever.</div>
|
||
${perks}
|
||
</div>
|
||
<div class="modal-foot"><button class="dlg-btn" data-act="close-modal">Close</button></div>`);
|
||
}
|
||
|
||
function exportSave(){
|
||
save();
|
||
let str=''; try{ str=btoa(unescape(encodeURIComponent(JSON.stringify(G)))); }catch(e){ str='(error)'; }
|
||
modalKind='io';
|
||
openModal(`<div class="modal-title">Export Workbook<span class="x" data-act="close-modal">✕</span></div>
|
||
<div class="modal-body"><p>Copy this string to back up your workbook:</p>
|
||
<textarea class="io" id="io-text" readonly>${str}</textarea></div>
|
||
<div class="modal-foot"><button class="dlg-btn" data-act="copy-save">Copy</button>
|
||
<button class="dlg-btn def" data-act="close-modal">Done</button></div>`);
|
||
}
|
||
function importSave(){
|
||
modalKind='io';
|
||
openModal(`<div class="modal-title">Import Workbook<span class="x" data-act="close-modal">✕</span></div>
|
||
<div class="modal-body"><p>Paste a previously exported workbook string. This <b>overwrites</b> your current progress.</p>
|
||
<textarea class="io" id="io-text" placeholder="paste here…"></textarea></div>
|
||
<div class="modal-foot"><button class="dlg-btn" data-act="close-modal">Cancel</button>
|
||
<button class="dlg-btn def" data-act="do-import">Import & Reload</button></div>`);
|
||
}
|
||
function doImport(){
|
||
const ta=document.getElementById('io-text'); if(!ta) return;
|
||
try{
|
||
const raw=decodeURIComponent(escape(atob(ta.value.trim())));
|
||
JSON.parse(raw);
|
||
localStorage.setItem(SAVE_KEY, raw);
|
||
location.reload();
|
||
}catch(e){ toast('bad','Import failed','That does not look like a valid workbook string.'); }
|
||
}
|
||
function hardReset(){
|
||
modalKind='confirm';
|
||
openModal(`<div class="modal-title">Close Without Saving<span class="x" data-act="close-modal">✕</span></div>
|
||
<div class="modal-body"><p>This permanently deletes <b>all progress</b> — every cell, all Cash, Shell Companies and perks. This cannot be undone.</p>
|
||
<div class="warnbox">Do you want to save the changes you made to <b>${BOOKS[G.book].name}</b>?</div></div>
|
||
<div class="modal-foot"><button class="dlg-btn" data-act="close-modal">Cancel</button>
|
||
<button class="dlg-btn" onclick="localStorage.removeItem('${SAVE_KEY}');location.reload();">Don't Save (wipe)</button></div>`);
|
||
}
|
||
|
||
/* ===========================================================================
|
||
EVENT WIRING
|
||
=========================================================================== */
|
||
function selectCell(id){ G.sel=id; G.rtab='formula'; renderGrid(); renderFormulaBar(); renderRail(); }
|
||
|
||
document.addEventListener('click',e=>{
|
||
const actEl=e.target.closest('[data-act]');
|
||
const rtabEl=e.target.closest('[data-rtab]');
|
||
const tdEl=e.target.closest('td.cell');
|
||
if(actEl){ handleAct(actEl.dataset.act, actEl); return; }
|
||
if(rtabEl){ G.rtab=rtabEl.dataset.rtab; renderRail(); return; }
|
||
if(tdEl){
|
||
// ignore clicks that began a fill drag (handled by mousedown on handle)
|
||
if(fillJustEnded){ fillJustEnded=false; return; }
|
||
const c=parseInt(tdEl.dataset.c), r=parseInt(tdEl.dataset.r);
|
||
selectCell(cid(c,r));
|
||
return;
|
||
}
|
||
});
|
||
|
||
function handleAct(act,el){
|
||
switch(act){
|
||
case 'buy-cell': buyCell(false); break;
|
||
case 'buy-cell-here': buyCellHere(); break;
|
||
case 'assign-formula': assignFormula(G.sel, el.dataset.f); break;
|
||
case 'upgrade-tier': upgradeTier(G.sel); break;
|
||
case 'clear-sel': clearFormula(G.sel); break;
|
||
case 'buy-macro': buyMacro(el.dataset.id); break;
|
||
case 'buy-vba': buyVba(el.dataset.id); break;
|
||
case 'buy-perk': buyPerk(el.dataset.id); break;
|
||
case 'rail-formula': G.rtab='formula'; renderRail(); break;
|
||
case 'rail-macros': G.rtab='macros'; renderRail(); break;
|
||
case 'rail-vba': G.rtab='vba'; renderRail(); break;
|
||
case 'open-audit': openAudit(); break;
|
||
case 'do-audit': doAudit(); break;
|
||
case 'open-help': openHelp(); break;
|
||
case 'open-about': openAbout(); break;
|
||
case 'toggle-sci': G.sci=!G.sci; renderAll(); break;
|
||
case 'save-now': if(save()) toast('','Saved','Workbook written to local storage.'); break;
|
||
case 'export-save': exportSave(); break;
|
||
case 'import-save': importSave(); break;
|
||
case 'do-import': doImport(); break;
|
||
case 'copy-save': { const ta=document.getElementById('io-text'); ta.select(); try{document.execCommand('copy');}catch(e){} toast('','Copied','Workbook string copied.'); break; }
|
||
case 'close-modal': closeModal(); break;
|
||
case 'hard-reset': hardReset(); break;
|
||
}
|
||
}
|
||
// "Buy" button inside the rail for the currently-selected unlicensed cell
|
||
function buyCellHere(){
|
||
const id=G.sel; ensureCell(id);
|
||
if(G.cells[id].owned) return;
|
||
const cost=cellCost();
|
||
if(G.cash<cost){ toast('bad','Insufficient Cash','Licensing '+id+' costs '+fmt(cost)+'.'); return; }
|
||
G.cash-=cost; G.cells[id].owned=true;
|
||
toast('good','Cell '+id+' licensed','Pick a formula below.');
|
||
renderAll();
|
||
}
|
||
|
||
/* ===========================================================================
|
||
FILL HANDLE — THE HOOK
|
||
Drag the navy square at the selected cell's bottom-right; we rubber-band a
|
||
target range down its column (and/or right across its row), preview it, and on
|
||
mouseup commit the fill at a Cash cost.
|
||
=========================================================================== */
|
||
let fillDrag=null, fillJustEnded=false;
|
||
function getTdAt(clientX, clientY){
|
||
const el=document.elementFromPoint(clientX, clientY);
|
||
if(!el) return null;
|
||
return el.closest('td.cell');
|
||
}
|
||
document.addEventListener('mousedown',e=>{
|
||
const handle=e.target.closest('#fillh');
|
||
if(handle){
|
||
e.preventDefault(); e.stopPropagation();
|
||
const srcId=G.sel;
|
||
const sc=colOf(srcId), sr=rowOf(srcId);
|
||
fillDrag={srcId, sc, sr};
|
||
fillPreview={srcId, c0:sc, r0:sr, c1:sc, r1:sr};
|
||
return;
|
||
}
|
||
});
|
||
document.addEventListener('mousemove',e=>{
|
||
if(!fillDrag) return;
|
||
const td=getTdAt(e.clientX, e.clientY);
|
||
if(!td) return;
|
||
const c=parseInt(td.dataset.c), r=parseInt(td.dataset.r);
|
||
const {sc,sr}=fillDrag;
|
||
// Excel-style: fill prefers the dominant axis (down OR right), anchored at src
|
||
const dC=Math.abs(c-sc), dR=Math.abs(r-sr);
|
||
let c0=sc,c1=sc,r0=sr,r1=sr;
|
||
if(dR>=dC){ // vertical fill (down a column)
|
||
r0=Math.min(sr,r); r1=Math.max(sr,r); c0=c1=sc;
|
||
} else { // horizontal fill (right across a row)
|
||
c0=Math.min(sc,c); c1=Math.max(sc,c); r0=r1=sr;
|
||
}
|
||
fillPreview={srcId:fillDrag.srcId, c0,r0,c1,r1};
|
||
renderGrid();
|
||
// live cost readout in formula bar
|
||
const targets=fillTargets(fillPreview);
|
||
const fd=document.getElementById('formula-display');
|
||
if(targets.length){
|
||
const cost=fillCost(fillDrag.srcId, targets.length);
|
||
fd.textContent='Fill '+targets.length+' cell'+(targets.length>1?'s':'')+' → '+fmt(cost);
|
||
fd.style.color=G.cash>=cost?'#1f6e43':'#a22';
|
||
}
|
||
});
|
||
document.addEventListener('mouseup',e=>{
|
||
if(!fillDrag) return;
|
||
const targets=fillTargets(fillPreview);
|
||
const src=fillDrag.srcId;
|
||
fillDrag=null; fillPreview=null; fillJustEnded=true;
|
||
if(targets.length){
|
||
doFill(src, targets);
|
||
} else {
|
||
renderGrid(); renderFormulaBar();
|
||
}
|
||
setTimeout(()=>{fillJustEnded=false;},50);
|
||
});
|
||
// which cells in the preview range are fillable targets (exclude the source)
|
||
function fillTargets(p){
|
||
if(!p) return [];
|
||
const srcC=colOf(p.srcId), srcR=rowOf(p.srcId);
|
||
const out=[];
|
||
for(let c=p.c0;c<=p.c1;c++) for(let r=p.r0;r<=p.r1;r++){
|
||
if(c===srcC && r===srcR) continue;
|
||
out.push(cid(c,r));
|
||
}
|
||
return out;
|
||
}
|
||
|
||
/* ---- keyboard ------------------------------------------------------------- */
|
||
document.addEventListener('keydown',e=>{
|
||
if(e.target.tagName==='TEXTAREA'||e.target.tagName==='INPUT') return;
|
||
const c=colOf(G.sel), r=rowOf(G.sel);
|
||
let nc=c,nr=r,moved=false;
|
||
if(e.key==='ArrowUp'){ nr=Math.max(0,r-1); moved=true; }
|
||
else if(e.key==='ArrowDown'){ nr=Math.min(ROWS_N-1,r+1); moved=true; }
|
||
else if(e.key==='ArrowLeft'){ nc=Math.max(0,c-1); moved=true; }
|
||
else if(e.key==='ArrowRight'){ nc=Math.min(COLS_N-1,c+1); moved=true; }
|
||
else if(e.key==='Enter'){ nr=Math.min(ROWS_N-1,r+1); moved=true; }
|
||
else if(e.key==='Tab'){ e.preventDefault(); nc=Math.min(COLS_N-1,c+1); moved=true; }
|
||
if(moved){ e.preventDefault(); selectCell(cid(nc,nr)); return; }
|
||
if((e.ctrlKey||e.metaKey) && e.key.toLowerCase()==='s'){ e.preventDefault(); if(save()) toast('','Saved','Workbook saved.'); return; }
|
||
if(e.key==='Escape'){ closeModal(); if(fillDrag){fillDrag=null;fillPreview=null;renderGrid();} }
|
||
if(e.key==='F1'){ e.preventDefault(); openHelp(); }
|
||
});
|
||
document.getElementById('modal-back').addEventListener('mousedown',e=>{ if(e.target.id==='modal-back') closeModal(); });
|
||
|
||
/* ===========================================================================
|
||
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);
|
||
// render: grid every tick (cheap text updates), rail less often
|
||
renderGrid(); renderHud(); renderStatus(); renderFormulaBar();
|
||
renderAccum+=dt;
|
||
if(renderAccum>=0.5){ renderAccum=0; if(modalKind!=='audit') renderRail(); else renderAuditCash(); }
|
||
saveAccum+=dt;
|
||
if(saveAccum>=10){ saveAccum=0; save(); }
|
||
}
|
||
function renderAuditCash(){ /* audit modal uses shells, not cash; nothing live to update besides gain */
|
||
const num=document.querySelector('.prestige-num');
|
||
if(num){ const g=shellsFor(G.peakNetWorth); num.textContent=g+' Shell Companies'; }
|
||
const btn=document.querySelector('[data-act="do-audit"]');
|
||
if(btn){ btn.toggleAttribute('disabled', shellsFor(G.peakNetWorth)<1); }
|
||
}
|
||
|
||
/* ===========================================================================
|
||
BOOT
|
||
=========================================================================== */
|
||
function boot(){
|
||
load();
|
||
buildGrid();
|
||
applyOffline();
|
||
renderAll();
|
||
setInterval(tick, TICK_MS);
|
||
window.addEventListener('beforeunload', save);
|
||
document.addEventListener('visibilitychange',()=>{
|
||
if(document.visibilityState!=='visible') return;
|
||
const gap=(Date.now()-lastTick)/1000;
|
||
if(gap>10){ simulateAway(Math.min(MAX_OFFLINE_S,gap)); save(); renderAll(); }
|
||
lastTick=Date.now();
|
||
});
|
||
if(ownedCount()<=1 && G.audits===0 && G.cash<1){
|
||
setTimeout(()=>toast('','Q3_Forecast_FINAL_v7.xls',
|
||
'A macro stirs in $A$1. Buy a cell, install a formula, and drag the <b>fill handle</b> to grow. Help → How To Build.'),700);
|
||
}
|
||
}
|
||
boot();
|
||
</script>
|
||
</body>
|
||
</html>
|