Skip to content

Commit 302f80c

Browse files
committed
FIX - spell & psi power targeting + resistance defense rolling dice
1 parent 2e6d4ba commit 302f80c

13 files changed

Lines changed: 386 additions & 69 deletions

src/module/actor/ABFActor.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { INITIAL_SPELL_CASTING_DATA } from '../types/mystic/SpellItemConfig.js';
2121
import ABFFoundryRoll from '../rolls/ABFFoundryRoll';
2222
import { openModDialog } from '../utils/dialogs/openSimpleInputDialog';
2323
import ABFItem from '../items/ABFItem';
24+
import { FormulaEvaluator } from '../../utils/formulaEvaluator.js';
2425

2526
export class ABFActor extends Actor {
2627
i18n = game.i18n;
@@ -870,4 +871,48 @@ export class ABFActor extends Actor {
870871
getItem(itemId) {
871872
return this.getEmbeddedDocument('Item', itemId);
872873
}
874+
875+
applyActiveEffects() {
876+
const originals = new Map();
877+
878+
try {
879+
for (const effect of this.effects.contents) {
880+
if (!effect.active) continue;
881+
882+
const changes = effect.changes;
883+
if (!Array.isArray(changes) || changes.length === 0) continue;
884+
885+
// Save originals
886+
originals.set(
887+
effect,
888+
changes.map(c => c.value)
889+
);
890+
891+
// Patch values (in-memory only)
892+
for (const change of changes) {
893+
change.value = this._applyDynamicEffectValue(change.value);
894+
}
895+
}
896+
897+
// Let Foundry core do the real AE logic (modes, priority, etc.)
898+
return super.applyActiveEffects();
899+
} finally {
900+
// Restore original values so nothing "sticks" on the document
901+
for (const [effect, values] of originals.entries()) {
902+
const changes = effect.changes;
903+
for (let i = 0; i < changes.length; i++) {
904+
changes[i].value = values[i];
905+
}
906+
}
907+
}
908+
}
909+
910+
_applyDynamicEffectValue(value) {
911+
if (typeof value !== 'string') return value;
912+
913+
const evaluated = FormulaEvaluator.evaluate(value, this);
914+
if (evaluated !== null && !Number.isNaN(evaluated)) return evaluated;
915+
916+
return value;
917+
}
873918
}

src/module/actor/utils/buttonCallbacks/castPsychicPower.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ABFAttackData } from '../../../combat/ABFAttackData.js';
33
import { ABFSupernaturalShieldData } from '../../../combat/ABFSupernaturalShieldData.js';
44
import { shieldValueCheck } from '../../../combat/utils/shieldValueCheck.js';
55
import { openModDialog } from '../../../utils/dialogs/openSimpleInputDialog.js';
6+
import { getSnapshotTargets } from '../getSnapshotTargets.js';
67

78
function _getBestEffectKey(effects, rolledValue) {
89
if (!effects) return null;
@@ -81,7 +82,8 @@ async function _sendPsychicAttackToChat({
8182
power,
8283
difficultyKey,
8384
effectData,
84-
baseDamage
85+
baseDamage,
86+
targets
8587
}) {
8688
// Quick attack like spells: ask mod, roll offensive projection, post roll, then attack data to chat
8789
const mod = Number(await openModDialog({ title: 'Modificador de Proyección' })) || 0;
@@ -116,7 +118,7 @@ async function _sendPsychicAttackToChat({
116118
.critBonus(0)
117119
.attackerId(actor.id)
118120
.weaponId(power.id)
119-
.targets([])
121+
.targets(targets ?? [])
120122
.build()
121123
.toChatMessage({ actor, weapon: power });
122124

@@ -195,12 +197,15 @@ export async function castPsychicPower(sheet, event) {
195197
if (fatigueResult === 0) {
196198
const baseDamage = Number(effectData?.damage?.value ?? 0) || 0;
197199

200+
const snapshotTargets = getSnapshotTargets();
201+
198202
await _sendPsychicAttackToChat({
199203
actor,
200204
power,
201205
difficultyKey,
202206
effectData,
203-
baseDamage
207+
baseDamage,
208+
targets: snapshotTargets
204209
});
205210

206211
return;
@@ -272,12 +277,15 @@ export async function castPsychicPowerDifficulty(sheet, event) {
272277
if (fatigueResult === 0) {
273278
const baseDamage = Number(effectData?.damage?.value ?? 0) || 0;
274279

280+
const snapshotTargets = getSnapshotTargets();
281+
275282
await _sendPsychicAttackToChat({
276283
actor,
277284
power,
278285
difficultyKey,
279286
effectData,
280-
baseDamage
287+
baseDamage,
288+
targets: snapshotTargets
281289
});
282290

283291
return;

src/module/actor/utils/buttonCallbacks/castSpellGrade.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,12 @@ import { shieldValueCheck } from '../../../combat/utils/shieldValueCheck.js';
55
import { Templates } from '../../../utils/constants';
66
import { openModDialog } from '../../../utils/dialogs/openSimpleInputDialog.js';
77
import { SpellAttackConfigurationDialog } from '../../../dialogs/SpellAttackConfigurationDialog.js';
8+
import { getSnapshotTargets } from '../getSnapshotTargets.js';
89

9-
// Comments in English
1010
function localizeGrade(grade) {
1111
return game.i18n.localize(`anima.ui.mystic.spell.grade.${grade}.title`);
1212
}
1313

14-
// Comments in English
1514
async function openShieldConfigDialog({ spell, grade }) {
1615
const content = await renderTemplate(Templates.Dialog.SpellShieldConfigDialog, {
1716
formulaBonus: 0
@@ -81,7 +80,8 @@ export async function castSpellGrade(sheet, event) {
8180
new SpellAttackConfigurationDialog({
8281
attacker: token,
8382
spell,
84-
grade
83+
grade,
84+
targets: getSnapshotTargets()
8585
});
8686

8787
return;
@@ -116,7 +116,7 @@ export async function castSpellGrade(sheet, event) {
116116
.critBonus(0)
117117
.attackerId(actor.id)
118118
.weaponId(spell.id)
119-
.targets([])
119+
.targets(getSnapshotTargets())
120120
.build()
121121
.toChatMessage({ actor, weapon: spell });
122122
}

src/module/actor/utils/buttonCallbacks/createWeaponAttack.js

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { AttackConfigurationDialog } from '../../../dialogs/AttackConfigurationDialog.js';
2+
import { getSnapshotTargets } from '../getSnapshotTargets.js';
23

34
/**
45
* Open AttackConfigurationDialog with the selected weapon locked.
@@ -13,18 +14,7 @@ export function createWeaponAttack(sheet, e) {
1314
const attackerToken = sheet.token ?? sheet.actor?.getActiveTokens?.()[0];
1415
if (!attackerToken) return ui.notifications.warn('No attacker token found.');
1516

16-
const snapshotTargets = Array.from(game.user?.targets ?? [])
17-
.map(t => {
18-
const tok = t?.document ?? t;
19-
const actorUuid = tok?.actor?.id ?? tok?.actorId ?? '';
20-
// Prefer UUID; fallback to id
21-
const tokenUuid = tok?.uuid ?? tok?.document?.uuid ?? tok?.id ?? '';
22-
const label = tok?.name ?? tok?.actor?.name ?? '';
23-
return actorUuid && tokenUuid
24-
? { actorUuid, tokenUuid, state: 'pending', label, updatedAt: Date.now() }
25-
: null;
26-
})
27-
.filter(Boolean);
17+
const snapshotTargets = getSnapshotTargets();
2818

2919
new AttackConfigurationDialog(
3020
{ attacker: attackerToken, weaponId, targets: snapshotTargets },
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export function getSnapshotTargets(user = game.user) {
2+
return Array.from(user?.targets ?? [])
3+
.map(t => {
4+
const tok = t?.document ?? t;
5+
6+
const actorUuid = tok?.actor?.id ?? tok?.actorId ?? '';
7+
// Prefer UUID; fallback to id
8+
const tokenUuid = tok?.uuid ?? tok?.document?.uuid ?? tok?.id ?? '';
9+
const label = tok?.name ?? tok?.actor?.name ?? '';
10+
11+
return actorUuid && tokenUuid
12+
? { actorUuid, tokenUuid, state: 'pending', label, updatedAt: Date.now() }
13+
: null;
14+
})
15+
.filter(Boolean);
16+
}

src/module/combat/ABFAttackData.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ export class ABFAttackData {
5353
: [];
5454
}
5555

56-
// Comments in English
5756
static _normalizeTarget(t = {}) {
5857
return {
5958
actorUuid: String(t.actorUuid ?? t.actorId ?? ''),
Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,70 @@
1-
// Comments in English
21
export class ABFCombatResultData {
32
/** @param {Partial<ABFCombatResultData>} p */
43
constructor(p = {}) {
5-
this.difference = p.difference ?? 0; // Diferencia
4+
this.difference = p.difference ?? 0; // Diferencia
65
this.counterAttackValue = p.counterAttackValue ?? 0; // Valor de contraataque
76
this.hasCounterAttack = p.hasCounterAttack ?? false; // ¿Hay contraataque?
8-
this.damageFinal = p.damageFinal ?? 0; // Daño final
7+
this.damageFinal = p.damageFinal ?? 0; // Daño final
98
this.lifePercentRemoved = p.lifePercentRemoved ?? 0; // % de vida quitada
10-
this.isCritical = p.isCritical ?? false; // ¿Es crítico?
9+
this.isCritical = p.isCritical ?? false; // ¿Es crítico?
1110
this.baseCriticalValue = p.baseCriticalValue ?? 0; // Valor base del crítico
12-
this.attackBreak = p.attackBreak ?? 0; // Rotura del ataque
11+
this.attackBreak = p.attackBreak ?? 0; // Rotura del ataque
1312
}
1413

15-
toJSON() { return { ...this }; }
14+
toJSON() {
15+
return { ...this };
16+
}
1617

1718
static fromJSON(json) {
1819
const obj = typeof json === 'string' ? JSON.parse(json) : json;
1920
return new ABFCombatResultData(obj);
2021
}
2122

2223
/** Fluent builder */
23-
static builder() { return new ABFCombatResultDataBuilder(); }
24+
static builder() {
25+
return new ABFCombatResultDataBuilder();
26+
}
2427
}
2528

2629
export class ABFCombatResultDataBuilder {
27-
constructor() { this._p = {}; }
30+
constructor() {
31+
this._p = {};
32+
}
2833

29-
difference(v) { this._p.difference = Number(v) || 0; return this; }
30-
counterAttackValue(v) { this._p.counterAttackValue = Number(v) || 0; return this; }
31-
hasCounterAttack(v) { this._p.hasCounterAttack = !!v; return this; }
32-
damageFinal(v) { this._p.damageFinal = Number(v) || 0; return this; }
33-
lifePercentRemoved(v) { this._p.lifePercentRemoved = Number(v) || 0; return this; }
34-
isCritical(v) { this._p.isCritical = !!v; return this; }
35-
baseCriticalValue(v) { this._p.baseCriticalValue = Number(v) || 0; return this; }
36-
attackBreak(v) { this._p.attackBreak = Number(v) || 0; return this; }
34+
difference(v) {
35+
this._p.difference = Number(v) || 0;
36+
return this;
37+
}
38+
counterAttackValue(v) {
39+
this._p.counterAttackValue = Number(v) || 0;
40+
return this;
41+
}
42+
hasCounterAttack(v) {
43+
this._p.hasCounterAttack = !!v;
44+
return this;
45+
}
46+
damageFinal(v) {
47+
this._p.damageFinal = Number(v) || 0;
48+
return this;
49+
}
50+
lifePercentRemoved(v) {
51+
this._p.lifePercentRemoved = Number(v) || 0;
52+
return this;
53+
}
54+
isCritical(v) {
55+
this._p.isCritical = !!v;
56+
return this;
57+
}
58+
baseCriticalValue(v) {
59+
this._p.baseCriticalValue = Number(v) || 0;
60+
return this;
61+
}
62+
attackBreak(v) {
63+
this._p.attackBreak = Number(v) || 0;
64+
return this;
65+
}
3766

38-
build() { return new ABFCombatResultData(this._p); }
67+
build() {
68+
return new ABFCombatResultData(this._p);
69+
}
3970
}

src/module/combat/autoRollDefenseAgainstAttack.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,43 @@ function getDefensesCounter(actor) {
3737
);
3838
}
3939

40+
function buildZeroDefenseResult({ actor, defenderToken, attackData }) {
41+
const armorType = attackData?.armorType;
42+
const taFinal =
43+
armorType != null ? actor.system?.combat?.totalArmor?.at?.[armorType]?.value ?? 0 : 0;
44+
45+
const defenseData = ABFDefenseData.builder()
46+
.defenseAbility(0)
47+
.armor(taFinal)
48+
.inmodifiableArmor(false)
49+
.defenseType('resistance')
50+
.defenderId(actor.id)
51+
.defenderTokenId(defenderToken?.id ?? '')
52+
.weaponId('')
53+
.shieldId('')
54+
.stackDefense(false)
55+
.applyMultipleDefensePenalty(false)
56+
.projectilePenalty(0)
57+
.build();
58+
59+
const combatResult = computeCombatResult(attackData, defenseData);
60+
61+
return {
62+
actor,
63+
token: defenderToken ?? null,
64+
defenseType: 'resistance',
65+
defenseTotal: 0,
66+
weaponId: '',
67+
shieldId: '',
68+
defenseData,
69+
combatResult,
70+
appliedPenalties: {
71+
projectilePenalty: 0,
72+
multipleDefensePenalty: 0
73+
}
74+
};
75+
}
76+
4077
export async function autoRollDefenseAgainstAttack({
4178
defenderToken = null,
4279
defenderActor = null,
@@ -46,6 +83,13 @@ export async function autoRollDefenseAgainstAttack({
4683
const actor = defenderActor ?? defenderToken?.actor ?? null;
4784
if (!actor) throw new Error('autoRollDefenseAgainstAttack: defender actor missing');
4885

86+
const defenseMode = actor.system?.general?.settings?.defenseType?.value;
87+
88+
// Accumulation/resistance defenders: base defense 0, no roll, no penalties.
89+
if (defenseMode === 'resistance') {
90+
return buildZeroDefenseResult({ actor, defenderToken, attackData });
91+
}
92+
4993
const defensesCounter = getDefensesCounter(actor);
5094

5195
const candidate = pickBestDefenseCandidate(actor, { attackData, defensesCounter });

src/module/dialogs/AttackConfigurationDialog.js

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { Templates } from '../utils/constants';
22
import { ABFConfig } from '../ABFConfig';
33
import { ABFAttackData } from '../combat/ABFAttackData';
4+
import { getSnapshotTargets } from '../actor/utils/getSnapshotTargets.js';
5+
///dialogs/AttackConfigurationDialog.js
6+
///actor/utils/getSnapshotTargets.js
47

58
export class AttackConfigurationDialog extends FormApplication {
69
constructor(object = {}, options = {}) {
@@ -16,6 +19,7 @@ export class AttackConfigurationDialog extends FormApplication {
1619
ui.notifications?.error('AttackConfigurationDialog: attacker is required');
1720
return { allowed: false };
1821
}
22+
1923
const attackerActor = attacker.actor;
2024

2125
const resolvedWeapon =
@@ -25,18 +29,8 @@ export class AttackConfigurationDialog extends FormApplication {
2529
ui.notifications?.warn('Arma no encontrada.');
2630
}
2731

28-
// Snapshot de targets si no te lo pasan (tokenUuid = UUID si existe)
29-
const fallbackSnapshot = Array.from(game.user?.targets ?? [])
30-
.map(t => {
31-
const token = t?.document ?? t;
32-
const actorUuid = token?.actor?.id ?? token?.actorId ?? '';
33-
const tokenUuid = token?.uuid ?? token?.document?.uuid ?? token?.id ?? '';
34-
const label = token?.name ?? token?.actor?.name ?? '';
35-
return actorUuid && tokenUuid
36-
? { actorUuid, tokenUuid, state: 'pending', label, updatedAt: Date.now() }
37-
: null;
38-
})
39-
.filter(Boolean);
32+
// Fallback targets snapshot (reusing shared helper)
33+
const fallbackSnapshot = getSnapshotTargets();
4034

4135
const isOwner = attackerActor.testUserPermission?.(
4236
game.user,

0 commit comments

Comments
 (0)