v2.11.0 : bestiaire etape 4 - les monstres du bestiaire apparaissent (2 modes)
game.js : buildBestiaryMonster(list, ageMults, ageNames, depth, x, y) partage - tranche de niveau autour de la profondeur (bande +-3), age au hasard dans [minAge,maxAge], multiplicateur applique, stats tirees dans les plages, nom = prefixe d'age + nom, emoji de famille, attMag/degMag si magic. makeMonster bascule dessus (repli ancien systeme si BESTIARY absent). index.html charge js/bestiary.js (solo). mp.js spawnMonster -> bestiaire (db.bestiaryAll + db.monsterAges, tunes admin) ; worldDepth borne a 45. Editeur de niveaux + Behemoth gardent l'ancien systeme. Tests mp.js mis a jour (anciens tests de spawn 7-monstres remplaces par tests bestiaire). 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
|
## Versions
|
||||||
|
|
||||||
|
- **2.11.0** (2026-06-15) — Bestiaire, étape 4 : **les monstres du bestiaire
|
||||||
|
apparaissent dans le jeu** (solo ET monde partagé), à la place des 7 anciens.
|
||||||
|
À chaque apparition, on choisit un monstre dont le niveau de base tombe dans la
|
||||||
|
**tranche autour de la profondeur** (± quelques niveaux), un **âge au hasard**
|
||||||
|
dans sa fourchette (multiplicateur d'âge appliqué), puis chaque stat est tirée
|
||||||
|
dans sa plage ; le nom prend le préfixe d'âge (« Mûr Ankheg ») et un emoji de
|
||||||
|
famille. La **profondeur** du monde partagé est désormais réglable **jusqu'à 45**
|
||||||
|
(pour atteindre les monstres de haut niveau). Le multi lit la base (tunée
|
||||||
|
admin), le solo les valeurs statiques. *(Les monstres placés à la main dans
|
||||||
|
l'éditeur de niveaux et le Béhémoth gardent l'ancien système ; les capacités
|
||||||
|
spéciales et drapeaux sont portés mais pas encore actifs.)*
|
||||||
- **2.10.2** (2026-06-15) — Bestiaire, étape 3 : **encyclopédie**. Nouvelle
|
- **2.10.2** (2026-06-15) — Bestiaire, étape 3 : **encyclopédie**. Nouvelle
|
||||||
section « 🐲 Bestiaire » dans « Trésors du Hall » (accessible par le menu) qui
|
section « 🐲 Bestiaire » dans « Trésors du Hall » (accessible par le menu) qui
|
||||||
liste les 150 monstres **groupés par famille**, avec leur **blason**, leurs
|
liste les 150 monstres **groupés par famille**, avec leur **blason**, leurs
|
||||||
|
|||||||
@@ -231,6 +231,7 @@
|
|||||||
<script src="js/potions.js?v=__V__"></script>
|
<script src="js/potions.js?v=__V__"></script>
|
||||||
<script src="js/scrolls.js?v=__V__"></script>
|
<script src="js/scrolls.js?v=__V__"></script>
|
||||||
<script src="js/gear.js?v=__V__"></script>
|
<script src="js/gear.js?v=__V__"></script>
|
||||||
|
<script src="js/bestiary.js?v=__V__"></script>
|
||||||
<script src="js/game.js?v=__V__"></script>
|
<script src="js/game.js?v=__V__"></script>
|
||||||
<script src="js/editor.js?v=__V__"></script>
|
<script src="js/editor.js?v=__V__"></script>
|
||||||
<script src="js/mp-client.js?v=__V__"></script>
|
<script src="js/mp-client.js?v=__V__"></script>
|
||||||
|
|||||||
+44
-2
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const APP_VERSION = "2.10.2";
|
const APP_VERSION = "2.11.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;
|
||||||
@@ -210,7 +210,49 @@ function applyTemplate(type, tpl, x, y) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FAMILY_EMOJI_G = { Insecte: "🐛", Animal: "🐾", "Démon": "👿", Humanoide: "🧝", Monstre: "👹", "Mort-Vivant": "💀" };
|
||||||
|
|
||||||
|
/* Construit un monstre du BESTIAIRE pour une profondeur : on choisit un monstre
|
||||||
|
* dont le niveau de base tombe dans une tranche autour de la profondeur (± bande),
|
||||||
|
* un âge au hasard dans sa fourchette (le multiplicateur d'âge est appliqué), et
|
||||||
|
* on tire chaque stat dans sa plage. `list` = lignes du bestiaire, `ageMults` =
|
||||||
|
* [m0..m7], `ageNames` = noms d'âge par famille. Partagé solo (données statiques
|
||||||
|
* de bestiary.js) / multi (données de la base, tunées admin). */
|
||||||
|
function buildBestiaryMonster(list, ageMults, ageNames, depth, x, y) {
|
||||||
|
if (!list || !list.length) return null;
|
||||||
|
const band = 3;
|
||||||
|
let pool = list.filter(m => m.levelMin <= depth + band && m.levelMax >= depth - band);
|
||||||
|
if (!pool.length) { // hors tranche : on prend les plus proches en niveau
|
||||||
|
let best = Infinity;
|
||||||
|
for (const m of list) best = Math.min(best, Math.abs((m.levelMin + m.levelMax) / 2 - depth));
|
||||||
|
pool = list.filter(m => Math.abs((m.levelMin + m.levelMax) / 2 - depth) <= best + 1);
|
||||||
|
}
|
||||||
|
const b = pool[Math.floor(Math.random() * pool.length)];
|
||||||
|
const age = b.minAge + Math.floor(Math.random() * (b.maxAge - b.minAge + 1));
|
||||||
|
const f = (ageMults[age] || 1) / (ageMults[b.minAge] || 1);
|
||||||
|
const roll = (mn, mx) => { const lo = Math.round(mn * f), hi = Math.round(mx * f); return lo + Math.floor(Math.random() * (Math.max(lo, hi) - lo + 1)); };
|
||||||
|
const att = Math.max(1, roll(b.attMin, b.attMax)), deg = Math.max(1, roll(b.degMin, b.degMax)), pv = Math.max(1, roll(b.pvMin, b.pvMax));
|
||||||
|
const names = (ageNames && ageNames[b.family]) || [];
|
||||||
|
return {
|
||||||
|
name: ((names[age] ? names[age] + " " : "") + b.name).trim(),
|
||||||
|
emoji: FAMILY_EMOJI_G[b.family] || "👹",
|
||||||
|
level: Math.max(1, roll(b.levelMin, b.levelMax)),
|
||||||
|
att, esq: Math.max(1, roll(b.esqMin, b.esqMax)), deg,
|
||||||
|
reg: Math.max(0, roll(b.regMin, b.regMax)),
|
||||||
|
attMag: b.magic ? att : 0, degMag: b.magic ? deg : 0,
|
||||||
|
pv, pvMax: pv,
|
||||||
|
armor: Math.max(0, roll(b.armPhysMin, b.armPhysMax)), armorMag: Math.max(0, roll(b.armMagMin, b.armMagMax)),
|
||||||
|
vue: Math.max(1, roll(b.vueMin, b.vueMax)),
|
||||||
|
static: false, boss: false, x, y,
|
||||||
|
family: b.family, fly: !!b.fly, ranged: !!b.ranged, seesHidden: !!b.seesHidden, speed: b.speed, nbAtt: b.nbAtt, capacities: b.capacities,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function makeMonster(depth, x, y) {
|
function makeMonster(depth, x, y) {
|
||||||
|
// Bestiaire (données statiques chargées dans le navigateur en solo)
|
||||||
|
if (typeof BESTIARY !== "undefined" && BESTIARY.length)
|
||||||
|
return buildBestiaryMonster(BESTIARY, AGE_MULT, AGE_NAMES, depth, x, y);
|
||||||
|
// Repli (bestiaire non chargé) : ancien système des 7 types
|
||||||
const pool = MONSTER_TYPES.filter(m => m.level <= depth + 1 && m.level >= Math.max(1, depth - 2));
|
const pool = MONSTER_TYPES.filter(m => m.level <= depth + 1 && m.level >= Math.max(1, depth - 2));
|
||||||
const type = pool[Math.floor(Math.random() * pool.length)];
|
const type = pool[Math.floor(Math.random() * pool.length)];
|
||||||
const tplMax = Math.min(TEMPLATES.length - 1, depth - 1);
|
const tplMax = Math.min(TEMPLATES.length - 1, depth - 1);
|
||||||
@@ -1702,7 +1744,7 @@ if (typeof document !== "undefined") {
|
|||||||
if (typeof module !== "undefined" && module.exports) {
|
if (typeof module !== "undefined" && module.exports) {
|
||||||
module.exports = {
|
module.exports = {
|
||||||
APP_VERSION, rollDice, resolveAttack, resolveSpell, masteryRoll, improveCost, levelFromTotalPI, killPX,
|
APP_VERSION, rollDice, resolveAttack, resolveSpell, masteryRoll, improveCost, levelFromTotalPI, killPX,
|
||||||
RACES, MONSTER_TYPES, BOSS, TEMPLATES, applyTemplate, makeMonster, monsterFromSpec, itemFromSpec, equipGear, unequipToBag,
|
RACES, MONSTER_TYPES, BOSS, TEMPLATES, applyTemplate, makeMonster, buildBestiaryMonster, monsterFromSpec, itemFromSpec, equipGear, unequipToBag,
|
||||||
generateCavern, largestRegion,
|
generateCavern, largestRegion,
|
||||||
MAP_W, MAP_H, T_WALL, T_FLOOR, T_STAIRS,
|
MAP_W, MAP_H, T_WALL, T_FLOOR, T_STAIRS,
|
||||||
COSTS, PA_PER_TURN, START_COMP_PCT, START_SORT_PCT,
|
COSTS, PA_PER_TURN, START_COMP_PCT, START_SORT_PCT,
|
||||||
|
|||||||
@@ -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 bestiaryLib = require("./js/bestiary.js");
|
||||||
const db = require("./db.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) ---------- */
|
||||||
@@ -45,7 +46,7 @@ const DEFAULT_CONFIG = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const CONFIG_BOUNDS = {
|
const CONFIG_BOUNDS = {
|
||||||
mapW: [24, 80], mapH: [16, 60], worldDepth: [1, 5],
|
mapW: [24, 80], mapH: [16, 60], worldDepth: [1, 45],
|
||||||
monsterDlaMinSec: [5, 3600], monsterDlaMaxSec: [5, 7200],
|
monsterDlaMinSec: [5, 3600], monsterDlaMaxSec: [5, 7200],
|
||||||
trollDlaSec: [5, 3600], pollSec: [1, 60],
|
trollDlaSec: [5, 3600], pollSec: [1, 60],
|
||||||
monsterTarget: [0, 120], repopSec: [10, 3600],
|
monsterTarget: [0, 120], repopSec: [10, 3600],
|
||||||
@@ -158,16 +159,12 @@ function monsterDlaMs(config) {
|
|||||||
function spawnMonster(world, now = Date.now()) {
|
function spawnMonster(world, now = Date.now()) {
|
||||||
const pos = randomFloor(world);
|
const pos = randomFloor(world);
|
||||||
if (!pos) return null;
|
if (!pos) return null;
|
||||||
// même logique que makeMonster (pool par profondeur + gabarit d'âge),
|
// Bestiaire : on tire un monstre dont le niveau tombe dans la tranche de la
|
||||||
// mais sur les types tunés par l'admin
|
// profondeur, avec un âge au hasard. Données de la base (tunées admin) +
|
||||||
|
// multiplicateurs d'âge.
|
||||||
const depth = world.config.worldDepth;
|
const depth = world.config.worldDepth;
|
||||||
const types = tunedMonsterTypes().filter(t => !t.boss);
|
const m = g.buildBestiaryMonster(db.bestiaryAll(), db.monsterAges().map(a => a.mult), bestiaryLib.AGE_NAMES, depth, pos.x, pos.y);
|
||||||
const pool = types.filter(t => t.level <= depth + 1 && t.level >= Math.max(1, depth - 2));
|
if (!m) return null;
|
||||||
const list = pool.length ? pool : types;
|
|
||||||
const type = list[Math.floor(Math.random() * list.length)];
|
|
||||||
const tplMax = Math.min(g.TEMPLATES.length - 1, depth - 1);
|
|
||||||
const tpl = g.TEMPLATES[Math.floor(Math.random() * (tplMax + 1))];
|
|
||||||
const m = g.applyTemplate(type, tpl, pos.x, pos.y);
|
|
||||||
m.id = world.nextId++;
|
m.id = world.nextId++;
|
||||||
m.dlaMs = Math.round(monsterDlaMs(world.config));
|
m.dlaMs = Math.round(monsterDlaMs(world.config));
|
||||||
m.nextDla = now + m.dlaMs;
|
m.nextDla = now + m.dlaMs;
|
||||||
|
|||||||
+25
-24
@@ -238,29 +238,38 @@ function makeWorld(over = {}) {
|
|||||||
assert(!st.includes(r.troll.passHash) && !st.includes(r.troll.salt), "pas de fuite du hash/sel");
|
assert(!st.includes(r.troll.passHash) && !st.includes(r.troll.salt), "pas de fuite du hash/sel");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tuning admin : bestiaire appliqué aux nouveaux spawns
|
// Tuning admin : ancienne table monsters (legacy) — validation/bornage/reset
|
||||||
{
|
{
|
||||||
const w = makeWorld({ monsterTarget: 0, itemTarget: 0, worldDepth: 1 });
|
const w = makeWorld({ monsterTarget: 0, itemTarget: 0, worldDepth: 1 });
|
||||||
const tun1 = mp.adminSetTuning(w, { monsters: { "Gobelin": { att: 9, pv: 77, armorMag: 3 } } });
|
const tun1 = mp.adminSetTuning(w, { monsters: { "Gobelin": { att: 9 } } });
|
||||||
assert.strictEqual(tun1.monsters.Gobelin.att, 9, "écart ATT visible dans le tuning courant");
|
assert.strictEqual(tun1.monsters.Gobelin.att, 9, "écart ATT visible dans le tuning courant");
|
||||||
// spawn forcé jusqu'à obtenir un Gobelin
|
|
||||||
let gob = null;
|
|
||||||
for (let i = 0; i < 300 && !gob; i++) {
|
|
||||||
const m = mp.spawnMonster(w);
|
|
||||||
if (m && m.name.includes("Gobelin")) gob = m;
|
|
||||||
}
|
|
||||||
assert(gob, "un Gobelin a fini par apparaître");
|
|
||||||
// gabarit « Jeune/normal » à depth 1 : mult 0.7 ou 1.0 → att 6 ou 9, pv 54 ou 77
|
|
||||||
assert([6, 9].includes(gob.att), "ATT tunée (×gabarit) : " + gob.att);
|
|
||||||
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)");
|
|
||||||
// bornage et noms inconnus ignorés
|
|
||||||
const tun2 = mp.adminSetTuning(w, { monsters: { "Dragon": { att: 5 }, "Gobelin": { att: 5000 } } });
|
const tun2 = mp.adminSetTuning(w, { monsters: { "Dragon": { att: 5 }, "Gobelin": { att: 5000 } } });
|
||||||
assert(!tun2.monsters.Dragon, "type inconnu ignoré");
|
assert(!tun2.monsters.Dragon, "type inconnu ignoré");
|
||||||
assert.strictEqual(tun2.monsters.Gobelin.att, 99, "ATT bornée à 99");
|
assert.strictEqual(tun2.monsters.Gobelin.att, 99, "ATT bornée à 99");
|
||||||
// retour au vanilla : reset explicite de la catégorie
|
|
||||||
const tun3 = mp.adminSetTuning(w, { reset: ["monsters"] });
|
const tun3 = mp.adminSetTuning(w, { reset: ["monsters"] });
|
||||||
assert.strictEqual(Object.keys(tun3.monsters).length, 0, "bestiaire d'origine restauré");
|
assert.strictEqual(Object.keys(tun3.monsters).length, 0, "monstres legacy d'origine restaurés");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bestiaire : les apparitions du monde partagé viennent du bestiaire
|
||||||
|
{
|
||||||
|
const best = require("../js/bestiary.js");
|
||||||
|
const db = require("../db.js");
|
||||||
|
const w = makeWorld({ monsterTarget: 0, itemTarget: 0, worldDepth: 12 });
|
||||||
|
const m = mp.spawnMonster(w);
|
||||||
|
assert(m && m.name && m.att >= 1 && m.deg >= 1 && m.pv >= 1 && typeof m.family === "string", "spawn d'un monstre du bestiaire : " + (m && m.name));
|
||||||
|
// tirage déterministe : 1 monstre, plages fixes, multiplicateurs neutres
|
||||||
|
const one = [{ name: "Cobaye", family: "Monstre", minAge: 0, maxAge: 0, levelMin: 10, levelMax: 10,
|
||||||
|
pvMin: 5000, pvMax: 5000, attMin: 7, attMax: 7, esqMin: 3, esqMax: 3, degMin: 4, degMax: 4, regMin: 1, regMax: 1,
|
||||||
|
armPhysMin: 2, armPhysMax: 2, armMagMin: 0, armMagMax: 0, vueMin: 4, vueMax: 4, mmMin: 0, mmMax: 0, rmMin: 0, rmMax: 0,
|
||||||
|
fly: 0, ranged: 0, magic: 1, seesHidden: 0, speed: "Normale", nbAtt: 1, capacities: "", blason: "" }];
|
||||||
|
const bm = g.buildBestiaryMonster(one, [1, 1, 1, 1, 1, 1, 1, 1], best.AGE_NAMES, 10, 0, 0);
|
||||||
|
assert.strictEqual(bm.pv, 5000, "PV tiré dans la plage du bestiaire");
|
||||||
|
assert.strictEqual(bm.att, 7, "ATT tirée dans la plage");
|
||||||
|
assert.strictEqual(bm.attMag, 7, "monstre magique : attMag = att");
|
||||||
|
// édition admin du bestiaire reflétée en base
|
||||||
|
const name = db.bestiaryAll()[0].name;
|
||||||
|
mp.adminSetTuning(w, { bestiary: { [name]: { pvMin: 4242, pvMax: 4242 } } });
|
||||||
|
assert.strictEqual(db.bestiaryAll().find(b => b.name === name).pvMin, 4242, "édition du bestiaire écrite en base");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tuning admin : mises à jour CIBLÉES — éditer un objet n'écrase pas les autres
|
// Tuning admin : mises à jour CIBLÉES — éditer un objet n'écrase pas les autres
|
||||||
@@ -327,17 +336,9 @@ function makeWorld(over = {}) {
|
|||||||
{
|
{
|
||||||
const w = makeWorld({ monsterTarget: 0, itemTarget: 0, worldDepth: 1 });
|
const w = makeWorld({ monsterTarget: 0, itemTarget: 0, worldDepth: 1 });
|
||||||
const tunMag = mp.adminSetTuning(w, {
|
const tunMag = mp.adminSetTuning(w, {
|
||||||
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(tunMag.monsters.Gobelin.attMag, 6, "ATT mag du Gobelin tunée");
|
|
||||||
assert.strictEqual(tunMag.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;
|
|
||||||
for (let i = 0; i < 300 && !gob; i++) {
|
|
||||||
const m = mp.spawnMonster(w);
|
|
||||||
if (m && m.name.includes("Gobelin")) gob = m;
|
|
||||||
}
|
|
||||||
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(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é");
|
||||||
|
|||||||
Reference in New Issue
Block a user