1.9.0 : bonus/malus physiques (équipement) vs magiques (potions/parchemins), armure physique et magique distinctes (troll + monstres)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
perco
2026-06-12 10:22:18 +02:00
co-authored by Claude Fable 5
parent 231e4d3da3
commit e5a4148ee8
7 changed files with 169 additions and 56 deletions
+13
View File
@@ -39,6 +39,8 @@ lance directement une partie (utilisé pour les captures d'écran et les tests).
| Combat | Somme ATT D6 vs somme ESQ D6, dégâts DEG D3 armure, coup critique (attaque ≥ 2 × esquive) aux dégâts doublés | Identique, avec un panneau « Détail du combat » au format des rapports MH (jets, Seuil de Résistance, jet de Résistance, maîtrise, butin, PX) à droite du journal | | Combat | Somme ATT D6 vs somme ESQ D6, dégâts DEG D3 armure, coup critique (attaque ≥ 2 × esquive) aux dégâts doublés | Identique, avec un panneau « Détail du combat » au format des rapports MH (jets, Seuil de Résistance, jet de Résistance, maîtrise, butin, PX) à droite du journal |
| Talents | Compétence + sortilège réservés par race, 15 % de maîtrise initiale, jet D100 sous le %, progression à l'usage (+1D6 % < 50, +1D3 % < 75, +1 % ensuite ; sous 50 %, un échec donne quand même +1 %), plafonds 90 % compétences / 80 % sortilèges, échec = PA partiellement remboursés | Identique | | Talents | Compétence + sortilège réservés par race, 15 % de maîtrise initiale, jet D100 sous le %, progression à l'usage (+1D6 % < 50, +1D3 % < 75, +1 % ensuite ; sous 50 %, un échec donne quand même +1 %), plafonds 90 % compétences / 80 % sortilèges, échec = PA partiellement remboursés | Identique |
| Magie | Seuil de Résistance basé sur MM vs RM, borné 1090 % | Identique : jet de résistance des monstres → effets de moitié | | Magie | Seuil de Résistance basé sur MM vs RM, borné 1090 % | Identique : jet de résistance des monstres → effets de moitié |
| Bonus/malus | Physiques (équipement) et magiques (potions, sortilèges…) distincts | Identique : équipement = physique (exceptions à venir), potions/parchemins = magique ; décomposition « P/M » affichée dans le profil |
| Armure | L'armure réduit les dégâts physiques ; seule sa composante magique réduit les dégâts magiques | Identique : armure physique (naturelle en D3 + équipement) + armure magique (potions/parchemins) — troll et monstres |
| Progression | PX → PI, le N-ième achat coûte N × coût de base racial (table de Rules_3.php) | Identique : 16 PI de base (12 pour la caractéristique favorite), REG 30 (Darkling 22), Armure naturelle 30 pour tous, PV par tranche de 10 | | Progression | PX → PI, le N-ième achat coûte N × coût de base racial (table de Rules_3.php) | Identique : 16 PI de base (12 pour la caractéristique favorite), REG 30 (Darkling 22), Armure naturelle 30 pour tous, PV par tranche de 10 |
| Niveaux | 10 × N PI pour atteindre le niveau N | Identique | | Niveaux | 10 × N PI pour atteindre le niveau N | Identique |
| Races | 5 races, chacune avec sort/compétence propre | Les 5 races avec leur capacité signature (voir ci-dessous) | | Races | 5 races, chacune avec sort/compétence propre | Les 5 races avec leur capacité signature (voir ci-dessous) |
@@ -152,6 +154,17 @@ non affilié au jeu original de Mountyhall SARL.
## Versions ## Versions
- **1.9.0** (2026-06-12) — Bonus/malus **physiques** et **magiques** distincts, comme
à MountyHall : l'équipement donne des bonus physiques (exceptions à venir), les
potions et parchemins des bonus magiques. L'armure est scindée en **armure
physique** (base + naturelle en D3 + équipement) et **armure magique** (effets
de potions/parchemins, ex. Extrait du Glacier) : les dégâts physiques sont
réduits par l'armure totale, les dégâts magiques (Rafale Psychique, Vampirisme,
Projectile Magique, Rune Explosive) par la seule armure magique — le Siphon des
Âmes continue d'ignorer toute armure. Les monstres ont eux aussi une armure
physique et une armure magique distinctes (valeurs du bestiaire à venir).
Affichage : Armure phy./mag. séparées dans le profil, décomposition « P/M » des
bonus fixes, absorption d'armure détaillée dans les rapports de combat.
- **1.8.0** (2026-06-12) — Habillage « pack parchemin » officiel de MountyHall - **1.8.0** (2026-06-12) — Habillage « pack parchemin » officiel de MountyHall
(https://www.mountyhall.com/MH_Pack/packMH_parchemin/) : fond lin, tableaux (https://www.mountyhall.com/MH_Pack/packMH_parchemin/) : fond lin, tableaux
crème à bordure noire (tableau1/2.jpg), liens rouge sombre #990000, boutons crème à bordure noire (tableau1/2.jpg), liens rouge sombre #990000, boutons
+56 -36
View File
@@ -4,7 +4,7 @@
"use strict"; "use strict";
const APP_VERSION = "1.8.0"; const APP_VERSION = "1.9.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;
@@ -31,7 +31,10 @@ function rollDice(n, faces) {
/* Jet d'attaque MountyHall : somme de ATT D6 contre somme de ESQ D6. /* Jet d'attaque MountyHall : somme de ATT D6 contre somme de ESQ D6.
* Si l'attaque dépasse l'esquive, dégâts = DEG D3 (+ bonus d'arme) armure. * Si l'attaque dépasse l'esquive, dégâts = DEG D3 (+ bonus d'arme) armure.
* (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
* (physique + magique) ; les dégâts MAGIQUES (opts.magic) par la seule armure
* magique ; opts.ignoreArmor ignore tout (Siphon des Âmes). */
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);
@@ -50,9 +53,16 @@ function resolveAttack(attacker, defender, opts = {}) {
const deg = rollDice(attacker.deg, 3); const deg = rollDice(attacker.deg, 3);
const degFlat = attacker.degFlat || 0; const degFlat = attacker.degFlat || 0;
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 : réduction fixe (équipement) + armure naturelle en D3 (achetée en PI) // armure physique : fixe (base + équipement) + naturelle en D3 (achetée en PI)
const armor = opts.ignoreArmor ? 0 // armure magique : effets de potions/parchemins (armorMag, peut être négative)
: (defender.armor || 0) + (defender.armorDice ? rollDice(defender.armorDice, 3).total : 0); let armor = 0;
if (!opts.ignoreArmor) {
const phys = (defender.armorPhys != null ? defender.armorPhys : defender.armor || 0)
+ (defender.armorDice ? rollDice(defender.armorDice, 3).total : 0);
const mag = defender.armorMag || 0;
armor = Math.max(0, opts.magic ? mag : phys + mag);
}
result.armorReduction = armor;
result.damage = Math.max(1, result.rawDamage - armor); result.damage = Math.max(1, result.rawDamage - armor);
} }
return result; return result;
@@ -126,21 +136,21 @@ const RACES = {
desc: "Les tanks du Hall, durs au mal et infatigables.", desc: "Les tanks du Hall, durs au mal et infatigables.",
stats: { att: 3, esq: 3, deg: 3, reg: 1, pvMax: 40, vue: 3 }, stats: { att: 3, esq: 3, deg: 3, reg: 1, pvMax: 40, vue: 3 },
comp: { name: "Régénération Accrue", cost: 2, desc: "Soigne immédiatement 1D3 par tranche de 15 PV max." }, comp: { name: "Régénération Accrue", cost: 2, desc: "Soigne immédiatement 1D3 par tranche de 15 PV max." },
sort: { name: "Rafale Psychique", cost: 4, desc: "Touche automatiquement (imparable) : 1D3 par dé de DEG, ignore l'armure (résistance : moitié)." }, sort: { name: "Rafale Psychique", cost: 4, desc: "Touche automatiquement (imparable) : 1D3 par dé de DEG, ignore l'armure physique (résistance : moitié)." },
}, },
Kastar: { Kastar: {
emoji: "🔴", favored: "deg", emoji: "🔴", favored: "deg",
desc: "Les vampires du Hall : leurs coups les nourrissent.", desc: "Les vampires du Hall : leurs coups les nourrissent.",
stats: { att: 3, esq: 3, deg: 4, reg: 1, pvMax: 30, vue: 3 }, stats: { att: 3, esq: 3, deg: 4, reg: 1, pvMax: 30, vue: 3 },
comp: { name: "Accélération du Métabolisme", cost: 2, desc: "Sacrifie des PV (1D3 + fatigue) pour regagner 4 PA. La fatigue monte à chaque usage." }, comp: { name: "Accélération du Métabolisme", cost: 2, desc: "Sacrifie des PV (1D3 + fatigue) pour regagner 4 PA. La fatigue monte à chaque usage." },
sort: { name: "Vampirisme", cost: 4, desc: "Cible adjacente : 2D6 par tranche de 3 dés de DEG vs ESQ, dégâts 1D3 par dé de DEG, ignore l'armure, soigne 50 % (résistance : moitié)." }, sort: { name: "Vampirisme", cost: 4, desc: "Cible adjacente : 2D6 par tranche de 3 dés de DEG vs ESQ, dégâts 1D3 par dé de DEG, ignore l'armure physique, soigne 50 % (résistance : moitié)." },
}, },
Tomawak: { Tomawak: {
emoji: "🟡", favored: "vue", emoji: "🟡", favored: "vue",
desc: "Trõlls furtifs, chasseurs embusqués aux yeux perçants.", desc: "Trõlls furtifs, chasseurs embusqués aux yeux perçants.",
stats: { att: 3, esq: 3, deg: 3, reg: 1, pvMax: 30, vue: 4 }, stats: { att: 3, esq: 3, deg: 3, reg: 1, pvMax: 30, vue: 4 },
comp: { name: "Camouflage", cost: 2, desc: "Invisible aux monstres tant que tu n'attaques pas ; à chaque pas, jet sous 75 % de la maîtrise pour rester caché." }, comp: { name: "Camouflage", cost: 2, desc: "Invisible aux monstres tant que tu n'attaques pas ; à chaque pas, jet sous 75 % de la maîtrise pour rester caché." },
sort: { name: "Projectile Magique", cost: 4, desc: "À distance (portée = Vue) : 1D6 par case de Vue + bonus de proximité, dégâts 1D3 par tranche de 2 cases de Vue, ignore l'armure (résistance : moitié)." }, sort: { name: "Projectile Magique", cost: 4, desc: "À distance (portée = Vue) : 1D6 par case de Vue + bonus de proximité, dégâts 1D3 par tranche de 2 cases de Vue, ignore l'armure physique (résistance : moitié)." },
}, },
Darkling: { Darkling: {
emoji: "🟣", favored: "reg", emoji: "🟣", favored: "reg",
@@ -153,18 +163,21 @@ 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
* dégâts magiques). Valeurs provisoires à 0 — le bestiaire sera mis à jour
* avec les vraies valeurs une fois le système validé. */
const MONSTER_TYPES = [ const MONSTER_TYPES = [
{ name: "Gobelin", emoji: "👺", level: 1, att: 2, esq: 2, deg: 2, pv: 12, armor: 0, vue: 4 }, { name: "Gobelin", emoji: "👺", level: 1, att: 2, esq: 2, deg: 2, 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, vue: 1, static: true }, { 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: "Araignée Géante", emoji: "🕷️", level: 2, att: 3, esq: 3, deg: 3, pv: 16, armor: 0, vue: 5 }, { name: "Araignée Géante", emoji: "🕷️", level: 2, att: 3, esq: 3, deg: 3, pv: 16, armor: 0, armorMag: 0, vue: 5 },
{ name: "Gargouille", emoji: "🦇", level: 3, att: 3, esq: 3, deg: 3, pv: 22, armor: 2, vue: 4 }, { name: "Gargouille", emoji: "🦇", level: 3, att: 3, esq: 3, deg: 3, pv: 22, armor: 2, armorMag: 0, vue: 4 },
{ name: "Momie", emoji: "🧟", level: 3, att: 4, esq: 2, deg: 4, pv: 26, armor: 1, vue: 3 }, { name: "Momie", emoji: "🧟", level: 3, att: 4, esq: 2, deg: 4, pv: 26, armor: 1, armorMag: 0, vue: 3 },
{ name: "Sorcière", emoji: "🧙", level: 4, att: 4, esq: 3, deg: 5, pv: 24, armor: 0, vue: 6 }, { name: "Sorcière", emoji: "🧙", level: 4, att: 4, esq: 3, deg: 5, 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, vue: 3 }, { name: "Golem de Pierre", emoji: "🗿", level: 5, att: 4, esq: 1, deg: 6, 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, vue: 8, boss: true }; 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 };
/* 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 = [
@@ -183,7 +196,7 @@ function applyTemplate(type, tpl, x, y) {
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)),
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, 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,
}; };
} }
@@ -608,7 +621,7 @@ function attackMonster(m, opts = {}) {
if (r.hit) { if (r.hit) {
m.pv -= r.damage; m.pv -= r.damage;
gainPX(1); // +1 PX par attaque réussie (converti en PI) gainPX(1); // +1 PX par attaque réussie (converti en PI)
cdLine(`Vous lui avez infligé <span class="cd-val">${r.damage} points de dégâts</span>.`); cdLine(`Vous lui avez infligé <span class="cd-val">${r.damage} points de dégâts</span>${r.armorReduction ? ` (son armure a absorbé ${r.armorReduction} point(s))` : ""}.`);
log(`Tu attaques ${m.name} : ATT ${r.attRoll} vs ESQ ${r.esqRoll}${r.critical ? "CRITIQUE ! " : "touché ! "}${r.damage} dégâts.`, "combat"); log(`Tu attaques ${m.name} : ATT ${r.attRoll} vs ESQ ${r.esqRoll}${r.critical ? "CRITIQUE ! " : "touché ! "}${r.damage} dégâts.`, "combat");
if (m.pv <= 0) killMonster(m, "attack"); if (m.pv <= 0) killMonster(m, "attack");
} else { } else {
@@ -813,12 +826,13 @@ function useSort() {
if (!tryTalent(t.sort, 80, sort.cost, "🔮 Rafale Psychique", "sort")) { cdFlush(); afterAction(); return; } if (!tryTalent(t.sort, 80, sort.cost, "🔮 Rafale Psychique", "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.`);
cdLine(`Votre jet d'Attaque est : <span class="cd-good">automatiquement réussi</span> (imparable).`); cdLine(`Votre jet d'Attaque est : <span class="cd-good">automatiquement réussi</span> (imparable).`);
let dmg = rollDice(te.deg, 3).total; const armMag = Math.max(0, effMonster(m).armorMag);
let dmg = Math.max(1, rollDice(te.deg, 3).total - armMag);
const res = resistInfo(m); const res = resistInfo(m);
if (res) dmg = Math.max(1, Math.ceil(dmg / 2)); if (res) dmg = Math.max(1, Math.ceil(dmg / 2));
m.pv -= dmg; m.pv -= dmg;
cdLine(`Vous lui avez infligé <span class="cd-val">${dmg} points de dégâts</span> (armure ignorée).`); cdLine(`Vous lui avez infligé <span class="cd-val">${dmg} points de dégâts</span> (armure physique ignorée${armMag ? `, armure magique ${armMag}` : ""}).`);
log(`Rafale Psychique : touche automatique, ${dmg} dégâts${res ? " (résisté : moitié)" : ""}, armure ignorée.`, "combat"); log(`Rafale Psychique : touche automatique, ${dmg} dégâts${res ? " (résisté : moitié)" : ""}, armure physique ignorée.`, "combat");
if (m.pv <= 0) killMonster(m, "sort"); if (m.pv <= 0) killMonster(m, "sort");
} else if (t.race === "Kastar") { // Vampirisme : dégâts + vol de vie } else if (t.race === "Kastar") { // Vampirisme : dégâts + vol de vie
const m = adjacentMonster(); const m = adjacentMonster();
@@ -829,7 +843,7 @@ function useSort() {
att: Math.max(1, Math.floor(te.deg * 2 / 3)), deg: te.deg, att: Math.max(1, Math.floor(te.deg * 2 / 3)), deg: te.deg,
degBonus: te.degBonus, attFlat: te.attFlat, degFlat: te.degFlat, degBonus: te.degBonus, attFlat: te.attFlat, degFlat: te.degFlat,
}; };
const r = resolveAttack(pseudo, effMonster(m), { ignoreArmor: true }); const r = resolveAttack(pseudo, effMonster(m), { magic: true });
cdAttackRolls(r); cdAttackRolls(r);
if (r.hit) { if (r.hit) {
let dmg = r.damage; let dmg = r.damage;
@@ -838,7 +852,7 @@ function useSort() {
m.pv -= dmg; m.pv -= dmg;
const heal = Math.ceil(dmg / 2); const heal = Math.ceil(dmg / 2);
t.pv = Math.min(t.pvMax, t.pv + heal); t.pv = Math.min(t.pvMax, t.pv + heal);
cdLine(`Vous lui avez infligé <span class="cd-val">${dmg} points de dégâts</span> (armure ignorée).`); cdLine(`Vous lui avez infligé <span class="cd-val">${dmg} points de dégâts</span> (armure physique ignorée${r.armorReduction ? `, armure magique ${r.armorReduction}` : ""}).`);
cdLine(`Vous avez également gagné <span class="cd-good">${heal} points de Vie</span> grâce au Vampirisme.`); cdLine(`Vous avez également gagné <span class="cd-good">${heal} points de Vie</span> grâce au Vampirisme.`);
log(`Vampirisme : ${dmg} dégâts${res ? " (résisté)" : ""}, tu draines ${heal} PV.`, "good"); log(`Vampirisme : ${dmg} dégâts${res ? " (résisté)" : ""}, tu draines ${heal} PV.`, "good");
if (m.pv <= 0) killMonster(m, "sort"); if (m.pv <= 0) killMonster(m, "sort");
@@ -853,14 +867,14 @@ function useSort() {
const proxBonus = Math.max(0, te.vue - dist); const proxBonus = Math.max(0, te.vue - dist);
cdLine(`Vous avez attaqué <b>${m.name}</b> grâce à un sortilège (distance ${dist}, bonus de proximité +${proxBonus}D6).`); cdLine(`Vous avez attaqué <b>${m.name}</b> grâce à un sortilège (distance ${dist}, bonus de proximité +${proxBonus}D6).`);
const pseudo = { att: te.vue + proxBonus, deg: Math.max(1, Math.floor(te.vue / 2)), degBonus: 0 }; const pseudo = { att: te.vue + proxBonus, deg: Math.max(1, Math.floor(te.vue / 2)), degBonus: 0 };
const r = resolveAttack(pseudo, effMonster(m), { ignoreArmor: true }); const r = resolveAttack(pseudo, effMonster(m), { magic: true });
cdAttackRolls(r); cdAttackRolls(r);
if (r.hit) { if (r.hit) {
let dmg = r.damage; let dmg = r.damage;
const res = resistInfo(m); const res = resistInfo(m);
if (res) dmg = Math.max(1, Math.ceil(dmg / 2)); if (res) dmg = Math.max(1, Math.ceil(dmg / 2));
m.pv -= dmg; m.pv -= dmg;
cdLine(`Vous lui avez infligé <span class="cd-val">${dmg} points de dégâts</span> (armure ignorée).`); cdLine(`Vous lui avez infligé <span class="cd-val">${dmg} points de dégâts</span> (armure physique ignorée${r.armorReduction ? `, armure magique ${r.armorReduction}` : ""}).`);
log(`Projectile Magique sur ${m.name} (distance ${dist}) : ${dmg} dégâts${res ? " (résisté)" : ""}.`, "combat"); log(`Projectile Magique sur ${m.name} (distance ${dist}) : ${dmg} dégâts${res ? " (résisté)" : ""}.`, "combat");
if (m.pv <= 0) killMonster(m, "sort"); if (m.pv <= 0) killMonster(m, "sort");
} else { } else {
@@ -907,6 +921,8 @@ function effMonster(m) {
att: Math.max(1, m.att - (m.attDownTurns > 0 ? m.attDownDice || 0 : 0)), att: Math.max(1, m.att - (m.attDownTurns > 0 ? m.attDownDice || 0 : 0)),
esq: m.esqHalfTurns > 0 ? Math.max(1, Math.floor(m.esq / 2)) : m.esq, esq: m.esqHalfTurns > 0 ? Math.max(1, Math.floor(m.esq / 2)) : m.esq,
vue: Math.max(1, m.vue - (m.vueMalusTurns > 0 ? m.vueMalus || 0 : 0)), vue: Math.max(1, m.vue - (m.vueMalusTurns > 0 ? m.vueMalus || 0 : 0)),
armorPhys: m.armor || 0,
armorMag: m.armorMag || 0,
}; };
} }
@@ -1006,9 +1022,12 @@ function applyScrollZone(zone) {
if (zone.type === "damage") { if (zone.type === "damage") {
cdStart("💥 Effet de zone"); cdStart("💥 Effet de zone");
for (const m of targets) { for (const m of targets) {
m.pv -= zone.total; // dégâts magiques : seule l'armure magique du monstre les réduit
cdLine(`${m.emoji} ${m.name} subit <span class="cd-val">${zone.total} points de dégâts</span> (${zone.label}).`); const armMag = Math.max(0, m.armorMag || 0);
log(`💥 ${m.name} est pris dans l'explosion : ${zone.total} PV.`, "combat"); const dmg = Math.max(1, zone.total - armMag);
m.pv -= dmg;
cdLine(`${m.emoji} ${m.name} subit <span class="cd-val">${dmg} points de dégâts</span> (${zone.label}${armMag ? `, armure magique ${armMag}` : ""}).`);
log(`💥 ${m.name} est pris dans l'explosion : ${dmg} PV.`, "combat");
if (m.pv <= 0) killMonster(m, "scroll"); if (m.pv <= 0) killMonster(m, "scroll");
} }
cdFlush(); cdFlush();
@@ -1077,7 +1096,7 @@ function passDLA() {
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>.`); 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))` : ""}.`);
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 : ${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 {
@@ -1254,11 +1273,12 @@ function renderPanels() {
<div><span>Tour</span><span class="stat-val">${t.tour}</span></div> <div><span>Tour</span><span class="stat-val">${t.tour}</span></div>
<div><span>PV</span><span class="stat-val">${t.pv} / ${t.pvMax}</span></div> <div><span>PV</span><span class="stat-val">${t.pv} / ${t.pvMax}</span></div>
<div class="hp-bar-wrap"><div class="hp-bar ${hpClass}" style="width:${pct * 100}%"></div></div> <div class="hp-bar-wrap"><div class="hp-bar ${hpClass}" style="width:${pct * 100}%"></div></div>
<div><span>Attaque</span><span class="stat-val">${fmtStatLine(t.att, te.att, 6, te.attFlat)}</span></div> <div><span>Attaque</span><span class="stat-val">${fmtStatLine(t.att, te.att, 6, te.attFlat, 0, { phys: te.attFlatPhys, mag: te.attFlatMag })}</span></div>
<div><span>Esquive</span><span class="stat-val">${fmtStatLine(t.esq, te.esq, 6, te.esqFlat)}</span></div> <div><span>Esquive</span><span class="stat-val">${fmtStatLine(t.esq, te.esq, 6, te.esqFlat, 0, { phys: te.esqFlatPhys, mag: te.esqFlatMag })}</span></div>
<div><span>Dégâts</span><span class="stat-val">${fmtStatLine(t.deg, te.deg, 3, te.degFlat, te.degBonus)}</span></div> <div><span>Dégâts</span><span class="stat-val">${fmtStatLine(t.deg, te.deg, 3, te.degFlat, te.degBonus, { phys: te.degFlatPhys, mag: te.degFlatMag })}</span></div>
<div><span>Régénération</span><span class="stat-val">${fmtStatLine(t.reg, te.reg, 3, te.regFlat)}</span></div> <div><span>Régénération</span><span class="stat-val">${fmtStatLine(t.reg, te.reg, 3, te.regFlat, 0, { phys: te.regFlatPhys, mag: te.regFlatMag })}</span></div>
<div><span>Armure</span><span class="stat-val">${te.armor}${t.armorDice ? "+" + t.armorDice + "D3" : ""}</span></div> <div><span>Armure phy.</span><span class="stat-val">${te.armorPhys}${t.armorDice ? "+" + t.armorDice + "D3" : ""}</span></div>
<div><span>Armure mag.</span><span class="stat-val">${te.armorMag > 0 ? "+" : ""}${te.armorMag}</span></div>
<div><span>Vue</span><span class="stat-val">${te.vue}</span></div> <div><span>Vue</span><span class="stat-val">${te.vue}</span></div>
<div><span>${RACES[t.race].comp.name}</span><span class="stat-val">${t.comp.pct} %</span></div> <div><span>${RACES[t.race].comp.name}</span><span class="stat-val">${t.comp.pct} %</span></div>
<div><span>${RACES[t.race].sort.name}</span><span class="stat-val">${t.sort.pct} %</span></div> <div><span>${RACES[t.race].sort.name}</span><span class="stat-val">${t.sort.pct} %</span></div>
@@ -1283,7 +1303,7 @@ function renderPanels() {
const improve = $("improve"); const improve = $("improve");
improve.innerHTML = ""; improve.innerHTML = "";
const labels = { att: "Attaque +1D6", esq: "Esquive +1D6", deg: "Dégâts +1D3", reg: "Régén. +1D3", pv: "PV max +10", vue: "Vue +1", armor: "Armure +1D3" }; const labels = { att: "Attaque +1D6", esq: "Esquive +1D6", deg: "Dégâts +1D3", reg: "Régén. +1D3", pv: "PV max +10", vue: "Vue +1", armor: "Armure phy. +1D3" };
for (const stat of Object.keys(labels)) { for (const stat of Object.keys(labels)) {
const cost = improveCost(stat, t.bought[stat], t.race); const cost = improveCost(stat, t.bought[stat], t.race);
const btn = document.createElement("button"); const btn = document.createElement("button");
+3 -1
View File
@@ -2,6 +2,8 @@
* 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
* à venir) — les bonus magiques viennent des potions et parchemins.
* `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. */
@@ -92,7 +94,7 @@ const LEGACY_ARMOR_NAMES = ["Armure de cuir", "Haubert de mailles", "Armure de p
const GEAR_MOD_LABELS = [ const GEAR_MOD_LABELS = [
["att", "ATT", ""], ["esq", "ESQ", ""], ["deg", "DEG", ""], ["att", "ATT", ""], ["esq", "ESQ", ""], ["deg", "DEG", ""],
["reg", "REG", ""], ["arm", "Armure", ""], ["vue", "VUE", ""], ["pv", "PV", ""], ["reg", "REG", ""], ["arm", "Armure phy.", ""], ["vue", "VUE", ""], ["pv", "PV", ""],
["rmPct", "RM", " %"], ["mmPct", "MM", " %"], ["rmPct", "RM", " %"], ["mmPct", "MM", " %"],
]; ];
+28 -11
View File
@@ -400,37 +400,52 @@ function sumPotionMods(effects) {
return m; return m;
} }
/* Affichage caractéristique : valeur effective + rappel de la base si modifiée. */ /* Affichage caractéristique : valeur effective + rappel de la base si modifiée.
function fmtStatLine(base, eff, faces, flat = 0, extra = 0) { * `split` ({ phys, mag }) détaille l'origine du bonus fixe : P = physique
* (équipement), M = magique (potions, parchemins). */
function fmtStatLine(base, eff, faces, flat = 0, extra = 0, split = null) {
const diceDelta = eff - base; const diceDelta = eff - base;
const jetBonus = (flat || 0) + (extra || 0); const jetBonus = (flat || 0) + (extra || 0);
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 = [];
if (split && split.phys && split.mag) {
const f = v => `${v > 0 ? "+" : ""}${v}`;
hints.push(`P ${f(split.phys)} · M ${f(split.mag)}`);
}
if (diceDelta !== 0) { if (diceDelta !== 0) {
const sign = diceDelta > 0 ? "+" : ""; const sign = diceDelta > 0 ? "+" : "";
return `${main} <small class="stat-hint">(${base}D${faces} ${sign}${diceDelta})</small>`; hints.push(`${base}D${faces} ${sign}${diceDelta}`);
} }
if (hints.length) return `${main} <small class="stat-hint">(${hints.join(" · ")})</small>`;
return main; return main;
} }
/* Caractéristiques effectives : base + effets magiques (potions, parchemins) /* Caractéristiques effectives : base + effets magiques (potions, parchemins)
* + é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
* décomposés (xxxFlatPhys / xxxFlatMag) ; de même pour l'armure :
* armorPhys = base + naturelle + équipement, armorMag = effets magiques.
* 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); const m = sumPotionMods(troll.potionEffects); // magique
const g = troll.gearMods || {}; const g = troll.gearMods || {}; // physique
return { return {
att: Math.max(1, troll.att + m.att), att: Math.max(1, troll.att + m.att),
attFlat: m.attFlat + (g.att || 0), attFlat: m.attFlat + (g.att || 0), attFlatPhys: g.att || 0, attFlatMag: m.attFlat,
esq: Math.max(1, troll.esq + m.esq), esq: Math.max(1, troll.esq + m.esq),
esqFlat: m.esqFlat + (g.esq || 0), 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), degFlat: m.degFlat + (g.deg || 0), degFlatPhys: g.deg || 0, degFlatMag: m.degFlat,
reg: Math.max(1, troll.reg + m.reg), reg: Math.max(1, troll.reg + m.reg),
regFlat: m.regFlat + (g.reg || 0), 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)),
armorPhys: Math.max(0, troll.armor + (g.arm || 0)),
armorMag: m.armor,
armorDice: troll.armorDice, armorDice: troll.armorDice,
degBonus: troll.degBonus || 0, degBonus: troll.degBonus || 0,
pvMax: troll.pvMax, pvMax: troll.pvMax,
@@ -504,8 +519,10 @@ function tickPotionTurns(troll, logFn) {
return sumPotionMods(troll.potionEffects).dlaBonusPA; return sumPotionMods(troll.potionEffects).dlaBonusPA;
} }
/* Les effets de potions/parchemins sont MAGIQUES : leur armure est de
* l'armure magique (seule à réduire les dégâts magiques). */
const EFFECT_MOD_LABELS = { const EFFECT_MOD_LABELS = {
att: "ATT", esq: "ESQ", deg: "DEG", reg: "REG", vue: "VUE", armor: "Armure", att: "ATT", esq: "ESQ", deg: "DEG", reg: "REG", vue: "VUE", armor: "Armure mag.",
}; };
function formatEffectMods(effect) { function formatEffectMods(effect) {
+6 -3
View File
@@ -52,7 +52,8 @@ const TALENT_RULES = {
effect: `La Rafale Psychique est la seule attaque <b>imparable</b> du jeu :<br> effect: `La Rafale Psychique est la seule attaque <b>imparable</b> du jeu :<br>
• Jet d'Attaque : <b>automatiquement réussi</b> (aucune esquive possible) ;<br> • Jet d'Attaque : <b>automatiquement réussi</b> (aucune esquive possible) ;<br>
• Jet de Dégâts : <b>1 D3 par D3 de Dégâts</b> ;<br> • Jet de Dégâts : <b>1 D3 par D3 de Dégâts</b> ;<br>
• l'<b>armure physique est ignorée</b>.<br><br> • l'<b>armure physique est ignorée</b> (seule l'armure magique de la cible
réduit les dégâts).<br><br>
Un Jet de Résistance réussi de la part de la cible lui permet de ne subir Un Jet de Résistance réussi de la part de la cible lui permet de ne subir
que la moitié des dégâts.`, que la moitié des dégâts.`,
flavor: `Le Durakuir fronce les sourcils — un exploit en soi — et une onde de flavor: `Le Durakuir fronce les sourcils — un exploit en soi — et une onde de
@@ -87,7 +88,8 @@ const TALENT_RULES = {
L'attaque par Vampirisme a les caractéristiques suivantes :<br> L'attaque par Vampirisme a les caractéristiques suivantes :<br>
• Jet d'Attaque : <b>2 D6 par 3 D3 de Dégâts</b> ;<br> • Jet d'Attaque : <b>2 D6 par 3 D3 de Dégâts</b> ;<br>
• Jet de Dégâts : <b>1 D3 par D3 de Dégâts</b> ;<br> • Jet de Dégâts : <b>1 D3 par D3 de Dégâts</b> ;<br>
• l'<b>armure physique est ignorée</b> ;<br> • l'<b>armure physique est ignorée</b> (seule l'armure magique de la cible
réduit les dégâts) ;<br>
• Vampirisme : <b>50 % des dégâts infligés</b> rendus en PV.<br><br> • Vampirisme : <b>50 % des dégâts infligés</b> rendus en PV.<br><br>
Un Jet de Résistance réussi de la part de la cible lui permet de ne subir que Un Jet de Résistance réussi de la part de la cible lui permet de ne subir que
la moitié des dégâts (et réduit d'autant le gain de PV). Si la cible est tuée, la moitié des dégâts (et réduit d'autant le gain de PV). Si la cible est tuée,
@@ -126,7 +128,8 @@ const TALENT_RULES = {
• Jet d'Attaque : <b>1 D6 par case de Vue + 1 D6 par case de proximité</b> • Jet d'Attaque : <b>1 D6 par case de Vue + 1 D6 par case de proximité</b>
(bonus quand la cible est plus proche que la portée maximale) ;<br> (bonus quand la cible est plus proche que la portée maximale) ;<br>
• Jet de Dégâts : <b>1 D3 par 2 cases de Vue</b> ;<br> • Jet de Dégâts : <b>1 D3 par 2 cases de Vue</b> ;<br>
• l'<b>armure physique est ignorée</b>.<br><br> • l'<b>armure physique est ignorée</b> (seule l'armure magique de la cible
réduit les dégâts).<br><br>
Un Jet de Résistance réussi de la part de la cible lui permet de ne subir Un Jet de Résistance réussi de la part de la cible lui permet de ne subir
que la moitié des dégâts.`, que la moitié des dégâts.`,
flavor: `Une bille de lumière verte file entre les stalactites et explose en flavor: `Une bille de lumière verte file entre les stalactites et explose en
+8 -5
View File
@@ -12,17 +12,17 @@ const TREASURE_POTIONS = [
["calvok", "2 à 6", "1 tour", ["Concentration 5·X %"]], ["calvok", "2 à 6", "1 tour", ["Concentration 5·X %"]],
["cervelle", "2 à 6", "1 tour", ["Concentration +5·X %"]], ["cervelle", "2 à 6", "1 tour", ["Concentration +5·X %"]],
["chronometre", "1 à 5", "3 tours", ["TOUR accéléré : +X PA par tour"]], ["chronometre", "1 à 5", "3 tours", ["TOUR accéléré : +X PA par tour"]],
["corruption", "3 à 7", "5 tours", ["ATT +X D3", "ESQ X D3", "DEG +X", "REG X", "VUE X", "Armure +X", "RM Y %, MM Z % (selon X)"]], ["corruption", "3 à 7", "5 tours", ["ATT +X D3", "ESQ X D3", "DEG +X", "REG X", "VUE X", "Armure mag. +X", "RM Y %, MM Z % (selon X)"]],
["djhinTonik", "1 à 5", "4 tours", ["DEG +X", "REG +X", "PV +2D3 (immédiat)"]], ["djhinTonik", "1 à 5", "4 tours", ["DEG +X", "REG +X", "PV +2D3 (immédiat)"]],
["doverPowa", "11 à 100", "2 tours", ["MM +Y %", "RM X %"]], ["doverPowa", "11 à 100", "2 tours", ["MM +Y %", "RM X %"]],
["fertilite", "3 à 7", "5 tours", ["ATT +X D3", "DEG +X"]], ["fertilite", "3 à 7", "5 tours", ["ATT +X D3", "DEG +X"]],
["feu", "3 à 7", "5 tours", ["ESQ +X D3", "VUE +X"]], ["feu", "3 à 7", "5 tours", ["ESQ +X D3", "VUE +X"]],
["glacier", "3 à 7", "5 tours", ["REG +X", "Armure +X"]], ["glacier", "3 à 7", "5 tours", ["REG +X", "Armure mag. +X"]],
["grippe", "3 à 4", "3 tours", ["ATT X D3", "ESQ X D3", "DEG X", "REG X"]], ["grippe", "3 à 4", "3 tours", ["ATT X D3", "ESQ X D3", "DEG X", "REG X"]],
["guerison", "1 à 5", "immédiat", ["PV +2·X D3"]], ["guerison", "1 à 5", "immédiat", ["PV +2·X D3"]],
["kouleMann", "1 à 5", "4 tours", ["REG +X", "VUE +X", "PV +(X/2) D3 (immédiat)"]], ["kouleMann", "1 à 5", "4 tours", ["REG +X", "VUE +X", "PV +(X/2) D3 (immédiat)"]],
["longueVue", "1, 2, 3, 5 ou 8", "3 tours", ["VUE +X"]], ["longueVue", "1, 2, 3, 5 ou 8", "3 tours", ["VUE +X"]],
["metomol", "1 à 5", "2 tours", ["TOUR accéléré : +X PA par tour", "Armure 2·X"]], ["metomol", "1 à 5", "2 tours", ["TOUR accéléré : +X PA par tour", "Armure mag. 2·X"]],
["painture", "1 à 5", "X tours", ["Visible — camouflage impossible"]], ["painture", "1 à 5", "X tours", ["Visible — camouflage impossible"]],
["pneumonie", "5 (fixe)", "3 tours", ["ATT 5 D3", "ESQ 5 D3", "DEG 5", "REG 5"]], ["pneumonie", "5 (fixe)", "3 tours", ["ATT 5 D3", "ESQ 5 D3", "DEG 5", "REG 5"]],
["pufPuff", "0 à 2", "3 tours", ["ATT X D3", "ESQ X D3", "VUE (X+1)", "PV 2D3 si X ≥ 2", "Effet de Zone"]], ["pufPuff", "0 à 2", "3 tours", ["ATT X D3", "ESQ X D3", "VUE (X+1)", "PV 2D3 si X ≥ 2", "Effet de Zone"]],
@@ -66,6 +66,8 @@ function treasuresRender() {
<a href="https://mountypedia.mountyhall.com/Mountyhall/Potion" target="_blank" rel="noopener">Mountypedia</a>. <a href="https://mountypedia.mountyhall.com/Mountyhall/Potion" target="_blank" rel="noopener">Mountypedia</a>.
Un trésor ne fait qu'<b>un seul jet de dés</b> : une formule « X D3 » donne le même total Un trésor ne fait qu'<b>un seul jet de dés</b> : une formule « X D3 » donne le même total
partout où elle s'applique. Les bonus « +X » sont des valeurs fixes ajoutées aux jets. partout où elle s'applique. Les bonus « +X » sont des valeurs fixes ajoutées aux jets.
Tous les bonus/malus des potions et parchemins sont <b>magiques</b> — leur armure est de
l'<b>armure magique</b>, seule à réduire les dégâts magiques.
1 tour = 1 DLA. Boire une potion ou lire un parchemin coûte <b>1 PA</b>.</p>`; 1 tour = 1 DLA. Boire une potion ou lire un parchemin coûte <b>1 PA</b>.</p>`;
html += `<h2 class="tz-section">🧪 Potions (${TREASURE_POTIONS.length})</h2><div class="tz-grid">`; html += `<h2 class="tz-section">🧪 Potions (${TREASURE_POTIONS.length})</h2><div class="tz-grid">`;
@@ -83,8 +85,9 @@ function treasuresRender() {
const gearCount = Object.values(GEAR).reduce((n, list) => n + list.length, 0); const gearCount = Object.values(GEAR).reduce((n, list) => n + list.length, 0);
html += `<h2 class="tz-section">⚔️ Équipement (${gearCount})</h2> html += `<h2 class="tz-section">⚔️ Équipement (${gearCount})</h2>
<p class="tz-intro">Six emplacements : ${Object.values(GEAR_SLOTS).map(s => s.label).join(", ")}. <p class="tz-intro">Six emplacements : ${Object.values(GEAR_SLOTS).map(s => s.label).join(", ")}.
Tous les bonus d'équipement sont <b>fixes</b> : ATT/ESQ/DEG/REG s'ajoutent aux jets Tous les bonus d'équipement sont <b>fixes</b> et <b>physiques</b> : ATT/ESQ/DEG/REG
(jamais en dés), Armure/VUE/PV aussi, RM/MM en %. s'ajoutent aux jets (jamais en dés), Armure physique/VUE/PV aussi, RM/MM en %.
L'armure d'équipement réduit les dégâts physiques mais pas les dégâts magiques.
Une arme <b>à 2 mains</b> est incompatible avec un bouclier. S'équiper coûte <b>2 PA</b>. Une arme <b>à 2 mains</b> est incompatible avec un bouclier. S'équiper coûte <b>2 PA</b>.
Valeurs de base de la Mountypedia (sans templates) ; les objets puissants ne se trouvent Valeurs de base de la Mountypedia (sans templates) ; les objets puissants ne se trouvent
qu'en profondeur.</p>`; qu'en profondeur.</p>`;
+55
View File
@@ -76,6 +76,61 @@ assert.strictEqual(g.improveCost("reg", 0, "Skrim"), 30, "REG non favorisée = 3
assert.strictEqual(g.improveCost("armor", 0, "Durakuir"), 30, "Armure = 30 PI pour tous"); assert.strictEqual(g.improveCost("armor", 0, "Durakuir"), 30, "Armure = 30 PI pour tous");
assert.strictEqual(g.improveCost("esq", 1, "Tomawak"), 32, "2e dé d'ESQ = 32 PI"); assert.strictEqual(g.improveCost("esq", 1, "Tomawak"), 32, "2e dé d'ESQ = 32 PI");
// Armure physique vs magique : les dégâts physiques sont réduits par l'armure
// totale ; les dégâts magiques (opts.magic) par la seule armure magique ;
// opts.ignoreArmor ignore tout (Siphon des Âmes).
{
const fort = { att: 50, deg: 10 }; // touche toujours, dégâts élevés
for (let i = 0; i < 200; i++) {
const cible = { esq: 1, armorPhys: 5, armorMag: 3 };
const phy = g.resolveAttack(fort, cible);
assert.strictEqual(phy.armorReduction, 8, "attaque physique : armure totale 5+3");
const mag = g.resolveAttack(fort, cible, { magic: true });
assert.strictEqual(mag.armorReduction, 3, "attaque magique : armure magique seule");
const tout = g.resolveAttack(fort, cible, { ignoreArmor: true });
assert.strictEqual(tout.armorReduction, 0, "ignoreArmor : aucune réduction");
}
// armure magique négative (Métomol) : réduit l'armure totale, jamais sous 0
const r1 = g.resolveAttack(fort, { esq: 1, armorPhys: 2, armorMag: -5 });
assert.strictEqual(r1.armorReduction, 0, "armure totale plancher 0");
const r2 = g.resolveAttack(fort, { esq: 1, armorPhys: 0, armorMag: -4 }, { magic: true });
assert.strictEqual(r2.armorReduction, 0, "armure magique plancher 0");
// rétrocompat : un défenseur avec seulement `armor` = armure physique
const r3 = g.resolveAttack(fort, { esq: 1, armor: 4 });
assert.strictEqual(r3.armorReduction, 4, "fallback armor = physique");
}
// effTroll : équipement → armure physique, potions → armure magique
{
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 };
g.equipGear(t, gearLib.gearItemByName("armure", "Armure de cuir")); // arm +4 (physique)
p.drinkPotion(t, p.makePotionItem("glacier", 5), g.rollDice, () => {}); // Armure mag. +5
const e = p.effTroll(t);
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.armor, 9, "armure totale = physique + magique");
// décomposition des bonus fixes : équipement = physique, potion = magique
g.equipGear(t, gearLib.gearItemByName("arme", "Gourdin")); // att +2 (physique)
p.drinkPotion(t, p.makePotionItem("bonneBouffe", 5), g.rollDice, () => {}); // DEG +5 (magique)
const e2 = p.effTroll(t);
assert.strictEqual(e2.attFlatPhys, 2, "Gourdin : ATT +2 physique");
assert.strictEqual(e2.attFlatMag, 0, "aucun bonus magique d'ATT");
assert.strictEqual(e2.degFlatPhys, 5, "Gourdin : DEG +5 physique");
assert.strictEqual(e2.degFlatMag, 5, "Bonne Bouffe : DEG +5 magique");
assert.strictEqual(e2.degFlat, 10, "total = physique + magique");
}
// Monstres : armure physique et magique distinctes
{
const m = g.makeMonster(3, 1, 1);
assert(typeof m.armorMag === "number", "monstre : armorMag présent");
const boss = g.monsterFromSpec({ x: 1, y: 1, boss: true });
assert(typeof boss.armorMag === "number", "Béhémoth : armorMag présent");
}
// L'armure naturelle en D3 réduit bien les dégâts // L'armure naturelle en D3 réduit bien les dégâts
{ {
let withArmor = 0, without = 0; let withArmor = 0, without = 0;