Files
ClickerZ/Frontend/src/lib/core/migrateSave.ts
Tetardtek 7a8f4f325c
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 21s
feat: click upgrades — buy click power with tadpoles, tied to generators
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.
2026-03-28 21:19:01 +01:00

179 lines
5.5 KiB
TypeScript

// migrateSave.ts — Migration lazy des saves entre versions
// Appliqué au chargement (frontend + backend). Jamais de migration en DB.
// Chaque sprint ajoute un step (v2→v3, etc.)
import { CURRENT_SAVE_VERSION } from "./balance";
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.
* Entrée : objet brut depuis la DB/localStorage (potentiellement incomplet).
* Sortie : GameState conforme à la version courante.
*/
export function migrateSave(raw: Record<string, unknown>): GameState {
const version = typeof raw.saveVersion === "number" ? raw.saveVersion : 1;
let state = raw as Record<string, unknown>;
if (version < 2) {
state = migrateV1toV2(state);
}
// Futures migrations :
// if (version < 3) state = migrateV2toV3(state);
// Always rebuild tree & generators from defaults — the server/localStorage
// may not store all fields (branch, cost, effect, baseProduction, etc.)
state.evolutionTree = mergeEvolutionTree(
state.evolutionTree as Array<Record<string, unknown>> | undefined
);
state.generators = mergeGenerators(
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;
}
/**
* v1 → v2 : Sprint 2 → Sprint 3
* - Ajoute saveVersion
* - Ajoute runStats (vide)
* - Ajoute freeResetAvailable + extraResetsUsed
* - Merge les nouveaux nœuds arbre (conserve l'état des 18 existants)
* - Backfill champs manquants (cosmeticInventory, cosmeticEquipped, lastOnline)
*/
function migrateV1toV2(raw: Record<string, unknown>): Record<string, unknown> {
const state = { ...raw };
// saveVersion
state.saveVersion = 2;
// RunStats (nouveau Sprint 3)
if (!state.runStats) {
state.runStats = {
startedAt: typeof state.lastTick === "number" ? state.lastTick : Date.now(),
tadpolesProduced: 0,
bestRun: null,
};
}
// Reset arbre : 1 gratuit par prestige
if (typeof state.freeResetAvailable !== "boolean") {
state.freeResetAvailable = true;
}
if (typeof state.extraResetsUsed !== "number") {
state.extraResetsUsed = 0;
}
// Milestones (Sprint 3)
if (!Array.isArray(state.claimedMilestones)) {
state.claimedMilestones = [];
}
// Backfill champs Sprint 2 potentiellement manquants
if (!state.lastOnline) state.lastOnline = state.lastTick;
if (!Array.isArray(state.cosmeticInventory)) state.cosmeticInventory = [];
if (!state.cosmeticEquipped || typeof state.cosmeticEquipped !== "object") {
state.cosmeticEquipped = {};
}
// Merge arbre : conserver les 18 nœuds existants + ajouter les nouveaux
state.evolutionTree = mergeEvolutionTree(
state.evolutionTree as Array<Record<string, unknown>> | undefined
);
// Merge générateurs : conserver owned + ajouter les potentiels nouveaux
state.generators = mergeGenerators(
state.generators as Array<Record<string, unknown>> | undefined
);
return state;
}
/**
* Merge l'arbre sauvegardé avec DEFAULT_EVOLUTION_TREE.
* - Nœuds existants : conserve unlocked state
* - Nœuds nouveaux : ajoutés avec unlocked: false
* - Nœuds supprimés du default : retirés (forward compat)
*/
function mergeEvolutionTree(
savedTree: Array<Record<string, unknown>> | undefined
): typeof DEFAULT_EVOLUTION_TREE {
if (!savedTree || !Array.isArray(savedTree)) {
return DEFAULT_EVOLUTION_TREE.map((n) => ({ ...n }));
}
const savedById = new Map(
savedTree.map((n) => [n.id as string, n])
);
return DEFAULT_EVOLUTION_TREE.map((defaultNode) => {
const saved = savedById.get(defaultNode.id);
if (saved) {
// Conserver l'état unlocked, tout le reste vient du default
// (permet de corriger des valeurs rebalancées sans casser les saves)
return {
...defaultNode,
unlocked: saved.unlocked === true,
};
}
// Nouveau nœud — ajouté verrouillé
return { ...defaultNode };
});
}
/**
* Merge les générateurs sauvegardés avec DEFAULT_GENERATORS.
* Conserve le owned count, met à jour les stats de base.
*/
function mergeGenerators(
savedGens: Array<Record<string, unknown>> | undefined
): typeof DEFAULT_GENERATORS {
if (!savedGens || !Array.isArray(savedGens)) {
return DEFAULT_GENERATORS.map((g) => ({ ...g }));
}
const savedById = new Map(
savedGens.map((g) => [g.id as string, g])
);
return DEFAULT_GENERATORS.map((defaultGen) => {
const saved = savedById.get(defaultGen.id);
if (saved) {
return {
...defaultGen,
owned: typeof saved.owned === "number" ? saved.owned : 0,
};
}
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 };
});
}