diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
new file mode 100644
index 0000000..25e62fb
--- /dev/null
+++ b/.github/workflows/pages.yml
@@ -0,0 +1,55 @@
+name: Deploy to GitHub Pages
+
+on:
+ push:
+ branches: [master, mobile]
+
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+concurrency:
+ group: pages
+ cancel-in-progress: true
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - name: Checkout master
+ uses: actions/checkout@v4
+ with:
+ ref: master
+ path: master
+
+ - name: Checkout mobile
+ uses: actions/checkout@v4
+ with:
+ ref: mobile
+ path: mobile
+
+ - name: Build site structure
+ run: |
+ mkdir -p _site/mobile
+ # Copy master as root
+ cp master/index.html master/style.css master/game.js _site/ 2>/dev/null || true
+ cp -r master/assets _site/assets 2>/dev/null || true
+ # Copy mobile under /mobile/
+ cp mobile/index.html mobile/style.css mobile/game.js _site/mobile/ 2>/dev/null || true
+ cp -r mobile/assets _site/mobile/assets 2>/dev/null || true
+
+ - name: Setup Pages
+ uses: actions/configure-pages@v4
+
+ - name: Upload artifact
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: _site
+
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/MOBILE-PHASE2.md b/MOBILE-PHASE2.md
new file mode 100644
index 0000000..88c73bf
--- /dev/null
+++ b/MOBILE-PHASE2.md
@@ -0,0 +1,51 @@
+# Quarter Close — Mobile Phase 2: Improvements & Polish
+
+*Post-launch refinements to the mobile port. Phase 1 delivered full playability; Phase 2 focuses on polish, information density, and UX improvements.*
+
+## Baseline Rules (Same as Phase 1)
+
+- **ONLY work on the `mobile` branch. NEVER touch master.**
+- **NEVER checkout, merge from, or modify the master branch.**
+- Commit and push frequently — live preview auto-deploys at https://kpa-clawbot.github.io/quarter-close/mobile/
+- Increment `?v=` cache busters on game.js and style.css in index.html with each change
+- **Playability > aesthetics** — never break core mechanics for visual improvements
+- **All 15 core mechanics must keep working** after every change (test on mobile viewport)
+- Single page app: `index.html`, `style.css`, `game.js` — no frameworks
+- Mobile detection: `isMobile()` using `matchMedia('(max-width: 600px)')`
+- Mobile CSS in `@media (max-width: 600px)` blocks
+- Read `MOBILE.md` for full architecture documentation
+
+## Technical Reference
+
+- **Ops tab cash header**: `#row-cash` (the desktop stats row, restyled for mobile with CSS)
+- **Non-ops tab mini header**: `#mobile-cash-header` (JS-managed, shown on P&L/Board/Settings tabs)
+- **Cash header update (ops)**: `updateGridValues()` in game.js updates `#cash-display`, `#per-tick-display`, `#stock-price-cell`
+- **Cash header update (non-ops)**: `updateMobileCashHeader()` updates `#mob-cash-amount` and `#mob-cash-perday`
+- **Desktop Row 1 info**: Cash, $/day, Total Rev/yr, Stock Price (post-IPO)
+
+## Known Issues
+
+### ✅ Fixed — Cash Header Missing Stock Price & Total Rev/yr
+- The non-ops `#mobile-cash-header` only showed cash and $/day
+- Stock price (post-IPO) and Total Rev/yr were missing
+- **Fix**: Added stock price and total rev/yr to both the ops header and non-ops mini header
+
+### ✅ Fixed — Cash Header White Flash on Tap
+- Tapping the `#row-cash` header caused a white flash
+- Missing `-webkit-tap-highlight-color: transparent` on the header
+- **Fix**: Added tap highlight suppression to `#row-cash` and `#mobile-cash-header`
+
+### ✅ Fixed — Cash Header Information Density
+- Desktop shows: Cash, $/day, Total Rev/yr, Stock Price
+- Mobile ops header was missing Total Rev/yr
+- **Fix**: Restructured mobile cash header to show all key metrics in a compact layout
+
+## Phase 2 Improvements
+
+*Placeholder — add items here as they're identified.*
+
+- [ ] *(Add future improvement items here)*
+
+---
+
+*Created: 2026-02-14*
diff --git a/MOBILE.md b/MOBILE.md
new file mode 100644
index 0000000..b290d29
--- /dev/null
+++ b/MOBILE.md
@@ -0,0 +1,166 @@
+# Quarter Close — Mobile Support
+
+## Status: ✅ Phase 1 Complete
+
+The mobile version is available at `/mobile/` and includes all features from master.
+
+### Phase 1 Summary (Completed 2026-02-14)
+
+Phase 1 delivered a fully playable mobile port of Quarter Close with:
+
+- **Card-based mobile layout** replacing the desktop spreadsheet grid — each department rendered as a rounded card with action buttons
+- **Bottom navigation bar** with four tabs: Ops, P&L, Board, More — with Board tab auto-hidden pre-IPO
+- **All 15 core mechanics working on mobile** — verified and tested (see checklist below)
+- **Touch-friendly UI** with minimum 44px tap targets across all interactive elements
+- **Haptic feedback** on all major actions (hire, upgrade, automate, collect, unlock, tab switch, etc.)
+- **Animations and active states** — button press feedback, cash pulse, card unlock animations, toast slide-ups, mini-task pop-in
+- **Nav badges and collect highlights** — P&L tab alert badge for tax debts, green highlight on collect buttons with pending revenue
+- **Multiple polish passes** — disabled button styling, hidden scrollbars, modal backdrop blur, overtime card styling, toast vertical stacking, safe area insets for notched phones
+
+## Architecture
+
+Mobile support is built as a **CSS + JS layer on top of master's codebase**:
+
+- **Same `game.js`** — mobile detection via `isMobile()` (`max-width: 600px` media query)
+- **Same `index.html`** — extra mobile-only elements added (cash header, views, bottom nav)
+- **Same `style.css`** — mobile overrides in `@media (max-width: 600px)` blocks
+
+This ensures mobile stays in sync with master's features automatically.
+
+## Mobile Layout
+
+### Operations Tab (Main View)
+- **Green cash header** — sticky at top, shows cash balance, rev/day, stock price (post-IPO)
+- **Card-based departments** — each department is a rounded card with:
+ - Row 1: Name, level badge, employee count, rev/day
+ - Row 2: Max(N) + Hire button (full width)
+ - Row 3: Max(N) + Upgrade button + Collect button (side by side)
+ - Row 4: Automate button OR compact "⚡ AUTO" badge (when automated)
+- **Restructure buttons** — gold-styled RE buttons appear next to Upgrade after IPO (full-width on automated depts)
+- **Automated department indicator** — teal left border on automated cards (with transition)
+- **Overtime row** — dashed border card at bottom, orange styling
+- **Locked departments** — greyed out with Unlock button; non-unlockable ones hidden
+- **Mini-task bar** — yellow gradient bar with pop-in animation, styled approve/skip buttons
+
+### P&L Tab
+- Profit & Loss card (quarterly + lifetime financials)
+- Investor Relations card (stock price, guidance, earnings tracking)
+- Tax debt cards with settle buttons
+- Mini cash header at top (shows cash on non-ops tabs)
+- **Alert badge on tab** when tax debts exist
+
+### Board Room Tab
+- RE balance header
+- Upgrade cards with name, description, cost, Buy/Locked status
+- Only visible after IPO
+
+### Settings Tab (More)
+- Save Game, Auto-save toggle, Boss Mode, Game Options, Help, New Game
+- Game stats display (date, rev/sec, rev/day, rev/year, total earned, clicks, play time, stock, RE)
+- **Active effects display** (power outage, revenue penalty/bonus, DB outage, hire freeze, IRS garnishment with countdown timers)
+
+### Bottom Navigation
+- Ops | P&L | Board | More
+- Board tab hidden pre-IPO
+- Active tab highlighted in green with top indicator line
+- Subtle press/active state for touch feedback
+- **Alert badge** on P&L tab when tax debts exist (animated pop)
+
+## Mobile-Specific Behavior
+
+### Touch & Interaction
+- **Haptic feedback** — vibration on all major actions (hire, upgrade, automate, collect, unlock, restructure, deal clicks, mini-tasks, overtime, golden cell, arc selection, tab switch, event choices)
+- **Button active states** — scale + color change feedback on all button types (hire=blue, upgrade=green, collect=green, automate=teal, unlock=orange, prestige=gold)
+- **Cash header pulse** — green pulse animation on revenue collection
+- **Card unlock animation** — scale + opacity + color transition when unlocking departments
+- **Toast slide-up animation** — toasts slide up from bottom instead of appearing
+- **Mini-task pop-in** — animated appearance for mini-task bar
+- **Double-tap prevention** — 400ms debounce on automate buttons
+- **Scroll-to-top** — smooth scroll on tab switch
+- **Scroll to unlocked card** — auto-scrolls to newly unlocked department
+
+### Visual Polish
+- **Collect button highlight** — green background when pending revenue available
+- **Disabled button styling** — reduced opacity for unaffordable actions
+- **Hidden scrollbars** — cleaner look on all scrollable views
+- **Modal backdrop blur** — subtle blur behind modals
+- **Overtime card** — warm orange styling with active state
+- **Toast buttons stacked vertically** — full-width for easy tapping on earnings guidance (4 options)
+- **Status bar** — wider text area, subtle background, shows game date and timer
+
+### Button Labels
+- **Clear button labels**: "Hire $5K", "Upgrade $30K", "💰 Collect $41K", "Automate $500"
+- **Upgrade always visible** — no longer hidden when not automated
+- **Collect shows pending amount** — "💰 Collect $X" or just "💰 Collect" when nothing pending
+- **Auto-badge compact** — small "⚡ AUTO" text, doesn't waste vertical space
+
+### Layout & Positioning
+- **CSS flex ordering** — status bar (order 90) and nav (order 100) always at bottom, cash header (order -2) at top on non-ops tabs
+- **Toast/notification positioning** — fixed above bottom nav + status bar, no overlap with nav
+- **Deal popup** — fixed above bottom nav
+- **Golden cell** — applies to whole card row
+- **Splash screen** — skipped on mobile
+- **Touch targets** — minimum 44px for all interactive elements
+- **Safe area insets** — supports notched phones
+
+### Arc Selection
+- **Flex layout** — icon on left, text content on right
+- **Active press state** — scale + color feedback on tap
+- **Full-height** — uses 100dvh for proper mobile viewport
+- **Gradient background** — subtle green gradient
+
+### Data Updates
+- **P&L hash** includes tax debt details (current amount + stage) for live updates
+- **P&L hash** includes cash for real-time settle button enable/disable
+- **Board Room hash** includes cash for buy button enable/disable
+
+## All 15 Game Mechanics Tested on Mobile
+
+1. ✅ Start new game — arc selection flows smoothly
+2. ✅ Collect revenue — 💰 Collect buttons with clear amounts + haptic + cash pulse
+3. ✅ Hire employees — Hire + Max(N) buttons + haptic
+4. ✅ Upgrade departments — always visible (not hidden behind Auto) + haptic
+5. ✅ Automate departments — Automate button → ⚡ AUTO badge + teal border + haptic
+6. ✅ Unlock new departments — Unlock buttons, locked ones properly greyed + unlock animation
+7. ✅ Mini-tasks — yellow bar with Approve/Sign buttons + haptic
+8. ✅ Events/notifications — toasts with action buttons, slide-up animation, vertical stacking
+9. ✅ IPO / Earnings — earnings modal with guidance selection (4 vertical buttons)
+10. ✅ IRS taxes — Pay/Ignore buttons + P&L tab badge
+11. ✅ Board Room — browse RE upgrades, Buy buttons, requirement locks
+12. ✅ Settings — save/load, new game confirm, boss mode, help, game options, active effects
+13. ✅ Overtime (Push It) — overtime card with orange styling + haptic + cash pulse
+14. ✅ Close the Deal — deal popup with Sign button + haptic per click + success celebration
+15. ✅ Management Focus — department name tap for focus boost
+
+## Polish Pass 3 (Latest)
+
+### Fixes Applied
+1. **Stock price visible in Ops cash header** — `#stock-price-cell` was hidden by `.cell-h { display: none }` rule. Added `display: flex !important` override with higher specificity (`#row-cash #stock-price-cell`). Shows "📈 $X.XX" via CSS `::before` pseudo-element, hiding the "Stock:" label for space efficiency.
+2. **Penalty/bonus indicators improved** — Colors lightened for dark green header background (#ff8a80 red, #ffab40 orange, #a5d6a7 green). Font size bumped from 9px to 11px for readability.
+3. **Non-ops tabs cash header shows effects** — `updateMobileCashHeader()` now renders penalty/bonus/outage indicators with mobile-appropriate colors, matching the Ops tab behavior.
+4. **Prestige button disabled state** — Changed from flat grey (#f5f5f0) to muted gold gradient, maintaining visual identity even when unaffordable.
+
+### Verified Working
+- All 15 core mechanics tested end-to-end
+- Penalty/bonus/outage indicators on both Ops and non-ops headers
+- Boss mode (Excel disguise) renders correctly on mobile viewport
+- Event toasts with action buttons properly positioned above nav
+- Deal popup positioned and functional
+- Mini-task bar with pop-in animation
+- Board Room scrolling through all categories
+- Earnings guidance selection (4 vertical buttons)
+- Swipe gesture between tabs
+- No console errors or warnings
+
+## Files Modified
+
+All changes are in the standard three files:
+- `index.html` — mobile viewport meta, mobile-only elements, bottom nav
+- `style.css` — `@media (max-width: 600px)` blocks (~800 lines)
+- `game.js` — `isMobile()`, `mobileSwitchTab()`, mobile P&L/Board Room/Settings builders, `mobileTickUpdate()`, haptic feedback, animations, window exposures
+
+## Deployment
+
+GitHub Pages workflow deploys both branches:
+- `/` — master (desktop)
+- `/mobile/` — mobile branch
diff --git a/game.js b/game.js
index 5391425..6740ac3 100644
--- a/game.js
+++ b/game.js
@@ -50,7 +50,7 @@ const ARCS = {
{ name: 'Garage Sale', flavor: 'One man\'s trash...' },
{ name: 'eBay Reselling', flavor: 'Buy low, sell high' },
{ name: 'Dropshipping Store', flavor: 'Never touch inventory' },
- { name: 'Amazon FBA', flavor: 'Let Bezos handle logistics' },
+ { name: 'Drop Shipping', flavor: 'Let the warehouse handle logistics' },
{ name: 'Warehouse & Distro', flavor: 'Cutting out the middleman' },
{ name: 'Private Label Brand', flavor: 'Your name on the box' },
{ name: 'Retail Chain', flavor: 'Brick and mortar comeback' },
@@ -149,8 +149,13 @@ function miniTaskReward(task) {
}
// ===== EVENTS DEFINITIONS =====
+// Global event frequency multiplier — scales all event weights together
+// 1.0 = default, 2.0 = twice as frequent, 0.5 = half as frequent, 0 = off
+let EVENT_FREQ_MULT = 1.0;
+
const EVENTS = [
{
+ weight: 3,
sender: 'Mom',
subject: 'Quick investment opportunity',
body: 'Honey, I believe in your little business! Here\'s a little something to help out.',
@@ -164,6 +169,7 @@ const EVENTS = [
]
},
{
+ weight: 4,
sender: 'Angry Customer',
subject: 'RE: TERRIBLE SERVICE!!!',
body: 'I want a FULL REFUND or I\'m leaving a 1-star review everywhere!',
@@ -180,19 +186,32 @@ const EVENTS = [
]
},
{
+ weight: 1,
sender: 'College Buddy',
- subject: 'Hey can I get a discount??',
- body: 'Bro remember me from college?? Hook me up with a discount! For old times\' sake 🤙',
+ subject: 'Business proposal over beers? 🍺',
+ body: 'Dude, I\'ve got this idea that could be huge. Let me pitch you — worst case we grab drinks and catch up.',
actions: [
- { label: 'Give discount (-1% cash)', effect: (gs) => {
- const cost = Math.max(5, Math.floor(gs.cash * 0.01));
+ { label: '🍺 Take the meeting (5% cash)', effect: (gs) => {
+ const cost = Math.max(50, Math.floor(gs.cash * 0.05));
gs.cash -= cost;
- return `Gave ${formatMoney(cost)} discount. Your friend is happy!`;
+ // 40% chance it's actually good
+ if (Math.random() < 0.4) {
+ const rev = totalRevPerTick();
+ const bonus = rev * (60 + Math.floor(Math.random() * 120)); // 1-3 min of revenue
+ gs.cash += bonus;
+ gs.totalEarned += bonus;
+ gs.quarterRevenue += bonus;
+ if (gs.isPublic) gs.earningsQuarterRevenue += bonus;
+ return `Spent ${formatMoney(cost)} on dinner... but the idea was legit! Closed a ${formatMoney(bonus)} side deal.`;
+ } else {
+ return `Spent ${formatMoney(cost)} on drinks. The pitch was an MLM. Classic.`;
+ }
}},
- { label: 'Full price (no effect)', effect: () => 'They understand. Business is business.' },
+ { label: '📱 "Super busy rn"', effect: () => 'Left on read. He\'ll try again in 6 months.' },
]
},
{
+ weight: 2,
sender: 'IT Department',
subject: '⚠️ POWER OUTAGE - Building 3',
body: 'Emergency maintenance required. All systems will be offline for approximately 15 seconds. This cannot be prevented.',
@@ -205,6 +224,142 @@ const EVENTS = [
actions: []
},
{
+ weight: 1,
+ sender: 'IT Security',
+ subject: '🔒 RANSOMWARE DETECTED - All Systems',
+ body: 'Ransomware detected on the network. All file shares encrypted. Pay the ransom or wait for IT to rebuild from backups.',
+ actions: [
+ { label: '💰 Pay ransom (15% cash)', effect: (gs) => {
+ const cost = Math.max(100, Math.floor(gs.cash * 0.15));
+ gs.cash -= cost;
+ return `Paid ${formatMoney(cost)} ransom. Systems restored. IT is "looking into it."`;
+ }},
+ { label: '🛡️ Refuse — rebuild from backups', effect: (gs) => {
+ const duration = 30000 + Math.floor(Math.random() * 30000); // 30-60s
+ gs.powerOutage = { until: Date.now() + duration };
+ return `Revenue frozen for ${Math.round(duration/1000)}s while IT rebuilds. Should\'ve had better backups.`;
+ }},
+ ]
+ },
+ {
+ weight: 1,
+ sender: 'Network Operations',
+ subject: '🌐 DDoS ATTACK IN PROGRESS',
+ body: 'Massive distributed denial-of-service attack hitting all public-facing services. Mitigation in progress but performance is degraded.',
+ actions: [
+ { label: 'Nothing we can do', effect: (gs) => {
+ const duration = 20000 + Math.floor(Math.random() * 10000); // 20-30s
+ gs.revPenalty = { mult: 0.5, until: Date.now() + duration };
+ return `⚠️ DDoS — revenue at 50% for ${Math.round(duration/1000)}s while CloudFlare mitigates.`;
+ }},
+ ]
+ },
+ {
+ weight: 1,
+ sender: 'DBA Team',
+ subject: '💾 CRITICAL: Database corruption detected',
+ body: 'Primary database showing consistency errors. We can do an emergency restore ($$$) or let auto-recovery run (slower).',
+ actions: [
+ { label: '💰 Emergency restore (3% cash)', effect: (gs) => {
+ const cost = Math.max(50, Math.floor(gs.cash * 0.03));
+ gs.cash -= cost;
+ return `Paid ${formatMoney(cost)} for emergency DB restore. Crisis averted.`;
+ }},
+ { label: '⏳ Wait for auto-recovery', effect: (gs) => {
+ const duration = 15000 + Math.floor(Math.random() * 5000); // 15-20s
+ const unlocked = gs.sources.map((s, i) => ({ s, i })).filter(x => x.s.unlocked && x.s.employees > 0);
+ if (unlocked.length > 0) {
+ const pick = unlocked[Math.floor(Math.random() * unlocked.length)];
+ gs.dbOutage = { sourceIndex: pick.i, until: Date.now() + duration };
+ const arc = ARCS[gs.arc];
+ const name = arc.sources[pick.i].name || getSourceDef(pick.i).name;
+ return `${name} offline for ${Math.round(duration/1000)}s while database recovers.`;
+ }
+ console.log('DB outage: no unlocked sources');
+ return 'Auto-recovery running... minimal impact.';
+ }},
+ ]
+ },
+ {
+ weight: 3,
+ sender: 'IT Department',
+ subject: '📧 Email server is DOWN',
+ body: 'Mail server is unreachable. No one can send or receive email until it\'s fixed. Approval workflows are frozen.',
+ actions: [
+ { label: 'Wait it out', effect: (gs) => {
+ const duration = 45000 + Math.floor(Math.random() * 15000); // 45-60s
+ gs.miniTaskBlocked = { until: Date.now() + duration };
+ return `📧 Email down — no approvals for ${Math.round(duration/1000)}s. Your streak is in danger.`;
+ }},
+ ]
+ },
+ {
+ weight: 4,
+ sender: 'IT Security',
+ subject: '🔑 MANDATORY PASSWORD RESET',
+ body: 'Security audit requires all employees to reset passwords immediately. Productivity will be impacted.',
+ timed: true,
+ timedDelay: 3000,
+ timedEffect: (gs) => {
+ gs.powerOutage = { until: Date.now() + 10000 };
+ return '🔑 Company-wide password reset. Revenue paused for 10 seconds.';
+ },
+ actions: []
+ },
+ {
+ weight: 2,
+ sender: 'Status Page',
+ subject: '☁️ CLOUD PROVIDER OUTAGE',
+ body: 'Major incident at your cloud provider. Multiple availability zones affected. ETA for resolution: "We\'re working on it." Helpful.',
+ actions: [
+ { label: 'Welcome to the cloud', effect: (gs) => {
+ const duration = 15000 + Math.floor(Math.random() * 10000); // 15-25s
+ gs.revPenalty = { mult: 0.25, until: Date.now() + duration };
+ return `☁️ Cloud outage — revenue at 25% for ${Math.round(duration/1000)}s. Nothing you can do.`;
+ }},
+ ]
+ },
+ {
+ weight: 3,
+ sender: 'Engineering Lead',
+ subject: '🐛 P0 BUG: Production is on fire',
+ body: 'Critical bug in prod. Customers are seeing errors. We can hotfix now (costs money for the war room) or punt to next sprint and eat the churn.',
+ actions: [
+ { label: '🚨 Hotfix now (5% cash)', effect: (gs) => {
+ const cost = Math.max(100, Math.floor(gs.cash * 0.05));
+ gs.cash -= cost;
+ return `Spent ${formatMoney(cost)} on an emergency war room. Bug squashed. Engineers need therapy.`;
+ }},
+ { label: '📋 Next sprint', effect: (gs) => {
+ // Random department loses 50% revenue for 60s
+ const unlocked = gs.sources.map((s, i) => ({ s, i })).filter(x => x.s.unlocked && x.s.employees > 0);
+ if (unlocked.length > 0) {
+ const pick = unlocked[Math.floor(Math.random() * unlocked.length)];
+ const duration = 60000;
+ gs.revPenalty = { mult: 0.5, until: Date.now() + duration };
+ const arc = ARCS[gs.arc];
+ const name = arc.sources[pick.i].name || getSourceDef(pick.i).name;
+ return `Customers churning — revenue at 50% for 60s. "${name}" taking the biggest hit.`;
+ }
+ return 'Customers aren\'t happy but they\'ll survive... probably.';
+ }},
+ ]
+ },
+ {
+ weight: 2,
+ sender: 'IT Asset Management',
+ subject: '💻 LAPTOP RECALL - Security Vulnerability',
+ body: 'Critical firmware vulnerability discovered. All company laptops must be collected for patching. Employees will work from their phones (poorly).',
+ actions: [
+ { label: 'Comply with recall', effect: (gs) => {
+ const duration = 20000;
+ gs.revPenalty = { mult: 0.7, until: Date.now() + duration };
+ return '💻 Laptop recall — revenue at 70% for 20s while everyone works from phones.';
+ }},
+ ]
+ },
+ {
+ weight: 2,
sender: 'Google Alerts',
subject: '📈 Your company is trending on TikTok!',
body: 'A customer posted a viral video about your product. 2.3M views and counting! Revenue is spiking.',
@@ -216,6 +371,7 @@ const EVENTS = [
]
},
{
+ weight: 2,
sender: 'PR Team',
subject: 'Forbes wants to feature us! 🎉',
body: 'Forbes is running a "30 Under 30" style piece and wants to include us. This will be huge for brand awareness.',
@@ -228,6 +384,7 @@ const EVENTS = [
]
},
{
+ weight: 2,
sender: 'Social Media',
subject: '🚀 We hit the front page of Reddit!',
body: 'Someone posted about us on r/technology and it exploded. Server traffic is through the roof!',
@@ -239,6 +396,7 @@ const EVENTS = [
]
},
{
+ weight: 2,
sender: 'Marketing',
subject: '📺 Local news wants to do a segment on us',
body: 'Channel 7 heard about us and wants to do a feel-good local business story. Free advertising!',
@@ -251,6 +409,7 @@ const EVENTS = [
]
},
{
+ weight: 3,
sender: 'Sales Team',
subject: '🎉 Big client just signed!',
body: null, // dynamic — set at trigger time
@@ -261,7 +420,7 @@ const EVENTS = [
const pick = unlocked[Math.floor(Math.random() * unlocked.length)];
const src = getSourceDef(pick.i);
const arc = ARCS[gs.arc];
- const name = arc.names[pick.i] || src.name;
+ const name = arc.sources[pick.i].name || src.name;
const mult = 5 + Math.floor(Math.random() * 6); // 5-10×
const bonus = Math.floor(sourceRevPerTick(pick.s) * mult);
if (bonus <= 0) return null;
@@ -279,6 +438,35 @@ const EVENTS = [
};
},
},
+ // R&D Breakthrough — permanent 2× revenue for a random source
+ {
+ weight: 2,
+ debugLabel: 'R&D Breakthrough',
+ generate: () => {
+ const arc = ARCS[gameState.arc];
+ const unlocked = gameState.sources.map((s, i) => ({ s, i })).filter(x => x.s.unlocked && x.s.employees > 0);
+ if (unlocked.length === 0) return null;
+ const pick = unlocked[Math.floor(Math.random() * unlocked.length)];
+ const name = arc.sources[pick.i].name;
+ const currentMult = pick.s.breakthroughMult || 1;
+ return {
+ sender: 'R&D Department',
+ subject: '🔬 Breakthrough Innovation!',
+ body: `Your ${name} team has made a major breakthrough! They've developed a revolutionary new process that will permanently double their revenue output.\n\nCurrent multiplier: ×${currentMult}\nNew multiplier: ×${currentMult * 2}`,
+ actions: [
+ { label: `Implement (+×2 ${name})`, effect: (gs) => {
+ gs.sources[pick.i].breakthroughMult = (gs.sources[pick.i].breakthroughMult || 1) * 2;
+ return `🔬 Breakthrough! ${name} revenue permanently doubled (now ×${gs.sources[pick.i].breakthroughMult})`;
+ }},
+ { label: 'File patent (5% cash)', effect: (gs) => {
+ const patent = Math.max(10, Math.floor(gs.cash * 0.05));
+ gs.cash += patent;
+ return `Filed patent for ${formatMoney(patent)} instead. Your lawyers are happy.`;
+ }},
+ ]
+ };
+ },
+ },
];
// ===== BOARD ROOM UPGRADES (Phase 2.2) =====
@@ -286,7 +474,7 @@ const BOARD_ROOM_UPGRADES = [
{
id: 'finance_dept_1',
name: 'Finance Dept Lv1',
- desc: 'The Intern — auto-earnings, but randomizes guidance (often wrong).',
+ desc: 'The Intern — auto-earnings, randomizes guidance (often wrong).',
cost: 500,
requires: null,
maxCount: 1,
@@ -295,7 +483,7 @@ const BOARD_ROOM_UPGRADES = [
{
id: 'finance_dept_2',
name: 'Finance Dept Lv2',
- desc: 'Competent CFO — analyzes trends, picks right guidance ~70% of the time.',
+ desc: 'Competent CFO — analyzes trends, picks right ~70% of the time.',
cost: 2500,
requires: 'finance_dept_1',
maxCount: 1,
@@ -364,6 +552,15 @@ const BOARD_ROOM_UPGRADES = [
maxCount: 1,
category: 'Investor',
},
+ {
+ id: 'cpa',
+ name: 'CPA on Retainer',
+ desc: 'Auto-pays taxes when affordable, auto-settles debts. No more IRS toasts.',
+ cost: 750,
+ requires: null,
+ maxCount: 1,
+ category: 'Tax',
+ },
{
id: 'golden_parachute',
name: 'Golden Parachute',
@@ -373,6 +570,16 @@ const BOARD_ROOM_UPGRADES = [
maxCount: Infinity,
category: 'Protection',
},
+ {
+ id: 'growth_initiative',
+ name: 'Growth Initiative',
+ desc: '+2% revenue multiplier (stacks). Cost scales 10% each.',
+ cost: 50,
+ requires: null,
+ maxCount: Infinity,
+ category: 'Revenue',
+ scalingCost: 1.10, // each purchase costs 10% more
+ },
];
function hasBoardRoomUpgrade(id) {
@@ -383,11 +590,22 @@ function getBoardRoomUpgradeCount(id) {
return gameState.boardRoomPurchases[id] || 0;
}
+function getUpgradeCost(upgrade) {
+ if (upgrade.scalingCost) {
+ const owned = getBoardRoomUpgradeCount(upgrade.id);
+ return Math.floor(upgrade.cost * Math.pow(upgrade.scalingCost, owned));
+ }
+ return upgrade.cost;
+}
+
function getBoardRoomRevMultiplier() {
let mult = 1.0;
if (hasBoardRoomUpgrade('rev_mult_1')) mult *= 1.1;
if (hasBoardRoomUpgrade('rev_mult_2')) mult *= 1.25;
if (hasBoardRoomUpgrade('rev_mult_3')) mult *= 1.5;
+ // Growth Initiative: +2% per purchase, stacks multiplicatively
+ const gi = getBoardRoomUpgradeCount('growth_initiative');
+ if (gi > 0) mult *= Math.pow(1.02, gi);
return mult;
}
@@ -421,39 +639,125 @@ function pickCFOGuidance(level) {
return 'ambitious';
}
- // Lv2 and Lv3: project revenue vs each guidance target
- const projectedRev = totalRevPerTick() * EARNINGS_QUARTER_DAYS;
- const safetyMargin = level >= 3 ? 1.05 : 1.20;
-
- // Find the most aggressive guidance we can safely beat
- let bestPick = 'conservative'; // fallback
- for (const key of keys) {
- const gl = GUIDANCE_LEVELS[key];
- const target = projectedRev * gl.pct * gameState.analystBaseline;
- if (projectedRev > target * safetyMargin) {
- bestPick = key;
+ // Lv2/Lv3: context-aware weighted pick
+ // Base weights for each guidance level
+ const weights = { conservative: 0, 'in-line': 0, ambitious: 0, aggressive: 0 };
+ const streak = gameState.earningsStreak;
+ const baseline = gameState.analystBaseline;
+
+ if (level === 2) {
+ // Competent CFO — reads trends but not perfectly
+ if (streak <= -2) {
+ // After multiple misses: play it safe
+ weights.conservative = 70;
+ weights['in-line'] = 25;
+ weights.ambitious = 5;
+ } else if (streak < 0) {
+ // After one miss: cautious
+ weights.conservative = 40;
+ weights['in-line'] = 45;
+ weights.ambitious = 15;
+ } else if (streak === 0) {
+ // Fresh start or reset: balanced
+ weights.conservative = 20;
+ weights['in-line'] = 50;
+ weights.ambitious = 25;
+ weights.aggressive = 5;
+ } else if (streak <= 3) {
+ // Short beat streak: getting confident
+ weights.conservative = 10;
+ weights['in-line'] = 35;
+ weights.ambitious = 40;
+ weights.aggressive = 15;
+ } else {
+ // Long beat streak: bold but not reckless
+ weights.conservative = 5;
+ weights['in-line'] = 20;
+ weights.ambitious = 45;
+ weights.aggressive = 30;
}
- }
- if (level >= 3) {
- // Elite CFO adjustments
- const keyIdx = keys.indexOf(bestPick);
+ // Analyst pressure adjustment — high baseline means analysts expect more
+ if (baseline > 1.15) {
+ weights.conservative += 20;
+ weights['in-line'] += 10;
+ weights.ambitious -= 15;
+ weights.aggressive -= 15;
+ }
+ } else {
+ // Lv3: Elite CFO — smarter weights, factors in more context
+ if (streak <= -2) {
+ weights.conservative = 50;
+ weights['in-line'] = 40;
+ weights.ambitious = 10;
+ } else if (streak < 0) {
+ weights.conservative = 25;
+ weights['in-line'] = 50;
+ weights.ambitious = 20;
+ weights.aggressive = 5;
+ } else if (streak === 0) {
+ weights.conservative = 10;
+ weights['in-line'] = 40;
+ weights.ambitious = 35;
+ weights.aggressive = 15;
+ } else if (streak <= 3) {
+ weights.conservative = 5;
+ weights['in-line'] = 20;
+ weights.ambitious = 45;
+ weights.aggressive = 30;
+ } else if (streak <= 6) {
+ // Strong streak but analysts ratcheting — pull back slightly
+ weights.conservative = 5;
+ weights['in-line'] = 25;
+ weights.ambitious = 40;
+ weights.aggressive = 30;
+ } else {
+ // Very long streak — analysts are aggressive, play safer
+ weights.conservative = 10;
+ weights['in-line'] = 35;
+ weights.ambitious = 35;
+ weights.aggressive = 20;
+ }
- // Long streak (>5): one notch safer — analysts are ratcheting hard
- if (gameState.earningsStreak > 5 && keyIdx > 0) {
- bestPick = keys[keyIdx - 1];
+ // High analyst baseline: elite CFO recognizes danger
+ if (baseline > 1.15) {
+ weights.conservative += 15;
+ weights['in-line'] += 10;
+ weights.ambitious -= 10;
+ weights.aggressive -= 15;
+ } else if (baseline < 0.85) {
+ // Low expectations: be bolder
+ weights.ambitious += 10;
+ weights.aggressive += 10;
+ weights.conservative -= 10;
+ weights['in-line'] -= 10;
}
- // Active revenue bonus: one notch more aggressive
- else if (gameState.revBonus && Date.now() < gameState.revBonus.until && keyIdx < keys.length - 1) {
- bestPick = keys[keyIdx + 1];
+
+ // Active revenue penalty: notch safer
+ if (gameState.revPenalty && Date.now() < gameState.revPenalty.until) {
+ weights.conservative += 20;
+ weights.ambitious -= 10;
+ weights.aggressive -= 10;
}
- // Active revenue penalty: one notch safer
- else if (gameState.revPenalty && Date.now() < gameState.revPenalty.until && keyIdx > 0) {
- bestPick = keys[keyIdx - 1];
+ // Active revenue bonus: notch bolder
+ if (gameState.revBonus && Date.now() < gameState.revBonus.until) {
+ weights.ambitious += 10;
+ weights.aggressive += 10;
+ weights.conservative -= 10;
}
}
- return bestPick;
+ // Ensure no negative weights
+ for (const k of keys) weights[k] = Math.max(0, weights[k]);
+
+ // Weighted random pick
+ const total = keys.reduce((sum, k) => sum + weights[k], 0);
+ let roll = Math.random() * total;
+ for (const key of keys) {
+ roll -= weights[key];
+ if (roll <= 0) return key;
+ }
+ return 'in-line'; // fallback
}
function setActiveCFOLevel(level) {
@@ -474,6 +778,7 @@ let gameState = {
revPenalty: null,
revBonus: null,
powerOutage: null,
+ dbOutage: null,
hireFrozen: null,
taxDebts: [],
seriesAShown: false,
@@ -484,6 +789,7 @@ let gameState = {
totalPlayTime: 0,
miniTaskCooldown: 0,
miniTaskActive: false,
+ miniTaskBlocked: null,
miniTaskStreak: 0,
goldenCellActive: false,
goldenCellCooldown: 60, // don't spawn for first 60s
@@ -519,6 +825,7 @@ let gameState = {
activeCFOLevel: 0, // 0 = manual, 1/2/3 = Finance Dept level in use
cfoRecords: {}, // { 1: {beats:0,total:0}, 2: {...}, 3: {...} }
revenueHistory: [], // last 3 quarterly revenues for trend analysis
+ lastQuarterRE: 0, // RE earned last quarter (for ETA display)
};
let gridBuilt = false;
@@ -554,6 +861,34 @@ function automateCost(source) {
return Math.floor(Math.max(50, stats.unlockCost) * stats.autoCostMult);
}
+// Prestige: restructure a department for 10× revenue, costs RE
+function prestigeCost(source) {
+ const tier = source.id;
+ const level = source.prestigeLevel || 0;
+ // Mild tier scaling: lemonade stand is cheap, megacorp is expensive
+ // 50 × (1 + tier) × 3^level
+ // Tier 0: 50 → 150 → 450 | Tier 11: 600 → 1800 → 5400
+ return Math.floor(50 * (1 + tier) * Math.pow(3, level));
+}
+
+function restructureSource(index) {
+ if (!gameState.isPublic) return; // requires IPO
+ const state = gameState.sources[index];
+ if (!state.unlocked || state.employees === 0) return;
+ const cost = prestigeCost(state);
+ if (gameState.retainedEarnings < cost) return;
+ gameState.retainedEarnings -= cost;
+ state.prestigeLevel = (state.prestigeLevel || 0) + 1;
+ const src = getSourceDef(index);
+ document.getElementById('status-text').textContent = `★ ${src.name} restructured! Revenue ×10 (Prestige ${state.prestigeLevel})`;
+ setTimeout(() => { document.getElementById('status-text').textContent = 'Ready'; }, 4000);
+ _lastTaxPanelHash = ''; // force IR rebuild (RE changed)
+ updateDisplay();
+ updateGridValues();
+ saveGame();
+ mobileHaptic('heavy');
+}
+
function maxAffordable(source) {
let cash = gameState.cash;
let emps = source.employees;
@@ -590,13 +925,18 @@ function sourceRevPerTick(source) {
if (!source.unlocked || source.employees === 0) return 0;
const stats = SOURCE_STATS[source.id];
const upgradeMult = 1 + source.upgradeLevel * 0.5;
- return source.employees * stats.baseRate * upgradeMult / 365.25;
+ const prestigeMult = Math.pow(10, source.prestigeLevel || 0);
+ const breakthroughMult = source.breakthroughMult || 1;
+ const focusMult = isFeatureEnabled('managementFocus') ? 1 + (source.focus || 0) * 0.05 : 1;
+ return source.employees * stats.baseRate * upgradeMult * prestigeMult * breakthroughMult * focusMult / 365.25;
}
function totalRevPerTick() {
let total = 0;
- for (const s of gameState.sources) {
- total += sourceRevPerTick(s);
+ const dbOut = gameState.dbOutage && Date.now() < gameState.dbOutage.until ? gameState.dbOutage.sourceIndex : -1;
+ for (let i = 0; i < gameState.sources.length; i++) {
+ if (i === dbOut) continue; // department offline from DB corruption
+ total += sourceRevPerTick(gameState.sources[i]);
}
// Board Room revenue multiplier
total *= getBoardRoomRevMultiplier();
@@ -621,7 +961,10 @@ function sourceAnnualRev(source) {
if (!source.unlocked || source.employees === 0) return 0;
const stats = SOURCE_STATS[source.id];
const upgradeMult = 1 + source.upgradeLevel * 0.5;
- return source.employees * stats.baseRate * upgradeMult;
+ const prestigeMult = Math.pow(10, source.prestigeLevel || 0);
+ const breakthroughMult = source.breakthroughMult || 1;
+ const focusMult = isFeatureEnabled('managementFocus') ? 1 + (source.focus || 0) * 0.05 : 1;
+ return source.employees * stats.baseRate * upgradeMult * prestigeMult * breakthroughMult * focusMult;
}
function totalAnnualRev() {
@@ -636,6 +979,12 @@ function totalAnnualRev() {
// ===== FORMATTING =====
function formatMoney(n) {
+ if (n >= 1e33) return '$' + (n / 1e33).toFixed(2) + 'Dc';
+ if (n >= 1e30) return '$' + (n / 1e30).toFixed(2) + 'No';
+ if (n >= 1e27) return '$' + (n / 1e27).toFixed(2) + 'Oc';
+ if (n >= 1e24) return '$' + (n / 1e24).toFixed(2) + 'Sp';
+ if (n >= 1e21) return '$' + (n / 1e21).toFixed(2) + 'Sx';
+ if (n >= 1e18) return '$' + (n / 1e18).toFixed(2) + 'Qi';
if (n >= 1e15) return '$' + (n / 1e15).toFixed(2) + 'Q';
if (n >= 1e12) return '$' + (n / 1e12).toFixed(2) + 'T';
if (n >= 1e9) return '$' + (n / 1e9).toFixed(2) + 'B';
@@ -650,6 +999,12 @@ function formatNum(n) {
function formatRate(annualRev) {
// Show rate in the most readable unit
+ if (annualRev >= 1e33) return '$' + (annualRev / 1e33).toFixed(1) + 'Dc/yr';
+ if (annualRev >= 1e30) return '$' + (annualRev / 1e30).toFixed(1) + 'No/yr';
+ if (annualRev >= 1e27) return '$' + (annualRev / 1e27).toFixed(1) + 'Oc/yr';
+ if (annualRev >= 1e24) return '$' + (annualRev / 1e24).toFixed(1) + 'Sp/yr';
+ if (annualRev >= 1e21) return '$' + (annualRev / 1e21).toFixed(1) + 'Sx/yr';
+ if (annualRev >= 1e18) return '$' + (annualRev / 1e18).toFixed(1) + 'Qi/yr';
if (annualRev >= 1e15) return '$' + (annualRev / 1e15).toFixed(1) + 'Q/yr';
if (annualRev >= 1e12) return '$' + (annualRev / 1e12).toFixed(1) + 'T/yr';
if (annualRev >= 1e9) return '$' + (annualRev / 1e9).toFixed(1) + 'B/yr';
@@ -659,6 +1014,12 @@ function formatRate(annualRev) {
}
function formatPerTick(perTick) {
+ if (perTick >= 1e33) return '$' + (perTick / 1e33).toFixed(1) + 'Dc';
+ if (perTick >= 1e30) return '$' + (perTick / 1e30).toFixed(1) + 'No';
+ if (perTick >= 1e27) return '$' + (perTick / 1e27).toFixed(1) + 'Oc';
+ if (perTick >= 1e24) return '$' + (perTick / 1e24).toFixed(1) + 'Sp';
+ if (perTick >= 1e21) return '$' + (perTick / 1e21).toFixed(1) + 'Sx';
+ if (perTick >= 1e18) return '$' + (perTick / 1e18).toFixed(1) + 'Qi';
if (perTick >= 1e15) return '$' + (perTick / 1e15).toFixed(1) + 'Q';
if (perTick >= 1e12) return '$' + (perTick / 1e12).toFixed(1) + 'T';
if (perTick >= 1e9) return '$' + (perTick / 1e9).toFixed(1) + 'B';
@@ -707,6 +1068,10 @@ function selectArc(arcKey) {
upgradeLevel: 0,
automated: false,
pendingCollect: 0,
+ prestigeLevel: 0,
+ breakthroughMult: 1,
+ focus: 0,
+ lastFocusClick: 0,
}));
gameState.seriesAShown = false;
gameState.totalPlayTime = 0;
@@ -714,6 +1079,7 @@ function selectArc(arcKey) {
gameState.eventCooldown = 30;
gameState.miniTaskCooldown = 10;
gameState.miniTaskActive = false;
+ gameState.miniTaskBlocked = null;
gameState.miniTaskStreak = 0;
gameState.goldenCellActive = false;
gameState.goldenCellCooldown = 60;
@@ -722,6 +1088,8 @@ function selectArc(arcKey) {
gameState.revPenalty = null;
gameState.revBonus = null;
gameState.powerOutage = null;
+ gameState.dbOutage = null;
+ gameState.miniTaskBlocked = null;
gameState.hireFrozen = null;
gameState.taxDebts = [];
gameState.quarterRevenue = 0;
@@ -753,6 +1121,7 @@ function selectArc(arcKey) {
gameState.activeCFOLevel = 0;
gameState.cfoRecords = {};
gameState.revenueHistory = [];
+ gameState.lastQuarterRE = 0;
// Clear stale panels
const taxPanel = document.getElementById('tax-panel');
@@ -770,6 +1139,25 @@ function selectArc(arcKey) {
document.getElementById('arc-select').classList.add('hidden');
document.getElementById('game-view').classList.remove('hidden');
+ mobileHaptic('heavy');
+
+ // Reset mobile state
+ if (isMobile()) {
+ _mobileActiveTab = 'operations';
+ const cashHeader = document.getElementById('mobile-cash-header');
+ if (cashHeader) cashHeader.classList.remove('visible');
+ document.querySelectorAll('.mob-nav-btn').forEach(btn => {
+ btn.classList.toggle('active', btn.dataset.tab === 'operations');
+ });
+ const gridContainer = document.getElementById('grid-container');
+ if (gridContainer) gridContainer.style.display = '';
+ const pnlView = document.getElementById('mobile-pnl-view');
+ const brView = document.getElementById('mobile-boardroom-view');
+ const settingsView = document.getElementById('mobile-settings-view');
+ if (pnlView) pnlView.classList.add('hidden');
+ if (brView) brView.classList.add('hidden');
+ if (settingsView) settingsView.classList.add('hidden');
+ }
buildGrid();
updateDisplay();
@@ -785,9 +1173,11 @@ function showArcSelect() {
div.onclick = () => selectArc(key);
div.innerHTML = `
${arc.icon}
- ${arc.name}
- ${arc.desc}
- ${arc.sources[0].name} → ${arc.sources[arc.sources.length - 1].name}
+
+
${arc.name}
+
${arc.desc}
+
${arc.sources[0].name} → ${arc.sources[arc.sources.length - 1].name}
+
`;
container.appendChild(div);
}
@@ -798,6 +1188,7 @@ function showArcSelect() {
// ===== MINI-TASKS =====
function trySpawnMiniTask() {
if (gameState.miniTaskActive) return;
+ if (gameState.miniTaskBlocked && Date.now() < gameState.miniTaskBlocked.until) return; // email server down
if (gameState.miniTaskCooldown > 0) { gameState.miniTaskCooldown--; return; }
// Frequency decreases as passive income grows
@@ -871,6 +1262,8 @@ function completeMiniTask() {
flashCash();
updateDisplay();
+ mobileHaptic('success');
+ mobileCashPulse();
}
function skipMiniTask() {
@@ -884,6 +1277,7 @@ function skipMiniTask() {
setTimeout(() => { document.getElementById('status-text').textContent = 'Ready'; }, 2000);
}
gameState.miniTaskStreak = 0;
+ mobileHaptic('light');
}
// ===== TIME SCALE CHANGE NOTIFICATION =====
@@ -904,8 +1298,12 @@ function trySpawnGoldenCell() {
const pick = unlocked[Math.floor(Math.random() * unlocked.length)];
const rowIndex = pick.i;
- // Pick a random visible cell in that row (columns b through h)
- const cols = ['cell-b', 'cell-c', 'cell-d', 'cell-e', 'cell-f', 'cell-g', 'cell-h'];
+ // Pick a random visible cell in that row
+ // On mobile, apply golden to the whole card row instead of a single cell
+ const _mob = isMobile();
+ const cols = _mob
+ ? ['cell-a', 'cell-b', 'cell-c', 'cell-d']
+ : ['cell-b', 'cell-c', 'cell-d', 'cell-e', 'cell-f', 'cell-g', 'cell-h'];
const colClass = cols[Math.floor(Math.random() * cols.length)];
// Find the grid row for this source
@@ -913,7 +1311,7 @@ function trySpawnGoldenCell() {
if (rowIndex >= gridRows.length) return;
const row = gridRows[rowIndex];
- const cell = row.querySelector('.' + colClass);
+ const cell = _mob ? row : row.querySelector('.' + colClass);
if (!cell) return;
const reward = Math.floor(totalRevPerTick() * 20);
@@ -952,6 +1350,8 @@ function clickGoldenCell(cell) {
setTimeout(() => { document.getElementById('status-text').textContent = 'Ready'; }, 2000);
flashCash();
+ mobileHaptic('heavy');
+ mobileCashPulse();
updateDisplay();
return true;
}
@@ -965,7 +1365,7 @@ function buildGrid() {
for (let i = 0; i < SOURCE_STATS.length; i++) {
const src = getSourceDef(i);
const state = gameState.sources[i];
- const rowNum = i + 4;
+ const rowNum = i + 3;
const row = document.createElement('div');
row.className = 'grid-row source-row';
@@ -974,6 +1374,8 @@ function buildGrid() {
if (!state.unlocked) {
row.classList.add('source-locked');
const nextUnlockable = isNextUnlock(i);
+ // On mobile, hide non-unlockable locked rows
+ if (!nextUnlockable) row.classList.add('mob-hidden');
row.innerHTML = `
${rowNum}
🔒 ${src.name}
@@ -1004,6 +1406,27 @@ function buildGrid() {
container.appendChild(row);
}
+ // Overtime row (below sources, hidden until feature enabled + has revenue)
+ const overtimeRow = document.createElement('div');
+ overtimeRow.className = 'grid-row source-row';
+ overtimeRow.id = 'overtime-row';
+ overtimeRow.style.display = 'none';
+ const otRowNum = SOURCE_STATS.length + 4;
+ overtimeRow.innerHTML = `
+ ${otRowNum}
+ ⏰ Overtime
+
+
+
+ Push It
+
+
+
+
+
+ `;
+ container.appendChild(overtimeRow);
+
// Filler rows
buildFillerRows();
@@ -1030,7 +1453,7 @@ function buildFillerRows() {
const available = viewportHeight - gridBottom - bottomChrome;
const fillerCount = Math.max(3, Math.ceil(available / ROW_HEIGHT) + 1);
- const startRow = SOURCE_STATS.length + 4 + taxRowCount;
+ const startRow = SOURCE_STATS.length + 3 + taxRowCount;
for (let i = 0; i < fillerCount; i++) {
const row = document.createElement('div');
row.className = 'filler-row';
@@ -1070,16 +1493,48 @@ function updateGridValues() {
const hCost = hireCost(state);
const uCost = upgradeCost(state);
const aCost = automateCost(state);
+ const _mob = isMobile();
- // Name
+ // Name (clickable for focus)
const nameCell = row.querySelector('[data-field="name"]');
- nameCell.innerHTML = src.name + (state.upgradeLevel > 0 ? ` Lv${state.upgradeLevel} ` : '');
+ const prestigeTag = (state.prestigeLevel || 0) > 0 ? ` ★${state.prestigeLevel} ` : '';
+ const breakthroughTag = (state.breakthroughMult || 1) > 1 ? ` 🔬×${state.breakthroughMult} ` : '';
+ const focusLevel = state.focus || 0;
+ const focusable = isFeatureEnabled('managementFocus') && state.automated;
+ const focusIcon = focusable ? `🎯 ` : '';
+ const lvBadgeCls = _mob ? 'mob-level-badge' : '';
+ const tags = (state.upgradeLevel > 0 ? `Lv${state.upgradeLevel} ` : '') + prestigeTag + breakthroughTag;
+ const autoTag = (_mob && state.automated) ? '⚡ ' : '';
+ nameCell.innerHTML = `${focusIcon}${src.name}${autoTag} ${tags} `;
+ if (focusable) {
+ nameCell.style.cursor = 'pointer';
+ nameCell.classList.add('focusable');
+ nameCell.onclick = () => clickFocus(i);
+ } else {
+ nameCell.style.cursor = '';
+ nameCell.classList.remove('focusable');
+ nameCell.onclick = null;
+ }
// Employees
row.querySelector('[data-field="emp"]').textContent = state.employees;
- // Rev/day (column C)
- row.querySelector('[data-field="rate"]').textContent = formatPerTick(revPerDay);
+ // Rev/day (column C) — show focus bonus when active
+ const rateCell = row.querySelector('[data-field="rate"]');
+ // DB outage visual
+ const isDbDown = gameState.dbOutage && Date.now() < gameState.dbOutage.until && gameState.dbOutage.sourceIndex === i;
+ if (isDbDown) {
+ row.classList.add('db-outage');
+ const secsLeft = Math.ceil((gameState.dbOutage.until - Date.now()) / 1000);
+ rateCell.innerHTML = `💾 OFFLINE ${secsLeft}s `;
+ } else {
+ row.classList.remove('db-outage');
+ if (isFeatureEnabled('managementFocus') && focusLevel > 0) {
+ rateCell.innerHTML = `${formatPerTick(revPerDay)} +${focusLevel * 5}% `;
+ } else {
+ rateCell.textContent = formatPerTick(revPerDay);
+ }
+ }
// Rev/yr (column G)
row.querySelector('[data-field="annual"]').textContent = formatRate(rev);
@@ -1094,31 +1549,61 @@ function updateGridValues() {
const maxHires = maxAffordable(state);
const upgradeMult = 1 + state.upgradeLevel * 0.5;
const hireGainPerDay = src.baseRate * upgradeMult / 365.25;
- a1.innerHTML = (maxHires > 1 ? `Max(${maxHires}) ` : '') +
- `= hCost ? '' : 'disabled'} title="Hire 1 employee — adds ${formatPerTick(hireGainPerDay)}/day">Hire ${formatMoney(hCost)} (+${formatPerTick(hireGainPerDay)}/d) `;
+ const hireLbl = _mob
+ ? `Hire ${formatMoney(hCost)}`
+ : `Hire ${formatMoney(hCost)} (+${formatPerTick(hireGainPerDay)}/d)`;
+ a1.innerHTML = (maxHires > 1 ? `Max(${maxHires}) ` : '') +
+ `= hCost ? '' : 'disabled'} title="Hire 1 employee — adds ${formatPerTick(hireGainPerDay)}/day">${hireLbl} `;
}
// Action 2: Upgrade or Automate
const a2 = row.querySelector('[data-field="action2"]');
- if (!state.automated) {
+ if (_mob) {
+ // Mobile: Always show Upgrade in cell-e (action2)
+ const maxUpgrades = maxAffordableUpgrades(state);
+ const revGainPerDay = state.employees * src.baseRate * 0.5 / 365.25;
+ const upgLbl = `Upgrade ${formatMoney(uCost)}`;
+ a2.innerHTML = (maxUpgrades > 1 ? `Max(${maxUpgrades}) ` : '') +
+ `= uCost ? '' : 'disabled'} title="+50% efficiency per employee — adds ${formatPerTick(revGainPerDay)}/day">${upgLbl} `;
+ } else if (!state.automated) {
a2.innerHTML = `= aCost ? '' : 'disabled'} title="Revenue flows automatically">Auto ${formatMoney(aCost)} `;
} else {
const maxUpgrades = maxAffordableUpgrades(state);
const revGainPerDay = state.employees * src.baseRate * 0.5 / 365.25;
- a2.innerHTML = (maxUpgrades > 1 ? `Max(${maxUpgrades}) ` : '') +
- `= uCost ? '' : 'disabled'} title="+50% efficiency per employee — adds ${formatPerTick(revGainPerDay)}/day">⬆ ${formatMoney(uCost)} (+${formatPerTick(revGainPerDay)}/d) `;
+ const upgLbl = `⬆ ${formatMoney(uCost)} (+${formatPerTick(revGainPerDay)}/d)`;
+ a2.innerHTML = (maxUpgrades > 1 ? `Max(${maxUpgrades}) ` : '') +
+ `= uCost ? '' : 'disabled'} title="+50% efficiency per employee — adds ${formatPerTick(revGainPerDay)}/day">${upgLbl} `;
}
- // Action 3: Collect (click) or AUTO badge
+ // Action 3: Collect (click) or AUTO badge + Restructure
const a3 = row.querySelector('[data-field="action3"]');
if (!state.automated) {
const pending = state.pendingCollect;
const clickVal = src.clickValue;
const hasPending = pending > 0.005;
- a3.innerHTML = `Collect${hasPending ? ' ' + formatMoney(pending) : ''} (+${formatMoney(clickVal)}) `;
+ const collectLbl = _mob
+ ? `💰 Collect${hasPending ? ' ' + formatMoney(pending) : ''}`
+ : `Collect${hasPending ? ' ' + formatMoney(pending) : ''} (+${formatMoney(clickVal)})`;
+ a3.innerHTML = `${collectLbl} `;
+ } else if (gameState.isPublic) {
+ const pCost = prestigeCost(state);
+ const canPrestige = gameState.retainedEarnings >= pCost;
+ a3.innerHTML = `⚡ ★ ${pCost.toLocaleString()} RE `;
} else {
a3.innerHTML = '⚡ AUTO ';
}
+
+ // Mobile: Auto button in separate row (cell-g repurposed, shown via CSS)
+ if (_mob) {
+ const autoCell = row.querySelector('[data-field="annual"]');
+ if (!state.automated) {
+ autoCell.innerHTML = `= aCost ? '' : 'disabled'} title="Revenue flows automatically">Automate ${formatMoney(aCost)} `;
+ row.classList.remove('mob-automated');
+ } else {
+ autoCell.innerHTML = '⚡ AUTO ';
+ row.classList.add('mob-automated');
+ }
+ }
}
}
@@ -1138,15 +1623,32 @@ function updateDisplay() {
const perTick = totalRevPerTick();
document.getElementById('total-rev').textContent = formatRate(totalRev);
- // Per-tick display (= per day, prominent)
- document.getElementById('per-tick-display').textContent = formatPerTick(perTick) + '/day';
+ // Per-tick display (= per day, prominent) — color-coded for active effects
+ const ptEl = document.getElementById('per-tick-display');
+ const hasOutage = gameState.powerOutage && Date.now() < gameState.powerOutage.until;
+ const hasPenalty = gameState.revPenalty && Date.now() < gameState.revPenalty.until;
+ const hasBonus = gameState.revBonus && Date.now() < gameState.revBonus.until;
+ const hasDbOut = gameState.dbOutage && Date.now() < gameState.dbOutage.until;
+
+ if (hasOutage) {
+ ptEl.innerHTML = `⚡ $0.00/day `;
+ } else if (hasPenalty) {
+ const pct = Math.round((1 - gameState.revPenalty.mult) * 100);
+ ptEl.innerHTML = `${formatPerTick(perTick)}/day ▼${pct}% `;
+ } else if (hasDbOut) {
+ ptEl.innerHTML = `${formatPerTick(perTick)}/day 💾 `;
+ } else if (hasBonus) {
+ ptEl.innerHTML = `${formatPerTick(perTick)}/day ▲×${gameState.revBonus.mult} `;
+ } else {
+ ptEl.textContent = formatPerTick(perTick) + '/day';
+ }
// Stock price in header (Phase 2.1)
const stockCell = document.getElementById('stock-price-cell');
if (stockCell) {
if (gameState.isPublic) {
const sp = getStockPrice();
- stockCell.innerHTML = `📈 ${formatMoney(sp)} `;
+ stockCell.innerHTML = `Stock: ${formatMoney(sp)} `;
} else {
stockCell.innerHTML = '';
}
@@ -1209,12 +1711,27 @@ function updateDisplay() {
document.getElementById('status-text').textContent = `⚡ SYSTEMS OFFLINE — back in ${secsLeft}s`;
} else if (gameState.powerOutage) {
gameState.powerOutage = null;
+ gameState.dbOutage = null;
+ gameState.miniTaskBlocked = null;
document.getElementById('status-text').textContent = '✅ Systems restored';
setTimeout(() => {
if (document.getElementById('status-text').textContent === '✅ Systems restored') {
document.getElementById('status-text').textContent = 'Ready';
}
}, 3000);
+ } else if (gameState.dbOutage && Date.now() < gameState.dbOutage.until) {
+ const secsLeft = Math.ceil((gameState.dbOutage.until - Date.now()) / 1000);
+ const arc = ARCS[gameState.arc];
+ const name = arc ? arc.sources[gameState.dbOutage.sourceIndex].name || 'Department' : 'Department';
+ document.getElementById('status-text').textContent = `💾 ${name} offline — DB recovering ${secsLeft}s`;
+ } else if (gameState.dbOutage) {
+ gameState.dbOutage = null;
+ document.getElementById('status-text').textContent = '✅ Database restored';
+ setTimeout(() => {
+ if (document.getElementById('status-text').textContent === '✅ Database restored') {
+ document.getElementById('status-text').textContent = 'Ready';
+ }
+ }, 3000);
} else if (gameState.revPenalty && Date.now() < gameState.revPenalty.until) {
const secsLeft = Math.ceil((gameState.revPenalty.until - Date.now()) / 1000);
const hireFroze = gameState.hireFrozen && Date.now() < gameState.hireFrozen;
@@ -1232,14 +1749,21 @@ function updateDisplay() {
} else if (gameState.hireFrozen && Date.now() < gameState.hireFrozen) {
const secsLeft = Math.ceil((gameState.hireFrozen - Date.now()) / 1000);
document.getElementById('status-text').textContent = `🚫 Hiring frozen — ${secsLeft}s remaining`;
+ } else if (gameState.miniTaskBlocked && Date.now() < gameState.miniTaskBlocked.until) {
+ const secsLeft = Math.ceil((gameState.miniTaskBlocked.until - Date.now()) / 1000);
+ document.getElementById('status-text').textContent = `📧 Email down — no approvals for ${secsLeft}s`;
} else {
if (gameState.hireFrozen) gameState.hireFrozen = null;
+ if (gameState.miniTaskBlocked) gameState.miniTaskBlocked = null;
}
// Don't overwrite mini-task feedback messages
// Update P&L section
updateTaxPanel();
+ // Update overtime row
+ updateOvertimeRow();
+
// Update Board Room tab visibility and content
updateBoardRoomTab();
if (gameState.activeTab === 'boardroom') {
@@ -1261,6 +1785,18 @@ function unlockSource(index) {
buildGrid();
updateDisplay();
flashCash();
+ mobileHaptic('success');
+ // Animate newly unlocked card on mobile
+ if (isMobile()) {
+ mobileCashPulse();
+ const newRow = document.getElementById(`source-row-${index}`);
+ if (newRow) {
+ newRow.classList.add('mob-just-unlocked');
+ setTimeout(() => newRow.classList.remove('mob-just-unlocked'), 600);
+ // Scroll to the new card
+ setTimeout(() => newRow.scrollIntoView({ behavior: 'smooth', block: 'center' }), 100);
+ }
+ }
}
function hireEmployee(index) {
@@ -1277,6 +1813,7 @@ function hireEmployee(index) {
updateGridValues();
updateDisplay();
flashCash();
+ mobileHaptic('light');
}
function hireMax(index) {
@@ -1338,6 +1875,7 @@ function upgradeSource(index) {
updateGridValues();
updateDisplay();
flashCash();
+ mobileHaptic('medium');
}
function automateSource(index) {
@@ -1346,6 +1884,10 @@ function automateSource(index) {
const cost = automateCost(state);
if (gameState.cash < cost) return;
+ // Prevent double-tap on mobile
+ if (isMobile() && automateSource._lastTap && Date.now() - automateSource._lastTap < 400) return;
+ automateSource._lastTap = Date.now();
+
gameState.cash -= cost;
state.automated = true;
addCapitalExpense(cost);
@@ -1358,6 +1900,19 @@ function automateSource(index) {
updateGridValues();
updateDisplay();
flashCash();
+ mobileHaptic('success');
+
+ // Focus tip — show once per game after first automate
+ if (isFeatureEnabled('managementFocus') && !gameState.focusTipShown) {
+ gameState.focusTipShown = true;
+ setTimeout(() => {
+ document.getElementById('status-text').textContent = '💡 Tip: Click department names to boost their revenue';
+ setTimeout(() => {
+ const st = document.getElementById('status-text');
+ if (st && st.textContent.startsWith('💡')) st.textContent = 'Ready';
+ }, 5000);
+ }, 2000);
+ }
}
function collectSource(index) {
@@ -1383,6 +1938,8 @@ function collectSource(index) {
updateGridValues();
updateDisplay();
flashCash();
+ mobileHaptic('light');
+ mobileCashPulse();
}
// ===== DEPRECIATION =====
@@ -1428,6 +1985,7 @@ function flashCash() {
// ===== TAX DEBT SYSTEM =====
function processQuarterlyTax() {
+ const hasCPA = hasBoardRoomUpgrade('cpa');
const depreciation = getQuarterlyDepreciation();
const taxableIncome = gameState.quarterRevenue - depreciation;
const taxRate = getBoardRoomTaxRate();
@@ -1451,23 +2009,69 @@ function processQuarterlyTax() {
gameState.quarterExpenses = 0;
gameState.quarterTaxPaid = 0;
+ const currentDay = Math.floor(gameState.gameElapsedSecs / SECS_PER_DAY);
+ const quarter = Math.floor(currentDay / 90);
+ const qLabel = `Q${(quarter % 4) + 1}`;
+
+ // CPA auto-pay: handle taxes silently
+ if (hasCPA) {
+ if (taxOwed <= 0) {
+ // No tax — CPA files a zero return silently
+ return;
+ }
+ if (gameState.cash >= taxOwed) {
+ // Can afford — auto-pay
+ gameState.cash -= taxOwed;
+ gameState.quarterTaxPaid += taxOwed;
+ gameState.totalTaxPaid += taxOwed;
+ document.getElementById('status-text').textContent = `📋 CPA paid ${qLabel} taxes (${formatMoney(taxOwed)})`;
+ setTimeout(() => { document.getElementById('status-text').textContent = 'Ready'; }, 4000);
+ updateDisplay();
+ flashCash();
+ return;
+ } else {
+ // Can't afford — CPA defers (creates debt like Ignore, but silently)
+ if (!gameState.taxDebts) gameState.taxDebts = [];
+ gameState.taxDebts.push({
+ original: taxOwed,
+ current: taxOwed,
+ dayCreated: currentDay,
+ daysOverdue: 0,
+ stage: 'notice1',
+ quarter: qLabel,
+ });
+ document.getElementById('status-text').textContent = `📋 CPA deferred ${qLabel} taxes — insufficient funds`;
+ setTimeout(() => { document.getElementById('status-text').textContent = 'Ready'; }, 4000);
+ updateTaxPanel();
+ return;
+ }
+ }
+
if (taxOwed <= 0) {
- const currentDay = Math.floor(gameState.gameElapsedSecs / SECS_PER_DAY);
- const quarter = Math.floor(currentDay / 90);
- const qLabel = `Q${(quarter % 4) + 1}`;
showEventToast('IRS', `${qLabel} Quarterly Tax Assessment`,
- `Revenue: ${formatMoney(qRev)}\nDepreciation: (${formatMoney(qDep)})\nTaxable income: ${formatMoney(Math.max(0, taxableIncome))}\n\nNo tax owed this quarter.`,
- [{ label: 'OK', effect: () => `No tax liability for ${qLabel}. Keep investing!` }]);
+ `
+
Revenue ${formatMoney(qRev)}
+
Depreciation (${formatMoney(qDep)})
+
Taxable Income ${formatMoney(Math.max(0, taxableIncome))}
+
+
Tax Owed $0 ✓
+
`,
+ [{ label: 'OK', effect: () => `No tax liability for ${qLabel}. Keep investing!` }],
+ { html: true });
return;
}
- const currentDay = Math.floor(gameState.gameElapsedSecs / SECS_PER_DAY);
- const quarter = Math.floor(currentDay / 90);
- const qLabel = `Q${(quarter % 4) + 1}`;
-
- const amtNote = isAMT ? `\n⚠️ AMT applies (${Math.round(amtRate*100)}% of revenue > regular tax)` : '';
+ const amtNote = isAMT ? `⚠️ AMT ${Math.round(amtRate*100)}% of revenue > regular tax
` : '';
showEventToast('IRS', `${qLabel} Quarterly Tax Assessment`,
- `Revenue: ${formatMoney(qRev)}\nDepreciation: (${formatMoney(qDep)})\nTaxable income: ${formatMoney(taxableIncome)}\n\nTax owed (${isAMT ? 'AMT ' + Math.round(amtRate*100) + '%' : Math.round(taxRate*100) + '%'}): ${formatMoney(taxOwed)}${amtNote}`,
+ `
+
Revenue ${formatMoney(qRev)}
+
Depreciation (${formatMoney(qDep)})
+
Taxable Income ${formatMoney(taxableIncome)}
+
+
Tax Rate ${isAMT ? 'AMT ' + Math.round(amtRate*100) + '%' : Math.round(taxRate*100) + '%'}
+
Tax Owed ${formatMoney(taxOwed)}
+ ${amtNote}
+
`,
[
{ label: `Pay ${formatMoney(taxOwed)}`,
disabledLabel: 'Not enough cash',
@@ -1491,12 +2095,33 @@ function processQuarterlyTax() {
updateTaxPanel();
return `You ignored the ${qLabel} tax bill. ${formatMoney(taxOwed)} added to tax liability. Interest is accruing...`;
}},
- ], { closable: false });
+ ], { closable: false, html: true });
}
function processTaxDebts() {
if (!gameState.taxDebts || gameState.taxDebts.length === 0) return;
+ // CPA auto-settle: try to pay off debts before they escalate
+ if (hasBoardRoomUpgrade('cpa')) {
+ for (let i = gameState.taxDebts.length - 1; i >= 0; i--) {
+ const debt = gameState.taxDebts[i];
+ if (debt.settled) continue;
+ if (gameState.cash >= debt.current) {
+ gameState.cash -= debt.current;
+ gameState.quarterTaxPaid += debt.current;
+ gameState.totalTaxPaid += debt.current;
+ document.getElementById('status-text').textContent = `📋 CPA settled ${debt.quarter} tax debt (${formatMoney(debt.current)})`;
+ setTimeout(() => { document.getElementById('status-text').textContent = 'Ready'; }, 4000);
+ gameState.taxDebts.splice(i, 1);
+ _lastTaxPanelHash = '';
+ updateTaxPanel();
+ updateDisplay();
+ flashCash();
+ }
+ }
+ if (gameState.taxDebts.length === 0) return;
+ }
+
let needsUpdate = false;
for (const debt of gameState.taxDebts) {
// Accrue 1% daily interest
@@ -1645,10 +2270,18 @@ function updateTaxPanel() {
panel.classList.remove('hidden');
const sourceCount = SOURCE_STATS.length;
- let rowNum = sourceCount + 4;
+ let rowNum = sourceCount + 3;
let html = '';
- // ===== P&L SECTION =====
+ // ===== P&L SECTION (collapsible) =====
+ const pnlCollapsed = gameState.pnlCollapsed || false;
+ const pnlArrow = pnlCollapsed ? '▶' : '▼';
+
+ const currentDay = Math.floor(gameState.gameElapsedSecs / SECS_PER_DAY);
+ const daysIntoQuarter = currentDay - gameState.lastQuarterDay;
+ const daysToTax = Math.max(0, 90 - daysIntoQuarter);
+ const garnishActive = gameState.taxDebts && gameState.taxDebts.some(d => d.stage === 'garnish');
+
// Separator
html += `
${rowNum++}
@@ -1658,123 +2291,119 @@ function updateTaxPanel() {
`;
- const currentDay = Math.floor(gameState.gameElapsedSecs / SECS_PER_DAY);
- const daysIntoQuarter = currentDay - gameState.lastQuarterDay;
- const daysToTax = Math.max(0, 90 - daysIntoQuarter);
-
- // P&L Header
+ // P&L Header (clickable to collapse)
html += ``;
- const totalExpenses = gameState.totalSpentHires + gameState.totalSpentUpgrades + gameState.totalSpentAuto;
- const qDepreciation = getQuarterlyDepreciation();
-
- // Revenue
- html += `
-
${rowNum++}
-
Revenue
-
-
${formatMoney(gameState.quarterRevenue)}
-
${formatMoney(gameState.totalEarned)}
-
-
-
`;
+ if (!pnlCollapsed) {
+ const totalExpenses = gameState.totalSpentHires + gameState.totalSpentUpgrades + gameState.totalSpentAuto;
+ const qDepreciation = getQuarterlyDepreciation();
- // Garnishment (if active)
- const garnishActive = gameState.taxDebts && gameState.taxDebts.some(d => d.stage === 'garnish');
- if (garnishActive) {
- const garnishLoss = Math.floor(gameState.quarterRevenue * 0.15 / 0.85); // estimate pre-garnish revenue lost
+ // Revenue
html += `
${rowNum++}
-
🔴 IRS Garnishment
+
Revenue
-
−15%
-
-
settle debt to remove
+
${formatMoney(gameState.quarterRevenue)}
+
${formatMoney(gameState.totalEarned)}
+
`;
- }
- // Capital Spending (this quarter)
- html += `
-
${rowNum++}
-
Capital Spending
-
-
(${formatMoney(gameState.quarterExpenses)})
-
(${formatMoney(totalExpenses)})
-
not tax-deductible
-
-
`;
+ // Garnishment (if active)
+ if (garnishActive) {
+ html += `
+
${rowNum++}
+
🔴 IRS Garnishment
+
+
−15%
+
+
settle debt to remove
+
+
`;
+ }
- // Depreciation
- const capCount = gameState.capitalExpenses ? gameState.capitalExpenses.length : 0;
- html += `
-
${rowNum++}
-
Depreciation
-
-
(${formatMoney(qDepreciation)})
-
-
${capCount} assets depreciating
-
-
`;
+ // Capital Spending
+ html += `
+
${rowNum++}
+
Capital Spending
+
+
(${formatMoney(gameState.quarterExpenses)})
+
(${formatMoney(totalExpenses)})
+
not tax-deductible
+
+
`;
- // Taxes paid
- html += `
-
${rowNum++}
-
Taxes Paid
-
-
(${formatMoney(gameState.quarterTaxPaid)})
-
(${formatMoney(gameState.totalTaxPaid)})
-
-
-
`;
+ // Depreciation
+ const capCount = gameState.capitalExpenses ? gameState.capitalExpenses.length : 0;
+ html += `
+
${rowNum++}
+
Depreciation
+
+
(${formatMoney(qDepreciation)})
+
+
${capCount} assets depreciating
+
+
`;
- // Taxable Income (revenue - depreciation, what IRS taxes — or AMT if higher)
- const qTaxable = Math.max(0, gameState.quarterRevenue - qDepreciation);
- const currentTaxRate = getBoardRoomTaxRate();
- const currentAMTRate = getBoardRoomAMTRate();
- const qRegularTax = Math.floor(qTaxable * currentTaxRate);
- const qAMT = Math.floor(gameState.quarterRevenue * currentAMTRate);
- const amtApplies = qAMT > qRegularTax && gameState.quarterRevenue > 0;
- const effectiveTaxable = amtApplies ? gameState.quarterRevenue : qTaxable;
- const taxRatePct = Math.round(currentTaxRate * 100);
- const amtRatePct = Math.round(currentAMTRate * 100);
- const taxableLabel = amtApplies ? 'AMT Taxable Income' : 'Taxable Income';
- const taxableNote = amtApplies ? `⚠️ AMT floor (${amtRatePct}% of revenue)` : 'rev − depreciation';
- html += `
-
${rowNum++}
-
${taxableLabel}
-
-
${formatMoney(effectiveTaxable)}
-
-
${taxableNote}
-
-
`;
+ // Taxes paid
+ html += `
+
${rowNum++}
+
Taxes Paid
+
+
(${formatMoney(gameState.quarterTaxPaid)})
+
(${formatMoney(gameState.totalTaxPaid)})
+
+
+
`;
- // Net Income (double-underline like real accounting)
- const qNet = gameState.quarterRevenue - gameState.quarterExpenses - gameState.quarterTaxPaid;
- const ltNet = gameState.totalEarned - totalExpenses - gameState.totalTaxPaid;
- const qColor = qNet >= 0 ? '#217346' : '#c00';
- const ltColor = ltNet >= 0 ? '#217346' : '#c00';
+ // Taxable Income
+ const qTaxable = Math.max(0, gameState.quarterRevenue - qDepreciation);
+ const currentTaxRate = getBoardRoomTaxRate();
+ const currentAMTRate = getBoardRoomAMTRate();
+ const qRegularTax = Math.floor(qTaxable * currentTaxRate);
+ const qAMT = Math.floor(gameState.quarterRevenue * currentAMTRate);
+ const amtApplies = qAMT > qRegularTax && gameState.quarterRevenue > 0;
+ const effectiveTaxable = amtApplies ? gameState.quarterRevenue : qTaxable;
+ const taxRatePct = Math.round(currentTaxRate * 100);
+ const amtRatePct = Math.round(currentAMTRate * 100);
+ const taxableLabel = amtApplies ? 'AMT Taxable Income' : 'Taxable Income';
+ const taxableNote = amtApplies ? `⚠️ AMT floor (${amtRatePct}% of revenue)` : 'rev − depreciation';
+ html += `
+
${rowNum++}
+
${taxableLabel}
+
+
${formatMoney(effectiveTaxable)}
+
+
${taxableNote}
+
+
`;
- html += `
-
${rowNum++}
-
Net Income
-
-
${qNet < 0 ? '(' + formatMoney(-qNet) + ')' : formatMoney(qNet)}
-
${ltNet < 0 ? '(' + formatMoney(-ltNet) + ')' : formatMoney(ltNet)}
-
-
-
`;
+ // Net Income
+ const qNet = gameState.quarterRevenue - gameState.quarterExpenses - gameState.quarterTaxPaid;
+ const ltNet = gameState.totalEarned - totalExpenses - gameState.totalTaxPaid;
+ const qColor = qNet >= 0 ? '#217346' : '#c00';
+ const ltColor = ltNet >= 0 ? '#217346' : '#c00';
+
+ html += `
+
${rowNum++}
+
Net Income
+
+
${qNet < 0 ? '(' + formatMoney(-qNet) + ')' : formatMoney(qNet)}
+
${ltNet < 0 ? '(' + formatMoney(-ltNet) + ')' : formatMoney(ltNet)}
+
+
+
`;
+ }
// ===== INVESTOR RELATIONS SECTION (Phase 2.1) =====
if (gameState.isPublic) {
@@ -1800,7 +2429,8 @@ function updateTaxPanel() {
const trackColor = onTrack ? '#217346' : '#c00';
const trackDetail = `${onTrack ? '+' : ''}${formatCompact(trackDiff)} (${trackPct >= 0 ? '+' : ''}${trackPct.toFixed(1)}%)`;
const streakVal = gameState.earningsStreak;
- const streakStr = streakVal > 0 ? `🔥 ${streakVal} beat${streakVal > 1 ? 's' : ''}` :
+ const irStreakMult = streakVal >= 1 ? Math.min(2.0, 1 + streakVal * 0.1) : 1;
+ const streakStr = streakVal > 0 ? `🔥 ${streakVal} beat${streakVal > 1 ? 's' : ''} (${irStreakMult.toFixed(1)}× RE)` :
streakVal < 0 ? `❄️ ${Math.abs(streakVal)} miss${Math.abs(streakVal) > 1 ? 'es' : ''}` : '—';
// Separator
@@ -1843,7 +2473,7 @@ function updateTaxPanel() {
${rowNum++}
Revenue vs Target
${trackLabel}
- ${trackDetail}
+ ${trackPct >= 0 ? '+' : ''}${trackPct.toFixed(1)}%
${formatCompact(qRev)} / ${formatCompact(target)}
@@ -1871,10 +2501,16 @@ function updateTaxPanel() {
const btnStyle = (active) => active
? 'cursor:pointer;font-weight:700;color:#fff;background:#0078d4;padding:1px 6px;border-radius:2px;font-size:11px;margin-right:3px'
: 'cursor:pointer;color:#0078d4;border:1px solid #0078d4;padding:1px 5px;border-radius:2px;font-size:10px;margin-right:3px;background:transparent';
- let cfoButtons = `Manual `;
+ let cfoManual = `Manual `;
const labels = { 1: '👶 1', 2: '📊 2', 3: '🎩 3' };
+ const tooltips = {
+ 1: 'The Intern — auto-sets guidance randomly (often wrong)',
+ 2: 'Competent CFO — analyzes trends, ~70% optimal',
+ 3: 'Elite CFO — factors in everything, ~90% optimal'
+ };
+ let cfoLevels = '';
for (let lvl = 1; lvl <= maxCFOLevel; lvl++) {
- cfoButtons += `${labels[lvl]} `;
+ cfoLevels += `${labels[lvl]} `;
}
// Record for active CFO
const record = gameState.cfoRecords[activeCFO];
@@ -1888,8 +2524,8 @@ function updateTaxPanel() {
html += `
${rowNum++}
Finance Dept
-
${cfoButtons}
-
+
${cfoManual}
+
${cfoLevels}
${activeCFO > 0 ? 'Record' : ''}
${recordStr}
@@ -1925,14 +2561,41 @@ function updateTaxPanel() {
`;
// Retained Earnings
+ const lastQRE = gameState.lastQuarterRE || 0;
+ const lastQStr = lastQRE > 0 ? `Last Q: +${lastQRE}` : '';
+ // ETA: find cheapest purchasable upgrade, estimate quarters to afford
+ let etaStr = '';
+ if (lastQRE > 0) {
+ const available = BOARD_ROOM_UPGRADES.filter(u => {
+ const owned = getBoardRoomUpgradeCount(u.id);
+ if (owned >= u.maxCount) return false;
+ if (u.requires && !hasBoardRoomUpgrade(u.requires)) return false;
+ return true;
+ });
+ const nextUpgrade = available.sort((a, b) => getUpgradeCost(a) - getUpgradeCost(b))[0];
+ if (nextUpgrade) {
+ const cost = getUpgradeCost(nextUpgrade);
+ const remaining = cost - gameState.retainedEarnings;
+ if (remaining > 0) {
+ const quartersNeeded = Math.ceil(remaining / lastQRE);
+ const etaSecs = quartersNeeded * 90;
+ const etaTime = etaSecs >= 3600 ? `${Math.floor(etaSecs/3600)}h${Math.floor((etaSecs%3600)/60)}m`
+ : etaSecs >= 60 ? `${Math.floor(etaSecs/60)}m${etaSecs%60 ? etaSecs%60 + 's' : ''}`
+ : `${etaSecs}s`;
+ etaStr = `${nextUpgrade.name} in ~${quartersNeeded}Q (${etaTime})`;
+ } else {
+ etaStr = `${nextUpgrade.name} — affordable!`;
+ }
+ }
+ }
html += `
${rowNum++}
Retained Earnings
${gameState.retainedEarnings.toLocaleString()} RE
-
+
${lastQStr}
-
Spend in Board Room tab
+
${etaStr}
`;
@@ -2007,7 +2670,6 @@ function updateTaxPanel() {
}
// Only rebuild DOM if content actually changed (prevents click-swallowing race)
- // Hash key parts that change: P&L numbers, tax debt count/amounts/stages, days-to-tax, IR data
const hashParts = [
gameState.quarterRevenue|0, gameState.totalEarned|0,
gameState.quarterExpenses|0, gameState.quarterTaxPaid|0, gameState.totalTaxPaid|0,
@@ -2025,6 +2687,7 @@ function updateTaxPanel() {
// Phase 2.2: Board Room effects on display
getBoardRoomTaxRate(),
getBoardRoomRevMultiplier(),
+ gameState.pnlCollapsed ? 1 : 0,
].join('|');
if (hashParts !== _lastTaxPanelHash) {
@@ -2106,6 +2769,7 @@ function gameTick() {
if (currentDay - gameState.lastQuarterDay >= 90) {
processQuarterlyTax();
gameState.lastQuarterDay = currentDay;
+ gameState.overtimeClicks = 0; // reset overtime each quarter
}
// Tax debt processing (each tick = 1 day)
@@ -2136,9 +2800,9 @@ function gameTick() {
} else if (!document.getElementById('event-toast').classList.contains('hidden')) {
// Toast already visible
} else {
- if (Math.random() < 0.015 && gameState.totalPlayTime > 30) {
+ if (Math.random() < 0.02 * EVENT_FREQ_MULT && gameState.totalPlayTime > 30) {
triggerRandomEvent();
- gameState.eventCooldown = 60 + Math.floor(Math.random() * 60);
+ gameState.eventCooldown = Math.floor((30 + Math.floor(Math.random() * 30)) / (EVENT_FREQ_MULT || 1));
}
}
@@ -2148,10 +2812,27 @@ function gameTick() {
showSeriesA();
}
+ // Close the Deal spawning
+ if (isFeatureEnabled('closeTheDeals') && !gameState.dealActive) {
+ if (gameState.dealCooldown === undefined || gameState.dealCooldown === null) {
+ gameState.dealCooldown = 60 + Math.floor(Math.random() * 120);
+ }
+ if (gameState.dealCooldown > 0) {
+ gameState.dealCooldown--;
+ } else if (totalRevPerTick() > 0 && !isPowerOut &&
+ gameState.totalPlayTime > 60) {
+ spawnDeal();
+ gameState.dealCooldown = 90 + Math.floor(Math.random() * 150); // 1.5-4 min
+ }
+ }
+
// Valuation chart
sampleValuation();
drawValuationChart();
+ // Management focus decay
+ decayFocus();
+
updateToastButtons();
updateGridValues();
updateDisplay();
@@ -2159,8 +2840,13 @@ function gameTick() {
// ===== EVENTS =====
function triggerRandomEvent() {
- const event = EVENTS[Math.floor(Math.random() * EVENTS.length)];
- showEvent(event);
+ const totalWeight = EVENTS.reduce((sum, e) => sum + (e.weight || 1), 0);
+ let roll = Math.random() * totalWeight;
+ for (const event of EVENTS) {
+ roll -= (event.weight || 1);
+ if (roll <= 0) { showEvent(event); return; }
+ }
+ showEvent(EVENTS[EVENTS.length - 1]); // fallback
}
let eventToastTimer = null;
@@ -2168,7 +2854,11 @@ let _eventToastActions = null; // track actions with cashRequired for live updat
function showEvent(event) {
// Handle dynamic events that generate content at trigger time
- if (event.dynamic && event.setup) {
+ if (event.generate) {
+ const result = event.generate();
+ if (!result) return;
+ event = result;
+ } else if (event.dynamic && event.setup) {
const result = event.setup(gameState);
if (!result) return; // couldn't generate (e.g., no unlocked sources)
event = { ...event, body: result.body, actions: result.actions };
@@ -2179,21 +2869,31 @@ function showEvent(event) {
const toast = document.getElementById('event-toast');
- // Restore saved position if available
- const savedPos = localStorage.getItem('quarterClose_toastPos');
- if (savedPos) {
- const pos = JSON.parse(savedPos);
- toast.style.left = pos.left + 'px';
- toast.style.top = pos.top + 'px';
- toast.style.transform = 'none';
+ // On mobile, let CSS handle positioning; on desktop, restore saved position
+ if (isMobile()) {
+ toast.style.left = '';
+ toast.style.top = '';
+ toast.style.transform = '';
} else {
- toast.style.left = '50%';
- toast.style.top = '50%';
- toast.style.transform = 'translate(-50%, -50%)';
+ const savedPos = localStorage.getItem('quarterClose_toastPos');
+ if (savedPos) {
+ const pos = JSON.parse(savedPos);
+ toast.style.left = pos.left + 'px';
+ toast.style.top = pos.top + 'px';
+ toast.style.transform = 'none';
+ } else {
+ toast.style.left = '50%';
+ toast.style.top = '50%';
+ toast.style.transform = 'translate(-50%, -50%)';
+ }
}
document.getElementById('toast-sender').textContent = event.sender;
- document.getElementById('toast-body').textContent = event.body;
+ if (event.html) {
+ document.getElementById('toast-body').innerHTML = event.body;
+ } else {
+ document.getElementById('toast-body').textContent = event.body;
+ }
const actionsDiv = document.getElementById('toast-actions');
actionsDiv.innerHTML = '';
@@ -2252,6 +2952,7 @@ function showEvent(event) {
if (btn.disabled) return;
if (eventToastTimer) { clearTimeout(eventToastTimer); eventToastTimer = null; }
_eventToastActions = null;
+ mobileHaptic('medium');
const result = action.effect(gameState);
document.getElementById('status-text').textContent = result;
setTimeout(() => {
@@ -2306,15 +3007,66 @@ function updateToastButtons() {
}
}
-// ===== BOSS KEY =====
-function toggleBossMode() {
- gameState.bossMode = !gameState.bossMode;
- const gameVisible = !gameState.bossMode && gameState.arc;
- document.getElementById('game-view').classList.toggle('hidden', !gameVisible);
+// ===== DEBUG MODE — tap cash label 7× quickly =====
+let debugTapCount = 0;
+let debugTapTimer = null;
+let debugMode = false;
+
+function initDebugTap() {
+ const cashLabel = document.querySelector('.cash-label');
+ if (!cashLabel) return;
+ cashLabel.style.cursor = 'default';
+ cashLabel.addEventListener('click', () => {
+ debugTapCount++;
+ clearTimeout(debugTapTimer);
+ debugTapTimer = setTimeout(() => { debugTapCount = 0; }, 2000);
+ if (debugTapCount >= 7) {
+ debugTapCount = 0;
+ debugMode = !debugMode;
+ document.getElementById('debug-tools').classList.toggle('hidden', !debugMode);
+ document.getElementById('status-text').textContent = debugMode ? '🧪 Debug mode enabled' : '🧪 Debug mode disabled';
+ }
+ });
+}
+
+// ===== DEBUG: TRIGGER EVENTS =====
+function toggleDebugEventDropdown() {
+ const dd = document.getElementById('debug-event-dropdown');
+ if (!dd.classList.contains('hidden')) {
+ dd.classList.add('hidden');
+ return;
+ }
+ dd.innerHTML = '';
+ EVENTS.forEach((event) => {
+ const btn = document.createElement('div');
+ btn.style.cssText = 'padding:5px 10px;cursor:pointer;font-size:11px;border-bottom:1px solid #eee;white-space:nowrap;overflow:hidden;text-overflow:ellipsis';
+ btn.textContent = event.debugLabel || `${event.sender || 'Dynamic'} — ${(event.subject || '(dynamic event)').replace(/[\u{1F300}-\u{1FAFF}]/gu, '').trim()}`;
+ btn.onmouseenter = () => btn.style.background = '#e8f0fe';
+ btn.onmouseleave = () => btn.style.background = '';
+ btn.onclick = () => { dd.classList.add('hidden'); showEvent(event); };
+ dd.appendChild(btn);
+ });
+ dd.classList.remove('hidden');
+}
+
+// Close dropdown on outside click
+document.addEventListener('click', (e) => {
+ const dd = document.getElementById('debug-event-dropdown');
+ if (dd && !dd.classList.contains('hidden') && !e.target.closest('#debug-event-dropdown') && !e.target.textContent.includes('Event ▾')) {
+ dd.classList.add('hidden');
+ }
+});
+
+// ===== BOSS KEY =====
+function toggleBossMode() {
+ gameState.bossMode = !gameState.bossMode;
+ const gameVisible = !gameState.bossMode && gameState.arc;
+ document.getElementById('game-view').classList.toggle('hidden', !gameVisible);
document.getElementById('boss-view').classList.toggle('hidden', !gameState.bossMode);
if (gameState.bossMode) {
document.getElementById('event-toast').classList.add('hidden');
+ document.getElementById('deal-popup').classList.add('hidden');
document.getElementById('mini-task-bar').classList.add('hidden');
document.title = 'Book1 - Excel';
} else {
@@ -2336,6 +3088,8 @@ function saveGame() {
upgradeLevel: s.upgradeLevel,
automated: s.automated,
pendingCollect: s.pendingCollect,
+ prestigeLevel: s.prestigeLevel || 0,
+ breakthroughMult: s.breakthroughMult || 1,
})),
seriesAShown: gameState.seriesAShown,
totalPlayTime: gameState.totalPlayTime,
@@ -2372,6 +3126,11 @@ function saveGame() {
activeCFOLevel: gameState.activeCFOLevel || 0,
cfoRecords: gameState.cfoRecords || {},
revenueHistory: gameState.revenueHistory || [],
+ lastQuarterRE: gameState.lastQuarterRE || 0,
+ featureToggles: gameState.featureToggles || DEFAULT_FEATURES,
+ eventFreqMult: EVENT_FREQ_MULT,
+ overtimeClicks: gameState.overtimeClicks || 0,
+ focusTipShown: gameState.focusTipShown || false,
savedAt: Date.now(),
};
@@ -2430,18 +3189,26 @@ function loadGame() {
if (gameState.retainedEarnings > 100000) gameState.retainedEarnings = 0;
gameState.analystBaseline = data.analystBaseline || 1.0;
if (gameState.analystBaseline > 2.5) gameState.analystBaseline = 1.5; // cap inflated saves
+ if (gameState.analystBaseline < 0.5) gameState.analystBaseline = 0.8; // rescue crushed saves
gameState.earningsStreak = data.earningsStreak || 0;
gameState.currentGuidance = data.currentGuidance || null;
gameState.guidanceTarget = data.guidanceTarget || 0;
gameState.lastEarningsDay = data.lastEarningsDay || 0;
gameState.earningsQuarterRevenue = data.earningsQuarterRevenue || 0;
gameState.ipoStockPriceStart = data.ipoStockPriceStart || 0;
- gameState._earningsMultiplier = data._earningsMultiplier || 1.0;
+ gameState._earningsMultiplier = Math.max(0.1, data._earningsMultiplier || 1.0);
+ // Rescue saves where multiplier was destroyed by repeated misses
+ if (gameState._earningsMultiplier < 0.3) gameState._earningsMultiplier = 0.5;
// Phase 2.2: Board Room
gameState.boardRoomPurchases = data.boardRoomPurchases || {};
gameState.activeCFOLevel = data.activeCFOLevel || 0;
gameState.cfoRecords = data.cfoRecords || {};
gameState.revenueHistory = data.revenueHistory || [];
+ gameState.lastQuarterRE = data.lastQuarterRE || 0;
+ gameState.featureToggles = data.featureToggles || { ...DEFAULT_FEATURES };
+ EVENT_FREQ_MULT = data.eventFreqMult != null ? data.eventFreqMult : 1.0;
+ gameState.overtimeClicks = data.overtimeClicks || 0;
+ gameState.focusTipShown = data.focusTipShown || false;
gameState.activeTab = 'operations';
// Rebuild sources for selected arc
@@ -2452,6 +3219,10 @@ function loadGame() {
upgradeLevel: 0,
automated: false,
pendingCollect: 0,
+ prestigeLevel: 0,
+ breakthroughMult: 1,
+ focus: 0,
+ lastFocusClick: 0,
}));
if (data.sources) {
@@ -2462,6 +3233,9 @@ function loadGame() {
gameState.sources[i].upgradeLevel = saved.upgradeLevel;
gameState.sources[i].automated = saved.automated;
gameState.sources[i].pendingCollect = saved.pendingCollect || 0;
+ gameState.sources[i].prestigeLevel = saved.prestigeLevel || 0;
+ gameState.sources[i].breakthroughMult = saved.breakthroughMult || 1;
+ // Focus is transient — don't load from save (resets on load)
}
});
}
@@ -2488,10 +3262,14 @@ function loadGame() {
// Show game view (skip arc select)
document.getElementById('arc-select').classList.add('hidden');
document.getElementById('game-view').classList.remove('hidden');
- buildGrid();
- updateDisplay();
- updateTaxPanel();
- updateBoardRoomTab();
+ try {
+ buildGrid();
+ updateDisplay();
+ updateTaxPanel();
+ updateBoardRoomTab();
+ } catch (renderErr) {
+ console.error('Render error after load (non-fatal):', renderErr);
+ }
return true;
} catch (e) {
@@ -2514,6 +3292,8 @@ function resetGame() {
gameState.revPenalty = null;
gameState.revBonus = null;
gameState.powerOutage = null;
+ gameState.dbOutage = null;
+ gameState.miniTaskBlocked = null;
gameState.hireFrozen = null;
gameState.taxDebts = [];
gameState.quarterRevenue = 0;
@@ -2545,6 +3325,11 @@ function resetGame() {
gameState.activeCFOLevel = 0;
gameState.cfoRecords = {};
gameState.revenueHistory = [];
+ gameState.lastQuarterRE = 0;
+ gameState.featureToggles = { ...DEFAULT_FEATURES };
+ EVENT_FREQ_MULT = 1.0;
+ gameState.overtimeClicks = 0;
+ gameState.focusTipShown = false;
gameState.eventCooldown = 0;
gameState.miniTaskCooldown = 0;
gameState.miniTaskActive = false;
@@ -2585,6 +3370,8 @@ function dismissSeriesA() {
let toastDragState = null;
function initToastDrag() {
+ // No drag on mobile
+ if (isMobile()) return;
const header = document.getElementById('toast-header');
header.style.cursor = 'move';
@@ -2617,6 +3404,38 @@ function initToastDrag() {
toastDragState = null;
}
});
+
+ // Deal popup drag
+ const dealHeader = document.getElementById('deal-header');
+ let dealDragState = null;
+
+ dealHeader.addEventListener('mousedown', (e) => {
+ e.preventDefault();
+ const popup = document.getElementById('deal-popup');
+ const rect = popup.getBoundingClientRect();
+ popup.style.transform = 'none';
+ popup.style.left = rect.left + 'px';
+ popup.style.top = rect.top + 'px';
+ dealDragState = { startX: e.clientX, startY: e.clientY, origLeft: rect.left, origTop: rect.top };
+ });
+
+ document.addEventListener('mousemove', (e) => {
+ if (!dealDragState) return;
+ const dx = e.clientX - dealDragState.startX;
+ const dy = e.clientY - dealDragState.startY;
+ const popup = document.getElementById('deal-popup');
+ popup.style.left = (dealDragState.origLeft + dx) + 'px';
+ popup.style.top = (dealDragState.origTop + dy) + 'px';
+ });
+
+ document.addEventListener('mouseup', () => {
+ if (dealDragState) {
+ const popup = document.getElementById('deal-popup');
+ const rect = popup.getBoundingClientRect();
+ localStorage.setItem('quarterClose_dealPos', JSON.stringify({ left: rect.left, top: rect.top }));
+ dealDragState = null;
+ }
+ });
}
// ===== BOSS VIEW GENERATION =====
@@ -2703,6 +3522,267 @@ function updateAutosaveToggle() {
if (el) el.classList.toggle('off', !autosaveEnabled);
}
+// ===== DATA MENU =====
+let dataMenuOpen = false;
+
+function toggleDataMenu(e) {
+ e.stopPropagation();
+ dataMenuOpen = !dataMenuOpen;
+ const dropdown = document.getElementById('data-dropdown');
+ dropdown.classList.toggle('open', dataMenuOpen);
+ closeFileMenu();
+}
+
+function closeDataMenu() {
+ dataMenuOpen = false;
+ const dropdown = document.getElementById('data-dropdown');
+ if (dropdown) dropdown.classList.remove('open');
+}
+
+// ===== FEATURE TOGGLES =====
+const DEFAULT_FEATURES = {
+ closeTheDeals: true,
+ overtime: true,
+ managementFocus: true,
+};
+
+function getFeatureToggles() {
+ if (!gameState.featureToggles) {
+ gameState.featureToggles = { ...DEFAULT_FEATURES };
+ }
+ return gameState.featureToggles;
+}
+
+function isFeatureEnabled(key) {
+ return getFeatureToggles()[key] !== false;
+}
+
+function toggleFeature(key, enabled) {
+ getFeatureToggles()[key] = enabled;
+}
+
+// ===== GAME OPTIONS MODAL =====
+function showGameOptions() {
+ closeDataMenu();
+ const toggles = getFeatureToggles();
+ document.getElementById('toggle-deals').checked = toggles.closeTheDeals !== false;
+ document.getElementById('toggle-overtime').checked = toggles.overtime !== false;
+ document.getElementById('toggle-focus').checked = toggles.managementFocus !== false;
+ document.getElementById('event-freq-slider').value = Math.round(EVENT_FREQ_MULT * 100);
+ const initTag = EVENT_FREQ_MULT === 0 ? 'off' : EVENT_FREQ_MULT <= 1 ? '' : EVENT_FREQ_MULT <= 3 ? '🔥' : EVENT_FREQ_MULT <= 6 ? '💀' : '☠️';
+ document.getElementById('event-freq-label').textContent = `📬 Events: ${EVENT_FREQ_MULT.toFixed(1)}× ${initTag}`;
+ document.getElementById('options-modal').classList.remove('hidden');
+}
+
+function dismissOptions() {
+ document.getElementById('options-modal').classList.add('hidden');
+}
+
+// ===== MANAGEMENT FOCUS =====
+function clickFocus(sourceIndex) {
+ if (!isFeatureEnabled('managementFocus')) return;
+ const state = gameState.sources[sourceIndex];
+ if (!state || !state.unlocked || state.employees === 0) return;
+
+ if (!state.focus) state.focus = 0;
+ state.focus = Math.min(10, state.focus + 1);
+ state.lastFocusClick = Date.now();
+
+ // Brief cell flash
+ const row = document.getElementById(`source-row-${sourceIndex}`);
+ if (row) {
+ const nameCell = row.querySelector('[data-field="name"]');
+ if (nameCell) {
+ nameCell.style.transition = 'background 0.15s';
+ nameCell.style.background = '#e8f5e9';
+ setTimeout(() => { nameCell.style.background = ''; }, 300);
+ }
+ }
+}
+
+function decayFocus() {
+ if (!isFeatureEnabled('managementFocus')) return;
+ const now = Date.now();
+ for (const state of gameState.sources) {
+ if (!state.focus || state.focus <= 0) continue;
+ const elapsed = now - (state.lastFocusClick || 0);
+ // Lose 1 focus point every 10 seconds since last click
+ const shouldHave = Math.max(0, (state.focus || 0) - Math.floor(elapsed / 10000));
+ if (shouldHave < state.focus) {
+ state.focus = shouldHave;
+ }
+ }
+}
+
+// ===== CLOSE THE DEAL =====
+const DEAL_CLIENTS = [
+ 'Acme Corp', 'Globex Industries', 'Initech', 'Umbrella Corp', 'Wayne Enterprises',
+ 'Stark Industries', 'Cyberdyne Systems', 'Weyland-Yutani', 'Soylent Corp', 'Oscorp',
+ 'Tyrell Corporation', 'Massive Dynamic', 'Pied Piper', 'Hooli', 'Vandelay Industries',
+];
+
+let dealTimer = null;
+
+function spawnDeal() {
+ const rev = totalRevPerTick();
+ if (rev <= 0) return;
+
+ const seconds = 30 + Math.floor(Math.random() * 31); // 30-60s of revenue
+ const amount = rev * seconds;
+ const client = DEAL_CLIENTS[Math.floor(Math.random() * DEAL_CLIENTS.length)];
+ const clicksNeeded = Math.max(10, Math.min(30, 15 + Math.floor(Math.log10(Math.max(1, totalAnnualRev())) * 2)));
+ const timeLimit = 12000; // 12 seconds
+
+ gameState.dealActive = {
+ client,
+ amount,
+ clicksNeeded,
+ clicksDone: 0,
+ timeLimit,
+ startedAt: Date.now(),
+ };
+
+ // Use dedicated deal popup (not the event toast)
+ const popup = document.getElementById('deal-popup');
+ const savedPos = localStorage.getItem('quarterClose_dealPos');
+ if (isMobile()) {
+ // Let CSS handle positioning on mobile
+ popup.style.left = '';
+ popup.style.top = '';
+ popup.style.transform = '';
+ } else if (savedPos) {
+ const pos = JSON.parse(savedPos);
+ popup.style.left = pos.left + 'px';
+ popup.style.top = pos.top + 'px';
+ popup.style.transform = 'none';
+ } else {
+ popup.style.left = '50%';
+ popup.style.top = '50%';
+ popup.style.transform = 'translate(-50%, -50%)';
+ }
+
+ document.getElementById('deal-client').textContent = client;
+ document.getElementById('deal-body').textContent = `Enterprise contract worth ${formatMoney(amount)}. Click to get signatures before they walk!`;
+ document.getElementById('deal-progress-text').textContent = `Signatures: 0 / ${clicksNeeded}`;
+ document.getElementById('deal-bar').style.width = '0%';
+
+ const timerFill = document.getElementById('deal-timer-fill');
+ timerFill.style.animation = 'none';
+ timerFill.offsetHeight; // force reflow
+ timerFill.style.animation = `toast-btn-fill ${timeLimit / 1000}s linear forwards`;
+
+ popup.classList.remove('hidden');
+
+ // Timer — auto-fail
+ dealTimer = setTimeout(() => {
+ failDeal();
+ }, timeLimit);
+}
+
+function clickDeal() {
+ if (!gameState.dealActive) return;
+ const deal = gameState.dealActive;
+ deal.clicksDone++;
+ mobileHaptic('light');
+
+ // Update progress
+ const progress = document.getElementById('deal-progress-text');
+ if (progress) progress.textContent = `Signatures: ${deal.clicksDone} / ${deal.clicksNeeded}`;
+ const bar = document.getElementById('deal-bar');
+ if (bar) bar.style.width = (deal.clicksDone / deal.clicksNeeded * 100) + '%';
+
+ if (deal.clicksDone >= deal.clicksNeeded) {
+ // Success!
+ if (dealTimer) { clearTimeout(dealTimer); dealTimer = null; }
+ gameState.cash += deal.amount;
+ gameState.totalEarned += deal.amount;
+ gameState.quarterRevenue += deal.amount;
+ if (gameState.isPublic) gameState.earningsQuarterRevenue += deal.amount;
+
+ document.getElementById('status-text').textContent = `🤝 Closed ${formatMoney(deal.amount)} deal with ${deal.client}!`;
+ setTimeout(() => {
+ const st = document.getElementById('status-text');
+ if (st && st.textContent.startsWith('🤝')) st.textContent = 'Ready';
+ }, 3000);
+
+ gameState.dealActive = null;
+ document.getElementById('deal-popup').classList.add('hidden');
+ updateDisplay();
+ mobileHaptic('success');
+ mobileCashPulse();
+ }
+}
+
+function failDeal() {
+ if (!gameState.dealActive) return;
+ const deal = gameState.dealActive;
+ dealTimer = null;
+
+ document.getElementById('status-text').textContent = `😔 ${deal.client} walked — deal lost`;
+ setTimeout(() => {
+ const st = document.getElementById('status-text');
+ if (st && st.textContent.startsWith('😔')) st.textContent = 'Ready';
+ }, 3000);
+
+ gameState.dealActive = null;
+ document.getElementById('deal-popup').classList.add('hidden');
+}
+
+// ===== OVERTIME =====
+function clickOvertime() {
+ if (!isFeatureEnabled('overtime')) return;
+ const rev = totalRevPerTick();
+ if (rev <= 0) return;
+
+ if (!gameState.overtimeClicks) gameState.overtimeClicks = 0;
+
+ // Base = 5 seconds of revenue, diminishing returns
+ const diminish = 1 / (1 + gameState.overtimeClicks * 0.15);
+ const amount = rev * 5 * diminish;
+
+ gameState.cash += amount;
+ gameState.totalEarned += amount;
+ gameState.quarterRevenue += amount;
+ if (gameState.isPublic) gameState.earningsQuarterRevenue += amount;
+ gameState.overtimeClicks++;
+
+ // Status bar feedback
+ document.getElementById('status-text').textContent = `⏰ Overtime! +${formatMoney(amount)}`;
+ setTimeout(() => {
+ const st = document.getElementById('status-text');
+ if (st && st.textContent.startsWith('⏰')) st.textContent = 'Ready';
+ }, 2000);
+
+ updateOvertimeRow();
+ updateDisplay();
+ mobileHaptic('medium');
+ mobileCashPulse();
+}
+
+function updateOvertimeRow() {
+ const row = document.getElementById('overtime-row');
+ if (!row) return;
+
+ const enabled = isFeatureEnabled('overtime');
+ const hasRev = totalRevPerTick() > 0;
+
+ if (!enabled || !hasRev) {
+ row.style.display = 'none';
+ return;
+ }
+ row.style.display = '';
+
+ const clicks = gameState.overtimeClicks || 0;
+ const rev = totalRevPerTick();
+ const nextDiminish = 1 / (1 + clicks * 0.15);
+ const nextAmount = rev * 5 * nextDiminish;
+ const pct = Math.round(nextDiminish * 100);
+
+ document.getElementById('overtime-clicks').textContent = clicks > 0 ? `${clicks} this Q` : '';
+ document.getElementById('overtime-next').textContent = `+${formatMoney(nextAmount)}`;
+ document.getElementById('overtime-diminish').textContent = clicks > 0 ? `${pct}% efficiency` : '';
+}
+
function showAbout() {
closeFileMenu();
document.getElementById('about-modal').classList.remove('hidden');
@@ -2721,6 +3801,16 @@ function dismissHelp() {
document.getElementById('help-modal').classList.add('hidden');
}
+function showHelpTab(tabId) {
+ document.querySelectorAll('.help-page').forEach(p => p.classList.remove('active'));
+ document.querySelectorAll('.help-tab').forEach(t => t.classList.remove('active'));
+ document.getElementById('help-' + tabId).classList.add('active');
+ // Find the tab button that matches
+ document.querySelectorAll('.help-tab').forEach(t => {
+ if (t.getAttribute('onclick').includes(tabId)) t.classList.add('active');
+ });
+}
+
let pendingConfirmAction = null;
function confirmNewGame() {
@@ -2752,6 +3842,9 @@ document.addEventListener('click', (e) => {
if (fileMenuOpen && !e.target.closest('#file-menu')) {
closeFileMenu();
}
+ if (dataMenuOpen && !e.target.closest('#data-menu')) {
+ closeDataMenu();
+ }
// Formula bar: update cell reference on click
const cell = e.target.closest('.cell');
@@ -2861,6 +3954,7 @@ function resetBoardRoom() {
gameState.activeCFOLevel = 0;
gameState.cfoRecords = {};
gameState.revenueHistory = [];
+ gameState.lastQuarterRE = 0;
if (gameState.activeTab === 'boardroom') renderBoardRoom();
_lastTaxPanelHash = '';
updateTaxPanel();
@@ -2962,6 +4056,8 @@ function processEarnings() {
}
}
}
+ // Floor analyst baseline — can't go below 0.5
+ gameState.analystBaseline = Math.max(0.5, gameState.analystBaseline);
// Apply stock price change via valuation manipulation
// We affect the market sentiment to create the price jump
@@ -2971,17 +4067,24 @@ function processEarnings() {
// But we want a discrete jump, so we'll adjust a persistent earnings multiplier instead
if (!gameState._earningsMultiplier) gameState._earningsMultiplier = 1.0;
gameState._earningsMultiplier *= (1 + stockChange);
+ // Floor at 0.1 so valuation can't be completely destroyed
+ gameState._earningsMultiplier = Math.max(0.1, gameState._earningsMultiplier);
+ // Gentle mean-reversion toward 1.0 (5% per quarter)
+ gameState._earningsMultiplier += (1.0 - gameState._earningsMultiplier) * 0.05;
const newPrice = getStockPrice();
// Reset for next quarter
const oldQuarterRev = gameState.earningsQuarterRevenue;
gameState.earningsQuarterRevenue = 0;
+ gameState.lastQuarterRE = reEarned;
// Show earnings modal with next quarter guidance selection
const marginPct = (margin * 100).toFixed(1);
const resultEmoji = result === 'BEAT' ? '📈' : result === 'MISS' ? '📉' : '➡️';
- const streakText = gameState.earningsStreak > 1 ? `🔥 ${gameState.earningsStreak} consecutive beats` :
+ const streakBonus = gameState.earningsStreak >= 1 ?
+ Math.min(2.0, 1 + gameState.earningsStreak * 0.1) : 1;
+ const streakText = gameState.earningsStreak > 1 ? `🔥 ${gameState.earningsStreak} consecutive beats (${streakBonus.toFixed(1)}× RE)` :
gameState.earningsStreak < -1 ? `❄️ ${Math.abs(gameState.earningsStreak)} consecutive misses` : '';
const analystText = gameState.earningsStreak >= 3 ? 'Analyst Upgrade ⬆️' :
gameState.earningsStreak <= -2 ? 'Analyst Downgrade ⬇️' : '';
@@ -3040,7 +4143,18 @@ function processEarnings() {
function showEarningsModal(data) {
const guidanceLabel = GUIDANCE_LEVELS[data.guidanceKey].label;
- const body = `Revenue: ${formatMoney(data.revenue)}\nGuidance: ${formatMoney(data.target)} (${guidanceLabel})\nResult: ${data.result} (${data.marginPct > 0 ? '+' : ''}${data.marginPct}%) ${data.resultEmoji}\n\nStock: ${data.stockChange}\nRetained Earnings: +${data.reEarned} RE${data.streakText ? '\n' + data.streakText : ''}${data.analystText ? '\n' + data.analystText : ''}\n\nSet next quarter guidance:`;
+ const body = `
+
Revenue ${formatMoney(data.revenue)}
+
Guidance ${formatMoney(data.target)} (${guidanceLabel})
+
Result ${data.result} ${data.resultEmoji} (${data.marginPct > 0 ? '+' : ''}${data.marginPct}%)
+
+
Stock ${data.stockChange}
+
RE Earned +${data.reEarned} RE
+ ${data.streakText ? `
Streak ${data.streakText}
` : ''}
+ ${data.analystText ? `
Analysts ${data.analystText}
` : ''}
+
+
Set next quarter guidance:
+
`;
// Build actions — 4 guidance buttons + close
const actions = Object.entries(GUIDANCE_LEVELS).map(([key, level]) => ({
@@ -3053,7 +4167,7 @@ function showEarningsModal(data) {
}));
showEventToast('Investor Relations', `${data.qLabel} EARNINGS REPORT`,
- body, actions, { expiresMs: 0, closable: false });
+ body, actions, { expiresMs: 0, closable: false, html: true });
}
function trackEarningsRevenue(amount) {
@@ -3079,6 +4193,8 @@ function switchTab(tab) {
const tabOps = document.getElementById('tab-operations');
const tabBR = document.getElementById('tab-board-room');
+ const gridArea = document.getElementById('grid-container');
+
if (tab === 'boardroom') {
revenueRows.classList.add('hidden');
taxPanel.classList.add('hidden');
@@ -3086,6 +4202,7 @@ function switchTab(tab) {
boardRoom.classList.remove('hidden');
tabOps.classList.remove('active');
tabBR.classList.add('active');
+ gridArea.classList.add('boardroom-layout');
buildBoardRoom();
} else {
revenueRows.classList.remove('hidden');
@@ -3094,6 +4211,7 @@ function switchTab(tab) {
boardRoom.classList.add('hidden');
tabOps.classList.add('active');
tabBR.classList.remove('active');
+ gridArea.classList.remove('boardroom-layout');
_lastTaxPanelHash = ''; // force rebuild
updateTaxPanel();
buildFillerRows();
@@ -3107,6 +4225,8 @@ function updateBoardRoomTab() {
} else {
tabBR.classList.add('hidden');
}
+ // Also update mobile nav
+ if (typeof updateMobileNav === 'function') updateMobileNav();
}
let _lastBoardRoomHash = '';
@@ -3124,7 +4244,7 @@ function buildBoardRoom() {
_lastBoardRoomHash = hashParts;
let html = '';
- let rowNum = 4; // starts after the header rows (1-3)
+ let rowNum = 3; // starts after the header rows (1-2)
// Board Room header
html += ``;
+ // Group upgrades by category, sort each group by cost ascending
+ const categoryOrder = ['Finance', 'Revenue', 'Tax', 'Investor', 'Protection'];
+ const categoryLabels = {
+ Finance: '📊 Finance',
+ Revenue: '💰 Revenue',
+ Tax: '🏛️ Tax',
+ Investor: '📈 Investor Relations',
+ Protection: '🛡️ Protection',
+ };
+ const grouped = {};
for (const upgrade of BOARD_ROOM_UPGRADES) {
+ if (!grouped[upgrade.category]) grouped[upgrade.category] = [];
+ grouped[upgrade.category].push(upgrade);
+ }
+ // Sort each group by effective cost
+ for (const cat of Object.keys(grouped)) {
+ grouped[cat].sort((a, b) => getUpgradeCost(a) - getUpgradeCost(b));
+ }
+
+ let totalUpgradeRows = 0;
+ for (const cat of categoryOrder) {
+ const upgrades = grouped[cat];
+ if (!upgrades || upgrades.length === 0) continue;
+
+ // Category header row
+ html += `
+
${rowNum++}
+
${categoryLabels[cat] || cat}
+
+
+
+
+
+
+
+
`;
+ totalUpgradeRows++;
+
+ for (const upgrade of upgrades) {
const owned = getBoardRoomUpgradeCount(upgrade.id);
const isOwned = owned > 0 && upgrade.maxCount !== Infinity;
const requiresMet = !upgrade.requires || hasBoardRoomUpgrade(upgrade.requires);
- const canAfford = gameState.retainedEarnings >= upgrade.cost;
+ const cost = getUpgradeCost(upgrade);
+ const canAfford = gameState.retainedEarnings >= cost;
const isLocked = !requiresMet;
let statusCell = '';
@@ -3175,18 +4334,31 @@ function buildBoardRoom() {
const costColor = isOwned ? '#999' : isLocked ? '#ccc' : canAfford ? '#7b1fa2' : '#c00';
+ const costLabel = upgrade.maxCount === Infinity && owned > 0
+ ? `${cost.toLocaleString()} RE (×${owned})`
+ : `${cost.toLocaleString()} RE`;
+
+ // Dynamic description for Growth Initiative — show total bonus
+ let desc = upgrade.desc;
+ if (upgrade.id === 'growth_initiative' && owned > 0) {
+ const totalBonus = ((Math.pow(1.02, owned) - 1) * 100).toFixed(1);
+ desc = `+2% revenue per purchase (stacks). Current: +${totalBonus}% total (×${owned})`;
+ }
+
html += `
${rowNum++}
${upgrade.name}
${upgrade.category}
-
${upgrade.cost.toLocaleString()} RE
+
${costLabel}
${statusCell}
-
-
${upgrade.desc}
+
${desc}
+
`;
- }
+ totalUpgradeRows++;
+ } // end upgrade loop
+ } // end category loop
// Filler rows for the board room view
const ROW_HEIGHT = 28;
@@ -3198,7 +4370,7 @@ function buildBoardRoom() {
const bottomChrome = (revBar ? revBar.offsetHeight : 0) +
(sheetTabs ? sheetTabs.offsetHeight : 0) +
(statusBar ? statusBar.offsetHeight : 0);
- const usedRows = BOARD_ROOM_UPGRADES.length + 3; // header + sep + upgrades
+ const usedRows = totalUpgradeRows + 3; // header + sep + upgrades + category headers
const available = viewportHeight - gridBottom - bottomChrome - (usedRows * ROW_HEIGHT);
const fillerCount = Math.max(3, Math.ceil(available / ROW_HEIGHT) + 1);
@@ -3221,9 +4393,10 @@ function purchaseBoardRoomUpgrade(id) {
const owned = getBoardRoomUpgradeCount(id);
if (owned >= upgrade.maxCount) return;
if (upgrade.requires && !hasBoardRoomUpgrade(upgrade.requires)) return;
- if (gameState.retainedEarnings < upgrade.cost) return;
+ const cost = getUpgradeCost(upgrade);
+ if (gameState.retainedEarnings < cost) return;
- gameState.retainedEarnings -= upgrade.cost;
+ gameState.retainedEarnings -= cost;
gameState.boardRoomPurchases[id] = (gameState.boardRoomPurchases[id] || 0) + 1;
// Auto-activate Finance Dept when first purchased
@@ -3251,9 +4424,18 @@ function init() {
generateBossGrid();
initChartDrag();
initToastDrag();
+ initDebugTap();
// Delegated click handler for tax panel buttons (survives innerHTML rebuilds)
document.getElementById('tax-panel').addEventListener('click', (e) => {
+ // P&L collapse toggle
+ const togglePnl = e.target.closest('[data-toggle-pnl]');
+ if (togglePnl) {
+ gameState.pnlCollapsed = !gameState.pnlCollapsed;
+ _lastTaxPanelHash = ''; // force rebuild
+ updateTaxPanel();
+ return;
+ }
const settleBtn = e.target.closest('[data-settle]');
if (settleBtn) {
e.stopPropagation();
@@ -3282,7 +4464,33 @@ function init() {
if (!loaded) {
showArcSelect();
}
- setInterval(gameTick, 1000);
+
+ // Use mobile-aware tick if on mobile
+ setInterval(function() {
+ gameTick();
+ if (typeof mobileTickUpdate === 'function') mobileTickUpdate();
+ }, 1000);
+
+ // Mobile initialization
+ if (isMobile()) {
+ if (typeof updateMobileNav === 'function') updateMobileNav();
+ // Skip splash on mobile
+ const splash = document.getElementById('splash-screen');
+ if (splash) splash.remove();
+ } else {
+ // Splash screen animation (desktop only)
+ const splash = document.getElementById('splash-screen');
+ if (splash) {
+ requestAnimationFrame(() => {
+ const bar = document.getElementById('splash-progress-bar');
+ if (bar) bar.style.width = '100%';
+ });
+ setTimeout(() => {
+ splash.classList.add('fade-out');
+ setTimeout(() => splash.remove(), 500);
+ }, 2500);
+ }
+ }
}
// Expose for inline onclick
@@ -3535,6 +4743,12 @@ function drawValuationChart() {
function formatCompact(n) {
const sign = n < 0 ? '-' : '';
const a = Math.abs(n);
+ if (a >= 1e33) return sign + '$' + (a / 1e33).toFixed(1) + 'Dc';
+ if (a >= 1e30) return sign + '$' + (a / 1e30).toFixed(1) + 'No';
+ if (a >= 1e27) return sign + '$' + (a / 1e27).toFixed(1) + 'Oc';
+ if (a >= 1e24) return sign + '$' + (a / 1e24).toFixed(1) + 'Sp';
+ if (a >= 1e21) return sign + '$' + (a / 1e21).toFixed(1) + 'Sx';
+ if (a >= 1e18) return sign + '$' + (a / 1e18).toFixed(1) + 'Qi';
if (a >= 1e15) return sign + '$' + (a / 1e15).toFixed(1) + 'Q';
if (a >= 1e12) return sign + '$' + (a / 1e12).toFixed(1) + 'T';
if (a >= 1e9) return sign + '$' + (a / 1e9).toFixed(1) + 'B';
@@ -3543,11 +4757,20 @@ function formatCompact(n) {
return sign + '$' + Math.floor(a);
}
+function setEventFreqMult(val) {
+ EVENT_FREQ_MULT = parseInt(val) / 100;
+ const tag = EVENT_FREQ_MULT === 0 ? 'off' : EVENT_FREQ_MULT <= 1 ? '' : EVENT_FREQ_MULT <= 3 ? '🔥' : EVENT_FREQ_MULT <= 6 ? '💀' : '☠️';
+ document.getElementById('event-freq-label').textContent = `📬 Events: ${EVENT_FREQ_MULT.toFixed(1)}× ${tag}`;
+ saveGame();
+}
+
+window.setEventFreqMult = setEventFreqMult;
window.unlockSource = unlockSource;
window.hireEmployee = hireEmployee;
window.upgradeSource = upgradeSource;
window.automateSource = automateSource;
window.collectSource = collectSource;
+window.restructureSource = restructureSource;
window.dismissEvent = dismissEvent;
window.dismissOffline = dismissOffline;
window.dismissSeriesA = dismissSeriesA;
@@ -3565,6 +4788,682 @@ window.dismissConfirm = dismissConfirm;
window.showHelp = showHelp;
window.dismissHelp = dismissHelp;
window.switchTab = switchTab;
+// Additional window exposures for mobile and new features
+window.toggleBossMode = toggleBossMode;
+window.showGameOptions = showGameOptions;
+window.dismissOptions = dismissOptions;
+window.toggleFeature = toggleFeature;
+window.hireMax = hireMax;
+window.upgradeMax = upgradeMax;
+window.settleTaxDebt = settleTaxDebt;
+window.settleAllTax = settleAllTax;
+window.triggerIRS = triggerIRS;
+window.triggerBonus = triggerBonus;
+window.clickDeal = clickDeal;
+window.clickOvertime = clickOvertime;
+window.showHelpTab = showHelpTab;
+window.toggleDataMenu = toggleDataMenu;
+window.saveGame = saveGame;
+window.toggleDebugEventDropdown = toggleDebugEventDropdown;
+
+// ============================================
+// MOBILE SUPPORT
+// ============================================
+
+function isMobile() {
+ return window.matchMedia('(max-width: 600px)').matches;
+}
+
+// Haptic feedback for mobile — light tap feel
+function mobileHaptic(style) {
+ if (!isMobile() || !navigator.vibrate) return;
+ switch (style) {
+ case 'light': navigator.vibrate(10); break;
+ case 'medium': navigator.vibrate(20); break;
+ case 'heavy': navigator.vibrate([15, 30, 15]); break;
+ case 'success': navigator.vibrate([10, 40, 20]); break;
+ case 'error': navigator.vibrate([30, 20, 30]); break;
+ default: navigator.vibrate(10);
+ }
+}
+
+// Cash header pulse animation on collection
+function mobileCashPulse() {
+ if (!isMobile()) return;
+ const header = document.getElementById('row-cash');
+ if (!header) return;
+ header.classList.remove('cash-pulse');
+ void header.offsetWidth; // force reflow
+ header.classList.add('cash-pulse');
+}
+
+// Mark collect buttons that have pending revenue
+function mobileUpdateCollectHighlights() {
+ if (!isMobile() || !gameState.arc) return;
+ document.querySelectorAll('.cell-btn.btn-collect').forEach(btn => {
+ // Collect buttons show "💰 Collect" (no pending) or "💰 Collect $X" (has pending)
+ const text = btn.textContent;
+ const hasPending = text.includes('$') && !text.includes('$0.00');
+ btn.classList.toggle('has-pending', hasPending);
+ });
+}
+
+let _mobileActiveTab = 'operations';
+let _lastMobilePnlHash = '';
+let _lastMobileBRHash = '';
+
+function mobileSwitchTab(tab) {
+ _mobileActiveTab = tab;
+
+ // Update nav buttons
+ document.querySelectorAll('.mob-nav-btn').forEach(btn => {
+ btn.classList.toggle('active', btn.dataset.tab === tab);
+ });
+
+ // Show/hide views
+ const gridContainer = document.getElementById('grid-container');
+ const pnlView = document.getElementById('mobile-pnl-view');
+ const brView = document.getElementById('mobile-boardroom-view');
+ const settingsView = document.getElementById('mobile-settings-view');
+ const cashHeader = document.getElementById('mobile-cash-header');
+
+ // Hide all
+ gridContainer.style.display = 'none';
+ pnlView.classList.add('hidden');
+ brView.classList.add('hidden');
+ settingsView.classList.add('hidden');
+
+ // Show mini cash header on non-ops tabs
+ if (tab !== 'operations' && cashHeader) {
+ cashHeader.classList.add('visible');
+ updateMobileCashHeader();
+ } else if (cashHeader) {
+ cashHeader.classList.remove('visible');
+ }
+
+ // Re-trigger tab fade animation
+ const retrigger = (el) => { el.style.animation = 'none'; el.offsetHeight; el.style.animation = ''; };
+
+ switch (tab) {
+ case 'operations':
+ gridContainer.style.display = '';
+ retrigger(gridContainer);
+ gridContainer.scrollTo({ top: 0, behavior: 'smooth' });
+ if (gameState.activeTab !== 'operations') {
+ gameState.activeTab = 'operations';
+ }
+ break;
+ case 'pnl':
+ pnlView.classList.remove('hidden');
+ retrigger(pnlView);
+ pnlView.scrollTo({ top: 0 });
+ _lastMobilePnlHash = '';
+ buildMobilePnL();
+ break;
+ case 'boardroom':
+ brView.classList.remove('hidden');
+ retrigger(brView);
+ brView.scrollTo({ top: 0 });
+ _lastMobileBRHash = '';
+ buildMobileBoardRoom();
+ break;
+ case 'settings':
+ settingsView.classList.remove('hidden');
+ retrigger(settingsView);
+ settingsView.scrollTo({ top: 0 });
+ updateMobileSettings();
+ break;
+ }
+ mobileHaptic('light');
+}
+window.mobileSwitchTab = mobileSwitchTab;
+
+// ===== SWIPE GESTURE SUPPORT =====
+(function initMobileSwipe() {
+ if (typeof window === 'undefined') return;
+ const MOB_TABS = ['operations', 'pnl', 'boardroom', 'settings'];
+ let _swipeStartX = 0, _swipeStartY = 0, _swiping = false;
+ const MIN_SWIPE = 60, MAX_Y_DRIFT = 80;
+
+ function getVisibleTabs() {
+ return MOB_TABS.filter(t => {
+ if (t === 'boardroom') {
+ const btn = document.getElementById('mob-nav-boardroom');
+ return btn && !btn.classList.contains('hidden');
+ }
+ return true;
+ });
+ }
+
+ document.addEventListener('touchstart', function(e) {
+ if (!isMobile()) return;
+ _swipeStartX = e.touches[0].clientX;
+ _swipeStartY = e.touches[0].clientY;
+ _swiping = true;
+ }, { passive: true });
+
+ document.addEventListener('touchend', function(e) {
+ if (!_swiping || !isMobile()) return;
+ _swiping = false;
+ const dx = e.changedTouches[0].clientX - _swipeStartX;
+ const dy = Math.abs(e.changedTouches[0].clientY - _swipeStartY);
+ if (Math.abs(dx) < MIN_SWIPE || dy > MAX_Y_DRIFT) return;
+
+ const tabs = getVisibleTabs();
+ const curIdx = tabs.indexOf(_mobileActiveTab);
+ if (curIdx < 0) return;
+
+ if (dx < 0 && curIdx < tabs.length - 1) {
+ mobileSwitchTab(tabs[curIdx + 1]);
+ } else if (dx > 0 && curIdx > 0) {
+ mobileSwitchTab(tabs[curIdx - 1]);
+ }
+ }, { passive: true });
+})();
+
+function updateMobileNav() {
+ if (!isMobile()) return;
+ const brBtn = document.getElementById('mob-nav-boardroom');
+ if (brBtn) {
+ if (gameState.isPublic) {
+ brBtn.classList.remove('hidden');
+ } else {
+ brBtn.classList.add('hidden');
+ }
+ }
+
+ // Show tax alert badge on P&L tab when debts exist
+ const pnlBtn = document.querySelector('.mob-nav-btn[data-tab="pnl"]');
+ if (pnlBtn) {
+ const hasTaxDebt = gameState.taxDebts && gameState.taxDebts.length > 0;
+ let badge = pnlBtn.querySelector('.mob-nav-badge');
+ if (hasTaxDebt && !badge) {
+ badge = document.createElement('span');
+ badge.className = 'mob-nav-badge';
+ badge.textContent = '!';
+ pnlBtn.appendChild(badge);
+ } else if (!hasTaxDebt && badge) {
+ badge.remove();
+ }
+ }
+}
+
+function updateMobileCashHeader() {
+ const amountEl = document.getElementById('mob-cash-amount');
+ const perdayEl = document.getElementById('mob-cash-perday');
+ const revyrEl = document.getElementById('mob-cash-revyr');
+ const stockEl = document.getElementById('mob-cash-stock');
+ if (!amountEl || !perdayEl || !gameState.arc) return;
+ amountEl.textContent = formatMoney(gameState.cash);
+
+ // Show penalty/bonus indicators on mobile cash header
+ const perTick = totalRevPerTick();
+ const hasOutage = gameState.powerOutage && Date.now() < gameState.powerOutage.until;
+ const hasPenalty = gameState.revPenalty && Date.now() < gameState.revPenalty.until;
+ const hasBonus = gameState.revBonus && Date.now() < gameState.revBonus.until;
+ const hasDbOut = gameState.dbOutage && Date.now() < gameState.dbOutage.until;
+
+ if (hasOutage) {
+ perdayEl.innerHTML = `⚡ $0.00/day `;
+ } else if (hasPenalty) {
+ const pct = Math.round((1 - gameState.revPenalty.mult) * 100);
+ perdayEl.innerHTML = `${formatPerTick(perTick)}/day ▼${pct}% `;
+ } else if (hasDbOut) {
+ perdayEl.innerHTML = `${formatPerTick(perTick)}/day 💾 `;
+ } else if (hasBonus) {
+ perdayEl.innerHTML = `${formatPerTick(perTick)}/day ▲×${gameState.revBonus.mult} `;
+ } else {
+ perdayEl.textContent = formatPerTick(perTick) + '/day';
+ }
+
+ if (revyrEl) {
+ revyrEl.textContent = formatRate(totalAnnualRev());
+ }
+ if (stockEl) {
+ if (gameState.isPublic) {
+ stockEl.textContent = '📈 ' + formatMoney(getStockPrice());
+ } else {
+ stockEl.textContent = '';
+ }
+ }
+}
+
+function updateMobileSettings() {
+ const statsEl = document.getElementById('mob-stats');
+ if (!statsEl || !gameState.arc) return;
+
+ const totalRev = totalAnnualRev();
+ const perTick = totalRevPerTick();
+
+ statsEl.innerHTML = `
+ Game Date: ${document.getElementById('game-date').textContent}
+ Revenue/sec: ${formatStatMoney(totalRev / SECS_PER_YEAR)}/s
+ Revenue/day: ${formatStatMoney(perTick)}/d
+ Revenue/year: ${formatRate(totalRev)}
+ Total Earned: ${formatMoney(gameState.totalEarned)}
+ Clicks: ${gameState.totalClicks.toLocaleString()}
+ Play Time: ${formatDuration(gameState.totalPlayTime)}
+ ${gameState.isPublic ? `Stock Price: ${formatMoney(getStockPrice())}
+ Valuation: ${formatCompact(getCompanyValuation())}
+ Retained Earnings: ${gameState.retainedEarnings.toLocaleString()} RE
` : ''}
+ `;
+
+ // Active effects
+ const now = Date.now();
+ const effects = [];
+ if (gameState.powerOutage && now < gameState.powerOutage.until) {
+ const secs = Math.ceil((gameState.powerOutage.until - now) / 1000);
+ effects.push(`⚡ Power Outage (${secs}s)`);
+ }
+ if (gameState.revPenalty && now < gameState.revPenalty.until) {
+ const secs = Math.ceil((gameState.revPenalty.until - now) / 1000);
+ const pct = Math.round((1 - gameState.revPenalty.mult) * 100);
+ effects.push(`📉 Revenue -${pct}% (${secs}s)`);
+ }
+ if (gameState.revBonus && now < gameState.revBonus.until) {
+ const secs = Math.ceil((gameState.revBonus.until - now) / 1000);
+ effects.push(`📈 Revenue ×${gameState.revBonus.mult} (${secs}s)`);
+ }
+ if (gameState.dbOutage && now < gameState.dbOutage.until) {
+ const secs = Math.ceil((gameState.dbOutage.until - now) / 1000);
+ effects.push(`💾 DB Outage (${secs}s)`);
+ }
+ if (gameState.hireFrozen && now < gameState.hireFrozen) {
+ const secs = Math.ceil((gameState.hireFrozen - now) / 1000);
+ effects.push(`🚫 Hiring Frozen (${secs}s)`);
+ }
+ const garnishActive = gameState.taxDebts && gameState.taxDebts.some(d => d.stage === 'garnish');
+ if (garnishActive) effects.push('🔴 IRS Garnishment (-15%)');
+
+ if (effects.length > 0) {
+ statsEl.innerHTML += `Active Effects:
`;
+ effects.forEach(e => { statsEl.innerHTML += `${e}
`; });
+ }
+
+ // Update autosave button text
+ const btn = document.getElementById('mob-autosave-btn');
+ if (btn) {
+ btn.textContent = `Auto-save: ${autosaveEnabled ? 'ON' : 'OFF'}`;
+ }
+}
+
+function toggleAutosaveMobile() {
+ autosaveEnabled = !autosaveEnabled;
+ if (autosaveEnabled) {
+ autosaveInterval = setInterval(() => { saveGame(); }, 30000);
+ document.getElementById('status-text').textContent = 'Auto-save enabled';
+ } else {
+ clearInterval(autosaveInterval);
+ document.getElementById('status-text').textContent = 'Auto-save disabled';
+ }
+ updateMobileSettings();
+ setTimeout(() => { document.getElementById('status-text').textContent = 'Ready'; }, 2000);
+}
+window.toggleAutosaveMobile = toggleAutosaveMobile;
+
+// ===== MOBILE P&L RENDERING =====
+function buildMobilePnL() {
+ if (!isMobile() || !gameState.arc) return;
+ const container = document.getElementById('mobile-pnl-content');
+ if (!container) return;
+
+ const currentDay = Math.floor(gameState.gameElapsedSecs / SECS_PER_DAY);
+ const daysIntoQuarter = currentDay - gameState.lastQuarterDay;
+ const daysToTax = Math.max(0, 90 - daysIntoQuarter);
+ const totalExpenses = gameState.totalSpentHires + gameState.totalSpentUpgrades + gameState.totalSpentAuto;
+ const qDepreciation = getQuarterlyDepreciation();
+ const qTaxable = Math.max(0, gameState.quarterRevenue - qDepreciation);
+ const currentTaxRate = getBoardRoomTaxRate();
+ const currentAMTRate = getBoardRoomAMTRate();
+ const qRegularTax = Math.floor(qTaxable * currentTaxRate);
+ const qAMT = Math.floor(gameState.quarterRevenue * currentAMTRate);
+ const amtApplies = qAMT > qRegularTax && gameState.quarterRevenue > 0;
+ const qNet = gameState.quarterRevenue - gameState.quarterExpenses - gameState.quarterTaxPaid;
+ const ltNet = gameState.totalEarned - totalExpenses - gameState.totalTaxPaid;
+ const garnishActive = gameState.taxDebts && gameState.taxDebts.some(d => d.stage === 'garnish');
+
+ // Hash for change detection
+ const hashParts = [
+ gameState.quarterRevenue|0, gameState.totalEarned|0,
+ gameState.quarterExpenses|0, gameState.quarterTaxPaid|0,
+ daysToTax,
+ gameState.taxDebts ? gameState.taxDebts.map(d => `${d.current|0}:${d.stage}`).join(',') : '',
+ gameState.isPublic ? 1 : 0,
+ gameState.earningsQuarterRevenue|0,
+ gameState.retainedEarnings|0,
+ gameState.earningsStreak|0,
+ gameState.currentGuidance || '',
+ gameState.activeCFOLevel,
+ gameState.cash|0,
+ ].join('|');
+
+ if (hashParts === _lastMobilePnlHash) return;
+ _lastMobilePnlHash = hashParts;
+
+ let html = '';
+
+ // P&L Card
+ html += `
+
+
+ Revenue (Qtr)
+ ${formatMoney(gameState.quarterRevenue)}
+
+
+ Revenue (Lifetime)
+ ${formatMoney(gameState.totalEarned)}
+
+ ${garnishActive ? `
+ 🔴 IRS Garnishment
+ −15%
+
` : ''}
+
+ Capital Spending (Qtr)
+ (${formatMoney(gameState.quarterExpenses)})
+
+
+ Depreciation
+ (${formatMoney(qDepreciation)})
+
+
+ Taxes Paid (Qtr)
+ (${formatMoney(gameState.quarterTaxPaid)})
+
+
+ ${amtApplies ? '⚠️ AMT Taxable' : 'Taxable Income'}
+ ${formatMoney(amtApplies ? gameState.quarterRevenue : qTaxable)}
+
+
+ Net Income (Qtr)
+ ${qNet < 0 ? '(' + formatMoney(-qNet) + ')' : formatMoney(qNet)}
+
+
+ Net Income (Lifetime)
+ ${ltNet < 0 ? '(' + formatMoney(-ltNet) + ')' : formatMoney(ltNet)}
+
+
`;
+
+ // Investor Relations Card (if public)
+ if (gameState.isPublic) {
+ const earningsDaysSince = currentDay - gameState.lastEarningsDay;
+ const earningsDaysLeft = Math.max(0, EARNINGS_QUARTER_DAYS - earningsDaysSince);
+ const earningsQLabel = getEarningsQuarterLabel();
+ const stockPrice = getStockPrice();
+ const stockPriceStart = gameState.ipoStockPriceStart || stockPrice;
+ const qtdChange = stockPriceStart > 0 ? ((stockPrice - stockPriceStart) / stockPriceStart * 100) : 0;
+ const qtdColor = qtdChange >= 0 ? '#217346' : '#c00';
+ const qtdStr = (qtdChange >= 0 ? '+' : '') + qtdChange.toFixed(1) + '%';
+ const guidanceKey = gameState.currentGuidance || 'in-line';
+ const guidanceLevel = GUIDANCE_LEVELS[guidanceKey];
+ const target = gameState.guidanceTarget;
+ const qRev = gameState.earningsQuarterRevenue;
+ const daysElapsed = earningsDaysSince || 1;
+ const expectedAtThisPoint = target > 0 ? (target * daysElapsed / EARNINGS_QUARTER_DAYS) : 0;
+ const onTrack = qRev >= expectedAtThisPoint;
+ const trackColor = onTrack ? '#217346' : '#c00';
+ const trackLabel = onTrack ? '✅ On Track' : '⚠️ Off Track';
+ const streakVal = gameState.earningsStreak;
+ const streakStr = streakVal > 0 ? `🔥 ${streakVal} beat${streakVal > 1 ? 's' : ''}` :
+ streakVal < 0 ? `❄️ ${Math.abs(streakVal)} miss${Math.abs(streakVal) > 1 ? 'es' : ''}` : '—';
+
+ html += `
+
+
+ Quarter
+ ${earningsQLabel}
+
+
+ Stock Price
+ ${formatMoney(stockPrice)} ${qtdStr}
+
+
+ Revenue vs Target
+ ${trackLabel}
+
+
+ Progress
+ ${formatCompact(qRev)} / ${formatCompact(target)}
+
+
+ Guidance
+ ${guidanceLevel.emoji} ${guidanceLevel.label} (${guidanceLevel.reMult}× RE)
+
+
+ Streak
+ ${streakStr}
+
+
+ Analyst Expectation
+ ${(gameState.analystBaseline).toFixed(2)}×
+
+
+ Retained Earnings
+ ${gameState.retainedEarnings.toLocaleString()} RE
+
`;
+
+ // CFO selector
+ const maxCFOLevel = getFinanceDeptLevel();
+ if (maxCFOLevel > 0) {
+ const activeCFO = gameState.activeCFOLevel;
+ const labels = { 0: 'Manual', 1: '👶 Intern', 2: '📊 CFO', 3: '🎩 Elite' };
+ let cfoHtml = '
';
+ for (let lvl = 0; lvl <= maxCFOLevel; lvl++) {
+ cfoHtml += `${labels[lvl]} `;
+ }
+ cfoHtml += '
';
+ const record = gameState.cfoRecords[activeCFO];
+ const recordStr = activeCFO > 0 && record && record.total > 0
+ ? `Record: ${record.beats}/${record.total} (${Math.round(record.beats / record.total * 100)}%)`
+ : '';
+ html += `
Finance Dept${recordStr ? ' — ' + recordStr : ''}
${cfoHtml}`;
+ }
+
+ html += '
';
+ }
+
+ // Tax Debts
+ if (gameState.taxDebts && gameState.taxDebts.length > 0) {
+ const stageLabels = {
+ notice1: '1st Notice',
+ notice2: '⚠ 2nd Notice',
+ garnish: '🔴 Garnishment',
+ seizure: '🚨 Seizure',
+ };
+
+ html += `
+ `;
+
+ for (let i = 0; i < gameState.taxDebts.length; i++) {
+ const d = gameState.taxDebts[i];
+ const interest = d.current - d.original;
+ const qLabel = d.quarter || '';
+
+ // Next escalation countdown (matching desktop updateTaxPanel)
+ const daysToNext = d.stage === 'notice1' ? (30 - d.daysOverdue) :
+ d.stage === 'notice2' ? (90 - d.daysOverdue) :
+ d.stage === 'garnish' ? (180 - d.daysOverdue) : 0;
+ const nextLabel = d.stage === 'notice1' ? '2nd Notice' :
+ d.stage === 'notice2' ? 'Garnish' :
+ d.stage === 'garnish' ? 'Seizure' : '—';
+ const nextText = daysToNext > 0 ? `${nextLabel} in ${daysToNext}d` : '';
+
+ html += `
+
+
+ Original
+ ${formatMoney(d.original)}
+
+
+ Interest
+ ${formatMoney(interest)}
+
+
+ Total Due
+ ${formatMoney(d.current)}
+
+
+ Status
+ ${stageLabels[d.stage]} (${d.daysOverdue}d)
+
+ ${nextText ? `
+ Next Escalation
+ ⏳ ${nextText}
+
` : ''}
+
= d.current ? '' : 'disabled'}>Settle ${formatMoney(d.current)}
+
`;
+ }
+
+ if (gameState.taxDebts.length > 1) {
+ const total = totalTaxOwed();
+ html += `
= total ? '' : 'disabled'}>Settle All — ${formatMoney(total)} `;
+ }
+
+ html += '
';
+ }
+
+ container.innerHTML = html;
+}
+
+// ===== MOBILE BOARD ROOM RENDERING =====
+function buildMobileBoardRoom() {
+ if (!isMobile() || !gameState.arc) return;
+ const container = document.getElementById('mobile-boardroom-content');
+ if (!container) return;
+
+ const hashParts = [
+ gameState.retainedEarnings,
+ JSON.stringify(gameState.boardRoomPurchases),
+ gameState.cash|0,
+ ].join('|');
+ if (hashParts === _lastMobileBRHash && container.innerHTML !== '') return;
+ _lastMobileBRHash = hashParts;
+
+ let html = '';
+
+ // RE Balance header
+ html += `
+
+
`;
+
+ // Group upgrades by category (matching desktop buildBoardRoom)
+ const categoryOrder = ['Finance', 'Revenue', 'Tax', 'Investor', 'Protection'];
+ const categoryLabels = {
+ Finance: '📊 Finance',
+ Revenue: '💰 Revenue',
+ Tax: '🏛️ Tax',
+ Investor: '📈 Investor Relations',
+ Protection: '🛡️ Protection',
+ };
+ const grouped = {};
+ for (const upgrade of BOARD_ROOM_UPGRADES) {
+ if (!grouped[upgrade.category]) grouped[upgrade.category] = [];
+ grouped[upgrade.category].push(upgrade);
+ }
+ for (const cat of Object.keys(grouped)) {
+ grouped[cat].sort((a, b) => getUpgradeCost(a) - getUpgradeCost(b));
+ }
+
+ for (const cat of categoryOrder) {
+ const upgrades = grouped[cat];
+ if (!upgrades || upgrades.length === 0) continue;
+
+ // Category header
+ html += `${categoryLabels[cat] || cat}
`;
+
+ for (const upgrade of upgrades) {
+ const owned = getBoardRoomUpgradeCount(upgrade.id);
+ const isOwned = owned > 0 && upgrade.maxCount !== Infinity;
+ const requiresMet = !upgrade.requires || hasBoardRoomUpgrade(upgrade.requires);
+ const cost = getUpgradeCost(upgrade);
+ const canAfford = gameState.retainedEarnings >= cost;
+ const isLocked = !requiresMet;
+
+ let cardClass = 'mob-br-card';
+ if (isOwned) cardClass += ' owned';
+ if (isLocked) cardClass += ' locked';
+
+ let statusHtml = '';
+ if (isOwned) {
+ const label = upgrade.maxCount === 1 ? '✅ Owned' : `✅ Owned (×${owned})`;
+ statusHtml = `${label} `;
+ } else if (isLocked) {
+ const reqUpgrade = BOARD_ROOM_UPGRADES.find(u => u.id === upgrade.requires);
+ const reqName = reqUpgrade ? reqUpgrade.name : upgrade.requires;
+ statusHtml = `🔒 Requires ${reqName} `;
+ } else {
+ const countLabel = upgrade.maxCount === Infinity && owned > 0 ? ` (have ${owned})` : '';
+ statusHtml = `Buy${countLabel} `;
+ }
+
+ // Dynamic description for Growth Initiative — show total bonus
+ let desc = upgrade.desc;
+ if (upgrade.id === 'growth_initiative' && owned > 0) {
+ const totalBonus = ((Math.pow(1.02, owned) - 1) * 100).toFixed(1);
+ desc = `+2% revenue per purchase (stacks). Current: +${totalBonus}% total (×${owned})`;
+ }
+
+ const costLabel = upgrade.maxCount === Infinity && owned > 0
+ ? `${cost.toLocaleString()} RE (×${owned})`
+ : `${cost.toLocaleString()} RE`;
+
+ html += `
+
${upgrade.name}
+
${desc}
+
+
`;
+ } // end upgrade loop
+ } // end category loop
+
+ container.innerHTML = html;
+
+ // Re-attach buy button handlers via delegation
+ container.onclick = (e) => {
+ const buyBtn = e.target.closest('[data-buy-upgrade]');
+ if (buyBtn) {
+ e.stopPropagation();
+ purchaseBoardRoomUpgrade(buyBtn.dataset.buyUpgrade);
+ _lastMobileBRHash = ''; // force rebuild
+ buildMobileBoardRoom();
+ }
+ };
+}
+
+// ===== MOBILE TICK UPDATE =====
+function mobileTickUpdate() {
+ if (!isMobile()) return;
+
+ updateMobileNav();
+ mobileUpdateCollectHighlights();
+
+ // Always update cash header if visible
+ if (_mobileActiveTab !== 'operations') {
+ updateMobileCashHeader();
+ }
+
+ // Update active mobile tab content
+ if (_mobileActiveTab === 'pnl') {
+ buildMobilePnL();
+ } else if (_mobileActiveTab === 'boardroom') {
+ buildMobileBoardRoom();
+ } else if (_mobileActiveTab === 'settings') {
+ updateMobileSettings();
+ }
+}
document.addEventListener('DOMContentLoaded', init);
-window.addEventListener('resize', () => { if (gridBuilt) buildFillerRows(); });
+window.addEventListener('resize', () => {
+ if (gridBuilt) buildFillerRows();
+ if (typeof updateMobileNav === 'function') updateMobileNav();
+});
diff --git a/index.html b/index.html
index 5eaea8d..89e68a3 100644
--- a/index.html
+++ b/index.html
@@ -2,7 +2,7 @@
-
+
@@ -20,9 +20,46 @@
Q4 Financials - Operations.xlsx - Quarter Close
-
+
+
+
+
+
+
+
v0.2.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Q4 Financials.xlsx
+
+
Quarter Close
+
LOOK BUSY. GET RICH.
+
+
+
An idle tycoon game disguised as a spreadsheet
+
+
+
@@ -46,7 +83,7 @@
Q4 Financials - Operations.xlsx
- ─
+ ─
□
✕
@@ -69,7 +106,11 @@
-
+
@@ -136,44 +177,32 @@
-
@@ -217,6 +246,16 @@
+
+
+
@@ -239,6 +278,55 @@
100%
+
+
+
+
+
+
+
+
+
+
+
📊 Quarter Close
+
v0.2.0 — Late-Game Progression
+
+
+ 💾 Save Game
+ Auto-save: ON
+ 🕶️ Boss Mode
+ ⚙️ Game Options
+ ❓ Help
+ 🗑️ New Game
+
+
+
+
+
+
+
+ 📊
+ Ops
+
+
+ 📋
+ P&L
+
+
+ 🏢
+ Board
+
+
+ ⚙️
+ More
+
+
@@ -251,7 +339,7 @@
Book1 - Excel
- ─
+ ─
□
✕
@@ -297,6 +385,18 @@
+
+
+
@@ -331,13 +431,56 @@
📊 Quarter Close
-
v0.3.0 — Phase 2.2: Board Room
+
v0.2.0 — Late-Game Progression
An idle tycoon game disguised as a spreadsheet.
Build your empire. Look like you're working.
OK
+
+
+
+
+
+
+
+
Event Frequency
+
How often random events fire. Default: ~1 event per quarter.
+
+
+ 📬 Events: 1.0×
+ 0 = off, 1× = default, 10× = absolute mayhem
+
+
+
+
+
+
+
@@ -345,73 +488,198 @@
📊 Quarter Close
📊 Quarter Close — Help
✕
+
+ Basics
+ Active Play
+ Events & Taxes
+ Board Room
+ Tips
+
-
-
🎯 Goal
-
Build a company from nothing. Start with a lemonade stand, end with a tech empire. The spreadsheet is your cover — it looks like you're working.
-
-
-
-
💰 Making Money
-
Click "Collect" on a department to grab its pending revenue. Early on, clicking is your main income.
-
Hire employees to increase a department's revenue per day. More staff = more money.
-
Upgrade departments to multiply each employee's output (2×, 4×, 8×...).
-
Automate a department so it collects revenue automatically — no clicking needed.
-
Unlock new departments as you earn enough. Higher tiers earn exponentially more.
-
-
-
📋 Mini-Tasks
-
Desktop-style task notifications pop up periodically. Click ✔ Approve for bonus cash. They auto-expire in 10 seconds — if you miss it, your streak resets.
-
Streaks: 3 in a row = 1.5× reward, 5 = 2×, 10 = 3×.
+
+
+
+
+
🎯 Goal
+
Build a company from nothing. Start with a blog, end with a tech empire. The spreadsheet is your cover — it looks like you're working.
+
+
+
🏗️ Departments
+
Each department is a revenue source with employees. Higher tiers unlock as you earn more and produce exponentially more revenue.
+
+
+
👥 Hiring
+
Hire employees to increase revenue per day. The Max(N) button hires as many as you can afford at once.
+
+
+
⬆️ Upgrades
+
Upgrade a department to multiply each employee's output (2×, 4×, 8×...). Upgrades are the biggest revenue multiplier.
+
+
+
🤖 Automation
+
Automate a department so it collects revenue automatically — no more clicking "Collect". Essential for scaling.
+
+
+
📈 Earnings & IPO
+
Once big enough, you'll go public . Each quarter you set earnings guidance, then try to beat it. Beating guidance earns Retained Earnings (RE) — the prestige currency for Board Room upgrades.
+
+
-
-
📧 Events
-
Random events pop up as desktop-style toasts (Mom's investment, angry customers, viral TikTok moments). Each has a 10-second timer — the last option auto-fires if you don't choose.
-
Golden cells occasionally glow gold in the grid — click them fast for a big bonus!
+
+
+
All three mechanics are optional — toggle them in Data → Game Options .
+
+
+
🎯 Management Focus
+
Click a department's name (look for the 🎯 icon) to add focus points.
+
Each point = +5% revenue for that department, up to +50% at max focus.
+
Focus decays over time — 1 point lost every 10 seconds of not clicking. Keep rotating between departments for maximum benefit.
+
The bonus shows as a green percentage in the Rev/day column.
+
+
+
⏰ Overtime
+
The "Push It" button below your departments gives instant cash equal to 5 seconds of revenue.
+
Diminishing returns: each click in the same quarter gives less. Click 1 = 100%, click 5 ≈ 57%, click 10 ≈ 40%.
+
Resets every quarter — fresh energy each Q.
+
Shows your click count, next payout, and efficiency %.
+
+
+
🤝 Close the Deal
+
Random enterprise contracts pop up as toasts every 1–4 minutes.
+
Click "✍️ Sign" rapidly to collect enough signatures before the 12-second timer expires.
+
Required clicks scale with company size. Reward = 30–60 seconds of revenue.
+
Miss it? The client walks — no penalty, just missed money.
+
+
+
📋 Mini-Tasks
+
Desktop-style notifications pop up periodically. Click ✔ Approve for bonus cash.
+
Auto-expire in 10 seconds — miss it and your streak resets.
+
Streaks: 3 in a row = 1.5×, 5 = 2×, 10 = 3× reward.
+
+
-
-
🏛️ Taxes
-
Every 90 game-days, the IRS sends a quarterly tax bill.
-
Regular tax: 25% of (Revenue − Depreciation).
-
AMT floor: You always pay at least 15% of gross revenue — no deducting your way to zero.
-
Pay or ignore — but ignored bills accrue 1% daily interest and escalate: Notice → 2nd Notice → Garnishment (−15% revenue) → Asset Seizure.
-
Depreciation: Capital spending (hires, upgrades, automation) is deducted over 4 quarters, not immediately.
+
+
+
+
+
📧 Random Events
+
Toast notifications with choices: Mom's investment, angry customers, viral moments, IT disasters, R&D breakthroughs, and more.
+
Each has a 10-second timer — the last option auto-fires if you don't choose.
+
Events have different rarities — angry customers and password resets happen often, while ransomware and DDoS attacks are rare.
+
At default frequency, expect roughly 1 event per quarter .
+
Event frequency can be adjusted in Settings (0× = off, 10× = absolute mayhem).
+
Golden cells occasionally glow in the grid — click them fast for a big bonus!
+
+
+
🔬 R&D Breakthroughs
+
Rare event that permanently doubles a random department's revenue.
+
Appears roughly every 3-4 game years at default event frequency.
+
Stacks: a second breakthrough on the same dept = 4× total.
+
+
+
⚡ IT Disasters
+
8 types: ransomware, DDoS, DB corruption, email outage, password reset, cloud outage, P0 bug, laptop recall.
+
Effects range from revenue penalties to full production freezes. Some offer a costly quick-fix vs. waiting it out.
+
Active effects show as color-coded indicators in the $/day display.
+
+
+
🏛️ Quarterly Taxes
+
Every 90 game-days, the IRS sends a tax bill.
+
Regular tax: 25% of (Revenue − Depreciation).
+
AMT floor: You always pay at least 15% of gross revenue — can't deduct your way to zero.
+
+
+
⚠️ Tax Consequences
+
Pay or ignore — but ignored bills accrue 1% daily interest and escalate:
+
Notice → 2nd Notice → Garnishment (−15% revenue) → Asset Seizure .
+
Depreciation: Capital spending (hires, upgrades, automation) is deducted over 4 quarters, not immediately.
+
+
-
-
📈 Valuation Chart
-
The floating chart shows your company's valuation over time. Drag to move, resize from the corner. Valuation = Cash + (Annual Revenue × market multiple).
+
+
+
Unlocks after IPO . Spend Retained Earnings (RE) on permanent upgrades.
+
+
+
📊 CFO (Finance)
+
Auto-handles quarterly earnings guidance. Three levels:
+
Lv1 "The Intern" — picks guidance randomly. Often wrong, but no more popups.
+
Lv2 "Competent CFO" — reads context: plays safe after misses, gets bolder on beat streaks. ~70% optimal.
+
Lv3 "Elite CFO" — factors in streaks, analyst pressure, bonuses, and penalties. Knows when to push and when to pull back. ~90% optimal.
+
Switch between owned levels anytime in the IR section.
+
+
+
🔥 Earnings Streaks
+
Consecutive beats earn bonus RE: +10% per beat , up to 2.0× at 10 beats .
+
The current multiplier shows in the IR section and earnings report.
+
A miss resets your streak to zero. High streaks also make analysts ratchet up expectations faster — the CFO factors this in.
+
+
+
💰 Revenue Boosts
+
Revenue Multipliers — permanent boosts that stack (1.1× → 1.25× → 1.5×).
+
Growth Initiative — repeatable +2% per purchase. Cost scales 10% each time. Stacks multiplicatively — buy early, buy often.
+
+
+
🏛️ Tax & Protection
+
Lobbyist / Tax Haven — reduce tax rate (25% → 20% → 15%).
+
CPA on Retainer — auto-pays taxes and settles debts. No more IRS toasts.
+
Golden Parachute — blocks one IRS asset seizure (consumed, rebuyable).
+
Analyst Relations — slows analyst expectation ratchet by 50%.
+
+
+
★ Restructure
+
Each automated department gets a ★ Restructure button post-IPO.
+
Each level = 10× revenue for that department. Costs RE.
+
Stacks: ★2 = 100×, ★3 = 1,000×.
+
+
+
🔬 R&D Breakthroughs
+
Random event: R&D discovers a breakthrough that permanently doubles a department's revenue.
+
Choose: implement (×2 revenue) or file patent (5% cash).
+
Stacks on same dept: ×4, ×8, ×16...
+
+
+
📈 Stock & Valuation
+
Valuation = Cash + (Annual Revenue × market multiple).
+
The floating chart tracks it over time. Drag to move, resize from corner.
+
Stock price shown in column H of the stats row.
+
+
-
-
⌨️ Tips
-
Esc — Boss key! Switches to a fake empty spreadsheet instantly.
-
Ctrl+S — Save game. Auto-save is on by default.
-
File → New Game — Start over with a different career path.
-
Click any cell to see its reference in the formula bar (just like Excel).
+
+
+
+
+
⌨️ Keyboard Shortcuts
+
Esc — Boss key! Instantly switches to a fake empty spreadsheet.
+
Ctrl+S — Manual save (auto-save is always on).
+
+
+
📊 Spreadsheet Details
+
Click any cell to see its reference in the formula bar, just like real Excel.
+
Column headers (A, B, C...) are purely cosmetic — for immersion.
+
+
+
🎮 Game Management
+
File → New Game — start over with a fresh company.
+
Data → Game Options — toggle active play mechanics on/off.
+
File → Save — auto-save runs, but manual save works too.
+
+
+
💡 Strategy Tips
+
Focus spending on the highest tier you can afford — lower tiers become irrelevant fast.
+
Automate early — the sooner a dept runs itself, the sooner you can focus elsewhere.
+
Beat earnings guidance to earn RE — conservative guidance is safer but earns less RE.
+
Growth Initiative compounds — 10 purchases = +21.9% total revenue.
+
+
-
-
🔢 Max Buttons
-
When you can afford 2+ hires or upgrades, a Max(N) link appears — click it to buy them all at once.
-
-
-
-
🏢 Board Room (Post-IPO)
-
After going public, the Board Room tab unlocks. Spend Retained Earnings (RE) on permanent upgrades:
-
Finance Dept — auto-handles quarterly earnings. Three levels with increasing intelligence:
-
Lv1 "The Intern" — randomizes guidance (often wrong, but no more popups).
-
Lv2 "Competent CFO" — analyzes revenue trends, picks smartly ~70% of the time.
-
Lv3 "Elite CFO" — factors in streaks, bonuses, and analyst pressure. ~90% optimal.
-
You can switch between owned levels anytime in the IR section.
-
Revenue Multipliers — permanent boosts that stack (1.1× → 1.25× → 1.5×).
-
Lobbyist / Tax Haven — reduce your tax rate (25% → 20% → 15%).
-
Analyst Relations — slows the analyst expectation ratchet by 50%.
-
Golden Parachute — protects against IRS asset seizure (consumed on use, rebuyable).
-
@@ -427,7 +695,7 @@
🏢 Board Room (Post-IPO)
-
+