feat: buy x1/x5/x10/xMax + production preview per generator
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 21s

- buyGenerator() supports quantity param (multi-buy loop)
- maxAffordable() / bulkCost() — compute max purchasable + total cost
- GeneratorShop: mode selector (x1/x5/x10/MAX)
- Each generator shows +X/s in amber — what the next purchase adds
- Button shows total cost + quantity (e.g. "1.5k (x5)")
This commit is contained in:
2026-03-28 20:52:30 +01:00
parent 38e63fdf22
commit 120f4bedca
3 changed files with 91 additions and 20 deletions

View File

@@ -2,9 +2,14 @@
import { scale } from 'svelte/transition';
import { quintOut } from 'svelte/easing';
import { game } from '$lib/stores/game.svelte';
import { generatorEffectiveProduction } from '$lib/core/economy';
import { generatorEffectiveProduction, maxAffordable, bulkCost } from '$lib/core/economy';
import { formatNumber } from '$lib/utils/formatNumber';
import CollapsiblePanel from './CollapsiblePanel.svelte';
type BuyMode = 1 | 5 | 10 | 'max';
let buyMode = $state<BuyMode>(1);
const MODES: BuyMode[] = [1, 5, 10, 'max'];
</script>
<CollapsiblePanel
@@ -12,11 +17,28 @@
badge="{formatNumber(game.productionPerSecond)}/s"
accentClass=""
>
<!-- Buy mode selector -->
<div class="flex gap-0.5 p-0.5 rounded-lg" style="background: rgba(255,255,255,0.04);">
{#each MODES as mode}
<button
class="flex-1 py-1 rounded-md text-[0.65rem] font-semibold transition-all duration-150"
style="font-family: var(--font); {buyMode === mode
? 'background: rgba(255,255,255,0.1); color: rgba(255,255,255,0.95);'
: 'background: transparent; color: rgba(255,255,255,0.4);'
}"
onclick={() => buyMode = mode}
>
{mode === 'max' ? 'MAX' : `x${mode}`}
</button>
{/each}
</div>
{#each game.state.generators as gen, i}
{@const cost = game.generatorCostWithTree(gen)}
{@const canAfford = game.state.resources >= cost}
{@const qty = buyMode === 'max' ? maxAffordable(game.state, gen.id) : buyMode}
{@const cost = qty <= 1 ? game.generatorCostWithTree(gen) : bulkCost(game.state, gen.id, qty)}
{@const canAfford = game.state.resources >= cost && qty > 0}
{@const effectiveProd = generatorEffectiveProduction(gen, game.state)}
{@const nextUnitProd = generatorEffectiveProduction({ ...gen, owned: 1 }, game.state)}
{@const nextGain = generatorEffectiveProduction({ ...gen, owned: qty || 1 }, game.state)}
{@const share = game.productionPerSecond > 0 ? (effectiveProd / game.productionPerSecond * 100) : 0}
<div
class="gp-row {canAfford ? 'gp-row--active' : 'gp-row--locked'}"
@@ -39,21 +61,30 @@
<span class="gp-label gp-accent-green">{formatNumber(effectiveProd)}/s</span>
<span class="gp-label">·</span>
<span class="gp-label">{share.toFixed(0)}%</span>
<span class="gp-label">·</span>
<span class="gp-label gp-accent-amber">+{formatNumber(nextGain)}/s</span>
</div>
<!-- Mini progress showing share of total production -->
<div class="h-[2px] rounded-full mt-0.5" style="background: rgba(255,255,255,0.06);">
<div class="h-full rounded-full" style="width: {Math.min(share, 100)}%; background: var(--color-gp-accent-green); opacity: 0.5;"></div>
</div>
{:else}
<span class="gp-label">+{formatNumber(nextUnitProd)}/s par unite</span>
<span class="gp-label">
<span class="gp-accent-amber">+{formatNumber(nextGain)}/s</span> par unite
</span>
{/if}
</div>
<button
onclick={() => game.buy(gen.id)}
onclick={() => game.buy(gen.id, qty || 1)}
disabled={!canAfford}
class="gp-btn {canAfford ? 'gp-btn--buy' : 'gp-btn--disabled'}"
>
{#if buyMode === 'max' && qty > 1}
{formatNumber(cost)} (x{qty})
{:else if buyMode !== 1 && buyMode !== 'max'}
{formatNumber(cost)} (x{buyMode})
{:else}
{formatNumber(cost)}
{/if}
</button>
</div>
{/each}

View File

@@ -661,22 +661,62 @@ export function applyClick(state: GameState, rng: number = Math.random()): Click
}
// Achat d'un générateur (retourne null si fonds insuffisants)
export function buyGenerator(state: GameState, genId: string): GameState | null {
export function buyGenerator(state: GameState, genId: string, quantity = 1): GameState | null {
const genIndex = state.generators.findIndex((g) => g.id === genId);
if (genIndex === -1) return null;
const gen = state.generators[genIndex];
let gen = { ...state.generators[genIndex] };
let resources = state.resources;
let bought = 0;
for (let i = 0; i < quantity; i++) {
const cost = generatorCost(gen, state.evolutionTree);
if (state.resources < cost) return null;
if (resources < cost) break;
resources -= cost;
gen = { ...gen, owned: gen.owned + 1 };
bought++;
}
if (bought === 0) return null;
const updatedGenerators = [...state.generators];
updatedGenerators[genIndex] = { ...gen, owned: gen.owned + 1 };
updatedGenerators[genIndex] = gen;
return {
...state,
resources: state.resources - cost,
generators: updatedGenerators,
};
return { ...state, resources, generators: updatedGenerators };
}
// Calcule combien d'unités on peut acheter avec les ressources actuelles
export function maxAffordable(state: GameState, genId: string): number {
const genIndex = state.generators.findIndex((g) => g.id === genId);
if (genIndex === -1) return 0;
let gen = { ...state.generators[genIndex] };
let resources = state.resources;
let count = 0;
while (true) {
const cost = generatorCost(gen, state.evolutionTree);
if (resources < cost) break;
resources -= cost;
gen = { ...gen, owned: gen.owned + 1 };
count++;
if (count > 1000) break; // safety
}
return count;
}
// Cout total pour acheter N unités
export function bulkCost(state: GameState, genId: string, quantity: number): number {
const genIndex = state.generators.findIndex((g) => g.id === genId);
if (genIndex === -1) return Infinity;
let gen = { ...state.generators[genIndex] };
let total = 0;
for (let i = 0; i < quantity; i++) {
total += generatorCost(gen, state.evolutionTree);
gen = { ...gen, owned: gen.owned + 1 };
}
return total;
}
// Prestige : reset run, gain ADN, arbre persiste

View File

@@ -127,9 +127,9 @@ class Game {
this.lastClickCrit = result.isCrit;
}
buy(genId: string) {
buy(genId: string, quantity = 1) {
if (!this.ready) return;
const updated = buyGenerator(applyIdleGains(this.state, Date.now()), genId);
const updated = buyGenerator(applyIdleGains(this.state, Date.now()), genId, quantity);
if (updated) this.applyState(updated);
}