1.1.0 : éditeur de niveaux et partage communautaire
Éditeur visuel (terrain, monstres avec gabarits, objets, départ, sortie), test en un clic, publication via une API node sans dépendance (server.js, validation serveur, stockage JSON sur volume), écran « Niveaux de la communauté » et liens partageables ?level=<id>. Le conteneur passe de nginx à node:22-alpine. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+357
@@ -0,0 +1,357 @@
|
||||
/* Éditeur de niveaux MountyCrawl — partage les globales de game.js (chargé avant). */
|
||||
|
||||
"use strict";
|
||||
|
||||
const ED = {
|
||||
grid: null, // tableau 2D de caractères : '#' mur, '.' sol, '>' sortie
|
||||
start: null, // {x, y}
|
||||
monsters: [], // specs {x, y, type, tpl} ou {x, y, boss: true}
|
||||
items: [], // specs {x, y, kind, idx?, gold?}
|
||||
brush: { mode: "tile", char: "#" },
|
||||
painting: false,
|
||||
};
|
||||
|
||||
function edReset() {
|
||||
ED.grid = [];
|
||||
for (let y = 0; y < MAP_H; y++) {
|
||||
ED.grid.push([]);
|
||||
for (let x = 0; x < MAP_W; x++) {
|
||||
const border = x === 0 || y === 0 || x === MAP_W - 1 || y === MAP_H - 1;
|
||||
ED.grid[y].push(border ? "#" : ".");
|
||||
}
|
||||
}
|
||||
ED.start = null;
|
||||
ED.monsters = [];
|
||||
ED.items = [];
|
||||
}
|
||||
|
||||
/* ---------- Palette ---------- */
|
||||
|
||||
function edBuildPalette() {
|
||||
const tiles = [
|
||||
{ label: "🟫 Mur", mode: "tile", char: "#" },
|
||||
{ label: "⬜ Sol", mode: "tile", char: "." },
|
||||
{ label: "▼ Sortie", mode: "tile", char: ">" },
|
||||
{ label: "🧌 Départ du Trõll", mode: "start" },
|
||||
{ label: "🧽 Gomme (entités)", mode: "erase" },
|
||||
];
|
||||
const tilesDiv = document.getElementById("ed-tiles");
|
||||
tilesDiv.innerHTML = "";
|
||||
for (const t of tiles) edAddBrushBtn(tilesDiv, t.label, t);
|
||||
|
||||
const tplSel = document.getElementById("ed-tpl");
|
||||
tplSel.innerHTML = "";
|
||||
TEMPLATES.forEach((tpl, i) => {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = i;
|
||||
opt.textContent = (tpl.prefix.trim() || "Normal") + ` (×${tpl.mult})`;
|
||||
if (i === 1) opt.selected = true;
|
||||
tplSel.appendChild(opt);
|
||||
});
|
||||
|
||||
const monstersDiv = document.getElementById("ed-monsters");
|
||||
monstersDiv.innerHTML = "";
|
||||
MONSTER_TYPES.forEach((m, i) => {
|
||||
edAddBrushBtn(monstersDiv, `${m.emoji} ${m.name} (niv. ${m.level})`, { mode: "monster", type: i });
|
||||
});
|
||||
edAddBrushBtn(monstersDiv, `${BOSS.emoji} ${BOSS.name} (boss)`, { mode: "monster", boss: true });
|
||||
|
||||
const itemsDiv = document.getElementById("ed-items");
|
||||
itemsDiv.innerHTML = "";
|
||||
edAddBrushBtn(itemsDiv, "🧪 Potion de Vie", { mode: "item", kind: "potion" });
|
||||
edAddBrushBtn(itemsDiv, "💰 Mountyzédons", { mode: "item", kind: "gold" });
|
||||
WEAPONS.forEach((w, i) => edAddBrushBtn(itemsDiv, `${w.emoji} ${w.name} (+${w.bonus})`, { mode: "item", kind: "weapon", idx: i }));
|
||||
ARMORS.forEach((a, i) => edAddBrushBtn(itemsDiv, `${a.emoji} ${a.name} (+${a.bonus})`, { mode: "item", kind: "armor", idx: i }));
|
||||
|
||||
const toolsDiv = document.getElementById("ed-tools");
|
||||
toolsDiv.innerHTML = "";
|
||||
const fill = (char, label) => {
|
||||
const b = document.createElement("button");
|
||||
b.textContent = label;
|
||||
b.onclick = () => {
|
||||
for (let y = 1; y < MAP_H - 1; y++)
|
||||
for (let x = 1; x < MAP_W - 1; x++) ED.grid[y][x] = char;
|
||||
if (char === "#") { ED.start = null; ED.monsters = []; ED.items = []; }
|
||||
edRender();
|
||||
};
|
||||
toolsDiv.appendChild(b);
|
||||
};
|
||||
fill(".", "⬜ Tout remplir de sol");
|
||||
fill("#", "🟫 Tout remplir de mur (vide les entités)");
|
||||
const caves = document.createElement("button");
|
||||
caves.textContent = "🎲 Caverne aléatoire";
|
||||
caves.onclick = () => {
|
||||
const cavern = generateCavern(MAP_W, MAP_H);
|
||||
for (let y = 0; y < MAP_H; y++)
|
||||
for (let x = 0; x < MAP_W; x++)
|
||||
ED.grid[y][x] = cavern[y][x] === T_WALL ? "#" : ".";
|
||||
edPruneEntities();
|
||||
edRender();
|
||||
};
|
||||
toolsDiv.appendChild(caves);
|
||||
}
|
||||
|
||||
function edAddBrushBtn(parent, label, brush) {
|
||||
const b = document.createElement("button");
|
||||
b.textContent = label;
|
||||
b.className = "ed-brush";
|
||||
b.onclick = () => {
|
||||
ED.brush = brush;
|
||||
parent.parentElement.querySelectorAll(".ed-brush").forEach(x => x.classList.remove("selected"));
|
||||
b.classList.add("selected");
|
||||
};
|
||||
parent.appendChild(b);
|
||||
}
|
||||
|
||||
/* Supprime les entités qui se retrouvent dans un mur après modification du terrain. */
|
||||
function edPruneEntities() {
|
||||
const onFloor = e => ED.grid[e.y][e.x] !== "#";
|
||||
ED.monsters = ED.monsters.filter(onFloor);
|
||||
ED.items = ED.items.filter(onFloor);
|
||||
if (ED.start && !onFloor(ED.start)) ED.start = null;
|
||||
}
|
||||
|
||||
/* ---------- Application du pinceau ---------- */
|
||||
|
||||
function edApply(x, y) {
|
||||
if (x < 0 || y < 0 || x >= MAP_W || y >= MAP_H) return;
|
||||
const b = ED.brush;
|
||||
if (b.mode === "tile") {
|
||||
// bordure inviolable pour garder le troll dans la carte
|
||||
const border = x === 0 || y === 0 || x === MAP_W - 1 || y === MAP_H - 1;
|
||||
if (border && b.char !== "#") return;
|
||||
ED.grid[y][x] = b.char;
|
||||
if (b.char === "#") {
|
||||
ED.monsters = ED.monsters.filter(m => m.x !== x || m.y !== y);
|
||||
ED.items = ED.items.filter(i => i.x !== x || i.y !== y);
|
||||
if (ED.start && ED.start.x === x && ED.start.y === y) ED.start = null;
|
||||
}
|
||||
} else if (b.mode === "start") {
|
||||
if (ED.grid[y][x] === "#") return;
|
||||
ED.start = { x, y };
|
||||
} else if (b.mode === "erase") {
|
||||
ED.monsters = ED.monsters.filter(m => m.x !== x || m.y !== y);
|
||||
ED.items = ED.items.filter(i => i.x !== x || i.y !== y);
|
||||
if (ED.start && ED.start.x === x && ED.start.y === y) ED.start = null;
|
||||
} else if (b.mode === "monster") {
|
||||
if (ED.grid[y][x] === "#") return;
|
||||
ED.monsters = ED.monsters.filter(m => m.x !== x || m.y !== y);
|
||||
if (b.boss) ED.monsters.push({ x, y, boss: true });
|
||||
else ED.monsters.push({ x, y, type: b.type, tpl: Number(document.getElementById("ed-tpl").value) });
|
||||
} else if (b.mode === "item") {
|
||||
if (ED.grid[y][x] === "#") return;
|
||||
ED.items = ED.items.filter(i => i.x !== x || i.y !== y);
|
||||
const spec = { x, y, kind: b.kind };
|
||||
if (b.idx !== undefined) spec.idx = b.idx;
|
||||
if (b.kind === "gold") spec.gold = 60;
|
||||
ED.items.push(spec);
|
||||
}
|
||||
edRender();
|
||||
}
|
||||
|
||||
/* ---------- Rendu ---------- */
|
||||
|
||||
function edRender() {
|
||||
const canvas = document.getElementById("ed-map");
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.font = "18px serif";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
|
||||
for (let y = 0; y < MAP_H; y++) {
|
||||
for (let x = 0; x < MAP_W; x++) {
|
||||
const c = ED.grid[y][x];
|
||||
ctx.fillStyle = c === "#" ? "#4a3a22" : "#7a6a45";
|
||||
ctx.fillRect(x * TILE, y * TILE, TILE - 1, TILE - 1);
|
||||
if (c === ">") {
|
||||
ctx.fillStyle = "#111";
|
||||
ctx.fillText("▼", x * TILE + TILE / 2, y * TILE + TILE / 2 + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const disc = (x, y, color) => {
|
||||
ctx.fillStyle = color;
|
||||
ctx.beginPath();
|
||||
ctx.arc(x * TILE + TILE / 2, y * TILE + TILE / 2, TILE / 2 - 3, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
};
|
||||
for (const i of ED.items) {
|
||||
disc(i.x, i.y, i.kind === "gold" ? "#caa53d" : i.kind === "potion" ? "#5d8535" : "#7a8db0");
|
||||
ctx.fillStyle = "#1a140e";
|
||||
const emoji = i.kind === "potion" ? "🧪" : i.kind === "gold" ? "💰"
|
||||
: i.kind === "weapon" ? WEAPONS[i.idx || 0].emoji : ARMORS[i.idx || 0].emoji;
|
||||
ctx.fillText(emoji, i.x * TILE + TILE / 2, i.y * TILE + TILE / 2 + 1);
|
||||
}
|
||||
for (const m of ED.monsters) {
|
||||
disc(m.x, m.y, m.boss ? "#7a2070" : "#8a3030");
|
||||
ctx.fillStyle = "#1a140e";
|
||||
const emoji = m.boss ? BOSS.emoji : MONSTER_TYPES[m.type].emoji;
|
||||
ctx.fillText(emoji, m.x * TILE + TILE / 2, m.y * TILE + TILE / 2 + 1);
|
||||
}
|
||||
if (ED.start) {
|
||||
disc(ED.start.x, ED.start.y, "#8fbf5a");
|
||||
ctx.fillStyle = "#1a140e";
|
||||
ctx.fillText("🧌", ED.start.x * TILE + TILE / 2, ED.start.y * TILE + TILE / 2 + 1);
|
||||
}
|
||||
|
||||
const status = document.getElementById("ed-status");
|
||||
const missing = [];
|
||||
if (!ED.start) missing.push("un point de départ 🧌");
|
||||
if (ED.monsters.length === 0) missing.push("au moins un monstre");
|
||||
status.textContent = missing.length
|
||||
? "Il manque : " + missing.join(" et ") + "."
|
||||
: `Prêt : ${ED.monsters.length} monstre(s), ${ED.items.length} objet(s)${ED.grid.some(r => r.includes(">")) ? ", une sortie" : ""}.`;
|
||||
status.className = missing.length ? "ed-warn" : "ed-ok";
|
||||
}
|
||||
|
||||
/* ---------- Export / test / publication ---------- */
|
||||
|
||||
function edToLevel() {
|
||||
return {
|
||||
name: document.getElementById("ed-name").value.trim(),
|
||||
author: document.getElementById("ed-author").value.trim(),
|
||||
grid: ED.grid.map(row => row.join("")),
|
||||
start: ED.start,
|
||||
monsters: ED.monsters,
|
||||
items: ED.items,
|
||||
};
|
||||
}
|
||||
|
||||
function edValidate(level) {
|
||||
if (!level.start) return "place un point de départ 🧌";
|
||||
if (level.monsters.length === 0) return "place au moins un monstre";
|
||||
return null;
|
||||
}
|
||||
|
||||
function edTest() {
|
||||
const level = edToLevel();
|
||||
const err = edValidate(level);
|
||||
if (err) { edFlash(err, true); return; }
|
||||
if (!level.name) level.name = "Test sans nom";
|
||||
if (!level.author) level.author = "moi";
|
||||
window.MC_afterEnd = "editor";
|
||||
document.getElementById("screen-editor").classList.add("hidden");
|
||||
startGame(level);
|
||||
}
|
||||
|
||||
async function edPublish() {
|
||||
const level = edToLevel();
|
||||
const err = edValidate(level) ||
|
||||
(!level.name ? "donne un nom à ton niveau" : null) ||
|
||||
(!level.author ? "indique ton pseudo d'auteur" : null);
|
||||
if (err) { edFlash(err, true); return; }
|
||||
try {
|
||||
const res = await fetch("api/levels", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(level),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) { edFlash("Refusé par le serveur : " + data.error, true); return; }
|
||||
edFlash(`🌍 Publié ! « ${level.name} » est maintenant jouable par tous.`, false);
|
||||
} catch {
|
||||
edFlash("Impossible de joindre le serveur.", true);
|
||||
}
|
||||
}
|
||||
|
||||
function edFlash(msg, isError) {
|
||||
const status = document.getElementById("ed-status");
|
||||
status.textContent = msg;
|
||||
status.className = isError ? "ed-warn" : "ed-ok";
|
||||
}
|
||||
|
||||
/* ---------- Niveaux communautaires ---------- */
|
||||
|
||||
async function communityShow() {
|
||||
document.getElementById("screen-create").classList.add("hidden");
|
||||
document.getElementById("screen-community").classList.remove("hidden");
|
||||
const list = document.getElementById("level-list");
|
||||
list.innerHTML = '<p class="lore">Chargement…</p>';
|
||||
try {
|
||||
const res = await fetch("api/levels");
|
||||
const levels = await res.json();
|
||||
list.innerHTML = "";
|
||||
if (levels.length === 0) {
|
||||
list.innerHTML = '<p class="lore">Aucun niveau publié pour l\'instant. Sois le premier, l\'éditeur t\'attend !</p>';
|
||||
return;
|
||||
}
|
||||
for (const l of levels.slice().reverse()) {
|
||||
const row = document.createElement("button");
|
||||
row.className = "level-row";
|
||||
row.textContent = `⚔️ ${l.name} — par ${l.author} · ${l.monsters} monstre(s) · joué ${l.plays} fois · ${l.date}`;
|
||||
row.onclick = async () => {
|
||||
const r = await fetch("api/levels/" + l.id);
|
||||
if (!r.ok) return;
|
||||
const level = await r.json();
|
||||
document.getElementById("screen-community").classList.add("hidden");
|
||||
window.MC_afterEnd = null;
|
||||
startGame(level);
|
||||
};
|
||||
list.appendChild(row);
|
||||
}
|
||||
} catch {
|
||||
list.innerHTML = '<p class="lore">Impossible de joindre le serveur (le mode communautaire nécessite le site en ligne).</p>';
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Branchement ---------- */
|
||||
|
||||
if (typeof document !== "undefined") {
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
edReset();
|
||||
edBuildPalette();
|
||||
|
||||
document.getElementById("btn-editor").onclick = () => {
|
||||
document.getElementById("screen-create").classList.add("hidden");
|
||||
document.getElementById("screen-editor").classList.remove("hidden");
|
||||
edRender();
|
||||
};
|
||||
document.getElementById("ed-back").onclick = () => {
|
||||
document.getElementById("screen-editor").classList.add("hidden");
|
||||
document.getElementById("screen-create").classList.remove("hidden");
|
||||
};
|
||||
document.getElementById("btn-community").onclick = communityShow;
|
||||
document.getElementById("btn-comm-back").onclick = () => {
|
||||
document.getElementById("screen-community").classList.add("hidden");
|
||||
document.getElementById("screen-create").classList.remove("hidden");
|
||||
};
|
||||
document.getElementById("ed-test").onclick = edTest;
|
||||
document.getElementById("ed-publish").onclick = edPublish;
|
||||
|
||||
const canvas = document.getElementById("ed-map");
|
||||
const cellOf = e => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
return {
|
||||
x: Math.floor((e.clientX - rect.left) * (canvas.width / rect.width) / TILE),
|
||||
y: Math.floor((e.clientY - rect.top) * (canvas.height / rect.height) / TILE),
|
||||
};
|
||||
};
|
||||
canvas.addEventListener("mousedown", e => {
|
||||
ED.painting = true;
|
||||
const { x, y } = cellOf(e);
|
||||
edApply(x, y);
|
||||
});
|
||||
canvas.addEventListener("mousemove", e => {
|
||||
if (!ED.painting || ED.brush.mode !== "tile") return;
|
||||
const { x, y } = cellOf(e);
|
||||
edApply(x, y);
|
||||
});
|
||||
document.addEventListener("mouseup", () => { ED.painting = false; });
|
||||
|
||||
// ?screen=editor | community : accès direct (captures d'écran, raccourcis)
|
||||
const params = new URLSearchParams(location.search);
|
||||
const screen = params.get("screen");
|
||||
if (screen === "editor") document.getElementById("btn-editor").onclick();
|
||||
else if (screen === "community") communityShow();
|
||||
|
||||
// ?level=<id> : lien partageable vers un niveau communautaire
|
||||
const levelId = params.get("level");
|
||||
if (levelId && /^[a-f0-9]{12}$/.test(levelId)) {
|
||||
fetch("api/levels/" + levelId)
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(level => { if (level) startGame(level); });
|
||||
}
|
||||
});
|
||||
}
|
||||
+106
-25
@@ -113,11 +113,7 @@ const TEMPLATES = [
|
||||
{ prefix: "Mythique ", mult: 2.0 },
|
||||
];
|
||||
|
||||
function makeMonster(depth, x, y) {
|
||||
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);
|
||||
const tpl = TEMPLATES[Math.floor(Math.random() * (tplMax + 1))];
|
||||
function applyTemplate(type, tpl, x, y) {
|
||||
return {
|
||||
name: tpl.prefix + type.name, emoji: type.emoji,
|
||||
level: Math.max(1, Math.round(type.level * tpl.mult)),
|
||||
@@ -130,6 +126,34 @@ function makeMonster(depth, x, y) {
|
||||
};
|
||||
}
|
||||
|
||||
function makeMonster(depth, x, y) {
|
||||
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);
|
||||
const tpl = TEMPLATES[Math.floor(Math.random() * (tplMax + 1))];
|
||||
return applyTemplate(type, tpl, x, y);
|
||||
}
|
||||
|
||||
/* Instancie un monstre depuis une spec d'éditeur : {x, y, type, tpl} ou {x, y, boss: true} */
|
||||
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);
|
||||
}
|
||||
|
||||
/* Instancie un objet depuis une spec d'éditeur : {x, y, kind, idx?, gold?} */
|
||||
function itemFromSpec(spec) {
|
||||
const base = { x: spec.x, y: spec.y };
|
||||
if (spec.kind === "potion") return { ...base, kind: "potion", name: "Potion de Vie", emoji: "🧪" };
|
||||
if (spec.kind === "gold") {
|
||||
const gold = spec.gold || 60;
|
||||
return { ...base, kind: "gold", name: `${gold} Mountyzédons`, emoji: "💰", gold };
|
||||
}
|
||||
const list = spec.kind === "weapon" ? WEAPONS : ARMORS;
|
||||
return { ...base, kind: "gear", ...list[(spec.idx || 0) % list.length] };
|
||||
}
|
||||
|
||||
/* ================= Génération du Monde Souterrain ================= */
|
||||
|
||||
const MAP_W = 28, MAP_H = 20;
|
||||
@@ -237,9 +261,10 @@ const COSTS = { move: 1, attack: 3, pickup: 1, equip: 2, potion: 1 };
|
||||
|
||||
let G = null; // état global de la partie
|
||||
|
||||
function newGame(name, race) {
|
||||
function newGame(name, race, customLevel = null) {
|
||||
const s = RACES[race].stats;
|
||||
G = {
|
||||
custom: customLevel,
|
||||
troll: {
|
||||
name, race,
|
||||
att: s.att, esq: s.esq, deg: s.deg, reg: s.reg,
|
||||
@@ -252,8 +277,33 @@ function newGame(name, race) {
|
||||
depth: 1, grid: null, monsters: [], items: [], stairs: null,
|
||||
seen: new Set(), over: false,
|
||||
};
|
||||
buildLevel();
|
||||
log(`${name} le ${race} pénètre dans le Monde Souterrain. Que les Dieux Trõlls te gardent !`, "good");
|
||||
if (customLevel) buildCustomLevel(customLevel);
|
||||
else buildLevel();
|
||||
if (customLevel) {
|
||||
log(`${name} le ${race} entre dans « ${customLevel.name} », un niveau de ${customLevel.author}.`, "good");
|
||||
log("Objectif : terrasser tous les monstres, ou atteindre la sortie ▼ s'il y en a une.", "info");
|
||||
} else {
|
||||
log(`${name} le ${race} pénètre dans le Monde Souterrain. Que les Dieux Trõlls te gardent !`, "good");
|
||||
}
|
||||
}
|
||||
|
||||
/* Construit un niveau venu de l'éditeur : grid = tableau de chaînes '#' mur, '.' sol, '>' sortie. */
|
||||
function buildCustomLevel(level) {
|
||||
const grid = [];
|
||||
for (let y = 0; y < MAP_H; y++) {
|
||||
grid.push([]);
|
||||
for (let x = 0; x < MAP_W; x++) {
|
||||
const c = level.grid[y][x];
|
||||
grid[y].push(c === "#" ? T_WALL : c === ">" ? T_STAIRS : T_FLOOR);
|
||||
}
|
||||
}
|
||||
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.items = level.items.map(itemFromSpec);
|
||||
G.stairs = null;
|
||||
updateFov();
|
||||
}
|
||||
|
||||
function buildLevel() {
|
||||
@@ -333,7 +383,8 @@ function tryMove(dx, dy) {
|
||||
updateFov();
|
||||
|
||||
if (G.grid[ny][nx] === T_STAIRS) {
|
||||
log("Un passage s'enfonce vers les profondeurs… (bouton « Descendre »)", "info");
|
||||
if (G.custom) log("La sortie ! (bouton « Sortir »)", "info");
|
||||
else log("Un passage s'enfonce vers les profondeurs… (bouton « Descendre »)", "info");
|
||||
}
|
||||
const item = G.items.find(i => i.x === nx && i.y === ny);
|
||||
if (item) log(`Tu vois : ${item.emoji} ${item.name}. Ramasse-le pour ${COSTS.pickup} PA.`, "info");
|
||||
@@ -371,7 +422,11 @@ function killMonster(m) {
|
||||
G.troll.kills++;
|
||||
log(`💀 ${m.name} est terrassé ! +${px} PX (convertis en PI à l'entraînement).`, "good");
|
||||
G.monsters = G.monsters.filter(x => x !== m);
|
||||
if (m.boss) win();
|
||||
if (G.custom) {
|
||||
if (G.monsters.length === 0) win();
|
||||
} else if (m.boss) {
|
||||
win();
|
||||
}
|
||||
}
|
||||
|
||||
function useAbility() {
|
||||
@@ -463,6 +518,12 @@ function useBagItem(idx) {
|
||||
|
||||
function descend() {
|
||||
if (G.grid[G.troll.y][G.troll.x] !== T_STAIRS) return;
|
||||
if (G.custom) {
|
||||
G.over = true;
|
||||
log("🚪 Tu atteins la sortie, sain et sauf !", "good");
|
||||
showEnd(true);
|
||||
return;
|
||||
}
|
||||
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");
|
||||
@@ -554,7 +615,8 @@ function die(killer) {
|
||||
|
||||
function win() {
|
||||
G.over = true;
|
||||
log("🏆 Le Béhémoth s'effondre ! Le Trésor de MountyHall est à toi !", "good");
|
||||
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");
|
||||
showEnd(true);
|
||||
}
|
||||
|
||||
@@ -644,7 +706,9 @@ function render() {
|
||||
|
||||
function renderPanels() {
|
||||
const t = G.troll;
|
||||
$("depth-label").textContent = `Profondeur −${G.depth} · DLA n°${t.dla}`;
|
||||
$("depth-label").textContent = G.custom
|
||||
? `« ${G.custom.name} » par ${G.custom.author} · DLA n°${t.dla} · ${G.monsters.length} monstre(s) restant(s)`
|
||||
: `Profondeur −${G.depth} · DLA n°${t.dla}`;
|
||||
$("troll-title").textContent = `${RACES[t.race].emoji} ${t.name}, ${t.race} niv. ${levelFromTotalPI(t.totalPI)}`;
|
||||
|
||||
const pct = Math.max(0, t.pv / t.pvMax);
|
||||
@@ -694,7 +758,7 @@ function renderPanels() {
|
||||
const onItem = G.items.some(i => i.x === t.x && i.y === t.y);
|
||||
addBtn(`🖐️ Ramasser (${COSTS.pickup} PA)`, pickup, onItem && t.pa >= COSTS.pickup);
|
||||
const onStairs = G.grid[t.y][t.x] === T_STAIRS;
|
||||
addBtn("⬇️ Descendre", descend, onStairs);
|
||||
addBtn(G.custom ? "🚪 Sortir" : "⬇️ Descendre", descend, onStairs);
|
||||
addBtn("⏳ Passer la DLA", passDLA, true);
|
||||
|
||||
$("equipment").innerHTML =
|
||||
@@ -714,18 +778,29 @@ function renderPanels() {
|
||||
});
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
return String(s).replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
||||
}
|
||||
|
||||
function showEnd(victory, killer) {
|
||||
const t = G.troll;
|
||||
$("screen-game").classList.add("hidden");
|
||||
$("screen-end").classList.remove("hidden");
|
||||
$("end-title").textContent = victory ? "🏆 GLOIRE AU TRÕLL !" : "☠️ MORT DANS LES PROFONDEURS";
|
||||
const score = t.gold + t.totalPI * 10 + t.kills * 25 + (victory ? 1000 : 0);
|
||||
$("end-text").innerHTML = victory
|
||||
? `${t.name} le ${t.race} a terrassé le Béhémoth et rapporte le Trésor de MountyHall à la Taverne !<br><br>
|
||||
Monstres tués : ${t.kills} · Mountyzédons : ${t.gold} · DLA écoulées : ${t.dla}<br><b>Score : ${score}</b>`
|
||||
: `${t.name} le ${t.race} a été terrassé par ${killer ? killer.name : "les profondeurs"} à la profondeur −${G.depth}.<br>
|
||||
À MountyHall on ne meurt jamais vraiment : les Dieux Trõlls te ramèneront à la Taverne.<br><br>
|
||||
Monstres tués : ${t.kills} · Mountyzédons : ${t.gold} · DLA écoulées : ${t.dla}<br><b>Score : ${score}</b>`;
|
||||
const stats = `Monstres tués : ${t.kills} · Mountyzédons : ${t.gold} · DLA écoulées : ${t.dla}<br><b>Score : ${score}</b>`;
|
||||
let text;
|
||||
if (G.custom) {
|
||||
text = victory
|
||||
? `${esc(t.name)} le ${t.race} a vaincu « ${esc(G.custom.name)} », le niveau de ${esc(G.custom.author)} !`
|
||||
: `${esc(t.name)} le ${t.race} a été terrassé par ${esc(killer ? killer.name : "les profondeurs")} dans « ${esc(G.custom.name)} » (niveau de ${esc(G.custom.author)}).`;
|
||||
} else {
|
||||
text = victory
|
||||
? `${esc(t.name)} le ${t.race} a terrassé le Béhémoth et rapporte le Trésor de MountyHall à la Taverne !`
|
||||
: `${esc(t.name)} le ${t.race} a été terrassé par ${esc(killer ? killer.name : "les profondeurs")} à la profondeur −${G.depth}.<br>
|
||||
À MountyHall on ne meurt jamais vraiment : les Dieux Trõlls te ramèneront à la Taverne.`;
|
||||
}
|
||||
$("end-text").innerHTML = text + "<br><br>" + stats;
|
||||
}
|
||||
|
||||
/* ================= Création du personnage ================= */
|
||||
@@ -750,13 +825,13 @@ function initCreateScreen() {
|
||||
}
|
||||
}
|
||||
|
||||
function startGame() {
|
||||
function startGame(customLevel = null) {
|
||||
const name = $("troll-name").value.trim() || "Trõllinet";
|
||||
$("screen-create").classList.add("hidden");
|
||||
$("screen-end").classList.add("hidden");
|
||||
$("screen-game").classList.remove("hidden");
|
||||
$("log").innerHTML = "";
|
||||
newGame(name, selectedRace);
|
||||
newGame(name, selectedRace, customLevel);
|
||||
render();
|
||||
renderPanels();
|
||||
}
|
||||
@@ -781,7 +856,7 @@ if (typeof document !== "undefined") {
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
initCreateScreen();
|
||||
bindKeys();
|
||||
$("btn-start").onclick = startGame;
|
||||
$("btn-start").onclick = () => startGame();
|
||||
// ?autostart=1&race=Durakuir&name=Grosbill : lance directement une partie
|
||||
const params = new URLSearchParams(location.search);
|
||||
if (params.get("autostart")) {
|
||||
@@ -791,8 +866,13 @@ if (typeof document !== "undefined") {
|
||||
}
|
||||
$("btn-restart").onclick = () => {
|
||||
$("screen-end").classList.add("hidden");
|
||||
$("screen-create").classList.remove("hidden");
|
||||
initCreateScreen();
|
||||
if (window.MC_afterEnd === "editor") {
|
||||
window.MC_afterEnd = null;
|
||||
$("screen-editor").classList.remove("hidden");
|
||||
} else {
|
||||
$("screen-create").classList.remove("hidden");
|
||||
initCreateScreen();
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -801,7 +881,8 @@ if (typeof document !== "undefined") {
|
||||
if (typeof module !== "undefined" && module.exports) {
|
||||
module.exports = {
|
||||
rollDice, resolveAttack, resolveSpell, improveCost, levelFromTotalPI,
|
||||
RACES, MONSTER_TYPES, BOSS, makeMonster, generateCavern, largestRegion,
|
||||
RACES, MONSTER_TYPES, BOSS, TEMPLATES, makeMonster, monsterFromSpec, itemFromSpec,
|
||||
generateCavern, largestRegion,
|
||||
MAP_W, MAP_H, T_WALL, T_FLOOR, T_STAIRS,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user