From 18f5e7fc3b75527062ffee91d85525f4d72cf921 Mon Sep 17 00:00:00 2001 From: perco Date: Mon, 15 Jun 2026 16:50:01 +0200 Subject: [PATCH] v2.10.1 : bestiaire etape 2 - editeur admin (ages + monstres) mp.js adminSetTuning : gere patch.ages (setMonsterAge, borne 0-99) et patch.bestiary (setBestiary, champs numeriques clampes 0-99999, textes family/speed/capacities/blason) ; reset des categories bestiary/ages. admin.html : section Bestiaire - 8 multiplicateurs d'age + editeur monstre (filtre famille + recherche, formulaire de tous les champs) lisant /api/reference/bestiary+/ages, sauvegarde via le tuning. CSS dedie. Co-Authored-By: Claude Opus 4.8 --- README.md | 6 ++++ admin.html | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ js/game.js | 2 +- mp.js | 23 ++++++++++++- 4 files changed, 126 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a3efd85..353d022 100644 --- a/README.md +++ b/README.md @@ -234,6 +234,12 @@ non affilié au jeu original de Mountyhall SARL. ## Versions +- **2.10.1** (2026-06-15) — Bestiaire, étape 2 : **éditeur admin**. Nouvelle + section « 🐲 Bestiaire » dans la page admin pour régler les **multiplicateurs + d'âge** (les 8 âges) et **éditer chaque monstre** (filtre par famille + + recherche par nom, puis tous ses champs : plages de stats, capacités, + drapeaux, vitesse, blason). Sauvegarde à chaud via le tuning admin (mises à + jour ciblées, bouton « tout remettre d'origine » par catégorie). - **2.10.0** (2026-06-15) — **Nouveau bestiaire** (étape 1 : les données). Le bestiaire complet fourni (945 entrées monstre × âge) est replié, comme demandé, en **1 ligne par monstre** (150 monstres, stats de base = âge le plus jeune, en diff --git a/admin.html b/admin.html index 7e37cc6..5aacb3b 100644 --- a/admin.html +++ b/admin.html @@ -22,6 +22,14 @@ .admin-log { max-height: 220px; overflow-y: auto; font-size: 0.82em; } .admin-msg { font-weight: bold; margin: 8px 0; } #admin-login { max-width: 480px; margin: 60px auto; } + .bestiary-filters { display: flex; flex-wrap: wrap; gap: 8px; margin: 8px 0; } + .bestiary-filters select, .bestiary-filters input { padding: 4px 6px; border: 1px solid #aaa; background: #fff; } + .bestiary-filters #best-pick { min-width: 220px; } + .age-mult { display: inline-block; margin: 0 8px 4px 0; font-size: .85em; } + .age-mult input { width: 64px; padding: 1px 3px; border: 1px solid #aaa; } + .best-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 6px; margin: 8px 0; } + .best-grid label { display: flex; flex-direction: column; font-size: .78em; color: #333; } + .best-grid input { padding: 2px 4px; border: 1px solid #aaa; background: #fff; } @@ -98,6 +106,27 @@ + +
+

🐲 Bestiaire

+

1 ligne par monstre (stats de base = âge le plus jeune, en plages + min/max) et une table d'âges (multiplicateurs appliqués à la génération / + croissance). Tirage au hasard dans les plages à chaque apparition.

+

Multiplicateurs d'âge

+
+ + +

Monstres

+
+ + + +
+
+ + +
+
@@ -297,6 +326,7 @@ async function refresh(withForm = false) { async function connect() { try { await refresh(true); + await loadBestiary(); localStorage.setItem("mc_admin_token", TOKEN); $("admin-login").classList.add("hidden"); $("admin-body").classList.remove("hidden"); @@ -306,6 +336,56 @@ async function connect() { } } +/* ---------- Bestiaire ---------- */ +let BEST = [], AGES = []; +const BEST_FIELDS = [ + ["family", "Famille", "text"], ["minAge", "Âge min", "int"], ["maxAge", "Âge max", "int"], + ["levelMin", "Niveau min", "int"], ["levelMax", "Niveau max", "int"], + ["pvMin", "PV min", "int"], ["pvMax", "PV max", "int"], + ["attMin", "ATT min (D6)", "int"], ["attMax", "ATT max", "int"], + ["esqMin", "ESQ min (D6)", "int"], ["esqMax", "ESQ max", "int"], + ["degMin", "DEG min (D3)", "int"], ["degMax", "DEG max", "int"], + ["regMin", "REG min (D3)", "int"], ["regMax", "REG max", "int"], + ["armPhysMin", "Arm.phy min", "int"], ["armPhysMax", "Arm.phy max", "int"], + ["armMagMin", "Arm.mag min", "int"], ["armMagMax", "Arm.mag max", "int"], + ["vueMin", "Vue min", "int"], ["vueMax", "Vue max", "int"], + ["mmMin", "MM min", "int"], ["mmMax", "MM max", "int"], + ["rmMin", "RM min", "int"], ["rmMax", "RM max", "int"], ["nbAtt", "Attaques/tour", "int"], + ["fly", "Vole (0/1)", "int"], ["ranged", "Att. distance (0/1)", "int"], + ["magic", "Att. magique (0/1)", "int"], ["seesHidden", "Voit caché (0/1)", "int"], + ["speed", "Vitesse", "text"], ["capacities", "Capacités", "text"], ["blason", "Blason (URL)", "text"], +]; + +async function loadBestiary() { + try { + [BEST, AGES] = await Promise.all([ + fetch("api/reference/bestiary").then(r => r.json()), + fetch("api/reference/ages").then(r => r.json()), + ]); + } catch { BEST = []; AGES = []; } + $("tune-ages").innerHTML = AGES.map(a => + ``).join(""); + const fams = [...new Set(BEST.map(b => b.family))].sort(); + $("best-family").innerHTML = '' + fams.map(f => ``).join(""); + renderMonsterPicker(); +} + +function renderMonsterPicker() { + const fam = $("best-family").value, q = $("best-search").value.trim().toLowerCase(); + const list = BEST.filter(b => (!fam || b.family === fam) && (!q || b.name.toLowerCase().includes(q))); + const pick = $("best-pick"), cur = pick.value; + pick.innerHTML = list.map(b => ``).join(""); + if (list.some(b => b.name === cur)) pick.value = cur; + renderMonsterForm(); +} + +function renderMonsterForm() { + const b = BEST.find(x => x.name === $("best-pick").value); + if (!b) { $("best-form").innerHTML = "

Aucun monstre pour ce filtre.

"; return; } + $("best-form").innerHTML = `
` + BEST_FIELDS.map(([k, label, type]) => + ``).join("") + "
"; +} + document.addEventListener("DOMContentLoaded", () => { $("admin-connect").onclick = () => { TOKEN = $("admin-token").value.trim(); @@ -327,6 +407,23 @@ document.addEventListener("DOMContentLoaded", () => { scrolls: collectTreasureTuning("scrolls"), }, "Trésors"); $("tune-treasures-reset").onclick = () => saveTuning({ reset: ["potions", "scrolls"] }, "Trésors d'origine"); + // Bestiaire + $("best-family").onchange = renderMonsterPicker; + $("best-search").oninput = renderMonsterPicker; + $("best-pick").onchange = renderMonsterForm; + $("tune-ages-save").onclick = async () => { + const ages = {}; + for (const i of document.querySelectorAll("#tune-ages input")) ages[i.dataset.age] = Number(i.value); + await saveTuning({ ages }, "Âges"); await loadBestiary(); + }; + $("tune-ages-reset").onclick = async () => { await saveTuning({ reset: ["ages"] }, "Âges d'origine"); await loadBestiary(); }; + $("best-save").onclick = async () => { + const name = $("best-pick").value; if (!name) return; + const vals = {}; + for (const i of $("best-form").querySelectorAll("input")) vals[i.dataset.key] = i.value; + await saveTuning({ bestiary: { [name]: vals } }, "Monstre « " + name + " »"); await loadBestiary(); + }; + $("best-reset").onclick = async () => { await saveTuning({ reset: ["bestiary"] }, "Bestiaire d'origine"); await loadBestiary(); }; $("tune-gear-save").onclick = () => saveTuning({ gear: collectTuning("gear") }, "Équipement"); $("tune-gear-reset").onclick = () => saveTuning({ reset: ["gear"] }, "Équipement d'origine"); $("admin-reset").onclick = async () => { diff --git a/js/game.js b/js/game.js index c09e171..815568f 100644 --- a/js/game.js +++ b/js/game.js @@ -4,7 +4,7 @@ "use strict"; -const APP_VERSION = "2.10.0"; +const APP_VERSION = "2.10.1"; /* 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 7b29378..ba3849f 100644 --- a/mp.js +++ b/mp.js @@ -994,7 +994,28 @@ function adminSetTuning(world, patch) { }; if (Array.isArray(patch.reset)) { for (const cat of patch.reset) { - if (["monsters", "gear", "potions", "scrolls"].includes(cat)) db.resetCategory(cat); + if (["monsters", "gear", "potions", "scrolls", "bestiary", "ages"].includes(cat)) db.resetCategory(cat); + } + } + // Bestiaire : multiplicateurs d'âge + édition d'un monstre (valeurs absolues). + if (patch.ages && typeof patch.ages === "object") { + for (const [age, mult] of Object.entries(patch.ages)) { + const a = Number(age), m = Number(mult); + if (Number.isInteger(a) && a >= 0 && a <= 7 && Number.isFinite(m)) db.setMonsterAge(a, Math.max(0, Math.min(99, m))); + } + } + if (patch.bestiary && typeof patch.bestiary === "object") { + const knownB = new Set(db.bestiaryAll().map(b => b.name)); + const txt = new Set(["family", "speed", "capacities", "blason"]); + for (const [name, vals] of Object.entries(patch.bestiary)) { + if (!knownB.has(name) || typeof vals !== "object") continue; + const entry = {}; + for (const k of db.BESTIARY_KEYS) { + if (vals[k] == null) continue; + if (txt.has(k)) entry[k] = String(vals[k]).slice(0, 300); + else { const v = Number(vals[k]); if (Number.isFinite(v)) entry[k] = clampInt(v, 0, 99999); } + } + if (Object.keys(entry).length) db.setBestiary(name, entry); } } if (patch.monsters && typeof patch.monsters === "object") {