Files
mountycrawl/db.js
T
percoandClaude Opus 4.8 836837fa55 v2.10.0 : bestiaire (etape 1, donnees) - table BDD + 150 monstres + table d'ages
Bestiaire fourni (945 entrees monstre x age) parse et replie selon le
modele demande par Perco : 1 ligne par monstre (js/bestiary.js, 150
monstres, stats de base = age le plus jeune en min/max + famille +
capacites + drapeaux vol/distance/magie/voit-cache + vitesse + nb attaques
+ blason) + table d'ages (8 multiplicateurs derives des donnees). db.js :
tables bestiary + monster_ages, seed, bestiaryAll/monsterAges,
setBestiary/setMonsterAge, resetCategory. server.js : /api/reference/
bestiary + /ages. Non destructif (le jeu tourne encore sur l'ancien
systeme). Etapes suivantes : encyclopedie, editeur admin, integration spawn.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 16:12:56 +02:00

282 lines
12 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 bestiaryLib = require("./js/bestiary.js");
// Bestiaire complet (demande Perco) : 1 ligne par monstre (stats de base = âge
// le plus jeune, en plages min/max) + une table d'âges (multiplicateurs).
const BESTIARY_STATS = ["level", "pv", "att", "esq", "deg", "reg", "armPhys", "armMag", "vue", "mm", "rm"];
const BESTIARY_RANGE_KEYS = BESTIARY_STATS.flatMap(k => [k + "Min", k + "Max"]);
const BESTIARY_INT_KEYS = [...BESTIARY_RANGE_KEYS, "minAge", "maxAge", "fly", "ranged", "magic", "seesHidden", "nbAtt"];
const BESTIARY_TXT_KEYS = ["family", "speed", "capacities", "blason"];
const BESTIARY_KEYS = [...BESTIARY_INT_KEYS, ...BESTIARY_TXT_KEYS]; // hors `name` (clé)
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(", ")}
);
CREATE TABLE IF NOT EXISTS bestiary (
name TEXT PRIMARY KEY,
${BESTIARY_INT_KEYS.map(k => `${k} INTEGER`).join(", ")},
${BESTIARY_TXT_KEYS.map(k => `${k} TEXT`).join(", ")}
);
CREATE TABLE IF NOT EXISTS monster_ages (
age INTEGER PRIMARY KEY, mult REAL
);
`);
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]));
const insB = DB.prepare(`INSERT OR IGNORE INTO bestiary (name, ${BESTIARY_KEYS.join(", ")})
VALUES (?, ${BESTIARY_KEYS.map(() => "?").join(", ")})`);
for (const b of bestiaryLib.BESTIARY) insB.run(b.name, ...BESTIARY_KEYS.map(k => b[k]));
const insA = DB.prepare("INSERT OR IGNORE INTO monster_ages (age, mult) VALUES (?, ?)");
bestiaryLib.AGE_MULT.forEach((mult, age) => insA.run(age, mult));
// 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();
}
function bestiaryAll() {
return db().prepare("SELECT * FROM bestiary ORDER BY family, name").all();
}
function monsterAges() {
return db().prepare("SELECT * FROM monster_ages ORDER BY age").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);
}
function setBestiary(name, vals) {
const keys = BESTIARY_KEYS.filter(k => vals[k] != null);
if (!keys.length) return;
db().prepare(`UPDATE bestiary SET ${keys.map(k => `${k} = ?`).join(", ")} WHERE name = ?`)
.run(...keys.map(k => vals[k]), name);
}
function setMonsterAge(age, mult) {
db().prepare("UPDATE monster_ages SET mult = ? WHERE age = ?").run(mult, age);
}
/* 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 === "bestiary") for (const b of bestiaryLib.BESTIARY) setBestiary(b.name, b);
if (cat === "ages") bestiaryLib.AGE_MULT.forEach((mult, age) => setMonsterAge(age, mult));
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, BESTIARY_KEYS, POTION_POWER_DEFAULTS, SCROLL_POWER_DEFAULTS,
vanillaMonsters, vanillaGear, vanillaTreasures, vanillaTemplates,
monsters, gearAll, gearRow, treasureRange, treasuresAll, templatesAll, bestiaryAll, monsterAges,
setMonster, setGear, setTreasureRange, setTemplate, setBestiary, setMonsterAge, resetCategory,
};