Merge branch 'main' into feature/-179-apply-items-filter-in-actors-sheet

This commit is contained in:
Joaquin Pereyra 2025-07-02 23:32:19 -03:00
commit 09eaa0d771
143 changed files with 5790 additions and 2780 deletions

View file

@ -1,5 +1,6 @@
export { default as DhCharacterSheet } from './sheets/character.mjs';
export { default as DhpAdversarySheet } from './sheets/adversary.mjs';
export { default as DhCharacterSheet } from './sheets/actors/character.mjs';
export { default as DhpAdversarySheet } from './sheets/actors/adversary.mjs';
export { default as DhCompanionSheet } from './sheets/companion.mjs';
export { default as DhpClassSheet } from './sheets/items/class.mjs';
export { default as DhpSubclass } from './sheets/items/subclass.mjs';
export { default as DhpFeatureSheet } from './sheets/items/feature.mjs';
@ -11,9 +12,10 @@ export { default as DhpConsumable } from './sheets/items/consumable.mjs';
export { default as DhpWeapon } from './sheets/items/weapon.mjs';
export { default as DhpArmor } from './sheets/items/armor.mjs';
export { default as DhpChatMessage } from './chatMessage.mjs';
export { default as DhpEnvironment } from './sheets/environment.mjs';
export { default as DhpEnvironment } from './sheets/actors/environment.mjs';
export { default as DhActiveEffectConfig } from './sheets/activeEffectConfig.mjs';
export { default as DhContextMenu } from './contextMenu.mjs';
export { default as DhTooltipManager } from './tooltipManager.mjs';
export * as api from './sheets/api/_modules.mjs';
export * as ux from "./ux/_module.mjs";

View file

@ -143,7 +143,7 @@ export default class AncestrySelectionDialog extends HandlebarsApplicationMixin(
}
static _onEditImage() {
const fp = new FilePicker({
const fp = new foundry.applications.apps.FilePicker.implementation({
current: this.data.ancestryInfo.img,
type: 'image',
redirectToRoot: ['icons/svg/mystery-man.svg'],

View file

@ -1,6 +1,10 @@
export default class DhpChatMessage extends foundry.documents.ChatMessage {
async renderHTML() {
if(this.system.messageTemplate) this.content = await foundry.applications.handlebars.renderTemplate(this.system.messageTemplate, this.system);
if (this.system.messageTemplate)
this.content = await foundry.applications.handlebars.renderTemplate(
this.system.messageTemplate,
this.system
);
/* We can change to fully implementing the renderHTML function if needed, instead of augmenting it. */
const html = await super.renderHTML();
@ -14,7 +18,7 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
break;
case -1:
html.classList.add('fear');
break;
break;
default:
html.classList.add('critical');
break;

View file

@ -1,4 +1,4 @@
export default class DhContextMenu extends ContextMenu {
export default class DhContextMenu extends foundry.applications.ux.ContextMenu.implementation {
constructor(container, selector, menuItems, options) {
super(container, selector, menuItems, options);
@ -26,10 +26,16 @@ export default class DhContextMenu extends ContextMenu {
event.preventDefault();
event.stopPropagation();
const { clientX, clientY } = event;
const selector = "[data-item-id]";
const selector = '[data-item-id]';
const target = event.target.closest(selector) ?? event.currentTarget.closest(selector);
target?.dispatchEvent(new PointerEvent("contextmenu", {
view: window, bubbles: true, cancelable: true, clientX, clientY
}));
target?.dispatchEvent(
new PointerEvent('contextmenu', {
view: window,
bubbles: true,
cancelable: true,
clientX,
clientY
})
);
}
}

View file

@ -1,5 +1,6 @@
import { countdownTypes } from '../config/generalConfig.mjs';
import { GMUpdateEvent, RefreshType, socketEvent } from '../helpers/socket.mjs';
import constructHTMLButton from '../helpers/utils.mjs';
import OwnershipSelection from './ownershipSelection.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -25,14 +26,15 @@ class Countdowns extends HandlebarsApplicationMixin(ApplicationV2) {
frame: true,
title: 'Countdowns',
resizable: true,
minimizable: true
minimizable: false
},
actions: {
addCountdown: this.addCountdown,
removeCountdown: this.removeCountdown,
editImage: this.onEditImage,
openOwnership: this.openOwnership,
openCountdownOwnership: this.openCountdownOwnership
openCountdownOwnership: this.openCountdownOwnership,
toggleSimpleView: this.toggleSimpleView
},
form: { handler: this.updateData, submitOnChange: true }
};
@ -53,11 +55,47 @@ class Countdowns extends HandlebarsApplicationMixin(ApplicationV2) {
});
}
async _onFirstRender(context, options) {
super._onFirstRender(context, options);
async _preFirstRender(context, options) {
options.position =
game.user.getFlag(SYSTEM.id, SYSTEM.FLAGS[`${this.basePath}Countdown`].position) ??
Countdowns.DEFAULT_OPTIONS.position;
this.element.querySelector('.expanded-view').classList.toggle('hidden');
this.element.querySelector('.minimized-view').classList.toggle('hidden');
const viewSetting =
game.user.getFlag(SYSTEM.id, SYSTEM.FLAGS[`${this.basePath}Countdown`].simple) ?? !game.user.isGM;
this.simpleView =
game.user.isGM || !this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER) ? viewSetting : true;
context.simple = this.simpleView;
}
_onPosition(position) {
game.user.setFlag(SYSTEM.id, SYSTEM.FLAGS[`${this.basePath}Countdown`].position, position);
}
async _renderFrame(options) {
const frame = await super._renderFrame(options);
if (this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER)) {
const button = constructHTMLButton({
label: '',
classes: ['header-control', 'icon', 'fa-solid', 'fa-wrench'],
dataset: { action: 'toggleSimpleView', tooltip: 'DAGGERHEART.Countdown.ToggleSimple' }
});
this.window.controls.after(button);
}
return frame;
}
testUserPermission(level, exact, altSettings) {
if (game.user.isGM) return true;
const settings =
altSettings ?? game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns)[this.basePath];
const defaultAllowed = exact ? settings.ownership.default === level : settings.ownership.default >= level;
const userAllowed = exact
? settings.playerOwnership[game.user.id]?.value === level
: settings.playerOwnership[game.user.id]?.value >= level;
return defaultAllowed || userAllowed;
}
async _prepareContext(_options) {
@ -67,15 +105,17 @@ class Countdowns extends HandlebarsApplicationMixin(ApplicationV2) {
context.isGM = game.user.isGM;
context.base = this.basePath;
context.canCreate = countdownData.playerOwnership[game.user.id].value === CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER;
context.canCreate = this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER, true);
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 };
if (this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.LIMITED, false, countdown)) {
acc[key] = {
...countdown,
canEdit: this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER, true, countdown)
};
}
return acc;
@ -83,7 +123,7 @@ class Countdowns extends HandlebarsApplicationMixin(ApplicationV2) {
};
context.systemFields = countdownData.schema.fields;
context.countdownFields = context.systemFields.countdowns.element.fields;
context.minimized = this.minimized || _options.isFirstRender;
context.simple = this.simpleView;
return context;
}
@ -110,28 +150,6 @@ class Countdowns extends HandlebarsApplicationMixin(ApplicationV2) {
}
}
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);
@ -160,7 +178,7 @@ class Countdowns extends HandlebarsApplicationMixin(ApplicationV2) {
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({
const fp = new foundry.applications.apps.FilePicker.implementation({
current,
type: 'image',
callback: async path => this.updateImage.bind(this)(path, target.dataset.countdown),
@ -213,11 +231,17 @@ class Countdowns extends HandlebarsApplicationMixin(ApplicationV2) {
});
}
static async toggleSimpleView() {
this.simpleView = !this.simpleView;
await game.user.setFlag(SYSTEM.id, SYSTEM.FLAGS[`${this.basePath}Countdown`].simple, this.simpleView);
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) {
if (!this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER)) {
return;
}

View file

@ -0,0 +1,383 @@
import LevelUpBase from './levelup.mjs';
import { DhLevelup } from '../../data/levelup.mjs';
import { domains } from '../../config/domainConfig.mjs';
import { abilities } from '../../config/actorConfig.mjs';
export default class DhCharacterLevelUp extends LevelUpBase {
constructor(actor) {
super(actor);
this.levelTiers = this.addBonusChoices(game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.LevelTiers));
const playerLevelupData = actor.system.levelData;
this.levelup = new DhLevelup(DhLevelup.initializeData(this.levelTiers, playerLevelupData));
}
async _preparePartContext(partId, context) {
await super._preparePartContext(partId, context);
const currentLevel = this.levelup.levels[this.levelup.currentLevel];
switch (partId) {
case 'selections':
const advancementChoices = Object.keys(currentLevel.choices).reduce((acc, choiceKey) => {
Object.keys(currentLevel.choices[choiceKey]).forEach(checkboxNr => {
const checkbox = currentLevel.choices[choiceKey][checkboxNr];
const data = {
...checkbox,
path: `levels.${this.levelup.currentLevel}.choices.${choiceKey}.${checkboxNr}`,
level: this.levelup.currentLevel
};
if (!acc[choiceKey]) acc[choiceKey] = [];
acc[choiceKey].push(data);
});
return acc;
}, {});
const traits = Object.values(advancementChoices.trait ?? {});
const traitValues = traits.filter(trait => trait.data.length > 0).flatMap(trait => trait.data);
context.traits = {
values: traitValues,
active: traits.length > 0,
progress: {
selected: traitValues.length,
max: traits.reduce((acc, exp) => acc + exp.amount, 0)
}
};
const experienceIncreases = Object.values(advancementChoices.experience ?? {});
const experienceIncreaseValues = experienceIncreases
.filter(exp => exp.data.length > 0)
.flatMap(exp =>
exp.data.map(data => {
const experience = Object.keys(this.actor.system.experiences).find(x => x === data);
return this.actor.system.experiences[experience].name;
})
);
context.experienceIncreases = {
values: experienceIncreaseValues,
active: experienceIncreases.length > 0,
progress: {
selected: experienceIncreaseValues.length,
max: experienceIncreases.reduce((acc, exp) => acc + exp.amount, 0)
}
};
context.newExperiences = Object.keys(currentLevel.achievements.experiences).map(key => {
const experience = currentLevel.achievements.experiences[key];
return {
...experience,
level: this.levelup.currentLevel,
key: key
};
});
const allDomainCards = {
...advancementChoices.domainCard,
...currentLevel.achievements.domainCards
};
const allDomainCardKeys = Object.keys(allDomainCards);
const classDomainsData = this.actor.system.class.value.system.domains.map(domain => ({
domain,
multiclass: false
}));
const multiclassDomainsData = (this.actor.system.multiclass?.value?.system?.domains ?? []).map(
domain => ({ domain, multiclass: true })
);
const domainsData = [...classDomainsData, ...multiclassDomainsData];
const multiclassDomain = this.levelup.classUpgradeChoices?.multiclass?.domain;
if (multiclassDomain) {
if (!domainsData.some(x => x.domain === multiclassDomain))
domainsData.push({ domain: multiclassDomain, multiclass: true });
}
context.domainCards = [];
for (var key of allDomainCardKeys) {
const domainCard = allDomainCards[key];
if (domainCard.level > this.levelup.endLevel) continue;
const uuid = domainCard.data?.length > 0 ? domainCard.data[0] : domainCard.uuid;
const card = uuid ? await foundry.utils.fromUuid(uuid) : {};
context.domainCards.push({
...(card.toObject?.() ?? card),
emptySubtexts: domainsData.map(domain => {
const levelBase = domain.multiclass
? Math.ceil(this.levelup.currentLevel / 2)
: this.levelup.currentLevel;
const levelMax = domainCard.secondaryData?.limit
? Math.min(domainCard.secondaryData.limit, levelBase)
: levelBase;
return game.i18n.format('DAGGERHEART.Application.LevelUp.Selections.emptyDomainCardHint', {
domain: game.i18n.localize(domains[domain.domain].label),
level: levelMax
});
}),
path: domainCard.data
? `${domainCard.path}.data`
: `levels.${domainCard.level}.achievements.domainCards.${key}.uuid`,
limit: domainCard.secondaryData?.limit ?? null,
compendium: 'domains'
});
}
const subclassSelections = advancementChoices.subclass?.flatMap(x => x.data) ?? [];
const possibleSubclasses = [this.actor.system.class.subclass];
if (this.actor.system.multiclass?.subclass) {
possibleSubclasses.push(this.actor.system.multiclass.subclass);
}
context.subclassCards = [];
if (advancementChoices.subclass?.length > 0) {
const featureStateIncrease = Object.values(this.levelup.levels).reduce((acc, level) => {
acc += Object.values(level.choices).filter(choice => {
return Object.values(choice).every(checkbox => checkbox.type === 'subclass');
}).length;
return acc;
}, 0);
for (var subclass of possibleSubclasses) {
const choice =
advancementChoices.subclass.find(x => x.data[0] === subclass.uuid) ??
advancementChoices.subclass.find(x => x.data.length === 0);
const featureState = subclass.system.featureState + featureStateIncrease;
const data = await foundry.utils.fromUuid(subclass.uuid);
context.subclassCards.push({
...data.toObject(),
path: choice?.path,
uuid: data.uuid,
selected: subclassSelections.includes(subclass.uuid),
featureState: featureState,
featureLabel: game.i18n.localize(subclassFeatureLabels[featureState]),
isMulticlass: subclass.system.isMulticlass ? 'true' : 'false'
});
}
}
const multiclasses = Object.values(advancementChoices.multiclass ?? {});
if (multiclasses?.[0]) {
const data = multiclasses[0];
const multiclass = data.data.length > 0 ? await foundry.utils.fromUuid(data.data[0]) : {};
context.multiclass = {
...data,
...(multiclass.toObject?.() ?? multiclass),
uuid: multiclass.uuid,
domains:
multiclass?.system?.domains.map(key => {
const domain = domains[key];
const alreadySelected = this.actor.system.class.value.system.domains.includes(key);
return {
...domain,
selected: key === data.secondaryData.domain,
disabled:
(data.secondaryData.domain && key !== data.secondaryData.domain) ||
alreadySelected
};
}) ?? [],
subclasses:
multiclass?.system?.subclasses.map(subclass => ({
...subclass,
uuid: subclass.uuid,
selected: data.secondaryData.subclass === subclass.uuid,
disabled: data.secondaryData.subclass && data.secondaryData.subclass !== subclass.uuid
})) ?? [],
compendium: 'classes',
limit: 1
};
}
break;
case 'summary':
const { current: currentActorLevel, changed: changedActorLevel } = this.actor.system.levelData.level;
const actorArmor = this.actor.system.armor;
const levelKeys = Object.keys(this.levelup.levels);
let achivementProficiency = 0;
const achievementCards = [];
let achievementExperiences = [];
for (var levelKey of levelKeys) {
const level = this.levelup.levels[levelKey];
if (Number(levelKey) < this.levelup.startLevel) continue;
achivementProficiency += level.achievements.proficiency ?? 0;
const cards = level.achievements.domainCards ? Object.values(level.achievements.domainCards) : null;
if (cards) {
for (var card of cards) {
const itemCard = await foundry.utils.fromUuid(card.uuid);
achievementCards.push(itemCard);
}
}
achievementExperiences = level.achievements.experiences
? Object.values(level.achievements.experiences).reduce((acc, experience) => {
if (experience.name) acc.push(experience);
return acc;
}, [])
: [];
}
context.achievements = {
proficiency: {
old: this.actor.system.proficiency.total,
new: this.actor.system.proficiency.total + achivementProficiency,
shown: achivementProficiency > 0
},
damageThresholds: {
major: {
old: this.actor.system.damageThresholds.major,
new: this.actor.system.damageThresholds.major + changedActorLevel - currentActorLevel
},
severe: {
old: this.actor.system.damageThresholds.severe,
new:
this.actor.system.damageThresholds.severe +
(actorArmor
? changedActorLevel - currentActorLevel
: (changedActorLevel - currentActorLevel) * 2)
},
unarmored: !actorArmor
},
domainCards: {
values: achievementCards,
shown: achievementCards.length > 0
},
experiences: {
values: achievementExperiences,
shown: achievementExperiences.length > 0
}
};
const advancement = {};
for (var levelKey of levelKeys) {
const level = this.levelup.levels[levelKey];
if (Number(levelKey) < this.levelup.startLevel) continue;
for (var choiceKey of Object.keys(level.choices)) {
const choice = level.choices[choiceKey];
for (var checkbox of Object.values(choice)) {
switch (choiceKey) {
case 'proficiency':
case 'hitPoint':
case 'stress':
case 'evasion':
advancement[choiceKey] = advancement[choiceKey]
? advancement[choiceKey] + Number(checkbox.value)
: Number(checkbox.value);
break;
case 'trait':
if (!advancement[choiceKey]) advancement[choiceKey] = {};
for (var traitKey of checkbox.data) {
if (!advancement[choiceKey][traitKey]) advancement[choiceKey][traitKey] = 0;
advancement[choiceKey][traitKey] += 1;
}
break;
case 'domainCard':
if (!advancement[choiceKey]) advancement[choiceKey] = [];
if (checkbox.data.length === 1) {
const choiceItem = await foundry.utils.fromUuid(checkbox.data[0]);
advancement[choiceKey].push(choiceItem.toObject());
}
break;
case 'experience':
if (!advancement[choiceKey]) advancement[choiceKey] = [];
const data = checkbox.data.map(data => {
const experience = Object.keys(this.actor.system.experiences).find(
x => x === data
);
return this.actor.system.experiences[experience]?.description ?? '';
});
advancement[choiceKey].push({ data: data, value: checkbox.value });
break;
case 'subclass':
if (checkbox.data[0]) {
const subclassItem = await foundry.utils.fromUuid(checkbox.data[0]);
if (!advancement[choiceKey]) advancement[choiceKey] = [];
advancement[choiceKey].push({
...subclassItem.toObject(),
featureLabel: game.i18n.localize(
subclassFeatureLabels[Number(checkbox.secondaryData.featureState)]
)
});
}
break;
case 'multiclass':
const multiclassItem = await foundry.utils.fromUuid(checkbox.data[0]);
const subclass = multiclassItem
? await foundry.utils.fromUuid(checkbox.secondaryData.subclass)
: null;
advancement[choiceKey] = multiclassItem
? {
...multiclassItem.toObject(),
domain: checkbox.secondaryData.domain
? game.i18n.localize(domains[checkbox.secondaryData.domain].label)
: null,
subclass: subclass ? subclass.name : null
}
: {};
break;
}
}
}
}
context.advancements = {
statistics: {
proficiency: {
old: context.achievements.proficiency.new,
new: context.achievements.proficiency.new + (advancement.proficiency ?? 0)
},
hitPoints: {
old: this.actor.system.resources.hitPoints.maxTotal,
new: this.actor.system.resources.hitPoints.maxTotal + (advancement.hitPoint ?? 0)
},
stress: {
old: this.actor.system.resources.stress.maxTotal,
new: this.actor.system.resources.stress.maxTotal + (advancement.stress ?? 0)
},
evasion: {
old: this.actor.system.evasion.total,
new: this.actor.system.evasion.total + (advancement.evasion ?? 0)
}
},
traits: Object.keys(this.actor.system.traits).reduce((acc, traitKey) => {
if (advancement.trait?.[traitKey]) {
if (!acc) acc = {};
acc[traitKey] = {
label: game.i18n.localize(abilities[traitKey].label),
old: this.actor.system.traits[traitKey].total,
new: this.actor.system.traits[traitKey].total + advancement.trait[traitKey]
};
}
return acc;
}, null),
domainCards: advancement.domainCard ?? [],
experiences:
advancement.experience?.flatMap(x => x.data.map(data => ({ name: data, modifier: x.value }))) ??
[],
multiclass: advancement.multiclass,
subclass: advancement.subclass
};
context.advancements.statistics.proficiency.shown =
context.advancements.statistics.proficiency.new > context.advancements.statistics.proficiency.old;
context.advancements.statistics.hitPoints.shown =
context.advancements.statistics.hitPoints.new > context.advancements.statistics.hitPoints.old;
context.advancements.statistics.stress.shown =
context.advancements.statistics.stress.new > context.advancements.statistics.stress.old;
context.advancements.statistics.evasion.shown =
context.advancements.statistics.evasion.new > context.advancements.statistics.evasion.old;
context.advancements.statistics.shown =
context.advancements.statistics.proficiency.shown ||
context.advancements.statistics.hitPoints.shown ||
context.advancements.statistics.stress.shown ||
context.advancements.statistics.evasion.shown;
break;
}
return context;
}
}

View file

@ -0,0 +1,163 @@
import BaseLevelUp from './levelup.mjs';
import { defaultCompanionTier, LevelOptionType } from '../../data/levelTier.mjs';
import { DhLevelup } from '../../data/levelup.mjs';
import { diceTypes, range } from '../../config/generalConfig.mjs';
export default class DhCompanionLevelUp extends BaseLevelUp {
constructor(actor) {
super(actor);
this.levelTiers = this.addBonusChoices(defaultCompanionTier);
const playerLevelupData = actor.system.levelData;
this.levelup = new DhLevelup(DhLevelup.initializeData(this.levelTiers, playerLevelupData));
}
async _preparePartContext(partId, context) {
await super._preparePartContext(partId, context);
const currentLevel = this.levelup.levels[this.levelup.currentLevel];
switch (partId) {
case 'selections':
const advancementChoices = Object.keys(currentLevel.choices).reduce((acc, choiceKey) => {
Object.keys(currentLevel.choices[choiceKey]).forEach(checkboxNr => {
const checkbox = currentLevel.choices[choiceKey][checkboxNr];
const data = {
...checkbox,
path: `levels.${this.levelup.currentLevel}.choices.${choiceKey}.${checkboxNr}`,
level: this.levelup.currentLevel
};
if (!acc[choiceKey]) acc[choiceKey] = [];
acc[choiceKey].push(data);
});
return acc;
}, {});
const experienceIncreases = Object.values(advancementChoices.experience ?? {});
const experienceIncreaseValues = experienceIncreases
.filter(exp => exp.data.length > 0)
.flatMap(exp =>
exp.data.map(data => {
const experience = Object.keys(this.actor.system.experiences).find(x => x === data);
return this.actor.system.experiences[experience].name;
})
);
context.experienceIncreases = {
values: experienceIncreaseValues,
active: experienceIncreases.length > 0,
progress: {
selected: experienceIncreaseValues.length,
max: experienceIncreases.reduce((acc, exp) => acc + exp.amount, 0)
}
};
context.newExperiences = Object.keys(currentLevel.achievements.experiences).map(key => {
const experience = currentLevel.achievements.experiences[key];
return {
...experience,
level: this.levelup.currentLevel,
key: key
};
});
context.vicious = advancementChoices.vicious ? Object.values(advancementChoices.vicious) : null;
context.viciousChoices = {
damage: game.i18n.localize('DAGGERHEART.Application.LevelUp.Selections.viciousDamage'),
range: game.i18n.localize('DAGGERHEART.Application.LevelUp.Selections.viciousRange')
};
break;
case 'summary':
const levelKeys = Object.keys(this.levelup.levels);
const actorDamageDice = this.actor.system.attack.damage.parts[0].value.dice;
const actorRange = this.actor.system.attack.range;
const advancement = {};
for (var levelKey of levelKeys) {
const level = this.levelup.levels[levelKey];
if (Number(levelKey) < this.levelup.startLevel) continue;
for (var choiceKey of Object.keys(level.choices)) {
const choice = level.choices[choiceKey];
for (var checkbox of Object.values(choice)) {
switch (choiceKey) {
case 'stress':
case 'evasion':
advancement[choiceKey] = advancement[choiceKey]
? advancement[choiceKey] + Number(checkbox.value)
: Number(checkbox.value);
break;
case 'experience':
if (!advancement[choiceKey]) advancement[choiceKey] = [];
const data = checkbox.data.map(data => {
const experience = Object.keys(this.actor.system.experiences).find(
x => x === data
);
return this.actor.system.experiences[experience]?.name ?? '';
});
advancement[choiceKey].push({ data: data, value: checkbox.value });
break;
case 'vicious':
if (!advancement[choiceKey]) advancement[choiceKey] = { damage: null, range: null };
const isDamage = checkbox.data[0] === 'damage';
const options = isDamage ? diceTypes : range;
const keys = Object.keys(options);
const actorKey = keys.indexOf(isDamage ? actorDamageDice : actorRange);
const currentIndex = advancement[choiceKey][checkbox.data[0]]
? keys.indexOf(advancement[choiceKey][checkbox.data[0]])
: actorKey;
advancement[choiceKey][checkbox.data[0]] =
options[keys[Math.min(currentIndex + 1, keys.length - 1)]];
default:
if (!advancement.simple) advancement.simple = {};
advancement.simple[choiceKey] = game.i18n.localize(
LevelOptionType[checkbox.type].label
);
break;
}
}
}
}
context.advancements = {
statistics: {
stress: {
old: this.actor.system.resources.stress.maxTotal,
new: this.actor.system.resources.stress.maxTotal + (advancement.stress ?? 0)
},
evasion: {
old: this.actor.system.evasion.total,
new: this.actor.system.evasion.total + (advancement.evasion ?? 0)
}
},
experiences:
advancement.experience?.flatMap(x => x.data.map(data => ({ name: data, modifier: x.value }))) ??
[],
vicious: {
damage: advancement.vicious?.damage
? {
old: actorDamageDice,
new: advancement.vicious.damage
}
: null,
range: advancement.vicious?.range
? {
old: game.i18n.localize(`DAGGERHEART.Range.${actorRange}.name`),
new: game.i18n.localize(advancement.vicious.range.label)
}
: null
},
simple: advancement.simple ?? {}
};
context.advancements.statistics.stress.shown =
context.advancements.statistics.stress.new > context.advancements.statistics.stress.old;
context.advancements.statistics.evasion.shown =
context.advancements.statistics.evasion.new > context.advancements.statistics.evasion.old;
context.advancements.statistics.shown =
context.advancements.statistics.stress.shown || context.advancements.statistics.evasion.shown;
}
return context;
}
}

View file

@ -1,7 +1,6 @@
import { abilities, subclassFeatureLabels } from '../config/actorConfig.mjs';
import { domains } from '../config/domainConfig.mjs';
import { DhLevelup } from '../data/levelup.mjs';
import { getDeleteKeys, tagifyElement } from '../helpers/utils.mjs';
import { abilities, subclassFeatureLabels } from '../../config/actorConfig.mjs';
import { domains } from '../../config/domainConfig.mjs';
import { getDeleteKeys, tagifyElement } from '../../helpers/utils.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -10,10 +9,6 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
super({});
this.actor = actor;
this.levelTiers = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.LevelTiers);
const playerLevelupData = actor.system.levelData;
this.levelup = new DhLevelup(DhLevelup.initializeData(this.levelTiers, playerLevelupData, actor.system.level));
this._dragDrop = this._createDragDropHandlers();
this.tabGroups.primary = 'advancements';
@ -81,6 +76,21 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
}
};
addBonusChoices(levelTiers) {
for (var tierKey in levelTiers.tiers) {
const tier = levelTiers.tiers[tierKey];
tier.maxSelections = [...Array(tier.levels.end - tier.levels.start + 1).keys()].reduce((acc, index) => {
const level = tier.levels.start + index;
const bonus = this.actor.system.levelData.level.bonuses[level];
acc[level] = tier.availableOptions + (bonus ?? 0);
return acc;
}, {});
}
return levelTiers;
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.levelup = this.levelup;
@ -118,181 +128,6 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
context.tabs.advancements.progress = { selected: selections, max: currentLevel.maxSelections };
context.showTabs = this.tabGroups.primary !== 'summary';
break;
case 'selections':
const advancementChoices = Object.keys(currentLevel.choices).reduce((acc, choiceKey) => {
Object.keys(currentLevel.choices[choiceKey]).forEach(checkboxNr => {
const checkbox = currentLevel.choices[choiceKey][checkboxNr];
const data = {
...checkbox,
path: `levels.${this.levelup.currentLevel}.choices.${choiceKey}.${checkboxNr}`,
level: this.levelup.currentLevel
};
if (!acc[choiceKey]) acc[choiceKey] = [];
acc[choiceKey].push(data);
});
return acc;
}, {});
const traits = Object.values(advancementChoices.trait ?? {});
const traitValues = traits.filter(trait => trait.data.length > 0).flatMap(trait => trait.data);
context.traits = {
values: traitValues,
active: traits.length > 0,
progress: {
selected: traitValues.length,
max: traits.reduce((acc, exp) => acc + exp.amount, 0)
}
};
const experienceIncreases = Object.values(advancementChoices.experience ?? {});
const experienceIncreaseValues = experienceIncreases
.filter(exp => exp.data.length > 0)
.flatMap(exp =>
exp.data.map(data => {
const experience = Object.keys(this.actor.system.experiences).find(x => x === data);
return this.actor.system.experiences[experience].description;
})
);
context.experienceIncreases = {
values: experienceIncreaseValues,
active: experienceIncreases.length > 0,
progress: {
selected: experienceIncreaseValues.length,
max: experienceIncreases.reduce((acc, exp) => acc + exp.amount, 0)
}
};
context.newExperiences = Object.keys(currentLevel.achievements.experiences).map(key => {
const experience = currentLevel.achievements.experiences[key];
return {
...experience,
level: this.levelup.currentLevel,
key: key
};
});
const allDomainCards = {
...advancementChoices.domainCard,
...currentLevel.achievements.domainCards
};
const allDomainCardKeys = Object.keys(allDomainCards);
const classDomainsData = this.actor.system.class.value.system.domains.map(domain => ({
domain,
multiclass: false
}));
const multiclassDomainsData = (this.actor.system.multiclass?.value?.system?.domains ?? []).map(
domain => ({ domain, multiclass: true })
);
const domainsData = [...classDomainsData, ...multiclassDomainsData];
const multiclassDomain = this.levelup.classUpgradeChoices?.multiclass?.domain;
if (multiclassDomain) {
if (!domainsData.some(x => x.domain === multiclassDomain))
domainsData.push({ domain: multiclassDomain, multiclass: true });
}
context.domainCards = [];
for (var key of allDomainCardKeys) {
const domainCard = allDomainCards[key];
if (domainCard.level > this.levelup.endLevel) continue;
const uuid = domainCard.data?.length > 0 ? domainCard.data[0] : domainCard.uuid;
const card = uuid ? await foundry.utils.fromUuid(uuid) : {};
context.domainCards.push({
...(card.toObject?.() ?? card),
emptySubtexts: domainsData.map(domain => {
const levelBase = domain.multiclass
? Math.ceil(this.levelup.currentLevel / 2)
: this.levelup.currentLevel;
const levelMax = domainCard.secondaryData?.limit
? Math.min(domainCard.secondaryData.limit, levelBase)
: levelBase;
return game.i18n.format('DAGGERHEART.Application.LevelUp.Selections.emptyDomainCardHint', {
domain: game.i18n.localize(domains[domain.domain].label),
level: levelMax
});
}),
path: domainCard.data
? `${domainCard.path}.data`
: `levels.${domainCard.level}.achievements.domainCards.${key}.uuid`,
limit: domainCard.secondaryData?.limit ?? null,
compendium: 'domains'
});
}
const subclassSelections = advancementChoices.subclass?.flatMap(x => x.data) ?? [];
const possibleSubclasses = [this.actor.system.class.subclass];
if (this.actor.system.multiclass?.subclass) {
possibleSubclasses.push(this.actor.system.multiclass.subclass);
}
context.subclassCards = [];
if (advancementChoices.subclass?.length > 0) {
const featureStateIncrease = Object.values(this.levelup.levels).reduce((acc, level) => {
acc += Object.values(level.choices).filter(choice => {
return Object.values(choice).every(checkbox => checkbox.type === 'subclass');
}).length;
return acc;
}, 0);
for (var subclass of possibleSubclasses) {
const choice =
advancementChoices.subclass.find(x => x.data[0] === subclass.uuid) ??
advancementChoices.subclass.find(x => x.data.length === 0);
const featureState = subclass.system.featureState + featureStateIncrease;
const data = await foundry.utils.fromUuid(subclass.uuid);
context.subclassCards.push({
...data.toObject(),
path: choice?.path,
uuid: data.uuid,
selected: subclassSelections.includes(subclass.uuid),
featureState: featureState,
featureLabel: game.i18n.localize(subclassFeatureLabels[featureState]),
isMulticlass: subclass.system.isMulticlass ? 'true' : 'false'
});
}
}
const multiclasses = Object.values(advancementChoices.multiclass ?? {});
if (multiclasses?.[0]) {
const data = multiclasses[0];
const multiclass = data.data.length > 0 ? await foundry.utils.fromUuid(data.data[0]) : {};
context.multiclass = {
...data,
...(multiclass.toObject?.() ?? multiclass),
uuid: multiclass.uuid,
domains:
multiclass?.system?.domains.map(key => {
const domain = domains[key];
const alreadySelected = this.actor.system.class.value.system.domains.includes(key);
return {
...domain,
selected: key === data.secondaryData.domain,
disabled:
(data.secondaryData.domain && key !== data.secondaryData.domain) ||
alreadySelected
};
}) ?? [],
subclasses:
multiclass?.system?.subclasses.map(subclass => ({
...subclass,
uuid: subclass.uuid,
selected: data.secondaryData.subclass === subclass.uuid,
disabled: data.secondaryData.subclass && data.secondaryData.subclass !== subclass.uuid
})) ?? [],
compendium: 'classes',
limit: 1
};
}
break;
case 'summary':
const { current: currentActorLevel, changed: changedActorLevel } = this.actor.system.levelData.level;
const actorArmor = this.actor.system.armor;
const levelKeys = Object.keys(this.levelup.levels);
@ -516,7 +351,7 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
experienceIncreaseTagify,
Object.keys(this.actor.system.experiences).reduce((acc, id) => {
const experience = this.actor.system.experiences[id];
acc[id] = { label: experience.description };
acc[id] = { label: experience.name };
return acc;
}, {}),
@ -594,20 +429,20 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
return;
}
if (
Object.values(this.levelup.levels).some(level => {
const achievementExists = Object.values(level.achievements.domainCards).some(
card => card.uuid === item.uuid
);
const advancementExists = Object.keys(level.choices).some(choiceKey => {
if (choiceKey !== 'domainCard') return false;
const choice = level.choices[choiceKey];
return Object.values(choice).some(checkbox => checkbox.data.includes(item.uuid));
});
const cardExistsInCharacter = this.actor.items.find(x => x.name === item.name); // Any other way to check? The item is a copy so different ids
const cardExistsInLevelup = Object.values(this.levelup.levels).some(level => {
const achievementExists = Object.values(level.achievements.domainCards).some(
card => card.uuid === item.uuid
);
const advancementExists = Object.keys(level.choices).some(choiceKey => {
if (choiceKey !== 'domainCard') return false;
const choice = level.choices[choiceKey];
return Object.values(choice).some(checkbox => checkbox.data.includes(item.uuid));
});
return achievementExists || advancementExists;
})
) {
return achievementExists || advancementExists;
});
if (cardExistsInCharacter || cardExistsInLevelup) {
ui.notifications.error(
game.i18n.localize('DAGGERHEART.Application.LevelUp.notifications.error.domainCardDuplicate')
);

View file

@ -1,3 +1,5 @@
/** NOT USED ANYMORE - TO BE DELETED **/
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class NpcRollSelectionDialog extends HandlebarsApplicationMixin(ApplicationV2) {

View file

@ -1,5 +1,7 @@
import DHDamageRoll from '../data/chat-message/damageRoll.mjs';
import D20RollDialog from '../dialogs/d20RollDialog.mjs';
import DamageDialog from '../dialogs/damageDialog.mjs';
import { setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs';
/*
- Damage & other resources roll
@ -7,6 +9,7 @@ import DamageDialog from '../dialogs/damageDialog.mjs';
*/
export class DHRoll extends Roll {
baseTerms = [];
constructor(formula, data, options) {
super(formula, data, options);
}
@ -29,17 +32,18 @@ export class DHRoll extends Roll {
for (const hook of config.hooks) {
if (Hooks.call(`${SYSTEM.id}.preRoll${hook.capitalize()}`, config, message) === false) return null;
}
this.applyKeybindings(config);
let roll = new this(config.roll.formula, config.data, config);
if (config.dialog.configure !== false) {
// Open Roll Dialog
const DialogClass = config.dialog?.class ?? this.DefaultDialog;
console.log(roll, config);
const configDialog = await DialogClass.configure(roll, config, message);
if (!configDialog) return;
}
for (const hook of config.hooks) {
if (Hooks.call(`${SYSTEM.id}.post${hook.capitalize()}RollConfiguration`, roll, config, message) === false)
return [];
@ -58,7 +62,7 @@ export class DHRoll extends Roll {
}
// Create Chat Message
if (message.data) {
if (config.source?.message) {
} else {
const messageData = {};
config.message = await this.toMessage(roll, config);
@ -66,7 +70,7 @@ export class DHRoll extends Roll {
}
static postEvaluate(roll, config = {}) {
if(!config.roll) config.roll = {};
if (!config.roll) config.roll = {};
config.roll.total = roll.total;
config.roll.formula = roll.formula;
config.roll.dice = [];
@ -93,12 +97,31 @@ export class DHRoll extends Roll {
}
static applyKeybindings(config) {
config.dialog.configure ??= !(config.event.shiftKey || config.event.altKey || config.event.ctrlKey);
if (config.event)
config.dialog.configure ??= !(config.event.shiftKey || config.event.altKey || config.event.ctrlKey);
}
formatModifier(modifier) {
const numTerm = modifier < 0 ? '-' : '+';
return [
new foundry.dice.terms.OperatorTerm({ operator: numTerm }),
new foundry.dice.terms.NumericTerm({ number: Math.abs(modifier) })
];
}
getFaces(faces) {
return Number(faces.startsWith('d') ? faces.replace('d', '') : faces);
}
constructFormula(config) {
// const formula = Roll.replaceFormulaData(this.options.roll.formula, config.data);
this.terms = Roll.parse(this.options.roll.formula, config.data)
this.terms = Roll.parse(this.options.roll.formula, config.data);
if (this.options.extraFormula) {
this.terms.push(
new foundry.dice.terms.OperatorTerm({ operator: '+' }),
...this.constructor.parse(this.options.extraFormula, this.options.data)
);
}
return (this._formula = this.constructor.getFormula(this.terms));
}
}
@ -112,10 +135,6 @@ export class DualityDie extends foundry.dice.terms.Die {
export class D20Roll extends DHRoll {
constructor(formula, data = {}, options = {}) {
super(formula, data, options);
// this.createBaseDice();
// this.configureModifiers();
// this._formula = this.resetFormula();
this.constructFormula();
}
@ -138,7 +157,7 @@ export class D20Roll extends DHRoll {
set d20(faces) {
if (!(this.terms[0] instanceof foundry.dice.terms.Die)) this.createBaseDice();
this.terms[0].faces = faces;
this.terms[0].faces = this.getFaces(faces);
}
get dAdvantage() {
@ -151,36 +170,82 @@ export class D20Roll extends DHRoll {
}
get hasAdvantage() {
return this.options.advantage === this.constructor.ADV_MODE.ADVANTAGE;
return this.options.roll.advantage === this.constructor.ADV_MODE.ADVANTAGE;
}
get hasDisadvantage() {
return this.options.advantage === this.constructor.ADV_MODE.DISADVANTAGE;
return this.options.roll.advantage === this.constructor.ADV_MODE.DISADVANTAGE;
}
static applyKeybindings(config) {
const keys = {
normal: config.event.shiftKey || config.event.altKey || config.event.ctrlKey,
advantage: config.event.altKey,
disadvantage: config.event.ctrlKey
let keys = {
normal: true,
advantage: false,
disadvantage: false
};
if (config.event) {
keys = {
normal: config.event.shiftKey || config.event.altKey || config.event.ctrlKey,
advantage: config.event.altKey,
disadvantage: config.event.ctrlKey
};
}
// Should the roll configuration dialog be displayed?
config.dialog.configure ??= !Object.values(keys).some(k => k);
// Determine advantage mode
const advantage = config.advantage || keys.advantage;
const disadvantage = config.disadvantage || keys.disadvantage;
if (advantage && !disadvantage) config.advantage = this.ADV_MODE.ADVANTAGE;
else if (!advantage && disadvantage) config.advantage = this.ADV_MODE.DISADVANTAGE;
else config.advantage = this.ADV_MODE.NORMAL;
const advantage = config.roll.advantage === this.ADV_MODE.ADVANTAGE || keys.advantage || config.advantage;
const disadvantage =
config.roll.advantage === this.ADV_MODE.DISADVANTAGE || keys.disadvantage || config.disadvantage;
if (advantage && !disadvantage) config.roll.advantage = this.ADV_MODE.ADVANTAGE;
else if (!advantage && disadvantage) config.roll.advantage = this.ADV_MODE.DISADVANTAGE;
else config.roll.advantage = this.ADV_MODE.NORMAL;
}
constructFormula(config) {
// this.terms = [];
this.createBaseDice();
this.configureModifiers();
this.resetFormula();
return this._formula;
}
createBaseDice() {
if (this.terms[0] instanceof foundry.dice.terms.Die) return;
if (this.terms[0] instanceof foundry.dice.terms.Die) {
this.terms = [this.terms[0]];
return;
}
this.terms[0] = new foundry.dice.terms.Die({ faces: 20 });
}
configureModifiers() {
this.applyAdvantage();
this.applyBaseBonus();
this.options.experiences?.forEach(m => {
if (this.options.data.experiences?.[m])
this.options.roll.modifiers.push({
label: this.options.data.experiences[m].name,
value: this.options.data.experiences[m].total ?? this.options.data.experiences[m].value
});
});
this.options.roll.modifiers?.forEach(m => {
this.terms.push(...this.formatModifier(m.value));
});
this.baseTerms = foundry.utils.deepClone(this.terms);
if (this.options.extraFormula) {
this.terms.push(
new foundry.dice.terms.OperatorTerm({ operator: '+' }),
...this.constructor.parse(this.options.extraFormula, this.options.data)
);
}
}
applyAdvantage() {
this.d20.modifiers.findSplice(m => ['kh', 'kl'].includes(m));
if (!this.hasAdvantage && !this.hasDisadvantage) this.number = 1;
@ -190,45 +255,26 @@ export class D20Roll extends DHRoll {
}
}
// Trait bonus != Adversary
configureModifiers() {
this.applyAdvantage();
// this.options.roll.modifiers = [];
this.applyBaseBonus();
this.options.experiences?.forEach(m => {
if (this.options.data.experiences?.[m])
this.options.roll.modifiers.push({
label: this.options.data.experiences[m].name,
value: this.options.data.experiences[m].total ?? this.options.data.experiences[m].value
});
});
this.options.roll.modifiers?.forEach(m => {
this.terms.push(...this.formatModifier(m.value));
});
if (this.options.extraFormula) {
this.terms.push(
new foundry.dice.terms.OperatorTerm({ operator: '+' }),
...this.constructor.parse(this.options.extraFormula, this.getRollData())
);
}
// this.resetFormula();
}
constructFormula(config) {
this.terms = [];
this.createBaseDice();
this.configureModifiers();
this.resetFormula();
return this._formula;
}
applyBaseBonus() {
this.options.roll.modifiers = [{
label : 'Bonus to Hit',
value: Roll.replaceFormulaData('@attackBonus', this.data)
}];
this.options.roll.modifiers = [];
if (!this.options.roll.bonus) return;
this.options.roll.modifiers.push({
label: 'Bonus to Hit',
value: this.options.roll.bonus
// value: Roll.replaceFormulaData('@attackBonus', this.data)
});
}
static async buildEvaluate(roll, config = {}, message = {}) {
if (config.evaluate !== false) await roll.evaluate();
const advantageState =
config.roll.advantage == this.ADV_MODE.ADVANTAGE
? true
: config.roll.advantage == this.ADV_MODE.DISADVANTAGE
? false
: null;
setDiceSoNiceForDualityRoll(roll, advantageState);
this.postEvaluate(roll, config);
}
static postEvaluate(roll, config = {}) {
@ -238,25 +284,30 @@ export class D20Roll extends DHRoll {
const difficulty = config.roll.difficulty ?? target.difficulty ?? target.evasion;
target.hit = this.isCritical || roll.total >= difficulty;
});
} else if (config.roll.difficulty) config.roll.success = roll.isCritical || roll.total >= config.roll.difficulty;
} else if (config.roll.difficulty)
config.roll.success = roll.isCritical || roll.total >= config.roll.difficulty;
config.roll.advantage = {
type: config.advantage,
type: config.roll.advantage,
dice: roll.dAdvantage?.denomination,
value: roll.dAdvantage?.total
};
config.roll.modifierTotal = config.roll.modifiers.reduce((a, c) => a + Number(c.value), 0);
}
getRollData() {
return this.options.data;
}
formatModifier(modifier) {
const numTerm = modifier < 0 ? '-' : '+';
return [
new foundry.dice.terms.OperatorTerm({ operator: numTerm }),
new foundry.dice.terms.NumericTerm({ number: Math.abs(modifier) })
];
config.roll.extra = roll.dice
.filter(d => !roll.baseTerms.includes(d))
.map(d => {
return {
dice: d.denomination,
value: d.total
};
});
config.roll.modifierTotal = 0;
for (let i = 0; i < roll.terms.length; i++) {
if (
roll.terms[i] instanceof foundry.dice.terms.NumericTerm &&
!!roll.terms[i - 1] &&
roll.terms[i - 1] instanceof foundry.dice.terms.OperatorTerm
)
config.roll.modifierTotal += Number(`${roll.terms[i - 1].operator}${roll.terms[i].total}`);
}
}
resetFormula() {
@ -265,6 +316,8 @@ export class D20Roll extends DHRoll {
}
export class DualityRoll extends D20Roll {
_advantageFaces = 6;
constructor(formula, data = {}, options = {}) {
super(formula, data, options);
}
@ -282,7 +335,7 @@ export class DualityRoll extends D20Roll {
set dHope(faces) {
if (!(this.dice[0] instanceof CONFIG.Dice.daggerheart.DualityDie)) this.createBaseDice();
this.terms[0].faces = faces;
this.terms[0].faces = this.getFaces(faces);
// this.#hopeDice = `d${face}`;
}
@ -295,7 +348,7 @@ export class DualityRoll extends D20Roll {
set dFear(faces) {
if (!(this.dice[1] instanceof CONFIG.Dice.daggerheart.DualityDie)) this.createBaseDice();
this.dice[1].faces = faces;
this.dice[1].faces = this.getFaces(faces);
// this.#fearDice = `d${face}`;
}
@ -303,6 +356,14 @@ export class DualityRoll extends D20Roll {
return this.dice[2];
}
get advantageFaces() {
return this._advantageFaces;
}
set advantageFaces(faces) {
this._advantageFaces = this.getFaces(faces);
}
get isCritical() {
if (!this.dHope._evaluated || !this.dFear._evaluated) return;
return this.dHope.total === this.dFear.total;
@ -332,25 +393,27 @@ export class DualityRoll extends D20Roll {
return game.i18n.localize(label);
}
updateFormula() {}
createBaseDice() {
if (
this.dice[0] instanceof CONFIG.Dice.daggerheart.DualityDie &&
this.dice[1] instanceof CONFIG.Dice.daggerheart.DualityDie
)
) {
this.terms = [this.terms[0], this.terms[1], this.terms[2]];
return;
if (!(this.dice[0] instanceof CONFIG.Dice.daggerheart.DualityDie))
this.terms[0] = new CONFIG.Dice.daggerheart.DualityDie();
}
this.terms[0] = new CONFIG.Dice.daggerheart.DualityDie();
this.terms[1] = new foundry.dice.terms.OperatorTerm({ operator: '+' });
if (!(this.dice[2] instanceof CONFIG.Dice.daggerheart.DualityDie))
this.terms[2] = new CONFIG.Dice.daggerheart.DualityDie();
this.terms[2] = new CONFIG.Dice.daggerheart.DualityDie();
}
applyAdvantage() {
const dieFaces = 6,
const dieFaces = this.advantageFaces,
bardRallyFaces = this.hasBarRally,
advDie = new foundry.dice.terms.Die({ faces: dieFaces });
if (this.hasAdvantage || this.hasDisadvantage || bardRallyFaces)
this.terms.push(new foundry.dice.terms.OperatorTerm({ operator: (this.hasDisadvantage ? '-' : '+') }));
this.terms.push(new foundry.dice.terms.OperatorTerm({ operator: this.hasDisadvantage ? '-' : '+' }));
if (bardRallyFaces) {
const rallyDie = new foundry.dice.terms.Die({ faces: bardRallyFaces });
if (this.hasAdvantage) {
@ -367,10 +430,12 @@ export class DualityRoll extends D20Roll {
}
applyBaseBonus() {
this.options.roll.modifiers = [{
label : `DAGGERHEART.Abilities.${this.options.roll.trait}.name`,
this.options.roll.modifiers = [];
if (!this.options.roll.trait) return;
this.options.roll.modifiers.push({
label: `DAGGERHEART.Abilities.${this.options.roll.trait}.name`,
value: Roll.replaceFormulaData(`@traits.${this.options.roll.trait}.total`, this.data)
}];
});
}
static postEvaluate(roll, config = {}) {
@ -388,7 +453,6 @@ export class DualityRoll extends D20Roll {
total: roll.dHope.total + roll.dFear.total,
label: roll.totalLabel
};
console.log(roll, config)
}
}
@ -404,5 +468,9 @@ export class DamageRoll extends DHRoll {
static async postEvaluate(roll, config = {}) {
super.postEvaluate(roll, config);
config.roll.type = config.type;
if (config.source?.message) {
const chatMessage = ui.chat.collection.get(config.source.message);
chatMessage.update({ 'system.damage': config });
}
}
}

View file

@ -1,3 +1,5 @@
/** NOT USED ANYMORE - TO BE DELETED **/
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export default class RollSelectionDialog extends HandlebarsApplicationMixin(ApplicationV2) {

View file

@ -81,7 +81,7 @@ export default class DhSettingsActionView extends HandlebarsApplicationMixin(App
}
static onEditImage() {
const fp = new FilePicker({
const fp = new foundry.applications.apps.FilePicker.implementation({
current: this.img,
type: 'image',
callback: async path => {

View file

@ -1,20 +1,23 @@
import DHActionConfig from '../config/Action.mjs';
import DaggerheartSheet from './daggerheart-sheet.mjs';
import DHActionConfig from '../../config/Action.mjs';
import DaggerheartSheet from '../daggerheart-sheet.mjs';
import DHAdversarySettings from '../applications/adversary-settings.mjs';
const { ActorSheetV2 } = foundry.applications.sheets;
export default class AdversarySheet extends DaggerheartSheet(ActorSheetV2) {
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'sheet', 'actor', 'dh-style', 'adversary'],
position: { width: 450, height: 1000 },
position: { width: 660, height: 766 },
actions: {
reactionRoll: this.reactionRoll,
attackRoll: this.attackRoll,
useItem: this.useItem,
toChat: this.toChat,
attackConfigure: this.attackConfigure,
addExperience: this.addExperience,
removeExperience: this.removeExperience,
toggleHP: this.toggleHP,
toggleStress: this.toggleStress
toggleStress: this.toggleStress,
openSettings: this.openSettings
},
form: {
handler: this.updateForm,
@ -24,28 +27,37 @@ export default class AdversarySheet extends DaggerheartSheet(ActorSheetV2) {
};
static PARTS = {
sidebar: { template: 'systems/daggerheart/templates/sheets/actors/adversary/sidebar.hbs' },
header: { template: 'systems/daggerheart/templates/sheets/actors/adversary/header.hbs' },
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
main: { template: 'systems/daggerheart/templates/sheets/actors/adversary/main.hbs' },
information: { template: 'systems/daggerheart/templates/sheets/actors/adversary/information.hbs' }
actions: { template: 'systems/daggerheart/templates/sheets/actors/adversary/actions.hbs' },
notes: { template: 'systems/daggerheart/templates/sheets/actors/adversary/notes.hbs' },
effects: { template: 'systems/daggerheart/templates/sheets/actors/adversary/effects.hbs' }
};
static TABS = {
main: {
actions: {
active: true,
cssClass: '',
group: 'primary',
id: 'main',
id: 'actions',
icon: null,
label: 'DAGGERHEART.Sheets.Adversary.Tabs.Main'
label: 'DAGGERHEART.General.tabs.actions'
},
information: {
notes: {
active: false,
cssClass: '',
group: 'primary',
id: 'information',
id: 'notes',
icon: null,
label: 'DAGGERHEART.Sheets.Adversary.Tabs.Information'
label: 'DAGGERHEART.Sheets.Adversary.Tabs.notes'
},
effects: {
active: false,
cssClass: '',
group: 'primary',
id: 'effects',
icon: null,
label: 'DAGGERHEART.Sheets.Adversary.Tabs.effects'
}
};
@ -56,10 +68,15 @@ export default class AdversarySheet extends DaggerheartSheet(ActorSheetV2) {
context.systemFields.attack.fields = this.document.system.attack.schema.fields;
context.getEffectDetails = this.getEffectDetails.bind(this);
context.isNPC = true;
console.log(context)
return context;
}
getAction(element) {
const itemId = (element.target ?? element).closest('[data-item-id]').dataset.itemId,
item = this.document.system.actions.find(x => x.id === itemId);
return item;
}
static async updateForm(event, _, formData) {
await this.document.update(formData.object);
this.render();
@ -70,7 +87,7 @@ export default class AdversarySheet extends DaggerheartSheet(ActorSheetV2) {
event: event,
title: `${this.actor.name} - Reaction Roll`,
roll: {
modifier: null,
// modifier: null,
type: 'reaction'
},
chatMessage: {
@ -86,8 +103,40 @@ export default class AdversarySheet extends DaggerheartSheet(ActorSheetV2) {
return {};
}
static async attackRoll(event) {
this.actor.system.attack.use(event);
static async openSettings() {
await new DHAdversarySettings(this.document).render(true);
}
static async useItem(event) {
const action = this.getAction(event) ?? this.actor.system.attack;
action.use(event);
}
static async toChat(event, button) {
if (button?.dataset?.type === 'experience') {
const experience = this.document.system.experiences[button.dataset.uuid];
const cls = getDocumentClass('ChatMessage');
const systemData = {
name: game.i18n.localize('DAGGERHEART.General.Experience.Single'),
description: `${experience.name} ${
experience.modifier < 0 ? experience.modifier : `+${experience.modifier}`
}`
};
const msg = new cls({
type: 'abilityUse',
user: game.user.id,
system: systemData,
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/chat/ability-use.hbs',
systemData
)
});
cls.create(msg.toObject());
} else {
const item = this.getAction(event) ?? this.document.system.attack;
item.toChat(this.document.id);
}
}
static async attackConfigure(event) {

View file

@ -1,12 +1,12 @@
import { capitalize } from '../../helpers/utils.mjs';
import DhpDeathMove from '../deathMove.mjs';
import DhpDowntime from '../downtime.mjs';
import AncestrySelectionDialog from '../ancestrySelectionDialog.mjs';
import DaggerheartSheet from './daggerheart-sheet.mjs';
import { abilities } from '../../config/actorConfig.mjs';
import DhlevelUp from '../levelup.mjs';
import DhCharacterCreation from '../characterCreation.mjs';
import FilterMenu from '../ux/filter-menu.mjs';
import { capitalize } from '../../../helpers/utils.mjs';
import DhpDeathMove from '../../deathMove.mjs';
import DhpDowntime from '../../downtime.mjs';
import AncestrySelectionDialog from '../../ancestrySelectionDialog.mjs';
import DaggerheartSheet from '.././daggerheart-sheet.mjs';
import { abilities } from '../../../config/actorConfig.mjs';
import DhCharacterlevelUp from '../../levelup/characterLevelup.mjs';
import DhCharacterCreation from '../../characterCreation.mjs';
import FilterMenu from '../../ux/filter-menu.mjs';
const { ActorSheetV2 } = foundry.applications.sheets;
const { TextEditor } = foundry.applications.ux;
@ -305,8 +305,11 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
}
getItem(element) {
const itemId = (element.target ?? element).closest('[data-item-id]').dataset.itemId,
item = this.document.items.get(itemId);
const listElement = (element.target ?? element).closest('[data-item-id]');
const document = listElement.dataset.companion ? this.document.system.companion : this.document;
const itemId = listElement.dataset.itemId,
item = document.items.get(itemId);
return item;
}
@ -315,7 +318,7 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
}
static _onEditImage() {
const fp = new FilePicker({
const fp = new foundry.applications.apps.FilePicker.implementation({
current: this.document.img,
type: 'image',
redirectToRoot: ['icons/svg/mystery-man.svg'],
@ -330,25 +333,8 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
const context = await super._prepareContext(_options);
context.document = this.document;
context.tabs = super._getTabs(this.constructor.TABS);
context.config = SYSTEM;
const selectedAttributes = Object.values(this.document.system.traits).map(x => x.base);
context.abilityScoreArray = await game.settings
.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Homebrew)
.traitArray.reduce((acc, x) => {
const selectedIndex = selectedAttributes.indexOf(x);
if (selectedIndex !== -1) {
selectedAttributes.splice(selectedIndex, 1);
} else {
acc.push({ name: x, value: x });
}
return acc;
}, []);
if (!context.abilityScoreArray.includes(0)) context.abilityScoreArray.push({ name: 0, value: 0 });
context.abilityScoresFinished = context.abilityScoreArray.every(x => x.value === 0);
context.attributes = Object.keys(this.document.system.traits).reduce((acc, key) => {
acc[key] = {
...this.document.system.traits[key],
@ -359,67 +345,7 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
return acc;
}, {});
const ancestry = await this.mapFeatureType(
this.document.system.ancestry ? [this.document.system.ancestry] : [],
SYSTEM.GENERAL.objectTypes
);
const community = await this.mapFeatureType(
this.document.system.community ? [this.document.system.community] : [],
SYSTEM.GENERAL.objectTypes
);
const foundation = {
ancestry: ancestry[0],
community: community[0],
advancement: {}
};
const nrLoadoutCards = this.document.system.domainCards.loadout.length;
const loadout = await this.mapFeatureType(this.document.system.domainCards.loadout, SYSTEM.DOMAIN.cardTypes);
const vault = await this.mapFeatureType(this.document.system.domainCards.vault, SYSTEM.DOMAIN.cardTypes);
context.abilities = {
foundation: foundation,
loadout: {
top: loadout.slice(0, Math.min(2, nrLoadoutCards)),
bottom: nrLoadoutCards > 2 ? loadout.slice(2, Math.min(5, nrLoadoutCards)) : [],
nrTotal: nrLoadoutCards,
listView: game.user.getFlag(SYSTEM.id, SYSTEM.FLAGS.displayDomainCardsAsList)
},
vault: vault.map(x => ({
...x,
uuid: x.uuid,
sendToLoadoutDisabled: this.document.system.domainCards.loadout.length >= 5
}))
};
context.inventory = {
consumable: {
titles: {
name: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.ConsumableTitle'),
quantity: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.QuantityTitle')
},
items: this.document.items.filter(x => x.type === 'consumable')
},
miscellaneous: {
titles: {
name: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.MiscellaneousTitle'),
quantity: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.QuantityTitle')
},
items: this.document.items.filter(x => x.type === 'miscellaneous')
},
weapons: {
titles: {
name: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.WeaponsTitle'),
quantity: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.QuantityTitle')
},
items: this.document.items.filter(x => x.type === 'weapon')
},
armor: {
titles: {
name: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.ArmorsTitle'),
quantity: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.QuantityTitle')
},
items: this.document.items.filter(x => x.type === 'armor')
},
currency: {
title: game.i18n.localize('DAGGERHEART.Sheets.PC.Gold.Title'),
coins: game.i18n.localize('DAGGERHEART.Sheets.PC.Gold.Coins'),
@ -557,102 +483,6 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
}
}
/* -------------------------------------------- */
/* Filter Menus */
/* -------------------------------------------- */
_createFilterMenus() {
//Menus could be a application option if needed
const menus = [
{
key: 'inventory',
container: '[data-application-part="inventory"]',
content: '.items-section',
callback: this._onMenuFilterInventory.bind(this),
target: '.filter-button',
filters: FilterMenu.invetoryFilters
},
{
key: 'loadout',
container: '[data-application-part="loadout"]',
content: '.items-section',
callback: this._onMenuFilterLoadout.bind(this),
target: '.filter-button',
filters: FilterMenu.cardsFilters
}
];
menus.forEach(m => {
const container = this.element.querySelector(m.container);
this.#menu[m.key] = new FilterMenu(container, m.target, m.filters, m.callback, {
contentSelector: m.content
});
});
}
/**
* Callback when filters change
* @param {PointerEvent} event
* @param {HTMLElement} html
* @param {import('../ux/filter-menu.mjs').FilterItem[]} filters
*/
_onMenuFilterInventory(event, html, filters) {
this.#filteredItems.inventory.menu.clear();
for (const li of html.querySelectorAll('.inventory-item')) {
const item = this.document.items.get(li.dataset.itemId);
const matchesMenu =
filters.length === 0 || filters.some(f => foundry.applications.ux.SearchFilter.evaluateFilter(item, f));
if (matchesMenu) this.#filteredItems.inventory.menu.add(item.id);
const { search } = this.#filteredItems.inventory;
li.hidden = !(search.has(item.id) && matchesMenu);
}
}
/**
* Callback when filters change
* @param {PointerEvent} event
* @param {HTMLElement} html
* @param {import('../ux/filter-menu.mjs').FilterItem[]} filters
*/
_onMenuFilterLoadout(event, html, filters) {
this.#filteredItems.loadout.menu.clear();
for (const li of html.querySelectorAll('.items-list .inventory-item, .card-list .card-item')) {
const item = this.document.items.get(li.dataset.itemId);
const matchesMenu =
filters.length === 0 || filters.some(f => foundry.applications.ux.SearchFilter.evaluateFilter(item, f));
if (matchesMenu) this.#filteredItems.loadout.menu.add(item.id);
const { search } = this.#filteredItems.loadout;
li.hidden = !(search.has(item.id) && matchesMenu);
}
}
/* -------------------------------------------- */
async mapFeatureType(data, configType) {
return await Promise.all(
data.map(async x => {
const abilities = x.system.abilities
? await Promise.all(x.system.abilities.map(async x => await fromUuid(x.uuid)))
: [];
return {
...x,
uuid: x.uuid,
system: {
...x.system,
abilities: abilities,
type: game.i18n.localize(configType[x.system.type ?? x.type].label)
}
};
})
);
}
static async rollAttribute(event, button) {
const abilityLabel = game.i18n.localize(abilities[button.dataset.attribute].label);
const config = {
@ -738,7 +568,7 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
return;
}
new DhlevelUp(this.document).render(true);
new DhCharacterlevelUp(this.document).render(true);
}
static async useDomainCard(event, button) {
@ -804,15 +634,22 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
static async useItem(event, button) {
const item = this.getItem(button);
if (!item) return;
const wasUsed = await item.use(event);
if (wasUsed && item.type === 'weapon') {
Hooks.callAll(SYSTEM.HOOKS.characterAttack, {});
// Should dandle its actions. Or maybe they'll be separate buttons as per an Issue on the board
if (item.type === 'feature') {
item.toChat();
} else {
const wasUsed = await item.use(event);
if (wasUsed && item.type === 'weapon') {
Hooks.callAll(SYSTEM.HOOKS.characterAttack, {});
}
}
}
static async viewObject(event, button) {
static async viewObject(event) {
const item = this.getItem(event);
if (!item) return;
item.sheet.render(true);
}
@ -866,9 +703,10 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
this.render();
}
static async deleteItem(event, button) {
static async deleteItem(event) {
const item = this.getItem(event);
if (!item) return;
await item.delete();
}
@ -910,9 +748,7 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
const cls = getDocumentClass('ChatMessage');
const systemData = {
name: game.i18n.localize('DAGGERHEART.General.Experience.Single'),
description: `${experience.description} ${
experience.total < 0 ? experience.total : `+${experience.total}`
}`
description: `${experience.name} ${experience.total < 0 ? experience.total : `+${experience.total}`}`
};
const msg = new cls({
type: 'abilityUse',

View file

@ -0,0 +1,140 @@
import DaggerheartSheet from '../daggerheart-sheet.mjs';
import DHEnvironmentSettings from '../applications/environment-settings.mjs';
const { ActorSheetV2 } = foundry.applications.sheets;
export default class DhpEnvironment extends DaggerheartSheet(ActorSheetV2) {
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'sheet', 'actor', 'dh-style', 'environment'],
position: {
width: 500
},
actions: {
addAdversary: this.addAdversary,
deleteProperty: this.deleteProperty,
openSettings: this.openSettings,
useItem: this.useItem,
toChat: this.toChat
},
form: {
handler: this._updateForm,
submitOnChange: true,
closeOnSubmit: false
},
dragDrop: [{ dragSelector: '.action-section .inventory-item', dropSelector: null }]
};
static PARTS = {
header: { template: 'systems/daggerheart/templates/sheets/actors/environment/header.hbs' },
actions: { template: 'systems/daggerheart/templates/sheets/actors/environment/actions.hbs' },
potentialAdversaries: {
template: 'systems/daggerheart/templates/sheets/actors/environment/potentialAdversaries.hbs'
},
notes: { template: 'systems/daggerheart/templates/sheets/actors/environment/notes.hbs' }
};
static TABS = {
actions: {
active: true,
cssClass: '',
group: 'primary',
id: 'actions',
icon: null,
label: 'DAGGERHEART.General.tabs.actions'
},
potentialAdversaries: {
active: false,
cssClass: '',
group: 'primary',
id: 'potentialAdversaries',
icon: null,
label: 'DAGGERHEART.General.tabs.potentialAdversaries'
},
notes: {
active: false,
cssClass: '',
group: 'primary',
id: 'notes',
icon: null,
label: 'DAGGERHEART.Sheets.Adversary.Tabs.notes'
}
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.document = this.document;
context.tabs = super._getTabs(this.constructor.TABS);
context.getEffectDetails = this.getEffectDetails.bind(this);
return context;
}
getAction(element) {
const itemId = (element.target ?? element).closest('[data-item-id]').dataset.itemId,
item = this.document.system.actions.find(x => x.id === itemId);
return item;
}
static async openSettings() {
await new DHEnvironmentSettings(this.document).render(true);
}
static async _updateForm(event, _, formData) {
await this.document.update(formData.object);
this.render();
}
getEffectDetails(id) {
return {};
}
static async addAdversary() {
await this.document.update({
[`system.potentialAdversaries.${foundry.utils.randomID()}.label`]: game.i18n.localize(
'DAGGERHEART.Sheets.Environment.newAdversary'
)
});
this.render();
}
static async deleteProperty(_, target) {
await this.document.update({ [`${target.dataset.path}.-=${target.id}`]: null });
this.render();
}
async viewAdversary(_, button) {
const target = button.closest('[data-item-uuid]');
const adversary = await foundry.utils.fromUuid(target.dataset.itemUuid);
if (!adversary) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.notifications.adversaryMissing'));
return;
}
adversary.sheet.render(true);
}
static async useItem(event, button) {
const action = this.getAction(event);
if (!action) {
await this.viewAdversary(event, button);
} else {
action.use(event);
}
}
static async toChat(event) {
const item = this.getAction(event);
item.toChat(this.document.id);
}
async _onDragStart(event) {
const item = event.currentTarget.closest('.inventory-item');
if (item) {
const adversary = game.actors.find(x => x.type === 'adversary' && x.id === item.dataset.itemId);
const adversaryData = { type: 'Actor', uuid: adversary.uuid };
event.dataTransfer.setData('text/plain', JSON.stringify(adversaryData));
event.dataTransfer.setDragImage(item, 60, 0);
}
}
}

View file

@ -46,19 +46,18 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
switch (partId) {
case 'description':
const value = foundry.utils.getProperty(this.document, "system.description") ?? "";
const value = foundry.utils.getProperty(this.document, 'system.description') ?? '';
context.enrichedDescription = await TextEditor.enrichHTML(value, {
relativeTo: this.item,
rollData: this.item.getRollData(),
secrets: this.item.isOwner
})
});
break;
}
return context;
}
/* -------------------------------------------- */
/* Application Clicks Actions */
/* -------------------------------------------- */
@ -70,11 +69,11 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
*/
static async selectActionType() {
const content = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/views/actionType.hbs',
{ types: SYSTEM.ACTIONS.actionTypes }
),
title = 'Select Action Type'
'systems/daggerheart/templates/views/actionType.hbs',
{ types: SYSTEM.ACTIONS.actionTypes }
),
title = 'Select Action Type';
return foundry.applications.api.DialogV2.prompt({
window: { title },
content,
@ -92,7 +91,7 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
*/
static async #addAction(_event, _button) {
const actionType = await DHBaseItemSheet.selectActionType();
if(!actionType) return;
if (!actionType) return;
try {
const cls = actionsTypes[actionType] ?? actionsTypes.attack,
action = new cls(
@ -134,9 +133,7 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
event.stopPropagation();
const actionIndex = button.closest('[data-index]').dataset.index;
await this.document.update({
'system.actions': this.document.system.actions.filter(
(_, index) => index !== Number.parseInt(actionIndex)
)
'system.actions': this.document.system.actions.filter((_, index) => index !== Number.parseInt(actionIndex))
});
}
}

View file

@ -0,0 +1,181 @@
import DHActionConfig from '../../config/Action.mjs';
import DHBaseItemSheet from '../api/base-item.mjs';
import { actionsTypes } from '../../../data/_module.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class DHAdversarySettings extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(actor) {
super({});
this.actor = actor;
}
get title() {
return `${game.i18n.localize('DAGGERHEART.Sheets.TABS.settings')}`;
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'dh-style', 'dialog', 'adversary-settings'],
window: {
icon: 'fa-solid fa-wrench',
resizable: false
},
position: { width: 455, height: 'auto' },
actions: {
addExperience: this.#addExperience,
removeExperience: this.#removeExperience,
addAction: this.#addAction,
editAction: this.#editAction,
removeAction: this.#removeAction
},
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false
}
};
static PARTS = {
header: {
id: 'header',
template: 'systems/daggerheart/templates/sheets/applications/adversary-settings/header.hbs'
},
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
details: {
id: 'details',
template: 'systems/daggerheart/templates/sheets/applications/adversary-settings/details.hbs'
},
attack: {
id: 'attack',
template: 'systems/daggerheart/templates/sheets/applications/adversary-settings/attack.hbs'
},
experiences: {
id: 'experiences',
template: 'systems/daggerheart/templates/sheets/applications/adversary-settings/experiences.hbs'
},
actions: {
id: 'actions',
template: 'systems/daggerheart/templates/sheets/applications/adversary-settings/actions.hbs'
}
};
static TABS = {
details: {
active: true,
cssClass: '',
group: 'primary',
id: 'details',
icon: null,
label: 'DAGGERHEART.General.tabs.details'
},
attack: {
active: false,
cssClass: '',
group: 'primary',
id: 'attack',
icon: null,
label: 'DAGGERHEART.General.tabs.attack'
},
experiences: {
active: false,
cssClass: '',
group: 'primary',
id: 'experiences',
icon: null,
label: 'DAGGERHEART.General.tabs.experiences'
},
actions: {
active: false,
cssClass: '',
group: 'primary',
id: 'actions',
icon: null,
label: 'DAGGERHEART.General.tabs.actions'
}
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.document = this.actor;
context.tabs = this._getTabs(this.constructor.TABS);
context.systemFields = this.actor.system.schema.fields;
context.systemFields.attack.fields = this.actor.system.attack.schema.fields;
context.isNPC = true;
console.log(context)
return context;
}
_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' : '';
}
return tabs;
}
static async #addExperience() {
const newExperience = {
name: 'Experience',
modifier: 0
};
await this.actor.update({ [`system.experiences.${foundry.utils.randomID()}`]: newExperience });
this.render();
}
static async #removeExperience(_, target) {
await this.actor.update({ [`system.experiences.-=${target.dataset.experience}`]: null });
this.render();
}
static async #addAction(_event, _button) {
const actionType = await DHBaseItemSheet.selectActionType();
if (!actionType) return;
try {
const cls = actionsTypes[actionType] ?? actionsTypes.attack,
action = new cls(
{
_id: foundry.utils.randomID(),
type: actionType,
name: game.i18n.localize(SYSTEM.ACTIONS.actionTypes[actionType].name),
...cls.getSourceConfig(this.actor)
},
{
parent: this.actor
}
);
await this.actor.update({ 'system.actions': [...this.actor.system.actions, action] });
await new DHActionConfig(this.actor.system.actions[this.actor.system.actions.length - 1]).render({
force: true
});
this.render();
} catch (error) {
console.log(error);
}
}
static async #editAction(event, target) {
event.stopPropagation();
const actionIndex = target.dataset.index;
await new DHActionConfig(this.actor.system.actions[actionIndex]).render({
force: true
});
}
static async #removeAction(event, target) {
event.stopPropagation();
const actionIndex = target.dataset.index;
await this.actor.update({
'system.actions': this.actor.system.actions.filter((_, index) => index !== Number.parseInt(actionIndex))
});
this.render();
}
static async updateForm(event, _, formData) {
await this.actor.update(formData.object);
this.render();
}
}

View file

@ -0,0 +1,220 @@
import DHActionConfig from '../../config/Action.mjs';
import DHBaseItemSheet from '../api/base-item.mjs';
import { actionsTypes } from '../../../data/_module.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class DHEnvironmentSettings extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(actor) {
super({});
this.actor = actor;
this._dragDrop = this._createDragDropHandlers();
}
get title() {
return `${game.i18n.localize('DAGGERHEART.Sheets.TABS.settings')}`;
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'dh-style', 'dialog', 'environment-settings'],
window: {
icon: 'fa-solid fa-wrench',
resizable: false
},
position: { width: 455, height: 'auto' },
actions: {
addAction: this.#addAction,
editAction: this.#editAction,
removeAction: this.#removeAction,
addCategory: this.#addCategory,
deleteProperty: this.#deleteProperty,
viewAdversary: this.#viewAdversary,
deleteAdversary: this.#deleteAdversary
},
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false
},
dragDrop: [{ dragSelector: null, dropSelector: '.category-container' }]
};
static PARTS = {
header: {
id: 'header',
template: 'systems/daggerheart/templates/sheets/applications/environment-settings/header.hbs'
},
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
details: {
id: 'details',
template: 'systems/daggerheart/templates/sheets/applications/environment-settings/details.hbs'
},
actions: {
id: 'actions',
template: 'systems/daggerheart/templates/sheets/applications/environment-settings/actions.hbs'
},
adversaries: {
id: 'adversaries',
template: 'systems/daggerheart/templates/sheets/applications/environment-settings/adversaries.hbs'
}
};
static TABS = {
details: {
active: true,
cssClass: '',
group: 'primary',
id: 'details',
icon: null,
label: 'DAGGERHEART.General.tabs.details'
},
actions: {
active: false,
cssClass: '',
group: 'primary',
id: 'actions',
icon: null,
label: 'DAGGERHEART.General.tabs.actions'
},
adversaries: {
active: false,
cssClass: '',
group: 'primary',
id: 'adversaries',
icon: null,
label: 'DAGGERHEART.General.tabs.adversaries'
}
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.document = this.actor;
context.tabs = this._getTabs(this.constructor.TABS);
context.systemFields = this.actor.system.schema.fields;
context.isNPC = true;
return context;
}
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
this._dragDrop.forEach(d => d.bind(htmlElement));
}
_createDragDropHandlers() {
return this.options.dragDrop.map(d => {
d.callbacks = {
drop: this._onDrop.bind(this)
};
return new foundry.applications.ux.DragDrop.implementation(d);
});
}
_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' : '';
}
return tabs;
}
static async #addAction(_event, _button) {
const actionType = await DHBaseItemSheet.selectActionType();
if (!actionType) return;
try {
const cls = actionsTypes[actionType] ?? actionsTypes.attack,
action = new cls(
{
_id: foundry.utils.randomID(),
type: actionType,
name: game.i18n.localize(SYSTEM.ACTIONS.actionTypes[actionType].name),
...cls.getSourceConfig(this.actor)
},
{
parent: this.actor
}
);
await this.actor.update({ 'system.actions': [...this.actor.system.actions, action] });
await new DHActionConfig(this.actor.system.actions[this.actor.system.actions.length - 1]).render({
force: true
});
this.render();
} catch (error) {
console.log(error);
}
}
static async #editAction(event, target) {
event.stopPropagation();
const actionIndex = target.dataset.index;
await new DHActionConfig(this.actor.system.actions[actionIndex]).render({
force: true
});
}
static async #removeAction(event, target) {
event.stopPropagation();
const actionIndex = target.dataset.index;
await this.actor.update({
'system.actions': this.actor.system.actions.filter((_, index) => index !== Number.parseInt(actionIndex))
});
this.render();
}
static async #addCategory() {
await this.actor.update({
[`system.potentialAdversaries.${foundry.utils.randomID()}.label`]: game.i18n.localize(
'DAGGERHEART.Sheets.Environment.newAdversary'
)
});
this.render();
}
static async #deleteProperty(_, target) {
await this.actor.update({ [`${target.dataset.path}.-=${target.id}`]: null });
this.render();
}
static async #viewAdversary(_, button) {
const adversary = await foundry.utils.fromUuid(button.dataset.adversary);
if (!adversary) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.notifications.adversaryMissing'));
return;
}
adversary.sheet.render(true);
}
static async #deleteAdversary(event, target) {
const adversaryKey = target.dataset.adversary;
const path = `system.potentialAdversaries.${target.dataset.potentialAdversary}.adversaries`;
const newAdversaries = foundry.utils
.getProperty(this.actor, path)
.filter(x => x && (x?.uuid ?? x) !== adversaryKey);
await this.actor.update({ [path]: newAdversaries });
this.render();
}
async _onDrop(event) {
const data = TextEditor.getDragEventData(event);
const item = await fromUuid(data.uuid);
if (item.type === 'adversary') {
const target = event.target.closest('.category-container');
const path = `system.potentialAdversaries.${target.dataset.potentialAdversary}.adversaries`;
const current = foundry.utils.getProperty(this.actor, path).map(x => x.uuid);
await this.actor.update({
[path]: [...current, item.uuid]
});
this.render();
}
}
static async updateForm(event, _, formData) {
await this.actor.update(formData.object);
this.render();
}
}

View file

@ -0,0 +1,86 @@
import { GMUpdateEvent, socketEvent } from '../../helpers/socket.mjs';
import DhCompanionlevelUp from '../levelup/companionLevelup.mjs';
import DaggerheartSheet from './daggerheart-sheet.mjs';
const { ActorSheetV2 } = foundry.applications.sheets;
export default class DhCompanionSheet extends DaggerheartSheet(ActorSheetV2) {
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'sheet', 'actor', 'dh-style', 'companion'],
position: { width: 700, height: 1000 },
actions: {
attackRoll: this.attackRoll,
levelUp: this.levelUp
},
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false
}
};
static PARTS = {
sidebar: { template: 'systems/daggerheart/templates/sheets/actors/companion/tempMain.hbs' }
};
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
htmlElement.querySelector('.partner-value')?.addEventListener('change', this.onPartnerChange.bind(this));
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.document = this.document;
context.playerCharacters = game.actors
.filter(
x =>
x.type === 'character' &&
(x.ownership.default === 3 ||
x.ownership[game.user.id] === 3 ||
this.document.system.partner?.uuid === x.uuid)
)
.map(x => ({ key: x.uuid, name: x.name }));
return context;
}
static async updateForm(event, _, formData) {
await this.document.update(formData.object);
this.render();
}
async onPartnerChange(event) {
const partnerDocument = event.target.value
? await foundry.utils.fromUuid(event.target.value)
: this.document.system.partner;
const partnerUpdate = { 'system.companion': event.target.value ? this.document.uuid : null };
if (!partnerDocument.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER)) {
await game.socket.emit(`system.${SYSTEM.id}`, {
action: socketEvent.GMUpdate,
data: {
action: GMUpdateEvent.UpdateDocument,
uuid: partnerDocument.uuid,
update: update
}
});
} else {
await partnerDocument.update(partnerUpdate);
}
await this.document.update({ 'system.partner': event.target.value });
if (!event.target.value) {
await this.document.updateLevel(1);
}
}
static async attackRoll(event) {
this.actor.system.attack.use(event);
}
static async levelUp() {
new DhCompanionlevelUp(this.document).render(true);
}
}

View file

@ -38,7 +38,7 @@ export default function DhpApplicationMixin(Base) {
const attr = target.dataset.edit;
const current = foundry.utils.getProperty(this.document, attr);
const { img } = this.document.constructor.getDefaultArtwork?.(this.document.toObject()) ?? {};
const fp = new FilePicker({
const fp = new foundry.applications.apps.FilePicker.implementation({
current,
type: 'image',
redirectToRoot: img ? [img] : [],
@ -60,7 +60,7 @@ export default function DhpApplicationMixin(Base) {
// drop: this._canDragDrop.bind(this)
// };
d.callbacks = {
// dragstart: this._onDragStart.bind(this),
dragstart: this._onDragStart.bind(this),
// dragover: this._onDragOver.bind(this),
drop: this._onDrop.bind(this)
};
@ -68,6 +68,7 @@ export default function DhpApplicationMixin(Base) {
});
}
async _onDragStart(event) {}
_onDrop(event) {}
_getTabs(tabs) {

View file

@ -1,107 +0,0 @@
import DaggerheartSheet from './daggerheart-sheet.mjs';
const { ActorSheetV2 } = foundry.applications.sheets;
export default class DhpEnvironment extends DaggerheartSheet(ActorSheetV2) {
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'sheet', 'actor', 'dh-style', 'environment'],
position: {
width: 450,
height: 1000
},
actions: {
addAdversary: this.addAdversary,
addFeature: this.addFeature,
deleteProperty: this.deleteProperty,
viewAdversary: this.viewAdversary
},
form: {
handler: this._updateForm,
submitOnChange: true,
closeOnSubmit: false
},
dragDrop: [{ dragSelector: null, dropSelector: '.adversary-container' }]
};
static PARTS = {
header: { template: 'systems/daggerheart/templates/sheets/actors/environment/header.hbs' },
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
main: { template: 'systems/daggerheart/templates/sheets/actors/environment/main.hbs' },
information: { template: 'systems/daggerheart/templates/sheets/actors/environment/information.hbs' }
};
static TABS = {
main: {
active: true,
cssClass: '',
group: 'primary',
id: 'main',
icon: null,
label: 'DAGGERHEART.Sheets.Environment.Tabs.Main'
},
information: {
active: false,
cssClass: '',
group: 'primary',
id: 'information',
icon: null,
label: 'DAGGERHEART.Sheets.Environment.Tabs.Information'
}
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.document = this.document;
context.tabs = super._getTabs(this.constructor.TABS);
context.getEffectDetails = this.getEffectDetails.bind(this);
return context;
}
static async _updateForm(event, _, formData) {
await this.document.update(formData.object);
this.render();
}
getEffectDetails(id) {
return {};
}
static async addAdversary() {
await this.document.update({
[`system.potentialAdversaries.${foundry.utils.randomID()}.label`]: game.i18n.localize(
'DAGGERHEART.Sheets.Environment.newAdversary'
)
});
this.render();
}
static async addFeature() {
ui.notifications.error('Not Implemented yet. Awaiting datamodel rework');
}
static async deleteProperty(_, target) {
await this.document.update({ [`${target.dataset.path}.-=${target.id}`]: null });
this.render();
}
static async viewAdversary(_, button) {
const adversary = foundry.utils.getProperty(
this.document.system.potentialAdversaries,
`${button.dataset.potentialAdversary}.adversaries.${button.dataset.adversary}`
);
adversary.sheet.render(true);
}
async _onDrop(event) {
const data = TextEditor.getDragEventData(event);
const item = await fromUuid(data.uuid);
if (item.type === 'adversary') {
const target = event.target.closest('.adversary-container');
const path = `system.potentialAdversaries.${target.dataset.potentialAdversary}.adversaries.${item.id}`;
await this.document.update({
[path]: item.uuid
});
}
}
}

View file

@ -6,11 +6,7 @@ export default class FeatureSheet extends DHBaseItemSheet {
id: 'daggerheart-feature',
classes: ['feature'],
position: { height: 600 },
window: { resizable: true },
actions: {
addEffect: this.addEffect,
removeEffect: this.removeEffect
}
window: { resizable: true }
};
/**@override */
@ -22,27 +18,16 @@ export default class FeatureSheet extends DHBaseItemSheet {
template: 'systems/daggerheart/templates/sheets/global/tabs/tab-actions.hbs',
scrollable: ['.actions']
},
settings: {
template: 'systems/daggerheart/templates/sheets/items/feature/settings.hbs',
scrollable: ['.settings']
},
effects: {
template: 'systems/daggerheart/templates/sheets/items/feature/effects.hbs',
template: 'systems/daggerheart/templates/sheets/global/tabs/tab-effects.hbs',
scrollable: ['.effects']
}
};
/**
* Internally tracks the selected effect type from the select.
* @type {String}
* @private
*/
_selectedEffectType;
/**@override */
static TABS = {
primary: {
tabs: [{ id: 'description' }, { id: 'actions' }, { id: 'settings' }, { id: 'effects' }],
tabs: [{ id: 'description' }, { id: 'actions' }, { id: 'effects' }],
initial: 'description',
labelPrefix: 'DAGGERHEART.Sheets.TABS'
}
@ -50,68 +35,10 @@ export default class FeatureSheet extends DHBaseItemSheet {
/* -------------------------------------------- */
/**@inheritdoc*/
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
if (partId === 'effects')
htmlElement.querySelector('.effect-select')?.addEventListener('change', this._effectSelect.bind(this));
}
/**
* Handles selection of a new effect type.
* @param {Event} event - Change Event
*/
_effectSelect(event) {
const value = event.currentTarget.value;
this._selectedEffectType = value;
this.render({ parts: ['effects'] });
}
/* -------------------------------------------- */
/**@inheritdoc */
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.properties = CONFIG.daggerheart.ACTOR.featureProperties;
context.dice = CONFIG.daggerheart.GENERAL.diceTypes;
context.effectConfig = CONFIG.daggerheart.EFFECTS;
context.selectedEffectType = this._selectedEffectType;
return context;
}
/* -------------------------------------------- */
/* Application Clicks Actions */
/* -------------------------------------------- */
/**
* Adds a new effect to the item, based on the selected effect type.
* @param {PointerEvent} _event - The originating click event
* @param {HTMLElement} _target - The capturing HTML element which defines the [data-action]
* @returns
*/
static async addEffect(_event, _target) {
const type = this._selectedEffectType;
if (!type) return;
const { id, name, ...rest } = CONFIG.daggerheart.EFFECTS.effectTypes[type];
await this.item.update({
[`system.effects.${foundry.utils.randomID()}`]: {
type,
value: '',
...rest
}
});
}
/**
* Removes an effect from the item.
* @param {PointerEvent} _event - The originating click event
* @param {HTMLElement} target - The capturing HTML element which defines the [data-action]
* @returns
*/
static async removeEffect(_event, target) {
const path = `system.effects.-=${target.dataset.effect}`;
await this.item.update({ [path]: null });
}
}

View file

@ -0,0 +1,16 @@
export default class DhTooltipManager extends TooltipManager {
async activate(element, options = {}) {
let html = options.html;
if (element.dataset.tooltip.startsWith('#item#')) {
const item = await foundry.utils.fromUuid(element.dataset.tooltip.slice(6));
if (item) {
html = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/tooltip/${item.type}.hbs`,
item
);
}
}
super.activate(element, { ...options, html: html });
}
}

View file

@ -1,42 +1,42 @@
export const actionTypes = {
attack: {
id: 'attack',
name: 'DAGGERHEART.Actions.Types.Attack.Name',
name: 'DAGGERHEART.Actions.Types.attack.name',
icon: 'fa-swords'
},
// spellcast: {
// id: 'spellcast',
// name: 'DAGGERHEART.Actions.Types.Spellcast.Name',
// name: 'DAGGERHEART.Actions.Types.spellcast.name',
// icon: 'fa-book-sparkles'
// },
healing: {
id: 'healing',
name: 'DAGGERHEART.Actions.Types.Healing.Name',
name: 'DAGGERHEART.Actions.Types.healing.name',
icon: 'fa-kit-medical'
},
// resource: {
// id: 'resource',
// name: 'DAGGERHEART.Actions.Types.Resource.Name',
// name: 'DAGGERHEART.Actions.Types.resource.name',
// icon: 'fa-honey-pot'
// },
damage: {
id: 'damage',
name: 'DAGGERHEART.Actions.Types.Damage.Name',
name: 'DAGGERHEART.Actions.Types.damage.name',
icon: 'fa-bone-break'
},
summon: {
id: 'summon',
name: 'DAGGERHEART.Actions.Types.Summon.Name',
name: 'DAGGERHEART.Actions.Types.summon.name',
icon: 'fa-ghost'
},
effect: {
id: 'effect',
name: 'DAGGERHEART.Actions.Types.Effect.Name',
name: 'DAGGERHEART.Actions.Types.effect.name',
icon: 'fa-person-rays'
},
macro: {
id: 'macro',
name: 'DAGGERHEART.Actions.Types.Macro.Name',
name: 'DAGGERHEART.Actions.Types.macro.name',
icon: 'fa-scroll'
}
};
@ -76,7 +76,7 @@ export const damageOnSave = {
label: 'Full damage',
mod: 1
}
}
};
export const diceCompare = {
below: {
@ -104,4 +104,19 @@ export const diceCompare = {
label: 'Above',
operator: '>'
}
};
export const advandtageState = {
disadvantage: {
label: 'DAGGERHEART.General.Disadvantage.Full',
value: -1
},
neutral: {
label: 'DAGGERHEART.General.Neutral.Full',
value: 0
},
advantage: {
label: 'DAGGERHEART.General.Advantage.Full',
value: 1
}
}

View file

@ -83,72 +83,72 @@ export const featureProperties = {
export const adversaryTypes = {
bruiser: {
id: 'bruiser',
label: 'DAGGERHEART.Adversary.Type.Bruiser.label',
description: 'DAGGERHEART.Adversary.Bruiser.Description'
label: 'DAGGERHEART.Adversary.Type.bruiser.label',
description: 'DAGGERHEART.Adversary.bruiser.description'
},
horde: {
id: 'horde',
label: 'DAGGERHEART.Adversary.Type.Horde.label',
description: 'DAGGERHEART.Adversary.Horde.Description'
label: 'DAGGERHEART.Adversary.Type.horde.label',
description: 'DAGGERHEART.Adversary.horde.description'
},
leader: {
id: 'leader',
label: 'DAGGERHEART.Adversary.Type.Leader.label',
description: 'DAGGERHEART.Adversary.Leader.Description'
label: 'DAGGERHEART.Adversary.Type.leader.label',
description: 'DAGGERHEART.Adversary.leader.description'
},
minion: {
id: 'minion',
label: 'DAGGERHEART.Adversary.Type.Minion.label',
description: 'DAGGERHEART.Adversary.Minion.Description'
label: 'DAGGERHEART.Adversary.Type.minion.label',
description: 'DAGGERHEART.Adversary.minion.description'
},
ranged: {
id: 'ranged',
label: 'DAGGERHEART.Adversary.Type.Ranged.label',
description: 'DAGGERHEART.Adversary.Ranged.Description'
label: 'DAGGERHEART.Adversary.Type.ranged.label',
description: 'DAGGERHEART.Adversary.ranged.description'
},
skulk: {
id: 'skulk',
label: 'DAGGERHEART.Adversary.Type.Skulk.label',
description: 'DAGGERHEART.Adversary.Skulk.Description'
label: 'DAGGERHEART.Adversary.Type.skulk.label',
description: 'DAGGERHEART.Adversary.skulk.description'
},
social: {
id: 'social',
label: 'DAGGERHEART.Adversary.Type.Social.label',
description: 'DAGGERHEART.Adversary.Social.Description'
label: 'DAGGERHEART.Adversary.Type.social.label',
description: 'DAGGERHEART.Adversary.social.description'
},
solo: {
id: 'solo',
label: 'DAGGERHEART.Adversary.Type.Solo.label',
description: 'DAGGERHEART.Adversary.Solo.Description'
label: 'DAGGERHEART.Adversary.Type.solo.label',
description: 'DAGGERHEART.Adversary.solo.description'
},
standard: {
id: 'standard',
label: 'DAGGERHEART.Adversary.Type.Standard.label',
description: 'DAGGERHEART.Adversary.Standard.Description'
label: 'DAGGERHEART.Adversary.Type.standard.label',
description: 'DAGGERHEART.Adversary.standard.description'
},
support: {
id: 'support',
label: 'DAGGERHEART.Adversary.Type.Support.label',
description: 'DAGGERHEART.Adversary.Support.Description'
label: 'DAGGERHEART.Adversary.Type.support.label',
description: 'DAGGERHEART.Adversary.support.description'
}
};
export const environmentTypes = {
exploration: {
label: 'DAGGERHEART.Environment.Type.Exploration.label',
description: 'DAGGERHEART.Environment.Type.Exploration.description'
label: 'DAGGERHEART.Environment.Type.exploration.label',
description: 'DAGGERHEART.Environment.Type.exploration.description'
},
social: {
label: 'DAGGERHEART.Environment.Type.Social.label',
description: 'DAGGERHEART.Environment.Type.Social.description'
label: 'DAGGERHEART.Environment.Type.social.label',
description: 'DAGGERHEART.Environment.Type.social.description'
},
traversal: {
label: 'DAGGERHEART.Environment.Type.Traversal.label',
description: 'DAGGERHEART.Environment.Type.Traversal.description'
label: 'DAGGERHEART.Environment.Type.traversal.label',
description: 'DAGGERHEART.Environment.Type.traversal.description'
},
event: {
label: 'DAGGERHEART.Environment.Type.Event.label',
description: 'DAGGERHEART.Environment.Type.Event.description'
label: 'DAGGERHEART.Environment.Type.event.label',
description: 'DAGGERHEART.Environment.Type.event.description'
}
};

View file

@ -1 +1,9 @@
export const displayDomainCardsAsList = 'displayDomainCardsAsList';
export const narrativeCountdown = {
simple: 'countdown-narrative-simple',
position: 'countdown-narrative-position'
};
export const encounterCountdown = {
simple: 'countdown-encounter-simple',
position: 'countdown-encounter-position'
};

View file

@ -261,48 +261,6 @@ export const tiers = {
}
};
export const objectTypes = {
character: {
name: 'TYPES.Actor.character'
},
npc: {
name: 'TYPES.Actor.npc'
},
adversary: {
name: 'TYPES.Actor.adversary'
},
ancestry: {
name: 'TYPES.Item.ancestry'
},
community: {
name: 'TYPES.Item.community'
},
class: {
name: 'TYPES.Item.class'
},
subclass: {
name: 'TYPES.Item.subclass'
},
feature: {
name: 'TYPES.Item.feature'
},
domainCard: {
name: 'TYPES.Item.domainCard'
},
consumable: {
name: 'TYPES.Item.consumable'
},
miscellaneous: {
name: 'TYPES.Item.miscellaneous'
},
weapon: {
name: 'TYPES.Item.weapon'
},
armor: {
name: 'TYPES.Item.armor'
}
};
export const diceTypes = {
d4: 'd4',
d6: 'd6',
@ -325,7 +283,7 @@ export const diceSetNumbers = {
cast: 'Spellcast',
scale: 'Cost Scaling',
flat: 'Flat'
}
};
export const getDiceSoNicePresets = () => {
const { diceSoNice } = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance);
@ -384,39 +342,53 @@ export const refreshTypes = {
export const abilityCosts = {
hope: {
id: 'hope',
label: 'Hope'
label: 'Hope',
group: 'TYPES.Actor.character'
},
stress: {
id: 'stress',
label: 'DAGGERHEART.HealingType.Stress.Name'
label: 'DAGGERHEART.HealingType.Stress.Name',
group: 'TYPES.Actor.character'
},
armor: {
id: 'armor',
label: 'Armor Stack'
label: 'Armor Stack',
group: 'TYPES.Actor.character'
},
hp: {
id: 'hp',
label: 'DAGGERHEART.HealingType.HitPoints.Name'
label: 'DAGGERHEART.HealingType.HitPoints.Name',
group: 'TYPES.Actor.character'
},
prayer: {
id: 'prayer',
label: 'Prayer Dice'
label: 'Prayer Dice',
group: 'TYPES.Actor.character'
},
favor: {
id: 'favor',
label: 'Favor Points'
label: 'Favor Points',
group: 'TYPES.Actor.character'
},
slayer: {
id: 'slayer',
label: 'Slayer Dice'
label: 'Slayer Dice',
group: 'TYPES.Actor.character'
},
tide: {
id: 'tide',
label: 'Tide'
label: 'Tide',
group: 'TYPES.Actor.character'
},
chaos: {
id: 'chaos',
label: 'Chaos'
label: 'Chaos',
group: 'TYPES.Actor.character'
},
fear: {
id: 'fear',
label: 'Fear',
group: 'TYPES.Actor.adversary'
}
};

View file

@ -712,14 +712,14 @@ export const valueTypes = {
export const actionTypes = {
passive: {
id: 'passive',
label: 'DAGGERHEART.ActionType.Passive'
label: 'DAGGERHEART.ActionType.passive'
},
action: {
id: 'action',
label: 'DAGGERHEART.ActionType.Action'
label: 'DAGGERHEART.ActionType.action'
},
reaction: {
id: 'reaction',
label: 'DAGGERHEART.ActionType.Reaction'
label: 'DAGGERHEART.ActionType.reaction'
}
};

View file

@ -1,4 +1,3 @@
import CostSelectionDialog from '../../applications/costSelectionDialog.mjs';
import { DHActionDiceData, DHActionRollData, DHDamageData, DHDamageField } from './actionDice.mjs';
import DhpActor from '../../documents/actor.mjs';
import D20RollDialog from '../../dialogs/d20RollDialog.mjs';
@ -73,7 +72,10 @@ export class DHBaseAction extends foundry.abstract.DataModel {
save: new fields.SchemaField({
trait: new fields.StringField({ nullable: true, initial: null, choices: SYSTEM.ACTOR.abilities }),
difficulty: new fields.NumberField({ nullable: true, initial: 10, integer: true, min: 0 }),
damageMod: new fields.StringField({ initial: SYSTEM.ACTIONS.damageOnSave.none.id, choices: SYSTEM.ACTIONS.damageOnSave })
damageMod: new fields.StringField({
initial: SYSTEM.ACTIONS.damageOnSave.none.id,
choices: SYSTEM.ACTIONS.damageOnSave
})
}),
target: new fields.SchemaField({
type: new fields.StringField({
@ -98,9 +100,12 @@ export class DHBaseAction extends foundry.abstract.DataModel {
initial: SYSTEM.GENERAL.healingTypes.hitPoints.id,
label: 'Healing'
}),
resultBased: new fields.BooleanField({ initial: false, label: "DAGGERHEART.Actions.Settings.ResultBased.label" }),
resultBased: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.Actions.Settings.ResultBased.label'
}),
value: new fields.EmbeddedDataField(DHActionDiceData),
valueAlt: new fields.EmbeddedDataField(DHActionDiceData),
valueAlt: new fields.EmbeddedDataField(DHActionDiceData)
})
},
extraSchemas = {};
@ -124,7 +129,11 @@ export class DHBaseAction extends foundry.abstract.DataModel {
}
get actor() {
return this.item instanceof DhpActor ? this.item : this.item?.actor;
return this.item instanceof DhpActor
? this.item
: this.item?.parent instanceof DhpActor
? this.item.parent
: this.item?.actor;
}
get chatTemplate() {
@ -153,7 +162,7 @@ export class DHBaseAction extends foundry.abstract.DataModel {
return updateSource;
}
getRollData(data={}) {
getRollData(data = {}) {
const actorData = this.actor.getRollData(false);
// Remove when included directly in Actor getRollData
@ -166,11 +175,11 @@ export class DHBaseAction extends foundry.abstract.DataModel {
return a;
}, {})
: 1; */
actorData.scale = data.costs?.length // Right now only return the first scalable cost.
? (data.costs.find(c => c.scalable)?.total ?? 1)
: 1;
actorData.scale = data.costs?.length // Right now only return the first scalable cost.
? (data.costs.find(c => c.scalable)?.total ?? 1)
: 1;
actorData.roll = {};
return actorData;
}
@ -191,12 +200,14 @@ export class DHBaseAction extends foundry.abstract.DataModel {
// Prepare Costs
const costsConfig = this.prepareCost();
if(isFastForward && !this.hasCost(costsConfig)) return ui.notifications.warn("You don't have the resources to use that action.");
if (isFastForward && !this.hasCost(costsConfig))
return ui.notifications.warn("You don't have the resources to use that action.");
// config = this.prepareUseCost(config)
// Prepare Uses
const usesConfig = this.prepareUse();
if(isFastForward && !this.hasUses(usesConfig)) return ui.notifications.warn("That action doesn't have remaining uses.");
if (isFastForward && !this.hasUses(usesConfig))
return ui.notifications.warn("That action doesn't have remaining uses.");
// config = this.prepareUseCost(config)
// Prepare Roll Data
@ -209,24 +220,24 @@ export class DHBaseAction extends foundry.abstract.DataModel {
costs: costsConfig,
uses: usesConfig,
data: actorData
}
if ( Hooks.call(`${SYSTEM.id}.preUseAction`, this, config) === false ) return;
};
if (Hooks.call(`${SYSTEM.id}.preUseAction`, this, config) === false) return;
// Display configuration window if necessary
if ( config.dialog?.configure && this.requireConfigurationDialog(config) ) {
if (config.dialog?.configure && this.requireConfigurationDialog(config)) {
config = await D20RollDialog.configure(config);
if (!config) return;
}
if ( this.hasRoll ) {
if (this.hasRoll) {
const rollConfig = this.prepareRoll(config);
config.roll = rollConfig;
config = await this.actor.diceRoll(config);
if (!config) return;
}
if( this.hasSave ) {
if (this.hasSave) {
/* config.targets.forEach((t) => {
if(t.hit) {
const target = game.canvas.tokens.get(t.id),
@ -258,16 +269,16 @@ export class DHBaseAction extends foundry.abstract.DataModel {
}) */
}
if ( this.doFollowUp() ) {
if(this.rollDamage) await this.rollDamage(event, config);
if(this.rollHealing) await this.rollHealing(event, config);
if(this.trigger) await this.trigger(event, config);
if (this.doFollowUp()) {
if (this.rollDamage) await this.rollDamage(event, config);
if (this.rollHealing) await this.rollHealing(event, config);
if (this.trigger) await this.trigger(event, config);
}
// Consume resources
await this.consume(config);
if ( Hooks.call(`${SYSTEM.id}.postUseAction`, this, config) === false ) return;
if (Hooks.call(`${SYSTEM.id}.postUseAction`, this, config) === false) return;
return config;
}
@ -287,7 +298,7 @@ export class DHBaseAction extends foundry.abstract.DataModel {
hasHealing: !!this.healing,
hasEffect: !!this.effects?.length,
hasSave: this.hasSave
}
};
}
requireConfigurationDialog(config) {
@ -308,16 +319,15 @@ export class DHBaseAction extends foundry.abstract.DataModel {
prepareTarget() {
let targets;
if (this.target?.type === SYSTEM.ACTIONS.targetTypes.self.id)
targets = this.formatTarget(this.actor.token ?? this.actor.prototypeToken);
targets = this.constructor.formatTarget(this.actor.token ?? this.actor.prototypeToken);
targets = Array.from(game.user.targets);
// foundry.CONST.TOKEN_DISPOSITIONS.FRIENDLY
if (this.target?.type && this.target.type !== SYSTEM.ACTIONS.targetTypes.any.id) {
targets = targets.filter(t => this.isTargetFriendly(t));
if (this.target.amount && targets.length > this.target.amount) targets = [];
}
targets = targets.map(t => this.formatTarget(t));
targets = targets.map(t => this.constructor.formatTarget(t));
return targets;
}
prepareRange() {
@ -326,16 +336,18 @@ export class DHBaseAction extends foundry.abstract.DataModel {
}
prepareRoll() {
const roll = {
const roll = {
modifiers: [],
trait: this.roll?.trait,
label: 'Attack',
type: this.actionType,
difficulty: this.roll?.difficulty,
formula: this.roll.getFormula()
formula: this.roll.getFormula(),
bonus: this.roll.bonus,
advantage: SYSTEM.ACTIONS.advandtageState[this.roll.advState].value
};
if(this.roll?.type === 'diceSet') roll.lite = true;
if (this.roll?.type === 'diceSet') roll.lite = true;
return roll;
}
@ -344,12 +356,14 @@ export class DHBaseAction extends foundry.abstract.DataModel {
}
async consume(config) {
const resources = config.costs.filter(c => c.enabled !== false).map(c => {
return { type: c.type, value: (c.total ?? c.value) * -1 };
});
const resources = config.costs
.filter(c => c.enabled !== false)
.map(c => {
return { type: c.type, value: (c.total ?? c.value) * -1 };
});
await this.actor.modifyResource(resources);
if(config.uses?.enabled) {
if (config.uses?.enabled) {
const newActions = foundry.utils.getProperty(this.item.system, this.systemPath).map(x => x.toObject());
newActions[this.index].uses.value++;
await this.item.update({ [`system.${this.systemPath}`]: newActions });
@ -359,7 +373,7 @@ export class DHBaseAction extends foundry.abstract.DataModel {
/* ROLL */
get hasRoll() {
return !!this.roll?.type;
return !!this.roll?.type || !!this.roll?.bonus;
}
/* ROLL */
@ -387,14 +401,26 @@ export class DHBaseAction extends foundry.abstract.DataModel {
}
hasCost(costs) {
const realCosts = this.getRealCosts(costs);
return realCosts.reduce((a, c) => a && this.actor.system.resources[c.type]?.value >= (c.total ?? c.value), true);
const realCosts = this.getRealCosts(costs),
hasFearCost = realCosts.findIndex(c => c.type === 'fear');
if (hasFearCost > -1) {
const fearCost = realCosts.splice(hasFearCost, 1);
if (
!game.user.isGM ||
fearCost[0].total > game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear)
)
return false;
}
return realCosts.reduce(
(a, c) => a && this.actor.system.resources[c.type]?.value >= (c.total ?? c.value),
true
);
}
/* COST */
/* USES */
calcUses(uses) {
if(!uses) return null;
if (!uses) return null;
return {
...uses,
enabled: uses.hasOwnProperty('enabled') ? uses.enabled : true
@ -402,7 +428,7 @@ export class DHBaseAction extends foundry.abstract.DataModel {
}
hasUses(uses) {
if(!uses) return true;
if (!uses) return true;
return (uses.hasOwnProperty('enabled') && !uses.enabled) || uses.value + 1 <= uses.max;
}
/* USES */
@ -419,7 +445,7 @@ export class DHBaseAction extends foundry.abstract.DataModel {
);
}
formatTarget(actor) {
static formatTarget(actor) {
return {
id: actor.id,
actorId: actor.actor.uuid,
@ -432,19 +458,20 @@ export class DHBaseAction extends foundry.abstract.DataModel {
/* TARGET */
/* RANGE */
/* RANGE */
/* EFFECTS */
async applyEffects(event, data, force = false) {
if (!this.effects?.length || !data.system.targets.length) return;
async applyEffects(event, data, targets) {
targets ??= data.system.targets;
if (!this.effects?.length || !targets.length) return;
let effects = this.effects;
data.system.targets.forEach(async token => {
targets.forEach(async token => {
if (!token.hit && !force) return;
if(this.hasSave && token.saved.success === true) {
effects = this.effects.filter(e => e.onSave === true)
if (this.hasSave && token.saved.success === true) {
effects = this.effects.filter(e => e.onSave === true);
}
if(!effects.length) return;
if (!effects.length) return;
effects.forEach(async e => {
const actor = canvas.tokens.get(token.id)?.actor,
effect = this.item.effects.get(e._id);
@ -479,53 +506,76 @@ export class DHBaseAction extends foundry.abstract.DataModel {
/* SAVE */
async rollSave(target, event, message) {
if(!target?.actor) return;
target.actor.diceRoll({
event,
title: 'Roll Save',
roll: {
trait: this.save.trait,
difficulty: this.save.difficulty,
type: "reaction"
},
data: target.actor.getRollData()
}).then(async (result) => {
if(result) this.updateChatMessage(message, target.id, {result: result.roll.total, success: result.roll.success});
})
if (!target?.actor) return;
return target.actor
.diceRoll({
event,
title: 'Roll Save',
roll: {
trait: this.save.trait,
difficulty: this.save.difficulty,
type: 'reaction'
},
data: target.actor.getRollData()
})
.then(async result => {
if (result)
this.updateChatMessage(message, target.id, {
result: result.roll.total,
success: result.roll.success
});
});
}
async updateChatMessage(message, targetId, changes, chain=true) {
async updateChatMessage(message, targetId, changes, chain = true) {
setTimeout(async () => {
const chatMessage = ui.chat.collection.get(message._id),
msgTargets = chatMessage.system.targets,
msgTarget = msgTargets.find(mt => mt.id === targetId);
msgTarget.saved = changes;
await chatMessage.update({'system.targets': msgTargets});
},100);
if(chain) {
if(message.system.source.message) this.updateChatMessage(ui.chat.collection.get(message.system.source.message), targetId, changes, false);
await chatMessage.update({ 'system.targets': msgTargets });
}, 100);
if (chain) {
if (message.system.source.message)
this.updateChatMessage(ui.chat.collection.get(message.system.source.message), targetId, changes, false);
const relatedChatMessages = ui.chat.collection.filter(c => c.system.source.message === message._id);
relatedChatMessages.forEach(c => {
this.updateChatMessage(c, targetId, changes, false);
})
});
}
}
/* SAVE */
async toChat(origin) {
const cls = getDocumentClass('ChatMessage');
const systemData = {
title: game.i18n.localize('DAGGERHEART.ActionType.action'),
origin: origin,
img: this.img,
name: this.name,
description: this.description,
actions: []
};
const msg = new cls({
type: 'abilityUse',
user: game.user.id,
system: systemData,
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/chat/ability-use.hbs',
systemData
)
});
cls.create(msg.toObject());
}
}
export class DHDamageAction extends DHBaseAction {
static extraSchemas = ['damage', 'target', 'effects'];
/* async use(event, ...args) {
const config = await super.use(event, args);
if (!config || ['error', 'warning'].includes(config.type)) return;
if (!this.directDamage) return;
return await this.rollDamage(event, config);
} */
getFormulaValue(part, data) {
let formulaValue = part.value;
if(this.hasRoll && part.resultBased && data.system.roll.result.duality === -1) return part.valueAlt;
if (this.hasRoll && part.resultBased && data.system.roll.result.duality === -1) return part.valueAlt;
return formulaValue;
}
@ -535,19 +585,19 @@ export class DHDamageAction extends DHBaseAction {
if (!formula || formula == '') return;
let roll = { formula: formula, total: formula },
bonusDamage = [];
if(isNaN(formula)) formula = Roll.replaceFormulaData(formula, this.getRollData(data.system ?? data));
if (isNaN(formula)) formula = Roll.replaceFormulaData(formula, this.getRollData(data.system ?? data));
const config = {
title: game.i18n.format('DAGGERHEART.Chat.DamageRoll.Title', { damage: this.name }),
roll: {formula},
targets: (data.system?.targets.filter(t => t.hit) ?? data.targets),
roll: { formula },
targets: data.system?.targets.filter(t => t.hit) ?? data.targets,
hasSave: this.hasSave,
source: data.system?.source,
event
};
if(this.hasSave) config.onSave = this.save.damageMod;
if(data.system) {
if (this.hasSave) config.onSave = this.save.damageMod;
if (data.system) {
config.source.message = data._id;
config.directDamage = false;
}
@ -597,7 +647,8 @@ export class DHHealingAction extends DHBaseAction {
getFormulaValue(data) {
let formulaValue = this.healing.value;
if(this.hasRoll && this.healing.resultBased && data.system.roll.result.duality === -1) return this.healing.valueAlt;
if (this.hasRoll && this.healing.resultBased && data.system.roll.result.duality === -1)
return this.healing.valueAlt;
return formulaValue;
}
@ -608,12 +659,12 @@ export class DHHealingAction extends DHBaseAction {
if (!formula || formula == '') return;
let roll = { formula: formula, total: formula },
bonusDamage = [];
const config = {
title: game.i18n.format('DAGGERHEART.Chat.HealingRoll.Title', {
healing: game.i18n.localize(SYSTEM.GENERAL.healingTypes[this.healing.type].label)
}),
roll: {formula},
roll: { formula },
targets: (data.system?.targets ?? data.targets).filter(t => t.hit),
messageType: 'healing',
type: this.healing.type,

View file

@ -11,7 +11,8 @@ export class DHActionRollData extends foundry.abstract.DataModel {
type: new fields.StringField({ nullable: true, initial: null, choices: SYSTEM.GENERAL.rollTypes }),
trait: new fields.StringField({ nullable: true, initial: null, choices: SYSTEM.ACTOR.abilities }),
difficulty: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 }),
bonus: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 }),
bonus: new fields.NumberField({ nullable: true, initial: null, integer: true }),
advState: new fields.StringField({ choices: SYSTEM.ACTIONS.advandtageState, initial: 'neutral' }),
diceRolling: new fields.SchemaField({
multiplier: new fields.StringField({
choices: SYSTEM.GENERAL.diceSetNumbers,
@ -25,17 +26,20 @@ export class DHActionRollData extends foundry.abstract.DataModel {
initial: 'above',
label: 'Should be'
}),
treshold: new fields.NumberField({ initial: 1, integer: true, min: 1, label: 'Treshold' }),
treshold: new fields.NumberField({ initial: 1, integer: true, min: 1, label: 'Treshold' })
})
};
}
getFormula() {
if(!this.type) return;
if (!this.type) return;
let formula = '';
switch (this.type) {
case 'diceSet':
const multiplier = this.diceRolling.multiplier === 'flat' ? this.diceRolling.flatMultiplier : `@${this.diceRolling.multiplier}`;
const multiplier =
this.diceRolling.multiplier === 'flat'
? this.diceRolling.flatMultiplier
: `@${this.diceRolling.multiplier}`;
formula = `${multiplier}${this.diceRolling.dice}cs${SYSTEM.ACTIONS.diceCompare[this.diceRolling.compare].operator}${this.diceRolling.treshold}`;
break;
default:
@ -59,7 +63,7 @@ export class DHActionDiceData extends foundry.abstract.DataModel {
label: 'Multiplier'
}),
flatMultiplier: new fields.NumberField({ nullable: true, initial: 1, label: 'Flat Multiplier' }),
dice: new fields.StringField({ choices: SYSTEM.GENERAL.diceTypes, initial: 'd6', label: 'Formula' }),
dice: new fields.StringField({ choices: SYSTEM.GENERAL.diceTypes, initial: 'd6', label: 'Dice' }),
bonus: new fields.NumberField({ nullable: true, initial: null, label: 'Bonus' }),
custom: new fields.SchemaField({
enabled: new fields.BooleanField({ label: 'Custom Formula' }),
@ -75,9 +79,7 @@ export class DHActionDiceData extends foundry.abstract.DataModel {
: `${multiplier ?? 1}${this.dice}${this.bonus ? (this.bonus < 0 ? ` - ${Math.abs(this.bonus)}` : ` + ${this.bonus}`) : ''}`; */
const multiplier = this.multiplier === 'flat' ? this.flatMultiplier : `@${this.multiplier}`,
bonus = this.bonus ? (this.bonus < 0 ? ` - ${Math.abs(this.bonus)}` : ` + ${this.bonus}`) : '';
return this.custom.enabled
? this.custom.formula
: `${multiplier ?? 1}${this.dice}${bonus}`;
return this.custom.enabled ? this.custom.formula : `${multiplier ?? 1}${this.dice}${bonus}`;
}
}
@ -105,9 +107,12 @@ export class DHDamageData extends foundry.abstract.DataModel {
nullable: false,
required: true
}),
resultBased: new fields.BooleanField({ initial: false, label: "DAGGERHEART.Actions.Settings.ResultBased.label" }),
resultBased: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.Actions.Settings.ResultBased.label'
}),
value: new fields.EmbeddedDataField(DHActionDiceData),
valueAlt: new fields.EmbeddedDataField(DHActionDiceData),
valueAlt: new fields.EmbeddedDataField(DHActionDiceData)
};
}
}

View file

@ -1,11 +1,13 @@
import DhCharacter from './character.mjs';
import DhCompanion from './companion.mjs';
import DhAdversary from './adversary.mjs';
import DhEnvironment from './environment.mjs';
export { DhCharacter, DhAdversary, DhEnvironment };
export { DhCharacter, DhCompanion, DhAdversary, DhEnvironment };
export const config = {
character: DhCharacter,
companion: DhCompanion,
adversary: DhAdversary,
environment: DhEnvironment
};

View file

@ -30,8 +30,11 @@ export default class DhpAdversary extends BaseDataActor {
choices: SYSTEM.ACTOR.adversaryTypes,
initial: SYSTEM.ACTOR.adversaryTypes.standard.id
}),
motivesAndTactics: new fields.HTMLField(),
description: new fields.StringField(),
motivesAndTactics: new fields.StringField(),
notes: new fields.HTMLField(),
difficulty: new fields.NumberField({ required: true, initial: 1, integer: true }),
hordeHp: new fields.NumberField({ required: true, initial: 1, integer: true }),
damageThresholds: new fields.SchemaField({
major: new fields.NumberField({ required: true, initial: 0, integer: true }),
severe: new fields.NumberField({ required: true, initial: 0, integer: true })
@ -40,6 +43,7 @@ export default class DhpAdversary extends BaseDataActor {
hitPoints: resourceField(),
stress: resourceField()
}),
actions: new fields.ArrayField(new ActionField()),
attack: new ActionField({
initial: {
name: 'Attack',
@ -66,7 +70,7 @@ export default class DhpAdversary extends BaseDataActor {
experiences: new fields.TypedObjectField(
new fields.SchemaField({
name: new fields.StringField(),
value: new fields.NumberField({ required: true, integer: true, initial: 1 })
modifier: new fields.NumberField({ required: true, integer: true, initial: 1 })
})
),
bonuses: new fields.SchemaField({

View file

@ -1,6 +1,7 @@
import { burden } from '../../config/generalConfig.mjs';
import ActionField from '../fields/actionField.mjs';
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
import { LevelOptionType } from '../levelTier.mjs';
import DhLevelData from '../levelData.mjs';
import BaseDataActor from './base.mjs';
const attributeField = () =>
@ -96,7 +97,8 @@ export default class DhCharacter extends BaseDataActor {
value: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }),
subclass: new ForeignDocumentUUIDField({ type: 'Item', nullable: true })
}),
levelData: new fields.EmbeddedDataField(DhPCLevelData),
actions: new fields.ArrayField(new ActionField()),
levelData: new fields.EmbeddedDataField(DhLevelData),
bonuses: new fields.SchemaField({
armorScore: new fields.NumberField({ integer: true, initial: 0 }),
damageThresholds: new fields.SchemaField({
@ -115,6 +117,7 @@ export default class DhCharacter extends BaseDataActor {
magic: new fields.NumberField({ integer: true, initial: 0 })
})
}),
companion: new ForeignDocumentUUIDField({ type: 'Actor', nullable: true, initial: null }),
rules: new fields.SchemaField({
maxArmorMarked: new fields.SchemaField({
value: new fields.NumberField({ required: true, integer: true, initial: 1 }),
@ -154,10 +157,25 @@ export default class DhCharacter extends BaseDataActor {
return this.parent.items.find(x => x.type === 'community') ?? null;
}
get features() {
return this.parent.items.filter(x => x.type === 'feature') ?? [];
}
get companionFeatures() {
return this.companion ? this.companion.items.filter(x => x.type === 'feature') : [];
}
get needsCharacterSetup() {
return !this.class.value || !this.class.subclass;
}
get spellcastingModifiers() {
return {
main: this.class.subclass?.system?.spellcastingTrait,
multiclass: this.multiclass.subclass?.system?.spellcastingTrait
};
}
get domains() {
const classDomains = this.class.value ? this.class.value.system.domains : [];
const multiclassDomains = this.multiclass.value ? this.multiclass.value.system.domains : [];
@ -197,6 +215,12 @@ export default class DhCharacter extends BaseDataActor {
: null;
}
get deathMoveViable() {
return (
this.resources.hitPoints.maxTotal > 0 && this.resources.hitPoints.value >= this.resources.hitPoints.maxTotal
);
}
static async unequipBeforeEquip(itemToEquip) {
const primary = this.primaryWeapon,
secondary = this.secondaryWeapon;
@ -307,58 +331,10 @@ export default class DhCharacter extends BaseDataActor {
level: this.levelData.level.current
};
}
}
class DhPCLevelData extends foundry.abstract.DataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
level: new fields.SchemaField({
current: new fields.NumberField({ required: true, integer: true, initial: 1 }),
changed: new fields.NumberField({ required: true, integer: true, initial: 1 })
}),
levelups: new fields.TypedObjectField(
new fields.SchemaField({
achievements: new fields.SchemaField(
{
experiences: new fields.TypedObjectField(
new fields.SchemaField({
name: new fields.StringField({ required: true }),
modifier: new fields.NumberField({ required: true, integer: true })
})
),
domainCards: new fields.ArrayField(
new fields.SchemaField({
uuid: new fields.StringField({ required: true }),
itemUuid: new fields.StringField({ required: true })
})
),
proficiency: new fields.NumberField({ integer: true })
},
{ nullable: true, initial: null }
),
selections: new fields.ArrayField(
new fields.SchemaField({
tier: new fields.NumberField({ required: true, integer: true }),
level: new fields.NumberField({ required: true, integer: true }),
optionKey: new fields.StringField({ required: true }),
type: new fields.StringField({ required: true, choices: LevelOptionType }),
checkboxNr: new fields.NumberField({ required: true, integer: true }),
value: new fields.NumberField({ integer: true }),
minCost: new fields.NumberField({ integer: true }),
amount: new fields.NumberField({ integer: true }),
data: new fields.ArrayField(new fields.StringField({ required: true })),
secondaryData: new fields.TypedObjectField(new fields.StringField({ required: true })),
itemUuid: new fields.StringField({ required: true })
})
)
})
)
};
}
get canLevelUp() {
return this.level.current < this.level.changed;
async _preDelete() {
if (this.companion) {
this.companion.updateLevel(1);
}
}
}

View file

@ -0,0 +1,139 @@
import BaseDataActor from './base.mjs';
import DhLevelData from '../levelData.mjs';
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
import ActionField from '../fields/actionField.mjs';
import { adjustDice, adjustRange } from '../../helpers/utils.mjs';
export default class DhCompanion extends BaseDataActor {
static LOCALIZATION_PREFIXES = ['DAGGERHEART.Sheets.Companion'];
static get metadata() {
return foundry.utils.mergeObject(super.metadata, {
label: 'TYPES.Actor.companion',
type: 'companion'
});
}
static defineSchema() {
const fields = foundry.data.fields;
return {
partner: new ForeignDocumentUUIDField({ type: 'Actor' }),
resources: new fields.SchemaField({
stress: new fields.SchemaField({
value: new fields.NumberField({ initial: 0, integer: true }),
bonus: new fields.NumberField({ initial: 0, integer: true }),
max: new fields.NumberField({ initial: 3, integer: true })
}),
hope: new fields.NumberField({ initial: 0, integer: true })
}),
evasion: new fields.SchemaField({
value: new fields.NumberField({ required: true, min: 1, initial: 10, integer: true }),
bonus: new fields.NumberField({ initial: 0, integer: true })
}),
experiences: new fields.TypedObjectField(
new fields.SchemaField({
name: new fields.StringField({}),
value: new fields.NumberField({ integer: true, initial: 0 }),
bonus: new fields.NumberField({ integer: true, initial: 0 })
}),
{
initial: {
experience1: { value: 2 },
experience2: { value: 2 }
}
}
),
attack: new ActionField({
initial: {
name: 'Attack',
_id: foundry.utils.randomID(),
systemPath: 'attack',
type: 'attack',
range: 'melee',
target: {
type: 'any',
amount: 1
},
roll: {
type: 'weapon',
bonus: 0
},
damage: {
parts: [
{
multiplier: 'flat',
value: {
dice: 'd6',
multiplier: 'flat'
}
}
]
}
}
}),
actions: new fields.ArrayField(new ActionField()),
levelData: new fields.EmbeddedDataField(DhLevelData)
};
}
get attackBonus() {
return this.attack.roll.bonus ?? 0;
}
prepareBaseData() {
const partnerSpellcastingModifier = this.partner?.system?.spellcastingModifiers?.main;
const spellcastingModifier = this.partner?.system?.traits?.[partnerSpellcastingModifier]?.total;
this.attack.roll.bonus = spellcastingModifier ?? 0; // Needs to expand on which modifier it is that should be used because of multiclassing;
for (let levelKey in this.levelData.levelups) {
const level = this.levelData.levelups[levelKey];
for (let selection of level.selections) {
switch (selection.type) {
case 'hope':
this.resources.hope += selection.value;
break;
case 'vicious':
if (selection.data[0] === 'damage') {
this.attack.damage.parts[0].value.dice = adjustDice(this.attack.damage.parts[0].value.dice);
} else {
this.attack.range = adjustRange(this.attack.range);
}
break;
case 'stress':
this.resources.stress.bonus += selection.value;
break;
case 'evasion':
this.evasion.bonus += selection.value;
break;
case 'experience':
Object.keys(this.experiences).forEach(key => {
const experience = this.experiences[key];
experience.bonus += selection.value;
});
break;
}
}
}
}
prepareDerivedData() {
for (var experienceKey in this.experiences) {
var experience = this.experiences[experienceKey];
experience.total = experience.value + experience.bonus;
}
if (this.partner) {
this.partner.system.resources.hope.max += this.resources.hope;
}
this.resources.stress.maxTotal = this.resources.stress.max + this.resources.stress.bonus;
this.evasion.total = this.evasion.value + this.evasion.bonus;
}
async _preDelete() {
if (this.partner) {
await this.partner.update({ 'system.companion': null });
}
}
}

View file

@ -1,6 +1,8 @@
import { environmentTypes } from '../../config/actorConfig.mjs';
import BaseDataActor from './base.mjs';
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
import ActionField from '../fields/actionField.mjs';
import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs';
export default class DhEnvironment extends BaseDataActor {
static LOCALIZATION_PREFIXES = ['DAGGERHEART.Sheets.Environment'];
@ -21,15 +23,17 @@ export default class DhEnvironment extends BaseDataActor {
initial: SYSTEM.GENERAL.tiers.tier1.id
}),
type: new fields.StringField({ choices: environmentTypes }),
impulses: new fields.HTMLField(),
description: new fields.StringField(),
impulses: new fields.StringField(),
difficulty: new fields.NumberField({ required: true, initial: 11, integer: true }),
potentialAdversaries: new fields.TypedObjectField(
new fields.SchemaField({
label: new fields.StringField(),
adversaries: new fields.TypedObjectField(new ForeignDocumentUUIDField({ type: 'Actor' }))
adversaries: new ForeignDocumentUUIDArrayField({ type: 'Actor' })
})
)
/* Features pending datamodel rework */
),
actions: new fields.ArrayField(new ActionField()),
notes: new fields.HTMLField()
};
}
}

View file

@ -1,7 +1,9 @@
import { DHBaseAction } from '../action/action.mjs';
const fields = foundry.data.fields;
export default class DHAdversaryRoll extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
title: new fields.StringField(),
roll: new fields.DataField(),
@ -20,6 +22,7 @@ export default class DHAdversaryRoll extends foundry.abstract.TypeDataModel {
})
})
),
targetSelection: new fields.BooleanField({ initial: true }),
hasDamage: new fields.BooleanField({ initial: false }),
hasHealing: new fields.BooleanField({ initial: false }),
hasEffect: new fields.BooleanField({ initial: false }),
@ -28,11 +31,20 @@ export default class DHAdversaryRoll extends foundry.abstract.TypeDataModel {
actor: new fields.StringField(),
item: new fields.StringField(),
action: new fields.StringField()
})
}),
damage: new fields.ObjectField()
};
}
get messageTemplate() {
return 'systems/daggerheart/templates/chat/adversary-roll.hbs';
}
prepareDerivedData() {
this.hasHitTarget = this.targets.filter(t => t.hit === true).length > 0;
this.currentTargets =
this.targetSelection !== true
? Array.from(game.user.targets).map(t => DHBaseAction.formatTarget(t))
: this.targets;
}
}

View file

@ -3,7 +3,7 @@ export default class DHDamageRoll extends foundry.abstract.TypeDataModel {
const fields = foundry.data.fields;
return {
messageType: new fields.StringField({initial: 'damage'}),
messageType: new fields.StringField({ initial: 'damage' }),
title: new fields.StringField(),
roll: new fields.DataField({}),
targets: new fields.ArrayField(
@ -19,6 +19,7 @@ export default class DHDamageRoll extends foundry.abstract.TypeDataModel {
})
})
),
targetSelection: new fields.BooleanField({ initial: true }),
hasSave: new fields.BooleanField({ initial: false }),
onSave: new fields.StringField(),
source: new fields.SchemaField({
@ -27,11 +28,19 @@ export default class DHDamageRoll extends foundry.abstract.TypeDataModel {
action: new fields.StringField(),
message: new fields.StringField()
}),
directDamage: new fields.BooleanField({initial: true})
directDamage: new fields.BooleanField({ initial: true })
};
}
get messageTemplate() {
return `systems/daggerheart/templates/chat/${this.messageType}-roll.hbs`;
}
prepareDerivedData() {
this.hasHitTarget = this.targets.filter(t => t.hit === true).length > 0;
this.currentTargets =
this.targetSelection !== true
? Array.from(game.user.targets).map(t => DHBaseAction.formatTarget(t))
: this.targets;
}
}

View file

@ -1,44 +1,6 @@
const fields = foundry.data.fields;
export default class DHDualityRoll extends foundry.abstract.TypeDataModel {
static dualityResult = {
hope: 1,
fear: 2,
critical: 3
};
static defineSchema() {
return {
title: new fields.StringField(),
roll: new fields.DataField({}),
targets: new fields.ArrayField(
new fields.SchemaField({
id: new fields.StringField({}),
actorId: new fields.StringField({}),
name: new fields.StringField({}),
img: new fields.StringField({}),
difficulty: new fields.NumberField({ integer: true, nullable: true }),
evasion: new fields.NumberField({ integer: true }),
hit: new fields.BooleanField({ initial: false }),
saved: new fields.SchemaField({
result: new fields.NumberField(),
success: new fields.BooleanField({ nullable: true, initial: null })
})
})
),
costs: new fields.ArrayField(new fields.ObjectField()),
hasDamage: new fields.BooleanField({ initial: false }),
hasHealing: new fields.BooleanField({ initial: false }),
hasEffect: new fields.BooleanField({ initial: false }),
hasSave: new fields.BooleanField({ initial: false }),
source: new fields.SchemaField({
actor: new fields.StringField(),
item: new fields.StringField(),
action: new fields.StringField()
})
};
}
import DHAdversaryRoll from './adversaryRoll.mjs';
export default class DHDualityRoll extends DHAdversaryRoll {
get messageTemplate() {
return 'systems/daggerheart/templates/chat/duality-roll.hbs';
}

View file

View file

@ -36,7 +36,8 @@ class DhCountdownData extends foundry.abstract.DataModel {
})
})
)
})
}),
window: new fields.SchemaField({})
};
}

View file

@ -1,9 +1,9 @@
import { actionsTypes } from '../action/_module.mjs';
// import { actionsTypes } from '../action/_module.mjs';
// Temporary Solution
export default class ActionField extends foundry.data.fields.ObjectField {
getModel(value) {
return actionsTypes[value.type] ?? actionsTypes.attack;
return game.system.api.models.actionsTypes[value.type] ?? game.system.api.models.actionsTypes.attack;
}
/* -------------------------------------------- */

View file

@ -10,7 +10,7 @@ export default class DHArmor extends BaseDataItem {
type: 'armor',
hasDescription: true,
isQuantifiable: true,
isInventoryItem: true,
isInventoryItem: true
});
}

View file

@ -1,4 +1,4 @@
import { actionsTypes } from '../action/_module.mjs';
// import { actionsTypes } from '../action/_module.mjs';
/**
* Describes metadata about the item data model type
@ -20,7 +20,7 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
type: 'base',
hasDescription: false,
isQuantifiable: false,
isInventoryItem: false,
isInventoryItem: false
};
}
@ -54,13 +54,14 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
const data = { ...actorRollData, item: { ...this } };
return data;
}
async _preCreate(data, options, user) {
if(!this.constructor.metadata.hasInitialAction || !foundry.utils.isEmpty(this.actions)) return;
if (!this.constructor.metadata.hasInitialAction || !foundry.utils.isEmpty(this.actions)) return;
const actionType = {
weapon: 'attack'
}[this.constructor.metadata.type],
cls = actionsTypes.attack,
cls = game.system.api.models.actionsTypes[actionType],
// cls = actionsTypes.attack,
action = new cls(
{
_id: foundry.utils.randomID(),
@ -72,6 +73,6 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
parent: this.parent
}
);
this.updateSource({actions: [action]});
this.updateSource({ actions: [action] });
}
}

View file

@ -9,7 +9,7 @@ export default class DHConsumable extends BaseDataItem {
type: 'consumable',
hasDescription: true,
isQuantifiable: true,
isInventoryItem: true,
isInventoryItem: true
});
}

View file

@ -1,4 +1,3 @@
import { getTier } from '../../helpers/utils.mjs';
import BaseDataItem from './base.mjs';
import ActionField from '../fields/actionField.mjs';
@ -17,135 +16,7 @@ export default class DHFeature extends BaseDataItem {
const fields = foundry.data.fields;
return {
...super.defineSchema(),
//A type of feature seems unnecessary
type: new fields.StringField({ choices: SYSTEM.ITEM.featureTypes }),
//TODO: remove actionType field
actionType: new fields.StringField({
choices: SYSTEM.ITEM.actionTypes,
initial: SYSTEM.ITEM.actionTypes.passive.id
}),
//TODO: remove featureType field
featureType: new fields.SchemaField({
type: new fields.StringField({
choices: SYSTEM.ITEM.valueTypes,
initial: Object.keys(SYSTEM.ITEM.valueTypes).find(x => x === 'normal')
}),
data: new fields.SchemaField({
value: new fields.StringField({}),
property: new fields.StringField({
choices: SYSTEM.ACTOR.featureProperties,
initial: Object.keys(SYSTEM.ACTOR.featureProperties).find(x => x === 'spellcastingTrait')
}),
max: new fields.NumberField({ initial: 1, integer: true }),
numbers: new fields.TypedObjectField(
new fields.SchemaField({
value: new fields.NumberField({ integer: true }),
used: new fields.BooleanField({ initial: false })
})
)
})
}),
refreshData: new fields.SchemaField(
{
type: new fields.StringField({ choices: SYSTEM.GENERAL.refreshTypes }),
uses: new fields.NumberField({ initial: 1, integer: true }),
//TODO: remove refreshed field
refreshed: new fields.BooleanField({ initial: true })
},
{ nullable: true, initial: null }
),
//TODO: remove refreshed field
multiclass: new fields.NumberField({ initial: null, nullable: true, integer: true }),
disabled: new fields.BooleanField({ initial: false }),
//TODO: re do it completely or just remove it
effects: new fields.TypedObjectField(
new fields.SchemaField({
type: new fields.StringField({ choices: SYSTEM.EFFECTS.effectTypes }),
valueType: new fields.StringField({ choices: SYSTEM.EFFECTS.valueTypes }),
parseType: new fields.StringField({ choices: SYSTEM.EFFECTS.parseTypes }),
initiallySelected: new fields.BooleanField({ initial: true }),
options: new fields.ArrayField(
new fields.SchemaField({
name: new fields.StringField({}),
value: new fields.StringField({})
}),
{ nullable: true, initial: null }
),
dataField: new fields.StringField({}),
appliesOn: new fields.StringField(
{
choices: SYSTEM.EFFECTS.applyLocations
},
{ nullable: true, initial: null }
),
applyLocationChoices: new fields.TypedObjectField(new fields.StringField({}), {
nullable: true,
initial: null
}),
valueData: new fields.SchemaField({
value: new fields.StringField({}),
fromValue: new fields.StringField({ initial: null, nullable: true }),
type: new fields.StringField({ initial: null, nullable: true }),
hopeIncrease: new fields.StringField({ initial: null, nullable: true })
})
})
),
actions: new fields.ArrayField(new ActionField())
};
}
get multiclassTier() {
return getTier(this.multiclass);
}
async refresh() {
if (this.refreshData) {
if (this.featureType.type === SYSTEM.ITEM.valueTypes.dice.id) {
const update = { 'system.refreshData.refreshed': true };
Object.keys(this.featureType.data.numbers).forEach(
x => (update[`system.featureType.data.numbers.-=${x}`] = null)
);
await this.parent.update(update);
} else {
await this.parent.update({ 'system.refreshData.refreshed': true });
}
}
}
get effectData() {
const effectValues = Object.values(this.effects);
const effectCategories = Object.keys(SYSTEM.EFFECTS.effectTypes).reduce((acc, effectType) => {
acc[effectType] = effectValues.reduce((acc, effect) => {
if (effect.type === effectType) {
acc.push({ ...effect, valueData: this.#parseValues(effect.parseType, effect.valueData) });
}
return acc;
}, []);
return acc;
}, {});
return effectCategories;
}
#parseValues(parseType, values) {
return Object.keys(values).reduce((acc, prop) => {
acc[prop] = this.#parseValue(parseType, values[prop]);
return acc;
}, {});
}
#parseValue(parseType, value) {
switch (parseType) {
case SYSTEM.EFFECTS.parseTypes.number.id:
return Number.parseInt(value);
default:
return value;
}
}
}

View file

@ -9,7 +9,7 @@ export default class DHMiscellaneous extends BaseDataItem {
type: 'miscellaneous',
hasDescription: true,
isQuantifiable: true,
isInventoryItem: true,
isInventoryItem: true
});
}

View file

@ -2,7 +2,7 @@ import BaseDataItem from './base.mjs';
import FormulaField from '../fields/formulaField.mjs';
import ActionField from '../fields/actionField.mjs';
import { weaponFeatures } from '../../config/itemConfig.mjs';
import { actionsTypes } from '../action/_module.mjs';
import { actionsTypes } from '../action/_module.mjs';
export default class DHWeapon extends BaseDataItem {
/** @inheritDoc */
@ -13,7 +13,7 @@ export default class DHWeapon extends BaseDataItem {
hasDescription: true,
isQuantifiable: true,
isInventoryItem: true,
hasInitialAction: true,
hasInitialAction: true
});
}

61
module/data/levelData.mjs Normal file
View file

@ -0,0 +1,61 @@
import { LevelOptionType } from './levelTier.mjs';
export default class DhLevelData extends foundry.abstract.DataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
level: new fields.SchemaField({
current: new fields.NumberField({ required: true, integer: true, initial: 1 }),
changed: new fields.NumberField({ required: true, integer: true, initial: 1 }),
bonuses: new fields.TypedObjectField(new fields.NumberField({ integer: true, nullable: false }))
}),
levelups: new fields.TypedObjectField(
new fields.SchemaField({
achievements: new fields.SchemaField(
{
experiences: new fields.TypedObjectField(
new fields.SchemaField({
name: new fields.StringField({ required: true }),
modifier: new fields.NumberField({ required: true, integer: true })
})
),
domainCards: new fields.ArrayField(
new fields.SchemaField({
uuid: new fields.StringField({ required: true }),
itemUuid: new fields.StringField({ required: true })
})
),
proficiency: new fields.NumberField({ integer: true })
},
{ nullable: true, initial: null }
),
selections: new fields.ArrayField(
new fields.SchemaField({
tier: new fields.NumberField({ required: true, integer: true }),
level: new fields.NumberField({ required: true, integer: true }),
optionKey: new fields.StringField({ required: true }),
type: new fields.StringField({ required: true, choices: LevelOptionType }),
checkboxNr: new fields.NumberField({ required: true, integer: true }),
value: new fields.NumberField({ integer: true }),
minCost: new fields.NumberField({ integer: true }),
amount: new fields.NumberField({ integer: true }),
data: new fields.ArrayField(new fields.StringField({ required: true })),
secondaryData: new fields.TypedObjectField(new fields.StringField({ required: true })),
itemUuid: new fields.DocumentUUIDField({ required: true }),
featureIds: new fields.ArrayField(new fields.StringField())
})
)
})
)
};
}
get actions() {
return Object.values(this.levelups).flatMap(level => level.selections.flatMap(s => s.actions));
}
get canLevelUp() {
return this.level.current < this.level.changed;
}
}

View file

@ -58,6 +58,58 @@ class DhLevelOption extends foundry.abstract.DataModel {
}
}
export const CompanionLevelOptionType = {
hope: {
id: 'hope',
label: 'Light In The Dark'
},
creatureComfort: {
id: 'creatureComfort',
label: 'Creature Comfort',
features: [
{
name: 'DAGGERHEART.LevelUp.Actions.CreatureComfort.Name',
img: 'icons/magic/life/heart-cross-purple-orange.webp',
description: 'DAGGERHEART.LevelUp.Actions.CreatureComfort.Description'
}
]
},
armored: {
id: 'armored',
label: 'Armored',
features: [
{
name: 'DAGGERHEART.LevelUp.Actions.Armored.Name',
img: 'icons/equipment/shield/kite-wooden-oak-glow.webp',
description: 'DAGGERHEART.LevelUp.Actions.Armored.Description'
}
]
},
vicious: {
id: 'vicious',
label: 'Viscious'
},
resilient: {
id: 'resilient',
label: 'Resilient'
},
bonded: {
id: 'bonded',
label: 'Bonded',
features: [
{
name: 'DAGGERHEART.LevelUp.Actions.Bonded.Name',
img: 'icons/magic/life/heart-red-blue.webp',
description: 'DAGGERHEART.LevelUp.Actions.Bonded.Description'
}
]
},
aware: {
id: 'aware',
label: 'Aware'
}
};
export const LevelOptionType = {
trait: {
id: 'trait',
@ -106,7 +158,8 @@ export const LevelOptionType = {
multiclass: {
id: 'multiclass',
label: 'Multiclass'
}
},
...CompanionLevelOptionType
};
export const defaultLevelTiers = {
@ -338,3 +391,80 @@ export const defaultLevelTiers = {
}
}
};
export const defaultCompanionTier = {
tiers: {
2: {
tier: 2,
name: 'Companion Choices',
levels: {
start: 2,
end: 10
},
initialAchievements: {},
availableOptions: 1,
domainCardByLevel: 0,
options: {
experience: {
label: 'DAGGERHEART.LevelUp.Options.intelligent',
checkboxSelections: 3,
minCost: 1,
type: LevelOptionType.experience.id,
value: 1,
amount: 1
},
hope: {
label: 'DAGGERHEART.LevelUp.Options.lightInTheDark',
checkboxSelections: 1,
minCost: 1,
type: CompanionLevelOptionType.hope.id,
value: 1
},
creatureComfort: {
label: 'DAGGERHEART.LevelUp.Options.creatureComfort',
checkboxSelections: 1,
minCost: 1,
type: CompanionLevelOptionType.creatureComfort.id,
value: 1
},
armored: {
label: 'DAGGERHEART.LevelUp.Options.armored',
checkboxSelections: 1,
minCost: 1,
type: CompanionLevelOptionType.armored.id,
value: 1
},
vicious: {
label: 'DAGGERHEART.LevelUp.Options.vicious',
checkboxSelections: 3,
minCost: 1,
type: CompanionLevelOptionType.vicious.id,
value: 1,
amount: 1
},
stress: {
label: 'DAGGERHEART.LevelUp.Options.resilient',
checkboxSelections: 3,
minCost: 1,
type: LevelOptionType.stress.id,
value: 1
},
bonded: {
label: 'DAGGERHEART.LevelUp.Options.bonded',
checkboxSelections: 1,
minCost: 1,
type: CompanionLevelOptionType.bonded.id,
value: 1
},
evasion: {
label: 'DAGGERHEART.LevelUp.Options.aware',
checkboxSelections: 3,
minCost: 1,
type: LevelOptionType.evasion.id,
value: 2,
amount: 1
}
}
}
}
};

View file

@ -32,7 +32,7 @@ export class DhLevelup extends foundry.abstract.DataModel {
return acc;
}, {});
levels[i] = DhLevelupLevel.initializeData(pcLevelData.levelups[i], tier.availableOptions, {
levels[i] = DhLevelupLevel.initializeData(pcLevelData.levelups[i], tier.maxSelections[i], {
...initialAchievements,
experiences,
domainCards
@ -46,7 +46,7 @@ export class DhLevelup extends foundry.abstract.DataModel {
name: tier.name,
belongingLevels: belongingLevels,
options: Object.keys(tier.options).reduce((acc, key) => {
acc[key] = tier.options[key].toObject();
acc[key] = tier.options[key].toObject?.() ?? tier.options[key];
return acc;
}, {})
};
@ -98,6 +98,7 @@ export class DhLevelup extends foundry.abstract.DataModel {
case 'experience':
case 'domainCard':
case 'subclass':
case 'vicious':
return checkbox.data.length === (checkbox.amount ?? 1);
case 'multiclass':
const classSelected = checkbox.data.length === 1;

View file

@ -9,7 +9,8 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
this.config.experiences = [];
if (config.source?.action) {
this.item = config.data.parent.items.get(config.source.item);
console.log(config)
this.item = config.data.parent.items.get(config.source.item) ?? config.data.parent;
this.action =
config.data.attack?._id == config.source.action
? config.data.attack
@ -50,15 +51,18 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
};
async _prepareContext(_options) {
console.log(this.config, this.roll)
const context = await super._prepareContext(_options);
context.hasRoll = !!this.config.roll;
context.roll = this.roll;
context.rollType = this.roll?.constructor.name;
context.experiences = Object.keys(this.config.data.experiences).map(id => ({
id,
...this.config.data.experiences[id]
}));
context.selectedExperiences = this.config.experiences;
context.advantage = this.config.advantage;
/* context.diceOptions = this.diceOptions; */
context.advantage = this.config.roll?.advantage;
context.diceOptions = SYSTEM.GENERAL.diceTypes;
context.canRoll = true;
context.isLite = this.config.roll?.lite;
if (this.config.costs?.length) {
@ -71,7 +75,9 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
context.uses = this.action.calcUses(this.config.uses);
context.canRoll = context.canRoll && this.action.hasUses(context.uses);
}
context.extraFormula = this.config.extraFormula;
context.formula = this.roll.constructFormula(this.config);
return context;
}
@ -81,12 +87,18 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
this.config.costs = foundry.utils.mergeObject(this.config.costs, rest.costs);
}
if (this.config.uses) this.config.uses = foundry.utils.mergeObject(this.config.uses, rest.uses);
if(rest.roll?.dice) {
Object.entries(rest.roll.dice).forEach(([key, value]) => {
this.roll[key] = value;
})
}
this.config.extraFormula = rest.extraFormula;
this.render();
}
static updateIsAdvantage(_, button) {
const advantage = Number(button.dataset.advantage);
this.config.advantage = this.config.advantage === advantage ? 0 : advantage;
this.config.roll.advantage = this.config.roll.advantage === advantage ? 0 : advantage;
this.render();
}
@ -96,7 +108,10 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
} else {
this.config.experiences = [...this.config.experiences, button.dataset.key];
} */
this.config.experiences = this.config.experiences.indexOf(button.dataset.key) > -1 ? this.config.experiences.filter(x => x !== button.dataset.key) : [...this.config.experiences, button.dataset.key];
this.config.experiences =
this.config.experiences.indexOf(button.dataset.key) > -1
? this.config.experiences.filter(x => x !== button.dataset.key)
: [...this.config.experiences, button.dataset.key];
this.render();
}
@ -109,7 +124,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
if (!options.submitted) this.config = false;
}
static async configure(roll, config = {}, options={}) {
static async configure(roll, config = {}, options = {}) {
return new Promise(resolve => {
const app = new this(roll, config, options);
app.addEventListener('close', () => resolve(app.config), { once: true });

View file

@ -1,7 +1,7 @@
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export default class DamageDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(roll, config={}, options={}) {
constructor(roll, config = {}, options = {}) {
super(options);
this.roll = roll;
@ -37,24 +37,31 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.title = this.config.title;
context.formula = this.config.roll.formula;
context.extraFormula = this.config.extraFormula;
context.formula = this.roll.constructFormula(this.config);;
return context;
}
static updateRollConfiguration(event, _, formData) {
const { ...rest } = foundry.utils.expandObject(formData.object);
this.config.extraFormula = rest.extraFormula;
this.render();
}
static async submitRoll() {
await this.close({ submitted: true });
await this.close({ submitted: true });
}
/** @override */
_onClose(options={}) {
if ( !options.submitted ) this.config = false;
_onClose(options = {}) {
if (!options.submitted) this.config = false;
}
static async configure(roll, config={}) {
static async configure(roll, config = {}) {
return new Promise(resolve => {
const app = new this(roll, config);
app.addEventListener("close", () => resolve(app.config), { once: true });
app.addEventListener('close', () => resolve(app.config), { once: true });
app.render({ force: true });
});
}
}
}

View file

@ -1,6 +1,8 @@
import DamageSelectionDialog from '../applications/damageSelectionDialog.mjs';
import { GMUpdateEvent, socketEvent } from '../helpers/socket.mjs';
import DamageReductionDialog from '../applications/damageReductionDialog.mjs';
import { LevelOptionType } from '../data/levelTier.mjs';
import DHFeature from '../data/item/feature.mjs';
export default class DhpActor extends Actor {
async _preCreate(data, options, user) {
@ -8,7 +10,7 @@ export default class DhpActor extends Actor {
// Configure prototype token settings
const prototypeToken = {};
if (this.type === 'character')
if (['character', 'companion'].includes(this.type))
Object.assign(prototypeToken, {
sight: { enabled: true },
actorLink: true,
@ -18,7 +20,7 @@ export default class DhpActor extends Actor {
}
async updateLevel(newLevel) {
if (this.type !== 'character' || newLevel === this.system.levelData.level.changed) return;
if (!['character', 'companion'].includes(this.type) || newLevel === this.system.levelData.level.changed) return;
if (newLevel > this.system.levelData.level.current) {
const maxLevel = Object.values(
@ -41,6 +43,7 @@ export default class DhpActor extends Actor {
return acc;
}, {});
const featureIds = [];
const domainCards = [];
const experiences = [];
const subclassFeatureState = { class: null, multiclass: null };
@ -53,6 +56,7 @@ export default class DhpActor extends Actor {
const advancementCards = level.selections.filter(x => x.type === 'domainCard').map(x => x.itemUuid);
domainCards.push(...achievementCards, ...advancementCards);
experiences.push(...Object.keys(level.achievements.experiences));
featureIds.push(...level.selections.flatMap(x => x.featureIds));
const subclass = level.selections.find(x => x.type === 'subclass');
if (subclass) {
@ -66,13 +70,21 @@ export default class DhpActor extends Actor {
multiclass = level.selections.find(x => x.type === 'multiclass');
});
for (let featureId of featureIds) {
this.items.get(featureId).delete();
}
if (experiences.length > 0) {
this.update({
const getUpdate = () => ({
'system.experiences': experiences.reduce((acc, key) => {
acc[`-=${key}`] = null;
return acc;
}, {})
});
this.update(getUpdate());
if (this.system.companion) {
this.system.companion.update(getUpdate());
}
}
if (subclassFeatureState.class) {
@ -114,10 +126,15 @@ export default class DhpActor extends Actor {
}
}
});
if (this.system.companion) {
this.system.companion.updateLevel(newLevel);
}
}
}
async levelUp(levelupData) {
const actions = [];
const levelups = {};
for (var levelKey of Object.keys(levelupData)) {
const level = levelupData[levelKey];
@ -126,13 +143,23 @@ export default class DhpActor extends Actor {
const experience = level.achievements.experiences[experienceKey];
await this.update({
[`system.experiences.${experienceKey}`]: {
description: experience.name,
name: experience.name,
value: experience.modifier
}
});
if (this.system.companion) {
await this.system.companion.update({
[`system.experiences.${experienceKey}`]: {
name: '',
value: experience.modifier
}
});
}
}
let multiclass = null;
const featureAdditions = [];
const domainCards = [];
const subclassFeatureState = { class: null, multiclass: null };
const selections = [];
@ -141,7 +168,18 @@ export default class DhpActor extends Actor {
for (var checkboxNr of Object.keys(selection)) {
const checkbox = selection[checkboxNr];
if (checkbox.type === 'multiclass') {
const tierOption = LevelOptionType[checkbox.type];
if (tierOption.features?.length > 0) {
featureAdditions.push({
checkbox: {
...checkbox,
level: Number(levelKey),
optionKey: optionKey,
checkboxNr: Number(checkboxNr)
},
features: tierOption.features
});
} else if (checkbox.type === 'multiclass') {
multiclass = {
...checkbox,
level: Number(levelKey),
@ -174,6 +212,28 @@ export default class DhpActor extends Actor {
}
}
for (var addition of featureAdditions) {
for (var featureData of addition.features) {
const feature = new DHFeature({
...featureData,
description: game.i18n.localize(featureData.description)
});
const embeddedItem = await this.createEmbeddedDocuments('Item', [
{
...featureData,
name: game.i18n.localize(featureData.name),
type: 'feature',
system: feature
}
]);
addition.checkbox.featureIds = !addition.checkbox.featureIds
? [embeddedItem[0].id]
: [...addition.checkbox.featureIds, embeddedItem[0].id];
}
selections.push(addition.checkbox);
}
if (multiclass) {
const subclassItem = await foundry.utils.fromUuid(multiclass.secondaryData.subclass);
const subclassData = subclassItem.toObject();
@ -238,6 +298,7 @@ export default class DhpActor extends Actor {
await this.update({
system: {
actions: [...this.system.actions, ...actions],
levelData: {
level: {
current: this.system.levelData.level.changed
@ -246,6 +307,10 @@ export default class DhpActor extends Actor {
}
}
});
if (this.system.companion) {
this.system.companion.updateLevel(this.system.levelData.level.changed);
}
}
/**
@ -266,7 +331,7 @@ export default class DhpActor extends Actor {
* @param {object} [config.costs]
*/
async diceRoll(config) {
config.source = {...(config.source ?? {}), actor: this.uuid};
config.source = { ...(config.source ?? {}), actor: this.uuid };
config.data = this.getRollData();
const rollClass = config.roll.lite ? CONFIG.Dice.daggerheart['DHRoll'] : this.rollClass;
return await rollClass.build(config);
@ -364,6 +429,11 @@ export default class DhpActor extends Actor {
}
async takeDamage(damage, type) {
if (this.type === 'companion') {
await this.modifyResource([{ value: 1, type: 'stress' }]);
return;
}
const hpDamage =
damage >= this.system.damageThresholds.severe
? 3
@ -414,6 +484,11 @@ export default class DhpActor extends Actor {
let updates = { actor: { target: this, resources: {} }, armor: { target: this.system.armor, resources: {} } };
resources.forEach(r => {
switch (r.type) {
case 'fear':
ui.resources.updateFear(
game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear) + r.value
);
break;
case 'armorStack':
updates.armor.resources['system.marks.value'] = Math.max(
Math.min(this.system.armor.system.marks.value + r.value, this.system.armorScore),
@ -422,14 +497,16 @@ export default class DhpActor extends Actor {
break;
default:
updates.actor.resources[`system.resources.${r.type}.value`] = Math.max(
Math.min(this.system.resources[r.type].value + r.value, (this.system.resources[r.type].maxTotal ?? this.system.resources[r.type].max)),
Math.min(
this.system.resources[r.type].value + r.value,
this.system.resources[r.type].maxTotal ?? this.system.resources[r.type].max
),
0
);
break;
}
});
Object.values(updates).forEach(async u => {
console.log(updates, u)
if (Object.keys(u.resources).length > 0) {
if (game.user.isGM) {
await u.target.update(u.resources);

View file

@ -55,8 +55,8 @@ export default class DHItem extends foundry.documents.Item {
isInventoryItem === true
? 'Inventory Items'
: isInventoryItem === false
? 'Character Items'
: 'Other';
? 'Character Items'
: 'Other';
return { value: type, label, group };
}
@ -83,13 +83,14 @@ export default class DHItem extends foundry.documents.Item {
{ actions: this.system.actions }
),
title = 'Select Action';
return foundry.applications.api.DialogV2.prompt({
window: { title },
content,
ok: {
label: title,
callback: (event, button, dialog) => this.system.actions.find(a => a._id === button.form.elements.actionId.value)
callback: (event, button, dialog) =>
this.system.actions.find(a => a._id === button.form.elements.actionId.value)
}
});
}
@ -115,7 +116,9 @@ export default class DHItem extends foundry.documents.Item {
this.type === 'ancestry'
? game.i18n.localize('DAGGERHEART.Chat.FoundationCard.AncestryTitle')
: this.type === 'community'
? game.i18n.localize('DAGGERHEART.Chat.FoundationCard.CommunityTitle')
? game.i18n.localize('DAGGERHEART.Chat.FoundationCard.CommunityTitle')
: this.type === 'feature'
? game.i18n.localize('TYPES.Item.feature')
: game.i18n.localize('DAGGERHEART.Chat.FoundationCard.SubclassFeatureTitle'),
origin: origin,
img: this.img,

View file

@ -1,4 +1,4 @@
import { getDiceSoNicePresets } from '../config/generalConfig.mjs';
import { diceTypes, getDiceSoNicePresets, range } from '../config/generalConfig.mjs';
import Tagify from '@yaireo/tagify';
export const loadCompendiumOptions = async compendiums => {
@ -124,13 +124,13 @@ export const getCommandTarget = () => {
export const setDiceSoNiceForDualityRoll = (rollResult, advantageState) => {
const diceSoNicePresets = getDiceSoNicePresets();
rollResult.dice[0].options.appearance = diceSoNicePresets.hope;
rollResult.dice[1].options.appearance = diceSoNicePresets.fear;
rollResult.dice[0].options = { appearance: diceSoNicePresets.hope };
rollResult.dice[1].options = { appearance: diceSoNicePresets.fear }; //diceSoNicePresets.fear;
if (rollResult.dice[2]) {
if (advantageState === true) {
rollResult.dice[2].options.appearance = diceSoNicePresets.advantage;
rollResult.dice[2].options = { appearance: diceSoNicePresets.advantage };
} else if (advantageState === false) {
rollResult.dice[2].options.appearance = diceSoNicePresets.disadvantage;
rollResult.dice[2].options = { appearance: diceSoNicePresets.disadvantage };
}
}
};
@ -225,10 +225,10 @@ export const getDeleteKeys = (property, innerProperty, innerPropertyDefaultValue
// Fix on Foundry native formula replacement for DH
const nativeReplaceFormulaData = Roll.replaceFormulaData;
Roll.replaceFormulaData = function (formula, data={}, { missing, warn = false } = {}) {
Roll.replaceFormulaData = function (formula, data = {}, { missing, warn = false } = {}) {
const terms = Object.keys(SYSTEM.GENERAL.multiplierTypes).map(type => {
return { term: type, default: 1}
})
return { term: type, default: 1 };
});
formula = terms.reduce((a, c) => a.replaceAll(`@${c.term}`, data[c.term] ?? c.default), formula);
return nativeReplaceFormulaData(formula, data, { missing, warn });
};
@ -258,3 +258,39 @@ export const damageKeyToNumber = key => {
return 0;
}
};
export default function constructHTMLButton({
label,
dataset = {},
classes = [],
icon = '',
type = 'button',
disabled = false
}) {
const button = document.createElement('button');
button.type = type;
for (const [key, value] of Object.entries(dataset)) {
button.dataset[key] = value;
}
button.classList.add(...classes);
if (icon) icon = `<i class="${icon}"></i> `;
if (disabled) button.disabled = true;
button.innerHTML = `${icon}${label}`;
return button;
}
export const adjustDice = (dice, decrease) => {
const diceKeys = Object.keys(diceTypes);
const index = diceKeys.indexOf(dice);
const newIndex = decrease ? Math.max(index - 1, 0) : Math.min(index + 1, diceKeys.length - 1);
return diceTypes[diceKeys[newIndex]];
};
export const adjustRange = (rangeVal, decrease) => {
const rangeKeys = Object.keys(range);
const index = rangeKeys.indexOf(rangeVal);
const newIndex = decrease ? Math.max(index - 1, 0) : Math.min(index + 1, rangeKeys.length - 1);
return range[rangeKeys[newIndex]];
};

View file

@ -25,6 +25,9 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
html.querySelectorAll('.target-save-container').forEach(element =>
element.addEventListener('click', event => this.onRollSave(event, data.message))
);
html.querySelectorAll('.roll-all-save-button').forEach(element =>
element.addEventListener('click', event => this.onRollAllSave(event, data.message))
);
html.querySelectorAll('.duality-action-effect').forEach(element =>
element.addEventListener('click', event => this.onApplyEffect(event, data.message))
);
@ -33,6 +36,9 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
element.addEventListener('mouseleave', this.unhoverTarget);
element.addEventListener('click', this.clickTarget);
});
html.querySelectorAll('.button-target-selection').forEach(element => {
element.addEventListener('click', event => this.onTargetSelection(event, data.message));
});
html.querySelectorAll('.damage-button').forEach(element =>
element.addEventListener('click', event => this.onDamage(event, data.message))
);
@ -107,7 +113,6 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
tokenId = event.target.closest('[data-token]')?.dataset.token,
token = game.canvas.tokens.get(tokenId);
if (!token?.actor || !token.isOwner) return true;
console.log(token.actor.canUserModify(game.user, 'update'));
if (message.system.source.item && message.system.source.action) {
const action = this.getAction(actor, message.system.source.item, message.system.source.action);
if (!action || !action?.hasSave) return;
@ -115,6 +120,16 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
}
};
onRollAllSave = async (event, message) => {
event.stopPropagation();
const targets = event.target.parentElement.querySelectorAll(
'.target-section > [data-token] .target-save-container'
);
targets.forEach(el => {
el.dispatchEvent(new PointerEvent('click', { shiftKey: true }));
});
};
onApplyEffect = async (event, message) => {
event.stopPropagation();
const actor = await this.getActor(message.system.source.actor);
@ -122,10 +137,38 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
if (message.system.source.item && message.system.source.action) {
const action = this.getAction(actor, message.system.source.item, message.system.source.action);
if (!action || !action?.applyEffects) return;
await action.applyEffects(event, message);
const { isHit, targets } = this.getTargetList(event, message);
if (targets.length === 0)
ui.notifications.info(game.i18n.localize('DAGGERHEART.Notification.Info.NoTargetsSelected'));
await action.applyEffects(event, message, targets);
}
};
onTargetSelection = async (event, message) => {
event.stopPropagation();
const targetSelection = Boolean(event.target.dataset.targetHit),
msg = ui.chat.collection.get(message._id);
if (msg.system.targetSelection === targetSelection) return;
if (targetSelection !== true && !Array.from(game.user.targets).length)
return ui.notifications.info(game.i18n.localize('DAGGERHEART.Notification.Info.NoTargetsSelected'));
msg.system.targetSelection = targetSelection;
msg.system.prepareDerivedData();
ui.chat.updateMessage(msg);
};
getTargetList = (event, message) => {
const targetSelection = event.target
.closest('.message-content')
.querySelector('.button-target-selection.target-selected'),
isHit = Boolean(targetSelection.dataset.targetHit);
return {
isHit,
targets: isHit
? message.system.targets.filter(t => t.hit === true).map(target => game.canvas.tokens.get(target.id))
: Array.from(game.user.targets)
};
};
hoverTarget = event => {
event.stopPropagation();
const token = canvas.tokens.get(event.currentTarget.dataset.token);
@ -150,18 +193,18 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
onDamage = async (event, message) => {
event.stopPropagation();
const targets = event.currentTarget.dataset.targetHit
? message.system.targets.map(target => game.canvas.tokens.get(target.id))
: Array.from(game.user.targets);
const { isHit, targets } = this.getTargetList(event, message);
if(message.system.onSave && event.currentTarget.dataset.targetHit) {
const pendingingSaves = message.system.targets.filter(target => target.hit && target.saved.success === null);
if(pendingingSaves.length) {
if (message.system.onSave && isHit) {
const pendingingSaves = message.system.targets.filter(
target => target.hit && target.saved.success === null
);
if (pendingingSaves.length) {
const confirm = await foundry.applications.api.DialogV2.confirm({
window: {title: "Pending Reaction Rolls found"},
window: { title: 'Pending Reaction Rolls found' },
content: `<p>Some Tokens still need to roll their Reaction Roll.</p><p>Are you sure you want to continue ?</p><p><i>Undone reaction rolls will be considered as failed</i></p>`
});
if ( !confirm ) return;
if (!confirm) return;
}
}
@ -169,8 +212,9 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
ui.notifications.info(game.i18n.localize('DAGGERHEART.Notification.Info.NoTargetsSelected'));
for (let target of targets) {
let damage = message.system.roll.total;
if(message.system.onSave && message.system.targets.find(t => t.id === target.id)?.saved?.success === true) damage = Math.ceil(damage * (SYSTEM.ACTIONS.damageOnSave[message.system.onSave]?.mod ?? 1));
if (message.system.onSave && message.system.targets.find(t => t.id === target.id)?.saved?.success === true)
damage = Math.ceil(damage * (SYSTEM.ACTIONS.damageOnSave[message.system.onSave]?.mod ?? 1));
await target.actor.takeDamage(damage, message.system.roll.type);
}
};
@ -181,7 +225,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
if (targets.length === 0)
ui.notifications.info(game.i18n.localize('DAGGERHEART.Notification.Info.NoTargetsSelected'));
for (var target of targets) {
await target.actor.takeHealing([{ value: message.system.roll.total, type: message.system.roll.type }]);
}