From 55b573001b681421e07886728f6eb98a5f439b69 Mon Sep 17 00:00:00 2001 From: perco Date: Sat, 13 Jun 2026 23:25:01 +0200 Subject: [PATCH] v2.6.2 : edition admin et BDD non conflictuelles (updates cibles) adminSetTuning ne fait plus resetCategory + reapplique tout (ce qui ecrasait les editions sqlite-web) : mises a jour ciblees des seuls objets fournis (valeurs absolues), sans toucher au reste. Le bouton reset passe par patch.reset (tableau de categories). admin.html : collectTuning renvoie les objets MODIFIES dans la session (data-init) avec leurs valeurs completes. Tests : non-regression (edition externe preservee) + mise a jour du test de reset. Co-Authored-By: Claude Opus 4.8 --- README.md | 9 +++++++++ admin.html | 46 ++++++++++++++++++++++++++++------------------ js/game.js | 2 +- mp.js | 23 +++++++++++++++-------- test/mp.js | 21 +++++++++++++++++++-- 5 files changed, 72 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 466e8a8..2e36d8f 100644 --- a/README.md +++ b/README.md @@ -234,6 +234,15 @@ non affilié au jeu original de Mountyhall SARL. ## Versions +- **2.6.2** (2026-06-13) — La page admin et l'édition directe en base + (sqlite-web) **ne se marchent plus dessus**. Avant, sauver le tuning d'une + catégorie la **remettait entièrement au vanilla** puis réappliquait le + formulaire — donc une édition faite en base pouvait être écrasée par une + sauvegarde admin (et un onglet admin périmé écrasait les éditions récentes). + Désormais l'admin fait des **mises à jour ciblées** : seuls les objets + réellement modifiés dans la session sont écrits, le reste de la catégorie est + laissé tel quel. Le bouton « Tout remettre d'origine » passe par un reset + explicite (`patch.reset`). Les deux chemins d'édition cohabitent enfin. - **2.6.1** (2026-06-13) — Suite du branchement de la base de référence sur l'encyclopédie. Les **plages de puissance des potions et parchemins** sont désormais lues dans la BDD (`/api/reference/potions`, `/api/reference/scrolls`, diff --git a/admin.html b/admin.html index 88b248a..6345973 100644 --- a/admin.html +++ b/admin.html @@ -194,8 +194,11 @@ const MONSTER_LABELS = { level: "Niv.", att: "ATT phy", attMag: "ATT mag", esq: const GEAR_LABELS = { att: "ATT phy", attMag: "ATT mag", esq: "ESQ", deg: "DEG phy", degMag: "DEG mag", reg: "REG", arm: "Arm. phy", armMag: "Arm. mag", vue: "VUE", pv: "PV", rmPct: "RM %", mmPct: "MM %" }; function tuneInput(cat, id, key, def, cur) { + const val = cur !== undefined ? cur : def; const tuned = cur !== undefined && Number(cur) !== Number(def); - return ``; + // data-def = valeur d'origine (vanilla) ; data-init = valeur chargée (pour + // détecter ce que l'utilisateur change réellement dans cette session). + return ``; } function markTuned(e) { @@ -240,25 +243,32 @@ function renderTuning(defaults, tuning) { } } -/* Collecte les écarts à la valeur d'origine d'une catégorie. */ +/* Collecte les objets MODIFIÉS dans cette session (au moins un champ ≠ valeur + * chargée) avec TOUTES leurs valeurs courantes (absolues). La sauvegarde ne + * touchera donc que ces objets-là, sans réinitialiser le reste : on peut éditer + * en parallèle via la base (sqlite-web) sans que l'admin écrase ces éditions. */ function collectTuning(cat) { - const out = {}; + const byId = {}; for (const input of document.querySelectorAll(`input[data-cat="${cat}"]`)) { - const v = Number(input.value), def = Number(input.dataset.def); - if (!Number.isFinite(v) || v === def) continue; - const id = input.dataset.id, key = input.dataset.key; - (out[id] = out[id] || {})[key] = v; + (byId[input.dataset.id] = byId[input.dataset.id] || []).push(input); + } + const out = {}; + for (const [id, inputs] of Object.entries(byId)) { + if (!inputs.some(i => Number(i.value) !== Number(i.dataset.init))) continue; + out[id] = {}; + for (const i of inputs) { + const v = Number(i.value); + if (Number.isFinite(v)) out[id][i.dataset.key] = v; + } } return out; } -/* Potions/parchemins : chaque entrée doit devenir [min, max] complet. */ -function collectTreasureTuning(cat, defaults) { - const deltas = collectTuning(cat); +/* Potions/parchemins : chaque objet modifié devient [min, max] complet. */ +function collectTreasureTuning(cat) { const out = {}; - for (const [id, vals] of Object.entries(deltas)) { - const def = defaults.find(t => t.id === id); - out[id] = [vals.min !== undefined ? vals.min : def.min, vals.max !== undefined ? vals.max : def.max]; + for (const [id, vals] of Object.entries(collectTuning(cat))) { + out[id] = [vals.min, vals.max]; } return out; } @@ -310,14 +320,14 @@ document.addEventListener("DOMContentLoaded", () => { } catch (e) { $("admin-msg").textContent = "⚠️ " + e.message; } }; $("tune-monsters-save").onclick = () => saveTuning({ monsters: collectTuning("monsters") }, "Bestiaire"); - $("tune-monsters-reset").onclick = () => saveTuning({ monsters: {} }, "Bestiaire d'origine"); + $("tune-monsters-reset").onclick = () => saveTuning({ reset: ["monsters"] }, "Bestiaire d'origine"); $("tune-treasures-save").onclick = () => saveTuning({ - potions: collectTreasureTuning("potions", LAST_OV.defaults.potions), - scrolls: collectTreasureTuning("scrolls", LAST_OV.defaults.scrolls), + potions: collectTreasureTuning("potions"), + scrolls: collectTreasureTuning("scrolls"), }, "Trésors"); - $("tune-treasures-reset").onclick = () => saveTuning({ potions: {}, scrolls: {} }, "Trésors d'origine"); + $("tune-treasures-reset").onclick = () => saveTuning({ reset: ["potions", "scrolls"] }, "Trésors d'origine"); $("tune-gear-save").onclick = () => saveTuning({ gear: collectTuning("gear") }, "Équipement"); - $("tune-gear-reset").onclick = () => saveTuning({ gear: {} }, "Équipement d'origine"); + $("tune-gear-reset").onclick = () => saveTuning({ reset: ["gear"] }, "Équipement d'origine"); $("admin-reset").onclick = async () => { if (!confirm("Régénérer la carte, les monstres et les trésors ? (les trolls sont conservés et replacés)")) return; try { diff --git a/js/game.js b/js/game.js index b6f0c36..2d9ee65 100644 --- a/js/game.js +++ b/js/game.js @@ -4,7 +4,7 @@ "use strict"; -const APP_VERSION = "2.6.1"; +const APP_VERSION = "2.6.2"; /* Alpha : maîtrise initiale haute pour les tests. Remettre 15 % / 15 % à la v1.0 officielle. */ const START_COMP_PCT = 90; diff --git a/mp.js b/mp.js index 14c33a9..784fc64 100644 --- a/mp.js +++ b/mp.js @@ -922,8 +922,11 @@ function currentTuning() { const clampInt = (v, lo, hi) => Math.max(lo, Math.min(hi, Math.round(v))); -/* Applique un patch de tuning : chaque catégorie fournie est remise au vanilla - * puis les écarts envoyés sont écrits en base (validés et bornés). */ +/* Applique un patch de tuning. Mises à jour CIBLÉES : seuls les objets fournis + * (valeurs absolues) sont écrits, sans toucher au reste de la catégorie — ainsi + * les éditions faites en parallèle dans la base (sqlite-web) ne sont PAS écrasées. + * `patch.reset` (tableau de catégories) remet explicitement une catégorie entière + * au vanilla (bouton « Tout remettre d'origine »). */ function adminSetTuning(world, patch) { const known = { monsters: new Set(db.vanillaMonsters().map(t => t.name)), @@ -931,8 +934,12 @@ function adminSetTuning(world, patch) { potions: new Set(p.POTION_IDS), scrolls: new Set(sc.SCROLL_IDS), }; + if (Array.isArray(patch.reset)) { + for (const cat of patch.reset) { + if (["monsters", "gear", "potions", "scrolls"].includes(cat)) db.resetCategory(cat); + } + } if (patch.monsters && typeof patch.monsters === "object") { - db.resetCategory("monsters"); for (const [name, vals] of Object.entries(patch.monsters)) { if (!known.monsters.has(name) || typeof vals !== "object") continue; const entry = {}; @@ -940,11 +947,10 @@ function adminSetTuning(world, patch) { const v = Number(vals[k]); if (Number.isFinite(v)) entry[k] = clampInt(v, MONSTER_TUNE_BOUNDS[k][0], MONSTER_TUNE_BOUNDS[k][1]); } - db.setMonster(name, entry); + if (Object.keys(entry).length) db.setMonster(name, entry); } } if (patch.gear && typeof patch.gear === "object") { - db.resetCategory("gear"); for (const [key, mods] of Object.entries(patch.gear)) { if (!known.gear.has(key) || typeof mods !== "object") continue; const entry = {}; @@ -952,13 +958,14 @@ function adminSetTuning(world, patch) { const v = Number(mods[k]); if (Number.isFinite(v)) entry[k] = clampInt(v, GEAR_TUNE_BOUNDS[0], GEAR_TUNE_BOUNDS[1]); } - const slash = key.indexOf("/"); - db.setGear(key.slice(0, slash), key.slice(slash + 1), entry); + if (Object.keys(entry).length) { + const slash = key.indexOf("/"); + db.setGear(key.slice(0, slash), key.slice(slash + 1), entry); + } } } for (const cat of ["potions", "scrolls"]) { if (!patch[cat] || typeof patch[cat] !== "object") continue; - db.resetCategory(cat); for (const [id, range] of Object.entries(patch[cat])) { if (!known[cat].has(id) || !Array.isArray(range)) continue; const lo = Number(range[0]), hi = Number(range[1]); diff --git a/test/mp.js b/test/mp.js index ab53857..c6e2311 100644 --- a/test/mp.js +++ b/test/mp.js @@ -258,11 +258,28 @@ function makeWorld(over = {}) { const tun2 = mp.adminSetTuning(w, { monsters: { "Dragon": { att: 5 }, "Gobelin": { att: 5000 } } }); assert(!tun2.monsters.Dragon, "type inconnu ignoré"); assert.strictEqual(tun2.monsters.Gobelin.att, 99, "ATT bornée à 99"); - // retour au vanilla - const tun3 = mp.adminSetTuning(w, { monsters: {} }); + // retour au vanilla : reset explicite de la catégorie + const tun3 = mp.adminSetTuning(w, { reset: ["monsters"] }); assert.strictEqual(Object.keys(tun3.monsters).length, 0, "bestiaire d'origine restauré"); } +// Tuning admin : mises à jour CIBLÉES — éditer un objet n'écrase pas les autres +// (cohabitation avec les éditions directes en base, ex. sqlite-web). +{ + const db = require("../db.js"); + const w = makeWorld({ monsterTarget: 0, itemTarget: 0 }); + // édition « externe » (façon sqlite-web) sur le Chapeau pointu + db.setGear("casque", "Chapeau pointu", { mmPct: 20 }); + // l'admin sauve un AUTRE objet : le chapeau ne doit PAS être réinitialisé + mp.adminSetTuning(w, { gear: { "arme/Gourdin": { att: 7 } } }); + assert.strictEqual(db.gearRow("casque", "Chapeau pointu").mmPct, 20, "édition externe préservée"); + assert.strictEqual(db.gearRow("arme", "Gourdin").att, 7, "objet édité par l'admin appliqué"); + // reset explicite : tout revient au vanilla + mp.adminSetTuning(w, { reset: ["gear"] }); + assert.strictEqual(db.gearRow("casque", "Chapeau pointu").mmPct, 5, "reset gear → vanilla (chapeau)"); + assert.strictEqual(db.gearRow("arme", "Gourdin").att, 2, "reset gear → vanilla (gourdin)"); +} + // Tuning admin : puissance des potions/parchemins et bonus d'équipement { const w = makeWorld({ monsterTarget: 0, itemTarget: 0 });