feat: buy x1/x5/x10/xMax + production preview per generator
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 21s
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:
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user