Implemented Active Effects.
Used AE to implement exhaustion, stimulants, and rest.
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
import {
|
||||
applyAgentResourceMaxBonuses,
|
||||
calculateHealthMax,
|
||||
setSkillTargetProficiencies,
|
||||
calculateMeleeDamageBonusFormula,
|
||||
clampSanityRitualValue,
|
||||
prepareAgentSkillFlags,
|
||||
prepareBreakingPointHit,
|
||||
prepareStatisticsX5,
|
||||
} from "./derived-actor-data.js";
|
||||
|
||||
/**
|
||||
* @param {number} current
|
||||
* @param {number} base
|
||||
* @returns {string}
|
||||
*/
|
||||
function getActiveEffectModificationClass(current, base) {
|
||||
if (current == null || base == null || current === base) return "";
|
||||
return current > base ? "ae-mod-increase" : "ae-mod-decrease";
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach sheet display classes comparing prepared data to persisted source (pre–AE).
|
||||
* @param {Actor} actor
|
||||
* @returns {void}
|
||||
*/
|
||||
export function prepareAgentActiveEffectDisplay(actor) {
|
||||
if (actor.type !== "agent") return;
|
||||
|
||||
const system = actor.system;
|
||||
const source = actor._source?.system;
|
||||
if (!source) return;
|
||||
|
||||
for (const [key, stat] of Object.entries(system.statistics ?? {})) {
|
||||
const baseValue = source.statistics?.[key]?.value;
|
||||
const effective = stat.effectiveValue ?? stat.value;
|
||||
stat.valueDisplayClass = getActiveEffectModificationClass(
|
||||
effective,
|
||||
baseValue,
|
||||
);
|
||||
stat.statisticAeModifier = stat.valueDisplayClass
|
||||
? effective - (baseValue ?? 0)
|
||||
: 0;
|
||||
}
|
||||
|
||||
const persistedStats = source.statistics ?? system.statistics;
|
||||
const healthFormulaMax = calculateHealthMax(persistedStats, persistedStats);
|
||||
const wpFormulaMax = persistedStats.pow?.value ?? 0;
|
||||
const persistedUnnatural =
|
||||
source.skills?.unnatural?.proficiency ?? system.skills?.unnatural?.proficiency ?? 0;
|
||||
const sanityFormulaMax = 99 - persistedUnnatural;
|
||||
|
||||
const healthBaseMax = healthFormulaMax + (source.health?.maxBonus ?? 0);
|
||||
system.health.maxDisplayClass = getActiveEffectModificationClass(
|
||||
system.health.max,
|
||||
healthBaseMax,
|
||||
);
|
||||
|
||||
const wpBaseMax = wpFormulaMax + (source.wp?.maxBonus ?? 0);
|
||||
system.wp.maxDisplayClass = getActiveEffectModificationClass(
|
||||
system.wp.max,
|
||||
wpBaseMax,
|
||||
);
|
||||
|
||||
const sanityBaseMax = sanityFormulaMax + (source.sanity?.maxBonus ?? 0);
|
||||
system.sanity.maxDisplayClass = getActiveEffectModificationClass(
|
||||
system.sanity.max,
|
||||
sanityBaseMax,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recompute agent fields that depend on Active Effects (final phase) and refresh sheet display.
|
||||
* Single post-AE pass for agents — do not mirror this logic in AgentData.prepareDerivedData.
|
||||
* @param {Actor} actor
|
||||
* @returns {void}
|
||||
*/
|
||||
export function refreshDerivedAfterActiveEffects(actor) {
|
||||
if (actor.type !== "agent") return;
|
||||
|
||||
const system = actor.system;
|
||||
|
||||
const sourceStatistics = actor._source?.system?.statistics;
|
||||
prepareStatisticsX5(system.statistics, sourceStatistics);
|
||||
applyAgentResourceMaxBonuses(system, sourceStatistics);
|
||||
setSkillTargetProficiencies(system.skills);
|
||||
setSkillTargetProficiencies(system.typedSkills);
|
||||
prepareAgentSkillFlags(system.skills);
|
||||
clampSanityRitualValue(system.sanity);
|
||||
prepareBreakingPointHit(system.sanity);
|
||||
system.statistics.str.meleeDamageBonusFormula =
|
||||
calculateMeleeDamageBonusFormula(
|
||||
system.statistics.str.effectiveValue ?? system.statistics.str.value,
|
||||
);
|
||||
|
||||
prepareAgentActiveEffectDisplay(actor);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import DG from "../config.js";
|
||||
import { getStatisticEffectiveValue } from "./derived-actor-data.js";
|
||||
|
||||
/**
|
||||
* Strip form values that match post–AE prepared numbers so persisted data stays at base values.
|
||||
* @param {Actor} actor
|
||||
* @param {object} data Update payload (mutated in place).
|
||||
* @returns {object}
|
||||
*/
|
||||
export function sanitizeActiveEffectBackedUpdateData(actor, data) {
|
||||
if (!data?.system) return data;
|
||||
|
||||
const source = actor._source?.system;
|
||||
if (!source) return data;
|
||||
|
||||
sanitizeStatisticValues(actor, data, source);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Actor} actor
|
||||
* @param {object} data
|
||||
* @param {object} source
|
||||
*/
|
||||
function sanitizeStatisticValues(actor, data, source) {
|
||||
for (const key of DG.statistics) {
|
||||
const submitted = foundry.utils.getProperty(
|
||||
data,
|
||||
`system.statistics.${key}.value`,
|
||||
);
|
||||
if (submitted === undefined) continue;
|
||||
|
||||
const sourceStat = source.statistics?.[key];
|
||||
const preparedStat = actor.system.statistics?.[key];
|
||||
if (!sourceStat || !preparedStat) continue;
|
||||
|
||||
const base = sourceStat.value;
|
||||
const effective = getStatisticEffectiveValue(preparedStat, sourceStat);
|
||||
|
||||
if (submitted === effective && submitted !== base) {
|
||||
foundry.utils.setProperty(data, `system.statistics.${key}.value`, base);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,12 @@
|
||||
* @returns {void}
|
||||
*/
|
||||
export function flagUnnaturalResourceMaxUpdates(submittedData, actor) {
|
||||
const sourceStats = actor._source?.system?.statistics;
|
||||
|
||||
submittedData.system.wp.maxNeedsUpdate =
|
||||
submittedData.system?.statistics.pow.value !==
|
||||
actor.system.statistics.pow.value;
|
||||
submittedData.system?.statistics?.pow?.value !== sourceStats?.pow?.value;
|
||||
|
||||
submittedData.system.health.maxNeedsUpdate =
|
||||
submittedData.system?.statistics.str.value !==
|
||||
actor.system.statistics.str.value ||
|
||||
submittedData.system?.statistics.con.value !==
|
||||
actor.system.statistics.con.value;
|
||||
submittedData.system?.statistics?.str?.value !== sourceStats?.str?.value ||
|
||||
submittedData.system?.statistics?.con?.value !== sourceStats?.con?.value;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
/**
|
||||
* Shared derived-data helpers for actor TypeDataModels.
|
||||
*
|
||||
* Active Effect validation layers (do not duplicate clamp logic elsewhere):
|
||||
* - Schema `NumberField` min/max: enforced when values pass through `field.clean()`
|
||||
* (document updates, Active Effect `applyChange` on registered paths).
|
||||
* - `cleanDerivedNumber`: use when assigning derived fields in code (e.g. `health.max`,
|
||||
* `sanity.max`); plain assignment bypasses schema cleaners unless the path has a NumberField.
|
||||
* - `clampStatisticValue`: effective ratings after statistic modifier AEs (min 0).
|
||||
* - `applyAgentResourceMaxBonuses`: recompute resource max after AE;
|
||||
* agents call these only from `refreshDerivedAfterActiveEffects` (post–final phase).
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -28,20 +37,86 @@ export function calculateMeleeDamageBonusFormula(strengthValue) {
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} value
|
||||
* @returns {number}
|
||||
*/
|
||||
function clampStatisticValue(value) {
|
||||
const numeric = Number(value);
|
||||
if (!Number.isFinite(numeric)) return 0;
|
||||
return Math.max(0, Math.round(numeric));
|
||||
}
|
||||
|
||||
/**
|
||||
* Effective statistic rating: persisted value plus AE modifier (minimum 0).
|
||||
* @param {object} stat Prepared statistic.
|
||||
* @param {object} [sourceStat] Persisted statistic from `_source`.
|
||||
* @returns {number}
|
||||
*/
|
||||
export function getStatisticEffectiveValue(stat, sourceStat) {
|
||||
const base = clampStatisticValue(sourceStat?.value ?? stat?.value);
|
||||
const modifier = Number(stat?.modifier) || 0;
|
||||
return clampStatisticValue(base + modifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} statistics
|
||||
* @param {object} [sourceStatistics] Persisted statistics from `_source`.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function prepareStatisticsX5(statistics) {
|
||||
for (const statistic of Object.values(statistics)) {
|
||||
statistic.x5 = statistic.value * 5;
|
||||
export function prepareStatisticsX5(statistics, sourceStatistics) {
|
||||
for (const [key, statistic] of Object.entries(statistics ?? {})) {
|
||||
statistic.effectiveValue = getStatisticEffectiveValue(
|
||||
statistic,
|
||||
sourceStatistics?.[key],
|
||||
);
|
||||
statistic.x5 = statistic.effectiveValue * 5;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} skills
|
||||
* Clamp a derived numeric value through the TypeDataModel field definition when available.
|
||||
* @param {foundry.abstract.TypeDataModel} model
|
||||
* @param {string} path Dot-delimited path (e.g. "health.max")
|
||||
* @param {number} raw
|
||||
* @returns {number}
|
||||
*/
|
||||
export function cleanDerivedNumber(model, path, raw) {
|
||||
const field = model.getFieldForProperty?.(path);
|
||||
const numeric = Number(raw);
|
||||
if (!field) {
|
||||
return Number.isFinite(numeric) ? Math.max(0, Math.round(numeric)) : 0;
|
||||
}
|
||||
return field.clean(numeric, { persisted: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {foundry.abstract.TypeDataModel} model Agent system TypeDataModel.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function applyAgentResourceMaxBonuses(model, sourceStatistics) {
|
||||
const sourceStats = sourceStatistics ?? model._source?.statistics;
|
||||
model.health.max = cleanDerivedNumber(
|
||||
model,
|
||||
"health.max",
|
||||
calculateHealthMax(model.statistics, sourceStats) +
|
||||
(model.health.maxBonus ?? 0),
|
||||
);
|
||||
model.wp.max = cleanDerivedNumber(
|
||||
model,
|
||||
"wp.max",
|
||||
getStatisticEffectiveValue(model.statistics.pow, sourceStats?.pow) +
|
||||
(model.wp.maxBonus ?? 0),
|
||||
);
|
||||
model.sanity.max = cleanDerivedNumber(
|
||||
model,
|
||||
"sanity.max",
|
||||
99 -
|
||||
model.skills.unnatural.proficiency +
|
||||
(model.sanity.maxBonus ?? 0),
|
||||
);
|
||||
}
|
||||
|
||||
export function setSkillTargetProficiencies(skills) {
|
||||
for (const skill of Object.values(skills)) {
|
||||
skill.targetProficiency = skill.proficiency;
|
||||
@@ -56,7 +131,9 @@ export function setSkillTargetProficiencies(skills) {
|
||||
export function initializeSanityIfUnset(sanity, statistics) {
|
||||
if (sanity.value >= 100) {
|
||||
sanity.value = statistics.pow.x5;
|
||||
sanity.currentBreakingPoint = sanity.value - statistics.pow.value;
|
||||
sanity.currentBreakingPoint =
|
||||
sanity.value -
|
||||
(statistics.pow.effectiveValue ?? statistics.pow.value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +184,6 @@ export function prepareAgentSkillFlags(skills) {
|
||||
skill.cannotBeImprovedByFailure =
|
||||
key === "unnatural" || key === "luck" || key === "ritual";
|
||||
skill.isCalculatedValue = key === "ritual";
|
||||
skill.targetProficiency = skill.proficiency;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,10 +203,15 @@ export function removeLegacyRitualSkill(system) {
|
||||
* @param {object} statistics
|
||||
* @returns {number}
|
||||
*/
|
||||
export function calculateHealthMax(statistics) {
|
||||
export function calculateHealthMax(statistics, sourceStatistics) {
|
||||
try {
|
||||
return Math.ceil(
|
||||
(statistics.con.value + statistics.str.value) / 2,
|
||||
(getStatisticEffectiveValue(
|
||||
statistics.con,
|
||||
sourceStatistics?.con,
|
||||
) +
|
||||
getStatisticEffectiveValue(statistics.str, sourceStatistics?.str)) /
|
||||
2,
|
||||
);
|
||||
} catch {
|
||||
return 10;
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import DG from "../config.js";
|
||||
import { ROLL_TARGET_FIELD_KEYS } from "../active-effect/effect-fields.js";
|
||||
import { getEffectiveSuppressExhaustion } from "./stimulant-effect.js";
|
||||
|
||||
/** @type {WeakMap<Actor, Promise<void>>} */
|
||||
const syncLocks = new WeakMap();
|
||||
|
||||
/**
|
||||
* @param {Actor} actor
|
||||
* @returns {ActiveEffect[]}
|
||||
*/
|
||||
function getExhaustionEffects(actor) {
|
||||
return actor.effects?.filter((effect) => effect.getFlag(DG.ID, "exhaustion")) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Actor} actor
|
||||
* @returns {ActiveEffect|undefined}
|
||||
*/
|
||||
function getExhaustionEffect(actor) {
|
||||
return getExhaustionEffects(actor)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} penalty
|
||||
* @returns {object[]}
|
||||
*/
|
||||
function buildExhaustionChanges(penalty) {
|
||||
const value = String(penalty);
|
||||
return ROLL_TARGET_FIELD_KEYS.map((key) => ({
|
||||
key,
|
||||
type: "add",
|
||||
value,
|
||||
phase: "final",
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ActiveEffect} effect
|
||||
* @param {number} penalty
|
||||
* @param {boolean} suppressExhaustion
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function effectMatchesState(effect, penalty, suppressExhaustion) {
|
||||
if (Boolean(effect.disabled) !== Boolean(suppressExhaustion)) return false;
|
||||
const changes = effect.system?.changes ?? [];
|
||||
if (changes.length !== ROLL_TARGET_FIELD_KEYS.length) return false;
|
||||
return changes.every(
|
||||
(change) =>
|
||||
ROLL_TARGET_FIELD_KEYS.includes(change.key) &&
|
||||
change.type === "add" &&
|
||||
String(change.value) === String(penalty),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Actor} actor
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function pruneDuplicateExhaustionEffects(actor) {
|
||||
const effects = getExhaustionEffects(actor);
|
||||
if (effects.length <= 1) return;
|
||||
for (const effect of effects.slice(1)) {
|
||||
await effect.delete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Actor} actor
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function syncExhaustionEffectInner(actor) {
|
||||
if (actor.type !== "agent") return;
|
||||
|
||||
await pruneDuplicateExhaustionEffects(actor);
|
||||
|
||||
const { exhausted, exhaustedPenalty } = actor.system.physical;
|
||||
const suppressExhaustion = getEffectiveSuppressExhaustion(actor);
|
||||
const effect = getExhaustionEffect(actor);
|
||||
const penalty = exhausted
|
||||
? -1 * Math.abs(Number(exhaustedPenalty) || 0)
|
||||
: 0;
|
||||
|
||||
if (!exhausted) {
|
||||
if (effect) await effect.delete();
|
||||
return;
|
||||
}
|
||||
|
||||
const changes = buildExhaustionChanges(penalty);
|
||||
const disabled = Boolean(suppressExhaustion);
|
||||
|
||||
if (effect) {
|
||||
if (effectMatchesState(effect, penalty, suppressExhaustion)) return;
|
||||
await effect.update({ changes, disabled });
|
||||
return;
|
||||
}
|
||||
|
||||
const documentClass = getDocumentClass("ActiveEffect");
|
||||
await documentClass.create(
|
||||
{
|
||||
name: game.i18n.localize("DG.Physical.ExhaustionEffectName"),
|
||||
img: "systems/deltagreen/assets/icons/magic-shield.svg",
|
||||
transfer: false,
|
||||
disabled,
|
||||
changes,
|
||||
flags: { [DG.ID]: { exhaustion: true } },
|
||||
},
|
||||
{ parent: actor },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps the embedded exhaustion Active Effect aligned with physical exhaustion data.
|
||||
*
|
||||
* @param {Actor} actor
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function syncExhaustionEffect(actor) {
|
||||
const pending = syncLocks.get(actor);
|
||||
if (pending) return pending;
|
||||
|
||||
const promise = syncExhaustionEffectInner(actor).finally(() => {
|
||||
if (syncLocks.get(actor) === promise) syncLocks.delete(actor);
|
||||
});
|
||||
syncLocks.set(actor, promise);
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} data Actor update payload.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function updateTouchesExhaustionPhysical(data) {
|
||||
if (!data) return false;
|
||||
|
||||
const physical = foundry.utils.getProperty(data, "system.physical");
|
||||
if (physical) {
|
||||
if (
|
||||
"exhausted" in physical ||
|
||||
"exhaustedPenalty" in physical ||
|
||||
"suppressExhaustion" in physical
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
"system.physical.exhausted",
|
||||
"system.physical.exhaustedPenalty",
|
||||
"system.physical.suppressExhaustion",
|
||||
].some((key) => foundry.utils.hasProperty(data, key));
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* @param {object} params
|
||||
* @param {Actor} params.actor
|
||||
* @param {TokenDocument|null} [params.token]
|
||||
* @param {string} params.contentKey i18n key for message body.
|
||||
* @param {Record<string, string|number>} params.i18nData
|
||||
* @returns {Promise<ChatMessage>}
|
||||
*/
|
||||
export async function createAgentResourceChatMessage({
|
||||
actor,
|
||||
token = null,
|
||||
contentKey,
|
||||
i18nData,
|
||||
}) {
|
||||
return ChatMessage.create({
|
||||
speaker: ChatMessage.getSpeaker({
|
||||
actor,
|
||||
token,
|
||||
alias: actor.name,
|
||||
}),
|
||||
content: game.i18n.format(contentKey, i18nData),
|
||||
messageMode: game.settings.get("core", "messageMode"),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import DG from "../config.js";
|
||||
import { syncExhaustionEffect } from "./exhaustion-effect.js";
|
||||
|
||||
/**
|
||||
* @param {Actor} actor
|
||||
* @returns {ActiveEffect[]}
|
||||
*/
|
||||
function getStimulantEffects(actor) {
|
||||
return (
|
||||
actor.effects?.filter((effect) => effect.getFlag(DG.ID, "stimulant")) ?? []
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Actor} actor
|
||||
* @returns {ActiveEffect|undefined}
|
||||
*/
|
||||
function getStimulantEffect(actor) {
|
||||
return getStimulantEffects(actor).find((effect) => !effect.isSuppressed);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Actor} actor
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function hasActiveStimulantEffect(actor) {
|
||||
return getStimulantEffects(actor).some((effect) => !effect.isSuppressed);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ActiveEffect} effect
|
||||
* @returns {number}
|
||||
*/
|
||||
function getStimulantRemainingHours(effect) {
|
||||
effect.updateDuration();
|
||||
const { remaining, secondsRemaining, units } = effect.duration ?? {};
|
||||
if (Number.isFinite(remaining) && remaining > 0 && units === "hours") {
|
||||
return Math.ceil(remaining);
|
||||
}
|
||||
if (Number.isFinite(secondsRemaining) && secondsRemaining > 0) {
|
||||
const { calendar } = game.time;
|
||||
const components = calendar.difference(
|
||||
game.time.worldTime + secondsRemaining,
|
||||
game.time.worldTime,
|
||||
);
|
||||
const hours = calendar.componentsToUnit(components, "hour", {
|
||||
roundFn: "ceil",
|
||||
});
|
||||
return Math.max(0, Math.ceil(Math.abs(hours)));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persisted manual suppress or an active stimulant AE.
|
||||
* @param {Actor} actor
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function getEffectiveSuppressExhaustion(actor) {
|
||||
const persisted =
|
||||
actor._source?.system?.physical?.suppressExhaustion ?? false;
|
||||
return Boolean(persisted) || hasActiveStimulantEffect(actor);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} hours
|
||||
* @returns {object}
|
||||
*/
|
||||
function buildStimulantEffectData(hours) {
|
||||
return {
|
||||
name: game.i18n.localize("DG.Physical.StimulantsEffectName"),
|
||||
img: "systems/deltagreen/assets/icons/tablet.svg",
|
||||
transfer: false,
|
||||
disabled: false,
|
||||
start: { time: game.time.worldTime },
|
||||
duration: {
|
||||
value: hours,
|
||||
units: "hours",
|
||||
},
|
||||
changes: [],
|
||||
flags: { [DG.ID]: { stimulant: true } },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or refresh the stimulant AE; duration is max(remaining, newHours).
|
||||
* @param {Actor} actor
|
||||
* @param {number} newHours
|
||||
* @returns {Promise<number>} Hours applied on the stimulant AE.
|
||||
*/
|
||||
export async function applyStimulantEffect(actor, newHours) {
|
||||
if (actor.type !== "agent") return 0;
|
||||
|
||||
const rolledHours = Math.max(1, Math.ceil(Number(newHours) || 0));
|
||||
const existing = getStimulantEffect(actor);
|
||||
const remainingHours = existing ? getStimulantRemainingHours(existing) : 0;
|
||||
const hours = Math.max(remainingHours, rolledHours);
|
||||
|
||||
const data = buildStimulantEffectData(hours);
|
||||
|
||||
if (existing) {
|
||||
await existing.update({
|
||||
start: data.start,
|
||||
duration: data.duration,
|
||||
changes: data.changes,
|
||||
disabled: false,
|
||||
});
|
||||
return hours;
|
||||
}
|
||||
|
||||
const documentClass = getDocumentClass("ActiveEffect");
|
||||
await documentClass.create(data, { parent: actor });
|
||||
return hours;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Actor} actor
|
||||
* @returns {ActiveEffect[]}
|
||||
*/
|
||||
function getExpiredStimulantEffects(actor) {
|
||||
return getStimulantEffects(actor).filter(
|
||||
(effect) => effect.duration?.expired,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove stimulant AEs that have expired.
|
||||
* @param {Actor} actor
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function pruneExpiredStimulantEffects(actor) {
|
||||
if (actor.type !== "agent") return;
|
||||
const expired = getExpiredStimulantEffects(actor);
|
||||
if (!expired.length) return;
|
||||
await actor.deleteEmbeddedDocuments(
|
||||
"ActiveEffect",
|
||||
expired.map((effect) => effect.id),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all stimulant AEs on the actor (e.g. after a full rest).
|
||||
* @param {Actor} actor
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function clearStimulantEffects(actor) {
|
||||
if (actor.type !== "agent") return;
|
||||
const effects = getStimulantEffects(actor);
|
||||
if (!effects.length) return;
|
||||
await actor.deleteEmbeddedDocuments(
|
||||
"ActiveEffect",
|
||||
effects.map((effect) => effect.id),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function pruneAllAgentsExpiredStimulants() {
|
||||
if (!game.user.isActiveGM) return;
|
||||
for (const actor of game.actors) {
|
||||
if (actor.type !== "agent") continue;
|
||||
await pruneExpiredStimulantEffects(actor);
|
||||
await syncExhaustionEffect(actor);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user