Session 10: Buy Max bulk-buy feature for buildings
This commit is contained in:
@@ -301,6 +301,32 @@ h2, h3 {
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.buy-max-btn {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 6px;
|
||||||
|
background: var(--accent-purple);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 5px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buy-max-btn:hover:not(:disabled) {
|
||||||
|
background: #9b59b6;
|
||||||
|
transform: scale(1.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.buy-max-btn:disabled {
|
||||||
|
background: var(--text-muted);
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
/* Side Panel Tabs */
|
/* Side Panel Tabs */
|
||||||
.tabs {
|
.tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
+27
-1
@@ -266,5 +266,31 @@ firstPrestige achievement, save/load round-trip preserving essence.
|
|||||||
(Requires `pip install py_mini_racer`.)
|
(Requires `pip install py_mini_racer`.)
|
||||||
|
|
||||||
#### Next goals:
|
#### Next goals:
|
||||||
- Achievement progress tracking in stats panel
|
|
||||||
- Consider adding a "next essence at X stardust" progress hint
|
- Consider adding a "next essence at X stardust" progress hint
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Session 10 - 2026-08-22 (Morning)
|
||||||
|
**Status:** ✅ Buy Max (Bulk-Buy) Feature Added
|
||||||
|
|
||||||
|
#### What was done:
|
||||||
|
- Added a "Buy Max" bulk-buy mechanic — a staple of idle/clicker games that was previously missing (you could only buy one building at a time, forcing many tedious clicks).
|
||||||
|
- Added `Game.getMaxAffordable(buildingId)` which computes how many of a given building the player can afford right now using an iterative cost-scaling loop. It is read-only and does not mutate state.
|
||||||
|
- Added `Game.buyMaxBuilding(buildingId)` which loops buying one at a time until unaffordable, deducting the correct total stardust and incrementing counts properly (respects prestige costReduction upgrades since it reuses `getBuildingCost`).
|
||||||
|
- Wired a "Buy Max" button onto every building card in `UI.renderBuildings()` with an affordable-count badge (e.g. "5x Max").
|
||||||
|
- Updated `UI.updateBuildings()` so the Buy Max badge refreshes its count and enabled/disabled state every game tick, keeping it accurate as stardust accrues.
|
||||||
|
- Added CSS styling for `.buy-max-btn` (purple theme, hover scale, disabled state) to match existing building-card layout.
|
||||||
|
|
||||||
|
#### Files modified:
|
||||||
|
- `js/game.js` - added getMaxAffordable() and buyMaxBuilding() methods
|
||||||
|
- `js/ui.js` - rendered Buy Max button per building card + tick refresh in updateBuildings()
|
||||||
|
- `css/game.css` - .buy-max-btn styles
|
||||||
|
- `dev-journal.md` - appended Session 10 entry
|
||||||
|
|
||||||
|
#### Verification:
|
||||||
|
Wrote a headless Node.js test (`/opt/data/idle-game-test/buy-max-test.mjs`, lives OUTSIDE the repo). Evaluated the real game.js in-process with DOM/UI stubs. 6/6 checks pass: getMaxAffordable matches manual cost-scaling calc, buyMax deducts correct stardust and increments counts correctly, unaffordable purchase returns 0, getMaxAffordable is idempotent, and notifications fire on purchase.
|
||||||
|
|
||||||
|
#### Next goals:
|
||||||
|
- Consider adding "Buy Max" to upgrades too (upgrades are one-time so less critical, but a bulk "buy all affordable" could be nice).
|
||||||
|
- Potential new building tiers or prestige milestones.
|
||||||
|
- Sound variations or background music.
|
||||||
|
|||||||
+58
-1
@@ -233,7 +233,7 @@ const Game = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Buy a building
|
* Buy a single building
|
||||||
*/
|
*/
|
||||||
buyBuilding(buildingId) {
|
buyBuilding(buildingId) {
|
||||||
const cost = this.getBuildingCost(buildingId);
|
const cost = this.getBuildingCost(buildingId);
|
||||||
@@ -261,6 +261,63 @@ const Game = {
|
|||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate how many of a building the player can currently afford.
|
||||||
|
* Does not modify game state.
|
||||||
|
*/
|
||||||
|
getMaxAffordable(buildingId) {
|
||||||
|
const building = BUILDINGS[buildingId];
|
||||||
|
if (!building) return 0;
|
||||||
|
|
||||||
|
let stardust = this.state.stardust;
|
||||||
|
let count = 0;
|
||||||
|
let owned = this.state.buildings[buildingId] || 0;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const cost = Math.floor(building.baseCost * Math.pow(this.config.costScaling, owned + count));
|
||||||
|
if (stardust < cost) break;
|
||||||
|
stardust -= cost;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Buy as many of a building as the player can afford at once.
|
||||||
|
* Returns the number purchased.
|
||||||
|
*/
|
||||||
|
buyMaxBuilding(buildingId) {
|
||||||
|
const building = BUILDINGS[buildingId];
|
||||||
|
if (!building) return 0;
|
||||||
|
|
||||||
|
let count = 0;
|
||||||
|
while (this.state.stardust >= this.getBuildingCost(buildingId)) {
|
||||||
|
const cost = this.getBuildingCost(buildingId);
|
||||||
|
this.state.stardust -= cost;
|
||||||
|
this.state.buildings[buildingId] = (this.state.buildings[buildingId] || 0) + 1;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count > 0) {
|
||||||
|
UI.showNotification(`Bought ${count}x ${building.name}!`, 'success');
|
||||||
|
|
||||||
|
// Audio feedback
|
||||||
|
Audio.playPurchase();
|
||||||
|
|
||||||
|
// Particle effect on building purchase
|
||||||
|
const card = document.getElementById(`building-${buildingId}`);
|
||||||
|
if (card) {
|
||||||
|
const rect = card.getBoundingClientRect();
|
||||||
|
Particles.createBuildingEffect(rect.left + rect.width / 2, rect.top + rect.height / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
UI.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Buy an upgrade
|
* Buy an upgrade
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ const UI = {
|
|||||||
<div class="building-cost">${Game.formatNumber(cost)} ✨</div>
|
<div class="building-cost">${Game.formatNumber(cost)} ✨</div>
|
||||||
<div class="building-count">${count}</div>
|
<div class="building-count">${count}</div>
|
||||||
<button class="buy-btn" ${!canAfford || locked ? 'disabled' : ''}>Buy</button>
|
<button class="buy-btn" ${!canAfford || locked ? 'disabled' : ''}>Buy</button>
|
||||||
|
<button class="buy-max-btn" data-building="${id}" ${locked || count === 0 ? 'title="Buy as many as you can afford"' : 'disabled'}>${Game.formatNumber(Game.getMaxAffordable(id))}x Max</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -76,6 +77,12 @@ const UI = {
|
|||||||
card.querySelector('.buy-btn').addEventListener('click', () => {
|
card.querySelector('.buy-btn').addEventListener('click', () => {
|
||||||
Game.buyBuilding(id);
|
Game.buyBuilding(id);
|
||||||
});
|
});
|
||||||
|
const maxBtn = card.querySelector('.buy-max-btn');
|
||||||
|
if (maxBtn) {
|
||||||
|
maxBtn.addEventListener('click', () => {
|
||||||
|
Game.buyMaxBuilding(id);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
container.appendChild(card);
|
container.appendChild(card);
|
||||||
@@ -103,6 +110,15 @@ const UI = {
|
|||||||
btn.disabled = !canAfford || locked;
|
btn.disabled = !canAfford || locked;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Buy Max button - reflect how many can be bought at current stardust
|
||||||
|
const maxBtn = card.querySelector('.buy-max-btn');
|
||||||
|
if (maxBtn) {
|
||||||
|
const affordable = Game.getMaxAffordable(id);
|
||||||
|
maxBtn.textContent = `${Game.formatNumber(affordable)}x Max`;
|
||||||
|
maxBtn.disabled = locked || affordable === 0;
|
||||||
|
maxBtn.title = affordable > 0 ? `Buy up to ${affordable} ${building.name}(s)` : 'Not enough stardust';
|
||||||
|
}
|
||||||
|
|
||||||
card.classList.toggle('locked', locked);
|
card.classList.toggle('locked', locked);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user