From e5a4148ee878b4d761523783c34f2891d6f8eab1 Mon Sep 17 00:00:00 2001 From: perco Date: Fri, 12 Jun 2026 10:22:18 +0200 Subject: [PATCH] =?UTF-8?q?1.9.0=20:=20bonus/malus=20physiques=20(=C3=A9qu?= =?UTF-8?q?ipement)=20vs=20magiques=20(potions/parchemins),=20armure=20phy?= =?UTF-8?q?sique=20et=20magique=20distinctes=20(troll=20+=20monstres)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- README.md | 13 +++++++ js/game.js | 92 ++++++++++++++++++++++++++++++------------------- js/gear.js | 4 ++- js/potions.js | 39 +++++++++++++++------ js/rules.js | 9 +++-- js/treasures.js | 13 ++++--- test/smoke.js | 55 +++++++++++++++++++++++++++++ 7 files changed, 169 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index 0971293..75dc339 100644 --- a/README.md +++ b/README.md @@ -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 | | 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é 10–90 % | 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 | | 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) | @@ -152,6 +154,17 @@ non affilié au jeu original de Mountyhall SARL. ## 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 (https://www.mountyhall.com/MH_Pack/packMH_parchemin/) : fond lin, tableaux crème à bordure noire (tableau1/2.jpg), liens rouge sombre #990000, boutons diff --git a/js/game.js b/js/game.js index d44459c..1d4c1c2 100644 --- a/js/game.js +++ b/js/game.js @@ -4,7 +4,7 @@ "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. */ 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. * 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 = {}) { const att = rollDice(attacker.att, 6); const esq = rollDice(defender.esq, 6); @@ -50,9 +53,16 @@ function resolveAttack(attacker, defender, opts = {}) { const deg = rollDice(attacker.deg, 3); const degFlat = attacker.degFlat || 0; 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) - const armor = opts.ignoreArmor ? 0 - : (defender.armor || 0) + (defender.armorDice ? rollDice(defender.armorDice, 3).total : 0); + // armure physique : fixe (base + équipement) + naturelle en D3 (achetée en PI) + // armure magique : effets de potions/parchemins (armorMag, peut être négative) + 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); } return result; @@ -126,21 +136,21 @@ const RACES = { desc: "Les tanks du Hall, durs au mal et infatigables.", 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." }, - 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: { emoji: "🔴", favored: "deg", desc: "Les vampires du Hall : leurs coups les nourrissent.", 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." }, - 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: { emoji: "🟡", favored: "vue", desc: "Trõlls furtifs, chasseurs embusqués aux yeux perçants.", 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é." }, - 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: { emoji: "🟣", favored: "reg", @@ -153,18 +163,21 @@ const RACES = { /* ================= 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 = [ - { name: "Gobelin", emoji: "👺", level: 1, att: 2, esq: 2, deg: 2, pv: 12, armor: 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: "Araignée Géante", emoji: "🕷️", level: 2, att: 3, esq: 3, deg: 3, pv: 16, armor: 0, vue: 5 }, - { name: "Gargouille", emoji: "🦇", level: 3, att: 3, esq: 3, deg: 3, pv: 22, armor: 2, vue: 4 }, - { name: "Momie", emoji: "🧟", level: 3, att: 4, esq: 2, deg: 4, pv: 26, armor: 1, vue: 3 }, - { name: "Sorcière", emoji: "🧙", level: 4, att: 4, esq: 3, deg: 5, pv: 24, armor: 0, vue: 6 }, - { name: "Golem de Pierre", emoji: "🗿", level: 5, att: 4, esq: 1, deg: 6, pv: 40, armor: 4, vue: 3 }, + { 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, 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: "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, 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: "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. */ const TEMPLATES = [ @@ -183,7 +196,7 @@ function applyTemplate(type, tpl, x, y) { esq: Math.max(1, Math.round(type.esq * 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), - 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, }; } @@ -608,7 +621,7 @@ function attackMonster(m, opts = {}) { if (r.hit) { m.pv -= r.damage; gainPX(1); // +1 PX par attaque réussie (converti en PI) - cdLine(`Vous lui avez infligé ${r.damage} points de dégâts.`); + cdLine(`Vous lui avez infligé ${r.damage} points de dégâts${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"); if (m.pv <= 0) killMonster(m, "attack"); } else { @@ -813,12 +826,13 @@ function useSort() { if (!tryTalent(t.sort, 80, sort.cost, "🔮 Rafale Psychique", "sort")) { cdFlush(); afterAction(); return; } cdLine(`Vous avez attaqué ${m.name} grâce à un sortilège.`); cdLine(`Votre jet d'Attaque est : automatiquement réussi (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); if (res) dmg = Math.max(1, Math.ceil(dmg / 2)); m.pv -= dmg; - cdLine(`Vous lui avez infligé ${dmg} points de dégâts (armure ignorée).`); - log(`Rafale Psychique : touche automatique, ${dmg} dégâts${res ? " (résisté : moitié)" : ""}, armure ignorée.`, "combat"); + cdLine(`Vous lui avez infligé ${dmg} points de dégâts (armure physique ignorée${armMag ? `, armure magique −${armMag}` : ""}).`); + 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"); } else if (t.race === "Kastar") { // Vampirisme : dégâts + vol de vie const m = adjacentMonster(); @@ -829,7 +843,7 @@ function useSort() { 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), { ignoreArmor: true }); + const r = resolveAttack(pseudo, effMonster(m), { magic: true }); cdAttackRolls(r); if (r.hit) { let dmg = r.damage; @@ -838,7 +852,7 @@ function useSort() { m.pv -= dmg; const heal = Math.ceil(dmg / 2); t.pv = Math.min(t.pvMax, t.pv + heal); - cdLine(`Vous lui avez infligé ${dmg} points de dégâts (armure ignorée).`); + cdLine(`Vous lui avez infligé ${dmg} points de dégâts (armure physique ignorée${r.armorReduction ? `, armure magique −${r.armorReduction}` : ""}).`); cdLine(`Vous avez également gagné ${heal} points de Vie grâce au Vampirisme.`); log(`Vampirisme : ${dmg} dégâts${res ? " (résisté)" : ""}, tu draines ${heal} PV.`, "good"); if (m.pv <= 0) killMonster(m, "sort"); @@ -853,14 +867,14 @@ function useSort() { const proxBonus = Math.max(0, te.vue - dist); cdLine(`Vous avez attaqué ${m.name} 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 r = resolveAttack(pseudo, effMonster(m), { ignoreArmor: true }); + const r = resolveAttack(pseudo, effMonster(m), { magic: true }); cdAttackRolls(r); if (r.hit) { let dmg = r.damage; const res = resistInfo(m); if (res) dmg = Math.max(1, Math.ceil(dmg / 2)); m.pv -= dmg; - cdLine(`Vous lui avez infligé ${dmg} points de dégâts (armure ignorée).`); + cdLine(`Vous lui avez infligé ${dmg} points de dégâts (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"); if (m.pv <= 0) killMonster(m, "sort"); } else { @@ -907,6 +921,8 @@ function effMonster(m) { 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, 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") { cdStart("💥 Effet de zone"); for (const m of targets) { - m.pv -= zone.total; - cdLine(`${m.emoji} ${m.name} subit ${zone.total} points de dégâts (${zone.label}).`); - log(`💥 ${m.name} est pris dans l'explosion : −${zone.total} PV.`, "combat"); + // dégâts magiques : seule l'armure magique du monstre les réduit + const armMag = Math.max(0, m.armorMag || 0); + const dmg = Math.max(1, zone.total - armMag); + m.pv -= dmg; + cdLine(`${m.emoji} ${m.name} subit ${dmg} points de dégâts (${zone.label}${armMag ? `, armure magique −${armMag}` : ""}).`); + log(`💥 ${m.name} est pris dans l'explosion : −${dmg} PV.`, "combat"); if (m.pv <= 0) killMonster(m, "scroll"); } cdFlush(); @@ -1077,7 +1096,7 @@ function passDLA() { cdLine(`Votre jet d'Esquive est de : ${r.esqRoll} (${r.esqDice}D6)`); if (r.hit) { t.pv -= r.damage; - cdLine(`Il vous a TOUCHÉ${r.critical ? ` d'un coup critique` : ""} et vous a infligé ${r.damage} points de dégâts.`); + cdLine(`Il vous a TOUCHÉ${r.critical ? ` d'un coup critique` : ""} et vous a infligé ${r.damage} points de dégâts${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"); if (t.pv <= 0) { cdLine(`Il vous a TERRASSÉ…`); cdFlush(); die(m); return; } } else { @@ -1254,11 +1273,12 @@ function renderPanels() {
Tour${t.tour}
PV${t.pv} / ${t.pvMax}
-
Attaque${fmtStatLine(t.att, te.att, 6, te.attFlat)}
-
Esquive${fmtStatLine(t.esq, te.esq, 6, te.esqFlat)}
-
Dégâts${fmtStatLine(t.deg, te.deg, 3, te.degFlat, te.degBonus)}
-
Régénération${fmtStatLine(t.reg, te.reg, 3, te.regFlat)}
-
Armure${te.armor}${t.armorDice ? "+" + t.armorDice + "D3" : ""}
+
Attaque${fmtStatLine(t.att, te.att, 6, te.attFlat, 0, { phys: te.attFlatPhys, mag: te.attFlatMag })}
+
Esquive${fmtStatLine(t.esq, te.esq, 6, te.esqFlat, 0, { phys: te.esqFlatPhys, mag: te.esqFlatMag })}
+
Dégâts${fmtStatLine(t.deg, te.deg, 3, te.degFlat, te.degBonus, { phys: te.degFlatPhys, mag: te.degFlatMag })}
+
Régénération${fmtStatLine(t.reg, te.reg, 3, te.regFlat, 0, { phys: te.regFlatPhys, mag: te.regFlatMag })}
+
Armure phy.${te.armorPhys}${t.armorDice ? "+" + t.armorDice + "D3" : ""}
+
Armure mag.${te.armorMag > 0 ? "+" : ""}${te.armorMag}
Vue${te.vue}
${RACES[t.race].comp.name}${t.comp.pct} %
${RACES[t.race].sort.name}${t.sort.pct} %
@@ -1283,7 +1303,7 @@ function renderPanels() { const improve = $("improve"); 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)) { const cost = improveCost(stat, t.bought[stat], t.race); const btn = document.createElement("button"); diff --git a/js/gear.js b/js/gear.js index 17eb22e..cdc327b 100644 --- a/js/gear.js +++ b/js/gear.js @@ -2,6 +2,8 @@ * 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 * 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. * 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 = [ ["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", " %"], ]; diff --git a/js/potions.js b/js/potions.js index 51c2e4d..3cdd545 100644 --- a/js/potions.js +++ b/js/potions.js @@ -400,37 +400,52 @@ function sumPotionMods(effects) { return m; } -/* Affichage caractéristique : valeur effective + rappel de la base si modifiée. */ -function fmtStatLine(base, eff, faces, flat = 0, extra = 0) { +/* Affichage caractéristique : valeur effective + rappel de la base si modifiée. + * `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 jetBonus = (flat || 0) + (extra || 0); let main = `${eff}D${faces}`; 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) { const sign = diceDelta > 0 ? "+" : ""; - return `${main} (${base}D${faces} ${sign}${diceDelta})`; + hints.push(`${base}D${faces} ${sign}${diceDelta}`); } + if (hints.length) return `${main} (${hints.join(" · ")})`; return main; } /* Caractéristiques effectives : base + effets magiques (potions, parchemins) * + é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, - * 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) { - const m = sumPotionMods(troll.potionEffects); - const g = troll.gearMods || {}; + const m = sumPotionMods(troll.potionEffects); // magique + const g = troll.gearMods || {}; // physique return { 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), - 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), - 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), - 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)), 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, degBonus: troll.degBonus || 0, pvMax: troll.pvMax, @@ -504,8 +519,10 @@ function tickPotionTurns(troll, logFn) { 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 = { - 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) { diff --git a/js/rules.js b/js/rules.js index 1a2d3e9..113c0cc 100644 --- a/js/rules.js +++ b/js/rules.js @@ -52,7 +52,8 @@ const TALENT_RULES = { effect: `La Rafale Psychique est la seule attaque imparable du jeu :
• Jet d'Attaque : automatiquement réussi (aucune esquive possible) ;
• Jet de Dégâts : 1 D3 par D3 de Dégâts ;
- • l'armure physique est ignorée.

+ • l'armure physique est ignorée (seule l'armure magique de la cible + réduit les dégâts).

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.`, 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 :
• Jet d'Attaque : 2 D6 par 3 D3 de Dégâts ;
• Jet de Dégâts : 1 D3 par D3 de Dégâts ;
- • l'armure physique est ignorée ;
+ • l'armure physique est ignorée (seule l'armure magique de la cible + réduit les dégâts) ;
• Vampirisme : 50 % des dégâts infligés rendus en PV.

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, @@ -126,7 +128,8 @@ const TALENT_RULES = { • Jet d'Attaque : 1 D6 par case de Vue + 1 D6 par case de proximité (bonus quand la cible est plus proche que la portée maximale) ;
• Jet de Dégâts : 1 D3 par 2 cases de Vue ;
- • l'armure physique est ignorée.

+ • l'armure physique est ignorée (seule l'armure magique de la cible + réduit les dégâts).

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.`, flavor: `Une bille de lumière verte file entre les stalactites et explose en diff --git a/js/treasures.js b/js/treasures.js index fd3c276..0ba3f6b 100644 --- a/js/treasures.js +++ b/js/treasures.js @@ -12,17 +12,17 @@ const TREASURE_POTIONS = [ ["calvok", "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"]], - ["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)"]], ["doverPowa", "11 à 100", "2 tours", ["MM +Y %", "RM −X %"]], ["fertilite", "3 à 7", "5 tours", ["ATT +X D3", "DEG +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"]], ["guerison", "1 à 5", "immédiat", ["PV +2·X D3"]], ["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"]], - ["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"]], ["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"]], @@ -66,6 +66,8 @@ function treasuresRender() { Mountypedia. Un trésor ne fait qu'un seul jet de dés : 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. + Tous les bonus/malus des potions et parchemins sont magiques — leur armure est de + l'armure magique, seule à réduire les dégâts magiques. 1 tour = 1 DLA. Boire une potion ou lire un parchemin coûte 1 PA.

`; html += `

🧪 Potions (${TREASURE_POTIONS.length})

`; @@ -83,8 +85,9 @@ function treasuresRender() { const gearCount = Object.values(GEAR).reduce((n, list) => n + list.length, 0); html += `

⚔️ Équipement (${gearCount})

Six emplacements : ${Object.values(GEAR_SLOTS).map(s => s.label).join(", ")}. - Tous les bonus d'équipement sont fixes : ATT/ESQ/DEG/REG s'ajoutent aux jets - (jamais en dés), Armure/VUE/PV aussi, RM/MM en %. + Tous les bonus d'équipement sont fixes et physiques : ATT/ESQ/DEG/REG + 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 à 2 mains est incompatible avec un bouclier. S'équiper coûte 2 PA. Valeurs de base de la Mountypedia (sans templates) ; les objets puissants ne se trouvent qu'en profondeur.

`; diff --git a/test/smoke.js b/test/smoke.js index 66a92bb..44323b4 100644 --- a/test/smoke.js +++ b/test/smoke.js @@ -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("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 { let withArmor = 0, without = 0;