Files
mountycrawl/db.js
T
percoandClaude Opus 4.8 00a4669928 v2.7.0 : templates de drop (table BDD, proba admin, application au drop)
- db.js : table templates + seed des 19 templates du CSV (mappes vers les
  mods + nouveau champ tour), templatesAll/setTemplate/resetCategory.
- server.js : /api/reference/templates.
- mp.js : config templateChance (0-100, bornee) ; applyGearTemplates applique
  jusqu'a 3 templates (6 si 2 mains), distincts, proba par emplacement,
  fusionne les mods (dont tour) et compose le nom ; applique au spawnItem.
  Solo vanilla.
- gear.js : GEAR_MOD_LABELS += tour (affichage 'TOUR +X mn').
- admin.html : champ Proba de template / emplacement.
- test/mp.js : test des templates (nb selon mains, distincts, proba 0).

Note : effet gameplay de tour (DLA) non cable, en attente de la regle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 00:17:53 +02:00

239 lines
10 KiB
JavaScript

/* Base de référence MountyCrawl — SQLite natif de Node (node:sqlite), toujours
* zéro dépendance npm. Elle contient les VALEURS DE RÉFÉRENCE du monde partagé :
* bestiaire, équipement, fourchettes de puissance des potions et parchemins.
* Chaque spawn/drop du serveur relit la base : une modification (page admin ou
* n'importe quel éditeur SQLite sur le fichier) est prise en compte à chaud.
* L'état vivant du monde (trolls, monstres actifs, objets au sol) reste dans
* world.json — seule la référence vit ici.
*
* Au premier démarrage la base est créée et remplie avec les valeurs vanilla
* (Mountypedia) ; ensuite les lignes existantes ne sont JAMAIS écrasées au
* démarrage (les retouches survivent aux mises à jour du jeu, et les nouveaux
* objets/monstres d'une future version sont ajoutés avec INSERT OR IGNORE). */
"use strict";
const { DatabaseSync } = require("node:sqlite");
const g = require("./js/game.js");
const p = require("./js/potions.js");
const sc = require("./js/scrolls.js");
const gearLib = require("./js/gear.js");
const MONSTER_KEYS = ["level", "att", "attMag", "esq", "deg", "degMag", "pv", "armor", "armorMag", "vue"];
const GEAR_KEYS = ["att", "attMag", "esq", "deg", "degMag", "reg", "arm", "armMag", "vue", "pv", "rmPct", "mmPct"];
/* Templates de drop façon MountyHall : des suffixes (« de l'Aigle », « des
* Mages »…) ajoutés aléatoirement à l'équipement qui tombe, selon une proba
* réglable côté admin. Leurs bonus s'ajoutent aux mods de l'objet ; `tour` est
* un modificateur de TOUR (DLA) propre aux templates. */
const TEMPLATE_KEYS = ["att", "attMag", "esq", "vue", "deg", "degMag", "arm", "armMag", "rmPct", "mmPct", "reg", "pv", "tour"];
const VANILLA_TEMPLATES = [
[1, "de l'Aigle", [0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
[2, "des Béhémoths", [0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 30]],
[3, "des Cyclopes", [0, 1, 0, -1, 0, 1, 0, 0, 0, 0, 0, 0, 0]],
[4, "des Enragés", [0, 1, -1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0]],
[5, "de Feu", [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0]],
[6, "des Mages", [0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 0, 0, 0]],
[7, "de l'Orage", [0, 0, 2, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0]],
[8, "de l'Ours", [0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 5, 30]],
[9, "du Pic", [0, 0, -1, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0]],
[10, "du Rat", [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
[11, "de Résistance", [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0]],
[12, "de la Salamandre", [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0]],
[13, "du Temps", [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -30]],
[14, "de la Terre", [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 5, 30]],
[15, "du Sable", [0, 0, 3, -1, 0, 0, 0, -1, 0, 0, 0, 0, 0]],
[16, "des Vampires", [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0]],
[17, "des Duellistes", [0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
[18, "des Champions", [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
[19, "des Anciens", [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
];
/* Fourchettes de puissance « niveau X » vanilla (Mountypedia). La Longue-Vue
* vanilla tire dans {1,2,3,5,8} : tant que sa fourchette en base reste celle
* d'origine, on garde le tirage officiel. */
const POTION_POWER_DEFAULTS = {
biskot: [0, 0], doverPowa: [11, 100], bonneBouffe: [3, 7], corruption: [3, 7],
fertilite: [3, 7], feu: [3, 7], longueVue: [1, 8], kouleMann: [1, 5],
djhinTonik: [1, 5], glacier: [3, 7], calvok: [2, 6], rhume: [1, 2],
grippe: [3, 4], pneumonie: [5, 5], cervelle: [2, 6], chronometre: [1, 5],
metomol: [1, 5], guerison: [1, 5], painture: [1, 5], pufPuff: [1, 3],
sangToh: [1, 5], sinneKhole: [11, 100], toxine: [1, 5], voiputrin: [1, 5],
zetCrak: [1, 5],
};
const SCROLL_POWER_DEFAULTS = Object.fromEntries(sc.SCROLL_IDS.map(id => [id, [1, 5]]));
let DB = null;
/* ---------- Valeurs vanilla (sources JS du jeu) ---------- */
function vanillaMonsters() {
return [...g.MONSTER_TYPES, g.BOSS].map(t => ({
name: t.name, emoji: t.emoji, boss: !!t.boss, static: !!t.static,
...Object.fromEntries(MONSTER_KEYS.map(k => [k, t[k] || 0])),
}));
}
function vanillaGear() {
return Object.entries(gearLib.GEAR).flatMap(([slot, list]) => list.map(def => ({
slot, name: def.name, emoji: def.emoji, tier: def.tier, twoHanded: !!def.twoHanded,
...Object.fromEntries(GEAR_KEYS.map(k => [k, def.mods[k] || 0])),
})));
}
function vanillaTemplates() {
return VANILLA_TEMPLATES.map(([id, name, vals]) => ({
id, name, ...Object.fromEntries(TEMPLATE_KEYS.map((k, i) => [k, vals[i]])),
}));
}
function vanillaTreasures(cat) {
const [ids, defs, ranges] = cat === "potions"
? [p.POTION_IDS, p.POTION_DEFS, POTION_POWER_DEFAULTS]
: [sc.SCROLL_IDS, sc.SCROLL_DEFS, SCROLL_POWER_DEFAULTS];
return ids.map(id => ({
id, name: defs[id].name, emoji: defs[id].emoji,
powerMin: ranges[id][0], powerMax: ranges[id][1],
}));
}
/* ---------- Ouverture et seed ---------- */
function init(file = ":memory:") {
if (DB) DB.close();
DB = new DatabaseSync(file);
DB.exec("PRAGMA journal_mode = WAL;");
DB.exec(`
CREATE TABLE IF NOT EXISTS monsters (
name TEXT PRIMARY KEY, emoji TEXT, boss INTEGER, isStatic INTEGER,
level INTEGER, att INTEGER, attMag INTEGER, esq INTEGER,
deg INTEGER, degMag INTEGER, pv INTEGER,
armor INTEGER, armorMag INTEGER, vue INTEGER
);
CREATE TABLE IF NOT EXISTS gear (
slot TEXT, name TEXT, emoji TEXT, tier INTEGER, twoHanded INTEGER,
att INTEGER, attMag INTEGER, esq INTEGER, deg INTEGER, degMag INTEGER,
reg INTEGER, arm INTEGER, armMag INTEGER, vue INTEGER, pv INTEGER,
rmPct INTEGER, mmPct INTEGER,
PRIMARY KEY (slot, name)
);
CREATE TABLE IF NOT EXISTS potions (
id TEXT PRIMARY KEY, name TEXT, emoji TEXT,
powerMin INTEGER, powerMax INTEGER
);
CREATE TABLE IF NOT EXISTS scrolls (
id TEXT PRIMARY KEY, name TEXT, emoji TEXT,
powerMin INTEGER, powerMax INTEGER
);
CREATE TABLE IF NOT EXISTS templates (
id INTEGER PRIMARY KEY, name TEXT, ${TEMPLATE_KEYS.map(k => `${k} INTEGER`).join(", ")}
);
`);
const insM = DB.prepare(`INSERT OR IGNORE INTO monsters
(name, emoji, boss, isStatic, ${MONSTER_KEYS.join(", ")})
VALUES (?, ?, ?, ?, ${MONSTER_KEYS.map(() => "?").join(", ")})`);
for (const m of vanillaMonsters()) {
insM.run(m.name, m.emoji, m.boss ? 1 : 0, m.static ? 1 : 0, ...MONSTER_KEYS.map(k => m[k]));
}
const insG = DB.prepare(`INSERT OR IGNORE INTO gear
(slot, name, emoji, tier, twoHanded, ${GEAR_KEYS.join(", ")})
VALUES (?, ?, ?, ?, ?, ${GEAR_KEYS.map(() => "?").join(", ")})`);
for (const it of vanillaGear()) {
insG.run(it.slot, it.name, it.emoji, it.tier, it.twoHanded ? 1 : 0, ...GEAR_KEYS.map(k => it[k]));
}
for (const cat of ["potions", "scrolls"]) {
const ins = DB.prepare(`INSERT OR IGNORE INTO ${cat} (id, name, emoji, powerMin, powerMax) VALUES (?, ?, ?, ?, ?)`);
for (const t of vanillaTreasures(cat)) ins.run(t.id, t.name, t.emoji, t.powerMin, t.powerMax);
}
const insT = DB.prepare(`INSERT OR IGNORE INTO templates (id, name, ${TEMPLATE_KEYS.join(", ")})
VALUES (?, ?, ${TEMPLATE_KEYS.map(() => "?").join(", ")})`);
for (const t of vanillaTemplates()) insT.run(t.id, t.name, ...TEMPLATE_KEYS.map(k => t[k]));
// Correctif des bases déjà créées : PufPuff avait été seedé à tort en [0,2]
// (le niveau 0 d'une potion n'existe pas, minimum 1) → on le ramène à [1,3].
// Le garde `powerMin = 0` évite d'écraser une plage éditée volontairement.
DB.prepare("UPDATE potions SET powerMin = 1, powerMax = 3 WHERE id = 'pufPuff' AND powerMin = 0").run();
return DB;
}
function db() {
if (!DB) init(process.env.DB_FILE || ":memory:");
return DB;
}
/* ---------- Lecture (chaque spawn/drop relit la base : modifs à chaud) ---------- */
function monsters() {
return db().prepare("SELECT * FROM monsters").all().map(r => ({
...r, boss: !!r.boss, static: !!r.isStatic, isStatic: undefined,
}));
}
function gearAll() {
return db().prepare("SELECT * FROM gear").all().map(r => ({ ...r, twoHanded: !!r.twoHanded }));
}
function gearRow(slot, name) {
const r = db().prepare("SELECT * FROM gear WHERE slot = ? AND name = ?").get(slot, name);
return r ? { ...r, twoHanded: !!r.twoHanded } : null;
}
function treasureRange(cat, id) {
const r = db().prepare(`SELECT powerMin, powerMax FROM ${cat === "scrolls" ? "scrolls" : "potions"} WHERE id = ?`).get(id);
return r ? [r.powerMin, r.powerMax] : null;
}
function treasuresAll(cat) {
return db().prepare(`SELECT * FROM ${cat === "scrolls" ? "scrolls" : "potions"}`).all();
}
function templatesAll() {
return db().prepare("SELECT * FROM templates ORDER BY id").all();
}
/* ---------- Écriture (page admin) ---------- */
function setMonster(name, vals) {
const keys = MONSTER_KEYS.filter(k => vals[k] != null);
if (!keys.length) return;
db().prepare(`UPDATE monsters SET ${keys.map(k => `${k} = ?`).join(", ")} WHERE name = ?`)
.run(...keys.map(k => vals[k]), name);
}
function setGear(slot, name, vals) {
const keys = GEAR_KEYS.filter(k => vals[k] != null);
if (!keys.length) return;
db().prepare(`UPDATE gear SET ${keys.map(k => `${k} = ?`).join(", ")} WHERE slot = ? AND name = ?`)
.run(...keys.map(k => vals[k]), slot, name);
}
function setTreasureRange(cat, id, [min, max]) {
db().prepare(`UPDATE ${cat === "scrolls" ? "scrolls" : "potions"} SET powerMin = ?, powerMax = ? WHERE id = ?`)
.run(min, max, id);
}
function setTemplate(id, vals) {
const keys = TEMPLATE_KEYS.filter(k => vals[k] != null);
if (!keys.length) return;
db().prepare(`UPDATE templates SET ${keys.map(k => `${k} = ?`).join(", ")} WHERE id = ?`)
.run(...keys.map(k => vals[k]), id);
}
/* Remet une catégorie entière aux valeurs vanilla. */
function resetCategory(cat) {
if (cat === "monsters") for (const m of vanillaMonsters()) setMonster(m.name, m);
if (cat === "gear") for (const it of vanillaGear()) setGear(it.slot, it.name, it);
if (cat === "templates") for (const t of vanillaTemplates()) setTemplate(t.id, t);
if (cat === "potions" || cat === "scrolls") {
for (const t of vanillaTreasures(cat)) setTreasureRange(cat, t.id, [t.powerMin, t.powerMax]);
}
}
module.exports = {
init, db,
MONSTER_KEYS, GEAR_KEYS, TEMPLATE_KEYS, POTION_POWER_DEFAULTS, SCROLL_POWER_DEFAULTS,
vanillaMonsters, vanillaGear, vanillaTreasures, vanillaTemplates,
monsters, gearAll, gearRow, treasureRange, treasuresAll, templatesAll,
setMonster, setGear, setTreasureRange, setTemplate, resetCategory,
};