feat(sprint1-step3b): backend save system + anti-cheat + données rattrapées
- game_saves table + migration 002 (JSON state, anti-cheat metadata) - saveControllers.js : load/save avec validation delta ressources (750k/s × 1.1) - GameSaveManager : upsert MySQL ON DUPLICATE KEY UPDATE - useSaveSync hook : auto-save 30s + keepalive beforeunload + guest fallback - save-validation.test.ts : 8 tests anti-cheat - economy.ts : arbre d'évolution 5 nœuds + prestige ADN (rattrapage step 2) - economy.test.ts : +40 tests (évolution tree, multipliers, start bonus) - GDD + SPRINT1.md : docs sprint complètes - Rethème data : shop.json, Achievements.json, Cookie, Legal (rattrapage step 1)
This commit is contained in:
@@ -9,15 +9,94 @@ export interface Generator {
|
||||
owned: number;
|
||||
}
|
||||
|
||||
export type EffectType = "click_multiplier" | "production_multiplier" | "start_bonus" | "unlock_generator" | "achievement_scaling";
|
||||
|
||||
export interface EvolutionNode {
|
||||
id: string;
|
||||
name: string;
|
||||
cost: number; // en ADN Ancestral
|
||||
effect: EffectType;
|
||||
value: number;
|
||||
unlocked: boolean;
|
||||
requires: string | null; // id du nœud prérequis (null = racine)
|
||||
}
|
||||
|
||||
export interface GameState {
|
||||
resources: number;
|
||||
clickMultiplier: number;
|
||||
generators: Generator[];
|
||||
lastTick: number; // timestamp ms — lazy calc reference
|
||||
prestigeCount: number;
|
||||
prestigeMultiplier: number; // 1 + prestigeCount * 0.1
|
||||
prestigeMultiplier: number; // legacy — conservé pour compat, remplacé par arbre
|
||||
ancestralDna: number;
|
||||
evolutionTree: EvolutionNode[];
|
||||
lifetimeTadpoles: number; // total cumulé de la run (pour calcul ADN)
|
||||
}
|
||||
|
||||
// --- 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" },
|
||||
];
|
||||
|
||||
// Calcule l'ADN gagné lors d'un prestige : floor(150 × sqrt(lifetime / 1e9))
|
||||
export function computePrestigeDna(lifetimeTadpoles: number): number {
|
||||
return Math.floor(150 * Math.sqrt(lifetimeTadpoles / 1e9));
|
||||
}
|
||||
|
||||
// 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 || node.unlocked) return false;
|
||||
if (state.ancestralDna < node.cost) return false;
|
||||
if (node.requires) {
|
||||
const prereq = state.evolutionTree.find((n) => n.id === node.requires);
|
||||
if (!prereq || !prereq.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)!;
|
||||
return {
|
||||
...state,
|
||||
ancestralDna: state.ancestralDna - node.cost,
|
||||
evolutionTree: state.evolutionTree.map((n) =>
|
||||
n.id === nodeId ? { ...n, unlocked: true } : n
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// --- 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));
|
||||
@@ -29,7 +108,8 @@ export function totalProductionPerSecond(state: GameState): number {
|
||||
(sum, gen) => sum + gen.baseProduction * gen.owned,
|
||||
0
|
||||
);
|
||||
return base * state.prestigeMultiplier;
|
||||
const treeMultiplier = getProductionMultiplierFromTree(state.evolutionTree);
|
||||
return base * state.prestigeMultiplier * treeMultiplier;
|
||||
}
|
||||
|
||||
// Lazy calculation : ressources accumulées depuis lastTick
|
||||
@@ -44,15 +124,19 @@ export function applyIdleGains(state: GameState, now: number): GameState {
|
||||
return {
|
||||
...state,
|
||||
resources: state.resources + gains,
|
||||
lifetimeTadpoles: state.lifetimeTadpoles + gains,
|
||||
lastTick: now,
|
||||
};
|
||||
}
|
||||
|
||||
// Clic manuel
|
||||
export function applyClick(state: GameState): GameState {
|
||||
const treeClickMult = getClickMultiplierFromTree(state.evolutionTree);
|
||||
const gain = state.clickMultiplier * state.prestigeMultiplier * treeClickMult;
|
||||
return {
|
||||
...state,
|
||||
resources: state.resources + state.clickMultiplier * state.prestigeMultiplier,
|
||||
resources: state.resources + gain,
|
||||
lifetimeTadpoles: state.lifetimeTadpoles + gain,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -75,30 +159,36 @@ export function buyGenerator(state: GameState, genId: string): GameState | null
|
||||
};
|
||||
}
|
||||
|
||||
// Prestige : reset ressources + générateurs, +0.1× multiplicateur permanent
|
||||
// Prestige : reset run, gain ADN, arbre persiste
|
||||
export function canPrestige(state: GameState): boolean {
|
||||
return state.resources >= 1_000_000;
|
||||
}
|
||||
|
||||
export function applyPrestige(state: GameState): GameState {
|
||||
const newPrestigeCount = state.prestigeCount + 1;
|
||||
const dnaGained = computePrestigeDna(state.lifetimeTadpoles);
|
||||
const startBonus = getStartBonusFromTree(state.evolutionTree);
|
||||
|
||||
return {
|
||||
...state,
|
||||
resources: 0,
|
||||
resources: startBonus,
|
||||
generators: state.generators.map((g) => ({ ...g, owned: 0 })),
|
||||
prestigeCount: newPrestigeCount,
|
||||
prestigeMultiplier: 1 + newPrestigeCount * 0.1,
|
||||
ancestralDna: state.ancestralDna + dnaGained,
|
||||
lifetimeTadpoles: 0,
|
||||
lastTick: Date.now(),
|
||||
// evolutionTree persiste — jamais reset
|
||||
};
|
||||
}
|
||||
|
||||
// Valeurs par défaut — 5 tiers alignés GDD (x10 coût / tier, x5 production)
|
||||
// Valeurs par défaut — 5 tiers alignés GDD Tetard Universe (x10 coût / tier)
|
||||
export const DEFAULT_GENERATORS: Generator[] = [
|
||||
{ id: "manic", name: "Manic", baseCost: 10, baseProduction: 0.1, owned: 0 },
|
||||
{ id: "coffee", name: "Tasse à café", baseCost: 100, baseProduction: 0.5, owned: 0 },
|
||||
{ id: "sugar", name: "Sucre", baseCost: 1_000, baseProduction: 3, owned: 0 },
|
||||
{ id: "factory", name: "Usine", baseCost: 10_000, baseProduction: 20, owned: 0 },
|
||||
{ id: "portal", name: "Portail", baseCost: 100_000, baseProduction: 150, owned: 0 },
|
||||
{ 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 = {
|
||||
@@ -108,4 +198,7 @@ export const DEFAULT_STATE: GameState = {
|
||||
lastTick: Date.now(),
|
||||
prestigeCount: 0,
|
||||
prestigeMultiplier: 1,
|
||||
ancestralDna: 0,
|
||||
evolutionTree: DEFAULT_EVOLUTION_TREE,
|
||||
lifetimeTadpoles: 0,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user