feat: migrate frontend React 18 → Svelte 5 + SvelteKit
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 22s
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 22s
Core logic portable (economy, balance, cosmetics, migrateSave) — zero rewrite. 136 tests green, identiques. Backend inchangé. - Svelte 5 runes stores (game, auth, toast) remplacent Zustand - SvelteKit adapter-static SPA (dist/ output, fallback index.html) - Tailwind v4 conservé, design system .gp-* porté - Transitions natives : slide, fly, scale, fade sur toute l'UI - Sidebar tabbée (Production/Evolution/Collection) + CollapsiblePanel - Mobile bottom sheet avec FAB toggle + backdrop blur - Click particles réactifs Svelte (plus de DOM impératif) - TadpoleSprite bounce + glow ring au clic - Guide refait en accordéon, Achievements avec filtres - a11y : focus-visible, Escape modals, aria-current, aria-labels - CI/CD adapté (tests + build + rsync) - Build 504K (vs ~1.2MB React)
This commit is contained in:
142
Frontend/src/lib/core/migrateSave.ts
Normal file
142
Frontend/src/lib/core/migrateSave.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
// 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 } from "./economy";
|
||||
import { DEFAULT_EVOLUTION_TREE, DEFAULT_GENERATORS } 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);
|
||||
|
||||
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 };
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user