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:
@@ -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
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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>`;
|
||||
|
||||
@@ -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
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user