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 <noreply@anthropic.com>
This commit is contained in:
@@ -234,6 +234,15 @@ non affilié au jeu original de Mountyhall SARL.
|
|||||||
|
|
||||||
## Versions
|
## 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
|
- **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
|
l'encyclopédie. Les **plages de puissance des potions et parchemins** sont
|
||||||
désormais lues dans la BDD (`/api/reference/potions`, `/api/reference/scrolls`,
|
désormais lues dans la BDD (`/api/reference/potions`, `/api/reference/scrolls`,
|
||||||
|
|||||||
+28
-18
@@ -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 %" };
|
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) {
|
function tuneInput(cat, id, key, def, cur) {
|
||||||
|
const val = cur !== undefined ? cur : def;
|
||||||
const tuned = cur !== undefined && Number(cur) !== Number(def);
|
const tuned = cur !== undefined && Number(cur) !== Number(def);
|
||||||
return `<input type="number" class="${tuned ? "tuned" : ""}" data-cat="${cat}" data-id="${id}" data-key="${key}" data-def="${def}" value="${cur !== undefined ? cur : def}">`;
|
// 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 `<input type="number" class="${tuned ? "tuned" : ""}" data-cat="${cat}" data-id="${id}" data-key="${key}" data-def="${def}" data-init="${val}" value="${val}">`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function markTuned(e) {
|
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) {
|
function collectTuning(cat) {
|
||||||
const out = {};
|
const byId = {};
|
||||||
for (const input of document.querySelectorAll(`input[data-cat="${cat}"]`)) {
|
for (const input of document.querySelectorAll(`input[data-cat="${cat}"]`)) {
|
||||||
const v = Number(input.value), def = Number(input.dataset.def);
|
(byId[input.dataset.id] = byId[input.dataset.id] || []).push(input);
|
||||||
if (!Number.isFinite(v) || v === def) continue;
|
}
|
||||||
const id = input.dataset.id, key = input.dataset.key;
|
const out = {};
|
||||||
(out[id] = out[id] || {})[key] = v;
|
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;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Potions/parchemins : chaque entrée doit devenir [min, max] complet. */
|
/* Potions/parchemins : chaque objet modifié devient [min, max] complet. */
|
||||||
function collectTreasureTuning(cat, defaults) {
|
function collectTreasureTuning(cat) {
|
||||||
const deltas = collectTuning(cat);
|
|
||||||
const out = {};
|
const out = {};
|
||||||
for (const [id, vals] of Object.entries(deltas)) {
|
for (const [id, vals] of Object.entries(collectTuning(cat))) {
|
||||||
const def = defaults.find(t => t.id === id);
|
out[id] = [vals.min, vals.max];
|
||||||
out[id] = [vals.min !== undefined ? vals.min : def.min, vals.max !== undefined ? vals.max : def.max];
|
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
@@ -310,14 +320,14 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
} catch (e) { $("admin-msg").textContent = "⚠️ " + e.message; }
|
} catch (e) { $("admin-msg").textContent = "⚠️ " + e.message; }
|
||||||
};
|
};
|
||||||
$("tune-monsters-save").onclick = () => saveTuning({ monsters: collectTuning("monsters") }, "Bestiaire");
|
$("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({
|
$("tune-treasures-save").onclick = () => saveTuning({
|
||||||
potions: collectTreasureTuning("potions", LAST_OV.defaults.potions),
|
potions: collectTreasureTuning("potions"),
|
||||||
scrolls: collectTreasureTuning("scrolls", LAST_OV.defaults.scrolls),
|
scrolls: collectTreasureTuning("scrolls"),
|
||||||
}, "Trésors");
|
}, "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-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 () => {
|
$("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;
|
if (!confirm("Régénérer la carte, les monstres et les trésors ? (les trolls sont conservés et replacés)")) return;
|
||||||
try {
|
try {
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
"use strict";
|
"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. */
|
/* Alpha : maîtrise initiale haute pour les tests. Remettre 15 % / 15 % à la v1.0 officielle. */
|
||||||
const START_COMP_PCT = 90;
|
const START_COMP_PCT = 90;
|
||||||
|
|||||||
@@ -922,8 +922,11 @@ function currentTuning() {
|
|||||||
|
|
||||||
const clampInt = (v, lo, hi) => Math.max(lo, Math.min(hi, Math.round(v)));
|
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
|
/* Applique un patch de tuning. Mises à jour CIBLÉES : seuls les objets fournis
|
||||||
* puis les écarts envoyés sont écrits en base (validés et bornés). */
|
* (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) {
|
function adminSetTuning(world, patch) {
|
||||||
const known = {
|
const known = {
|
||||||
monsters: new Set(db.vanillaMonsters().map(t => t.name)),
|
monsters: new Set(db.vanillaMonsters().map(t => t.name)),
|
||||||
@@ -931,8 +934,12 @@ function adminSetTuning(world, patch) {
|
|||||||
potions: new Set(p.POTION_IDS),
|
potions: new Set(p.POTION_IDS),
|
||||||
scrolls: new Set(sc.SCROLL_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") {
|
if (patch.monsters && typeof patch.monsters === "object") {
|
||||||
db.resetCategory("monsters");
|
|
||||||
for (const [name, vals] of Object.entries(patch.monsters)) {
|
for (const [name, vals] of Object.entries(patch.monsters)) {
|
||||||
if (!known.monsters.has(name) || typeof vals !== "object") continue;
|
if (!known.monsters.has(name) || typeof vals !== "object") continue;
|
||||||
const entry = {};
|
const entry = {};
|
||||||
@@ -940,11 +947,10 @@ function adminSetTuning(world, patch) {
|
|||||||
const v = Number(vals[k]);
|
const v = Number(vals[k]);
|
||||||
if (Number.isFinite(v)) entry[k] = clampInt(v, MONSTER_TUNE_BOUNDS[k][0], MONSTER_TUNE_BOUNDS[k][1]);
|
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") {
|
if (patch.gear && typeof patch.gear === "object") {
|
||||||
db.resetCategory("gear");
|
|
||||||
for (const [key, mods] of Object.entries(patch.gear)) {
|
for (const [key, mods] of Object.entries(patch.gear)) {
|
||||||
if (!known.gear.has(key) || typeof mods !== "object") continue;
|
if (!known.gear.has(key) || typeof mods !== "object") continue;
|
||||||
const entry = {};
|
const entry = {};
|
||||||
@@ -952,13 +958,14 @@ function adminSetTuning(world, patch) {
|
|||||||
const v = Number(mods[k]);
|
const v = Number(mods[k]);
|
||||||
if (Number.isFinite(v)) entry[k] = clampInt(v, GEAR_TUNE_BOUNDS[0], GEAR_TUNE_BOUNDS[1]);
|
if (Number.isFinite(v)) entry[k] = clampInt(v, GEAR_TUNE_BOUNDS[0], GEAR_TUNE_BOUNDS[1]);
|
||||||
}
|
}
|
||||||
|
if (Object.keys(entry).length) {
|
||||||
const slash = key.indexOf("/");
|
const slash = key.indexOf("/");
|
||||||
db.setGear(key.slice(0, slash), key.slice(slash + 1), entry);
|
db.setGear(key.slice(0, slash), key.slice(slash + 1), entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
for (const cat of ["potions", "scrolls"]) {
|
for (const cat of ["potions", "scrolls"]) {
|
||||||
if (!patch[cat] || typeof patch[cat] !== "object") continue;
|
if (!patch[cat] || typeof patch[cat] !== "object") continue;
|
||||||
db.resetCategory(cat);
|
|
||||||
for (const [id, range] of Object.entries(patch[cat])) {
|
for (const [id, range] of Object.entries(patch[cat])) {
|
||||||
if (!known[cat].has(id) || !Array.isArray(range)) continue;
|
if (!known[cat].has(id) || !Array.isArray(range)) continue;
|
||||||
const lo = Number(range[0]), hi = Number(range[1]);
|
const lo = Number(range[0]), hi = Number(range[1]);
|
||||||
|
|||||||
+19
-2
@@ -258,11 +258,28 @@ function makeWorld(over = {}) {
|
|||||||
const tun2 = mp.adminSetTuning(w, { monsters: { "Dragon": { att: 5 }, "Gobelin": { att: 5000 } } });
|
const tun2 = mp.adminSetTuning(w, { monsters: { "Dragon": { att: 5 }, "Gobelin": { att: 5000 } } });
|
||||||
assert(!tun2.monsters.Dragon, "type inconnu ignoré");
|
assert(!tun2.monsters.Dragon, "type inconnu ignoré");
|
||||||
assert.strictEqual(tun2.monsters.Gobelin.att, 99, "ATT bornée à 99");
|
assert.strictEqual(tun2.monsters.Gobelin.att, 99, "ATT bornée à 99");
|
||||||
// retour au vanilla
|
// retour au vanilla : reset explicite de la catégorie
|
||||||
const tun3 = mp.adminSetTuning(w, { monsters: {} });
|
const tun3 = mp.adminSetTuning(w, { reset: ["monsters"] });
|
||||||
assert.strictEqual(Object.keys(tun3.monsters).length, 0, "bestiaire d'origine restauré");
|
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
|
// Tuning admin : puissance des potions/parchemins et bonus d'équipement
|
||||||
{
|
{
|
||||||
const w = makeWorld({ monsterTarget: 0, itemTarget: 0 });
|
const w = makeWorld({ monsterTarget: 0, itemTarget: 0 });
|
||||||
|
|||||||
Reference in New Issue
Block a user