v2.8.0 : empilement des cases (monde partage) + rendu par coins + cible d'attaque
mp.js : deplacement libre (se deplacer n'attaque plus, on s'empile) ; moveMonsterMP autorise l'empilement de monstres ; drop multi-tresors ; loot de monstre empilable. mp-client.js : rendu par COINS (regroupement par case, troll haut-gauche / monstre haut-droite / tresor bas-gauche / lieu bas-droite, badge ×N) ; bouton d'attaque -> menu deroulant quand plusieurs monstres atteignables (cheby<=1, meme case incluse). CSS .mp-attack-sel. Tests mp.js mis a jour (drop empile, deplacement libre). PvP (trolls) et solo : a faire. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -234,6 +234,16 @@ non affilié au jeu original de Mountyhall SARL.
|
|||||||
|
|
||||||
## Versions
|
## Versions
|
||||||
|
|
||||||
|
- **2.8.0** (2026-06-15) — **Empilement sur les cases** dans le monde partagé.
|
||||||
|
Une case accueille maintenant **plusieurs trolls, plusieurs monstres et
|
||||||
|
plusieurs trésors** (un seul « lieu » à terme). Se déplacer ne fait **plus
|
||||||
|
attaquer** : on se pose sur la case (cohabitation), et l'**attaque** passe par
|
||||||
|
le bouton ⚔️ — avec un **menu déroulant** pour choisir la cible quand
|
||||||
|
plusieurs monstres sont sur ta case ou adjacents. Rendu **par coins** pour
|
||||||
|
rester lisible : 🧌 trolls en haut-gauche, 👹 monstres en haut-droite,
|
||||||
|
💰 trésors en bas-gauche, lieux en bas-droite, avec un « ×N » quand il y en a
|
||||||
|
plusieurs. (Le PvP — taper d'autres trolls — et le portage en solo restent à
|
||||||
|
faire.)
|
||||||
- **2.7.1** (2026-06-14) — Encyclopédie « Trésors du Hall » : nouvelle section
|
- **2.7.1** (2026-06-14) — Encyclopédie « Trésors du Hall » : nouvelle section
|
||||||
**✨ Templates** (les 19 templates de drop, lus dans la base), un **menu de
|
**✨ Templates** (les 19 templates de drop, lus dans la base), un **menu de
|
||||||
navigation** collant en haut pour sauter directement à chaque section (Potions,
|
navigation** collant en haut pour sauter directement à chaque section (Potions,
|
||||||
|
|||||||
@@ -257,6 +257,19 @@ header h1 { font-size: 1.3em; color: #FFFFCC; text-shadow: 1px 1px 2px #000; }
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
/* Sélecteur de cible d'attaque (empilement : plusieurs monstres) */
|
||||||
|
.mp-attack-sel { margin: 4px 0; }
|
||||||
|
.mp-attack-sel select {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
padding: 4px 6px;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: .85em;
|
||||||
|
background: #FFFFEE;
|
||||||
|
border: 1px solid #666633;
|
||||||
|
color: #333300;
|
||||||
|
}
|
||||||
|
.mp-attack-sel button { margin: 0 !important; }
|
||||||
#improve button:hover:not(:disabled),
|
#improve button:hover:not(:disabled),
|
||||||
#inventory button:hover:not(:disabled),
|
#inventory button:hover:not(:disabled),
|
||||||
#actions button:hover:not(:disabled),
|
#actions button:hover:not(:disabled),
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const APP_VERSION = "2.7.1";
|
const APP_VERSION = "2.8.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;
|
||||||
|
|||||||
+73
-29
@@ -213,46 +213,68 @@ function mpRenderMap(st) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const disc = (x, y, color) => {
|
// Empilement : une case peut contenir plusieurs trolls/monstres/trésors et un
|
||||||
|
// lieu. On regroupe par case et on dessine chaque TYPE dans un coin —
|
||||||
|
// troll ↖, monstre ↗, trésor ↙, lieu ↘ — avec un « ×N » si plusieurs.
|
||||||
|
const cells = {};
|
||||||
|
const cell = (x, y) => (cells[x + "," + y] = cells[x + "," + y] || { x, y, trolls: [], monsters: [], items: [], place: null, youHere: false });
|
||||||
|
for (const i of st.items) cell(i.x, i.y).items.push(i);
|
||||||
|
for (const m of st.monsters) cell(m.x, m.y).monsters.push(m);
|
||||||
|
for (const o of st.trolls) cell(o.x, o.y).trolls.push(o);
|
||||||
|
for (const pl of (st.places || [])) cell(pl.x, pl.y).place = pl;
|
||||||
|
if (!you.dead) cell(you.x, you.y).youHere = true;
|
||||||
|
|
||||||
|
const Q = MP_TILE / 4; // demi-coin
|
||||||
|
const corner = (c, qx, qy, color, emoji, count, mine) => {
|
||||||
|
const cx = c.x * MP_TILE + qx, cy = c.y * MP_TILE + qy;
|
||||||
ctx.fillStyle = color;
|
ctx.fillStyle = color;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(x * MP_TILE + MP_TILE / 2, y * MP_TILE + MP_TILE / 2, MP_TILE / 2 - 3, 0, Math.PI * 2);
|
ctx.arc(cx, cy, Q, 0, Math.PI * 2);
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
|
if (mine) { ctx.strokeStyle = "#eaffdc"; ctx.lineWidth = 1.5; ctx.stroke(); }
|
||||||
|
ctx.fillStyle = "#1a140e";
|
||||||
|
ctx.font = "11px serif";
|
||||||
|
ctx.fillText(emoji, cx, cy + 1);
|
||||||
|
if (count > 1) {
|
||||||
|
ctx.font = "bold 9px sans-serif";
|
||||||
|
ctx.fillStyle = "#ffe";
|
||||||
|
ctx.strokeStyle = "#1a140e";
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.strokeText(count, cx + Q, cy + Q);
|
||||||
|
ctx.fillText(count, cx + Q, cy + Q);
|
||||||
|
}
|
||||||
|
ctx.font = "18px serif";
|
||||||
};
|
};
|
||||||
for (const i of st.items) {
|
|
||||||
disc(i.x, i.y, i.kind === "gold" ? "#caa53d" : i.kind === "potion" || i.kind === "scroll" ? (i.color || "#5d8535") : "#7a8db0");
|
for (const c of Object.values(cells)) {
|
||||||
ctx.fillStyle = "#1a140e";
|
const nTrolls = c.trolls.length + (c.youHere ? 1 : 0);
|
||||||
ctx.fillText(i.emoji, i.x * MP_TILE + MP_TILE / 2, i.y * MP_TILE + MP_TILE / 2 + 1);
|
if (nTrolls) corner(c, Q, Q, c.youHere ? "#8fbf5a" : "#5a7abf", "🧌", nTrolls, c.youHere); // ↖
|
||||||
}
|
if (c.monsters.length) {
|
||||||
for (const m of st.monsters) {
|
corner(c, MP_TILE - Q, Q, "#8a3030", c.monsters[0].emoji, c.monsters.length); // ↗
|
||||||
disc(m.x, m.y, "#8a3030");
|
const low = c.monsters.reduce((a, b) => (b.pvPct < a.pvPct ? b : a));
|
||||||
ctx.fillStyle = "#1a140e";
|
const bw = Math.max(2, Math.round((MP_TILE - 4) * low.pvPct));
|
||||||
ctx.fillText(m.emoji, m.x * MP_TILE + MP_TILE / 2, m.y * MP_TILE + MP_TILE / 2 + 1);
|
|
||||||
const bw = Math.max(2, Math.round((MP_TILE - 6) * m.pvPct));
|
|
||||||
ctx.fillStyle = "#b03030";
|
ctx.fillStyle = "#b03030";
|
||||||
ctx.fillRect(m.x * MP_TILE + 3, m.y * MP_TILE + 1, bw, 3);
|
ctx.fillRect(c.x * MP_TILE + 2, c.y * MP_TILE + 1, bw, 2);
|
||||||
}
|
}
|
||||||
// les autres trolls : pastille bleue + nom
|
if (c.items.length) {
|
||||||
for (const o of st.trolls) {
|
const it = c.items[0];
|
||||||
disc(o.x, o.y, "#5a7abf");
|
const col = it.kind === "gold" ? "#caa53d" : (it.kind === "potion" || it.kind === "scroll") ? (it.color || "#5d8535") : "#7a8db0";
|
||||||
ctx.fillStyle = "#1a140e";
|
corner(c, Q, MP_TILE - Q, col, it.emoji, c.items.length); // ↙
|
||||||
ctx.fillText("🧌", o.x * MP_TILE + MP_TILE / 2, o.y * MP_TILE + MP_TILE / 2 + 1);
|
}
|
||||||
|
if (c.place) corner(c, MP_TILE - Q, MP_TILE - Q, "#6a5a8a", c.place.emoji, 1); // ↘
|
||||||
|
// nom d'un autre troll seul sur sa case (au-dessus) ; masqué si empilement
|
||||||
|
if (c.trolls.length === 1 && !c.youHere) {
|
||||||
ctx.fillStyle = "#dfe8ff";
|
ctx.fillStyle = "#dfe8ff";
|
||||||
ctx.font = "10px sans-serif";
|
ctx.font = "9px sans-serif";
|
||||||
ctx.fillText(o.name, o.x * MP_TILE + MP_TILE / 2, o.y * MP_TILE - 5);
|
ctx.fillText(c.trolls[0].name, c.x * MP_TILE + MP_TILE / 2, c.y * MP_TILE - 4);
|
||||||
ctx.font = "18px serif";
|
ctx.font = "18px serif";
|
||||||
}
|
}
|
||||||
// toi
|
}
|
||||||
if (!you.dead) {
|
if (!you.dead && you.camo) {
|
||||||
disc(you.x, you.y, "#8fbf5a");
|
|
||||||
ctx.fillStyle = "#1a140e";
|
|
||||||
ctx.fillText("🧌", you.x * MP_TILE + MP_TILE / 2, you.y * MP_TILE + MP_TILE / 2 + 1);
|
|
||||||
if (you.camo) {
|
|
||||||
ctx.strokeStyle = "#8fbf5a";
|
ctx.strokeStyle = "#8fbf5a";
|
||||||
ctx.strokeRect(you.x * MP_TILE + 1, you.y * MP_TILE + 1, MP_TILE - 2, MP_TILE - 2);
|
ctx.strokeRect(you.x * MP_TILE + 1, you.y * MP_TILE + 1, MP_TILE - 2, MP_TILE - 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function mpRenderPanels(st) {
|
function mpRenderPanels(st) {
|
||||||
const you = st.you;
|
const you = st.you;
|
||||||
@@ -314,8 +336,30 @@ function mpRenderPanels(st) {
|
|||||||
b.onclick = fn;
|
b.onclick = fn;
|
||||||
actions.appendChild(b);
|
actions.appendChild(b);
|
||||||
};
|
};
|
||||||
const adj = st.monsters.find(m => Math.max(Math.abs(m.x - you.x), Math.abs(m.y - you.y)) <= 1);
|
// Cibles atteignables : monstres sur ta case ou adjacents (cheby ≤ 1). S'il y
|
||||||
addBtn(`⚔️ Attaquer (${3} PA)`, () => adj && mpAction({ type: "attack", target: adj.id }), adj && you.pa >= 3);
|
// en a plusieurs (empilement), on propose un menu déroulant pour choisir.
|
||||||
|
const attackable = st.monsters.filter(m => Math.max(Math.abs(m.x - you.x), Math.abs(m.y - you.y)) <= 1);
|
||||||
|
const canAttack = attackable.length > 0 && you.pa >= 3 && !you.dead;
|
||||||
|
if (attackable.length > 1) {
|
||||||
|
const wrap = document.createElement("div");
|
||||||
|
wrap.className = "mp-attack-sel";
|
||||||
|
const sel = document.createElement("select");
|
||||||
|
for (const m of attackable) {
|
||||||
|
const opt = document.createElement("option");
|
||||||
|
opt.textContent = `${m.emoji} ${m.name} (niv. ${m.level} · ${Math.round(m.pvPct * 100)} % PV)`;
|
||||||
|
sel.appendChild(opt);
|
||||||
|
}
|
||||||
|
const b = document.createElement("button");
|
||||||
|
b.textContent = "⚔️ Attaquer (3 PA)";
|
||||||
|
b.disabled = !canAttack;
|
||||||
|
b.onclick = () => { const m = attackable[sel.selectedIndex]; if (m) mpAction({ type: "attack", target: m.id }); };
|
||||||
|
wrap.appendChild(sel);
|
||||||
|
wrap.appendChild(b);
|
||||||
|
actions.appendChild(wrap);
|
||||||
|
} else {
|
||||||
|
const target = attackable[0];
|
||||||
|
addBtn("⚔️ Attaquer (3 PA)", () => target && mpAction({ type: "attack", target: target.id }), canAttack);
|
||||||
|
}
|
||||||
const comp = RACES[you.race].comp, sort = RACES[you.race].sort;
|
const comp = RACES[you.race].comp, sort = RACES[you.race].sort;
|
||||||
addBtn(`🥋 ${comp.name} (${comp.cost} PA · ${you.comp.pct} %)`, () => mpAction({ type: "comp" }), you.pa >= comp.cost);
|
addBtn(`🥋 ${comp.name} (${comp.cost} PA · ${you.comp.pct} %)`, () => mpAction({ type: "comp" }), you.pa >= comp.cost);
|
||||||
addBtn(`🔮 ${sort.name} (${sort.cost} PA · ${you.sort.pct} %)`, () => mpAction({ type: "sort" }), you.pa >= sort.cost);
|
addBtn(`🔮 ${sort.name} (${sort.cost} PA · ${you.sort.pct} %)`, () => mpAction({ type: "sort" }), you.pa >= sort.cost);
|
||||||
|
|||||||
@@ -396,8 +396,9 @@ function moveMonsterMP(world, m, nx, ny) {
|
|||||||
const { mapW, mapH } = world.config;
|
const { mapW, mapH } = world.config;
|
||||||
if (nx < 0 || ny < 0 || nx >= mapW || ny >= mapH) return false;
|
if (nx < 0 || ny < 0 || nx >= mapW || ny >= mapH) return false;
|
||||||
if (world.grid[ny][nx] === g.T_WALL) return false;
|
if (world.grid[ny][nx] === g.T_WALL) return false;
|
||||||
|
// Empilement autorisé : les monstres peuvent partager une case (mais ne se
|
||||||
|
// déplacent pas sur un troll — ils l'attaquent quand il est adjacent).
|
||||||
if (Object.values(world.trolls).some(t => !t.dead && t.x === nx && t.y === ny)) return false;
|
if (Object.values(world.trolls).some(t => !t.dead && t.x === nx && t.y === ny)) return false;
|
||||||
if (world.monsters.some(o => o !== m && o.x === nx && o.y === ny)) return false;
|
|
||||||
m.x = nx; m.y = ny;
|
m.x = nx; m.y = ny;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -451,7 +452,7 @@ function killMonsterMP(world, t, m) {
|
|||||||
t.kills++;
|
t.kills++;
|
||||||
privLog(t, `💀 ${m.name} est terrassé !${px > 0 ? ` +${px} PX.` : " Pas de PX (adversaire trop faible)."}`, "good");
|
privLog(t, `💀 ${m.name} est terrassé !${px > 0 ? ` +${px} PX.` : " Pas de PX (adversaire trop faible)."}`, "good");
|
||||||
worldLog(world, `⚔️ ${t.name} a terrassé ${m.emoji} ${m.name} !`, "combat");
|
worldLog(world, `⚔️ ${t.name} a terrassé ${m.emoji} ${m.name} !`, "combat");
|
||||||
if (Math.random() < 0.4 && !world.items.some(i => i.x === m.x && i.y === m.y)) {
|
if (Math.random() < 0.4) {
|
||||||
const lr = Math.random();
|
const lr = Math.random();
|
||||||
const drop = lr < 0.4 ? { ...tunedRandomPotion(), x: m.x, y: m.y }
|
const drop = lr < 0.4 ? { ...tunedRandomPotion(), x: m.x, y: m.y }
|
||||||
: lr < 0.65 ? { ...tunedRandomScroll(), x: m.x, y: m.y }
|
: lr < 0.65 ? { ...tunedRandomScroll(), x: m.x, y: m.y }
|
||||||
@@ -668,10 +669,9 @@ function action(world, t, act) {
|
|||||||
const nx = t.x + dx, ny = t.y + dy;
|
const nx = t.x + dx, ny = t.y + dy;
|
||||||
if (nx < 0 || ny < 0 || nx >= mapW || ny >= mapH) return { error: "hors du monde" };
|
if (nx < 0 || ny < 0 || nx >= mapW || ny >= mapH) return { error: "hors du monde" };
|
||||||
if (world.grid[ny][nx] === g.T_WALL) return { error: "un mur" };
|
if (world.grid[ny][nx] === g.T_WALL) return { error: "un mur" };
|
||||||
const m = world.monsters.find(m => m.x === nx && m.y === ny);
|
// Empilement autorisé : une case accueille plusieurs trolls/monstres/trésors.
|
||||||
if (m) return actAttack(world, t, m);
|
// Se déplacer ne fait plus attaquer — l'attaque passe par l'action « attack »
|
||||||
if (Object.values(world.trolls).some(o => o !== t && !o.dead && o.x === nx && o.y === ny))
|
// (avec choix de la cible si plusieurs sur la case ou adjacentes).
|
||||||
return { error: "un troll occupe cette case" };
|
|
||||||
if (!spendPA(t, g.COSTS.move)) return { error: "pas assez de PA" };
|
if (!spendPA(t, g.COSTS.move)) return { error: "pas assez de PA" };
|
||||||
t.x = nx; t.y = ny;
|
t.x = nx; t.y = ny;
|
||||||
if (t.camo) {
|
if (t.camo) {
|
||||||
@@ -767,7 +767,7 @@ function action(world, t, act) {
|
|||||||
if (act.type === "drop") {
|
if (act.type === "drop") {
|
||||||
const item = t.bag[act.idx];
|
const item = t.bag[act.idx];
|
||||||
if (!item) return { error: "objet introuvable dans le sac" };
|
if (!item) return { error: "objet introuvable dans le sac" };
|
||||||
if (world.items.some(i => i.x === t.x && i.y === t.y)) return { error: "il y a déjà quelque chose à terre ici" };
|
// Empilement autorisé : plusieurs trésors peuvent cohabiter sur une case.
|
||||||
if (!spendPA(t, g.COSTS.drop)) return { error: "pas assez de PA" };
|
if (!spendPA(t, g.COSTS.drop)) return { error: "pas assez de PA" };
|
||||||
t.bag.splice(act.idx, 1);
|
t.bag.splice(act.idx, 1);
|
||||||
world.items.push({ ...item, x: t.x, y: t.y });
|
world.items.push({ ...item, x: t.x, y: t.y });
|
||||||
|
|||||||
+26
-3
@@ -376,15 +376,17 @@ function makeWorld(over = {}) {
|
|||||||
assert.strictEqual(t.bag.length, 0, "sac vidé");
|
assert.strictEqual(t.bag.length, 0, "sac vidé");
|
||||||
const ground = w.items.find(i => i.x === t.x && i.y === t.y);
|
const ground = w.items.find(i => i.x === t.x && i.y === t.y);
|
||||||
assert(ground && ground.name === "Armure de bois", "l'objet est à terre");
|
assert(ground && ground.name === "Armure de bois", "l'objet est à terre");
|
||||||
// case occupée : on ne peut pas jeter par-dessus
|
// empilement : on peut désormais jeter plusieurs trésors sur la même case
|
||||||
t.bag.push(gearLib.gearItemByName("arme", "Torche"));
|
t.bag.push(gearLib.gearItemByName("arme", "Torche"));
|
||||||
assert(mp.action(w, t, { type: "drop", idx: 0 }).error, "case occupée refusée");
|
assert(mp.action(w, t, { type: "drop", idx: 0 }).ok, "second jet sur la même case accepté (empilement)");
|
||||||
// on peut le re-ramasser
|
assert.strictEqual(w.items.filter(i => i.x === t.x && i.y === t.y).length, 2, "deux trésors empilés");
|
||||||
|
// on peut re-ramasser (un objet à la fois)
|
||||||
r = mp.action(w, t, { type: "pickup" });
|
r = mp.action(w, t, { type: "pickup" });
|
||||||
assert(r.ok && t.bag.some(i => i.name === "Armure de bois"), "objet re-ramassé");
|
assert(r.ok && t.bag.some(i => i.name === "Armure de bois"), "objet re-ramassé");
|
||||||
// goinfrer : objet détruit, un des trois effets appliqué
|
// goinfrer : objet détruit, un des trois effets appliqué
|
||||||
t.pa = 6;
|
t.pa = 6;
|
||||||
t.pv = 1; // pour voir un éventuel soin MIAM
|
t.pv = 1; // pour voir un éventuel soin MIAM
|
||||||
|
t.bag.push(gearLib.gearItemByName("arme", "Torche"));
|
||||||
const before = t.bag.length;
|
const before = t.bag.length;
|
||||||
const idxTorche = t.bag.findIndex(i => i.name === "Torche");
|
const idxTorche = t.bag.findIndex(i => i.name === "Torche");
|
||||||
r = mp.action(w, t, { type: "eat", idx: idxTorche });
|
r = mp.action(w, t, { type: "eat", idx: idxTorche });
|
||||||
@@ -399,6 +401,27 @@ function makeWorld(over = {}) {
|
|||||||
assert(mp.action(w, t, { type: "eat", idx: t.bag.length - 1 }).error, "potion non goinfrable");
|
assert(mp.action(w, t, { type: "eat", idx: t.bag.length - 1 }).error, "potion non goinfrable");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Empilement : se déplacer sur la case d'un monstre l'empile (ne l'attaque plus) ;
|
||||||
|
// l'attaque explicite reste possible (même case, cheby 0)
|
||||||
|
{
|
||||||
|
const w = makeWorld({ monsterTarget: 0, itemTarget: 0 });
|
||||||
|
const { troll: t } = mp.newTroll(w, "Empileur", "Skrim");
|
||||||
|
t.pa = 6;
|
||||||
|
const dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]];
|
||||||
|
const dir = dirs.find(([dx, dy]) => w.grid[t.y + dy] && w.grid[t.y + dy][t.x + dx] !== undefined && w.grid[t.y + dy][t.x + dx] !== g.T_WALL);
|
||||||
|
assert(dir, "le troll a un voisin praticable");
|
||||||
|
const m = mp.spawnMonster(w);
|
||||||
|
m.x = t.x + dir[0]; m.y = t.y + dir[1];
|
||||||
|
const hpBefore = m.pv;
|
||||||
|
const r = mp.action(w, t, { type: "move", dx: dir[0], dy: dir[1] });
|
||||||
|
assert(r.ok, "déplacement sur la case du monstre accepté");
|
||||||
|
assert(t.x === m.x && t.y === m.y, "troll et monstre empilés sur la même case");
|
||||||
|
assert.strictEqual(m.pv, hpBefore, "se déplacer n'attaque pas le monstre");
|
||||||
|
t.pa = 6;
|
||||||
|
const ra = mp.action(w, t, { type: "attack", target: m.id });
|
||||||
|
assert(ra.ok, "attaque explicite possible sur la même case");
|
||||||
|
}
|
||||||
|
|
||||||
// Base de référence : les retouches survivent au redémarrage (seed non destructif)
|
// Base de référence : les retouches survivent au redémarrage (seed non destructif)
|
||||||
{
|
{
|
||||||
const os = require("os");
|
const os = require("os");
|
||||||
|
|||||||
Reference in New Issue
Block a user