feat: arbre d'évolution 3 voies — ponte/marais/adaptation

18 nœuds (6/branche), nœuds exclusifs (pick one), reset gratuit.
Nouveaux effets : double_click, auto_click, crit, generator_boost,
cost_reduction, prestige_dna_bonus, offline_boost, threshold_reduction.
UI 3 colonnes colorées, seuil prestige dynamique, coût réduit.
75 tests (tous passent).
This commit is contained in:
2026-03-28 11:52:51 +01:00
parent 3ba10dad5f
commit ae50908bc9
7 changed files with 405 additions and 76 deletions

View File

@@ -9,16 +9,33 @@ export interface Generator {
owned: number;
}
export type EffectType = "click_multiplier" | "production_multiplier" | "start_bonus" | "unlock_generator" | "achievement_scaling";
export type EffectType =
| "click_multiplier"
| "production_multiplier"
| "start_bonus"
| "unlock_generator"
| "achievement_scaling"
| "double_click_chance"
| "auto_click"
| "crit_click_chance"
| "generator_boost"
| "cost_reduction"
| "prestige_dna_bonus"
| "offline_boost"
| "prestige_threshold_reduction";
export type Branch = "ponte" | "marais" | "adaptation";
export interface EvolutionNode {
id: string;
name: string;
cost: number; // en ADN Ancestral
cost: number; // en ADN Ancestral
effect: EffectType;
value: number;
unlocked: boolean;
requires: string | null; // id du nœud prérequis (null = racine)
requires: string | null; // id du nœud prérequis (null = racine)
branch: Branch;
exclusive_with?: string; // id du nœud alternatif (pick one)
}
export interface GameState {
@@ -37,11 +54,29 @@ export interface GameState {
// --- Arbre d'Évolution ---
export const DEFAULT_EVOLUTION_TREE: EvolutionNode[] = [
{ id: "ponte_amelioree", name: "Ponte Améliorée", cost: 1, effect: "click_multiplier", value: 2, unlocked: false, requires: null },
{ id: "instinct_gregaire", name: "Instinct Grégaire", cost: 3, effect: "production_multiplier", value: 1.5, unlocked: false, requires: "ponte_amelioree" },
{ id: "memoire_genetique", name: "Mémoire Génétique", cost: 10, effect: "start_bonus", value: 100, unlocked: false, requires: "instinct_gregaire" },
{ id: "mutation_alpha", name: "Mutation Alpha", cost: 25, effect: "unlock_generator", value: 0, unlocked: false, requires: "memoire_genetique" },
{ id: "symbiose", name: "Symbiose", cost: 50, effect: "achievement_scaling", value: 0.01, unlocked: false, requires: "mutation_alpha" },
// --- Ponte (click) ---
{ id: "ponte_amelioree", name: "Ponte Améliorée", cost: 1, effect: "click_multiplier", value: 2, unlocked: false, requires: null, branch: "ponte" },
{ id: "double_ponte", name: "Double Ponte", cost: 3, effect: "double_click_chance", value: 0.10, unlocked: false, requires: "ponte_amelioree", branch: "ponte" },
{ id: "ponte_frenetique", name: "Ponte Frénétique", cost: 8, effect: "click_multiplier", value: 3, unlocked: false, requires: "double_ponte", branch: "ponte", exclusive_with: "auto_ponte" },
{ id: "auto_ponte", name: "Auto-Ponte", cost: 8, effect: "auto_click", value: 1, unlocked: false, requires: "double_ponte", branch: "ponte", exclusive_with: "ponte_frenetique" },
{ id: "ponte_critique", name: "Ponte Critique", cost: 20, effect: "crit_click_chance", value: 0.05, unlocked: false, requires: "ponte_frenetique", branch: "ponte" },
{ id: "maitre_pondeur", name: "Maître Pondeur", cost: 40, effect: "click_multiplier", value: 5, unlocked: false, requires: "ponte_critique", branch: "ponte" },
// --- Marais (production) ---
{ id: "instinct_gregaire", name: "Instinct Grégaire", cost: 1, effect: "production_multiplier", value: 1.5, unlocked: false, requires: null, branch: "marais" },
{ id: "symbiose_algale", name: "Symbiose Algale", cost: 3, effect: "generator_boost", value: 2, unlocked: false, requires: "instinct_gregaire", branch: "marais" },
{ id: "courant_profond", name: "Courant Profond", cost: 8, effect: "production_multiplier", value: 2, unlocked: false, requires: "symbiose_algale", branch: "marais", exclusive_with: "maree_haute" },
{ id: "maree_haute", name: "Marée Haute", cost: 8, effect: "cost_reduction", value: 0.20, unlocked: false, requires: "symbiose_algale", branch: "marais", exclusive_with: "courant_profond" },
{ id: "ecosysteme_mature", name: "Écosystème Mature", cost: 20, effect: "production_multiplier", value: 3, unlocked: false, requires: "courant_profond", branch: "marais" },
{ id: "marais_eternel", name: "Marais Éternel", cost: 40, effect: "production_multiplier", value: 5, unlocked: false, requires: "ecosysteme_mature", branch: "marais" },
// --- Adaptation (utility) ---
{ id: "memoire_genetique", name: "Mémoire Génétique", cost: 1, effect: "start_bonus", value: 100, unlocked: false, requires: null, branch: "adaptation" },
{ id: "adn_renforce", name: "ADN Renforcé", cost: 3, effect: "prestige_dna_bonus", value: 0.25, unlocked: false, requires: "memoire_genetique", branch: "adaptation" },
{ id: "eveil_rapide", name: "Éveil Rapide", cost: 8, effect: "offline_boost", value: 0.50, unlocked: false, requires: "adn_renforce", branch: "adaptation", exclusive_with: "resilience" },
{ id: "resilience", name: "Résilience", cost: 8, effect: "unlock_generator", value: 0, unlocked: false, requires: "adn_renforce", branch: "adaptation", exclusive_with: "eveil_rapide" },
{ id: "heritage", name: "Héritage", cost: 20, effect: "prestige_dna_bonus", value: 0.50, unlocked: false, requires: "eveil_rapide", branch: "adaptation" },
{ id: "transcendance", name: "Transcendance", cost: 40, effect: "prestige_threshold_reduction", value: 0.50, unlocked: false, requires: "heritage", branch: "adaptation" },
];
// Calcule l'ADN gagné lors d'un prestige : floor(150 × sqrt(lifetime / 1e9))
@@ -58,6 +93,11 @@ export function canBuyEvolutionNode(state: GameState, nodeId: string): boolean {
const prereq = state.evolutionTree.find((n) => n.id === node.requires);
if (!prereq || !prereq.unlocked) return false;
}
// Exclusive node: can't buy if the alternative is already unlocked
if (node.exclusive_with) {
const alt = state.evolutionTree.find((n) => n.id === node.exclusive_with);
if (alt && alt.unlocked) return false;
}
return true;
}
@@ -75,6 +115,24 @@ export function buyEvolutionNode(state: GameState, nodeId: string): GameState |
};
}
// Reset l'arbre — rembourse tout l'ADN dépensé, relock tous les nœuds
export function resetEvolutionTree(state: GameState): GameState {
const spentDna = state.evolutionTree
.filter((n) => n.unlocked)
.reduce((sum, n) => sum + n.cost, 0);
return {
...state,
ancestralDna: state.ancestralDna + spentDna,
evolutionTree: state.evolutionTree.map((n) => ({ ...n, unlocked: false })),
};
}
// Compte l'ADN total investi dans l'arbre
export function getSpentDna(tree: EvolutionNode[]): number {
return tree.filter((n) => n.unlocked).reduce((sum, n) => sum + n.cost, 0);
}
// Calcule le multiplicateur click total depuis l'arbre
export function getClickMultiplierFromTree(tree: EvolutionNode[]): number {
return tree
@@ -96,6 +154,62 @@ export function getStartBonusFromTree(tree: EvolutionNode[]): number {
.reduce((sum, n) => sum + n.value, 0);
}
// Chance de double click (0-1)
export function getDoubleClickChance(tree: EvolutionNode[]): number {
return tree
.filter((n) => n.unlocked && n.effect === "double_click_chance")
.reduce((sum, n) => sum + n.value, 0);
}
// Auto-clicks par seconde depuis l'arbre
export function getAutoClicksPerSecond(tree: EvolutionNode[]): number {
return tree
.filter((n) => n.unlocked && n.effect === "auto_click")
.reduce((sum, n) => sum + n.value, 0);
}
// Chance de crit click (0-1), crit = x10
export function getCritClickChance(tree: EvolutionNode[]): number {
return tree
.filter((n) => n.unlocked && n.effect === "crit_click_chance")
.reduce((sum, n) => sum + n.value, 0);
}
// Multiplicateur boost sur Nid (generator_boost)
export function getGeneratorBoostFromTree(tree: EvolutionNode[]): number {
return tree
.filter((n) => n.unlocked && n.effect === "generator_boost")
.reduce((mult, n) => mult * n.value, 1);
}
// Réduction de coût générateurs (0-1)
export function getCostReduction(tree: EvolutionNode[]): number {
return tree
.filter((n) => n.unlocked && n.effect === "cost_reduction")
.reduce((sum, n) => sum + n.value, 0);
}
// Bonus ADN prestige (additif, ex: 0.25 = +25%)
export function getPrestigeDnaBonus(tree: EvolutionNode[]): number {
return tree
.filter((n) => n.unlocked && n.effect === "prestige_dna_bonus")
.reduce((sum, n) => sum + n.value, 0);
}
// Boost offline (additif, ex: 0.50 = +50% efficacité offline)
export function getOfflineBoost(tree: EvolutionNode[]): number {
return tree
.filter((n) => n.unlocked && n.effect === "offline_boost")
.reduce((sum, n) => sum + n.value, 0);
}
// Réduction seuil prestige (multiplicatif, ex: 0.50 = seuil divisé par 2)
export function getPrestigeThresholdReduction(tree: EvolutionNode[]): number {
return tree
.filter((n) => n.unlocked && n.effect === "prestige_threshold_reduction")
.reduce((sum, n) => sum + n.value, 0);
}
// --- Offline gains (courbe inversée) ---
const OFFLINE_THRESHOLD = 60_000; // 60s — en-dessous = idle normal, au-dessus = offline
@@ -131,6 +245,8 @@ export function computeOfflineGains(state: GameState, now: number): number {
const pps = totalProductionPerSecond(state);
if (pps <= 0) return 0;
const offlineBoost = 1 + getOfflineBoost(state.evolutionTree);
// Intégration par tranches de 60s
const STEP = 60_000;
let total = 0;
@@ -139,20 +255,27 @@ export function computeOfflineGains(state: GameState, now: number): number {
const eff = offlineEfficiency(t + chunk / 2); // milieu de la tranche
total += pps * (chunk / 1000) * eff;
}
return total;
return total * offlineBoost;
}
// --- Core economy (mis à jour pour intégrer l'arbre) ---
// Coût d'achat du N-ième générateur : baseCost × 1.15^owned
export function generatorCost(gen: Generator): number {
return Math.floor(gen.baseCost * Math.pow(1.15, gen.owned));
// Coût d'achat du N-ième générateur : baseCost × 1.15^owned × (1 - costReduction)
export function generatorCost(gen: Generator, tree?: EvolutionNode[]): number {
const base = Math.floor(gen.baseCost * Math.pow(1.15, gen.owned));
if (!tree) return base;
const reduction = getCostReduction(tree);
return Math.max(1, Math.floor(base * (1 - reduction)));
}
// Production totale par seconde de tous les générateurs
export function totalProductionPerSecond(state: GameState): number {
const nidBoost = getGeneratorBoostFromTree(state.evolutionTree);
const base = state.generators.reduce(
(sum, gen) => sum + gen.baseProduction * gen.owned,
(sum, gen) => {
const boost = gen.id === "nid" ? nidBoost : 1;
return sum + gen.baseProduction * gen.owned * boost;
},
0
);
const treeMultiplier = getProductionMultiplierFromTree(state.evolutionTree);
@@ -198,7 +321,7 @@ export function buyGenerator(state: GameState, genId: string): GameState | null
if (genIndex === -1) return null;
const gen = state.generators[genIndex];
const cost = generatorCost(gen);
const cost = generatorCost(gen, state.evolutionTree);
if (state.resources < cost) return null;
const updatedGenerators = [...state.generators];
@@ -212,13 +335,22 @@ export function buyGenerator(state: GameState, genId: string): GameState | null
}
// Prestige : reset run, gain ADN, arbre persiste
const BASE_PRESTIGE_THRESHOLD = 1_000_000;
export function getPrestigeThreshold(state: GameState): number {
const reduction = getPrestigeThresholdReduction(state.evolutionTree);
return Math.floor(BASE_PRESTIGE_THRESHOLD * (1 - reduction));
}
export function canPrestige(state: GameState): boolean {
return state.resources >= 1_000_000;
return state.resources >= getPrestigeThreshold(state);
}
export function applyPrestige(state: GameState): GameState {
const newPrestigeCount = state.prestigeCount + 1;
const dnaGained = computePrestigeDna(state.lifetimeTadpoles);
const dnaBonus = getPrestigeDnaBonus(state.evolutionTree);
const baseDna = computePrestigeDna(state.lifetimeTadpoles);
const dnaGained = Math.floor(baseDna * (1 + dnaBonus));
const startBonus = getStartBonusFromTree(state.evolutionTree);
return {