v2.12.0 : editeur pose les monstres du bestiaire (mob+age) + retrait du Behemoth

game.js : rollBestiaryMonster(b, age, ..) extrait (build d'un monstre
precis) ; monsterFromSpec gere {mob:nom, age} (bestiaire) + retrocompat
{type,tpl} et boss:true ; bestiaryBoss() = le plus fort du bestiaire ;
buildLevel MAX_DEPTH -> bestiaryBoss (plus de Behemoth statique) ; messages
'gardien du Hall' ; applyLevel filtre les specs nulles. index.html : selects
famille/monstre/age dans l'editeur. editor.js : edSetupMobPickers/edFillMobs/
edFillAges, pinceau 'Poser le monstre choisi', placement {mob,age}, rendu
emoji de famille (retrocompat type/boss). server.js validateLevel accepte
{mob,age}. Behemoth (BOSS) garde comme repli si bestiaire absent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
perco
2026-06-17 11:21:45 +02:00
co-authored by Claude Opus 4.8
parent e2e749fa41
commit dc3685de11
5 changed files with 103 additions and 41 deletions
+48 -22
View File
@@ -4,7 +4,7 @@
"use strict";
const APP_VERSION = "2.11.2";
const APP_VERSION = "2.12.0";
/* Alpha : maîtrise initiale haute pour les tests. Remettre 15 % / 15 % à la v1.0 officielle. */
const START_COMP_PCT = 90;
@@ -218,17 +218,10 @@ const FAMILY_EMOJI_G = { Insecte: "🐛", Animal: "🐾", "Démon": "👿", Huma
* 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, ageNamesF, 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));
/* Construit un monstre concret depuis une ligne de bestiaire `b` et un âge donné
* (le multiplicateur d'âge est appliqué, chaque stat tirée dans sa plage). */
function rollBestiaryMonster(b, age, ageMults, ageNames, ageNamesF, x, y) {
age = Math.max(b.minAge, Math.min(b.maxAge, age | 0));
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));
@@ -248,6 +241,20 @@ function buildBestiaryMonster(list, ageMults, ageNames, ageNamesF, depth, x, y)
};
}
function buildBestiaryMonster(list, ageMults, ageNames, ageNamesF, 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));
return rollBestiaryMonster(b, age, ageMults, ageNames, ageNamesF, x, y);
}
function makeMonster(depth, x, y) {
// Bestiaire (données statiques chargées dans le navigateur en solo)
if (typeof BESTIARY !== "undefined" && BESTIARY.length)
@@ -260,12 +267,31 @@ function makeMonster(depth, x, y) {
return applyTemplate(type, tpl, x, y);
}
/* Instancie un monstre depuis une spec d'éditeur : {x, y, type, tpl} ou {x, y, boss: true} */
/* Le monstre le plus puissant du bestiaire (à son âge le plus vieux) — sert de
* « gardien » pour le dernier niveau de la partie aléatoire. */
function bestiaryBoss(x, y) {
if (typeof BESTIARY === "undefined" || !BESTIARY.length) return { ...BOSS, pvMax: BOSS.pv, x, y, boss: true, static: false };
const b = BESTIARY.reduce((a, c) => (c.levelMax > a.levelMax ? c : a));
const m = rollBestiaryMonster(b, b.maxAge, AGE_MULT, AGE_NAMES, AGE_NAMES_F, x, y);
m.boss = true;
return m;
}
/* Instancie un monstre depuis une spec d'éditeur. Nouveau format :
* {x, y, mob: "Nom", age: N} → tiré du bestiaire. Rétrocompat : {x, y, type, tpl}
* (anciens niveaux) et {x, y, boss: true} (ancien Béhémoth → plus fort du bestiaire). */
function monsterFromSpec(spec) {
if (spec.boss) return { ...BOSS, pvMax: BOSS.pv, x: spec.x, y: spec.y, boss: true, static: false };
const type = MONSTER_TYPES[spec.type % MONSTER_TYPES.length];
const tpl = TEMPLATES[spec.tpl % TEMPLATES.length];
return applyTemplate(type, tpl, spec.x, spec.y);
if (spec.mob && typeof BESTIARY !== "undefined") {
const b = BESTIARY.find(m => m.name === spec.mob);
if (b) return rollBestiaryMonster(b, spec.age != null ? spec.age : b.minAge, AGE_MULT, AGE_NAMES, AGE_NAMES_F, spec.x, spec.y);
}
if (spec.boss) return bestiaryBoss(spec.x, spec.y);
if (Number.isInteger(spec.type)) {
const type = MONSTER_TYPES[spec.type % MONSTER_TYPES.length];
const tpl = TEMPLATES[(spec.tpl || 0) % TEMPLATES.length];
return applyTemplate(type, tpl, spec.x, spec.y);
}
return null;
}
/* Instancie un objet depuis une spec d'éditeur : {x, y, kind, slot?, idx?, gold?}.
@@ -425,7 +451,7 @@ function buildCustomLevel(level) {
G.grid = grid;
G.troll.x = level.start.x; G.troll.y = level.start.y;
G.seen = new Set();
G.monsters = level.monsters.map(monsterFromSpec);
G.monsters = level.monsters.map(monsterFromSpec).filter(Boolean);
G.items = level.items.map(itemFromSpec);
G.doors = (level.doors || []).map(d => ({ ...d }));
G.stairs = null;
@@ -470,7 +496,7 @@ function buildLevel() {
G.monsters = [];
if (G.depth === MAX_DEPTH) {
const bp = randomFloor(grid, taken);
G.monsters.push({ ...BOSS, pvMax: BOSS.pv, x: bp.x, y: bp.y, static: false });
G.monsters.push(bestiaryBoss(bp.x, bp.y)); // gardien : le plus fort du bestiaire
}
const count = 4 + G.depth * 2 - (G.depth === MAX_DEPTH ? 3 : 0);
for (let i = 0; i < count; i++) {
@@ -1192,7 +1218,7 @@ function descend() {
}
G.depth++;
log(`⬇️ Tu descends. Profondeur ${G.depth}. L'air devient lourd…`, "info");
if (G.depth === MAX_DEPTH) log("👹 Le sol tremble. Le Béhémoth est proche.", "bad");
if (G.depth === MAX_DEPTH) log("👹 Le sol tremble. Le gardien du Hall est proche.", "bad");
buildLevel();
afterAction();
}
@@ -1309,7 +1335,7 @@ function die(killer) {
function win() {
G.over = true;
if (G.custom) log("🏆 Tous les monstres sont terrassés ! Niveau vaincu !", "good");
else log("🏆 Le Béhémoth s'effondre ! Le Trésor de MountyHall est à toi !", "good");
else log("🏆 Le gardien du Hall s'effondre ! Le Trésor de MountyHall est à toi !", "good");
showEnd(true);
}
@@ -1744,7 +1770,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, buildBestiaryMonster, monsterFromSpec, itemFromSpec, equipGear, unequipToBag,
RACES, MONSTER_TYPES, BOSS, TEMPLATES, applyTemplate, makeMonster, buildBestiaryMonster, rollBestiaryMonster, bestiaryBoss, 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,