mirror of
https://github.com/Foundryborne/daggerheart.git
synced 2026-01-12 03:31:07 +01:00
Refactor/84 data models structure (#131)
* - Move all DataModel item files to a new 'items' subfolder for better organization - Add _module.mjs file to simplify imports - Update all import paths - Rename class for use the new acronym DH * FIX: remove unnecessary import * FEAT: BaseDataItem class add TODO comments for future improvements FIX: Remove effect field on template FIX: remove unused DhpEffects file * FEAT: new FormulaField class FEAT: add getRollData on BaseDataItem Class FEAT: weapon FIX: remove inventoryWeapon field on Weapon Data Model * FEAT: add class prepareBaseData for domains * FEAT: new ForeignDocumentUUIDField FIX: Remove unnecessary fields FEAT: use ForeignDocumentUUIDField in the Item Class DataModel * FIX: remove wrong option in String Field * FIX: remove unused import * FIX: ADD htmlFields description in manifest * FIX: minor fixes * REFACTOR: rename folder `data/items` -> `data/item` REFACTOR: rename folder `data/messages` -> `data/chat-message`. * FIX: imports FIX: items sheet new paths FIX: ItemDataModelMetadata type jsdoc * FEAT: formatting code FIX: fix fields used FEAT: add jsdoc * 110 - Class Data Model (#111) * Added PreCreate/Create/Delete logic for Class/Subclass and set it as foreignUUID fields in PC * Moved methods into TypedModelData * Simplified Subclass * Fixed up data model and a basic placeholder template (#117) * 118 - adversary data model (#119) * Fixed datamodel and set up basic template in new style * Added in a temp attack button, because why not * Restored HitPoints counting up * 113 - Character Data Model (#114) * Improved Character datamodel * Removed additional unneccessary getters * Preliminary cleanup in the class sheet * Cleanup of 'pc' references * Corrected Duality rolling from Character * Fix to damage roll * Added a basic BaseDataActor data model * Gathered exports * getRollData recursion fix * Feature/112 items use action datamodel (#127) * Create new actions classes * actions types - attack roll * fixes before merge * First PR * Add daggerheart.css to gitignore * Update ToDo * Remove console log * Fixed chat /dr roll * Remove jQuery * Fixed so the different chat themes work again * Fixed duality roll buttons * Fix to advantage/disadvantage shortcut * Extand action to other item types * Roll fixes * Fixes to adversary rolls * resources * Fixed adversary dice --------- Co-authored-by: WBHarry <williambjrklund@gmail.com> * Feature/116-implementation-of-pseudo-documents (#125) * FEAT: add baseDataModel logic * FEAT: new PseudoDocumentsField FIX: BasePseudoDocument 's getEmbeddedDocument * FEAT: PseudoDocument class * FEAT: add TypedPseudoDocument REFACTOR: PreudoDocument FIX: Typos Bug * FIX: CONFIG types * FEAT: basic PseudoDocumentSheet * FIX: remove schema ADD: input of example --------- Co-authored-by: Joaquin Pereyra <joaquinpereyra98@users.noreply.github.com> Co-authored-by: WBHarry <williambjrklund@gmail.com> * Levelup Followup (#126) * Levelup applies bonuses to character * Added visualisation of domain card levels * Fixed domaincard level max for selections in a tier * A trait can now only be level up once within the same tier --------- Co-authored-by: Joaquin Pereyra <joaquinpereyra98@users.noreply.github.com> Co-authored-by: joaquinpereyra98 <24190917+joaquinpereyra98@users.noreply.github.com> Co-authored-by: Dapoulp <74197441+Dapoulp@users.noreply.github.com>
This commit is contained in:
parent
90bc2dc488
commit
187ee3e1bd
153 changed files with 5481 additions and 4829 deletions
|
|
@ -1,19 +1,11 @@
|
|||
export { default as DhpPC } from './pc.mjs';
|
||||
export { default as DhpClass } from './class.mjs';
|
||||
export { default as DhpSubclass } from './subclass.mjs';
|
||||
export { default as DhClass } from './item/class.mjs';
|
||||
export { default as DhSubclass } from './item/subclass.mjs';
|
||||
export { default as DhCombat } from './combat.mjs';
|
||||
export { default as DhCombatant } from './combatant.mjs';
|
||||
export { default as DhpAdversary } from './adversary.mjs';
|
||||
export { default as DhpFeature } from './feature.mjs';
|
||||
export { default as DhpDomainCard } from './domainCard.mjs';
|
||||
export { default as DhpAncestry } from './ancestry.mjs';
|
||||
export { default as DhpCommunity } from './community.mjs';
|
||||
export { default as DhpMiscellaneous } from './miscellaneous.mjs';
|
||||
export { default as DhpConsumable } from './consumable.mjs';
|
||||
export { default as DhpWeapon } from './weapon.mjs';
|
||||
export { default as DhpArmor } from './armor.mjs';
|
||||
export { default as DhpDualityRoll } from './dualityRoll.mjs';
|
||||
export { default as DhpAdversaryRoll } from './adversaryRoll.mjs';
|
||||
export { default as DhpDamageRoll } from './damageRoll.mjs';
|
||||
export { default as DhpAbilityUse } from './abilityUse.mjs';
|
||||
export { default as DhpEnvironment } from './environment.mjs';
|
||||
|
||||
export * as actors from './actor/_module.mjs';
|
||||
export * as items from './item/_module.mjs';
|
||||
export { actionsTypes } from './action/_module.mjs';
|
||||
export * as messages from './chat-message/_modules.mjs';
|
||||
export * as fields from './fields/_module.mjs';
|
||||
export * as pseudoDocuments from './pseudo-documents/_module.mjs';
|
||||
|
|
|
|||
|
|
@ -1,44 +0,0 @@
|
|||
export default class DaggerheartAction extends foundry.abstract.DataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
id: new fields.StringField({}),
|
||||
name: new fields.StringField({ initial: 'New Action' }),
|
||||
img: new fields.StringField({ initial: '' }),
|
||||
damage: new fields.SchemaField({
|
||||
type: new fields.StringField({ choices: SYSTEM.GENERAL.damageTypes, nullable: true, initial: null }),
|
||||
value: new fields.StringField({})
|
||||
}),
|
||||
healing: new fields.SchemaField({
|
||||
type: new fields.StringField({ choices: SYSTEM.GENERAL.healingTypes, nullable: true, initial: null }),
|
||||
value: new fields.StringField()
|
||||
}),
|
||||
conditions: new fields.ArrayField(
|
||||
new fields.SchemaField({
|
||||
name: new fields.StringField(),
|
||||
icon: new fields.StringField(),
|
||||
description: new fields.StringField()
|
||||
})
|
||||
),
|
||||
cost: new fields.SchemaField({
|
||||
type: new fields.StringField({ choices: SYSTEM.GENERAL.abilityCosts, nullable: true, initial: null }),
|
||||
value: new fields.NumberField({ nullable: true, initial: null })
|
||||
}),
|
||||
target: new fields.SchemaField({
|
||||
type: new fields.StringField({
|
||||
choices: SYSTEM.ACTIONS.targetTypes,
|
||||
initial: SYSTEM.ACTIONS.targetTypes.other.id
|
||||
})
|
||||
})
|
||||
// uses: new fields.SchemaField({
|
||||
// nr: new fields.StringField({}),
|
||||
// refreshType: new fields.StringField({ choices: SYSTEM.GENERAL.refreshTypes, initial: SYSTEM.GENERAL.refreshTypes.session.id }),
|
||||
// refreshed: new fields.BooleanField({ initial: true }),
|
||||
// }),
|
||||
};
|
||||
}
|
||||
|
||||
use = async () => {
|
||||
console.log('Test Use');
|
||||
};
|
||||
}
|
||||
23
module/data/action/_module.mjs
Normal file
23
module/data/action/_module.mjs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import {
|
||||
DHAttackAction,
|
||||
DHBaseAction,
|
||||
DHDamageAction,
|
||||
DHEffectAction,
|
||||
DHHealingAction,
|
||||
DHMacroAction,
|
||||
DHResourceAction,
|
||||
DHSpellCastAction,
|
||||
DHSummonAction
|
||||
} from './action.mjs';
|
||||
|
||||
export const actionsTypes = {
|
||||
base: DHBaseAction,
|
||||
attack: DHAttackAction,
|
||||
spellcast: DHSpellCastAction,
|
||||
resource: DHResourceAction,
|
||||
damage: DHDamageAction,
|
||||
healing: DHHealingAction,
|
||||
summon: DHSummonAction,
|
||||
effect: DHEffectAction,
|
||||
macro: DHMacroAction
|
||||
};
|
||||
386
module/data/action/action.mjs
Normal file
386
module/data/action/action.mjs
Normal file
|
|
@ -0,0 +1,386 @@
|
|||
import { abilities } from '../../config/actorConfig.mjs';
|
||||
import { DHActionDiceData, DHDamageData, DHDamageField } from './actionDice.mjs';
|
||||
|
||||
export default class DHAction extends foundry.abstract.DataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
id: new fields.DocumentIdField(),
|
||||
name: new fields.StringField({ initial: 'New Action' }),
|
||||
damage: new fields.SchemaField({
|
||||
type: new fields.StringField({ choices: SYSTEM.GENERAL.damageTypes, nullable: true, initial: null }),
|
||||
value: new fields.StringField({})
|
||||
}),
|
||||
healing: new fields.SchemaField({
|
||||
type: new fields.StringField({ choices: SYSTEM.GENERAL.healingTypes, nullable: true, initial: null }),
|
||||
value: new fields.StringField()
|
||||
}),
|
||||
conditions: new fields.ArrayField(
|
||||
new fields.SchemaField({
|
||||
name: new fields.StringField(),
|
||||
icon: new fields.StringField(),
|
||||
description: new fields.StringField()
|
||||
})
|
||||
),
|
||||
cost: new fields.SchemaField({
|
||||
type: new fields.StringField({ choices: SYSTEM.GENERAL.abilityCosts, nullable: true, initial: null }),
|
||||
value: new fields.NumberField({ nullable: true, initial: null })
|
||||
}),
|
||||
target: new fields.SchemaField({
|
||||
type: new fields.StringField({
|
||||
choices: SYSTEM.ACTIONS.targetTypes,
|
||||
initial: SYSTEM.ACTIONS.targetTypes.other.id
|
||||
})
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const fields = foundry.data.fields;
|
||||
|
||||
/*
|
||||
ToDo
|
||||
- Apply ActiveEffect => Add to Chat message like Damage Button ?
|
||||
- Add Drag & Drop for documentUUID field (Macro & Summon)
|
||||
- Add optionnal Role for Healing ?
|
||||
- Handle Roll result as part of formula if needed
|
||||
- Target Check
|
||||
- Cost Check
|
||||
- Range Check
|
||||
- Area of effect and measurement placement
|
||||
- Auto use costs and action
|
||||
*/
|
||||
|
||||
export class DHBaseAction extends foundry.abstract.DataModel {
|
||||
static defineSchema() {
|
||||
return {
|
||||
_id: new fields.DocumentIdField(),
|
||||
type: new fields.StringField({ initial: undefined, readonly: true, required: true }),
|
||||
name: new fields.StringField({ initial: undefined }),
|
||||
img: new fields.FilePathField({ initial: undefined, categories: ['IMAGE'], base64: false }),
|
||||
actionType: new fields.StringField({ choices: SYSTEM.ITEM.actionTypes, initial: 'action', nullable: true }),
|
||||
cost: new fields.ArrayField(
|
||||
new fields.SchemaField({
|
||||
type: new fields.StringField({
|
||||
choices: SYSTEM.GENERAL.abilityCosts,
|
||||
nullable: false,
|
||||
required: true,
|
||||
initial: 'hope'
|
||||
}),
|
||||
value: new fields.NumberField({ nullable: true, initial: 1 }),
|
||||
scalable: new fields.BooleanField({ initial: false }),
|
||||
step: new fields.NumberField({ nullable: true, initial: null })
|
||||
})
|
||||
),
|
||||
uses: new fields.SchemaField({
|
||||
value: new fields.NumberField({ nullable: true, initial: null }),
|
||||
max: new fields.NumberField({ nullable: true, initial: null }),
|
||||
recovery: new fields.StringField({
|
||||
choices: SYSTEM.GENERAL.refreshTypes,
|
||||
initial: null,
|
||||
nullable: true
|
||||
})
|
||||
}),
|
||||
range: new fields.StringField({
|
||||
choices: SYSTEM.GENERAL.range,
|
||||
required: true,
|
||||
blank: false,
|
||||
initial: 'self'
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
prepareData() {}
|
||||
|
||||
get index() {
|
||||
return this.parent.actions.indexOf(this);
|
||||
}
|
||||
|
||||
get item() {
|
||||
return this.parent.parent;
|
||||
}
|
||||
|
||||
get actor() {
|
||||
return this.item?.actor;
|
||||
}
|
||||
|
||||
get chatTemplate() {
|
||||
return 'systems/daggerheart/templates/chat/attack-roll.hbs';
|
||||
}
|
||||
|
||||
static getRollType() {
|
||||
return 'ability';
|
||||
}
|
||||
|
||||
static getSourceConfig(parent) {
|
||||
const updateSource = {};
|
||||
updateSource.img ??= parent?.img ?? parent?.system?.img;
|
||||
if (parent?.system?.trait) {
|
||||
updateSource['roll'] = {
|
||||
type: this.getRollType(),
|
||||
trait: parent.system.trait
|
||||
};
|
||||
}
|
||||
if (parent?.system?.range) {
|
||||
updateSource['range'] = parent?.system?.range;
|
||||
}
|
||||
return updateSource;
|
||||
}
|
||||
|
||||
async use(event) {
|
||||
if (this.roll.type && this.roll.trait) {
|
||||
const modifierValue = this.actor.system.traits[this.roll.trait].value;
|
||||
const config = {
|
||||
event: event,
|
||||
title: this.item.name,
|
||||
roll: {
|
||||
modifier: modifierValue,
|
||||
label: game.i18n.localize(abilities[this.roll.trait].label),
|
||||
type: this.actionType,
|
||||
difficulty: this.roll?.difficulty
|
||||
},
|
||||
chatMessage: {
|
||||
template: this.chatTemplate
|
||||
}
|
||||
};
|
||||
if (this.target?.type) config.checkTarget = true;
|
||||
if (this.damage.parts.length) {
|
||||
config.damage = {
|
||||
value: this.damage.parts.map(p => p.getFormula(this.actor)).join(' + '),
|
||||
type: this.damage.parts[0].type
|
||||
};
|
||||
}
|
||||
if (this.effects.length) {
|
||||
// Apply Active Effects. In Chat Message ?
|
||||
}
|
||||
return this.actor.diceRoll(config);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const extraDefineSchema = (field, option) => {
|
||||
return {
|
||||
[field]: {
|
||||
// damage: new fields.SchemaField({
|
||||
// parts: new fields.ArrayField(new fields.EmbeddedDataField(DHDamageData))
|
||||
// }),
|
||||
damage: new DHDamageField(option),
|
||||
roll: new fields.SchemaField({
|
||||
type: new fields.StringField({ nullable: true, initial: null, choices: SYSTEM.GENERAL.rollTypes }),
|
||||
trait: new fields.StringField({ nullable: true, initial: null, choices: SYSTEM.ACTOR.abilities }),
|
||||
difficulty: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 })
|
||||
}),
|
||||
target: new fields.SchemaField({
|
||||
type: new fields.StringField({
|
||||
choices: SYSTEM.ACTIONS.targetTypes,
|
||||
initial: SYSTEM.ACTIONS.targetTypes.other.id
|
||||
})
|
||||
}),
|
||||
effects: new fields.ArrayField( // ActiveEffect
|
||||
new fields.SchemaField({
|
||||
_id: new fields.DocumentIdField()
|
||||
})
|
||||
)
|
||||
}[field]
|
||||
};
|
||||
};
|
||||
|
||||
export class DHAttackAction extends DHBaseAction {
|
||||
static defineSchema() {
|
||||
return {
|
||||
...super.defineSchema(),
|
||||
...extraDefineSchema('damage', true),
|
||||
...extraDefineSchema('roll'),
|
||||
...extraDefineSchema('target'),
|
||||
...extraDefineSchema('effects')
|
||||
};
|
||||
}
|
||||
|
||||
static getRollType() {
|
||||
return 'weapon';
|
||||
}
|
||||
|
||||
prepareData() {
|
||||
super.prepareData();
|
||||
if (this.damage.includeBase && !!this.item?.system?.damage) {
|
||||
const baseDamage = this.getParentDamage();
|
||||
this.damage.parts.unshift(new DHDamageData(baseDamage));
|
||||
}
|
||||
}
|
||||
|
||||
getParentDamage() {
|
||||
return {
|
||||
multiplier: 'proficiency',
|
||||
dice: this.item?.system?.damage.value,
|
||||
bonus: this.item?.system?.damage.bonus ?? 0,
|
||||
type: this.item?.system?.damage.type,
|
||||
base: true
|
||||
};
|
||||
}
|
||||
|
||||
// Temporary until full formula parser
|
||||
// getDamageFormula() {
|
||||
// return this.damage.parts.map(p => p.formula).join(' + ');
|
||||
// }
|
||||
}
|
||||
|
||||
export class DHSpellCastAction extends DHBaseAction {
|
||||
static defineSchema() {
|
||||
return {
|
||||
...super.defineSchema(),
|
||||
...extraDefineSchema('damage'),
|
||||
...extraDefineSchema('roll'),
|
||||
...extraDefineSchema('target'),
|
||||
...extraDefineSchema('effects')
|
||||
};
|
||||
}
|
||||
|
||||
static getRollType() {
|
||||
return 'spellcast';
|
||||
}
|
||||
}
|
||||
|
||||
export class DHDamageAction extends DHBaseAction {
|
||||
static defineSchema() {
|
||||
return {
|
||||
...super.defineSchema(),
|
||||
...extraDefineSchema('damage', false),
|
||||
...extraDefineSchema('target'),
|
||||
...extraDefineSchema('effects')
|
||||
};
|
||||
}
|
||||
|
||||
async use(event) {
|
||||
const formula = this.damage.parts.map(p => p.getFormula(this.actor)).join(' + ');
|
||||
if (!formula || formula == '') return;
|
||||
|
||||
let roll = { formula: formula, total: formula };
|
||||
if (isNaN(formula)) {
|
||||
roll = await new Roll(formula).evaluate();
|
||||
}
|
||||
|
||||
const cls = getDocumentClass('ChatMessage');
|
||||
const msg = new cls({
|
||||
user: game.user.id,
|
||||
content: await foundry.applications.handlebars.renderTemplate(
|
||||
'systems/daggerheart/templates/chat/damage-roll.hbs',
|
||||
{
|
||||
roll: roll.formula,
|
||||
total: roll.total,
|
||||
type: this.damage.parts.map(p => p.type)
|
||||
}
|
||||
)
|
||||
});
|
||||
|
||||
cls.create(msg.toObject());
|
||||
}
|
||||
}
|
||||
|
||||
export class DHHealingAction extends DHBaseAction {
|
||||
static defineSchema() {
|
||||
return {
|
||||
...super.defineSchema(),
|
||||
healing: new fields.SchemaField({
|
||||
type: new fields.StringField({
|
||||
choices: SYSTEM.GENERAL.healingTypes,
|
||||
required: true,
|
||||
blank: false,
|
||||
initial: SYSTEM.GENERAL.healingTypes.health.id,
|
||||
label: 'Healing'
|
||||
}),
|
||||
value: new fields.EmbeddedDataField(DHActionDiceData)
|
||||
}),
|
||||
...extraDefineSchema('target'),
|
||||
...extraDefineSchema('effects')
|
||||
};
|
||||
}
|
||||
|
||||
async use(event) {
|
||||
const formula = this.healing.value.getFormula(this.actor);
|
||||
if (!formula || formula == '') return;
|
||||
|
||||
// const roll = await super.use(event);
|
||||
let roll = { formula: formula, total: formula };
|
||||
if (isNaN(formula)) {
|
||||
roll = await new Roll(formula).evaluate();
|
||||
}
|
||||
|
||||
const cls = getDocumentClass('ChatMessage');
|
||||
const msg = new cls({
|
||||
user: game.user.id,
|
||||
content: await foundry.applications.handlebars.renderTemplate(
|
||||
'systems/daggerheart/templates/chat/healing-roll.hbs',
|
||||
{
|
||||
roll: roll.formula,
|
||||
total: roll.total,
|
||||
type: this.healing.type
|
||||
}
|
||||
)
|
||||
});
|
||||
|
||||
cls.create(msg.toObject());
|
||||
}
|
||||
|
||||
get chatTemplate() {
|
||||
return 'systems/daggerheart/templates/chat/healing-roll.hbs';
|
||||
}
|
||||
}
|
||||
|
||||
export class DHResourceAction extends DHBaseAction {
|
||||
static defineSchema() {
|
||||
return {
|
||||
...super.defineSchema(),
|
||||
// ...extraDefineSchema('roll'),
|
||||
...extraDefineSchema('target'),
|
||||
...extraDefineSchema('effects'),
|
||||
resource: new fields.SchemaField({
|
||||
type: new fields.StringField({
|
||||
choices: [],
|
||||
blank: true,
|
||||
required: false,
|
||||
initial: '',
|
||||
label: 'Resource'
|
||||
}),
|
||||
value: new fields.NumberField({ initial: 0, label: 'Value' })
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class DHSummonAction extends DHBaseAction {
|
||||
static defineSchema() {
|
||||
return {
|
||||
...super.defineSchema(),
|
||||
documentUUID: new fields.StringField({ blank: true, initial: '', placeholder: 'Enter a Creature UUID' })
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class DHEffectAction extends DHBaseAction {
|
||||
static defineSchema() {
|
||||
return {
|
||||
...super.defineSchema(),
|
||||
...extraDefineSchema('effects')
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class DHMacroAction extends DHBaseAction {
|
||||
static defineSchema() {
|
||||
return {
|
||||
...super.defineSchema(),
|
||||
documentUUID: new fields.StringField({ blank: true, initial: '', placeholder: 'Enter a macro UUID' })
|
||||
};
|
||||
}
|
||||
|
||||
async use(event) {
|
||||
const fixUUID = !this.documentUUID.includes('Macro.') ? `Macro.${this.documentUUID}` : this.documentUUID,
|
||||
macro = await fromUuid(fixUUID);
|
||||
try {
|
||||
if (!macro) throw new Error(`No macro found for the UUID: ${this.documentUUID}.`);
|
||||
macro.execute();
|
||||
} catch (error) {
|
||||
ui.notifications.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
55
module/data/action/actionDice.mjs
Normal file
55
module/data/action/actionDice.mjs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import FormulaField from '../fields/formulaField.mjs';
|
||||
|
||||
const fields = foundry.data.fields;
|
||||
|
||||
export class DHActionDiceData extends foundry.abstract.DataModel {
|
||||
/** @override */
|
||||
static defineSchema() {
|
||||
return {
|
||||
multiplier: new fields.StringField({
|
||||
choices: SYSTEM.GENERAL.multiplierTypes,
|
||||
initial: 'proficiency',
|
||||
label: 'Multiplier'
|
||||
}),
|
||||
dice: new fields.StringField({ choices: SYSTEM.GENERAL.diceTypes, initial: 'd6', label: 'Formula' }),
|
||||
bonus: new fields.NumberField({ nullable: true, initial: null, label: 'Bonus' }),
|
||||
custom: new fields.SchemaField({
|
||||
enabled: new fields.BooleanField({ label: 'Custom Formula' }),
|
||||
formula: new FormulaField({ label: 'Formula' })
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
getFormula(actor) {
|
||||
return this.custom.enabled
|
||||
? this.custom.formula
|
||||
: `${actor.system[this.multiplier] ?? 1}${this.dice}${this.bonus ? (this.bonus < 0 ? ` - ${Math.abs(this.bonus)}` : ` + ${this.bonus}`) : ''}`;
|
||||
}
|
||||
}
|
||||
|
||||
export class DHDamageField extends fields.SchemaField {
|
||||
constructor(hasBase, options, context = {}) {
|
||||
const damageFields = {
|
||||
parts: new fields.ArrayField(new fields.EmbeddedDataField(DHDamageData))
|
||||
};
|
||||
if (hasBase) damageFields.includeBase = new fields.BooleanField({ initial: true });
|
||||
super(damageFields, options, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class DHDamageData extends DHActionDiceData {
|
||||
/** @override */
|
||||
static defineSchema() {
|
||||
return {
|
||||
...super.defineSchema(),
|
||||
base: new fields.BooleanField({ initial: false, readonly: true, label: 'Base' }),
|
||||
type: new fields.StringField({
|
||||
choices: SYSTEM.GENERAL.damageTypes,
|
||||
initial: 'physical',
|
||||
label: 'Type',
|
||||
nullable: false,
|
||||
required: true
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
11
module/data/actor/_module.mjs
Normal file
11
module/data/actor/_module.mjs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import DhCharacter from './character.mjs';
|
||||
import DhAdversary from './adversary.mjs';
|
||||
import DhEnvironment from './environment.mjs';
|
||||
|
||||
export { DhCharacter, DhAdversary, DhEnvironment };
|
||||
|
||||
export const config = {
|
||||
character: DhCharacter,
|
||||
adversary: DhAdversary,
|
||||
environment: DhEnvironment
|
||||
};
|
||||
68
module/data/actor/adversary.mjs
Normal file
68
module/data/actor/adversary.mjs
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import BaseDataActor from './base.mjs';
|
||||
|
||||
const resourceField = () =>
|
||||
new foundry.data.fields.SchemaField({
|
||||
value: new foundry.data.fields.NumberField({ initial: 0, integer: true }),
|
||||
max: new foundry.data.fields.NumberField({ initial: 0, integer: true })
|
||||
});
|
||||
|
||||
export default class DhpAdversary extends BaseDataActor {
|
||||
static LOCALIZATION_PREFIXES = ['DAGGERHEART.Sheets.Adversary'];
|
||||
|
||||
static get metadata() {
|
||||
return foundry.utils.mergeObject(super.metadata, {
|
||||
label: 'TYPES.Actor.adversary',
|
||||
type: 'adversary'
|
||||
});
|
||||
}
|
||||
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
tier: new fields.StringField({
|
||||
required: true,
|
||||
choices: SYSTEM.GENERAL.tiers,
|
||||
initial: SYSTEM.GENERAL.tiers.tier1.id
|
||||
}),
|
||||
type: new fields.StringField({
|
||||
required: true,
|
||||
choices: SYSTEM.ACTOR.adversaryTypes,
|
||||
initial: SYSTEM.ACTOR.adversaryTypes.standard.id
|
||||
}),
|
||||
motivesAndTactics: new fields.HTMLField(),
|
||||
difficulty: new fields.NumberField({ required: true, initial: 1, integer: true }),
|
||||
damageThresholds: new fields.SchemaField({
|
||||
major: new fields.NumberField({ required: true, initial: 0, integer: true }),
|
||||
severe: new fields.NumberField({ required: true, initial: 0, integer: true })
|
||||
}),
|
||||
resources: new fields.SchemaField({
|
||||
hitPoints: resourceField(),
|
||||
stress: resourceField()
|
||||
}),
|
||||
attack: new fields.SchemaField({
|
||||
name: new fields.StringField({}),
|
||||
modifier: new fields.NumberField({ required: true, integer: true, initial: 0 }),
|
||||
range: new fields.StringField({
|
||||
required: true,
|
||||
choices: SYSTEM.GENERAL.range,
|
||||
initial: SYSTEM.GENERAL.range.melee.id
|
||||
}),
|
||||
damage: new fields.SchemaField({
|
||||
value: new fields.StringField(),
|
||||
type: new fields.StringField({
|
||||
required: true,
|
||||
choices: SYSTEM.GENERAL.damageTypes,
|
||||
initial: SYSTEM.GENERAL.damageTypes.physical.id
|
||||
})
|
||||
})
|
||||
}),
|
||||
experiences: new fields.TypedObjectField(
|
||||
new fields.SchemaField({
|
||||
name: new fields.StringField(),
|
||||
value: new fields.NumberField({ required: true, integer: true, initial: 1 })
|
||||
})
|
||||
)
|
||||
/* Features waiting on pseudo-document data model addition */
|
||||
};
|
||||
}
|
||||
}
|
||||
34
module/data/actor/base.mjs
Normal file
34
module/data/actor/base.mjs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
/**
|
||||
* Describes metadata about the actor data model type
|
||||
* @typedef {Object} ActorDataModelMetadata
|
||||
* @property {string} label - A localizable label used on application.
|
||||
* @property {string} type - The system type that this data model represents.
|
||||
*/
|
||||
export default class BaseDataActor extends foundry.abstract.TypeDataModel {
|
||||
/** @returns {ActorDataModelMetadata}*/
|
||||
static get metadata() {
|
||||
return {
|
||||
label: 'Base Actor',
|
||||
type: 'base'
|
||||
};
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
|
||||
return {
|
||||
description: new fields.HTMLField({ required: true, nullable: true })
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain a data object used to evaluate any dice rolls associated with this Item Type
|
||||
* @param {object} [options] - Options which modify the getRollData method.
|
||||
* @returns {object}
|
||||
*/
|
||||
getRollData() {
|
||||
const data = { ...this };
|
||||
return data;
|
||||
}
|
||||
}
|
||||
300
module/data/actor/character.mjs
Normal file
300
module/data/actor/character.mjs
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
import { burden } from '../../config/generalConfig.mjs';
|
||||
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
|
||||
import { LevelOptionType } from '../levelTier.mjs';
|
||||
import BaseDataActor from './base.mjs';
|
||||
|
||||
const attributeField = () =>
|
||||
new foundry.data.fields.SchemaField({
|
||||
value: new foundry.data.fields.NumberField({ initial: 0, integer: true }),
|
||||
bonus: new foundry.data.fields.NumberField({ initial: 0, integer: true }),
|
||||
tierMarked: new foundry.data.fields.BooleanField({ initial: false })
|
||||
});
|
||||
|
||||
const resourceField = max =>
|
||||
new foundry.data.fields.SchemaField({
|
||||
value: new foundry.data.fields.NumberField({ initial: 0, integer: true }),
|
||||
bonus: new foundry.data.fields.NumberField({ initial: 0, integer: true }),
|
||||
max: new foundry.data.fields.NumberField({ initial: max, integer: true })
|
||||
});
|
||||
|
||||
export default class DhCharacter extends BaseDataActor {
|
||||
static get metadata() {
|
||||
return foundry.utils.mergeObject(super.metadata, {
|
||||
label: 'TYPES.Actor.character',
|
||||
type: 'character'
|
||||
});
|
||||
}
|
||||
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
|
||||
return {
|
||||
resources: new fields.SchemaField({
|
||||
hitPoints: resourceField(6),
|
||||
stress: resourceField(6),
|
||||
hope: resourceField(6)
|
||||
}),
|
||||
traits: new fields.SchemaField({
|
||||
agility: attributeField(),
|
||||
strength: attributeField(),
|
||||
finesse: attributeField(),
|
||||
instinct: attributeField(),
|
||||
presence: attributeField(),
|
||||
knowledge: attributeField()
|
||||
}),
|
||||
proficiency: new fields.SchemaField({
|
||||
value: new fields.NumberField({ initial: 1, integer: true }),
|
||||
bonus: new fields.NumberField({ initial: 0, integer: true })
|
||||
}),
|
||||
evasion: new fields.SchemaField({
|
||||
bonus: new fields.NumberField({ initial: 0, integer: true })
|
||||
}),
|
||||
experiences: new fields.TypedObjectField(
|
||||
new fields.SchemaField({
|
||||
description: new fields.StringField({}),
|
||||
value: new fields.NumberField({ integer: true, initial: 0 }),
|
||||
bonus: new fields.NumberField({ integer: true, initial: 0 })
|
||||
}),
|
||||
{
|
||||
initial: {
|
||||
[foundry.utils.randomID()]: { description: '', value: 2 },
|
||||
[foundry.utils.randomID()]: { description: '', value: 2 }
|
||||
}
|
||||
}
|
||||
),
|
||||
gold: new fields.SchemaField({
|
||||
coins: new fields.NumberField({ initial: 0, integer: true }),
|
||||
handfulls: new fields.NumberField({ initial: 0, integer: true }),
|
||||
bags: new fields.NumberField({ initial: 0, integer: true }),
|
||||
chests: new fields.NumberField({ initial: 0, integer: true })
|
||||
}),
|
||||
pronouns: new fields.StringField({}),
|
||||
scars: new fields.TypedObjectField(
|
||||
new fields.SchemaField({
|
||||
name: new fields.StringField({}),
|
||||
description: new fields.HTMLField()
|
||||
})
|
||||
),
|
||||
story: new fields.HTMLField(),
|
||||
class: new fields.SchemaField({
|
||||
value: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }),
|
||||
subclass: new ForeignDocumentUUIDField({ type: 'Item', nullable: true })
|
||||
}),
|
||||
multiclass: new fields.SchemaField({
|
||||
value: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }),
|
||||
subclass: new ForeignDocumentUUIDField({ type: 'Item', nullable: true })
|
||||
}),
|
||||
levelData: new fields.EmbeddedDataField(DhPCLevelData)
|
||||
};
|
||||
}
|
||||
|
||||
get ancestry() {
|
||||
return this.parent.items.find(x => x.type === 'ancestry') ?? null;
|
||||
}
|
||||
|
||||
get community() {
|
||||
return this.parent.items.find(x => x.type === 'community') ?? null;
|
||||
}
|
||||
|
||||
get domains() {
|
||||
const classDomains = this.class.value ? this.class.value.system.domains : [];
|
||||
const multiclassDomains = this.multiclass.value ? this.multiclass.value.system.domains : [];
|
||||
return [...classDomains, ...multiclassDomains];
|
||||
}
|
||||
|
||||
get domainCards() {
|
||||
const domainCards = this.parent.items.filter(x => x.type === 'domainCard');
|
||||
const loadout = domainCards.filter(x => !x.system.inVault);
|
||||
const vault = domainCards.filter(x => x.system.inVault);
|
||||
|
||||
return {
|
||||
loadout: loadout,
|
||||
vault: vault,
|
||||
total: [...loadout, ...vault]
|
||||
};
|
||||
}
|
||||
|
||||
get armor() {
|
||||
return this.parent.items.find(x => x.type === 'armor' && x.system.equipped);
|
||||
}
|
||||
|
||||
get primaryWeapon() {
|
||||
return this.parent.items.find(x => x.type === 'weapon' && x.system.equipped && !x.system.secondary);
|
||||
}
|
||||
|
||||
get secondaryWeapon() {
|
||||
return this.parent.items.find(x => x.type === 'weapon' && x.system.equipped && x.system.secondary);
|
||||
}
|
||||
|
||||
get getWeaponBurden() {
|
||||
return this.primaryWeapon?.system?.burden === burden.twoHanded.value ||
|
||||
(this.primaryWeapon && this.secondaryWeapon)
|
||||
? burden.twoHanded.value
|
||||
: this.primaryWeapon || this.secondaryWeapon
|
||||
? burden.oneHanded.value
|
||||
: null;
|
||||
}
|
||||
|
||||
get refreshableFeatures() {
|
||||
return this.parent.items.reduce(
|
||||
(acc, x) => {
|
||||
if (x.type === 'feature' && x.system.refreshData?.type === 'feature' && x.system.refreshData?.type) {
|
||||
acc[x.system.refreshData.type].push(x);
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{ shortRest: [], longRest: [] }
|
||||
);
|
||||
}
|
||||
|
||||
static async unequipBeforeEquip(itemToEquip) {
|
||||
const primary = this.primaryWeapon,
|
||||
secondary = this.secondaryWeapon;
|
||||
if (itemToEquip.system.secondary) {
|
||||
if (primary && primary.burden === SYSTEM.GENERAL.burden.twoHanded.value) {
|
||||
await primary.update({ 'system.equipped': false });
|
||||
}
|
||||
|
||||
if (secondary) {
|
||||
await secondary.update({ 'system.equipped': false });
|
||||
}
|
||||
} else {
|
||||
if (secondary && itemToEquip.system.burden === SYSTEM.GENERAL.burden.twoHanded.value) {
|
||||
await secondary.update({ 'system.equipped': false });
|
||||
}
|
||||
|
||||
if (primary) {
|
||||
await primary.update({ 'system.equipped': false });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prepareBaseData() {
|
||||
const currentLevel = this.levelData.level.current;
|
||||
const currentTier =
|
||||
currentLevel === 1
|
||||
? null
|
||||
: Object.values(game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.LevelTiers).tiers).find(
|
||||
tier => currentLevel >= tier.levels.start && currentLevel <= tier.levels.end
|
||||
).tier;
|
||||
for (let levelKey in this.levelData.levelups) {
|
||||
const level = this.levelData.levelups[levelKey];
|
||||
|
||||
this.proficiency.bonus += level.achievements.proficiency;
|
||||
|
||||
for (let selection of level.selections) {
|
||||
switch (selection.type) {
|
||||
case 'trait':
|
||||
selection.data.forEach(data => {
|
||||
this.traits[data].bonus += 1;
|
||||
this.traits[data].tierMarked = selection.tier === currentTier;
|
||||
});
|
||||
break;
|
||||
case 'hitPoint':
|
||||
this.resources.hitPoints.bonus += selection.value;
|
||||
break;
|
||||
case 'stress':
|
||||
this.resources.stress.bonus += selection.value;
|
||||
break;
|
||||
case 'evasion':
|
||||
this.evasion.bonus += selection.value;
|
||||
break;
|
||||
case 'proficiency':
|
||||
this.proficiency.bonus = selection.value;
|
||||
break;
|
||||
case 'experience':
|
||||
Object.keys(this.experiences).forEach(key => {
|
||||
const experience = this.experiences[key];
|
||||
experience.bonus += selection.value;
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const armor = this.armor;
|
||||
this.damageThresholds = {
|
||||
major: armor
|
||||
? armor.system.baseThresholds.major + this.levelData.level.current
|
||||
: this.levelData.level.current,
|
||||
severe: armor
|
||||
? armor.system.baseThresholds.severe + this.levelData.level.current
|
||||
: this.levelData.level.current * 2
|
||||
};
|
||||
}
|
||||
|
||||
prepareDerivedData() {
|
||||
this.resources.hope.max -= Object.keys(this.scars).length;
|
||||
this.resources.hope.value = Math.min(this.resources.hope.value, this.resources.hope.max);
|
||||
|
||||
for (var traitKey in this.traits) {
|
||||
var trait = this.traits[traitKey];
|
||||
trait.total = trait.value + trait.bonus;
|
||||
}
|
||||
|
||||
for (var experienceKey in this.experiences) {
|
||||
var experience = this.experiences[experienceKey];
|
||||
experience.total = experience.value + experience.bonus;
|
||||
}
|
||||
|
||||
this.resources.hitPoints.maxTotal = this.resources.hitPoints.max + this.resources.hitPoints.bonus;
|
||||
this.resources.stress.maxTotal = this.resources.stress.max + this.resources.stress.bonus;
|
||||
this.evasion.total = (this.class?.evasion ?? 0) + this.evasion.bonus;
|
||||
this.proficiency.total = this.proficiency.value + this.proficiency.bonus;
|
||||
}
|
||||
}
|
||||
|
||||
class DhPCLevelData extends foundry.abstract.DataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
|
||||
return {
|
||||
level: new fields.SchemaField({
|
||||
current: new fields.NumberField({ required: true, integer: true, initial: 1 }),
|
||||
changed: new fields.NumberField({ required: true, integer: true, initial: 1 })
|
||||
}),
|
||||
levelups: new fields.TypedObjectField(
|
||||
new fields.SchemaField({
|
||||
achievements: new fields.SchemaField(
|
||||
{
|
||||
experiences: new fields.TypedObjectField(
|
||||
new fields.SchemaField({
|
||||
name: new fields.StringField({ required: true }),
|
||||
modifier: new fields.NumberField({ required: true, integer: true })
|
||||
})
|
||||
),
|
||||
domainCards: new fields.ArrayField(
|
||||
new fields.SchemaField({
|
||||
uuid: new fields.StringField({ required: true }),
|
||||
itemUuid: new fields.StringField({ required: true })
|
||||
})
|
||||
),
|
||||
proficiency: new fields.NumberField({ integer: true })
|
||||
},
|
||||
{ nullable: true, initial: null }
|
||||
),
|
||||
selections: new fields.ArrayField(
|
||||
new fields.SchemaField({
|
||||
tier: new fields.NumberField({ required: true, integer: true }),
|
||||
level: new fields.NumberField({ required: true, integer: true }),
|
||||
optionKey: new fields.StringField({ required: true }),
|
||||
type: new fields.StringField({ required: true, choices: LevelOptionType }),
|
||||
checkboxNr: new fields.NumberField({ required: true, integer: true }),
|
||||
value: new fields.NumberField({ integer: true }),
|
||||
minCost: new fields.NumberField({ integer: true }),
|
||||
amount: new fields.NumberField({ integer: true }),
|
||||
data: new fields.ArrayField(new fields.StringField({ required: true })),
|
||||
secondaryData: new fields.TypedObjectField(new fields.StringField({ required: true })),
|
||||
itemUuid: new fields.StringField({ required: true })
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
get canLevelUp() {
|
||||
return this.level.current < this.level.changed;
|
||||
}
|
||||
}
|
||||
35
module/data/actor/environment.mjs
Normal file
35
module/data/actor/environment.mjs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { environmentTypes } from '../../config/actorConfig.mjs';
|
||||
import BaseDataActor from './base.mjs';
|
||||
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
|
||||
|
||||
export default class DhEnvironment extends BaseDataActor {
|
||||
static LOCALIZATION_PREFIXES = ['DAGGERHEART.Sheets.Environment'];
|
||||
|
||||
static get metadata() {
|
||||
return foundry.utils.mergeObject(super.metadata, {
|
||||
label: 'TYPES.Actor.environment',
|
||||
type: 'environment'
|
||||
});
|
||||
}
|
||||
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
tier: new fields.StringField({
|
||||
required: true,
|
||||
choices: SYSTEM.GENERAL.tiers,
|
||||
initial: SYSTEM.GENERAL.tiers.tier1.id
|
||||
}),
|
||||
type: new fields.StringField({ choices: environmentTypes }),
|
||||
impulses: new fields.HTMLField(),
|
||||
difficulty: new fields.NumberField({ required: true, initial: 11, integer: true }),
|
||||
potentialAdversaries: new fields.TypedObjectField(
|
||||
new fields.SchemaField({
|
||||
label: new fields.StringField(),
|
||||
adversaries: new fields.TypedObjectField(new ForeignDocumentUUIDField({ type: 'Actor' }))
|
||||
})
|
||||
)
|
||||
/* Features pending datamodel rework */
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
export default class DhpAdversary extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
resources: new fields.SchemaField({
|
||||
health: new fields.SchemaField({
|
||||
value: new fields.NumberField({ initial: 0, integer: true }),
|
||||
min: new fields.NumberField({ initial: 0, integer: true }),
|
||||
max: new fields.NumberField({ initial: 0, integer: true })
|
||||
}),
|
||||
stress: new fields.SchemaField({
|
||||
value: new fields.NumberField({ initial: 0, integer: true }),
|
||||
min: new fields.NumberField({ initial: 0, integer: true }),
|
||||
max: new fields.NumberField({ initial: 0, integer: true })
|
||||
})
|
||||
}),
|
||||
tier: new fields.StringField({ choices: Object.keys(SYSTEM.GENERAL.tiers), integer: false }),
|
||||
type: new fields.StringField({
|
||||
choices: Object.keys(SYSTEM.ACTOR.adversaryTypes),
|
||||
integer: false,
|
||||
initial: Object.keys(SYSTEM.ACTOR.adversaryTypes).find(x => x === 'standard')
|
||||
}),
|
||||
description: new fields.StringField({}),
|
||||
motivesAndTactics: new fields.ArrayField(new fields.StringField({})),
|
||||
attackModifier: new fields.NumberField({ integer: true, nullabe: true, initial: null }),
|
||||
attack: new fields.SchemaField({
|
||||
name: new fields.StringField({}),
|
||||
range: new fields.StringField({ choices: Object.keys(SYSTEM.GENERAL.range), integer: false }),
|
||||
damage: new fields.SchemaField({
|
||||
value: new fields.StringField({}),
|
||||
type: new fields.StringField({ choices: Object.keys(SYSTEM.GENERAL.damageTypes), integer: false })
|
||||
})
|
||||
}),
|
||||
difficulty: new fields.NumberField({ initial: 1, integer: true }),
|
||||
damageThresholds: new fields.SchemaField({
|
||||
major: new fields.NumberField({ initial: 0, integer: true }),
|
||||
severe: new fields.NumberField({ initial: 0, integer: true })
|
||||
}),
|
||||
experiences: new fields.TypedObjectField(
|
||||
new fields.SchemaField({
|
||||
id: new fields.StringField({ required: true }),
|
||||
name: new fields.StringField(),
|
||||
value: new fields.NumberField({ integer: true, nullable: true, initial: null })
|
||||
})
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
get features() {
|
||||
return this.parent.items.filter(x => x.type === 'feature');
|
||||
}
|
||||
}
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
export default class DhpAdversaryRoll extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
|
||||
return {
|
||||
title: new fields.StringField(),
|
||||
origin: new fields.StringField({ required: true }),
|
||||
roll: new fields.StringField({}),
|
||||
total: new fields.NumberField({ integer: true }),
|
||||
modifiers: new fields.ArrayField(
|
||||
new fields.SchemaField({
|
||||
value: new fields.NumberField({ integer: true }),
|
||||
label: new fields.StringField({}),
|
||||
title: new fields.StringField({})
|
||||
})
|
||||
),
|
||||
advantageState: new fields.NumberField({ required: true, choices: [0, 1, 2], initial: 0 }),
|
||||
dice: new fields.EmbeddedDataField(DhpAdversaryRollDice),
|
||||
targets: new fields.ArrayField(
|
||||
new fields.SchemaField({
|
||||
id: new fields.StringField({}),
|
||||
name: new fields.StringField({}),
|
||||
img: new fields.StringField({}),
|
||||
difficulty: new fields.NumberField({ integer: true, nullable: true }),
|
||||
evasion: new fields.NumberField({ integer: true }),
|
||||
hit: new fields.BooleanField({ initial: false })
|
||||
})
|
||||
),
|
||||
damage: new fields.SchemaField(
|
||||
{
|
||||
value: new fields.StringField({}),
|
||||
type: new fields.StringField({ choices: Object.keys(SYSTEM.GENERAL.damageTypes), integer: false })
|
||||
},
|
||||
{ nullable: true, initial: null }
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
prepareDerivedData() {
|
||||
const diceKeys = Object.keys(this.dice.rolls);
|
||||
const highestDiceIndex =
|
||||
diceKeys.length < 2
|
||||
? null
|
||||
: this.dice.rolls[diceKeys[0]].value > this.dice.rolls[diceKeys[1]].value
|
||||
? 0
|
||||
: 1;
|
||||
if (highestDiceIndex !== null) {
|
||||
this.dice.rolls = this.dice.rolls.map((roll, index) => ({
|
||||
...roll,
|
||||
discarded: this.advantageState === 1 ? index !== highestDiceIndex : index === highestDiceIndex
|
||||
}));
|
||||
}
|
||||
|
||||
this.targets.forEach(target => {
|
||||
target.hit = target.difficulty ? this.total >= target.difficulty : this.total >= target.evasion;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class DhpAdversaryRollDice extends foundry.abstract.DataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
|
||||
return {
|
||||
type: new fields.StringField({ required: true }),
|
||||
rolls: new fields.ArrayField(
|
||||
new fields.SchemaField({
|
||||
value: new fields.NumberField({ required: true, integer: true }),
|
||||
discarded: new fields.BooleanField({ initial: false })
|
||||
})
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
get rollTotal() {
|
||||
return this.rolls.reduce((acc, roll) => acc + (!roll.discarded ? roll.value : 0), 0);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import featuresSchema from './interface/featuresSchema.mjs';
|
||||
|
||||
export default class DhpAncestry extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
description: new fields.HTMLField({}),
|
||||
abilities: featuresSchema()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
export default class DhpArmor extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
equipped: new fields.BooleanField({ initial: false }),
|
||||
baseScore: new fields.NumberField({ integer: true, initial: 0 }),
|
||||
feature: new fields.StringField({
|
||||
choices: SYSTEM.ITEM.armorFeatures,
|
||||
integer: false,
|
||||
blank: true
|
||||
}),
|
||||
marks: new fields.SchemaField({
|
||||
max: new fields.NumberField({ initial: 6, integer: true }),
|
||||
value: new fields.NumberField({ initial: 0, integer: true })
|
||||
}),
|
||||
baseThresholds: new fields.SchemaField({
|
||||
major: new fields.NumberField({ integer: true, initial: 0 }),
|
||||
severe: new fields.NumberField({ integer: true, initial: 0 })
|
||||
}),
|
||||
quantity: new fields.NumberField({ integer: true, initial: 1 }),
|
||||
description: new fields.HTMLField({})
|
||||
};
|
||||
}
|
||||
|
||||
get featureInfo() {
|
||||
return this.feature ? CONFIG.daggerheart.ITEM.armorFeatures[this.feature] : null;
|
||||
}
|
||||
|
||||
prepareDerivedData() {
|
||||
if (this.parent.parent) {
|
||||
this.applyLevels();
|
||||
}
|
||||
}
|
||||
|
||||
// Currently bugged as it double triggers. Should get fixed in an updated foundry version.
|
||||
applyLevels() {
|
||||
// let armorBonus = 0;
|
||||
// for(var level in this.parent.parent.system.levelData.levelups){
|
||||
// var levelData = this.parent.parent.system.levelData.levelups[level];
|
||||
// for(var tier in levelData){
|
||||
// var tierData = levelData[tier];
|
||||
// if(tierData){
|
||||
// armorBonus += Object.keys(tierData.armorOrEvasionSlot).filter(x => tierData.armorOrEvasionSlot[x] === 'armor').length;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// this.marks.max += armorBonus;
|
||||
}
|
||||
}
|
||||
18
module/data/chat-message/_modules.mjs
Normal file
18
module/data/chat-message/_modules.mjs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import DHAbilityUse from "./abilityUse.mjs";
|
||||
import DHAdversaryRoll from "./adversaryRoll.mjs";
|
||||
import DHDamageRoll from "./damageRoll.mjs";
|
||||
import DHDualityRoll from "./dualityRoll.mjs";
|
||||
|
||||
export {
|
||||
DHAbilityUse,
|
||||
DHAdversaryRoll,
|
||||
DHDamageRoll,
|
||||
DHDualityRoll,
|
||||
}
|
||||
|
||||
export const config = {
|
||||
abilityUse: DHAbilityUse,
|
||||
adversaryRoll: DHAdversaryRoll,
|
||||
damageRoll: DHDamageRoll,
|
||||
dualityRoll: DHDualityRoll,
|
||||
};
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
export default class DhpAbilityUse extends foundry.abstract.TypeDataModel {
|
||||
export default class DHAbilityUse extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
|
||||
46
module/data/chat-message/adversaryRoll.mjs
Normal file
46
module/data/chat-message/adversaryRoll.mjs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
export default class DHAdversaryRoll extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
|
||||
return {
|
||||
title: new fields.StringField(),
|
||||
origin: new fields.StringField({ required: true }),
|
||||
dice: new fields.DataField(),
|
||||
roll: new fields.DataField(),
|
||||
modifiers: new fields.ArrayField(
|
||||
new fields.SchemaField({
|
||||
value: new fields.NumberField({ integer: true }),
|
||||
label: new fields.StringField({})
|
||||
})
|
||||
),
|
||||
advantageState: new fields.BooleanField({ nullable: true, initial: null }),
|
||||
advantage: new fields.SchemaField({
|
||||
dice: new fields.StringField({}),
|
||||
value: new fields.NumberField({ integer: true })
|
||||
}),
|
||||
targets: new fields.ArrayField(
|
||||
new fields.SchemaField({
|
||||
id: new fields.StringField({}),
|
||||
name: new fields.StringField({}),
|
||||
img: new fields.StringField({}),
|
||||
difficulty: new fields.NumberField({ integer: true, nullable: true }),
|
||||
evasion: new fields.NumberField({ integer: true }),
|
||||
hit: new fields.BooleanField({ initial: false })
|
||||
})
|
||||
),
|
||||
damage: new fields.SchemaField(
|
||||
{
|
||||
value: new fields.StringField({}),
|
||||
type: new fields.StringField({ choices: Object.keys(SYSTEM.GENERAL.damageTypes), integer: false })
|
||||
},
|
||||
{ nullable: true, initial: null }
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
prepareDerivedData() {
|
||||
this.targets.forEach(target => {
|
||||
target.hit = target.difficulty ? this.total >= target.difficulty : this.total >= target.evasion;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
export default class DhpDamageRoll extends foundry.abstract.TypeDataModel {
|
||||
export default class DHDamageRoll extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
|
||||
|
|
@ -9,7 +9,13 @@ export default class DhpDamageRoll extends foundry.abstract.TypeDataModel {
|
|||
total: new fields.NumberField({ required: true, integer: true }),
|
||||
type: new fields.StringField({ choices: Object.keys(SYSTEM.GENERAL.damageTypes), integer: false })
|
||||
}),
|
||||
dice: new fields.ArrayField(new fields.EmbeddedDataField(DhpDamageDice)),
|
||||
dice: new fields.ArrayField(
|
||||
new fields.SchemaField({
|
||||
type: new fields.StringField({ required: true }),
|
||||
rolls: new fields.ArrayField(new fields.NumberField({ required: true, integer: true })),
|
||||
total: new fields.NumberField({ integer: true })
|
||||
})
|
||||
),
|
||||
modifiers: new fields.ArrayField(
|
||||
new fields.SchemaField({
|
||||
value: new fields.NumberField({ required: true, integer: true }),
|
||||
|
|
@ -26,18 +32,3 @@ export default class DhpDamageRoll extends foundry.abstract.TypeDataModel {
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
class DhpDamageDice extends foundry.abstract.DataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
|
||||
return {
|
||||
type: new fields.StringField({ required: true }),
|
||||
rolls: new fields.ArrayField(new fields.NumberField({ required: true, integer: true }))
|
||||
};
|
||||
}
|
||||
|
||||
get rollTotal() {
|
||||
return this.rolls.reduce((acc, roll) => acc + roll, 0);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { DualityRollColor } from './settings/Appearance.mjs';
|
||||
import { DualityRollColor } from '../settings/Appearance.mjs';
|
||||
|
||||
const fields = foundry.data.fields;
|
||||
const diceField = () =>
|
||||
|
|
@ -7,7 +7,7 @@ const diceField = () =>
|
|||
value: new fields.NumberField({ integer: true })
|
||||
});
|
||||
|
||||
export default class DhpDualityRoll extends foundry.abstract.TypeDataModel {
|
||||
export default class DHDualityRoll extends foundry.abstract.TypeDataModel {
|
||||
static dualityResult = {
|
||||
hope: 1,
|
||||
fear: 2,
|
||||
|
|
@ -18,18 +18,17 @@ export default class DhpDualityRoll extends foundry.abstract.TypeDataModel {
|
|||
return {
|
||||
title: new fields.StringField(),
|
||||
origin: new fields.StringField({ required: true }),
|
||||
roll: new fields.StringField({}),
|
||||
roll: new fields.DataField({}),
|
||||
modifiers: new fields.ArrayField(
|
||||
new fields.SchemaField({
|
||||
value: new fields.NumberField({ integer: true }),
|
||||
label: new fields.StringField({}),
|
||||
title: new fields.StringField({})
|
||||
label: new fields.StringField({})
|
||||
})
|
||||
),
|
||||
hope: diceField(),
|
||||
fear: diceField(),
|
||||
advantageState: new fields.BooleanField({ nullable: true, initial: null }),
|
||||
advantage: diceField(),
|
||||
disadvantage: diceField(),
|
||||
targets: new fields.ArrayField(
|
||||
new fields.SchemaField({
|
||||
id: new fields.StringField({}),
|
||||
|
|
@ -64,15 +63,6 @@ export default class DhpDualityRoll extends foundry.abstract.TypeDataModel {
|
|||
};
|
||||
}
|
||||
|
||||
get total() {
|
||||
const advantage = this.advantage.value
|
||||
? this.advantage.value
|
||||
: this.disadvantage.value
|
||||
? -this.disadvantage.value
|
||||
: 0;
|
||||
return this.diceTotal + advantage + this.modifierTotal.value;
|
||||
}
|
||||
|
||||
get diceTotal() {
|
||||
return this.hope.value + this.fear.value;
|
||||
}
|
||||
|
|
@ -112,13 +102,7 @@ export default class DhpDualityRoll extends foundry.abstract.TypeDataModel {
|
|||
}
|
||||
|
||||
prepareDerivedData() {
|
||||
const total = this.total;
|
||||
|
||||
this.hope.discarded = this.hope.value < this.fear.value;
|
||||
this.fear.discarded = this.fear.value < this.hope.value;
|
||||
|
||||
this.targets.forEach(target => {
|
||||
target.hit = target.difficulty ? total >= target.difficulty : total >= target.evasion;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,112 +0,0 @@
|
|||
import { getTier } from '../helpers/utils.mjs';
|
||||
import DhpFeature from './feature.mjs';
|
||||
|
||||
export default class DhpClass extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
domains: new fields.ArrayField(new fields.StringField({})),
|
||||
classItems: new fields.ArrayField(
|
||||
new fields.SchemaField({
|
||||
name: new fields.StringField({}),
|
||||
img: new fields.StringField({}),
|
||||
uuid: new fields.StringField({})
|
||||
})
|
||||
),
|
||||
evasion: new fields.NumberField({ initial: 0, integer: true }),
|
||||
features: new fields.ArrayField(
|
||||
new fields.SchemaField({
|
||||
name: new fields.StringField({}),
|
||||
img: new fields.StringField({}),
|
||||
uuid: new fields.StringField({})
|
||||
})
|
||||
),
|
||||
subclasses: new fields.ArrayField(
|
||||
new fields.SchemaField({
|
||||
name: new fields.StringField({}),
|
||||
img: new fields.StringField({}),
|
||||
uuid: new fields.StringField({})
|
||||
})
|
||||
),
|
||||
inventory: new fields.SchemaField({
|
||||
take: new fields.ArrayField(
|
||||
new fields.SchemaField({
|
||||
name: new fields.StringField({}),
|
||||
img: new fields.StringField({}),
|
||||
uuid: new fields.StringField({})
|
||||
})
|
||||
),
|
||||
choiceA: new fields.ArrayField(
|
||||
new fields.SchemaField({
|
||||
name: new fields.StringField({}),
|
||||
img: new fields.StringField({}),
|
||||
uuid: new fields.StringField({})
|
||||
})
|
||||
),
|
||||
choiceB: new fields.ArrayField(
|
||||
new fields.SchemaField({
|
||||
name: new fields.StringField({}),
|
||||
img: new fields.StringField({}),
|
||||
uuid: new fields.StringField({})
|
||||
})
|
||||
),
|
||||
extra: new fields.SchemaField(
|
||||
{
|
||||
title: new fields.StringField({}),
|
||||
description: new fields.StringField({})
|
||||
},
|
||||
{ initial: null, nullable: true }
|
||||
)
|
||||
}),
|
||||
characterGuide: new fields.SchemaField({
|
||||
suggestedTraits: new fields.SchemaField({
|
||||
agility: new fields.NumberField({ initial: 0, integer: true }),
|
||||
strength: new fields.NumberField({ initial: 0, integer: true }),
|
||||
finesse: new fields.NumberField({ initial: 0, integer: true }),
|
||||
instinct: new fields.NumberField({ initial: 0, integer: true }),
|
||||
presence: new fields.NumberField({ initial: 0, integer: true }),
|
||||
knowledge: new fields.NumberField({ initial: 0, integer: true })
|
||||
}),
|
||||
suggestedPrimaryWeapon: new fields.SchemaField(
|
||||
{
|
||||
name: new fields.StringField({}),
|
||||
img: new fields.StringField({}),
|
||||
uuid: new fields.StringField({})
|
||||
},
|
||||
{ initial: null, nullable: true }
|
||||
),
|
||||
suggestedSecondaryWeapon: new fields.SchemaField(
|
||||
{
|
||||
name: new fields.StringField({}),
|
||||
img: new fields.StringField({}),
|
||||
uuid: new fields.StringField({})
|
||||
},
|
||||
{ initial: null, nullable: true }
|
||||
),
|
||||
suggestedArmor: new fields.SchemaField(
|
||||
{
|
||||
name: new fields.StringField({}),
|
||||
img: new fields.StringField({}),
|
||||
uuid: new fields.StringField({})
|
||||
},
|
||||
{ initial: null, nullable: true }
|
||||
),
|
||||
characterDescription: new fields.SchemaField({
|
||||
clothes: new fields.StringField({}),
|
||||
eyes: new fields.StringField({}),
|
||||
body: new fields.StringField({}),
|
||||
color: new fields.StringField({}),
|
||||
attitude: new fields.StringField({})
|
||||
}),
|
||||
backgroundQuestions: new fields.ArrayField(new fields.StringField({}), { initial: ['', '', ''] }),
|
||||
connections: new fields.ArrayField(new fields.StringField({}), { initial: ['', '', ''] })
|
||||
}),
|
||||
multiclass: new fields.NumberField({ initial: null, nullable: true, integer: true }),
|
||||
description: new fields.HTMLField({})
|
||||
};
|
||||
}
|
||||
|
||||
get multiclassTier() {
|
||||
return getTier(this.multiclass, true);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import featuresSchema from './interface/featuresSchema.mjs';
|
||||
|
||||
export default class DhpCommunity extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
description: new fields.HTMLField({}),
|
||||
abilities: featuresSchema()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
export default class DhpConsumable extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
description: new fields.HTMLField({}),
|
||||
quantity: new fields.NumberField({ initial: 1, integer: true }),
|
||||
consumeOnUse: new fields.BooleanField({ initial: false })
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import DaggerheartAction from './action.mjs';
|
||||
|
||||
export default class DhpDomainCard extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
domain: new fields.StringField(
|
||||
{ choices: SYSTEM.DOMAIN.domains, integer: false },
|
||||
{ required: true, initial: [] }
|
||||
),
|
||||
level: new fields.NumberField({ initial: 1, integer: true }),
|
||||
recallCost: new fields.NumberField({ initial: 0, integer: true }),
|
||||
type: new fields.StringField(
|
||||
{ choices: SYSTEM.DOMAIN.cardTypes, integer: false },
|
||||
{ required: true, initial: [] }
|
||||
),
|
||||
foundation: new fields.BooleanField({ initial: false }),
|
||||
effect: new fields.HTMLField({}),
|
||||
inVault: new fields.BooleanField({ initial: false }),
|
||||
actions: new fields.ArrayField(new fields.EmbeddedDataField(DaggerheartAction))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
export default class DhpEnvironment extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
resources: new fields.SchemaField({}),
|
||||
tier: new fields.StringField({ choices: Object.keys(SYSTEM.GENERAL.tiers), integer: false }),
|
||||
type: new fields.StringField({
|
||||
choices: Object.keys(SYSTEM.ACTOR.adversaryTypes),
|
||||
integer: false,
|
||||
initial: Object.keys(SYSTEM.ACTOR.adversaryTypes).find(x => x === 'standard')
|
||||
}),
|
||||
description: new fields.StringField({}),
|
||||
toneAndFeel: new fields.StringField({}),
|
||||
difficulty: new fields.NumberField({ initial: 1, integer: true }),
|
||||
potentialAdversaries: new fields.StringField({})
|
||||
};
|
||||
}
|
||||
|
||||
get features() {
|
||||
return this.parent.items.filter(x => x.type === 'feature');
|
||||
}
|
||||
}
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
import { getTier } from '../helpers/utils.mjs';
|
||||
import DaggerheartAction from './action.mjs';
|
||||
import DhpEffect from './interface/effects.mjs';
|
||||
|
||||
export default class DhpFeature extends DhpEffect {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return foundry.utils.mergeObject(
|
||||
{},
|
||||
{
|
||||
type: new fields.StringField({ choices: SYSTEM.ITEM.featureTypes }),
|
||||
actionType: new fields.StringField({
|
||||
choices: SYSTEM.ITEM.actionTypes,
|
||||
initial: SYSTEM.ITEM.actionTypes.passive.id
|
||||
}),
|
||||
featureType: new fields.SchemaField({
|
||||
type: new fields.StringField({
|
||||
choices: SYSTEM.ITEM.valueTypes,
|
||||
initial: Object.keys(SYSTEM.ITEM.valueTypes).find(x => x === 'normal')
|
||||
}),
|
||||
data: new fields.SchemaField({
|
||||
value: new fields.StringField({}),
|
||||
property: new fields.StringField({
|
||||
choices: SYSTEM.ACTOR.featureProperties,
|
||||
initial: Object.keys(SYSTEM.ACTOR.featureProperties).find(x => x === 'spellcastingTrait')
|
||||
}),
|
||||
max: new fields.NumberField({ initial: 1, integer: true }),
|
||||
numbers: new fields.TypedObjectField(
|
||||
new fields.SchemaField({
|
||||
value: new fields.NumberField({ integer: true }),
|
||||
used: new fields.BooleanField({ initial: false })
|
||||
})
|
||||
)
|
||||
})
|
||||
}),
|
||||
refreshData: new fields.SchemaField(
|
||||
{
|
||||
type: new fields.StringField({ choices: SYSTEM.GENERAL.refreshTypes }),
|
||||
uses: new fields.NumberField({ initial: 1, integer: true }),
|
||||
refreshed: new fields.BooleanField({ initial: true })
|
||||
},
|
||||
{ nullable: true, initial: null }
|
||||
),
|
||||
multiclass: new fields.NumberField({ initial: null, nullable: true, integer: true }),
|
||||
disabled: new fields.BooleanField({ initial: false }),
|
||||
description: new fields.HTMLField({}),
|
||||
effects: new fields.TypedObjectField(
|
||||
new fields.SchemaField({
|
||||
type: new fields.StringField({ choices: SYSTEM.EFFECTS.effectTypes }),
|
||||
valueType: new fields.StringField({ choices: SYSTEM.EFFECTS.valueTypes }),
|
||||
parseType: new fields.StringField({ choices: SYSTEM.EFFECTS.parseTypes }),
|
||||
initiallySelected: new fields.BooleanField({ initial: true }),
|
||||
options: new fields.ArrayField(
|
||||
new fields.SchemaField({
|
||||
name: new fields.StringField({}),
|
||||
value: new fields.StringField({})
|
||||
}),
|
||||
{ nullable: true, initial: null }
|
||||
),
|
||||
dataField: new fields.StringField({}),
|
||||
appliesOn: new fields.StringField(
|
||||
{ choices: SYSTEM.EFFECTS.applyLocations },
|
||||
{ nullable: true, initial: null }
|
||||
),
|
||||
applyLocationChoices: new fields.TypedObjectField(new fields.StringField({}), {
|
||||
nullable: true,
|
||||
initial: null
|
||||
}),
|
||||
valueData: new fields.SchemaField({
|
||||
value: new fields.StringField({}),
|
||||
fromValue: new fields.StringField({ initial: null, nullable: true }),
|
||||
type: new fields.StringField({ initial: null, nullable: true }),
|
||||
hopeIncrease: new fields.StringField({ initial: null, nullable: true })
|
||||
})
|
||||
})
|
||||
),
|
||||
actions: new fields.ArrayField(new fields.EmbeddedDataField(DaggerheartAction))
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
get multiclassTier() {
|
||||
return getTier(this.multiclass);
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
if (this.refreshData) {
|
||||
if (this.featureType.type === SYSTEM.ITEM.valueTypes.dice.id) {
|
||||
const update = { 'system.refreshData.refreshed': true };
|
||||
Object.keys(this.featureType.data.numbers).forEach(
|
||||
x => (update[`system.featureType.data.numbers.-=${x}`] = null)
|
||||
);
|
||||
await this.parent.update(update);
|
||||
} else {
|
||||
await this.parent.update({ 'system.refreshData.refreshed': true });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
module/data/fields/_module.mjs
Normal file
3
module/data/fields/_module.mjs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export { default as FormulaField } from './formulaField.mjs';
|
||||
export { default as ForeignDocumentUUIDField } from './foreignDocumentUUIDField.mjs';
|
||||
export { default as PseudoDocumentsField } from './pseudoDocumentsField.mjs';
|
||||
40
module/data/fields/actionField.mjs
Normal file
40
module/data/fields/actionField.mjs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { actionsTypes } from '../action/_module.mjs';
|
||||
|
||||
// Temporary Solution
|
||||
export default class ActionField extends foundry.data.fields.ObjectField {
|
||||
getModel(value) {
|
||||
return actionsTypes[value.type] ?? actionsTypes.attack;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_cleanType(value, options) {
|
||||
if (!(typeof value === 'object')) value = {};
|
||||
|
||||
const cls = this.getModel(value);
|
||||
if (cls) return cls.cleanData(value, options);
|
||||
return value;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
initialize(value, model, options = {}) {
|
||||
const cls = this.getModel(value);
|
||||
if (cls) return new cls(value, { parent: model, ...options });
|
||||
return foundry.utils.deepClone(value);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Migrate this field's candidate source data.
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
41
module/data/fields/foreignDocumentUUIDField.mjs
Normal file
41
module/data/fields/foreignDocumentUUIDField.mjs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
/**
|
||||
* A subclass of {@link foundry.data.fields.DocumentUUIDField} to allow selecting a foreign document reference
|
||||
* that resolves to either the document, the index(for items in compenidums) or the UUID string.
|
||||
*/
|
||||
export default class ForeignDocumentUUIDField extends foundry.data.fields.DocumentUUIDField {
|
||||
/**
|
||||
* @param {foundry.data.types.DocumentUUIDFieldOptions} [options] Options which configure the behavior of the field
|
||||
* @param {foundry.data.types.DataFieldContext} [context] Additional context which describes the field
|
||||
*/
|
||||
constructor(options, context) {
|
||||
super(options, context);
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
static get _defaults() {
|
||||
return foundry.utils.mergeObject(super._defaults, {
|
||||
nullable: true,
|
||||
readonly: false,
|
||||
idOnly: false
|
||||
});
|
||||
}
|
||||
|
||||
/**@override */
|
||||
initialize(value, _model, _options = {}) {
|
||||
if (this.idOnly) return value;
|
||||
return (() => {
|
||||
try {
|
||||
const doc = fromUuidSync(value);
|
||||
return doc;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return value ?? null;
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
/**@override */
|
||||
toObject(value) {
|
||||
return value?.uuid ?? value;
|
||||
}
|
||||
}
|
||||
93
module/data/fields/formulaField.mjs
Normal file
93
module/data/fields/formulaField.mjs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
/**
|
||||
* @typedef {foundry.data.types.StringFieldOptions} StringFieldOptions
|
||||
* @typedef {foundry.data.types.DataFieldContext} DataFieldContext
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef _FormulaFieldOptions
|
||||
* @property {boolean} [deterministic] - Is this formula not allowed to have dice values?
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {StringFieldOptions & _FormulaFieldOptions} FormulaFieldOptions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Special case StringField which represents a formula.
|
||||
*/
|
||||
export default class FormulaField extends foundry.data.fields.StringField {
|
||||
/**
|
||||
* @param {FormulaFieldOptions} [options] - Options which configure the behavior of the field
|
||||
* @param {foundry.data.types.DataFieldContext} [context] - Additional context which describes the field
|
||||
*/
|
||||
constructor(options, context) {
|
||||
super(options, context);
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
static get _defaults() {
|
||||
return foundry.utils.mergeObject(super._defaults, {
|
||||
deterministic: false
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
_validateType(value) {
|
||||
const roll = new Roll(value.replace(/@([a-z.0-9_-]+)/gi, '1'));
|
||||
roll.evaluateSync({ strict: false });
|
||||
if (this.options.deterministic && !roll.isDeterministic)
|
||||
throw new Error(`must not contain dice terms: ${value}`);
|
||||
super._validateType(value);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Active Effect Integration */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_castChangeDelta(delta) {
|
||||
return this._cast(delta).trim();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_applyChangeAdd(value, delta, model, change) {
|
||||
if (!value) return delta;
|
||||
const operator = delta.startsWith('-') ? '-' : '+';
|
||||
delta = delta.replace(/^[+-]/, '').trim();
|
||||
return `${value} ${operator} ${delta}`;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_applyChangeMultiply(value, delta, model, change) {
|
||||
if (!value) return delta;
|
||||
const terms = new Roll(value).terms;
|
||||
if (terms.length > 1) return `(${value}) * ${delta}`;
|
||||
return `${value} * ${delta}`;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_applyChangeUpgrade(value, delta, model, change) {
|
||||
if (!value) return delta;
|
||||
const terms = new Roll(value).terms;
|
||||
if (terms.length === 1 && terms[0].fn === 'max') return value.replace(/\)$/, `, ${delta})`);
|
||||
return `max(${value}, ${delta})`;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_applyChangeDowngrade(value, delta, model, change) {
|
||||
if (!value) return delta;
|
||||
const terms = new Roll(value).terms;
|
||||
if (terms.length === 1 && terms[0].fn === 'min') return value.replace(/\)$/, `, ${delta})`);
|
||||
return `min(${value}, ${delta})`;
|
||||
}
|
||||
}
|
||||
56
module/data/fields/pseudoDocumentsField.mjs
Normal file
56
module/data/fields/pseudoDocumentsField.mjs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import PseudoDocument from '../pseudo-documents/base/pseudoDocument.mjs';
|
||||
|
||||
const { TypedObjectField, TypedSchemaField } = foundry.data.fields;
|
||||
|
||||
/**
|
||||
* @typedef _PseudoDocumentsFieldOptions
|
||||
* @property {Number} [max] - The maximum amount of elements (default: `Infinity`)
|
||||
* @property {String[]} [validTypes] - Allowed pseudo-documents types (default: `[]`)
|
||||
* @property {Function} [validateKey] - callback for validate keys of the object;
|
||||
|
||||
* @typedef {foundry.data.types.DataFieldOptions & _PseudoDocumentsFieldOptions} PseudoDocumentsFieldOptions
|
||||
*/
|
||||
export default class PseudoDocumentsField extends TypedObjectField {
|
||||
/**
|
||||
* @param {PseudoDocument} model - The PseudoDocument of each entry in this collection.
|
||||
* @param {PseudoDocumentsFieldOptions} [options] - Options which configure the behavior of the field
|
||||
* @param {foundry.data.types.DataFieldContext} [context] - Additional context which describes the field
|
||||
*/
|
||||
constructor(model, options = {}, context = {}) {
|
||||
options.validateKey ||= key => foundry.data.validators.isValidId(key);
|
||||
if (!foundry.utils.isSubclass(model, PseudoDocument)) throw new Error('The model must be a PseudoDocument');
|
||||
|
||||
const allTypes = model.TYPES;
|
||||
|
||||
const filteredTypes = options.validTypes
|
||||
? Object.fromEntries(
|
||||
Object.entries(allTypes).filter(([key]) => options.validTypes.includes(key))
|
||||
)
|
||||
: allTypes;
|
||||
|
||||
const field = new TypedSchemaField(filteredTypes);
|
||||
super(field, options, context);
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
static get _defaults() {
|
||||
return Object.assign(super._defaults, {
|
||||
max: Infinity,
|
||||
validTypes: []
|
||||
});
|
||||
}
|
||||
|
||||
/** @override */
|
||||
_validateType(value, options = {}) {
|
||||
if (Object.keys(value).length > this.max) throw new Error(`cannot have more than ${this.max} elements`);
|
||||
return super._validateType(value, options);
|
||||
}
|
||||
|
||||
/** @override */
|
||||
initialize(value, model, options = {}) {
|
||||
if (!value) return;
|
||||
value = super.initialize(value, model, options);
|
||||
const collection = new foundry.utils.Collection(Object.values(value).map(d => [d._id, d]));
|
||||
return collection;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
import DaggerheartAction from '../action.mjs';
|
||||
|
||||
export default class DhpEffects extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
effects: new fields.TypedObjectField(
|
||||
new fields.SchemaField({
|
||||
type: new fields.StringField({ choices: Object.keys(SYSTEM.EFFECTS.effectTypes) }),
|
||||
valueType: new fields.StringField({ choices: Object.keys(SYSTEM.EFFECTS.valueTypes) }),
|
||||
parseType: new fields.StringField({ choices: Object.keys(SYSTEM.EFFECTS.parseTypes) }),
|
||||
initiallySelected: new fields.BooleanField({ initial: true }),
|
||||
options: new fields.ArrayField(
|
||||
new fields.SchemaField({
|
||||
name: new fields.StringField({}),
|
||||
value: new fields.StringField({})
|
||||
}),
|
||||
{ nullable: true, initial: null }
|
||||
),
|
||||
dataField: new fields.StringField({}),
|
||||
appliesOn: new fields.StringField(
|
||||
{ choices: Object.keys(SYSTEM.EFFECTS.applyLocations) },
|
||||
{ nullable: true, initial: null }
|
||||
),
|
||||
applyLocationChoices: new fields.TypedObjectField(new fields.StringField({}), {
|
||||
nullable: true,
|
||||
initial: null
|
||||
}),
|
||||
valueData: new fields.SchemaField({
|
||||
value: new fields.StringField({}),
|
||||
fromValue: new fields.StringField({ initial: null, nullable: true }),
|
||||
type: new fields.StringField({ initial: null, nullable: true }),
|
||||
hopeIncrease: new fields.StringField({ initial: null, nullable: true })
|
||||
})
|
||||
})
|
||||
),
|
||||
actions: new fields.ArrayField(new fields.EmbeddedDataField(DaggerheartAction))
|
||||
// actions: new fields.SchemaField({
|
||||
// damage: new fields.ArrayField(new fields.SchemaField({
|
||||
// type: new fields.StringField({ choices: Object.keys(SYSTEM.GENERAL.extendedDamageTypes), initial: SYSTEM.GENERAL.extendedDamageTypes.physical.id }),
|
||||
// value: new fields.StringField({}),
|
||||
// })),
|
||||
// uses: new fields.SchemaField({
|
||||
// nr: new fields.StringField({}),
|
||||
// refreshType: new fields.StringField({ choices: Object.keys(SYSTEM.GENERAL.refreshTypes), initial: SYSTEM.GENERAL.refreshTypes.session.id }),
|
||||
// refreshed: new fields.BooleanField({ initial: true }),
|
||||
// }),
|
||||
// }),
|
||||
};
|
||||
}
|
||||
|
||||
get effectData() {
|
||||
const effectValues = Object.values(this.effects);
|
||||
const effectCategories = Object.keys(SYSTEM.EFFECTS.effectTypes).reduce((acc, effectType) => {
|
||||
acc[effectType] = effectValues.reduce((acc, effect) => {
|
||||
if (effect.type === effectType) {
|
||||
acc.push({ ...effect, valueData: this.#parseValues(effect.parseType, effect.valueData) });
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return effectCategories;
|
||||
}
|
||||
|
||||
#parseValues(parseType, values) {
|
||||
return Object.keys(values).reduce((acc, prop) => {
|
||||
acc[prop] = this.#parseValue(parseType, values[prop]);
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
#parseValue(parseType, value) {
|
||||
switch (parseType) {
|
||||
case SYSTEM.EFFECTS.parseTypes.number.id:
|
||||
return Number.parseInt(value);
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
const fields = foundry.data.fields;
|
||||
|
||||
const featuresSchema = () =>
|
||||
new fields.ArrayField(
|
||||
new fields.SchemaField({
|
||||
name: new fields.StringField({}),
|
||||
img: new fields.StringField({}),
|
||||
uuid: new fields.StringField({}),
|
||||
subclassLevel: new fields.StringField({})
|
||||
})
|
||||
);
|
||||
|
||||
export default featuresSchema;
|
||||
36
module/data/item/_module.mjs
Normal file
36
module/data/item/_module.mjs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import DHAncestry from './ancestry.mjs';
|
||||
import DHArmor from './armor.mjs';
|
||||
import DHClass from './class.mjs';
|
||||
import DHCommunity from './community.mjs';
|
||||
import DHConsumable from './consumable.mjs';
|
||||
import DHDomainCard from './domainCard.mjs';
|
||||
import DHFeature from './feature.mjs';
|
||||
import DHMiscellaneous from './miscellaneous.mjs';
|
||||
import DHSubclass from './subclass.mjs';
|
||||
import DHWeapon from './weapon.mjs';
|
||||
|
||||
export {
|
||||
DHAncestry,
|
||||
DHArmor,
|
||||
DHClass,
|
||||
DHCommunity,
|
||||
DHConsumable,
|
||||
DHDomainCard,
|
||||
DHFeature,
|
||||
DHMiscellaneous,
|
||||
DHSubclass,
|
||||
DHWeapon
|
||||
};
|
||||
|
||||
export const config = {
|
||||
ancestry: DHAncestry,
|
||||
armor: DHArmor,
|
||||
class: DHClass,
|
||||
community: DHCommunity,
|
||||
consumable: DHConsumable,
|
||||
domainCard: DHDomainCard,
|
||||
feature: DHFeature,
|
||||
miscellaneous: DHMiscellaneous,
|
||||
subclass: DHSubclass,
|
||||
weapon: DHWeapon
|
||||
};
|
||||
21
module/data/item/ancestry.mjs
Normal file
21
module/data/item/ancestry.mjs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import BaseDataItem from './base.mjs';
|
||||
|
||||
export default class DHAncestry extends BaseDataItem {
|
||||
/** @inheritDoc */
|
||||
static get metadata() {
|
||||
return foundry.utils.mergeObject(super.metadata, {
|
||||
label: "TYPES.Item.ancestry",
|
||||
type: "ancestry",
|
||||
hasDescription: true,
|
||||
});
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
...super.defineSchema(),
|
||||
//TODO: add features field
|
||||
};
|
||||
}
|
||||
}
|
||||
36
module/data/item/armor.mjs
Normal file
36
module/data/item/armor.mjs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import BaseDataItem from './base.mjs';
|
||||
|
||||
export default class DHArmor extends BaseDataItem {
|
||||
/** @inheritDoc */
|
||||
static get metadata() {
|
||||
return foundry.utils.mergeObject(super.metadata, {
|
||||
label: "TYPES.Item.armor",
|
||||
type: "armor",
|
||||
hasDescription: true,
|
||||
isQuantifiable: true,
|
||||
});
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
...super.defineSchema(),
|
||||
equipped: new fields.BooleanField({ initial: false }),
|
||||
baseScore: new fields.NumberField({ integer: true, initial: 0 }),
|
||||
feature: new fields.StringField({ choices: SYSTEM.ITEM.armorFeatures, blank: true }),
|
||||
marks: new fields.SchemaField({
|
||||
max: new fields.NumberField({ initial: 6, integer: true }),
|
||||
value: new fields.NumberField({ initial: 0, integer: true })
|
||||
}),
|
||||
baseThresholds: new fields.SchemaField({
|
||||
major: new fields.NumberField({ integer: true, initial: 0 }),
|
||||
severe: new fields.NumberField({ integer: true, initial: 0 })
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
get featureInfo() {
|
||||
return this.feature ? CONFIG.daggerheart.ITEM.armorFeatures[this.feature] : null;
|
||||
}
|
||||
}
|
||||
56
module/data/item/base.mjs
Normal file
56
module/data/item/base.mjs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
/**
|
||||
* Describes metadata about the item data model type
|
||||
* @typedef {Object} ItemDataModelMetadata
|
||||
* @property {string} label - A localizable label used on application.
|
||||
* @property {string} type - The system type that this data model represents.
|
||||
* @property {boolean} hasDescription - Indicates whether items of this type have description field
|
||||
* @property {boolean} isQuantifiable - Indicates whether items of this type have quantity field
|
||||
* @property {Record<string,string>} embedded - Record of document names of pseudo-documents and the path to the collection
|
||||
*/
|
||||
|
||||
const fields = foundry.data.fields;
|
||||
|
||||
export default class BaseDataItem extends foundry.abstract.TypeDataModel {
|
||||
/** @returns {ItemDataModelMetadata}*/
|
||||
static get metadata() {
|
||||
return {
|
||||
label: "Base Item",
|
||||
type: "base",
|
||||
hasDescription: false,
|
||||
isQuantifiable: false,
|
||||
embedded: {},
|
||||
};
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
static defineSchema() {
|
||||
const schema = {};
|
||||
|
||||
if (this.metadata.hasDescription)
|
||||
schema.description = new fields.HTMLField({ required: true, nullable: true });
|
||||
|
||||
if (this.metadata.isQuantifiable)
|
||||
schema.quantity = new fields.NumberField({ integer: true, initial: 1, min: 0, required: true });
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenient access to the item's actor, if it exists.
|
||||
* @returns {foundry.documents.Actor | null}
|
||||
*/
|
||||
get actor() {
|
||||
return this.parent.actor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain a data object used to evaluate any dice rolls associated with this Item Type
|
||||
* @param {object} [options] - Options which modify the getRollData method.
|
||||
* @returns {object}
|
||||
*/
|
||||
getRollData(options = {}) {
|
||||
const actorRollData = this.actor?.getRollData() ?? {};
|
||||
const data = { ...actorRollData, item: { ...this } };
|
||||
return data;
|
||||
}
|
||||
}
|
||||
87
module/data/item/class.mjs
Normal file
87
module/data/item/class.mjs
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import BaseDataItem from './base.mjs';
|
||||
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
|
||||
|
||||
export default class DHClass extends BaseDataItem {
|
||||
/** @inheritDoc */
|
||||
static get metadata() {
|
||||
return foundry.utils.mergeObject(super.metadata, {
|
||||
label: 'TYPES.Item.class',
|
||||
type: 'class',
|
||||
hasDescription: true
|
||||
});
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
...super.defineSchema(),
|
||||
domains: new fields.ArrayField(new fields.StringField(), { max: 2 }),
|
||||
classItems: new fields.ArrayField(new ForeignDocumentUUIDField({ type: 'Item' })),
|
||||
evasion: new fields.NumberField({ initial: 0, integer: true }),
|
||||
features: new fields.ArrayField(new ForeignDocumentUUIDField({ type: 'Item' })),
|
||||
subclasses: new fields.ArrayField(
|
||||
new ForeignDocumentUUIDField({ type: 'Item', required: false, nullable: true, initial: undefined })
|
||||
),
|
||||
inventory: new fields.SchemaField({
|
||||
take: new fields.ArrayField(
|
||||
new ForeignDocumentUUIDField({ type: 'Item', required: false, nullable: true, initial: undefined })
|
||||
),
|
||||
choiceA: new fields.ArrayField(
|
||||
new ForeignDocumentUUIDField({ type: 'Item', required: false, nullable: true, initial: undefined })
|
||||
),
|
||||
choiceB: new fields.ArrayField(
|
||||
new ForeignDocumentUUIDField({ type: 'Item', required: false, nullable: true, initial: undefined })
|
||||
)
|
||||
}),
|
||||
characterGuide: new fields.SchemaField({
|
||||
suggestedTraits: new fields.SchemaField({
|
||||
agility: new fields.NumberField({ initial: 0, integer: true }),
|
||||
strength: new fields.NumberField({ initial: 0, integer: true }),
|
||||
finesse: new fields.NumberField({ initial: 0, integer: true }),
|
||||
instinct: new fields.NumberField({ initial: 0, integer: true }),
|
||||
presence: new fields.NumberField({ initial: 0, integer: true }),
|
||||
knowledge: new fields.NumberField({ initial: 0, integer: true })
|
||||
}),
|
||||
suggestedPrimaryWeapon: new ForeignDocumentUUIDField({ type: 'Item' }),
|
||||
suggestedSecondaryWeapon: new ForeignDocumentUUIDField({ type: 'Item' }),
|
||||
suggestedArmor: new ForeignDocumentUUIDField({ type: 'Item' })
|
||||
}),
|
||||
isMulticlass: new fields.BooleanField({ initial: false })
|
||||
};
|
||||
}
|
||||
|
||||
async _preCreate(data, options, user) {
|
||||
const allowed = await super._preCreate(data, options, user);
|
||||
if (allowed === false) return;
|
||||
|
||||
if (this.actor?.type === 'character') {
|
||||
const path = data.system.isMulticlass ? 'system.multiclass.value' : 'system.class.value';
|
||||
if (foundry.utils.getProperty(this.actor, path)) {
|
||||
ui.notifications.error(game.i18n.localize('DAGGERHEART.Item.Errors.ClassAlreadySelected'));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onCreate(data, options, userId) {
|
||||
super._onCreate(data, options, userId);
|
||||
if (options.parent?.type === 'character') {
|
||||
const path = `system.${data.system.isMulticlass ? 'multiclass.value' : 'class.value'}`;
|
||||
options.parent.update({ [path]: `${options.parent.uuid}.Item.${data._id}` });
|
||||
}
|
||||
}
|
||||
|
||||
_onDelete(options, userId) {
|
||||
super._onDelete(options, userId);
|
||||
|
||||
if (options.parent?.type === 'character') {
|
||||
const path = `system.${this.isMulticlass ? 'multiclass' : 'class'}`;
|
||||
options.parent.update({
|
||||
[`${path}.value`]: null
|
||||
});
|
||||
|
||||
foundry.utils.getProperty(options.parent, `${path}.subclass`)?.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
22
module/data/item/community.mjs
Normal file
22
module/data/item/community.mjs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import BaseDataItem from './base.mjs';
|
||||
|
||||
export default class DHCommunity extends BaseDataItem {
|
||||
/** @inheritDoc */
|
||||
static get metadata() {
|
||||
return foundry.utils.mergeObject(super.metadata, {
|
||||
label: "TYPES.Item.community",
|
||||
type: "community",
|
||||
hasDescription: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/** @inheritDoc */
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
...super.defineSchema(),
|
||||
//TODO: add features field
|
||||
};
|
||||
}
|
||||
}
|
||||
22
module/data/item/consumable.mjs
Normal file
22
module/data/item/consumable.mjs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import BaseDataItem from "./base.mjs";
|
||||
|
||||
export default class DHConsumable extends BaseDataItem {
|
||||
/** @inheritDoc */
|
||||
static get metadata() {
|
||||
return foundry.utils.mergeObject(super.metadata, {
|
||||
label: "TYPES.Item.consumable",
|
||||
type: "consumable",
|
||||
hasDescription: true,
|
||||
isQuantifiable: true,
|
||||
});
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
...super.defineSchema(),
|
||||
consumeOnUse: new fields.BooleanField({ initial: false })
|
||||
};
|
||||
}
|
||||
}
|
||||
50
module/data/item/domainCard.mjs
Normal file
50
module/data/item/domainCard.mjs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import DHAction from '../action/action.mjs';
|
||||
import BaseDataItem from './base.mjs';
|
||||
|
||||
export default class DHDomainCard extends BaseDataItem {
|
||||
/** @inheritDoc */
|
||||
static get metadata() {
|
||||
return foundry.utils.mergeObject(super.metadata, {
|
||||
label: 'TYPES.Item.domainCard',
|
||||
type: 'domainCard',
|
||||
hasDescription: true
|
||||
});
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
...super.defineSchema(),
|
||||
domain: new fields.StringField({ choices: SYSTEM.DOMAIN.domains, required: true, blank: true }),
|
||||
level: new fields.NumberField({ initial: 1, integer: true }),
|
||||
recallCost: new fields.NumberField({ initial: 0, integer: true }),
|
||||
type: new fields.StringField({ choices: SYSTEM.DOMAIN.cardTypes, required: true, blank: true }),
|
||||
foundation: new fields.BooleanField({ initial: false }),
|
||||
inVault: new fields.BooleanField({ initial: false }),
|
||||
actions: new fields.ArrayField(new fields.EmbeddedDataField(DHAction))
|
||||
};
|
||||
}
|
||||
|
||||
async _preCreate(data, options, user) {
|
||||
const allowed = await super._preCreate(data, options, user);
|
||||
if (allowed === false) return;
|
||||
|
||||
if (this.actor?.type === 'character') {
|
||||
if (!this.actor.system.class.value) {
|
||||
ui.notifications.error(game.i18n.localize('DAGGERHEART.Item.Errors.NoClassSelected'));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.actor.system.domains.find(x => x === this.domain)) {
|
||||
ui.notifications.error(game.i18n.localize('DAGGERHEART.Item.Errors.LacksDomain'));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.actor.system.domainCards.total.find(x => x.name === this.parent.name)) {
|
||||
ui.notifications.error(game.i18n.localize('DAGGERHEART.Item.Errors.DuplicateDomainCard'));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
151
module/data/item/feature.mjs
Normal file
151
module/data/item/feature.mjs
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
import { getTier } from '../../helpers/utils.mjs';
|
||||
import DHAction from '../action/action.mjs';
|
||||
import BaseDataItem from './base.mjs';
|
||||
|
||||
export default class DHFeature extends BaseDataItem {
|
||||
/** @inheritDoc */
|
||||
static get metadata() {
|
||||
return foundry.utils.mergeObject(super.metadata, {
|
||||
label: 'TYPES.Item.feature',
|
||||
type: 'feature',
|
||||
hasDescription: true
|
||||
});
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
...super.defineSchema(),
|
||||
|
||||
//A type of feature seems unnecessary
|
||||
type: new fields.StringField({ choices: SYSTEM.ITEM.featureTypes }),
|
||||
|
||||
//TODO: remove actionType field
|
||||
actionType: new fields.StringField({
|
||||
choices: SYSTEM.ITEM.actionTypes,
|
||||
initial: SYSTEM.ITEM.actionTypes.passive.id
|
||||
}),
|
||||
//TODO: remove featureType field
|
||||
featureType: new fields.SchemaField({
|
||||
type: new fields.StringField({
|
||||
choices: SYSTEM.ITEM.valueTypes,
|
||||
initial: Object.keys(SYSTEM.ITEM.valueTypes).find(x => x === 'normal')
|
||||
}),
|
||||
data: new fields.SchemaField({
|
||||
value: new fields.StringField({}),
|
||||
property: new fields.StringField({
|
||||
choices: SYSTEM.ACTOR.featureProperties,
|
||||
initial: Object.keys(SYSTEM.ACTOR.featureProperties).find(x => x === 'spellcastingTrait')
|
||||
}),
|
||||
max: new fields.NumberField({ initial: 1, integer: true }),
|
||||
numbers: new fields.TypedObjectField(
|
||||
new fields.SchemaField({
|
||||
value: new fields.NumberField({ integer: true }),
|
||||
used: new fields.BooleanField({ initial: false })
|
||||
})
|
||||
)
|
||||
})
|
||||
}),
|
||||
refreshData: new fields.SchemaField(
|
||||
{
|
||||
type: new fields.StringField({ choices: SYSTEM.GENERAL.refreshTypes }),
|
||||
uses: new fields.NumberField({ initial: 1, integer: true }),
|
||||
//TODO: remove refreshed field
|
||||
refreshed: new fields.BooleanField({ initial: true })
|
||||
},
|
||||
{ nullable: true, initial: null }
|
||||
),
|
||||
//TODO: remove refreshed field
|
||||
multiclass: new fields.NumberField({ initial: null, nullable: true, integer: true }),
|
||||
disabled: new fields.BooleanField({ initial: false }),
|
||||
|
||||
//TODO: re do it completely or just remove it
|
||||
effects: new fields.TypedObjectField(
|
||||
new fields.SchemaField({
|
||||
type: new fields.StringField({ choices: SYSTEM.EFFECTS.effectTypes }),
|
||||
valueType: new fields.StringField({ choices: SYSTEM.EFFECTS.valueTypes }),
|
||||
parseType: new fields.StringField({ choices: SYSTEM.EFFECTS.parseTypes }),
|
||||
initiallySelected: new fields.BooleanField({ initial: true }),
|
||||
options: new fields.ArrayField(
|
||||
new fields.SchemaField({
|
||||
name: new fields.StringField({}),
|
||||
value: new fields.StringField({})
|
||||
}),
|
||||
{ nullable: true, initial: null }
|
||||
),
|
||||
dataField: new fields.StringField({}),
|
||||
appliesOn: new fields.StringField(
|
||||
{
|
||||
choices: SYSTEM.EFFECTS.applyLocations
|
||||
},
|
||||
{ nullable: true, initial: null }
|
||||
),
|
||||
applyLocationChoices: new fields.TypedObjectField(new fields.StringField({}), {
|
||||
nullable: true,
|
||||
initial: null
|
||||
}),
|
||||
valueData: new fields.SchemaField({
|
||||
value: new fields.StringField({}),
|
||||
fromValue: new fields.StringField({ initial: null, nullable: true }),
|
||||
type: new fields.StringField({ initial: null, nullable: true }),
|
||||
hopeIncrease: new fields.StringField({ initial: null, nullable: true })
|
||||
})
|
||||
})
|
||||
),
|
||||
actions: new fields.ArrayField(new fields.EmbeddedDataField(DHAction))
|
||||
};
|
||||
}
|
||||
|
||||
get multiclassTier() {
|
||||
return getTier(this.multiclass);
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
if (this.refreshData) {
|
||||
if (this.featureType.type === SYSTEM.ITEM.valueTypes.dice.id) {
|
||||
const update = { 'system.refreshData.refreshed': true };
|
||||
Object.keys(this.featureType.data.numbers).forEach(
|
||||
x => (update[`system.featureType.data.numbers.-=${x}`] = null)
|
||||
);
|
||||
await this.parent.update(update);
|
||||
} else {
|
||||
await this.parent.update({ 'system.refreshData.refreshed': true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get effectData() {
|
||||
const effectValues = Object.values(this.effects);
|
||||
const effectCategories = Object.keys(SYSTEM.EFFECTS.effectTypes).reduce((acc, effectType) => {
|
||||
acc[effectType] = effectValues.reduce((acc, effect) => {
|
||||
if (effect.type === effectType) {
|
||||
acc.push({ ...effect, valueData: this.#parseValues(effect.parseType, effect.valueData) });
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return effectCategories;
|
||||
}
|
||||
|
||||
#parseValues(parseType, values) {
|
||||
return Object.keys(values).reduce((acc, prop) => {
|
||||
acc[prop] = this.#parseValue(parseType, values[prop]);
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
#parseValue(parseType, value) {
|
||||
switch (parseType) {
|
||||
case SYSTEM.EFFECTS.parseTypes.number.id:
|
||||
return Number.parseInt(value);
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
21
module/data/item/miscellaneous.mjs
Normal file
21
module/data/item/miscellaneous.mjs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import BaseDataItem from './base.mjs';
|
||||
|
||||
export default class DHMiscellaneous extends BaseDataItem {
|
||||
/** @inheritDoc */
|
||||
static get metadata() {
|
||||
return foundry.utils.mergeObject(super.metadata, {
|
||||
label: "TYPES.Item.miscellaneous",
|
||||
type: "miscellaneous",
|
||||
hasDescription: true,
|
||||
isQuantifiable: true,
|
||||
});
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
...super.defineSchema(),
|
||||
};
|
||||
}
|
||||
}
|
||||
82
module/data/item/subclass.mjs
Normal file
82
module/data/item/subclass.mjs
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
|
||||
import BaseDataItem from './base.mjs';
|
||||
|
||||
export default class DHSubclass extends BaseDataItem {
|
||||
/** @inheritDoc */
|
||||
static get metadata() {
|
||||
return foundry.utils.mergeObject(super.metadata, {
|
||||
label: 'TYPES.Item.subclass',
|
||||
type: 'subclass',
|
||||
hasDescription: true
|
||||
});
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
...super.defineSchema(),
|
||||
spellcastingTrait: new fields.StringField({
|
||||
choices: SYSTEM.ACTOR.abilities,
|
||||
integer: false,
|
||||
nullable: true,
|
||||
initial: null
|
||||
}),
|
||||
foundationFeature: new ForeignDocumentUUIDField({ type: 'Item' }),
|
||||
specializationFeature: new ForeignDocumentUUIDField({ type: 'Item' }),
|
||||
masteryFeature: new ForeignDocumentUUIDField({ type: 'Item' }),
|
||||
featureState: new fields.NumberField({ required: true, initial: 1, min: 1 }),
|
||||
isMulticlass: new fields.BooleanField({ initial: false })
|
||||
};
|
||||
}
|
||||
|
||||
get features() {
|
||||
return {
|
||||
foundation: this.foundationFeature,
|
||||
specialization: this.featureState >= 2 ? this.specializationFeature : null,
|
||||
mastery: this.featureState === 3 ? this.masteryFeature : null
|
||||
};
|
||||
}
|
||||
|
||||
async _preCreate(data, options, user) {
|
||||
const allowed = await super._preCreate(data, options, user);
|
||||
if (allowed === false) return;
|
||||
|
||||
if (this.actor?.type === 'character') {
|
||||
const classData = this.actor.items.find(
|
||||
x => x.type === 'class' && x.system.isMulticlass === data.system.isMulticlass
|
||||
);
|
||||
const subclassData = this.actor.items.find(
|
||||
x => x.type === 'subclass' && x.system.isMulticlass === data.system.isMulticlass
|
||||
);
|
||||
if (!classData) {
|
||||
ui.notifications.error(game.i18n.localize('DAGGERHEART.Item.Errors.MissingClass'));
|
||||
return false;
|
||||
} else if (subclassData) {
|
||||
ui.notifications.error(game.i18n.localize('DAGGERHEART.Item.Errors.SubclassAlreadySelected'));
|
||||
return false;
|
||||
} else if (classData.system.subclasses.every(x => x.uuid !== `Item.${data._id}`)) {
|
||||
ui.notifications.error(game.i18n.localize('DAGGERHEART.Item.Errors.SubclassNotInClass'));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onCreate(data, options, userId) {
|
||||
super._onCreate(data, options, userId);
|
||||
|
||||
if (options.parent?.type === 'character') {
|
||||
const path = `system.${data.system.isMulticlass ? 'multiclass.subclass' : 'class.subclass'}`;
|
||||
options.parent.update({ [path]: `${options.parent.uuid}.Item.${data._id}` });
|
||||
}
|
||||
}
|
||||
|
||||
_onDelete(options, userId) {
|
||||
super._onDelete(options, userId);
|
||||
|
||||
if (options.parent?.type === 'character') {
|
||||
const path = `system.${this.isMulticlass ? 'multiclass.subclass' : 'class.subclass'}`;
|
||||
options.parent.update({ [path]: null });
|
||||
}
|
||||
}
|
||||
}
|
||||
53
module/data/item/weapon.mjs
Normal file
53
module/data/item/weapon.mjs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import BaseDataItem from './base.mjs';
|
||||
import FormulaField from '../fields/formulaField.mjs';
|
||||
import PseudoDocumentsField from '../fields/pseudoDocumentsField.mjs';
|
||||
import BaseFeatureData from '../pseudo-documents/feature/baseFeatureData.mjs';
|
||||
import ActionField from '../fields/actionField.mjs';
|
||||
|
||||
export default class DHWeapon extends BaseDataItem {
|
||||
/** @inheritDoc */
|
||||
static get metadata() {
|
||||
return foundry.utils.mergeObject(super.metadata, {
|
||||
label: 'TYPES.Item.weapon',
|
||||
type: 'weapon',
|
||||
hasDescription: true,
|
||||
isQuantifiable: true,
|
||||
embedded: {
|
||||
feature: 'featureTest'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
...super.defineSchema(),
|
||||
equipped: new fields.BooleanField({ initial: false }),
|
||||
|
||||
//SETTINGS
|
||||
secondary: new fields.BooleanField({ initial: false }),
|
||||
trait: new fields.StringField({ required: true, choices: SYSTEM.ACTOR.abilities, initial: 'agility' }),
|
||||
range: new fields.StringField({ required: true, choices: SYSTEM.GENERAL.range, initial: 'melee' }),
|
||||
burden: new fields.StringField({ required: true, choices: SYSTEM.GENERAL.burden, initial: 'oneHanded' }),
|
||||
//DAMAGE
|
||||
damage: new fields.SchemaField({
|
||||
value: new FormulaField({ initial: 'd6' }),
|
||||
type: new fields.StringField({
|
||||
required: true,
|
||||
choices: SYSTEM.GENERAL.damageTypes,
|
||||
initial: 'physical'
|
||||
})
|
||||
}),
|
||||
feature: new fields.StringField({ choices: SYSTEM.ITEM.weaponFeatures, blank: true }),
|
||||
featureTest: new PseudoDocumentsField(BaseFeatureData, {
|
||||
required: true,
|
||||
nullable: true,
|
||||
max: 1,
|
||||
validTypes: ['weapon']
|
||||
}),
|
||||
// actions: new fields.ArrayField(new fields.EmbeddedDataField(DHAttackAction))
|
||||
actions: new fields.ArrayField(new ActionField())
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import { abilities } from '../config/actorConfig.mjs';
|
||||
import { chunkify } from '../helpers/utils.mjs';
|
||||
import { LevelOptionType } from './levelTier.mjs';
|
||||
|
||||
|
|
@ -97,11 +98,12 @@ export class DhLevelup extends foundry.abstract.DataModel {
|
|||
case 'experience':
|
||||
case 'domainCard':
|
||||
case 'subclass':
|
||||
return checkbox.amount ? checkbox.data.length === checkbox.amount : checkbox.data.length === 1;
|
||||
return checkbox.data.length === (checkbox.amount ?? 1);
|
||||
case 'multiclass':
|
||||
const classSelected = checkbox.data.length === 1;
|
||||
const domainSelected = checkbox.secondaryData;
|
||||
return classSelected && domainSelected;
|
||||
const domainSelected = checkbox.secondaryData.domain;
|
||||
const subclassSelected = checkbox.secondaryData.subclass;
|
||||
return classSelected && domainSelected && subclassSelected;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
|
|
@ -128,8 +130,37 @@ export class DhLevelup extends foundry.abstract.DataModel {
|
|||
.every(this.#levelFinished.bind(this));
|
||||
}
|
||||
|
||||
get unmarkedTraits() {
|
||||
const possibleLevels = Object.values(this.tiers).reduce((acc, tier) => {
|
||||
if (tier.belongingLevels.includes(this.currentLevel)) acc = tier.belongingLevels;
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return Object.keys(this.levels)
|
||||
.filter(key => possibleLevels.some(x => x === Number(key)))
|
||||
.reduce(
|
||||
(acc, levelKey) => {
|
||||
const level = this.levels[levelKey];
|
||||
Object.values(level.choices).forEach(choice =>
|
||||
Object.values(choice).forEach(checkbox => {
|
||||
if (
|
||||
checkbox.type === 'trait' &&
|
||||
checkbox.data.length > 0 &&
|
||||
Number(levelKey) !== this.currentLevel
|
||||
) {
|
||||
checkbox.data.forEach(data => delete acc[data]);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return acc;
|
||||
},
|
||||
{ ...abilities }
|
||||
);
|
||||
}
|
||||
|
||||
get classUpgradeChoices() {
|
||||
let subclass = null;
|
||||
let subclasses = [];
|
||||
let multiclass = null;
|
||||
Object.keys(this.levels).forEach(levelKey => {
|
||||
const level = this.levels[levelKey];
|
||||
|
|
@ -138,21 +169,22 @@ export class DhLevelup extends foundry.abstract.DataModel {
|
|||
if (checkbox.type === 'multiclass') {
|
||||
multiclass = {
|
||||
class: checkbox.data.length > 0 ? checkbox.data[0] : null,
|
||||
domain: checkbox.secondaryData ?? null,
|
||||
domain: checkbox.secondaryData.domain ?? null,
|
||||
subclass: checkbox.secondaryData.subclass ?? null,
|
||||
tier: checkbox.tier,
|
||||
level: levelKey
|
||||
};
|
||||
}
|
||||
if (checkbox.type === 'subclass') {
|
||||
subclass = {
|
||||
subclasses.push({
|
||||
tier: checkbox.tier,
|
||||
level: levelKey
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
return { subclass, multiclass };
|
||||
return { subclasses, multiclass };
|
||||
}
|
||||
|
||||
get tiersForRendering() {
|
||||
|
|
@ -179,11 +211,11 @@ export class DhLevelup extends foundry.abstract.DataModel {
|
|||
}, {})
|
||||
);
|
||||
|
||||
const { multiclass, subclass } = this.classUpgradeChoices;
|
||||
return tierKeys.map(tierKey => {
|
||||
const { multiclass, subclasses } = this.classUpgradeChoices;
|
||||
return tierKeys.map((tierKey, tierIndex) => {
|
||||
const tier = this.tiers[tierKey];
|
||||
const multiclassInTier = multiclass?.tier === Number(tierKey);
|
||||
const subclassInTier = subclass?.tier === Number(tierKey);
|
||||
const subclassInTier = subclasses.some(x => x.tier === Number(tierKey));
|
||||
|
||||
return {
|
||||
name: tier.name,
|
||||
|
|
@ -214,8 +246,15 @@ export class DhLevelup extends foundry.abstract.DataModel {
|
|||
|
||||
return checkbox;
|
||||
});
|
||||
|
||||
let label = game.i18n.localize(option.label);
|
||||
if (optionKey === 'domainCard') {
|
||||
const maxLevel = tier.belongingLevels[tier.belongingLevels.length - 1];
|
||||
label = game.i18n.format(option.label, { maxLevel });
|
||||
}
|
||||
|
||||
return {
|
||||
label: game.i18n.localize(option.label),
|
||||
label: label,
|
||||
checkboxGroups: chunkify(checkboxes, option.minCost, chunkedBoxes => {
|
||||
const anySelected = chunkedBoxes.some(x => x.selected);
|
||||
const anyDisabled = chunkedBoxes.some(x => x.disabled);
|
||||
|
|
@ -287,7 +326,7 @@ export class DhLevelupLevel extends foundry.abstract.DataModel {
|
|||
amount: new fields.NumberField({ integer: true }),
|
||||
value: new fields.StringField(),
|
||||
data: new fields.ArrayField(new fields.StringField()),
|
||||
secondaryData: new fields.StringField(),
|
||||
secondaryData: new fields.TypedObjectField(new fields.StringField()),
|
||||
type: new fields.StringField({ required: true })
|
||||
})
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
export default class DhpMiscellaneous extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
description: new fields.HTMLField({}),
|
||||
quantity: new fields.NumberField({ initial: 1, integer: true })
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,411 +0,0 @@
|
|||
import { getPathValue } from '../helpers/utils.mjs';
|
||||
import { LevelOptionType } from './levelTier.mjs';
|
||||
|
||||
const fields = foundry.data.fields;
|
||||
|
||||
const attributeField = () =>
|
||||
new fields.SchemaField({
|
||||
bonus: new fields.NumberField({ initial: 0, integer: true }),
|
||||
base: new fields.NumberField({ initial: 0, integer: true }),
|
||||
tierMarked: new fields.BooleanField({ required: true, initial: false })
|
||||
});
|
||||
|
||||
const resourceField = max =>
|
||||
new fields.SchemaField({
|
||||
value: new fields.NumberField({ initial: 0, integer: true }),
|
||||
bonus: new fields.NumberField({ initial: 0, integer: true }),
|
||||
min: new fields.NumberField({ initial: 0, integer: true }),
|
||||
baseMax: new fields.NumberField({ initial: max, integer: true })
|
||||
});
|
||||
|
||||
export default class DhpPC extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
return {
|
||||
resources: new fields.SchemaField({
|
||||
hitPoints: resourceField(6),
|
||||
stress: resourceField(6),
|
||||
hope: new fields.SchemaField({
|
||||
value: new fields.NumberField({ initial: -1, integer: true }), // FIXME. Logic is gte and needs -1 in PC/Hope. Change to 0
|
||||
min: new fields.NumberField({ initial: 0, integer: true })
|
||||
})
|
||||
}),
|
||||
bonuses: new fields.SchemaField({
|
||||
damage: new fields.ArrayField(
|
||||
new fields.SchemaField({
|
||||
value: new fields.NumberField({ integer: true, initial: 0 }),
|
||||
type: new fields.StringField({ nullable: true }),
|
||||
initiallySelected: new fields.BooleanField(),
|
||||
hopeIncrease: new fields.StringField({ initial: null, nullable: true }),
|
||||
description: new fields.StringField({})
|
||||
})
|
||||
)
|
||||
}),
|
||||
traits: new fields.SchemaField({
|
||||
agility: attributeField(),
|
||||
strength: attributeField(),
|
||||
finesse: attributeField(),
|
||||
instinct: attributeField(),
|
||||
presence: attributeField(),
|
||||
knowledge: attributeField()
|
||||
}),
|
||||
proficiency: new fields.SchemaField({
|
||||
base: new fields.NumberField({ required: true, initial: 1, integer: true }),
|
||||
bonus: new fields.NumberField({ required: true, initial: 0, integer: true })
|
||||
}),
|
||||
evasion: new fields.SchemaField({
|
||||
bonus: new fields.NumberField({ initial: 0, integer: true })
|
||||
}),
|
||||
experiences: new fields.ArrayField(
|
||||
new fields.SchemaField({
|
||||
id: new fields.StringField({ required: true }),
|
||||
description: new fields.StringField({}),
|
||||
value: new fields.NumberField({ integer: true, nullable: true, initial: null })
|
||||
}),
|
||||
{
|
||||
initial: [
|
||||
{ id: foundry.utils.randomID(), description: '', value: 2 },
|
||||
{ id: foundry.utils.randomID(), description: '', value: 2 }
|
||||
]
|
||||
}
|
||||
),
|
||||
gold: new fields.SchemaField({
|
||||
coins: new fields.NumberField({ initial: 0, integer: true }),
|
||||
handfulls: new fields.NumberField({ initial: 0, integer: true }),
|
||||
bags: new fields.NumberField({ initial: 0, integer: true }),
|
||||
chests: new fields.NumberField({ initial: 0, integer: true })
|
||||
}),
|
||||
pronouns: new fields.StringField({}),
|
||||
domainData: new fields.SchemaField({
|
||||
maxLoadout: new fields.NumberField({ initial: 2, integer: true }),
|
||||
maxCards: new fields.NumberField({ initial: 2, integer: true })
|
||||
}),
|
||||
story: new fields.SchemaField({
|
||||
background: new fields.HTMLField(),
|
||||
appearance: new fields.HTMLField(),
|
||||
connections: new fields.HTMLField(),
|
||||
scars: new fields.ArrayField(
|
||||
new fields.SchemaField({
|
||||
name: new fields.StringField({}),
|
||||
description: new fields.HTMLField()
|
||||
})
|
||||
)
|
||||
}),
|
||||
description: new fields.StringField({}),
|
||||
//Temporary until new FoundryVersion fix --> See Armor.Mjs DataPreparation
|
||||
armorMarks: new fields.SchemaField({
|
||||
max: new fields.NumberField({ initial: 6, integer: true }),
|
||||
value: new fields.NumberField({ initial: 0, integer: true })
|
||||
}),
|
||||
levelData: new fields.EmbeddedDataField(DhPCLevelData)
|
||||
};
|
||||
}
|
||||
|
||||
get tier() {
|
||||
return this.#getTier(this.levelData.currentLevel);
|
||||
}
|
||||
|
||||
get ancestry() {
|
||||
return this.parent.items.find(x => x.type === 'ancestry') ?? null;
|
||||
}
|
||||
|
||||
get class() {
|
||||
return this.parent.items.find(x => x.type === 'class' && !x.system.multiclass) ?? null;
|
||||
}
|
||||
|
||||
get multiclass() {
|
||||
return this.parent.items.find(x => x.type === 'class' && x.system.multiclass) ?? null;
|
||||
}
|
||||
|
||||
get multiclassSubclass() {
|
||||
return this.parent.items.find(x => x.type === 'subclass' && x.system.multiclass) ?? null;
|
||||
}
|
||||
|
||||
get subclass() {
|
||||
return this.parent.items.find(x => x.type === 'subclass' && !x.system.multiclass) ?? null;
|
||||
}
|
||||
|
||||
get subclassFeatures() {
|
||||
const subclass = this.subclass;
|
||||
const multiclass = this.multiclassSubclass;
|
||||
const subclassItems = this.parent.items.filter(x => x.type === 'feature' && x.system.type === 'subclass');
|
||||
return {
|
||||
subclass: !subclass
|
||||
? {}
|
||||
: {
|
||||
foundation: subclassItems.filter(x =>
|
||||
subclass.system.foundationFeature.abilities.some(ability => ability.uuid === x.uuid)
|
||||
),
|
||||
specialization: subclassItems.filter(x =>
|
||||
subclass.system.specializationFeature.abilities.some(ability => ability.uuid === x.uuid)
|
||||
),
|
||||
mastery: subclassItems.filter(x =>
|
||||
subclass.system.masteryFeature.abilities.some(ability => ability.uuid === x.uuid)
|
||||
)
|
||||
},
|
||||
multiclassSubclass: !multiclass
|
||||
? {}
|
||||
: {
|
||||
foundation: subclassItems.filter(x =>
|
||||
multiclass.system.foundationFeature.abilities.some(ability => ability.uuid === x.uuid)
|
||||
),
|
||||
specialization: subclassItems.filter(x =>
|
||||
multiclass.system.specializationFeature.abilities.some(ability => ability.uuid === x.uuid)
|
||||
),
|
||||
mastery: subclassItems.filter(x =>
|
||||
multiclass.system.masteryFeature.abilities.some(ability => ability.uuid === x.uuid)
|
||||
)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
get community() {
|
||||
return this.parent.items.find(x => x.type === 'community') ?? null;
|
||||
}
|
||||
|
||||
get classFeatures() {
|
||||
return this.parent.items.filter(
|
||||
x => x.type === 'feature' && x.system.type === SYSTEM.ITEM.featureTypes.class.id && !x.system.multiclass
|
||||
);
|
||||
}
|
||||
|
||||
get multiclassFeatures() {
|
||||
return this.parent.items.filter(
|
||||
x => x.type === 'feature' && x.system.type === SYSTEM.ITEM.featureTypes.class.id && x.system.multiclass
|
||||
);
|
||||
}
|
||||
|
||||
get domains() {
|
||||
const classDomains = this.class ? this.class.system.domains : [];
|
||||
const multiclassDomains = this.multiclass ? this.multiclass.system.domains : [];
|
||||
return [...classDomains, ...multiclassDomains];
|
||||
}
|
||||
|
||||
get domainCards() {
|
||||
const domainCards = this.parent.items.filter(x => x.type === 'domainCard');
|
||||
const loadout = domainCards.filter(x => !x.system.inVault);
|
||||
const vault = domainCards.filter(x => x.system.inVault);
|
||||
|
||||
return {
|
||||
loadout: loadout,
|
||||
vault: vault,
|
||||
total: [...loadout, ...vault]
|
||||
};
|
||||
}
|
||||
|
||||
get armor() {
|
||||
return this.parent.items.find(x => x.type === 'armor' && x.system.equipped);
|
||||
}
|
||||
|
||||
get equippedWeapons() {
|
||||
const primaryWeapon = this.parent.items.find(
|
||||
x => x.type === 'weapon' && x.system.equipped && !x.system.secondary
|
||||
);
|
||||
const secondaryWeapon = this.parent.items.find(
|
||||
x => x.type === 'weapon' && x.system.equipped && x.system.secondary
|
||||
);
|
||||
return {
|
||||
primary: this.#weaponData(primaryWeapon),
|
||||
secondary: this.#weaponData(secondaryWeapon),
|
||||
burden: this.getBurden(primaryWeapon, secondaryWeapon)
|
||||
};
|
||||
}
|
||||
|
||||
static async unequipBeforeEquip(itemToEquip) {
|
||||
const equippedWeapons = this.equippedWeapons;
|
||||
|
||||
if (itemToEquip.system.secondary) {
|
||||
if (equippedWeapons.primary && equippedWeapons.primary.burden === SYSTEM.GENERAL.burden.twoHanded.value) {
|
||||
await this.parent.items.get(equippedWeapons.primary.id).update({ 'system.equipped': false });
|
||||
}
|
||||
|
||||
if (equippedWeapons.secondary) {
|
||||
await this.parent.items.get(equippedWeapons.secondary.id).update({ 'system.equipped': false });
|
||||
}
|
||||
} else {
|
||||
if (equippedWeapons.secondary && itemToEquip.system.burden === SYSTEM.GENERAL.burden.twoHanded.value) {
|
||||
await this.parent.items.get(equippedWeapons.secondary.id).update({ 'system.equipped': false });
|
||||
}
|
||||
|
||||
if (equippedWeapons.primary) {
|
||||
await this.parent.items.get(equippedWeapons.primary.id).update({ 'system.equipped': false });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get effects() {
|
||||
return this.parent.items.reduce((acc, item) => {
|
||||
const effects = item.system.effectData;
|
||||
if (effects && !item.system.disabled) {
|
||||
for (var key in effects) {
|
||||
const effect = effects[key];
|
||||
for (var effectEntry of effect) {
|
||||
if (!acc[key]) acc[key] = [];
|
||||
acc[key].push({ name: item.name, value: effectEntry });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
get refreshableFeatures() {
|
||||
return this.parent.items.reduce(
|
||||
(acc, x) => {
|
||||
if (x.type === 'feature' && x.system.refreshData?.type === 'feature' && x.system.refreshData?.type) {
|
||||
acc[x.system.refreshData.type].push(x);
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{ shortRest: [], longRest: [] }
|
||||
);
|
||||
}
|
||||
|
||||
//Should not be done in data?
|
||||
#weaponData(weapon) {
|
||||
return weapon
|
||||
? {
|
||||
id: weapon.id,
|
||||
name: weapon.name,
|
||||
trait: game.i18n.localize(CONFIG.daggerheart.ACTOR.abilities[weapon.system.trait].label),
|
||||
range: CONFIG.daggerheart.GENERAL.range[weapon.system.range],
|
||||
damage: {
|
||||
value: weapon.system.damage.value,
|
||||
type: CONFIG.daggerheart.GENERAL.damageTypes[weapon.system.damage.type]
|
||||
},
|
||||
burden: weapon.system.burden,
|
||||
feature: CONFIG.daggerheart.ITEM.weaponFeatures[weapon.system.feature],
|
||||
img: weapon.img,
|
||||
uuid: weapon.uuid
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
prepareBaseData() {
|
||||
this.resources.hitPoints.max = this.resources.hitPoints.baseMax + this.resources.hitPoints.bonus;
|
||||
this.resources.stress.max = this.resources.stress.baseMax + this.resources.stress.bonus;
|
||||
this.evasion.value = (this.class?.system?.evasion ?? 0) + this.evasion.bonus;
|
||||
this.proficiency.value = this.proficiency.base + this.proficiency.bonus;
|
||||
|
||||
for (var attributeKey in this.traits) {
|
||||
const attribute = this.traits[attributeKey];
|
||||
attribute.value = attribute.base + attribute.bonus;
|
||||
}
|
||||
}
|
||||
|
||||
prepareDerivedData() {
|
||||
this.resources.hope.max = 6 - this.story.scars.length;
|
||||
if (this.resources.hope.value >= this.resources.hope.max) {
|
||||
this.resources.hope.value = Math.max(this.resources.hope.max - 1, 0);
|
||||
}
|
||||
|
||||
const armor = this.armor;
|
||||
this.damageThresholds = {
|
||||
major: armor
|
||||
? armor.system.baseThresholds.major + this.levelData.level.current
|
||||
: this.levelData.level.current,
|
||||
severe: armor
|
||||
? armor.system.baseThresholds.severe + this.levelData.level.current
|
||||
: this.levelData.level.current * 2
|
||||
};
|
||||
|
||||
this.applyEffects();
|
||||
}
|
||||
|
||||
applyEffects() {
|
||||
const effects = this.effects;
|
||||
for (var key in effects) {
|
||||
const effectType = effects[key];
|
||||
for (var effect of effectType) {
|
||||
switch (key) {
|
||||
case SYSTEM.EFFECTS.effectTypes.health.id:
|
||||
this.resources.hitPoints.bonus += effect.value.valueData.value;
|
||||
break;
|
||||
case SYSTEM.EFFECTS.effectTypes.stress.id:
|
||||
this.resources.stress.bonus += effect.value.valueData.value;
|
||||
break;
|
||||
case SYSTEM.EFFECTS.effectTypes.damage.id:
|
||||
this.bonuses.damage.push({
|
||||
value: getPathValue(effect.value.valueData.value, this),
|
||||
type: 'physical',
|
||||
description: effect.name,
|
||||
hopeIncrease: effect.value.valueData.hopeIncrease,
|
||||
initiallySelected: effect.value.initiallySelected,
|
||||
appliesOn: effect.value.appliesOn
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getBurden(primary, secondary) {
|
||||
const twoHanded =
|
||||
primary?.system?.burden === 'twoHanded' ||
|
||||
secondary?.system?.burden === 'twoHanded' ||
|
||||
(primary?.system?.burden === 'oneHanded' && secondary?.system?.burden === 'oneHanded');
|
||||
const oneHanded =
|
||||
!twoHanded && (primary?.system?.burden === 'oneHanded' || secondary?.system?.burden === 'oneHanded');
|
||||
|
||||
return twoHanded ? 'twoHanded' : oneHanded ? 'oneHanded' : null;
|
||||
}
|
||||
|
||||
#getTier(level) {
|
||||
if (level >= 8) return 3;
|
||||
else if (level >= 5) return 2;
|
||||
else if (level >= 2) return 1;
|
||||
else return 0;
|
||||
}
|
||||
}
|
||||
|
||||
class DhPCLevelData extends foundry.abstract.DataModel {
|
||||
static defineSchema() {
|
||||
return {
|
||||
level: new fields.SchemaField({
|
||||
current: new fields.NumberField({ required: true, integer: true, initial: 1 }),
|
||||
changed: new fields.NumberField({ required: true, integer: true, initial: 1 })
|
||||
}),
|
||||
levelups: new fields.TypedObjectField(
|
||||
new fields.SchemaField({
|
||||
achievements: new fields.SchemaField(
|
||||
{
|
||||
experiences: new fields.TypedObjectField(
|
||||
new fields.SchemaField({
|
||||
name: new fields.StringField({ required: true }),
|
||||
modifier: new fields.NumberField({ required: true, integer: true })
|
||||
})
|
||||
),
|
||||
domainCards: new fields.ArrayField(
|
||||
new fields.SchemaField({
|
||||
uuid: new fields.StringField({ required: true }),
|
||||
itemUuid: new fields.StringField({ required: true })
|
||||
})
|
||||
),
|
||||
proficiency: new fields.NumberField({ integer: true })
|
||||
},
|
||||
{ nullable: true, initial: null }
|
||||
),
|
||||
selections: new fields.ArrayField(
|
||||
new fields.SchemaField({
|
||||
tier: new fields.NumberField({ required: true, integer: true }),
|
||||
level: new fields.NumberField({ required: true, integer: true }),
|
||||
optionKey: new fields.StringField({ required: true }),
|
||||
type: new fields.StringField({ required: true, choices: LevelOptionType }),
|
||||
checkboxNr: new fields.NumberField({ required: true, integer: true }),
|
||||
value: new fields.NumberField({ integer: true }),
|
||||
minCost: new fields.NumberField({ integer: true }),
|
||||
amount: new fields.NumberField({ integer: true }),
|
||||
data: new fields.ArrayField(new fields.StringField({ required: true })),
|
||||
secondaryData: new fields.StringField(),
|
||||
itemUuid: new fields.StringField({ required: true })
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
get canLevelUp() {
|
||||
return this.level.current < this.level.changed;
|
||||
}
|
||||
}
|
||||
2
module/data/pseudo-documents/_module.mjs
Normal file
2
module/data/pseudo-documents/_module.mjs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { default as base } from './base/pseudoDocument.mjs';
|
||||
export * as feature from './feature/_module.mjs';
|
||||
213
module/data/pseudo-documents/base/base.mjs
Normal file
213
module/data/pseudo-documents/base/base.mjs
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
/**
|
||||
* @typedef {object} PseudoDocumentMetadata
|
||||
* @property {string} name - The document name of this pseudo-document
|
||||
* @property {Record<string, string>} embedded - Record of document names and their collection paths
|
||||
* @property {typeof foundry.applications.api.ApplicationV2} [sheetClass] - The class used to render this pseudo-document
|
||||
* @property {string} defaultArtwork - The default image used for newly created documents
|
||||
*/
|
||||
|
||||
/**
|
||||
* @class Base class for pseudo-documents
|
||||
* @extends {foundry.abstract.DataModel}
|
||||
*/
|
||||
export default class BasePseudoDocument extends foundry.abstract.DataModel {
|
||||
/**
|
||||
* Pseudo-document metadata.
|
||||
* @returns {PseudoDocumentMetadata}
|
||||
*/
|
||||
static get metadata() {
|
||||
return {
|
||||
name: '',
|
||||
embedded: {},
|
||||
defaultArtwork: foundry.documents.Item.DEFAULT_ICON,
|
||||
sheetClass: CONFIG.daggerheart.pseudoDocuments.sheetClass,
|
||||
};
|
||||
}
|
||||
|
||||
/** @override */
|
||||
static LOCALIZATION_PREFIXES = ['DOCUMENT'];
|
||||
|
||||
/** @inheritdoc */
|
||||
static defineSchema() {
|
||||
const { fields } = foundry.data;
|
||||
|
||||
return {
|
||||
_id: new fields.DocumentIdField({ initial: () => foundry.utils.randomID() }),
|
||||
name: new fields.StringField({ required: true, blank: false, textSearch: true }),
|
||||
img: new fields.FilePathField({ categories: ['IMAGE'], initial: this.metadata.defaultArtwork }),
|
||||
description: new fields.HTMLField({ textSearch: true })
|
||||
};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Instance Properties */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The id of this pseudo-document.
|
||||
* @type {string}
|
||||
*/
|
||||
get id() {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The uuid of this document.
|
||||
* @type {string}
|
||||
*/
|
||||
get uuid() {
|
||||
let parent = this.parent;
|
||||
while (!(parent instanceof BasePseudoDocument) && !(parent instanceof foundry.abstract.Document))
|
||||
parent = parent.parent;
|
||||
return [parent.uuid, this.constructor.metadata.name, this.id].join('.');
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The parent document of this pseudo-document.
|
||||
* @type {foundry.abstract.Document}
|
||||
*/
|
||||
get document() {
|
||||
let parent = this;
|
||||
while (!(parent instanceof foundry.abstract.Document)) parent = parent.parent;
|
||||
return parent;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Item to which this PseudoDocument belongs, if applicable.
|
||||
* @type {foundry.documents.Item|null}
|
||||
*/
|
||||
get item() {
|
||||
return this.parent?.parent instanceof Item ? this.parent.parent : null;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Actor to which this PseudoDocument's item belongs, if the item is embedded.
|
||||
* @type {foundry.documents.Actor|null}
|
||||
*/
|
||||
get actor() {
|
||||
return this.item?.parent ?? null;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The property path to this pseudo document relative to its parent document.
|
||||
* @type {string}
|
||||
*/
|
||||
get fieldPath() {
|
||||
const fp = this.schema.fieldPath;
|
||||
let path = fp.slice(0, fp.lastIndexOf('element') - 1);
|
||||
|
||||
if (this.parent instanceof BasePseudoDocument) {
|
||||
path = [this.parent.fieldPath, this.parent.id, path].join('.');
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Embedded Document Methods */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Retrieve an embedded pseudo-document.
|
||||
* @param {string} embeddedName The document name of the embedded pseudo-document.
|
||||
* @param {string} id The id of the embedded pseudo-document.
|
||||
* @param {object} [options] Retrieval options.
|
||||
* @param {boolean} [options.strinct] Throw an error if the embedded pseudo-document does not exist?
|
||||
* @returns {PseudoDocument|null}
|
||||
*/
|
||||
getEmbeddedDocument(embeddedName, id, { strict = false } = {}) {
|
||||
const embeds = this.constructor.metadata.embedded ?? {};
|
||||
if (embeddedName in embeds) {
|
||||
return foundry.utils.getProperty(this, embeds[embeddedName]).get(id, { strict }) ?? null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* CRUD Operations */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Does this pseudo-document exist in the document's source?
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isSource() {
|
||||
const source = foundry.utils.getProperty(this.document._source, this.fieldPath);
|
||||
if (foundry.utils.getType(source) !== 'Object') {
|
||||
throw new Error('Source is not an object!');
|
||||
}
|
||||
return this.id in source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of this pseudo-document.
|
||||
* @param {object} [data] The data used for the creation.
|
||||
* @param {object} operation The context of the update operation.
|
||||
* @param {foundry.abstract.Document} operation.parent The parent of this document.
|
||||
* @returns {Promise<foundry.abstract.Document>} A promise that resolves to the updated document.
|
||||
*/
|
||||
static async create(data = {}, { parent, ...operation } = {}) {
|
||||
if (!parent) {
|
||||
throw new Error('A parent document must be specified for the creation of a pseudo-document!');
|
||||
}
|
||||
const id =
|
||||
operation.keepId && foundry.data.validators.isValidId(data._id) ? data._id : foundry.utils.randomID();
|
||||
|
||||
const fieldPath = parent.system.constructor.metadata.embedded?.[this.metadata.name];
|
||||
if (!fieldPath) {
|
||||
throw new Error(
|
||||
`A ${parent.documentName} of type '${parent.type}' does not support ${this.metadata.name}!`
|
||||
);
|
||||
}
|
||||
|
||||
const update = { [`system.${fieldPath}.${id}`]: { ...data, _id: id } };
|
||||
const updatedParent = await parent.update(update, operation);
|
||||
return foundry.utils.getProperty(updatedParent, `system.${fieldPath}.${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete this pseudo-document.
|
||||
* @param {object} [operation] The context of the operation.
|
||||
* @returns {Promise<foundry.abstract.Document>} A promise that resolves to the updated document.
|
||||
*/
|
||||
async delete(operation = {}) {
|
||||
if (!this.isSource) throw new Error('You cannot delete a non-source pseudo-document!');
|
||||
const update = { [`${this.fieldPath}.-=${this.id}`]: null };
|
||||
return this.document.update(update, operation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicate this pseudo-document.
|
||||
* @returns {Promise<foundry.abstract.Document>} A promise that resolves to the updated document.
|
||||
*/
|
||||
async duplicate() {
|
||||
if (!this.isSource) throw new Error('You cannot duplicate a non-source pseudo-document!');
|
||||
const docData = foundry.utils.mergeObject(this.toObject(), {
|
||||
name: game.i18n.format('DOCUMENT.CopyOf', { name: this.name })
|
||||
});
|
||||
return this.constructor.create(docData, { parent: this.document });
|
||||
}
|
||||
|
||||
/**
|
||||
* Update this pseudo-document.
|
||||
* @param {object} [change] The change to perform.
|
||||
* @param {object} [operation] The context of the operation.
|
||||
* @returns {Promise<foundry.abstract.Document>} A promise that resolves to the updated document.
|
||||
*/
|
||||
async update(change = {}, operation = {}) {
|
||||
if (!this.isSource) throw new Error('You cannot update a non-source pseudo-document!');
|
||||
const path = [this.fieldPath, this.id].join('.');
|
||||
const update = { [path]: change };
|
||||
return this.document.update(update, operation);
|
||||
}
|
||||
}
|
||||
59
module/data/pseudo-documents/base/pseudoDocument.mjs
Normal file
59
module/data/pseudo-documents/base/pseudoDocument.mjs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import BasePseudoDocument from './base.mjs';
|
||||
import SheetManagementMixin from './sheetManagementMixin.mjs';
|
||||
|
||||
/** @extends BasePseudoDocument */
|
||||
export default class PseudoDocument extends SheetManagementMixin(BasePseudoDocument) {
|
||||
static get TYPES() {
|
||||
const { types } = CONFIG.daggerheart.pseudoDocuments[this.metadata.name];
|
||||
const typeEntries = Object.entries(types).map(([key, { documentClass }]) => [key, documentClass]);
|
||||
return (this._TYPES ??= Object.freeze(Object.fromEntries(typeEntries)));
|
||||
}
|
||||
|
||||
static _TYPES;
|
||||
|
||||
/**
|
||||
* The type of this shape.
|
||||
* @type {string}
|
||||
*/
|
||||
static TYPE = '';
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
static getTypesChoices(validTypes) {
|
||||
const { types } = CONFIG.daggerheart.pseudoDocuments[model.metadata.name];
|
||||
const typeEntries = Object.entries(types)
|
||||
.map(([key, { label }]) => [key, label])
|
||||
.filter(([key]) => !validTypes || validTypes.includes(key));
|
||||
|
||||
return Object.entries(typeEntries);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
static defineSchema() {
|
||||
const { fields } = foundry.data;
|
||||
|
||||
return Object.assign(super.defineSchema(), {
|
||||
type: new fields.StringField({
|
||||
required: true,
|
||||
blank: false,
|
||||
initial: this.TYPE,
|
||||
validate: value => value === this.TYPE,
|
||||
validationError: `must be equal to "${this.TYPE}"`
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
static async create(data = {}, { parent, ...operation } = {}) {
|
||||
data = foundry.utils.deepClone(data);
|
||||
if (!data.type) data.type = Object.keys(this.TYPES)[0];
|
||||
if (!(data.type in this.TYPES)) {
|
||||
throw new Error(
|
||||
`The '${data.type}' type is not a valid type for a '${this.metadata.documentName}' pseudo-document!`
|
||||
);
|
||||
}
|
||||
return super.create(data, { parent, ...operation });
|
||||
}
|
||||
}
|
||||
158
module/data/pseudo-documents/base/sheetManagementMixin.mjs
Normal file
158
module/data/pseudo-documents/base/sheetManagementMixin.mjs
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
import BasePseudoDocument from './base.mjs';
|
||||
const { ApplicationV2 } = foundry.applications.api;
|
||||
|
||||
/**
|
||||
* A mixin that adds sheet management capabilities to pseudo-documents
|
||||
* @template {typeof BasePseudoDocument} T
|
||||
* @param {T} Base
|
||||
* @returns {T & typeof PseudoDocumentWithSheets}
|
||||
*/
|
||||
export default function SheetManagementMixin(Base) {
|
||||
class PseudoDocumentWithSheets extends Base {
|
||||
/**
|
||||
* Reference to the sheet of this pseudo-document.
|
||||
* @type {ApplicationV2|null}
|
||||
*/
|
||||
get sheet() {
|
||||
if (this._sheet) return this._sheet;
|
||||
const cls = this.constructor.metadata.sheetClass ?? ApplicationV2;
|
||||
|
||||
if (!foundry.utils.isSubclass(cls, ApplicationV2)) {
|
||||
return void ui.notifications.error(
|
||||
'Daggerheart | Error on PseudoDocument | sheetClass must be ApplicationV2'
|
||||
);
|
||||
}
|
||||
|
||||
const sheet = new cls({ document: this });
|
||||
this._sheet = sheet;
|
||||
return sheet;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Static Properties */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Set of apps what should be re-render.
|
||||
* @type {Set<ApplicationV2>}
|
||||
* @internal
|
||||
*/
|
||||
_apps = new Set();
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Existing sheets of a specific type for a specific document.
|
||||
* @type {ApplicationV2 | null}
|
||||
*/
|
||||
_sheet = null;
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Display Methods */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Render all the Application instances which are connected to this PseudoDocument.
|
||||
* @param {ApplicationRenderOptions} [options] Rendering options.
|
||||
*/
|
||||
render(options) {
|
||||
for (const app of this._apps ?? []) {
|
||||
app.render({ window: { title: app.title }, ...options });
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Register an application to respond to updates to a certain document.
|
||||
* @param {ApplicationV2} app Application to update.
|
||||
* @internal
|
||||
*/
|
||||
_registerApp(app) {
|
||||
this._apps.add(app);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Remove an application from the render registry.
|
||||
* @param {ApplicationV2} app Application to stop watching.
|
||||
*/
|
||||
_unregisterApp(app) {
|
||||
this._apps.delete(app);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Drag and Drop */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Serialize salient information for this PseudoDocument when dragging it.
|
||||
* @returns {object} An object of drag data.
|
||||
*/
|
||||
toDragData() {
|
||||
const dragData = { type: this.documentName, data: this.toObject() };
|
||||
if (this.id) dragData.uuid = this.uuid;
|
||||
return dragData;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Dialog Methods */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Spawn a dialog for creating a new PseudoDocument.
|
||||
* @param {object} [data] Data to pre-populate the document with.
|
||||
* @param {object} context
|
||||
* @param {foundry.documents.Item} context.parent A parent for the document.
|
||||
* @param {string[]|null} [context.types] A list of types to restrict the choices to, or null for no restriction.
|
||||
* @returns {Promise<BasePseudoDocument|null>}
|
||||
*/
|
||||
static async createDialog(data = {}, { parent, types = null, ...options } = {}) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
/**
|
||||
* Present a Dialog form to confirm deletion of this PseudoDocument.
|
||||
* @param {object} [options] - Additional options passed to `DialogV2.confirm`;
|
||||
* @returns {Promise<foundry.abstract.Document>} A Promise which resolves to the deleted PseudoDocument.
|
||||
*/
|
||||
async deleteDialog(options = {}) {
|
||||
const type = game.i18n.localize(this.constructor.metadata.label);
|
||||
const content = options.content ?? `<p>
|
||||
<strong>${game.i18n.localize("AreYouSure")}</strong>
|
||||
${game.i18n.format("SIDEBAR.DeleteWarning", { type })}
|
||||
</p>`;
|
||||
|
||||
return foundry.applications.api.DialogV2.confirm({
|
||||
content,
|
||||
yes: { callback: () => this.delete(operation) },
|
||||
window: {
|
||||
icon: "fa-solid fa-trash",
|
||||
title: `${game.i18n.format("DOCUMENT.Delete", { type })}: ${this.name}`
|
||||
},
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default new name for a Document
|
||||
* @param {object} collection - Collection of Documents
|
||||
* @returns {string}
|
||||
*/
|
||||
static defaultName(collection) {
|
||||
const documentName = this.metadata.name;
|
||||
const takenNames = new Set();
|
||||
for (const document of collection) takenNames.add(document.name);
|
||||
|
||||
const config = CONFIG.daggerheart.pseudoDocuments[documentName];
|
||||
const baseName = game.i18n.localize(config.label);
|
||||
let name = baseName;
|
||||
let index = 1;
|
||||
while (takenNames.has(name)) name = `${baseName} (${++index})`;
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
return PseudoDocumentWithSheets;
|
||||
}
|
||||
2
module/data/pseudo-documents/feature/_module.mjs
Normal file
2
module/data/pseudo-documents/feature/_module.mjs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { default as BaseFeatureData } from './baseFeatureData.mjs';
|
||||
export { default as WeaponFeature } from './weaponFeature.mjs';
|
||||
24
module/data/pseudo-documents/feature/baseFeatureData.mjs
Normal file
24
module/data/pseudo-documents/feature/baseFeatureData.mjs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import PseudoDocument from '../base/pseudoDocument.mjs';
|
||||
|
||||
export default class BaseFeatureData extends PseudoDocument {
|
||||
/**@inheritdoc */
|
||||
static get metadata() {
|
||||
return foundry.utils.mergeObject(
|
||||
super.metadata,
|
||||
{
|
||||
name: 'feature',
|
||||
embedded: {},
|
||||
//sheetClass: null //TODO: define feature-sheet
|
||||
},
|
||||
{ inplace: false }
|
||||
);
|
||||
}
|
||||
|
||||
static defineSchema() {
|
||||
const { fields } = foundry.data;
|
||||
const schema = super.defineSchema();
|
||||
return Object.assign(schema, {
|
||||
subtype: new fields.StringField({ initial: 'test' })
|
||||
});
|
||||
}
|
||||
}
|
||||
6
module/data/pseudo-documents/feature/weaponFeature.mjs
Normal file
6
module/data/pseudo-documents/feature/weaponFeature.mjs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import BaseFeatureData from './baseFeatureData.mjs';
|
||||
|
||||
export default class WeaponFeature extends BaseFeatureData {
|
||||
/**@override */
|
||||
static TYPE = 'weapon';
|
||||
}
|
||||
|
|
@ -1,11 +1,14 @@
|
|||
export default class DhVariantRules extends foundry.abstract.DataModel {
|
||||
static LOCALIZATION_PREFIXES = ['DAGGERHEART.Settings.VariantRules'];
|
||||
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
actionTokens: new fields.SchemaField({
|
||||
enabled: new fields.BooleanField({ required: true, initial: false }),
|
||||
tokens: new fields.NumberField({ required: true, integer: true, initial: 3 })
|
||||
})
|
||||
}),
|
||||
useCoins: new fields.BooleanField({ initial: false })
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,57 +0,0 @@
|
|||
import { getTier } from '../helpers/utils.mjs';
|
||||
import featuresSchema from './interface/featuresSchema.mjs';
|
||||
import DaggerheartFeature from './feature.mjs';
|
||||
|
||||
export default class DhpSubclass extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
description: new fields.HTMLField({}),
|
||||
spellcastingTrait: new fields.StringField({
|
||||
choices: SYSTEM.ACTOR.abilities,
|
||||
integer: false,
|
||||
nullable: true,
|
||||
initial: null
|
||||
}),
|
||||
foundationFeature: new fields.SchemaField({
|
||||
description: new fields.HTMLField({}),
|
||||
abilities: new fields.ArrayField(
|
||||
new fields.SchemaField({
|
||||
name: new fields.StringField({}),
|
||||
img: new fields.StringField({}),
|
||||
uuid: new fields.StringField({})
|
||||
})
|
||||
)
|
||||
}),
|
||||
specializationFeature: new fields.SchemaField({
|
||||
unlocked: new fields.BooleanField({ initial: false }),
|
||||
tier: new fields.NumberField({ initial: null, nullable: true, integer: true }),
|
||||
description: new fields.HTMLField({}),
|
||||
abilities: new fields.ArrayField(
|
||||
new fields.SchemaField({
|
||||
name: new fields.StringField({}),
|
||||
img: new fields.StringField({}),
|
||||
uuid: new fields.StringField({})
|
||||
})
|
||||
)
|
||||
}),
|
||||
masteryFeature: new fields.SchemaField({
|
||||
unlocked: new fields.BooleanField({ initial: false }),
|
||||
tier: new fields.NumberField({ initial: null, nullable: true, integer: true }),
|
||||
description: new fields.HTMLField({}),
|
||||
abilities: new fields.ArrayField(
|
||||
new fields.SchemaField({
|
||||
name: new fields.StringField({}),
|
||||
img: new fields.StringField({}),
|
||||
uuid: new fields.StringField({})
|
||||
})
|
||||
)
|
||||
}),
|
||||
multiclass: new fields.NumberField({ initial: null, nullable: true, integer: true })
|
||||
};
|
||||
}
|
||||
|
||||
get multiclassTier() {
|
||||
return getTier(this.multiclass);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
export default class DhpWeapon extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
equipped: new fields.BooleanField({ initial: false }),
|
||||
inventoryWeapon: new fields.NumberField({ initial: null, nullable: true, integer: true }),
|
||||
secondary: new fields.BooleanField({ initial: false }),
|
||||
trait: new fields.StringField({ choices: SYSTEM.ACTOR.abilities, integer: false, initial: 'agility' }),
|
||||
range: new fields.StringField({ choices: SYSTEM.GENERAL.range, integer: false, initial: 'melee' }),
|
||||
damage: new fields.SchemaField({
|
||||
value: new fields.StringField({ initial: 'd6' }),
|
||||
type: new fields.StringField({
|
||||
choices: SYSTEM.GENERAL.damageTypes,
|
||||
integer: false,
|
||||
initial: 'physical'
|
||||
})
|
||||
}),
|
||||
burden: new fields.StringField({ choices: SYSTEM.GENERAL.burden, integer: false, initial: 'oneHanded' }),
|
||||
feature: new fields.StringField({ choices: SYSTEM.ITEM.weaponFeatures, integer: false, blank: true }),
|
||||
quantity: new fields.NumberField({ initial: 1, integer: true }),
|
||||
description: new fields.HTMLField({})
|
||||
};
|
||||
}
|
||||
|
||||
prepareDerivedData() {
|
||||
if (this.parent.parent) {
|
||||
this.applyEffects();
|
||||
}
|
||||
}
|
||||
|
||||
applyEffects() {
|
||||
const effects = this.parent.parent.system.effects;
|
||||
for (var key in effects) {
|
||||
const effectType = effects[key];
|
||||
for (var effect of effectType) {
|
||||
switch (key) {
|
||||
case SYSTEM.EFFECTS.effectTypes.reach.id:
|
||||
if (
|
||||
SYSTEM.GENERAL.range[this.range].distance <
|
||||
SYSTEM.GENERAL.range[effect.valueData.value].distance
|
||||
) {
|
||||
this.range = effect.valueData.value;
|
||||
}
|
||||
|
||||
break;
|
||||
// case SYSTEM.EFFECTS.effectTypes.damage.id:
|
||||
|
||||
// if(this.damage.type === 'physical') this.damage.value = (`${this.damage.value} + ${this.parent.parent.system.levelData.currentLevel}`);
|
||||
// break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue