mirror of
https://github.com/Foundryborne/daggerheart.git
synced 2026-04-22 07:23:37 +02:00
Merged with v14-Dev
This commit is contained in:
commit
88be00567e
650 changed files with 6323 additions and 4508 deletions
|
|
@ -26,23 +26,23 @@ export default class DHAttackAction extends DHDamageAction {
|
|||
return {
|
||||
value: {
|
||||
multiplier: 'prof',
|
||||
dice: this.item?.system?.attack.damage.parts[0].value.dice,
|
||||
bonus: this.item?.system?.attack.damage.parts[0].value.bonus ?? 0
|
||||
dice: this.item?.system?.attack.damage.parts.hitPoints.value.dice,
|
||||
bonus: this.item?.system?.attack.damage.parts.hitPoints.value.bonus ?? 0
|
||||
},
|
||||
type: this.item?.system?.attack.damage.parts[0].type,
|
||||
type: this.item?.system?.attack.damage.parts.hitPoints.type,
|
||||
base: true
|
||||
};
|
||||
}
|
||||
|
||||
get damageFormula() {
|
||||
const hitPointsPart = this.damage.parts.find(x => x.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id);
|
||||
const hitPointsPart = this.damage.parts.hitPoints;
|
||||
if (!hitPointsPart) return '0';
|
||||
|
||||
return hitPointsPart.value.getFormula();
|
||||
}
|
||||
|
||||
get altDamageFormula() {
|
||||
const hitPointsPart = this.damage.parts.find(x => x.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id);
|
||||
const hitPointsPart = this.damage.parts.hitPoints;
|
||||
if (!hitPointsPart) return '0';
|
||||
|
||||
return hitPointsPart.valueAlt.getFormula();
|
||||
|
|
|
|||
|
|
@ -355,11 +355,11 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
|
|||
}
|
||||
|
||||
get hasDamage() {
|
||||
return this.damage?.parts?.length && this.type !== 'healing';
|
||||
return !foundry.utils.isEmpty(this.damage?.parts) && this.type !== 'healing';
|
||||
}
|
||||
|
||||
get hasHealing() {
|
||||
return this.damage?.parts?.length && this.type === 'healing';
|
||||
return !foundry.utils.isEmpty(this.damage?.parts) && this.type === 'healing';
|
||||
}
|
||||
|
||||
get hasSave() {
|
||||
|
|
@ -379,6 +379,15 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
|
|||
|
||||
return tags;
|
||||
}
|
||||
|
||||
static migrateData(source) {
|
||||
if (source.damage?.parts && Array.isArray(source.damage.parts)) {
|
||||
source.damage.parts = source.damage.parts.reduce((acc, part) => {
|
||||
acc[part.applyTo] = part;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ResourceUpdateMap extends Map {
|
||||
|
|
|
|||
|
|
@ -2,84 +2,4 @@ import DHBaseAction from './baseAction.mjs';
|
|||
|
||||
export default class DhBeastformAction extends DHBaseAction {
|
||||
static extraSchemas = [...super.extraSchemas, 'beastform'];
|
||||
|
||||
/* async use(event, options) {
|
||||
const beastformConfig = this.prepareBeastformConfig();
|
||||
|
||||
const abort = await this.handleActiveTransformations();
|
||||
if (abort) return;
|
||||
|
||||
const calcCosts = game.system.api.fields.ActionFields.CostField.calcCosts.call(this, this.cost);
|
||||
const hasCost = game.system.api.fields.ActionFields.CostField.hasCost.call(this, calcCosts);
|
||||
if (!hasCost) {
|
||||
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.insufficientResources'));
|
||||
return;
|
||||
}
|
||||
|
||||
const { selected, evolved, hybrid } = await BeastformDialog.configure(beastformConfig, this.item);
|
||||
if (!selected) return;
|
||||
|
||||
const result = await super.use(event, options);
|
||||
if (!result) return;
|
||||
|
||||
await this.transform(selected, evolved, hybrid);
|
||||
}
|
||||
|
||||
prepareBeastformConfig(config) {
|
||||
const settingsTiers = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers;
|
||||
const actorLevel = this.actor.system.levelData.level.current;
|
||||
const actorTier =
|
||||
Object.values(settingsTiers).find(
|
||||
tier => actorLevel >= tier.levels.start && actorLevel <= tier.levels.end
|
||||
) ?? 1;
|
||||
|
||||
return {
|
||||
tierLimit: this.beastform.tierAccess.exact ?? actorTier
|
||||
};
|
||||
}
|
||||
|
||||
async transform(selectedForm, evolvedData, hybridData) {
|
||||
const formData = evolvedData?.form ? evolvedData.form.toObject() : selectedForm.toObject();
|
||||
const beastformEffect = formData.effects.find(x => x.type === 'beastform');
|
||||
if (!beastformEffect) {
|
||||
ui.notifications.error('DAGGERHEART.UI.Notifications.beastformMissingEffect');
|
||||
return;
|
||||
}
|
||||
|
||||
if (evolvedData?.form) {
|
||||
const evolvedForm = selectedForm.effects.find(x => x.type === 'beastform');
|
||||
if (!evolvedForm) {
|
||||
ui.notifications.error('DAGGERHEART.UI.Notifications.beastformMissingEffect');
|
||||
return;
|
||||
}
|
||||
|
||||
beastformEffect.changes = [...beastformEffect.changes, ...evolvedForm.changes];
|
||||
formData.system.features = [...formData.system.features, ...selectedForm.system.features.map(x => x.uuid)];
|
||||
}
|
||||
|
||||
if (selectedForm.system.beastformType === CONFIG.DH.ITEM.beastformTypes.hybrid.id) {
|
||||
formData.system.advantageOn = Object.values(hybridData.advantages).reduce((advantages, formCategory) => {
|
||||
Object.keys(formCategory).forEach(advantageKey => {
|
||||
advantages[advantageKey] = formCategory[advantageKey];
|
||||
});
|
||||
return advantages;
|
||||
}, {});
|
||||
formData.system.features = [
|
||||
...formData.system.features,
|
||||
...Object.values(hybridData.features).flatMap(x => Object.keys(x))
|
||||
];
|
||||
}
|
||||
|
||||
this.actor.createEmbeddedDocuments('Item', [formData]);
|
||||
}
|
||||
|
||||
async handleActiveTransformations() {
|
||||
const beastformEffects = this.actor.effects.filter(x => x.type === 'beastform');
|
||||
const existingEffects = beastformEffects.length > 0;
|
||||
await this.actor.deleteEmbeddedDocuments(
|
||||
'ActiveEffect',
|
||||
beastformEffects.map(x => x.id)
|
||||
);
|
||||
return existingEffects;
|
||||
} */
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,11 +12,35 @@
|
|||
* "Anything that uses another data model value as its value": +1 - Effects that increase traits have to be calculated first at Base priority. (EX: Raise evasion by half your agility)
|
||||
*/
|
||||
|
||||
export default class BaseEffect extends foundry.abstract.TypeDataModel {
|
||||
export default class BaseEffect extends foundry.data.ActiveEffectTypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
|
||||
return {
|
||||
...super.defineSchema(),
|
||||
changes: new fields.ArrayField(
|
||||
new fields.SchemaField({
|
||||
key: new fields.StringField({ required: true }),
|
||||
type: new fields.StringField({
|
||||
required: true,
|
||||
blank: false,
|
||||
choices: CONFIG.DH.GENERAL.activeEffectModes,
|
||||
initial: CONFIG.DH.GENERAL.activeEffectModes.add.id,
|
||||
validate: BaseEffect.#validateType
|
||||
}),
|
||||
value: new fields.AnyField({ required: true, nullable: true, serializable: true, initial: '' }),
|
||||
phase: new fields.StringField({ required: true, blank: false, initial: 'initial' }),
|
||||
priority: new fields.NumberField()
|
||||
})
|
||||
),
|
||||
duration: new fields.SchemaField({
|
||||
type: new fields.StringField({
|
||||
choices: CONFIG.DH.GENERAL.activeEffectDurations,
|
||||
blank: true,
|
||||
label: 'DAGGERHEART.GENERAL.type'
|
||||
}),
|
||||
description: new fields.HTMLField({ label: 'DAGGERHEART.GENERAL.description' })
|
||||
}),
|
||||
rangeDependence: new fields.SchemaField({
|
||||
enabled: new fields.BooleanField({
|
||||
required: true,
|
||||
|
|
@ -45,6 +69,23 @@ export default class BaseEffect extends foundry.abstract.TypeDataModel {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that an {@link EffectChangeData#type} string is well-formed.
|
||||
* @param {string} type The string to be validated
|
||||
* @returns {true}
|
||||
* @throws {Error} An error if the type string is malformed
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
static getDefaultObject() {
|
||||
return {
|
||||
name: 'New Effect',
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ export default class BeastformEffect extends BaseEffect {
|
|||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
...super.defineSchema(),
|
||||
characterTokenData: new fields.SchemaField({
|
||||
usesDynamicToken: new fields.BooleanField({ initial: false }),
|
||||
tokenImg: new fields.FilePathField({
|
||||
|
|
@ -99,7 +100,7 @@ export default class BeastformEffect extends BaseEffect {
|
|||
token.flags.daggerheart?.beastformSubjectTexture ?? this.characterTokenData.tokenRingImg
|
||||
}
|
||||
},
|
||||
'flags.daggerheart': { '-=beastformTokenImg': null, '-=beastformSubjectTexture': null }
|
||||
'flags.daggerheart': { beastformTokenImg: _del, beastformSubjectTexture: _del }
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -85,14 +85,14 @@ export default class DhpAdversary extends DhCreature {
|
|||
type: 'attack'
|
||||
},
|
||||
damage: {
|
||||
parts: [
|
||||
{
|
||||
parts: {
|
||||
hitPoints: {
|
||||
type: ['physical'],
|
||||
value: {
|
||||
multiplier: 'flat'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
|
@ -265,12 +265,12 @@ export default class DhpAdversary extends DhCreature {
|
|||
}
|
||||
|
||||
// Update damage in item actions
|
||||
// Parse damage, and convert all formula matches in the descriptions to the new damage
|
||||
for (const action of Object.values(item.system.actions)) {
|
||||
if (!action.damage) continue;
|
||||
|
||||
// Parse damage, and convert all formula matches in the descriptions to the new damage
|
||||
try {
|
||||
const result = this.#adjustActionDamage(action, { ...damageMeta, type: 'action' });
|
||||
if (!result) continue;
|
||||
|
||||
for (const { previousFormula, formula } of Object.values(result)) {
|
||||
const oldFormulaRegexp = new RegExp(
|
||||
previousFormula.replace(' ', '').replace('+', '(?:\\s)?\\+(?:\\s)?')
|
||||
|
|
@ -372,16 +372,14 @@ export default class DhpAdversary extends DhCreature {
|
|||
/**
|
||||
* Updates damage to reflect a specific value.
|
||||
* @throws if damage structure is invalid for conversion
|
||||
* @returns the converted formula and value as a simplified term
|
||||
* @returns the converted formula and value as a simplified term, or null if it doesn't deal HP damage
|
||||
*/
|
||||
#adjustActionDamage(action, damageMeta) {
|
||||
// The current algorithm only returns a value if there is a single damage part
|
||||
const hpDamageParts = action.damage.parts.filter(d => d.applyTo === 'hitPoints');
|
||||
if (hpDamageParts.length !== 1) throw new Error('incorrect number of hp parts');
|
||||
if (!action.damage?.parts.hitPoints) return null;
|
||||
|
||||
const result = {};
|
||||
for (const property of ['value', 'valueAlt']) {
|
||||
const data = hpDamageParts[0][property];
|
||||
const data = action.damage.parts.hitPoints[property];
|
||||
const previousFormula = data.custom.enabled
|
||||
? data.custom.formula
|
||||
: [data.flatMultiplier ? `${data.flatMultiplier}${data.dice}` : 0, data.bonus ?? 0]
|
||||
|
|
|
|||
|
|
@ -96,8 +96,8 @@ export default class DhCharacter extends DhCreature {
|
|||
trait: 'strength'
|
||||
},
|
||||
damage: {
|
||||
parts: [
|
||||
{
|
||||
parts: {
|
||||
hitPoints: {
|
||||
type: ['physical'],
|
||||
value: {
|
||||
custom: {
|
||||
|
|
@ -106,7 +106,7 @@ export default class DhCharacter extends DhCreature {
|
|||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
|
@ -685,7 +685,7 @@ export default class DhCharacter extends DhCreature {
|
|||
isReversed: true
|
||||
};
|
||||
|
||||
this.attack.damage.parts[0].value.custom.formula = `@prof${this.basicAttackDamageDice}${this.rules.attack.damage.bonus ? ` + ${this.rules.attack.damage.bonus}` : ''}`;
|
||||
this.attack.damage.parts.hitPoints.value.custom.formula = `@prof${this.basicAttackDamageDice}${this.rules.attack.damage.bonus ? ` + ${this.rules.attack.damage.bonus}` : ''}`;
|
||||
}
|
||||
|
||||
getRollData() {
|
||||
|
|
@ -721,7 +721,8 @@ export default class DhCharacter extends DhCreature {
|
|||
const newHopeMax = this.system.resources.hope.max + diff;
|
||||
const newHopeValue = Math.min(newHopeMax, this.system.resources.hope.value);
|
||||
if (newHopeValue != this.system.resources.hope.value) {
|
||||
if (!changes.system.resources) changes.system.resources = { hope: { value: 0 } };
|
||||
if (!changes.system.resources.hope) changes.system.resources.hope = { value: 0 };
|
||||
|
||||
changes.system.resources.hope = {
|
||||
...changes.system.resources.hope,
|
||||
value: changes.system.resources.hope.value + newHopeValue
|
||||
|
|
|
|||
|
|
@ -81,15 +81,15 @@ export default class DhCompanion extends DhCreature {
|
|||
bonus: 0
|
||||
},
|
||||
damage: {
|
||||
parts: [
|
||||
{
|
||||
parts: {
|
||||
hitPoints: {
|
||||
type: ['physical'],
|
||||
value: {
|
||||
dice: 'd6',
|
||||
multiplier: 'prof'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
|
@ -135,7 +135,9 @@ export default class DhCompanion extends DhCreature {
|
|||
break;
|
||||
case 'vicious':
|
||||
if (selection.data[0] === 'damage') {
|
||||
this.attack.damage.parts[0].value.dice = adjustDice(this.attack.damage.parts[0].value.dice);
|
||||
this.attack.damage.parts.hitPoints.value.dice = adjustDice(
|
||||
this.attack.damage.parts.hitPoints.value.dice
|
||||
);
|
||||
} else {
|
||||
this.attack.range = adjustRange(this.attack.range).id;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
export { ActionCollection } from './actionField.mjs';
|
||||
export { default as IterableTypedObjectField } from './iterableTypedObjectField.mjs';
|
||||
export { default as FormulaField } from './formulaField.mjs';
|
||||
export { default as ForeignDocumentUUIDField } from './foreignDocumentUUIDField.mjs';
|
||||
export { default as ForeignDocumentUUIDArrayField } from './foreignDocumentUUIDArrayField.mjs';
|
||||
export { default as TriggerField } from './triggerField.mjs';
|
||||
export { default as MappingField } from './mappingField.mjs';
|
||||
export * as ActionFields from './action/_module.mjs';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import FormulaField from '../formulaField.mjs';
|
||||
import { setsEqual } from '../../../helpers/utils.mjs';
|
||||
import IterableTypedObjectField from '../iterableTypedObjectField.mjs';
|
||||
|
||||
const fields = foundry.data.fields;
|
||||
|
||||
|
|
@ -12,7 +13,7 @@ export default class DamageField extends fields.SchemaField {
|
|||
/** @inheritDoc */
|
||||
constructor(options, context = {}) {
|
||||
const damageFields = {
|
||||
parts: new fields.ArrayField(new fields.EmbeddedDataField(DHDamageData)),
|
||||
parts: new IterableTypedObjectField(DHDamageData),
|
||||
includeBase: new fields.BooleanField({
|
||||
initial: false,
|
||||
label: 'DAGGERHEART.ACTIONS.Settings.includeBase.label'
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import DHActionConfig from '../../applications/sheets-configs/action-config.mjs';
|
||||
import { itemAbleRollParse } from '../../helpers/utils.mjs';
|
||||
import MappingField from './mappingField.mjs';
|
||||
|
||||
/**
|
||||
* Specialized collection type for stored actions.
|
||||
|
|
@ -11,9 +10,9 @@ export class ActionCollection extends Collection {
|
|||
constructor(model, entries) {
|
||||
super();
|
||||
this.#model = model;
|
||||
for (const entry of entries) {
|
||||
if (!(entry instanceof game.system.api.models.actions.actionsTypes.base)) continue;
|
||||
this.set(entry._id, entry);
|
||||
for (const [key, value] of entries) {
|
||||
if (!(value instanceof game.system.api.models.actions.actionsTypes.base)) continue;
|
||||
this.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -61,7 +60,7 @@ export class ActionCollection extends Collection {
|
|||
/**
|
||||
* Field that stores actions.
|
||||
*/
|
||||
export class ActionsField extends MappingField {
|
||||
export class ActionsField extends foundry.data.fields.TypedObjectField {
|
||||
constructor(options) {
|
||||
super(new ActionField(), options);
|
||||
}
|
||||
|
|
@ -70,7 +69,7 @@ export class ActionsField extends MappingField {
|
|||
|
||||
/** @inheritDoc */
|
||||
initialize(value, model, options) {
|
||||
const actions = Object.values(super.initialize(value, model, options));
|
||||
const actions = Object.entries(super.initialize(value, model, options));
|
||||
return new ActionCollection(model, actions);
|
||||
}
|
||||
}
|
||||
|
|
@ -88,10 +87,10 @@ export class ActionField extends foundry.data.fields.ObjectField {
|
|||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_cleanType(value, options) {
|
||||
_cleanType(value, options, _state) {
|
||||
if (!(typeof value === 'object')) value = {};
|
||||
const cls = this.getModel(value);
|
||||
if (cls) return cls.cleanData(value, options);
|
||||
if (cls) return cls.cleanData(value, options, _state);
|
||||
return value;
|
||||
}
|
||||
|
||||
|
|
@ -111,9 +110,17 @@ export class ActionField extends foundry.data.fields.ObjectField {
|
|||
* @param {object} sourceData Candidate source data of the root model.
|
||||
* @param {any} fieldData The value of this field within the source data.
|
||||
*/
|
||||
migrateSource(sourceData, fieldData) {
|
||||
const cls = this.getModel(fieldData);
|
||||
if (cls) cls.migrateDataSafe(fieldData);
|
||||
_migrate(sourceData, _fieldData) {
|
||||
const source = sourceData ?? this.options.initial;
|
||||
if (!source) return sourceData;
|
||||
|
||||
const cls = this.getModel(source);
|
||||
if (cls) {
|
||||
cls.migrateDataSafe(source);
|
||||
return source;
|
||||
}
|
||||
|
||||
return sourceData;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -237,11 +244,11 @@ export function ActionMixin(Base) {
|
|||
: foundry.utils.getProperty(result, basePath);
|
||||
}
|
||||
|
||||
delete() {
|
||||
async delete() {
|
||||
if (!this.inCollection) return this.item;
|
||||
const action = foundry.utils.getProperty(this.item, `system.${this.systemPath}`)?.get(this.id);
|
||||
if (!action) return this.item;
|
||||
this.item.update({ [`system.${this.systemPath}.-=${this.id}`]: null });
|
||||
await this.item.update({ [`system.${this.systemPath}.${this.id}`]: _del }); // Does not work. Unsure why. It worked in v13 <_<'
|
||||
this.constructor._sheets.get(this.uuid)?.close();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ const bonusField = label =>
|
|||
dice: new fields.ArrayField(new fields.StringField(), { label: `${game.i18n.localize(label)} Dice` })
|
||||
});
|
||||
|
||||
/**
|
||||
/**
|
||||
* Field used for actor resources. It is a resource that validates dynamically based on the config.
|
||||
* Because "max" may be defined during runtime, we don't attempt to clamp the maximum value.
|
||||
*/
|
||||
|
|
@ -52,8 +52,8 @@ class ResourcesField extends fields.TypedObjectField {
|
|||
return key in CONFIG.DH.RESOURCE[this.actorType].all;
|
||||
}
|
||||
|
||||
_cleanType(value, options) {
|
||||
value = super._cleanType(value, options);
|
||||
_cleanType(value, options, _state) {
|
||||
value = super._cleanType(value, options, _state);
|
||||
|
||||
// If not partial, ensure all data exists
|
||||
if (!options.partial) {
|
||||
|
|
@ -78,10 +78,28 @@ class ResourcesField extends fields.TypedObjectField {
|
|||
const resource = resources[key];
|
||||
value.label = resource.label;
|
||||
value.isReversed = resources[key].reverse;
|
||||
value.max = typeof resource.max === 'number' ? value.max ?? resource.max : null;
|
||||
value.max = typeof resource.max === 'number' ? (value.max ?? resource.max) : null;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Foundry bar attributes are unable to handle finding the schema field nor the label normally.
|
||||
* This returns the element if its a valid resource key and overwrites the element's label for that retrieval.
|
||||
*/
|
||||
_getField(path) {
|
||||
if (path.length === 0) return this;
|
||||
const first = path.shift();
|
||||
if (first === this.element.name) return this.element_getField(path);
|
||||
|
||||
const resources = CONFIG.DH.RESOURCE[this.actorType].all;
|
||||
if (first in resources) {
|
||||
this.element.label = resources[first].label;
|
||||
return this.element._getField(path);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export { attributeField, ResourcesField, stressDamageReductionRule, bonusField };
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export default class ForeignDocumentUUIDArrayField extends foundry.data.fields.A
|
|||
|
||||
/** @inheritdoc */
|
||||
initialize(value, model, options = {}) {
|
||||
const v = super.initialize(value, model, options);
|
||||
const v = super.initialize(value ?? [], model, options);
|
||||
return () => {
|
||||
const data = v.map(entry => (typeof entry === 'function' ? entry() : entry));
|
||||
return this.options.prune ? data.filter(d => !!d) : data;
|
||||
|
|
|
|||
32
module/data/fields/iterableTypedObjectField.mjs
Normal file
32
module/data/fields/iterableTypedObjectField.mjs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
export default class IterableTypedObjectField extends foundry.data.fields.TypedObjectField {
|
||||
constructor(model, options = { collectionClass: foundry.utils.Collection }, context = {}) {
|
||||
super(new foundry.data.fields.EmbeddedDataField(model), options, context);
|
||||
this.#elementClass = model;
|
||||
}
|
||||
|
||||
#elementClass;
|
||||
|
||||
/** Initializes an object with an iterator. This modifies the prototype instead of */
|
||||
initialize(values) {
|
||||
const object = Object.create(IterableObjectPrototype);
|
||||
for (const [key, value] of Object.entries(values)) {
|
||||
object[key] = new this.#elementClass(value);
|
||||
}
|
||||
return object;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The prototype of an iterable object.
|
||||
* This allows the functionality of a class but also allows foundry.utils.getType() to return "Object" instead of "Unknown".
|
||||
*/
|
||||
const IterableObjectPrototype = {
|
||||
[Symbol.iterator]: function* () {
|
||||
for (const value of Object.values(this)) {
|
||||
yield value;
|
||||
}
|
||||
},
|
||||
map: function (func) {
|
||||
return Array.from(this, func);
|
||||
}
|
||||
};
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
/**
|
||||
* A subclass of ObjectField that represents a mapping of keys to the provided DataField type.
|
||||
*
|
||||
* @param {DataField} model The class of DataField which should be embedded in this field.
|
||||
* @param {MappingFieldOptions} [options={}] Options which configure the behavior of the field.
|
||||
* @property {string[]} [initialKeys] Keys that will be created if no data is provided.
|
||||
* @property {MappingFieldInitialValueBuilder} [initialValue] Function to calculate the initial value for a key.
|
||||
* @property {boolean} [initialKeysOnly=false] Should the keys in the initialized data be limited to the keys provided
|
||||
* by `options.initialKeys`?
|
||||
*/
|
||||
export default class MappingField extends foundry.data.fields.ObjectField {
|
||||
constructor(model, options) {
|
||||
if (!(model instanceof foundry.data.fields.DataField)) {
|
||||
throw new Error('MappingField must have a DataField as its contained element');
|
||||
}
|
||||
super(options);
|
||||
|
||||
/**
|
||||
* The embedded DataField definition which is contained in this field.
|
||||
* @type {DataField}
|
||||
*/
|
||||
this.model = model;
|
||||
model.parent = this;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
static get _defaults() {
|
||||
return foundry.utils.mergeObject(super._defaults, {
|
||||
initialKeys: null,
|
||||
initialValue: null,
|
||||
initialKeysOnly: false
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
_cleanType(value, options) {
|
||||
Object.entries(value).forEach(([k, v]) => {
|
||||
if (k.startsWith('-=')) return;
|
||||
value[k] = this.model.clean(v, options);
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
getInitialValue(data) {
|
||||
let keys = this.initialKeys;
|
||||
const initial = super.getInitialValue(data);
|
||||
if (!keys || !foundry.utils.isEmpty(initial)) return initial;
|
||||
if (!(keys instanceof Array)) keys = Object.keys(keys);
|
||||
for (const key of keys) initial[key] = this._getInitialValueForKey(key);
|
||||
return initial;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Get the initial value for the provided key.
|
||||
* @param {string} key Key within the object being built.
|
||||
* @param {object} [object] Any existing mapping data.
|
||||
* @returns {*} Initial value based on provided field type.
|
||||
*/
|
||||
_getInitialValueForKey(key, object) {
|
||||
const initial = this.model.getInitialValue();
|
||||
return this.initialValue?.(key, initial, object) ?? initial;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_validateType(value, options = {}) {
|
||||
if (foundry.utils.getType(value) !== 'Object') throw new Error('must be an Object');
|
||||
const errors = this._validateValues(value, options);
|
||||
if (!foundry.utils.isEmpty(errors)) {
|
||||
const failure = new foundry.data.validation.DataModelValidationFailure();
|
||||
failure.elements = Object.entries(errors).map(([id, failure]) => ({ id, failure }));
|
||||
throw failure.asError();
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Validate each value of the object.
|
||||
* @param {object} value The object to validate.
|
||||
* @param {object} options Validation options.
|
||||
* @returns {Record<string, Error>} An object of value-specific errors by key.
|
||||
*/
|
||||
_validateValues(value, options) {
|
||||
const errors = {};
|
||||
for (const [k, v] of Object.entries(value)) {
|
||||
if (k.startsWith('-=')) continue;
|
||||
const error = this.model.validate(v, options);
|
||||
if (error) errors[k] = error;
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
initialize(value, model, options = {}) {
|
||||
if (!value) return value;
|
||||
const obj = {};
|
||||
const initialKeys = this.initialKeys instanceof Array ? this.initialKeys : Object.keys(this.initialKeys ?? {});
|
||||
const keys = this.initialKeysOnly ? initialKeys : Object.keys(value);
|
||||
for (const key of keys) {
|
||||
const data = value[key] ?? this._getInitialValueForKey(key, value);
|
||||
obj[key] = this.model.initialize(data, model, options);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
_getField(path) {
|
||||
if (path.length === 0) return this;
|
||||
else if (path.length === 1) return this.model;
|
||||
path.shift();
|
||||
return this.model._getField(path);
|
||||
}
|
||||
}
|
||||
|
|
@ -84,7 +84,7 @@ export default class DHArmor extends AttachableItem {
|
|||
}
|
||||
await this.parent.deleteEmbeddedDocuments('ActiveEffect', effectIds);
|
||||
changes.system.actions = actionIds.reduce((acc, id) => {
|
||||
acc[`-=${id}`] = null;
|
||||
acc[id] = _del;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
|
|
|
|||
|
|
@ -230,9 +230,9 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
|
|||
|
||||
if (changed.system?.actions) {
|
||||
const triggersToRemove = Object.keys(changed.system.actions).reduce((acc, key) => {
|
||||
if (!changed.system.actions[key]) {
|
||||
const strippedKey = key.replace('-=', '');
|
||||
acc.push(...this.actions.get(strippedKey).triggers.map(x => x.trigger));
|
||||
const action = changed.system.actions[key];
|
||||
if (action && Object.keys(action).length === 0) {
|
||||
acc.push(...this.actions.get(key).triggers.map(x => x.trigger));
|
||||
}
|
||||
|
||||
return acc;
|
||||
|
|
|
|||
|
|
@ -101,12 +101,13 @@ export default class DHBeastform extends BaseDataItem {
|
|||
const effect = this.parent.effects.find(x => x.type === 'beastform');
|
||||
if (!effect) return null;
|
||||
|
||||
const traitBonus = effect.changes.find(x => x.key === `system.traits.${this.mainTrait}.value`)?.value ?? 0;
|
||||
const evasionBonus = effect.changes.find(x => x.key === 'system.evasion')?.value ?? 0;
|
||||
const traitBonus =
|
||||
effect.system.changes.find(x => x.key === `system.traits.${this.mainTrait}.value`)?.value ?? 0;
|
||||
const evasionBonus = effect.system.changes.find(x => x.key === 'system.evasion')?.value ?? 0;
|
||||
|
||||
const damageDiceIndex = effect.changes.find(x => x.key === 'system.rules.attack.damage.diceIndex');
|
||||
const damageDiceIndex = effect.system.changes.find(x => x.key === 'system.rules.attack.damage.diceIndex');
|
||||
const damageDice = damageDiceIndex ? Object.keys(CONFIG.DH.GENERAL.diceTypes)[damageDiceIndex.value] : null;
|
||||
const damageBonus = effect.changes.find(x => x.key === 'system.rules.attack.damage.bonus')?.value ?? 0;
|
||||
const damageBonus = effect.system.changes.find(x => x.key === 'system.rules.attack.damage.bonus')?.value ?? 0;
|
||||
|
||||
return {
|
||||
trait: game.i18n.localize(CONFIG.DH.ACTOR.abilities[this.mainTrait].label),
|
||||
|
|
@ -169,17 +170,17 @@ export default class DHBeastform extends BaseDataItem {
|
|||
|
||||
const beastformEffect = this.parent.effects.find(x => x.type === 'beastform');
|
||||
await beastformEffect.updateSource({
|
||||
changes: [
|
||||
...beastformEffect.changes,
|
||||
{
|
||||
key: 'system.advantageSources',
|
||||
mode: 2,
|
||||
value: Object.values(this.advantageOn)
|
||||
.map(x => x.value)
|
||||
.join(', ')
|
||||
}
|
||||
],
|
||||
system: {
|
||||
changes: [
|
||||
...beastformEffect.system.changes,
|
||||
{
|
||||
key: 'system.advantageSources',
|
||||
mode: 2,
|
||||
value: Object.values(this.advantageOn)
|
||||
.map(x => x.value)
|
||||
.join(', ')
|
||||
}
|
||||
],
|
||||
characterTokenData: {
|
||||
usesDynamicToken: this.parent.parent.prototypeToken.ring.enabled,
|
||||
tokenImg: this.parent.parent.prototypeToken.texture.src,
|
||||
|
|
|
|||
|
|
@ -52,6 +52,9 @@ export default class DHSubclass extends BaseDataItem {
|
|||
}
|
||||
|
||||
async _preCreate(data, options, user) {
|
||||
const allowed = await super._preCreate(data, options, user);
|
||||
if (allowed === false) return;
|
||||
|
||||
if (this.actor?.type === 'character') {
|
||||
const dataUuid = data.uuid ?? data._stats.compendiumSource ?? `Item.${data._id}`;
|
||||
if (this.actor.system.class.subclass) {
|
||||
|
|
@ -86,9 +89,6 @@ export default class DHSubclass extends BaseDataItem {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
const allowed = await super._preCreate(data, options, user);
|
||||
if (allowed === false) return;
|
||||
}
|
||||
|
||||
/**@inheritdoc */
|
||||
|
|
|
|||
|
|
@ -63,15 +63,15 @@ export default class DHWeapon extends AttachableItem {
|
|||
type: 'attack'
|
||||
},
|
||||
damage: {
|
||||
parts: [
|
||||
{
|
||||
parts: {
|
||||
hitPoints: {
|
||||
type: ['physical'],
|
||||
value: {
|
||||
multiplier: 'prof',
|
||||
dice: 'd8'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
|
@ -148,7 +148,7 @@ export default class DHWeapon extends AttachableItem {
|
|||
|
||||
await this.parent.deleteEmbeddedDocuments('ActiveEffect', removedEffectsUpdate);
|
||||
changes.system.actions = removedActionsUpdate.reduce((acc, id) => {
|
||||
acc[`-=${id}`] = null;
|
||||
acc[id] = _del;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,16 @@
|
|||
export default class DhAppearance extends foundry.abstract.DataModel {
|
||||
static LOCALIZATION_PREFIXES = ['DAGGERHEART.SETTINGS.Appearance'];
|
||||
|
||||
static sfxSchema = () =>
|
||||
new foundry.data.fields.SchemaField({
|
||||
class: new foundry.data.fields.StringField({
|
||||
nullable: true,
|
||||
initial: null,
|
||||
blank: true,
|
||||
choices: CONFIG.DH.GENERAL.diceSoNiceSFXClasses
|
||||
})
|
||||
});
|
||||
|
||||
static defineSchema() {
|
||||
const { StringField, ColorField, BooleanField, SchemaField } = foundry.data.fields;
|
||||
|
||||
|
|
@ -15,7 +25,10 @@ export default class DhAppearance extends foundry.abstract.DataModel {
|
|||
colorset: new StringField({ initial: 'inspired', required: true, blank: false }),
|
||||
material: new StringField({ initial: 'metal', required: true, blank: false }),
|
||||
system: new StringField({ initial: 'standard', required: true, blank: false }),
|
||||
font: new StringField({ initial: 'auto', required: true, blank: false })
|
||||
font: new StringField({ initial: 'auto', required: true, blank: false }),
|
||||
sfx: new SchemaField({
|
||||
higher: DhAppearance.sfxSchema()
|
||||
})
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
@ -30,7 +43,10 @@ export default class DhAppearance extends foundry.abstract.DataModel {
|
|||
hope: diceStyle({ fg: '#ffffff', bg: '#ffe760', outline: '#000000', edge: '#ffffff' }),
|
||||
fear: diceStyle({ fg: '#000000', bg: '#0032b1', outline: '#ffffff', edge: '#000000' }),
|
||||
advantage: diceStyle({ fg: '#ffffff', bg: '#008000', outline: '#000000', edge: '#ffffff' }),
|
||||
disadvantage: diceStyle({ fg: '#000000', bg: '#b30000', outline: '#ffffff', edge: '#000000' })
|
||||
disadvantage: diceStyle({ fg: '#000000', bg: '#b30000', outline: '#ffffff', edge: '#000000' }),
|
||||
sfx: new SchemaField({
|
||||
critical: DhAppearance.sfxSchema()
|
||||
})
|
||||
}),
|
||||
extendCharacterDescriptions: new BooleanField(),
|
||||
extendAdversaryDescriptions: new BooleanField(),
|
||||
|
|
@ -65,4 +81,48 @@ export default class DhAppearance extends foundry.abstract.DataModel {
|
|||
showGenericStatusEffects: new BooleanField({ initial: true })
|
||||
};
|
||||
}
|
||||
|
||||
get diceSoNiceData() {
|
||||
const globalOverrides = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.GlobalOverrides);
|
||||
const getSFX = (baseClientData, overrideKey) => {
|
||||
if (!globalOverrides.diceSoNice.sfx.overrideEnabled) return baseClientData;
|
||||
const overrideData = globalOverrides.diceSoNice.sfx[overrideKey];
|
||||
const clientData = foundry.utils.deepClone(baseClientData);
|
||||
return Object.keys(clientData).reduce((acc, key) => {
|
||||
const data = clientData[key];
|
||||
acc[key] = Object.keys(data).reduce((acc, dataKey) => {
|
||||
const value = data[dataKey];
|
||||
acc[dataKey] = value ? value : overrideData[key][dataKey];
|
||||
return acc;
|
||||
}, {});
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
return {
|
||||
...this.diceSoNice,
|
||||
sfx: getSFX(this.diceSoNice.sfx, 'global'),
|
||||
hope: {
|
||||
...this.diceSoNice.hope,
|
||||
sfx: getSFX(this.diceSoNice.hope.sfx, 'hope')
|
||||
},
|
||||
fear: {
|
||||
...this.diceSoNice.fear,
|
||||
sfx: getSFX(this.diceSoNice.fear.sfx, 'fear')
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** Invoked by the setting when data changes */
|
||||
handleChange() {
|
||||
if (this.displayFear) {
|
||||
if (ui.resources) {
|
||||
if (this.displayFear === 'hide') ui.resources.close({ allowed: true });
|
||||
else ui.resources.render({ force: true });
|
||||
}
|
||||
}
|
||||
|
||||
const globalOverrides = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.GlobalOverrides);
|
||||
globalOverrides.diceSoNiceSFXUpdate(this);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -196,6 +196,11 @@ export default class DhAutomation extends foundry.abstract.DataModel {
|
|||
})
|
||||
})
|
||||
}),
|
||||
autoExpireActiveEffects: new fields.BooleanField({
|
||||
required: true,
|
||||
initial: true,
|
||||
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.autoExpireActiveEffects.label'
|
||||
}),
|
||||
triggers: new fields.SchemaField({
|
||||
enabled: new fields.BooleanField({
|
||||
nullable: false,
|
||||
|
|
|
|||
55
module/data/settings/GlobalOverrides.mjs
Normal file
55
module/data/settings/GlobalOverrides.mjs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import DhAppearance from './Appearance.mjs';
|
||||
|
||||
/**
|
||||
* A setting to handle cases where we want to allow the GM to set a global default for client settings.
|
||||
*/
|
||||
export default class DhGlobalOverrides extends foundry.abstract.DataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
diceSoNice: new fields.SchemaField({
|
||||
sfx: new fields.SchemaField({
|
||||
overrideEnabled: new fields.BooleanField(),
|
||||
global: new fields.SchemaField({
|
||||
critical: DhAppearance.sfxSchema()
|
||||
}),
|
||||
hope: new fields.SchemaField({
|
||||
higher: DhAppearance.sfxSchema()
|
||||
}),
|
||||
fear: new fields.SchemaField({
|
||||
higher: DhAppearance.sfxSchema()
|
||||
})
|
||||
})
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
async diceSoNiceSFXUpdate(appearanceSettings, enabled) {
|
||||
if (!game.user.isGM) return;
|
||||
|
||||
const newEnabled = enabled !== undefined ? enabled : this.diceSoNice.sfx.overrideEnabled;
|
||||
if (newEnabled) {
|
||||
const newOverrides = foundry.utils.mergeObject(this.toObject(), {
|
||||
diceSoNice: {
|
||||
sfx: {
|
||||
overrideEnabled: true,
|
||||
global: appearanceSettings.diceSoNice.sfx,
|
||||
hope: appearanceSettings.diceSoNice.hope.sfx,
|
||||
fear: appearanceSettings.diceSoNice.fear.sfx
|
||||
}
|
||||
}
|
||||
});
|
||||
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.GlobalOverrides, newOverrides);
|
||||
} else {
|
||||
const newOverrides = {
|
||||
...this.toObject(),
|
||||
diceSoNice: {
|
||||
sfx: {
|
||||
overrideEnabled: false
|
||||
}
|
||||
}
|
||||
};
|
||||
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.GlobalOverrides, newOverrides);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -220,7 +220,7 @@ export default class DhHomebrew extends foundry.abstract.DataModel {
|
|||
return result;
|
||||
}, {}),
|
||||
...config.custom,
|
||||
...config.base,
|
||||
...config.base
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,3 +3,4 @@ export { default as DhAutomation } from './Automation.mjs';
|
|||
export { default as DhHomebrew } from './Homebrew.mjs';
|
||||
export { default as DhMetagaming } from './Metagaming.mjs';
|
||||
export { default as DhVariantRules } from './VariantRules.mjs';
|
||||
export { default as DhGlobalOverrides } from './GlobalOverrides.mjs';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue