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
|
## 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
|
- **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é,
|
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
|
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-log { max-height: 220px; overflow-y: auto; font-size: 0.82em; }
|
||||||
.admin-msg { font-weight: bold; margin: 8px 0; }
|
.admin-msg { font-weight: bold; margin: 8px 0; }
|
||||||
#admin-login { max-width: 480px; margin: 60px auto; }
|
#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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -98,6 +106,27 @@
|
|||||||
<button id="tune-gear-save" class="big-btn">💾 Appliquer l'équipement</button>
|
<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>
|
<button id="tune-gear-reset" class="menu-btn">↩️ Tout remettre d'origine</button>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -297,6 +326,7 @@ async function refresh(withForm = false) {
|
|||||||
async function connect() {
|
async function connect() {
|
||||||
try {
|
try {
|
||||||
await refresh(true);
|
await refresh(true);
|
||||||
|
await loadBestiary();
|
||||||
localStorage.setItem("mc_admin_token", TOKEN);
|
localStorage.setItem("mc_admin_token", TOKEN);
|
||||||
$("admin-login").classList.add("hidden");
|
$("admin-login").classList.add("hidden");
|
||||||
$("admin-body").classList.remove("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", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
$("admin-connect").onclick = () => {
|
$("admin-connect").onclick = () => {
|
||||||
TOKEN = $("admin-token").value.trim();
|
TOKEN = $("admin-token").value.trim();
|
||||||
@@ -327,6 +407,23 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
scrolls: collectTreasureTuning("scrolls"),
|
scrolls: collectTreasureTuning("scrolls"),
|
||||||
}, "Trésors");
|
}, "Trésors");
|
||||||
$("tune-treasures-reset").onclick = () => saveTuning({ reset: ["potions", "scrolls"] }, "Trésors d'origine");
|
$("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-save").onclick = () => saveTuning({ gear: collectTuning("gear") }, "Équipement");
|
||||||
$("tune-gear-reset").onclick = () => saveTuning({ reset: ["gear"] }, "Équipement d'origine");
|
$("tune-gear-reset").onclick = () => saveTuning({ reset: ["gear"] }, "Équipement d'origine");
|
||||||
$("admin-reset").onclick = async () => {
|
$("admin-reset").onclick = async () => {
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
"use strict";
|
"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. */
|
/* 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;
|
||||||
|
|||||||
@@ -994,7 +994,28 @@ function adminSetTuning(world, patch) {
|
|||||||
};
|
};
|
||||||
if (Array.isArray(patch.reset)) {
|
if (Array.isArray(patch.reset)) {
|
||||||
for (const cat of 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") {
|
if (patch.monsters && typeof patch.monsters === "object") {
|
||||||
|
|||||||
Reference in New Issue
Block a user