1.6.0 : parchemins standards + page « Trésors du Hall »
- js/scrolls.js : les 7 parchemins standards de Mountypedia (X 1-5), effets via le système d'effets magiques, un seul jet de D3 par parchemin - Effets de zone (rayon 3 cases) : Rune Explosive (dégâts aux monstres), Yeu'Ki'Pic (VUE des monstres réduite, statut à durée) - TOUR ±X*30 min converti en ±X PA par tour (PA min. 1) - Drops : coffres (12 %), butin de monstres (25 %), pinceau éditeur, validation serveur du kind « scroll » - js/treasures.js : page encyclopédie des 25 potions et 7 parchemins (niveau X, durée, formules), boutons accueil + en jeu 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.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
|
||||
et pinceau d'éditeur. Effets de zone (rayon 3 cases : dégâts ou aveuglement des
|
||||
monstres), TOUR accéléré/ralenti converti en PA (±30 min = ±1 PA, min. 1 PA par
|
||||
tour). Nouvelle page « 🧪 Trésors du Hall » : fiches des 25 potions et des
|
||||
7 parchemins (niveau X, durée, formules), accessible depuis l'accueil et en jeu.
|
||||
- **1.5.2** (2026-06-11) — Les bonus/malus DEG et REG des potions sont des valeurs
|
||||
fixes ajoutées au jet, plus des dés supplémentaires (seules les notations « X D3 »
|
||||
de Mountypedia sont des jets). Concerne Bonne Bouffe, Fertilité, Corruption,
|
||||
|
||||
@@ -473,3 +473,50 @@ header h1 { font-size: 1.3em; color: #8fbf5a; }
|
||||
#log .l-bad { color: #e06060; }
|
||||
#log .l-good { color: #8fbf5a; }
|
||||
#log .l-info { color: #9a8a65; }
|
||||
|
||||
/* ---------- Trésors du Hall (potions & parchemins) ---------- */
|
||||
#treasures-body {
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
padding: 10px 16px 40px;
|
||||
}
|
||||
.tz-intro, .tz-note {
|
||||
color: #b9a878;
|
||||
line-height: 1.5;
|
||||
font-size: .92em;
|
||||
}
|
||||
.tz-intro a { color: #d8a040; }
|
||||
.tz-note { font-style: italic; margin-top: 18px; }
|
||||
.tz-section {
|
||||
color: #8fbf5a;
|
||||
border-bottom: 2px solid #4a3a20;
|
||||
margin: 24px 0 12px;
|
||||
font-size: 1.3em;
|
||||
}
|
||||
.tz-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.tz-card {
|
||||
background: #2a2014;
|
||||
border: 2px solid #6b5430;
|
||||
border-left: 6px solid var(--tz-color, #6b5430);
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
font-size: .92em;
|
||||
}
|
||||
.tz-name { color: #d8a040; font-weight: bold; }
|
||||
.tz-meta { color: #a89568; font-size: .88em; margin: 4px 0 8px; }
|
||||
.tz-meta b { color: #d8c79a; }
|
||||
.tz-fx-list { display: flex; flex-wrap: wrap; gap: 4px; }
|
||||
.tz-fx {
|
||||
background: #1f1810;
|
||||
border: 1px solid #4a3a20;
|
||||
border-radius: 4px;
|
||||
padding: 2px 7px;
|
||||
color: #d8c79a;
|
||||
font-size: .9em;
|
||||
}
|
||||
.tz-good { color: #8fbf5a; }
|
||||
.tz-bad { color: #d88080; }
|
||||
|
||||
+13
@@ -23,6 +23,7 @@
|
||||
<button id="btn-community" class="menu-btn">🌍 Niveaux de la communauté</button>
|
||||
<button id="btn-editor" class="menu-btn">🛠️ Éditeur de niveaux</button>
|
||||
<button id="btn-rules" class="menu-btn">📜 Règles des talents</button>
|
||||
<button id="btn-treasures" class="menu-btn">🧪 Potions & Parchemins</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -87,6 +88,7 @@
|
||||
<div class="header-right">
|
||||
<div id="depth-label">Profondeur −1</div>
|
||||
<button id="btn-rules-game" class="menu-btn">📜 Règles</button>
|
||||
<button id="btn-treasures-game" class="menu-btn">🧪 Trésors</button>
|
||||
</div>
|
||||
</header>
|
||||
<div id="layout">
|
||||
@@ -142,6 +144,15 @@
|
||||
<div id="rules-body"></div>
|
||||
</div>
|
||||
|
||||
<!-- Écran des trésors : potions et parchemins -->
|
||||
<div id="screen-treasures" class="screen hidden">
|
||||
<header>
|
||||
<h1>🧪 Trésors du Hall — Potions & Parchemins</h1>
|
||||
<button id="treasures-back" class="menu-btn">← Retour</button>
|
||||
</header>
|
||||
<div id="treasures-body"></div>
|
||||
</div>
|
||||
|
||||
<!-- Écran de fin -->
|
||||
<div id="screen-end" class="screen hidden">
|
||||
<div class="create-box">
|
||||
@@ -154,8 +165,10 @@
|
||||
<footer id="app-footer" class="app-footer" aria-label="Version"></footer>
|
||||
|
||||
<script src="js/potions.js"></script>
|
||||
<script src="js/scrolls.js"></script>
|
||||
<script src="js/game.js"></script>
|
||||
<script src="js/editor.js"></script>
|
||||
<script src="js/rules.js"></script>
|
||||
<script src="js/treasures.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+4
-2
@@ -122,6 +122,7 @@ function edBuildPalette() {
|
||||
const itemsDiv = document.getElementById("ed-items");
|
||||
itemsDiv.innerHTML = "";
|
||||
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 }));
|
||||
@@ -274,9 +275,10 @@ function edRender() {
|
||||
ctx.fill();
|
||||
};
|
||||
for (const i of ED.items) {
|
||||
disc(i.x, i.y, i.kind === "gold" ? "#caa53d" : i.kind === "potion" ? "#5d8535" : "#7a8db0");
|
||||
disc(i.x, i.y, i.kind === "gold" ? "#caa53d" : i.kind === "potion" ? "#5d8535"
|
||||
: i.kind === "scroll" ? "#d8c890" : "#7a8db0");
|
||||
ctx.fillStyle = "#1a140e";
|
||||
const emoji = i.kind === "potion" ? "🧪" : i.kind === "gold" ? "💰"
|
||||
const emoji = i.kind === "potion" ? "🧪" : i.kind === "scroll" ? "📜" : i.kind === "gold" ? "💰"
|
||||
: i.kind === "weapon" ? WEAPONS[i.idx || 0].emoji : ARMORS[i.idx || 0].emoji;
|
||||
ctx.fillText(emoji, i.x * TILE + TILE / 2, i.y * TILE + TILE / 2 + 1);
|
||||
}
|
||||
|
||||
+57
-11
@@ -4,16 +4,19 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
const APP_VERSION = "1.5.2";
|
||||
const APP_VERSION = "1.6.0";
|
||||
|
||||
/* Alpha : maîtrise initiale haute pour les tests. Remettre 15 % / 15 % à la v1.0 officielle. */
|
||||
const START_COMP_PCT = 90;
|
||||
const START_SORT_PCT = 80;
|
||||
|
||||
/* potions.js est chargé avant ce fichier dans le navigateur ; en node, require explicite. */
|
||||
/* potions.js et scrolls.js sont chargés avant ce fichier dans le navigateur ; en node, require explicite. */
|
||||
if (typeof module !== "undefined" && module.exports && typeof makeRandomPotion === "undefined") {
|
||||
Object.assign(globalThis, require("./potions.js"));
|
||||
}
|
||||
if (typeof module !== "undefined" && module.exports && typeof makeRandomScroll === "undefined") {
|
||||
Object.assign(globalThis, require("./scrolls.js"));
|
||||
}
|
||||
|
||||
/* ================= Dés & règles de base ================= */
|
||||
|
||||
@@ -202,6 +205,7 @@ function monsterFromSpec(spec) {
|
||||
function itemFromSpec(spec) {
|
||||
const base = { x: spec.x, y: spec.y };
|
||||
if (spec.kind === "potion") return { ...base, ...makePotionItem(spec.potionId, spec.power) };
|
||||
if (spec.kind === "scroll") return { ...base, ...makeScrollItem(spec.scrollId, spec.power) };
|
||||
if (spec.kind === "gold") {
|
||||
const gold = spec.gold || 60;
|
||||
return { ...base, kind: "gold", name: `${gold} Mountyzédons`, emoji: "💰", gold };
|
||||
@@ -296,8 +300,9 @@ const ARMORS = [
|
||||
|
||||
function makeItem(depth) {
|
||||
const r = Math.random();
|
||||
if (r < 0.35) return makeRandomPotion();
|
||||
if (r < 0.55) {
|
||||
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 };
|
||||
}
|
||||
@@ -313,7 +318,7 @@ function makeItem(depth) {
|
||||
|
||||
const MAX_DEPTH = 5;
|
||||
const PA_PER_TURN = 6;
|
||||
const COSTS = { move: 1, attack: 3, pickup: 1, equip: 2, potion: 1 };
|
||||
const COSTS = { move: 1, attack: 3, pickup: 1, equip: 2, potion: 1, scroll: 1 };
|
||||
|
||||
let G = null; // état global de la partie
|
||||
|
||||
@@ -651,9 +656,12 @@ function killMonster(m, source = "attack") {
|
||||
}
|
||||
// butin : les monstres lâchent parfois un trésor en mourant
|
||||
if (Math.random() < 0.4 && !G.items.some(i => i.x === m.x && i.y === m.y)) {
|
||||
const drop = Math.random() < 0.5
|
||||
const lr = Math.random();
|
||||
const drop = lr < 0.4
|
||||
? { ...makeRandomPotion(), x: m.x, y: m.y }
|
||||
: { kind: "gold", gold: m.level * 10, name: `${m.level * 10} Mountyzédons`, emoji: "💰", x: m.x, y: m.y };
|
||||
: lr < 0.65
|
||||
? { ...makeRandomScroll(), x: m.x, y: m.y }
|
||||
: { kind: "gold", gold: m.level * 10, name: `${m.level * 10} Mountyzédons`, emoji: "💰", x: m.x, y: m.y };
|
||||
G.items.push(drop);
|
||||
cdLine(`Il a de plus laissé tomber <span class="cd-val">${drop.emoji} ${drop.name}</span>.`);
|
||||
log(`${m.name} laisse tomber ${drop.emoji} ${drop.name}.`, "info");
|
||||
@@ -906,6 +914,7 @@ function effMonster(m) {
|
||||
...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)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -948,6 +957,14 @@ function useBagItem(idx) {
|
||||
t.bag.splice(idx, 1);
|
||||
if (countActiveEffects(t) > 0) switchLeftTab("effects");
|
||||
updateFov();
|
||||
} else if (item.kind === "scroll") {
|
||||
if (!spendPA(COSTS.scroll)) return;
|
||||
const res = readScroll(t, item, rollDice, log);
|
||||
if (!res) return;
|
||||
t.bag.splice(idx, 1);
|
||||
if (res.zone) applyScrollZone(res.zone);
|
||||
if (countActiveEffects(t) > 0) switchLeftTab("effects");
|
||||
updateFov();
|
||||
} else if (item.kind === "gear") {
|
||||
if (!spendPA(COSTS.equip)) return;
|
||||
if (item.slot === "weapon") {
|
||||
@@ -963,6 +980,31 @@ function useBagItem(idx) {
|
||||
afterAction();
|
||||
}
|
||||
|
||||
/* 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) {
|
||||
const t = G.troll;
|
||||
const targets = G.monsters.filter(m =>
|
||||
Math.max(Math.abs(m.x - t.x), Math.abs(m.y - t.y)) <= SCROLL_ZONE_RADIUS);
|
||||
if (!targets.length) return;
|
||||
if (zone.type === "damage") {
|
||||
cdStart("💥 Effet de zone");
|
||||
for (const m of targets) {
|
||||
m.pv -= zone.total;
|
||||
cdLine(`${m.emoji} ${m.name} subit <span class="cd-val">${zone.total} points de dégâts</span> (${zone.label}).`);
|
||||
log(`💥 ${m.name} est pris dans l'explosion : −${zone.total} PV.`, "combat");
|
||||
if (m.pv <= 0) killMonster(m, "scroll");
|
||||
}
|
||||
cdFlush();
|
||||
} else if (zone.type === "vue") {
|
||||
for (const m of targets) {
|
||||
m.vueMalus = Math.max(m.vueMalus || 0, zone.malus);
|
||||
m.vueMalusTurns = Math.max(m.vueMalusTurns || 0, zone.turns);
|
||||
log(`🌶️ ${m.name} est aveuglé : VUE −${zone.malus} pendant ${zone.turns} tour(s).`, "good");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function descend() {
|
||||
if (G.grid[G.troll.y][G.troll.x] !== T_STAIRS) return;
|
||||
if (G.custom) {
|
||||
@@ -1010,7 +1052,7 @@ function passDLA() {
|
||||
continue;
|
||||
}
|
||||
const dist = Math.max(Math.abs(m.x - t.x), Math.abs(m.y - t.y));
|
||||
const seesTroll = !t.camo && dist <= m.vue;
|
||||
const seesTroll = !t.camo && dist <= effMonster(m).vue;
|
||||
if (dist <= 1 && !t.camo) {
|
||||
const eff = effMonster(m);
|
||||
const r = resolveAttack(eff, effTroll(t));
|
||||
@@ -1037,6 +1079,7 @@ function passDLA() {
|
||||
// les statuts s'estompent à la fin de l'activation du monstre
|
||||
if (m.esqHalfTurns > 0) m.esqHalfTurns--;
|
||||
if (m.attDownTurns > 0 && --m.attDownTurns === 0) m.attDownDice = 0;
|
||||
if (m.vueMalusTurns > 0 && --m.vueMalusTurns === 0) m.vueMalus = 0;
|
||||
}
|
||||
|
||||
// Régénération (REG D3, comme à MountyHall) — bonus potions pris en compte
|
||||
@@ -1051,7 +1094,8 @@ function passDLA() {
|
||||
log(`💤 Tour n°${t.tour} (DLA n°${t.dla}).`, "info");
|
||||
}
|
||||
|
||||
t.pa = Math.min(PA_PER_TURN + 3, PA_PER_TURN + dlaBonusPA);
|
||||
// bonus/malus de PA (potions, parchemins) : entre 1 et PA_PER_TURN + 3
|
||||
t.pa = Math.max(1, Math.min(PA_PER_TURN + 3, PA_PER_TURN + dlaBonusPA));
|
||||
t.compUsed = false;
|
||||
t.compPXTurn = false;
|
||||
t.sortPXTurn = false;
|
||||
@@ -1150,7 +1194,8 @@ function render() {
|
||||
};
|
||||
for (const i of G.items) {
|
||||
if (!inSightAt(i.x, i.y)) continue;
|
||||
disc(i.x, i.y, i.kind === "gold" ? "#caa53d" : i.kind === "potion" ? (i.color || "#5d8535") : "#7a8db0");
|
||||
disc(i.x, i.y, i.kind === "gold" ? "#caa53d"
|
||||
: i.kind === "potion" || i.kind === "scroll" ? (i.color || "#5d8535") : "#7a8db0");
|
||||
ctx.fillStyle = "#1a140e";
|
||||
ctx.fillText(i.emoji, i.x * TILE + TILE / 2, i.y * TILE + TILE / 2 + 1);
|
||||
}
|
||||
@@ -1265,7 +1310,8 @@ function renderPanels() {
|
||||
if (t.bag.length === 0) inv.innerHTML = '<div class="empty">— vide —</div>';
|
||||
t.bag.forEach((item, idx) => {
|
||||
const b = document.createElement("button");
|
||||
const action = item.kind === "potion" ? `boire (${COSTS.potion} PA)` : `équiper (${COSTS.equip} PA)`;
|
||||
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}`;
|
||||
b.disabled = G.over;
|
||||
b.onclick = () => useBagItem(idx);
|
||||
|
||||
+3
-3
@@ -518,7 +518,7 @@ function formatEffectMods(effect) {
|
||||
if (effect.mmPct) parts.push(`MM ${effect.mmPct > 0 ? "+" : ""}${effect.mmPct} %`);
|
||||
if (effect.rmPct) parts.push(`RM ${effect.rmPct > 0 ? "+" : ""}${effect.rmPct} %`);
|
||||
if (effect.concentrationPct) parts.push(`Concentration ${effect.concentrationPct > 0 ? "+" : ""}${effect.concentrationPct} %`);
|
||||
if (effect.dlaBonusPA) parts.push(`DLA +${effect.dlaBonusPA} PA/tour`);
|
||||
if (effect.dlaBonusPA) parts.push(`DLA ${effect.dlaBonusPA > 0 ? "+" : ""}${effect.dlaBonusPA} PA/tour`);
|
||||
if (effect.blockCamo) parts.push("Visible — camouflage impossible");
|
||||
return parts;
|
||||
}
|
||||
@@ -581,7 +581,7 @@ function renderEffectsPanel(troll) {
|
||||
if (total.mmPct) totalLines.push(`MM ${total.mmPct > 0 ? "+" : ""}${total.mmPct} %`);
|
||||
if (total.rmPct) totalLines.push(`RM ${total.rmPct > 0 ? "+" : ""}${total.rmPct} %`);
|
||||
if (total.concentrationPct) totalLines.push(`Concentration ${total.concentrationPct > 0 ? "+" : ""}${total.concentrationPct} %`);
|
||||
if (total.dlaBonusPA) totalLines.push(`DLA +${total.dlaBonusPA} PA/tour`);
|
||||
if (total.dlaBonusPA) totalLines.push(`DLA ${total.dlaBonusPA > 0 ? "+" : ""}${total.dlaBonusPA} PA/tour`);
|
||||
if (troll.blockCamoTurns > 0) totalLines.push("Camouflage bloqué");
|
||||
|
||||
let html = "";
|
||||
@@ -613,6 +613,6 @@ if (typeof module !== "undefined" && module.exports) {
|
||||
POTION_IDS, POTION_DEFS, sumPotionMods, effTroll, formatPotionItem,
|
||||
makeRandomPotion, makePotionItem, drinkPotion, tickPotionTurns,
|
||||
describeActiveEffects, renderEffectsPanel, countActiveEffects, formatEffectMods,
|
||||
sharedMod, fmtStatLine, talentPctWithPotions, corruptionYZ,
|
||||
sharedMod, makeEffect, fmtStatLine, talentPctWithPotions, corruptionYZ,
|
||||
};
|
||||
}
|
||||
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
/* Parchemins MountyHall — trésors à lire, effets et durée en tours (1 tour = 1 DLA).
|
||||
* Source : https://mountypedia.mountyhall.com/Mountyhall/Parchemins
|
||||
* Convention temps → PA : −30 min de TOUR = +1 PA au début du tour (comme le
|
||||
* Jus de Chronomètre) ; −15 min = +1 PA par tranche de 2 niveaux (arrondi sup.).
|
||||
* Les effets de zone touchent le lecteur et tout monstre dans un rayon de
|
||||
* SCROLL_ZONE_RADIUS cases. */
|
||||
|
||||
"use strict";
|
||||
|
||||
/* potions.js est chargé avant ce fichier dans le navigateur ; en node, require explicite. */
|
||||
if (typeof module !== "undefined" && module.exports && typeof sharedMod === "undefined") {
|
||||
Object.assign(globalThis, require("./potions.js"));
|
||||
}
|
||||
|
||||
const SCROLL_ZONE_RADIUS = 3;
|
||||
|
||||
const SCROLL_IDS = [
|
||||
"runeCyclopes", "clairvoyance", "ideesConfuses", "runeExplosive",
|
||||
"planGenial", "yeuKiPic", "runeFoins",
|
||||
];
|
||||
|
||||
function scrollRand(min, max) {
|
||||
return min + Math.floor(Math.random() * (max - min + 1));
|
||||
}
|
||||
|
||||
const SCROLL_DEFS = {
|
||||
runeCyclopes: {
|
||||
name: "Rune des Cyclopes", emoji: "👁️", color: "#c89048",
|
||||
duration: 4,
|
||||
build(_t, p, rollDice) {
|
||||
const attR = sharedMod(rollDice(p, 3).total, p, 3, false);
|
||||
const lines = [`ATT ${attR.line}`, `DEG +${p}`, `VUE −${p}`];
|
||||
return {
|
||||
log: `Rune des Cyclopes : ${lines.join(", ")} pendant 4 tours.`,
|
||||
effects: [makeEffect("Rune des Cyclopes", "👁️", 4, {
|
||||
attFlat: attR.value, degFlat: p, vue: -p,
|
||||
}, lines)],
|
||||
};
|
||||
},
|
||||
},
|
||||
clairvoyance: {
|
||||
name: "Traité de Clairvoyance", emoji: "🔮", color: "#88a8e0",
|
||||
duration: 4,
|
||||
build(_t, p) {
|
||||
const lines = [`VUE +${p}`, `DLA +${p} PA/tour`];
|
||||
return {
|
||||
log: `Traité de Clairvoyance : VUE +${p}, TOUR accéléré (+${p} PA) pendant 4 tours.`,
|
||||
effects: [makeEffect("Traité de Clairvoyance", "🔮", 4, {
|
||||
vue: p, dlaBonusPA: p,
|
||||
}, lines)],
|
||||
};
|
||||
},
|
||||
},
|
||||
ideesConfuses: {
|
||||
name: "Idées Confuses", emoji: "💫", color: "#b078b0",
|
||||
duration: 3,
|
||||
build(_t, p, rollDice) {
|
||||
const attR = sharedMod(rollDice(p, 3).total, p, 3, true);
|
||||
const lines = [`ATT ${attR.line}`, `DLA −${p} PA/tour`];
|
||||
return {
|
||||
log: `Idées Confuses : ATT ${attR.line}, TOUR ralenti (−${p} PA) pendant 3 tours.`,
|
||||
effects: [makeEffect("Idées Confuses", "💫", 3, {
|
||||
attFlat: attR.value, dlaBonusPA: -p,
|
||||
}, lines)],
|
||||
};
|
||||
},
|
||||
},
|
||||
runeExplosive: {
|
||||
name: "Rune Explosive", emoji: "💥", color: "#e06030",
|
||||
duration: 0, zone: true,
|
||||
build(troll, p, rollDice) {
|
||||
const dmg = rollDice(2 * p, 3).total;
|
||||
troll.pv = Math.max(1, troll.pv - dmg);
|
||||
return {
|
||||
log: `Rune Explosive : −${dmg} PV (${2 * p}D3), l'explosion ravage la zone !`,
|
||||
effects: [],
|
||||
zone: { type: "damage", total: dmg, label: `${2 * p}D3` },
|
||||
};
|
||||
},
|
||||
},
|
||||
planGenial: {
|
||||
name: "Plan Génial", emoji: "💡", color: "#e0c048",
|
||||
duration: 3,
|
||||
build(_t, p, rollDice) {
|
||||
const pa = Math.ceil(p / 2);
|
||||
const attR = sharedMod(rollDice(p, 3).total, p, 3, false);
|
||||
const lines = [`ATT ${attR.line}`, `DEG +${p}`, `DLA +${pa} PA/tour`];
|
||||
return {
|
||||
log: `Plan Génial : ${lines.join(", ")} pendant 3 tours.`,
|
||||
effects: [makeEffect("Plan Génial", "💡", 3, {
|
||||
attFlat: attR.value, degFlat: p, dlaBonusPA: pa,
|
||||
}, lines)],
|
||||
};
|
||||
},
|
||||
},
|
||||
yeuKiPic: {
|
||||
name: "Yeu'Ki'Pic", emoji: "🌶️", color: "#c04040",
|
||||
duration: 2, zone: true,
|
||||
build(_t, p) {
|
||||
const malus = 3 * p;
|
||||
return {
|
||||
log: `Yeu'Ki'Pic : un nuage piquant ! VUE −${malus} pour toute la zone pendant 2 tours.`,
|
||||
effects: [makeEffect("Yeu'Ki'Pic", "🌶️", 2, { vue: -malus })],
|
||||
zone: { type: "vue", malus, turns: 2 },
|
||||
};
|
||||
},
|
||||
},
|
||||
runeFoins: {
|
||||
name: "Rune des Foins", emoji: "🌾", color: "#a8a048",
|
||||
duration: 3,
|
||||
build(troll, p, rollDice) {
|
||||
// « PV : -2/4 D3 » (Mountypedia) : interprété comme 2D3 ou 4D3 au hasard
|
||||
const dice = Math.random() < 0.5 ? 2 : 4;
|
||||
const dmg = rollDice(dice, 3).total;
|
||||
troll.pv = Math.max(1, troll.pv - dmg);
|
||||
const lines = [`DEG −${p}`, "VUE −1"];
|
||||
return {
|
||||
log: `Rune des Foins : atchoum ! −${dmg} PV (${dice}D3), ${lines.join(", ")} pendant 3 tours.`,
|
||||
effects: [makeEffect("Rune des Foins", "🌾", 3, { degFlat: -p, vue: -1 }, lines)],
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function formatScrollItem(scrollId, power) {
|
||||
const def = SCROLL_DEFS[scrollId];
|
||||
if (!def) return null;
|
||||
return {
|
||||
kind: "scroll",
|
||||
scrollId,
|
||||
power,
|
||||
name: `${def.name} (niv. ${power})`,
|
||||
emoji: def.emoji,
|
||||
color: def.color,
|
||||
};
|
||||
}
|
||||
|
||||
function makeRandomScroll() {
|
||||
const id = SCROLL_IDS[Math.floor(Math.random() * SCROLL_IDS.length)];
|
||||
return formatScrollItem(id, scrollRand(1, 5));
|
||||
}
|
||||
|
||||
function makeScrollItem(scrollId, power) {
|
||||
if (!SCROLL_DEFS[scrollId]) return makeRandomScroll();
|
||||
return formatScrollItem(scrollId, power != null ? power : scrollRand(1, 5));
|
||||
}
|
||||
|
||||
/* Lit un parchemin. Retourne null si inconnu, sinon { zone } — game.js applique
|
||||
* l'éventuel effet de zone aux monstres alentour. */
|
||||
function readScroll(troll, item, rollDice, logFn) {
|
||||
const def = SCROLL_DEFS[item.scrollId];
|
||||
if (!def) {
|
||||
logFn("Ce parchemin est illisible.", "info");
|
||||
return null;
|
||||
}
|
||||
const p = item.power != null ? item.power : scrollRand(1, 5);
|
||||
const result = def.build(troll, p, rollDice);
|
||||
if (result.effects.length) {
|
||||
troll.potionEffects = troll.potionEffects || [];
|
||||
troll.potionEffects.push(...result.effects);
|
||||
}
|
||||
logFn(`📜 ${result.log}`, "good");
|
||||
return { zone: result.zone || null };
|
||||
}
|
||||
|
||||
if (typeof module !== "undefined" && module.exports) {
|
||||
module.exports = {
|
||||
SCROLL_IDS, SCROLL_DEFS, SCROLL_ZONE_RADIUS,
|
||||
formatScrollItem, makeRandomScroll, makeScrollItem, readScroll,
|
||||
};
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
/* Page « Trésors du Hall » : encyclopédie des potions et des parchemins,
|
||||
* formules officielles de Mountypedia (X = niveau du trésor).
|
||||
* Rappel : un trésor ne fait qu'un seul jet de dés — « X D3 » donne le même
|
||||
* total partout où il s'applique. Partage les globales de potions.js/scrolls.js. */
|
||||
|
||||
"use strict";
|
||||
|
||||
/* [id, niveau X, durée affichée, effets] — ordre d'affichage de la page. */
|
||||
const TREASURE_POTIONS = [
|
||||
["biskot", "—", "2 tours", ["REG +2", "PV +2D3 (immédiat)"]],
|
||||
["bonneBouffe", "3 à 7", "5 tours", ["DEG +X", "REG +X"]],
|
||||
["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)"]],
|
||||
["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"]],
|
||||
["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"]],
|
||||
["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"]],
|
||||
["rhume", "1 à 2", "3 tours", ["ATT −X D3", "ESQ −X D3", "DEG −X", "REG −X"]],
|
||||
["sangToh", "1 à 5", "4 tours", ["ATT +X D3", "ESQ +X D3", "VUE +X"]],
|
||||
["sinneKhole", "11 à 100", "2 tours", ["RM +X %", "MM −Y %"]],
|
||||
["toxine", "1 à 5", "immédiat", ["PV −2·X D3"]],
|
||||
["voiputrin", "1 à 5", "2 tours", ["VUE −10·X"]],
|
||||
["zetCrak", "1 à 5", "3 tours", ["ATT −X D3", "ESQ −X D3", "VUE −X", "PV +X D3 (immédiat)"]],
|
||||
];
|
||||
|
||||
const TREASURE_SCROLLS = [
|
||||
["runeCyclopes", "1 à 5", "4 tours", ["ATT +X D3", "DEG +X", "VUE −X"]],
|
||||
["clairvoyance", "1 à 5", "4 tours", ["VUE +X", "TOUR accéléré : +X PA par tour"]],
|
||||
["ideesConfuses","1 à 5", "3 tours", ["ATT −X D3", "TOUR ralenti : −X PA par tour"]],
|
||||
["runeExplosive","1 à 5", "immédiat", ["PV −2·X D3", "Effet de Zone (rayon 3 cases)"]],
|
||||
["planGenial", "1 à 5", "3 tours", ["ATT +X D3", "DEG +X", "TOUR accéléré : +X/2 PA par tour"]],
|
||||
["yeuKiPic", "1 à 5", "2 tours", ["VUE −3·X", "Effet de Zone (rayon 3 cases)"]],
|
||||
["runeFoins", "1 à 5", "3 tours", ["DEG −X", "VUE −1", "PV −2D3 ou −4D3 (immédiat)"]],
|
||||
];
|
||||
|
||||
function treasureCard(def, x, duration, fx) {
|
||||
const fxHtml = fx.map(f => {
|
||||
const neg = f.includes("−");
|
||||
const pos = f.includes("+");
|
||||
const cls = neg && !pos ? "tz-fx tz-bad" : pos && !neg ? "tz-fx tz-good" : "tz-fx";
|
||||
return `<span class="${cls}">${f}</span>`;
|
||||
}).join("");
|
||||
return `
|
||||
<div class="tz-card" style="--tz-color:${def.color}">
|
||||
<div class="tz-head"><span class="tz-name">${def.emoji} ${def.name}</span></div>
|
||||
<div class="tz-meta">Niveau X : <b>${x}</b> · Durée : <b>${duration}</b></div>
|
||||
<div class="tz-fx-list">${fxHtml}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function treasuresRender() {
|
||||
const body = document.getElementById("treasures-body");
|
||||
let html = `
|
||||
<p class="tz-intro">Les trésors du Monde Souterrain, fidèles aux formules de la
|
||||
<a href="https://mountypedia.mountyhall.com/Mountyhall/Potion" target="_blank" rel="noopener">Mountypedia</a>.
|
||||
Un trésor ne fait qu'<b>un seul jet de dés</b> : 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.
|
||||
1 tour = 1 DLA. Boire une potion ou lire un parchemin coûte <b>1 PA</b>.</p>`;
|
||||
|
||||
html += `<h2 class="tz-section">🧪 Potions (${TREASURE_POTIONS.length})</h2><div class="tz-grid">`;
|
||||
for (const [id, x, duration, fx] of TREASURE_POTIONS) {
|
||||
html += treasureCard(POTION_DEFS[id], x, duration, fx);
|
||||
}
|
||||
html += "</div>";
|
||||
|
||||
html += `<h2 class="tz-section">📜 Parchemins standards (${TREASURE_SCROLLS.length})</h2><div class="tz-grid">`;
|
||||
for (const [id, x, duration, fx] of TREASURE_SCROLLS) {
|
||||
html += treasureCard(SCROLL_DEFS[id], x, duration, fx);
|
||||
}
|
||||
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>`;
|
||||
|
||||
body.innerHTML = html;
|
||||
}
|
||||
|
||||
let treasuresReturnTo = "create";
|
||||
|
||||
function treasuresShow(from) {
|
||||
treasuresReturnTo = from;
|
||||
document.getElementById("screen-" + from).classList.add("hidden");
|
||||
document.getElementById("screen-treasures").classList.remove("hidden");
|
||||
treasuresRender();
|
||||
document.getElementById("screen-treasures").scrollTop = 0;
|
||||
}
|
||||
|
||||
if (typeof document !== "undefined") {
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
document.getElementById("btn-treasures").onclick = () => treasuresShow("create");
|
||||
document.getElementById("btn-treasures-game").onclick = () => treasuresShow("game");
|
||||
document.getElementById("treasures-back").onclick = () => {
|
||||
document.getElementById("screen-treasures").classList.add("hidden");
|
||||
document.getElementById("screen-" + treasuresReturnTo).classList.remove("hidden");
|
||||
};
|
||||
if (new URLSearchParams(location.search).get("screen") === "treasures") {
|
||||
treasuresShow("create");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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", "gold", "weapon", "armor"].includes(i.kind)) return "type d'objet invalide";
|
||||
if (!["potion", "scroll", "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 || [];
|
||||
|
||||
+43
-1
@@ -5,6 +5,7 @@ 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++) {
|
||||
@@ -211,6 +212,47 @@ assert.strictEqual(p.corruptionYZ(3).y, 0);
|
||||
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 });
|
||||
@@ -233,7 +275,7 @@ 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" }],
|
||||
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é");
|
||||
|
||||
Reference in New Issue
Block a user