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:
perco
2026-06-11 23:35:41 +02:00
co-authored by Claude Fable 5
parent 9b3e2c8455
commit 6039497e72
10 changed files with 458 additions and 18 deletions
+57 -11
View File
@@ -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);