refactor: resturcture files so that lincensing info can be bundled properly
This commit is contained in:
parent
699ba74840
commit
1aa284311f
484 changed files with 119 additions and 179 deletions
443
src/actor/actor.ts
Normal file
443
src/actor/actor.ts
Normal file
|
@ -0,0 +1,443 @@
|
|||
// SPDX-FileCopyrightText: 2021 Johannes Loher
|
||||
// SPDX-FileCopyrightText: 2021 Oliver RÜmpelein
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { ModifiableDataBaseTotal } from "../common/common-data";
|
||||
import { DS4 } from "../config";
|
||||
import { getGame } from "../helpers";
|
||||
import { DS4Item } from "../item/item";
|
||||
import { DS4ArmorDataProperties, DS4ShieldDataProperties } from "../item/item-data-properties";
|
||||
import { ItemType } from "../item/item-data-source";
|
||||
import { createCheckRoll } from "../rolls/check-factory";
|
||||
import { Check } from "./actor-data-properties";
|
||||
import { isAttribute, isTrait } from "./actor-data-source";
|
||||
|
||||
declare global {
|
||||
interface DocumentClassConfig {
|
||||
Actor: typeof DS4Actor;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The Actor class for DS4
|
||||
*/
|
||||
export class DS4Actor extends Actor {
|
||||
/** @override */
|
||||
prepareData(): void {
|
||||
this.data.reset();
|
||||
this.prepareBaseData();
|
||||
this.prepareEmbeddedDocuments();
|
||||
this.applyActiveEffectsToBaseData();
|
||||
this.prepareDerivedData();
|
||||
this.applyActiveEffectsToDerivedData();
|
||||
this.prepareFinalDerivedData();
|
||||
}
|
||||
|
||||
/** @override */
|
||||
prepareBaseData(): void {
|
||||
const data = this.data;
|
||||
|
||||
data.data.rolling = {
|
||||
minimumFumbleResult: 20,
|
||||
maximumCoupResult: 1,
|
||||
};
|
||||
|
||||
const attributes = data.data.attributes;
|
||||
Object.values(attributes).forEach(
|
||||
(attribute: ModifiableDataBaseTotal<number>) => (attribute.total = attribute.base + attribute.mod),
|
||||
);
|
||||
|
||||
const traits = data.data.traits;
|
||||
Object.values(traits).forEach(
|
||||
(trait: ModifiableDataBaseTotal<number>) => (trait.total = trait.base + trait.mod),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
* We override this with an empty implementation because we have our own custom way of applying
|
||||
* {@link ActiveEffect}s and {@link Actor#prepareEmbeddedDocuments} calls this.
|
||||
*/
|
||||
applyActiveEffects(): void {
|
||||
return;
|
||||
}
|
||||
|
||||
applyActiveEffectsToBaseData(): void {
|
||||
// reset overrides because our variant of applying active effects does not set them, it only adds overrides
|
||||
this.overrides = {};
|
||||
this.applyActiveEffectsFiltered(
|
||||
(change) =>
|
||||
!this.derivedDataProperties.includes(change.key) &&
|
||||
!this.finalDerivedDataProperties.includes(change.key),
|
||||
);
|
||||
}
|
||||
|
||||
applyActiveEffectsToDerivedData(): void {
|
||||
this.applyActiveEffectsFiltered((change) => this.derivedDataProperties.includes(change.key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply ActiveEffectChanges to the Actor data which are caused by ActiveEffects and satisfy the given predicate.
|
||||
*
|
||||
* @param predicate - The predicate that ActiveEffectChanges need to satisfy in order to be applied
|
||||
*/
|
||||
applyActiveEffectsFiltered(predicate: (change: foundry.data.ActiveEffectData["changes"][number]) => boolean): void {
|
||||
const overrides: Record<string, unknown> = {};
|
||||
|
||||
// Organize non-disabled and -surpressed effects by their application priority
|
||||
const changes: (foundry.data.ActiveEffectData["changes"][number] & { effect: ActiveEffect })[] =
|
||||
this.effects.reduce(
|
||||
(changes: (foundry.data.ActiveEffectData["changes"][number] & { effect: ActiveEffect })[], e) => {
|
||||
if (e.data.disabled || e.isSurpressed) return changes;
|
||||
|
||||
const newChanges = e.data.changes.filter(predicate).flatMap((c) => {
|
||||
const changeSource = c.toObject();
|
||||
changeSource.priority = changeSource.priority ?? changeSource.mode * 10;
|
||||
return Array(e.factor).fill({ ...changeSource, effect: e });
|
||||
});
|
||||
|
||||
return changes.concat(newChanges);
|
||||
},
|
||||
[],
|
||||
);
|
||||
changes.sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
|
||||
|
||||
// Apply all changes
|
||||
for (const change of changes) {
|
||||
const result = change.effect.apply(this, change);
|
||||
if (result !== null) overrides[change.key] = result;
|
||||
}
|
||||
|
||||
// Expand the set of final overrides
|
||||
this.overrides = foundry.utils.expandObject({ ...foundry.utils.flattenObject(this.overrides), ...overrides });
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply transformations to the Actor data after effects have been applied to the base data.
|
||||
* @override
|
||||
*/
|
||||
prepareDerivedData(): void {
|
||||
this.prepareCombatValues();
|
||||
this.prepareChecks();
|
||||
}
|
||||
|
||||
/**
|
||||
* The list of properties that are derived from others, given in dot notation.
|
||||
*/
|
||||
get derivedDataProperties(): Array<string> {
|
||||
const combatValueProperties = Object.keys(DS4.i18n.combatValues).map(
|
||||
(combatValue) => `data.combatValues.${combatValue}.total`,
|
||||
);
|
||||
const checkProperties = Object.keys(DS4.i18n.checks)
|
||||
.filter((check) => check !== "defend")
|
||||
.map((check) => `data.checks.${check}`);
|
||||
return combatValueProperties.concat(checkProperties);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply final transformations to the Actor data after all effects have been applied.
|
||||
*/
|
||||
prepareFinalDerivedData(): void {
|
||||
Object.values(this.data.data.attributes).forEach(
|
||||
(attribute: ModifiableDataBaseTotal<number>) => (attribute.total = Math.ceil(attribute.total)),
|
||||
);
|
||||
Object.values(this.data.data.traits).forEach(
|
||||
(trait: ModifiableDataBaseTotal<number>) => (trait.total = Math.ceil(trait.total)),
|
||||
);
|
||||
Object.entries(this.data.data.combatValues)
|
||||
.filter(([key]) => key !== "movement")
|
||||
.map(([, value]) => value)
|
||||
.forEach(
|
||||
(combatValue: ModifiableDataBaseTotal<number>) => (combatValue.total = Math.ceil(combatValue.total)),
|
||||
);
|
||||
(Object.keys(this.data.data.checks) as (keyof typeof this.data.data.checks)[]).forEach((key) => {
|
||||
this.data.data.checks[key] = Math.ceil(this.data.data.checks[key]);
|
||||
});
|
||||
|
||||
this.data.data.combatValues.hitPoints.max = this.data.data.combatValues.hitPoints.total;
|
||||
this.data.data.checks.defend = this.data.data.combatValues.defense.total;
|
||||
if (this.data.type === "character") {
|
||||
this.data.data.slayerPoints.max = 3;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The list of properties that are completely derived (i.e. {@link ActiveEffect}s cannot be applied to them),
|
||||
* given in dot notation.
|
||||
*/
|
||||
get finalDerivedDataProperties(): string[] {
|
||||
return ["data.combatValues.hitPoints.max", "data.checks.defend"].concat(
|
||||
this.data.type === "character" ? ["data.slayerPoints.max"] : [],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The list of item types that can be owned by this actor.
|
||||
*/
|
||||
get ownableItemTypes(): Array<ItemType> {
|
||||
switch (this.data.type) {
|
||||
case "character":
|
||||
return [
|
||||
"weapon",
|
||||
"armor",
|
||||
"shield",
|
||||
"equipment",
|
||||
"loot",
|
||||
"spell",
|
||||
"talent",
|
||||
"racialAbility",
|
||||
"language",
|
||||
"alphabet",
|
||||
];
|
||||
case "creature":
|
||||
return ["weapon", "armor", "shield", "equipment", "loot", "spell", "specialCreatureAbility"];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not the given item type can be owned by the actor.
|
||||
* @param itemType - The item type to check
|
||||
*/
|
||||
canOwnItemType(itemType: ItemType): boolean {
|
||||
return this.ownableItemTypes.includes(itemType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the combat values of the actor.
|
||||
*/
|
||||
protected prepareCombatValues(): void {
|
||||
const data = this.data.data;
|
||||
const armorValueOfEquippedItems = this.calculateArmorValueOfEquippedItems();
|
||||
const spellMalusOfEquippedItems = this.calculateSpellMaluesOfEquippedItems();
|
||||
|
||||
data.combatValues.hitPoints.base = data.attributes.body.total + data.traits.constitution.total + 10;
|
||||
data.combatValues.defense.base =
|
||||
data.attributes.body.total + data.traits.constitution.total + armorValueOfEquippedItems;
|
||||
data.combatValues.initiative.base = data.attributes.mobility.total + data.traits.agility.total;
|
||||
data.combatValues.movement.base = data.attributes.mobility.total / 2 + 1;
|
||||
data.combatValues.meleeAttack.base = data.attributes.body.total + data.traits.strength.total;
|
||||
data.combatValues.rangedAttack.base = data.attributes.mobility.total + data.traits.dexterity.total;
|
||||
data.combatValues.spellcasting.base =
|
||||
data.attributes.mind.total + data.traits.aura.total - spellMalusOfEquippedItems;
|
||||
data.combatValues.targetedSpellcasting.base =
|
||||
data.attributes.mind.total + data.traits.dexterity.total - spellMalusOfEquippedItems;
|
||||
|
||||
Object.values(data.combatValues).forEach(
|
||||
(combatValue: ModifiableDataBaseTotal<number>) => (combatValue.total = combatValue.base + combatValue.mod),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the total armor value of the equipped items.
|
||||
*/
|
||||
protected calculateArmorValueOfEquippedItems(): number {
|
||||
return this.getEquippedItemsWithArmor()
|
||||
.map((item) => item.data.data.armorValue)
|
||||
.reduce((a, b) => a + b, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the spell malus from equipped items.
|
||||
*/
|
||||
protected calculateSpellMaluesOfEquippedItems(): number {
|
||||
return this.getEquippedItemsWithArmor()
|
||||
.filter(
|
||||
(item) =>
|
||||
!(item.data.type === "armor" && ["cloth", "natural"].includes(item.data.data.armorMaterialType)),
|
||||
)
|
||||
.map((item) => item.data.data.armorValue)
|
||||
.reduce((a, b) => a + b, 0);
|
||||
}
|
||||
|
||||
private getEquippedItemsWithArmor() {
|
||||
return this.items
|
||||
.filter(
|
||||
(item): item is DS4Item & { data: DS4ArmorDataProperties | DS4ShieldDataProperties } =>
|
||||
item.data.type === "armor" || item.data.type === "shield",
|
||||
)
|
||||
.filter((item) => item.data.data.equipped);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the check target numbers of checks for the actor.
|
||||
*/
|
||||
protected prepareChecks(): void {
|
||||
const data = this.data.data;
|
||||
data.checks = {
|
||||
appraise: data.attributes.mind.total + data.traits.intellect.total,
|
||||
changeSpell: data.attributes.mind.total + data.traits.intellect.total,
|
||||
climb: data.attributes.mobility.total + data.traits.strength.total,
|
||||
communicate: data.attributes.mind.total + data.traits.dexterity.total,
|
||||
decipherScript: data.attributes.mind.total + data.traits.intellect.total,
|
||||
defend: 0, // assigned in prepareFinalDerivedData as it must always match data.combatValues.defense.total and is not changeable by effects
|
||||
defyPoison: data.attributes.body.total + data.traits.constitution.total,
|
||||
disableTraps: data.attributes.mind.total + data.traits.dexterity.total,
|
||||
featOfStrength: data.attributes.body.total + data.traits.strength.total,
|
||||
flirt: data.attributes.mind.total + data.traits.aura.total,
|
||||
haggle: data.attributes.mind.total + Math.max(data.traits.intellect.total, data.traits.intellect.total),
|
||||
hide: data.attributes.mobility.total + data.traits.agility.total,
|
||||
identifyMagic: data.attributes.mind.total + data.traits.intellect.total,
|
||||
jump: data.attributes.mobility.total + data.traits.agility.total,
|
||||
knowledge: data.attributes.mind.total + data.traits.intellect.total,
|
||||
openLock: data.attributes.mind.total + data.traits.dexterity.total,
|
||||
perception: Math.max(data.attributes.mind.total + data.traits.intellect.total, 8),
|
||||
pickPocket: data.attributes.mobility.total + data.traits.dexterity.total,
|
||||
readTracks: data.attributes.mind.total + data.traits.intellect.total,
|
||||
resistDisease: data.attributes.body.total + data.traits.constitution.total,
|
||||
ride: data.attributes.mobility.total + Math.max(data.traits.agility.total, data.traits.aura.total),
|
||||
search: Math.max(data.attributes.mind.total + data.traits.intellect.total, 8),
|
||||
senseMagic: data.attributes.mind.total + data.traits.aura.total,
|
||||
sneak: data.attributes.mobility.total + data.traits.agility.total,
|
||||
startFire: data.attributes.mind.total + data.traits.dexterity.total,
|
||||
swim: data.attributes.mobility.total + data.traits.strength.total,
|
||||
wakeUp: data.attributes.mind.total + data.traits.intellect.total,
|
||||
workMechanism:
|
||||
data.attributes.mind.total + Math.max(data.traits.dexterity.total, data.traits.intellect.total),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle how changes to a Token attribute bar are applied to the Actor.
|
||||
* This only differs from the base implementation by also allowing negative values.
|
||||
* @override
|
||||
*/
|
||||
async modifyTokenAttribute(
|
||||
attribute: string,
|
||||
value: number,
|
||||
isDelta = false,
|
||||
isBar = true,
|
||||
): Promise<this | undefined> {
|
||||
const current = foundry.utils.getProperty(this.data.data, attribute);
|
||||
|
||||
// Determine the updates to make to the actor data
|
||||
let updates: Record<string, number>;
|
||||
if (isBar) {
|
||||
if (isDelta) value = Math.min(Number(current.value) + value, current.max);
|
||||
updates = { [`data.${attribute}.value`]: value };
|
||||
} else {
|
||||
if (isDelta) value = Number(current) + value;
|
||||
updates = { [`data.${attribute}`]: value };
|
||||
}
|
||||
|
||||
// Call a hook to handle token resource bar updates
|
||||
const allowed = Hooks.call("modifyTokenAttribute", { attribute, value, isDelta, isBar }, updates);
|
||||
return allowed !== false ? this.update(updates) : this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Roll for a given check.
|
||||
* @param check - The check to perform
|
||||
* @param options - Additional options to customize the roll
|
||||
*/
|
||||
async rollCheck(
|
||||
check: Check,
|
||||
options: { speaker?: { token?: TokenDocument; alias?: string } } = {},
|
||||
): Promise<void> {
|
||||
const speaker = ChatMessage.getSpeaker({ actor: this, ...options.speaker });
|
||||
await createCheckRoll(this.data.data.checks[check], {
|
||||
rollMode: getGame().settings.get("core", "rollMode"),
|
||||
maximumCoupResult: this.data.data.rolling.maximumCoupResult,
|
||||
minimumFumbleResult: this.data.data.rolling.minimumFumbleResult,
|
||||
flavor: "DS4.ActorCheckFlavor",
|
||||
flavorData: { actor: speaker.alias ?? this.name, check: DS4.i18nKeys.checks[check] },
|
||||
speaker,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Roll a generic check. A dialog is presented to select the combination of
|
||||
* Attribute and Trait to perform the check against.
|
||||
* @param options - Additional options to customize the roll
|
||||
*/
|
||||
async rollGenericCheck(options: { speaker?: { token?: TokenDocument; alias?: string } } = {}): Promise<void> {
|
||||
const attributeAndTrait = await this.selectAttributeAndTrait();
|
||||
if (!attributeAndTrait) {
|
||||
return;
|
||||
}
|
||||
const { attribute, trait } = attributeAndTrait;
|
||||
const checkTargetNumber = this.data.data.attributes[attribute].total + this.data.data.traits[trait].total;
|
||||
const speaker = ChatMessage.getSpeaker({ actor: this, ...options.speaker });
|
||||
await createCheckRoll(checkTargetNumber, {
|
||||
rollMode: getGame().settings.get("core", "rollMode"),
|
||||
maximumCoupResult: this.data.data.rolling.maximumCoupResult,
|
||||
minimumFumbleResult: this.data.data.rolling.minimumFumbleResult,
|
||||
flavor: "DS4.ActorGenericCheckFlavor",
|
||||
flavorData: {
|
||||
actor: speaker.alias ?? this.name,
|
||||
attribute: DS4.i18n.attributes[attribute],
|
||||
trait: DS4.i18n.traits[trait],
|
||||
},
|
||||
speaker,
|
||||
});
|
||||
}
|
||||
|
||||
protected async selectAttributeAndTrait(): Promise<{
|
||||
attribute: keyof typeof DS4.i18n.attributes;
|
||||
trait: keyof typeof DS4.i18n.traits;
|
||||
} | null> {
|
||||
const attributeIdentifier = "attribute-trait-selection-attribute";
|
||||
const traitIdentifier = "attribute-trait-selection-trait";
|
||||
return Dialog.prompt({
|
||||
title: getGame().i18n.localize("DS4.DialogAttributeTraitSelection"),
|
||||
content: await renderTemplate("systems/ds4/templates/dialogs/simple-select-form.hbs", {
|
||||
selects: [
|
||||
{
|
||||
label: getGame().i18n.localize("DS4.Attribute"),
|
||||
identifier: attributeIdentifier,
|
||||
options: Object.fromEntries(
|
||||
(Object.entries(DS4.i18n.attributes) as [keyof typeof DS4.i18n.attributes, string][]).map(
|
||||
([attribute, translation]) => [
|
||||
attribute,
|
||||
`${translation} (${this.data.data.attributes[attribute].total})`,
|
||||
],
|
||||
),
|
||||
),
|
||||
},
|
||||
{
|
||||
label: getGame().i18n.localize("DS4.Trait"),
|
||||
identifier: traitIdentifier,
|
||||
options: Object.fromEntries(
|
||||
(Object.entries(DS4.i18n.traits) as [keyof typeof DS4.i18n.traits, string][]).map(
|
||||
([trait, translation]) => [
|
||||
trait,
|
||||
`${translation} (${this.data.data.traits[trait].total})`,
|
||||
],
|
||||
),
|
||||
),
|
||||
},
|
||||
],
|
||||
}),
|
||||
label: getGame().i18n.localize("DS4.GenericOkButton"),
|
||||
callback: (html) => {
|
||||
const selectedAttribute = html.find(`#${attributeIdentifier}`).val();
|
||||
if (!isAttribute(selectedAttribute)) {
|
||||
throw new Error(
|
||||
getGame().i18n.format("DS4.ErrorUnexpectedAttribute", {
|
||||
actualAttribute: selectedAttribute,
|
||||
expectedTypes: Object.keys(DS4.i18n.attributes)
|
||||
.map((attribute) => `'${attribute}'`)
|
||||
.join(", "),
|
||||
}),
|
||||
);
|
||||
}
|
||||
const selectedTrait = html.find(`#${traitIdentifier}`).val();
|
||||
if (!isTrait(selectedTrait)) {
|
||||
throw new Error(
|
||||
getGame().i18n.format("DS4.ErrorUnexpectedTrait", {
|
||||
actualTrait: selectedTrait,
|
||||
expectedTypes: Object.keys(DS4.i18n.traits)
|
||||
.map((attribute) => `'${attribute}'`)
|
||||
.join(", "),
|
||||
}),
|
||||
);
|
||||
}
|
||||
return {
|
||||
attribute: selectedAttribute,
|
||||
trait: selectedTrait,
|
||||
};
|
||||
},
|
||||
rejectClose: false,
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue