Session 14: Buy All Upgrades (bulk-buy for upgrades) + empty-slot boost leak fix
- Added Game.buyAllUpgrades(): buys every affordable upgrade at once, respecting per-upgrade requirements and looping until nothing remains. - Added a 'Buy All' button to the Upgrades tab header; UI.updateBuyAllButton() shows/hides & enables/disables it live each tick based on affordability. - Fixed empty-slot branch of switchSlot(): added productionBoostUntil: 0 so a temporary golden-bloom boost cannot leak into a fresh slot. - Added .upgrades-header / .buy-all-btn CSS (gold accent theme). Verified: headless test buy-all-upgrades-test.mjs 24/24 pass; existing golden-bloom (22/22) and buy-max (6/6) tests re-run clean, no regressions.
This commit is contained in:
@@ -327,6 +327,42 @@ h2, h3 {
|
|||||||
opacity: 0.4;
|
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 */
|
/* Side Panel Tabs */
|
||||||
.tabs {
|
.tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -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).
|
- Consider adding a "Buy Max" button to upgrades too (one-time purchases — less critical).
|
||||||
- Potential new building tiers or prestige milestones.
|
- Potential new building tiers or prestige milestones.
|
||||||
- Sound variations or background music.
|
- 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.
|
||||||
|
|||||||
+4
-1
@@ -58,7 +58,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="upgrades-tab" class="tab-content active">
|
<div id="upgrades-tab" class="tab-content active">
|
||||||
<h3>Upgrades</h3>
|
<div class="upgrades-header">
|
||||||
|
<h3>Upgrades</h3>
|
||||||
|
<button id="buy-all-upgrades" class="buy-all-btn" style="display:none" title="Buy every upgrade you can currently afford">Buy All</button>
|
||||||
|
</div>
|
||||||
<div id="upgrades-list"></div>
|
<div id="upgrades-list"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+53
@@ -124,6 +124,14 @@ const Game = {
|
|||||||
UI.hideImportModal();
|
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', () => {
|
document.getElementById('reset-btn').addEventListener('click', () => {
|
||||||
if (confirm('Are you sure you want to reset all progress? This cannot be undone!')) {
|
if (confirm('Are you sure you want to reset all progress? This cannot be undone!')) {
|
||||||
this.reset();
|
this.reset();
|
||||||
@@ -419,6 +427,48 @@ const Game = {
|
|||||||
return false;
|
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.
|
* Golden Starbloom - random event system.
|
||||||
* A rare, time-limited bloom occasionally appears on screen. Clicking it
|
* A rare, time-limited bloom occasionally appears on screen. Clicking it
|
||||||
@@ -758,6 +808,9 @@ const Game = {
|
|||||||
totalPrestigeEssence: this.state.totalPrestigeEssence || 0,
|
totalPrestigeEssence: this.state.totalPrestigeEssence || 0,
|
||||||
prestigeUpgrades: this.state.prestigeUpgrades || [],
|
prestigeUpgrades: this.state.prestigeUpgrades || [],
|
||||||
goldenBlooms: { active: false, timer: 0, lastSpawn: Date.now(), totalClaimed: 0 },
|
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,
|
activeSlot: slot,
|
||||||
autoSave: this.state.autoSave
|
autoSave: this.state.autoSave
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -198,6 +198,43 @@ const UI = {
|
|||||||
costEl.textContent = canAfford ? `${Game.formatNumber(upgrade.cost)} ✨` : `❌ ${Game.formatNumber(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;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user