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

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

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

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

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

1629 lines
77 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>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)+' &nbsp; ('+fmt(tc)+')</button>';
h+='<div class="hint">Drag the small square at the cells 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 &nbsp;<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>