1.7.0 : équipement Mountypedia — 6 emplacements, 55 objets

- js/gear.js : Arme (12 + 3 à deux mains), Armure (13), Casque (9),
  Bouclier (5), Talisman (7), Bottes (6), valeurs de base officielles
- ATT/ESQ/DEG/REG en dés, Armure/VUE/PV fixes, RM/MM en % (spellMM/spellRM)
- equipGear : échange par emplacement, exclusion 2 mains/bouclier,
  PV max dynamique, gearMods intégré à effTroll
- Drops par tier ≤ profondeur + 1, butin éditeur par emplacement,
  rétrocompat des specs weapon/armor des niveaux publiés
- Page Trésors : section Équipement (6 sous-sections)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
perco
2026-06-11 23:56:30 +02:00
co-authored by Claude Fable 5
parent 6039497e72
commit 2f21f867a5
10 changed files with 314 additions and 51 deletions
+7
View File
@@ -152,6 +152,13 @@ non affilié au jeu original de Mountyhall SARL.
## Versions
- **1.7.0** (2026-06-11) — Équipement Mountypedia complet : 6 emplacements (Arme,
Armure, Casque, Bouclier, Talisman, Bottes) et 55 objets dropables aux valeurs
de base officielles (ATT/ESQ/DEG/REG en dés, Armure/VUE/PV fixes, RM/MM en %).
Armes à 2 mains incompatibles avec un bouclier, PV max d'équipement, tiers de
drop par profondeur, pinceaux d'éditeur par emplacement, rétrocompatibilité des
anciens niveaux publiés (specs weapon/armor mappées). Section « Équipement »
dans la page Trésors.
- **1.6.0** (2026-06-11) — Parchemins 📜 : les 7 parchemins standards de Mountypedia
(Rune des Cyclopes, Traité de Clairvoyance, Idées Confuses, Rune Explosive,
Plan Génial, Yeu'Ki'Pic, Rune des Foins) en trésors à trouver, butin de monstre
+11
View File
@@ -520,3 +520,14 @@ header h1 { font-size: 1.3em; color: #8fbf5a; }
}
.tz-good { color: #8fbf5a; }
.tz-bad { color: #d88080; }
/* ---------- Équipement 6 emplacements ---------- */
.eq-line { margin-bottom: 6px; line-height: 1.3; }
.eq-slot {
display: inline-block;
min-width: 72px;
color: #a89568;
font-size: .85em;
}
.eq-mods { color: #8a7a55; font-size: .78em; margin-left: 72px; }
.tz-subsection { color: #d8a040; margin: 16px 0 8px; font-size: 1.05em; }
+1
View File
@@ -166,6 +166,7 @@
<script src="js/potions.js"></script>
<script src="js/scrolls.js"></script>
<script src="js/gear.js"></script>
<script src="js/game.js"></script>
<script src="js/editor.js"></script>
<script src="js/rules.js"></script>
+6 -3
View File
@@ -124,8 +124,9 @@ function edBuildPalette() {
edAddBrushBtn(itemsDiv, "🧪 Potion (aléatoire)", { mode: "item", kind: "potion" });
edAddBrushBtn(itemsDiv, "📜 Parchemin (aléatoire)", { mode: "item", kind: "scroll" });
edAddBrushBtn(itemsDiv, "💰 Mountyzédons", { mode: "item", kind: "gold" });
WEAPONS.forEach((w, i) => edAddBrushBtn(itemsDiv, `${w.emoji} ${w.name} (+${w.bonus})`, { mode: "item", kind: "weapon", idx: i }));
ARMORS.forEach((a, i) => edAddBrushBtn(itemsDiv, `${a.emoji} ${a.name} (+${a.bonus})`, { mode: "item", kind: "armor", idx: i }));
for (const [slot, info] of Object.entries(GEAR_SLOTS)) {
edAddBrushBtn(itemsDiv, `${info.emoji} ${info.label} (aléatoire)`, { mode: "item", kind: "gear", slot });
}
const doorsDiv = document.getElementById("ed-doors");
doorsDiv.innerHTML = "";
@@ -240,6 +241,7 @@ function edApply(x, y) {
ED.items = ED.items.filter(i => i.x !== x || i.y !== y);
const spec = { x, y, kind: b.kind };
if (b.idx !== undefined) spec.idx = b.idx;
if (b.slot !== undefined) spec.slot = b.slot;
if (b.kind === "gold") spec.gold = 60;
ED.items.push(spec);
}
@@ -279,7 +281,8 @@ function edRender() {
: i.kind === "scroll" ? "#d8c890" : "#7a8db0");
ctx.fillStyle = "#1a140e";
const emoji = i.kind === "potion" ? "🧪" : i.kind === "scroll" ? "📜" : i.kind === "gold" ? "💰"
: i.kind === "weapon" ? WEAPONS[i.idx || 0].emoji : ARMORS[i.idx || 0].emoji;
: i.kind === "gear" ? (GEAR_SLOTS[i.slot] || GEAR_SLOTS.arme).emoji
: i.kind === "weapon" ? "🗡️" : "🦺";
ctx.fillText(emoji, i.x * TILE + TILE / 2, i.y * TILE + TILE / 2 + 1);
}
for (const d of ED.doors) {
+60 -38
View File
@@ -4,7 +4,7 @@
"use strict";
const APP_VERSION = "1.6.0";
const APP_VERSION = "1.7.0";
/* Alpha : maîtrise initiale haute pour les tests. Remettre 15 % / 15 % à la v1.0 officielle. */
const START_COMP_PCT = 90;
@@ -17,6 +17,9 @@ if (typeof module !== "undefined" && module.exports && typeof makeRandomPotion =
if (typeof module !== "undefined" && module.exports && typeof makeRandomScroll === "undefined") {
Object.assign(globalThis, require("./scrolls.js"));
}
if (typeof module !== "undefined" && module.exports && typeof makeRandomGear === "undefined") {
Object.assign(globalThis, require("./gear.js"));
}
/* ================= Dés & règles de base ================= */
@@ -201,7 +204,9 @@ function monsterFromSpec(spec) {
return applyTemplate(type, tpl, spec.x, spec.y);
}
/* Instancie un objet depuis une spec d'éditeur : {x, y, kind, idx?, gold?} */
/* Instancie un objet depuis une spec d'éditeur : {x, y, kind, slot?, idx?, gold?}.
* Les specs « weapon »/« armor » à idx des anciens niveaux publiés sont mappées
* vers l'équipement Mountypedia équivalent. */
function itemFromSpec(spec) {
const base = { x: spec.x, y: spec.y };
if (spec.kind === "potion") return { ...base, ...makePotionItem(spec.potionId, spec.power) };
@@ -210,8 +215,13 @@ function itemFromSpec(spec) {
const gold = spec.gold || 60;
return { ...base, kind: "gold", name: `${gold} Mountyzédons`, emoji: "💰", gold };
}
const list = spec.kind === "weapon" ? WEAPONS : ARMORS;
return { ...base, kind: "gear", ...list[(spec.idx || 0) % list.length] };
if (spec.kind === "gear") return { ...base, ...makeRandomGear(5, spec.slot) };
if (spec.kind === "weapon") {
const name = LEGACY_WEAPON_NAMES[(spec.idx || 0) % LEGACY_WEAPON_NAMES.length];
return { ...base, ...gearItemByName("arme", name) };
}
const name = LEGACY_ARMOR_NAMES[(spec.idx || 0) % LEGACY_ARMOR_NAMES.length];
return { ...base, ...gearItemByName("armure", name) };
}
/* ================= Génération du Monde Souterrain ================= */
@@ -286,30 +296,11 @@ function randomFloor(grid, taken) {
/* ================= Objets ================= */
const WEAPONS = [
{ name: "Dague Rouillée", emoji: "🗡️", slot: "weapon", bonus: 1 },
{ name: "Hache de Jet", emoji: "🪓", slot: "weapon", bonus: 2 },
{ name: "Épée Longue", emoji: "⚔️", slot: "weapon", bonus: 3 },
{ name: "Masse d'Armes", emoji: "🔨", slot: "weapon", bonus: 4 },
];
const ARMORS = [
{ name: "Armure de Cuir", emoji: "🦺", slot: "armor", bonus: 1 },
{ name: "Cotte de Mailles", emoji: "🥋", slot: "armor", bonus: 2 },
{ name: "Armure de Plates", emoji: "🛡️", slot: "armor", bonus: 3 },
];
function makeItem(depth) {
const r = Math.random();
if (r < 0.3) return makeRandomPotion();
if (r < 0.42) return makeRandomScroll();
if (r < 0.58) {
const w = WEAPONS[Math.min(WEAPONS.length - 1, Math.floor(Math.random() * (depth + 1)))];
return { kind: "gear", ...w };
}
if (r < 0.7) {
const a = ARMORS[Math.min(ARMORS.length - 1, Math.floor(Math.random() * depth))];
return { kind: "gear", ...a };
}
if (r < 0.7) return makeRandomGear(depth);
const gold = rollDice(depth, 6).total * 10;
return { kind: "gold", name: `${gold} Mountyzédons`, emoji: "💰", gold };
}
@@ -334,7 +325,8 @@ function newGame(name, race, customLevel = null) {
bought: { att: 0, esq: 0, deg: 0, reg: 0, pv: 0, vue: 0, armor: 0 },
comp: { pct: START_COMP_PCT }, sort: { pct: START_SORT_PCT }, fatigue: 0, compUsed: false,
compPXTurn: false, sortPXTurn: false,
weapon: null, armorItem: null,
equip: { arme: null, armure: null, casque: null, bouclier: null, talisman: null, bottes: null },
gearMods: null,
pa: PA_PER_TURN, pi: 0, totalPI: 0, gold: 0,
bag: [], camo: false, kills: 0, dla: 1, tour: 1,
potionEffects: [], blockCamoTurns: 0,
@@ -610,7 +602,7 @@ function attackMonster(m, opts = {}) {
const cost = opts.cost ?? COSTS.attack;
if (!spendPA(cost)) return false;
cdStart(`⚔️ Attaque sur ${m.name}`);
cdLine(`Vous avez attaqué <b>${m.name}</b> avec votre ${G.troll.weapon ? G.troll.weapon.name : "Grosse Patte de Trõll"}.`);
cdLine(`Vous avez attaqué <b>${m.name}</b> avec votre ${G.troll.equip.arme ? G.troll.equip.arme.name : "Grosse Patte de Trõll"}.`);
const r = resolveAttack(effTroll(G.troll), effMonster(m));
cdAttackRolls(r);
if (r.hit) {
@@ -967,19 +959,43 @@ function useBagItem(idx) {
updateFov();
} else if (item.kind === "gear") {
if (!spendPA(COSTS.equip)) return;
if (item.slot === "weapon") {
if (t.weapon) t.bag.push(t.weapon);
t.weapon = item; t.degBonus = item.bonus;
} else {
if (t.armorItem) t.bag.push(t.armorItem);
t.armorItem = item; t.armor = item.bonus;
}
t.bag.splice(t.bag.indexOf(item), 1);
log(`Tu t'équipes : ${item.emoji} ${item.name} (+${item.bonus}).`, "good");
equipGear(t, item);
}
afterAction();
}
/* Équipe une pièce : range l'ancienne au sac, gère l'exclusion arme à 2 mains /
* bouclier, applique le bonus de PV max et recalcule les modificateurs. */
function equipGear(t, item) {
const unequipTo = (slot) => {
const old = t.equip[slot];
if (!old) return;
t.equip[slot] = null;
t.bag.push(old);
if (old.mods.pv) t.pvMax = Math.max(1, t.pvMax - old.mods.pv);
log(`Tu ranges ${old.emoji} ${old.name} dans ton sac.`, "info");
};
if (item.slot === "arme" && item.twoHanded && t.equip.bouclier) {
unequipTo("bouclier");
log("Une arme à deux mains ne laisse pas de place au bouclier.", "info");
}
if (item.slot === "bouclier" && t.equip.arme && t.equip.arme.twoHanded) {
unequipTo("arme");
log("Impossible de tenir un bouclier avec une arme à deux mains : tu la ranges.", "info");
}
unequipTo(item.slot);
t.equip[item.slot] = item;
if (item.mods.pv) {
t.pvMax = Math.max(1, t.pvMax + item.mods.pv);
t.pv = Math.max(1, Math.min(t.pv, t.pvMax));
}
t.gearMods = gearMods(t.equip);
const fx = formatGearMods(item.mods);
log(`Tu t'équipes : ${item.emoji} ${item.name}${fx ? ` (${fx})` : ""}.`, "good");
if (G) updateFov();
}
/* Effet de zone d'un parchemin : touche tous les monstres à SCROLL_ZONE_RADIUS
* cases ou moins du troll (le lecteur a déjà subi sa part dans readScroll). */
function applyScrollZone(zone) {
@@ -1301,9 +1317,14 @@ function renderPanels() {
if (doorAt(t.x, t.y)) addBtn("🚪 Franchir la porte", enterDoor, true);
addBtn("⏳ Passer la DLA", passDLA, true);
$("equipment").innerHTML =
`<div>Arme : ${t.weapon ? t.weapon.emoji + " " + t.weapon.name + " (+" + t.weapon.bonus + ")" : "—"}</div>` +
`<div>Armure : ${t.armorItem ? t.armorItem.emoji + " " + t.armorItem.name + " (+" + t.armorItem.bonus + ")" : "—"}</div>`;
$("equipment").innerHTML = Object.entries(GEAR_SLOTS).map(([slot, info]) => {
const it = t.equip[slot];
if (!it) return `<div class="eq-line"><span class="eq-slot">${info.label}</span> —</div>`;
const fx = formatGearMods(it.mods);
return `<div class="eq-line"><span class="eq-slot">${info.label}</span> ${it.emoji} ${it.name}` +
`${it.twoHanded ? " <small>(2 mains)</small>" : ""}` +
`${fx ? `<div class="eq-mods">${fx}</div>` : ""}</div>`;
}).join("");
const inv = $("inventory");
inv.innerHTML = "";
@@ -1313,6 +1334,7 @@ function renderPanels() {
const action = item.kind === "potion" ? `boire (${COSTS.potion} PA)`
: item.kind === "scroll" ? `lire (${COSTS.scroll} PA)` : `équiper (${COSTS.equip} PA)`;
b.textContent = `${item.emoji} ${item.name}${action}`;
if (item.kind === "gear") b.title = formatGearMods(item.mods) + (item.twoHanded ? " · 2 mains" : "");
b.disabled = G.over;
b.onclick = () => useBagItem(idx);
inv.appendChild(b);
@@ -1459,7 +1481,7 @@ if (typeof document !== "undefined") {
if (typeof module !== "undefined" && module.exports) {
module.exports = {
APP_VERSION, rollDice, resolveAttack, resolveSpell, masteryRoll, improveCost, levelFromTotalPI, killPX,
RACES, MONSTER_TYPES, BOSS, TEMPLATES, makeMonster, monsterFromSpec, itemFromSpec,
RACES, MONSTER_TYPES, BOSS, TEMPLATES, makeMonster, monsterFromSpec, itemFromSpec, equipGear,
generateCavern, largestRegion,
MAP_W, MAP_H, T_WALL, T_FLOOR, T_STAIRS,
newGame, refreshFov, inSightAt,
+149
View File
@@ -0,0 +1,149 @@
/* Équipement MountyHall — 6 emplacements, objets dropables actuels de la Mountypedia :
* https://mountypedia.mountyhall.com/Mountyhall/{Arme,Armure,Casque,Bouclier,Talisman,Bottes}
* Valeurs de base (sans templates) : ATT/ESQ/DEG/REG modifient le nombre de dés,
* ARM/VUE/PV sont fixes, RM/MM en %. `tier` (1-5) borne la profondeur de drop.
* Les armes à deux mains (twoHanded) sont incompatibles avec un bouclier. */
"use strict";
const GEAR_SLOTS = {
arme: { label: "Arme", emoji: "🗡️" },
armure: { label: "Armure", emoji: "🦺" },
casque: { label: "Casque", emoji: "🪖" },
bouclier: { label: "Bouclier", emoji: "🛡️" },
talisman: { label: "Talisman", emoji: "📿" },
bottes: { label: "Bottes", emoji: "🥾" },
};
const GEAR = {
arme: [
{ name: "Torche", emoji: "🔥", tier: 1, mods: { vue: 1 } },
{ name: "Lame en os", emoji: "🦴", tier: 1, mods: { mmPct: 5 } },
{ name: "Épée Courte", emoji: "🗡️", tier: 1, mods: { deg: 2, esq: 2 } },
{ name: "Gantelet", emoji: "🥊", tier: 1, mods: { att: -1, esq: 1, deg: 1, arm: 2 } },
{ name: "Lame en pierre", emoji: "🪨", tier: 2, mods: { att: -1, deg: 4, reg: 2 } },
{ name: "Épée Longue", emoji: "⚔️", tier: 2, mods: { att: -1, deg: 4, esq: 1 } },
{ name: "Gourdin", emoji: "🏏", tier: 2, mods: { att: 2, esq: -2, deg: 5 } },
{ name: "Boulet et chaîne", emoji: "⛓️", tier: 2, mods: { att: -1, deg: 5 } },
{ name: "Grosse racine", emoji: "🌿", tier: 2, mods: { esq: -1, arm: 2, pv: 5, rmPct: 5 } },
{ name: "Coutelas d'obsidienne", emoji: "🔪", tier: 3, mods: { att: 2, deg: 3, esq: 2, reg: -2, rmPct: -5, mmPct: -15 } },
{ name: "Fouet", emoji: "🪢", tier: 4, mods: { att: 7, deg: -1 } },
{ name: "Lame d'obsidienne", emoji: "🖤", tier: 4, mods: { att: 3, deg: 7, reg: -3, rmPct: -30, mmPct: -10 } },
{ name: "Bâton de mage", emoji: "🪄", tier: 3, twoHanded: true, mods: { mmPct: 15 } },
{ name: "Hallebarde", emoji: "🪓", tier: 4, twoHanded: true, mods: { att: -5, deg: 12 } },
{ name: "Hache à deux mains d'Obsidienne", emoji: "⚒️", tier: 5, twoHanded: true, mods: { att: -8, deg: 16, reg: -4, rmPct: -45, mmPct: -15 } },
],
armure: [
{ name: "Culotte en Cuir", emoji: "🩲", tier: 1, mods: { esq: 5, rmPct: 60 } },
{ name: "Pagne en Cuir", emoji: "🧵", tier: 1, mods: { esq: 4, arm: 2, rmPct: 50 } },
{ name: "Armure de cuir", emoji: "🦺", tier: 2, mods: { esq: 3, arm: 4, rmPct: 40 } },
{ name: "Robe de mage", emoji: "🥻", tier: 2, mods: { esq: 2, arm: 1, rmPct: 10, mmPct: 10 } },
{ name: "Fourrures", emoji: "🐻", tier: 2, mods: { esq: 2, arm: 6, rmPct: 30 } },
{ name: "Cuir bouilli", emoji: "🟤", tier: 3, mods: { esq: 1, arm: 8, rmPct: 20 } },
{ name: "Armure de peaux", emoji: "🐗", tier: 3, mods: { esq: -3, arm: 9, rmPct: 70 } },
{ name: "Armure de bois", emoji: "🪵", tier: 3, mods: { esq: -3, arm: 6, pv: 15, rmPct: 50 } },
{ name: "Cuirasse d'Ossements", emoji: "💀", tier: 4, mods: { esq: -3, arm: 6, rmPct: 50, mmPct: 20 } },
{ name: "Armure d'Anneaux", emoji: "⭕", tier: 4, mods: { esq: -4, arm: 8, rmPct: 90 } },
{ name: "Haubert de mailles", emoji: "🥋", tier: 4, mods: { esq: -3, arm: 9, rmPct: 70 } },
{ name: "Armure de plates", emoji: "🛡️", tier: 5, mods: { esq: -2, arm: 10, rmPct: 50 } },
{ name: "Armure de pierre", emoji: "🗿", tier: 5, mods: { esq: -6, arm: 12, reg: 3, rmPct: 75 } },
],
casque: [
{ name: "Chapeau pointu", emoji: "🎩", tier: 1, mods: { esq: 3, mmPct: 5 } },
{ name: "Lorgnons", emoji: "👓", tier: 1, mods: { vue: 1, mmPct: 5 } },
{ name: "Couronne de ronces", emoji: "🌹", tier: 2, mods: { deg: 3, pv: -5 } },
{ name: "Couronne d'obsidienne", emoji: "👑", tier: 2, mods: { reg: -1, arm: 1, rmPct: 5 } },
{ name: "Casque à Cornes", emoji: "🐮", tier: 3, mods: { deg: 1, arm: 3, vue: -1, rmPct: 5 } },
{ name: "Casque à Pointes", emoji: "🦔", tier: 3, mods: { att: 1, deg: 1, arm: 3, vue: -1 } },
{ name: "Couronne de Cristal", emoji: "💎", tier: 3, mods: { vue: 3, mmPct: 5 } },
{ name: "Heaume", emoji: "🪖", tier: 4, mods: { att: -1, arm: 4, vue: -2, rmPct: 10 } },
{ name: "Turban", emoji: "👳", tier: 4, mods: { rmPct: 15, mmPct: 15 } },
],
bouclier: [
{ name: "Targe", emoji: "🎯", tier: 1, mods: { esq: 3, att: 1 } },
{ name: "Grimoire", emoji: "📖", tier: 2, mods: { mmPct: 5 }, note: "DLA +15 mn (non simulé)" },
{ name: "Rondache en Bois", emoji: "🪵", tier: 2, mods: { esq: 1, arm: 3, pv: 5 } },
{ name: "Bouclier à Pointes", emoji: "🛡️", tier: 3, mods: { att: 1, deg: 1, arm: 4 } },
{ name: "Gros'Porte", emoji: "🚪", tier: 4, mods: { arm: 5, rmPct: 10 } },
],
talisman: [
{ name: "Collier à Pointes", emoji: "📿", tier: 1, mods: { deg: 2, arm: 1 } },
{ name: "Talisman de pierre", emoji: "🪨", tier: 2, mods: { reg: 2, rmPct: 10, mmPct: 10 } },
{ name: "Gorgeron en métal", emoji: "⚙️", tier: 2, mods: { reg: -1, arm: 3, rmPct: 10 } },
{ name: "Œil de sang", emoji: "🩸", tier: 3, mods: { pv: -5, mmPct: 10 } },
{ name: "Pendentif Incandescent", emoji: "🔥", tier: 3, mods: { esq: 3, rmPct: 10, mmPct: 10 } },
{ name: "Torque en pierre", emoji: "🌀", tier: 4, mods: { reg: -2, rmPct: 20, mmPct: 20 } },
{ name: "Talisman d'Obsidienne", emoji: "🖤", tier: 5, mods: { att: 1, deg: 2, reg: -4, rmPct: 22, mmPct: 22 } },
],
bottes: [
{ name: "Bottes", emoji: "🥾", tier: 1, mods: { esq: 3 } },
{ name: "Jambière en cuir", emoji: "🦵", tier: 2, mods: { esq: 1, arm: 2, rmPct: 5 } },
{ name: "Jambière en os", emoji: "🦴", tier: 2, mods: { esq: -1, arm: 2, rmPct: 5, mmPct: 5 } },
{ name: "Souliers dorés", emoji: "👞", tier: 3, mods: { esq: -1, arm: 1 } },
{ name: "Jambière en mailles", emoji: "⛓️", tier: 3, mods: { arm: 3, rmPct: 5 } },
{ name: "Jambière en métal", emoji: "🦿", tier: 4, mods: { esq: -1, arm: 4, rmPct: 5 } },
],
};
/* Anciennes listes (v1 « bonus simple ») : mapping pour les niveaux déjà publiés. */
const LEGACY_WEAPON_NAMES = ["Épée Courte", "Gourdin", "Épée Longue", "Lame d'obsidienne"];
const LEGACY_ARMOR_NAMES = ["Armure de cuir", "Haubert de mailles", "Armure de plates"];
const GEAR_MOD_LABELS = [
["att", "ATT", " dé(s)"], ["esq", "ESQ", " dé(s)"], ["deg", "DEG", " dé(s)"],
["reg", "REG", " dé(s)"], ["arm", "Armure", ""], ["vue", "VUE", ""], ["pv", "PV", ""],
["rmPct", "RM", " %"], ["mmPct", "MM", " %"],
];
function formatGearMods(mods, sep = " · ") {
const parts = [];
for (const [key, label, unit] of GEAR_MOD_LABELS) {
const v = mods[key];
if (!v) continue;
parts.push(`${label} ${v > 0 ? "+" : ""}${Math.abs(v)}${unit}`);
}
return parts.join(sep);
}
function makeGearItem(slot, idx) {
const list = GEAR[slot];
if (!list) return null;
const def = list[((idx % list.length) + list.length) % list.length];
return {
kind: "gear", slot, gearIdx: list.indexOf(def),
name: def.name, emoji: def.emoji,
mods: { ...def.mods }, twoHanded: !!def.twoHanded, note: def.note,
};
}
function gearItemByName(slot, name) {
const idx = (GEAR[slot] || []).findIndex(g => g.name === name);
return idx >= 0 ? makeGearItem(slot, idx) : null;
}
/* Tire une pièce d'équipement adaptée à la profondeur (tier ≤ depth + 1). */
function makeRandomGear(depth, slot = null) {
const slots = Object.keys(GEAR);
const s = slot && GEAR[slot] ? slot : slots[Math.floor(Math.random() * slots.length)];
const pool = GEAR[s].filter(g => g.tier <= (depth || 1) + 1);
const list = pool.length ? pool : GEAR[s];
const def = list[Math.floor(Math.random() * list.length)];
return makeGearItem(s, GEAR[s].indexOf(def));
}
/* Somme des modificateurs de l'équipement porté ({ arme: item|null, … }). */
function gearMods(equip) {
const total = { att: 0, esq: 0, deg: 0, reg: 0, arm: 0, vue: 0, pv: 0, rmPct: 0, mmPct: 0 };
for (const item of Object.values(equip || {})) {
if (!item || !item.mods) continue;
for (const k of Object.keys(total)) total[k] += item.mods[k] || 0;
}
return total;
}
if (typeof module !== "undefined" && module.exports) {
module.exports = {
GEAR, GEAR_SLOTS, GEAR_MOD_LABELS, LEGACY_WEAPON_NAMES, LEGACY_ARMOR_NAMES,
makeGearItem, gearItemByName, makeRandomGear, gearMods, formatGearMods,
};
}
+11 -8
View File
@@ -413,24 +413,27 @@ function fmtStatLine(base, eff, faces, flat = 0, extra = 0) {
return main;
}
/* Caractéristiques effectives : base + effets magiques (potions, parchemins)
* + équipement (troll.gearMods, recalculé par game.js à chaque équipement). */
function effTroll(troll) {
const m = sumPotionMods(troll.potionEffects);
const g = troll.gearMods || {};
return {
att: Math.max(1, troll.att + m.att),
att: Math.max(1, troll.att + m.att + (g.att || 0)),
attFlat: m.attFlat,
esq: Math.max(1, troll.esq + m.esq),
esq: Math.max(1, troll.esq + m.esq + (g.esq || 0)),
esqFlat: m.esqFlat,
deg: Math.max(1, troll.deg + m.deg),
deg: Math.max(1, troll.deg + m.deg + (g.deg || 0)),
degFlat: m.degFlat,
reg: Math.max(1, troll.reg + m.reg),
reg: Math.max(1, troll.reg + m.reg + (g.reg || 0)),
regFlat: m.regFlat,
vue: Math.max(1, troll.vue + m.vue),
armor: Math.max(0, troll.armor + m.armor),
vue: Math.max(1, troll.vue + m.vue + (g.vue || 0)),
armor: Math.max(0, troll.armor + m.armor + (g.arm || 0)),
armorDice: troll.armorDice,
degBonus: troll.degBonus || 0,
pvMax: troll.pvMax,
mmPct: m.mmPct,
rmPct: m.rmPct,
mmPct: m.mmPct + (g.mmPct || 0),
rmPct: m.rmPct + (g.rmPct || 0),
concentrationPct: m.concentrationPct,
dlaBonusPA: m.dlaBonusPA,
};
+29
View File
@@ -80,6 +80,35 @@ function treasuresRender() {
}
html += "</div>";
const gearCount = Object.values(GEAR).reduce((n, list) => n + list.length, 0);
html += `<h2 class="tz-section">⚔️ Équipement (${gearCount})</h2>
<p class="tz-intro">Six emplacements : ${Object.values(GEAR_SLOTS).map(s => s.label).join(", ")}.
Les bonus ATT/ESQ/DEG/REG s'ajoutent en <b>dés</b>, Armure/VUE/PV sont fixes, RM/MM en %.
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
qu'en profondeur.</p>`;
for (const [slot, info] of Object.entries(GEAR_SLOTS)) {
html += `<h3 class="tz-subsection">${info.emoji} ${info.label}s</h3><div class="tz-grid">`;
for (const def of GEAR[slot]) {
const fx = [];
for (const [key, label, unit] of GEAR_MOD_LABELS) {
const v = def.mods[key];
if (!v) continue;
const cls = v > 0 ? "tz-fx tz-good" : "tz-fx tz-bad";
fx.push(`<span class="${cls}">${label} ${v > 0 ? "+" : ""}${Math.abs(v)}${unit}</span>`);
}
if (def.twoHanded) fx.push('<span class="tz-fx">2 mains</span>');
if (def.note) fx.push(`<span class="tz-fx">${def.note}</span>`);
html += `
<div class="tz-card" style="--tz-color:#9a8a65">
<div class="tz-head"><span class="tz-name">${def.emoji} ${def.name}</span></div>
<div class="tz-meta">Profondeur : <b>${Math.max(1, def.tier - 1)} et au-delà</b></div>
<div class="tz-fx-list">${fx.join("")}</div>
</div>`;
}
html += "</div>";
}
html += `<p class="tz-note">Effet de Zone : touche le lecteur et tous les monstres à 3 cases
ou moins. « PV 2D3 ou 4D3 » (Rune des Foins) : la Mountypedia note « -2/4 D3 », interprété
ici comme un tirage au sort entre 2D3 et 4D3.</p>`;
+1 -1
View File
@@ -57,7 +57,7 @@ function validateLevel(l) {
if (!Array.isArray(l.items) || l.items.length > 60) return "objets invalides (max 60)";
for (const i of l.items) {
if (!inBounds(i) || tile(l, i) === "#") return "objet hors-sol";
if (!["potion", "scroll", "gold", "weapon", "armor"].includes(i.kind)) return "type d'objet invalide";
if (!["potion", "scroll", "gear", "gold", "weapon", "armor"].includes(i.kind)) return "type d'objet invalide";
}
if (l.doors !== undefined && !Array.isArray(l.doors)) return "portes invalides";
const doors = l.doors || [];
+39 -1
View File
@@ -260,9 +260,47 @@ assert.strictEqual(p.corruptionYZ(3).y, 0);
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(sword.bonus > 0);
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
g.equipGear(t, gear.gearItemByName("armure", "Armure de cuir")); // esq +3, arm +4, rm +40 %
let e = p.effTroll(t);
assert.strictEqual(e.att, 5, "Gourdin : ATT 3 + 2 dés");
assert.strictEqual(e.deg, 8, "Gourdin : DEG 3 + 5 dés");
assert.strictEqual(e.esq, 4, "ESQ 3 2 + 3");
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.att, 1, "Hallebarde : ATT 3 5 → min 1");
assert.strictEqual(e.deg, 15, "Hallebarde : DEG 3 + 12");
// 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");