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 tests**: `npm test` is a stub (exits 1). No test framework installed.
|
||||||
- **No CI**: `.github/` is gitignored; no workflows configured.
|
- **No CI**: `.github/` is gitignored; no workflows configured.
|
||||||
- **HotReload**: `system.json` flags hotReload for css, scss, hbs, json extensions.
|
- **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;
|
const creatureLocked = !isCreature && creatureTargets.length && creatureTargets.length === targets.length;
|
||||||
if (creatureLocked) {
|
if (creatureLocked) {
|
||||||
const ref = creatureTargets[0];
|
const ref = creatureTargets[0];
|
||||||
const creature = ref.id ? game.actors.get(ref.id) : null;
|
// uuid d'abord : un token non lié ne doit pas résoudre vers l'acteur
|
||||||
const actor = creature ?? (ref.uuid ? await fromUuid(ref.uuid) : null);
|
// monde dont il est issu (même principe que VermineExchange).
|
||||||
const d = parseInt(actor?.system?.combatStatus?.difficulty, 10) || 7;
|
const creature = await VermineExchange.resolveDefender({ id: ref.id, uuid: ref.uuid });
|
||||||
|
const d = parseInt(creature?.system?.combatStatus?.difficulty, 10) || 7;
|
||||||
lockedDifficulty = d;
|
lockedDifficulty = d;
|
||||||
difficultyOptions = [{ difficulty: d, label: "", locked: true }];
|
difficultyOptions = [{ difficulty: d, label: "", locked: true }];
|
||||||
defaultDifficulty = d;
|
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
|
* 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).
|
* à cette cible, ou abstention si aucune défense).
|
||||||
* @param {ChatMessage} attackMessage
|
* @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
|
* @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)
|
const exchange = attackMessage.getFlag('world', FLAG_EXCHANGE)
|
||||||
if (!exchange) return null
|
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
|
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 defenderMessage = defs[defs.length - 1]
|
||||||
const defense = defenderMessage ? defenderMessage.getFlag('world', FLAG_DEFENSE) : null
|
const defense = defenderMessage ? defenderMessage.getFlag('world', FLAG_DEFENSE) : null
|
||||||
|
|
||||||
@@ -281,7 +283,7 @@ export class VermineExchange {
|
|||||||
const defenderRef = defense
|
const defenderRef = defense
|
||||||
? { id: defense.actorId ?? null, uuid: defense.actorUuid ?? null, name: defense.actorName ?? '' }
|
? { id: defense.actorId ?? null, uuid: defense.actorUuid ?? null, name: defense.actorName ?? '' }
|
||||||
: target
|
: target
|
||||||
const defender = await this.#resolveDefender(defenderRef)
|
const defender = await this.resolveDefender(defenderRef)
|
||||||
|
|
||||||
const damage = (exchange.baseDamage || 0) + attackerSuccesses
|
const damage = (exchange.baseDamage || 0) + attackerSuccesses
|
||||||
const parryLevel = defense?.type === 'parade' ? (defense.parryLevel ?? 0) : null
|
const parryLevel = defense?.type === 'parade' ? (defense.parryLevel ?? 0) : null
|
||||||
@@ -384,7 +386,7 @@ export class VermineExchange {
|
|||||||
const exchange = attackMessage.getFlag('world', FLAG_EXCHANGE)
|
const exchange = attackMessage.getFlag('world', FLAG_EXCHANGE)
|
||||||
if (!exchange) return
|
if (!exchange) return
|
||||||
const type = btn.dataset.type
|
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) {
|
if (!actor) {
|
||||||
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_actor_selected'))
|
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_actor_selected'))
|
||||||
return
|
return
|
||||||
@@ -474,7 +476,7 @@ export class VermineExchange {
|
|||||||
|
|
||||||
const targets = exchange.targets?.length ? exchange.targets : [null]
|
const targets = exchange.targets?.length ? exchange.targets : [null]
|
||||||
for (const target of targets) {
|
for (const target of targets) {
|
||||||
const result = await this.resolve(attackMessage, target?.id ?? null)
|
const result = await this.resolve(attackMessage, target)
|
||||||
if (!result) continue
|
if (!result) continue
|
||||||
const block = document.createElement('div')
|
const block = document.createElement('div')
|
||||||
block.innerHTML = this.renderResolution(result)
|
block.innerHTML = this.renderResolution(result)
|
||||||
@@ -491,7 +493,7 @@ export class VermineExchange {
|
|||||||
ev.preventDefault()
|
ev.preventDefault()
|
||||||
ev.stopPropagation()
|
ev.stopPropagation()
|
||||||
const btn = ev.currentTarget
|
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) {
|
if (!actor) {
|
||||||
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_actor_selected'))
|
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_actor_selected'))
|
||||||
return
|
return
|
||||||
@@ -541,9 +543,9 @@ export class VermineExchange {
|
|||||||
}
|
}
|
||||||
let applied = 0
|
let applied = 0
|
||||||
for (const target of exchange.targets) {
|
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
|
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
|
if (!defender || !this.canApplyWound(defender)) continue
|
||||||
const result = await this.applyWound(defender, r.woundCategory)
|
const result = await this.applyWound(defender, r.woundCategory)
|
||||||
if (result && result !== false) applied += 1
|
if (result && result !== false) applied += 1
|
||||||
@@ -559,7 +561,7 @@ export class VermineExchange {
|
|||||||
static async #onApply(ev, attackMessage) {
|
static async #onApply(ev, attackMessage) {
|
||||||
ev.preventDefault()
|
ev.preventDefault()
|
||||||
const btn = ev.currentTarget
|
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
|
const wound = btn.dataset.wound
|
||||||
if (!defender || !wound) return
|
if (!defender || !wound) return
|
||||||
if (!this.canApplyWound(defender)) {
|
if (!this.canApplyWound(defender)) {
|
||||||
@@ -588,31 +590,58 @@ export class VermineExchange {
|
|||||||
// ── Helpers internes ────────────────────────────────────────────────
|
// ── Helpers internes ────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Résout un acteur depuis son id d'acteur monde (acteurs liés) ou son
|
* Résout l'acteur d'une cible depuis sa référence (id d'acteur monde ou
|
||||||
* uuid (tokens non liés via fromUuid).
|
* 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
|
* @param {{id: string|null, uuid: string|null}} ref
|
||||||
* @returns {Promise<Actor|null>}
|
* @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) {
|
if (id) {
|
||||||
const actor = game.actors.get(id)
|
const actor = game.actors.get(id)
|
||||||
if (actor) return actor
|
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
|
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 => {
|
return game.messages.contents.filter(m => {
|
||||||
const f = m.getFlag('world', FLAG_DEFENSE)
|
const f = m.getFlag('world', FLAG_DEFENSE)
|
||||||
if (!f || f.exchangeId !== exchangeId) return false
|
if (!f || f.exchangeId !== exchangeId) return false
|
||||||
if (targetId) return f.actorId === targetId
|
if (!target) return true
|
||||||
return true
|
if (target.uuid && f.actorUuid === target.uuid) return true
|
||||||
|
return f.actorId === target.id
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user