/* 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 mag. +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 mag. +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 mag. −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", "1 à 3", "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)"]], ]; /* Carte d'un template de drop : ses bonus, lus directement sur la ligne BDD * (mêmes clés que les mods d'équipement, donc on réutilise GEAR_MOD_LABELS). */ function templateCard(t) { const fx = []; for (const [key, label, unit] of GEAR_MOD_LABELS) { const v = t[key]; if (!v) continue; fx.push(`${label} ${v > 0 ? "+" : "−"}${Math.abs(v)}${unit}`); } if (!fx.length) fx.push('aucun bonus (titre honorifique)'); return `
✨ ${t.name}
${fx.join("")}
`; } /* Carte d'un monstre du bestiaire : blason + stats (plages) + âges + capacités. */ function monsterCard(m) { const rg = (a, b, s = "") => (a === b ? `${a}${s}` : `${a}-${b}${s}`); const ages = BESTIARY_AGE_NAMES[m.family] || []; const ageTxt = m.minAge === m.maxAge ? (ages[m.minAge] || "—") : `${ages[m.minAge] || m.minAge} → ${ages[m.maxAge] || m.maxAge}`; const fx = [ `PV ${rg(m.pvMin, m.pvMax)}`, `ATT ${rg(m.attMin, m.attMax, "D6")}`, `ESQ ${rg(m.esqMin, m.esqMax, "D6")}`, `DEG ${rg(m.degMin, m.degMax, "D3")}`, `REG ${rg(m.regMin, m.regMax, "D3")}`, `Arm.phy ${rg(m.armPhysMin, m.armPhysMax)}`, ]; if (m.armMagMax) fx.push(`Arm.mag ${rg(m.armMagMin, m.armMagMax)}`); fx.push(`Vue ${rg(m.vueMin, m.vueMax)}`, `MM ${rg(m.mmMin, m.mmMax)}`, `RM ${rg(m.rmMin, m.rmMax)}`); const tags = []; if (m.fly) tags.push("Vole"); if (m.ranged) tags.push("À distance"); if (m.magic) tags.push("Att. magique"); if (m.seesHidden) tags.push("Voit le caché"); if (m.nbAtt > 1) tags.push(m.nbAtt + " att./tour"); if (m.speed && m.speed !== "Normale") tags.push("Vitesse " + m.speed); for (const t of tags) fx.push(`${t}`); if (m.capacities) fx.push(`${m.capacities}`); const img = m.blason ? `` : ""; return `
${img}${m.name}
Niveau ${rg(m.levelMin, m.levelMax)} · Âge : ${ageTxt}
${fx.join("")}
`; } 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 `${f}`; }).join(""); return `
${def.emoji} ${def.name}
Niveau X : ${x} · Durée : ${duration}
${fxHtml}
`; } function treasuresRender() { const body = document.getElementById("treasures-body"); let html = `

Les trésors du Monde Souterrain, fidèles aux formules de la Mountypedia. Un trésor ne fait qu'un seul jet de dés : 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. Tous les bonus/malus des potions et parchemins sont magiques — leur armure est de l'armure magique, seule à réduire les dégâts magiques. 1 tour = 1 DLA. Boire une potion ou lire un parchemin coûte 1 PA.

`; const templates = TEMPLATES_DB || []; html += ``; html += `

🧪 Potions (${TREASURE_POTIONS.length})

`; for (const [id, x, duration, fx] of TREASURE_POTIONS) { html += treasureCard(POTION_DEFS[id], powerLabel(x, POTIONS_DB && POTIONS_DB[id]), duration, fx); } html += "
"; html += `

📜 Parchemins standards (${TREASURE_SCROLLS.length})

`; for (const [id, x, duration, fx] of TREASURE_SCROLLS) { html += treasureCard(SCROLL_DEFS[id], powerLabel(x, SCROLLS_DB && SCROLLS_DB[id]), duration, fx); } html += "
"; // Source des équipements : la base de référence (BDD SQLite, éditable) si elle // a pu être chargée, sinon les valeurs statiques de gear.js (ex. hors-ligne). const gearSource = GEAR_DB || GEAR; const gearCount = Object.values(gearSource).reduce((n, list) => n + list.length, 0); html += `

⚔️ Équipement (${gearCount})

Six emplacements : ${Object.values(GEAR_SLOTS).map(s => s.label).join(", ")}. Les bonus d'équipement sont fixes (jamais en dés) et s'ajoutent toujours aux jets. La plupart sont physiques ; certains objets portent aussi des bonus magiques (ATT mag., DEG mag., Armure mag.), et les deux saveurs se cumulent. Les dégâts physiques sont encaissés par l'armure totale, les dégâts magiques par la seule armure magique. Une arme à 2 mains est incompatible avec un bouclier. S'équiper coûte 2 PA. Valeurs lues dans la base de référence (modifiables côté admin) ; les objets puissants ne se trouvent qu'en profondeur.

`; for (const [slot, info] of Object.entries(GEAR_SLOTS)) { html += `

${info.emoji} ${info.label}s

`; for (const def of (gearSource[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(`${label} ${v > 0 ? "+" : "−"}${Math.abs(v)}${unit}`); } if (def.twoHanded) fx.push('2 mains'); if (def.note) fx.push(`${def.note}`); html += `
${def.emoji} ${def.name}
Profondeur : −${Math.max(1, def.tier - 1)} et au-delà
${fx.join("")}
`; } html += "
"; } html += `

✨ Templates de drop (${templates.length})

Suffixes à la MountyHall ajoutés aux objets qui tombent dans le monde partagé : un objet reçoit jusqu'à 3 templates (6 pour une arme à 2 mains), chacun tiré au hasard selon une probabilité réglable sur la page admin. Leurs bonus s'ajoutent à ceux de l'objet (le mod TOUR est en minutes de DLA), et leur nom s'accole à celui de l'objet (« Épée Courte de l'Aigle des Mages »). Valeurs lues dans la base de référence (modifiables côté admin).

`; if (templates.length) { html += `
${templates.map(templateCard).join("")}
`; } else { html += `

Liste indisponible (base non joignable).

`; } const bestiary = BESTIARY_DB || []; html += `

🐲 Bestiaire (${bestiary.length})

Les créatures du Hall. Chaque monstre est défini par ses stats de base (l'âge le plus jeune, en plages) ; en vieillissant il gagne en puissance (multiplicateurs d'âge réglables côté admin). Les stats affichées sont des plages tirées au hasard à chaque apparition.

`; if (bestiary.length) { const fams = [...new Set(bestiary.map(m => m.family))].sort(); for (const fam of fams) { const list = bestiary.filter(m => m.family === fam); html += `

${FAMILY_EMOJI[fam] || "🐾"} ${fam} (${list.length})

${list.map(monsterCard).join("")}
`; } } else { html += `

Bestiaire indisponible (base non joignable).

`; } html += `

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.

`; body.innerHTML = html; } let treasuresReturnTo = "create"; // Données de la base de référence (BDD), rechargées à chaque ouverture pour // refléter les éditions (admin OU sqlite-web) ; null tant qu'on n'a pas (ou pas // pu) charger → repli sur les valeurs statiques du code. let GEAR_DB = null, POTIONS_DB = null, SCROLLS_DB = null, TEMPLATES_DB = null, BESTIARY_DB = null; // Noms d'âge par famille (table de Perco) — pour afficher la fourchette d'âges. const BESTIARY_AGE_NAMES = { Insecte: ["Larve", "Immature", "Juvénile", "Imago", "Développé", "Mûr", "Accompli", "Achevé"], Animal: ["Bébé", "Enfançon", "Jeune", "Adulte", "Mature", "Chef de harde", "Ancien", "Ancêtre"], "Démon": ["Initial", "Novice", "Mineur", "Favori", "Majeur", "Supérieur", "Suprême", "Ultime"], Humanoide: ["Nouveau", "Jeune", "Adulte", "Vétéran", "Briscard", "Doyen", "Légendaire", "Mythique"], Monstre: ["Nouveau", "Jeune", "Adulte", "Vétéran", "Briscard", "Doyen", "Légendaire", "Mythique"], "Mort-Vivant": ["Naissant", "Récent", "Ancien", "Vénérable", "Séculaire", "Antique", "Ancestral", "Antédiluvien"], }; const FAMILY_EMOJI = { Insecte: "🐛", Animal: "🐾", "Démon": "👿", Humanoide: "🧝", Monstre: "👹", "Mort-Vivant": "💀" }; async function fetchRef(cat) { try { const res = await fetch("api/reference/" + cat); if (!res.ok) return null; const rows = await res.json(); return Array.isArray(rows) && rows.length ? rows : null; } catch { return null; // base injoignable (ex. ouverture en file://) } } async function loadReference() { const [gear, potions, scrolls, templates, bestiary] = await Promise.all([ fetchRef("gear"), fetchRef("potions"), fetchRef("scrolls"), fetchRef("templates"), fetchRef("bestiary"), ]); BESTIARY_DB = bestiary; GEAR_DB = gear ? gearBySlotFromRows(gear) : null; POTIONS_DB = potions ? indexPowerById(potions) : null; SCROLLS_DB = scrolls ? indexPowerById(scrolls) : null; TEMPLATES_DB = templates; } function gearBySlotFromRows(rows) { const bySlot = {}; for (const r of rows) { const mods = {}; for (const [key] of GEAR_MOD_LABELS) mods[key] = r[key] || 0; const staticDef = (GEAR[r.slot] || []).find(d => d.name === r.name); (bySlot[r.slot] = bySlot[r.slot] || []).push({ emoji: r.emoji, name: r.name, tier: r.tier, twoHanded: !!r.twoHanded, note: staticDef && staticDef.note, mods, }); } return bySlot; } function indexPowerById(rows) { const map = {}; for (const r of rows) map[r.id] = { min: r.powerMin, max: r.powerMax }; return map; } // Libellé « Niveau X » : on remplace par la plage BDD (min/max) UNIQUEMENT pour // les plages numériques simples « A à B » ; les libellés spéciaux (« — », // « 5 (fixe) », « 1, 2, 3, 5 ou 8 »…) sont conservés tels quels car la base ne // stocke qu'un min/max qui ne saurait les représenter. function powerLabel(staticLabel, range) { if (!range || !/^\d+ à \d+$/.test(staticLabel)) return staticLabel; return range.min === range.max ? `${range.min} (fixe)` : `${range.min} à ${range.max}`; } async function treasuresShow(from) { treasuresReturnTo = from; document.getElementById("screen-" + from).classList.add("hidden"); document.getElementById("screen-treasures").classList.remove("hidden"); window.scrollTo(0, 0); await loadReference(); treasuresRender(); } 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"); }; // Bouton « remonter en haut » : visible dès qu'on a un peu défilé. const topBtn = document.getElementById("tz-top"); if (topBtn) { topBtn.onclick = () => window.scrollTo({ top: 0, behavior: "smooth" }); window.addEventListener("scroll", () => { const onTz = !document.getElementById("screen-treasures").classList.contains("hidden"); topBtn.classList.toggle("hidden", !onTz || window.scrollY < 400); }); } if (new URLSearchParams(location.search).get("screen") === "treasures") { treasuresShow("create"); } }); }