feat: click upgrades — buy click power with tadpoles, tied to generators
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
5 click upgrades, each linked to a generator type: - Nid Douillet (+1/clic, 50 base) — requires owning a Nid - Eau Fertile (+3/clic, 500 base) — requires a Mare - Spores Actives (+8/clic, 5k base) — requires a Marecage - Courant Vital (+20/clic, 50k base) — requires an Etang - Source Ancestrale (+50/clic, 500k base) — requires a Lac Cost scales x1.2 per level. Reset at prestige (like generators). Click gain = (base + upgradePower) × prestige × tree × infraBonus. ClickPanel shows upgrade shop with level badges and gen requirements. Adds tadpole sink for active play — strategic choice vs buying generators.
This commit is contained in:
@@ -1,34 +1,36 @@
|
||||
<script lang="ts">
|
||||
import { game } from '$lib/stores/game.svelte';
|
||||
import { getClickBreakdown } from '$lib/core/economy';
|
||||
import { getClickBreakdown, clickUpgradeCost } from '$lib/core/economy';
|
||||
import { formatNumber } from '$lib/utils/formatNumber';
|
||||
import CollapsiblePanel from './CollapsiblePanel.svelte';
|
||||
|
||||
let b = $derived(getClickBreakdown(game.state));
|
||||
let expected = $derived(b.total * (1 + b.doubleChance + b.critChance * 9));
|
||||
// Estimate: 5 clicks/sec manual → effective click production
|
||||
|
||||
const CLICKS_PER_SEC = 5;
|
||||
let manualProd = $derived(expected * CLICKS_PER_SEC);
|
||||
let totalWithClicks = $derived(game.productionPerSecond + b.effectivePerSec + manualProd);
|
||||
let clickShare = $derived(totalWithClicks > 0 ? ((b.effectivePerSec + manualProd) / totalWithClicks * 100) : 0);
|
||||
|
||||
// Generator names for display
|
||||
const GEN_NAMES: Record<string, string> = {
|
||||
nid: 'Nid', mare: 'Mare', marecage: 'Marecage', etang: 'Etang', lac: 'Lac',
|
||||
};
|
||||
</script>
|
||||
|
||||
<CollapsiblePanel title="Ponte (clic)" badge="{formatNumber(expected)}" accentClass="gp-accent-amber" defaultOpen={false}>
|
||||
<CollapsiblePanel title="Ponte (clic)" badge="{formatNumber(b.total)}" accentClass="gp-accent-amber" defaultOpen={false}>
|
||||
<!-- Gain par clic -->
|
||||
<div class="gp-row gp-row--active">
|
||||
<div class="flex flex-col flex-1">
|
||||
<span class="gp-value">Gain par clic</span>
|
||||
<span class="gp-label">
|
||||
x{b.prestigeMult.toFixed(1)} prestige · x{b.treeMult.toFixed(0)} arbre · x{b.genBonus.toFixed(1)} infra
|
||||
</span>
|
||||
<span class="gp-label" style="opacity: 0.5;">
|
||||
{b.genTypes} types (+{b.genTypes * 2}) · {b.genTotal} unites (+{(b.genTotal * 0.05).toFixed(1)})
|
||||
base {b.base} × x{b.prestigeMult.toFixed(1)} × x{b.treeMult.toFixed(0)} × x{b.genBonus.toFixed(1)}
|
||||
</span>
|
||||
</div>
|
||||
<span class="gp-value gp-accent-amber text-lg!">{formatNumber(b.total)}</span>
|
||||
</div>
|
||||
|
||||
<!-- Click contribution estimate -->
|
||||
<!-- Click contribution -->
|
||||
<div class="gp-row" style="border-color: rgba(251,191,36,0.15); background: rgba(251,191,36,0.04);">
|
||||
<div class="flex flex-col flex-1">
|
||||
<span class="gp-value text-[0.7rem]!">Contribution clics</span>
|
||||
@@ -40,67 +42,80 @@
|
||||
<span class="gp-label gp-accent-amber">{clickShare.toFixed(0)}%</span>
|
||||
</div>
|
||||
|
||||
<!-- Double ponte -->
|
||||
<!-- Click upgrades shop -->
|
||||
{#if (game.state.clickUpgrades ?? []).length > 0}
|
||||
<span class="gp-zone-label mt-1">Ameliorations de ponte</span>
|
||||
{#each game.state.clickUpgrades ?? [] as upgrade}
|
||||
{@const gen = game.state.generators.find((g) => g.id === upgrade.generatorId)}
|
||||
{@const hasGen = gen && gen.owned > 0}
|
||||
{@const cost = clickUpgradeCost(upgrade)}
|
||||
{@const canAfford = hasGen && game.state.resources >= cost}
|
||||
<div class="gp-row {canAfford ? 'gp-row--active' : 'gp-row--locked'}">
|
||||
<div class="flex flex-col min-w-0 flex-1">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="gp-value text-[0.7rem]!">{upgrade.name}</span>
|
||||
{#if upgrade.level > 0}
|
||||
<span
|
||||
class="gp-label px-1.5 py-0 rounded-full text-[0.6rem]!"
|
||||
style="background: rgba(251,191,36,0.15); color: var(--color-gp-accent-amber);"
|
||||
>
|
||||
nv.{upgrade.level}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<span class="gp-label">
|
||||
{#if hasGen}
|
||||
+{upgrade.baseClickPower}/clic par niveau ({GEN_NAMES[upgrade.generatorId]})
|
||||
{:else}
|
||||
Necessite un {GEN_NAMES[upgrade.generatorId]}
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
{#if hasGen}
|
||||
<button
|
||||
onclick={() => game.buyClickUpgrade(upgrade.id)}
|
||||
disabled={!canAfford}
|
||||
class="gp-btn {canAfford ? 'gp-btn--buy' : 'gp-btn--disabled'}"
|
||||
>
|
||||
{formatNumber(cost)}
|
||||
</button>
|
||||
{:else}
|
||||
<span class="gp-label">—</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<!-- Double/Crit/Auto -->
|
||||
<span class="gp-zone-label mt-1">Bonus (arbre)</span>
|
||||
|
||||
<div class="gp-row {b.doubleChance > 0 ? 'gp-row--unlocked' : 'gp-row--locked'}">
|
||||
<div class="flex flex-col">
|
||||
<span class="gp-value text-[0.7rem]!">Double Ponte</span>
|
||||
<span class="gp-label">
|
||||
{#if b.doubleChance > 0}
|
||||
{(b.doubleChance * 100).toFixed(0)}% chance de doubler
|
||||
{:else}
|
||||
Branche Ponte — "Double Ponte" (5 ADN)
|
||||
{/if}
|
||||
{b.doubleChance > 0 ? `${(b.doubleChance * 100).toFixed(0)}% chance de doubler` : 'Branche Ponte — 5 ADN'}
|
||||
</span>
|
||||
</div>
|
||||
{#if b.doubleChance > 0}
|
||||
<span class="gp-label gp-accent-purple">{(b.doubleChance * 100).toFixed(0)}%</span>
|
||||
{:else}
|
||||
<span class="gp-label">—</span>
|
||||
{/if}
|
||||
<span class="gp-label {b.doubleChance > 0 ? 'gp-accent-purple' : ''}">{b.doubleChance > 0 ? `${(b.doubleChance * 100).toFixed(0)}%` : '—'}</span>
|
||||
</div>
|
||||
|
||||
<!-- Crit -->
|
||||
<div class="gp-row {b.critChance > 0 ? 'gp-row--unlocked' : 'gp-row--locked'}">
|
||||
<div class="flex flex-col">
|
||||
<span class="gp-value text-[0.7rem]!">Ponte Critique</span>
|
||||
<span class="gp-label">
|
||||
{#if b.critChance > 0}
|
||||
{(b.critChance * 100).toFixed(0)}% chance de x10
|
||||
{:else}
|
||||
Branche Ponte — "Ponte Critique" (20 ADN)
|
||||
{/if}
|
||||
{b.critChance > 0 ? `${(b.critChance * 100).toFixed(0)}% chance de x10` : 'Branche Ponte — 20 ADN'}
|
||||
</span>
|
||||
</div>
|
||||
{#if b.critChance > 0}
|
||||
<span class="gp-label gp-accent-amber">{(b.critChance * 100).toFixed(0)}%</span>
|
||||
{:else}
|
||||
<span class="gp-label">—</span>
|
||||
{/if}
|
||||
<span class="gp-label {b.critChance > 0 ? 'gp-accent-amber' : ''}">{b.critChance > 0 ? `${(b.critChance * 100).toFixed(0)}%` : '—'}</span>
|
||||
</div>
|
||||
|
||||
<!-- Auto-click -->
|
||||
<div class="gp-row {b.autoClicksPerSec > 0 ? 'gp-row--unlocked' : 'gp-row--locked'}">
|
||||
<div class="flex flex-col">
|
||||
<span class="gp-value text-[0.7rem]!">Auto-Ponte</span>
|
||||
<span class="gp-label">
|
||||
{#if b.autoClicksPerSec > 0}
|
||||
{b.autoClicksPerSec.toFixed(1)} clics/s auto → {formatNumber(b.effectivePerSec)}/s
|
||||
{:else}
|
||||
Capstone Ponte — "Ponte Automatique" (200 ADN)
|
||||
{/if}
|
||||
{b.autoClicksPerSec > 0 ? `${b.autoClicksPerSec.toFixed(1)} clics/s → ${formatNumber(b.effectivePerSec)}/s` : 'Capstone Ponte — 200 ADN'}
|
||||
</span>
|
||||
</div>
|
||||
{#if b.autoClicksPerSec > 0}
|
||||
<span class="gp-label gp-accent-green">{formatNumber(b.effectivePerSec)}/s</span>
|
||||
{:else}
|
||||
<span class="gp-label">—</span>
|
||||
{/if}
|
||||
<span class="gp-label {b.autoClicksPerSec > 0 ? 'gp-accent-green' : ''}">{b.autoClicksPerSec > 0 ? `${formatNumber(b.effectivePerSec)}/s` : '—'}</span>
|
||||
</div>
|
||||
|
||||
<!-- Hint -->
|
||||
{#if b.treeMult <= 1}
|
||||
<div class="text-center py-1">
|
||||
<span class="gp-label gp-accent-amber">Depense ton ADN dans la branche Ponte pour booster tes clics</span>
|
||||
</div>
|
||||
{/if}
|
||||
</CollapsiblePanel>
|
||||
|
||||
@@ -86,11 +86,39 @@ export interface RunStats {
|
||||
} | null;
|
||||
}
|
||||
|
||||
// --- Click Upgrades (achetables en têtards, liés aux générateurs) ---
|
||||
|
||||
export interface ClickUpgrade {
|
||||
id: string;
|
||||
name: string;
|
||||
generatorId: string; // lié à quel générateur
|
||||
baseClickPower: number; // bonus clic par niveau
|
||||
baseCost: number; // coût de base
|
||||
level: number; // niveaux achetés
|
||||
}
|
||||
|
||||
export const DEFAULT_CLICK_UPGRADES: ClickUpgrade[] = [
|
||||
{ id: "nid_douillet", name: "Nid Douillet", generatorId: "nid", baseClickPower: 1, baseCost: 50, level: 0 },
|
||||
{ id: "eau_fertile", name: "Eau Fertile", generatorId: "mare", baseClickPower: 3, baseCost: 500, level: 0 },
|
||||
{ id: "spores_actives", name: "Spores Actives", generatorId: "marecage", baseClickPower: 8, baseCost: 5_000, level: 0 },
|
||||
{ id: "courant_vital", name: "Courant Vital", generatorId: "etang", baseClickPower: 20, baseCost: 50_000, level: 0 },
|
||||
{ id: "source_ancestrale", name: "Source Ancestrale", generatorId: "lac", baseClickPower: 50, baseCost: 500_000, level: 0 },
|
||||
];
|
||||
|
||||
export function clickUpgradeCost(upgrade: ClickUpgrade): number {
|
||||
return Math.floor(upgrade.baseCost * Math.pow(1.2, upgrade.level));
|
||||
}
|
||||
|
||||
export function totalClickUpgradePower(clickUpgrades: ClickUpgrade[]): number {
|
||||
return clickUpgrades.reduce((sum, u) => sum + u.baseClickPower * u.level, 0);
|
||||
}
|
||||
|
||||
export interface GameState {
|
||||
saveVersion: number;
|
||||
resources: number;
|
||||
clickMultiplier: number;
|
||||
generators: Generator[];
|
||||
clickUpgrades: ClickUpgrade[];
|
||||
lastTick: number; // timestamp ms — lazy calc reference
|
||||
lastOnline: number; // timestamp ms — dernière activité réelle (tick actif)
|
||||
prestigeCount: number;
|
||||
@@ -618,11 +646,32 @@ export function getGeneratorClickBonus(generators: Generator[]): number {
|
||||
return 1 + typesOwned * 2 + totalOwned * 0.05;
|
||||
}
|
||||
|
||||
// Gain par clic — scaling propre : base × prestige × arbre × generateurs
|
||||
// Gain par clic — base + upgrades, le tout multiplié par prestige × arbre × infra
|
||||
export function getClickGain(state: GameState): number {
|
||||
const treeClickMult = getClickMultiplierFromTree(state.evolutionTree);
|
||||
const genBonus = getGeneratorClickBonus(state.generators);
|
||||
return Math.floor(state.clickMultiplier * state.prestigeMultiplier * treeClickMult * genBonus);
|
||||
const upgradePower = totalClickUpgradePower(state.clickUpgrades ?? []);
|
||||
const base = state.clickMultiplier + upgradePower;
|
||||
return Math.floor(base * state.prestigeMultiplier * treeClickMult * genBonus);
|
||||
}
|
||||
|
||||
// Achat d'un click upgrade (coûte des têtards)
|
||||
export function buyClickUpgrade(state: GameState, upgradeId: string): GameState | null {
|
||||
const idx = (state.clickUpgrades ?? []).findIndex((u) => u.id === upgradeId);
|
||||
if (idx === -1) return null;
|
||||
|
||||
const upgrade = state.clickUpgrades[idx];
|
||||
// Requires owning the linked generator
|
||||
const gen = state.generators.find((g) => g.id === upgrade.generatorId);
|
||||
if (!gen || gen.owned === 0) return null;
|
||||
|
||||
const cost = clickUpgradeCost(upgrade);
|
||||
if (state.resources < cost) return null;
|
||||
|
||||
const updatedUpgrades = [...state.clickUpgrades];
|
||||
updatedUpgrades[idx] = { ...upgrade, level: upgrade.level + 1 };
|
||||
|
||||
return { ...state, resources: state.resources - cost, clickUpgrades: updatedUpgrades };
|
||||
}
|
||||
|
||||
// Breakdown complet du clic (pour affichage cockpit)
|
||||
@@ -641,7 +690,8 @@ export interface ClickBreakdown {
|
||||
}
|
||||
|
||||
export function getClickBreakdown(state: GameState): ClickBreakdown {
|
||||
const base = state.clickMultiplier;
|
||||
const upgradePower = totalClickUpgradePower(state.clickUpgrades ?? []);
|
||||
const base = state.clickMultiplier + upgradePower;
|
||||
const prestigeMult = state.prestigeMultiplier;
|
||||
const treeMult = getClickMultiplierFromTree(state.evolutionTree);
|
||||
const genBonus = getGeneratorClickBonus(state.generators);
|
||||
@@ -817,6 +867,8 @@ export function applyPrestige(state: GameState): GameState {
|
||||
},
|
||||
freeResetAvailable: true, // 1 reset gratuit offert par prestige
|
||||
extraResetsUsed: 0,
|
||||
// Click upgrades reset au prestige (comme les générateurs)
|
||||
clickUpgrades: (state.clickUpgrades ?? DEFAULT_CLICK_UPGRADES).map((u) => ({ ...u, level: 0 })),
|
||||
// evolutionTree persiste — jamais reset
|
||||
};
|
||||
}
|
||||
@@ -835,6 +887,7 @@ export const DEFAULT_STATE: GameState = {
|
||||
resources: 0,
|
||||
clickMultiplier: 1,
|
||||
generators: DEFAULT_GENERATORS,
|
||||
clickUpgrades: DEFAULT_CLICK_UPGRADES,
|
||||
lastTick: Date.now(),
|
||||
lastOnline: Date.now(),
|
||||
prestigeCount: 0,
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
// Chaque sprint ajoute un step (v2→v3, etc.)
|
||||
|
||||
import { CURRENT_SAVE_VERSION } from "./balance";
|
||||
import type { GameState } from "./economy";
|
||||
import { DEFAULT_EVOLUTION_TREE, DEFAULT_GENERATORS } from "./economy";
|
||||
import type { GameState, ClickUpgrade } from "./economy";
|
||||
import { DEFAULT_EVOLUTION_TREE, DEFAULT_GENERATORS, DEFAULT_CLICK_UPGRADES } from "./economy";
|
||||
|
||||
/**
|
||||
* Détecte la version d'une save et applique les migrations nécessaires.
|
||||
@@ -32,6 +32,11 @@ export function migrateSave(raw: Record<string, unknown>): GameState {
|
||||
state.generators as Array<Record<string, unknown>> | undefined
|
||||
);
|
||||
|
||||
// Click upgrades — merge with defaults (preserves levels, adds new upgrades)
|
||||
state.clickUpgrades = mergeClickUpgrades(
|
||||
state.clickUpgrades as Array<Record<string, unknown>> | undefined
|
||||
);
|
||||
|
||||
return state as unknown as GameState;
|
||||
}
|
||||
|
||||
@@ -149,3 +154,25 @@ function mergeGenerators(
|
||||
return { ...defaultGen };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge les click upgrades sauvegardés avec DEFAULT_CLICK_UPGRADES.
|
||||
* Conserve le level, met à jour les stats de base.
|
||||
*/
|
||||
function mergeClickUpgrades(
|
||||
saved: Array<Record<string, unknown>> | undefined
|
||||
): ClickUpgrade[] {
|
||||
if (!saved || !Array.isArray(saved)) {
|
||||
return DEFAULT_CLICK_UPGRADES.map((u) => ({ ...u }));
|
||||
}
|
||||
|
||||
const savedById = new Map(saved.map((u) => [u.id as string, u]));
|
||||
|
||||
return DEFAULT_CLICK_UPGRADES.map((def) => {
|
||||
const s = savedById.get(def.id);
|
||||
if (s) {
|
||||
return { ...def, level: typeof s.level === "number" ? s.level : 0 };
|
||||
}
|
||||
return { ...def };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
totalProductionPerSecond,
|
||||
generatorCost as genCost,
|
||||
computeOfflineGains,
|
||||
buyClickUpgrade as buyClickUpgradeFn,
|
||||
} from '$lib/core/economy';
|
||||
import { migrateSave } from '$lib/core/migrateSave';
|
||||
import { toast } from './toast.svelte';
|
||||
@@ -133,6 +134,12 @@ class Game {
|
||||
if (updated) this.applyState(updated);
|
||||
}
|
||||
|
||||
buyClickUpgrade(upgradeId: string) {
|
||||
if (!this.ready) return;
|
||||
const updated = buyClickUpgradeFn(this.state, upgradeId);
|
||||
if (updated) this.applyState(updated);
|
||||
}
|
||||
|
||||
buyNode(nodeId: string) {
|
||||
if (!this.ready) return;
|
||||
const updated = buyEvolutionNode(this.state, nodeId);
|
||||
|
||||
Reference in New Issue
Block a user