/** * sotc-clash-arrow | DieInteraction.js * * Gère la logique de drag entre dés de vitesse. * Reçoit les événements depuis SIOBridge (mousedown/move/up natifs sur #board). * * Coordonnées : * - Les hooks sotcca:* reçoivent des coords ÉCRAN (relatives à #board) * - ClashArrowLayer.drawPreview() et ArrowStore travaillent en coords MONDE * - getSIODieCenter() retourne des coords monde * - _screenToWorld() convertit écran → monde */ import { ArrowStore } from "./ArrowStore.js"; import { getSIODieCenter, findDieUnderScreenPoint, tokenHasActiveSkillOnDie, getAttackWeightForDie, getDieState } from "./SIOBridge.js"; import { ClashArrowLayer } from "./ClashArrowLayer.js"; // ── Couleurs ────────────────────────────────────────────────────────────────── const COLOR_BLUE = 0x4a9eff; // attaque directe const COLOR_YELLOW = 0xffd84a; // clash mutuel potentiel const COLOR_PREVIEW = 0x888899; // drag en cours, cible indéfinie // ── État du drag ────────────────────────────────────────────────────────────── let _dragging = false; let _srcToken = null; // TokenDocument source let _srcDieIdx = null; // index du dé source let _srcWorldX = 0; // coords monde du centre du dé source let _srcWorldY = 0; // ── Init ────────────────────────────────────────────────────────────────────── export function initDieInteraction() { Hooks.on("sotcca:diePointerDown", _onDieDown); Hooks.on("sotcca:boardMouseMove", _onMove); Hooks.on("sotcca:boardMouseUp", _onUp); Hooks.on("sotcca:dieRightClick", _onRightClick); } // ── Handlers ────────────────────────────────────────────────────────────────── function _onDieDown(tokenDoc, dieIndex, screenX, screenY) { // Bloquer si le dé source n'a pas de compétence assignée if (!tokenHasActiveSkillOnDie(tokenDoc, dieIndex)) { ui.notifications.warn(game.i18n.localize("SOTCCA.NoSkillOnDie")); return; } _dragging = true; _srcToken = tokenDoc; _srcDieIdx = dieIndex; const world = getSIODieCenter(tokenDoc, dieIndex); _srcWorldX = world.x; _srcWorldY = world.y; } function _onMove(screenX, screenY) { if (!_dragging) return; const worldPos = _screenToWorld(screenX, screenY); const hit = findDieUnderScreenPoint(screenX, screenY); const color = _previewColor(hit?.tokenDoc, hit?.dieIndex); ClashArrowLayer.drawPreview(_srcWorldX, _srcWorldY, worldPos.x, worldPos.y, color); } function _onUp(screenX, screenY) { if (!_dragging) return; _dragging = false; ClashArrowLayer.clearPreview(); const hit = findDieUnderScreenPoint(screenX, screenY); if (!hit) return; const { tokenDoc: tgtToken, dieIndex: tgtDie } = hit; if (tgtToken.id === _srcToken.id) { ui.notifications.warn(game.i18n.localize("SOTCCA.CannotTargetSelf")); return; } // Vérifier la limite d'attack weight // En état Saved Defense Dice (2) : toujours limité à 1 flèche const isSavedDefense = getDieState(_srcToken, _srcDieIdx) === 2; const maxArrows = isSavedDefense ? 1 : getAttackWeightForDie(_srcToken, _srcDieIdx); const existingArrows = ArrowStore.all().filter( a => a.fromToken === _srcToken.id && a.fromDie === _srcDieIdx ); if (existingArrows.length >= maxArrows) { ui.notifications.warn( game.i18n.format("SOTCCA.AttackWeightExceeded", { max: maxArrows }) ); return; } const src = getSIODieCenter(_srcToken, _srcDieIdx); const tgt = getSIODieCenter(tgtToken, tgtDie); ArrowStore.addArrow({ fromToken: _srcToken.id, fromDie : _srcDieIdx, toToken : tgtToken.id, toDie : tgtDie, x1: src.x, y1: src.y, x2: tgt.x, y2: tgt.y, }); } function _onRightClick(_tokenDoc, _dieIndex) { // Suppression par clic droit désactivée. // Les flèches sont supprimées uniquement via le changement de dieState (Used/Unused). } // ── Helpers ─────────────────────────────────────────────────────────────────── /** * Détermine la couleur de la flèche preview en fonction de la cible survolée. */ function _previewColor(targetTokenDoc, targetDieIdx) { if (!targetTokenDoc || targetTokenDoc.id === _srcToken?.id) return COLOR_PREVIEW; if (tokenHasActiveSkillOnDie(targetTokenDoc, targetDieIdx)) return COLOR_YELLOW; return COLOR_BLUE; } /** * Convertit des coordonnées écran (#board) en coordonnées monde (canvas). * @param {number} sx * @param {number} sy * @returns {{ x: number, y: number }} */ function _screenToWorld(sx, sy) { return canvas.stage.toLocal({ x: sx, y: sy }); }