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:
WBHarry 2025-06-13 14:17:13 +02:00 committed by GitHub
parent 90bc2dc488
commit 187ee3e1bd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
153 changed files with 5481 additions and 4829 deletions

0
module/_types.d.ts vendored
View file

View file

@ -1,4 +1,4 @@
export { default as DhpPCSheet } from './sheets/pc.mjs';
export { default as DhCharacterSheet } from './sheets/character.mjs';
export { default as DhpAdversarySheet } from './sheets/adversary.mjs';
export { default as DhpClassSheet } from './sheets/items/class.mjs';
export { default as DhpSubclass } from './sheets/items/subclass.mjs';
@ -13,3 +13,5 @@ export { default as DhpArmor } from './sheets/items/armor.mjs';
export { default as DhpChatMessage } from './chatMessage.mjs';
export { default as DhpEnvironment } from './sheets/environment.mjs';
export { default as DhActiveEffectConfig } from './sheets/activeEffectConfig.mjs';
export * as pseudoDocumentSheet from './sheets/pseudo-documents/_module.mjs';

View file

@ -1,9 +1,9 @@
import DhpDualityRoll from '../data/dualityRoll.mjs';
import { DualityRollColor } from '../data/settings/Appearance.mjs';
import DHDualityRoll from '../data/chat-message/dualityRoll.mjs';
export default class DhpChatMessage extends ChatMessage {
export default class DhpChatMessage extends foundry.documents.ChatMessage {
async renderHTML() {
if (this.type === 'dualityRoll' || this.type === 'adversaryRoll' || this.type === 'abilityUse') {
if (this.type === 'dualityRoll' || this.type === 'adversaryRoll') {
this.content = await foundry.applications.handlebars.renderTemplate(this.content, this.system);
}
@ -17,8 +17,8 @@ export default class DhpChatMessage extends ChatMessage {
) {
html.classList.add('duality');
const dualityResult = this.system.dualityResult;
if (dualityResult === DhpDualityRoll.dualityResult.hope) html.classList.add('hope');
else if (dualityResult === DhpDualityRoll.dualityResult.fear) html.classList.add('fear');
if (dualityResult === DHDualityRoll.dualityResult.hope) html.classList.add('hope');
else if (dualityResult === DHDualityRoll.dualityResult.fear) html.classList.add('fear');
else html.classList.add('critical');
}

View file

@ -1,7 +1,7 @@
import DaggerheartSheet from '../sheets/daggerheart-sheet.mjs';
const { ApplicationV2 } = foundry.applications.api;
export default class DaggerheartActionConfig extends DaggerheartSheet(ApplicationV2) {
export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
constructor(action) {
super({});
@ -9,21 +9,25 @@ export default class DaggerheartActionConfig extends DaggerheartSheet(Applicatio
this.openSection = null;
}
// get title(){
// return `Action - ${this.action.name}`;
// }
static DEFAULT_OPTIONS = {
tag: 'form',
id: 'daggerheart-action',
classes: ['daggerheart', 'views', 'action'],
position: { width: 600, height: 'auto' },
actions: {
toggleSection: this.toggleSection
toggleSection: this.toggleSection,
addEffect: this.addEffect,
removeEffect: this.removeEffect,
addElement: this.addElement,
removeElement: this.removeElement,
editEffect: this.editEffect,
addDamage: this.addDamage,
removeDamage: this.removeDamage
},
form: {
handler: this.updateForm,
closeOnSubmit: true
submitOnChange: true,
closeOnSubmit: false
}
};
@ -36,16 +40,9 @@ export default class DaggerheartActionConfig extends DaggerheartSheet(Applicatio
_getTabs() {
const tabs = {
effects: { active: true, cssClass: '', group: 'primary', id: 'effects', icon: null, label: 'Effects' },
useage: { active: false, cssClass: '', group: 'primary', id: 'useage', icon: null, label: 'Useage' },
conditions: {
active: false,
cssClass: '',
group: 'primary',
id: 'conditions',
icon: null,
label: 'Conditions'
}
base: { active: true, cssClass: '', group: 'primary', id: 'base', icon: null, label: 'Base' },
config: { active: false, cssClass: '', group: 'primary', id: 'config', icon: null, label: 'Configuration' },
effect: { active: false, cssClass: '', group: 'primary', id: 'effect', icon: null, label: 'Effect' }
};
for (const v of Object.values(tabs)) {
@ -58,9 +55,13 @@ export default class DaggerheartActionConfig extends DaggerheartSheet(Applicatio
async _prepareContext(_options) {
const context = await super._prepareContext(_options, 'action');
context.source = this.action.toObject(false);
context.openSection = this.openSection;
context.tabs = this._getTabs();
context.config = SYSTEM;
if (!!this.action.effects) context.effects = this.action.effects.map(e => this.action.item.effects.get(e._id));
if (this.action.damage?.hasOwnProperty('includeBase')) context.hasBaseDamage = !!this.action.parent.damage;
context.getRealIndex = this.getRealIndex.bind(this);
return context;
}
@ -69,15 +70,91 @@ export default class DaggerheartActionConfig extends DaggerheartSheet(Applicatio
this.render(true);
}
static async updateForm(event, _, formData) {
const data = foundry.utils.expandObject(
foundry.utils.mergeObject(this.action.toObject(), foundry.utils.expandObject(formData.object))
);
const newActions = this.action.parent.actions.map(x => x.toObject());
if (!newActions.findSplice(x => x.id === data.id, data)) {
newActions.push(data);
}
await this.action.parent.parent.update({ 'system.actions': newActions });
getRealIndex(index) {
const data = this.action.toObject(false);
return data.damage.parts.find(d => d.base) ? index - 1 : index;
}
_prepareSubmitData(event, formData) {
const submitData = foundry.utils.expandObject(formData.object);
// this.element.querySelectorAll("fieldset[disabled] :is(input, select)").forEach(input => {
// foundry.utils.setProperty(submitData, input.name, input.value);
// });
return submitData;
}
static async updateForm(event, _, formData) {
const submitData = this._prepareSubmitData(event, formData),
data = foundry.utils.expandObject(foundry.utils.mergeObject(this.action.toObject(), submitData)),
newActions = this.action.parent.actions.map(x => x.toObject()); // Find better way
if (!newActions.findSplice(x => x._id === data._id, data)) newActions.push(data);
const updates = await this.action.parent.parent.update({ 'system.actions': newActions });
if (!updates) return;
this.action = updates.system.actions[this.action.index];
this.render();
}
static addElement(event) {
const data = this.action.toObject(),
key = event.target.closest('.action-category-data').dataset.key;
if (!this.action[key]) return;
data[key].push({});
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
static removeElement(event) {
const data = this.action.toObject(),
key = event.target.closest('.action-category-data').dataset.key,
index = event.target.dataset.index;
data[key].splice(index, 1);
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
static addDamage(event) {
if (!this.action.damage.parts) return;
const data = this.action.toObject();
data.damage.parts.push({});
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
static removeDamage(event) {
if (!this.action.damage.parts) return;
const data = this.action.toObject(),
index = event.target.dataset.index;
data.damage.parts.splice(index, 1);
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
static async addEffect(event) {
if (!this.action.effects) return;
const effectData = this._addEffectData.bind(this)(),
[created] = await this.action.item.createEmbeddedDocuments('ActiveEffect', [effectData], { render: false }),
data = this.action.toObject();
data.effects.push({ _id: created._id });
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
/**
* The data for a newly created applied effect.
* @returns {object}
* @protected
*/
_addEffectData() {
return {
name: this.action.item.name,
img: this.action.item.img,
origin: this.action.item.uuid,
transfer: false
};
}
static removeEffect(event) {
if (!this.action.effects) return;
const index = event.target.dataset.index,
effectId = this.action.effects[index]._id;
this.constructor.removeElement.bind(this)(event);
this.action.item.deleteEmbeddedDocuments('ActiveEffect', [effectId]);
}
static editEffect(event) {}
}

View file

@ -1,4 +1,4 @@
import { abilities } from '../config/actorConfig.mjs';
import { abilities, subclassFeatureLabels } from '../config/actorConfig.mjs';
import { domains } from '../config/domainConfig.mjs';
import { DhLevelup } from '../data/levelup.mjs';
import { getDeleteKeys, tagifyElement } from '../helpers/utils.mjs';
@ -35,6 +35,7 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
viewCompendium: this.viewCompendium,
selectPreview: this.selectPreview,
selectDomain: this.selectDomain,
selectSubclass: this.selectSubclass,
updateCurrentLevel: this.updateCurrentLevel,
activatePart: this.activatePart
},
@ -149,7 +150,10 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
const experienceIncreaseValues = experienceIncreases
.filter(exp => exp.data.length > 0)
.flatMap(exp =>
exp.data.map(data => this.actor.system.experiences.find(x => x.id === data).description)
exp.data.map(data => {
const experience = Object.keys(this.actor.system.experiences).find(x => x === data);
return this.actor.system.experiences[experience].description;
})
);
context.experienceIncreases = {
values: experienceIncreaseValues,
@ -175,6 +179,20 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
};
const allDomainCardKeys = Object.keys(allDomainCards);
const classDomainsData = this.actor.system.class.value.system.domains.map(domain => ({
domain,
multiclass: false
}));
const multiclassDomainsData = (this.actor.system.multiclass?.value?.system?.domains ?? []).map(
domain => ({ domain, multiclass: true })
);
const domainsData = [...classDomainsData, ...multiclassDomainsData];
const multiclassDomain = this.levelup.classUpgradeChoices?.multiclass?.domain;
if (multiclassDomain) {
if (!domainsData.some(x => x.domain === multiclassDomain))
domainsData.push({ domain: multiclassDomain, multiclass: true });
}
context.domainCards = [];
for (var key of allDomainCardKeys) {
const domainCard = allDomainCards[key];
@ -185,35 +203,56 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
context.domainCards.push({
...(card.toObject?.() ?? card),
emptySubtext: game.i18n.format(
'DAGGERHEART.Application.LevelUp.Selections.emptyDomainCardHint',
{ level: domainCard.level }
),
emptySubtexts: domainsData.map(domain => {
const levelBase = domain.multiclass
? Math.ceil(this.levelup.currentLevel / 2)
: this.levelup.currentLevel;
const levelMax = domainCard.secondaryData?.limit
? Math.min(domainCard.secondaryData.limit, levelBase)
: levelBase;
return game.i18n.format('DAGGERHEART.Application.LevelUp.Selections.emptyDomainCardHint', {
domain: game.i18n.localize(domains[domain.domain].label),
level: levelMax
});
}),
path: domainCard.data
? `${domainCard.path}.data`
: `levels.${domainCard.level}.achievements.domainCards.${key}.uuid`,
limit: domainCard.level,
limit: domainCard.secondaryData?.limit ?? null,
compendium: 'domains'
});
}
const subclassSelections = advancementChoices.subclass?.flatMap(x => x.data) ?? [];
const possibleSubclasses = [this.actor.system.class.subclass];
if (this.actor.system.multiclass?.subclass) {
possibleSubclasses.push(this.actor.system.multiclass.subclass);
}
const multiclassSubclass = this.actor.system.multiclass?.system?.subclasses?.[0];
const possibleSubclasses = [
this.actor.system.subclass,
...(multiclassSubclass ? [multiclassSubclass] : [])
];
const selectedSubclasses = possibleSubclasses.filter(x => subclassSelections.includes(x.uuid));
context.subclassCards = [];
if (advancementChoices.subclass?.length > 0) {
const featureStateIncrease = Object.values(this.levelup.levels).reduce((acc, level) => {
acc += Object.values(level.choices).filter(choice => {
return Object.values(choice).every(checkbox => checkbox.type === 'subclass');
}).length;
return acc;
}, 0);
for (var subclass of possibleSubclasses) {
const choice =
advancementChoices.subclass.find(x => x.data[0] === subclass.uuid) ??
advancementChoices.subclass.find(x => x.data.length === 0);
const featureState = subclass.system.featureState + featureStateIncrease;
const data = await foundry.utils.fromUuid(subclass.uuid);
const selected = selectedSubclasses.some(x => x.uuid === data.uuid);
context.subclassCards.push({
...data.toObject(),
path: choice?.path,
uuid: data.uuid,
selected: selected
selected: subclassSelections.includes(subclass.uuid),
featureState: featureState,
featureLabel: game.i18n.localize(subclassFeatureLabels[featureState]),
isMulticlass: subclass.system.isMulticlass ? 'true' : 'false'
});
}
}
@ -230,14 +269,23 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
domains:
multiclass?.system?.domains.map(key => {
const domain = domains[key];
const alreadySelected = this.actor.system.class.system.domains.includes(key);
const alreadySelected = this.actor.system.class.value.system.domains.includes(key);
return {
...domain,
selected: key === data.secondaryData,
disabled: (data.secondaryData && key !== data.secondaryData) || alreadySelected
selected: key === data.secondaryData.domain,
disabled:
(data.secondaryData.domain && key !== data.secondaryData.domain) ||
alreadySelected
};
}) ?? [],
subclasses:
multiclass?.system?.subclasses.map(subclass => ({
...subclass,
uuid: subclass.uuid,
selected: data.secondaryData.subclass === subclass.uuid,
disabled: data.secondaryData.subclass && data.secondaryData.subclass !== subclass.uuid
})) ?? [],
compendium: 'classes',
limit: 1
};
@ -274,8 +322,8 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
context.achievements = {
proficiency: {
old: this.actor.system.proficiency.value,
new: this.actor.system.proficiency.value + achivementProficiency,
old: this.actor.system.proficiency.total,
new: this.actor.system.proficiency.total + achivementProficiency,
shown: achivementProficiency > 0
},
damageThresholds: {
@ -319,6 +367,13 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
? advancement[choiceKey] + Number(checkbox.value)
: Number(checkbox.value);
break;
case 'trait':
if (!advancement[choiceKey]) advancement[choiceKey] = {};
for (var traitKey of checkbox.data) {
if (!advancement[choiceKey][traitKey]) advancement[choiceKey][traitKey] = 0;
advancement[choiceKey][traitKey] += 1;
}
break;
case 'domainCard':
if (!advancement[choiceKey]) advancement[choiceKey] = [];
if (checkbox.data.length === 1) {
@ -328,12 +383,41 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
break;
case 'experience':
if (!advancement[choiceKey]) advancement[choiceKey] = [];
const data = checkbox.data.map(
data =>
this.actor.system.experiences.find(x => x.id === data)?.description ?? ''
);
const data = checkbox.data.map(data => {
const experience = Object.keys(this.actor.system.experiences).find(
x => x === data
);
return this.actor.system.experiences[experience]?.description ?? '';
});
advancement[choiceKey].push({ data: data, value: checkbox.value });
break;
case 'subclass':
if (checkbox.data[0]) {
const subclassItem = await foundry.utils.fromUuid(checkbox.data[0]);
if (!advancement[choiceKey]) advancement[choiceKey] = [];
advancement[choiceKey].push({
...subclassItem.toObject(),
featureLabel: game.i18n.localize(
subclassFeatureLabels[Number(checkbox.secondaryData.featureState)]
)
});
}
break;
case 'multiclass':
const multiclassItem = await foundry.utils.fromUuid(checkbox.data[0]);
const subclass = multiclassItem
? await foundry.utils.fromUuid(checkbox.secondaryData.subclass)
: null;
advancement[choiceKey] = multiclassItem
? {
...multiclassItem.toObject(),
domain: checkbox.secondaryData.domain
? game.i18n.localize(domains[checkbox.secondaryData.domain].label)
: null,
subclass: subclass ? subclass.name : null
}
: {};
break;
}
}
}
@ -346,26 +430,35 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
new: context.achievements.proficiency.new + (advancement.proficiency ?? 0)
},
hitPoints: {
old: this.actor.system.resources.hitPoints.max,
new: this.actor.system.resources.hitPoints.max + (advancement.hitPoint ?? 0)
old: this.actor.system.resources.hitPoints.maxTotal,
new: this.actor.system.resources.hitPoints.maxTotal + (advancement.hitPoint ?? 0)
},
stress: {
old: this.actor.system.resources.stress.max,
new: this.actor.system.resources.stress.max + (advancement.stress ?? 0)
old: this.actor.system.resources.stress.maxTotal,
new: this.actor.system.resources.stress.maxTotal + (advancement.stress ?? 0)
},
evasion: {
old: this.actor.system.evasion.value,
new: this.actor.system.evasion.value + (advancement.evasion ?? 0)
old: this.actor.system.evasion.total,
new: this.actor.system.evasion.total + (advancement.evasion ?? 0)
}
},
traits:
advancement.trait?.flatMap(x =>
x.data.map(data => game.i18n.localize(abilities[data].label))
) ?? [],
traits: Object.keys(this.actor.system.traits).reduce((acc, traitKey) => {
if (advancement.trait?.[traitKey]) {
if (!acc) acc = {};
acc[traitKey] = {
label: game.i18n.localize(abilities[traitKey].label),
old: this.actor.system.traits[traitKey].total,
new: this.actor.system.traits[traitKey].total + advancement.trait[traitKey]
};
}
return acc;
}, null),
domainCards: advancement.domainCard ?? [],
experiences:
advancement.experience?.flatMap(x => x.data.map(data => ({ name: data, modifier: x.value }))) ??
[]
[],
multiclass: advancement.multiclass,
subclass: advancement.subclass
};
context.advancements.statistics.proficiency.shown =
@ -414,15 +507,16 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
const traitsTagify = htmlElement.querySelector('.levelup-trait-increases');
if (traitsTagify) {
tagifyElement(traitsTagify, abilities, this.tagifyUpdate('trait').bind(this));
tagifyElement(traitsTagify, this.levelup.unmarkedTraits, this.tagifyUpdate('trait').bind(this));
}
const experienceIncreaseTagify = htmlElement.querySelector('.levelup-experience-increases');
if (experienceIncreaseTagify) {
tagifyElement(
experienceIncreaseTagify,
this.actor.system.experiences.reduce((acc, experience) => {
acc[experience.id] = { label: experience.description };
Object.keys(this.actor.system.experiences).reduce((acc, id) => {
const experience = this.actor.system.experiences[id];
acc[id] = { label: experience.description };
return acc;
}, {}),
@ -479,8 +573,10 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
if (event.target.closest('.domain-cards')) {
const target = event.target.closest('.card-preview-container');
if (item.type === 'domainCard') {
const { multiclass } = this.levelup.classUpgradeChoices;
const isMulticlass = !multiclass ? false : item.system.domain === multiclass.domain;
if (
!this.actor.system.class.system.domains.includes(item.system.domain) &&
!this.actor.system.domains.includes(item.system.domain) &&
this.levelup.classUpgradeChoices?.multiclass?.domain !== item.system.domain
) {
ui.notifications.error(
@ -489,7 +585,9 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
return;
}
if (item.system.level > Number(target.dataset.limit)) {
const levelBase = isMulticlass ? Math.ceil(this.levelup.currentLevel / 2) : this.levelup.currentLevel;
const levelMax = target.dataset.limit ? Math.min(Number(target.dataset.limit), levelBase) : levelBase;
if (levelMax < item.system.level) {
ui.notifications.error(
game.i18n.localize('DAGGERHEART.Application.LevelUp.notifications.error.domainCardToHighLevel')
);
@ -522,7 +620,7 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
} else if (event.target.closest('.multiclass-cards')) {
const target = event.target.closest('.multiclass-cards');
if (item.type === 'class') {
if (item.name === this.actor.system.class.name) {
if (item.name === this.actor.system.class.value.name) {
ui.notifications.error(
game.i18n.localize('DAGGERHEART.Application.LevelUp.notifications.error.alreadySelectedClass')
);
@ -541,8 +639,7 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
amount: target.dataset.amount ? Number(target.dataset.amount) : null,
value: target.dataset.value,
type: target.dataset.type,
data: item.uuid,
secondaryData: null
data: item.uuid
}
});
this.render();
@ -556,16 +653,16 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
const update = {};
if (!button.checked) {
if (button.dataset.cost > 1) {
const basePath = `levels.${this.levelup.currentLevel}.choices`;
const current = foundry.utils.getProperty(this.levelup, `${basePath}.${button.dataset.option}`);
if (Number(button.dataset.cost) > 1 || Object.keys(current).length === 1) {
// Simple handling that doesn't cover potential Custom LevelTiers.
update[`levels.${this.levelup.currentLevel}.choices.-=${button.dataset.option}`] = null;
update[`${basePath}.-=${button.dataset.option}`] = null;
} else {
update[
`levels.${this.levelup.currentLevel}.choices.${button.dataset.option}.-=${button.dataset.checkboxNr}`
] = null;
update[`${basePath}.${button.dataset.option}.-=${button.dataset.checkboxNr}`] = null;
}
} else {
if (!this.levelup.levels[this.levelup.currentLevel].nrSelections.available) {
if (this.levelup.levels[this.levelup.currentLevel].nrSelections.available < Number(button.dataset.cost)) {
ui.notifications.info(
game.i18n.localize('DAGGERHEART.Application.LevelUp.notifications.info.insufficentAdvancements')
);
@ -573,15 +670,23 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
return;
}
update[
`levels.${this.levelup.currentLevel}.choices.${button.dataset.option}.${button.dataset.checkboxNr}`
] = {
const updateData = {
tier: Number(button.dataset.tier),
minCost: Number(button.dataset.cost),
amount: button.dataset.amount ? Number(button.dataset.amount) : null,
value: button.dataset.value,
type: button.dataset.type
};
if (button.dataset.type === 'domainCard') {
updateData.secondaryData = {
limit: Math.max(...this.levelup.tiers[button.dataset.tier].belongingLevels)
};
}
update[
`levels.${this.levelup.currentLevel}.choices.${button.dataset.option}.${button.dataset.checkboxNr}`
] = updateData;
}
await this.levelup.updateSource(update);
@ -594,24 +699,35 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
static async selectPreview(_, button) {
const remove = button.dataset.selected;
const selectionData = Object.values(this.levelup.selectionData);
const option = remove
? selectionData.find(x => x.type === 'subclass' && x.data.includes(button.dataset.uuid))
: selectionData.find(x => x.type === 'subclass' && x.data.length === 0);
if (!option) return;
await this.levelup.updateSource({
[`${button.dataset.path}`]: {
data: remove ? [] : [button.dataset.uuid],
secondaryData: {
featureState: button.dataset.featureState,
isMulticlass: button.dataset.isMulticlass
}
}
});
const path = `tiers.${option.tier}.levels.${option.level}.optionSelections.${option.optionKey}.${option.checkboxNr}.data`;
await this.levelup.updateSource({ [path]: remove ? [] : button.dataset.uuid });
this.render();
}
static async selectDomain(_, button) {
const option = foundry.utils.getProperty(this.levelup, button.dataset.path);
const domain = option.secondaryData ? null : button.dataset.domain;
const domain = option.secondaryData.domain ? null : button.dataset.domain;
await this.levelup.updateSource({
multiclass: { domain },
[`${button.dataset.path}.secondaryData`]: domain
[`${button.dataset.path}.secondaryData.domain`]: domain
});
this.render();
}
static async selectSubclass(_, button) {
const option = foundry.utils.getProperty(this.levelup, button.dataset.path);
const subclass = option.secondaryData.subclass ? null : button.dataset.subclass;
await this.levelup.updateSource({
[`${button.dataset.path}.secondaryData.subclass`]: subclass
});
this.render();
}

View file

@ -1,113 +0,0 @@
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class DhpMulticlassDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(actorName, actorClass, resolve) {
super({});
this.actorName = actorName;
this.actorClass = actorClass;
this.resolve = resolve;
this.classChoices = Array.from(
game.items.reduce((acc, x) => {
if (x.type === 'class' && x.name !== actorClass.name) {
acc.add(x);
}
return acc;
}, new Set())
);
this.subclassChoices = [];
this.domainChoices = [];
this.data = {
class: null,
subclass: null,
domain: null
};
}
get title() {
return `${this.actorName} - Multiclass`;
}
static DEFAULT_OPTIONS = {
classes: ['daggerheart', 'views', 'multiclass'],
position: { width: 600, height: 'auto' },
actions: {
selectClass: this.selectClass,
selectSubclass: this.selectSubclass,
selectDomain: this.selectDomain,
finish: this.finish
}
};
static PARTS = {
form: {
id: 'levelup',
template: 'systems/daggerheart/templates/views/multiclass.hbs'
}
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.classChoices = this.classChoices;
context.subclassChoices = this.subclassChoices;
context.domainChoices = this.domainChoices;
context.disabledFinish = !this.data.class || !this.data.subclass || !this.data.domain;
context.data = this.data;
return context;
}
static async selectClass(_, button) {
const oldClass = this.data.class;
this.data.class = this.data.class?.uuid === button.dataset.class ? null : await fromUuid(button.dataset.class);
if (oldClass !== button.dataset.class) {
this.data.subclass = null;
this.data.domain = null;
this.subclassChoices = this.data.class ? this.data.class.system.subclasses : [];
this.domainChoices = this.data.class
? this.data.class.system.domains.map(x => {
const config = SYSTEM.DOMAIN.domains[x];
return {
name: game.i18n.localize(config.name),
id: config.id,
img: config.src,
disabled: this.actorClass.system.domains.includes(config.id)
};
})
: [];
}
this.render(true);
}
static async selectSubclass(_, button) {
this.data.subclass =
this.data.subclass?.uuid === button.dataset.subclass
? null
: this.subclassChoices.find(x => x.uuid === button.dataset.subclass);
this.render(true);
}
static async selectDomain(_, button) {
const domain =
this.data.domain?.id === button.dataset.domain
? null
: this.domainChoices.find(x => x.id === button.dataset.domain);
if (domain?.disabled) return;
this.data.domain = domain;
this.render(true);
}
static finish() {
this.close({}, this.data);
}
async close(options = {}, data = null) {
this.resolve(data);
super.close(options);
}
}

View file

@ -14,7 +14,7 @@ export default class NpcRollSelectionDialog extends HandlebarsApplicationMixin(A
}
get title() {
return game.i18n.localize('DAGGERHEART.Application.Settings.Title');
return game.i18n.localize('DAGGERHEART.Application.RollSelection.Title');
}
static DEFAULT_OPTIONS = {

View file

@ -1,7 +1,7 @@
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export default class RollSelectionDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(experiences, bonusDamage, hopeResource, resolve, isNpc) {
constructor(experiences, hopeResource, resolve) {
super({}, {});
this.experiences = experiences;
@ -16,35 +16,21 @@ export default class RollSelectionDialog extends HandlebarsApplicationMixin(Appl
hope: ['d12'],
fear: ['d12'],
advantage: null,
disadvantage: null,
bonusDamage: bonusDamage.reduce((acc, x) => {
if (x.appliesOn === SYSTEM.EFFECTS.applyLocations.attackRoll.id) {
acc.push({
...x,
hopeUses: 0
});
}
return acc;
}, []),
hopeResource: hopeResource
};
}
static DEFAULT_OPTIONS = {
tag: 'form',
id: 'roll-selection', //Having an id causes a new instance to overwrite previous.
id: 'roll-selection',
classes: ['daggerheart', 'views', 'roll-selection'],
position: {
width: 400,
height: 'auto'
},
actions: {
updateIsAdvantage: this.updateIsAdvantage,
selectExperience: this.selectExperience,
decreaseHopeUse: this.decreaseHopeUse,
increaseHopeUse: this.increaseHopeUse,
setAdvantage: this.setAdvantage,
setDisadvantage: this.setDisadvantage,
finish: this.finish
},
form: {
@ -73,28 +59,14 @@ export default class RollSelectionDialog extends HandlebarsApplicationMixin(Appl
context.hope = this.data.hope;
context.fear = this.data.fear;
context.advantage = this.data.advantage;
context.disadvantage = this.data.disadvantage;
context.experiences = this.experiences.map(x => ({
...x,
selected: this.selectedExperiences.find(selected => selected.id === x.id)
}));
context.bonusDamage = this.data.bonusDamage;
context.experiences = Object.keys(this.experiences).map(id => ({ id, ...this.experiences[id] }));
context.hopeResource = this.data.hopeResource + 1;
context.hopeUsed = this.getHopeUsed();
return context;
}
static updateSelection(event, _, formData) {
const { bonusDamage, ...rest } = foundry.utils.expandObject(formData.object);
for (var index in bonusDamage) {
this.data.bonusDamage[index].initiallySelected = bonusDamage[index].initiallySelected;
if (bonusDamage[index].hopeUses) {
const value = Number.parseInt(bonusDamage[index].hopeUses);
if (!Number.isNaN(value)) this.data.bonusDamage[index].hopeUses = value;
}
}
const { ...rest } = foundry.utils.expandObject(formData.object);
this.data = foundry.utils.mergeObject(this.data, rest);
this.render();
@ -104,56 +76,24 @@ export default class RollSelectionDialog extends HandlebarsApplicationMixin(Appl
if (this.selectedExperiences.find(x => x.id === button.dataset.key)) {
this.selectedExperiences = this.selectedExperiences.filter(x => x.id !== button.dataset.key);
} else {
this.selectedExperiences = [
...this.selectedExperiences,
this.experiences.find(x => x.id === button.dataset.key)
];
this.selectedExperiences = [...this.selectedExperiences, button.dataset.key];
}
this.render();
}
getHopeUsed() {
return this.data.bonusDamage.reduce((acc, x) => acc + x.hopeUses, 0);
}
static decreaseHopeUse(_, button) {
const index = Number.parseInt(button.dataset.index);
if (this.data.bonusDamage[index].hopeUses - 1 >= 0) {
this.data.bonusDamage[index].hopeUses -= 1;
this.render(true);
}
}
static increaseHopeUse(_, button) {
const index = Number.parseInt(button.dataset.index);
if (this.data.bonusDamage[index].hopeUses <= this.data.hopeResource + 1) {
this.data.bonusDamage[index].hopeUses += 1;
this.render(true);
}
}
static setAdvantage() {
this.data.advantage = this.data.advantage ? null : 'd6';
this.data.disadvantage = null;
this.render(true);
}
static setDisadvantage() {
this.data.advantage = null;
this.data.disadvantage = this.data.disadvantage ? null : 'd6';
this.render(true);
static updateIsAdvantage(_, button) {
const advantage = Boolean(button.dataset.advantage);
this.data.advantage = this.data.advantage === advantage ? null : advantage;
this.render();
}
static async finish() {
const { diceOptions, ...rest } = this.data;
this.resolve({
...rest,
experiences: this.selectedExperiences,
hopeUsed: this.getHopeUsed(),
bonusDamage: this.data.bonusDamage.reduce((acc, x) => acc.concat(` + ${1 + x.hopeUses}${x.value}`), '')
experiences: this.selectedExperiences.map(x => ({ id: x, ...this.experiences[x] }))
});
this.close();
}

View file

@ -166,8 +166,6 @@ class DhpRangeSettings extends FormApplication {
}
export const registerDHSettings = () => {
// const debouncedReload = foundry.utils.debounce(() => window.location.reload(), 100);
game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.General.AbilityArray, {
name: game.i18n.localize('DAGGERHEART.Settings.General.AbilityArray.Name'),
hint: game.i18n.localize('DAGGERHEART.Settings.General.AbilityArray.Hint'),
@ -274,7 +272,7 @@ export const registerDHSettings = () => {
name: game.i18n.localize('DAGGERHEART.Settings.DualityRollColor.Name'),
hint: game.i18n.localize('DAGGERHEART.Settings.DualityRollColor.Hint'),
scope: 'world',
config: true,
config: false,
type: Number,
choices: Object.values(DualityRollColor),
default: DualityRollColor.colorful.value

View file

@ -54,6 +54,20 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
static async save() {
await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance, this.settings.toObject());
const reload = await foundry.applications.api.DialogV2.confirm({
id: 'reload-world-confirm',
modal: true,
rejectClose: false,
window: { title: 'SETTINGS.ReloadPromptTitle' },
position: { width: 400 },
content: `<p>${game.i18n.localize('SETTINGS.ReloadPromptBody')}</p>`
});
if (reload) {
await game.socket.emit('reload');
foundry.utils.debouncedReload();
}
this.close();
}
}

View file

@ -1,219 +1,12 @@
// import DhpApplicationMixin from '../daggerheart-sheet.mjs';
// export class Teest extends DhpApplicationMixin(ActorSheet) {
// static documentType = "adversary";
// constructor(options){
// super(options);
// this.editMode = false;
// }
// /** @override */
// static get defaultOptions() {
// return foundry.utils.mergeObject(super.defaultOptions, {
// classes: ["daggerheart", "sheet", "adversary"],
// width: 600,
// height: 'auto',
// resizable: false,
// });
// }
// async getData() {
// const context = super.getData();
// context.config = SYSTEM;
// context.editMode = this.editMode;
// context.title = `${this.actor.name} - ${game.i18n.localize(SYSTEM.ACTOR.adversaryTypes[this.actor.system.type].name)}`;
// context.data = {
// description: this.object.system.description,
// motivesAndTactics: this.object.system.motivesAndTactics.join(', '),
// tier: this.object.system.tier,
// type: game.i18n.localize(SYSTEM.ACTOR.adversaryTypes[this.object.system.type].name),
// attack: {
// name: this.object.system.attack.name,
// attackModifier: this.object.system.attackModifier,
// range: this.object.system.attack.range ? game.i18n.localize(SYSTEM.GENERAL.range[this.object.system.attack.range].name) : null,
// damage: {
// value: this.object.system.attack.damage.value,
// type: this.object.system.attack.damage.type,
// typeName: this.object.system.attack.damage.type ? game.i18n.localize(SYSTEM.GENERAL.damageTypes[this.object.system.attack.damage.type].abbreviation).toLowerCase() : null,
// },
// },
// damageThresholds: this.object.system.damageThresholds,
// difficulty: this.object.system.difficulty,
// hp: { ...this.object.system.resources.health, lastRowIndex: Math.floor(this.object.system.resources.health.max/5)*5 },
// stress: { ...this.object.system.resources.stress, lastRowIndex: Math.floor(this.object.system.resources.stress.max/5)*5 },
// moves: this.object.system.moves,
// };
// return context;
// }
// async _handleAction(action, event, button) {
// switch(action){
// case 'viewMove':
// await this.viewMove(button);
// break;
// case 'addMove':
// this.addMove();
// break;
// case 'removeMove':
// await this.removeMove(button);
// break;
// case 'toggleSlider':
// this.toggleEditMode();
// break;
// case 'addMotive':
// await this.addMotive();
// break;
// case 'removeMotive':
// await this.removeMotive(button);
// break;
// case 'reactionRoll':
// await this.reactionRoll(event);
// break;
// case 'attackRoll':
// await this.attackRoll(event);
// break;
// case 'addExperience':
// await this.addExperience();
// break;
// case 'removeExperience':
// await this.removeExperience(button);
// break;
// case 'toggleHP':
// await this.toggleHP(button);
// break;
// case 'toggleStress':
// await this.toggleStress(button);
// break;
// }
// }
// async viewMove(button){
// const move = await fromUuid(button.dataset.move);
// move.sheet.render(true);
// }
// async addMove(){
// const result = await this.object.createEmbeddedDocuments("Item", [{
// name: game.i18n.localize('DAGGERHEART.Sheets.Adversary.NewMove'),
// type: 'feature',
// }]);
// await result[0].sheet.render(true);
// }
// async removeMove(button){
// await this.object.items.find(x => x.uuid === button.dataset.move).delete();
// }
// toggleEditMode(){
// this.editMode = !this.editMode;
// this.render();
// }
// async addMotive(){
// await this.object.update({ "system.motivesAndTactics": [...this.object.system.motivesAndTactics, ''] });
// }
// async removeMotive(button){
// await this.object.update({ "system.motivesAndTactics": this.object.system.motivesAndTactics.filter((_, index) => index !== Number.parseInt(button.dataset.motive) )});
// }
// async reactionRoll(event){
// const { roll, diceResults, modifiers } = await this.actor.diceRoll({ title: `${this.actor.name} - Reaction Roll`, value: 0 }, event.shiftKey);
// const cls = getDocumentClass("ChatMessage");
// const msg = new cls({
// type: 'adversaryRoll',
// system: {
// roll: roll._formula,
// total: roll._total,
// modifiers: modifiers,
// diceResults: diceResults,
// },
// content: "systems/daggerheart/templates/chat/adversary-roll.hbs",
// rolls: [roll]
// });
// cls.create(msg.toObject());
// }
// async attackRoll(event){
// const modifier = Number.parseInt(event.currentTarget.dataset.value);
// const { roll, diceResults, modifiers } = await this.actor.diceRoll({ title: `${this.actor.name} - Attack Roll`, value: modifier }, event.shiftKey);
// const targets = Array.from(game.user.targets).map(x => ({
// id: x.id,
// name: x.actor.name,
// img: x.actor.img,
// difficulty: x.actor.system.difficulty,
// evasion: x.actor.system.evasion,
// }));
// const cls = getDocumentClass("ChatMessage");
// const msg = new cls({
// type: 'adversaryRoll',
// system: {
// roll: roll._formula,
// total: roll._total,
// modifiers: modifiers,
// diceResults: diceResults,
// targets: targets,
// damage: { value: event.currentTarget.dataset.damage, type: event.currentTarget.dataset.damageType },
// },
// content: "systems/daggerheart/templates/chat/adversary-attack-roll.hbs",
// rolls: [roll]
// });
// cls.create(msg.toObject());
// }
// async addExperience(){
// await this.object.update({ "system.experiences": [...this.object.system.experiences, { name: 'Experience', value: 1 }] });
// }
// async removeExperience(button){
// await this.object.update({ "system.experiences": this.object.system.experiences.filter((_, index) => index !== Number.parseInt(button.dataset.experience) )});
// }
// async toggleHP(button){
// const index = Number.parseInt(button.dataset.index);
// const newHP = index < this.object.system.resources.health.value ? index : index+1;
// await this.object.update({ "system.resources.health.value": newHP });
// }
// async toggleStress(button){
// const index = Number.parseInt(button.dataset.index);
// const newStress = index < this.object.system.resources.stress.value ? index : index+1;
// await this.object.update({ "system.resources.stress.value": newStress });
// }
// }
import DaggerheartSheet from './daggerheart-sheet.mjs';
const { ActorSheetV2 } = foundry.applications.sheets;
export default class AdversarySheet extends DaggerheartSheet(ActorSheetV2) {
constructor(options = {}) {
super(options);
this.editMode = false;
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'sheet', 'adversary'],
position: { width: 600 },
classes: ['daggerheart', 'sheet', 'actor', 'dh-style', 'adversary'],
position: { width: 450, height: 1000 },
actions: {
viewMove: this.viewMove,
addMove: this.addMove,
removeMove: this.removeMove,
toggleSlider: this.toggleEditMode,
addMotive: this.addMotive,
removeMotive: this.removeMotive,
reactionRoll: this.reactionRoll,
attackRoll: this.attackRoll,
addExperience: this.addExperience,
@ -229,54 +22,35 @@ export default class AdversarySheet extends DaggerheartSheet(ActorSheetV2) {
};
static PARTS = {
form: {
id: 'feature',
template: 'systems/daggerheart/templates/sheets/adversary.hbs'
header: { template: 'systems/daggerheart/templates/sheets/actors/adversary/header.hbs' },
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
main: { template: 'systems/daggerheart/templates/sheets/actors/adversary/main.hbs' },
information: { template: 'systems/daggerheart/templates/sheets/actors/adversary/information.hbs' }
};
static TABS = {
main: {
active: true,
cssClass: '',
group: 'primary',
id: 'main',
icon: null,
label: 'DAGGERHEART.Sheets.Adversary.Tabs.Main'
},
information: {
active: false,
cssClass: '',
group: 'primary',
id: 'information',
icon: null,
label: 'DAGGERHEART.Sheets.Adversary.Tabs.Information'
}
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.document = this.document;
context.config = SYSTEM;
context.editMode = this.editMode;
context.title = `${this.actor.name} - ${game.i18n.localize(SYSTEM.ACTOR.adversaryTypes[this.actor.system.type].name)}`;
context.data = {
description: this.document.system.description,
motivesAndTactics: this.document.system.motivesAndTactics.join(', '),
tier: this.document.system.tier,
type: game.i18n.localize(SYSTEM.ACTOR.adversaryTypes[this.document.system.type].name),
attack: {
name: this.document.system.attack.name,
attackModifier: this.document.system.attackModifier,
range: this.document.system.attack.range
? game.i18n.localize(SYSTEM.GENERAL.range[this.document.system.attack.range].name)
: null,
damage: {
value: this.document.system.attack.damage.value,
type: this.document.system.attack.damage.type,
typeName: this.document.system.attack.damage.type
? game.i18n
.localize(
SYSTEM.GENERAL.damageTypes[this.document.system.attack.damage.type].abbreviation
)
.toLowerCase()
: null
}
},
damageThresholds: this.document.system.damageThresholds,
difficulty: this.document.system.difficulty,
hp: {
...this.document.system.resources.health,
lastRowIndex: Math.floor(this.document.system.resources.health.max / 5) * 5
},
stress: {
...this.document.system.resources.stress,
lastRowIndex: Math.floor(this.document.system.resources.stress.max / 5) * 5
},
moves: this.document.system.moves
};
context.tabs = super._getTabs(this.constructor.TABS);
return context;
}
@ -286,109 +60,43 @@ export default class AdversarySheet extends DaggerheartSheet(ActorSheetV2) {
this.render();
}
static async viewMove(_, button) {
const move = await fromUuid(button.dataset.move);
move.sheet.render(true);
}
static async addMove() {
const result = await this.document.createEmbeddedDocuments('Item', [
{
name: game.i18n.localize('DAGGERHEART.Sheets.Adversary.NewMove'),
type: 'feature'
}
]);
await result[0].sheet.render(true);
}
static async removeMove(_, button) {
await this.document.items.find(x => x.uuid === button.dataset.move).delete();
}
static toggleEditMode() {
this.editMode = !this.editMode;
this.render();
}
static async addMotive() {
await this.document.update({ 'system.motivesAndTactics': [...this.document.system.motivesAndTactics, ''] });
}
static async removeMotive(button) {
await this.document.update({
'system.motivesAndTactics': this.document.system.motivesAndTactics.filter(
(_, index) => index !== Number.parseInt(button.dataset.motive)
)
});
}
static async reactionRoll(event) {
const { roll, diceResults, modifiers } = await this.actor.diceRoll(
{ title: `${this.actor.name} - Reaction Roll`, value: 0 },
event.shiftKey
);
const cls = getDocumentClass('ChatMessage');
const systemData = {
roll: roll._formula,
total: roll._total,
modifiers: modifiers,
diceResults: diceResults
const config = {
event: event,
title: `${this.actor.name} - Reaction Roll`,
roll: {
modifier: null,
type: 'reaction'
},
chatMessage: {
type: 'adversaryRoll',
template: 'systems/daggerheart/templates/chat/adversary-roll.hbs',
mute: true
}
};
const msg = new cls({
type: 'adversaryRoll',
system: systemData,
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/chat/adversary-roll.hbs',
systemData
),
rolls: [roll]
});
cls.create(msg.toObject());
this.actor.diceRoll(config);
}
static async attackRoll(event, button) {
const modifier = Number.parseInt(button.dataset.value);
const { roll, dice, advantageState, modifiers } = await this.actor.diceRoll(
{ title: `${this.actor.name} - Attack Roll`, value: modifier },
event.shiftKey
);
const targets = Array.from(game.user.targets).map(x => ({
id: x.id,
name: x.actor.name,
img: x.actor.img,
difficulty: x.actor.system.difficulty,
evasion: x.actor.system.evasion.value
}));
const cls = getDocumentClass('ChatMessage');
const systemData = {
title: button.dataset.name,
origin: this.document.id,
roll: roll._formula,
advantageState,
total: roll._total,
modifiers: modifiers,
dice: dice,
targets: targets,
damage: { value: button.dataset.damage, type: button.dataset.damageType }
};
const msg = new cls({
type: 'adversaryRoll',
sound: CONFIG.sounds.dice,
system: systemData,
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/chat/adversary-attack-roll.hbs',
systemData
),
rolls: [roll]
});
cls.create(msg.toObject());
static async attackRoll(event) {
const { modifier, damage, name: attackName } = this.actor.system.attack,
config = {
event: event,
title: attackName,
roll: {
modifier: modifier,
type: 'action'
},
chatMessage: {
type: 'adversaryRoll',
template: 'systems/daggerheart/templates/chat/adversary-attack-roll.hbs'
},
damage: {
value: damage.value,
type: damage.type
},
checkTarget: true
};
this.actor.diceRoll(config);
}
static async addExperience() {

View file

@ -0,0 +1,698 @@
import { capitalize } from '../../helpers/utils.mjs';
import DhpDeathMove from '../deathMove.mjs';
import DhpDowntime from '../downtime.mjs';
import AncestrySelectionDialog from '../ancestrySelectionDialog.mjs';
import DaggerheartSheet from './daggerheart-sheet.mjs';
import { abilities } from '../../config/actorConfig.mjs';
import DhlevelUp from '../levelup.mjs';
import DHDualityRoll from '../../data/chat-message/dualityRoll.mjs';
const { ActorSheetV2 } = foundry.applications.sheets;
const { TextEditor } = foundry.applications.ux;
export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
constructor(options = {}) {
super(options);
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'sheet', 'pc'],
position: { width: 810, height: 1080 },
actions: {
attributeRoll: this.rollAttribute,
toggleMarks: this.toggleMarks,
toggleHP: this.toggleHP,
toggleStress: this.toggleStress,
toggleHope: this.toggleHope,
toggleGold: this.toggleGold,
attackRoll: this.attackRoll,
useDomainCard: this.useDomainCard,
removeCard: this.removeDomainCard,
selectClass: this.selectClass,
selectSubclass: this.selectSubclass,
selectAncestry: this.selectAncestry,
selectCommunity: this.selectCommunity,
viewObject: this.viewObject,
useItem: this.useItem,
useFeature: this.useFeature,
takeShortRest: this.takeShortRest,
takeLongRest: this.takeLongRest,
deleteItem: this.deleteItem,
addScar: this.addScar,
deleteScar: this.deleteScar,
makeDeathMove: this.makeDeathMove,
itemQuantityDecrease: (_, button) => this.setItemQuantity(button, -1),
itemQuantityIncrease: (_, button) => this.setItemQuantity(button, 1),
useAbility: this.useAbility,
useAdvancementCard: this.useAdvancementCard,
useAdvancementAbility: this.useAdvancementAbility,
toggleEquipItem: this.toggleEquipItem,
levelup: this.openLevelUp
},
window: {
minimizable: false,
resizable: true
},
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false
},
dragDrop: [
{ dragSelector: null, dropSelector: '.weapon-section' },
{ dragSelector: null, dropSelector: '.armor-section' },
{ dragSelector: '.item-list .item', dropSelector: null }
]
};
static PARTS = {
form: {
id: 'character',
template: 'systems/daggerheart/templates/sheets/character/character.hbs'
}
};
_getTabs() {
const setActive = tabs => {
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] ? this.tabGroups[v.group] === v.id : v.active;
v.cssClass = v.active ? 'active' : '';
}
};
const primaryTabs = {
features: {
active: true,
cssClass: '',
group: 'primary',
id: 'features',
icon: null,
label: game.i18n.localize('DAGGERHEART.Sheets.PC.Tabs.Features')
},
loadout: {
active: false,
cssClass: '',
group: 'primary',
id: 'loadout',
icon: null,
label: game.i18n.localize('DAGGERHEART.Sheets.PC.Tabs.Loadout')
},
inventory: {
active: false,
cssClass: '',
group: 'primary',
id: 'inventory',
icon: null,
label: game.i18n.localize('DAGGERHEART.Sheets.PC.Tabs.Inventory')
},
story: {
active: false,
cssClass: '',
group: 'primary',
id: 'story',
icon: null,
label: game.i18n.localize('DAGGERHEART.Sheets.PC.Tabs.Story')
}
};
const secondaryTabs = {
foundation: {
active: true,
cssClass: '',
group: 'secondary',
id: 'foundation',
icon: null,
label: game.i18n.localize('DAGGERHEART.Sheets.PC.Tabs.Foundation')
},
loadout: {
active: false,
cssClass: '',
group: 'secondary',
id: 'loadout',
icon: null,
label: game.i18n.localize('DAGGERHEART.Sheets.PC.Tabs.Loadout')
},
vault: {
active: false,
cssClass: '',
group: 'secondary',
id: 'vault',
icon: null,
label: game.i18n.localize('DAGGERHEART.Sheets.PC.Tabs.Vault')
}
};
setActive(primaryTabs);
setActive(secondaryTabs);
return { primary: primaryTabs, secondary: secondaryTabs };
}
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
htmlElement.querySelector('.level-value').addEventListener('change', this.onLevelChange.bind(this));
// To Remove when ContextMenu Handler is made
htmlElement
.querySelectorAll('[data-item-id]')
.forEach(element => element.addEventListener('contextmenu', this.editItem.bind(this)));
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.document = this.document;
context.tabs = this._getTabs();
context.config = SYSTEM;
const selectedAttributes = Object.values(this.document.system.traits).map(x => x.base);
context.abilityScoreArray = JSON.parse(
await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.General.AbilityArray)
).reduce((acc, x) => {
const selectedIndex = selectedAttributes.indexOf(x);
if (selectedIndex !== -1) {
selectedAttributes.splice(selectedIndex, 1);
} else {
acc.push({ name: x, value: x });
}
return acc;
}, []);
if (!context.abilityScoreArray.includes(0)) context.abilityScoreArray.push({ name: 0, value: 0 });
context.abilityScoresFinished = context.abilityScoreArray.every(x => x.value === 0);
context.attributes = Object.keys(this.document.system.traits).reduce((acc, key) => {
acc[key] = {
...this.document.system.traits[key],
name: game.i18n.localize(SYSTEM.ACTOR.abilities[key].name),
verbs: SYSTEM.ACTOR.abilities[key].verbs.map(x => game.i18n.localize(x))
};
return acc;
}, {});
const ancestry = await this.mapFeatureType(
this.document.system.ancestry ? [this.document.system.ancestry] : [],
SYSTEM.GENERAL.objectTypes
);
const community = await this.mapFeatureType(
this.document.system.community ? [this.document.system.community] : [],
SYSTEM.GENERAL.objectTypes
);
const foundation = {
ancestry: ancestry[0],
community: community[0],
advancement: {}
};
const nrLoadoutCards = this.document.system.domainCards.loadout.length;
const loadout = await this.mapFeatureType(this.document.system.domainCards.loadout, SYSTEM.DOMAIN.cardTypes);
const vault = await this.mapFeatureType(this.document.system.domainCards.vault, SYSTEM.DOMAIN.cardTypes);
context.abilities = {
foundation: foundation,
loadout: {
top: loadout.slice(0, Math.min(2, nrLoadoutCards)),
bottom: nrLoadoutCards > 2 ? loadout.slice(2, Math.min(5, nrLoadoutCards)) : [],
nrTotal: nrLoadoutCards
},
vault: vault.map(x => ({
...x,
uuid: x.uuid,
sendToLoadoutDisabled: this.document.system.domainCards.loadout.length >= 5
}))
};
context.inventory = {
consumable: {
titles: {
name: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.ConsumableTitle'),
quantity: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.QuantityTitle')
},
items: this.document.items.filter(x => x.type === 'consumable')
},
miscellaneous: {
titles: {
name: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.MiscellaneousTitle'),
quantity: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.QuantityTitle')
},
items: this.document.items.filter(x => x.type === 'miscellaneous')
},
weapons: {
titles: {
name: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.WeaponsTitle'),
quantity: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.QuantityTitle')
},
items: this.document.items.filter(x => x.type === 'weapon')
},
armor: {
titles: {
name: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.ArmorsTitle'),
quantity: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.QuantityTitle')
},
items: this.document.items.filter(x => x.type === 'armor')
}
};
if (context.inventory.length === 0) {
context.inventory = Array(1).fill(Array(5).fill([]));
}
return context;
}
static async updateForm(event, _, formData) {
await this.document.update(formData.object);
this.render();
}
async mapFeatureType(data, configType) {
return await Promise.all(
data.map(async x => {
const abilities = x.system.abilities
? await Promise.all(x.system.abilities.map(async x => await fromUuid(x.uuid)))
: [];
return {
...x,
uuid: x.uuid,
system: {
...x.system,
abilities: abilities,
type: game.i18n.localize(configType[x.system.type ?? x.type].label)
}
};
})
);
}
static async rollAttribute(event, button) {
const abilityLabel = game.i18n.localize(abilities[button.dataset.attribute].label);
const config = {
event: event,
title: game.i18n.format('DAGGERHEART.Chat.DualityRoll.AbilityCheckTitle', {
ability: abilityLabel
}),
roll: {
label: abilityLabel,
modifier: button.dataset.value
},
chatMessage: {
template: 'systems/daggerheart/templates/chat/duality-roll.hbs'
}
};
this.document.diceRoll(config);
// Delete when new roll logic test done
/* const { roll, hope, fear, advantage, disadvantage, modifiers } = await this.document.dualityRoll(
{ title: game.i18n.localize(abilities[button.dataset.attribute].label), value: button.dataset.value },
event.shiftKey
);
const cls = getDocumentClass('ChatMessage');
const systemContent = new DHDualityRoll({
title: game.i18n.format('DAGGERHEART.Chat.DualityRoll.AbilityCheckTitle', {
ability: game.i18n.localize(abilities[button.dataset.attribute].label)
}),
origin: this.document.id,
roll: roll._formula,
modifiers: modifiers,
hope: hope,
fear: fear,
advantage: advantage,
disadvantage: disadvantage
});
await cls.create({
type: 'dualityRoll',
sound: CONFIG.sounds.dice,
system: systemContent,
user: game.user.id,
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/chat/duality-roll.hbs',
systemContent
),
rolls: [roll]
}); */
}
static async toggleMarks(_, button) {
const markValue = Number.parseInt(button.dataset.value);
const newValue = this.document.system.armor.system.marks.value >= markValue ? markValue - 1 : markValue;
await this.document.system.armor.update({ 'system.marks.value': newValue });
}
static async toggleHP(_, button) {
const healthValue = Number.parseInt(button.dataset.value);
const newValue = this.document.system.resources.hitPoints.value >= healthValue ? healthValue - 1 : healthValue;
await this.document.update({ 'system.resources.hitPoints.value': newValue });
}
static async toggleStress(_, button) {
const healthValue = Number.parseInt(button.dataset.value);
const newValue = this.document.system.resources.stress.value >= healthValue ? healthValue - 1 : healthValue;
await this.document.update({ 'system.resources.stress.value': newValue });
}
static async toggleHope(_, button) {
const hopeValue = Number.parseInt(button.dataset.value);
const newValue = this.document.system.resources.hope.value >= hopeValue ? hopeValue - 1 : hopeValue;
await this.document.update({ 'system.resources.hope.value': newValue });
}
static async toggleGold(_, button) {
const goldValue = Number.parseInt(button.dataset.value);
const goldType = button.dataset.type;
const newValue = this.document.system.gold[goldType] >= goldValue ? goldValue - 1 : goldValue;
const update = `system.gold.${goldType}`;
await this.document.update({ [update]: newValue });
}
static async attackRoll(event, button) {
const weapon = await fromUuid(button.dataset.weapon);
if (!weapon) return;
weapon.use(event);
}
static openLevelUp() {
if (!this.document.system.class.value || !this.document.system.class.subclass) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.Sheets.PC.Errors.missingClassOrSubclass'));
return;
}
new DhlevelUp(this.document).render(true);
}
static async useDomainCard(_, button) {
const card = this.document.items.find(x => x.uuid === button.dataset.key);
const cls = getDocumentClass('ChatMessage');
const systemData = {
title: `${game.i18n.localize('DAGGERHEART.Chat.DomainCard.Title')} - ${capitalize(button.dataset.domain)}`,
origin: this.document.id,
img: card.img,
name: card.name,
description: card.system.effect,
actions: card.system.actions
};
const msg = new cls({
type: 'abilityUse',
user: game.user.id,
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/chat/ability-use.hbs',
systemData
),
system: systemData
});
cls.create(msg.toObject());
}
static async removeDomainCard(_, button) {
if (button.dataset.type === 'domainCard') {
const card = this.document.items.find(x => x.uuid === button.dataset.key);
await card.delete();
}
}
static async selectClass() {
(await game.packs.get('daggerheart.classes'))?.render(true);
}
static async selectSubclass() {
(await game.packs.get('daggerheart.subclasses'))?.render(true);
}
static async selectAncestry() {
const dialogClosed = new Promise((resolve, _) => {
new AncestrySelectionDialog(resolve).render(true);
});
const result = await dialogClosed;
for (var ancestry of this.document.items.filter(x => x => x.type === 'ancestry')) {
await ancestry.delete();
}
const createdItems = [];
for (var feature of this.document.items.filter(
x => x.type === 'feature' && x.system.type === SYSTEM.ITEM.featureTypes.ancestry.id
)) {
await feature.delete();
}
createdItems.push(result.data);
await this.document.createEmbeddedDocuments('Item', createdItems);
}
static async selectCommunity() {
(await game.packs.get('daggerheart.communities'))?.render(true);
}
static useItem(event) {
const uuid = event.target.closest('[data-item-id]').dataset.itemId,
item = this.document.items.find(i => i.uuid === uuid);
item.use(event);
}
static async viewObject(_, button) {
const object = await fromUuid(button.dataset.value);
if (!object) return;
const tab = button.dataset.tab;
if (tab && object.sheet._tabs) object.sheet._tabs[0].active = tab;
if (object.sheet.editMode) object.sheet.editMode = false;
object.sheet.render(true);
}
editItem(event) {
const uuid = event.target.closest('[data-item-id]').dataset.itemId,
item = this.document.items.find(i => i.uuid === uuid);
if (!item) return;
if (item.sheet.editMode) item.sheet.editMode = false;
item.sheet.render(true);
}
static async takeShortRest() {
await new DhpDowntime(this.document, true).render(true);
await this.minimize();
}
static async takeLongRest() {
await new DhpDowntime(this.document, false).render(true);
await this.minimize();
}
static async addScar() {
if (this.document.system.story.scars.length === 5) return;
await this.document.update({
'system.story.scars': [
...this.document.system.story.scars,
{ name: game.i18n.localize('DAGGERHEART.Sheets.PC.NewScar'), description: '' }
]
});
}
static async deleteScar(event, button) {
event.stopPropagation();
await this.document.update({
'system.story.scars': this.document.system.story.scars.filter(
(_, index) => index !== Number.parseInt(button.currentTarget.dataset.scar)
)
});
}
static async makeDeathMove() {
if (this.document.system.resources.hitPoints.value === this.document.system.resources.hitPoints.max) {
await new DhpDeathMove(this.document).render(true);
await this.minimize();
}
}
async itemUpdate(event) {
const name = event.currentTarget.dataset.item;
const item = await fromUuid($(event.currentTarget).closest('[data-item-id]')[0].dataset.itemId);
await item.update({ [name]: event.currentTarget.value });
}
async onLevelChange(event) {
await this.document.updateLevel(Number(event.currentTarget.value));
this.render();
}
static async deleteItem(_, button) {
const item = await fromUuid($(button).closest('[data-item-id]')[0].dataset.itemId);
await item.delete();
}
static async setItemQuantity(button, value) {
const item = await fromUuid($(button).closest('[data-item-id]')[0].dataset.itemId);
await item.update({ 'system.quantity': Math.max(item.system.quantity + value, 1) });
}
static async useFeature(_, button) {
const item = await fromUuid(button.dataset.id);
const cls = getDocumentClass('ChatMessage');
const systemData = {
title: game.i18n.localize('DAGGERHEART.Chat.FeatureTitle'),
origin: this.document.id,
img: item.img,
name: item.name,
description: item.system.description,
actions: item.system.actions
};
const msg = new cls({
type: 'abilityUse',
user: game.user.id,
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/chat/ability-use.hbs',
systemData
),
system: systemData
});
cls.create(msg.toObject());
}
static async useAbility(_, button) {
const item = await fromUuid(button.dataset.feature);
const type = button.dataset.type;
const cls = getDocumentClass('ChatMessage');
const systemData = {
title:
type === 'ancestry'
? game.i18n.localize('DAGGERHEART.Chat.FoundationCard.AncestryTitle')
: type === 'community'
? game.i18n.localize('DAGGERHEART.Chat.FoundationCard.CommunityTitle')
: game.i18n.localize('DAGGERHEART.Chat.FoundationCard.SubclassFeatureTitle'),
origin: this.document.id,
img: item.img,
name: item.name,
description: item.system.description,
actions: []
};
const msg = new cls({
type: 'abilityUse',
user: game.user.id,
system: systemData,
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/chat/ability-use.hbs',
systemData
)
});
cls.create(msg.toObject());
}
static async useAdvancementCard(_, button) {
const item =
button.dataset.multiclass === 'true'
? this.document.system.multiclass.subclass
: this.document.system.class.subclass;
const ability = item.system[`${button.dataset.key}Feature`];
const title = `${item.name} - ${game.i18n.localize(`DAGGERHEART.Sheets.PC.DomainCard.${capitalize(button.dataset.key)}Title`)}`;
const cls = getDocumentClass('ChatMessage');
const systemData = {
title: game.i18n.localize('DAGGERHEART.Chat.FoundationCard.SubclassFeatureTitle'),
origin: this.document.id,
name: title,
img: item.img,
description: ability.description
};
const msg = new cls({
type: 'abilityUse',
user: game.user.id,
system: systemData,
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/chat/ability-use.hbs',
systemData
)
});
cls.create(msg.toObject());
}
static async useAdvancementAbility(_, button) {
const item = this.document.items.find(x => x.uuid === button.dataset.id);
const cls = getDocumentClass('ChatMessage');
const systemData = {
title: game.i18n.localize('DAGGERHEART.Chat.FoundationCard.SubclassFeatureTitle'),
origin: this.document.id,
name: item.name,
img: item.img,
description: item.system.description
};
const msg = new cls({
user: game.user.id,
system: systemData,
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/chat/ability-use.hbs',
systemData
)
});
cls.create(msg.toObject());
}
static async toggleEquipItem(_, button) {
const item = this.document.items.get(button.id);
if (item.system.equipped) {
await item.update({ 'system.equipped': false });
return;
}
switch (item.type) {
case 'armor':
const currentArmor = this.document.system.armor;
if (currentArmor) {
await currentArmor.update({ 'system.equipped': false });
}
await item.update({ 'system.equipped': true });
break;
case 'weapon':
await this.document.system.constructor.unequipBeforeEquip.bind(this.document.system)(item);
await item.update({ 'system.equipped': true });
break;
}
this.render();
}
async _onDragStart(_, event) {
super._onDragStart(event);
}
async _onDrop(event) {
super._onDrop(event);
this._onDropItem(event, TextEditor.getDragEventData(event));
}
async _onDropItem(event, data) {
const item = await Item.implementation.fromDropData(data);
const itemData = item.toObject();
if (item.type === 'domainCard' && this.document.system.domainCards.loadout.length >= 5) {
itemData.system.inVault = true;
}
if (this.document.uuid === item.parent?.uuid) return this._onSortItem(event, itemData);
const createdItem = await this._onDropItemCreate(itemData);
return createdItem;
}
async _onDropItemCreate(itemData, event) {
itemData = itemData instanceof Array ? itemData : [itemData];
return this.document.createEmbeddedDocuments('Item', itemData);
}
}

View file

@ -27,7 +27,7 @@ export default function DhpApplicationMixin(Base) {
async _prepareContext(_options, objectPath = 'document') {
const context = await super._prepareContext(_options);
context.source = this[objectPath].toObject();
context.source = this[objectPath];
context.fields = this[objectPath].schema.fields;
context.systemFields = this[objectPath].system ? this[objectPath].system.schema.fields : {};

View file

@ -1,78 +1,60 @@
import DaggerheartSheet from './daggerheart-sheet.mjs';
const { DocumentSheetV2 } = foundry.applications.api;
export default class DhpEnvironment extends DaggerheartSheet(DocumentSheetV2) {
constructor(options) {
super(options);
this.editMode = false;
}
const { ActorSheetV2 } = foundry.applications.sheets;
export default class DhpEnvironment extends DaggerheartSheet(ActorSheetV2) {
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'sheet', 'adversary', 'environment'],
classes: ['daggerheart', 'sheet', 'actor', 'dh-style', 'environment'],
position: {
width: 600,
height: 'auto'
width: 450,
height: 1000
},
actions: {
toggleSlider: this.toggleSlider,
viewFeature: this.viewFeature,
addAdversary: this.addAdversary,
addFeature: this.addFeature,
removeFeature: this.removeFeature,
addTone: this.addTone,
removeTone: this.removeTone,
useFeature: this.useFeature
deleteProperty: this.deleteProperty,
viewAdversary: this.viewAdversary
},
form: {
handler: this._updateForm,
closeOnSubmit: false,
submitOnChange: true
}
submitOnChange: true,
closeOnSubmit: false
},
dragDrop: [{ dragSelector: null, dropSelector: '.adversary-container' }]
};
/** @override */
static PARTS = {
form: {
id: 'form',
template: 'systems/daggerheart/templates/sheets/environment.hbs'
}
header: { template: 'systems/daggerheart/templates/sheets/actors/environment/header.hbs' },
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
main: { template: 'systems/daggerheart/templates/sheets/actors/environment/main.hbs' },
information: { template: 'systems/daggerheart/templates/sheets/actors/environment/information.hbs' }
};
/* -------------------------------------------- */
/** @inheritDoc */
get title() {
return `${game.i18n.localize('Environment')} - ${this.document.name}`;
}
static TABS = {
main: {
active: true,
cssClass: '',
group: 'primary',
id: 'main',
icon: null,
label: 'DAGGERHEART.Sheets.Environment.Tabs.Main'
},
information: {
active: false,
cssClass: '',
group: 'primary',
id: 'information',
icon: null,
label: 'DAGGERHEART.Sheets.Environment.Tabs.Information'
}
};
async _prepareContext(_options) {
return {
title: `${this.document.name} - ${game.i18n.localize(SYSTEM.ACTOR.adversaryTypes[this.document.system.type].name)}`,
user: this.document,
source: this.document.toObject(),
fields: this.document.schema.fields,
data: {
type: game.i18n.localize(SYSTEM.ACTOR.adversaryTypes[this.document.system.type].name),
features: this.document.items.reduce((acc, x) => {
if (x.type === 'feature') {
const feature = x.toObject();
acc.push({
...feature,
system: {
...feature.system,
actionType: game.i18n.localize(SYSTEM.ITEM.actionTypes[feature.system.actionType].name)
},
uuid: x.uuid
});
}
const context = await super._prepareContext(_options);
context.document = this.document;
context.tabs = super._getTabs(this.constructor.TABS);
return acc;
}, [])
},
editMode: this.editMode,
config: SYSTEM
};
return context;
}
static async _updateForm(event, _, formData) {
@ -80,60 +62,41 @@ export default class DhpEnvironment extends DaggerheartSheet(DocumentSheetV2) {
this.render();
}
static toggleSlider() {
this.editMode = !this.editMode;
static async addAdversary() {
await this.document.update({
[`system.potentialAdversaries.${foundry.utils.randomID()}.label`]: game.i18n.localize(
'DAGGERHEART.Sheets.Environment.newAdversary'
)
});
this.render();
}
static async viewFeature(_, button) {
const move = await fromUuid(button.dataset.feature);
move.sheet.render(true);
}
static async addFeature() {
const result = await this.document.createEmbeddedDocuments('Item', [
{
name: game.i18n.localize('DAGGERHEART.Sheets.Environment.NewFeature'),
type: 'feature'
}
]);
await result[0].sheet.render(true);
ui.notifications.error('Not Implemented yet. Awaiting datamodel rework');
}
static async removeFeature(_, button) {
await this.document.items.find(x => x.uuid === button.dataset.feature).delete();
static async deleteProperty(_, target) {
await this.document.update({ [`${target.dataset.path}.-=${target.id}`]: null });
this.render();
}
static async addTone() {
await this.document.update({ 'system.toneAndFeel': [...this.document.system.toneAndFeel, ''] });
static async viewAdversary(_, button) {
const adversary = foundry.utils.getProperty(
this.document.system.potentialAdversaries,
`${button.dataset.potentialAdversary}.adversaries.${button.dataset.adversary}`
);
adversary.sheet.render(true);
}
static async removeTone(button) {
await this.document.update({
'system.toneAndFeel': this.document.system.toneAndFeel.filter(
(_, index) => index !== Number.parseInt(button.dataset.tone)
)
});
}
static async useFeature(_, button) {
const item = this.document.items.find(x => x.uuid === button.dataset.feature);
const cls = getDocumentClass('ChatMessage');
const msg = new cls({
user: game.user.id,
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/chat/ability-use.hbs',
{
title: game.i18n.format('DAGGERHEART.Chat.EnvironmentTitle', {
actionType: button.dataset.actionType
}),
card: { name: item.name, img: item.img, description: item.system.description }
}
)
});
cls.create(msg.toObject());
async _onDrop(event) {
const data = TextEditor.getDragEventData(event);
const item = await fromUuid(data.uuid);
if (item.type === 'adversary') {
const target = event.target.closest('.adversary-container');
const path = `system.potentialAdversaries.${target.dataset.potentialAdversary}.adversaries.${item.id}`;
await this.document.update({
[path]: item.uuid
});
}
}
}

View file

@ -0,0 +1,132 @@
import DhpApplicationMixin from './daggerheart-sheet.mjs';
import DHActionConfig from '../config/Action.mjs';
import { actionsTypes } from '../../data/_module.mjs';
export default function DHItemMixin(Base) {
return class DHItemSheetV2 extends DhpApplicationMixin(Base) {
constructor(options = {}) {
super(options);
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'sheet', 'item', 'dh-style'],
position: { width: 600 },
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false
},
actions: {
addAction: this.addAction,
editAction: this.editAction,
removeAction: this.removeAction
}
};
static TABS = {
description: {
active: true,
cssClass: '',
group: 'primary',
id: 'description',
icon: null,
label: 'DAGGERHEART.Sheets.Feature.Tabs.Description'
},
actions: {
active: false,
cssClass: '',
group: 'primary',
id: 'actions',
icon: null,
label: 'DAGGERHEART.Sheets.Feature.Tabs.Actions'
},
settings: {
active: false,
cssClass: '',
group: 'primary',
id: 'settings',
icon: null,
label: 'DAGGERHEART.Sheets.Feature.Tabs.Settings'
}
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.document = this.document;
context.config = CONFIG.daggerheart;
context.tabs = super._getTabs(this.constructor.TABS);
return context;
}
static async updateForm(event, _, formData) {
await this.document.update(formData.object);
this.render();
}
static async selectActionType() {
const content = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/views/actionType.hbs',
{ types: SYSTEM.ACTIONS.actionTypes }
),
title = 'Select Action Type',
type = 'form',
data = {};
return Dialog.prompt({
title,
label: title,
content,
type,
callback: html => {
const form = html[0].querySelector('form'),
fd = new foundry.applications.ux.FormDataExtended(form);
foundry.utils.mergeObject(data, fd.object, { inplace: true });
// if (!data.name?.trim()) data.name = game.i18n.localize(SYSTEM.ACTIONS.actionTypes[data.type].name);
return data;
},
rejectClose: false
});
}
static async addAction() {
const actionType = await DHItemSheetV2.selectActionType(),
actionIndexes = this.document.system.actions.map(x => x._id.split('-')[2]).sort((a, b) => a - b);
try {
const cls = actionsTypes[actionType?.type] ?? actionsTypes.attack,
action = new cls(
{
// id: `${this.document.id}-Action-${actionIndexes.length > 0 ? actionIndexes[0] + 1 : 1}`
_id: foundry.utils.randomID(),
type: actionType.type,
name: game.i18n.localize(SYSTEM.ACTIONS.actionTypes[actionType.type].name),
...cls.getSourceConfig(this.document)
},
{
parent: this.document
}
);
await this.document.update({ 'system.actions': [...this.document.system.actions, action] });
await new DHActionConfig(this.document.system.actions[this.document.system.actions.length - 1]).render(
true
);
} catch (error) {
console.log(error);
}
}
static async editAction(_, button) {
const action = this.document.system.actions[button.dataset.index];
await new DHActionConfig(action).render(true);
}
static async removeAction(event, button) {
event.stopPropagation();
await this.document.update({
'system.actions': this.document.system.actions.filter(
(_, index) => index !== Number.parseInt(button.dataset.index)
)
});
}
};
}

View file

@ -1,16 +1,9 @@
import DaggerheartSheet from '../daggerheart-sheet.mjs';
import DHItemSheetV2 from '../item.mjs';
const { ItemSheetV2 } = foundry.applications.sheets;
export default class ArmorSheet extends DaggerheartSheet(ItemSheetV2) {
export default class ArmorSheet extends DHItemSheetV2(ItemSheetV2) {
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'sheet', 'item', 'dh-style', 'armor'],
position: { width: 600 },
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false
},
classes: ['armor'],
dragDrop: [{ dragSelector: null, dropSelector: null }]
};
@ -18,42 +11,13 @@ export default class ArmorSheet extends DaggerheartSheet(ItemSheetV2) {
header: { template: 'systems/daggerheart/templates/sheets/items/armor/header.hbs' },
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
description: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-description.hbs' },
actions: {
template: 'systems/daggerheart/templates/sheets/global/tabs/tab-actions.hbs',
scrollable: ['.actions']
},
settings: {
template: 'systems/daggerheart/templates/sheets/items/armor/settings.hbs',
scrollable: ['.settings']
}
};
static TABS = {
description: {
active: true,
cssClass: '',
group: 'primary',
id: 'description',
icon: null,
label: 'DAGGERHEART.Sheets.Feature.Tabs.Description'
},
settings: {
active: false,
cssClass: '',
group: 'primary',
id: 'settings',
icon: null,
label: 'DAGGERHEART.Sheets.Feature.Tabs.Settings'
}
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.document = this.document;
context.config = CONFIG.daggerheart;
context.tabs = super._getTabs(this.constructor.TABS);
return context;
}
static async updateForm(event, _, formData) {
await this.document.update(formData.object);
this.render();
}
}

View file

@ -1,5 +1,5 @@
import { tagifyElement } from '../../../helpers/utils.mjs';
import DaggerheartSheet from '../daggerheart-sheet.mjs';
import Tagify from '@yaireo/tagify';
const { ItemSheetV2 } = foundry.applications.sheets;
const { TextEditor } = foundry.applications.ux;
@ -11,8 +11,8 @@ export default class ClassSheet extends DaggerheartSheet(ItemSheetV2) {
actions: {
removeSubclass: this.removeSubclass,
viewSubclass: this.viewSubclass,
removeFeature: this.removeFeature,
viewFeature: this.viewFeature,
deleteFeature: this.deleteFeature,
editFeature: this.editFeature,
removeItem: this.removeItem,
viewItem: this.viewItem,
removePrimaryWeapon: this.removePrimaryWeapon,
@ -72,55 +72,14 @@ export default class ClassSheet extends DaggerheartSheet(ItemSheetV2) {
super._attachPartListeners(partId, htmlElement, options);
const domainInput = htmlElement.querySelector('.domain-input');
const domainTagify = new Tagify(domainInput, {
tagTextProp: 'name',
enforceWhitelist: true,
whitelist: Object.keys(SYSTEM.DOMAIN.domains).map(key => {
const domain = SYSTEM.DOMAIN.domains[key];
return {
value: key,
name: game.i18n.localize(domain.label),
src: domain.src,
background: domain.background
};
}),
maxTags: 2,
callbacks: { invalid: this.onAddTag },
dropdown: {
mapValueTo: 'name',
searchKeys: ['name'],
enabled: 0,
maxItems: 20,
closeOnSelect: true,
highlightFirst: false
},
templates: {
tag(tagData) {
//z-index: unset; background-image: ${tagData.background}; Maybe a domain specific background for the chips?
return `<tag title="${tagData.title || tagData.value}"
contenteditable='false'
spellcheck='false'
tabIndex="${this.settings.a11y.focusableTags ? 0 : -1}"
class="${this.settings.classNames.tag} ${tagData.class ? tagData.class : ''}"
${this.getAttributes(tagData)}>
<x class="${this.settings.classNames.tagX}" role='button' aria-label='remove tag'></x>
<div>
<span class="${this.settings.classNames.tagText}">${tagData[this.settings.tagTextProp] || tagData.value}</span>
<img src="${tagData.src}"></i>
</div>
</tag>`;
}
}
});
domainTagify.on('change', this.onDomainSelect.bind(this));
tagifyElement(domainInput, SYSTEM.DOMAIN.domains, this.onDomainSelect.bind(this));
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.document = this.document;
context.tabs = super._getTabs(this.constructor.TABS);
context.domains = this.document.system.domains.map(x => SYSTEM.DOMAIN.domains[x].label);
context.domains = this.document.system.domains;
return context;
}
@ -136,8 +95,7 @@ export default class ClassSheet extends DaggerheartSheet(ItemSheetV2) {
}
}
async onDomainSelect(event) {
const domains = event.detail?.value ? JSON.parse(event.detail.value) : [];
async onDomainSelect(domains) {
await this.document.update({ 'system.domains': domains.map(x => x.value) });
this.render(true);
}
@ -153,13 +111,13 @@ export default class ClassSheet extends DaggerheartSheet(ItemSheetV2) {
subclass.sheet.render(true);
}
static async removeFeature(_, button) {
static async deleteFeature(_, button) {
await this.document.update({
'system.features': this.document.system.features.filter(x => x.uuid !== button.dataset.feature)
'system.features': this.document.system.features.map(x => x.uuid).filter(x => x !== button.dataset.feature)
});
}
static async viewFeature(_, button) {
static async editFeature(_, button) {
const feature = await fromUuid(button.dataset.feature);
feature.sheet.render(true);
}
@ -198,71 +156,48 @@ export default class ClassSheet extends DaggerheartSheet(ItemSheetV2) {
const item = await fromUuid(data.uuid);
if (item.type === 'subclass') {
await this.document.update({
'system.subclasses': [
...this.document.system.subclasses,
{ img: item.img, name: item.name, uuid: item.uuid }
]
'system.subclasses': [...this.document.system.subclasses.map(x => x.uuid), item.uuid]
});
} else if (item.type === 'feature') {
await this.document.update({
'system.features': [
...this.document.system.features,
{ img: item.img, name: item.name, uuid: item.uuid }
]
'system.features': [...this.document.system.features.map(x => x.uuid), item.uuid]
});
} else if (item.type === 'weapon') {
if (event.currentTarget.classList.contains('primary-weapon-section')) {
if (!this.document.system.characterGuide.suggestedPrimaryWeapon && !item.system.secondary)
await this.document.update({
'system.characterGuide.suggestedPrimaryWeapon': {
img: item.img,
name: item.name,
uuid: item.uuid
}
'system.characterGuide.suggestedPrimaryWeapon': item.uuid
});
} else if (event.currentTarget.classList.contains('secondary-weapon-section')) {
if (!this.document.system.characterGuide.suggestedSecondaryWeapon && item.system.secondary)
await this.document.update({
'system.characterGuide.suggestedSecondaryWeapon': {
img: item.img,
name: item.name,
uuid: item.uuid
}
'system.characterGuide.suggestedSecondaryWeapon': item.uuid
});
}
} else if (item.type === 'armor') {
if (event.currentTarget.classList.contains('armor-section')) {
if (!this.document.system.characterGuide.suggestedArmor)
await this.document.update({
'system.characterGuide.suggestedArmor': { img: item.img, name: item.name, uuid: item.uuid }
'system.characterGuide.suggestedArmor': item.uuid
});
}
} else if (event.currentTarget.classList.contains('choice-a-section')) {
if (item.type === 'miscellaneous' || item.type === 'consumable') {
if (this.document.system.inventory.choiceA.length < 2)
await this.document.update({
'system.inventory.choiceA': [
...this.document.system.inventory.choiceA,
{ img: item.img, name: item.name, uuid: item.uuid }
]
'system.inventory.choiceA': [...this.document.system.inventory.choiceA, item.uuid]
});
}
} else if (item.type === 'miscellaneous') {
if (event.currentTarget.classList.contains('take-section')) {
if (this.document.system.inventory.take.length < 3)
await this.document.update({
'system.inventory.take': [
...this.document.system.inventory.take,
{ img: item.img, name: item.name, uuid: item.uuid }
]
'system.inventory.take': [...this.document.system.inventory.take, item.uuid]
});
} else if (event.currentTarget.classList.contains('choice-b-section')) {
if (this.document.system.inventory.choiceB.length < 2)
await this.document.update({
'system.inventory.choiceB': [
...this.document.system.inventory.choiceB,
{ img: item.img, name: item.name, uuid: item.uuid }
]
'system.inventory.choiceB': [...this.document.system.inventory.choiceB, item.uuid]
});
}
}

View file

@ -1,57 +1,23 @@
import DaggerheartSheet from '../daggerheart-sheet.mjs';
import DHItemSheetV2 from '../item.mjs';
const { ItemSheetV2 } = foundry.applications.sheets;
export default class ConsumableSheet extends DaggerheartSheet(ItemSheetV2) {
export default class ConsumableSheet extends DHItemSheetV2(ItemSheetV2) {
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'sheet', 'item', 'dh-style', 'consumable'],
position: { width: 550 },
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false
}
classes: ['consumable'],
position: { width: 550 }
};
static PARTS = {
header: { template: 'systems/daggerheart/templates/sheets/items/consumable/header.hbs' },
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
description: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-description.hbs' },
actions: {
template: 'systems/daggerheart/templates/sheets/global/tabs/tab-actions.hbs',
scrollable: ['.actions']
},
settings: {
template: 'systems/daggerheart/templates/sheets/items/consumable/settings.hbs',
scrollable: ['.settings']
}
};
static TABS = {
description: {
active: true,
cssClass: '',
group: 'primary',
id: 'description',
icon: null,
label: 'DAGGERHEART.Sheets.Feature.Tabs.Description'
},
settings: {
active: false,
cssClass: '',
group: 'primary',
id: 'settings',
icon: null,
label: 'DAGGERHEART.Sheets.Feature.Tabs.Settings'
}
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.document = this.document;
context.tabs = super._getTabs(this.constructor.TABS);
return context;
}
static async updateForm(event, _, formData) {
await this.document.update(formData.object);
this.render();
}
}

View file

@ -1,23 +1,10 @@
import DaggerheartAction from '../../../data/action.mjs';
import DaggerheartActionConfig from '../../config/Action.mjs';
import DaggerheartSheet from '../daggerheart-sheet.mjs';
import DHItemSheetV2 from '../item.mjs';
const { ItemSheetV2 } = foundry.applications.sheets;
export default class DomainCardSheet extends DaggerheartSheet(ItemSheetV2) {
export default class DomainCardSheet extends DHItemSheetV2(ItemSheetV2) {
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'sheet', 'item', 'dh-style', 'domain-card'],
position: { width: 450, height: 700 },
actions: {
addAction: this.addAction,
editAction: this.editAction,
removeAction: this.removeAction
},
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false
}
classes: ['domain-card'],
position: { width: 450, height: 700 }
};
static PARTS = {
@ -33,74 +20,4 @@ export default class DomainCardSheet extends DaggerheartSheet(ItemSheetV2) {
scrollable: ['.settings']
}
};
static TABS = {
description: {
active: true,
cssClass: '',
group: 'primary',
id: 'description',
icon: null,
label: 'DAGGERHEART.Sheets.Feature.Tabs.Description'
},
actions: {
active: false,
cssClass: '',
group: 'primary',
id: 'actions',
icon: null,
label: 'DAGGERHEART.Sheets.Feature.Tabs.Actions'
},
settings: {
active: false,
cssClass: '',
group: 'primary',
id: 'settings',
icon: null,
label: 'DAGGERHEART.Sheets.Feature.Tabs.Settings'
}
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.config = CONFIG.daggerheart;
context.tabs = super._getTabs(this.constructor.TABS);
return context;
}
static async updateForm(event, _, formData) {
await this.document.update(formData.object);
this.render();
}
static async addAction() {
const actionIndexes = this.document.system.actions.map(x => x.id.split('-')[2]).sort((a, b) => a - b);
const action = await new DaggerheartAction(
{
id: `${this.document.id}-Action-${actionIndexes.length > 0 ? actionIndexes[0] + 1 : 1}`
},
{
parent: this.document
}
);
await this.document.update({ 'system.actions': [...this.document.system.actions, action] });
await new DaggerheartActionConfig(this.document.system.actions[this.document.system.actions.length - 1]).render(
true
);
}
static async editAction(_, button) {
const action = this.document.system.actions[button.dataset.index];
await new DaggerheartActionConfig(action).render(true);
}
static async removeAction(event, button) {
event.stopPropagation();
await this.document.update({
'system.actions': this.document.system.actions.filter(
(_, index) => index !== Number.parseInt(button.dataset.index)
)
});
}
}

View file

@ -1,9 +1,7 @@
import DaggerheartAction from '../../../data/action.mjs';
import DaggerheartActionConfig from '../../config/Action.mjs';
import DaggerheartSheet from '../daggerheart-sheet.mjs';
import DHItemSheetV2 from '../item.mjs';
const { ItemSheetV2 } = foundry.applications.sheets;
export default class FeatureSheet extends DaggerheartSheet(ItemSheetV2) {
export default class FeatureSheet extends DHItemSheetV2(ItemSheetV2) {
constructor(options = {}) {
super(options);
@ -11,22 +9,13 @@ export default class FeatureSheet extends DaggerheartSheet(ItemSheetV2) {
}
static DEFAULT_OPTIONS = {
tag: 'form',
id: 'daggerheart-feature',
classes: ['daggerheart', 'sheet', 'item', 'dh-style', 'feature'],
classes: ['feature'],
position: { width: 600, height: 600 },
window: { resizable: true },
actions: {
addEffect: this.addEffect,
removeEffect: this.removeEffect,
addAction: this.addAction,
editAction: this.editAction,
removeAction: this.removeAction
},
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false
removeEffect: this.removeEffect
}
};
@ -49,30 +38,7 @@ export default class FeatureSheet extends DaggerheartSheet(ItemSheetV2) {
};
static TABS = {
description: {
active: true,
cssClass: '',
group: 'primary',
id: 'description',
icon: null,
label: 'DAGGERHEART.Sheets.Feature.Tabs.Description'
},
actions: {
active: false,
cssClass: '',
group: 'primary',
id: 'actions',
icon: null,
label: 'DAGGERHEART.Sheets.Feature.Tabs.Actions'
},
settings: {
active: false,
cssClass: '',
group: 'primary',
id: 'settings',
icon: null,
label: 'DAGGERHEART.Sheets.Feature.Tabs.Settings'
},
...super.TABS,
effects: {
active: false,
cssClass: '',
@ -102,11 +68,6 @@ export default class FeatureSheet extends DaggerheartSheet(ItemSheetV2) {
return context;
}
static async updateForm(event, _, formData) {
await this.document.update(formData.object);
this.render();
}
effectSelect(event) {
this.selectedEffectType = event.currentTarget.value;
this.render(true);
@ -130,26 +91,4 @@ export default class FeatureSheet extends DaggerheartSheet(ItemSheetV2) {
const path = `system.effects.-=${button.dataset.effect}`;
await this.item.update({ [path]: null });
}
static async addAction() {
const action = await new DaggerheartAction({ img: this.document.img }, { parent: this.document });
await this.document.update({ 'system.actions': [...this.document.system.actions, action] });
await new DaggerheartActionConfig(this.document.system.actions[this.document.system.actions.length - 1]).render(
true
);
}
static async editAction(_, button) {
const action = this.document.system.actions[button.dataset.index];
await new DaggerheartActionConfig(action).render(true);
}
static async removeAction(event, button) {
event.stopPropagation();
await this.document.update({
'system.actions': this.document.system.actions.filter(
(_, index) => index !== Number.parseInt(button.dataset.index)
)
});
}
}

View file

@ -1,57 +1,23 @@
import DaggerheartSheet from '../daggerheart-sheet.mjs';
import DHItemSheetV2 from '../item.mjs';
const { ItemSheetV2 } = foundry.applications.sheets;
export default class MiscellaneousSheet extends DaggerheartSheet(ItemSheetV2) {
export default class MiscellaneousSheet extends DHItemSheetV2(ItemSheetV2) {
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'sheet', 'item', 'dh-style', 'miscellaneous'],
position: { width: 550 },
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false
}
classes: ['miscellaneous'],
position: { width: 550 }
};
static PARTS = {
header: { template: 'systems/daggerheart/templates/sheets/items/miscellaneous/header.hbs' },
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
description: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-description.hbs' },
actions: {
template: 'systems/daggerheart/templates/sheets/global/tabs/tab-actions.hbs',
scrollable: ['.actions']
},
settings: {
template: 'systems/daggerheart/templates/sheets/items/miscellaneous/settings.hbs',
scrollable: ['.settings']
}
};
static TABS = {
description: {
active: true,
cssClass: '',
group: 'primary',
id: 'description',
icon: null,
label: 'DAGGERHEART.Sheets.Feature.Tabs.Description'
},
settings: {
active: false,
cssClass: '',
group: 'primary',
id: 'settings',
icon: null,
label: 'DAGGERHEART.Sheets.Feature.Tabs.Settings'
}
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.document = this.document;
context.tabs = super._getTabs(this.constructor.TABS);
return context;
}
static async updateForm(event, _, formData) {
await this.document.update(formData.object);
this.render();
}
}

View file

@ -1,5 +1,4 @@
import DaggerheartSheet from '../daggerheart-sheet.mjs';
import DaggerheartFeature from '../../../data/feature.mjs';
const { ItemSheetV2 } = foundry.applications.sheets;
const { TextEditor } = foundry.applications.ux;

View file

@ -1,58 +1,22 @@
import DaggerheartSheet from '../daggerheart-sheet.mjs';
import DHItemSheetV2 from '../item.mjs';
const { ItemSheetV2 } = foundry.applications.sheets;
export default class WeaponSheet extends DaggerheartSheet(ItemSheetV2) {
export default class WeaponSheet extends DHItemSheetV2(ItemSheetV2) {
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'sheet', 'item', 'dh-style', 'weapon'],
position: { width: 600 },
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false
}
classes: ['weapon']
};
static PARTS = {
header: { template: 'systems/daggerheart/templates/sheets/items/weapon/header.hbs' },
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
description: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-description.hbs' },
actions: {
template: 'systems/daggerheart/templates/sheets/global/tabs/tab-actions.hbs',
scrollable: ['.actions']
},
settings: {
template: 'systems/daggerheart/templates/sheets/items/weapon/settings.hbs',
scrollable: ['.settings']
}
};
static TABS = {
description: {
active: true,
cssClass: '',
group: 'primary',
id: 'description',
icon: null,
label: 'DAGGERHEART.Sheets.Feature.Tabs.Description'
},
settings: {
active: false,
cssClass: '',
group: 'primary',
id: 'settings',
icon: null,
label: 'DAGGERHEART.Sheets.Feature.Tabs.Settings'
}
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.document = this.document;
context.config = CONFIG.daggerheart;
context.tabs = super._getTabs(this.constructor.TABS);
return context;
}
static async updateForm(event, _, formData) {
await this.document.update(formData.object);
this.render();
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1 @@
export {default as PseudoDocumentSheet }from "./pseudo-documents-sheet.mjs";

View file

@ -0,0 +1,66 @@
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export default class PseudoDocumentSheet extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(options) {
super(options);
this.#pseudoDocument = options.document;
}
/**
* The UUID of the associated pseudo-document
* @type {string}
*/
get pseudoUuid() {
return this.pseudoDocument.uuid;
}
#pseudoDocument;
/**
* The pseudo-document instance this sheet represents
* @type {object}
*/
get pseudoDocument() {
return this.#pseudoDocument;
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'sheet'],
position: { width: 600 },
form: {
handler: PseudoDocumentSheet.#onSubmitForm,
submitOnChange: true,
closeOnSubmit: false
},
dragDrop: [{ dragSelector: null, dropSelector: null }],
};
static PARTS = {
header: { template: 'systems/daggerheart/templates/sheets/pseudo-documents/header.hbs' },
};
/** @inheritDoc */
async _prepareContext(options) {
const context = await super._prepareContext(options);
const document = this.pseudoDocument;
return Object.assign(context, {
document,
source: document._source,
editable: this.isEditable,
user: game.user,
rootId: this.id,
});
}
/**
* Form submission handler
* @param {SubmitEvent | Event} event - The originating form submission or input change event
* @param {HTMLFormElement} form - The form element that was submitted
* @param {foundry.applications.ux.FormDataExtended} formData - Processed data for the submitted form
*/
static async #onSubmitForm(event, form, formData) {
const submitData = foundry.utils.expandObject(formData.object);
await this.pseudoDocument.update(submitData);
}
}

View file

@ -1,7 +1,43 @@
export const actionTypes = {
attack: {
id: 'attack',
name: 'DAGGERHEART.Actions.Types.Attack.Name',
icon: 'fa-swords'
},
spellcast: {
id: 'spellcast',
name: 'DAGGERHEART.Actions.Types.Spellcast.Name',
icon: 'fa-book-sparkles'
},
healing: {
id: 'healing',
name: 'DAGGERHEART.Actions.Types.Healing.Name',
icon: 'fa-kit-medical'
},
resource: {
id: 'resource',
name: 'DAGGERHEART.Actions.Types.Resource.Name',
icon: 'fa-honey-pot'
},
damage: {
id: 'damage',
name: 'DAGGERHEART.Effects.Types.Health.Name'
name: 'DAGGERHEART.Actions.Types.Damage.Name',
icon: 'fa-bone-break'
},
summon: {
id: 'summon',
name: 'DAGGERHEART.Actions.Types.Summon.Name',
icon: 'fa-ghost'
},
effect: {
id: 'effect',
name: 'DAGGERHEART.Actions.Types.Effect.Name',
icon: 'fa-person-rays'
},
macro: {
id: 'macro',
name: 'DAGGERHEART.Actions.Types.Macro.Name',
icon: 'fa-scroll'
}
};

View file

@ -76,53 +76,82 @@ export const featureProperties = {
},
spellcastingTrait: {
name: 'DAGGERHEART.FeatureProperty.SpellcastingTrait',
path: actor => actor.system.traits[actor.system.subclass.system.spellcastingTrait].data.value
path: actor => actor.system.traits[actor.system.class.subclass.system.spellcastingTrait].data.value
}
};
export const adversaryTypes = {
bruiser: {
name: 'DAGGERHEART.Adversary.Bruiser.Name',
id: 'bruiser',
label: 'DAGGERHEART.Adversary.Type.Bruiser.label',
description: 'DAGGERHEART.Adversary.Bruiser.Description'
},
horde: {
name: 'DAGGERHEART.Adversary.Horde.Name',
id: 'horde',
label: 'DAGGERHEART.Adversary.Type.Horde.label',
description: 'DAGGERHEART.Adversary.Horde.Description'
},
leader: {
name: 'DAGGERHEART.Adversary.Leader.Name',
id: 'leader',
label: 'DAGGERHEART.Adversary.Type.Leader.label',
description: 'DAGGERHEART.Adversary.Leader.Description'
},
minion: {
name: 'DAGGERHEART.Adversary.Minion.Name',
id: 'minion',
label: 'DAGGERHEART.Adversary.Type.Minion.label',
description: 'DAGGERHEART.Adversary.Minion.Description'
},
ranged: {
name: 'DAGGERHEART.Adversary.Ranged.Name',
id: 'ranged',
label: 'DAGGERHEART.Adversary.Type.Ranged.label',
description: 'DAGGERHEART.Adversary.Ranged.Description'
},
skulker: {
name: 'DAGGERHEART.Adversary.Skulker.Name',
description: 'DAGGERHEART.Adversary.Skulker.Description'
skulk: {
id: 'skulk',
label: 'DAGGERHEART.Adversary.Type.Skulk.label',
description: 'DAGGERHEART.Adversary.Skulk.Description'
},
social: {
name: 'DAGGERHEART.Adversary.Social.Name',
id: 'social',
label: 'DAGGERHEART.Adversary.Type.Social.label',
description: 'DAGGERHEART.Adversary.Social.Description'
},
solo: {
name: 'DAGGERHEART.Adversary.Solo.Name',
id: 'solo',
label: 'DAGGERHEART.Adversary.Type.Solo.label',
description: 'DAGGERHEART.Adversary.Solo.Description'
},
standard: {
name: 'DAGGERHEART.Adversary.Standard.Name',
id: 'standard',
label: 'DAGGERHEART.Adversary.Type.Standard.label',
description: 'DAGGERHEART.Adversary.Standard.Description'
},
support: {
name: 'DAGGERHEART.Adversary.Support.Name',
id: 'support',
label: 'DAGGERHEART.Adversary.Type.Support.label',
description: 'DAGGERHEART.Adversary.Support.Description'
}
};
export const environmentTypes = {
exploration: {
label: 'DAGGERHEART.Environment.Type.Exploration.label',
description: 'DAGGERHEART.Environment.Type.Exploration.description'
},
social: {
label: 'DAGGERHEART.Environment.Type.Social.label',
description: 'DAGGERHEART.Environment.Type.Social.description'
},
traversal: {
label: 'DAGGERHEART.Environment.Type.Traversal.label',
description: 'DAGGERHEART.Environment.Type.Traversal.description'
},
event: {
label: 'DAGGERHEART.Environment.Type.Event.label',
description: 'DAGGERHEART.Environment.Type.Event.description'
}
};
export const adversaryTraits = {
relentless: {
name: 'DAGGERHEART.Adversary.Trait..Name',
@ -380,3 +409,9 @@ export const levelupData = {
}
}
};
export const subclassFeatureLabels = {
1: 'DAGGERHEART.Sheets.PC.DomainCard.FoundationTitle',
2: 'DAGGERHEART.Sheets.PC.DomainCard.SpecializationTitle',
3: 'DAGGERHEART.Sheets.PC.DomainCard.MasteryTitle'
};

View file

@ -1,56 +1,56 @@
export const domains = {
arcana: {
id: 'arcana',
label: 'Arcana',
label: 'DAGGERHEART.Domains.Arcana.label',
src: 'icons/magic/symbols/circled-gem-pink.webp',
description: 'DAGGERHEART.Domains.Arcana'
},
blade: {
id: 'blade',
label: 'Blade',
label: 'DAGGERHEART.Domains.Blade.label',
src: 'icons/weapons/swords/sword-broad-crystal-paired.webp',
description: 'DAGGERHEART.Domains.Blade'
},
bone: {
id: 'bone',
label: 'Bone',
label: 'DAGGERHEART.Domains.Bone.label',
src: 'icons/skills/wounds/bone-broken-marrow-red.webp',
description: 'DAGGERHEART.Domains.Bone'
},
codex: {
id: 'codex',
label: 'Codex',
label: 'DAGGERHEART.Domains.Codex.label',
src: 'icons/sundries/books/book-embossed-jewel-gold-purple.webp',
description: 'DAGGERHEART.Domains.Codex'
},
grace: {
id: 'grace',
label: 'Grace',
label: 'DAGGERHEART.Domains.Grace.label',
src: 'icons/skills/movement/feet-winged-boots-glowing-yellow.webp',
description: 'DAGGERHEART.Domains.Grace'
},
midnight: {
id: 'midnight',
label: 'Midnight',
label: 'DAGGERHEART.Domains.Midnight.label',
src: 'icons/environment/settlement/watchtower-castle-night.webp',
background: 'systems/daggerheart/assets/backgrounds/MidnightBackground.webp',
description: 'DAGGERHEART.Domains.Midnight'
},
sage: {
id: 'sage',
label: 'Sage',
label: 'DAGGERHEART.Domains.Sage.label',
src: 'icons/sundries/misc/pipe-wooden-straight-brown.webp',
description: 'DAGGERHEART.Domains.Sage'
},
splendor: {
id: 'splendor',
label: 'Splendor',
label: 'DAGGERHEART.Domains.Splendor.label',
src: 'icons/magic/control/control-influence-crown-gold.webp',
description: 'DAGGERHEART.Domains.Splendor'
},
valor: {
id: 'valor',
label: 'Valor',
label: 'DAGGERHEART.Domains.Valor.label',
src: 'icons/magic/control/control-influence-rally-purple.webp',
description: 'DAGGERHEART.Domains.Valor'
}

View file

@ -1,25 +1,35 @@
export const range = {
self: {
label: 'DAGGERHEART.Range.self.name',
description: 'DAGGERHEART.Range.self.description',
distance: 0
},
melee: {
id: 'melee',
label: 'DAGGERHEART.Range.melee.name',
description: 'DAGGERHEART.Range.melee.description',
distance: 1
},
veryClose: {
id: 'veryClose',
label: 'DAGGERHEART.Range.veryClose.name',
description: 'DAGGERHEART.Range.veryClose.description',
distance: 3
},
close: {
id: 'close',
label: 'DAGGERHEART.Range.close.name',
description: 'DAGGERHEART.Range.close.description',
distance: 10
},
far: {
id: 'far',
label: 'DAGGERHEART.Range.far.name',
description: 'DAGGERHEART.Range.far.description',
distance: 20
},
veryFar: {
id: 'veryFar',
label: 'DAGGERHEART.Range.veryFar.name',
description: 'DAGGERHEART.Range.veryFar.description',
distance: 30
@ -175,31 +185,27 @@ export const deathMoves = {
};
export const tiers = {
0: {
key: 0,
id: 'tier0',
name: 'DAGGERHEART.General.Tier.0'
},
1: {
key: 1,
tier1: {
id: 'tier1',
name: 'DAGGERHEART.General.Tier.1'
label: 'DAGGERHEART.Tiers.tier1'
},
2: {
key: 2,
tier2: {
id: 'tier2',
name: 'DAGGERHEART.General.Tier.2'
label: 'DAGGERHEART.Tiers.tier2'
},
3: {
key: 3,
tier3: {
id: 'tier3',
name: 'DAGGERHEART.General.Tier.3'
label: 'DAGGERHEART.Tiers.tier3'
},
tier4: {
id: 'tier4',
label: 'DAGGERHEART.Tiers.tier4'
}
};
export const objectTypes = {
pc: {
name: 'TYPES.Actor.pc'
character: {
name: 'TYPES.Actor.character'
},
npc: {
name: 'TYPES.Actor.npc'
@ -247,6 +253,11 @@ export const diceTypes = {
d20: 'd20'
};
export const multiplierTypes = {
proficiency: 'Proficiency',
spellcast: 'Spellcast'
};
export const getDiceSoNicePresets = () => {
const { diceSoNice } = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance);
@ -311,3 +322,18 @@ export const abilityCosts = {
label: 'Stress'
}
};
export const rollTypes = {
weapon: {
id: 'weapon',
label: 'DAGGERHEART.RollTypes.weapon.name'
},
spellcast: {
id: 'spellcast',
label: 'DAGGERHEART.RollTypes.spellcast.name'
},
ability: {
id: 'ability',
label: 'DAGGERHEART.RollTypes.ability.name'
}
};

View file

@ -0,0 +1,17 @@
import { pseudoDocuments } from "../data/_module.mjs";
import { pseudoDocumentSheet } from "../applications/_module.mjs";
//CONFIG.daggerheart.pseudoDocuments
export default {
sheetClass: pseudoDocumentSheet.PseudoDocumentSheet,
feature: {
label: "DAGGERHEART.Feature.Label",
documentClass: pseudoDocuments.feature.BaseFeatureData,
types: {
weapon: {
label: "DAGGERHEART.Feature.Weapon.Label",
documentClass: pseudoDocuments.feature.WeaponFeature,
}
}
}
};

View file

@ -5,6 +5,7 @@ import * as ITEM from './itemConfig.mjs';
import * as SETTINGS from './settingsConfig.mjs';
import * as EFFECTS from './effectConfig.mjs';
import * as ACTIONS from './actionConfig.mjs';
import pseudoDocuments from "./pseudoConfig.mjs";
export const SYSTEM_ID = 'daggerheart';
@ -16,5 +17,6 @@ export const SYSTEM = {
ITEM,
SETTINGS,
EFFECTS,
ACTIONS
ACTIONS,
pseudoDocuments
};

View file

@ -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';

View file

@ -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');
};
}

View 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
};

View 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);
}
}
}

View 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
})
};
}
}

View 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
};

View 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 */
};
}
}

View 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;
}
}

View 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;
}
}

View 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 */
};
}
}

View file

@ -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');
}
}

View file

@ -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);
}
}

View file

@ -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()
};
}
}

View file

@ -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;
}
}

View 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,
};

View file

@ -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;

View 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;
});
}
}

View file

@ -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);
}
}

View file

@ -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;
});
}
}

View file

@ -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);
}
}

View file

@ -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()
};
}
}

View file

@ -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 })
};
}
}

View file

@ -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))
};
}
}

View file

@ -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');
}
}

View file

@ -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 });
}
}
}
}

View 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';

View 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);
}
}

View 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;
}
}

View 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})`;
}
}

View 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;
}
}

View file

@ -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;
}
}
}

View file

@ -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;

View 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
};

View 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
};
}
}

View 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
View 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;
}
}

View 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();
}
}
}

View 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
};
}
}

View 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 })
};
}
}

View 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;
}
}
}
}

View 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;
}
}
}

View 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(),
};
}
}

View 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 });
}
}
}

View 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())
};
}
}

View file

@ -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 })
})
)

View file

@ -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 })
};
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,2 @@
export { default as base } from './base/pseudoDocument.mjs';
export * as feature from './feature/_module.mjs';

View 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);
}
}

View 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 });
}
}

View 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;
}

View file

@ -0,0 +1,2 @@
export { default as BaseFeatureData } from './baseFeatureData.mjs';
export { default as WeaponFeature } from './weaponFeature.mjs';

View 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' })
});
}
}

View file

@ -0,0 +1,6 @@
import BaseFeatureData from './baseFeatureData.mjs';
export default class WeaponFeature extends BaseFeatureData {
/**@override */
static TYPE = 'weapon';
}

View file

@ -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 })
};
}

View file

@ -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);
}
}

View file

@ -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;
}
}
}
}
}

View file

@ -3,6 +3,7 @@ import NpcRollSelectionDialog from '../applications/npcRollSelectionDialog.mjs';
import RollSelectionDialog from '../applications/rollSelectionDialog.mjs';
import { GMUpdateEvent, socketEvent } from '../helpers/socket.mjs';
import { setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs';
import DHDualityRoll from '../data/chat-message/dualityRoll.mjs';
export default class DhpActor extends Actor {
async _preCreate(data, options, user) {
@ -10,7 +11,7 @@ export default class DhpActor extends Actor {
// Configure prototype token settings
const prototypeToken = {};
if (this.type === 'pc')
if (this.type === 'character')
Object.assign(prototypeToken, {
sight: { enabled: true },
actorLink: true,
@ -28,7 +29,7 @@ export default class DhpActor extends Actor {
}
async updateLevel(newLevel) {
if (this.type !== 'pc' || newLevel === this.system.levelData.level.changed) return;
if (this.type !== 'character' || newLevel === this.system.levelData.level.changed) return;
if (newLevel > this.system.levelData.level.current) {
await this.update({ 'system.levelData.level.changed': newLevel });
@ -39,17 +40,65 @@ export default class DhpActor extends Actor {
return acc;
}, {});
const domainCards = Object.keys(this.system.levelData.levelups)
const domainCards = [];
const experiences = [];
const subclassFeatureState = { class: null, multiclass: null };
let multiclass = null;
Object.keys(this.system.levelData.levelups)
.filter(x => x > newLevel)
.flatMap(levelKey => {
.forEach(levelKey => {
const level = this.system.levelData.levelups[levelKey];
const achievementCards = level.achievements.domainCards.map(x => x.itemUuid);
const advancementCards = level.selections.filter(x => x.type === 'domainCard').map(x => x.itemUuid);
return [...achievementCards, ...advancementCards];
domainCards.push(...achievementCards, ...advancementCards);
experiences.push(...Object.keys(level.achievements.experiences));
const subclass = level.selections.find(x => x.type === 'subclass');
if (subclass) {
const path = subclass.secondaryData.isMulticlass === 'true' ? 'multiclass' : 'class';
const subclassState = Number(subclass.secondaryData.featureState) - 1;
subclassFeatureState[path] = subclassFeatureState[path]
? Math.min(subclassState, subclassFeatureState[path])
: subclassState;
}
multiclass = level.selections.find(x => x.type === 'multiclass');
});
for (var domainCard of domainCards) {
const itemCard = await this.items.find(x => x.uuid === domainCard);
if (experiences.length > 0) {
this.update({
'system.experiences': experiences.reduce((acc, key) => {
acc[`-=${key}`] = null;
return acc;
}, {})
});
}
if (subclassFeatureState.class) {
this.system.class.subclass.update({ 'system.featureState': subclassFeatureState.class });
}
if (subclassFeatureState.multiclass) {
this.system.multiclass.subclass.update({ 'system.featureState': subclassFeatureState.multiclass });
}
if (multiclass) {
const multiclassSubclass = this.items.find(x => x.type === 'subclass' && x.system.isMulticlass);
const multiclassItem = this.items.find(x => x.uuid === multiclass.itemUuid);
multiclassSubclass.delete();
multiclassItem.delete();
this.update({
'system.multiclass': {
value: null,
subclass: null
}
});
}
for (let domainCard of domainCards) {
const itemCard = this.items.find(x => x.uuid === domainCard);
itemCard.delete();
}
@ -71,6 +120,94 @@ export default class DhpActor extends Actor {
const levelups = {};
for (var levelKey of Object.keys(levelupData)) {
const level = levelupData[levelKey];
for (var experienceKey in level.achievements.experiences) {
const experience = level.achievements.experiences[experienceKey];
await this.update({
[`system.experiences.${experienceKey}`]: {
description: experience.name,
value: experience.modifier
}
});
}
let multiclass = null;
const domainCards = [];
const subclassFeatureState = { class: null, multiclass: null };
const selections = [];
for (var optionKey of Object.keys(level.choices)) {
const selection = level.choices[optionKey];
for (var checkboxNr of Object.keys(selection)) {
const checkbox = selection[checkboxNr];
if (checkbox.type === 'multiclass') {
multiclass = {
...checkbox,
level: Number(levelKey),
optionKey: optionKey,
checkboxNr: Number(checkboxNr)
};
} else if (checkbox.type === 'domainCard') {
domainCards.push({
...checkbox,
level: Number(levelKey),
optionKey: optionKey,
checkboxNr: Number(checkboxNr)
});
} else {
if (checkbox.type === 'subclass') {
const path = checkbox.secondaryData.isMulticlass === 'true' ? 'multiclass' : 'class';
subclassFeatureState[path] = Math.max(
Number(checkbox.secondaryData.featureState),
subclassFeatureState[path]
);
}
selections.push({
...checkbox,
level: Number(levelKey),
optionKey: optionKey,
checkboxNr: Number(checkboxNr)
});
}
}
}
if (multiclass) {
const subclassItem = await foundry.utils.fromUuid(multiclass.secondaryData.subclass);
const subclassData = subclassItem.toObject();
const multiclassItem = await foundry.utils.fromUuid(multiclass.data[0]);
const multiclassData = multiclassItem.toObject();
const embeddedItem = await this.createEmbeddedDocuments('Item', [
{
...multiclassData,
system: {
...multiclassData.system,
domains: [multiclass.secondaryData.domain],
isMulticlass: true
}
}
]);
await this.createEmbeddedDocuments('Item', [
{
...subclassData,
system: {
...subclassData.system,
isMulticlass: true
}
}
]);
selections.push({ ...multiclass, itemUuid: embeddedItem[0].uuid });
}
for (var domainCard of domainCards) {
const item = await foundry.utils.fromUuid(domainCard.data[0]);
const embeddedItem = await this.createEmbeddedDocuments('Item', [item.toObject()]);
selections.push({ ...domainCard, itemUuid: embeddedItem[0].uuid });
}
const achievementDomainCards = [];
for (var card of Object.values(level.achievements.domainCards)) {
const item = await foundry.utils.fromUuid(card.uuid);
@ -79,27 +216,14 @@ export default class DhpActor extends Actor {
achievementDomainCards.push(card);
}
const selections = [];
for (var optionKey of Object.keys(level.choices)) {
const selection = level.choices[optionKey];
for (var checkboxNr of Object.keys(selection)) {
const checkbox = selection[checkboxNr];
let itemUuid = null;
if (subclassFeatureState.class) {
await this.system.class.subclass.update({ 'system.featureState': subclassFeatureState.class });
}
if (checkbox.type === 'domainCard') {
const item = await foundry.utils.fromUuid(checkbox.data[0]);
const embeddedItem = await this.createEmbeddedDocuments('Item', [item.toObject()]);
itemUuid = embeddedItem[0].uuid;
}
selections.push({
...checkbox,
level: Number(levelKey),
optionKey: optionKey,
checkboxNr: Number(checkboxNr),
itemUuid
});
}
if (subclassFeatureState.multiclass) {
await this.system.multiclass.subclass.update({
'system.featureState': subclassFeatureState.multiclass
});
}
levelups[levelKey] = {
@ -123,154 +247,176 @@ export default class DhpActor extends Actor {
});
}
async diceRoll(modifier, shiftKey) {
if (this.type === 'pc') {
return await this.dualityRoll(modifier, shiftKey);
} else {
return await this.npcRoll(modifier, shiftKey);
}
}
async npcRoll(modifier, shiftKey) {
let advantage = null;
const modifiers = [
{
value: Number.parseInt(modifier.value),
label: modifier.value >= 0 ? `+${modifier.value}` : `-${modifier.value}`,
title: modifier.title
}
];
if (!shiftKey) {
const dialogClosed = new Promise((resolve, _) => {
new NpcRollSelectionDialog(this.system.experiences, resolve).render(true);
});
const result = await dialogClosed;
advantage = result.advantage;
result.experiences.forEach(x =>
modifiers.push({
value: x.value,
label: x.value >= 0 ? `+${x.value}` : `-${x.value}`,
title: x.description
})
);
}
const roll = Roll.create(
`${advantage === true || advantage === false ? 2 : 1}d20${advantage === true ? 'kh' : advantage === false ? 'kl' : ''} ${modifiers.map(x => `+ ${x.value}`).join(' ')}`
);
let rollResult = await roll.evaluate();
const dice = [];
for (var i = 0; i < rollResult.terms.length; i++) {
const term = rollResult.terms[i];
if (term.faces) {
dice.push({ type: `d${term.faces}`, rolls: term.results.map(x => ({ value: x.result })) });
}
}
// There is Only ever one dice term here
return { roll, dice: dice[0], modifiers, advantageState: advantage === true ? 1 : advantage === false ? 2 : 0 };
}
async dualityRoll(modifier, shiftKey, bonusDamage = []) {
/**
* @param {object} config
* @param {Event} config.event
* @param {string} config.title
* @param {object} config.roll
* @param {number} config.roll.modifier
* @param {boolean} [config.roll.simple=false]
* @param {string} [config.roll.type]
* @param {number} [config.roll.difficulty]
* @param {any} [config.damage]
* @param {object} [config.chatMessage]
* @param {string} config.chatMessage.template
* @param {boolean} [config.chatMessage.mute]
* @param {boolean} [config.checkTarget]
*/
async diceRoll(config) {
let hopeDice = 'd12',
fearDice = 'd12',
advantageDice = null,
disadvantageDice = null,
bonusDamageString = '';
advantageDice = 'd6',
disadvantageDice = 'd6',
advantage = config.event.altKey ? true : config.event.ctrlKey ? false : null,
targets,
damage = config.damage,
modifiers = this.formatRollModifier(config.roll),
rollConfig,
formula,
hope,
fear;
const modifiers =
modifier.value !== null
? [
{
value: modifier.value ? Number.parseInt(modifier.value) : 0,
label:
modifier.value >= 0
? `${modifier.title} +${modifier.value}`
: `${modifier.title} ${modifier.value}`,
title: modifier.title
}
]
: [];
if (!shiftKey) {
if (!config.event.shiftKey && !config.event.altKey && !config.event.ctrlKey) {
const dialogClosed = new Promise((resolve, _) => {
new RollSelectionDialog(
this.system.experiences,
bonusDamage,
this.system.resources.hope.value,
resolve
).render(true);
this.type === 'character'
? new RollSelectionDialog(
this.system.experiences,
this.system.resources.hope.value,
resolve
).render(true)
: new NpcRollSelectionDialog(this.system.experiences, resolve).render(true);
});
const result = await dialogClosed;
(hopeDice = result.hope),
(fearDice = result.fear),
(advantageDice = result.advantage),
(disadvantageDice = result.disadvantage);
result.experiences.forEach(x =>
rollConfig = await dialogClosed;
advantage = rollConfig.advantage;
hopeDice = rollConfig.hope;
fearDice = rollConfig.fear;
rollConfig.experiences.forEach(x =>
modifiers.push({
value: x.value,
label: x.value >= 0 ? `+${x.value}` : `-${x.value}`,
title: x.description
})
);
bonusDamageString = result.bonusDamage;
const automateHope = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation.Hope);
if (this.type === 'character') {
const automateHope = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation.Hope);
if (automateHope && result.hopeUsed) {
await this.update({
'system.resources.hope.value': this.system.resources.hope.value - result.hopeUsed
});
if (automateHope && result.hopeUsed) {
await this.update({
'system.resources.hope.value': this.system.resources.hope.value - result.hopeUsed
});
}
}
}
const roll = new Roll(
`1${hopeDice} + 1${fearDice}${advantageDice ? ` + 1${advantageDice}` : disadvantageDice ? ` - 1${disadvantageDice}` : ''} ${modifiers.map(x => `+ ${x.value}`).join(' ')}`
);
let rollResult = await roll.evaluate();
setDiceSoNiceForDualityRoll(rollResult, advantageDice, disadvantageDice);
const hope = rollResult.dice[0].results[0].result;
const fear = rollResult.dice[1].results[0].result;
const advantage = advantageDice ? rollResult.dice[2].results[0].result : null;
const disadvantage = disadvantageDice ? rollResult.dice[2].results[0].result : null;
if (disadvantage) {
rollResult = { ...rollResult, total: rollResult.total - Math.max(hope, disadvantage) };
}
if (advantage) {
rollResult = { ...rollResult, total: 'Select Hope Die' };
if (this.type === 'character') {
formula = `1${hopeDice} + 1${fearDice}${advantage === true ? ` + 1d6` : advantage === false ? ` - 1d6` : ''}`;
} else {
formula = `${advantage === true || advantage === false ? 2 : 1}d20${advantage === true ? 'kh' : advantage === false ? 'kl' : ''}`;
}
formula += ` ${modifiers.map(x => `+ ${x.value}`).join(' ')}`;
const roll = await Roll.create(formula).evaluate();
const dice = roll.dice.flatMap(dice => ({
denomination: dice.denomination,
number: dice.number,
total: dice.total,
results: dice.results.map(result => ({ result: result.result, discarded: !result.active }))
}));
const automateHope = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation.Hope);
if (automateHope && hope > fear) {
await this.update({
'system.resources.hope.value': Math.min(
this.system.resources.hope.value + 1,
this.system.resources.hope.max
)
});
}
if (automateHope && hope === fear) {
await this.update({
'system.resources': {
'hope.value': Math.min(this.system.resources.hope.value + 1, this.system.resources.hope.max),
'stress.value': Math.max(this.system.resources.stress.value - 1, 0)
if (this.type === 'character') {
setDiceSoNiceForDualityRoll(roll, advantage);
hope = roll.dice[0].results[0].result;
fear = roll.dice[1].results[0].result;
if (
game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation.Hope) &&
config.roll.type === 'action'
) {
if (hope > fear) {
await this.update({
'system.resources.hope.value': Math.min(
this.system.resources.hope.value + 1,
this.system.resources.hope.max
)
});
} else if (hope === fear) {
await this.update({
'system.resources': {
'hope.value': Math.min(
this.system.resources.hope.value + 1,
this.system.resources.hope.max
),
'stress.value': Math.max(this.system.resources.stress.value - 1, 0)
}
});
}
}
}
if (config.checkTarget) {
targets = Array.from(game.user.targets).map(x => {
const target = {
id: x.id,
name: x.actor.name,
img: x.actor.img,
difficulty: x.actor.system.difficulty,
evasion: x.actor.system.evasion?.value
};
target.hit = target.difficulty ? roll.total >= target.difficulty : roll.total >= target.evasion;
return target;
});
}
return {
roll,
rollResult,
hope: { dice: hopeDice, value: hope },
fear: { dice: fearDice, value: fear },
advantage: { dice: advantageDice, value: advantage },
disadvantage: { dice: disadvantageDice, value: disadvantage },
modifiers: modifiers,
bonusDamageString
};
if (config.chatMessage) {
const configRoll = {
title: config.title,
origin: this.id,
dice,
roll,
modifiers: modifiers.filter(x => x.label),
advantageState: advantage
};
if (this.type === 'character') {
configRoll.hope = { dice: hopeDice, value: hope };
configRoll.fear = { dice: fearDice, value: fear };
configRoll.advantage = { dice: advantageDice, value: roll.dice[2]?.results[0].result ?? null };
}
if (damage) configRoll.damage = damage;
if (targets) configRoll.targets = targets;
const systemData =
this.type === 'character' && !config.roll.simple ? new DHDualityRoll(configRoll) : configRoll,
cls = getDocumentClass('ChatMessage'),
msg = new cls({
type: config.chatMessage.type ?? 'dualityRoll',
sound: config.chatMessage.mute ? null : CONFIG.sounds.dice,
system: systemData,
content: config.chatMessage.template,
rolls: [roll]
});
await cls.create(msg.toObject());
}
return roll;
}
formatRollModifier(roll) {
const modifier = roll.modifier !== null ? Number.parseInt(roll.modifier) : null;
return modifier !== null
? [
{
value: modifier,
label: roll.label
? modifier >= 0
? `${roll.label} +${modifier}`
: `${roll.label} ${modifier}`
: null,
title: roll.label
}
]
: [];
}
async damageRoll(title, damage, targets, shiftKey) {
@ -300,7 +446,11 @@ export default class DhpActor extends Actor {
for (var i = 0; i < rollResult.terms.length; i++) {
const term = rollResult.terms[i];
if (term.faces) {
dice.push({ type: `d${term.faces}`, rolls: term.results.map(x => x.result) });
dice.push({
type: `d${term.faces}`,
rolls: term.results.map(x => x.result),
total: term.results.reduce((acc, x) => acc + x.result, 0)
});
} else if (term.operator) {
} else if (term.number) {
const operator = i === 0 ? '' : rollResult.terms[i - 1].operator;
@ -401,11 +551,6 @@ export default class DhpActor extends Actor {
}
}
async emulateItemDrop(data) {
const event = new DragEvent('drop', { altKey: game.keyboard.isModifierActive('Alt') });
return this.sheet._onDropItem(event, { data: data });
}
//Move to action-scope?
async useAction(action) {
const userTargets = Array.from(game.user.targets);

View file

@ -1,25 +1,44 @@
export default class DhpItem extends Item {
_preCreate(data, changes, user) {
super._preCreate(data, changes, user);
/** @inheritdoc */
getEmbeddedDocument(embeddedName, id, { invalid = false, strict = false } = {}) {
const systemEmbeds = this.system.constructor.metadata.embedded ?? {};
if (embeddedName in systemEmbeds) {
const path = `system.${systemEmbeds[embeddedName]}`;
return foundry.utils.getProperty(this, path).get(id) ?? null;
}
return super.getEmbeddedDocument(embeddedName, id, { invalid, strict });
}
prepareData() {
super.prepareData();
/** @inheritDoc */
prepareEmbeddedDocuments() {
super.prepareEmbeddedDocuments();
for (const action of this.system.actions ?? []) action.prepareData();
}
if (this.type === 'class') {
// Bad. Make this better.
// this.system.domains = CONFIG.daggerheart.DOMAIN.classDomainMap[Object.keys(CONFIG.daggerheart.DOMAIN.classDomainMap).find(x => x === this.name.toLowerCase())];
/**
* @inheritdoc
* @param {object} options - Options which modify the getRollData method.
* @returns
*/
getRollData(options = {}) {
let data;
if (this.system.getRollData) data = this.system.getRollData(options);
else {
const actorRollData = this.actor?.getRollData(options) ?? {};
data = { ...actorRollData, item: { ...this.system } };
}
if (data?.item) {
data.item.flags = { ...this.flags };
data.item.name = this.name;
}
return data;
}
isInventoryItem() {
return ['weapon', 'armor', 'miscellaneous', 'consumable'].includes(this.type);
}
_onUpdate(data, options, userId) {
super._onUpdate(data, options, userId);
}
static async createDialog(data = {}, { parent = null, pack = null, ...options } = {}) {
const documentName = this.metadata.name;
const types = game.documentTypes[documentName].filter(t => t !== CONST.BASE_DOCUMENT_TYPE);
@ -79,4 +98,47 @@ export default class DhpItem extends Item {
options
});
}
async selectActionDialog() {
const content = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/views/actionSelect.hbs',
{ actions: this.system.actions }
),
title = 'Select Action',
type = 'div',
data = {};
return Dialog.prompt({
title,
// label: title,
content,
type,
callback: html => {
const form = html[0].querySelector('form'),
fd = new foundry.applications.ux.FormDataExtended(form);
return this.system.actions.find(a => a._id === fd.object.actionId);
},
rejectClose: false
});
}
async use(event) {
const actions = this.system.actions;
let response;
if (actions?.length) {
let action = actions[0];
if (actions.length > 1 && !event?.shiftKey) {
// Actions Choice Dialog
action = await this.selectActionDialog();
}
if (action) response = action.use(event);
// Check Target
// If action.roll => Roll Dialog
// Else If action.cost => Cost Dialog
// Then
// Apply Cost
// Apply Effect
}
// Display Item Card in chat
return response;
}
}

View file

@ -9,21 +9,26 @@ export function dualityRollEnricher(match, _options) {
}
export function getDualityMessage(roll) {
const attributeLabel =
roll.attribute && abilities[roll.attribute]
const traitLabel =
roll.trait && abilities[roll.trait]
? game.i18n.format('DAGGERHEART.General.Check', {
check: game.i18n.localize(abilities[roll.attribute].label)
check: game.i18n.localize(abilities[roll.trait].label)
})
: null;
const label = attributeLabel ?? game.i18n.localize('DAGGERHEART.General.Duality');
const label = traitLabel ?? game.i18n.localize('DAGGERHEART.General.Duality');
const dataLabel = traitLabel
? game.i18n.localize(abilities[roll.trait].label)
: game.i18n.localize('DAGGERHEART.General.Duality');
const dualityElement = document.createElement('span');
dualityElement.innerHTML = `
<button class="duality-roll-button"
data-label="${label}"
data-title="${label}"
data-label="${dataLabel}"
data-hope="${roll.hope ?? 'd12'}"
data-fear="${roll.fear ?? 'd12'}"
${roll.attribute && abilities[roll.attribute] ? `data-attribute="${roll.attribute}"` : ''}
${roll.trait && abilities[roll.trait] ? `data-trait="${roll.trait}"` : ''}
${roll.advantage ? 'data-advantage="true"' : ''}
${roll.disadvantage ? 'data-disadvantage="true"' : ''}
>

View file

@ -11,6 +11,7 @@ export default class RegisterHandlebarsHelpers {
includes: this.includes,
debug: this.debug,
signedNumber: this.signedNumber,
length: this.length,
switch: this.switch,
case: this.case
});
@ -82,6 +83,10 @@ export default class RegisterHandlebarsHelpers {
return number >= 0 ? `+${number}` : number;
}
static length(obj) {
return Object.keys(obj).length;
}
static switch(value, options) {
this.switch_value = value;
this.switch_break = false;

View file

@ -114,7 +114,7 @@ export const getCommandTarget = () => {
ui.notifications.error(game.i18n.localize('DAGGERHEART.Notification.Error.NoSelectedToken'));
return null;
}
if (target.type !== 'pc') {
if (target.type !== 'character') {
ui.notifications.error(game.i18n.localize('DAGGERHEART.Notification.Error.OnlyUseableByPC'));
return null;
}
@ -122,14 +122,16 @@ export const getCommandTarget = () => {
return target;
};
export const setDiceSoNiceForDualityRoll = (rollResult, advantage, disadvantage) => {
export const setDiceSoNiceForDualityRoll = (rollResult, advantageState) => {
const diceSoNicePresets = getDiceSoNicePresets();
rollResult.dice[0].options.appearance = diceSoNicePresets.hope;
rollResult.dice[1].options.appearance = diceSoNicePresets.fear;
if (advantage) {
rollResult.dice[2].options.appearance = diceSoNicePresets.advantage;
} else if (disadvantage) {
rollResult.dice[2].options.appearance = diceSoNicePresets.disadvantage;
if (rollResult.dice[2]) {
if (advantageState === true) {
rollResult.dice[2].options.appearance = diceSoNicePresets.advantage;
} else if (advantageState === false) {
rollResult.dice[2].options.appearance = diceSoNicePresets.disadvantage;
}
}
};
@ -172,17 +174,17 @@ export const tagifyElement = (element, options, onChange, tagifyOptions = {}) =>
templates: {
tag(tagData) {
return `<tag title="${tagData.title || tagData.value}"
contenteditable='false'
spellcheck='false'
tabIndex="${this.settings.a11y.focusableTags ? 0 : -1}"
class="${this.settings.classNames.tag} ${tagData.class ? tagData.class : ''}"
${this.getAttributes(tagData)}>
<x class="${this.settings.classNames.tagX}" role='button' aria-label='remove tag'></x>
<div>
<span class="${this.settings.classNames.tagText}">${tagData[this.settings.tagTextProp] || tagData.value}</span>
${tagData.src ? `<img src="${tagData.src}"></i>` : ''}
</div>
</tag>`;
contenteditable='false'
spellcheck='false'
tabIndex="${this.settings.a11y.focusableTags ? 0 : -1}"
class="${this.settings.classNames.tag} ${tagData.class ? tagData.class : ''}"
${this.getAttributes(tagData)}>
<x class="${this.settings.classNames.tagX}" role='button' aria-label='remove tag'></x>
<div>
<span class="${this.settings.classNames.tagText}">${tagData[this.settings.tagTextProp] || tagData.value}</span>
${tagData.src ? `<img src="${tagData.src}"></i>` : ''}
</div>
</tag>`;
}
}
});
@ -222,3 +224,14 @@ export const getDeleteKeys = (property, innerProperty, innerPropertyDefaultValue
return acc;
}, {});
};
// Fix on Foundry native formula replacement for DH
const nativeReplaceFormulaData = Roll.replaceFormulaData;
Roll.replaceFormulaData = function (formula, data, { missing, warn = false } = {}) {
const terms = [
{ term: 'prof', default: 1 },
{ term: 'cast', default: 1 }
];
formula = terms.reduce((a, c) => a.replaceAll(`@${c.term}`, data[c.term] ?? c.default), formula);
return nativeReplaceFormulaData(formula, data, { missing, warn });
};