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>
This commit is contained in:
@@ -234,6 +234,17 @@ non affilié au jeu original de Mountyhall SARL.
|
||||
|
||||
## Versions
|
||||
|
||||
- **2.7.0** (2026-06-14) — **Templates de drop** façon MountyHall. 19 suffixes
|
||||
(« de l'Aigle », « des Mages », « des Béhémoths »…) stockés dans une nouvelle
|
||||
table `templates` de la base (seedés au démarrage, éditables comme le reste via
|
||||
sqlite-web / `GET /api/reference/templates`). À chaque **drop d'équipement du
|
||||
monde partagé**, l'objet reçoit jusqu'à **3 templates** (6 pour une arme à
|
||||
**2 mains**), chaque emplacement étant rempli avec une **probabilité réglable
|
||||
sur la page admin** (`templateChance`, 0–100 %). Les bonus des templates
|
||||
s'ajoutent aux mods de l'objet (nouveau mod `TOUR`, en minutes) et leurs
|
||||
suffixes au nom (« Épée Courte de l'Aigle des Mages »). Le solo reste vanilla.
|
||||
*Note : le bonus `TOUR` est affiché mais son effet de jeu (DLA) reste à câbler
|
||||
une fois la règle confirmée.*
|
||||
- **2.6.3** (2026-06-13) — **Cache-busting** des fichiers du jeu : les balises
|
||||
`<script>`/`<link>` de `index.html` portent `?v=__V__`, jeton que le serveur
|
||||
remplace par la version de l'appli (donc différent à chaque déploiement). Le
|
||||
|
||||
@@ -116,6 +116,7 @@ const FIELDS = [
|
||||
["deathRespawnSec", "Réapparition des trolls (s)", "délai après une mort"],
|
||||
["worldDepth", "Profondeur du monde (1-5)", "puissance des monstres et des trésors"],
|
||||
["maxTrolls", "Trolls max", "limite de personnages dans le monde"],
|
||||
["templateChance", "Templates — proba / emplacement (%)", "chance qu'un emplacement de template soit rempli sur un drop (3 max en 1 main, 6 en 2 mains)"],
|
||||
["mapW", "Largeur de carte", "appliqué à la prochaine régénération du monde"],
|
||||
["mapH", "Hauteur de carte", "appliqué à la prochaine régénération du monde"],
|
||||
];
|
||||
|
||||
@@ -23,6 +23,33 @@ 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. */
|
||||
@@ -55,6 +82,12 @@ function vanillaGear() {
|
||||
})));
|
||||
}
|
||||
|
||||
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]
|
||||
@@ -93,6 +126,9 @@ function init(file = ":memory:") {
|
||||
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(", ")})
|
||||
@@ -110,6 +146,9 @@ function init(file = ":memory:") {
|
||||
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.
|
||||
@@ -148,6 +187,10 @@ 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) {
|
||||
@@ -169,10 +212,18 @@ function setTreasureRange(cat, id, [min, max]) {
|
||||
.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]);
|
||||
}
|
||||
@@ -180,8 +231,8 @@ function resetCategory(cat) {
|
||||
|
||||
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,
|
||||
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,
|
||||
};
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
const APP_VERSION = "2.6.3";
|
||||
const APP_VERSION = "2.7.0";
|
||||
|
||||
/* Alpha : maîtrise initiale haute pour les tests. Remettre 15 % / 15 % à la v1.0 officielle. */
|
||||
const START_COMP_PCT = 90;
|
||||
|
||||
@@ -102,6 +102,7 @@ const GEAR_MOD_LABELS = [
|
||||
["arm", "Armure phy.", ""], ["armMag", "Armure mag.", ""],
|
||||
["vue", "VUE", ""], ["pv", "PV", ""],
|
||||
["rmPct", "RM", " %"], ["mmPct", "MM", " %"],
|
||||
["tour", "TOUR", " mn"], // apporté par les templates de drop (modif de DLA)
|
||||
];
|
||||
|
||||
function formatGearMods(mods, sep = " · ") {
|
||||
|
||||
@@ -41,6 +41,7 @@ const DEFAULT_CONFIG = {
|
||||
deathRespawnSec: 90, // délai de réapparition d'un troll terrassé
|
||||
maxTrolls: 40,
|
||||
logSize: 150,
|
||||
templateChance: 0, // proba (%) qu'un emplacement de template soit rempli sur un drop d'équipement
|
||||
};
|
||||
|
||||
const CONFIG_BOUNDS = {
|
||||
@@ -50,6 +51,7 @@ const CONFIG_BOUNDS = {
|
||||
monsterTarget: [0, 120], repopSec: [10, 3600],
|
||||
itemTarget: [0, 120], deathRespawnSec: [0, 3600],
|
||||
maxTrolls: [1, 200], logSize: [20, 500],
|
||||
templateChance: [0, 100],
|
||||
};
|
||||
|
||||
/* ---------- Tuning : valeurs du bestiaire, de l'équipement et des trésors ----------
|
||||
@@ -99,6 +101,33 @@ function applyGearTuning(item) {
|
||||
return item;
|
||||
}
|
||||
|
||||
/* Templates de drop : un objet qui tombe reçoit jusqu'à 3 templates (6 pour une
|
||||
* arme à 2 mains), chaque emplacement étant rempli avec la proba `chance` (%)
|
||||
* par un template DISTINCT tiré au hasard en base. Les bonus (dont `tour`)
|
||||
* s'ajoutent aux mods de l'objet, et les suffixes au nom (« Épée Courte de
|
||||
* l'Aigle des Mages »). N'affecte que le monde partagé (drops) ; le solo reste
|
||||
* vanilla. */
|
||||
function applyGearTemplates(item, chance) {
|
||||
if (!item || item.kind !== "gear" || !(chance > 0)) return item;
|
||||
const pool = db.templatesAll();
|
||||
if (!pool.length) return item;
|
||||
const slots = item.twoHanded ? 6 : 3;
|
||||
const picked = [];
|
||||
for (let i = 0; i < slots && pool.length; i++) {
|
||||
if (Math.random() * 100 >= chance) continue;
|
||||
picked.push(pool.splice(Math.floor(Math.random() * pool.length), 1)[0]);
|
||||
}
|
||||
if (!picked.length) return item;
|
||||
const mods = { ...(item.mods || {}) };
|
||||
for (const t of picked) {
|
||||
for (const k of db.TEMPLATE_KEYS) if (t[k]) mods[k] = (mods[k] || 0) + t[k];
|
||||
}
|
||||
item.mods = Object.fromEntries(Object.entries(mods).filter(([, v]) => v));
|
||||
item.name = `${item.name} ${picked.map(t => t.name).join(" ")}`;
|
||||
item.templates = picked.map(t => t.name);
|
||||
return item;
|
||||
}
|
||||
|
||||
/* ---------- Création du monde ---------- */
|
||||
|
||||
function randomFloor(world, taken = null) {
|
||||
@@ -153,7 +182,7 @@ function spawnItem(world) {
|
||||
const depth = world.config.worldDepth;
|
||||
const item = r < 0.34 ? tunedRandomPotion()
|
||||
: r < 0.48 ? tunedRandomScroll()
|
||||
: r < 0.78 ? applyGearTuning(gearLib.makeRandomGear(depth))
|
||||
: r < 0.78 ? applyGearTemplates(applyGearTuning(gearLib.makeRandomGear(depth)), world.config.templateChance)
|
||||
: { kind: "gold", gold: g.rollDice(depth, 6).total * 10, emoji: "💰" };
|
||||
if (item.kind === "gold") item.name = `${item.gold} Mountyzédons`;
|
||||
world.items.push({ ...item, x: pos.x, y: pos.y });
|
||||
@@ -1038,5 +1067,5 @@ module.exports = {
|
||||
adminOverview, adminSetConfig, adminSetTuning, adminDefaults, adminResetWorld, adminKickTroll,
|
||||
currentTuning, saveWorld, loadWorld,
|
||||
spawnMonster, spawnItem, monsterAct, trollDla, worldLog,
|
||||
tunedRandomPotion, tunedRandomScroll, applyGearTuning,
|
||||
tunedRandomPotion, tunedRandomScroll, applyGearTuning, applyGearTemplates,
|
||||
};
|
||||
|
||||
@@ -275,8 +275,9 @@ function handleAPI(req, res, url) {
|
||||
monsters: () => db.monsters(),
|
||||
potions: () => db.treasuresAll("potions"),
|
||||
scrolls: () => db.treasuresAll("scrolls"),
|
||||
templates: () => db.templatesAll(),
|
||||
};
|
||||
const refMatch = url.pathname.match(/^\/api\/reference\/(gear|monsters|potions|scrolls)$/);
|
||||
const refMatch = url.pathname.match(/^\/api\/reference\/(gear|monsters|potions|scrolls|templates)$/);
|
||||
if (req.method === "GET" && refMatch) {
|
||||
return sendJSON(res, 200, REFERENCE[refMatch[1]]());
|
||||
}
|
||||
|
||||
+14
@@ -280,6 +280,20 @@ function makeWorld(over = {}) {
|
||||
assert.strictEqual(db.gearRow("arme", "Gourdin").att, 2, "reset gear → vanilla (gourdin)");
|
||||
}
|
||||
|
||||
// Templates de drop : nombre selon les mains, distincts, bonus fusionnés, proba 0
|
||||
{
|
||||
const one = mp.applyGearTemplates({ kind: "gear", slot: "arme", name: "Épée Courte", twoHanded: false, mods: { deg: 2 } }, 100);
|
||||
assert.strictEqual(one.templates.length, 3, "1 main : 3 templates max");
|
||||
assert.strictEqual(one.mods.deg >= 2, true, "mod de base conservé");
|
||||
assert(one.name.startsWith("Épée Courte "), "suffixes de template ajoutés au nom");
|
||||
const two = mp.applyGearTemplates({ kind: "gear", slot: "arme", name: "Épée Longue", twoHanded: true, mods: {} }, 100);
|
||||
assert.strictEqual(two.templates.length, 6, "2 mains : 6 templates max");
|
||||
assert.strictEqual(new Set(two.templates).size, 6, "templates tous distincts");
|
||||
const none = mp.applyGearTemplates({ kind: "gear", slot: "arme", name: "Gourdin", twoHanded: false, mods: { att: 2 } }, 0);
|
||||
assert(!none.templates, "proba 0 : aucun template");
|
||||
assert.strictEqual(none.name, "Gourdin", "nom inchangé sans template");
|
||||
}
|
||||
|
||||
// Tuning admin : puissance des potions/parchemins et bonus d'équipement
|
||||
{
|
||||
const w = makeWorld({ monsterTarget: 0, itemTarget: 0 });
|
||||
|
||||
Reference in New Issue
Block a user