1.3.1 : coûts d'amélioration officiels (Rules_3.php) + armure naturelle

Table officielle : N-ième achat = N × base, caractéristique favorite à
12 PI, REG à 30 (Darkling 22), PV par tranche de 10 à 16 (Durakuir 12),
et armure naturelle en D3 achetable à 30 PI pour toutes les races
(réduction de dégâts jetée en D3 en plus de l'équipement).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
perco
2026-06-11 14:13:20 +02:00
co-authored by Claude Fable 5
parent eae97ae9bb
commit ad33670468
3 changed files with 64 additions and 24 deletions
+20 -3
View File
@@ -39,7 +39,7 @@ 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 | Identique, avec le détail des jets dans le 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), plafonds 90 %/80 %, é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é |
| Progression | PX → PI à l'entraînement, coût (dés + 1) × coût racial | Identique, avec réduction sur la caractéristique favorite de la race |
| 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) |
| Coûts en PA | Déplacement 13, attaque, ramasser, équiper, potion… | Déplacement 1, attaque 3, ramasser 1, équiper 2, potion 1 |
@@ -61,8 +61,22 @@ Profils de base repris des pages officielles, compétence (🥋 2 PA) et sortil
| 🟡 Tomawak | ATT 3D6 · ESQ 3D6 · DEG 3D3 · REG 1D3 · PV 30 · Vue 4 | **Camouflage** : invisible, jet sous 75 % de la maîtrise à chaque pas | **Projectile Magique** : à distance (portée = Vue), bonus de proximité |
| 🟣 Darkling | ATT 3D6 · ESQ 3D6 · DEG 3D3 · REG 2D3 · PV 30 · Vue 3 | **Balayage** : déstabilise (la cible perd son tour) | **Siphon des Âmes** : 1D3 par dé de REG, ignore toute armure, nécrose ATT |
Le coût en PI reste réduit sur la caractéristique favorite de chaque race
(ATT Skrim, PV Durakuir, DEG Kastar, Vue Tomawak, REG Darkling).
### Coûts d'amélioration (officiels, [Rules_3.php](https://www.mountyhall.com/MH_Rules/Rules_3.php))
Le N-ième achat d'une caractéristique coûte N × le coût de base :
| Amélioration | Skrim | Durakuir | Kastar | Tomawak | Darkling |
|---|---|---|---|---|---|
| +1D6 Attaque | **12** | 16 | 16 | 16 | 16 |
| +1D6 Esquive | 16 | 16 | 16 | 16 | 16 |
| +1D3 Dégâts | 16 | 16 | **12** | 16 | 16 |
| +1D3 Régénération | 30 | 30 | 30 | 30 | **22** |
| +1D3 Armure naturelle | 30 | 30 | 30 | 30 | 30 |
| +1 Vue | 16 | 16 | 16 | **12** | 16 |
| +10 PV | 16 | **12** | 16 | 16 | 16 |
(L'amélioration « 30 minutes de DLA » du jeu original n'a pas d'équivalent ici,
la DLA étant instantanée.)
## Contrôles
@@ -128,6 +142,9 @@ non affilié au jeu original de Mountyhall SARL.
## Versions
- **1.3.1** (2026-06-11) — Coûts d'amélioration officiels de Rules_3.php (N-ième
achat = N × base, favorisée 12 PI, REG 30/22, PV par 10) et armure naturelle
en D3 achetable (30 PI pour toutes les races).
- **1.3.0** (2026-06-11) — Races fidèles aux règles officielles : profils de base
exacts, dégâts en D3, compétence + sortilège réservés par race avec maîtrise en %
progressant à l'usage, jets de résistance des monstres, statuts (à terre, hypnose,
+23 -17
View File
@@ -26,7 +26,9 @@ function resolveAttack(attacker, defender, opts = {}) {
if (result.hit) {
const deg = rollDice(attacker.deg, 3);
result.rawDamage = deg.total + (attacker.degBonus || 0);
const armor = opts.ignoreArmor ? 0 : (defender.armor || 0);
// 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);
result.damage = Math.max(1, result.rawDamage - armor);
}
return result;
@@ -53,14 +55,17 @@ function resolveSpell(mm, rm) {
return { sr, roll, success: roll <= sr };
}
/* Coût en PI du prochain dé d'une caractéristique : (dés actuels + 1) × coût de base,
* réduit pour la caractéristique favorite de la race (cf. règles MH). */
function improveCost(stat, currentValue, race) {
const base = { att: 18, esq: 18, deg: 18, reg: 15, pv: 10, vue: 15 };
/* Coûts d'amélioration officiels (MH_Rules/Rules_3.php) : le N-ième achat d'une
* caractéristique coûte N × coût de base de la race. La caractéristique favorite
* est à 12 PI (REG du Darkling : 22), le reste à 16, REG à 30, Armure à 30.
* `bought` = nombre d'améliorations déjà achetées dans cette caractéristique. */
const IMPROVE_BASE = { att: 16, esq: 16, deg: 16, reg: 30, pv: 16, vue: 16, armor: 30 };
const IMPROVE_FAVORED = { att: 12, deg: 12, pv: 12, vue: 12, reg: 22 };
function improveCost(stat, bought, race) {
const favored = RACES[race].favored === stat;
const mult = favored ? Math.ceil(base[stat] * 2 / 3) : base[stat];
if (stat === "pv") return mult; // +5 PV, coût fixe
return (currentValue + 1) * mult;
const base = favored ? IMPROVE_FAVORED[stat] : IMPROVE_BASE[stat];
return (bought + 1) * base;
}
/* Niveau d'ancienneté : passer au niveau N coûte 10×N PI gagnés (cumulés). */
@@ -292,7 +297,8 @@ function newGame(name, race, customLevel = null) {
name, race,
att: s.att, esq: s.esq, deg: s.deg, reg: s.reg,
pv: s.pvMax, pvMax: s.pvMax, vue: s.vue,
degBonus: 0, armor: 0,
degBonus: 0, armor: 0, armorDice: 0,
bought: { att: 0, esq: 0, deg: 0, reg: 0, pv: 0, vue: 0, armor: 0 },
comp: { pct: 15 }, sort: { pct: 15 }, fatigue: 0, compUsed: false,
weapon: null, armorItem: null,
pa: PA_PER_TURN, pi: 0, totalPI: 0, gold: 0,
@@ -744,13 +750,14 @@ function descend() {
function improveStat(stat) {
const t = G.troll;
const current = stat === "pv" ? t.pvMax : t[stat];
const cost = improveCost(stat, stat === "pv" ? 0 : current, t.race);
const cost = improveCost(stat, t.bought[stat], t.race);
if (t.pi < cost) return;
t.pi -= cost;
if (stat === "pv") { t.pvMax += 5; t.pv += 5; }
t.bought[stat] += 1;
if (stat === "pv") { t.pvMax += 10; t.pv += 10; }
else if (stat === "armor") t.armorDice += 1;
else t[stat] += 1;
const label = { att: "Attaque", esq: "Esquive", deg: "Dégâts", reg: "Régénération", pv: "PV max", vue: "Vue" }[stat];
const label = { att: "Attaque", esq: "Esquive", deg: "Dégâts", reg: "Régénération", pv: "PV max", vue: "Vue", armor: "Armure naturelle" }[stat];
log(`📈 Entraînement : ${label} améliorée pour ${cost} PI.`, "good");
if (stat === "vue") updateFov();
afterAction();
@@ -945,7 +952,7 @@ function renderPanels() {
<div><span>Esquive</span><span class="stat-val">${t.esq}D6</span></div>
<div><span>Dégâts</span><span class="stat-val">${t.deg}D3${t.degBonus ? "+" + t.degBonus : ""}</span></div>
<div><span>Régénération</span><span class="stat-val">${t.reg}D3</span></div>
<div><span>Armure</span><span class="stat-val">${t.armor}</span></div>
<div><span>Armure</span><span class="stat-val">${t.armor}${t.armorDice ? "+" + t.armorDice + "D3" : ""}</span></div>
<div><span>Vue</span><span class="stat-val">${t.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].sort.name}</span><span class="stat-val">${t.sort.pct} %</span></div>
@@ -957,10 +964,9 @@ 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 +5", vue: "Vue +1" };
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" };
for (const stat of Object.keys(labels)) {
const current = stat === "pv" ? 0 : t[stat];
const cost = improveCost(stat, current, t.race);
const cost = improveCost(stat, t.bought[stat], t.race);
const btn = document.createElement("button");
const fav = RACES[t.race].favored === stat ? " ★" : "";
btn.textContent = `${labels[stat]}${cost} PI${fav}`;
+21 -4
View File
@@ -42,10 +42,27 @@ for (let i = 0; i < 50; i++) {
assert.strictEqual(g.resolveSpell(100, 0).sr, 90);
assert.strictEqual(g.resolveSpell(0, 100).sr, 10);
// Coûts d'amélioration : la race favorisée paie moins cher
const skrimAtt = g.improveCost("att", 3, "Skrim");
const kastarAtt = g.improveCost("att", 3, "Kastar");
assert(skrimAtt < kastarAtt, `Skrim devrait payer son ATT moins cher (${skrimAtt} vs ${kastarAtt})`);
// Coûts d'amélioration officiels (Rules_3.php) : N-ième achat = N × coût de base
assert.strictEqual(g.improveCost("att", 0, "Skrim"), 12, "1er dé d'ATT Skrim = 12 PI");
assert.strictEqual(g.improveCost("att", 2, "Skrim"), 36, "3e dé d'ATT Skrim = 36 PI (exemple officiel)");
assert.strictEqual(g.improveCost("att", 0, "Kastar"), 16, "ATT non favorisée = 16 PI");
assert.strictEqual(g.improveCost("deg", 0, "Kastar"), 12, "DEG favorisé Kastar = 12 PI");
assert.strictEqual(g.improveCost("pv", 0, "Durakuir"), 12, "PV favorisés Durakuir = 12 PI");
assert.strictEqual(g.improveCost("vue", 0, "Tomawak"), 12, "Vue favorisée Tomawak = 12 PI");
assert.strictEqual(g.improveCost("reg", 0, "Darkling"), 22, "REG favorisée Darkling = 22 PI");
assert.strictEqual(g.improveCost("reg", 0, "Skrim"), 30, "REG non favorisée = 30 PI");
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");
// L'armure naturelle en D3 réduit bien les dégâts
{
let withArmor = 0, without = 0;
for (let i = 0; i < 400; i++) {
withArmor += g.resolveAttack({ att: 10, deg: 5 }, { esq: 1, armor: 0, armorDice: 4 }).damage;
without += g.resolveAttack({ att: 10, deg: 5 }, { esq: 1, armor: 0 }).damage;
}
assert(withArmor < without, "4D3 d'armure naturelle doit réduire les dégâts");
}
// Niveaux : 0 PI → niv 1 ; 20 PI → niv 2 ; 50 PI → niv 3 (20 + 30)
assert.strictEqual(g.levelFromTotalPI(0), 1);