v2.9.0 : cases x2, portee d'attaque = meme case (sauf Projectile Magique), PvP

mp-client.js : MP_TILE 24->48 + polices de coins proportionnelles. mp.js :
action attack ne porte que sur la meme case (cheby 0) et dispatch
monstre/troll ; actAttackTroll (PvP : combat, +1 PX/coup, kill -> killTrollMP,
pas de butin) ; comp/sort portee 0 (meme case) sauf sort Tomawak (Projectile
Magique, portee = Vue) ; stateFor envoie l'id des trolls. Client : cibles =
monstres + trolls sur ta case, deroulant pour choisir. Tests : combat meme
case, deplacement libre, PvP.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
perco
2026-06-15 15:45:53 +02:00
co-authored by Claude Opus 4.8
parent ce65435efb
commit a0547bf9a3
5 changed files with 89 additions and 28 deletions
+34 -5
View File
@@ -479,6 +479,25 @@ function actAttack(world, t, m) {
return { ok: true };
}
/* Attaque d'un troll sur un autre (PvP, 3 PA) — uniquement sur la même case. */
function actAttackTroll(world, t, v, now = Date.now()) {
if (!spendPA(t, g.COSTS.attack)) return { error: "pas assez de PA" };
const r = g.resolveAttack(effTrollMP(t), effTrollMP(v));
if (t.camo) { t.camo = false; privLog(t, "Ton attaque brise le camouflage.", "info"); }
if (r.hit) {
v.pv -= r.damage;
gainPXMP(t, 1);
const armTxt = r.armorReduction ? ` (armure ${r.armorReduction})` : "";
privLog(t, `Tu attaques ${v.name} : ${r.attRoll} vs ${r.esqRoll}${r.critical ? "CRITIQUE ! " : ""}${r.damage} dégâts${armTxt}.`, "combat");
privLog(v, `⚔️ ${t.name} t'attaque : ${r.attRoll} vs ${r.esqRoll}${r.damage} dégâts${armTxt} !`, "bad");
if (v.pv <= 0) { t.kills++; killTrollMP(world, v, t.name, now); }
} else {
privLog(t, `Tu attaques ${v.name} : ${r.attRoll} vs ${r.esqRoll} → esquivé !`, "combat");
privLog(v, `⚔️ ${t.name} t'attaque… et tu esquives !`, "combat");
}
return { ok: true };
}
/* Talent (compétence/sortilège) : jet de maîtrise mutualisé avec le solo. */
function tryTalentMP(t, talent, cap, cost) {
if (!spendPA(t, cost)) return { error: "pas assez de PA" };
@@ -685,17 +704,27 @@ function action(world, t, act) {
}
if (act.type === "attack") {
// On ne peut frapper (monstre OU troll) que sur SA PROPRE case (même case).
const m = monsterById(world, act.target);
if (!m || chebyshev(m, t) > 1) return { error: "aucun monstre adjacent à attaquer" };
return actAttack(world, t, m);
if (m) {
if (chebyshev(m, t) !== 0) return { error: "il faut être sur la case du monstre" };
return actAttack(world, t, m);
}
const v = world.trolls[act.target];
if (v && v !== t && !v.dead) {
if (chebyshev(v, t) !== 0) return { error: "il faut être sur la case du troll" };
return actAttackTroll(world, t, v);
}
return { error: "aucune cible sur ta case" };
}
if (act.type === "comp" || act.type === "sort") {
let m = act.target != null ? monsterById(world, act.target) : null;
if (!m) {
// cible implicite : adjacent (ou plus proche visible pour le Projectile)
// Cible implicite : un monstre sur TA case ; exception, le Projectile
// Magique (sortilège du Tomawak) frappe à distance (jusqu'à la Vue).
const te = effTrollMP(t);
const range = act.type === "sort" && t.race === "Tomawak" ? te.vue : 1;
const range = act.type === "sort" && t.race === "Tomawak" ? te.vue : 0;
let bestDist = Infinity;
for (const cand of world.monsters) {
const d = chebyshev(cand, t);
@@ -846,7 +875,7 @@ function stateFor(world, t, now = Date.now()) {
},
trolls: Object.values(world.trolls)
.filter(o => o !== t && !o.dead && !o.camo && seen(o))
.map(o => ({ name: o.name, race: o.race, x: o.x, y: o.y, level: g.levelFromTotalPI(o.totalPI), pvPct: Math.max(0, o.pv / o.pvMax) })),
.map(o => ({ id: o.id, name: o.name, race: o.race, x: o.x, y: o.y, level: g.levelFromTotalPI(o.totalPI), pvPct: Math.max(0, o.pv / o.pvMax) })),
monsters: world.monsters.filter(seen)
.map(m => ({ id: m.id, name: m.name, emoji: m.emoji, x: m.x, y: m.y, level: m.level, pvPct: Math.max(0, m.pv / m.pvMax), nextDlaIn: Math.max(0, m.nextDla - now) })),
items: world.items.filter(seen)