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 { scale } from 'svelte/transition';
import { quintOut } from 'svelte/easing'; import { quintOut } from 'svelte/easing';
import { game } from '$lib/stores/game.svelte'; 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 { formatNumber } from '$lib/utils/formatNumber';
import CollapsiblePanel from './CollapsiblePanel.svelte'; import CollapsiblePanel from './CollapsiblePanel.svelte';
type BuyMode = 1 | 5 | 10 | 'max';
let buyMode = $state<BuyMode>(1);
const MODES: BuyMode[] = [1, 5, 10, 'max'];
</script> </script>
<CollapsiblePanel <CollapsiblePanel
@@ -12,11 +17,28 @@
badge="{formatNumber(game.productionPerSecond)}/s" badge="{formatNumber(game.productionPerSecond)}/s"
accentClass="" 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} {#each game.state.generators as gen, i}
{@const cost = game.generatorCostWithTree(gen)} {@const qty = buyMode === 'max' ? maxAffordable(game.state, gen.id) : buyMode}
{@const canAfford = game.state.resources >= cost} {@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 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} {@const share = game.productionPerSecond > 0 ? (effectiveProd / game.productionPerSecond * 100) : 0}
<div <div
class="gp-row {canAfford ? 'gp-row--active' : 'gp-row--locked'}" 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 gp-accent-green">{formatNumber(effectiveProd)}/s</span>
<span class="gp-label">·</span> <span class="gp-label">·</span>
<span class="gp-label">{share.toFixed(0)}%</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> </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-[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 class="h-full rounded-full" style="width: {Math.min(share, 100)}%; background: var(--color-gp-accent-green); opacity: 0.5;"></div>
</div> </div>
{:else} {: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} {/if}
</div> </div>
<button <button
onclick={() => game.buy(gen.id)} onclick={() => game.buy(gen.id, qty || 1)}
disabled={!canAfford} disabled={!canAfford}
class="gp-btn {canAfford ? 'gp-btn--buy' : 'gp-btn--disabled'}" class="gp-btn {canAfford ? 'gp-btn--buy' : 'gp-btn--disabled'}"
> >
{formatNumber(cost)} {#if buyMode === 'max' && qty > 1}
{formatNumber(cost)} (x{qty})
{:else if buyMode !== 1 && buyMode !== 'max'}
{formatNumber(cost)} (x{buyMode})
{:else}
{formatNumber(cost)}
{/if}
</button> </button>
</div> </div>
{/each} {/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) // 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); const genIndex = state.generators.findIndex((g) => g.id === genId);
if (genIndex === -1) return null; if (genIndex === -1) return null;
const gen = state.generators[genIndex]; let gen = { ...state.generators[genIndex] };
const cost = generatorCost(gen, state.evolutionTree); let resources = state.resources;
if (state.resources < cost) return null;
let bought = 0;
for (let i = 0; i < quantity; i++) {
const cost = generatorCost(gen, state.evolutionTree);
if (resources < cost) break;
resources -= cost;
gen = { ...gen, owned: gen.owned + 1 };
bought++;
}
if (bought === 0) return null;
const updatedGenerators = [...state.generators]; const updatedGenerators = [...state.generators];
updatedGenerators[genIndex] = { ...gen, owned: gen.owned + 1 }; updatedGenerators[genIndex] = gen;
return { return { ...state, resources, generators: updatedGenerators };
...state, }
resources: state.resources - cost,
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 // Prestige : reset run, gain ADN, arbre persiste

View File

@@ -127,9 +127,9 @@ class Game {
this.lastClickCrit = result.isCrit; this.lastClickCrit = result.isCrit;
} }
buy(genId: string) { buy(genId: string, quantity = 1) {
if (!this.ready) return; 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); if (updated) this.applyState(updated);
} }