From 646a147774d033d347abf1e7fb14bab836e140a1 Mon Sep 17 00:00:00 2001 From: perco Date: Fri, 12 Jun 2026 15:37:21 +0200 Subject: [PATCH] =?UTF-8?q?2.1.0=20:=20connexion=20multi-appareils=20(mot?= =?UTF-8?q?=20de=20passe=20+=20login)=20et=20tuning=20admin=20du=20bestiai?= =?UTF-8?q?re,=20de=20l'=C3=A9quipement=20et=20des=20tr=C3=A9sors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- README.md | 42 +++++++++-- admin.html | 134 ++++++++++++++++++++++++++++++++- index.html | 11 +++ js/game.js | 2 +- js/mp-client.js | 59 +++++++++++++-- mp.js | 191 +++++++++++++++++++++++++++++++++++++++++++++--- server.js | 21 +++++- test/mp.js | 74 +++++++++++++++++++ 8 files changed, 509 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 75e9c10..a9776f4 100644 --- a/README.md +++ b/README.md @@ -117,8 +117,12 @@ commun à tous les joueurs, autoritaire côté serveur (`mp.js`) : sortilège de race, potions, parchemins, équipement, entraînement en PI. - **À MountyHall on ne meurt jamais vraiment** : un troll terrassé réapparaît après un délai paramétrable, PV pleins, en conservant tout son avoir. -- L'identité du troll (id + clé secrète) est gardée par le navigateur - (localStorage) : on retrouve son personnage d'une session à l'autre. +- **Connexion multi-appareils** : le troll vit côté serveur. À la création on + choisit un mot de passe (conseillé) ; « 🔑 Retrouver mon troll » permet ensuite + de le récupérer depuis n'importe quel appareil avec nom + mot de passe (les + noms sont uniques, le mot de passe est stocké hashé+salé). Sans mot de passe, + l'identité (id + clé secrète) reste gardée par le navigateur d'origine + (localStorage), comme avant. - Le client se rafraîchit par **polling** (intervalle conseillé par le serveur, paramétrable) ; les autres trolls visibles apparaissent en bleu avec leur nom, les camouflés sont invisibles. @@ -137,15 +141,31 @@ de trolls. Plus : vue d'ensemble (trolls avec dernière activité, monstres avec leur DLA individuelle et leur prochaine action), derniers échos du monde, et bouton « Régénérer le monde » (trolls conservés et replacés). +L'admin peut aussi **retoucher les règles du monde partagé**, à chaud (les +nouveaux spawns/drops utilisent les valeurs retouchées ; le solo reste vanilla) : + +- **Tuning du bestiaire** : tableau éditable par type de monstre (niveau, ATT, + ESQ, DEG, PV, armure physique/magique, VUE) — valeurs de base, avant gabarit + d'âge (Jeune/Vieux/Ancien/Mythique). +- **Tuning de l'équipement** : les 55 objets avec tous leurs bonus modifiables + (ATT/ESQ/DEG/REG/armure/VUE/PV fixes, RM/MM en %). +- **Tuning des potions & parchemins** : fourchette de puissance « niveau X » + (min/max) tirée à chaque drop, par trésor. + +Les cases modifiées sont surlignées ; seuls les écarts au vanilla sont stockés +(`world.tuning`), et chaque catégorie se remet d'origine d'un clic. + ### API multijoueur -- `POST /api/mp/join` `{name, race}` → `{id, secret, state}` +- `POST /api/mp/join` `{name, race, password}` → `{id, secret, state}` +- `POST /api/mp/login` `{name, password}` → `{id, secret, state}` (retrouver + son troll depuis un autre appareil) - `GET /api/mp/state?id=&secret=` → état visible (vue du troll) - `POST /api/mp/action` `{id, secret, action}` — `move`, `attack`, `comp`, `sort`, `pickup`, `use`, `train` - `GET /api/mp/info` → compteurs publics (trolls en ligne…) -- `GET /api/mp/admin` · `PUT /api/mp/admin/config` · `POST /api/mp/admin/reset` - (header `X-Admin-Token`) +- `GET /api/mp/admin` · `PUT /api/mp/admin/config` · `PUT /api/mp/admin/tuning` + · `POST /api/mp/admin/reset` (header `X-Admin-Token`) ## Éditeur de niveaux & partage @@ -201,6 +221,18 @@ non affilié au jeu original de Mountyhall SARL. ## Versions +- **2.1.0** (2026-06-12) — **Connexion multi-appareils** : à la création du troll + on choisit un mot de passe (stocké hashé+salé, jamais en clair) ; « 🔑 Retrouver + mon troll » (`POST /api/mp/login`) permet de récupérer son troll depuis + n'importe quel appareil avec nom + mot de passe. Les noms de troll deviennent + uniques (insensible à la casse). Structure prête pour une vraie inscription + plus tard. **Admin enrichie** (`PUT /api/mp/admin/tuning`, appliqué à chaud + aux nouveaux spawns/drops du monde partagé, solo vanilla) : bestiaire éditable + par type (niveau, ATT, ESQ, DEG, PV, armures phy/mag, VUE), les 55 objets + d'équipement avec tous leurs bonus modifiables, fourchette de puissance + « niveau X » min/max par potion et parchemin. Seuls les écarts au vanilla sont + stockés (`world.tuning`, persisté), cases modifiées surlignées, remise + d'origine par catégorie. - **2.0.0** (2026-06-12) — **Multijoueur** : monde souterrain partagé et persistant, autoritaire côté serveur (`mp.js`, node pur). Le monde vit en continu : chaque monstre agit à sa propre DLA paramétrable (fourchette min/max), erre ou attaque diff --git a/admin.html b/admin.html index 4e21a93..5ac15b1 100644 --- a/admin.html +++ b/admin.html @@ -16,6 +16,9 @@ .admin-field input { width: 80px; padding: 3px; } .admin-table { width: 100%; border-collapse: collapse; font-size: 0.82em; } .admin-table th, .admin-table td { border: 1px solid #999; padding: 2px 6px; text-align: left; } + .admin-table input[type=number] { width: 52px; padding: 1px 2px; border: 1px solid #aaa; background: #fff; } + .admin-table input.tuned { background: #fff3b0; border-color: #c90; font-weight: bold; } + .tune-slot { margin: 10px 0 4px; font-weight: bold; } .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; } @@ -61,6 +64,37 @@

👺 Monstres

+ +
+

🐗 Tuning du bestiaire

+

Valeurs de base par type (avant gabarit Jeune/Vieux/Ancien/Mythique). + Les cases modifiées (fond jaune) s'appliquent aux prochains spawns du monde + partagé — le solo reste vanilla. Vider/remettre la valeur d'origine pour annuler.

+
+ + +
+ +
+

🧪 Tuning des potions & parchemins

+

Fourchette de puissance « niveau X » de chaque trésor (tirée au drop). + S'applique aux prochains drops du monde partagé.

+
+

Potions

+

Parchemins

+
+ + +
+ +
+

⚔️ Tuning de l'équipement

+

Bonus/malus de chaque objet (physiques, fixes sur les jets ; RM/MM en %). + S'applique aux prochains drops du monde partagé.

+
+ + +
@@ -140,9 +174,98 @@ function renderOverview(ov) { `).join("")}`; } +/* ---------- Tuning : bestiaire, trésors, équipement ---------- */ + +const MONSTER_LABELS = { level: "Niv.", att: "ATT", esq: "ESQ", deg: "DEG", pv: "PV", armor: "Arm. phy", armorMag: "Arm. mag", vue: "VUE" }; +const GEAR_LABELS = { att: "ATT", esq: "ESQ", deg: "DEG", reg: "REG", arm: "Arm. phy", vue: "VUE", pv: "PV", rmPct: "RM %", mmPct: "MM %" }; + +function tuneInput(cat, id, key, def, cur) { + const tuned = cur !== undefined && Number(cur) !== Number(def); + return ``; +} + +function markTuned(e) { + if (e.target.dataset.def === undefined) return; + e.target.classList.toggle("tuned", Number(e.target.value) !== Number(e.target.dataset.def)); +} + +function renderTuning(defaults, tuning) { + const mKeys = defaults.monsterKeys; + document.getElementById("tune-monsters").innerHTML = ` + ${mKeys.map(k => ``).join("")} + ${defaults.monsters.map(m => `` + + mKeys.map(k => ``).join("") + + ``).join("")}
Type${MONSTER_LABELS[k]}
${m.emoji} ${m.name}${m.boss ? " (boss)" : ""}${tuneInput("monsters", m.name, k, m[k], (tuning.monsters[m.name] || {})[k])}
`; + + const treasureTable = (cat, list) => ` + + ${list.map(t => { + const cur = tuning[cat][t.id] || []; + return ` + + `; + }).join("")}
TrésorX minX max
${t.emoji} ${t.name}${tuneInput(cat, t.id, "min", t.min, cur[0])}${tuneInput(cat, t.id, "max", t.max, cur[1])}
`; + document.getElementById("tune-potions").innerHTML = treasureTable("potions", defaults.potions); + document.getElementById("tune-scrolls").innerHTML = treasureTable("scrolls", defaults.scrolls); + + const slots = {}; + for (const it of defaults.gear) (slots[it.slot] = slots[it.slot] || []).push(it); + document.getElementById("tune-gear").innerHTML = Object.entries(slots).map(([slot, list]) => ` +
${slot.charAt(0).toUpperCase() + slot.slice(1)}s
+ + ${defaults.gearKeys.map(k => ``).join("")} + ${list.map(it => { + const id = `${it.slot}/${it.name}`; + const cur = tuning.gear[id] || {}; + return `` + + defaults.gearKeys.map(k => ``).join("") + ``; + }).join("")}
Objet${GEAR_LABELS[k]}
${it.emoji} ${it.name}${it.twoHanded ? " (2 mains)" : ""}${tuneInput("gear", id, k, it.mods[k], cur[k])}
`).join(""); + + for (const input of document.querySelectorAll("#tune-monsters input, #tune-potions input, #tune-scrolls input, #tune-gear input")) { + input.addEventListener("input", markTuned); + } +} + +/* Collecte les écarts à la valeur d'origine d'une catégorie. */ +function collectTuning(cat) { + const out = {}; + 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; + } + return out; +} + +/* Potions/parchemins : chaque entrée doit devenir [min, max] complet. */ +function collectTreasureTuning(cat, defaults) { + const deltas = collectTuning(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]; + } + return out; +} + +async function saveTuning(patch, label) { + try { + await api("PUT", "api/mp/admin/tuning", patch); + $("admin-msg").textContent = `✅ ${label} appliqué à chaud.`; + await refresh(true); + } catch (e) { $("admin-msg").textContent = "⚠️ " + e.message; } +} + +let LAST_OV = null; + async function refresh(withForm = false) { const ov = await api("GET", "api/mp/admin"); - if (withForm) renderForm(ov.config, ov.bounds); + LAST_OV = ov; + if (withForm) { + renderForm(ov.config, ov.bounds); + renderTuning(ov.defaults, ov.tuning); + } renderOverview(ov); } @@ -172,6 +295,15 @@ document.addEventListener("DOMContentLoaded", () => { await refresh(true); } 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-treasures-save").onclick = () => saveTuning({ + potions: collectTreasureTuning("potions", LAST_OV.defaults.potions), + scrolls: collectTreasureTuning("scrolls", LAST_OV.defaults.scrolls), + }, "Trésors"); + $("tune-treasures-reset").onclick = () => saveTuning({ potions: {}, scrolls: {} }, "Trésors d'origine"); + $("tune-gear-save").onclick = () => saveTuning({ gear: collectTuning("gear") }, "Équipement"); + $("tune-gear-reset").onclick = () => saveTuning({ 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/index.html b/index.html index 0d52eea..c27b68f 100644 --- a/index.html +++ b/index.html @@ -145,6 +145,17 @@ +