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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
+97
@@ -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; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -98,6 +106,27 @@
|
||||
<button id="tune-gear-save" class="big-btn">💾 Appliquer l'équipement</button>
|
||||
<button id="tune-gear-reset" class="menu-btn">↩️ Tout remettre d'origine</button>
|
||||
</div>
|
||||
|
||||
<div class="admin-card" style="margin-top:16px">
|
||||
<h2>🐲 Bestiaire</h2>
|
||||
<p><small>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.</small></p>
|
||||
<h3>Multiplicateurs d'âge</h3>
|
||||
<div id="tune-ages"></div>
|
||||
<button id="tune-ages-save" class="big-btn">💾 Appliquer les âges</button>
|
||||
<button id="tune-ages-reset" class="menu-btn">↩️ Âges d'origine</button>
|
||||
<h3 style="margin-top:14px">Monstres</h3>
|
||||
<div class="bestiary-filters">
|
||||
<select id="best-family"><option value="">Toutes les familles</option></select>
|
||||
<input type="text" id="best-search" placeholder="Rechercher un monstre…">
|
||||
<select id="best-pick"></select>
|
||||
</div>
|
||||
<div id="best-form"></div>
|
||||
<button id="best-save" class="big-btn">💾 Appliquer ce monstre</button>
|
||||
<button id="best-reset" class="menu-btn">↩️ Tout le bestiaire d'origine</button>
|
||||
<div id="best-msg"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 =>
|
||||
`<label class="age-mult">âge ${a.age} <input type="number" step="0.05" data-age="${a.age}" value="${a.mult}"></label>`).join("");
|
||||
const fams = [...new Set(BEST.map(b => b.family))].sort();
|
||||
$("best-family").innerHTML = '<option value="">Toutes les familles</option>' + fams.map(f => `<option>${f}</option>`).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 => `<option>${b.name}</option>`).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 = "<p><small>Aucun monstre pour ce filtre.</small></p>"; return; }
|
||||
$("best-form").innerHTML = `<div class="best-grid">` + BEST_FIELDS.map(([k, label, type]) =>
|
||||
`<label>${label}<input data-key="${k}" type="${type === "int" ? "number" : "text"}" value="${String(b[k] ?? "").replace(/"/g, """)}"></label>`).join("") + "</div>";
|
||||
}
|
||||
|
||||
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 () => {
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
@@ -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") {
|
||||
|
||||
Reference in New Issue
Block a user