2.2.0 : ATT/DEG/ARM physiques et magiques partout (monstres + équipement), suppression de troll par l'admin
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -103,6 +103,12 @@ Gobelin, Champignon Vénéneux, Araignée Géante, Gargouille, Momie, Sorcière,
|
|||||||
Pierre — déclinés en gabarits *Jeune / Vieux / Ancien / Mythique* selon la profondeur,
|
Pierre — déclinés en gabarits *Jeune / Vieux / Ancien / Mythique* selon la profondeur,
|
||||||
et le **Béhémoth** comme boss final à la profondeur −5.
|
et le **Béhémoth** comme boss final à la profondeur −5.
|
||||||
|
|
||||||
|
Comme l'armure, l'ATT et les DEG des monstres existent en **physique** et en
|
||||||
|
**magique** : un monstre doté d'une attaque magique (ATT mag et DEG mag > 0)
|
||||||
|
alterne au hasard entre ses deux attaques ; l'attaque magique n'est réduite que
|
||||||
|
par l'armure magique du troll. En vanilla toutes les valeurs magiques sont à 0
|
||||||
|
(comportement inchangé) — elles se règlent par type via l'admin du monde partagé.
|
||||||
|
|
||||||
## Monde partagé (multijoueur)
|
## Monde partagé (multijoueur)
|
||||||
|
|
||||||
Depuis l'accueil : **🧌🧌 Monde partagé**. Un monde souterrain persistant et
|
Depuis l'accueil : **🧌🧌 Monde partagé**. Un monde souterrain persistant et
|
||||||
@@ -144,11 +150,11 @@ 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
|
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) :
|
nouveaux spawns/drops utilisent les valeurs retouchées ; le solo reste vanilla) :
|
||||||
|
|
||||||
- **Tuning du bestiaire** : tableau éditable par type de monstre (niveau, ATT,
|
- **Tuning du bestiaire** : tableau éditable par type de monstre (niveau, ATT
|
||||||
ESQ, DEG, PV, armure physique/magique, VUE) — valeurs de base, avant gabarit
|
phy/mag, ESQ, DEG phy/mag, PV, armure phy/mag, VUE) — valeurs de base, avant
|
||||||
d'âge (Jeune/Vieux/Ancien/Mythique).
|
gabarit d'âge (Jeune/Vieux/Ancien/Mythique).
|
||||||
- **Tuning de l'équipement** : les 55 objets avec tous leurs bonus modifiables
|
- **Tuning de l'équipement** : les 55 objets avec tous leurs bonus modifiables
|
||||||
(ATT/ESQ/DEG/REG/armure/VUE/PV fixes, RM/MM en %).
|
(ATT phy/mag, ESQ, DEG phy/mag, REG, armure phy/mag, VUE, PV fixes, RM/MM en %).
|
||||||
- **Tuning des potions & parchemins** : fourchette de puissance « niveau X »
|
- **Tuning des potions & parchemins** : fourchette de puissance « niveau X »
|
||||||
(min/max) tirée à chaque drop, par trésor.
|
(min/max) tirée à chaque drop, par trésor.
|
||||||
|
|
||||||
@@ -221,6 +227,20 @@ non affilié au jeu original de Mountyhall SARL.
|
|||||||
|
|
||||||
## Versions
|
## Versions
|
||||||
|
|
||||||
|
- **2.2.0** (2026-06-12) — Le couple **physique/magique partout** : comme l'armure,
|
||||||
|
l'ATT et les DEG existent désormais en deux saveurs pour les **monstres**
|
||||||
|
(`attMag`, `degMag` — un monstre qui en a alterne au hasard entre attaque
|
||||||
|
physique et magique, cette dernière n'étant réduite que par l'armure magique)
|
||||||
|
et pour l'**équipement** (`attMag`/`degMag`/`armMag` : bonus appliqués aux
|
||||||
|
sortilèges et à l'armure magique, là où `att`/`deg`/`arm` restent physiques).
|
||||||
|
ESQ, PV et VUE restent simples. Les bonus fixes du troll sont décomposés par
|
||||||
|
type d'action (`xxxFlatPhys`/`xxxFlatMag`) : les potions/parchemins modifient
|
||||||
|
le troll et comptent dans les deux, l'équipement selon sa saveur — au passage,
|
||||||
|
une arme physique ne booste plus les sortilèges (Vampirisme, Siphon des Âmes…).
|
||||||
|
Toutes les valeurs magiques vanilla sont à 0 (comportement inchangé) et se
|
||||||
|
règlent via le tuning admin, dont les tableaux gagnent les colonnes
|
||||||
|
ATT/DEG/Arm phy et mag. Admin : bouton 🗑️ pour **supprimer un troll**
|
||||||
|
(`POST /api/mp/admin/kick`).
|
||||||
- **2.1.0** (2026-06-12) — **Connexion multi-appareils** : à la création du troll
|
- **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
|
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
|
mon troll » (`POST /api/mp/login`) permet de récupérer son troll depuis
|
||||||
|
|||||||
+14
-3
@@ -158,12 +158,23 @@ function renderOverview(ov) {
|
|||||||
`<div>${new Date(l.t).toLocaleTimeString("fr-FR")} — ${l.msg}</div>`).reverse().join("");
|
`<div>${new Date(l.t).toLocaleTimeString("fr-FR")} — ${l.msg}</div>`).reverse().join("");
|
||||||
|
|
||||||
$("admin-trolls").innerHTML = ov.trolls.length ? `<table class="admin-table">
|
$("admin-trolls").innerHTML = ov.trolls.length ? `<table class="admin-table">
|
||||||
<tr><th>Nom</th><th>Race</th><th>Niv.</th><th>PV</th><th>PA</th><th>Kills</th><th>Or</th><th>Position</th><th>État</th><th>Vu il y a</th></tr>
|
<tr><th>Nom</th><th>Race</th><th>Niv.</th><th>PV</th><th>PA</th><th>Kills</th><th>Or</th><th>Position</th><th>État</th><th>Vu il y a</th><th></th></tr>
|
||||||
${ov.trolls.map(t => `<tr>
|
${ov.trolls.map(t => `<tr>
|
||||||
<td>${t.name}</td><td>${t.race}</td><td>${t.level}</td>
|
<td>${t.name}</td><td>${t.race}</td><td>${t.level}</td>
|
||||||
<td>${t.pv}/${t.pvMax}</td><td>${t.pa}</td><td>${t.kills}</td><td>${t.gold}</td>
|
<td>${t.pv}/${t.pvMax}</td><td>${t.pa}</td><td>${t.kills}</td><td>${t.gold}</td>
|
||||||
<td>${t.x},${t.y}</td><td>${t.dead ? "☠️ mort" : "vivant"}</td><td>${fmtAgo(t.lastSeenAgo)}</td>
|
<td>${t.x},${t.y}</td><td>${t.dead ? "☠️ mort" : "vivant"}</td><td>${fmtAgo(t.lastSeenAgo)}</td>
|
||||||
|
<td><button class="kick-troll" data-id="${t.id}" data-name="${t.name}" title="Supprimer ce troll">🗑️</button></td>
|
||||||
</tr>`).join("")}</table>` : "<p>Aucun troll pour l'instant.</p>";
|
</tr>`).join("")}</table>` : "<p>Aucun troll pour l'instant.</p>";
|
||||||
|
for (const btn of document.querySelectorAll(".kick-troll")) {
|
||||||
|
btn.onclick = async () => {
|
||||||
|
if (!confirm(`Supprimer définitivement le troll « ${btn.dataset.name} » ?`)) return;
|
||||||
|
try {
|
||||||
|
await api("POST", "api/mp/admin/kick", { id: btn.dataset.id });
|
||||||
|
$("admin-msg").textContent = `✅ ${btn.dataset.name} supprimé.`;
|
||||||
|
await refresh();
|
||||||
|
} catch (e) { $("admin-msg").textContent = "⚠️ " + e.message; }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
$("admin-monsters").innerHTML = `<table class="admin-table">
|
$("admin-monsters").innerHTML = `<table class="admin-table">
|
||||||
<tr><th></th><th>Nom</th><th>Niv.</th><th>PV</th><th>Position</th><th>DLA (s)</th><th>Prochaine action</th></tr>
|
<tr><th></th><th>Nom</th><th>Niv.</th><th>PV</th><th>Position</th><th>DLA (s)</th><th>Prochaine action</th></tr>
|
||||||
@@ -176,8 +187,8 @@ function renderOverview(ov) {
|
|||||||
|
|
||||||
/* ---------- Tuning : bestiaire, trésors, équipement ---------- */
|
/* ---------- 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 MONSTER_LABELS = { level: "Niv.", att: "ATT phy", attMag: "ATT mag", esq: "ESQ", deg: "DEG phy", degMag: "DEG mag", 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 %" };
|
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 tuned = cur !== undefined && Number(cur) !== Number(def);
|
const tuned = cur !== undefined && Number(cur) !== Number(def);
|
||||||
|
|||||||
+39
-31
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const APP_VERSION = "2.1.0";
|
const APP_VERSION = "2.2.0";
|
||||||
|
|
||||||
/* 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;
|
||||||
@@ -34,11 +34,19 @@ function rollDice(n, faces) {
|
|||||||
* (Les dégâts sont en D3 comme dans les profils officiels des races.)
|
* (Les dégâts sont en D3 comme dans les profils officiels des races.)
|
||||||
* Armure façon MH : les dégâts PHYSIQUES sont réduits par l'armure totale
|
* Armure façon MH : les dégâts PHYSIQUES sont réduits par l'armure totale
|
||||||
* (physique + magique) ; les dégâts MAGIQUES (opts.magic) par la seule armure
|
* (physique + magique) ; les dégâts MAGIQUES (opts.magic) par la seule armure
|
||||||
* magique ; opts.ignoreArmor ignore tout (Siphon des Âmes). */
|
* magique ; opts.ignoreArmor ignore tout (Siphon des Âmes).
|
||||||
|
* ATT et DEG existent en deux saveurs comme l'armure : les bonus fixes
|
||||||
|
* physiques (xxxFlatPhys) s'appliquent aux attaques physiques, les magiques
|
||||||
|
* (xxxFlatMag) aux attaques magiques — à défaut on retombe sur xxxFlat. */
|
||||||
|
function pickFlat(src, key, magic) {
|
||||||
|
const v = magic ? src[`${key}FlatMag`] : src[`${key}FlatPhys`];
|
||||||
|
return (v != null ? v : src[`${key}Flat`]) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
function resolveAttack(attacker, defender, opts = {}) {
|
function resolveAttack(attacker, defender, opts = {}) {
|
||||||
const att = rollDice(attacker.att, 6);
|
const att = rollDice(attacker.att, 6);
|
||||||
const esq = rollDice(defender.esq, 6);
|
const esq = rollDice(defender.esq, 6);
|
||||||
const attFlat = attacker.attFlat || 0;
|
const attFlat = pickFlat(attacker, "att", opts.magic);
|
||||||
const esqFlat = defender.esqFlat || 0;
|
const esqFlat = defender.esqFlat || 0;
|
||||||
const attTotal = att.total + attFlat;
|
const attTotal = att.total + attFlat;
|
||||||
const esqTotal = esq.total + esqFlat;
|
const esqTotal = esq.total + esqFlat;
|
||||||
@@ -51,7 +59,7 @@ function resolveAttack(attacker, defender, opts = {}) {
|
|||||||
};
|
};
|
||||||
if (result.hit) {
|
if (result.hit) {
|
||||||
const deg = rollDice(attacker.deg, 3);
|
const deg = rollDice(attacker.deg, 3);
|
||||||
const degFlat = attacker.degFlat || 0;
|
const degFlat = pickFlat(attacker, "deg", opts.magic);
|
||||||
result.rawDamage = (deg.total + (attacker.degBonus || 0) + degFlat) * (result.critical ? 2 : 1);
|
result.rawDamage = (deg.total + (attacker.degBonus || 0) + degFlat) * (result.critical ? 2 : 1);
|
||||||
// armure physique : fixe (base + équipement) + naturelle en D3 (achetée en PI)
|
// armure physique : fixe (base + équipement) + naturelle en D3 (achetée en PI)
|
||||||
// armure magique : effets de potions/parchemins (armorMag, peut être négative)
|
// armure magique : effets de potions/parchemins (armorMag, peut être négative)
|
||||||
@@ -164,20 +172,22 @@ const RACES = {
|
|||||||
/* ================= Bestiaire ================= */
|
/* ================= Bestiaire ================= */
|
||||||
|
|
||||||
/* Dégâts des monstres en D3, comme ceux des trolls.
|
/* Dégâts des monstres en D3, comme ceux des trolls.
|
||||||
* `armor` = armure physique, `armorMag` = armure magique (réduit seule les
|
* Comme l'armure, l'ATT et les DEG existent en physique (`att`, `deg`, `armor`)
|
||||||
* dégâts magiques). Valeurs provisoires à 0 — le bestiaire sera mis à jour
|
* et en magique (`attMag`, `degMag`, `armorMag`) : un monstre avec attMag et
|
||||||
* avec les vraies valeurs une fois le système validé. */
|
* degMag > 0 peut porter une attaque magique (attMag D6 vs ESQ, degMag D3,
|
||||||
|
* réduite par la seule armure magique). Valeurs magiques provisoires à 0 — le
|
||||||
|
* bestiaire sera mis à jour avec les vraies valeurs (réglables via l'admin). */
|
||||||
const MONSTER_TYPES = [
|
const MONSTER_TYPES = [
|
||||||
{ name: "Gobelin", emoji: "👺", level: 1, att: 2, esq: 2, deg: 2, pv: 12, armor: 0, armorMag: 0, vue: 4 },
|
{ name: "Gobelin", emoji: "👺", level: 1, att: 2, esq: 2, deg: 2, attMag: 0, degMag: 0, pv: 12, armor: 0, armorMag: 0, vue: 4 },
|
||||||
{ name: "Champignon Vénéneux", emoji: "🍄", level: 1, att: 3, esq: 1, deg: 3, pv: 10, armor: 0, armorMag: 0, vue: 1, static: true },
|
{ name: "Champignon Vénéneux", emoji: "🍄", level: 1, att: 3, esq: 1, deg: 3, attMag: 0, degMag: 0, pv: 10, armor: 0, armorMag: 0, vue: 1, static: true },
|
||||||
{ name: "Araignée Géante", emoji: "🕷️", level: 2, att: 3, esq: 3, deg: 3, pv: 16, armor: 0, armorMag: 0, vue: 5 },
|
{ name: "Araignée Géante", emoji: "🕷️", level: 2, att: 3, esq: 3, deg: 3, attMag: 0, degMag: 0, pv: 16, armor: 0, armorMag: 0, vue: 5 },
|
||||||
{ name: "Gargouille", emoji: "🦇", level: 3, att: 3, esq: 3, deg: 3, pv: 22, armor: 2, armorMag: 0, vue: 4 },
|
{ name: "Gargouille", emoji: "🦇", level: 3, att: 3, esq: 3, deg: 3, attMag: 0, degMag: 0, pv: 22, armor: 2, armorMag: 0, vue: 4 },
|
||||||
{ name: "Momie", emoji: "🧟", level: 3, att: 4, esq: 2, deg: 4, pv: 26, armor: 1, armorMag: 0, vue: 3 },
|
{ name: "Momie", emoji: "🧟", level: 3, att: 4, esq: 2, deg: 4, attMag: 0, degMag: 0, pv: 26, armor: 1, armorMag: 0, vue: 3 },
|
||||||
{ name: "Sorcière", emoji: "🧙", level: 4, att: 4, esq: 3, deg: 5, pv: 24, armor: 0, armorMag: 0, vue: 6 },
|
{ name: "Sorcière", emoji: "🧙", level: 4, att: 4, esq: 3, deg: 5, attMag: 0, degMag: 0, pv: 24, armor: 0, armorMag: 0, vue: 6 },
|
||||||
{ name: "Golem de Pierre", emoji: "🗿", level: 5, att: 4, esq: 1, deg: 6, pv: 40, armor: 4, armorMag: 0, vue: 3 },
|
{ name: "Golem de Pierre", emoji: "🗿", level: 5, att: 4, esq: 1, deg: 6, attMag: 0, degMag: 0, pv: 40, armor: 4, armorMag: 0, vue: 3 },
|
||||||
];
|
];
|
||||||
|
|
||||||
const BOSS = { name: "Béhémoth", emoji: "👹", level: 9, att: 6, esq: 4, deg: 8, pv: 90, armor: 3, armorMag: 0, vue: 8, boss: true };
|
const BOSS = { name: "Béhémoth", emoji: "👹", level: 9, att: 6, esq: 4, deg: 8, attMag: 0, degMag: 0, pv: 90, armor: 3, armorMag: 0, vue: 8, boss: true };
|
||||||
|
|
||||||
/* Gabarits d'âge façon MountyHall : plus on descend, plus les bêtes sont vieilles. */
|
/* Gabarits d'âge façon MountyHall : plus on descend, plus les bêtes sont vieilles. */
|
||||||
const TEMPLATES = [
|
const TEMPLATES = [
|
||||||
@@ -193,8 +203,10 @@ function applyTemplate(type, tpl, x, y) {
|
|||||||
name: tpl.prefix + type.name, emoji: type.emoji,
|
name: tpl.prefix + type.name, emoji: type.emoji,
|
||||||
level: Math.max(1, Math.round(type.level * tpl.mult)),
|
level: Math.max(1, Math.round(type.level * tpl.mult)),
|
||||||
att: Math.max(1, Math.round(type.att * tpl.mult)),
|
att: Math.max(1, Math.round(type.att * tpl.mult)),
|
||||||
|
attMag: Math.round((type.attMag || 0) * tpl.mult),
|
||||||
esq: Math.max(1, Math.round(type.esq * tpl.mult)),
|
esq: Math.max(1, Math.round(type.esq * tpl.mult)),
|
||||||
deg: Math.max(1, Math.round(type.deg * tpl.mult)),
|
deg: Math.max(1, Math.round(type.deg * tpl.mult)),
|
||||||
|
degMag: Math.round((type.degMag || 0) * tpl.mult),
|
||||||
pv: Math.round(type.pv * tpl.mult), pvMax: Math.round(type.pv * tpl.mult),
|
pv: Math.round(type.pv * tpl.mult), pvMax: Math.round(type.pv * tpl.mult),
|
||||||
armor: type.armor, armorMag: type.armorMag || 0, vue: type.vue, static: !!type.static,
|
armor: type.armor, armorMag: type.armorMag || 0, vue: type.vue, static: !!type.static,
|
||||||
x, y, boss: false,
|
x, y, boss: false,
|
||||||
@@ -739,10 +751,7 @@ function useComp() {
|
|||||||
if (!tryTalent(t.comp, 90, comp.cost, "🥋 Botte Secrète", "comp")) { cdFlush(); afterAction(); return; }
|
if (!tryTalent(t.comp, 90, comp.cost, "🥋 Botte Secrète", "comp")) { cdFlush(); afterAction(); return; }
|
||||||
t.compUsed = true;
|
t.compUsed = true;
|
||||||
cdLine(`Vous portez une <b>Botte Secrète</b> à <b>${m.name}</b>.`);
|
cdLine(`Vous portez une <b>Botte Secrète</b> à <b>${m.name}</b>.`);
|
||||||
const pseudo = {
|
const pseudo = { ...te, att: Math.max(1, Math.floor(te.att * 2 / 3)), deg: Math.max(1, Math.floor(te.att / 2)) };
|
||||||
att: Math.max(1, Math.floor(te.att * 2 / 3)), deg: Math.max(1, Math.floor(te.att / 2)),
|
|
||||||
degBonus: te.degBonus, attFlat: te.attFlat, degFlat: te.degFlat,
|
|
||||||
};
|
|
||||||
const r = resolveAttack(pseudo, effMonster(m));
|
const r = resolveAttack(pseudo, effMonster(m));
|
||||||
cdAttackRolls(r);
|
cdAttackRolls(r);
|
||||||
if (r.hit) {
|
if (r.hit) {
|
||||||
@@ -839,10 +848,7 @@ function useSort() {
|
|||||||
if (!m) { log("Aucun monstre adjacent.", "info"); return; }
|
if (!m) { log("Aucun monstre adjacent.", "info"); return; }
|
||||||
if (!tryTalent(t.sort, 80, sort.cost, "🔮 Vampirisme", "sort")) { cdFlush(); afterAction(); return; }
|
if (!tryTalent(t.sort, 80, sort.cost, "🔮 Vampirisme", "sort")) { cdFlush(); afterAction(); return; }
|
||||||
cdLine(`Vous avez attaqué <b>${m.name}</b> grâce à un sortilège.`);
|
cdLine(`Vous avez attaqué <b>${m.name}</b> grâce à un sortilège.`);
|
||||||
const pseudo = {
|
const pseudo = { ...te, att: Math.max(1, Math.floor(te.deg * 2 / 3)) };
|
||||||
att: Math.max(1, Math.floor(te.deg * 2 / 3)), deg: te.deg,
|
|
||||||
degBonus: te.degBonus, attFlat: te.attFlat, degFlat: te.degFlat,
|
|
||||||
};
|
|
||||||
const r = resolveAttack(pseudo, effMonster(m), { magic: true });
|
const r = resolveAttack(pseudo, effMonster(m), { magic: true });
|
||||||
cdAttackRolls(r);
|
cdAttackRolls(r);
|
||||||
if (r.hit) {
|
if (r.hit) {
|
||||||
@@ -891,8 +897,8 @@ function useSort() {
|
|||||||
if (!m) { log("Aucun monstre adjacent.", "info"); return; }
|
if (!m) { log("Aucun monstre adjacent.", "info"); return; }
|
||||||
if (!tryTalent(t.sort, 80, sort.cost, "🔮 Siphon des Âmes", "sort")) { cdFlush(); afterAction(); return; }
|
if (!tryTalent(t.sort, 80, sort.cost, "🔮 Siphon des Âmes", "sort")) { cdFlush(); afterAction(); return; }
|
||||||
cdLine(`Vous avez attaqué <b>${m.name}</b> grâce à un sortilège.`);
|
cdLine(`Vous avez attaqué <b>${m.name}</b> grâce à un sortilège.`);
|
||||||
const pseudo = { att: te.att, deg: te.reg, degBonus: 0, attFlat: te.attFlat, degFlat: te.degFlat };
|
const pseudo = { ...te, deg: te.reg, degBonus: 0 };
|
||||||
const r = resolveAttack(pseudo, effMonster(m), { ignoreArmor: true });
|
const r = resolveAttack(pseudo, effMonster(m), { ignoreArmor: true, magic: true });
|
||||||
cdAttackRolls(r);
|
cdAttackRolls(r);
|
||||||
if (r.hit) {
|
if (r.hit) {
|
||||||
let dmg = r.damage;
|
let dmg = r.damage;
|
||||||
@@ -1090,18 +1096,20 @@ function passDLA() {
|
|||||||
const seesTroll = !t.camo && dist <= effMonster(m).vue;
|
const seesTroll = !t.camo && dist <= effMonster(m).vue;
|
||||||
if (dist <= 1 && !t.camo) {
|
if (dist <= 1 && !t.camo) {
|
||||||
const eff = effMonster(m);
|
const eff = effMonster(m);
|
||||||
const r = resolveAttack(eff, effTroll(t));
|
// un monstre doté d'une attaque magique (attMag/degMag) alterne au hasard
|
||||||
cdStart(`${m.emoji} ${m.name} vous attaque`);
|
const magic = eff.attMag > 0 && eff.degMag > 0 && Math.random() < 0.5;
|
||||||
cdLine(`Son jet d'Attaque est de : <span class="cd-val">${r.attRoll}</span> (${r.attDice}D6)`);
|
const r = resolveAttack(magic ? { ...eff, att: eff.attMag, deg: eff.degMag } : eff, effTroll(t), { magic });
|
||||||
|
cdStart(`${m.emoji} ${m.name} vous ${magic ? "frappe d'une attaque magique" : "attaque"}`);
|
||||||
|
cdLine(`Son jet d'Attaque${magic ? " magique" : ""} est de : <span class="cd-val">${r.attRoll}</span> (${r.attDice}D6)`);
|
||||||
cdLine(`Votre jet d'Esquive est de : <span class="cd-val">${r.esqRoll}</span> (${r.esqDice}D6)`);
|
cdLine(`Votre jet d'Esquive est de : <span class="cd-val">${r.esqRoll}</span> (${r.esqDice}D6)`);
|
||||||
if (r.hit) {
|
if (r.hit) {
|
||||||
t.pv -= r.damage;
|
t.pv -= r.damage;
|
||||||
cdLine(`Il vous a <span class="cd-bad">TOUCHÉ</span>${r.critical ? ` d'un <span class="cd-bad">coup critique</span>` : ""} et vous a infligé <span class="cd-bad">${r.damage} points de dégâts</span>${r.armorReduction ? ` (votre armure a absorbé ${r.armorReduction} point(s))` : ""}.`);
|
cdLine(`Il vous a <span class="cd-bad">TOUCHÉ</span>${r.critical ? ` d'un <span class="cd-bad">coup critique</span>` : ""} et vous a infligé <span class="cd-bad">${r.damage} points de dégâts</span>${magic ? ` (seule votre armure magique compte${r.armorReduction ? ` : −${r.armorReduction}` : ""})` : r.armorReduction ? ` (votre armure a absorbé ${r.armorReduction} point(s))` : ""}.`);
|
||||||
log(`${m.emoji} ${m.name} t'attaque : ${r.attRoll} vs ${r.esqRoll} → ${r.damage} dégâts !`, "bad");
|
log(`${m.emoji} ${m.name} t'attaque${magic ? " (magie)" : ""} : ${r.attRoll} vs ${r.esqRoll} → ${r.damage} dégâts !`, "bad");
|
||||||
if (t.pv <= 0) { cdLine(`Il vous a <span class="cd-bad">TERRASSÉ</span>…`); cdFlush(); die(m); return; }
|
if (t.pv <= 0) { cdLine(`Il vous a <span class="cd-bad">TERRASSÉ</span>…`); cdFlush(); die(m); return; }
|
||||||
} else {
|
} else {
|
||||||
cdLine(`Vous avez <span class="cd-good">ESQUIVÉ</span> son coup.`);
|
cdLine(`Vous avez <span class="cd-good">ESQUIVÉ</span> son coup.`);
|
||||||
log(`${m.emoji} ${m.name} t'attaque… et tu esquives !`, "combat");
|
log(`${m.emoji} ${m.name} t'attaque${magic ? " (magie)" : ""}… et tu esquives !`, "combat");
|
||||||
}
|
}
|
||||||
cdFlush();
|
cdFlush();
|
||||||
} else if (seesTroll && !m.static) {
|
} else if (seesTroll && !m.static) {
|
||||||
|
|||||||
+11
-5
@@ -2,8 +2,12 @@
|
|||||||
* https://mountypedia.mountyhall.com/Mountyhall/{Arme,Armure,Casque,Bouclier,Talisman,Bottes}
|
* https://mountypedia.mountyhall.com/Mountyhall/{Arme,Armure,Casque,Bouclier,Talisman,Bottes}
|
||||||
* Valeurs de base (sans templates) : TOUS les bonus sont fixes — ATT/ESQ/DEG/REG
|
* Valeurs de base (sans templates) : TOUS les bonus sont fixes — ATT/ESQ/DEG/REG
|
||||||
* s'ajoutent aux jets (jamais en dés), ARM/VUE/PV sont fixes, RM/MM en %.
|
* s'ajoutent aux jets (jamais en dés), ARM/VUE/PV sont fixes, RM/MM en %.
|
||||||
* Les bonus/malus d'équipement sont PHYSIQUES (dont l'armure, sauf exceptions
|
* Comme l'armure, l'ATT et les DEG existent en physique et en magique :
|
||||||
* à venir) — les bonus magiques viennent des potions et parchemins.
|
* `att`/`deg`/`arm` = bonus physiques (appliqués aux attaques physiques,
|
||||||
|
* armure physique), `attMag`/`degMag`/`armMag` = bonus magiques (appliqués
|
||||||
|
* aux sortilèges, armure magique). ESQ/PV/VUE restent simples. Les valeurs
|
||||||
|
* magiques de la Mountypedia n'existant pas, elles sont à 0 en vanilla et se
|
||||||
|
* règlent via l'admin du monde partagé.
|
||||||
* `tier` (1-5) borne la profondeur de drop.
|
* `tier` (1-5) borne la profondeur de drop.
|
||||||
* Les armes à deux mains (twoHanded) sont incompatibles avec un bouclier. */
|
* Les armes à deux mains (twoHanded) sont incompatibles avec un bouclier. */
|
||||||
|
|
||||||
@@ -93,8 +97,10 @@ const LEGACY_WEAPON_NAMES = ["Épée Courte", "Gourdin", "Épée Longue", "Lame
|
|||||||
const LEGACY_ARMOR_NAMES = ["Armure de cuir", "Haubert de mailles", "Armure de plates"];
|
const LEGACY_ARMOR_NAMES = ["Armure de cuir", "Haubert de mailles", "Armure de plates"];
|
||||||
|
|
||||||
const GEAR_MOD_LABELS = [
|
const GEAR_MOD_LABELS = [
|
||||||
["att", "ATT", ""], ["esq", "ESQ", ""], ["deg", "DEG", ""],
|
["att", "ATT", ""], ["attMag", "ATT mag.", ""], ["esq", "ESQ", ""],
|
||||||
["reg", "REG", ""], ["arm", "Armure phy.", ""], ["vue", "VUE", ""], ["pv", "PV", ""],
|
["deg", "DEG", ""], ["degMag", "DEG mag.", ""], ["reg", "REG", ""],
|
||||||
|
["arm", "Armure phy.", ""], ["armMag", "Armure mag.", ""],
|
||||||
|
["vue", "VUE", ""], ["pv", "PV", ""],
|
||||||
["rmPct", "RM", " %"], ["mmPct", "MM", " %"],
|
["rmPct", "RM", " %"], ["mmPct", "MM", " %"],
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -136,7 +142,7 @@ function makeRandomGear(depth, slot = null) {
|
|||||||
|
|
||||||
/* Somme des modificateurs de l'équipement porté ({ arme: item|null, … }). */
|
/* Somme des modificateurs de l'équipement porté ({ arme: item|null, … }). */
|
||||||
function gearMods(equip) {
|
function gearMods(equip) {
|
||||||
const total = { att: 0, esq: 0, deg: 0, reg: 0, arm: 0, vue: 0, pv: 0, rmPct: 0, mmPct: 0 };
|
const total = { att: 0, attMag: 0, esq: 0, deg: 0, degMag: 0, reg: 0, arm: 0, armMag: 0, vue: 0, pv: 0, rmPct: 0, mmPct: 0 };
|
||||||
for (const item of Object.values(equip || {})) {
|
for (const item of Object.values(equip || {})) {
|
||||||
if (!item || !item.mods) continue;
|
if (!item || !item.mods) continue;
|
||||||
for (const k of Object.keys(total)) total[k] += item.mods[k] || 0;
|
for (const k of Object.keys(total)) total[k] += item.mods[k] || 0;
|
||||||
|
|||||||
+19
-14
@@ -409,9 +409,9 @@ function fmtStatLine(base, eff, faces, flat = 0, extra = 0, split = null) {
|
|||||||
let main = `${eff}D${faces}`;
|
let main = `${eff}D${faces}`;
|
||||||
if (jetBonus) main += jetBonus > 0 ? ` +${jetBonus}` : ` ${jetBonus}`;
|
if (jetBonus) main += jetBonus > 0 ? ` +${jetBonus}` : ` ${jetBonus}`;
|
||||||
const hints = [];
|
const hints = [];
|
||||||
if (split && split.phys && split.mag) {
|
if (split && (split.phys || split.mag) && split.phys !== split.mag) {
|
||||||
const f = v => `${v > 0 ? "+" : ""}${v}`;
|
const f = v => `${(v || 0) > 0 ? "+" : ""}${v || 0}`;
|
||||||
hints.push(`P ${f(split.phys)} · M ${f(split.mag)}`);
|
hints.push(`phy ${f(split.phys)} · mag ${f(split.mag)}`);
|
||||||
}
|
}
|
||||||
if (diceDelta !== 0) {
|
if (diceDelta !== 0) {
|
||||||
const sign = diceDelta > 0 ? "+" : "";
|
const sign = diceDelta > 0 ? "+" : "";
|
||||||
@@ -425,27 +425,32 @@ function fmtStatLine(base, eff, faces, flat = 0, extra = 0, split = null) {
|
|||||||
* + équipement (troll.gearMods, recalculé par game.js à chaque équipement).
|
* + équipement (troll.gearMods, recalculé par game.js à chaque équipement).
|
||||||
* Les bonus ATT/ESQ/DEG/REG de l'équipement sont des bonus FIXES sur les jets,
|
* Les bonus ATT/ESQ/DEG/REG de l'équipement sont des bonus FIXES sur les jets,
|
||||||
* jamais des dés supplémentaires.
|
* jamais des dés supplémentaires.
|
||||||
* Bonus/malus PHYSIQUES (équipement) et MAGIQUES (potions, parchemins) sont
|
* Comme l'armure, ATT et DEG existent en deux saveurs : xxxFlatPhys s'applique
|
||||||
* décomposés (xxxFlatPhys / xxxFlatMag) ; de même pour l'armure :
|
* aux attaques PHYSIQUES, xxxFlatMag aux attaques MAGIQUES (sortilèges). Les
|
||||||
* armorPhys = base + naturelle + équipement, armorMag = effets magiques.
|
* potions/parchemins modifient le troll lui-même : leurs bonus comptent dans
|
||||||
* Les dégâts physiques sont réduits par l'armure totale, les dégâts magiques
|
* les deux ; l'équipement compte selon la saveur de chaque bonus (att/deg/arm
|
||||||
* par la seule armure magique. */
|
* physiques, attMag/degMag/armMag magiques). Armure :
|
||||||
|
* armorPhys = base + naturelle + équipement phys, armorMag = effets magiques
|
||||||
|
* + équipement mag. Les dégâts physiques sont réduits par l'armure totale,
|
||||||
|
* les dégâts magiques par la seule armure magique. */
|
||||||
function effTroll(troll) {
|
function effTroll(troll) {
|
||||||
const m = sumPotionMods(troll.potionEffects); // magique
|
const m = sumPotionMods(troll.potionEffects); // magique (modifie le troll)
|
||||||
const g = troll.gearMods || {}; // physique
|
const g = troll.gearMods || {}; // équipement (phys + mag)
|
||||||
return {
|
return {
|
||||||
att: Math.max(1, troll.att + m.att),
|
att: Math.max(1, troll.att + m.att),
|
||||||
attFlat: m.attFlat + (g.att || 0), attFlatPhys: g.att || 0, attFlatMag: m.attFlat,
|
attFlat: m.attFlat + (g.att || 0),
|
||||||
|
attFlatPhys: m.attFlat + (g.att || 0), attFlatMag: m.attFlat + (g.attMag || 0),
|
||||||
esq: Math.max(1, troll.esq + m.esq),
|
esq: Math.max(1, troll.esq + m.esq),
|
||||||
esqFlat: m.esqFlat + (g.esq || 0), esqFlatPhys: g.esq || 0, esqFlatMag: m.esqFlat,
|
esqFlat: m.esqFlat + (g.esq || 0), esqFlatPhys: g.esq || 0, esqFlatMag: m.esqFlat,
|
||||||
deg: Math.max(1, troll.deg + m.deg),
|
deg: Math.max(1, troll.deg + m.deg),
|
||||||
degFlat: m.degFlat + (g.deg || 0), degFlatPhys: g.deg || 0, degFlatMag: m.degFlat,
|
degFlat: m.degFlat + (g.deg || 0),
|
||||||
|
degFlatPhys: m.degFlat + (g.deg || 0), degFlatMag: m.degFlat + (g.degMag || 0),
|
||||||
reg: Math.max(1, troll.reg + m.reg),
|
reg: Math.max(1, troll.reg + m.reg),
|
||||||
regFlat: m.regFlat + (g.reg || 0), regFlatPhys: g.reg || 0, regFlatMag: m.regFlat,
|
regFlat: m.regFlat + (g.reg || 0), regFlatPhys: g.reg || 0, regFlatMag: m.regFlat,
|
||||||
vue: Math.max(1, troll.vue + m.vue + (g.vue || 0)),
|
vue: Math.max(1, troll.vue + m.vue + (g.vue || 0)),
|
||||||
armor: Math.max(0, troll.armor + m.armor + (g.arm || 0)),
|
armor: Math.max(0, troll.armor + m.armor + (g.arm || 0) + (g.armMag || 0)),
|
||||||
armorPhys: Math.max(0, troll.armor + (g.arm || 0)),
|
armorPhys: Math.max(0, troll.armor + (g.arm || 0)),
|
||||||
armorMag: m.armor,
|
armorMag: m.armor + (g.armMag || 0),
|
||||||
armorDice: troll.armorDice,
|
armorDice: troll.armorDice,
|
||||||
degBonus: troll.degBonus || 0,
|
degBonus: troll.degBonus || 0,
|
||||||
pvMax: troll.pvMax,
|
pvMax: troll.pvMax,
|
||||||
|
|||||||
@@ -55,9 +55,9 @@ const CONFIG_BOUNDS = {
|
|||||||
* world.tuning ne stocke que les écarts à la valeur de base (vanilla). Il
|
* world.tuning ne stocke que les écarts à la valeur de base (vanilla). Il
|
||||||
* s'applique aux nouveaux spawns/drops du monde partagé — le solo reste vanilla. */
|
* s'applique aux nouveaux spawns/drops du monde partagé — le solo reste vanilla. */
|
||||||
|
|
||||||
const MONSTER_TUNE_KEYS = ["level", "att", "esq", "deg", "pv", "armor", "armorMag", "vue"];
|
const MONSTER_TUNE_KEYS = ["level", "att", "attMag", "esq", "deg", "degMag", "pv", "armor", "armorMag", "vue"];
|
||||||
const MONSTER_TUNE_BOUNDS = { level: [1, 99], att: [1, 99], esq: [1, 99], deg: [1, 99], pv: [1, 999], armor: [0, 99], armorMag: [0, 99], vue: [1, 30] };
|
const MONSTER_TUNE_BOUNDS = { level: [1, 99], att: [1, 99], attMag: [0, 99], esq: [1, 99], deg: [1, 99], degMag: [0, 99], pv: [1, 999], armor: [0, 99], armorMag: [0, 99], vue: [1, 30] };
|
||||||
const GEAR_TUNE_KEYS = ["att", "esq", "deg", "reg", "arm", "vue", "pv", "rmPct", "mmPct"];
|
const GEAR_TUNE_KEYS = ["att", "attMag", "esq", "deg", "degMag", "reg", "arm", "armMag", "vue", "pv", "rmPct", "mmPct"];
|
||||||
const GEAR_TUNE_BOUNDS = [-100, 100];
|
const GEAR_TUNE_BOUNDS = [-100, 100];
|
||||||
const POWER_BOUNDS = [0, 200];
|
const POWER_BOUNDS = [0, 200];
|
||||||
|
|
||||||
@@ -331,13 +331,15 @@ function monsterAct(world, m, now) {
|
|||||||
}
|
}
|
||||||
if (best && bestDist <= 1) {
|
if (best && bestDist <= 1) {
|
||||||
const te = effTrollMP(best);
|
const te = effTrollMP(best);
|
||||||
const r = g.resolveAttack(em, te);
|
// un monstre doté d'une attaque magique (attMag/degMag) alterne au hasard
|
||||||
|
const magic = em.attMag > 0 && em.degMag > 0 && Math.random() < 0.5;
|
||||||
|
const r = g.resolveAttack(magic ? { ...em, att: em.attMag, deg: em.degMag } : em, te, { magic });
|
||||||
if (r.hit) {
|
if (r.hit) {
|
||||||
best.pv -= r.damage;
|
best.pv -= r.damage;
|
||||||
privLog(best, `${m.emoji} ${m.name} t'attaque : ${r.attRoll} vs ${r.esqRoll} → ${r.damage} dégâts${r.armorReduction ? ` (armure −${r.armorReduction})` : ""} !`, "bad");
|
privLog(best, `${m.emoji} ${m.name} t'attaque${magic ? " (magie)" : ""} : ${r.attRoll} vs ${r.esqRoll} → ${r.damage} dégâts${r.armorReduction ? ` (armure${magic ? " magique" : ""} −${r.armorReduction})` : ""} !`, "bad");
|
||||||
if (best.pv <= 0) killTrollMP(world, best, m.name, now);
|
if (best.pv <= 0) killTrollMP(world, best, m.name, now);
|
||||||
} else {
|
} else {
|
||||||
privLog(best, `${m.emoji} ${m.name} t'attaque… et tu esquives !`, "combat");
|
privLog(best, `${m.emoji} ${m.name} t'attaque${magic ? " (magie)" : ""}… et tu esquives !`, "combat");
|
||||||
}
|
}
|
||||||
} else if (best && bestDist <= em.vue) {
|
} else if (best && bestDist <= em.vue) {
|
||||||
stepTowardMP(world, m, best);
|
stepTowardMP(world, m, best);
|
||||||
@@ -497,10 +499,7 @@ function actComp(world, t, m) {
|
|||||||
if (!res.ok) return res;
|
if (!res.ok) return res;
|
||||||
pxOnce();
|
pxOnce();
|
||||||
t.compUsed = true;
|
t.compUsed = true;
|
||||||
const pseudo = {
|
const pseudo = { ...te, att: Math.max(1, Math.floor(te.att * 2 / 3)), deg: Math.max(1, Math.floor(te.att / 2)) };
|
||||||
att: Math.max(1, Math.floor(te.att * 2 / 3)), deg: Math.max(1, Math.floor(te.att / 2)),
|
|
||||||
degBonus: te.degBonus, attFlat: te.attFlat, degFlat: te.degFlat,
|
|
||||||
};
|
|
||||||
const r = g.resolveAttack(pseudo, monsterEff(m));
|
const r = g.resolveAttack(pseudo, monsterEff(m));
|
||||||
if (r.hit) {
|
if (r.hit) {
|
||||||
m.pv -= r.damage;
|
m.pv -= r.damage;
|
||||||
@@ -583,10 +582,7 @@ function actSort(world, t, m) {
|
|||||||
const res = tryTalentMP(t, t.sort, 80, sort.cost);
|
const res = tryTalentMP(t, t.sort, 80, sort.cost);
|
||||||
if (!res.ok) return res;
|
if (!res.ok) return res;
|
||||||
pxOnce();
|
pxOnce();
|
||||||
const pseudo = {
|
const pseudo = { ...te, att: Math.max(1, Math.floor(te.deg * 2 / 3)) };
|
||||||
att: Math.max(1, Math.floor(te.deg * 2 / 3)), deg: te.deg,
|
|
||||||
degBonus: te.degBonus, attFlat: te.attFlat, degFlat: te.degFlat,
|
|
||||||
};
|
|
||||||
const r = g.resolveAttack(pseudo, monsterEff(m), { magic: true });
|
const r = g.resolveAttack(pseudo, monsterEff(m), { magic: true });
|
||||||
if (r.hit) {
|
if (r.hit) {
|
||||||
let dmg = r.damage;
|
let dmg = r.damage;
|
||||||
@@ -625,8 +621,8 @@ function actSort(world, t, m) {
|
|||||||
const res = tryTalentMP(t, t.sort, 80, sort.cost);
|
const res = tryTalentMP(t, t.sort, 80, sort.cost);
|
||||||
if (!res.ok) return res;
|
if (!res.ok) return res;
|
||||||
pxOnce();
|
pxOnce();
|
||||||
const pseudo = { att: te.att, deg: te.reg, degBonus: 0, attFlat: te.attFlat, degFlat: te.degFlat };
|
const pseudo = { ...te, deg: te.reg, degBonus: 0 };
|
||||||
const r = g.resolveAttack(pseudo, monsterEff(m), { ignoreArmor: true });
|
const r = g.resolveAttack(pseudo, monsterEff(m), { ignoreArmor: true, magic: true });
|
||||||
if (r.hit) {
|
if (r.hit) {
|
||||||
let dmg = r.damage;
|
let dmg = r.damage;
|
||||||
const resisted = resistMP(t, m);
|
const resisted = resistMP(t, m);
|
||||||
@@ -937,6 +933,15 @@ function adminSetTuning(world, patch) {
|
|||||||
return world.tuning;
|
return world.tuning;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Supprime définitivement un troll du monde (admin). */
|
||||||
|
function adminKickTroll(world, id) {
|
||||||
|
const t = world.trolls[id];
|
||||||
|
if (!t) return { error: "troll inconnu" };
|
||||||
|
delete world.trolls[id];
|
||||||
|
worldLog(world, `⚡ Les Dieux Trõlls ont rappelé ${t.name} hors du Monde Souterrain.`);
|
||||||
|
return { ok: true, name: t.name };
|
||||||
|
}
|
||||||
|
|
||||||
/* Régénère le monde (carte, monstres, trésors) en conservant trolls et config ;
|
/* Régénère le monde (carte, monstres, trésors) en conservant trolls et config ;
|
||||||
* les trolls sont replacés. */
|
* les trolls sont replacés. */
|
||||||
function adminResetWorld(world) {
|
function adminResetWorld(world) {
|
||||||
@@ -978,7 +983,7 @@ function loadWorld(file) {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
DEFAULT_CONFIG, CONFIG_BOUNDS,
|
DEFAULT_CONFIG, CONFIG_BOUNDS,
|
||||||
createWorld, tick, newTroll, authTroll, login, action, stateFor,
|
createWorld, tick, newTroll, authTroll, login, action, stateFor,
|
||||||
adminOverview, adminSetConfig, adminSetTuning, adminDefaults, adminResetWorld,
|
adminOverview, adminSetConfig, adminSetTuning, adminDefaults, adminResetWorld, adminKickTroll,
|
||||||
saveWorld, loadWorld,
|
saveWorld, loadWorld,
|
||||||
spawnMonster, spawnItem, monsterAct, trollDla, worldLog,
|
spawnMonster, spawnItem, monsterAct, trollDla, worldLog,
|
||||||
tunedRandomPotion, tunedRandomScroll, applyGearTuning,
|
tunedRandomPotion, tunedRandomScroll, applyGearTuning,
|
||||||
|
|||||||
@@ -230,6 +230,14 @@ function handleMP(req, res, url) {
|
|||||||
return sendJSON(res, 200, tuning);
|
return sendJSON(res, 200, tuning);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (req.method === "POST" && url.pathname === "/api/mp/admin/kick") {
|
||||||
|
return readBody(req, res, body => {
|
||||||
|
const r = mp.adminKickTroll(WORLD, body.id);
|
||||||
|
if (r.error) return sendJSON(res, 404, r);
|
||||||
|
worldDirty = true;
|
||||||
|
return sendJSON(res, 200, r);
|
||||||
|
});
|
||||||
|
}
|
||||||
if (req.method === "POST" && url.pathname === "/api/mp/admin/reset") {
|
if (req.method === "POST" && url.pathname === "/api/mp/admin/reset") {
|
||||||
mp.adminResetWorld(WORLD);
|
mp.adminResetWorld(WORLD);
|
||||||
worldDirty = true;
|
worldDirty = true;
|
||||||
|
|||||||
+27
@@ -291,6 +291,33 @@ function makeWorld(over = {}) {
|
|||||||
assert.strictEqual(defs.monsters.length, 8, "7 types + boss");
|
assert.strictEqual(defs.monsters.length, 8, "7 types + boss");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tuning admin : saveurs magiques (attMag/degMag/armMag) et suppression de troll
|
||||||
|
{
|
||||||
|
const w = makeWorld({ monsterTarget: 0, itemTarget: 0, worldDepth: 1 });
|
||||||
|
mp.adminSetTuning(w, {
|
||||||
|
monsters: { "Gobelin": { attMag: 6, degMag: 4 } },
|
||||||
|
gear: { "arme/Bâton de mage": { attMag: 4, armMag: 2 } },
|
||||||
|
});
|
||||||
|
assert.strictEqual(w.tuning.monsters.Gobelin.attMag, 6, "ATT mag du Gobelin tunée");
|
||||||
|
assert.strictEqual(w.tuning.gear["arme/Bâton de mage"].armMag, 2, "Armure mag du bâton tunée");
|
||||||
|
let gob = null;
|
||||||
|
for (let i = 0; i < 300 && !gob; i++) {
|
||||||
|
const m = mp.spawnMonster(w);
|
||||||
|
if (m && m.name.includes("Gobelin")) gob = m;
|
||||||
|
}
|
||||||
|
assert(gob && gob.attMag > 0 && gob.degMag > 0, "le Gobelin spawné a une attaque magique");
|
||||||
|
const gearLib = require("../js/gear.js");
|
||||||
|
const baton = mp.applyGearTuning(w, gearLib.gearItemByName("arme", "Bâton de mage"));
|
||||||
|
assert.strictEqual(baton.mods.attMag, 4, "ATT mag du bâton droppé");
|
||||||
|
assert.strictEqual(baton.mods.mmPct, 15, "MM % vanilla conservé");
|
||||||
|
// suppression admin d'un troll
|
||||||
|
const r = mp.newTroll(w, "Banni", "Skrim");
|
||||||
|
assert(mp.adminKickTroll(w, "inconnu").error, "kick d'un id inconnu refusé");
|
||||||
|
assert(mp.adminKickTroll(w, r.troll.id).ok, "troll supprimé");
|
||||||
|
assert(!w.trolls[r.troll.id], "le troll n'existe plus");
|
||||||
|
assert(!mp.newTroll(w, "Banni", "Skrim").error, "son nom redevient libre");
|
||||||
|
}
|
||||||
|
|
||||||
// Persistance : save + load
|
// Persistance : save + load
|
||||||
{
|
{
|
||||||
const os = require("os");
|
const os = require("os");
|
||||||
|
|||||||
+48
-8
@@ -112,15 +112,55 @@ assert.strictEqual(g.improveCost("esq", 1, "Tomawak"), 32, "2e dé d'ESQ = 32 PI
|
|||||||
assert.strictEqual(e.armorPhys, 4, "Armure de cuir : armure physique +4");
|
assert.strictEqual(e.armorPhys, 4, "Armure de cuir : armure physique +4");
|
||||||
assert.strictEqual(e.armorMag, 5, "Extrait du Glacier : armure magique +5");
|
assert.strictEqual(e.armorMag, 5, "Extrait du Glacier : armure magique +5");
|
||||||
assert.strictEqual(e.armor, 9, "armure totale = physique + magique");
|
assert.strictEqual(e.armor, 9, "armure totale = physique + magique");
|
||||||
// décomposition des bonus fixes : équipement = physique, potion = magique
|
// décomposition par type d'attaque : l'équipement compte selon sa saveur,
|
||||||
g.equipGear(t, gearLib.gearItemByName("arme", "Gourdin")); // att +2 (physique)
|
// les potions modifient le troll et comptent dans les deux
|
||||||
p.drinkPotion(t, p.makePotionItem("bonneBouffe", 5), g.rollDice, () => {}); // DEG +5 (magique)
|
g.equipGear(t, gearLib.gearItemByName("arme", "Gourdin")); // att +2, deg +5 (physiques)
|
||||||
|
p.drinkPotion(t, p.makePotionItem("bonneBouffe", 5), g.rollDice, () => {}); // DEG +5 (potion)
|
||||||
const e2 = p.effTroll(t);
|
const e2 = p.effTroll(t);
|
||||||
assert.strictEqual(e2.attFlatPhys, 2, "Gourdin : ATT +2 physique");
|
assert.strictEqual(e2.attFlatPhys, 2, "attaque physique : ATT +2 du Gourdin");
|
||||||
assert.strictEqual(e2.attFlatMag, 0, "aucun bonus magique d'ATT");
|
assert.strictEqual(e2.attFlatMag, 0, "attaque magique : pas de bonus d'ATT");
|
||||||
assert.strictEqual(e2.degFlatPhys, 5, "Gourdin : DEG +5 physique");
|
assert.strictEqual(e2.degFlatPhys, 10, "attaque physique : DEG +5 Gourdin +5 Bonne Bouffe");
|
||||||
assert.strictEqual(e2.degFlatMag, 5, "Bonne Bouffe : DEG +5 magique");
|
assert.strictEqual(e2.degFlatMag, 5, "attaque magique : seule la Bonne Bouffe compte");
|
||||||
assert.strictEqual(e2.degFlat, 10, "total = physique + magique");
|
assert.strictEqual(e2.degFlat, 10, "degFlat = bonus des attaques physiques");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ATT/DEG/ARM phys et mag partout : équipement magique et choix du bonus selon l'attaque
|
||||||
|
{
|
||||||
|
const gearLib = require("../js/gear.js");
|
||||||
|
const t = { att: 3, esq: 3, deg: 3, reg: 1, vue: 3, armor: 0, armorDice: 0, degBonus: 0, pvMax: 30, pv: 20,
|
||||||
|
potionEffects: [], blockCamoTurns: 0, tour: 1, bag: [],
|
||||||
|
equip: { arme: null, armure: null, casque: null, bouclier: null, talisman: null, bottes: null }, gearMods: null };
|
||||||
|
// objet retouché façon admin : bonus magiques sur une arme
|
||||||
|
const baton = gearLib.gearItemByName("arme", "Bâton de mage");
|
||||||
|
baton.mods = { ...baton.mods, attMag: 4, degMag: 6, armMag: 2 };
|
||||||
|
g.equipGear(t, baton);
|
||||||
|
const e = p.effTroll(t);
|
||||||
|
assert.strictEqual(e.attFlatPhys, 0, "le bâton ne donne rien aux attaques physiques");
|
||||||
|
assert.strictEqual(e.attFlatMag, 4, "ATT mag +4 sur les sortilèges");
|
||||||
|
assert.strictEqual(e.degFlatMag, 6, "DEG mag +6 sur les sortilèges");
|
||||||
|
assert.strictEqual(e.armorMag, 2, "Armure mag +2 d'équipement");
|
||||||
|
assert.strictEqual(e.armorPhys, 0, "pas d'armure physique");
|
||||||
|
// resolveAttack choisit le bon bonus selon le type d'attaque
|
||||||
|
const dummy = { esq: 1, armor: 0, armorMag: 0 };
|
||||||
|
const rPhys = g.resolveAttack(e, dummy, { autoHit: true });
|
||||||
|
const rMag = g.resolveAttack(e, dummy, { autoHit: true, magic: true });
|
||||||
|
assert.strictEqual(rPhys.attFlat, 0, "jet physique : ATT sans bonus");
|
||||||
|
assert.strictEqual(rMag.attFlat, 4, "jet magique : ATT +4");
|
||||||
|
assert(rMag.rawDamage >= 1 * 1 + 6 + 0 && rMag.rawDamage <= 3 * 3 * 2 + 12, "dégâts magiques avec DEG mag");
|
||||||
|
// le formatage des mods affiche les saveurs magiques
|
||||||
|
const label = gearLib.formatGearMods(baton.mods);
|
||||||
|
assert(label.includes("ATT mag. +4") && label.includes("Armure mag. +2"), "libellés magiques : " + label);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bestiaire : attMag/degMag présents et multipliés par les gabarits d'âge
|
||||||
|
{
|
||||||
|
const gob = g.MONSTER_TYPES.find(t2 => t2.name === "Gobelin");
|
||||||
|
const tuned = { ...gob, attMag: 4, degMag: 3 };
|
||||||
|
const old2 = g.applyTemplate(tuned, { prefix: "Mythique ", mult: 2.0 }, 1, 1);
|
||||||
|
assert.strictEqual(old2.attMag, 8, "ATT mag ×2 (Mythique)");
|
||||||
|
assert.strictEqual(old2.degMag, 6, "DEG mag ×2 (Mythique)");
|
||||||
|
const vanilla = g.applyTemplate(gob, { prefix: "Mythique ", mult: 2.0 }, 1, 1);
|
||||||
|
assert.strictEqual(vanilla.attMag, 0, "vanilla : pas d'attaque magique");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Monstres : armure physique et magique distinctes
|
// Monstres : armure physique et magique distinctes
|
||||||
|
|||||||
Reference in New Issue
Block a user