Files
mountycrawl/test/smoke.js
T

411 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* Tests de fumée du cœur des règles — exécuter avec : node test/smoke.js */
"use strict";
const assert = require("assert");
require("../js/potions.js");
const g = require("../js/game.js");
const p = require("../js/potions.js");
const sc = require("../js/scrolls.js");
// Dés
for (let i = 0; i < 200; i++) {
const r = g.rollDice(3, 6);
assert(r.total >= 3 && r.total <= 18, "3D6 hors bornes : " + r.total);
assert.strictEqual(r.rolls.length, 3);
}
// Combat : un attaquant écrasant doit toucher la plupart du temps. Dégâts en D3,
// doublés sur coup critique (jet d'attaque ≥ 2 × jet d'esquive).
let hits = 0, crits = 0;
for (let i = 0; i < 500; i++) {
const r = g.resolveAttack({ att: 10, deg: 3, degBonus: 2 }, { esq: 1, armor: 2 });
if (r.hit) {
hits++;
if (r.critical) crits++;
assert(r.damage >= 1, "dégâts minimum 1");
const max = (3 * 3 + 2) * (r.critical ? 2 : 1) - 2;
assert(r.damage <= max, "dégâts trop élevés : " + r.damage + (r.critical ? " (critique)" : ""));
}
}
assert(hits > 450, "10D6 vs 1D6 devrait presque toujours toucher (" + hits + "/500)");
assert(crits > 0, "10D6 vs 1D6 devrait produire des coups critiques");
// autoHit : ne rate jamais (Rafale Psychique)
for (let i = 0; i < 50; i++) {
assert(g.resolveAttack({ att: 1, deg: 1 }, { esq: 10 }, { autoHit: true }).hit, "autoHit doit toucher");
}
// Maîtrise : progresse en cas de succès, plafonnée, jamais au-delà du cap
{
const talent = { pct: 89 };
for (let i = 0; i < 200; i++) g.masteryRoll(talent, 90);
assert(talent.pct <= 90 && talent.pct >= 89, "plafond de maîtrise dépassé : " + talent.pct);
const sure = { pct: 100 };
const r = g.masteryRoll(sure, 200);
assert(r.success && sure.pct > 100, "succès garanti doit faire progresser");
// Sous 50 %, même un échec rapporte +1 %
const novice = { pct: 0 }; // 0 % : échec garanti, mais on apprend quand même
const rf = g.masteryRoll(novice, 90);
assert(!rf.success && rf.gain === 1 && novice.pct === 1, "échec sous 50 % doit donner +1 %");
for (let i = 0; i < 500; i++) g.masteryRoll(novice, 90);
assert(novice.pct === 90, "la maîtrise doit finir au plafond à force d'essais : " + novice.pct);
// À 50 % ou plus, l'échec ne rapporte plus rien (vérifiable à pct=50 : jet 51-100 → échec sec)
const adept = { pct: 50 };
for (let i = 0; i < 300; i++) {
const r2 = g.masteryRoll(adept, 90);
if (!r2.success) assert(r2.gain === 0, "échec à 50 %+ ne doit rien rapporter");
adept.pct = 50; // on fige pour garder la condition du test
}
}
// Sortilège : SR borné [10, 90]
assert.strictEqual(g.resolveSpell(100, 0).sr, 90);
assert.strictEqual(g.resolveSpell(0, 100).sr, 10);
// 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");
// 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;
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);
assert.strictEqual(g.levelFromTotalPI(20), 2);
assert.strictEqual(g.levelFromTotalPI(49), 2);
assert.strictEqual(g.levelFromTotalPI(50), 3);
// PX de mise à mort : 10 + 2 × (niveau cible niveau troll) + niveau cible
assert.strictEqual(g.killPX(1, 5), 23, "troll niv 1 vs cible niv 5");
assert.strictEqual(g.killPX(5, 5), 15, "même niveau");
assert.strictEqual(g.killPX(60, 1), -107, "troll niv 60 vs cible niv 1 → négatif");
assert.strictEqual(g.killPX(3, 1), 7, "troll niv 3 vs cible niv 1");
// Les 5 races existent avec stats officielles, compétence et sortilège réservés
assert.strictEqual(Object.keys(g.RACES).length, 5);
for (const r of Object.values(g.RACES)) {
for (const k of ["att", "esq", "deg", "reg", "pvMax", "vue"]) {
assert(typeof r.stats[k] === "number", "stat manquante : " + k);
}
assert(r.comp && r.comp.name && r.comp.cost > 0, "compétence raciale manquante");
assert(r.sort && r.sort.name && r.sort.cost > 0, "sortilège racial manquant");
}
// Profils de base officiels (Races_*.php)
assert.deepStrictEqual(g.RACES.Skrim.stats, { att: 4, esq: 3, deg: 3, reg: 1, pvMax: 30, vue: 3 });
assert.deepStrictEqual(g.RACES.Durakuir.stats, { att: 3, esq: 3, deg: 3, reg: 1, pvMax: 40, vue: 3 });
assert.deepStrictEqual(g.RACES.Kastar.stats, { att: 3, esq: 3, deg: 4, reg: 1, pvMax: 30, vue: 3 });
assert.deepStrictEqual(g.RACES.Tomawak.stats, { att: 3, esq: 3, deg: 3, reg: 1, pvMax: 30, vue: 4 });
assert.deepStrictEqual(g.RACES.Darkling.stats, { att: 3, esq: 3, deg: 3, reg: 2, pvMax: 30, vue: 3 });
assert.strictEqual(g.RACES.Skrim.comp.name, "Botte Secrète");
assert.strictEqual(g.RACES.Skrim.sort.name, "Hypnotisme");
assert.strictEqual(g.RACES.Durakuir.comp.name, "Régénération Accrue");
assert.strictEqual(g.RACES.Durakuir.sort.name, "Rafale Psychique");
assert.strictEqual(g.RACES.Kastar.comp.name, "Accélération du Métabolisme");
assert.strictEqual(g.RACES.Kastar.sort.name, "Vampirisme");
assert.strictEqual(g.RACES.Tomawak.comp.name, "Camouflage");
assert.strictEqual(g.RACES.Tomawak.sort.name, "Projectile Magique");
assert.strictEqual(g.RACES.Darkling.comp.name, "Balayage");
assert.strictEqual(g.RACES.Darkling.sort.name, "Siphon des Âmes");
// Génération de caverne : connexe, bordures murées, assez de sol
for (let i = 0; i < 20; i++) {
const grid = g.generateCavern(g.MAP_W, g.MAP_H);
let floor = 0;
for (let y = 0; y < g.MAP_H; y++) {
for (let x = 0; x < g.MAP_W; x++) {
if (grid[y][x] === g.T_FLOOR) floor++;
const border = x === 0 || y === 0 || x === g.MAP_W - 1 || y === g.MAP_H - 1;
if (border) assert.strictEqual(grid[y][x], g.T_WALL, "bordure non murée");
}
}
assert(floor > 50, "caverne trop petite : " + floor + " cases de sol");
const region = g.largestRegion(grid, g.MAP_W, g.MAP_H);
assert.strictEqual(region.size, floor, "caverne non connexe");
}
// Monstres : niveaux et stats positifs à toutes les profondeurs
for (let depth = 1; depth <= 5; depth++) {
for (let i = 0; i < 50; i++) {
const m = g.makeMonster(depth, 1, 1);
assert(m.level >= 1 && m.pv >= 1 && m.att >= 1 && m.deg >= 1, "monstre invalide : " + JSON.stringify(m));
}
}
// Specs d'éditeur → entités de jeu
const gob = g.monsterFromSpec({ x: 2, y: 3, type: 0, tpl: 2 });
assert.strictEqual(gob.name, "Vieux Gobelin");
assert.strictEqual(gob.x, 2);
const boss = g.monsterFromSpec({ x: 1, y: 1, boss: true });
assert.strictEqual(boss.name, "Béhémoth");
assert(boss.pv === boss.pvMax && boss.pv > 0);
const pot = g.itemFromSpec({ x: 1, y: 1, kind: "potion" });
assert.strictEqual(pot.kind, "potion");
assert(p.POTION_IDS.length === 25, "25 potions droppables");
assert.strictEqual(p.POTION_DEFS.guerison.duration, 0);
assert.strictEqual(p.corruptionYZ(3).y, 0);
{ const { y } = p.corruptionYZ(6); assert(y >= 11 && y <= 20, "Y pour X=6 : " + y); }
{
const troll = { 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 };
const item = p.makePotionItem("bonneBouffe", 5);
p.drinkPotion(troll, item, g.rollDice, () => {});
assert.strictEqual(troll.potionEffects.length, 1);
assert.strictEqual(p.effTroll(troll).deg, 3, "Bonne Bouffe n'ajoute pas de dés de DEG");
assert.strictEqual(p.effTroll(troll).degFlat, 5, "Bonne Bouffe : DEG +5 fixe");
assert.strictEqual(p.effTroll(troll).regFlat, 5, "Bonne Bouffe : REG +5 fixe");
assert.strictEqual(p.effTroll(troll).attFlat, 0, "Bonne Bouffe ne modifie pas ATT");
const fixedRoll = () => ({ total: 12, rolls: [4, 4, 4] });
const t2 = { 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 };
p.drinkPotion(t2, p.makePotionItem("fertilite", 5), fixedRoll, () => {});
assert.strictEqual(p.effTroll(t2).attFlat, 12, "Fertilité +5D3 → +12 sur le jet");
assert.strictEqual(p.effTroll(t2).att, 3);
const t3 = { 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 };
p.drinkPotion(t3, p.makePotionItem("sangToh", 2), fixedRoll, () => {});
assert.strictEqual(p.effTroll(t3).attFlat, 12, "2D3 fixé → +12 sur le jet d'attaque");
assert.strictEqual(p.effTroll(t3).esqFlat, 12);
assert.strictEqual(p.effTroll(t3).att, 3);
// Le jet de la potion est unique : même valeur sur ATT et ESQ, bornée par XD3
for (let i = 0; i < 50; i++) {
const t4 = { 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 };
p.drinkPotion(t4, p.makePotionItem("sangToh", 4), g.rollDice, () => {});
const e4 = p.effTroll(t4);
assert.strictEqual(e4.attFlat, e4.esqFlat, "Sang de Toh Réroh : un seul jet pour ATT et ESQ");
assert(e4.attFlat >= 4 && e4.attFlat <= 12, "4D3 borné [4,12] : " + e4.attFlat);
}
p.tickPotionTurns(troll, () => {});
assert.strictEqual(troll.tour, 2);
troll.potionEffects[0].turnsLeft = 0;
p.tickPotionTurns(troll, () => {});
assert.strictEqual(troll.potionEffects.length, 0);
assert.strictEqual(p.effTroll(troll).deg, 3);
assert.strictEqual(p.effTroll(troll).attFlat, 0);
}
// DjhinTonik : DEG +X et REG +X fixes (pas des dés en plus)
{
const t = { att: 3, esq: 3, deg: 4, reg: 1, vue: 3, armor: 0, armorDice: 0, degBonus: 0, pvMax: 30, pv: 20, potionEffects: [], blockCamoTurns: 0, tour: 1 };
p.drinkPotion(t, p.makePotionItem("djhinTonik", 5), g.rollDice, () => {});
assert.strictEqual(p.effTroll(t).deg, 4, "DEG reste 4D3");
assert.strictEqual(p.effTroll(t).degFlat, 5, "DEG +5 fixe");
assert.strictEqual(p.effTroll(t).reg, 1, "REG reste 1D3");
assert.strictEqual(p.effTroll(t).regFlat, 5, "REG +5 fixe");
}
// Fertilité : ATT en jet D3 partagé, DEG +X fixe
{
const t = { att: 3, esq: 3, deg: 4, reg: 1, vue: 3, armor: 0, armorDice: 0, degBonus: 0, pvMax: 30, pv: 20, potionEffects: [], blockCamoTurns: 0, tour: 1 };
p.drinkPotion(t, p.makePotionItem("fertilite", 6), g.rollDice, () => {});
assert.strictEqual(p.effTroll(t).deg, 4, "Fertilité n'ajoute pas de dés de DEG");
assert.strictEqual(p.effTroll(t).degFlat, 6, "Fertilité : DEG +6 fixe");
}
// Parchemins : effets via le système d'effets magiques, un seul jet par parchemin
{
const fixedRoll = () => ({ total: 12, rolls: [4, 4, 4] });
const mk = () => ({ 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 });
const t1 = mk();
const r1 = sc.readScroll(t1, sc.makeScrollItem("runeCyclopes", 3), fixedRoll, () => {});
assert(r1 && !r1.zone, "Rune des Cyclopes : pas d'effet de zone");
const e1 = p.effTroll(t1);
assert.strictEqual(e1.attFlat, 12, "Rune des Cyclopes : ATT +3D3 fixé → +12");
assert.strictEqual(e1.degFlat, 3, "Rune des Cyclopes : DEG +3 fixe");
assert.strictEqual(e1.vue, 1, "Rune des Cyclopes : VUE 3 3 → min 1");
const t2 = mk();
sc.readScroll(t2, sc.makeScrollItem("ideesConfuses", 3), g.rollDice, () => {});
assert.strictEqual(p.sumPotionMods(t2.potionEffects).dlaBonusPA, -3, "Idées Confuses : 3 PA/tour");
assert(p.effTroll(t2).attFlat <= -3, "Idées Confuses : malus d'ATT");
const t3 = mk();
const r3 = sc.readScroll(t3, sc.makeScrollItem("runeExplosive", 2), fixedRoll, () => {});
assert.strictEqual(t3.pv, 8, "Rune Explosive : 20 12 PV");
assert(r3.zone && r3.zone.type === "damage" && r3.zone.total === 12, "Rune Explosive : zone de dégâts");
const t4 = mk();
const r4 = sc.readScroll(t4, sc.makeScrollItem("yeuKiPic", 2), g.rollDice, () => {});
assert(r4.zone && r4.zone.type === "vue" && r4.zone.malus === 6 && r4.zone.turns === 2, "Yeu'Ki'Pic : zone VUE 6, 2 tours");
const t5 = mk();
sc.readScroll(t5, sc.makeScrollItem("clairvoyance", 4), g.rollDice, () => {});
const e5 = p.effTroll(t5);
assert.strictEqual(e5.vue, 7, "Clairvoyance : VUE +4");
assert.strictEqual(e5.dlaBonusPA, 4, "Clairvoyance : +4 PA/tour");
// item aléatoire et via spec d'éditeur
const it = sc.makeRandomScroll();
assert.strictEqual(it.kind, "scroll");
assert(it.power >= 1 && it.power <= 5);
const fromSpec = g.itemFromSpec({ x: 1, y: 1, kind: "scroll" });
assert.strictEqual(fromSpec.kind, "scroll");
}
// Jet d'attaque : bonus potion ajouté au total des D6
{
const r = g.resolveAttack({ att: 3, attFlat: 12, deg: 2, degBonus: 0 }, { esq: 1 });
assert.strictEqual(r.attFlat, 12);
assert(r.attRoll > 12, "attRoll = somme des D6 + bonus potion");
assert(r.attRoll <= 12 + 18, "attRoll borné (3D6 max 18 + 12)");
}
// Équipement Mountypedia (6 emplacements) + rétrocompat des anciens specs
const gear = require("../js/gear.js");
const sword = g.itemFromSpec({ x: 1, y: 1, kind: "weapon", idx: 2 });
assert.strictEqual(sword.kind, "gear");
assert.strictEqual(sword.slot, "arme");
assert.strictEqual(sword.name, "Épée Longue", "ancien idx 2 → Épée Longue");
assert.strictEqual(g.itemFromSpec({ x: 1, y: 1, kind: "armor", idx: 0 }).name, "Armure de cuir");
{
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, gear.gearItemByName("arme", "Gourdin")); // att +2, esq 2, deg +5 (fixes)
g.equipGear(t, gear.gearItemByName("armure", "Armure de cuir")); // esq +3, arm +4, rm +40 %
let e = p.effTroll(t);
assert.strictEqual(e.att, 3, "l'équipement n'ajoute jamais de dés");
assert.strictEqual(e.attFlat, 2, "Gourdin : ATT +2 fixe sur le jet");
assert.strictEqual(e.deg, 3, "DEG reste 3D3");
assert.strictEqual(e.degFlat, 5, "Gourdin : DEG +5 fixe");
assert.strictEqual(e.esq, 3, "ESQ reste 3D6");
assert.strictEqual(e.esqFlat, 1, "ESQ 2 (Gourdin) +3 (Armure de cuir) = +1 fixe");
assert.strictEqual(e.armor, 4, "Armure de cuir : armure +4");
assert.strictEqual(e.rmPct, 40, "Armure de cuir : RM +40 %");
// PV max d'équipement
g.equipGear(t, gear.gearItemByName("bouclier", "Rondache en Bois")); // pv +5
assert.strictEqual(t.pvMax, 35, "Rondache : PV max +5");
// arme à 2 mains : le bouclier retourne au sac
g.equipGear(t, gear.gearItemByName("arme", "Hallebarde"));
assert.strictEqual(t.equip.bouclier, null, "Hallebarde : bouclier déséquipé");
assert(t.bag.some(i => i.name === "Rondache en Bois"), "le bouclier est dans le sac");
assert.strictEqual(t.pvMax, 30, "PV max retombé après déséquipement de la Rondache");
assert(t.bag.some(i => i.name === "Gourdin"), "l'ancienne arme est dans le sac");
e = p.effTroll(t);
assert.strictEqual(e.attFlat, -5, "Hallebarde : ATT 5 fixe");
assert.strictEqual(e.degFlat, 12, "Hallebarde : DEG +12 fixe");
// rééquiper un bouclier range l'arme à 2 mains
g.equipGear(t, gear.gearItemByName("bouclier", "Targe"));
assert.strictEqual(t.equip.arme, null, "Targe : la Hallebarde est rangée");
// tirage par profondeur : tier ≤ depth + 1
for (let i = 0; i < 100; i++) {
const it = gear.makeRandomGear(1);
const def = gear.GEAR[it.slot][it.gearIdx];
assert(def.tier <= 2, "profondeur 1 : tier ≤ 2 (reçu " + def.tier + ")");
}
}
// Validation serveur des niveaux
const srv = require("../server.js");
const goodGrid = [];
for (let y = 0; y < srv.MAP_H; y++) {
const border = y === 0 || y === srv.MAP_H - 1;
goodGrid.push(border ? "#".repeat(srv.MAP_W) : "#" + ".".repeat(srv.MAP_W - 2) + "#");
}
const goodLevel = {
name: "Test", author: "perco", grid: goodGrid,
start: { x: 1, y: 1 },
monsters: [{ x: 2, y: 2, type: 0, tpl: 1 }],
items: [{ x: 3, y: 3, kind: "potion" }, { x: 4, y: 3, kind: "scroll" }],
};
assert.strictEqual(srv.validateLevel(goodLevel), null);
assert(srv.validateLevel({ ...goodLevel, name: "" }), "nom vide refusé");
assert(srv.validateLevel({ ...goodLevel, monsters: [] }), "niveau sans monstre ni porte ni sortie refusé");
// Portes : valides, et suffisantes comme objectif sans monstre
const door = { x: 4, y: 4, target: "abcdef012345" };
assert.strictEqual(srv.validateLevel({ ...goodLevel, doors: [door] }), null);
assert.strictEqual(srv.validateLevel({ ...goodLevel, monsters: [], doors: [door] }), null, "porte = objectif valable");
assert(srv.validateLevel({ ...goodLevel, doors: [{ x: 4, y: 4, target: "pas-un-id" }] }), "cible de porte invalide refusée");
assert(srv.validateLevel({ ...goodLevel, doors: [{ x: 0, y: 0, target: "abcdef012345" }] }), "porte dans un mur refusée");
const gridWithExit = goodGrid.map((r, y) => y === 5 ? r.slice(0, 5) + ">" + r.slice(6) : r);
assert.strictEqual(srv.validateLevel({ ...goodLevel, monsters: [], grid: gridWithExit }), null, "sortie = objectif valable");
assert(srv.validateLevel({ ...goodLevel, start: { x: 0, y: 0 } }), "départ dans un mur refusé");
assert(srv.validateLevel({ ...goodLevel, grid: goodGrid.slice(1) }), "grille tronquée refusée");
assert(srv.validateLevel({ ...goodLevel, monsters: [{ x: 0, y: 0, type: 0, tpl: 0 }] }), "monstre dans un mur refusé");
assert(srv.validateLevel({ ...goodLevel, items: [{ x: 3, y: 3, kind: "nawak" }] }), "objet inconnu refusé");
// Vue : mémoire du brouillard conservée, entités seulement à portée actuelle
{
g.newGame("Test", "Skrim");
const t = g.state.troll;
t.x = 5; t.y = 5;
const farKey = 5 * g.MAP_W + 10;
t.potionEffects = [{ name: "Test", emoji: "🔭", turnsLeft: 1, vue: 5 }];
g.refreshFov();
assert(g.inSightAt(10, 5), "vue 8 (3+5) doit voir à 5 cases");
assert(g.state.seen.has(farKey), "case lointaine mémorisée");
t.potionEffects = [];
g.refreshFov();
assert(!g.inSightAt(10, 5), "vue 3 ne voit plus à 5 cases");
assert(g.state.seen.has(farKey), "mémoire conservée (affichage sombre)");
assert(g.inSightAt(8, 5), "vue 3 voit encore à 3 cases");
}
console.log("✅ Tous les tests de fumée passent.");