Fix conflict

This commit is contained in:
Dapoolp 2025-06-22 22:23:08 +02:00
commit 22fa89b395
102 changed files with 5478 additions and 2183 deletions

View file

@ -13,5 +13,3 @@ export { default as DhpArmor } from './sheets/items/armor.mjs';
export { default as DhpChatMessage } from './chatMessage.mjs';
export { default as DhpEnvironment } from './sheets/environment.mjs';
export { default as DhActiveEffectConfig } from './sheets/activeEffectConfig.mjs';
export * as pseudoDocumentSheet from './sheets/pseudo-documents/_module.mjs';

View file

@ -0,0 +1,508 @@
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
? { ...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,
effects: Array.from(item.effects).map(x => x.toObject()),
uuid: item.uuid
};
} else if (item.type === 'community' && event.target.closest('.community-card')) {
this.setup.community = {
...item,
effects: Array.from(item.effects).map(x => x.toObject()),
uuid: item.uuid
};
} else if (item.type === 'class' && event.target.closest('.class-card')) {
this.setup.class = { ...item, effects: Array.from(item.effects).map(x => x.toObject()), 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,
effects: Array.from(item.effects).map(x => x.toObject()),
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

@ -9,10 +9,8 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
/* We can change to fully implementing the renderHTML function if needed, instead of augmenting it. */
const html = await super.renderHTML();
console.log(this.system)
if (
this.type === 'dualityRoll'
) {
if (this.type === 'dualityRoll') {
html.classList.add('duality');
/* const dualityResult = this.system.dualityResult; */
switch (this.system.roll.result.duality) {

View file

@ -0,0 +1,339 @@
import { countdownTypes } from '../config/generalConfig.mjs';
import { GMUpdateEvent, RefreshType, socketEvent } from '../helpers/socket.mjs';
import OwnershipSelection from './ownershipSelection.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
class Countdowns extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(basePath) {
super({});
this.basePath = basePath;
}
get title() {
return game.i18n.format('DAGGERHEART.Countdown.Title', {
type: game.i18n.localize(`DAGGERHEART.Countdown.Types.${this.basePath}`)
});
}
static DEFAULT_OPTIONS = {
classes: ['daggerheart', 'dh-style', 'countdown'],
tag: 'form',
position: { width: 740, height: 700 },
window: {
frame: true,
title: 'Countdowns',
resizable: true,
minimizable: true
},
actions: {
addCountdown: this.addCountdown,
removeCountdown: this.removeCountdown,
editImage: this.onEditImage,
openOwnership: this.openOwnership,
openCountdownOwnership: this.openCountdownOwnership
},
form: { handler: this.updateData, submitOnChange: true }
};
static PARTS = {
countdowns: {
template: 'systems/daggerheart/templates/views/countdowns.hbs',
scrollable: ['.expanded-view']
}
};
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
htmlElement.querySelectorAll('.mini-countdown-container').forEach(element => {
element.addEventListener('click', event => this.updateCountdownValue.bind(this)(event, true));
element.addEventListener('contextmenu', event => this.updateCountdownValue.bind(this)(event, false));
});
}
async _onFirstRender(context, options) {
super._onFirstRender(context, options);
this.element.querySelector('.expanded-view').classList.toggle('hidden');
this.element.querySelector('.minimized-view').classList.toggle('hidden');
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
const countdownData = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns)[this.basePath];
context.isGM = game.user.isGM;
context.base = this.basePath;
context.canCreate = countdownData.playerOwnership[game.user.id].value === CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER;
context.source = {
...countdownData,
countdowns: Object.keys(countdownData.countdowns).reduce((acc, key) => {
const countdown = countdownData.countdowns[key];
const ownershipValue = countdown.playerOwnership[game.user.id].value;
if (ownershipValue > CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE) {
acc[key] = { ...countdown, canEdit: ownershipValue === CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER };
}
return acc;
}, {})
};
context.systemFields = countdownData.schema.fields;
context.countdownFields = context.systemFields.countdowns.element.fields;
context.minimized = this.minimized || _options.isFirstRender;
return context;
}
static async updateData(event, _, formData) {
const data = foundry.utils.expandObject(formData.object);
const newSetting = foundry.utils.mergeObject(
game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns).toObject(),
data
);
if (game.user.isGM) {
await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns, newSetting);
this.render();
} else {
await game.socket.emit(`system.${SYSTEM.id}`, {
action: socketEvent.GMUpdate,
data: {
action: GMUpdateEvent.UpdateSetting,
uuid: SYSTEM.SETTINGS.gameSettings.Countdowns,
update: newSetting
}
});
}
}
async minimize() {
await super.minimize();
this.element.querySelector('.expanded-view').classList.toggle('hidden');
this.element.querySelector('.minimized-view').classList.toggle('hidden');
}
async maximize() {
if (this.minimized) {
const settings = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns)[this.basePath];
if (settings.playerOwnership[game.user.id].value <= CONST.DOCUMENT_OWNERSHIP_LEVELS.LIMITED) {
ui.notifications.info(game.i18n.localize('DAGGERHEART.Countdown.Notifications.LimitedOwnership'));
return;
}
this.element.querySelector('.expanded-view').classList.toggle('hidden');
this.element.querySelector('.minimized-view').classList.toggle('hidden');
}
await super.maximize();
}
async updateSetting(update) {
if (game.user.isGM) {
await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns, update);
await game.socket.emit(`system.${SYSTEM.id}`, {
action: socketEvent.Refresh,
data: {
refreshType: RefreshType.Countdown,
application: `${this.basePath}-countdowns`
}
});
this.render();
} else {
await game.socket.emit(`system.${SYSTEM.id}`, {
action: socketEvent.GMUpdate,
data: {
action: GMUpdateEvent.UpdateSetting,
uuid: SYSTEM.SETTINGS.gameSettings.Countdowns,
update: update,
refresh: { refreshType: RefreshType.Countdown, application: `${this.basePath}-countdowns` }
}
});
}
}
static onEditImage(_, target) {
const setting = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns)[this.basePath];
const current = setting.countdowns[target.dataset.countdown].img;
const fp = new FilePicker({
current,
type: 'image',
callback: async path => this.updateImage.bind(this)(path, target.dataset.countdown),
top: this.position.top + 40,
left: this.position.left + 10
});
return fp.browse();
}
async updateImage(path, countdown) {
const setting = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns);
await setting.updateSource({
[`${this.basePath}.countdowns.${countdown}.img`]: path
});
await this.updateSetting(setting);
}
static openOwnership(_, target) {
new Promise((resolve, reject) => {
const setting = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns)[this.basePath];
const ownership = { default: setting.ownership.default, players: setting.playerOwnership };
new OwnershipSelection(resolve, reject, this.title, ownership).render(true);
}).then(async ownership => {
const setting = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns);
await setting.updateSource({
[`${this.basePath}.ownership`]: ownership
});
await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns, setting.toObject());
this.render();
});
}
static openCountdownOwnership(_, target) {
const countdownId = target.dataset.countdown;
new Promise((resolve, reject) => {
const countdown = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns)[this.basePath]
.countdowns[countdownId];
const ownership = { default: countdown.ownership.default, players: countdown.playerOwnership };
new OwnershipSelection(resolve, reject, countdown.name, ownership).render(true);
}).then(async ownership => {
const setting = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns);
await setting.updateSource({
[`${this.basePath}.countdowns.${countdownId}.ownership`]: ownership
});
await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns, setting);
this.render();
});
}
async updateCountdownValue(event, increase) {
const countdownSetting = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns);
const countdown = countdownSetting[this.basePath].countdowns[event.currentTarget.dataset.countdown];
if (countdown.playerOwnership[game.user.id] < CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER) {
return;
}
const currentValue = countdown.progress.current;
if (increase && currentValue === countdown.progress.max) return;
if (!increase && currentValue === 0) return;
await countdownSetting.updateSource({
[`${this.basePath}.countdowns.${event.currentTarget.dataset.countdown}.progress.current`]: increase
? currentValue + 1
: currentValue - 1
});
await this.updateSetting(countdownSetting.toObject());
}
static async addCountdown() {
const countdownSetting = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns);
await countdownSetting.updateSource({
[`${this.basePath}.countdowns.${foundry.utils.randomID()}`]: {
name: game.i18n.localize('DAGGERHEART.Countdown.NewCountdown'),
ownership: game.user.isGM
? {}
: {
players: {
[game.user.id]: { type: CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER }
}
}
}
});
await this.updateSetting(countdownSetting.toObject());
}
static async removeCountdown(_, target) {
const countdownSetting = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns);
const countdownName = countdownSetting[this.basePath].countdowns[target.dataset.countdown].name;
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.localize('DAGGERHEART.Countdown.RemoveCountdownTitle')
},
content: game.i18n.format('DAGGERHEART.Countdown.RemoveCountdownText', { name: countdownName })
});
if (!confirmed) return;
await countdownSetting.updateSource({ [`${this.basePath}.countdowns.-=${target.dataset.countdown}`]: null });
await this.updateSetting(countdownSetting.toObject());
}
async open() {
await this.render(true);
if (
Object.keys(game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns)[this.basePath].countdowns)
.length > 0
) {
this.minimize();
}
}
}
export class NarrativeCountdowns extends Countdowns {
constructor() {
super('narrative');
}
static DEFAULT_OPTIONS = {
id: 'narrative-countdowns'
};
}
export class EncounterCountdowns extends Countdowns {
constructor() {
super('encounter');
}
static DEFAULT_OPTIONS = {
id: 'encounter-countdowns'
};
}
export const registerCountdownApplicationHooks = () => {
const updateCountdowns = async shouldProgress => {
if (game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation).countdowns) {
const countdownSetting = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns);
for (let countdownCategoryKey in countdownSetting) {
const countdownCategory = countdownSetting[countdownCategoryKey];
for (let countdownKey in countdownCategory.countdowns) {
const countdown = countdownCategory.countdowns[countdownKey];
if (shouldProgress(countdown)) {
await countdownSetting.updateSource({
[`${countdownCategoryKey}.countdowns.${countdownKey}.progress.current`]:
countdown.progress.current - 1
});
await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns, countdownSetting);
foundry.applications.instances.get(`${countdownCategoryKey}-countdowns`)?.render();
}
}
}
}
};
Hooks.on(SYSTEM.HOOKS.characterAttack, async () => {
updateCountdowns(countdown => {
return (
countdown.progress.type.value === countdownTypes.characterAttack.id && countdown.progress.current > 0
);
});
});
Hooks.on(SYSTEM.HOOKS.spotlight, async () => {
updateCountdowns(countdown => {
return countdown.progress.type.value === countdownTypes.spotlight.id && countdown.progress.current > 0;
});
});
};

View file

@ -1,3 +1,5 @@
import { actionsTypes } from '../data/_module.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV2) {
@ -5,25 +7,25 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
super({});
this.actor = actor;
this.selectedActivity = null;
this.shortrest = shortrest;
this.customActivity = SYSTEM.GENERAL.downtime.custom;
const options = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Homebrew).restMoves;
this.moveData = shortrest ? options.shortRest : options.longRest;
}
get title() {
return `${this.actor.name} - ${this.shortrest ? 'Short Rest' : 'Long Rest'}`;
return '';
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'views', 'downtime'],
position: { width: 800, height: 'auto' },
position: { width: 680, height: 'auto' },
actions: {
selectActivity: this.selectActivity,
selectMove: this.selectMove,
takeDowntime: this.takeDowntime
},
form: { handler: this.updateData, submitOnChange: true }
form: { handler: this.updateData, submitOnChange: true, closeOnSubmit: false }
};
static PARTS = {
@ -33,51 +35,63 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
}
};
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
htmlElement
.querySelectorAll('.activity-image')
.forEach(element => element.addEventListener('contextmenu', this.deselectMove.bind(this)));
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.selectedActivity = this.selectedActivity;
context.options = this.shortrest ? SYSTEM.GENERAL.downtime.shortRest : SYSTEM.GENERAL.downtime.longRest;
context.customActivity = this.customActivity;
context.disabledDowntime =
!this.selectedActivity ||
(this.selectedActivity.id === this.customActivity.id &&
(!this.customActivity.name || !this.customActivity.description));
context.moveData = this.moveData;
context.nrCurrentChoices = Object.values(this.moveData.moves).reduce((acc, x) => acc + (x.selected ?? 0), 0);
context.disabledDowntime = context.nrCurrentChoices < context.moveData.nrChoices;
return context;
}
static selectActivity(_, button) {
const activity = button.dataset.activity;
this.selectedActivity =
activity === this.customActivity.id
? this.customActivity
: this.shortrest
? SYSTEM.GENERAL.downtime.shortRest[activity]
: SYSTEM.GENERAL.downtime.longRest[activity];
static selectMove(_, button) {
const nrSelected = Object.values(this.moveData.moves).reduce((acc, x) => acc + (x.selected ?? 0), 0);
if (nrSelected === this.moveData.nrChoices) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.Downtime.Notifications.NoMoreMoves'));
return;
}
const move = button.dataset.move;
this.moveData.moves[move].selected = this.moveData.moves[move].selected
? this.moveData.moves[move].selected + 1
: 1;
this.render();
}
deselectMove(event) {
const move = event.currentTarget.dataset.move;
this.moveData.moves[move].selected = this.moveData.moves[move].selected
? this.moveData.moves[move].selected - 1
: 0;
this.render();
}
static async takeDowntime() {
const refreshedFeatures = this.shortrest
? this.actor.system.refreshableFeatures.shortRest
: [...this.actor.system.refreshableFeatures.shortRest, ...this.actor.system.refreshableFeatures.longRest];
for (var feature of refreshedFeatures) {
await feature.system.refresh();
}
const moves = Object.values(this.moveData.moves).filter(x => x.selected);
const cls = getDocumentClass('ChatMessage');
const msg = new cls({
user: game.user.id,
system: {
moves: moves,
actor: this.actor.uuid
},
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/chat/downtime.hbs',
{
player: this.actor.name,
title: game.i18n.localize(this.selectedActivity.name),
img: this.selectedActivity.img,
description: game.i18n.localize(this.selectedActivity.description),
refreshedFeatures: refreshedFeatures
title: `${this.actor.name} - ${game.i18n.localize(`DAGGERHEART.Downtime.${this.shortRest ? 'ShortRest' : 'LongRest'}.title`)}`,
moves: moves
}
)
});

View file

@ -0,0 +1,72 @@
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class OwnershipSelection extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(resolve, reject, name, ownership) {
super({});
this.resolve = resolve;
this.reject = reject;
this.name = name;
this.ownership = ownership;
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'views', 'ownership-selection'],
position: {
width: 600,
height: 'auto'
},
form: { handler: this.updateData }
};
static PARTS = {
selection: {
template: 'systems/daggerheart/templates/views/ownershipSelection.hbs'
}
};
get title() {
return game.i18n.format('DAGGERHEART.OwnershipSelection.Title', { name: this.name });
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.ownershipOptions = Object.keys(CONST.DOCUMENT_OWNERSHIP_LEVELS).map(level => ({
value: CONST.DOCUMENT_OWNERSHIP_LEVELS[level],
label: game.i18n.localize(`OWNERSHIP.${level}`)
}));
context.ownership = {
default: this.ownership.default,
players: Object.keys(this.ownership.players).reduce((acc, x) => {
const user = game.users.get(x);
if (!user.isGM) {
acc[x] = {
img: user.character?.img ?? 'icons/svg/cowled.svg',
name: user.name,
ownership: this.ownership.players[x].value
};
}
return acc;
}, {})
};
return context;
}
static async updateData(event, _, formData) {
const { ownership } = foundry.utils.expandObject(formData.object);
this.resolve(ownership);
this.close(true);
}
async close(fromSave) {
if (!fromSave) {
this.reject();
}
await super.close();
}
}

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,178 +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';
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 DhCountdowns from '../data/countdowns.mjs';
import {
DhAppearance,
DhAutomation,
DhHomebrew,
DhRangeMeasurement,
DhVariantRules
} from '../data/settings/_module.mjs';
import {
DhAppearanceSettings,
DhAutomationSettings,
DhHomebrewSettings,
DhRangeMeasurementSettings,
DhVariantRuleSettings
} from './settings/_module.mjs';
export const registerDHSettings = () => {
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'),
registerMenuSettings();
registerMenus();
registerNonConfigSettings();
};
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, {
@ -188,143 +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'),
game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns, {
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: false,
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.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
type: DhCountdowns
});
};

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

@ -54,20 +54,11 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
static async save() {
await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance, this.settings.toObject());
/* const reload = await foundry.applications.api.DialogV2.confirm({
id: 'reload-world-confirm',
modal: true,
rejectClose: false,
window: { title: 'SETTINGS.ReloadPromptTitle' },
position: { width: 400 },
content: `<p>${game.i18n.localize('SETTINGS.ReloadPromptBody')}</p>`
});
if (reload) {
await game.socket.emit('reload');
foundry.utils.debouncedReload();
} */
document.body.classList.toggle('theme-colorful', game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance).dualityColorScheme === DualityRollColor.colorful.value);
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,144 @@
import { actionsTypes } from '../../../data/_module.mjs';
import DHActionConfig from '../../config/Action.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class DhSettingsActionView extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(resolve, reject, title, name, img, description, actions) {
super({});
this.resolve = resolve;
this.reject = reject;
this.viewTitle = title;
this.name = name;
this.img = img;
this.description = description;
this.actions = actions;
}
get title() {
return this.viewTitle;
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'setting', 'dh-style'],
position: { width: '400', height: 'auto' },
actions: {
editImage: this.onEditImage,
addItem: this.addItem,
editItem: this.editItem,
removeItem: this.removeItem,
resetMoves: this.resetMoves,
saveForm: this.saveForm
},
form: { handler: this.updateData, submitOnChange: true, closeOnSubmit: false }
};
static PARTS = {
header: { template: 'systems/daggerheart/templates/settings/components/action-view-header.hbs' },
main: {
template: 'systems/daggerheart/templates/settings/components/action-view.hbs'
},
footer: { template: 'systems/daggerheart/templates/settings/components/action-view-footer.hbs' }
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.name = this.name;
context.img = this.img;
context.description = this.description;
context.enrichedDescription = await foundry.applications.ux.TextEditor.enrichHTML(context.description);
context.actions = this.actions;
return context;
}
static async updateData(event, element, formData) {
const { name, img, description } = foundry.utils.expandObject(formData.object);
this.name = name;
this.description = description;
this.render();
}
static async saveForm(event) {
this.resolve({
name: this.name,
img: this.img,
description: this.description,
actions: this.actions
});
this.close(true);
}
static close(fromSave) {
if (!fromSave) {
this.reject();
}
super.close();
}
static onEditImage() {
const fp = new FilePicker({
current: this.img,
type: 'image',
callback: async path => {
this.img = path;
this.render();
},
top: this.position.top + 40,
left: this.position.left + 10
});
return fp.browse();
}
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 addItem() {
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)
});
this.actions.push(action);
this.render();
}
static async editItem(_, button) {
await new DHActionConfig(this.actions[button.dataset.id]).render(true);
}
static removeItem(event, button) {
this.actions = this.actions.filter((_, index) => index !== Number.parseInt(button.dataset.id));
this.render();
}
static resetMoves() {}
}

View file

@ -0,0 +1,157 @@
import { DhHomebrew } from '../../data/settings/_module.mjs';
import DhSettingsActionView from './components/settingsActionsView.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class DhHomebrewSettings 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: {
addItem: this.addItem,
editItem: this.editItem,
removeItem: this.removeItem,
resetMoves: this.resetMoves,
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 addItem(_, target) {
await this.settings.updateSource({
[`restMoves.${target.dataset.type}.moves.${foundry.utils.randomID()}`]: {
name: game.i18n.localize('DAGGERHEART.Settings.Homebrew.NewDowntimeMove'),
img: 'icons/magic/life/cross-worn-green.webp',
description: '',
actions: []
}
});
this.render();
}
static async editItem(_, target) {
const move = this.settings.restMoves[target.dataset.type].moves[target.dataset.id];
new Promise((resolve, reject) => {
new DhSettingsActionView(
resolve,
reject,
game.i18n.localize('DAGGERHEART.Settings.Homebrew.DowntimeMoves'),
move.name,
move.img,
move.description,
move.actions
).render(true);
}).then(data => this.updateAction.bind(this)(data, target.dataset.type, target.dataset.id));
}
async updateAction(data, type, id) {
await this.settings.updateSource({
[`restMoves.${type}.moves.${id}`]: {
name: data.name,
img: data.img,
description: data.description
}
});
this.render();
}
static async removeItem(_, target) {
await this.settings.updateSource({
[`restMoves.${target.dataset.type}.moves.-=${target.dataset.id}`]: null
});
this.render();
}
static async resetMoves(_, target) {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.format('DAGGERHEART.Settings.Homebrew.ResetMovesTitle', {
type: game.i18n.localize(
`DAGGERHEART.Downtime.${target.dataset.type === 'shortRest' ? 'ShortRest' : 'LongRest'}.title`
)
})
},
content: game.i18n.localize('DAGGERHEART.Settings.Homebrew.ResetMovesText')
});
if (!confirmed) return;
const fields = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Homebrew).schema.fields;
const removeUpdate = Object.keys(this.settings.restMoves[target.dataset.type].moves).reduce((acc, key) => {
acc[`-=${key}`] = null;
return acc;
}, {});
const updateBase =
target.dataset.type === 'shortRest'
? fields.restMoves.fields.shortRest.fields
: fields.restMoves.fields.longRest.fields;
const update = {
nrChoices: updateBase.nrChoices.initial,
moves: Object.keys(updateBase.moves.initial).reduce((acc, key) => {
const move = updateBase.moves.initial[key];
acc[key] = {
...move,
name: game.i18n.localize(move.name),
description: game.i18n.localize(move.description)
};
return acc;
}, {})
};
await this.settings.updateSource({
[`restMoves.${target.dataset.type}`]: {
...update,
moves: {
...removeUpdate,
...update.moves
}
}
});
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

@ -1,4 +1,4 @@
export default class DhActiveEffectConfig extends ActiveEffectConfig {
export default class DhActiveEffectConfig extends foundry.applications.sheets.ActiveEffectConfig {
static DEFAULT_OPTIONS = {
classes: ['daggerheart', 'sheet', 'dh-style']
};

View file

@ -165,18 +165,18 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
context.config = SYSTEM;
const selectedAttributes = Object.values(this.document.system.traits).map(x => x.base);
context.abilityScoreArray = JSON.parse(
await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.General.AbilityArray)
).reduce((acc, x) => {
const selectedIndex = selectedAttributes.indexOf(x);
if (selectedIndex !== -1) {
selectedAttributes.splice(selectedIndex, 1);
} else {
acc.push({ name: x, value: x });
}
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;
}, []);
return acc;
}, []);
if (!context.abilityScoreArray.includes(0)) context.abilityScoreArray.push({ name: 0, value: 0 });
context.abilityScoresFinished = context.abilityScoreArray.every(x => x.value === 0);
@ -370,7 +370,11 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
static async attackRoll(event, button) {
const weapon = await fromUuid(button.dataset.weapon);
if (!weapon) return;
weapon.use(event);
const wasUsed = await weapon.use(event);
if (wasUsed) {
Hooks.callAll(SYSTEM.HOOKS.characterAttack, {});
}
}
static openLevelUp() {

View file

@ -221,46 +221,53 @@ export default class ClassSheet extends DaggerheartSheet(ItemSheetV2) {
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.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': 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': 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': 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, item.uuid]
'system.inventory.choiceA': [
...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, 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, item.uuid]
'system.inventory.choiceB': [
...this.document.system.inventory.choiceB.map(x => x.uuid),
item.uuid
]
});
}
}

View file

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

View file

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

View file

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

View file

@ -1,35 +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
@ -104,65 +111,99 @@ export const conditions = {
}
};
export const downtime = {
shortRest: {
export const defaultRestOptions = {
shortRest: () => ({
tendToWounds: {
id: 'tendToWounds',
name: 'DAGGERHEART.Downtime.TendToWounds.Name',
name: game.i18n.localize('DAGGERHEART.Downtime.ShortRest.TendToWounds.Name'),
img: 'icons/magic/life/cross-worn-green.webp',
description: 'DAGGERHEART.Downtime.TendToWounds.Description'
description: game.i18n.localize('DAGGERHEART.Downtime.ShortRest.TendToWounds.Description'),
actions: [
{
type: 'healing',
name: game.i18n.localize('DAGGERHEART.Downtime.ShortRest.TendToWounds.Name'),
img: 'icons/magic/life/cross-worn-green.webp',
actionType: 'action',
healing: {
type: 'health',
value: {
custom: {
enabled: true,
formula: '1d4 + 1' // should be 1d4 + {tier}. How to use the roll param?
}
}
}
}
]
},
clearStress: {
id: 'clearStress',
name: 'DAGGERHEART.Downtime.ClearStress.Name',
name: game.i18n.localize('DAGGERHEART.Downtime.ShortRest.ClearStress.Name'),
img: 'icons/magic/perception/eye-ringed-green.webp',
description: 'DAGGERHEART.Downtime.ClearStress.Description'
description: game.i18n.localize('DAGGERHEART.Downtime.ShortRest.ClearStress.Description'),
actions: [
{
type: 'healing',
name: game.i18n.localize('DAGGERHEART.Downtime.ShortRest.ClearStress.Name'),
img: 'icons/magic/perception/eye-ringed-green.webp',
actionType: 'action',
healing: {
type: 'stress',
value: {
custom: {
enabled: true,
formula: '1d4 + 1' // should be 1d4 + {tier}. How to use the roll param?
}
}
}
}
]
},
repairArmor: {
id: 'repairArmor',
name: 'DAGGERHEART.Downtime.RepairArmor.Name',
name: game.i18n.localize('DAGGERHEART.Downtime.ShortRest.RepairArmor.Name'),
img: 'icons/skills/trades/smithing-anvil-silver-red.webp',
description: 'DAGGERHEART.Downtime.RepairArmor.Description'
description: game.i18n.localize('DAGGERHEART.Downtime.ShortRest.RepairArmor.Description')
},
prepare: {
id: 'prepare',
name: 'DAGGERHEART.Downtime.Prepare.Name',
name: game.i18n.localize('DAGGERHEART.Downtime.ShortRest.Prepare.Name'),
img: 'icons/skills/trades/academics-merchant-scribe.webp',
description: 'DAGGERHEART.Downtime.Prepare.Description'
description: game.i18n.localize('DAGGERHEART.Downtime.ShortRest.Prepare.Description')
}
},
longRest: {
}),
longRest: () => ({
tendToWounds: {
id: 'tendToWounds',
name: 'DAGGERHEART.Downtime.TendToWounds.Name',
name: game.i18n.localize('DAGGERHEART.Downtime.LongRest.TendToWounds.Name'),
img: 'icons/magic/life/cross-worn-green.webp',
description: 'DAGGERHEART.Downtime.TendToWounds.Description'
description: game.i18n.localize('DAGGERHEART.Downtime.LongRest.TendToWounds.Description')
},
clearStress: {
id: 'clearStress',
name: 'DAGGERHEART.Downtime.ClearStress.Name',
name: game.i18n.localize('DAGGERHEART.Downtime.LongRest.ClearStress.Name'),
img: 'icons/magic/perception/eye-ringed-green.webp',
description: 'DAGGERHEART.Downtime.ClearStress.Description'
description: game.i18n.localize('DAGGERHEART.Downtime.LongRest.ClearStress.Description')
},
repairArmor: {
id: 'repairArmor',
name: 'DAGGERHEART.Downtime.RepairArmor.Name',
name: game.i18n.localize('DAGGERHEART.Downtime.LongRest.RepairArmor.Name'),
img: 'icons/skills/trades/smithing-anvil-silver-red.webp',
description: 'DAGGERHEART.Downtime.RepairArmor.Description'
description: game.i18n.localize('DAGGERHEART.Downtime.LongRest.RepairArmor.Description')
},
prepare: {
id: 'prepare',
name: 'DAGGERHEART.Downtime.Prepare.Name',
name: game.i18n.localize('DAGGERHEART.Downtime.LongRest.Prepare.Name'),
img: 'icons/skills/trades/academics-merchant-scribe.webp',
description: 'DAGGERHEART.Downtime.Prepare.Description'
description: game.i18n.localize('DAGGERHEART.Downtime.LongRest.Prepare.Description')
},
workOnAProject: {
id: 'workOnAProject',
name: 'DAGGERHEART.Downtime.WorkOnAProject.Name',
name: game.i18n.localize('DAGGERHEART.Downtime.LongRest.WorkOnAProject.Name'),
img: 'icons/skills/social/thumbsup-approval-like.webp',
description: 'DAGGERHEART.Downtime.WorkOnAProject.Description'
description: game.i18n.localize('DAGGERHEART.Downtime.LongRest.WorkOnAProject.Description')
}
},
}),
custom: {
id: 'customActivity',
name: '',
@ -353,6 +394,20 @@ export const abilityCosts = {
}
};
export const countdownTypes = {
spotlight: {
id: 'spotlight',
label: 'DAGGERHEART.Countdown.Type.Spotlight'
},
characterAttack: {
id: 'characterAttack',
label: 'DAGGERHEART.Countdown.Type.CharacterAttack'
},
custom: {
id: 'custom',
label: 'DAGGERHEART.Countdown.Type.Custom'
}
};
export const rollTypes = {
weapon: {
id: 'weapon',
@ -367,3 +422,9 @@ export const rollTypes = {
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

@ -0,0 +1,4 @@
export const hooks = {
characterAttack: 'characterAttackHook',
spotlight: 'spotlightHook'
};

View file

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

View file

@ -18,23 +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',
Automation: 'Automation',
Homebrew: 'Homebrew',
RangeMeasurement: 'RangeMeasurement',
appearance: 'Appearance',
variantRules: 'VariantRules'
variantRules: 'VariantRules',
Resources: {
Fear: 'ResourcesFear'
},
LevelTiers: 'LevelTiers',
Countdowns: 'Countdowns'
};
export const DualityRollColor = {

View file

@ -3,9 +3,9 @@ import * as DOMAIN from './domainConfig.mjs';
import * as ACTOR from './actorConfig.mjs';
import * as ITEM from './itemConfig.mjs';
import * as SETTINGS from './settingsConfig.mjs';
import { hooks as HOOKS } from './hooksConfig.mjs';
import * as EFFECTS from './effectConfig.mjs';
import * as ACTIONS from './actionConfig.mjs';
import pseudoDocuments from "./pseudoConfig.mjs";
export const SYSTEM_ID = 'daggerheart';
@ -16,7 +16,7 @@ export const SYSTEM = {
ACTOR,
ITEM,
SETTINGS,
HOOKS,
EFFECTS,
ACTIONS,
pseudoDocuments
ACTIONS
};

View file

@ -1,5 +1,3 @@
export { default as DhClass } from './item/class.mjs';
export { default as DhSubclass } from './item/subclass.mjs';
export { default as DhCombat } from './combat.mjs';
export { default as DhCombatant } from './combatant.mjs';
@ -8,4 +6,3 @@ export * as items from './item/_module.mjs';
export { actionsTypes } from './action/_module.mjs';
export * as messages from './chat-message/_modules.mjs';
export * as fields from './fields/_module.mjs';
export * as pseudoDocuments from './pseudo-documents/_module.mjs';

View file

@ -5,7 +5,7 @@ import BaseDataActor from './base.mjs';
const attributeField = () =>
new foundry.data.fields.SchemaField({
value: new foundry.data.fields.NumberField({ initial: 0, integer: true }),
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 })
});
@ -54,13 +54,7 @@ export default class DhCharacter extends BaseDataActor {
description: new fields.StringField({}),
value: new fields.NumberField({ integer: true, initial: 0 }),
bonus: new fields.NumberField({ integer: true, initial: 0 })
}),
{
initial: {
[foundry.utils.randomID()]: { description: '', value: 2 },
[foundry.utils.randomID()]: { description: '', value: 2 }
}
}
})
),
gold: new fields.SchemaField({
coins: new fields.NumberField({ initial: 0, integer: true }),
@ -93,6 +87,14 @@ export default class DhCharacter extends BaseDataActor {
};
}
get tier() {
return this.levelData.level.current === 1
? 1
: Object.values(game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.LevelTiers).tiers).find(
tier => currentLevel >= tier.levels.start && currentLevel <= tier.levels.end
).tier;
}
get ancestry() {
return this.parent.items.find(x => x.type === 'ancestry') ?? null;
}
@ -140,19 +142,6 @@ export default class DhCharacter extends BaseDataActor {
: 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;
@ -235,7 +224,7 @@ export default class DhCharacter extends BaseDataActor {
for (var traitKey in this.traits) {
var trait = this.traits[traitKey];
trait.total = trait.value + trait.bonus;
trait.total = (trait.value ?? 0) + trait.bonus;
}
for (var experienceKey in this.experiences) {
@ -248,6 +237,14 @@ export default class DhCharacter extends BaseDataActor {
this.evasion.total = (this.class?.evasion ?? 0) + this.evasion.bonus;
this.proficiency.total = this.proficiency.value + this.proficiency.bonus;
}
getRollData() {
const data = super.getRollData();
return {
...data,
tier: this.tier
};
}
}
class DhPCLevelData extends foundry.abstract.DataModel {

View file

@ -18,4 +18,4 @@ export const config = {
damageRoll: DHDamageRoll,
dualityRoll: DHDualityRoll,
applyEffect: DHApplyEffect
};
};

139
module/data/countdowns.mjs Normal file
View file

@ -0,0 +1,139 @@
import { countdownTypes } from '../config/generalConfig.mjs';
import { RefreshType, socketEvent } from '../helpers/socket.mjs';
export default class DhCountdowns extends foundry.abstract.DataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
narrative: new fields.EmbeddedDataField(DhCountdownData),
encounter: new fields.EmbeddedDataField(DhCountdownData)
};
}
static CountdownCategories = { narrative: 'narrative', combat: 'combat' };
}
class DhCountdownData extends foundry.abstract.DataModel {
static LOCALIZATION_PREFIXES = ['DAGGERHEART.Countdown']; // Nots ure why this won't work. Setting labels manually for now
static defineSchema() {
const fields = foundry.data.fields;
return {
countdowns: new fields.TypedObjectField(new fields.EmbeddedDataField(DhCountdown)),
ownership: new fields.SchemaField({
default: new fields.NumberField({
required: true,
choices: Object.values(CONST.DOCUMENT_OWNERSHIP_LEVELS),
initial: CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE
}),
players: new fields.TypedObjectField(
new fields.SchemaField({
type: new fields.NumberField({
required: true,
choices: Object.values(CONST.DOCUMENT_OWNERSHIP_LEVELS),
initial: CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT
})
})
)
})
};
}
get playerOwnership() {
return Array.from(game.users).reduce((acc, user) => {
acc[user.id] = {
value: user.isGM
? CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER
: this.ownership.players[user.id] && this.ownership.players[user.id].type !== -1
? this.ownership.players[user.id].type
: this.ownership.default,
isGM: user.isGM
};
return acc;
}, {});
}
}
class DhCountdown extends foundry.abstract.DataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
name: new fields.StringField({
required: true,
label: 'DAGGERHEART.Countdown.FIELDS.countdowns.element.name.label'
}),
img: new fields.FilePathField({
categories: ['IMAGE'],
base64: false,
initial: 'icons/magic/time/hourglass-yellow-green.webp'
}),
ownership: new fields.SchemaField({
default: new fields.NumberField({
required: true,
choices: Object.values(CONST.DOCUMENT_OWNERSHIP_LEVELS),
initial: CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE
}),
players: new fields.TypedObjectField(
new fields.SchemaField({
type: new fields.NumberField({
required: true,
choices: Object.values(CONST.DOCUMENT_OWNERSHIP_LEVELS),
initial: CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT
})
})
)
}),
progress: new fields.SchemaField({
current: new fields.NumberField({
required: true,
integer: true,
initial: 1,
label: 'DAGGERHEART.Countdown.FIELDS.countdowns.element.progress.current.label'
}),
max: new fields.NumberField({
required: true,
integer: true,
initial: 1,
label: 'DAGGERHEART.Countdown.FIELDS.countdowns.element.progress.max.label'
}),
type: new fields.SchemaField({
value: new fields.StringField({
required: true,
choices: countdownTypes,
initial: countdownTypes.spotlight.id,
label: 'DAGGERHEART.Countdown.FIELDS.countdowns.element.progress.type.value.label'
}),
label: new fields.StringField({
label: 'DAGGERHEART.Countdown.FIELDS.countdowns.element.progress.type.label.label'
})
})
})
};
}
get playerOwnership() {
return Array.from(game.users).reduce((acc, user) => {
acc[user.id] = {
value: user.isGM
? CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER
: this.ownership.players[user.id] && this.ownership.players[user.id].type !== -1
? this.ownership.players[user.id].type
: this.ownership.default,
isGM: user.isGM
};
return acc;
}, {});
}
}
export const registerCountdownHooks = () => {
Hooks.on(socketEvent.Refresh, ({ refreshType, application }) => {
if (refreshType === RefreshType.Countdown) {
foundry.applications.instances.get(application)?.render();
return false;
}
});
};

View file

@ -1,3 +1,3 @@
export { default as FormulaField } from './formulaField.mjs';
export { default as ForeignDocumentUUIDField } from './foreignDocumentUUIDField.mjs';
export { default as PseudoDocumentsField } from './pseudoDocumentsField.mjs';
export { default as ForeignDocumentUUIDArrayField } from './foreignDocumentUUIDArrayField.mjs';

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

@ -23,7 +23,7 @@ export default class ForeignDocumentUUIDField extends foundry.data.fields.Docume
/**@override */
initialize(value, _model, _options = {}) {
if (this.idOnly) return value;
return (() => {
return () => {
try {
const doc = fromUuidSync(value);
return doc;
@ -31,7 +31,7 @@ export default class ForeignDocumentUUIDField extends foundry.data.fields.Docume
console.error(error);
return value ?? null;
}
})();
};
}
/**@override */

View file

@ -1,56 +0,0 @@
import PseudoDocument from '../pseudo-documents/base/pseudoDocument.mjs';
const { TypedObjectField, TypedSchemaField } = foundry.data.fields;
/**
* @typedef _PseudoDocumentsFieldOptions
* @property {Number} [max] - The maximum amount of elements (default: `Infinity`)
* @property {String[]} [validTypes] - Allowed pseudo-documents types (default: `[]`)
* @property {Function} [validateKey] - callback for validate keys of the object;
* @typedef {foundry.data.types.DataFieldOptions & _PseudoDocumentsFieldOptions} PseudoDocumentsFieldOptions
*/
export default class PseudoDocumentsField extends TypedObjectField {
/**
* @param {PseudoDocument} model - The PseudoDocument of each entry in this collection.
* @param {PseudoDocumentsFieldOptions} [options] - Options which configure the behavior of the field
* @param {foundry.data.types.DataFieldContext} [context] - Additional context which describes the field
*/
constructor(model, options = {}, context = {}) {
options.validateKey ||= key => foundry.data.validators.isValidId(key);
if (!foundry.utils.isSubclass(model, PseudoDocument)) throw new Error('The model must be a PseudoDocument');
const allTypes = model.TYPES;
const filteredTypes = options.validTypes
? Object.fromEntries(
Object.entries(allTypes).filter(([key]) => options.validTypes.includes(key))
)
: allTypes;
const field = new TypedSchemaField(filteredTypes);
super(field, options, context);
}
/** @inheritdoc */
static get _defaults() {
return Object.assign(super._defaults, {
max: Infinity,
validTypes: []
});
}
/** @override */
_validateType(value, options = {}) {
if (Object.keys(value).length > this.max) throw new Error(`cannot have more than ${this.max} elements`);
return super._validateType(value, options);
}
/** @override */
initialize(value, model, options = {}) {
if (!value) return;
value = super.initialize(value, model, options);
const collection = new foundry.utils.Collection(Object.values(value).map(d => [d._id, d]));
return collection;
}
}

View file

@ -5,52 +5,49 @@
* @property {string} type - The system type that this data model represents.
* @property {boolean} hasDescription - Indicates whether items of this type have description field
* @property {boolean} isQuantifiable - Indicates whether items of this type have quantity field
* @property {Record<string,string>} embedded - Record of document names of pseudo-documents and the path to the collection
*/
const fields = foundry.data.fields;
export default class BaseDataItem extends foundry.abstract.TypeDataModel {
/** @returns {ItemDataModelMetadata}*/
static get metadata() {
return {
label: "Base Item",
type: "base",
hasDescription: false,
isQuantifiable: false,
embedded: {},
};
}
/** @returns {ItemDataModelMetadata}*/
static get metadata() {
return {
label: 'Base Item',
type: 'base',
hasDescription: false,
isQuantifiable: false
};
}
/** @inheritDoc */
static defineSchema() {
const schema = {};
/** @inheritDoc */
static defineSchema() {
const schema = {};
if (this.metadata.hasDescription)
schema.description = new fields.HTMLField({ required: true, nullable: true });
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 });
if (this.metadata.isQuantifiable)
schema.quantity = new fields.NumberField({ integer: true, initial: 1, min: 0, required: true });
return schema;
}
return schema;
}
/**
* Convenient access to the item's actor, if it exists.
* @returns {foundry.documents.Actor | null}
*/
get actor() {
return this.parent.actor;
}
/**
* 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;
}
}
/**
* 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

@ -1,5 +1,6 @@
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 {
@ -18,23 +19,16 @@ export default class DHClass extends BaseDataItem {
return {
...super.defineSchema(),
domains: new fields.ArrayField(new fields.StringField(), { max: 2 }),
classItems: new fields.ArrayField(new ForeignDocumentUUIDField({ type: 'Item' })),
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 fields.ArrayField(
new ForeignDocumentUUIDField({ type: 'Item', required: false, nullable: true, initial: undefined })
),
subclasses: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }),
inventory: new fields.SchemaField({
take: new fields.ArrayField(
new ForeignDocumentUUIDField({ type: 'Item', required: false, nullable: true, initial: undefined })
),
choiceA: new fields.ArrayField(
new ForeignDocumentUUIDField({ type: 'Item', required: false, nullable: true, initial: undefined })
),
choiceB: new fields.ArrayField(
new ForeignDocumentUUIDField({ type: 'Item', required: false, nullable: true, initial: undefined })
)
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({

View file

@ -1,14 +1,14 @@
import BaseDataItem from "./base.mjs";
import BaseDataItem from './base.mjs';
import ActionField from '../fields/actionField.mjs';
export default class DHConsumable extends BaseDataItem {
/** @inheritDoc */
/** @inheritDoc */
static get metadata() {
return foundry.utils.mergeObject(super.metadata, {
label: "TYPES.Item.consumable",
type: "consumable",
label: 'TYPES.Item.consumable',
type: 'consumable',
hasDescription: true,
isQuantifiable: true,
isQuantifiable: true
});
}

View file

@ -5,10 +5,10 @@ export default class DHMiscellaneous extends BaseDataItem {
/** @inheritDoc */
static get metadata() {
return foundry.utils.mergeObject(super.metadata, {
label: "TYPES.Item.miscellaneous",
type: "miscellaneous",
label: 'TYPES.Item.miscellaneous',
type: 'miscellaneous',
hasDescription: true,
isQuantifiable: true,
isQuantifiable: true
});
}

View file

@ -1,11 +1,11 @@
import ActionField from '../fields/actionField.mjs';
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.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 foundry.data.fields.ArrayField(new ForeignDocumentUUIDField({ type: 'ActiveEffect' })),
effects: new ForeignDocumentUUIDArrayField({ type: 'ActiveEffect', required: false }),
actions: new foundry.data.fields.ArrayField(new ActionField())
});
};
@ -64,7 +64,7 @@ export default class DHSubclass extends BaseDataItem {
} else if (subclassData) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.Item.Errors.SubclassAlreadySelected'));
return false;
} else if (classData.system.subclasses.every(x => x.uuid !== `Item.${data._id}`)) {
} 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;
}

View file

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

View file

@ -1,213 +0,0 @@
/**
* @typedef {object} PseudoDocumentMetadata
* @property {string} name - The document name of this pseudo-document
* @property {Record<string, string>} embedded - Record of document names and their collection paths
* @property {typeof foundry.applications.api.ApplicationV2} [sheetClass] - The class used to render this pseudo-document
* @property {string} defaultArtwork - The default image used for newly created documents
*/
/**
* @class Base class for pseudo-documents
* @extends {foundry.abstract.DataModel}
*/
export default class BasePseudoDocument extends foundry.abstract.DataModel {
/**
* Pseudo-document metadata.
* @returns {PseudoDocumentMetadata}
*/
static get metadata() {
return {
name: '',
embedded: {},
defaultArtwork: foundry.documents.Item.DEFAULT_ICON,
sheetClass: CONFIG.daggerheart.pseudoDocuments.sheetClass,
};
}
/** @override */
static LOCALIZATION_PREFIXES = ['DOCUMENT'];
/** @inheritdoc */
static defineSchema() {
const { fields } = foundry.data;
return {
_id: new fields.DocumentIdField({ initial: () => foundry.utils.randomID() }),
name: new fields.StringField({ required: true, blank: false, textSearch: true }),
img: new fields.FilePathField({ categories: ['IMAGE'], initial: this.metadata.defaultArtwork }),
description: new fields.HTMLField({ textSearch: true })
};
}
/* -------------------------------------------- */
/* Instance Properties */
/* -------------------------------------------- */
/**
* The id of this pseudo-document.
* @type {string}
*/
get id() {
return this._id;
}
/* -------------------------------------------- */
/**
* The uuid of this document.
* @type {string}
*/
get uuid() {
let parent = this.parent;
while (!(parent instanceof BasePseudoDocument) && !(parent instanceof foundry.abstract.Document))
parent = parent.parent;
return [parent.uuid, this.constructor.metadata.name, this.id].join('.');
}
/* -------------------------------------------- */
/**
* The parent document of this pseudo-document.
* @type {foundry.abstract.Document}
*/
get document() {
let parent = this;
while (!(parent instanceof foundry.abstract.Document)) parent = parent.parent;
return parent;
}
/* -------------------------------------------- */
/**
* Item to which this PseudoDocument belongs, if applicable.
* @type {foundry.documents.Item|null}
*/
get item() {
return this.parent?.parent instanceof Item ? this.parent.parent : null;
}
/* -------------------------------------------- */
/**
* Actor to which this PseudoDocument's item belongs, if the item is embedded.
* @type {foundry.documents.Actor|null}
*/
get actor() {
return this.item?.parent ?? null;
}
/* -------------------------------------------- */
/**
* The property path to this pseudo document relative to its parent document.
* @type {string}
*/
get fieldPath() {
const fp = this.schema.fieldPath;
let path = fp.slice(0, fp.lastIndexOf('element') - 1);
if (this.parent instanceof BasePseudoDocument) {
path = [this.parent.fieldPath, this.parent.id, path].join('.');
}
return path;
}
/* -------------------------------------------- */
/* Embedded Document Methods */
/* -------------------------------------------- */
/**
* Retrieve an embedded pseudo-document.
* @param {string} embeddedName The document name of the embedded pseudo-document.
* @param {string} id The id of the embedded pseudo-document.
* @param {object} [options] Retrieval options.
* @param {boolean} [options.strinct] Throw an error if the embedded pseudo-document does not exist?
* @returns {PseudoDocument|null}
*/
getEmbeddedDocument(embeddedName, id, { strict = false } = {}) {
const embeds = this.constructor.metadata.embedded ?? {};
if (embeddedName in embeds) {
return foundry.utils.getProperty(this, embeds[embeddedName]).get(id, { strict }) ?? null;
}
return null;
}
/* -------------------------------------------- */
/* CRUD Operations */
/* -------------------------------------------- */
/**
* Does this pseudo-document exist in the document's source?
* @type {boolean}
*/
get isSource() {
const source = foundry.utils.getProperty(this.document._source, this.fieldPath);
if (foundry.utils.getType(source) !== 'Object') {
throw new Error('Source is not an object!');
}
return this.id in source;
}
/**
* Create a new instance of this pseudo-document.
* @param {object} [data] The data used for the creation.
* @param {object} operation The context of the update operation.
* @param {foundry.abstract.Document} operation.parent The parent of this document.
* @returns {Promise<foundry.abstract.Document>} A promise that resolves to the updated document.
*/
static async create(data = {}, { parent, ...operation } = {}) {
if (!parent) {
throw new Error('A parent document must be specified for the creation of a pseudo-document!');
}
const id =
operation.keepId && foundry.data.validators.isValidId(data._id) ? data._id : foundry.utils.randomID();
const fieldPath = parent.system.constructor.metadata.embedded?.[this.metadata.name];
if (!fieldPath) {
throw new Error(
`A ${parent.documentName} of type '${parent.type}' does not support ${this.metadata.name}!`
);
}
const update = { [`system.${fieldPath}.${id}`]: { ...data, _id: id } };
const updatedParent = await parent.update(update, operation);
return foundry.utils.getProperty(updatedParent, `system.${fieldPath}.${id}`);
}
/**
* Delete this pseudo-document.
* @param {object} [operation] The context of the operation.
* @returns {Promise<foundry.abstract.Document>} A promise that resolves to the updated document.
*/
async delete(operation = {}) {
if (!this.isSource) throw new Error('You cannot delete a non-source pseudo-document!');
const update = { [`${this.fieldPath}.-=${this.id}`]: null };
return this.document.update(update, operation);
}
/**
* Duplicate this pseudo-document.
* @returns {Promise<foundry.abstract.Document>} A promise that resolves to the updated document.
*/
async duplicate() {
if (!this.isSource) throw new Error('You cannot duplicate a non-source pseudo-document!');
const docData = foundry.utils.mergeObject(this.toObject(), {
name: game.i18n.format('DOCUMENT.CopyOf', { name: this.name })
});
return this.constructor.create(docData, { parent: this.document });
}
/**
* Update this pseudo-document.
* @param {object} [change] The change to perform.
* @param {object} [operation] The context of the operation.
* @returns {Promise<foundry.abstract.Document>} A promise that resolves to the updated document.
*/
async update(change = {}, operation = {}) {
if (!this.isSource) throw new Error('You cannot update a non-source pseudo-document!');
const path = [this.fieldPath, this.id].join('.');
const update = { [path]: change };
return this.document.update(update, operation);
}
}

View file

@ -1,59 +0,0 @@
import BasePseudoDocument from './base.mjs';
import SheetManagementMixin from './sheetManagementMixin.mjs';
/** @extends BasePseudoDocument */
export default class PseudoDocument extends SheetManagementMixin(BasePseudoDocument) {
static get TYPES() {
const { types } = CONFIG.daggerheart.pseudoDocuments[this.metadata.name];
const typeEntries = Object.entries(types).map(([key, { documentClass }]) => [key, documentClass]);
return (this._TYPES ??= Object.freeze(Object.fromEntries(typeEntries)));
}
static _TYPES;
/**
* The type of this shape.
* @type {string}
*/
static TYPE = '';
/* -------------------------------------------- */
static getTypesChoices(validTypes) {
const { types } = CONFIG.daggerheart.pseudoDocuments[model.metadata.name];
const typeEntries = Object.entries(types)
.map(([key, { label }]) => [key, label])
.filter(([key]) => !validTypes || validTypes.includes(key));
return Object.entries(typeEntries);
}
/* -------------------------------------------- */
/** @override */
static defineSchema() {
const { fields } = foundry.data;
return Object.assign(super.defineSchema(), {
type: new fields.StringField({
required: true,
blank: false,
initial: this.TYPE,
validate: value => value === this.TYPE,
validationError: `must be equal to "${this.TYPE}"`
})
});
}
/** @inheritdoc */
static async create(data = {}, { parent, ...operation } = {}) {
data = foundry.utils.deepClone(data);
if (!data.type) data.type = Object.keys(this.TYPES)[0];
if (!(data.type in this.TYPES)) {
throw new Error(
`The '${data.type}' type is not a valid type for a '${this.metadata.documentName}' pseudo-document!`
);
}
return super.create(data, { parent, ...operation });
}
}

View file

@ -1,158 +0,0 @@
import BasePseudoDocument from './base.mjs';
const { ApplicationV2 } = foundry.applications.api;
/**
* A mixin that adds sheet management capabilities to pseudo-documents
* @template {typeof BasePseudoDocument} T
* @param {T} Base
* @returns {T & typeof PseudoDocumentWithSheets}
*/
export default function SheetManagementMixin(Base) {
class PseudoDocumentWithSheets extends Base {
/**
* Reference to the sheet of this pseudo-document.
* @type {ApplicationV2|null}
*/
get sheet() {
if (this._sheet) return this._sheet;
const cls = this.constructor.metadata.sheetClass ?? ApplicationV2;
if (!foundry.utils.isSubclass(cls, ApplicationV2)) {
return void ui.notifications.error(
'Daggerheart | Error on PseudoDocument | sheetClass must be ApplicationV2'
);
}
const sheet = new cls({ document: this });
this._sheet = sheet;
return sheet;
}
/* -------------------------------------------- */
/* Static Properties */
/* -------------------------------------------- */
/**
* Set of apps what should be re-render.
* @type {Set<ApplicationV2>}
* @internal
*/
_apps = new Set();
/* -------------------------------------------- */
/**
* Existing sheets of a specific type for a specific document.
* @type {ApplicationV2 | null}
*/
_sheet = null;
/* -------------------------------------------- */
/* Display Methods */
/* -------------------------------------------- */
/**
* Render all the Application instances which are connected to this PseudoDocument.
* @param {ApplicationRenderOptions} [options] Rendering options.
*/
render(options) {
for (const app of this._apps ?? []) {
app.render({ window: { title: app.title }, ...options });
}
}
/* -------------------------------------------- */
/**
* Register an application to respond to updates to a certain document.
* @param {ApplicationV2} app Application to update.
* @internal
*/
_registerApp(app) {
this._apps.add(app);
}
/* -------------------------------------------- */
/**
* Remove an application from the render registry.
* @param {ApplicationV2} app Application to stop watching.
*/
_unregisterApp(app) {
this._apps.delete(app);
}
/* -------------------------------------------- */
/* Drag and Drop */
/* -------------------------------------------- */
/**
* Serialize salient information for this PseudoDocument when dragging it.
* @returns {object} An object of drag data.
*/
toDragData() {
const dragData = { type: this.documentName, data: this.toObject() };
if (this.id) dragData.uuid = this.uuid;
return dragData;
}
/* -------------------------------------------- */
/* Dialog Methods */
/* -------------------------------------------- */
/**
* Spawn a dialog for creating a new PseudoDocument.
* @param {object} [data] Data to pre-populate the document with.
* @param {object} context
* @param {foundry.documents.Item} context.parent A parent for the document.
* @param {string[]|null} [context.types] A list of types to restrict the choices to, or null for no restriction.
* @returns {Promise<BasePseudoDocument|null>}
*/
static async createDialog(data = {}, { parent, types = null, ...options } = {}) {
// TODO
}
/**
* Present a Dialog form to confirm deletion of this PseudoDocument.
* @param {object} [options] - Additional options passed to `DialogV2.confirm`;
* @returns {Promise<foundry.abstract.Document>} A Promise which resolves to the deleted PseudoDocument.
*/
async deleteDialog(options = {}) {
const type = game.i18n.localize(this.constructor.metadata.label);
const content = options.content ?? `<p>
<strong>${game.i18n.localize("AreYouSure")}</strong>
${game.i18n.format("SIDEBAR.DeleteWarning", { type })}
</p>`;
return foundry.applications.api.DialogV2.confirm({
content,
yes: { callback: () => this.delete(operation) },
window: {
icon: "fa-solid fa-trash",
title: `${game.i18n.format("DOCUMENT.Delete", { type })}: ${this.name}`
},
...options
});
}
/**
* Gets the default new name for a Document
* @param {object} collection - Collection of Documents
* @returns {string}
*/
static defaultName(collection) {
const documentName = this.metadata.name;
const takenNames = new Set();
for (const document of collection) takenNames.add(document.name);
const config = CONFIG.daggerheart.pseudoDocuments[documentName];
const baseName = game.i18n.localize(config.label);
let name = baseName;
let index = 1;
while (takenNames.has(name)) name = `${baseName} (${++index})`;
return name;
}
}
return PseudoDocumentWithSheets;
}

View file

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

View file

@ -1,24 +0,0 @@
import PseudoDocument from '../base/pseudoDocument.mjs';
export default class BaseFeatureData extends PseudoDocument {
/**@inheritdoc */
static get metadata() {
return foundry.utils.mergeObject(
super.metadata,
{
name: 'feature',
embedded: {},
//sheetClass: null //TODO: define feature-sheet
},
{ inplace: false }
);
}
static defineSchema() {
const { fields } = foundry.data;
const schema = super.defineSchema();
return Object.assign(schema, {
subtype: new fields.StringField({ initial: 'test' })
});
}
}

View file

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

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,12 @@
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 }),
countdowns: new fields.BooleanField({ requireD: true, initial: false })
};
}
}

View file

@ -0,0 +1,55 @@
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() }
)
}),
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

@ -5,12 +5,22 @@ export default class DhVariantRules extends foundry.abstract.DataModel {
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 })
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,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

@ -324,7 +324,7 @@ export default class DhpActor extends Actor {
);
if (this.type === 'character') {
const automateHope = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation.Hope);
const automateHope = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation).hope;
if (automateHope && result.hopeUsed) {
await this.update({
@ -354,7 +354,7 @@ export default class DhpActor extends Actor {
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) &&
game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation).hope &&
config.roll.type === 'action'
) {
if (hope > fear) {

View file

@ -1,7 +1,7 @@
import { abilities } from '../config/actorConfig.mjs';
import { rollCommandToJSON } from '../helpers/utils.mjs';
import { getCommandTarget, rollCommandToJSON } from '../helpers/utils.mjs';
export function dualityRollEnricher(match, _options) {
export default function DhDualityRollEnricher(match, _options) {
const roll = rollCommandToJSON(match[1]);
if (!roll) return match[0];
@ -39,3 +39,24 @@ export function getDualityMessage(roll) {
return dualityElement;
}
export const renderDualityButton = async event => {
const button = event.currentTarget,
traitValue = button.dataset.trait?.toLowerCase(),
target = getCommandTarget();
if (!target) return;
const config = {
event: event,
title: button.dataset.title,
roll: {
modifier: traitValue ? target.system.traits[traitValue].value : null,
label: button.dataset.label,
type: button.dataset.actionType ?? null // Need check
},
chatMessage: {
template: 'systems/daggerheart/templates/chat/duality-roll.hbs'
}
};
await target.diceRoll(config);
};

View file

@ -0,0 +1,60 @@
import { range as configRange } from '../config/generalConfig.mjs';
export default function DhTemplateEnricher(match, _options) {
const parts = match[1].split('|').map(x => x.trim());
let type = null,
range = null;
parts.forEach(part => {
const split = part.split(':').map(x => x.toLowerCase().trim());
if (split.length === 2) {
switch (split[0]) {
case 'type':
const matchedType = Object.values(CONST.MEASURED_TEMPLATE_TYPES).find(
x => x.toLowerCase() === split[1]
);
type = matchedType;
break;
case 'range':
const matchedRange = Object.values(configRange).find(
x => x.id.toLowerCase() === split[1] || x.short === split[1]
);
range = matchedRange?.id;
break;
}
}
});
if (!type || !range) return match[0];
const templateElement = document.createElement('span');
templateElement.innerHTML = `
<button class="measured-template-button" data-type="${type}" data-range="${range}">
${game.i18n.localize(`TEMPLATE.TYPES.${type}`)} - ${game.i18n.localize(`DAGGERHEART.Range.${range}.name`)}
</button>
`;
return templateElement;
}
export const renderMeasuredTemplate = async event => {
const button = event.currentTarget,
type = button.dataset.type,
range = button.dataset.range;
if (!type || !range || !game.canvas.scene) return;
const distance = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.RangeMeasurement)[range];
const { width, height } = game.canvas.scene.dimensions;
canvas.scene.createEmbeddedDocuments('MeasuredTemplate', [
{
x: width / 2,
y: height / 2,
t: type,
distance: distance,
width: type === CONST.MEASURED_TEMPLATE_TYPES.RAY ? 5 : undefined,
angle: type === CONST.MEASURED_TEMPLATE_TYPES.CONE ? CONFIG.MeasuredTemplate.defaults.angle : undefined
}
]);
};

View file

@ -0,0 +1,4 @@
import DhDualityRollEnricher from './DualityRollEnricher.mjs';
import DhTemplateEnricher from './TemplateEnricher.mjs';
export { DhDualityRollEnricher, DhTemplateEnricher };

View file

@ -1,20 +1,68 @@
export function handleSocketEvent({ action = null, data = {} } = {}) {
switch (action) {
case socketEvent.GMUpdate:
Hooks.callAll(socketEvent.GMUpdate, data.action, data.uuid, data.update);
Hooks.callAll(socketEvent.GMUpdate, data);
break;
case socketEvent.DhpFearUpdate:
Hooks.callAll(socketEvent.DhpFearUpdate);
break;
case socketEvent.Refresh:
Hooks.call(socketEvent.Refresh, data);
break;
}
}
export const socketEvent = {
GMUpdate: 'DhpGMUpdate',
DhpFearUpdate: 'DhpFearUpdate'
GMUpdate: 'DhGMUpdate',
Refresh: 'DhRefresh',
DhpFearUpdate: 'DhFearUpdate'
};
export const GMUpdateEvent = {
UpdateDocument: 'DhpGMUpdateDocument',
UpdateFear: 'DhpUpdateFear'
UpdateDocument: 'DhGMUpdateDocument',
UpdateSetting: 'DhGMUpdateSetting',
UpdateFear: 'DhGMUpdateFear'
};
export const RefreshType = {
Countdown: 'DhCoundownRefresh'
};
export const registerSocketHooks = () => {
Hooks.on(socketEvent.GMUpdate, async data => {
if (game.user.isGM) {
const document = data.uuid ? await fromUuid(data.uuid) : null;
switch (data.action) {
case GMUpdateEvent.UpdateDocument:
if (document && data.update) {
await document.update(data.update);
}
break;
case GMUpdateEvent.UpdateSetting:
if (game.user.isGM) {
await game.settings.set(SYSTEM.id, data.uuid, data.update);
}
break;
case GMUpdateEvent.UpdateFear:
if (game.user.isGM) {
await game.settings.set(
SYSTEM.id,
SYSTEM.SETTINGS.gameSettings.Resources.Fear,
Math.max(Math.min(data.update, 6), 0)
);
Hooks.callAll(socketEvent.DhpFearUpdate);
await game.socket.emit(`system.${SYSTEM.id}`, { action: socketEvent.DhpFearUpdate });
}
break;
}
if (data.refresh) {
await game.socket.emit(`system.${SYSTEM.id}`, {
action: socketEvent.Refresh,
data: data.refresh
});
Hooks.call(socketEvent.Refresh, data.refresh);
}
}
});
};

View file

@ -0,0 +1,3 @@
import DhMeasuredTemplate from './measuredTemplate.mjs';
export { DhMeasuredTemplate };

View file

@ -0,0 +1,35 @@
export default class DhMeasuredTemplate extends foundry.canvas.placeables.MeasuredTemplate {
_refreshRulerText() {
super._refreshRulerText();
const rangeMeasurementSettings = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.RangeMeasurement);
if (rangeMeasurementSettings.enabled) {
const splitRulerText = this.ruler.text.split(' ');
if (splitRulerText.length > 0) {
const rulerValue = Number(splitRulerText[0]);
const vagueLabel = this.constructor.getDistanceLabel(rulerValue, rangeMeasurementSettings);
this.ruler.text = vagueLabel;
}
}
}
static getDistanceLabel(distance, settings) {
if (distance <= settings.melee) {
return game.i18n.localize('DAGGERHEART.Range.melee.name');
}
if (distance <= settings.veryClose) {
return game.i18n.localize('DAGGERHEART.Range.veryClose.name');
}
if (distance <= settings.close) {
return game.i18n.localize('DAGGERHEART.Range.close.name');
}
if (distance <= settings.far) {
return game.i18n.localize('DAGGERHEART.Range.far.name');
}
if (distance <= settings.veryFar) {
return game.i18n.localize('DAGGERHEART.Range.veryFar.name');
}
return '';
}
}

View file

@ -1,3 +1,5 @@
import { actionsTypes } from '../data/_module.mjs';
export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLog {
constructor() {
super();
@ -44,6 +46,9 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
html.querySelectorAll('.ability-use-button').forEach(element =>
element.addEventListener('click', event => this.abilityUseButton.bind(this)(event, data.message))
);
html.querySelectorAll('.action-use-button').forEach(element =>
element.addEventListener('click', event => this.actionUseButton.bind(this)(event, data.message))
);
};
setupHooks() {
@ -173,4 +178,16 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
const actor = game.actors.get(message.system.source.actor);
await actor.useAction(action);
};
actionUseButton = async (_, message) => {
const parent = await foundry.utils.fromUuid(message.system.actor);
const testAction = Object.values(message.system.moves)[0].actions[0];
const cls = actionsTypes[testAction.type];
const action = new cls(
{ ...testAction, _id: foundry.utils.randomID(), name: game.i18n.localize(testAction.name) },
{ parent: parent }
);
action.use();
};
}

View file

@ -1,9 +1,12 @@
import { EncounterCountdowns } from '../applications/countdowns.mjs';
export default class DhCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker {
static DEFAULT_OPTIONS = {
actions: {
requestSpotlight: this.requestSpotlight,
toggleSpotlight: this.toggleSpotlight,
setActionTokens: this.setActionTokens
setActionTokens: this.setActionTokens,
openCountdowns: this.openCountdowns
}
};
@ -83,6 +86,8 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
.map(x => x.id)
.indexOf(combatantId);
if (this.viewed.turn !== toggleTurn) Hooks.callAll(SYSTEM.HOOKS.spotlight, {});
await this.viewed.update({ turn: this.viewed.turn === toggleTurn ? null : toggleTurn });
await combatant.update({ 'system.spotlight.requesting': false });
}
@ -97,4 +102,8 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
await combatant.update({ 'system.actionTokens': newIndex });
this.render();
}
static openCountdowns() {
new EncounterCountdowns().open();
}
}

View file

@ -1,34 +1,18 @@
import DhMeasuredTemplate from '../placeables/measuredTemplate.mjs';
export default class DhpRuler extends foundry.canvas.interaction.Ruler {
_getWaypointLabelContext(waypoint, state) {
const context = super._getWaypointLabelContext(waypoint, state);
if (!context) return;
const range = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.General.RangeMeasurement);
const range = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.RangeMeasurement);
if (range.enabled) {
const distance = this.#getRangeLabel(waypoint.measurement.distance.toNearest(0.01), range);
const distance = DhMeasuredTemplate.getDistanceLabel(waypoint.measurement.distance.toNearest(0.01), range);
context.cost = { total: distance, units: null };
context.distance = { total: distance, units: null };
}
return context;
}
#getRangeLabel(distance, settings) {
if (distance <= settings.melee) {
return game.i18n.localize('DAGGERHEART.Range.Melee.Name');
}
if (distance <= settings.veryClose) {
return game.i18n.localize('DAGGERHEART.Range.VeryClose.Name');
}
if (distance <= settings.close) {
return game.i18n.localize('DAGGERHEART.Range.Close.Name');
}
if (distance <= settings.far) {
return game.i18n.localize('DAGGERHEART.Range.Far.Name');
}
if (distance <= settings.veryFar) {
return game.i18n.localize('DAGGERHEART.Range.VeryFar.Name');
}
}
}

View file

@ -1,34 +1,18 @@
import DhMeasuredTemplate from '../placeables/measuredTemplate.mjs';
export default class DhpTokenRuler extends foundry.canvas.placeables.tokens.TokenRuler {
_getWaypointLabelContext(waypoint, state) {
const context = super._getWaypointLabelContext(waypoint, state);
if (!context) return;
const range = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.General.RangeMeasurement);
const range = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.RangeMeasurement);
if (range.enabled) {
const distance = this.#getRangeLabel(waypoint.measurement.distance.toNearest(0.01), range);
const distance = DhMeasuredTemplate.getDistanceLabel(waypoint.measurement.distance.toNearest(0.01), range);
context.cost = { total: distance, units: null };
context.distance = { total: distance, units: null };
}
return context;
}
#getRangeLabel(distance, settings) {
if (distance <= settings.melee) {
return game.i18n.localize('DAGGERHEART.Range.Melee.Name');
}
if (distance <= settings.veryClose) {
return game.i18n.localize('DAGGERHEART.Range.VeryClose.Name');
}
if (distance <= settings.close) {
return game.i18n.localize('DAGGERHEART.Range.Close.Name');
}
if (distance <= settings.far) {
return game.i18n.localize('DAGGERHEART.Range.Far.Name');
}
if (distance <= settings.veryFar) {
return game.i18n.localize('DAGGERHEART.Range.VeryFar.Name');
}
}
}