All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 21s
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.
179 lines
5.5 KiB
TypeScript
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 };
|
|
});
|
|
}
|