bestiary.js regenere avec champ gender (m/f, liste curatee de ~31 feminins) + export AGE_NAMES_F. db.js : gender en colonne (BESTIARY_TXT_KEYS) + migration ALTER TABLE ... ADD COLUMN gender AVANT le seed, puis UPDATE du genre des lignes existantes. game.js buildBestiaryMonster(.., ageNamesF, ..) : nom = 'Nom [Age]' avec age accorde au genre. mp.js + makeMonster passent AGE_NAMES_F. treasures.js : age affiche au feminin. admin : champ Genre. Tests : format + feminin (Plante [Doyenne]). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
288 lines
13 KiB
JavaScript
288 lines
13 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", "gender"];
|
|
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
|
|
);
|
|
`);
|
|
// Migration : la colonne `gender` du bestiaire a été ajoutée après coup → on
|
|
// l'ajoute aux bases déjà créées AVANT tout INSERT/UPDATE qui la référence.
|
|
try { DB.exec("ALTER TABLE bestiary ADD COLUMN gender TEXT"); } catch { /* déjà là */ }
|
|
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));
|
|
// Peuple le genre des lignes déjà présentes (sans genre) depuis la référence.
|
|
const updGender = DB.prepare("UPDATE bestiary SET gender = ? WHERE name = ? AND (gender IS NULL OR gender = '')");
|
|
for (const b of bestiaryLib.BESTIARY) updGender.run(b.gender, b.name);
|
|
// 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,
|
|
};
|