2.3.0 : valeurs de référence en base SQLite native (node:sqlite) — bestiaire, équipement, potions, parchemins éditables à chaud
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
FROM node:22-alpine
|
FROM node:24-alpine
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY index.html admin.html server.js mp.js ./
|
COPY index.html admin.html server.js mp.js db.js ./
|
||||||
COPY css ./css
|
COPY css ./css
|
||||||
COPY js ./js
|
COPY js ./js
|
||||||
COPY img ./img
|
COPY img ./img
|
||||||
|
|||||||
@@ -158,8 +158,15 @@ nouveaux spawns/drops utilisent les valeurs retouchées ; le solo reste vanilla)
|
|||||||
- **Tuning des potions & parchemins** : fourchette de puissance « niveau X »
|
- **Tuning des potions & parchemins** : fourchette de puissance « niveau X »
|
||||||
(min/max) tirée à chaque drop, par trésor.
|
(min/max) tirée à chaque drop, par trésor.
|
||||||
|
|
||||||
Les cases modifiées sont surlignées ; seuls les écarts au vanilla sont stockés
|
Les cases modifiées sont surlignées et chaque catégorie se remet d'origine d'un
|
||||||
(`world.tuning`), et chaque catégorie se remet d'origine d'un clic.
|
clic. Depuis la 2.3.0, ces valeurs vivent dans une **base SQLite**
|
||||||
|
(`DB_FILE`, défaut `/data/mountycrawl.db`, via le module natif `node:sqlite` —
|
||||||
|
toujours zéro dépendance npm) : tables `monsters`, `gear`, `potions`, `scrolls`,
|
||||||
|
seedées vanilla au premier démarrage puis jamais écrasées. On peut donc aussi
|
||||||
|
les éditer directement avec n'importe quel outil SQLite (DB Browser, DBeaver,
|
||||||
|
sqlite3) — chaque spawn/drop relit la base, c'est pris en compte à chaud.
|
||||||
|
L'état vivant du monde (trolls, monstres actifs, objets au sol) reste dans
|
||||||
|
`world.json` ; l'ancien `world.tuning` est migré automatiquement dans la base.
|
||||||
|
|
||||||
### API multijoueur
|
### API multijoueur
|
||||||
|
|
||||||
@@ -227,6 +234,16 @@ non affilié au jeu original de Mountyhall SARL.
|
|||||||
|
|
||||||
## Versions
|
## Versions
|
||||||
|
|
||||||
|
- **2.3.0** (2026-06-12) — Les valeurs de référence du monde partagé passent dans
|
||||||
|
une **base SQLite** (`db.js`, module natif `node:sqlite` — toujours zéro
|
||||||
|
dépendance npm) : tables `monsters` (bestiaire), `gear` (55 objets),
|
||||||
|
`potions`/`scrolls` (fourchettes de puissance), dans `DB_FILE` (défaut
|
||||||
|
`/data/mountycrawl.db`). Seed vanilla au premier démarrage, jamais écrasé
|
||||||
|
ensuite ; chaque spawn/drop **relit la base**, donc une modification — page
|
||||||
|
admin ou édition directe au DB Browser/DBeaver/sqlite3 — s'applique à chaud.
|
||||||
|
L'ancien `world.tuning` de world.json est migré automatiquement puis retiré.
|
||||||
|
L'état vivant (trolls, monstres actifs, objets au sol) reste dans `world.json`.
|
||||||
|
Image Docker en node:24 (SQLite natif stable). API et page admin inchangées.
|
||||||
- **2.2.0** (2026-06-12) — Le couple **physique/magique partout** : comme l'armure,
|
- **2.2.0** (2026-06-12) — Le couple **physique/magique partout** : comme l'armure,
|
||||||
l'ATT et les DEG existent désormais en deux saveurs pour les **monstres**
|
l'ATT et les DEG existent désormais en deux saveurs pour les **monstres**
|
||||||
(`attMag`, `degMag` — un monstre qui en a alterne au hasard entre attaque
|
(`attMag`, `degMag` — un monstre qui en a alterne au hasard entre attaque
|
||||||
|
|||||||
+5
-2
@@ -89,8 +89,11 @@
|
|||||||
|
|
||||||
<div class="admin-card" style="margin-top:16px">
|
<div class="admin-card" style="margin-top:16px">
|
||||||
<h2>⚔️ Tuning de l'équipement</h2>
|
<h2>⚔️ Tuning de l'équipement</h2>
|
||||||
<p><small>Bonus/malus de chaque objet (physiques, fixes sur les jets ; RM/MM en %).
|
<p><small>Bonus/malus de chaque objet (fixes sur les jets ; RM/MM en %).
|
||||||
S'applique aux prochains drops du monde partagé.</small></p>
|
S'applique aux prochains drops du monde partagé. Ces valeurs vivent dans la
|
||||||
|
base SQLite <code>data/mountycrawl.db</code> (tables <code>gear</code>,
|
||||||
|
<code>monsters</code>, <code>potions</code>, <code>scrolls</code>) — tu peux
|
||||||
|
aussi les éditer avec n'importe quel outil SQLite, c'est relu à chaque drop.</small></p>
|
||||||
<div id="tune-gear"></div>
|
<div id="tune-gear"></div>
|
||||||
<button id="tune-gear-save" class="big-btn">💾 Appliquer l'équipement</button>
|
<button id="tune-gear-save" class="big-btn">💾 Appliquer l'équipement</button>
|
||||||
<button id="tune-gear-reset" class="menu-btn">↩️ Tout remettre d'origine</button>
|
<button id="tune-gear-reset" class="menu-btn">↩️ Tout remettre d'origine</button>
|
||||||
|
|||||||
@@ -0,0 +1,183 @@
|
|||||||
|
/* 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"];
|
||||||
|
|
||||||
|
/* 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: [0, 2],
|
||||||
|
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 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
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- É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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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 === "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, POTION_POWER_DEFAULTS, SCROLL_POWER_DEFAULTS,
|
||||||
|
vanillaMonsters, vanillaGear, vanillaTreasures,
|
||||||
|
monsters, gearAll, gearRow, treasureRange, treasuresAll,
|
||||||
|
setMonster, setGear, setTreasureRange, resetCategory,
|
||||||
|
};
|
||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const APP_VERSION = "2.2.0";
|
const APP_VERSION = "2.3.0";
|
||||||
|
|
||||||
/* Alpha : maîtrise initiale haute pour les tests. Remettre 15 % / 15 % à la v1.0 officielle. */
|
/* Alpha : maîtrise initiale haute pour les tests. Remettre 15 % / 15 % à la v1.0 officielle. */
|
||||||
const START_COMP_PCT = 90;
|
const START_COMP_PCT = 90;
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const g = require("./js/game.js");
|
|||||||
const p = require("./js/potions.js");
|
const p = require("./js/potions.js");
|
||||||
const sc = require("./js/scrolls.js");
|
const sc = require("./js/scrolls.js");
|
||||||
const gearLib = require("./js/gear.js");
|
const gearLib = require("./js/gear.js");
|
||||||
|
const db = require("./db.js");
|
||||||
|
|
||||||
/* ---------- Configuration par défaut (tout est réglable via l'admin) ---------- */
|
/* ---------- Configuration par défaut (tout est réglable via l'admin) ---------- */
|
||||||
|
|
||||||
@@ -52,59 +53,49 @@ const CONFIG_BOUNDS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/* ---------- Tuning : valeurs du bestiaire, de l'équipement et des trésors ----------
|
/* ---------- Tuning : valeurs du bestiaire, de l'équipement et des trésors ----------
|
||||||
* world.tuning ne stocke que les écarts à la valeur de base (vanilla). Il
|
* Les valeurs de référence vivent dans la base SQLite (db.js) ; chaque
|
||||||
* s'applique aux nouveaux spawns/drops du monde partagé — le solo reste vanilla. */
|
* spawn/drop la relit, donc une modification (page admin ou éditeur SQLite)
|
||||||
|
* s'applique à chaud aux nouveaux spawns/drops — le solo reste vanilla. */
|
||||||
|
|
||||||
const MONSTER_TUNE_KEYS = ["level", "att", "attMag", "esq", "deg", "degMag", "pv", "armor", "armorMag", "vue"];
|
const MONSTER_TUNE_KEYS = db.MONSTER_KEYS;
|
||||||
const MONSTER_TUNE_BOUNDS = { level: [1, 99], att: [1, 99], attMag: [0, 99], esq: [1, 99], deg: [1, 99], degMag: [0, 99], pv: [1, 999], armor: [0, 99], armorMag: [0, 99], vue: [1, 30] };
|
const MONSTER_TUNE_BOUNDS = { level: [1, 99], att: [1, 99], attMag: [0, 99], esq: [1, 99], deg: [1, 99], degMag: [0, 99], pv: [1, 999], armor: [0, 99], armorMag: [0, 99], vue: [1, 30] };
|
||||||
const GEAR_TUNE_KEYS = ["att", "attMag", "esq", "deg", "degMag", "reg", "arm", "armMag", "vue", "pv", "rmPct", "mmPct"];
|
const GEAR_TUNE_KEYS = db.GEAR_KEYS;
|
||||||
const GEAR_TUNE_BOUNDS = [-100, 100];
|
const GEAR_TUNE_BOUNDS = [-100, 100];
|
||||||
const POWER_BOUNDS = [0, 200];
|
const POWER_BOUNDS = [0, 200];
|
||||||
|
|
||||||
/* Fourchettes de puissance « niveau X » par défaut (Mountypedia). La Longue-Vue
|
|
||||||
* vanilla tire dans {1,2,3,5,8} : sans override 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: [0, 2],
|
|
||||||
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]]));
|
|
||||||
|
|
||||||
function emptyTuning() {
|
|
||||||
return { monsters: {}, gear: {}, potions: {}, scrolls: {} };
|
|
||||||
}
|
|
||||||
|
|
||||||
function randRange([min, max]) {
|
function randRange([min, max]) {
|
||||||
const lo = Math.min(min, max), hi = Math.max(min, max);
|
const lo = Math.min(min, max), hi = Math.max(min, max);
|
||||||
return lo + Math.floor(Math.random() * (hi - lo + 1));
|
return lo + Math.floor(Math.random() * (hi - lo + 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Types de monstres avec le tuning admin appliqué (avant gabarit d'âge). */
|
const sameRange = (a, b) => a && b && Math.min(...a) === Math.min(...b) && Math.max(...a) === Math.max(...b);
|
||||||
function tunedMonsterTypes(world) {
|
|
||||||
const tune = (world.tuning && world.tuning.monsters) || {};
|
/* Types de monstres tels qu'en base (avant gabarit d'âge). */
|
||||||
return [...g.MONSTER_TYPES, g.BOSS].map(t => tune[t.name] ? { ...t, ...tune[t.name] } : t);
|
function tunedMonsterTypes() {
|
||||||
|
return db.monsters();
|
||||||
}
|
}
|
||||||
|
|
||||||
function tunedRandomPotion(world) {
|
function tunedRandomPotion() {
|
||||||
const item = p.makeRandomPotion();
|
const item = p.makeRandomPotion();
|
||||||
const range = (world.tuning && world.tuning.potions || {})[item.potionId];
|
const range = db.treasureRange("potions", item.potionId);
|
||||||
return range ? p.makePotionItem(item.potionId, randRange(range)) : item;
|
// fourchette d'origine → on garde le tirage vanilla (ex. Longue-Vue {1,2,3,5,8})
|
||||||
|
if (!range || sameRange(range, db.POTION_POWER_DEFAULTS[item.potionId])) return item;
|
||||||
|
return p.makePotionItem(item.potionId, randRange(range));
|
||||||
}
|
}
|
||||||
|
|
||||||
function tunedRandomScroll(world) {
|
function tunedRandomScroll() {
|
||||||
const item = sc.makeRandomScroll();
|
const item = sc.makeRandomScroll();
|
||||||
const range = (world.tuning && world.tuning.scrolls || {})[item.scrollId];
|
const range = db.treasureRange("scrolls", item.scrollId);
|
||||||
return range ? sc.makeScrollItem(item.scrollId, randRange(range)) : item;
|
if (!range || sameRange(range, db.SCROLL_POWER_DEFAULTS[item.scrollId])) return item;
|
||||||
|
return sc.makeScrollItem(item.scrollId, randRange(range));
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyGearTuning(world, item) {
|
function applyGearTuning(item) {
|
||||||
if (!item || item.kind !== "gear") return item;
|
if (!item || item.kind !== "gear") return item;
|
||||||
const tune = (world.tuning && world.tuning.gear || {})[`${item.slot}/${item.name}`];
|
const row = db.gearRow(item.slot, item.name);
|
||||||
if (tune) item.mods = { ...item.mods, ...tune };
|
if (row) {
|
||||||
|
item.mods = Object.fromEntries(db.GEAR_KEYS.map(k => [k, row[k] || 0]).filter(([, v]) => v));
|
||||||
|
}
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,7 +132,7 @@ function spawnMonster(world, now = Date.now()) {
|
|||||||
// même logique que makeMonster (pool par profondeur + gabarit d'âge),
|
// même logique que makeMonster (pool par profondeur + gabarit d'âge),
|
||||||
// mais sur les types tunés par l'admin
|
// mais sur les types tunés par l'admin
|
||||||
const depth = world.config.worldDepth;
|
const depth = world.config.worldDepth;
|
||||||
const types = tunedMonsterTypes(world).filter(t => !t.boss);
|
const types = tunedMonsterTypes().filter(t => !t.boss);
|
||||||
const pool = types.filter(t => t.level <= depth + 1 && t.level >= Math.max(1, depth - 2));
|
const pool = types.filter(t => t.level <= depth + 1 && t.level >= Math.max(1, depth - 2));
|
||||||
const list = pool.length ? pool : types;
|
const list = pool.length ? pool : types;
|
||||||
const type = list[Math.floor(Math.random() * list.length)];
|
const type = list[Math.floor(Math.random() * list.length)];
|
||||||
@@ -160,9 +151,9 @@ function spawnItem(world) {
|
|||||||
if (!pos) return null;
|
if (!pos) return null;
|
||||||
const r = Math.random();
|
const r = Math.random();
|
||||||
const depth = world.config.worldDepth;
|
const depth = world.config.worldDepth;
|
||||||
const item = r < 0.34 ? tunedRandomPotion(world)
|
const item = r < 0.34 ? tunedRandomPotion()
|
||||||
: r < 0.48 ? tunedRandomScroll(world)
|
: r < 0.48 ? tunedRandomScroll()
|
||||||
: r < 0.78 ? applyGearTuning(world, gearLib.makeRandomGear(depth))
|
: r < 0.78 ? applyGearTuning(gearLib.makeRandomGear(depth))
|
||||||
: { kind: "gold", gold: g.rollDice(depth, 6).total * 10, emoji: "💰" };
|
: { kind: "gold", gold: g.rollDice(depth, 6).total * 10, emoji: "💰" };
|
||||||
if (item.kind === "gold") item.name = `${item.gold} Mountyzédons`;
|
if (item.kind === "gold") item.name = `${item.gold} Mountyzédons`;
|
||||||
world.items.push({ ...item, x: pos.x, y: pos.y });
|
world.items.push({ ...item, x: pos.x, y: pos.y });
|
||||||
@@ -173,7 +164,6 @@ function createWorld(config = {}) {
|
|||||||
const cfg = { ...DEFAULT_CONFIG, ...config };
|
const cfg = { ...DEFAULT_CONFIG, ...config };
|
||||||
const world = {
|
const world = {
|
||||||
config: cfg,
|
config: cfg,
|
||||||
tuning: emptyTuning(),
|
|
||||||
grid: g.generateCavern(cfg.mapW, cfg.mapH),
|
grid: g.generateCavern(cfg.mapW, cfg.mapH),
|
||||||
trolls: {},
|
trolls: {},
|
||||||
monsters: [],
|
monsters: [],
|
||||||
@@ -434,8 +424,8 @@ function killMonsterMP(world, t, m) {
|
|||||||
worldLog(world, `⚔️ ${t.name} a terrassé ${m.emoji} ${m.name} !`, "combat");
|
worldLog(world, `⚔️ ${t.name} a terrassé ${m.emoji} ${m.name} !`, "combat");
|
||||||
if (Math.random() < 0.4 && !world.items.some(i => i.x === m.x && i.y === m.y)) {
|
if (Math.random() < 0.4 && !world.items.some(i => i.x === m.x && i.y === m.y)) {
|
||||||
const lr = Math.random();
|
const lr = Math.random();
|
||||||
const drop = lr < 0.4 ? { ...tunedRandomPotion(world), x: m.x, y: m.y }
|
const drop = lr < 0.4 ? { ...tunedRandomPotion(), x: m.x, y: m.y }
|
||||||
: lr < 0.65 ? { ...tunedRandomScroll(world), x: m.x, y: m.y }
|
: lr < 0.65 ? { ...tunedRandomScroll(), 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 };
|
: { kind: "gold", gold: m.level * 10, name: `${m.level * 10} Mountyzédons`, emoji: "💰", x: m.x, y: m.y };
|
||||||
world.items.push(drop);
|
world.items.push(drop);
|
||||||
privLog(t, `${m.name} laisse tomber ${drop.emoji} ${drop.name}.`, "info");
|
privLog(t, `${m.name} laisse tomber ${drop.emoji} ${drop.name}.`, "info");
|
||||||
@@ -811,7 +801,7 @@ function adminOverview(world, now = Date.now()) {
|
|||||||
return {
|
return {
|
||||||
config: world.config,
|
config: world.config,
|
||||||
bounds: CONFIG_BOUNDS,
|
bounds: CONFIG_BOUNDS,
|
||||||
tuning: world.tuning || emptyTuning(),
|
tuning: currentTuning(),
|
||||||
defaults: adminDefaults(),
|
defaults: adminDefaults(),
|
||||||
uptime: now - world.createdAt,
|
uptime: now - world.createdAt,
|
||||||
trolls: Object.values(world.trolls).map(t => ({
|
trolls: Object.values(world.trolls).map(t => ({
|
||||||
@@ -857,41 +847,60 @@ function adminSetConfig(world, patch, now = Date.now()) {
|
|||||||
/* Valeurs de base (vanilla) pour construire les formulaires de tuning admin. */
|
/* Valeurs de base (vanilla) pour construire les formulaires de tuning admin. */
|
||||||
function adminDefaults() {
|
function adminDefaults() {
|
||||||
return {
|
return {
|
||||||
monsters: [...g.MONSTER_TYPES, g.BOSS].map(t => ({
|
monsters: db.vanillaMonsters(),
|
||||||
name: t.name, emoji: t.emoji, boss: !!t.boss,
|
|
||||||
...Object.fromEntries(MONSTER_TUNE_KEYS.map(k => [k, t[k] || 0])),
|
|
||||||
})),
|
|
||||||
monsterKeys: MONSTER_TUNE_KEYS,
|
monsterKeys: MONSTER_TUNE_KEYS,
|
||||||
gear: Object.entries(gearLib.GEAR).flatMap(([slot, list]) => list.map(def => ({
|
gear: db.vanillaGear().map(it => ({
|
||||||
slot, name: def.name, emoji: def.emoji, twoHanded: !!def.twoHanded,
|
slot: it.slot, name: it.name, emoji: it.emoji, twoHanded: it.twoHanded,
|
||||||
mods: Object.fromEntries(GEAR_TUNE_KEYS.map(k => [k, def.mods[k] || 0])),
|
mods: Object.fromEntries(GEAR_TUNE_KEYS.map(k => [k, it[k] || 0])),
|
||||||
}))),
|
})),
|
||||||
gearKeys: GEAR_TUNE_KEYS,
|
gearKeys: GEAR_TUNE_KEYS,
|
||||||
potions: p.POTION_IDS.map(id => ({
|
potions: db.vanillaTreasures("potions").map(t => ({ id: t.id, name: t.name, emoji: t.emoji, min: t.powerMin, max: t.powerMax })),
|
||||||
id, name: p.POTION_DEFS[id].name, emoji: p.POTION_DEFS[id].emoji,
|
scrolls: db.vanillaTreasures("scrolls").map(t => ({ id: t.id, name: t.name, emoji: t.emoji, min: t.powerMin, max: t.powerMax })),
|
||||||
min: POTION_POWER_DEFAULTS[id][0], max: POTION_POWER_DEFAULTS[id][1],
|
|
||||||
})),
|
|
||||||
scrolls: sc.SCROLL_IDS.map(id => ({
|
|
||||||
id, name: sc.SCROLL_DEFS[id].name, emoji: sc.SCROLL_DEFS[id].emoji,
|
|
||||||
min: SCROLL_POWER_DEFAULTS[id][0], max: SCROLL_POWER_DEFAULTS[id][1],
|
|
||||||
})),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Écarts actuels entre la base et le vanilla (pour surligner dans l'admin). */
|
||||||
|
function currentTuning() {
|
||||||
|
const out = { monsters: {}, gear: {}, potions: {}, scrolls: {} };
|
||||||
|
const vm = Object.fromEntries(db.vanillaMonsters().map(m => [m.name, m]));
|
||||||
|
for (const m of db.monsters()) {
|
||||||
|
if (!vm[m.name]) continue;
|
||||||
|
const d = {};
|
||||||
|
for (const k of MONSTER_TUNE_KEYS) if ((m[k] || 0) !== vm[m.name][k]) d[k] = m[k];
|
||||||
|
if (Object.keys(d).length) out.monsters[m.name] = d;
|
||||||
|
}
|
||||||
|
const vg = Object.fromEntries(db.vanillaGear().map(i => [`${i.slot}/${i.name}`, i]));
|
||||||
|
for (const it of db.gearAll()) {
|
||||||
|
const key = `${it.slot}/${it.name}`;
|
||||||
|
if (!vg[key]) continue;
|
||||||
|
const d = {};
|
||||||
|
for (const k of GEAR_TUNE_KEYS) if ((it[k] || 0) !== vg[key][k]) d[k] = it[k];
|
||||||
|
if (Object.keys(d).length) out.gear[key] = d;
|
||||||
|
}
|
||||||
|
for (const cat of ["potions", "scrolls"]) {
|
||||||
|
const defaults = cat === "potions" ? db.POTION_POWER_DEFAULTS : db.SCROLL_POWER_DEFAULTS;
|
||||||
|
for (const t of db.treasuresAll(cat)) {
|
||||||
|
if (defaults[t.id] && !sameRange([t.powerMin, t.powerMax], defaults[t.id])) {
|
||||||
|
out[cat][t.id] = [t.powerMin, t.powerMax];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
const clampInt = (v, lo, hi) => Math.max(lo, Math.min(hi, Math.round(v)));
|
const clampInt = (v, lo, hi) => Math.max(lo, Math.min(hi, Math.round(v)));
|
||||||
|
|
||||||
/* Applique un patch de tuning : chaque catégorie fournie remplace l'existante
|
/* Applique un patch de tuning : chaque catégorie fournie est remise au vanilla
|
||||||
* (l'admin envoie l'ensemble de ses écarts). Tout est validé et borné. */
|
* puis les écarts envoyés sont écrits en base (validés et bornés). */
|
||||||
function adminSetTuning(world, patch) {
|
function adminSetTuning(world, patch) {
|
||||||
world.tuning = world.tuning || emptyTuning();
|
|
||||||
const known = {
|
const known = {
|
||||||
monsters: new Set([...g.MONSTER_TYPES, g.BOSS].map(t => t.name)),
|
monsters: new Set(db.vanillaMonsters().map(t => t.name)),
|
||||||
gear: new Set(Object.entries(gearLib.GEAR).flatMap(([slot, list]) => list.map(d => `${slot}/${d.name}`))),
|
gear: new Set(db.vanillaGear().map(i => `${i.slot}/${i.name}`)),
|
||||||
potions: new Set(p.POTION_IDS),
|
potions: new Set(p.POTION_IDS),
|
||||||
scrolls: new Set(sc.SCROLL_IDS),
|
scrolls: new Set(sc.SCROLL_IDS),
|
||||||
};
|
};
|
||||||
if (patch.monsters && typeof patch.monsters === "object") {
|
if (patch.monsters && typeof patch.monsters === "object") {
|
||||||
const out = {};
|
db.resetCategory("monsters");
|
||||||
for (const [name, vals] of Object.entries(patch.monsters)) {
|
for (const [name, vals] of Object.entries(patch.monsters)) {
|
||||||
if (!known.monsters.has(name) || typeof vals !== "object") continue;
|
if (!known.monsters.has(name) || typeof vals !== "object") continue;
|
||||||
const entry = {};
|
const entry = {};
|
||||||
@@ -899,12 +908,11 @@ function adminSetTuning(world, patch) {
|
|||||||
const v = Number(vals[k]);
|
const v = Number(vals[k]);
|
||||||
if (Number.isFinite(v)) entry[k] = clampInt(v, MONSTER_TUNE_BOUNDS[k][0], MONSTER_TUNE_BOUNDS[k][1]);
|
if (Number.isFinite(v)) entry[k] = clampInt(v, MONSTER_TUNE_BOUNDS[k][0], MONSTER_TUNE_BOUNDS[k][1]);
|
||||||
}
|
}
|
||||||
if (Object.keys(entry).length) out[name] = entry;
|
db.setMonster(name, entry);
|
||||||
}
|
}
|
||||||
world.tuning.monsters = out;
|
|
||||||
}
|
}
|
||||||
if (patch.gear && typeof patch.gear === "object") {
|
if (patch.gear && typeof patch.gear === "object") {
|
||||||
const out = {};
|
db.resetCategory("gear");
|
||||||
for (const [key, mods] of Object.entries(patch.gear)) {
|
for (const [key, mods] of Object.entries(patch.gear)) {
|
||||||
if (!known.gear.has(key) || typeof mods !== "object") continue;
|
if (!known.gear.has(key) || typeof mods !== "object") continue;
|
||||||
const entry = {};
|
const entry = {};
|
||||||
@@ -912,25 +920,24 @@ function adminSetTuning(world, patch) {
|
|||||||
const v = Number(mods[k]);
|
const v = Number(mods[k]);
|
||||||
if (Number.isFinite(v)) entry[k] = clampInt(v, GEAR_TUNE_BOUNDS[0], GEAR_TUNE_BOUNDS[1]);
|
if (Number.isFinite(v)) entry[k] = clampInt(v, GEAR_TUNE_BOUNDS[0], GEAR_TUNE_BOUNDS[1]);
|
||||||
}
|
}
|
||||||
if (Object.keys(entry).length) out[key] = entry;
|
const slash = key.indexOf("/");
|
||||||
|
db.setGear(key.slice(0, slash), key.slice(slash + 1), entry);
|
||||||
}
|
}
|
||||||
world.tuning.gear = out;
|
|
||||||
}
|
}
|
||||||
for (const cat of ["potions", "scrolls"]) {
|
for (const cat of ["potions", "scrolls"]) {
|
||||||
if (!patch[cat] || typeof patch[cat] !== "object") continue;
|
if (!patch[cat] || typeof patch[cat] !== "object") continue;
|
||||||
const out = {};
|
db.resetCategory(cat);
|
||||||
for (const [id, range] of Object.entries(patch[cat])) {
|
for (const [id, range] of Object.entries(patch[cat])) {
|
||||||
if (!known[cat].has(id) || !Array.isArray(range)) continue;
|
if (!known[cat].has(id) || !Array.isArray(range)) continue;
|
||||||
const lo = Number(range[0]), hi = Number(range[1]);
|
const lo = Number(range[0]), hi = Number(range[1]);
|
||||||
if (!Number.isFinite(lo) || !Number.isFinite(hi)) continue;
|
if (!Number.isFinite(lo) || !Number.isFinite(hi)) continue;
|
||||||
out[id] = [
|
db.setTreasureRange(cat, id, [
|
||||||
clampInt(Math.min(lo, hi), POWER_BOUNDS[0], POWER_BOUNDS[1]),
|
clampInt(Math.min(lo, hi), POWER_BOUNDS[0], POWER_BOUNDS[1]),
|
||||||
clampInt(Math.max(lo, hi), POWER_BOUNDS[0], POWER_BOUNDS[1]),
|
clampInt(Math.max(lo, hi), POWER_BOUNDS[0], POWER_BOUNDS[1]),
|
||||||
];
|
]);
|
||||||
}
|
}
|
||||||
world.tuning[cat] = out;
|
|
||||||
}
|
}
|
||||||
return world.tuning;
|
return currentTuning();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Supprime définitivement un troll du monde (admin). */
|
/* Supprime définitivement un troll du monde (admin). */
|
||||||
@@ -975,7 +982,13 @@ function loadWorld(file) {
|
|||||||
const world = JSON.parse(fs.readFileSync(file, "utf8"));
|
const world = JSON.parse(fs.readFileSync(file, "utf8"));
|
||||||
if (!world || !world.grid || !world.trolls) return null;
|
if (!world || !world.grid || !world.trolls) return null;
|
||||||
world.config = { ...DEFAULT_CONFIG, ...world.config };
|
world.config = { ...DEFAULT_CONFIG, ...world.config };
|
||||||
world.tuning = { ...emptyTuning(), ...world.tuning };
|
// migration < 2.3.0 : les écarts de tuning vivaient dans world.json,
|
||||||
|
// on les déverse une fois dans la base puis on les retire du monde
|
||||||
|
if (world.tuning) {
|
||||||
|
const hasDeltas = Object.values(world.tuning).some(cat => cat && Object.keys(cat).length);
|
||||||
|
if (hasDeltas) adminSetTuning(world, world.tuning);
|
||||||
|
delete world.tuning;
|
||||||
|
}
|
||||||
return world;
|
return world;
|
||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
}
|
}
|
||||||
@@ -984,7 +997,7 @@ module.exports = {
|
|||||||
DEFAULT_CONFIG, CONFIG_BOUNDS,
|
DEFAULT_CONFIG, CONFIG_BOUNDS,
|
||||||
createWorld, tick, newTroll, authTroll, login, action, stateFor,
|
createWorld, tick, newTroll, authTroll, login, action, stateFor,
|
||||||
adminOverview, adminSetConfig, adminSetTuning, adminDefaults, adminResetWorld, adminKickTroll,
|
adminOverview, adminSetConfig, adminSetTuning, adminDefaults, adminResetWorld, adminKickTroll,
|
||||||
saveWorld, loadWorld,
|
currentTuning, saveWorld, loadWorld,
|
||||||
spawnMonster, spawnItem, monsterAct, trollDla, worldLog,
|
spawnMonster, spawnItem, monsterAct, trollDla, worldLog,
|
||||||
tunedRandomPotion, tunedRandomScroll, applyGearTuning,
|
tunedRandomPotion, tunedRandomScroll, applyGearTuning,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
/* Serveur MountyCrawl — fichiers statiques + API de niveaux communautaires
|
/* Serveur MountyCrawl — fichiers statiques + API de niveaux communautaires
|
||||||
* + monde multijoueur persistant (mp.js).
|
* + monde multijoueur persistant (mp.js).
|
||||||
* Node pur, aucune dépendance. Stockage : fichiers JSON (LEVELS_FILE, WORLD_FILE). */
|
* Node pur, aucune dépendance npm. Stockage : fichiers JSON (LEVELS_FILE,
|
||||||
|
* WORLD_FILE pour l'état vivant) + base SQLite native (DB_FILE) pour les
|
||||||
|
* valeurs de référence — bestiaire, équipement, potions, parchemins —
|
||||||
|
* éditables à chaud par la page admin ou n'importe quel outil SQLite. */
|
||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
@@ -8,12 +11,15 @@ const http = require("http");
|
|||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const crypto = require("crypto");
|
const crypto = require("crypto");
|
||||||
const mp = require("./mp.js");
|
const db = require("./db.js");
|
||||||
|
|
||||||
const PORT = process.env.PORT || 80;
|
const PORT = process.env.PORT || 80;
|
||||||
const ROOT = __dirname;
|
const ROOT = __dirname;
|
||||||
const LEVELS_FILE = process.env.LEVELS_FILE || "/data/levels.json";
|
const LEVELS_FILE = process.env.LEVELS_FILE || "/data/levels.json";
|
||||||
const WORLD_FILE = process.env.WORLD_FILE || "/data/world.json";
|
const WORLD_FILE = process.env.WORLD_FILE || "/data/world.json";
|
||||||
|
const DB_FILE = process.env.DB_FILE || "/data/mountycrawl.db";
|
||||||
|
|
||||||
|
const mp = require("./mp.js");
|
||||||
const MAX_BODY = 100 * 1024; // 100 Ko par niveau, large
|
const MAX_BODY = 100 * 1024; // 100 Ko par niveau, large
|
||||||
const MAX_LEVELS = 500;
|
const MAX_LEVELS = 500;
|
||||||
|
|
||||||
@@ -121,6 +127,7 @@ let worldDirty = false;
|
|||||||
let ADMIN_TOKEN = null;
|
let ADMIN_TOKEN = null;
|
||||||
|
|
||||||
function initMP() {
|
function initMP() {
|
||||||
|
db.init(DB_FILE); // avant loadWorld : la migration du tuning < 2.3.0 écrit dedans
|
||||||
WORLD = mp.loadWorld(WORLD_FILE) || mp.createWorld();
|
WORLD = mp.loadWorld(WORLD_FILE) || mp.createWorld();
|
||||||
// token admin : variable d'environnement, sinon généré et persisté à côté du monde
|
// token admin : variable d'environnement, sinon généré et persisté à côté du monde
|
||||||
ADMIN_TOKEN = process.env.MP_ADMIN_TOKEN || null;
|
ADMIN_TOKEN = process.env.MP_ADMIN_TOKEN || null;
|
||||||
@@ -332,7 +339,7 @@ const server = http.createServer((req, res) => {
|
|||||||
|
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
initMP();
|
initMP();
|
||||||
server.listen(PORT, () => console.log(`MountyCrawl sur le port ${PORT}, niveaux dans ${LEVELS_FILE}, monde dans ${WORLD_FILE}`));
|
server.listen(PORT, () => console.log(`MountyCrawl sur le port ${PORT}, niveaux dans ${LEVELS_FILE}, monde dans ${WORLD_FILE}, référence dans ${DB_FILE}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { validateLevel, MAP_W, MAP_H };
|
module.exports = { validateLevel, MAP_W, MAP_H };
|
||||||
|
|||||||
+50
-16
@@ -241,7 +241,8 @@ function makeWorld(over = {}) {
|
|||||||
// Tuning admin : bestiaire appliqué aux nouveaux spawns
|
// Tuning admin : bestiaire appliqué aux nouveaux spawns
|
||||||
{
|
{
|
||||||
const w = makeWorld({ monsterTarget: 0, itemTarget: 0, worldDepth: 1 });
|
const w = makeWorld({ monsterTarget: 0, itemTarget: 0, worldDepth: 1 });
|
||||||
mp.adminSetTuning(w, { monsters: { "Gobelin": { att: 9, pv: 77, armorMag: 3 } } });
|
const tun1 = mp.adminSetTuning(w, { monsters: { "Gobelin": { att: 9, pv: 77, armorMag: 3 } } });
|
||||||
|
assert.strictEqual(tun1.monsters.Gobelin.att, 9, "écart ATT visible dans le tuning courant");
|
||||||
// spawn forcé jusqu'à obtenir un Gobelin
|
// spawn forcé jusqu'à obtenir un Gobelin
|
||||||
let gob = null;
|
let gob = null;
|
||||||
for (let i = 0; i < 300 && !gob; i++) {
|
for (let i = 0; i < 300 && !gob; i++) {
|
||||||
@@ -254,33 +255,33 @@ function makeWorld(over = {}) {
|
|||||||
assert([54, 77].includes(gob.pv), "PV tunés (×gabarit) : " + gob.pv);
|
assert([54, 77].includes(gob.pv), "PV tunés (×gabarit) : " + gob.pv);
|
||||||
assert.strictEqual(gob.armorMag, 3, "armure magique tunée (gabarit ne la multiplie pas)");
|
assert.strictEqual(gob.armorMag, 3, "armure magique tunée (gabarit ne la multiplie pas)");
|
||||||
// bornage et noms inconnus ignorés
|
// bornage et noms inconnus ignorés
|
||||||
mp.adminSetTuning(w, { monsters: { "Dragon": { att: 5 }, "Gobelin": { att: 5000 } } });
|
const tun2 = mp.adminSetTuning(w, { monsters: { "Dragon": { att: 5 }, "Gobelin": { att: 5000 } } });
|
||||||
assert(!w.tuning.monsters.Dragon, "type inconnu ignoré");
|
assert(!tun2.monsters.Dragon, "type inconnu ignoré");
|
||||||
assert.strictEqual(w.tuning.monsters.Gobelin.att, 99, "ATT bornée à 99");
|
assert.strictEqual(tun2.monsters.Gobelin.att, 99, "ATT bornée à 99");
|
||||||
// retour au vanilla
|
// retour au vanilla
|
||||||
mp.adminSetTuning(w, { monsters: {} });
|
const tun3 = mp.adminSetTuning(w, { monsters: {} });
|
||||||
assert.strictEqual(Object.keys(w.tuning.monsters).length, 0, "bestiaire d'origine restauré");
|
assert.strictEqual(Object.keys(tun3.monsters).length, 0, "bestiaire d'origine restauré");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tuning admin : puissance des potions/parchemins et bonus d'équipement
|
// Tuning admin : puissance des potions/parchemins et bonus d'équipement
|
||||||
{
|
{
|
||||||
const w = makeWorld({ monsterTarget: 0, itemTarget: 0 });
|
const w = makeWorld({ monsterTarget: 0, itemTarget: 0 });
|
||||||
mp.adminSetTuning(w, {
|
const tun = mp.adminSetTuning(w, {
|
||||||
potions: { guerison: [9, 9], nimporte: [1, 2] },
|
potions: { guerison: [9, 9], nimporte: [1, 2] },
|
||||||
scrolls: { runeExplosive: [7, 3] }, // inversé : doit devenir [3, 7]
|
scrolls: { runeExplosive: [7, 3] }, // inversé : doit devenir [3, 7]
|
||||||
gear: { "arme/Gourdin": { deg: 12 }, "arme/Excalibur": { deg: 99 } },
|
gear: { "arme/Gourdin": { deg: 12 }, "arme/Excalibur": { deg: 99 } },
|
||||||
});
|
});
|
||||||
assert(!w.tuning.potions.nimporte, "potion inconnue ignorée");
|
assert(!tun.potions.nimporte, "potion inconnue ignorée");
|
||||||
assert.deepStrictEqual(w.tuning.scrolls.runeExplosive, [3, 7], "fourchette remise dans l'ordre");
|
assert.deepStrictEqual(tun.scrolls.runeExplosive, [3, 7], "fourchette remise dans l'ordre");
|
||||||
assert(!w.tuning.gear["arme/Excalibur"], "objet inconnu ignoré");
|
assert(!tun.gear["arme/Excalibur"], "objet inconnu ignoré");
|
||||||
// la puissance tirée respecte l'override
|
// la puissance tirée respecte l'override
|
||||||
for (let i = 0; i < 100; i++) {
|
for (let i = 0; i < 100; i++) {
|
||||||
const it = mp.tunedRandomPotion(w);
|
const it = mp.tunedRandomPotion();
|
||||||
if (it.potionId === "guerison") assert.strictEqual(it.power, 9, "Guérison forcée à X=9");
|
if (it.potionId === "guerison") assert.strictEqual(it.power, 9, "Guérison forcée à X=9");
|
||||||
}
|
}
|
||||||
// l'équipement tuné garde ses autres mods
|
// l'équipement tuné garde ses autres mods
|
||||||
const gearLib = require("../js/gear.js");
|
const gearLib = require("../js/gear.js");
|
||||||
const club = mp.applyGearTuning(w, gearLib.gearItemByName("arme", "Gourdin"));
|
const club = mp.applyGearTuning(gearLib.gearItemByName("arme", "Gourdin"));
|
||||||
assert.strictEqual(club.mods.deg, 12, "DEG du Gourdin tuné");
|
assert.strictEqual(club.mods.deg, 12, "DEG du Gourdin tuné");
|
||||||
assert.strictEqual(club.mods.att, 2, "ATT du Gourdin inchangée");
|
assert.strictEqual(club.mods.att, 2, "ATT du Gourdin inchangée");
|
||||||
// les défauts pour l'admin sont complets
|
// les défauts pour l'admin sont complets
|
||||||
@@ -294,12 +295,12 @@ function makeWorld(over = {}) {
|
|||||||
// Tuning admin : saveurs magiques (attMag/degMag/armMag) et suppression de troll
|
// Tuning admin : saveurs magiques (attMag/degMag/armMag) et suppression de troll
|
||||||
{
|
{
|
||||||
const w = makeWorld({ monsterTarget: 0, itemTarget: 0, worldDepth: 1 });
|
const w = makeWorld({ monsterTarget: 0, itemTarget: 0, worldDepth: 1 });
|
||||||
mp.adminSetTuning(w, {
|
const tunMag = mp.adminSetTuning(w, {
|
||||||
monsters: { "Gobelin": { attMag: 6, degMag: 4 } },
|
monsters: { "Gobelin": { attMag: 6, degMag: 4 } },
|
||||||
gear: { "arme/Bâton de mage": { attMag: 4, armMag: 2 } },
|
gear: { "arme/Bâton de mage": { attMag: 4, armMag: 2 } },
|
||||||
});
|
});
|
||||||
assert.strictEqual(w.tuning.monsters.Gobelin.attMag, 6, "ATT mag du Gobelin tunée");
|
assert.strictEqual(tunMag.monsters.Gobelin.attMag, 6, "ATT mag du Gobelin tunée");
|
||||||
assert.strictEqual(w.tuning.gear["arme/Bâton de mage"].armMag, 2, "Armure mag du bâton tunée");
|
assert.strictEqual(tunMag.gear["arme/Bâton de mage"].armMag, 2, "Armure mag du bâton tunée");
|
||||||
let gob = null;
|
let gob = null;
|
||||||
for (let i = 0; i < 300 && !gob; i++) {
|
for (let i = 0; i < 300 && !gob; i++) {
|
||||||
const m = mp.spawnMonster(w);
|
const m = mp.spawnMonster(w);
|
||||||
@@ -307,7 +308,7 @@ function makeWorld(over = {}) {
|
|||||||
}
|
}
|
||||||
assert(gob && gob.attMag > 0 && gob.degMag > 0, "le Gobelin spawné a une attaque magique");
|
assert(gob && gob.attMag > 0 && gob.degMag > 0, "le Gobelin spawné a une attaque magique");
|
||||||
const gearLib = require("../js/gear.js");
|
const gearLib = require("../js/gear.js");
|
||||||
const baton = mp.applyGearTuning(w, gearLib.gearItemByName("arme", "Bâton de mage"));
|
const baton = mp.applyGearTuning(gearLib.gearItemByName("arme", "Bâton de mage"));
|
||||||
assert.strictEqual(baton.mods.attMag, 4, "ATT mag du bâton droppé");
|
assert.strictEqual(baton.mods.attMag, 4, "ATT mag du bâton droppé");
|
||||||
assert.strictEqual(baton.mods.mmPct, 15, "MM % vanilla conservé");
|
assert.strictEqual(baton.mods.mmPct, 15, "MM % vanilla conservé");
|
||||||
// suppression admin d'un troll
|
// suppression admin d'un troll
|
||||||
@@ -318,6 +319,39 @@ function makeWorld(over = {}) {
|
|||||||
assert(!mp.newTroll(w, "Banni", "Skrim").error, "son nom redevient libre");
|
assert(!mp.newTroll(w, "Banni", "Skrim").error, "son nom redevient libre");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Base de référence : les retouches survivent au redémarrage (seed non destructif)
|
||||||
|
{
|
||||||
|
const os = require("os");
|
||||||
|
const path = require("path");
|
||||||
|
const fs = require("fs");
|
||||||
|
const db = require("../db.js");
|
||||||
|
const file = path.join(os.tmpdir(), "mc-test-ref.db");
|
||||||
|
for (const f of [file, file + "-wal", file + "-shm"]) { try { fs.rmSync(f); } catch {} }
|
||||||
|
db.init(file);
|
||||||
|
db.setGear("arme", "Gourdin", { deg: 42 });
|
||||||
|
db.setMonster("Sorcière", { attMag: 7, degMag: 5 });
|
||||||
|
db.init(file); // « redémarrage » : INSERT OR IGNORE ne doit rien écraser
|
||||||
|
assert.strictEqual(db.gearRow("arme", "Gourdin").deg, 42, "retouche d'équipement conservée");
|
||||||
|
const sorciere = db.monsters().find(m => m.name === "Sorcière");
|
||||||
|
assert.strictEqual(sorciere.attMag, 7, "retouche du bestiaire conservée");
|
||||||
|
assert.strictEqual(db.gearRow("arme", "Torche").vue, 1, "valeurs vanilla seedées");
|
||||||
|
db.init(":memory:"); // base propre pour la suite des tests
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migration < 2.3.0 : le tuning de world.json est déversé une fois dans la base
|
||||||
|
{
|
||||||
|
const os = require("os");
|
||||||
|
const path = require("path");
|
||||||
|
const w = makeWorld();
|
||||||
|
w.tuning = { monsters: { "Gobelin": { att: 42 } }, gear: {}, potions: {}, scrolls: {} };
|
||||||
|
const file = path.join(os.tmpdir(), "mc-world-migration.json");
|
||||||
|
mp.saveWorld(w, file);
|
||||||
|
const loaded = mp.loadWorld(file);
|
||||||
|
assert(loaded && !loaded.tuning, "tuning retiré du monde migré");
|
||||||
|
assert.strictEqual(mp.currentTuning().monsters.Gobelin.att, 42, "écart migré en base");
|
||||||
|
mp.adminSetTuning(loaded, { monsters: {} }); // nettoyage
|
||||||
|
}
|
||||||
|
|
||||||
// Persistance : save + load
|
// Persistance : save + load
|
||||||
{
|
{
|
||||||
const os = require("os");
|
const os = require("os");
|
||||||
|
|||||||
Reference in New Issue
Block a user