fix(combat): blessures appliquées au seul acteur du token ciblé
Résout par uuid avant l'id d'acteur : pour un token non lié (PNJ mineur), token.actor.id expose encore l'id de l'acteur monde dont il est issu, et résoudre par id blessait alors tous les tokens de la fiche. Le uuid d'un acteur synthétique est résolu en extrayant directement la scène et le token, car fromUuid échoue sur les documents non indexés quand la résolution est asynchrone.
This commit is contained in:
@@ -61,3 +61,16 @@ make build / watch / lint # Makefile wrappers for above
|
||||
- **No tests**: `npm test` is a stub (exits 1). No test framework installed.
|
||||
- **No CI**: `.github/` is gitignored; no workflows configured.
|
||||
- **HotReload**: `system.json` flags hotReload for css, scss, hbs, json extensions.
|
||||
|
||||
|
||||
## Constraints and remarks
|
||||
|
||||
Foundryv14 only : foundryvtt.com/api/
|
||||
|
||||
## Tests
|
||||
|
||||
With chrome-devtools on port 9222
|
||||
|
||||
World is called "Vermine", thru https://localhost:31000, with Gamemaster user logged in (no password needed)
|
||||
|
||||
Always try to re-use existing chrome-devtools session.
|
||||
|
||||
@@ -119,9 +119,10 @@ export default class CombatDialog extends HandlebarsApplicationMixin(foundry.app
|
||||
const creatureLocked = !isCreature && creatureTargets.length && creatureTargets.length === targets.length;
|
||||
if (creatureLocked) {
|
||||
const ref = creatureTargets[0];
|
||||
const creature = ref.id ? game.actors.get(ref.id) : null;
|
||||
const actor = creature ?? (ref.uuid ? await fromUuid(ref.uuid) : null);
|
||||
const d = parseInt(actor?.system?.combatStatus?.difficulty, 10) || 7;
|
||||
// uuid d'abord : un token non lié ne doit pas résoudre vers l'acteur
|
||||
// monde dont il est issu (même principe que VermineExchange).
|
||||
const creature = await VermineExchange.resolveDefender({ id: ref.id, uuid: ref.uuid });
|
||||
const d = parseInt(creature?.system?.combatStatus?.difficulty, 10) || 7;
|
||||
lockedDifficulty = d;
|
||||
difficultyOptions = [{ difficulty: d, label: "", locked: true }];
|
||||
defaultDifficulty = d;
|
||||
|
||||
+52
-23
@@ -258,17 +258,19 @@ export class VermineExchange {
|
||||
* Résout l'échange pour une cible donnée (défense la plus récente liée
|
||||
* à cette cible, ou abstention si aucune défense).
|
||||
* @param {ChatMessage} attackMessage
|
||||
* @param {string|null} targetId id d'acteur de la cible (défaut : première cible)
|
||||
* @param {Object|string|null} targetRef cible de l'attaque (ou son uuid),
|
||||
* défaut : première cible — le matching se fait par uuid d'abord car des
|
||||
* PNJ mineurs (tokens non liés) issus de la même fiche partagent le même id
|
||||
* @returns {Promise<Object|null>} résultat de la résolution
|
||||
*/
|
||||
static async resolve(attackMessage, targetId = null) {
|
||||
static async resolve(attackMessage, targetRef = null) {
|
||||
const exchange = attackMessage.getFlag('world', FLAG_EXCHANGE)
|
||||
if (!exchange) return null
|
||||
|
||||
const target = (targetId && exchange.targets?.find(t => t.id === targetId)) || exchange.targets?.[0] || null
|
||||
const target = this.#matchTarget(exchange.targets || [], targetRef)
|
||||
if (!target) return null
|
||||
|
||||
const defs = this.#defenseMessagesFor(exchange.id, target.id)
|
||||
const defs = this.#defenseMessagesFor(exchange.id, target)
|
||||
const defenderMessage = defs[defs.length - 1]
|
||||
const defense = defenderMessage ? defenderMessage.getFlag('world', FLAG_DEFENSE) : null
|
||||
|
||||
@@ -281,7 +283,7 @@ export class VermineExchange {
|
||||
const defenderRef = defense
|
||||
? { id: defense.actorId ?? null, uuid: defense.actorUuid ?? null, name: defense.actorName ?? '' }
|
||||
: target
|
||||
const defender = await this.#resolveDefender(defenderRef)
|
||||
const defender = await this.resolveDefender(defenderRef)
|
||||
|
||||
const damage = (exchange.baseDamage || 0) + attackerSuccesses
|
||||
const parryLevel = defense?.type === 'parade' ? (defense.parryLevel ?? 0) : null
|
||||
@@ -384,7 +386,7 @@ export class VermineExchange {
|
||||
const exchange = attackMessage.getFlag('world', FLAG_EXCHANGE)
|
||||
if (!exchange) return
|
||||
const type = btn.dataset.type
|
||||
const actor = await this.#resolveDefender({ id: btn.dataset.targetId, uuid: btn.dataset.targetUuid })
|
||||
const actor = await this.resolveDefender({ id: btn.dataset.targetId, uuid: btn.dataset.targetUuid })
|
||||
if (!actor) {
|
||||
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_actor_selected'))
|
||||
return
|
||||
@@ -474,7 +476,7 @@ export class VermineExchange {
|
||||
|
||||
const targets = exchange.targets?.length ? exchange.targets : [null]
|
||||
for (const target of targets) {
|
||||
const result = await this.resolve(attackMessage, target?.id ?? null)
|
||||
const result = await this.resolve(attackMessage, target)
|
||||
if (!result) continue
|
||||
const block = document.createElement('div')
|
||||
block.innerHTML = this.renderResolution(result)
|
||||
@@ -491,7 +493,7 @@ export class VermineExchange {
|
||||
ev.preventDefault()
|
||||
ev.stopPropagation()
|
||||
const btn = ev.currentTarget
|
||||
const actor = await this.#resolveDefender({ id: btn.dataset.targetId, uuid: btn.dataset.targetUuid })
|
||||
const actor = await this.resolveDefender({ id: btn.dataset.targetId, uuid: btn.dataset.targetUuid })
|
||||
if (!actor) {
|
||||
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_actor_selected'))
|
||||
return
|
||||
@@ -541,9 +543,9 @@ export class VermineExchange {
|
||||
}
|
||||
let applied = 0
|
||||
for (const target of exchange.targets) {
|
||||
const r = await this.resolve(attackMessage, target?.id ?? null)
|
||||
const r = await this.resolve(attackMessage, target)
|
||||
if (!r?.hit || !r.woundCategory) continue
|
||||
const defender = await this.#resolveDefender({ id: r.defenderId, uuid: r.defenderUuid })
|
||||
const defender = await this.resolveDefender({ id: r.defenderId, uuid: r.defenderUuid })
|
||||
if (!defender || !this.canApplyWound(defender)) continue
|
||||
const result = await this.applyWound(defender, r.woundCategory)
|
||||
if (result && result !== false) applied += 1
|
||||
@@ -559,7 +561,7 @@ export class VermineExchange {
|
||||
static async #onApply(ev, attackMessage) {
|
||||
ev.preventDefault()
|
||||
const btn = ev.currentTarget
|
||||
const defender = await this.#resolveDefender({ id: btn.dataset.defenderId, uuid: btn.dataset.defenderUuid })
|
||||
const defender = await this.resolveDefender({ id: btn.dataset.defenderId, uuid: btn.dataset.defenderUuid })
|
||||
const wound = btn.dataset.wound
|
||||
if (!defender || !wound) return
|
||||
if (!this.canApplyWound(defender)) {
|
||||
@@ -588,31 +590,58 @@ export class VermineExchange {
|
||||
// ── Helpers internes ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Résout un acteur depuis son id d'acteur monde (acteurs liés) ou son
|
||||
* uuid (tokens non liés via fromUuid).
|
||||
* Résout l'acteur d'une cible depuis sa référence (id d'acteur monde ou
|
||||
* uuid). Le uuid est privilégié : pour un token non lié, `token.actor.id`
|
||||
* expose encore l'id de l'acteur monde dont le token est issu, et résoudre
|
||||
* par id blesserait alors tous les tokens liés à cette fiche (PNJ mineurs).
|
||||
* Le uuid d'un acteur synthétique ("Scene.x.Token.y.Actor.z") n'est pas
|
||||
* résolvable de façon fiable par fromUuid (document non indexé quand la
|
||||
* résolution est asynchrone) : on extrait donc directement le token.
|
||||
* @param {{id: string|null, uuid: string|null}} ref
|
||||
* @returns {Promise<Actor|null>}
|
||||
*/
|
||||
static async #resolveDefender({ id = null, uuid = null }) {
|
||||
static async resolveDefender({ id = null, uuid = null }) {
|
||||
if (uuid) {
|
||||
const parts = uuid.split('.')
|
||||
if (parts[0] === 'Scene' && parts[2] === 'Token') {
|
||||
const token = game.scenes.get(parts[1])?.tokens.get(parts[3])
|
||||
return token?.actor ?? null
|
||||
}
|
||||
if (parts[0] === 'Actor') {
|
||||
return game.actors.get(parts[1]) ?? null
|
||||
}
|
||||
}
|
||||
if (id) {
|
||||
const actor = game.actors.get(id)
|
||||
if (actor) return actor
|
||||
}
|
||||
if (uuid) {
|
||||
const doc = await fromUuid(uuid)
|
||||
if (!doc) return null
|
||||
if (doc.documentType === 'Actor') return doc
|
||||
if (doc.documentType === 'Token') return doc.actor ?? null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
static #defenseMessagesFor(exchangeId, targetId = null) {
|
||||
/**
|
||||
* Retrouve la cible d'un échange par uuid d'abord, puis par id. Le uuid est
|
||||
* fiable pour distinguer des PNJ mineurs (tokens non liés) issus de la même
|
||||
* fiche, qui partagent le même id d'acteur monde.
|
||||
* @param {Object[]} targets cibles du flag d'échange
|
||||
* @param {Object|string|null} ref référence de la cible (ou son uuid)
|
||||
* @returns {Object|null}
|
||||
*/
|
||||
static #matchTarget(targets, ref) {
|
||||
if (!ref) return targets[0] ?? null
|
||||
const uuid = typeof ref === 'string' ? ref : (ref.uuid ?? null)
|
||||
const id = typeof ref === 'string' ? null : (ref.id ?? null)
|
||||
if (uuid) return targets.find(t => t.uuid === uuid) ?? null
|
||||
if (id) return targets.find(t => t.id === id) ?? null
|
||||
return targets[0] ?? null
|
||||
}
|
||||
|
||||
static #defenseMessagesFor(exchangeId, target = null) {
|
||||
return game.messages.contents.filter(m => {
|
||||
const f = m.getFlag('world', FLAG_DEFENSE)
|
||||
if (!f || f.exchangeId !== exchangeId) return false
|
||||
if (targetId) return f.actorId === targetId
|
||||
return true
|
||||
if (!target) return true
|
||||
if (target.uuid && f.actorUuid === target.uuid) return true
|
||||
return f.actorId === target.id
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user