// economy.ts — Core clicker logic (lazy calculation pattern) // Jamais de timer actif : tout est calculé au read depuis lastTick import { PRESTIGE_ADN_BASE, PRESTIGE_ADN_THRESHOLD, PRESTIGE_BONUS_PER_PRESTIGE, PRESTIGE_BONUS_CAP, PRESTIGE_ADN_MIN, BASE_PRESTIGE_THRESHOLD, OFFLINE_THRESHOLD_MS as OFFLINE_THRESHOLD, OFFLINE_FULL_MS, OFFLINE_DECAY_END_MS, OFFLINE_ZERO_MS, OFFLINE_FLOOR, CURRENT_SAVE_VERSION, treeResetCost, postCapstoneCost, } from "./balance"; import { PRESTIGE_MILESTONES } from "../data/prestigeMilestones"; import type { PrestigeMilestone } from "../data/prestigeMilestones"; export interface Generator { id: string; name: string; baseCost: number; baseProduction: number; // ressource/s owned: number; } export type EffectType = | "click_multiplier" | "production_multiplier" | "start_bonus" | "unlock_generator" | "double_click_chance" | "auto_click" | "crit_click_chance" | "generator_boost" | "cost_reduction" | "prestige_dna_bonus" | "offline_boost" | "prestige_threshold_reduction" // Sprint 3 — capstones | "auto_click_scaling" // Ponte Auto — auto-click scale avec upgrades | "generator_synergy" // Symbiose Totale — +X% par type possédé | "offline_cap_boost" // Mémoire du Marais — offline cap + durée // Sprint 3 — Convergence | "all_effects_boost" // +X% à tous les effets | "post_capstone_discount"; // -X% coût post-capstones export type Branch = "ponte" | "marais" | "adaptation" | "cross"; export interface EvolutionNode { id: string; name: string; cost: number; // en ADN Ancestral (base cost for repeatables) effect: EffectType; value: number; unlocked: boolean; requires: string | null; // id du nœud prérequis (null = racine) branch: Branch; exclusive_with?: string; // id du nœud alternatif (pick one) // Sprint 3 — capstone & repeatable capstone?: boolean; // nœud capstone (bordure dorée, game-changer) repeatable?: boolean; // post-capstone achetable en boucle purchased?: number; // nombre d'achats pour les repeatables // Sprint 3 — Convergence (nœud évolutif) tier?: number; // tier actuel (1 = Alpha, 2 = Omega) maxTier?: number; // tier max tierUpgradeCost?: number; // coût upgrade au tier suivant tierUpgradeRequires?: string; // condition pour upgrade ("2_capstones") } export interface CosmeticSlotMap { [slot: string]: string | undefined; } export interface RunStats { startedAt: number; // timestamp ms début de la run tadpolesProduced: number; // têtards produits cette run (tracking granulaire) bestRun: { duration: number; // ms tadpoles: number; adn: number; } | null; } export interface GameState { saveVersion: number; resources: number; clickMultiplier: number; generators: Generator[]; lastTick: number; // timestamp ms — lazy calc reference lastOnline: number; // timestamp ms — dernière activité réelle (tick actif) prestigeCount: number; prestigeMultiplier: number; // legacy — conservé pour compat, remplacé par arbre ancestralDna: number; evolutionTree: EvolutionNode[]; lifetimeTadpoles: number; // total cumulé de la run (pour calcul ADN) cosmeticInventory: string[]; // ids des cosmétiques débloqués cosmeticEquipped: CosmeticSlotMap; // slot → cosmetic id // Sprint 3 runStats: RunStats; freeResetAvailable: boolean; // 1 gratuit par prestige extraResetsUsed: number; // resets payants dans la génération courante claimedMilestones: string[]; // IDs des milestones réclamés } // --- Arbre d'Évolution --- export const DEFAULT_EVOLUTION_TREE: EvolutionNode[] = [ // ═══ PONTE (click) — 10 nœuds ═══ // Tier 1 { id: "ponte_amelioree", name: "Ponte Améliorée", cost: 1, effect: "click_multiplier", value: 2, unlocked: false, requires: null, branch: "ponte" }, // Tier 2 { id: "double_ponte", name: "Double Ponte", cost: 5, effect: "double_click_chance", value: 0.10, unlocked: false, requires: "ponte_amelioree", branch: "ponte" }, // Tier 3 (exclusif) { id: "ponte_frenetique", name: "Frénésie", cost: 15, effect: "click_multiplier", value: 3, unlocked: false, requires: "double_ponte", branch: "ponte", exclusive_with: "concentration" }, { id: "concentration", name: "Concentration", cost: 15, effect: "click_multiplier", value: 4, unlocked: false, requires: "double_ponte", branch: "ponte", exclusive_with: "ponte_frenetique" }, // Tier 3 (parallèle) { id: "ponte_critique", name: "Ponte Critique", cost: 20, effect: "crit_click_chance", value: 0.05, unlocked: false, requires: "double_ponte", branch: "ponte" }, // Tier 4 { id: "maitre_pondeur", name: "Maître Pondeur", cost: 40, effect: "click_multiplier", value: 5, unlocked: false, requires: "ponte_critique", branch: "ponte" }, // Capstone { id: "ponte_auto", name: "Ponte Automatique", cost: 200, effect: "auto_click_scaling", value: 1, unlocked: false, requires: "maitre_pondeur", branch: "ponte", capstone: true }, // Post-capstone (repeatable) { id: "ponte_post", name: "+5% auto-ponte", cost: 500, effect: "auto_click", value: 0.05, unlocked: false, requires: "ponte_auto", branch: "ponte", repeatable: true, purchased: 0 }, // ═══ MARAIS (production) — 10 nœuds ═══ // Tier 1 { id: "instinct_gregaire", name: "Instinct Grégaire", cost: 3, effect: "production_multiplier", value: 1.5, unlocked: false, requires: null, branch: "marais" }, // Tier 2 { id: "symbiose_algale", name: "Symbiose Algale", cost: 8, effect: "generator_boost", value: 2, unlocked: false, requires: "instinct_gregaire", branch: "marais" }, // Tier 3 (exclusif) { id: "courant_profond", name: "Courant Profond", cost: 25, effect: "production_multiplier", value: 2, unlocked: false, requires: "symbiose_algale", branch: "marais", exclusive_with: "maree_haute" }, { id: "maree_haute", name: "Marée Haute", cost: 25, effect: "cost_reduction", value: 0.20, unlocked: false, requires: "symbiose_algale", branch: "marais", exclusive_with: "courant_profond" }, // Tier 3 (parallèle) { id: "ecosysteme_mature", name: "Écosystème Mature", cost: 25, effect: "production_multiplier", value: 3, unlocked: false, requires: "symbiose_algale", branch: "marais" }, // Tier 4 { id: "marais_eternel", name: "Marais Éternel", cost: 60, effect: "production_multiplier", value: 5, unlocked: false, requires: "ecosysteme_mature", branch: "marais" }, // Capstone { id: "symbiose_totale", name: "Symbiose Totale", cost: 300, effect: "generator_synergy", value: 0.02, unlocked: false, requires: "marais_eternel", branch: "marais", capstone: true }, // Post-capstone (repeatable) { id: "marais_post", name: "+1% synergie", cost: 600, effect: "generator_synergy", value: 0.01, unlocked: false, requires: "symbiose_totale", branch: "marais", repeatable: true, purchased: 0 }, // ═══ ADAPTATION (utility) — 10 nœuds ═══ // Tier 1 { id: "memoire_genetique", name: "Mémoire Génétique", cost: 2, effect: "start_bonus", value: 100, unlocked: false, requires: null, branch: "adaptation" }, // Tier 2 { id: "adn_renforce", name: "ADN Renforcé", cost: 10, effect: "prestige_dna_bonus", value: 0.25, unlocked: false, requires: "memoire_genetique", branch: "adaptation" }, // Tier 3 (exclusif) { id: "eveil_rapide", name: "Éveil Rapide", cost: 30, effect: "offline_boost", value: 0.50, unlocked: false, requires: "adn_renforce", branch: "adaptation", exclusive_with: "mutation_adn" }, { id: "mutation_adn", name: "Mutation ADN", cost: 30, effect: "prestige_dna_bonus", value: 0.25, unlocked: false, requires: "adn_renforce", branch: "adaptation", exclusive_with: "eveil_rapide" }, // Tier 3 (parallèle) { id: "heritage", name: "Héritage", cost: 30, effect: "prestige_dna_bonus", value: 0.50, unlocked: false, requires: "adn_renforce", branch: "adaptation" }, // Tier 4 { id: "transcendance", name: "Transcendance", cost: 60, effect: "prestige_threshold_reduction", value: 0.50, unlocked: false, requires: "heritage", branch: "adaptation" }, // Capstone { id: "memoire_marais", name: "Mémoire du Marais", cost: 250, effect: "offline_cap_boost", value: 0.75, unlocked: false, requires: "transcendance", branch: "adaptation", capstone: true }, // Post-capstone (repeatable) { id: "adapt_post", name: "+2% offline cap", cost: 500, effect: "offline_boost", value: 0.02, unlocked: false, requires: "memoire_marais", branch: "adaptation", repeatable: true, purchased: 0 }, // ═══ CROSS-BRANCHE — Convergence (nœud évolutif) ═══ { id: "convergence", name: "Convergence", cost: 500, effect: "all_effects_boost", value: 0.10, unlocked: false, requires: null, branch: "cross", tier: 1, maxTier: 2, tierUpgradeCost: 500, tierUpgradeRequires: "2_capstones" }, ]; // Formule ADN Sprint 3 : max(1, floor(base × log10(t / threshold) × (1 + bonus))) // Clamp min 1 si seuil atteint, cap bonus ×4 à 80 prestiges export function computePrestigeDna(lifetimeTadpoles: number, prestigeCount: number = 0): number { if (lifetimeTadpoles < PRESTIGE_ADN_THRESHOLD) return 0; const ratio = lifetimeTadpoles / PRESTIGE_ADN_THRESHOLD; if (ratio <= 1) return PRESTIGE_ADN_MIN; const bonus = Math.min(PRESTIGE_BONUS_PER_PRESTIGE * prestigeCount, PRESTIGE_BONUS_CAP); const raw = PRESTIGE_ADN_BASE * Math.log10(ratio) * (1 + bonus); return Math.max(PRESTIGE_ADN_MIN, Math.floor(raw)); } // --- Milestones prestige --- // Milestones disponibles mais pas encore réclamés export function getClaimableMilestones(state: GameState): PrestigeMilestone[] { const claimed = state.claimedMilestones ?? []; return PRESTIGE_MILESTONES.filter( (m) => state.prestigeCount >= m.threshold && !claimed.includes(m.id) ); } // Prochain milestone non atteint export function getNextMilestone(state: GameState): PrestigeMilestone | null { return PRESTIGE_MILESTONES.find((m) => state.prestigeCount < m.threshold) ?? null; } // Réclamer un milestone export function claimMilestone(state: GameState, milestoneId: string): GameState | null { const milestone = PRESTIGE_MILESTONES.find((m) => m.id === milestoneId); if (!milestone) return null; if (state.prestigeCount < milestone.threshold) return null; const claimed = state.claimedMilestones ?? []; if (claimed.includes(milestoneId)) return null; let newState = { ...state, claimedMilestones: [...claimed, milestoneId], }; // Appliquer la récompense if (milestone.reward.type === "cosmetic") { if (!newState.cosmeticInventory.includes(milestone.reward.cosmeticId)) { newState = { ...newState, cosmeticInventory: [...newState.cosmeticInventory, milestone.reward.cosmeticId], }; } } // Les bonus gameplay sont appliqués passivement via getMilestoneBonus() return newState; } // Bonus gameplay cumulés depuis les milestones réclamés export function getMilestoneStartNid(state: GameState): number { const claimed = state.claimedMilestones ?? []; if (claimed.includes("milestone_5")) return 1; // 1 Nid gratuit return 0; } export function getMilestoneOfflineBonus(state: GameState): number { const claimed = state.claimedMilestones ?? []; if (claimed.includes("milestone_15")) return 0.05; // +5% offline cap return 0; } // Compte les capstones débloqués export function getUnlockedCapstoneCount(tree: EvolutionNode[]): number { return tree.filter((n) => n.capstone && n.unlocked).length; } // Coût actuel d'un nœud repeatable (scaling par tranche via balance.ts) export function getRepeatableCost(node: EvolutionNode): number { if (!node.repeatable) return node.cost; return postCapstoneCost(node.cost, node.purchased ?? 0); } // Vérifie si le joueur peut acheter Convergence (condition spéciale) function canBuyConvergence(state: GameState, node: EvolutionNode): boolean { // Tier 1 : 1 capstone + au moins 1 nœud tier 3 d'une 2e branche if (!node.unlocked && (node.tier ?? 1) === 1) { const capstones = getUnlockedCapstoneCount(state.evolutionTree); if (capstones < 1) return false; // Check: au moins 1 nœud dans une branche différente de la capstone const capstoneBranches = new Set( state.evolutionTree.filter((n) => n.capstone && n.unlocked).map((n) => n.branch) ); const otherBranchNodes = state.evolutionTree.filter( (n) => n.unlocked && !capstoneBranches.has(n.branch) && n.branch !== "cross" && n.cost >= 15 ); return otherBranchNodes.length > 0 && state.ancestralDna >= node.cost; } return false; } // Vérifie si Convergence peut être upgradé au tier suivant export function canUpgradeConvergence(state: GameState): boolean { const conv = state.evolutionTree.find((n) => n.id === "convergence"); if (!conv || !conv.unlocked) return false; if ((conv.tier ?? 1) >= (conv.maxTier ?? 2)) return false; if (conv.tierUpgradeRequires === "2_capstones" && getUnlockedCapstoneCount(state.evolutionTree) < 2) return false; return state.ancestralDna >= (conv.tierUpgradeCost ?? 500); } // Upgrade Convergence au tier suivant export function upgradeConvergence(state: GameState): GameState | null { if (!canUpgradeConvergence(state)) return null; const conv = state.evolutionTree.find((n) => n.id === "convergence")!; const cost = conv.tierUpgradeCost ?? 500; return { ...state, ancestralDna: state.ancestralDna - cost, evolutionTree: state.evolutionTree.map((n) => n.id === "convergence" ? { ...n, tier: (n.tier ?? 1) + 1, effect: "post_capstone_discount" as EffectType, value: 0.20 } : n ), }; } // Vérifie si un nœud peut être acheté export function canBuyEvolutionNode(state: GameState, nodeId: string): boolean { const node = state.evolutionTree.find((n) => n.id === nodeId); if (!node) return false; // Convergence a sa propre logique if (node.id === "convergence") return canBuyConvergence(state, node); // Repeatable : toujours achetable si unlocked + prérequis + assez d'ADN if (node.repeatable && node.unlocked) { const cost = getRepeatableCost(node); return state.ancestralDna >= cost; } if (node.unlocked) return false; const cost = node.repeatable ? getRepeatableCost(node) : node.cost; if (state.ancestralDna < cost) return false; if (node.requires) { 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; } // Achète un nœud d'évolution (retourne null si impossible) export function buyEvolutionNode(state: GameState, nodeId: string): GameState | null { if (!canBuyEvolutionNode(state, nodeId)) return null; const node = state.evolutionTree.find((n) => n.id === nodeId)!; // Repeatable node — already unlocked, increment purchased if (node.repeatable && node.unlocked) { const cost = getRepeatableCost(node); return { ...state, ancestralDna: state.ancestralDna - cost, evolutionTree: state.evolutionTree.map((n) => n.id === nodeId ? { ...n, purchased: (n.purchased ?? 0) + 1 } : n ), }; } const cost = node.repeatable ? getRepeatableCost(node) : node.cost; return { ...state, ancestralDna: state.ancestralDna - cost, evolutionTree: state.evolutionTree.map((n) => n.id === nodeId ? { ...n, unlocked: true } : n ), }; } // Coût du prochain reset arbre (pour affichage UI) export function getTreeResetCost(state: GameState): number { return treeResetCost(state.freeResetAvailable, state.extraResetsUsed); } // Vérifie si le joueur peut reset l'arbre export function canResetTree(state: GameState): boolean { if (state.prestigeCount < 1) return false; const cost = getTreeResetCost(state); return state.ancestralDna >= cost; } // Reset l'arbre — rembourse l'ADN dépensé (y compris repeatables), déduit le coût du reset export function resetEvolutionTree(state: GameState): GameState { const cost = getTreeResetCost(state); if (state.ancestralDna < cost) return state; const spentDna = getSpentDna(state.evolutionTree); return { ...state, ancestralDna: state.ancestralDna + spentDna - cost, evolutionTree: state.evolutionTree.map((n) => ({ ...n, unlocked: false, purchased: n.repeatable ? 0 : n.purchased, tier: n.maxTier ? 1 : n.tier, })), freeResetAvailable: state.freeResetAvailable ? false : state.freeResetAvailable, extraResetsUsed: state.freeResetAvailable ? state.extraResetsUsed : state.extraResetsUsed + 1, }; } // Compte l'ADN total investi dans l'arbre (standard + repeatables + convergence upgrades) export function getSpentDna(tree: EvolutionNode[]): number { let total = 0; for (const n of tree) { if (!n.unlocked) continue; total += n.cost; // coût initial // Repeatables : somme des coûts de chaque achat if (n.repeatable && (n.purchased ?? 0) > 0) { for (let i = 0; i < n.purchased!; i++) { total += postCapstoneCost(n.cost, i); } } // Convergence tier upgrades if (n.maxTier && (n.tier ?? 1) > 1) { total += (n.tierUpgradeCost ?? 0) * ((n.tier ?? 1) - 1); } } return total; } // Calcule le multiplicateur click total depuis l'arbre export function getClickMultiplierFromTree(tree: EvolutionNode[]): number { return tree .filter((n) => n.unlocked && n.effect === "click_multiplier") .reduce((mult, n) => mult * n.value, 1); } // Calcule le multiplicateur production total depuis l'arbre export function getProductionMultiplierFromTree(tree: EvolutionNode[]): number { return tree .filter((n) => n.unlocked && n.effect === "production_multiplier") .reduce((mult, n) => mult * n.value, 1); } // Bonus de départ (têtards offerts au début de chaque run) export function getStartBonusFromTree(tree: EvolutionNode[]): number { return tree .filter((n) => n.unlocked && n.effect === "start_bonus") .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 (standard + capstone scaling) export function getAutoClicksPerSecond(tree: EvolutionNode[]): number { const standard = tree .filter((n) => n.unlocked && n.effect === "auto_click" && !n.repeatable) .reduce((sum, n) => sum + n.value, 0); const scaling = getAutoClickScaling(tree); return standard + scaling; } // 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); } // --- Sprint 3 — Nouveaux effets --- // Ponte Automatique (capstone) : 1 auto-click/s de base, scale avec les repeatables export function getAutoClickScaling(tree: EvolutionNode[]): number { const capstone = tree.find((n) => n.id === "ponte_auto" && n.unlocked); if (!capstone) return 0; const baseAutoClick = capstone.value; // 1/s // Post-capstone adds flat auto-click value per purchase const postNode = tree.find((n) => n.id === "ponte_post" && n.unlocked); const postBonus = postNode ? postNode.value * (postNode.purchased ?? 0) : 0; return baseAutoClick + postBonus; } // Symbiose Totale (capstone) : +X% par type de générateur possédé // Retourne le multiplicateur (ex: 5 types × 0.02 = 0.10 → ×1.10) export function getGeneratorSynergyMultiplier(tree: EvolutionNode[], generators: Generator[]): number { const synergyNodes = tree.filter((n) => n.unlocked && n.effect === "generator_synergy"); if (synergyNodes.length === 0) return 1; const totalSynergyRate = synergyNodes.reduce((sum, n) => { // For repeatables, each purchase adds to the rate const extra = n.repeatable ? n.value * (n.purchased ?? 0) : 0; return sum + n.value + extra; }, 0); const typesOwned = generators.filter((g) => g.owned > 0).length; return 1 + totalSynergyRate * typesOwned; } // Convergence : all_effects_boost — multiplicateur global sur tous les effets de l'arbre export function getAllEffectsBoost(tree: EvolutionNode[]): number { const conv = tree.find((n) => n.id === "convergence" && n.unlocked); if (!conv) return 1; return 1 + conv.value; // 0.10 = ×1.10 } // Convergence Omega : post_capstone_discount export function getPostCapstoneDiscount(tree: EvolutionNode[]): number { const conv = tree.find((n) => n.id === "convergence" && n.unlocked && n.effect === "post_capstone_discount"); if (!conv) return 0; return conv.value; // 0.20 = -20% } // --- Offline gains (courbe inversée) --- // Retourne le multiplicateur d'efficacité offline (1.0 → 0.0) // basé sur le temps d'absence en ms export function offlineEfficiency(elapsedMs: number): number { if (elapsedMs <= OFFLINE_THRESHOLD) return 1; // pas offline if (elapsedMs <= OFFLINE_FULL_MS) return 1; // 0-15min : 100% if (elapsedMs <= OFFLINE_DECAY_END_MS) { // 15min-1h : linéaire 1.0 → 0.25 const t = (elapsedMs - OFFLINE_FULL_MS) / (OFFLINE_DECAY_END_MS - OFFLINE_FULL_MS); return 1 - t * (1 - OFFLINE_FLOOR); } if (elapsedMs <= OFFLINE_ZERO_MS) { // 1h-2h : linéaire 0.25 → 0.0 const t = (elapsedMs - OFFLINE_DECAY_END_MS) / (OFFLINE_ZERO_MS - OFFLINE_DECAY_END_MS); return OFFLINE_FLOOR * (1 - t); } return 0; // >2h : rien } // Calcule les gains offline avec la courbe dégressive // Intègre la courbe par tranches de 1 minute pour plus de précision export function computeOfflineGains(state: GameState, now: number): number { const elapsed = now - state.lastTick; if (elapsed <= OFFLINE_THRESHOLD) return computeIdleGains(state, now); const pps = totalProductionPerSecond(state); if (pps <= 0) return 0; const offlineBoost = 1 + getOfflineBoost(state.evolutionTree) + getMilestoneOfflineBonus(state); // Intégration par tranches de 60s const STEP = 60_000; let total = 0; for (let t = 0; t < elapsed; t += STEP) { const chunk = Math.min(STEP, elapsed - t); const eff = offlineEfficiency(t + chunk / 2); // milieu de la tranche total += pps * (chunk / 1000) * eff; } 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 × (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 effective d'un seul générateur (avec tous les bonus appliqués) export function generatorEffectiveProduction(gen: Generator, state: GameState): number { if (gen.owned === 0) return 0; const nidBoost = gen.id === "nid" ? getGeneratorBoostFromTree(state.evolutionTree) : 1; const treeMultiplier = getProductionMultiplierFromTree(state.evolutionTree); const synergyMult = getGeneratorSynergyMultiplier(state.evolutionTree, state.generators); const convergenceBoost = getAllEffectsBoost(state.evolutionTree); return gen.baseProduction * gen.owned * nidBoost * state.prestigeMultiplier * treeMultiplier * synergyMult * convergenceBoost; } // Production totale par seconde de tous les générateurs export function totalProductionPerSecond(state: GameState): number { return state.generators.reduce((sum, gen) => sum + generatorEffectiveProduction(gen, state), 0); } // Lazy calculation : ressources accumulées depuis lastTick export function computeIdleGains(state: GameState, now: number): number { const elapsedSeconds = (now - state.lastTick) / 1000; return totalProductionPerSecond(state) * elapsedSeconds; } // Applique les gains idle et met à jour lastTick export function applyIdleGains(state: GameState, now: number): GameState { const gains = computeIdleGains(state, now); return { ...state, resources: state.resources + gains, lifetimeTadpoles: state.lifetimeTadpoles + gains, lastTick: now, runStats: { ...state.runStats, tadpolesProduced: state.runStats.tadpolesProduced + gains, }, }; } // Bonus clic depuis les générateurs (diversité + quantité) export function getGeneratorClickBonus(generators: Generator[]): number { const typesOwned = generators.filter((g) => g.owned > 0).length; const totalOwned = generators.reduce((sum, g) => sum + g.owned, 0); return 1 + typesOwned * 2 + totalOwned * 0.05; } // Gain par clic — scaling propre : base × prestige × arbre × generateurs 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); } // Breakdown complet du clic (pour affichage cockpit) export interface ClickBreakdown { base: number; prestigeMult: number; treeMult: number; genBonus: number; // multiplicateur depuis generateurs (types + quantite) genTypes: number; // types possedes genTotal: number; // total unites possedees total: number; // gain par clic (floor) doubleChance: number; critChance: number; autoClicksPerSec: number; effectivePerSec: number; } export function getClickBreakdown(state: GameState): ClickBreakdown { const base = state.clickMultiplier; const prestigeMult = state.prestigeMultiplier; const treeMult = getClickMultiplierFromTree(state.evolutionTree); const genBonus = getGeneratorClickBonus(state.generators); const genTypes = state.generators.filter((g) => g.owned > 0).length; const genTotal = state.generators.reduce((sum, g) => sum + g.owned, 0); const total = Math.floor(base * prestigeMult * treeMult * genBonus); const doubleChance = getDoubleClickChance(state.evolutionTree); const critChance = getCritClickChance(state.evolutionTree); const autoClicksPerSec = getAutoClicksPerSecond(state.evolutionTree); const effectivePerSec = autoClicksPerSec * total; return { base, prestigeMult, treeMult, genBonus, genTypes, genTotal, total, doubleChance, critChance, autoClicksPerSec, effectivePerSec }; } export interface ClickResult { state: GameState; gain: number; isDouble: boolean; isCrit: boolean; } // Clic manuel avec double ponte + crit export function applyClick(state: GameState, rng: number = Math.random()): ClickResult { let gain = getClickGain(state); let isDouble = false; let isCrit = false; const doubleChance = getDoubleClickChance(state.evolutionTree); if (doubleChance > 0 && rng < doubleChance) { gain *= 2; isDouble = true; } const critChance = getCritClickChance(state.evolutionTree); // Use a second "roll" derived from rng to avoid double+crit being correlated const critRng = (rng * 7.13) % 1; if (critChance > 0 && critRng < critChance) { gain *= 10; isCrit = true; } return { state: { ...state, resources: state.resources + gain, lifetimeTadpoles: state.lifetimeTadpoles + gain, runStats: { ...state.runStats, tadpolesProduced: state.runStats.tadpolesProduced + gain, }, }, gain, isDouble, isCrit, }; } // Achat d'un générateur (retourne null si fonds insuffisants) export function buyGenerator(state: GameState, genId: string, quantity = 1): GameState | null { const genIndex = state.generators.findIndex((g) => g.id === genId); if (genIndex === -1) return null; let gen = { ...state.generators[genIndex] }; let resources = state.resources; 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]; updatedGenerators[genIndex] = gen; return { ...state, resources, 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 export function getPrestigeThreshold(state: GameState): number { const reduction = getPrestigeThresholdReduction(state.evolutionTree); const scaling = Math.pow(1 + 0.1 * state.prestigeCount, 2); return Math.floor(BASE_PRESTIGE_THRESHOLD * scaling * (1 - reduction)); } export function canPrestige(state: GameState): boolean { return state.resources >= getPrestigeThreshold(state); } export function applyPrestige(state: GameState): GameState { const newPrestigeCount = state.prestigeCount + 1; const dnaBonus = getPrestigeDnaBonus(state.evolutionTree); const baseDna = computePrestigeDna(state.lifetimeTadpoles, state.prestigeCount); const dnaGained = Math.floor(baseDna * (1 + dnaBonus)); const startBonus = getStartBonusFromTree(state.evolutionTree); // Résilience : commencer avec 1 Lac Mystique const hasUnlockGen = state.evolutionTree.some( (n) => n.unlocked && n.effect === "unlock_generator" ); // Milestone bonus : Nid gratuit au départ const milestoneNid = getMilestoneStartNid(state); // RunStats : snapshot de la run qui se termine const now = Date.now(); const runDuration = now - state.runStats.startedAt; const bestRun = state.runStats.bestRun; const newBestRun = !bestRun || dnaGained > bestRun.adn ? { duration: runDuration, tadpoles: state.lifetimeTadpoles, adn: dnaGained } : bestRun; return { ...state, resources: startBonus, generators: state.generators.map((g) => ({ ...g, owned: (hasUnlockGen && g.id === "lac") ? 1 : (milestoneNid > 0 && g.id === "nid") ? milestoneNid : 0, })), prestigeCount: newPrestigeCount, prestigeMultiplier: 1 + newPrestigeCount * 0.1, ancestralDna: state.ancestralDna + dnaGained, lifetimeTadpoles: 0, lastTick: now, lastOnline: now, // Sprint 3 — nouvelle run runStats: { startedAt: now, tadpolesProduced: 0, bestRun: newBestRun, }, freeResetAvailable: true, // 1 reset gratuit offert par prestige extraResetsUsed: 0, // evolutionTree persiste — jamais reset }; } // Valeurs par défaut — 5 tiers alignés GDD Tetard Universe (x10 coût / tier) export const DEFAULT_GENERATORS: Generator[] = [ { id: "nid", name: "Nid", baseCost: 10, baseProduction: 0.1, owned: 0 }, { id: "mare", name: "Mare", baseCost: 100, baseProduction: 0.5, owned: 0 }, { id: "marecage", name: "Marécage", baseCost: 1_000, baseProduction: 3, owned: 0 }, { id: "etang", name: "Étang Ancien", baseCost: 10_000, baseProduction: 20, owned: 0 }, { id: "lac", name: "Lac Mystique", baseCost: 100_000, baseProduction: 150, owned: 0 }, ]; export const DEFAULT_STATE: GameState = { saveVersion: CURRENT_SAVE_VERSION, resources: 0, clickMultiplier: 1, generators: DEFAULT_GENERATORS, lastTick: Date.now(), lastOnline: Date.now(), prestigeCount: 0, prestigeMultiplier: 1, ancestralDna: 0, evolutionTree: DEFAULT_EVOLUTION_TREE, lifetimeTadpoles: 0, cosmeticInventory: [], cosmeticEquipped: {}, runStats: { startedAt: Date.now(), tadpolesProduced: 0, bestRun: null, }, freeResetAvailable: true, extraResetsUsed: 0, claimedMilestones: [], };