sotc-battle-show-ability : affichage des capacités en combat
This commit is contained in:
parent
84aa6b3437
commit
e35a3996ee
11 changed files with 1363 additions and 0 deletions
269
sotc-clash-arrow/scripts/ArrowStore.js
Normal file
269
sotc-clash-arrow/scripts/ArrowStore.js
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
/**
|
||||
* sotc-clash-arrow | ArrowStore.js
|
||||
*
|
||||
* Store centralisé et réactif des flèches de clash.
|
||||
* Gère les données en mémoire + persistance via socket pour la synchronisation
|
||||
* multi-clients + persistance via flags de scène (survit au F5).
|
||||
*
|
||||
* Structure d'une flèche :
|
||||
* {
|
||||
* id : string – identifiant unique "tokenId_dieIndex → tokenId_dieIndex"
|
||||
* fromToken: string – id du token attaquant
|
||||
* fromDie : number – index du dé source dans _initiativeMarkers[]
|
||||
* toToken : string – id du token cible
|
||||
* toDie : number – index du dé cible
|
||||
* x1, y1 : number – coordonnées canvas du centre du dé source
|
||||
* x2, y2 : number – coordonnées canvas du centre du dé cible
|
||||
* mutual : boolean – true si clash mutuel (flèche jaune)
|
||||
* used : boolean – true si le dé source est passé en Used (flèche marron)
|
||||
* }
|
||||
*/
|
||||
|
||||
const SOCKET_EVENT = "module.sotc-clash-arrow";
|
||||
const SCENE_FLAG = "arrows";
|
||||
|
||||
// ── EventEmitter minimal ──────────────────────────────────────────────────────
|
||||
class MiniEmitter {
|
||||
constructor() { this._handlers = {}; }
|
||||
on(evt, fn) { (this._handlers[evt] ??= []).push(fn); }
|
||||
off(evt, fn) {
|
||||
if (!fn) { delete this._handlers[evt]; return; }
|
||||
this._handlers[evt] = (this._handlers[evt] ?? []).filter(h => h !== fn);
|
||||
}
|
||||
emit(evt, ...args) { (this._handlers[evt] ?? []).forEach(h => h(...args)); }
|
||||
}
|
||||
|
||||
// ── ArrowStore ────────────────────────────────────────────────────────────────
|
||||
class _ArrowStore extends MiniEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
/** @type {Map<string, Object>} */
|
||||
this._arrows = new Map();
|
||||
}
|
||||
|
||||
// ── API publique ────────────────────────────────────────────────────────────
|
||||
|
||||
/** Retourne toutes les flèches. */
|
||||
all() { return Array.from(this._arrows.values()); }
|
||||
|
||||
/** Retourne une flèche par id. */
|
||||
get(id) { return this._arrows.get(id); }
|
||||
|
||||
/**
|
||||
* Marque une flèche comme "utilisée" (dé passé en Used).
|
||||
* La flèche reste visible mais passe en marron semi-transparent.
|
||||
*/
|
||||
markUsed(id) {
|
||||
const arrow = this._arrows.get(id);
|
||||
if (!arrow) return;
|
||||
arrow.used = true;
|
||||
this._arrows.set(id, arrow);
|
||||
this.emit("change");
|
||||
this._saveToScene();
|
||||
this._broadcast({ action: "markUsed", id });
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère l'id canonique d'une flèche.
|
||||
* L'id est directionnel : A→B ≠ B→A.
|
||||
*/
|
||||
static makeId(fromToken, fromDie, toToken, toDie) {
|
||||
return `${fromToken}:${fromDie}→${toToken}:${toDie}`;
|
||||
}
|
||||
|
||||
/** Id de la flèche inverse (pour détecter le clash mutuel). */
|
||||
static reverseId(fromToken, fromDie, toToken, toDie) {
|
||||
return `${toToken}:${toDie}→${fromToken}:${fromDie}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajoute ou met à jour une flèche.
|
||||
* Détecte automatiquement le clash mutuel et met à jour les deux flèches.
|
||||
* Broadcast socket aux autres clients.
|
||||
*/
|
||||
addArrow(data) {
|
||||
const id = _ArrowStore.makeId(data.fromToken, data.fromDie, data.toToken, data.toDie);
|
||||
const revId = _ArrowStore.reverseId(data.fromToken, data.fromDie, data.toToken, data.toDie);
|
||||
const reverse = this._arrows.get(revId);
|
||||
|
||||
const arrow = { ...data, id, mutual: !!reverse };
|
||||
this._arrows.set(id, arrow);
|
||||
|
||||
// Si une flèche inverse existe → clash mutuel des deux côtés
|
||||
if (reverse) {
|
||||
reverse.mutual = true;
|
||||
this._arrows.set(revId, reverse);
|
||||
}
|
||||
|
||||
this.emit("change");
|
||||
this._saveToScene();
|
||||
this._broadcast({ action: "add", arrow, reverseId: revId, reverseMutual: !!reverse });
|
||||
return arrow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime une flèche.
|
||||
* Si l'autre sens était en clash mutuel, on le repasse en bleu.
|
||||
*/
|
||||
removeArrow(id) {
|
||||
const arrow = this._arrows.get(id);
|
||||
if (!arrow) return;
|
||||
|
||||
this._arrows.delete(id);
|
||||
|
||||
// Dés-mutualiser la flèche inverse si elle existe encore
|
||||
const revId = _ArrowStore.reverseId(arrow.fromToken, arrow.fromDie, arrow.toToken, arrow.toDie);
|
||||
const reverse = this._arrows.get(revId);
|
||||
if (reverse) {
|
||||
reverse.mutual = false;
|
||||
this._arrows.set(revId, reverse);
|
||||
}
|
||||
|
||||
this.emit("change");
|
||||
this._saveToScene();
|
||||
this._broadcast({ action: "remove", id, reverseId: revId });
|
||||
}
|
||||
|
||||
/** Supprime toutes les flèches. */
|
||||
clear() {
|
||||
this._arrows.clear();
|
||||
this.emit("change");
|
||||
this._saveToScene();
|
||||
this._broadcast({ action: "clear" });
|
||||
}
|
||||
|
||||
// ── Persistance via flags de scène ──────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Sauvegarde l'état du store dans les flags de la scène courante.
|
||||
* Seul le GM écrit les flags (évite les conflits de permission).
|
||||
* Les coordonnées ne sont PAS sauvegardées — elles sont recalculées
|
||||
* à la restauration depuis les positions actuelles des marqueurs SIO.
|
||||
*/
|
||||
_saveToScene() {
|
||||
if (!game.user.isGM) return;
|
||||
if (!canvas.scene) return;
|
||||
|
||||
const data = this.all().map(a => ({
|
||||
id : a.id,
|
||||
fromToken : a.fromToken,
|
||||
fromDie : a.fromDie,
|
||||
toToken : a.toToken,
|
||||
toDie : a.toDie,
|
||||
mutual : a.mutual,
|
||||
used : a.used ?? false,
|
||||
}));
|
||||
|
||||
canvas.scene.setFlag("sotc-clash-arrow", SCENE_FLAG, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recharge les flèches depuis les flags de la scène courante.
|
||||
* Appelé au canvasReady depuis Hooks.js.
|
||||
* Les coordonnées sont recalculées depuis les marqueurs SIO actuels.
|
||||
* @param {Function} getDieCenter – getSIODieCenter(tokenDoc, dieIndex)
|
||||
*/
|
||||
loadFromScene(getDieCenter) {
|
||||
this._arrows.clear();
|
||||
|
||||
const data = canvas.scene?.getFlag("sotc-clash-arrow", SCENE_FLAG) ?? [];
|
||||
if (!data.length) return;
|
||||
|
||||
for (const saved of data) {
|
||||
const fromTokenDoc = canvas.scene.tokens.get(saved.fromToken);
|
||||
const toTokenDoc = canvas.scene.tokens.get(saved.toToken);
|
||||
|
||||
// Si l'un des tokens n'existe plus, on abandonne cette flèche
|
||||
if (!fromTokenDoc || !toTokenDoc) continue;
|
||||
|
||||
let x1, y1, x2, y2;
|
||||
try {
|
||||
({ x: x1, y: y1 } = getDieCenter(fromTokenDoc, saved.fromDie));
|
||||
({ x: x2, y: y2 } = getDieCenter(toTokenDoc, saved.toDie));
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
this._arrows.set(saved.id, { ...saved, x1, y1, x2, y2 });
|
||||
}
|
||||
|
||||
this.emit("change");
|
||||
}
|
||||
|
||||
// ── Synchronisation socket ──────────────────────────────────────────────────
|
||||
|
||||
/** Envoie une mise à jour à tous les autres clients. */
|
||||
_broadcast(payload) {
|
||||
game.socket.emit(SOCKET_EVENT, payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Demande au GM de broadcaster l'état actuel des flèches.
|
||||
* Utilisé par les joueurs au canvasReady pour recevoir les flèches existantes.
|
||||
*/
|
||||
requestSync() {
|
||||
game.socket.emit(SOCKET_EVENT, { action: "syncRequest" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Traite les messages reçus depuis le socket (autres clients).
|
||||
* On applique localement sans re-broadcaster.
|
||||
*/
|
||||
_applyRemote(payload) {
|
||||
switch (payload.action) {
|
||||
case "add": {
|
||||
this._arrows.set(payload.arrow.id, payload.arrow);
|
||||
if (payload.reverseMutual) {
|
||||
const rev = this._arrows.get(payload.reverseId);
|
||||
if (rev) { rev.mutual = true; this._arrows.set(payload.reverseId, rev); }
|
||||
}
|
||||
this.emit("change");
|
||||
break;
|
||||
}
|
||||
case "remove": {
|
||||
this._arrows.delete(payload.id);
|
||||
const rev = this._arrows.get(payload.reverseId);
|
||||
if (rev) { rev.mutual = false; this._arrows.set(payload.reverseId, rev); }
|
||||
this.emit("change");
|
||||
break;
|
||||
}
|
||||
case "markUsed": {
|
||||
const arrow = this._arrows.get(payload.id);
|
||||
if (arrow) { arrow.used = true; this._arrows.set(payload.id, arrow); }
|
||||
this.emit("change");
|
||||
break;
|
||||
}
|
||||
case "clear": {
|
||||
this._arrows.clear();
|
||||
this.emit("change");
|
||||
break;
|
||||
}
|
||||
// Un joueur demande la resynchronisation → seul le GM répond avec l'état complet
|
||||
case "syncRequest": {
|
||||
if (!game.user.isGM) break;
|
||||
game.socket.emit(SOCKET_EVENT, {
|
||||
action: "syncResponse",
|
||||
arrows: this.all(),
|
||||
});
|
||||
break;
|
||||
}
|
||||
// Le GM envoie l'état complet → les joueurs l'appliquent localement
|
||||
case "syncResponse": {
|
||||
if (game.user.isGM) break; // le GM n'a pas besoin de sa propre réponse
|
||||
this._arrows.clear();
|
||||
for (const arrow of payload.arrows) {
|
||||
this._arrows.set(arrow.id, arrow);
|
||||
}
|
||||
this.emit("change");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Initialise l'écoute socket. Appelé une seule fois depuis main.js. */
|
||||
initSocket() {
|
||||
game.socket.on(SOCKET_EVENT, payload => this._applyRemote(payload));
|
||||
}
|
||||
}
|
||||
|
||||
export const ArrowStore = new _ArrowStore();
|
||||
Loading…
Add table
Add a link
Reference in a new issue