Implemented Active Effects.

Used AE to implement exhaustion, stimulants, and rest.
This commit is contained in:
Aaron Lutomski
2026-05-29 21:07:33 -07:00
parent 67b70268b6
commit bf4ca9bf88
65 changed files with 5830 additions and 3480 deletions
@@ -0,0 +1,55 @@
const { ArrayField, SchemaField, StringField, NumberField, AnyField } =
foundry.data.fields;
/**
* Delta Green Active Effect system data — matches core change schema with `final` as the default phase.
* @extends {foundry.data.ActiveEffectTypeDataModel}
*/
export default class DGActiveEffectTypeDataModel extends foundry.data
.ActiveEffectTypeDataModel {
/** @override */
static defineSchema() {
return {
changes: new ArrayField(
new SchemaField({
key: new StringField({ required: true }),
type: new StringField({
required: true,
blank: false,
initial: "add",
validate: DGActiveEffectTypeDataModel.#validateType,
}),
value: new AnyField({
required: true,
nullable: true,
serializable: true,
initial: "",
}),
phase: new StringField({
required: true,
blank: false,
initial: "final",
}),
priority: new NumberField(),
}),
),
};
}
/**
* @param {string} type
* @returns {true}
*/
static #validateType(type) {
if (type.length < 3) throw new Error("must be at least three characters long");
if (
!/^custom\.-?\d+$/.test(type) &&
!type.split(".").every((s) => /^[a-z0-9]+$/i.test(s))
) {
throw new Error(
'A change type must either be a sequence of dot-delimited, alpha-numeric substrings or of the form "custom.{number}"',
);
}
return true;
}
}
+34 -19
View File
@@ -2,13 +2,11 @@ import HumanSkillsActorData from "./base/human-skills.js";
import CharacterData from "./base/character.js";
import DGHTMLField from "../fields/html-content-field.js";
import {
calculateHealthMax,
calculateMeleeDamageBonusFormula,
clampSanityRitualValue,
agentResourceField,
} from "./base/general.js";
import {
computeEquippedArmorProtection,
initializeSanityIfUnset,
prepareAgentSkillFlags,
prepareBreakingPointHit,
prepareSanityAdaptations,
prepareStatisticsX5,
removeLegacyRitualSkill,
@@ -21,13 +19,19 @@ export default class AgentData extends CharacterData {
static defineSchema() {
const superData = super.defineSchema();
const { health: _health, wp: _wp, ...restSuper } = superData;
return {
...superData,
...restSuper,
health: agentResourceField(10, 10),
wp: agentResourceField(10, 10),
...HumanSkillsActorData.defineSchema(),
schemaVersion: new NumberField({ initial: 2.0 }),
sanity: new SchemaField({
value: new NumberField({ initial: 100 }),
currentBreakingPoint: new NumberField({ initial: 101 }),
value: new NumberField({ initial: 100, min: 0, integer: true }),
max: new NumberField({ initial: 99, min: 0, integer: true }),
currentBreakingPoint: new NumberField({ initial: 101, integer: true }),
maxBonus: new NumberField({ initial: 0, integer: true }),
adaptations: new SchemaField({
violence: new SchemaField({
incident1: new BooleanField({ initial: false }),
@@ -41,12 +45,18 @@ export default class AgentData extends CharacterData {
}),
}),
}),
rollTarget: new SchemaField({
allSkills: new NumberField({ initial: 0, integer: true }),
sanity: new NumberField({ initial: 0, integer: true }),
statistics: new NumberField({ initial: 0, integer: true }),
}),
physical: new SchemaField({
description: DGHTMLField(),
wounds: new StringField({ initial: "" }),
firstAidAttempted: new BooleanField({ initial: false }),
exhausted: new BooleanField({ initial: false }),
exhaustedPenalty: new NumberField({ initial: -20 }),
suppressExhaustion: new BooleanField({ initial: false }),
}),
biography: new SchemaField({
profession: new StringField({ initial: "" }),
@@ -66,24 +76,29 @@ export default class AgentData extends CharacterData {
}
/** @inheritdoc */
prepareDerivedData() {
prepareStatisticsX5(this.statistics);
removeLegacyRitualSkill(this);
clampSanityRitualValue(this.sanity);
prepareAgentSkillFlags(this.skills);
prepareBaseData() {
// rollTarget is modified by add-type Active Effects each prepare cycle.
this.rollTarget.allSkills = 0;
this.rollTarget.sanity = 0;
this.rollTarget.statistics = 0;
}
this.wp.max = this.statistics.pow.value;
this.health.max = calculateHealthMax(this.statistics);
/** @inheritdoc */
prepareDerivedData() {
// Pre–final-AE setup only. Post-AE derived fields (max, x5 refresh,
// breaking point, etc.) are applied in DeltaGreenActor.prepareData via
// refreshDerivedAfterActiveEffects after the final Active Effect phase.
prepareStatisticsX5(
this.statistics,
this.parent?._source?.system?.statistics,
);
removeLegacyRitualSkill(this);
initializeSanityIfUnset(this.sanity, this.statistics);
this.sanity.max = 99 - this.skills.unnatural.proficiency;
prepareSanityAdaptations(this.sanity.adaptations);
prepareBreakingPointHit(this.sanity);
this.health.protection = computeEquippedArmorProtection(
this.parent.items,
);
this.statistics.str.meleeDamageBonusFormula =
calculateMeleeDamageBonusFormula(this.statistics.str.value);
if (this.physical.exhaustedPenalty > 0) {
this.physical.exhaustedPenalty =
+21 -5
View File
@@ -3,7 +3,12 @@ const { SchemaField, NumberField, StringField, BooleanField } =
export function skillField(initialValue, label) {
return new SchemaField({
proficiency: new NumberField({ initial: initialValue }),
proficiency: new NumberField({
initial: initialValue,
min: 0,
max: 99,
integer: true,
}),
label: new StringField({ initial: label }),
failure: new BooleanField({ initial: false }),
});
@@ -11,15 +16,26 @@ export function skillField(initialValue, label) {
export function resourceField(initialValue, initialMax) {
return new SchemaField({
min: new NumberField({ initial: 0 }),
value: new NumberField({ initial: initialValue }),
max: new NumberField({ initial: initialMax }),
min: new NumberField({ initial: 0, min: 0, integer: true }),
value: new NumberField({ initial: initialValue, min: 0, integer: true }),
max: new NumberField({ initial: initialMax, min: 0, integer: true }),
});
}
/** Agent resources that support Active Effect maximum bonuses. */
export function agentResourceField(initialValue, initialMax) {
return new SchemaField({
min: new NumberField({ initial: 0, min: 0, integer: true }),
value: new NumberField({ initial: initialValue, min: 0, integer: true }),
max: new NumberField({ initial: initialMax, min: 0, integer: true }),
maxBonus: new NumberField({ initial: 0, integer: true }),
});
}
export function statisticsField(initialValue) {
return new SchemaField({
value: new NumberField({ initial: initialValue }),
value: new NumberField({ initial: initialValue, min: 0, integer: true }),
modifier: new NumberField({ initial: 0, integer: true }),
distinguishing_feature: new StringField({ initial: "" }),
});
}
+1 -1
View File
@@ -45,7 +45,7 @@ export default class HumanSkillsActorData extends TypeDataModel {
swim: skillField(20, "Swim"),
unarmed_combat: skillField(40, "Unarmed Combat"),
unnatural: new SchemaField({
proficiency: new NumberField({ initial: 0 }),
proficiency: new NumberField({ initial: 0, min: 0, max: 99, integer: true }),
label: new StringField({ initial: "Unnatural" }),
}),
}),
+24 -7
View File
@@ -5,6 +5,7 @@ import {
calculateHealthMax,
calculateMeleeDamageBonusFormula,
clampSanityRitualValue,
cleanDerivedNumber,
computeEquippedArmorProtection,
initializeSanityIfUnset,
prepareStatisticsX5,
@@ -24,8 +25,9 @@ export default class NPCData extends CharacterData {
...HumanSkillsActorData.defineSchema(),
schemaVersion: new NumberField({ initial: 2.0 }),
sanity: new SchemaField({
value: new NumberField({ initial: 100 }),
currentBreakingPoint: new NumberField({ initial: 101 }),
value: new NumberField({ initial: 100, min: 0, integer: true }),
max: new NumberField({ initial: 99, min: 0, integer: true }),
currentBreakingPoint: new NumberField({ initial: 101, integer: true }),
}),
biography: new SchemaField({
profession: new StringField({ initial: "" }),
@@ -38,13 +40,28 @@ export default class NPCData extends CharacterData {
/** @inheritdoc */
prepareDerivedData() {
prepareStatisticsX5(this.statistics);
const sourceStatistics = this.parent?._source?.system?.statistics;
prepareStatisticsX5(this.statistics, sourceStatistics);
initializeSanityIfUnset(this.sanity, this.statistics);
this.sanity.max = 99 - this.skills.unnatural.proficiency;
this.wp.max = this.statistics.pow.value;
this.health.max = calculateHealthMax(this.statistics);
this.sanity.max = cleanDerivedNumber(
this,
"sanity.max",
99 - this.skills.unnatural.proficiency,
);
this.wp.max = cleanDerivedNumber(
this,
"wp.max",
this.statistics.pow.effectiveValue ?? this.statistics.pow.value,
);
this.health.max = cleanDerivedNumber(
this,
"health.max",
calculateHealthMax(this.statistics, sourceStatistics),
);
this.statistics.str.meleeDamageBonusFormula =
calculateMeleeDamageBonusFormula(this.statistics.str.value);
calculateMeleeDamageBonusFormula(
this.statistics.str.effectiveValue ?? this.statistics.str.value,
);
removeLegacyRitualSkill(this);
clampSanityRitualValue(this.sanity);
this.health.protection = computeEquippedArmorProtection(this.parent.items);
+13 -3
View File
@@ -3,6 +3,7 @@ import CharacterData from "./base/character.js";
import DGHTMLField from "../fields/html-content-field.js";
import {
calculateHealthMax,
cleanDerivedNumber,
computeEquippedArmorProtection,
prepareStatisticsX5,
removeLegacyRitualSkill,
@@ -33,14 +34,23 @@ export default class UnnaturalData extends CharacterData {
/** @inheritdoc */
prepareDerivedData() {
prepareStatisticsX5(this.statistics);
const sourceStatistics = this.parent?._source?.system?.statistics;
prepareStatisticsX5(this.statistics, sourceStatistics);
if (this.wp.maxNeedsUpdate) {
this.wp.max = this.statistics.pow.value;
this.wp.max = cleanDerivedNumber(
this,
"wp.max",
this.statistics.pow.effectiveValue ?? this.statistics.pow.value,
);
}
if (this.health.maxNeedsUpdate) {
this.health.max = calculateHealthMax(this.statistics);
this.health.max = cleanDerivedNumber(
this,
"health.max",
calculateHealthMax(this.statistics, sourceStatistics),
);
}
removeLegacyRitualSkill(this);
+1
View File
@@ -9,6 +9,7 @@ export default class MotivationItemData extends foundry.abstract.TypeDataModel {
disorder: new StringField({ initial: "" }),
crossedOut: new BooleanField({ initial: false }),
disorderCured: new BooleanField({ initial: false }),
acuteEpisode: new BooleanField({ initial: false }),
};
}
}