Merged with main

This commit is contained in:
WBHarry 2025-06-20 13:06:16 +02:00
commit f80244a773
199 changed files with 8977 additions and 6490 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';
@ -12,3 +12,4 @@ export { default as DhpWeapon } from './sheets/items/weapon.mjs';
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';

View file

@ -0,0 +1,496 @@
import { abilities } from '../config/actorConfig.mjs';
import { burden } from '../config/generalConfig.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class DhCharacterCreation extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(character) {
super({});
this.character = character;
this.setup = {
traits: this.character.system.traits,
ancestry: this.character.system.ancestry ?? {},
community: this.character.system.community ?? {},
class: this.character.system.class?.value ?? {},
subclass: this.character.system.class?.subclass ?? {},
experiences: {
[foundry.utils.randomID()]: { description: '', value: 2 },
[foundry.utils.randomID()]: { description: '', value: 2 }
},
domainCards: {
[foundry.utils.randomID()]: {},
[foundry.utils.randomID()]: {}
},
visibility: 1
};
this.equipment = {
armor: {},
primaryWeapon: {},
secondaryWeapon: {},
inventory: {
take: {},
choiceA: {},
choiceB: {}
}
};
this._dragDrop = this._createDragDropHandlers();
}
get title() {
return game.i18n.format('DAGGERHEART.CharacterCreation.Title', { actor: this.character.name });
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'dialog', 'dh-style', 'character-creation'],
position: { width: 800, height: 'auto' },
actions: {
viewCompendium: this.viewCompendium,
viewItem: this.viewItem,
useSuggestedTraits: this.useSuggestedTraits,
equipmentChoice: this.equipmentChoice,
finish: this.finish
},
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false
},
dragDrop: [
{ dragSelector: null, dropSelector: '.ancestry-card' },
{ dragSelector: null, dropSelector: '.community-card' },
{ dragSelector: null, dropSelector: '.class-card' },
{ dragSelector: null, dropSelector: '.subclass-card' },
{ dragSelector: null, dropSelector: '.domain-card' },
{ dragSelector: null, dropSelector: '.armor-card' },
{ dragSelector: null, dropSelector: '.primary-weapon-card' },
{ dragSelector: null, dropSelector: '.secondary-weapon-card' },
{ dragSelector: '.suggestion-inner-container', dropSelector: '.selections-container' }
]
};
static PARTS = {
tabs: { template: 'systems/daggerheart/templates/views/characterCreation/tabs.hbs' },
setup: { template: 'systems/daggerheart/templates/views/characterCreation/tabs/setup.hbs' },
equipment: { template: 'systems/daggerheart/templates/views/characterCreation/tabs/equipment.hbs' },
// story: { template: 'systems/daggerheart/templates/views/characterCreation/tabs/story.hbs' },
footer: { template: 'systems/daggerheart/templates/views/characterCreation/footer.hbs' }
};
static TABS = {
setup: {
active: true,
cssClass: '',
group: 'primary',
id: 'setup',
label: 'DAGGERHEART.CharacterCreation.Tabs.Setup'
},
equipment: {
active: false,
cssClass: '',
group: 'primary',
id: 'equipment',
label: 'DAGGERHEART.CharacterCreation.Tabs.Equipment',
optional: true
}
// story: {
// active: false,
// cssClass: '',
// group: 'primary',
// id: 'story',
// label: 'DAGGERHEART.CharacterCreation.Tabs.Story',
// optional: true
// }
};
_getTabs(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' : '';
switch (v.id) {
case 'setup':
const classFinished = this.setup.class.uuid && this.setup.subclass.uuid;
const heritageFinished = this.setup.ancestry.uuid && this.setup.community.uuid;
const traitsFinished = Object.values(this.setup.traits).every(x => x.value !== null);
const experiencesFinished = Object.values(this.setup.experiences).every(x => x.description);
const domainCardsFinished = Object.values(this.setup.domainCards).every(x => x.uuid);
v.finished =
classFinished &&
heritageFinished &&
traitsFinished &&
experiencesFinished &&
domainCardsFinished;
break;
case 'equipment':
const armorFinished = this.equipment.armor?.uuid;
const primaryFinished = this.equipment.primaryWeapon?.uuid;
const secondaryFinished =
this.equipment.secondaryWeapon?.uuid ||
(primaryFinished && this.equipment.primaryWeapon.system.burden == burden.twoHanded.value);
const choiceAFinished = this.equipment.inventory.choiceA?.uuid;
const choiceBFinished = this.equipment.inventory.choiceB?.uuid;
v.finished =
armorFinished && primaryFinished && secondaryFinished && choiceAFinished && choiceBFinished;
}
}
tabs.equipment.cssClass = tabs.setup.finished ? tabs.equipment.cssClass : 'disabled';
// tabs.story.cssClass = tabs.setup.finished ? tabs.story.cssClass : 'disabled';
return tabs;
}
changeTab(tab, group, options) {
super.changeTab(tab, group, options);
for (var listTab of Object.keys(this.constructor.TABS)) {
const marker = options.navElement.querySelector(`a[data-action="tab"].${listTab} .finish-marker`);
if (listTab === tab) {
marker.classList.add('active');
} else {
marker.classList.remove('active');
}
}
}
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
this._dragDrop.forEach(d => d.bind(htmlElement));
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.tabs = this._getTabs(this.constructor.TABS);
return context;
}
async _preparePartContext(partId, context) {
switch (partId) {
case 'setup':
const availableTraitModifiers = game.settings
.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Homebrew)
.traitArray.map(trait => ({ key: trait, name: trait }));
for (let trait of Object.values(this.setup.traits).filter(x => x.value !== null)) {
const index = availableTraitModifiers.findIndex(x => x.key === trait.value);
if (index !== -1) {
availableTraitModifiers.splice(index, 1);
}
}
context.suggestedTraits = this.setup.class.system
? Object.keys(this.setup.class.system.characterGuide.suggestedTraits).map(traitKey => {
const trait = this.setup.class.system.characterGuide.suggestedTraits[traitKey];
return `${game.i18n.localize(`DAGGERHEART.Abilities.${traitKey}.short`)} ${trait > 0 ? `+${trait}` : trait}`;
})
: [];
context.traits = {
values: Object.keys(this.setup.traits).map(traitKey => {
const trait = this.setup.traits[traitKey];
const options = [...availableTraitModifiers];
if (trait.value !== null && !options.some(x => x.key === trait.value))
options.push({ key: trait.value, name: trait.value });
return {
...trait,
key: traitKey,
name: game.i18n.localize(abilities[traitKey].label),
options: options
};
})
};
context.traits.nrTotal = Object.keys(context.traits.values).length;
context.traits.nrSelected = Object.values(context.traits.values).reduce(
(acc, trait) => acc + (trait.value !== null ? 1 : 0),
0
);
context.experience = {
values: this.setup.experiences,
nrTotal: Object.keys(this.setup.experiences).length,
nrSelected: Object.values(this.setup.experiences).reduce(
(acc, exp) => acc + (exp.description ? 1 : 0),
0
)
};
context.ancestry = { ...this.setup.ancestry, compendium: 'ancestries' };
context.community = { ...this.setup.community, compendium: 'communities' };
context.class = { ...this.setup.class, compendium: 'classes' };
context.subclass = { ...this.setup.subclass, compendium: 'subclasses' };
context.domainCards = Object.keys(this.setup.domainCards).reduce((acc, x) => {
acc[x] = { ...this.setup.domainCards[x], compendium: 'domains' };
return acc;
}, {});
context.visibility = this.setup.visibility;
break;
case 'equipment':
const suggestions = await this.getEquipmentSuggestions(
this.equipment.inventory.choiceA,
this.equipment.inventory.choiceB
);
context.armor = {
...this.equipment.armor,
suggestion: { ...suggestions.armor, taken: suggestions.armor?.uuid === this.equipment.armor?.uuid },
compendium: 'armors'
};
context.primaryWeapon = {
...this.equipment.primaryWeapon,
suggestion: {
...suggestions.primaryWeapon,
taken: suggestions.primaryWeapon?.uuid === this.equipment.primaryWeapon?.uuid
},
compendium: 'weapons'
};
context.secondaryWeapon = {
...this.equipment.secondaryWeapon,
suggestion: {
...suggestions.secondaryWeapon,
taken: suggestions.secondaryWeapon?.uuid === this.equipment.secondaryWeapon?.uuid
},
disabled: this.equipment.primaryWeapon?.system?.burden === burden.twoHanded.value,
compendium: 'weapons'
};
context.inventory = {
take: suggestions.inventory.take,
choiceA: { suggestions: suggestions.inventory.choiceA, compendium: 'consumables' },
choiceB: { suggestions: suggestions.inventory.choiceB, compendium: 'general-items' }
};
break;
}
return context;
}
static async updateForm(event, _, formData) {
this.setup = foundry.utils.mergeObject(this.setup, formData.object);
this.setup.visibility = this.getUpdateVisibility();
this.render();
}
getUpdateVisibility() {
switch (this.setup.visibility) {
case 5:
return 5;
case 4:
return Object.values(this.setup.experiences).every(x => x.description) ? 5 : 4;
case 3:
return Object.values(this.setup.traits).every(x => x.value !== null) ? 4 : 3;
case 2:
return this.setup.ancestry.uuid && this.setup.community.uuid ? 3 : 2;
case 1:
return this.setup.class.uuid && this.setup.subclass.uuid ? 2 : 1;
}
}
async getEquipmentSuggestions(choiceA, choiceB) {
if (!this.setup.class.uuid) return { inventory: { take: [] } };
const { inventory, characterGuide } = this.setup.class.system;
return {
armor: characterGuide.suggestedArmor ?? null,
primaryWeapon: characterGuide.suggestedPrimaryWeapon ?? null,
secondaryWeapon:
{ ...characterGuide.suggestedSecondaryWeapon, uuid: characterGuide.suggestedSecondaryWeapon.uuid } ??
null,
inventory: {
take: inventory.take ?? [],
choiceA:
inventory.choiceA?.map(x => ({ ...x, uuid: x.uuid, selected: x.uuid === choiceA?.uuid })) ?? [],
choiceB: inventory.choiceB?.map(x => ({ ...x, uuid: x.uuid, selected: x.uuid === choiceB?.uuid })) ?? []
}
};
}
_createDragDropHandlers() {
return this.options.dragDrop.map(d => {
d.callbacks = {
dragstart: this._onDragStart.bind(this),
drop: this._onDrop.bind(this)
};
return new foundry.applications.ux.DragDrop.implementation(d);
});
}
static async viewCompendium(_, target) {
(await game.packs.get(`daggerheart.${target.dataset.compendium}`))?.render(true);
}
static async viewItem(_, target) {
(await foundry.utils.fromUuid(target.dataset.uuid)).sheet.render(true);
}
static useSuggestedTraits() {
this.setup.traits = Object.keys(this.setup.traits).reduce((acc, traitKey) => {
acc[traitKey] = {
...this.setup.traits[traitKey],
value: this.setup.class.system.characterGuide.suggestedTraits[traitKey]
};
return acc;
}, {});
this.setup.visibility = this.getUpdateVisibility();
this.render();
}
static async equipmentChoice(_, target) {
this.equipment.inventory[target.dataset.path] = await foundry.utils.fromUuid(target.dataset.uuid);
this.render();
}
static async finish() {
const embeddedAncestries = await this.character.createEmbeddedDocuments('Item', [this.setup.ancestry]);
const embeddedCommunities = await this.character.createEmbeddedDocuments('Item', [this.setup.community]);
await this.character.createEmbeddedDocuments('Item', [this.setup.class]);
await this.character.createEmbeddedDocuments('Item', [this.setup.subclass]);
await this.character.createEmbeddedDocuments('Item', Object.values(this.setup.domainCards));
if (this.equipment.armor.uuid)
await this.character.createEmbeddedDocuments('Item', [
{ ...this.equipment.armor, system: { ...this.equipment.armor.system, equipped: true } }
]);
if (this.equipment.primaryWeapon.uuid)
await this.character.createEmbeddedDocuments('Item', [
{ ...this.equipment.primaryWeapon, system: { ...this.equipment.primaryWeapon.system, equipped: true } }
]);
if (this.equipment.secondaryWeapon.uuid)
await this.character.createEmbeddedDocuments('Item', [
{
...this.equipment.secondaryWeapon,
system: { ...this.equipment.secondaryWeapon.system, equipped: true }
}
]);
if (this.equipment.inventory.choiceA.uuid)
await this.character.createEmbeddedDocuments('Item', [this.equipment.inventory.choiceA]);
if (this.equipment.inventory.choiceB.uuid)
await this.character.createEmbeddedDocuments('Item', [this.equipment.inventory.choiceB]);
await this.character.createEmbeddedDocuments('Item', this.setup.class.system.inventory.take);
await this.character.update({
system: {
traits: this.setup.traits,
experiences: this.setup.experiences,
ancestry: embeddedAncestries[0].uuid,
community: embeddedCommunities[0].uuid
}
});
this.close();
}
async _onDragStart(event) {
const target = event.currentTarget;
event.dataTransfer.setData('text/plain', JSON.stringify(target.dataset));
event.dataTransfer.setDragImage(target, 60, 0);
}
async _onDrop(event) {
const data = TextEditor.getDragEventData(event);
const item = await foundry.utils.fromUuid(data.uuid);
if (item.type === 'ancestry' && event.target.closest('.ancestry-card')) {
this.setup.ancestry = { ...item, uuid: item.uuid };
} else if (item.type === 'community' && event.target.closest('.community-card')) {
this.setup.community = { ...item, uuid: item.uuid };
} else if (item.type === 'class' && event.target.closest('.class-card')) {
this.setup.class = { ...item, uuid: item.uuid };
this.setup.subclass = {};
this.setup.domainCards = {
[foundry.utils.randomID()]: {},
[foundry.utils.randomID()]: {}
};
} else if (item.type === 'subclass' && event.target.closest('.subclass-card')) {
if (this.setup.class.system.subclasses.every(subclass => subclass.uuid !== item.uuid)) {
ui.notifications.error(
game.i18n.localize('DAGGERHEART.CharacterCreation.Notifications.SubclassNotInClass')
);
return;
}
this.setup.subclass = { ...item, uuid: item.uuid };
} else if (item.type === 'domainCard' && event.target.closest('.domain-card')) {
if (!this.setup.class.uuid) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.CharacterCreation.Notifications.MissingClass'));
return;
}
if (!this.setup.class.system.domains.includes(item.system.domain)) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.CharacterCreation.Notifications.WrongDomain'));
return;
}
if (item.system.level > 1) {
ui.notifications.error(
game.i18n.localize('DAGGERHEART.CharacterCreation.Notifications.CardTooHighLevel')
);
return;
}
if (Object.values(this.setup.domainCards).some(card => card.uuid === item.uuid)) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.CharacterCreation.Notifications.DuplicateCard'));
return;
}
this.setup.domainCards[event.target.closest('.domain-card').dataset.card] = { ...item, uuid: item.uuid };
} else if (item.type === 'armor' && event.target.closest('.armor-card')) {
if (item.system.tier > 1) {
ui.notifications.error(
game.i18n.localize('DAGGERHEART.CharacterCreation.Notifications.ItemTooHighTier')
);
return;
}
this.equipment.armor = { ...item, uuid: item.uuid };
} else if (item.type === 'weapon' && event.target.closest('.primary-weapon-card')) {
if (item.system.secondary) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.CharacterCreation.Notifications.NotPrimary'));
return;
}
if (item.system.tier > 1) {
ui.notifications.error(
game.i18n.localize('DAGGERHEART.CharacterCreation.Notifications.ItemTooHighTier')
);
return;
}
this.equipment.primaryWeapon = { ...item, uuid: item.uuid };
} else if (item.type === 'weapon' && event.target.closest('.secondary-weapon-card')) {
if (this.equipment.primaryWeapon?.system?.burden === burden.twoHanded.value) {
ui.notifications.error(
game.i18n.localize('DAGGERHEART.CharacterCreation.Notifications.PrimaryIsTwoHanded')
);
return;
}
if (!item.system.secondary) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.CharacterCreation.Notifications.NotSecondary'));
return;
}
if (item.system.tier > 1) {
ui.notifications.error(
game.i18n.localize('DAGGERHEART.CharacterCreation.Notifications.ItemTooHighTier')
);
return;
}
this.equipment.secondaryWeapon = { ...item, uuid: item.uuid };
} else {
return;
}
this.setup.visibility = this.getUpdateVisibility();
this.render();
}
}

View file

@ -1,24 +1,20 @@
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);
}
/* We can change to fully implementing the renderHTML function if needed, instead of augmenting it. */
const html = await super.renderHTML();
if (
this.type === 'dualityRoll' &&
game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance).dualityColorScheme ===
DualityRollColor.colorful.value
) {
if (this.type === 'dualityRoll') {
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 = foundry.utils.getProperty(this.action.parent, this.action.systemPath).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.${this.action.systemPath}`]: newActions });
if (!updates) return;
this.action = foundry.utils.getProperty(updates.system, this.action.systemPath)[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

@ -50,7 +50,7 @@ export default class Resources extends HandlebarsApplicationMixin(ApplicationV2)
}
get maxFear() {
return game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.MaxFear);
return game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Homebrew).maxFear;
}
/* -------------------------------------------- */
@ -59,7 +59,7 @@ export default class Resources extends HandlebarsApplicationMixin(ApplicationV2)
/** @override */
async _prepareContext(_options) {
const display = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.DisplayFear),
const display = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance).displayFear,
current = this.currentFear,
max = this.maxFear,
percent = (current / max) * 100,

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

@ -1,181 +1,122 @@
import { DualityRollColor } from '../config/settingsConfig.mjs';
import { defaultLevelTiers, DhLevelTiers } from '../data/levelTier.mjs';
import DhAppearance from '../data/settings/Appearance.mjs';
import DHAppearanceSettings from './settings/appearanceSettings.mjs';
import DhVariantRules from '../data/settings/VariantRules.mjs';
import DHVariantRuleSettings from './settings/variantRuleSettings.mjs';
import DhCountdowns from '../data/countdowns.mjs';
class DhpAutomationSettings extends FormApplication {
constructor(object = {}, options = {}) {
super(object, options);
}
static get defaultOptions() {
const defaults = super.defaultOptions;
const overrides = {
height: 'auto',
width: 400,
id: 'daggerheart-automation-settings',
template: 'systems/daggerheart/templates/views/automation-settings.hbs',
closeOnSubmit: true,
submitOnChange: false,
classes: ['daggerheart', 'views', 'settings']
};
const mergedOptions = foundry.utils.mergeObject(defaults, overrides);
return mergedOptions;
}
async getData() {
const context = super.getData();
context.settings = SYSTEM.SETTINGS.gameSettings.Automation;
context.hope = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation.Hope);
context.actionPoints = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation.ActionPoints);
return context;
}
activateListeners(html) {
super.activateListeners(html);
}
async _updateObject(_, formData) {
const data = foundry.utils.expandObject(formData);
const updateSettingsKeys = Object.keys(data);
for (var i = 0; i < updateSettingsKeys.length; i++) {
await game.settings.set(SYSTEM.id, updateSettingsKeys[i], data[updateSettingsKeys[i]]);
}
}
}
class DhpHomebrewSettings extends FormApplication {
constructor(object = {}, options = {}) {
super(object, options);
}
static get defaultOptions() {
const defaults = super.defaultOptions;
const overrides = {
height: 'auto',
width: 400,
id: 'daggerheart-homebrew-settings',
template: 'systems/daggerheart/templates/views/homebrew-settings.hbs',
closeOnSubmit: true,
submitOnChange: false,
classes: ['daggerheart', 'views', 'settings']
};
const mergedOptions = foundry.utils.mergeObject(defaults, overrides);
return mergedOptions;
}
async getData() {
const context = super.getData();
context.settings = SYSTEM.SETTINGS.gameSettings.General;
context.abilityArray = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.General.AbilityArray);
return context;
}
activateListeners(html) {
super.activateListeners(html);
}
async _updateObject(_, formData) {
const data = foundry.utils.expandObject(formData);
const updateSettingsKeys = Object.keys(data);
for (var i = 0; i < updateSettingsKeys.length; i++) {
await game.settings.set(SYSTEM.id, updateSettingsKeys[i], data[updateSettingsKeys[i]]);
}
}
}
class DhpRangeSettings extends FormApplication {
constructor(object = {}, options = {}) {
super(object, options);
this.range = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.General.RangeMeasurement);
}
static get defaultOptions() {
const defaults = super.defaultOptions;
const overrides = {
height: 'auto',
width: 400,
id: 'daggerheart-range-settings',
template: 'systems/daggerheart/templates/views/range-settings.hbs',
closeOnSubmit: false,
submitOnChange: true,
classes: ['daggerheart', 'views', 'settings']
};
const mergedOptions = foundry.utils.mergeObject(defaults, overrides);
return mergedOptions;
}
async getData() {
const context = super.getData();
context.settings = SYSTEM.SETTINGS.gameSettings.General;
context.range = this.range;
context.disabled =
context.range.enabled &&
[
context.range.melee,
context.range.veryClose,
context.range.close,
context.range.far,
context.range.veryFar
].some(x => x === null || x === false);
return context;
}
activateListeners(html) {
super.activateListeners(html);
html.find('.range-reset').click(this.reset.bind(this));
html.find('.save').click(this.save.bind(this));
html.find('.close').click(this.close.bind(this));
}
async _updateObject(_, formData) {
const data = foundry.utils.expandObject(formData, { disabled: true });
this.range = foundry.utils.mergeObject(this.range, data);
this.render(true);
}
reset() {
this.range = {
enabled: false,
melee: 5,
veryClose: 15,
close: 30,
far: 60,
veryFar: 120
};
this.render(true);
}
async save() {
await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.General.RangeMeasurement, this.range);
this.close();
}
}
import {
DhAppearance,
DhAutomation,
DhHomebrew,
DhRangeMeasurement,
DhVariantRules
} from '../data/settings/_module.mjs';
import {
DhAppearanceSettings,
DhAutomationSettings,
DhHomebrewSettings,
DhRangeMeasurementSettings,
DhVariantRuleSettings
} from './settings/_module.mjs';
export const registerDHSettings = () => {
// const debouncedReload = foundry.utils.debounce(() => window.location.reload(), 100);
registerMenuSettings();
registerMenus();
registerNonConfigSettings();
};
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'),
const registerMenuSettings = () => {
game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.variantRules, {
scope: 'world',
config: false,
type: String,
default: '[2,1,1,0,0,-1]'
type: DhVariantRules
});
game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation, {
scope: 'world',
config: false,
type: DhAutomation
});
game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Homebrew, {
scope: 'world',
config: false,
type: DhHomebrew,
onChange: value => {
if (value.maxFear) {
if (ui.resources) ui.resources.render({ force: true });
}
}
});
game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance, {
scope: 'client',
config: false,
type: DhAppearance,
onChange: value => {
if (value.displayFear) {
if (ui.resources) {
if (value.displayFear === 'hide') ui.resources.close({ allowed: true });
else ui.resources.render({ force: true });
}
}
}
});
game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.RangeMeasurement, {
scope: 'client',
config: false,
type: DhRangeMeasurement
});
};
const registerMenus = () => {
game.settings.registerMenu(SYSTEM.id, SYSTEM.SETTINGS.menu.Automation.Name, {
name: game.i18n.localize('DAGGERHEART.Settings.Menu.Automation.Name'),
label: game.i18n.localize('DAGGERHEART.Settings.Menu.Automation.Label'),
hint: game.i18n.localize('DAGGERHEART.Settings.Menu.Automation.Hint'),
icon: SYSTEM.SETTINGS.menu.Automation.Icon,
type: DhAutomationSettings,
restricted: true
});
game.settings.registerMenu(SYSTEM.id, SYSTEM.SETTINGS.menu.Homebrew.Name, {
name: game.i18n.localize('DAGGERHEART.Settings.Menu.Homebrew.Name'),
label: game.i18n.localize('DAGGERHEART.Settings.Menu.Homebrew.Label'),
hint: game.i18n.localize('DAGGERHEART.Settings.Menu.Homebrew.Hint'),
icon: SYSTEM.SETTINGS.menu.Homebrew.Icon,
type: DhHomebrewSettings,
restricted: true
});
game.settings.registerMenu(SYSTEM.id, SYSTEM.SETTINGS.menu.Range.Name, {
name: game.i18n.localize('DAGGERHEART.Settings.Menu.Range.Name'),
label: game.i18n.localize('DAGGERHEART.Settings.Menu.Range.Label'),
hint: game.i18n.localize('DAGGERHEART.Settings.Menu.Range.Hint'),
icon: SYSTEM.SETTINGS.menu.Range.Icon,
type: DhRangeMeasurementSettings,
restricted: true
});
game.settings.registerMenu(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance, {
name: game.i18n.localize('DAGGERHEART.Settings.Menu.Appearance.title'),
label: game.i18n.localize('DAGGERHEART.Settings.Menu.Appearance.label'),
hint: game.i18n.localize('DAGGERHEART.Settings.Menu.Appearance.hint'),
icon: 'fa-solid fa-palette',
type: DhAppearanceSettings,
restricted: false
});
game.settings.registerMenu(SYSTEM.id, SYSTEM.SETTINGS.menu.VariantRules.Name, {
name: game.i18n.localize('DAGGERHEART.Settings.Menu.VariantRules.title'),
label: game.i18n.localize('DAGGERHEART.Settings.Menu.VariantRules.label'),
hint: game.i18n.localize('DAGGERHEART.Settings.Menu.VariantRules.hint'),
icon: SYSTEM.SETTINGS.menu.VariantRules.Icon,
type: DhVariantRuleSettings,
restricted: false
});
};
const registerNonConfigSettings = () => {
game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.LevelTiers, {
scope: 'world',
config: false,
type: DhLevelTiers,
default: defaultLevelTiers
});
game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear, {
@ -191,149 +132,9 @@ export const registerDHSettings = () => {
}
});
game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.MaxFear, {
name: game.i18n.localize('DAGGERHEART.Settings.Resources.MaxFear.Name'),
hint: game.i18n.localize('DAGGERHEART.Settings.Resources.MaxFear.Hint'),
scope: 'world',
config: true,
type: Number,
default: 12,
onChange: () => {
if (ui.resources) ui.resources.render({ force: true });
}
});
game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.DisplayFear, {
name: game.i18n.localize('DAGGERHEART.Settings.Resources.DisplayFear.Name'),
hint: game.i18n.localize('DAGGERHEART.Settings.Resources.DisplayFear.Hint'),
scope: 'client',
config: true,
type: String,
choices: {
token: 'Tokens',
bar: 'Bar',
hide: 'Hide'
},
default: 'token',
onChange: value => {
if (ui.resources) {
if (value === 'hide') ui.resources.close({ allowed: true });
else ui.resources.render({ force: true });
}
}
});
game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation.Hope, {
name: game.i18n.localize('DAGGERHEART.Settings.Automation.Hope.Name'),
hint: game.i18n.localize('DAGGERHEART.Settings.Automation.Hope.Hint'),
scope: 'world',
config: false,
type: Boolean,
default: false
});
game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation.ActionPoints, {
name: game.i18n.localize('DAGGERHEART.Settings.Automation.ActionPoints.Name'),
hint: game.i18n.localize('DAGGERHEART.Settings.Automation.ActionPoints.Hint'),
scope: 'world',
config: false,
type: Boolean,
default: true
});
game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.General.RangeMeasurement, {
name: game.i18n.localize('DAGGERHEART.Settings.General.RangeMeasurement.Name'),
hint: game.i18n.localize('DAGGERHEART.Settings.General.RangeMeasurement.Hint'),
scope: 'world',
config: false,
type: Object,
default: {
enabled: true,
melee: 5,
veryClose: 15,
close: 30,
far: 60,
veryFar: 120
}
});
game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.variantRules, {
scope: 'world',
config: false,
type: DhVariantRules,
default: DhVariantRules.defaultSchema
});
game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance, {
scope: 'client',
config: false,
type: DhAppearance,
default: DhAppearance.defaultSchema
});
game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.DualityRollColor, {
name: game.i18n.localize('DAGGERHEART.Settings.DualityRollColor.Name'),
hint: game.i18n.localize('DAGGERHEART.Settings.DualityRollColor.Hint'),
scope: 'world',
config: true,
type: Number,
choices: Object.values(DualityRollColor),
default: DualityRollColor.colorful.value
});
game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.LevelTiers, {
scope: 'world',
config: false,
type: DhLevelTiers,
default: defaultLevelTiers
});
game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns, {
scope: 'world',
config: false,
type: DhCountdowns
});
game.settings.registerMenu(SYSTEM.id, SYSTEM.SETTINGS.menu.Automation.Name, {
name: game.i18n.localize('DAGGERHEART.Settings.Menu.Automation.Name'),
label: game.i18n.localize('DAGGERHEART.Settings.Menu.Automation.Label'),
hint: game.i18n.localize('DAGGERHEART.Settings.Menu.Automation.Hint'),
icon: SYSTEM.SETTINGS.menu.Automation.Icon,
type: DhpAutomationSettings,
restricted: true
});
game.settings.registerMenu(SYSTEM.id, SYSTEM.SETTINGS.menu.Homebrew.Name, {
name: game.i18n.localize('DAGGERHEART.Settings.Menu.Homebrew.Name'),
label: game.i18n.localize('DAGGERHEART.Settings.Menu.Homebrew.Label'),
hint: game.i18n.localize('DAGGERHEART.Settings.Menu.Homebrew.Hint'),
icon: SYSTEM.SETTINGS.menu.Homebrew.Icon,
type: DhpHomebrewSettings,
restricted: true
});
game.settings.registerMenu(SYSTEM.id, SYSTEM.SETTINGS.menu.Range.Name, {
name: game.i18n.localize('DAGGERHEART.Settings.Menu.Range.Name'),
label: game.i18n.localize('DAGGERHEART.Settings.Menu.Range.Label'),
hint: game.i18n.localize('DAGGERHEART.Settings.Menu.Range.Hint'),
icon: SYSTEM.SETTINGS.menu.Range.Icon,
type: DhpRangeSettings,
restricted: true
});
game.settings.registerMenu(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance, {
name: game.i18n.localize('DAGGERHEART.Settings.Menu.Appearance.title'),
label: game.i18n.localize('DAGGERHEART.Settings.Menu.Appearance.label'),
hint: game.i18n.localize('DAGGERHEART.Settings.Menu.Appearance.hint'),
icon: 'fa-solid fa-palette',
type: DHAppearanceSettings,
restricted: false
});
game.settings.registerMenu(SYSTEM.id, SYSTEM.SETTINGS.menu.VariantRules.Name, {
name: game.i18n.localize('DAGGERHEART.Settings.Menu.VariantRules.title'),
label: game.i18n.localize('DAGGERHEART.Settings.Menu.VariantRules.label'),
hint: game.i18n.localize('DAGGERHEART.Settings.Menu.VariantRules.hint'),
icon: SYSTEM.SETTINGS.menu.VariantRules.Icon,
type: DHVariantRuleSettings,
restricted: false
});
};

View file

@ -0,0 +1,13 @@
import DhAppearanceSettings from './appearanceSettings.mjs';
import DhAutomationSettings from './automationSettings.mjs';
import DhHomebrewSettings from './homebrewSettings.mjs';
import DhRangeMeasurementSettings from './rangeMeasurementSettings.mjs';
import DhVariantRuleSettings from './variantRuleSettings.mjs';
export {
DhAppearanceSettings,
DhAutomationSettings,
DhHomebrewSettings,
DhRangeMeasurementSettings,
DhVariantRuleSettings
};

View file

@ -1,4 +1,4 @@
import DhAppearance from '../../data/settings/Appearance.mjs';
import DhAppearance, { DualityRollColor } from '../../data/settings/Appearance.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -54,6 +54,12 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
static async save() {
await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance, this.settings.toObject());
document.body.classList.toggle(
'theme-colorful',
game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance).dualityColorScheme ===
DualityRollColor.colorful.value
);
this.close();
}
}

View file

@ -0,0 +1,59 @@
import { DhAutomation } from '../../data/settings/_module.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class DhAutomationSettings extends HandlebarsApplicationMixin(ApplicationV2) {
constructor() {
super({});
this.settings = new DhAutomation(
game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation).toObject()
);
}
get title() {
return game.i18n.localize('DAGGERHEART.Settings.Menu.Automation.Name');
}
static DEFAULT_OPTIONS = {
tag: 'form',
id: 'daggerheart-automation-settings',
classes: ['daggerheart', 'setting', 'dh-style'],
position: { width: '600', height: 'auto' },
actions: {
reset: this.reset,
save: this.save
},
form: { handler: this.updateData, submitOnChange: true }
};
static PARTS = {
main: {
template: 'systems/daggerheart/templates/settings/automation-settings.hbs'
}
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.settingFields = this.settings;
return context;
}
static async updateData(event, element, formData) {
const updatedSettings = foundry.utils.expandObject(formData.object);
await this.settings.updateSource(updatedSettings);
this.render();
}
static async reset() {
this.settings = new DhAutomation();
this.render();
}
static async save() {
await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation, this.settings.toObject());
this.close();
}
}

View file

@ -0,0 +1,60 @@
import { DhHomebrew } from '../../data/settings/_module.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class DhAutomationSettings extends HandlebarsApplicationMixin(ApplicationV2) {
constructor() {
super({});
this.settings = new DhHomebrew(game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Homebrew).toObject());
}
get title() {
return game.i18n.localize('DAGGERHEART.Settings.Menu.Homebrew.Name');
}
static DEFAULT_OPTIONS = {
tag: 'form',
id: 'daggerheart-homebrew-settings',
classes: ['daggerheart', 'setting', 'dh-style'],
position: { width: '600', height: 'auto' },
actions: {
reset: this.reset,
save: this.save
},
form: { handler: this.updateData, submitOnChange: true }
};
static PARTS = {
main: {
template: 'systems/daggerheart/templates/settings/homebrew-settings.hbs'
}
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.settingFields = this.settings;
return context;
}
static async updateData(event, element, formData) {
const updatedSettings = foundry.utils.expandObject(formData.object);
await this.settings.updateSource({
...updatedSettings,
traitArray: Object.values(updatedSettings.traitArray)
});
this.render();
}
static async reset() {
this.settings = new DhHomebrew();
this.render();
}
static async save() {
await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Homebrew, this.settings.toObject());
this.close();
}
}

View file

@ -0,0 +1,59 @@
import { DhRangeMeasurement } from '../../data/settings/_module.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class DhRangeMeasurementSettings extends HandlebarsApplicationMixin(ApplicationV2) {
constructor() {
super({});
this.settings = new DhRangeMeasurement(
game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.RangeMeasurement).toObject()
);
}
get title() {
return game.i18n.localize('DAGGERHEART.Settings.Menu.Automation.Name');
}
static DEFAULT_OPTIONS = {
tag: 'form',
id: 'daggerheart-automation-settings',
classes: ['daggerheart', 'setting', 'dh-style'],
position: { width: '600', height: 'auto' },
actions: {
reset: this.reset,
save: this.save
},
form: { handler: this.updateData, submitOnChange: true }
};
static PARTS = {
main: {
template: 'systems/daggerheart/templates/settings/range-measurement-settings.hbs'
}
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.settingFields = this.settings;
return context;
}
static async updateData(event, element, formData) {
const updatedSettings = foundry.utils.expandObject(formData.object);
await this.settings.updateSource(updatedSettings);
this.render();
}
static async reset() {
this.settings = new DhRangeMeasurement();
this.render();
}
static async save() {
await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.RangeMeasurement, this.settings.toObject());
this.close();
}
}

View file

@ -0,0 +1,62 @@
export default class DhActiveEffectConfig extends foundry.applications.sheets.ActiveEffectConfig {
static DEFAULT_OPTIONS = {
classes: ['daggerheart', 'sheet', 'dh-style']
};
static PARTS = {
header: { template: 'systems/daggerheart/templates/sheets/activeEffect/header.hbs' },
tabs: { template: 'templates/generic/tab-navigation.hbs' },
details: { template: 'systems/daggerheart/templates/sheets/activeEffect/details.hbs', scrollable: [''] },
duration: { template: 'systems/daggerheart/templates/sheets/activeEffect/duration.hbs' },
changes: {
template: 'systems/daggerheart/templates/sheets/activeEffect/changes.hbs',
scrollable: ['ol[data-changes]']
},
footer: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-form-footer.hbs' }
};
static TABS = {
sheet: {
tabs: [
{ id: 'details', icon: 'fa-solid fa-book' },
{ id: 'duration', icon: 'fa-solid fa-clock' },
{ id: 'changes', icon: 'fa-solid fa-gears' }
],
initial: 'details',
labelPrefix: 'EFFECT.TABS'
}
};
async _preparePartContext(partId, context) {
const partContext = await super._preparePartContext(partId, context);
switch (partId) {
case 'changes':
const fieldPaths = [];
const validFieldPath = fieldPath => this.validFieldPath(fieldPath, this.#unapplicablePaths);
context.document.parent.system.schema.apply(function () {
if (!(this instanceof foundry.data.fields.SchemaField)) {
if (validFieldPath(this.fieldPath)) {
fieldPaths.push(this.fieldPath);
}
}
});
context.fieldPaths = fieldPaths;
break;
}
return partContext;
}
#unapplicablePaths = ['story', 'pronouns', 'description'];
validFieldPath(fieldPath, unapplicablePaths) {
const splitPath = fieldPath.split('.');
if (splitPath.length > 1 && unapplicablePaths.includes(splitPath[1])) return false;
/* The current value of a resource should not be modified */
if (new RegExp(/resources.*\.value/).exec(fieldPath)) return false;
return true;
}
}

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 = await game.settings
.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Homebrew)
.traitArray.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,88 +1,13 @@
import DaggerheartSheet from '../daggerheart-sheet.mjs';
import DHHeritageSheetV2 from './heritage.mjs';
const { ItemSheetV2 } = foundry.applications.sheets;
export default class AncestrySheet extends DaggerheartSheet(ItemSheetV2) {
export default class AncestrySheet extends DHHeritageSheetV2(ItemSheetV2) {
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'sheet', 'item', 'dh-style', 'ancestry'],
position: { width: 450, height: 700 },
actions: {
editFeature: this.editFeature,
deleteFeature: this.deleteFeature
},
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false
},
dragDrop: [{ dragSelector: null, dropSelector: null }]
classes: ['ancestry']
};
static PARTS = {
header: { template: 'systems/daggerheart/templates/sheets/items/ancestry/header.hbs' },
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
description: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-description.hbs' },
features: {
template: 'systems/daggerheart/templates/sheets/global/tabs/tab-feature-section.hbs',
scrollable: ['.features']
}
...super.PARTS
};
static TABS = {
description: {
active: true,
cssClass: '',
group: 'primary',
id: 'description',
icon: null,
label: 'DAGGERHEART.Sheets.Feature.Tabs.Description'
},
features: {
active: false,
cssClass: '',
group: 'primary',
id: 'features',
icon: null,
label: 'DAGGERHEART.Sheets.Feature.Tabs.Features'
}
};
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();
}
static async editFeature(_, target) {
const feature = await fromUuid(target.dataset.feature);
feature.sheet.render(true);
}
static async deleteFeature(event, target) {
event.preventDefault();
event.stopPropagation();
await this.item.update({
'system.abilities': this.item.system.abilities.filter(x => x.uuid !== target.dataset.feature)
});
}
async _onDrop(event) {
const data = TextEditor.getDragEventData(event);
const item = await fromUuid(data.uuid);
if (item.type === 'feature' && item.system.type === SYSTEM.ITEM.featureTypes.ancestry.id) {
await this.document.update({
'system.abilities': [
...this.document.system.abilities,
{ img: item.img, name: item.name, uuid: item.uuid }
]
});
}
}
}

View file

@ -1,16 +1,11 @@
import DaggerheartSheet from '../daggerheart-sheet.mjs';
import { armorFeatures } from '../../../config/itemConfig.mjs';
import { tagifyElement } from '../../../helpers/utils.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 +13,37 @@ 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 _preparePartContext(partId, context) {
super._preparePartContext(partId, context);
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.document = this.document;
context.config = CONFIG.daggerheart;
context.tabs = super._getTabs(this.constructor.TABS);
switch (partId) {
case 'settings':
context.features = this.document.system.features.map(x => x.value);
break;
}
return context;
}
static async updateForm(event, _, formData) {
await this.document.update(formData.object);
this.render();
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
const featureInput = htmlElement.querySelector('.features-input');
tagifyElement(featureInput, armorFeatures, this.onFeatureSelect.bind(this));
}
async onFeatureSelect(features) {
await this.document.update({ 'system.features': features.map(x => ({ value: x.value })) });
this.render(true);
}
}

View file

@ -1,8 +1,11 @@
import { actionsTypes } from '../../../data/_module.mjs';
import { tagifyElement } from '../../../helpers/utils.mjs';
import DHActionConfig from '../../config/Action.mjs';
import DaggerheartSheet from '../daggerheart-sheet.mjs';
import Tagify from '@yaireo/tagify';
const { ItemSheetV2 } = foundry.applications.sheets;
const { TextEditor } = foundry.applications.ux;
export default class ClassSheet extends DaggerheartSheet(ItemSheetV2) {
static DEFAULT_OPTIONS = {
tag: 'form',
@ -11,8 +14,9 @@ export default class ClassSheet extends DaggerheartSheet(ItemSheetV2) {
actions: {
removeSubclass: this.removeSubclass,
viewSubclass: this.viewSubclass,
removeFeature: this.removeFeature,
viewFeature: this.viewFeature,
addFeature: this.addFeature,
editFeature: this.editFeature,
deleteFeature: this.deleteFeature,
removeItem: this.removeItem,
viewItem: this.viewItem,
removePrimaryWeapon: this.removePrimaryWeapon,
@ -72,55 +76,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 +99,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 +115,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);
}
@ -193,75 +155,118 @@ export default class ClassSheet extends DaggerheartSheet(ItemSheetV2) {
await this.document.update({ 'system.characterGuide.suggestedArmor': null }, { diff: false });
}
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 });
return data;
},
rejectClose: false
});
}
getActionPath(type) {
return type === 'hope' ? 'hopeFeatures' : 'classFeatures';
}
static async addFeature(_, target) {
const actionPath = this.getActionPath(target.dataset.type);
const actionType = await this.selectActionType();
const cls = actionsTypes[actionType?.type] ?? actionsTypes.attack,
action = new cls(
{
_id: foundry.utils.randomID(),
systemPath: actionPath,
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.${actionPath}`]: [...this.document.system[actionPath], action] });
}
static async editFeature(_, target) {
const action = this.document.system[this.getActionPath(target.dataset.type)].find(
x => x._id === target.dataset.feature
);
await new DHActionConfig(action).render(true);
}
static async deleteFeature(_, target) {
const actionPath = this.getActionPath(target.dataset.type);
await this.document.update({
[`system.${actionPath}`]: this.document.system[actionPath].filter(
action => action._id !== target.dataset.feature
)
});
}
async _onDrop(event) {
const data = TextEditor.getDragEventData(event);
const item = await fromUuid(data.uuid);
const target = event.target.closest('fieldset.drop-section');
if (item.type === 'subclass') {
await this.document.update({
'system.subclasses': [
...this.document.system.subclasses,
{ img: item.img, name: item.name, 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.subclasses': [...this.document.system.subclasses.map(x => x.uuid), item.uuid]
});
} else if (item.type === 'weapon') {
if (event.currentTarget.classList.contains('primary-weapon-section')) {
if (target.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')) {
} else if (target.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 (target.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')) {
} else if (target.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 }
...this.document.system.inventory.choiceA.map(x => x.uuid),
item.uuid
]
});
}
} else if (item.type === 'miscellaneous') {
if (event.currentTarget.classList.contains('take-section')) {
if (target.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.map(x => x.uuid), item.uuid]
});
} else if (event.currentTarget.classList.contains('choice-b-section')) {
} else if (target.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 }
...this.document.system.inventory.choiceB.map(x => x.uuid),
item.uuid
]
});
}

View file

@ -1,88 +1,13 @@
import DaggerheartSheet from '../daggerheart-sheet.mjs';
import DHHeritageSheetV2 from './heritage.mjs';
const { ItemSheetV2 } = foundry.applications.sheets;
export default class CommunitySheet extends DaggerheartSheet(ItemSheetV2) {
export default class CommunitySheet extends DHHeritageSheetV2(ItemSheetV2) {
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'sheet', 'item', 'dh-style', 'community'],
position: { width: 450, height: 700 },
actions: {
editFeature: this.editFeature,
deleteFeature: this.deleteFeature
},
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false
},
dragDrop: [{ dragSelector: null, dropSelector: null }]
classes: ['community']
};
static PARTS = {
header: { template: 'systems/daggerheart/templates/sheets/items/community/header.hbs' },
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
description: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-description.hbs' },
features: {
template: 'systems/daggerheart/templates/sheets/global/tabs/tab-feature-section.hbs',
scrollable: ['.features']
}
...super.PARTS
};
static TABS = {
description: {
active: true,
cssClass: '',
group: 'primary',
id: 'description',
icon: null,
label: 'DAGGERHEART.Sheets.Feature.Tabs.Description'
},
features: {
active: false,
cssClass: '',
group: 'primary',
id: 'features',
icon: null,
label: 'DAGGERHEART.Sheets.Feature.Tabs.Features'
}
};
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();
}
static async editFeature(_, target) {
const feature = await fromUuid(target.dataset.feature);
feature.sheet.render(true);
}
static async deleteFeature(event, target) {
event.preventDefault();
event.stopPropagation();
await this.item.update({
'system.abilities': this.item.system.abilities.filter(x => x.uuid !== target.dataset.feature)
});
}
async _onDrop(event) {
const data = TextEditor.getDragEventData(event);
const item = await fromUuid(data.uuid);
if (item.type === 'feature' && item.system.type === SYSTEM.ITEM.featureTypes.community.id) {
await this.document.update({
'system.abilities': [
...this.document.system.abilities,
{ img: item.img, name: item.name, uuid: 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

@ -0,0 +1,147 @@
import { actionsTypes } from '../../../data/_module.mjs';
import DHActionConfig from '../../config/Action.mjs';
import DHItemMixin from '../item.mjs';
export default function DHHeritageMixin(Base) {
return class DHHeritageSheetV2 extends DHItemMixin(Base) {
static DEFAULT_OPTIONS = {
tag: 'form',
position: { width: 450, height: 700 },
actions: {
addAction: this.addAction,
editAction: this.editAction,
removeAction: this.removeAction,
addEffect: this.addEffect,
editEffect: this.editEffect,
removeEffect: this.removeEffect
},
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false
}
};
static PARTS = {
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']
},
effects: {
template: 'systems/daggerheart/templates/sheets/global/tabs/tab-effects.hbs',
scrollable: ['.effects']
}
};
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'
},
effects: {
active: false,
cssClass: '',
group: 'primary',
id: 'effects',
icon: null,
label: 'DAGGERHEART.Sheets.Feature.Tabs.Effects'
}
};
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();
}
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 });
return data;
},
rejectClose: false
});
}
static async addAction() {
const actionType = await this.selectActionType();
const cls = actionsTypes[actionType?.type] ?? actionsTypes.attack,
action = new cls(
{
_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] });
}
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)
)
});
}
static async addEffect() {
await this.document.createEmbeddedDocuments('ActiveEffect', [
{ name: game.i18n.localize('DAGGERHEART.Feature.NewEffect') }
]);
}
static async editEffect(_, target) {
const effect = this.document.effects.get(target.dataset.effect);
effect.sheet.render(true);
}
static async removeEffect(_, target) {
await this.document.effects.get(target.dataset.effect).delete();
}
};
}

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,29 +1,24 @@
import DaggerheartSheet from '../daggerheart-sheet.mjs';
import DaggerheartFeature from '../../../data/feature.mjs';
import { actionsTypes } from '../../../data/_module.mjs';
import DHActionConfig from '../../config/Action.mjs';
import DhpApplicationMixin from '../daggerheart-sheet.mjs';
const { ItemSheetV2 } = foundry.applications.sheets;
const { TextEditor } = foundry.applications.ux;
const { duplicate, getProperty } = foundry.utils;
export default class SubclassSheet extends DaggerheartSheet(ItemSheetV2) {
export default class SubclassSheet extends DhpApplicationMixin(ItemSheetV2) {
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'sheet', 'item', 'dh-style', 'subclass'],
position: { width: 600 },
window: { resizable: false },
actions: {
editAbility: this.editAbility,
deleteFeatureAbility: this.deleteFeatureAbility
addFeature: this.addFeature,
editFeature: this.editFeature,
deleteFeature: this.deleteFeature
},
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false
},
dragDrop: [
{ dragSelector: null, dropSelector: '.foundation-tab' },
{ dragSelector: null, dropSelector: '.specialization-tab' },
{ dragSelector: null, dropSelector: '.mastery-tab' }
]
}
};
static PARTS = {
@ -81,41 +76,99 @@ export default class SubclassSheet extends DaggerheartSheet(ItemSheetV2) {
this.render();
}
static async editAbility(_, button) {
const feature = await fromUuid(button.dataset.ability);
feature.sheet.render(true);
static addFeature(_, target) {
if (target.dataset.type === 'action') this.addAction(target.dataset.level);
else this.addEffect(target.dataset.level);
}
static async deleteFeatureAbility(event, button) {
event.preventDefault();
event.stopPropagation();
const feature = button.dataset.feature;
const newAbilities = this.document.system[`${feature}Feature`].abilities.filter(
x => x.uuid !== button.dataset.ability
);
const path = `system.${feature}Feature.abilities`;
await this.document.update({ [path]: newAbilities });
static async editFeature(_, target) {
if (target.dataset.type === 'action') this.editAction(target.dataset.level, target.dataset.feature);
else this.editEffect(target.dataset.feature);
}
async _onDrop(event) {
event.preventDefault();
const data = TextEditor.getDragEventData(event);
const item = await fromUuid(data.uuid);
if (!(item.type === 'feature' && item.system.type === SYSTEM.ITEM.featureTypes.subclass.id)) return;
static async deleteFeature(_, target) {
if (target.dataset.type === 'action') this.removeAction(target.dataset.level, target.dataset.feature);
else this.removeEffect(target.dataset.level, target.dataset.feature);
}
let featureField;
if (event.currentTarget.classList.contains('foundation-tab')) featureField = 'foundation';
else if (event.currentTarget.classList.contains('specialization-tab')) featureField = 'specialization';
else if (event.currentTarget.classList.contains('mastery-tab')) featureField = 'mastery';
else return;
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 });
return data;
},
rejectClose: false
});
}
const path = `system.${featureField}Feature.abilities`;
const abilities = duplicate(getProperty(this.document, path)) || [];
const featureData = { name: item.name, img: item.img, uuid: item.uuid };
abilities.push(featureData);
async addAction(level) {
const actionType = await this.#selectActionType();
const cls = actionsTypes[actionType?.type] ?? actionsTypes.attack,
action = new cls(
{
_id: foundry.utils.randomID(),
systemPath: `${level}.actions`,
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.${level}.actions`]: [...this.document.system[level].actions, action] });
await new DHActionConfig(
this.document.system[level].actions[this.document.system[level].actions.length - 1]
).render(true);
}
await this.document.update({ [path]: abilities });
async addEffect(level) {
const embeddedItems = await this.document.createEmbeddedDocuments('ActiveEffect', [
{ name: game.i18n.localize('DAGGERHEART.Feature.NewEffect') }
]);
await this.document.update({
[`system.${level}.effects`]: [
...this.document.system[level].effects.map(x => x.uuid),
embeddedItems[0].uuid
]
});
}
async editAction(level, id) {
const action = this.document.system[level].actions.find(x => x._id === id);
await new DHActionConfig(action).render(true);
}
async editEffect(id) {
const effect = this.document.effects.get(id);
effect.sheet.render(true);
}
async removeAction(level, id) {
await this.document.update({
[`system.${level}.actions`]: this.document.system[level].actions.filter(action => action._id !== id)
});
}
async removeEffect(level, id) {
await this.document.effects.get(id).delete();
await this.document.update({
[`system.${level}.effects`]: this.document.system[level].effects
.filter(x => x && x.id !== id)
.map(effect => effect.uuid)
});
}
}

View file

@ -1,58 +1,48 @@
import DaggerheartSheet from '../daggerheart-sheet.mjs';
import { weaponFeatures } from '../../../config/itemConfig.mjs';
import { tagifyElement } from '../../../helpers/utils.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 _preparePartContext(partId, context) {
super._preparePartContext(partId, context);
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.document = this.document;
context.config = CONFIG.daggerheart;
context.tabs = super._getTabs(this.constructor.TABS);
switch (partId) {
case 'settings':
context.features = this.document.system.features.map(x => x.value);
break;
}
return context;
}
static async updateForm(event, _, formData) {
await this.document.update(formData.object);
this.render();
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
const featureInput = htmlElement.querySelector('.features-input');
tagifyElement(featureInput, weaponFeatures, this.onFeatureSelect.bind(this));
}
async onFeatureSelect(features) {
await this.document.update({ 'system.features': features.map(x => ({ value: x.value })) });
this.render(true);
}
}

File diff suppressed because it is too large Load diff

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,42 @@
export const range = {
self: {
id: 'self',
short: 's',
label: 'DAGGERHEART.Range.self.name',
description: 'DAGGERHEART.Range.self.description',
distance: 0
},
melee: {
id: 'melee',
short: 'm',
label: 'DAGGERHEART.Range.melee.name',
description: 'DAGGERHEART.Range.melee.description',
distance: 1
},
veryClose: {
id: 'veryClose',
short: 'vc',
label: 'DAGGERHEART.Range.veryClose.name',
description: 'DAGGERHEART.Range.veryClose.description',
distance: 3
},
close: {
id: 'close',
short: 'c',
label: 'DAGGERHEART.Range.close.name',
description: 'DAGGERHEART.Range.close.description',
distance: 10
},
far: {
id: 'far',
short: 'f',
label: 'DAGGERHEART.Range.far.name',
description: 'DAGGERHEART.Range.far.description',
distance: 20
},
veryFar: {
id: 'veryFar',
short: 'vf',
label: 'DAGGERHEART.Range.veryFar.name',
description: 'DAGGERHEART.Range.veryFar.description',
distance: 30
@ -175,31 +192,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 +260,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);
@ -322,3 +340,23 @@ export const countdownTypes = {
label: 'DAGGERHEART.Countdown.Type.Custom'
}
};
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'
}
};
export const fearDisplay = {
token: { value: 'token', label: 'DAGGERHEART.Settings.Appearance.FearDisplay.Token' },
bar: { value: 'bar', label: 'DAGGERHEART.Settings.Appearance.FearDisplay.Bar' },
hide: { value: 'hide', label: 'DAGGERHEART.Settings.Appearance.FearDisplay.Hide' }
};

View file

@ -5,15 +5,78 @@ export const armorFeatures = {
},
channeling: {
label: 'DAGGERHEART.ArmorFeature.Channeling.Name',
description: 'DAGGERHEART.ArmorFeature.Channeling.Description'
description: 'DAGGERHEART.ArmorFeature.Channeling.Description',
effects: [
{
changes: [
{
key: 'system.bonuses.spellcast',
mode: 2,
value: '1'
}
]
}
]
},
difficult: {
label: 'DAGGERHEART.ArmorFeature.Difficult.Name',
description: 'DAGGERHEART.ArmorFeature.Difficult.Description'
description: 'DAGGERHEART.ArmorFeature.Difficult.Description',
effects: [
{
changes: [
{
key: 'system.traits.agility.bonus',
mode: 2,
value: '-1'
},
{
key: 'system.traits.strength.bonus',
mode: 2,
value: '-1'
},
{
key: 'system.traits.finesse.bonus',
mode: 2,
value: '-1'
},
{
key: 'system.traits.instinct.bonus',
mode: 2,
value: '-1'
},
{
key: 'system.traits.presence.bonus',
mode: 2,
value: '-1'
},
{
key: 'system.traits.knowledge.bonus',
mode: 2,
value: '-1'
},
{
key: 'system.evasion.bonus',
mode: 2,
value: '-1'
}
]
}
]
},
flexible: {
label: 'DAGGERHEART.ArmorFeature.Flexible.Name',
description: 'DAGGERHEART.ArmorFeature.Flexible.Description'
description: 'DAGGERHEART.ArmorFeature.Flexible.Description',
effects: [
{
changes: [
{
key: 'system.evasion.bonus',
mode: 2,
value: '1'
}
]
}
]
},
fortified: {
label: 'DAGGERHEART.ArmorFeature.Fortified.Name',
@ -21,11 +84,33 @@ export const armorFeatures = {
},
gilded: {
label: 'DAGGERHEART.ArmorFeature.Gilded.Name',
description: 'DAGGERHEART.ArmorFeature.Gilded.Description'
description: 'DAGGERHEART.ArmorFeature.Gilded.Description',
effects: [
{
changes: [
{
key: 'system.traits.presence.bonus',
mode: 2,
value: '1'
}
]
}
]
},
heavy: {
label: 'DAGGERHEART.ArmorFeature.Heavy.Name',
description: 'DAGGERHEART.ArmorFeature.Heavy.Description'
description: 'DAGGERHEART.ArmorFeature.Heavy.Description',
effects: [
{
changes: [
{
key: 'system.evasion.bonus',
mode: 2,
value: '-1'
}
]
}
]
},
hopeful: {
label: 'DAGGERHEART.ArmorFeature.Hopeful.Name',
@ -77,7 +162,23 @@ export const armorFeatures = {
},
veryheavy: {
label: 'DAGGERHEART.ArmorFeature.VeryHeavy.Name',
description: 'DAGGERHEART.ArmorFeature.VeryHeavy.Description'
description: 'DAGGERHEART.ArmorFeature.VeryHeavy.Description',
effects: [
{
changes: [
{
key: 'system.evasion.bonus',
mode: 2,
value: '-2'
},
{
key: 'system.traits.agility.bonus',
mode: 2,
value: '-1'
}
]
}
]
},
warded: {
label: 'DAGGERHEART.ArmorFeature.Warded.Name',
@ -89,13 +190,41 @@ export const weaponFeatures = {
barrier: {
label: 'DAGGERHEART.WeaponFeature.Barrier.Name',
description: 'DAGGERHEART.WeaponFeature.Barrier.Description',
override: {
armorBonus: 1
}
effects: [
{
changes: [
{
key: 'system.bonuses.armorScore',
mode: 2,
value: '@system.tier + 1'
}
]
},
{
changes: [
{
key: 'system.evasion.bonus',
mode: 2,
value: '-1'
}
]
}
]
},
bonded: {
label: 'DAGGERHEART.WeaponFeature.Bonded.Name',
description: 'DAGGERHEART.WeaponFeature.Bonded.Description'
description: 'DAGGERHEART.WeaponFeature.Bonded.Description',
effects: [
{
changes: [
{
key: 'system.bonuses.damage',
mode: 2,
value: 'system.levelData.levels.current'
}
]
}
]
},
bouncing: {
label: 'DAGGERHEART.WeaponFeature.Bouncing.Name',
@ -103,7 +232,27 @@ export const weaponFeatures = {
},
brave: {
label: 'DAGGERHEART.WeaponFeature.Brave.Name',
description: 'DAGGERHEART.WeaponFeature.Brave.Description'
description: 'DAGGERHEART.WeaponFeature.Brave.Description',
effects: [
{
changes: [
{
key: 'system.evasion.bonus',
mode: 2,
value: '-1'
}
]
},
{
changes: [
{
key: 'system.damageThresholds.severe',
mode: 2,
value: '3'
}
]
}
]
},
brutal: {
label: 'DAGGERHEART.WeaponFeature.Brutal.Name',
@ -111,15 +260,55 @@ export const weaponFeatures = {
},
charged: {
label: 'DAGGERHEART.WeaponFeature.Charged.Name',
description: 'DAGGERHEART.WeaponFeature.Charged.Description'
description: 'DAGGERHEART.WeaponFeature.Charged.Description',
actions: [
{
type: 'effect',
name: 'DAGGERHEART.WeaponFeature.Concussive.Name',
img: 'icons/skills/melee/shield-damaged-broken-brown.webp',
actionType: 'action',
cost: [
{
type: 'stress',
value: 1
}
]
// Should add an effect with path system.proficiency.bonus +1
}
]
},
concussive: {
label: 'DAGGERHEART.WeaponFeature.Concussive.Name',
description: 'DAGGERHEART.WeaponFeature.Concussive.Description'
description: 'DAGGERHEART.WeaponFeature.Concussive.Description',
actions: [
{
type: 'resource',
name: 'DAGGERHEART.WeaponFeature.Concussive.Name',
img: 'icons/skills/melee/shield-damaged-broken-brown.webp',
actionType: 'action',
cost: [
{
type: 'hope',
value: 1
}
]
}
]
},
cumbersome: {
label: 'DAGGERHEART.WeaponFeature.Cumbersome.Name',
description: 'DAGGERHEART.WeaponFeature.Cumbersome.Description'
description: 'DAGGERHEART.WeaponFeature.Cumbersome.Description',
effects: [
{
changes: [
{
key: 'system.traits.finesse.bonus',
mode: 2,
value: '-1'
}
]
}
]
},
deadly: {
label: 'DAGGERHEART.WeaponFeature.Deadly.Name',
@ -128,18 +317,64 @@ export const weaponFeatures = {
deflecting: {
label: 'DAGGERHEART.WeaponFeature.Deflecting.Name',
description: 'DAGGERHEART.WeaponFeature.Deflecting.Description'
// actions: [{
// type: 'effect',
// name: 'DAGGERHEART.WeaponFeature.Deflecting.Name',
// img: 'icons/skills/melee/strike-flail-destructive-yellow.webp',
// actionType: 'reaction',
// cost: [{
// type: 'armorSlot', // Needs armorSlot as type
// value: 1
// }],
// }],
},
destructive: {
label: 'DAGGERHEART.WeaponFeature.Destructive.Name',
description: 'DAGGERHEART.WeaponFeature.Destructive.Description'
description: 'DAGGERHEART.WeaponFeature.Destructive.Description',
effects: [
{
changes: [
{
key: 'system.traits.agility.bonus',
mode: 2,
value: '-1'
}
]
}
]
},
devastating: {
label: 'DAGGERHEART.WeaponFeature.Devastating.Name',
description: 'DAGGERHEART.WeaponFeature.Devastating.Description'
description: 'DAGGERHEART.WeaponFeature.Devastating.Description',
actions: [
{
type: 'resource',
name: 'DAGGERHEART.WeaponFeature.Devastating.Name',
img: 'icons/skills/melee/strike-flail-destructive-yellow.webp',
actionType: 'action',
cost: [
{
type: 'stress',
value: 1
}
]
}
]
},
doubleduty: {
label: 'DAGGERHEART.WeaponFeature.DoubleDuty.Name',
description: 'DAGGERHEART.WeaponFeature.DoubleDuty.Description'
description: 'DAGGERHEART.WeaponFeature.DoubleDuty.Description',
effects: [
{
changes: [
{
key: 'system.bonuses.armorScore',
mode: 2,
value: '1'
}
]
}
]
},
doubledup: {
label: 'DAGGERHEART.WeaponFeature.DoubledUp.Name',
@ -155,15 +390,61 @@ export const weaponFeatures = {
},
grappling: {
label: 'DAGGERHEART.WeaponFeature.Grappling.Name',
description: 'DAGGERHEART.WeaponFeature.Grappling.Description'
description: 'DAGGERHEART.WeaponFeature.Grappling.Description',
actions: [
{
type: 'resource',
name: 'DAGGERHEART.WeaponFeature.Grappling.Name',
img: 'icons/magic/control/debuff-chains-ropes-net-white.webp',
actionType: 'action',
cost: [
{
type: 'stress',
value: 1
}
]
}
]
},
greedy: {
label: 'DAGGERHEART.WeaponFeature.Greedy.Name',
description: 'DAGGERHEART.WeaponFeature.Greedy.Description'
},
healing: {
label: 'DAGGERHEART.WeaponFeature.Healing.Name',
description: 'DAGGERHEART.WeaponFeature.Healing.Description',
actions: [
{
type: 'healing',
name: 'DAGGERHEART.WeaponFeature.Healing.Name',
img: 'icons/magic/life/cross-beam-green.webp',
actionType: 'action',
healing: {
type: 'health',
value: {
custom: {
enabled: true,
formula: '1'
}
}
}
}
]
},
heavy: {
label: 'DAGGERHEART.WeaponFeature.Heavy.Name',
description: 'DAGGERHEART.WeaponFeature.Heavy.Description'
description: 'DAGGERHEART.WeaponFeature.Heavy.Description',
effects: [
{
changes: [
{
key: 'system.evasion.bonus',
mode: 2,
value: '-1'
}
]
}
]
},
hooked: {
label: 'DAGGERHEART.WeaponFeature.Hooked.Name',
@ -189,13 +470,56 @@ export const weaponFeatures = {
label: 'DAGGERHEART.WeaponFeature.Long.Name',
description: 'DAGGERHEART.WeaponFeature.Long.Description'
},
lucky: {
label: 'DAGGERHEART.WeaponFeature.Lucky.Name',
description: 'DAGGERHEART.WeaponFeature.Lucky.Description',
actions: [
{
type: 'resource',
name: 'DAGGERHEART.WeaponFeature.Lucky.Name',
img: 'icons/magic/control/buff-luck-fortune-green.webp',
actionType: 'action',
cost: [
{
type: 'stress',
value: 1
}
]
}
]
},
massive: {
label: 'DAGGERHEART.WeaponFeature.Massive.Name',
description: 'DAGGERHEART.WeaponFeature.Massive.Description'
description: 'DAGGERHEART.WeaponFeature.Massive.Description',
effects: [
{
changes: [
{
key: 'system.evasion.bonus',
mode: 2,
value: '-1'
}
]
}
]
},
painful: {
label: 'DAGGERHEART.WeaponFeature.Painful.Name',
description: 'DAGGERHEART.WeaponFeature.Painful.Description'
description: 'DAGGERHEART.WeaponFeature.Painful.Description',
actions: [
{
type: 'resource',
name: 'DAGGERHEART.WeaponFeature.Painful.Name',
img: 'icons/skills/wounds/injury-face-impact-orange.webp',
actionType: 'action',
cost: [
{
type: 'stress',
value: 1
}
]
}
]
},
paired: {
label: 'DAGGERHEART.WeaponFeature.Paired.Name',
@ -223,17 +547,50 @@ export const weaponFeatures = {
protective: {
label: 'DAGGERHEART.WeaponFeature.Protective.Name',
description: 'DAGGERHEART.WeaponFeature.Protective.Description',
override: {
armorBonus: 1
}
effects: [
{
changes: [
{
key: 'system.bonuses.armorScore',
mode: 2,
value: '@system.tier'
}
]
}
]
},
quick: {
label: 'DAGGERHEART.WeaponFeature.Quick.Name',
description: 'DAGGERHEART.WeaponFeature.Quick.Description'
description: 'DAGGERHEART.WeaponFeature.Quick.Description',
actions: [
{
type: 'resource',
name: 'DAGGERHEART.WeaponFeature.Quick.Name',
img: 'icons/skills/movement/arrow-upward-yellow.webp',
actionType: 'action',
cost: [
{
type: 'stress',
value: 1
}
]
}
]
},
reliable: {
label: 'DAGGERHEART.WeaponFeature.Reliable.Name',
description: 'DAGGERHEART.WeaponFeature.Reliable.Description'
description: 'DAGGERHEART.WeaponFeature.Reliable.Description',
effects: [
{
changes: [
{
key: 'system.bonuses.attack',
mode: 2,
value: 1
}
]
}
]
},
reloading: {
label: 'DAGGERHEART.WeaponFeature.Reloading.Name',
@ -265,7 +622,21 @@ export const weaponFeatures = {
},
startling: {
label: 'DAGGERHEART.WeaponFeature.Startling.Name',
description: 'DAGGERHEART.WeaponFeature.Startling.Description'
description: 'DAGGERHEART.WeaponFeature.Startling.Description',
actions: [
{
type: 'resource',
name: 'DAGGERHEART.WeaponFeature.Startling.Name',
img: 'icons/magic/control/fear-fright-mask-orange.webp',
actionType: 'action',
cost: [
{
type: 'stress',
value: 1
}
]
}
]
},
timebending: {
label: 'DAGGERHEART.WeaponFeature.Timebending.Name',

View file

@ -18,24 +18,16 @@ export const menu = {
};
export const gameSettings = {
Automation: {
Hope: 'AutomationHope',
ActionPoints: 'AutomationActionPoints'
},
Resources: {
Fear: 'ResourcesFear',
MaxFear: 'ResourcesMaxFear',
DisplayFear: 'DisplayFear'
},
General: {
AbilityArray: 'AbilityArray',
RangeMeasurement: 'RangeMeasurement'
},
DualityRollColor: 'DualityRollColor',
LevelTiers: 'LevelTiers',
Countdowns: 'Countdowns',
Automation: 'Automation',
Homebrew: 'Homebrew',
RangeMeasurement: 'RangeMeasurement',
appearance: 'Appearance',
variantRules: 'VariantRules'
variantRules: 'VariantRules',
Resources: {
Fear: 'ResourcesFear'
},
LevelTiers: 'LevelTiers',
Countdowns: 'Countdowns'
};
export const DualityRollColor = {

View file

@ -1,19 +1,8 @@
export { default as DhpPC } from './pc.mjs';
export { default as DhpClass } from './class.mjs';
export { default as DhpSubclass } from './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';

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,397 @@
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(),
systemPath: new fields.StringField({ required: true, initial: 'actions' }),
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 foundry.utils.getProperty(this.parent, this.systemPath).indexOf(this);
}
get item() {
return this.parent.parent;
}
get actor() {
return this.item?.actor;
}
get chatTemplate() {
return 'systems/daggerheart/templates/chat/duality-roll.hbs';
}
get chatTitle() {
return this.item.name;
}
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 + (this.actor.system.bonuses.attack ?? 0);
const config = {
event: event,
title: this.chatTitle,
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';
}
get chatTitle() {
return game.i18n.format('DAGGERHEART.Chat.AttackRoll.Title', {
attack: this.item.name
});
}
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]?.total ?? 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,299 @@
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: null, 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 })
})
),
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),
bonuses: new fields.SchemaField({
attack: new fields.NumberField({ integer: true, initial: 0 }),
spellcast: new fields.NumberField({ integer: true, initial: 0 }),
armorScore: new fields.NumberField({ integer: true, initial: 0 })
})
};
}
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 ?? 0) + 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,13 @@
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 ForeignDocumentUUIDArrayField } from './foreignDocumentUUIDArrayField.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,20 @@
import ForeignDocumentUUIDField from './foreignDocumentUUIDField.mjs';
/**
* A subclass of {@link foundry.data.fields.ArrayField} that defines an array of foreign document UUID references.
*/
export default class ForeignDocumentUUIDArrayField extends foundry.data.fields.ArrayField {
/**
* @param {foundry.data.types.DocumentUUIDFieldOptions} [fieldOption] - Options to configure each individual ForeignDocumentUUIDField.
* @param {foundry.data.types.ArrayFieldOptions} [options] - Options to configure the array behavior
* @param {foundry.data.types.DataFieldContext} [context] - Optional context for schema processing
*/
constructor(fieldOption = {}, options = {}, context = {}) {
super(new ForeignDocumentUUIDField(fieldOption), options, context);
}
/** @inheritdoc */
initialize(value, model, options = {}) {
const v = super.initialize(value, model, options);
return () => v.map(entry => (typeof entry === 'function' ? entry() : entry));
}
}

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

@ -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,22 @@
import ActionField from '../fields/actionField.mjs';
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(),
actions: new fields.ArrayField(new ActionField())
};
}
}

View file

@ -0,0 +1,89 @@
import BaseDataItem from './base.mjs';
import ActionField from '../fields/actionField.mjs';
import { armorFeatures } from '../../config/itemConfig.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(),
tier: new fields.NumberField({ required: true, integer: true, initial: 1, min: 1 }),
equipped: new fields.BooleanField({ initial: false }),
baseScore: new fields.NumberField({ integer: true, initial: 0 }),
features: new fields.ArrayField(
new fields.SchemaField({
value: new fields.StringField({ required: true, choices: SYSTEM.ITEM.armorFeatures, blank: true }),
effectIds: new fields.ArrayField(new fields.StringField({ required: true })),
actionIds: new fields.ArrayField(new fields.StringField({ required: 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 })
}),
actions: new fields.ArrayField(new ActionField())
};
}
get featureInfo() {
return this.feature ? CONFIG.daggerheart.ITEM.armorFeatures[this.feature] : null;
}
async _preUpdate(changes, options, user) {
const allowed = await super._preUpdate(changes, options, user);
if (allowed === false) return false;
if (changes.system.features) {
const removed = this.features.filter(x => !changes.system.features.includes(x));
const added = changes.system.features.filter(x => !this.features.includes(x));
for (var feature of removed) {
for (var effectId of feature.effectIds) {
await this.parent.effects.get(effectId).delete();
}
changes.system.actions = this.actions.filter(x => !feature.actionIds.includes(x._id));
}
for (var feature of added) {
const featureData = armorFeatures[feature.value];
if (featureData.effects?.length > 0) {
const embeddedItems = await this.parent.createEmbeddedDocuments('ActiveEffect', [
{
name: game.i18n.localize(featureData.label),
description: game.i18n.localize(featureData.description),
changes: featureData.effects.flatMap(x => x.changes)
}
]);
feature.effectIds = embeddedItems.map(x => x.id);
}
if (featureData.actions?.length > 0) {
const newActions = featureData.actions.map(action => {
const cls = actionsTypes[action.type];
return new cls(
{ ...action, _id: foundry.utils.randomID(), name: game.i18n.localize(action.name) },
{ parent: this }
);
});
changes.system.actions = [...this.actions, ...newActions];
feature.actionIds = newActions.map(x => x._id);
}
}
}
}
}

53
module/data/item/base.mjs Normal file
View file

@ -0,0 +1,53 @@
/**
* 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
*/
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
};
}
/** @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';
import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs';
import ActionField from '../fields/actionField.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 ForeignDocumentUUIDArrayField({ type: 'Item', required: false }),
evasion: new fields.NumberField({ initial: 0, integer: true }),
hopeFeatures: new foundry.data.fields.ArrayField(new ActionField()),
classFeatures: new foundry.data.fields.ArrayField(new ActionField()),
subclasses: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }),
inventory: new fields.SchemaField({
take: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }),
choiceA: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }),
choiceB: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false })
}),
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 })
};
}
get hopeFeature() {
return this.hopeFeatures.length > 0 ? this.hopeFeatures[0] : null;
}
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 ActionField from '../fields/actionField.mjs';
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(),
actions: new fields.ArrayField(new ActionField())
};
}
}

View file

@ -0,0 +1,24 @@
import BaseDataItem from './base.mjs';
import ActionField from '../fields/actionField.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 }),
actions: new fields.ArrayField(new ActionField())
};
}
}

View file

@ -0,0 +1,50 @@
import BaseDataItem from './base.mjs';
import ActionField from '../fields/actionField.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 ActionField())
};
}
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,152 @@
import { getTier } from '../../helpers/utils.mjs';
import DHAction from '../action/action.mjs';
import BaseDataItem from './base.mjs';
import ActionField from '../fields/actionField.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 ActionField())
};
}
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,23 @@
import BaseDataItem from './base.mjs';
import ActionField from '../fields/actionField.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(),
actions: new fields.ArrayField(new ActionField())
};
}
}

View file

@ -0,0 +1,91 @@
import ActionField from '../fields/actionField.mjs';
import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs';
import BaseDataItem from './base.mjs';
const featureSchema = () => {
return new foundry.data.fields.SchemaField({
name: new foundry.data.fields.StringField({ required: true }),
effects: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }),
actions: new foundry.data.fields.ArrayField(new ActionField())
});
};
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: featureSchema(),
specializationFeature: featureSchema(),
masteryFeature: featureSchema(),
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 !== data.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,96 @@
import BaseDataItem from './base.mjs';
import FormulaField from '../fields/formulaField.mjs';
import ActionField from '../fields/actionField.mjs';
import { weaponFeatures } from '../../config/itemConfig.mjs';
import { actionsTypes } from '../../data/_module.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(),
tier: new fields.NumberField({ required: true, integer: true, initial: 1, min: 1 }),
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'
})
}),
features: new fields.ArrayField(
new fields.SchemaField({
value: new fields.StringField({ required: true, choices: SYSTEM.ITEM.weaponFeatures, blank: true }),
effectIds: new fields.ArrayField(new fields.StringField({ required: true })),
actionIds: new fields.ArrayField(new fields.StringField({ required: true }))
})
),
actions: new fields.ArrayField(new ActionField())
};
}
async _preUpdate(changes, options, user) {
const allowed = await super._preUpdate(changes, options, user);
if (allowed === false) return false;
if (changes.system.features) {
const removed = this.features.filter(x => !changes.system.features.includes(x));
const added = changes.system.features.filter(x => !this.features.includes(x));
for (var feature of removed) {
for (var effectId of feature.effectIds) {
await this.parent.effects.get(effectId).delete();
}
changes.system.actions = this.actions.filter(x => !feature.actionIds.includes(x._id));
}
for (var feature of added) {
const featureData = weaponFeatures[feature.value];
if (featureData.effects?.length > 0) {
const embeddedItems = await this.parent.createEmbeddedDocuments('ActiveEffect', [
{
name: game.i18n.localize(featureData.label),
description: game.i18n.localize(featureData.description),
changes: featureData.effects.flatMap(x => x.changes)
}
]);
feature.effectIds = embeddedItems.map(x => x.id);
}
if (featureData.actions?.length > 0) {
const newActions = featureData.actions.map(action => {
const cls = actionsTypes[action.type];
return new cls(
{ ...action, _id: foundry.utils.randomID(), name: game.i18n.localize(action.name) },
{ parent: this }
);
});
changes.system.actions = [...this.actions, ...newActions];
feature.actionIds = newActions.map(x => x._id);
}
}
}
}
}

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

@ -1,7 +1,15 @@
import { fearDisplay } from '../../config/generalConfig.mjs';
export default class DhAppearance extends foundry.abstract.DataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
displayFear: new fields.StringField({
required: true,
choices: fearDisplay,
initial: fearDisplay.token.value,
label: 'DAGGERHEART.Settings.Appearance.FIELDS.displayFear.label'
}),
dualityColorScheme: new fields.StringField({
required: true,
choices: DualityRollColor,
@ -35,8 +43,6 @@ export default class DhAppearance extends foundry.abstract.DataModel {
})
};
}
static defaultSchema = {};
}
export const DualityRollColor = {

View file

@ -0,0 +1,11 @@
export default class DhAutomation extends foundry.abstract.DataModel {
static LOCALIZATION_PREFIXES = ['DAGGERHEART.Settings.Automation']; // Doesn't work for some reason
static defineSchema() {
const fields = foundry.data.fields;
return {
hope: new fields.BooleanField({ required: true, initial: false }),
actionPoints: new fields.BooleanField({ required: true, initial: false })
};
}
}

View file

@ -0,0 +1,56 @@
import { defaultRestOptions } from '../../config/generalConfig.mjs';
export default class DhHomebrew extends foundry.abstract.DataModel {
static LOCALIZATION_PREFIXES = ['DAGGERHEART.Settings.Homebrew']; // Doesn't work for some reason
static defineSchema() {
const fields = foundry.data.fields;
return {
maxFear: new fields.NumberField({
required: true,
integer: true,
min: 0,
initial: 12,
label: 'DAGGERHEART.Settings.Homebrew.FIELDS.maxFear.label'
}),
traitArray: new fields.ArrayField(new fields.NumberField({ required: true, integer: true }), {
initial: () => [2, 1, 1, 0, 0, -1]
}),
restMoves: new fields.SchemaField({
longRest: new fields.SchemaField({
nrChoices: new fields.NumberField({ required: true, integer: true, min: 1, initial: 2 }),
moves: new fields.TypedObjectField(
new fields.SchemaField({
name: new fields.StringField({ required: true }),
img: new fields.FilePathField({
initial: 'icons/magic/life/cross-worn-green.webp',
categories: ['IMAGE'],
base64: false
}),
description: new fields.HTMLField(),
actions: new fields.ArrayField(new fields.ObjectField())
}),
{ initial: defaultRestOptions.longRest() }
),
consequences: new fields.SchemaField({})
}),
shortRest: new fields.SchemaField({
nrChoices: new fields.NumberField({ required: true, integer: true, min: 1, initial: 2 }),
moves: new fields.TypedObjectField(
new fields.SchemaField({
name: new fields.StringField({ required: true }),
img: new fields.FilePathField({
initial: 'icons/magic/life/cross-worn-green.webp',
categories: ['IMAGE'],
base64: false
}),
description: new fields.HTMLField(),
actions: new fields.ArrayField(new fields.ObjectField())
}),
{ initial: defaultRestOptions.shortRest() }
)
})
})
};
}
}

View file

@ -0,0 +1,17 @@
export default class DhRangeMeasurement extends foundry.abstract.DataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
enabled: new fields.BooleanField({ required: true, initial: false, label: 'DAGGERHEART.General.Enabled' }),
melee: new fields.NumberField({ required: true, initial: 5, label: 'DAGGERHEART.Range.melee.name' }),
veryClose: new fields.NumberField({
required: true,
initial: 15,
label: 'DAGGERHEART.Range.veryClose.name'
}),
close: new fields.NumberField({ required: true, initial: 30, label: 'DAGGERHEART.Range.close.name' }),
far: new fields.NumberField({ required: true, initial: 60, label: 'DAGGERHEART.Range.far.name' }),
veryFar: new fields.NumberField({ required: true, initial: 120, label: 'DAGGERHEART.Range.veryFar.name' })
};
}
}

View file

@ -1,13 +1,26 @@
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 })
enabled: new fields.BooleanField({
required: true,
initial: false,
label: 'DAGGERHEART.Settings.VariantRules.FIELDS.actionTokens.enabled.label'
}),
tokens: new fields.NumberField({
required: true,
integer: true,
initial: 3,
label: 'DAGGERHEART.Settings.VariantRules.FIELDS.actionTokens.tokens.label'
})
}),
useCoins: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.Settings.VariantRules.FIELDS.useCoins.label'
})
};
}
static defaultSchema = {};
}

View file

@ -0,0 +1,7 @@
import DhAppearance from './Appearance.mjs';
import DhAutomation from './Automation.mjs';
import DhHomebrew from './Homebrew.mjs';
import DhRangeMeasurement from './RangeMeasurement.mjs';
import DhVariantRules from './VariantRules.mjs';
export { DhAppearance, DhAutomation, DhHomebrew, DhRangeMeasurement, DhVariantRules };

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

@ -1,97 +0,0 @@
export default class SelectDialog extends Dialog {
constructor(data, options) {
super(options);
this.data = {
title: data.title,
buttons: data.buttons,
content: foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/dialog/item-select.hbs',
{
items: data.choices
}
)
};
this.actor = data.actor;
this.actionCostMax = data.actionCostMax;
this.nrChoices = data.nrChoices;
this.validate = data.validate;
}
async getData(options = {}) {
let buttons = Object.keys(this.data.buttons).reduce((obj, key) => {
let b = this.data.buttons[key];
b.cssClass = (this.data.default === key ? [key, 'default', 'bright'] : [key]).join(' ');
if (b.condition !== false) obj[key] = b;
return obj;
}, {});
const content = await this.data.content;
return {
content: content,
buttons: buttons
};
}
activateListeners(html) {
super.activateListeners(html);
$(html).find('.item-button').click(this.selectChoice);
}
selectChoice = async event => {
if (this.validate) {
if (!this.validate(event.currentTarget.dataset.validateProp)) {
return;
}
}
event.currentTarget.classList.toggle('checked');
$(event.currentTarget).find('i')[0].classList.toggle('checked');
const buttons = $(this.element[0]).find('button.checked');
if (buttons.length === this.nrChoices) {
$(event.currentTarget).closest('.window-content').find('.confirm')[0].disabled = false;
} else {
$(event.currentTarget).closest('.window-content').find('.confirm')[0].disabled = true;
}
};
/**
*
* @param {*} data
* choices, actor, title, cancelMessage, nrChoices, validate
* @returns
*/
static async selectItem(data) {
return this.wait({
title: data.title ?? 'Selection',
buttons: {
no: {
icon: '<i class="fas fa-times"></i>',
label: game.i18n.localize('DAGGERHEART.General.Cancel'),
callback: _ => {
if (data.cancelMessage) {
ChatMessage.create({ content: data.cancelMessage });
}
return [];
}
},
confirm: {
icon: '<i class="fas fa-check"></i>',
label: game.i18n.localize('DAGGERHEART.General.OK'),
callback: html => {
const buttons = $(html).find('button.checked');
return buttons.map(key => Number.parseInt(buttons[key].dataset.index)).toArray();
},
disabled: true
}
},
choices: data.choices,
actor: data.actor,
nrChoices: data.nrChoices ?? 1,
validate: data.validate
});
}
}

View file

@ -1,3 +1,4 @@
export { default as DhpActor } from './actor.mjs';
export { default as DhpItem } from './item.mjs';
export { default as DhpCombat } from './combat.mjs';
export { default as DhActiveEffect } from './activeEffect.mjs';

View file

@ -0,0 +1,27 @@
export default class DhActiveEffect extends ActiveEffect {
get isSuppressed() {
if (['weapon', 'armor'].includes(this.parent.type)) {
return !this.parent.system.equipped;
}
return super.isSuppressed;
}
async _preCreate(data, options, user) {
const update = {};
if (!data.img) {
update.img = 'icons/magic/life/heart-cross-blue.webp';
}
if (Object.keys(update).length > 0) {
await this.updateSource(update);
}
await super._preCreate(data, options, user);
}
static applyField(model, change, field) {
change.value = Roll.safeEval(Roll.replaceFormulaData(change.value, change.effect.parent));
super.applyField(model, change, field);
}
}

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

Some files were not shown because too many files have changed in this diff Show more