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
|
||||
|
||||
- **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
|
||||
section « 🐲 Bestiaire » dans « Trésors du Hall » (accessible par le menu) qui
|
||||
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/scrolls.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/editor.js?v=__V__"></script>
|
||||
<script src="js/mp-client.js?v=__V__"></script>
|
||||
|
||||
+44
-2
@@ -4,7 +4,7 @@
|
||||
|
||||
"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. */
|
||||
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) {
|
||||
// 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 type = pool[Math.floor(Math.random() * pool.length)];
|
||||
const tplMax = Math.min(TEMPLATES.length - 1, depth - 1);
|
||||
@@ -1702,7 +1744,7 @@ if (typeof document !== "undefined") {
|
||||
if (typeof module !== "undefined" && module.exports) {
|
||||
module.exports = {
|
||||
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,
|
||||
MAP_W, MAP_H, T_WALL, T_FLOOR, T_STAIRS,
|
||||
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 sc = require("./js/scrolls.js");
|
||||
const gearLib = require("./js/gear.js");
|
||||
const bestiaryLib = require("./js/bestiary.js");
|
||||
const db = require("./db.js");
|
||||
|
||||
/* ---------- Configuration par défaut (tout est réglable via l'admin) ---------- */
|
||||
@@ -45,7 +46,7 @@ const DEFAULT_CONFIG = {
|
||||
};
|
||||
|
||||
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],
|
||||
trollDlaSec: [5, 3600], pollSec: [1, 60],
|
||||
monsterTarget: [0, 120], repopSec: [10, 3600],
|
||||
@@ -158,16 +159,12 @@ function monsterDlaMs(config) {
|
||||
function spawnMonster(world, now = Date.now()) {
|
||||
const pos = randomFloor(world);
|
||||
if (!pos) return null;
|
||||
// même logique que makeMonster (pool par profondeur + gabarit d'âge),
|
||||
// mais sur les types tunés par l'admin
|
||||
// Bestiaire : on tire un monstre dont le niveau tombe dans la tranche de la
|
||||
// profondeur, avec un âge au hasard. Données de la base (tunées admin) +
|
||||
// multiplicateurs d'âge.
|
||||
const depth = world.config.worldDepth;
|
||||
const types = tunedMonsterTypes().filter(t => !t.boss);
|
||||
const pool = types.filter(t => t.level <= depth + 1 && t.level >= Math.max(1, depth - 2));
|
||||
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);
|
||||
const m = g.buildBestiaryMonster(db.bestiaryAll(), db.monsterAges().map(a => a.mult), bestiaryLib.AGE_NAMES, depth, pos.x, pos.y);
|
||||
if (!m) return null;
|
||||
m.id = world.nextId++;
|
||||
m.dlaMs = Math.round(monsterDlaMs(world.config));
|
||||
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");
|
||||
}
|
||||
|
||||
// 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 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");
|
||||
// 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 } } });
|
||||
assert(!tun2.monsters.Dragon, "type inconnu ignoré");
|
||||
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"] });
|
||||
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
|
||||
@@ -327,17 +336,9 @@ function makeWorld(over = {}) {
|
||||
{
|
||||
const w = makeWorld({ monsterTarget: 0, itemTarget: 0, worldDepth: 1 });
|
||||
const tunMag = mp.adminSetTuning(w, {
|
||||
monsters: { "Gobelin": { attMag: 6, degMag: 4 } },
|
||||
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");
|
||||
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 baton = mp.applyGearTuning(gearLib.gearItemByName("arme", "Bâton de mage"));
|
||||
assert.strictEqual(baton.mods.attMag, 4, "ATT mag du bâton droppé");
|
||||
|
||||
Reference in New Issue
Block a user