fix: make ActiveEffects work properly

This commit is contained in:
Johannes Loher 2023-06-25 14:33:18 +02:00
parent 124824da83
commit 692d311558
Signed by: saluu
GPG key ID: 7CB0A9FB553DA045
11 changed files with 112 additions and 98 deletions

View file

@ -173,11 +173,11 @@
"DS4.SpellCasterClassWizard": "Zauberer",
"DS4.SpellPrice": "Preis (Gold)",
"DS4.SpellPriceDescription": "Der Kaufpreis des Zauberspruchs.",
"DS4.EffectEnabled": "Aktiv",
"DS4.EffectEnabledAbbr": "A",
"DS4.EffectEffectivelyEnabled": "Effektiv Aktiv (unter Betrachtung, ob ein eventuelles Quellen-Item ausgerüstet ist usw.)",
"DS4.EffectEffectivelyEnabledAbbr": "E",
"DS4.EffectLabel": "Bezeichnung",
"DS4.EffectEnabled": "Eingeschaltet",
"DS4.EffectEnabledAbbr": "E",
"DS4.EffectActive": "Aktiv (unter Betrachtung, ob ein eventuelles Quellen-Item ausgerüstet ist usw.)",
"DS4.EffectActiveAbbr": "A",
"DS4.EffectName": "Name",
"DS4.EffectSourceName": "Quelle",
"DS4.EffectFactor": "Faktor (wie oft der Effekt angewendet wird)",
"DS4.EffectFactorAbbr": "F",
@ -382,7 +382,7 @@
"DS4.NewLanguageName": "Neue Sprache",
"DS4.NewAlphabetName": "Neue Schriftzeichen",
"DS4.NewSpecialCreatureAbilityName": "Neue Besondere Kreaturenfähigkeit",
"DS4.NewEffectLabel": "Neuer Effekt",
"DS4.NewEffectName": "Neuer Effekt",
"DS4.ActiveEffectApplyToItems": "Auf Items Anwenden",
"DS4.ActiveEffectItemName": "Itemname",

View file

@ -175,9 +175,9 @@
"DS4.SpellPriceDescription": "The price to purchase the spell.",
"DS4.EffectEnabled": "Enabled",
"DS4.EffectEnabledAbbr": "E",
"DS4.EffectEffectivelyEnabled": "Effectively Enabled (taking into account whether a potential source item is equipped etc.)",
"DS4.EffectEffectivelyEnabledAbbr": "EE",
"DS4.EffectLabel": "Label",
"DS4.EffectActive": "Active (taking into account whether a potential source item is equipped etc.)",
"DS4.EffectActiveAbbr": "A",
"DS4.EffectName": "Name",
"DS4.EffectSourceName": "Source",
"DS4.EffectFactor": "Factor (the number of times the effect is being applied)",
"DS4.EffectFactorAbbr": "F",
@ -382,7 +382,7 @@
"DS4.NewLanguageName": "New Language",
"DS4.NewAlphabetName": "New Alphabet",
"DS4.NewSpecialCreatureAbilityName": "New Special Creature Ability",
"DS4.NewEffectLabel": "New Effect",
"DS4.NewEffectName": "New Effect",
"DS4.ActiveEffectApplyToItems": "Apply to Items",
"DS4.ActiveEffectItemName": "Item Name",

View file

@ -51,9 +51,9 @@ export class DS4ActorSheet extends ActorSheet {
const enrichedEffectPromises = this.actor.effects.map(async (effect) => {
return {
...effect.toObject(),
sourceName: await effect.getCurrentSourceName(),
sourceName: effect.sourceName,
factor: effect.factor,
isEffectivelyEnabled: !effect.disabled && !effect.isSurpressed,
active: effect.active,
};
});
const enrichedEffects = await Promise.all(enrichedEffectPromises);

View file

@ -35,11 +35,8 @@ export class DS4ActiveEffect extends ActiveEffect {
*/
source = undefined;
/**
* Whether or not this effect is currently surpressed.
* @type {boolean}
*/
get isSurpressed() {
/** @override */
get isSuppressed() {
const originatingItem = this.originatingItem;
if (!originatingItem) {
return false;
@ -82,30 +79,6 @@ export class DS4ActiveEffect extends ActiveEffect {
return super.apply(document, change);
}
/**
* Gets the current source name based on the cached source object.
* @returns {Promise<string>} The current source name
*/
async getCurrentSourceName() {
const game = getGame();
const origin = await this.getSource();
if (origin === null) return game.i18n.localize("None");
return origin.name ?? game.i18n.localize("Unknown");
}
/**
* Gets the source document for this effect. Uses the cached {@link DS4ActiveEffect#source} if it has already been
* set.
* @protected
* @returns {Promise<foundry.abstract.Document | null>}
*/
async getSource() {
if (this.source === undefined) {
this.source = this.origin != null ? await fromUuid(this.origin) : null;
}
return this.source;
}
/**
* Create a new {@link DS4ActiveEffect} using default values.
*
@ -115,7 +88,7 @@ export class DS4ActiveEffect extends ActiveEffect {
*/
static async createDefault(parent) {
const createData = {
label: getGame().i18n.localize(`DS4.NewEffectLabel`),
name: getGame().i18n.localize(`DS4.NewEffectName`),
icon: this.FALLBACK_ICON,
};
@ -141,13 +114,25 @@ export class DS4ActiveEffect extends ActiveEffect {
* @param {import("./item/item").DS4Item | import("./actor/actor").DS4Actor} document The Actor or Item to which to apply the effects
* @param {DS4ActiveEffect[]} effetcs The effects to apply
* @param {(change: EffectChangeData) => boolean} [predicate=() => true] Apply only changes that fullfill this predicate
* @returns {Set<string>} The statuses that are applied by this effect
*/
static applyEffetcs(document, effetcs, predicate = () => true) {
/** @type {Record<string, unknown>} */
const overrides = {};
// Organize non-disabled and -surpressed effects by their application priority
const changesWithEffect = effetcs.flatMap((e) => e.getFactoredChangesWithEffect(predicate));
/** @type {Set<string>} */
const statuses = new Set();
// Organize active effect changes by their application priority
const changesWithEffect = effetcs.flatMap((e) => {
if (!e.active) {
return [];
}
for (const statusId of e.statuses) {
statuses.add(statusId);
}
return e.getFactoredChangesWithEffect(predicate);
});
changesWithEffect.sort((a, b) => (a.change.priority ?? 0) - (b.change.priority ?? 0));
// Apply all changes
@ -162,6 +147,8 @@ export class DS4ActiveEffect extends ActiveEffect {
...foundry.utils.flattenObject(document.overrides),
...overrides,
});
return statuses;
}
/**
@ -171,13 +158,10 @@ export class DS4ActiveEffect extends ActiveEffect {
* @protected
*/
getFactoredChangesWithEffect(predicate = () => true) {
if (this.disabled || this.isSurpressed) {
return [];
}
return this.changes.filter(predicate).flatMap((change) => {
change.priority = change.priority ?? change.mode * 10;
return Array(this.factor).fill({ effect: this, change });
const c = foundry.utils.deepClone(change);
c.priority = c.priority ?? c.mode * 10;
return Array(this.factor).fill({ effect: this, change: c });
});
}
}

View file

@ -15,6 +15,9 @@ import { isAttribute, isTrait } from "./actor-data-source-base";
* The Actor class for DS4
*/
export class DS4Actor extends Actor {
/** @type {Set<string>} */
newStatuses = new Set();
/** @override */
prepareData() {
this.prepareBaseData();
@ -23,11 +26,14 @@ export class DS4Actor extends Actor {
this.applyActiveEffectsToBaseData();
this.prepareDerivedData();
this.applyActiveEffectsToDerivedData();
this.handleStatusChanges();
this.prepareFinalDerivedData();
}
/** @override */
prepareBaseData() {
this.newStatuses = new Set();
this.system.rolling = {
minimumFumbleResult: 20,
maximumCoupResult: 1,
@ -60,7 +66,13 @@ export class DS4Actor extends Actor {
* @protected
*/
get actorEffects() {
return this.effects.filter((effect) => !effect.flags.ds4?.itemEffectConfig?.applyToItems);
const effects = [];
for (const effect of this.allApplicableEffects()) {
if (!effect.flags.ds4?.itemEffectConfig?.applyToItems) {
effects.push(effect);
}
}
return effects;
}
/**
@ -69,7 +81,8 @@ export class DS4Actor extends Actor {
* @returns {import("../active-effect").DS4ActiveEffect[]} The array of effects that are candidates to be applied to the item
*/
itemEffects(item) {
return this.effects.filter((effect) => {
/** @type {(effect: DS4ActiveEffect) => boolean} */
const shouldEffectBeAppliedToItem = (effect, item) => {
const { applyToItems, itemName, condition } = effect.flags.ds4?.itemEffectConfig ?? {};
if (!applyToItems || (itemName !== undefined && itemName !== "" && itemName !== item.name)) {
@ -78,11 +91,7 @@ export class DS4Actor extends Actor {
if (condition !== undefined && condition !== "") {
try {
const replacedCondition = DS4Actor.replaceFormulaData(condition, {
item,
actor: this,
effect,
});
const replacedCondition = DS4Actor.replaceFormulaData(condition, { item, actor: this, effect });
return replacedCondition !== undefined ? Boolean(mathEvaluator.evaluate(replacedCondition)) : false;
} catch (error) {
logger.warn(error);
@ -91,7 +100,15 @@ export class DS4Actor extends Actor {
}
return true;
});
};
const effects = [];
for (const effect of this.allApplicableEffects()) {
if (shouldEffectBeAppliedToItem(effect, item)) {
effects.push(effect);
}
}
return effects;
}
/**
@ -153,7 +170,8 @@ export class DS4Actor extends Actor {
*/
applyActiveEffectsToItem(item) {
item.overrides = {};
DS4ActiveEffect.applyEffetcs(item, this.itemEffects(item));
item.reset();
DS4ActiveEffect.applyEffetcs(item, this.itemEffects(item)).forEach(this.newStatuses.add.bind(this.newStatuses));
}
/**
@ -168,7 +186,7 @@ export class DS4Actor extends Actor {
(change) =>
!this.derivedDataProperties.includes(change.key) &&
!this.finalDerivedDataProperties.includes(change.key),
);
).forEach(this.newStatuses.add.bind(this.newStatuses));
}
/**
@ -178,7 +196,7 @@ export class DS4Actor extends Actor {
applyActiveEffectsToDerivedData() {
DS4ActiveEffect.applyEffetcs(this, this.actorEffects, (change) =>
this.derivedDataProperties.includes(change.key),
);
).forEach(this.newStatuses.add.bind(this.newStatuses));
}
/**
@ -205,6 +223,30 @@ export class DS4Actor extends Actor {
return combatValueProperties.concat(checkProperties);
}
handleStatusChanges() {
this.statuses ??= new Set();
// Identify which special statuses had been active
const specialStatuses = new Map();
for (const statusId of Object.values(CONFIG.specialStatusEffects)) {
specialStatuses.set(statusId, this.statuses.has(statusId));
}
this.statuses.clear();
// set new statuses
this.newStatuses.forEach(this.statuses.add.bind(this.statuses));
// Apply special statuses that changed to active tokens
const tokens = this.getActiveTokens();
for (const [statusId, wasActive] of specialStatuses) {
const isActive = this.statuses.has(statusId);
if (isActive === wasActive) continue;
for (const token of tokens) {
token._onApplyStatusEffect(statusId, isActive);
}
}
}
/**
* Apply final transformations to the Actor data after all effects have been applied.
*/

View file

@ -38,12 +38,8 @@
"minimum": "10.290",
"verified": "10"
},
"esmodules": [
"ds4.js"
],
"styles": [
"css/ds4.css"
],
"esmodules": ["ds4.js"],
"styles": ["css/ds4.css"],
"languages": [
{
"lang": "en",

View file

@ -9,7 +9,9 @@ SPDX-License-Identifier: MIT
<!-- Effect Header -->
<header class="sheet-header">
<img class="effect-icon" src="{{ data.icon }}" data-edit="icon">
<h1 class="effect-title">{{ data.label }}</h1>
<h1 class="effect-title">
<input name="name" type="text" value="{{data.name}}" placeholder="{{ localize 'Name' }}" />
</h1>
</header>
<!-- Effect Configuration Tabs -->
@ -21,30 +23,19 @@ SPDX-License-Identifier: MIT
<!-- Details Tab -->
<section class="tab" data-tab="details">
<div class="form-group">
<label>{{ localize "EFFECT.Label" }}</label>
<div class="form-fields">
<input type="text" name="label" value="{{ data.label }}" />
</div>
</div>
<div class="form-group">
<label>{{ localize "EFFECT.Icon" }}</label>
<div class="form-fields">
{{filePicker target="icon" type="image"}}
<input class="image" type="text" name="icon" placeholder="path/image.png" value="{{data.icon}}" />
</div>
</div>
<div class="form-group">
<label>{{ localize "EFFECT.IconTint" }}</label>
<div class="form-fields">
<input class="color" type="text" name="tint" value="{{data.tint}}" />
<input type="color" value="{{data.tint}}" data-edit="tint" />
{{colorPicker name="tint" value=data.tint}}
</div>
</div>
<div class="form-group stacked">
<label>{{ localize "EFFECT.Description" }}</label>
{{editor descriptionHTML target="description" button=false editable=editable engine="prosemirror"
collaborate=false}}
</div>
<div class="form-group">
<label>{{ localize "EFFECT.Disabled" }}</label>
<div class="form-fields">
@ -63,10 +54,11 @@ SPDX-License-Identifier: MIT
{{#if isItemEffect}}
<div class="form-group">
<label>{{ localize "EFFECT.Transfer" }}</label>
<label>{{ labels.transfer.name }}</label>
<div class="form-fields">
<input type="checkbox" name="transfer" {{checked data.transfer}} />
</div>
<p class="hint">{{ labels.transfer.hint }}</p>
</div>
{{/if}}

View file

@ -15,15 +15,15 @@ SPDX-License-Identifier: MIT
type="checkbox" {{checked (ne effectData.disabled true)}} data-dtype="Boolean" data-property="disabled"
data-inverted="true" title="{{localize 'DS4.EffectEnabled'}}">
{{!-- effectively enabled --}}
{{#if effectData.isEffectivelyEnabled}}<i class="fas fa-check"></i>{{else}}<i class="fas fa-ban"></i>{{/if}}
{{!-- active --}}
{{#if effectData.active}}<i class="fas fa-check"></i>{{else}}<i class="fas fa-ban"></i>{{/if}}
{{!-- icon --}}
{{> systems/ds4/templates/sheets/shared/components/rollable-image.hbs rollable=false src=effectData.icon
alt=(localize "DS4.DocumentImageAltText" name=effectData.label) title=effectData.label}}
{{!-- label --}}
<div title="{{effectData.label}}">{{effectData.label}}</div>
{{!-- name --}}
<div title="{{effectData.name}}">{{effectData.name}}</div>
{{!-- source name --}}
<div title="{{effectData.sourceName}}">{{effectData.sourceName}}</div>

View file

@ -12,14 +12,14 @@ SPDX-License-Identifier: MIT
{{!-- enabled --}}
<div title="{{localize 'DS4.EffectEnabled'}}">{{localize 'DS4.EffectEnabledAbbr'}}</div>
{{!-- effectively enabled --}}
<div title="{{localize 'DS4.EffectEffectivelyEnabled'}}">{{localize 'DS4.EffectEffectivelyEnabledAbbr'}}</div>
{{!-- active --}}
<div title="{{localize 'DS4.EffectActive'}}">{{localize 'DS4.EffectActiveAbbr'}}</div>
{{!-- icon --}}
<div></div>
{{!-- label --}}
<div>{{localize 'DS4.EffectLabel'}}</div>
{{!-- name --}}
<div>{{localize 'DS4.EffectName'}}</div>
{{!-- source name --}}
<div>{{localize 'DS4.EffectSourceName'}}</div>

View file

@ -13,8 +13,8 @@ SPDX-License-Identifier: MIT
{{> systems/ds4/templates/sheets/shared/components/rollable-image.hbs rollable=false src=effectData.icon
alt=(localize "DS4.DocumentImageAltText" name=effectData.label) title=effectData.label}}
{{!-- label --}}
<div title="{{effectData.label}}">{{effectData.label}}</div>
{{!-- name --}}
<div title="{{effectData.name}}">{{effectData.name}}</div>
{{!-- control button group --}}
{{> systems/ds4/templates/sheets/shared/components/control-button-group.hbs documentType="effect"

View file

@ -12,7 +12,7 @@ SPDX-License-Identifier: MIT
<div></div>
{{!-- label --}}
<div>{{localize 'DS4.EffectLabel'}}</div>
<div>{{localize 'DS4.EffectName'}}</div>
{{!-- control buttons placeholder --}}
<div></div>