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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user