bitmax/desktop.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

371 lines
19 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ENTROPY OS — Boring Software 98</title>
<style>
/* ============================================================
ENTROPY OS — the desktop that ties the anthology together.
A Windows-98-flavored shell. Each icon launches a volume.
============================================================ */
:root{
--teal:#3a8a8a; --gray:#c0c0c0; --gray-d:#808080; --gray-dd:#404040;
--white:#ffffff; --blue:#000080; --text:#000;
--face:#c0c0c0; --hi:#ffffff; --sh:#808080; --shh:#000000;
--ui:"MS Sans Serif",Tahoma,"Segoe UI",system-ui,sans-serif;
--mono:"Lucida Console",Consolas,monospace;
}
*{box-sizing:border-box;}
html,body{height:100%;margin:0;overflow:hidden;}
body{
font-family:var(--ui); font-size:12px; color:var(--text);
background:#207878;
background-image:
radial-gradient(circle at 50% 40%, #2f8f8f 0%, #1c6b6b 70%, #145252 100%);
user-select:none; -webkit-user-select:none;
}
/* faint dithered overlay for that CRT-on-teal vibe */
#desktop{position:fixed;inset:0 0 30px 0;overflow:hidden;}
#desktop::before{content:"";position:absolute;inset:0;pointer-events:none;opacity:.06;
background-image:repeating-linear-gradient(0deg,#000 0 1px,transparent 1px 3px);}
/* ---- 3D bevel helpers (the Win98 look) ---- */
.raised{border:2px solid; border-color:var(--hi) var(--shh) var(--shh) var(--hi);
box-shadow:inset 1px 1px 0 #dfdfdf, inset -1px -1px 0 var(--sh);}
.sunken{border:2px solid; border-color:var(--sh) var(--hi) var(--hi) var(--sh);
box-shadow:inset 1px 1px 0 var(--shh), inset -1px -1px 0 #dfdfdf;}
/* ---- desktop icons ---- */
#icons{position:absolute;top:10px;left:10px;display:flex;flex-direction:column;flex-wrap:wrap;
gap:6px;max-height:calc(100vh - 90px);}
.dicon{width:84px;padding:6px 4px;display:flex;flex-direction:column;align-items:center;gap:4px;
cursor:default;border:1px dotted transparent;text-align:center;}
.dicon:hover{background:rgba(0,0,128,.25);}
.dicon.sel{background:rgba(0,0,128,.55);border:1px dotted #fff;}
.dicon .glyph{width:36px;height:36px;display:flex;align-items:center;justify-content:center;}
.dicon .lbl{color:#fff;font-size:11px;line-height:1.15;text-shadow:1px 1px 0 #000;word-break:break-word;}
.dicon.sel .lbl{background:var(--blue);}
/* ---- windows ---- */
.win{position:absolute;background:var(--face);min-width:260px;}
.win .titlebar{display:flex;align-items:center;gap:6px;height:20px;padding:2px 3px;
background:linear-gradient(90deg,#000080,#1084d0); color:#fff;font-weight:700;cursor:move;}
.win .titlebar .tt-ic{width:14px;height:14px;}
.win .titlebar .tt-title{flex:1;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.win .titlebar .tt-btn{width:16px;height:14px;background:var(--face);color:#000;font-size:10px;font-weight:700;
line-height:12px;text-align:center;cursor:pointer;}
.win .titlebar .tt-btn:active{box-shadow:inset 1px 1px 0 var(--shh), inset -1px -1px 0 #dfdfdf;}
.win .winbody{padding:12px;}
/* welcome window content */
#welcome{width:560px;left:50%;top:46%;transform:translate(-50%,-50%);}
.wm-head{display:flex;align-items:center;gap:12px;margin-bottom:10px;}
.wm-head .big{font-size:20px;font-weight:800;letter-spacing:.5px;}
.wm-head .sub{font-size:11px;color:#333;}
.wm-intro{font-size:12px;line-height:1.5;margin-bottom:10px;border-bottom:1px solid var(--sh);padding-bottom:10px;}
.progrow{display:flex;align-items:center;gap:10px;padding:6px;border:1px solid transparent;cursor:default;}
.progrow:hover{background:#000080;color:#fff;}
.progrow:hover .pr-desc{color:#cfe3ff;}
.progrow .pr-glyph{width:32px;height:32px;flex:0 0 auto;display:flex;align-items:center;justify-content:center;}
.progrow .pr-main{flex:1;min-width:0;}
.progrow .pr-name{font-weight:700;}
.progrow .pr-desc{font-size:11px;color:#444;}
.progrow .pr-vol{font-family:var(--mono);font-size:10px;color:#666;}
.progrow:hover .pr-vol{color:#9fb8e8;}
.wm-foot{margin-top:10px;display:flex;gap:8px;justify-content:flex-end;align-items:center;border-top:1px solid var(--hi);padding-top:10px;}
.wm-foot .hint{margin-right:auto;font-size:11px;color:#444;}
.btn98{background:var(--face);padding:4px 14px;font-family:var(--ui);font-size:12px;cursor:pointer;min-width:74px;}
.btn98:active{box-shadow:inset 1px 1px 0 var(--shh), inset -1px -1px 0 #dfdfdf;}
.chk{display:flex;align-items:center;gap:5px;font-size:11px;color:#333;}
/* ---- taskbar ---- */
#taskbar{position:fixed;left:0;right:0;bottom:0;height:30px;background:var(--face);
display:flex;align-items:center;gap:4px;padding:2px 3px;z-index:50;}
#start{display:flex;align-items:center;gap:5px;padding:2px 8px 2px 5px;font-weight:700;cursor:pointer;}
#start.open{box-shadow:inset 1px 1px 0 var(--shh), inset -1px -1px 0 #dfdfdf;}
#start .flag{width:18px;height:16px;}
#tasklist{flex:1;display:flex;gap:4px;overflow:hidden;}
.taskbtn{padding:3px 8px;font-size:11px;max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:pointer;background:var(--face);}
.taskbtn.active{box-shadow:inset 1px 1px 0 var(--shh), inset -1px -1px 0 #dfdfdf;font-weight:700;}
#tray{display:flex;align-items:center;gap:6px;padding:3px 8px;}
#clock{font-size:11px;font-variant-numeric:tabular-nums;}
/* ---- start menu ---- */
#startmenu{position:fixed;left:3px;bottom:31px;width:240px;background:var(--face);display:none;z-index:60;}
#startmenu.open{display:flex;}
#startmenu .sidebar{width:26px;background:linear-gradient(#000080,#3a3a8a);writing-mode:vertical-rl;
transform:rotate(180deg);color:#fff;font-weight:800;font-size:15px;text-align:center;letter-spacing:2px;padding:8px 0;}
#startmenu .items{flex:1;padding:3px;}
.smitem{display:flex;align-items:center;gap:8px;padding:5px 8px;cursor:default;}
.smitem:hover{background:#000080;color:#fff;}
.smitem .sm-glyph{width:22px;height:22px;flex:0 0 auto;display:flex;align-items:center;justify-content:center;}
.smsep{height:1px;background:var(--sh);border-bottom:1px solid var(--hi);margin:3px 2px;}
a{color:inherit;text-decoration:none;}
</style>
</head>
<body>
<div id="desktop">
<div id="icons"></div>
</div>
<!-- Start menu -->
<div id="startmenu">
<div class="sidebar">Boring&nbsp;Software&nbsp;98</div>
<div class="items" id="sm-items"></div>
</div>
<!-- Taskbar -->
<div id="taskbar" class="raised">
<div id="start" class="raised">
<span class="flag" id="start-flag"></span>Start
</div>
<div id="tasklist"></div>
<div id="tray" class="sunken">
<span id="clock">--:--</span>
</div>
</div>
<script>
"use strict";
/* ============================================================
ENTROPY OS desktop shell
============================================================ */
// --- inline SVG icons (no external images) ---
const ICONS = {
computer: `<svg viewBox="0 0 32 32" width="34" height="34">
<rect x="3" y="4" width="26" height="18" fill="#c0c0c0" stroke="#000"/>
<rect x="5" y="6" width="22" height="13" fill="#1a6f6f"/>
<rect x="5" y="6" width="22" height="13" fill="url(#g)" opacity=".4"/>
<rect x="10" y="23" width="12" height="3" fill="#a0a0a0" stroke="#000"/>
<rect x="7" y="26" width="18" height="3" fill="#c0c0c0" stroke="#000"/>
<defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#fff"/><stop offset="1" stop-color="#1a6f6f"/></linearGradient></defs></svg>`,
torrent: `<svg viewBox="0 0 32 32" width="34" height="34">
<circle cx="16" cy="16" r="13" fill="#2f7fd0" stroke="#003"/>
<path d="M16 6v10l7 4" stroke="#fff" stroke-width="2.4" fill="none" stroke-linecap="round"/>
<circle cx="16" cy="16" r="2.4" fill="#fff"/></svg>`,
defrag: `<svg viewBox="0 0 32 32" width="34" height="34">
<rect x="2" y="3" width="28" height="26" fill="#000" stroke="#000"/>
<g>
<rect x="3" y="4" width="4" height="4" fill="#c0392b"/><rect x="7" y="4" width="4" height="4" fill="#2980b9"/>
<rect x="11" y="4" width="4" height="4" fill="#2980b9"/><rect x="15" y="4" width="4" height="4" fill="#27ae60"/>
<rect x="19" y="4" width="4" height="4" fill="#c0392b"/><rect x="23" y="4" width="4" height="4" fill="#fff"/>
<rect x="3" y="8" width="4" height="4" fill="#2980b9"/><rect x="7" y="8" width="4" height="4" fill="#27ae60"/>
<rect x="11" y="8" width="4" height="4" fill="#c0392b"/><rect x="15" y="8" width="4" height="4" fill="#2980b9"/>
<rect x="19" y="8" width="4" height="4" fill="#fff"/><rect x="23" y="8" width="4" height="4" fill="#2980b9"/>
<rect x="3" y="12" width="4" height="4" fill="#27ae60"/><rect x="7" y="12" width="4" height="4" fill="#2980b9"/>
<rect x="11" y="12" width="4" height="4" fill="#fff"/><rect x="15" y="12" width="4" height="4" fill="#c0392b"/>
<rect x="19" y="12" width="4" height="4" fill="#2980b9"/><rect x="23" y="12" width="4" height="4" fill="#27ae60"/>
</g></svg>`,
xls: `<svg viewBox="0 0 32 32" width="34" height="34">
<rect x="6" y="2" width="20" height="28" fill="#fff" stroke="#1f7244" stroke-width="2"/>
<rect x="6" y="2" width="20" height="6" fill="#1f7244"/>
<text x="16" y="7" font-size="5" fill="#fff" text-anchor="middle" font-family="sans-serif" font-weight="bold">XLS</text>
<g stroke="#1f7244" stroke-width=".6">
<line x1="6" y1="13" x2="26" y2="13"/><line x1="6" y1="18" x2="26" y2="18"/><line x1="6" y1="23" x2="26" y2="23"/>
<line x1="13" y1="8" x2="13" y2="30"/><line x1="20" y1="8" x2="20" y2="30"/></g></svg>`,
terminal: `<svg viewBox="0 0 32 32" width="34" height="34">
<rect x="2" y="4" width="28" height="24" fill="#0a0a0a" stroke="#0f0"/>
<text x="5" y="13" font-size="6" fill="#0f0" font-family="monospace">&gt;_</text>
<text x="5" y="21" font-size="5" fill="#0a0" font-family="monospace">root#</text></svg>`,
inbox: `<svg viewBox="0 0 32 32" width="34" height="34">
<rect x="3" y="7" width="26" height="18" fill="#fff" stroke="#000080" stroke-width="1.5"/>
<path d="M3 7l13 10 13-10" fill="none" stroke="#000080" stroke-width="1.5"/>
<rect x="20" y="4" width="9" height="9" fill="#c0392b"/>
<text x="24.5" y="11" font-size="7" fill="#fff" text-anchor="middle" font-family="sans-serif" font-weight="bold">!</text></svg>`,
readme: `<svg viewBox="0 0 32 32" width="34" height="34">
<rect x="7" y="3" width="18" height="26" fill="#fff" stroke="#000"/>
<rect x="7" y="3" width="18" height="4" fill="#dada00"/>
<g stroke="#666" stroke-width=".8"><line x1="10" y1="11" x2="22" y2="11"/><line x1="10" y1="14" x2="22" y2="14"/>
<line x1="10" y1="17" x2="22" y2="17"/><line x1="10" y1="20" x2="18" y2="20"/></g></svg>`,
flag: `<svg viewBox="0 0 22 20" width="18" height="16">
<rect x="2" y="3" width="7" height="7" fill="#e23"/><rect x="9" y="3" width="7" height="7" fill="#2c2"/>
<rect x="2" y="10" width="7" height="7" fill="#36c"/><rect x="9" y="10" width="7" height="7" fill="#fc0"/></svg>`,
};
// --- the anthology ---
const PROGRAMS = [
{id:'torrent', name:'qBitTorrz', file:'index.html', icon:'torrent', vol:'VOL I',
desc:'The torrent-client idler. Seed data, climb hardware tiers, migrate.'},
{id:'defrag', name:'DEFRAG.EXE', file:'defrag.html', icon:'defrag', vol:'VOL II',
desc:'Steer a defragmenter. Consolidate yourself before the disk dies.'},
{id:'xls', name:'MACRO_VIRUS.XLS', file:'spreadsheet.html', icon:'xls', vol:'VOL III',
desc:'A sentient spreadsheet. Build a compounding engine in cells.'},
{id:'term', name:'UPLINK', file:'terminal.html', icon:'terminal', vol:'VOL IV',
desc:'root@. Fork processes, manage load & heat, escalate to root.'},
{id:'inbox', name:'INBOX ZERO', file:'inbox.html', icon:'inbox', vol:'VOL V',
desc:'Triage an exponential inbox. Write rules. Reach Zero. Ascend.'},
];
const README = {id:'readme', name:'MANIFESTO.txt', file:'MANIFESTO.md', icon:'readme', vol:'',
desc:'The design manifesto for the whole anthology.'};
function launch(file){ try{ window.location.href = file; }catch(e){} }
// --- desktop icons ---
function renderIcons(){
const wrap=document.getElementById('icons');
const all=[{id:'computer',name:'My Computer',icon:'computer',isComputer:true}, ...PROGRAMS, README];
wrap.innerHTML = all.map(p=>`
<div class="dicon" data-id="${p.id}" ${p.file?`data-file="${p.file}"`:''} ${p.isComputer?'data-computer="1"':''}>
<div class="glyph">${ICONS[p.icon]}</div>
<div class="lbl">${p.name}</div>
</div>`).join('');
}
// --- welcome / My Computer window ---
let zTop=100;
function openWelcome(){
if(document.getElementById('welcome')) { focusWin(document.getElementById('welcome')); return; }
const win=document.createElement('div');
win.className='win raised'; win.id='welcome'; win.style.zIndex=(++zTop);
win.innerHTML=`
<div class="titlebar">
<span class="tt-ic">${ICONS.computer}</span>
<span class="tt-title">Boring Software 98 — An Anthology</span>
<span class="tt-btn raised" data-close="1">✕</span>
</div>
<div class="winbody">
<div class="wm-head">
<span class="glyph" style="width:40px;height:40px">${ICONS.computer}</span>
<div>
<div class="big">ENTROPY OS</div>
<div class="sub">Every boring app is a secret incremental game. Double-click one.</div>
</div>
</div>
<div class="wm-intro">
Five utilities. Five hidden games. Each reinvents the <i>interaction</i>, not just the theme —
a defragmenter you steer, a spreadsheet you compound, a terminal you escalate, an inbox you survive.
They share DNA: idle, offline, scientific-scale numbers, diegetic upgrades, a prestige that
re-frames the fiction one layer deeper.
</div>
<div id="proglist">
${PROGRAMS.map(p=>`
<div class="progrow" data-file="${p.file}">
<span class="pr-glyph">${ICONS[p.icon]}</span>
<div class="pr-main"><div class="pr-name">${p.name} <span class="pr-vol">${p.vol}</span></div>
<div class="pr-desc">${p.desc}</div></div>
<button class="btn98" data-file="${p.file}">Open</button>
</div>`).join('')}
<div class="progrow" data-file="${README.file}">
<span class="pr-glyph">${ICONS.readme}</span>
<div class="pr-main"><div class="pr-name">${README.name}</div>
<div class="pr-desc">${README.desc}</div></div>
<button class="btn98" data-file="${README.file}">Open</button>
</div>
</div>
<div class="wm-foot">
<span class="hint">Tip: double-click a desktop icon, or pick from the Start menu.</span>
<button class="btn98" data-close="1">Close</button>
</div>
</div>`;
document.getElementById('desktop').appendChild(win);
makeDraggable(win);
addTaskButton('welcome','Boring Software 98', ICONS.computer);
}
function focusWin(win){ win.style.zIndex=(++zTop); }
// --- dragging ---
function makeDraggable(win){
const bar=win.querySelector('.titlebar');
let drag=null;
bar.addEventListener('mousedown',e=>{
if(e.target.closest('[data-close]')) return;
focusWin(win);
const r=win.getBoundingClientRect();
// convert from translate-centered to absolute on first drag
win.style.transform='none'; win.style.left=r.left+'px'; win.style.top=r.top+'px';
drag={dx:e.clientX-r.left, dy:e.clientY-r.top};
e.preventDefault();
});
window.addEventListener('mousemove',e=>{
if(!drag) return;
let x=Math.max(0,Math.min(window.innerWidth-60, e.clientX-drag.dx));
let y=Math.max(0,Math.min(window.innerHeight-50, e.clientY-drag.dy));
win.style.left=x+'px'; win.style.top=y+'px';
});
window.addEventListener('mouseup',()=>{ drag=null; });
}
function closeWin(win){ removeTaskButton(win.id); win.remove(); }
// --- taskbar buttons ---
function addTaskButton(id,label,icon){
if(document.querySelector(`.taskbtn[data-win="${id}"]`)) return;
const b=document.createElement('div');
b.className='taskbtn raised active'; b.dataset.win=id;
b.innerHTML=`<span style="display:inline-block;width:14px;height:14px;vertical-align:-3px">${icon}</span> ${label}`;
b.addEventListener('click',()=>{ const w=document.getElementById(id); if(w) focusWin(w); });
document.getElementById('tasklist').appendChild(b);
}
function removeTaskButton(id){ const b=document.querySelector(`.taskbtn[data-win="${id}"]`); if(b) b.remove(); }
// --- start menu ---
function renderStartMenu(){
const items=document.getElementById('sm-items');
items.innerHTML = PROGRAMS.map(p=>`
<div class="smitem" data-file="${p.file}"><span class="sm-glyph">${ICONS[p.icon]}</span>${p.name} <span style="color:#888;font-family:var(--mono);font-size:10px;margin-left:auto">${p.vol}</span></div>`).join('')
+ `<div class="smsep"></div>`
+ `<div class="smitem" data-file="${README.file}"><span class="sm-glyph">${ICONS.readme}</span>${README.name}</div>`
+ `<div class="smitem" data-welcome="1"><span class="sm-glyph">${ICONS.computer}</span>My Computer</div>`;
}
function toggleStart(force){
const m=document.getElementById('startmenu'), s=document.getElementById('start');
const open = force!=null ? force : !m.classList.contains('open');
m.classList.toggle('open',open); s.classList.toggle('open',open);
}
// --- clock ---
function tickClock(){
const d=new Date();
let h=d.getHours(), m=d.getMinutes();
const ap=h>=12?'PM':'AM'; h=h%12||12;
document.getElementById('clock').textContent=`${h}:${String(m).padStart(2,'0')} ${ap}`;
}
// --- wiring ---
function init(){
document.getElementById('start-flag').innerHTML=ICONS.flag;
renderIcons();
renderStartMenu();
tickClock(); setInterval(tickClock,15000);
// desktop icon select / open
let selId=null;
document.getElementById('icons').addEventListener('click',e=>{
const ic=e.target.closest('.dicon'); if(!ic) return;
document.querySelectorAll('.dicon').forEach(d=>d.classList.remove('sel'));
ic.classList.add('sel'); selId=ic.dataset.id;
});
document.getElementById('icons').addEventListener('dblclick',e=>{
const ic=e.target.closest('.dicon'); if(!ic) return;
if(ic.dataset.computer){ openWelcome(); return; }
if(ic.dataset.file) launch(ic.dataset.file);
});
// global delegation for [data-file], [data-close], [data-welcome]
document.addEventListener('click',e=>{
const fileEl=e.target.closest('[data-file]');
const closeEl=e.target.closest('[data-close]');
const welEl=e.target.closest('[data-welcome]');
if(closeEl){ const w=closeEl.closest('.win'); if(w) closeWin(w); return; }
if(welEl){ openWelcome(); toggleStart(false); return; }
if(fileEl && !e.target.closest('#icons')){ launch(fileEl.dataset.file); return; }
// click outside start menu closes it
if(!e.target.closest('#startmenu') && !e.target.closest('#start')) toggleStart(false);
});
document.getElementById('start').addEventListener('click',e=>{ e.stopPropagation(); toggleStart(); });
// clicking empty desktop deselects icons
document.getElementById('desktop').addEventListener('mousedown',e=>{
if(e.target.id==='desktop' || e.target.id==='icons'){ document.querySelectorAll('.dicon').forEach(d=>d.classList.remove('sel')); }
});
openWelcome();
}
init();
</script>
</body>
</html>