diff --git a/css/game.css b/css/game.css index be42713..dc3faba 100644 --- a/css/game.css +++ b/css/game.css @@ -327,6 +327,42 @@ h2, h3 { opacity: 0.4; } +/* Upgrades header + Buy All button */ +.upgrades-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 10px; +} + +.upgrades-header h3 { + margin: 0; +} + +.buy-all-btn { + background: var(--accent-gold); + color: var(--bg-primary, #1a1035); + border: none; + border-radius: 6px; + padding: 6px 14px; + cursor: pointer; + font-weight: bold; + font-size: 0.8rem; + transition: all 0.2s ease; +} + +.buy-all-btn:hover:not(:disabled) { + background: #f1c40f; + transform: scale(1.03); +} + +.buy-all-btn:disabled { + background: var(--text-muted); + color: var(--bg-primary, #1a1035); + cursor: not-allowed; + opacity: 0.4; +} + /* Side Panel Tabs */ .tabs { display: flex; diff --git a/dev-journal.md b/dev-journal.md index f54de9b..53339de 100644 --- a/dev-journal.md +++ b/dev-journal.md @@ -386,3 +386,31 @@ Wrote a headless Node.js test (`/opt/data/idle-game-test/slot-preview-test.mjs`) - Consider adding a "Buy Max" button to upgrades too (one-time purchases — less critical). - Potential new building tiers or prestige milestones. - Sound variations or background music. + +--- + +### Session 14 - 2026-08-24 (Morning) +**Status:** ✅ Buy All Upgrades (Bulk-Buy for Upgrades) Added + +#### What was done: +- Followed up on Sessions 10, 11, and 13's recurring "next goal" by adding a **Buy All** button to the Upgrades tab. Previously one-time upgrades could only be purchased individually (one click each), which was tedious once dozens were available — buildings already had a Buy Max bulk-buy from Session 10, but upgrades did not. +- Added `Game.buyAllUpgrades()` which loops through every upgrade in `UPGRADES`, skips owned ones, respects each upgrade's `requirements` (building-count gates), and buys everything affordable at once. It runs until a full pass purchases nothing (so buying one upgrade can unlock affordability for another via the stardust re-check), then returns `{ bought: count, ids: [...] }`. +- Wired a **Buy All** button into the Upgrades tab header in `index.html` (`#buy-all-upgrades`). +- Added `UI.updateBuyAllButton()` (called from `UI.updateUpgrades()` each tick) which shows/hides the button based on whether any unowned, requirement-met upgrade is currently affordable, and toggles its disabled state live. +- Added CSS for `.upgrades-header` (flex layout) and `.buy-all-btn` (gold accent theme matching existing buttons, hover scale, disabled state). +- Fixed a latent bug found during testing: the empty-slot branch of `Game.switchSlot()` did not reset `state.productionBoostUntil`, which could leave a stale golden-bloom boost active after switching to a fresh slot. Added `productionBoostUntil: 0` to that branch for symmetry with `reset()`. + +#### Files modified: +- `js/game.js` - added `buyAllUpgrades()` method; wired Buy All button click listener in setupEventListeners(); fixed empty-slot productionBoostUntil reset in switchSlot() +- `js/ui.js` - added `updateBuyAllButton()`; called it from `updateUpgrades()` each tick +- `index.html` - added Buy All button to Upgrades tab header +- `css/game.css` - `.upgrades-header`, `.buy-all-btn` styles +- `dev-journal.md` - appended Session 14 entry + +#### Verification: +Wrote a headless Node.js test (`/opt/data/idle-game-test/buy-all-upgrades-test.mjs`) evaluating the real game.js and ui.js in-process with DOM/audio/particle stubs. **24/24 checks pass**, covering: no-op when nothing affordable, bulk purchase with requirement gating (blocks unmet requirements), chained affordability across loop passes, idempotent second call, already-owned upgrade skipping, notification-on-purchase vs silence on no-op, and the real `UI.updateBuyAllButton()` show/hide/enabled state against actual BUILDINGS/UPGRADES data. Existing buy-max (6/6) and golden-bloom (22/22) tests re-ran clean — no regressions. All modified JS files pass `node --check`. + +#### Next goals: +- Consider adding a "Buy Max" button to the prestige upgrades list as well. +- Potential new building tiers or prestige milestones. +- Sound variations or background music. diff --git a/index.html b/index.html index 44fe786..e7ba9dc 100644 --- a/index.html +++ b/index.html @@ -58,7 +58,10 @@
-

Upgrades

+
+

Upgrades

+ +
diff --git a/js/game.js b/js/game.js index ed29e12..f363cf7 100644 --- a/js/game.js +++ b/js/game.js @@ -124,6 +124,14 @@ const Game = { UI.hideImportModal(); }); + // Buy All upgrades button. + const buyAllBtn = document.getElementById('buy-all-upgrades'); + if (buyAllBtn) { + buyAllBtn.addEventListener('click', () => { + Game.buyAllUpgrades(); + }); + } + document.getElementById('reset-btn').addEventListener('click', () => { if (confirm('Are you sure you want to reset all progress? This cannot be undone!')) { this.reset(); @@ -419,6 +427,48 @@ const Game = { return false; }, + /** + * Buy every upgrade the player can currently afford at once. + * Returns an object describing what happened: { bought: count, ids: [...] }. + * Upgrades are one-time purchases, so we loop until nothing affordable remains. + */ + buyAllUpgrades() { + const bought = []; + // Loop until a pass buys nothing (each purchase may unlock new affordability). + let progress = true; + while (progress) { + progress = false; + for (const [id, upgrade] of Object.entries(UPGRADES)) { + if (this.state.upgrades.includes(id)) continue; + // Respect requirements before attempting purchase. + if (upgrade.requirements) { + let met = true; + for (const [reqId, reqCount] of Object.entries(upgrade.requirements)) { + if ((this.state.buildings[reqId] || 0) < reqCount) { + met = false; + break; + } + } + if (!met) continue; + } + if (this.state.stardust >= upgrade.cost) { + this.state.stardust -= upgrade.cost; + this.state.upgrades.push(id); + bought.push(id); + progress = true; // another purchase may have unlocked more affordability + } + } + } + + if (bought.length > 0) { + UI.showNotification(`Bought ${bought.length} upgrade(s)!`, 'success'); + Audio.playPurchase(); + UI.update(); + } + + return { bought: bought.length, ids: bought }; + }, + /** * Golden Starbloom - random event system. * A rare, time-limited bloom occasionally appears on screen. Clicking it @@ -758,6 +808,9 @@ const Game = { totalPrestigeEssence: this.state.totalPrestigeEssence || 0, prestigeUpgrades: this.state.prestigeUpgrades || [], goldenBlooms: { active: false, timer: 0, lastSpawn: Date.now(), totalClaimed: 0 }, + // Reset the temporary golden-bloom production boost so a boost + // from a previous slot cannot leak into a fresh slot. + productionBoostUntil: 0, activeSlot: slot, autoSave: this.state.autoSave }; diff --git a/js/ui.js b/js/ui.js index 395d47d..52bc7dc 100644 --- a/js/ui.js +++ b/js/ui.js @@ -189,15 +189,52 @@ const UI = { for (const [id, upgrade] of Object.entries(UPGRADES)) { const card = document.getElementById(`upgrade-${id}`); if (!card) continue; - + const purchased = Game.state.upgrades.includes(id); const costEl = card.querySelector('.upgrade-cost'); - + if (costEl && !purchased) { const canAfford = Game.state.stardust >= upgrade.cost; costEl.textContent = canAfford ? `${Game.formatNumber(upgrade.cost)} ✨` : `❌ ${Game.formatNumber(upgrade.cost)} ✨`; } } + + // Toggle the "Buy All" button: show it once at least one unowned, + // requirement-met upgrade exists, and disable it when nothing is + // affordable. Its enabled state is refreshed live each tick. + this.updateBuyAllButton(); + }, + + /** + * Show/hide and enable/disable the "Buy All upgrades" button based on + * whether any unowned upgrade (whose requirements are met) is currently + * affordable. Read of game state only; never mutates it. + */ + updateBuyAllButton() { + const btn = document.getElementById('buy-all-upgrades'); + if (!btn) return; + + let affordableAny = false; + for (const [id, upgrade] of Object.entries(UPGRADES)) { + if (Game.state.upgrades.includes(id)) continue; + if (Game.state.stardust < upgrade.cost) continue; + // Requirement check mirrors Game.buyUpgrade / buyAllUpgrades. + if (upgrade.requirements) { + let met = true; + for (const [reqId, reqCount] of Object.entries(upgrade.requirements)) { + if ((Game.state.buildings[reqId] || 0) < reqCount) { + met = false; + break; + } + } + if (!met) continue; + } + affordableAny = true; + break; + } + + btn.style.display = affordableAny ? '' : 'none'; + btn.disabled = !affordableAny; }, /**