feat: Sprint 3 — Prestige Loop endless
Some checks failed
CI/CD — Build & Deploy / Build & Deploy (push) Failing after 35s
Some checks failed
CI/CD — Build & Deploy / Build & Deploy (push) Failing after 35s
- Migration saves: saveVersion pattern + migrateSave lazy (v1→v2) - Formule ADN rebalancée: log10 + clamp min 1 + cap bonus ×4 - Prestige Experience: modal fullscreen, preview ADN, stats run, best run - Arbre V2: 25 nœuds, 3 capstones, post-capstones repeatables (scaling par tranche) - Convergence évolutif Alpha→Omega (tier system) - Reset arbre: 1 gratuit/prestige, payant linéaire au-delà - Milestones prestige: 8 paliers (1→100), cosmétiques exclusifs, bonus gameplay - balance.ts: constantes centralisées pour playtest - 136 tests green, 0 regression
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
const tables = require("../tables");
|
||||
const { migrateSave } = require("../services/migrateSave");
|
||||
|
||||
// --- Anti-cheat validation ---
|
||||
|
||||
@@ -75,11 +76,14 @@ const load = async (req, res) => {
|
||||
}
|
||||
|
||||
// game_state est stocké en JSON — MySQL le retourne comme objet si type JSON
|
||||
const gameState =
|
||||
const rawState =
|
||||
typeof save.game_state === "string"
|
||||
? JSON.parse(save.game_state)
|
||||
: save.game_state;
|
||||
|
||||
// Migrate on load — lazy migration, never touch DB rows directly
|
||||
const gameState = migrateSave(rawState);
|
||||
|
||||
return res.status(200).json({
|
||||
gameState,
|
||||
lastSave: save.last_save,
|
||||
|
||||
@@ -15,18 +15,20 @@ class GameSaveManager extends AbstractManager {
|
||||
|
||||
async upsert(userId, gameState, metadata) {
|
||||
const { lifetimeTadpoles, prestigeCount, playTimeSeconds } = metadata;
|
||||
const saveVersion = gameState.saveVersion ?? 1;
|
||||
const gameStateJson = JSON.stringify(gameState);
|
||||
|
||||
const [result] = await this.database.query(
|
||||
`INSERT INTO ${this.table} (user_id, game_state, lifetime_tadpoles, prestige_count, play_time_seconds)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`INSERT INTO ${this.table} (user_id, game_state, save_version, lifetime_tadpoles, prestige_count, play_time_seconds)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
game_state = VALUES(game_state),
|
||||
save_version = VALUES(save_version),
|
||||
lifetime_tadpoles = VALUES(lifetime_tadpoles),
|
||||
prestige_count = VALUES(prestige_count),
|
||||
play_time_seconds = VALUES(play_time_seconds),
|
||||
last_save = CURRENT_TIMESTAMP`,
|
||||
[userId, gameStateJson, lifetimeTadpoles, prestigeCount, playTimeSeconds]
|
||||
[userId, gameStateJson, saveVersion, lifetimeTadpoles, prestigeCount, playTimeSeconds]
|
||||
);
|
||||
|
||||
return result.affectedRows;
|
||||
|
||||
65
Backend/src/services/migrateSave.js
Normal file
65
Backend/src/services/migrateSave.js
Normal file
@@ -0,0 +1,65 @@
|
||||
// migrateSave.js — Backend save migration (mirrors Frontend/src/core/migrateSave.ts)
|
||||
// Applied on load — lazy migration, never touch DB directly.
|
||||
|
||||
const CURRENT_SAVE_VERSION = 2;
|
||||
|
||||
// Default evolution tree (Sprint 2 — 18 nodes)
|
||||
// Used to merge new nodes into old saves
|
||||
const DEFAULT_TREE_IDS = [
|
||||
"ponte_amelioree", "double_ponte", "ponte_frenetique", "auto_ponte",
|
||||
"ponte_critique", "maitre_pondeur",
|
||||
"instinct_gregaire", "symbiose_algale", "courant_profond", "maree_haute",
|
||||
"ecosysteme_mature", "marais_eternel",
|
||||
"memoire_genetique", "adn_renforce", "eveil_rapide", "resilience",
|
||||
"heritage", "transcendance",
|
||||
];
|
||||
|
||||
/**
|
||||
* Migrate a raw game state to the current version.
|
||||
* Backend only needs structural migration for anti-cheat validation —
|
||||
* the full tree/generator merge happens on the frontend.
|
||||
*/
|
||||
function migrateSave(raw) {
|
||||
if (!raw || typeof raw !== "object") return raw;
|
||||
|
||||
const version = typeof raw.saveVersion === "number" ? raw.saveVersion : 1;
|
||||
let state = { ...raw };
|
||||
|
||||
if (version < 2) {
|
||||
state = migrateV1toV2(state);
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
function migrateV1toV2(state) {
|
||||
state.saveVersion = 2;
|
||||
|
||||
// RunStats
|
||||
if (!state.runStats) {
|
||||
state.runStats = {
|
||||
startedAt: state.lastTick || Date.now(),
|
||||
tadpolesProduced: 0,
|
||||
bestRun: null,
|
||||
};
|
||||
}
|
||||
|
||||
// Tree reset fields
|
||||
if (typeof state.freeResetAvailable !== "boolean") {
|
||||
state.freeResetAvailable = true;
|
||||
}
|
||||
if (typeof state.extraResetsUsed !== "number") {
|
||||
state.extraResetsUsed = 0;
|
||||
}
|
||||
|
||||
// Backfill cosmetics
|
||||
if (!state.lastOnline) state.lastOnline = state.lastTick;
|
||||
if (!Array.isArray(state.cosmeticInventory)) state.cosmeticInventory = [];
|
||||
if (!state.cosmeticEquipped || typeof state.cosmeticEquipped !== "object") {
|
||||
state.cosmeticEquipped = {};
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
module.exports = { migrateSave, CURRENT_SAVE_VERSION };
|
||||
Reference in New Issue
Block a user