mirror of
https://github.com/Foundryborne/daggerheart.git
synced 2026-03-07 14:36:13 +01:00
Merged with main
This commit is contained in:
commit
286944d2e6
207 changed files with 4909 additions and 1073 deletions
|
|
@ -1,5 +1,6 @@
|
|||
export { default as AttributionDialog } from './attributionDialog.mjs';
|
||||
export { default as BeastformDialog } from './beastformDialog.mjs';
|
||||
export { default as CharacterResetDialog } from './characterResetDialog.mjs';
|
||||
export { default as d20RollDialog } from './d20RollDialog.mjs';
|
||||
export { default as DamageDialog } from './damageDialog.mjs';
|
||||
export { default as DamageReductionDialog } from './damageReductionDialog.mjs';
|
||||
|
|
@ -14,3 +15,4 @@ export { default as ResourceDiceDialog } from './resourceDiceDialog.mjs';
|
|||
export { default as ActionSelectionDialog } from './actionSelectionDialog.mjs';
|
||||
export { default as GroupRollDialog } from './group-roll-dialog.mjs';
|
||||
export { default as TagTeamDialog } from './tagTeamDialog.mjs';
|
||||
export { default as RiskItAllDialog } from './riskItAllDialog.mjs';
|
||||
|
|
|
|||
105
module/applications/dialogs/characterResetDialog.mjs
Normal file
105
module/applications/dialogs/characterResetDialog.mjs
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
|
||||
|
||||
export default class CharacterResetDialog extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||
constructor(actor, options = {}) {
|
||||
super(options);
|
||||
|
||||
this.actor = actor;
|
||||
this.data = {
|
||||
delete: {
|
||||
class: { keep: false, label: 'TYPES.Item.class' },
|
||||
subclass: { keep: false, label: 'TYPES.Item.subclass' },
|
||||
ancestry: { keep: false, label: 'TYPES.Item.ancestry' },
|
||||
community: { keep: false, label: 'TYPES.Item.community' }
|
||||
},
|
||||
optional: {
|
||||
portrait: { keep: true, label: 'DAGGERHEART.GENERAL.portrait' },
|
||||
name: { keep: true, label: 'Name' },
|
||||
biography: { keep: true, label: 'DAGGERHEART.GENERAL.Tabs.biography' },
|
||||
inventory: { keep: true, label: 'DAGGERHEART.GENERAL.inventory' }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static DEFAULT_OPTIONS = {
|
||||
tag: 'form',
|
||||
classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'character-reset'],
|
||||
window: {
|
||||
icon: 'fa-solid fa-arrow-rotate-left',
|
||||
title: 'DAGGERHEART.APPLICATIONS.CharacterReset.title'
|
||||
},
|
||||
actions: {
|
||||
finishSelection: this.#finishSelection
|
||||
},
|
||||
form: {
|
||||
handler: this.updateData,
|
||||
submitOnChange: true,
|
||||
submitOnClose: false
|
||||
}
|
||||
};
|
||||
|
||||
/** @override */
|
||||
static PARTS = {
|
||||
resourceDice: {
|
||||
id: 'resourceDice',
|
||||
template: 'systems/daggerheart/templates/dialogs/characterReset.hbs'
|
||||
}
|
||||
};
|
||||
|
||||
async _prepareContext(_options) {
|
||||
const context = await super._prepareContext(_options);
|
||||
context.data = this.data;
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
static async updateData(event, _, formData) {
|
||||
const { data } = foundry.utils.expandObject(formData.object);
|
||||
|
||||
this.data = foundry.utils.mergeObject(this.data, data);
|
||||
this.render();
|
||||
}
|
||||
|
||||
static getUpdateData() {
|
||||
const update = {};
|
||||
if (!this.data.optional.portrait) update.if(!this.data.optional.biography);
|
||||
|
||||
if (!this.data.optional.inventory) return update;
|
||||
}
|
||||
|
||||
static async #finishSelection() {
|
||||
const update = {};
|
||||
if (!this.data.optional.name.keep) {
|
||||
const defaultName = game.system.api.documents.DhpActor.defaultName({ type: 'character' });
|
||||
foundry.utils.setProperty(update, 'name', defaultName);
|
||||
foundry.utils.setProperty(update, 'prototypeToken.name', defaultName);
|
||||
}
|
||||
|
||||
if (!this.data.optional.portrait.keep) {
|
||||
foundry.utils.setProperty(update, 'img', this.actor.schema.fields.img.initial(this.actor));
|
||||
foundry.utils.setProperty(update, 'prototypeToken.==texture', {});
|
||||
foundry.utils.setProperty(update, 'prototypeToken.==ring', {});
|
||||
}
|
||||
|
||||
if (this.data.optional.biography.keep)
|
||||
foundry.utils.setProperty(update, 'system.biography', this.actor.system.biography);
|
||||
|
||||
if (this.data.optional.inventory.keep) foundry.utils.setProperty(update, 'system.gold', this.actor.system.gold);
|
||||
|
||||
const { system, ...rest } = update;
|
||||
await this.actor.update({
|
||||
...rest,
|
||||
'==system': system ?? {}
|
||||
});
|
||||
|
||||
const inventoryItemTypes = ['weapon', 'armor', 'consumable', 'loot'];
|
||||
await this.actor.deleteEmbeddedDocuments(
|
||||
'Item',
|
||||
this.actor.items
|
||||
.filter(x => !inventoryItemTypes.includes(x.type) || !this.data.optional.inventory.keep)
|
||||
.map(x => x.id)
|
||||
);
|
||||
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
|
|||
this.config = config;
|
||||
this.config.experiences = [];
|
||||
this.reactionOverride = config.actionType === 'reaction';
|
||||
this.selectedEffects = this.config.bonusEffects;
|
||||
|
||||
if (config.source?.action) {
|
||||
this.item = config.data.parent.items.get(config.source.item) ?? config.data.parent;
|
||||
|
|
@ -35,6 +36,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
|
|||
selectExperience: this.selectExperience,
|
||||
toggleReaction: this.toggleReaction,
|
||||
toggleTagTeamRoll: this.toggleTagTeamRoll,
|
||||
toggleSelectedEffect: this.toggleSelectedEffect,
|
||||
submitRoll: this.submitRoll
|
||||
},
|
||||
form: {
|
||||
|
|
@ -76,6 +78,9 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
|
|||
icon
|
||||
}));
|
||||
|
||||
context.hasSelectedEffects = Boolean(this.selectedEffects && Object.keys(this.selectedEffects).length);
|
||||
context.selectedEffects = this.selectedEffects;
|
||||
|
||||
this.config.costs ??= [];
|
||||
if (this.config.costs?.length) {
|
||||
const updatedCosts = game.system.api.fields.ActionFields.CostField.calcCosts.call(
|
||||
|
|
@ -104,11 +109,17 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
|
|||
context.roll = this.roll;
|
||||
context.rollType = this.roll?.constructor.name;
|
||||
context.rallyDie = this.roll.rallyChoices;
|
||||
const experiences = this.config.data?.system?.experiences || {};
|
||||
|
||||
const actorExperiences = this.config.data?.system?.experiences || {};
|
||||
const companionExperiences = this.config.roll.companionRoll
|
||||
? (this.config.data?.companion?.system.experiences ?? {})
|
||||
: null;
|
||||
const experiences = companionExperiences ?? actorExperiences;
|
||||
context.experiences = Object.keys(experiences).map(id => ({
|
||||
id,
|
||||
...experiences[id]
|
||||
}));
|
||||
|
||||
context.selectedExperiences = this.config.experiences;
|
||||
context.advantage = this.config.roll?.advantage;
|
||||
context.disadvantage = this.config.roll?.disadvantage;
|
||||
|
|
@ -118,7 +129,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
|
|||
context.formula = this.roll.constructFormula(this.config);
|
||||
if (this.actor?.system?.traits) context.abilities = this.getTraitModifiers();
|
||||
|
||||
context.showReaction = !this.config.roll?.type && context.rollType === 'DualityRoll';
|
||||
context.showReaction = !this.config.skips?.reaction && context.rollType === 'DualityRoll';
|
||||
context.reactionOverride = this.reactionOverride;
|
||||
}
|
||||
|
||||
|
|
@ -208,6 +219,11 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
|
|||
this.render();
|
||||
}
|
||||
|
||||
static toggleSelectedEffect(_event, button) {
|
||||
this.selectedEffects[button.dataset.key].selected = !this.selectedEffects[button.dataset.key].selected;
|
||||
this.render();
|
||||
}
|
||||
|
||||
static async submitRoll() {
|
||||
await this.close({ submitted: true });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
|
|||
|
||||
this.roll = roll;
|
||||
this.config = config;
|
||||
this.selectedEffects = this.config.bonusEffects;
|
||||
}
|
||||
|
||||
static DEFAULT_OPTIONS = {
|
||||
|
|
@ -20,6 +21,7 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
|
|||
icon: 'fa-solid fa-dice'
|
||||
},
|
||||
actions: {
|
||||
toggleSelectedEffect: this.toggleSelectedEffect,
|
||||
submitRoll: this.submitRoll
|
||||
},
|
||||
form: {
|
||||
|
|
@ -57,6 +59,9 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
|
|||
icon
|
||||
}));
|
||||
context.modifiers = this.config.modifiers;
|
||||
context.hasSelectedEffects = Boolean(Object.keys(this.selectedEffects).length);
|
||||
context.selectedEffects = this.selectedEffects;
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
|
|
@ -69,6 +74,11 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
|
|||
this.render();
|
||||
}
|
||||
|
||||
static toggleSelectedEffect(_event, button) {
|
||||
this.selectedEffects[button.dataset.key].selected = !this.selectedEffects[button.dataset.key].selected;
|
||||
this.render();
|
||||
}
|
||||
|
||||
static async submitRoll() {
|
||||
await this.close({ submitted: true });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,16 @@
|
|||
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
||||
import { enrichedFateRoll } from '../../enrichers/FateRollEnricher.mjs';
|
||||
import { enrichedDualityRoll } from '../../enrichers/DualityRollEnricher.mjs';
|
||||
|
||||
export default class DhpDeathMove extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
||||
export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||
constructor(actor) {
|
||||
super({});
|
||||
|
||||
this.actor = actor;
|
||||
this.selectedMove = null;
|
||||
this.showRiskItAllButton = false;
|
||||
this.riskItAllButtonLabel = '';
|
||||
this.riskItAllHope = 0;
|
||||
}
|
||||
|
||||
get title() {
|
||||
|
|
@ -38,6 +43,111 @@ export default class DhpDeathMove extends HandlebarsApplicationMixin(Application
|
|||
return context;
|
||||
}
|
||||
|
||||
async handleAvoidDeath() {
|
||||
const target = this.actor.uuid;
|
||||
const config = await enrichedFateRoll({
|
||||
target,
|
||||
title: game.i18n.localize('DAGGERHEART.CONFIG.DeathMoves.avoidDeath.name'),
|
||||
label: `${game.i18n.localize('DAGGERHEART.GENERAL.hope')} ${game.i18n.localize('DAGGERHEART.GENERAL.fateRoll')}`,
|
||||
fateType: 'Hope'
|
||||
});
|
||||
|
||||
if (!config.roll.fate) return;
|
||||
|
||||
let returnMessage = game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.avoidScar');
|
||||
if (config.roll.fate.value <= this.actor.system.levelData.level.current) {
|
||||
const newScarAmount = this.actor.system.scars + 1;
|
||||
await this.actor.update({
|
||||
system: {
|
||||
scars: newScarAmount
|
||||
}
|
||||
});
|
||||
|
||||
if (newScarAmount >= this.actor.system.resources.hope.max) {
|
||||
await this.actor.setDeathMoveDefeated(CONFIG.DH.GENERAL.defeatedConditionChoices.dead.id);
|
||||
return game.i18n.format('DAGGERHEART.UI.Chat.deathMove.journeysEnd', { scars: newScarAmount });
|
||||
}
|
||||
|
||||
returnMessage = game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.gainScar');
|
||||
}
|
||||
|
||||
await this.actor.setDeathMoveDefeated(CONFIG.DH.GENERAL.defeatedConditionChoices.unconscious.id);
|
||||
return returnMessage;
|
||||
}
|
||||
|
||||
async handleRiskItAll() {
|
||||
const config = await enrichedDualityRoll({
|
||||
reaction: true,
|
||||
traitValue: null,
|
||||
target: this.actor,
|
||||
difficulty: null,
|
||||
title: game.i18n.localize('DAGGERHEART.CONFIG.DeathMoves.riskItAll.name'),
|
||||
label: game.i18n.localize('DAGGERHEART.GENERAL.dualityDice'),
|
||||
actionType: null,
|
||||
advantage: null,
|
||||
grantResources: false,
|
||||
customConfig: { skips: { resources: true, reaction: true } }
|
||||
});
|
||||
|
||||
if (!config.roll.result) return;
|
||||
|
||||
const clearAllStressAndHitpointsUpdates = [
|
||||
{ key: 'hitPoints', clear: true },
|
||||
{ key: 'stress', clear: true }
|
||||
];
|
||||
|
||||
let chatMessage = '';
|
||||
if (config.roll.isCritical) {
|
||||
config.resourceUpdates.addResources(clearAllStressAndHitpointsUpdates);
|
||||
chatMessage = game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.riskItAllCritical');
|
||||
}
|
||||
|
||||
if (config.roll.result.duality == 1) {
|
||||
if (
|
||||
config.roll.hope.value >=
|
||||
this.actor.system.resources.hitPoints.value + this.actor.system.resources.stress.value
|
||||
) {
|
||||
config.resourceUpdates.addResources(clearAllStressAndHitpointsUpdates);
|
||||
chatMessage = game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.riskItAllSuccessWithEnoughHope');
|
||||
} else {
|
||||
chatMessage = game.i18n.format('DAGGERHEART.UI.Chat.deathMove.riskItAllSuccess', {
|
||||
hope: config.roll.hope.value
|
||||
});
|
||||
this.showRiskItAllButton = true;
|
||||
this.riskItAllHope = config.roll.hope.value;
|
||||
this.riskItAllButtonLabel = game.i18n.format('DAGGERHEART.UI.Chat.deathMove.riskItAllDialogButton');
|
||||
}
|
||||
}
|
||||
|
||||
if (config.roll.result.duality == -1) {
|
||||
await this.actor.setDeathMoveDefeated(CONFIG.DH.GENERAL.defeatedConditionChoices.dead.id);
|
||||
chatMessage = game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.riskItAllFailure');
|
||||
}
|
||||
|
||||
await config.resourceUpdates.updateResources();
|
||||
return chatMessage;
|
||||
}
|
||||
|
||||
async handleBlazeOfGlory() {
|
||||
this.actor.createEmbeddedDocuments('ActiveEffect', [
|
||||
{
|
||||
name: game.i18n.localize('DAGGERHEART.CONFIG.DeathMoves.blazeOfGlory.name'),
|
||||
description: game.i18n.localize('DAGGERHEART.CONFIG.DeathMoves.blazeOfGlory.description'),
|
||||
img: CONFIG.DH.GENERAL.deathMoves.blazeOfGlory.img,
|
||||
changes: [
|
||||
{
|
||||
key: 'system.rules.roll.guaranteedCritical',
|
||||
mode: 2,
|
||||
value: 'true'
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
||||
await this.actor.setDeathMoveDefeated(CONFIG.DH.GENERAL.defeatedConditionChoices.dead.id);
|
||||
return game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.blazeOfGlory');
|
||||
}
|
||||
|
||||
static selectMove(_, button) {
|
||||
const move = button.dataset.move;
|
||||
this.selectedMove = CONFIG.DH.GENERAL.deathMoves[move];
|
||||
|
|
@ -46,23 +156,49 @@ export default class DhpDeathMove extends HandlebarsApplicationMixin(Application
|
|||
}
|
||||
|
||||
static async takeMove() {
|
||||
this.close();
|
||||
|
||||
let result = '';
|
||||
|
||||
if (CONFIG.DH.GENERAL.deathMoves.blazeOfGlory === this.selectedMove) {
|
||||
result = await this.handleBlazeOfGlory();
|
||||
}
|
||||
|
||||
if (CONFIG.DH.GENERAL.deathMoves.avoidDeath === this.selectedMove) {
|
||||
result = await this.handleAvoidDeath();
|
||||
}
|
||||
|
||||
if (CONFIG.DH.GENERAL.deathMoves.riskItAll === this.selectedMove) {
|
||||
result = await this.handleRiskItAll();
|
||||
}
|
||||
|
||||
if (!result) return;
|
||||
|
||||
const autoExpandDescription = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance)
|
||||
.expandRollMessage?.desc;
|
||||
const cls = getDocumentClass('ChatMessage');
|
||||
|
||||
const msg = {
|
||||
user: game.user.id,
|
||||
content: await foundry.applications.handlebars.renderTemplate(
|
||||
'systems/daggerheart/templates/ui/chat/deathMove.hbs',
|
||||
{
|
||||
player: this.actor.name,
|
||||
actor: { name: this.actor.name, img: this.actor.img },
|
||||
actor: this.actor,
|
||||
actorId: this.actor._id,
|
||||
author: game.users.get(game.user.id),
|
||||
title: game.i18n.localize(this.selectedMove.name),
|
||||
img: this.selectedMove.img,
|
||||
description: game.i18n.localize(this.selectedMove.description)
|
||||
description: game.i18n.localize(this.selectedMove.description),
|
||||
result: result,
|
||||
open: autoExpandDescription ? 'open' : '',
|
||||
chevron: autoExpandDescription ? 'fa-chevron-up' : 'fa-chevron-down',
|
||||
showRiskItAllButton: this.showRiskItAllButton,
|
||||
riskItAllButtonLabel: this.riskItAllButtonLabel,
|
||||
riskItAllHope: this.riskItAllHope
|
||||
}
|
||||
),
|
||||
title: game.i18n.localize(
|
||||
'DAGGERHEART.UI.Chat.deathMove.title'
|
||||
),
|
||||
title: game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.title'),
|
||||
speaker: cls.getSpeaker(),
|
||||
flags: {
|
||||
daggerheart: {
|
||||
|
|
@ -72,7 +208,5 @@ export default class DhpDeathMove extends HandlebarsApplicationMixin(Application
|
|||
};
|
||||
|
||||
cls.create(msg);
|
||||
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,27 +93,29 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
|
|||
}
|
||||
|
||||
getRefreshables() {
|
||||
const actionItems = this.actor.items.filter(x => this.actor.system.isItemAvailable(x)).reduce((acc, x) => {
|
||||
if (x.system.actions) {
|
||||
const recoverable = x.system.actions.reduce((acc, action) => {
|
||||
if (refreshIsAllowed([this.shortrest ? 'shortRest' : 'longRest'], action.uses.recovery)) {
|
||||
acc.push({
|
||||
title: x.name,
|
||||
name: action.name,
|
||||
uuid: action.uuid
|
||||
});
|
||||
const actionItems = this.actor.items
|
||||
.filter(x => this.actor.system.isItemAvailable(x))
|
||||
.reduce((acc, x) => {
|
||||
if (x.system.actions) {
|
||||
const recoverable = x.system.actions.reduce((acc, action) => {
|
||||
if (refreshIsAllowed([this.shortrest ? 'shortRest' : 'longRest'], action.uses.recovery)) {
|
||||
acc.push({
|
||||
title: x.name,
|
||||
name: action.name,
|
||||
uuid: action.uuid
|
||||
});
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
if (recoverable) {
|
||||
acc.push(...recoverable);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
if (recoverable) {
|
||||
acc.push(...recoverable);
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
return acc;
|
||||
}, []);
|
||||
const resourceItems = this.actor.items.reduce((acc, x) => {
|
||||
if (
|
||||
x.system.resource &&
|
||||
|
|
@ -189,7 +191,8 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
|
|||
}));
|
||||
});
|
||||
});
|
||||
const characters = game.actors.filter(x => x.type === 'character')
|
||||
const characters = game.actors
|
||||
.filter(x => x.type === 'character')
|
||||
.filter(x => x.testUserPermission(game.user, 'LIMITED'))
|
||||
.filter(x => x.uuid !== this.actor.uuid);
|
||||
|
||||
|
|
|
|||
94
module/applications/dialogs/riskItAllDialog.mjs
Normal file
94
module/applications/dialogs/riskItAllDialog.mjs
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
||||
export default class RiskItAllDialog extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||
constructor(actor, resourceValue) {
|
||||
super({});
|
||||
|
||||
this.actor = actor;
|
||||
this.resourceValue = resourceValue;
|
||||
this.choices = {
|
||||
hitPoints: 0,
|
||||
stress: 0
|
||||
};
|
||||
}
|
||||
|
||||
get title() {
|
||||
return game.i18n.format('DAGGERHEART.APPLICATIONS.RiskItAllDialog.title', { name: this.actor.name });
|
||||
}
|
||||
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ['daggerheart', 'dh-style', 'dialog', 'views', 'risk-it-all'],
|
||||
position: { width: 280, height: 'auto' },
|
||||
window: { icon: 'fa-solid fa-dice fa-xl' },
|
||||
actions: {
|
||||
finish: RiskItAllDialog.#finish
|
||||
}
|
||||
};
|
||||
|
||||
static PARTS = {
|
||||
application: {
|
||||
id: 'risk-it-all',
|
||||
template: 'systems/daggerheart/templates/dialogs/riskItAllDialog.hbs'
|
||||
}
|
||||
};
|
||||
|
||||
_attachPartListeners(partId, htmlElement, options) {
|
||||
super._attachPartListeners(partId, htmlElement, options);
|
||||
|
||||
for (const input of htmlElement.querySelectorAll('.resource-container input'))
|
||||
input.addEventListener('change', this.updateChoice.bind(this));
|
||||
}
|
||||
|
||||
async _prepareContext(_options) {
|
||||
const context = await super._prepareContext(_options);
|
||||
context.resourceValue = this.resourceValue;
|
||||
context.maxHitPointsValue = Math.min(this.resourceValue, this.actor.system.resources.hitPoints.max);
|
||||
context.maxStressValue = Math.min(this.resourceValue, this.actor.system.resources.stress.max);
|
||||
context.remainingResource = this.resourceValue - this.choices.hitPoints - this.choices.stress;
|
||||
context.unfinished = context.remainingResource !== 0;
|
||||
|
||||
context.choices = this.choices;
|
||||
context.final = {
|
||||
hitPoints: {
|
||||
value: this.actor.system.resources.hitPoints.value - this.choices.hitPoints,
|
||||
max: this.actor.system.resources.hitPoints.max
|
||||
},
|
||||
stress: {
|
||||
value: this.actor.system.resources.stress.value - this.choices.stress,
|
||||
max: this.actor.system.resources.stress.max
|
||||
}
|
||||
};
|
||||
|
||||
context;
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
updateChoice(event) {
|
||||
let value = Number.parseInt(event.target.value);
|
||||
const choiceKey = event.target.dataset.choice;
|
||||
const actorValue = this.actor.system.resources[choiceKey].value;
|
||||
const remaining = this.resourceValue - this.choices.hitPoints - this.choices.stress;
|
||||
const changeAmount = value - this.choices[choiceKey];
|
||||
|
||||
/* If trying to increase beyond remaining resource points, just increase to max available */
|
||||
if (remaining - changeAmount < 0) value = this.choices[choiceKey] + remaining;
|
||||
else if (actorValue - value < 0) value = actorValue;
|
||||
|
||||
this.choices[choiceKey] = value;
|
||||
this.render();
|
||||
}
|
||||
|
||||
static async #finish() {
|
||||
const resourceUpdate = Object.keys(this.choices).reduce((acc, resourceKey) => {
|
||||
const value = this.actor.system.resources[resourceKey].value - this.choices[resourceKey];
|
||||
acc[resourceKey] = { value };
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
await this.actor.update({
|
||||
'system.resources': resourceUpdate
|
||||
});
|
||||
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,8 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
|
|||
classes: ['daggerheart'],
|
||||
actions: {
|
||||
combat: DHTokenHUD.#onToggleCombat,
|
||||
togglePartyTokens: DHTokenHUD.#togglePartyTokens
|
||||
togglePartyTokens: DHTokenHUD.#togglePartyTokens,
|
||||
toggleCompanions: DHTokenHUD.#toggleCompanions
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -26,7 +27,7 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
|
|||
context.partyOnCanvas =
|
||||
this.actor.type === 'party' &&
|
||||
this.actor.system.partyMembers.some(member => member.getActiveTokens().length > 0);
|
||||
context.icons.toggleParty = 'systems/daggerheart/assets/icons/arrow-dunk.png';
|
||||
context.icons.toggleClowncar = 'systems/daggerheart/assets/icons/arrow-dunk.png';
|
||||
context.actorType = this.actor.type;
|
||||
context.usesEffects = this.actor.type !== 'party';
|
||||
context.canToggleCombat = DHTokenHUD.#nonCombatTypes.includes(this.actor.type)
|
||||
|
|
@ -56,6 +57,9 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
|
|||
}, {})
|
||||
: null;
|
||||
|
||||
context.hasCompanion = this.actor.system.companion;
|
||||
context.companionOnCanvas = context.hasCompanion && this.actor.system.companion.getActiveTokens().length > 0;
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
|
|
@ -101,8 +105,24 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
|
|||
: 'DAGGERHEART.APPLICATIONS.HUD.tokenHUD.depositPartyTokens'
|
||||
);
|
||||
|
||||
await this.toggleClowncar(this.actor.system.partyMembers);
|
||||
}
|
||||
|
||||
static async #toggleCompanions(_, button) {
|
||||
const icon = button.querySelector('img');
|
||||
icon.classList.toggle('flipped');
|
||||
button.dataset.tooltip = game.i18n.localize(
|
||||
icon.classList.contains('flipped')
|
||||
? 'DAGGERHEART.APPLICATIONS.HUD.tokenHUD.retrieveCompanionTokens'
|
||||
: 'DAGGERHEART.APPLICATIONS.HUD.tokenHUD.depositCompanionTokens'
|
||||
);
|
||||
|
||||
await this.toggleClowncar([this.actor.system.companion]);
|
||||
}
|
||||
|
||||
async toggleClowncar(actors) {
|
||||
const animationDuration = 500;
|
||||
const activeTokens = this.actor.system.partyMembers.flatMap(member => member.getActiveTokens());
|
||||
const activeTokens = actors.flatMap(member => member.getActiveTokens());
|
||||
const { x: actorX, y: actorY } = this.document;
|
||||
if (activeTokens.length > 0) {
|
||||
for (let token of activeTokens) {
|
||||
|
|
@ -114,14 +134,15 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
|
|||
}
|
||||
} else {
|
||||
const activeScene = game.scenes.find(x => x.id === game.user.viewedScene);
|
||||
const partyTokenData = [];
|
||||
for (let member of this.actor.system.partyMembers) {
|
||||
const tokenData = [];
|
||||
for (let member of actors) {
|
||||
const data = await member.getTokenDocument();
|
||||
partyTokenData.push(data.toObject());
|
||||
tokenData.push(data.toObject());
|
||||
}
|
||||
|
||||
const newTokens = await activeScene.createEmbeddedDocuments(
|
||||
'Token',
|
||||
partyTokenData.map(tokenData => ({
|
||||
tokenData.map(tokenData => ({
|
||||
...tokenData,
|
||||
alpha: 0,
|
||||
x: actorX,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import BaseLevelUp from './levelup.mjs';
|
||||
import { defaultCompanionTier, LevelOptionType } from '../../data/levelTier.mjs';
|
||||
import { DhLevelup } from '../../data/levelup.mjs';
|
||||
import { DhCompanionLevelup as DhLevelup } from '../../data/companionLevelup.mjs';
|
||||
import { diceTypes, range } from '../../config/generalConfig.mjs';
|
||||
|
||||
export default class DhCompanionLevelUp extends BaseLevelUp {
|
||||
|
|
@ -9,7 +9,9 @@ export default class DhCompanionLevelUp extends BaseLevelUp {
|
|||
|
||||
this.levelTiers = this.addBonusChoices(defaultCompanionTier);
|
||||
const playerLevelupData = actor.system.levelData;
|
||||
this.levelup = new DhLevelup(DhLevelup.initializeData(this.levelTiers, playerLevelupData));
|
||||
this.levelup = new DhLevelup(
|
||||
DhLevelup.initializeData(this.levelTiers, playerLevelupData, actor.system.levelupChoicesLeft)
|
||||
);
|
||||
}
|
||||
|
||||
async _preparePartContext(partId, context) {
|
||||
|
|
|
|||
|
|
@ -70,7 +70,10 @@ export default class DhlevelUpViewMode extends HandlebarsApplicationMixin(Applic
|
|||
return checkbox;
|
||||
});
|
||||
|
||||
let label = game.i18n.localize(option.label);
|
||||
let label =
|
||||
optionKey === 'domainCard'
|
||||
? game.i18n.format(option.label, { maxLevel: tier.levels.end })
|
||||
: game.i18n.localize(option.label);
|
||||
return {
|
||||
label: label,
|
||||
checkboxGroups: chunkify(checkboxes, option.minCost, chunkedBoxes => {
|
||||
|
|
|
|||
|
|
@ -5,10 +5,7 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S
|
|||
super(options);
|
||||
|
||||
Hooks.on(socketEvent.Refresh, ({ refreshType }) => {
|
||||
if (refreshType === RefreshType.Scene) {
|
||||
this.daggerheartFlag = new game.system.api.data.scenes.DHScene(this.document.flags.daggerheart);
|
||||
this.render();
|
||||
}
|
||||
if (refreshType === RefreshType.Scene) this.render();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -42,7 +39,9 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S
|
|||
|
||||
async _preRender(context, options) {
|
||||
await super._preFirstRender(context, options);
|
||||
this.daggerheartFlag = new game.system.api.data.scenes.DHScene(this.document.flags.daggerheart);
|
||||
|
||||
if (!options.internalRefresh)
|
||||
this.daggerheartFlag = new game.system.api.data.scenes.DHScene(this.document.flags.daggerheart);
|
||||
}
|
||||
|
||||
_attachPartListeners(partId, htmlElement, options) {
|
||||
|
|
@ -52,7 +51,7 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S
|
|||
case 'dh':
|
||||
htmlElement.querySelector('#rangeMeasurementSetting')?.addEventListener('change', async event => {
|
||||
this.daggerheartFlag.updateSource({ rangeMeasurement: { setting: event.target.value } });
|
||||
this.render();
|
||||
this.render({ internalRefresh: true });
|
||||
});
|
||||
|
||||
const dragArea = htmlElement.querySelector('.scene-environments');
|
||||
|
|
@ -66,10 +65,17 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S
|
|||
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
|
||||
const item = await foundry.utils.fromUuid(data.uuid);
|
||||
if (item instanceof game.system.api.documents.DhpActor && item.type === 'environment') {
|
||||
let sceneUuid = data.uuid;
|
||||
if (item.pack) {
|
||||
const inWorldActor = await game.system.api.documents.DhpActor.create([item.toObject()]);
|
||||
if (!inWorldActor.length) return;
|
||||
sceneUuid = inWorldActor[0].uuid;
|
||||
}
|
||||
|
||||
await this.daggerheartFlag.updateSource({
|
||||
sceneEnvironments: [...this.daggerheartFlag.sceneEnvironments, data.uuid]
|
||||
sceneEnvironments: [...this.daggerheartFlag.sceneEnvironments, sceneUuid]
|
||||
});
|
||||
this.render();
|
||||
this.render({ internalRefresh: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -92,12 +98,16 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S
|
|||
(_, index) => index !== Number.parseInt(button.dataset.index)
|
||||
)
|
||||
});
|
||||
this.render();
|
||||
this.render({ internalRefresh: true });
|
||||
}
|
||||
|
||||
/** @override */
|
||||
async _processSubmitData(event, form, submitData, options) {
|
||||
submitData.flags.daggerheart = this.daggerheartFlag.toObject();
|
||||
submitData.flags.daggerheart.sceneEnvironments = submitData.flags.daggerheart.sceneEnvironments.filter(x =>
|
||||
foundry.utils.fromUuidSync(x)
|
||||
);
|
||||
|
||||
for (const key of Object.keys(this.document._source.flags.daggerheart?.sceneEnvironments ?? {})) {
|
||||
if (!submitData.flags.daggerheart.sceneEnvironments[key]) {
|
||||
submitData.flags.daggerheart.sceneEnvironments[`-=${key}`] = null;
|
||||
|
|
|
|||
|
|
@ -36,7 +36,8 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
|
|||
addItem: this.addItem,
|
||||
editItem: this.editItem,
|
||||
removeItem: this.removeItem,
|
||||
resetMoves: this.resetMoves,
|
||||
resetDowntimeMoves: this.resetDowntimeMoves,
|
||||
resetItemFeatures: this.resetItemFeatures,
|
||||
addDomain: this.addDomain,
|
||||
toggleSelectedDomain: this.toggleSelectedDomain,
|
||||
deleteDomain: this.deleteDomain,
|
||||
|
|
@ -232,7 +233,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
|
|||
this.render();
|
||||
}
|
||||
|
||||
static async resetMoves(_, target) {
|
||||
static async resetDowntimeMoves(_, target) {
|
||||
const confirmed = await foundry.applications.api.DialogV2.confirm({
|
||||
window: {
|
||||
title: game.i18n.format('DAGGERHEART.SETTINGS.Homebrew.resetMovesTitle', {
|
||||
|
|
@ -266,7 +267,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
|
|||
...move,
|
||||
name: game.i18n.localize(move.name),
|
||||
description: game.i18n.localize(move.description),
|
||||
actions: move.actions.reduce((acc, key) => {
|
||||
actions: Object.keys(move.actions).reduce((acc, key) => {
|
||||
const action = move.actions[key];
|
||||
acc[key] = {
|
||||
...action,
|
||||
|
|
@ -293,6 +294,31 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
|
|||
this.render();
|
||||
}
|
||||
|
||||
static async resetItemFeatures(_, target) {
|
||||
const confirmed = await foundry.applications.api.DialogV2.confirm({
|
||||
window: {
|
||||
title: game.i18n.format('DAGGERHEART.SETTINGS.Homebrew.resetItemFeaturesTitle', {
|
||||
type: game.i18n.localize(`DAGGERHEART.GENERAL.${target.dataset.type}`)
|
||||
})
|
||||
},
|
||||
content: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.resetMovesText')
|
||||
});
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
await this.settings.updateSource({
|
||||
[`itemFeatures.${target.dataset.type}`]: Object.keys(
|
||||
this.settings.itemFeatures[target.dataset.type]
|
||||
).reduce((acc, key) => {
|
||||
acc[`-=${key}`] = null;
|
||||
|
||||
return acc;
|
||||
}, {})
|
||||
});
|
||||
|
||||
this.render();
|
||||
}
|
||||
|
||||
static async addDomain(event) {
|
||||
event.preventDefault();
|
||||
const content = new foundry.data.fields.StringField({
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
|
|||
editEffect: this.editEffect,
|
||||
addDamage: this.addDamage,
|
||||
removeDamage: this.removeDamage,
|
||||
editDoc: this.editDoc,
|
||||
addTrigger: this.addTrigger,
|
||||
removeTrigger: this.removeTrigger,
|
||||
expandTrigger: this.expandTrigger
|
||||
|
|
@ -39,7 +40,8 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
|
|||
handler: this.updateForm,
|
||||
submitOnChange: true,
|
||||
closeOnSubmit: false
|
||||
}
|
||||
},
|
||||
dragDrop: [{ dragSelector: null, dropSelector: '#summon-drop-zone', handlers: ['_onDrop'] }]
|
||||
};
|
||||
|
||||
static PARTS = {
|
||||
|
|
@ -101,7 +103,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
|
|||
}
|
||||
};
|
||||
|
||||
static CLEAN_ARRAYS = ['damage.parts', 'cost', 'effects'];
|
||||
static CLEAN_ARRAYS = ['damage.parts', 'cost', 'effects', 'summon'];
|
||||
|
||||
_getTabs(tabs) {
|
||||
for (const v of Object.values(tabs)) {
|
||||
|
|
@ -112,9 +114,25 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
|
|||
return tabs;
|
||||
}
|
||||
|
||||
_attachPartListeners(partId, htmlElement, options) {
|
||||
super._attachPartListeners(partId, htmlElement, options);
|
||||
|
||||
htmlElement.querySelectorAll('.summon-count-wrapper input').forEach(element => {
|
||||
element.addEventListener('change', this.updateSummonCount.bind(this));
|
||||
});
|
||||
}
|
||||
|
||||
async _prepareContext(_options) {
|
||||
const context = await super._prepareContext(_options, 'action');
|
||||
context.source = this.action.toObject(true);
|
||||
context.action = this.action;
|
||||
|
||||
context.summons = [];
|
||||
for (const summon of context.source.summon ?? []) {
|
||||
const actor = await foundry.utils.fromUuid(summon.actorUUID);
|
||||
context.summons.push({ actor, count: summon.count });
|
||||
}
|
||||
|
||||
context.openSection = this.openSection;
|
||||
context.tabs = this._getTabs(this.constructor.TABS);
|
||||
context.config = CONFIG.DH;
|
||||
|
|
@ -207,8 +225,9 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
|
|||
}
|
||||
|
||||
static async updateForm(event, _, formData) {
|
||||
const submitData = this._prepareSubmitData(event, formData),
|
||||
data = foundry.utils.mergeObject(this.action.toObject(), submitData);
|
||||
const submitData = this._prepareSubmitData(event, formData);
|
||||
|
||||
const data = foundry.utils.mergeObject(this.action.toObject(), submitData);
|
||||
this.action = await this.action.update(data);
|
||||
|
||||
this.sheetUpdate?.(this.action);
|
||||
|
|
@ -227,12 +246,26 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
|
|||
static removeElement(event, button) {
|
||||
event.stopPropagation();
|
||||
const data = this.action.toObject(),
|
||||
key = event.target.closest('[data-key]').dataset.key,
|
||||
index = button.dataset.index;
|
||||
key = event.target.closest('[data-key]').dataset.key;
|
||||
|
||||
// Prefer explicit index, otherwise find by uuid
|
||||
let index = button?.dataset.index;
|
||||
if (index === undefined || index === null || index === '') {
|
||||
const uuid = button?.dataset.uuid ?? button?.dataset.itemUuid;
|
||||
index = data[key].findIndex(e => (e?.actorUUID ?? e?.uuid) === uuid);
|
||||
if (index === -1) return;
|
||||
} else index = Number(index);
|
||||
|
||||
data[key].splice(index, 1);
|
||||
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
|
||||
}
|
||||
|
||||
static async editDoc(_event, target) {
|
||||
const element = target.closest('[data-item-uuid]');
|
||||
const doc = (await foundry.utils.fromUuid(element.dataset.itemUuid)) ?? null;
|
||||
if (doc) return doc.sheet.render({ force: true });
|
||||
}
|
||||
|
||||
static addDamage(_event) {
|
||||
if (!this.action.damage.parts) return;
|
||||
const data = this.action.toObject(),
|
||||
|
|
@ -304,6 +337,15 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
|
|||
}
|
||||
}
|
||||
|
||||
updateSummonCount(event) {
|
||||
event.stopPropagation();
|
||||
const wrapper = event.target.closest('.summon-count-wrapper');
|
||||
const index = wrapper.dataset.index;
|
||||
const data = this.action.toObject();
|
||||
data.summon[index].count = event.target.value;
|
||||
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
|
||||
}
|
||||
|
||||
/** Specific implementation in extending classes **/
|
||||
static async addEffect(_event) {}
|
||||
static removeEffect(_event, _button) {}
|
||||
|
|
@ -313,4 +355,29 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
|
|||
this.tabGroups.primary = 'base';
|
||||
await super.close(options);
|
||||
}
|
||||
|
||||
async _onDrop(event) {
|
||||
const data = foundry.applications.ux.TextEditor.getDragEventData(event);
|
||||
const item = await foundry.utils.fromUuid(data.uuid);
|
||||
if (!(item instanceof game.system.api.documents.DhpActor)) {
|
||||
ui.notifications.warn(game.i18n.localize('DAGGERHEART.ACTIONS.TYPES.summon.invalidDrop'));
|
||||
return;
|
||||
}
|
||||
|
||||
const actionData = this.action.toObject();
|
||||
let countvalue = 1;
|
||||
for (const entry of actionData.summon) {
|
||||
if (entry.actorUUID === data.uuid) {
|
||||
entry.count += 1;
|
||||
countvalue = entry.count;
|
||||
await this.constructor.updateForm.bind(this)(null, null, {
|
||||
object: foundry.utils.flattenObject(actionData)
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
actionData.summon.push({ actorUUID: data.uuid, count: countvalue });
|
||||
await this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(actionData) });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
export * as actors from './actors/_module.mjs';
|
||||
export * as api from './api/_modules.mjs';
|
||||
export * as items from './items/_module.mjs';
|
||||
export * as rollTables from './rollTables/_module.mjs';
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export default class AdversarySheet extends DHBaseActorSheet {
|
|||
dragSelector: '[data-item-id][draggable="true"], [data-item-id] [draggable="true"]',
|
||||
dropSelector: null
|
||||
}
|
||||
],
|
||||
]
|
||||
};
|
||||
|
||||
static PARTS = {
|
||||
|
|
@ -185,7 +185,6 @@ export default class AdversarySheet extends DHBaseActorSheet {
|
|||
super._onDragStart(event);
|
||||
}
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Application Clicks Actions */
|
||||
/* -------------------------------------------- */
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import DHBaseActorSheet from '../api/base-actor.mjs';
|
||||
import DhpDeathMove from '../../dialogs/deathMove.mjs';
|
||||
import DhDeathMove from '../../dialogs/deathMove.mjs';
|
||||
import { abilities } from '../../../config/actorConfig.mjs';
|
||||
import { CharacterLevelup, LevelupViewMode } from '../../levelup/_module.mjs';
|
||||
import DhCharacterCreation from '../../characterCreation/characterCreation.mjs';
|
||||
|
|
@ -27,6 +27,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
|||
makeDeathMove: CharacterSheet.#makeDeathMove,
|
||||
levelManagement: CharacterSheet.#levelManagement,
|
||||
viewLevelups: CharacterSheet.#viewLevelups,
|
||||
resetCharacter: CharacterSheet.#resetCharacter,
|
||||
toggleEquipItem: CharacterSheet.#toggleEquipItem,
|
||||
toggleResourceDice: CharacterSheet.#toggleResourceDice,
|
||||
handleResourceDice: CharacterSheet.#handleResourceDice,
|
||||
|
|
@ -42,6 +43,11 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
|||
icon: 'fa-solid fa-angles-up',
|
||||
label: 'DAGGERHEART.ACTORS.Character.viewLevelups',
|
||||
action: 'viewLevelups'
|
||||
},
|
||||
{
|
||||
icon: 'fa-solid fa-arrow-rotate-left',
|
||||
label: 'DAGGERHEART.ACTORS.Character.resetCharacter',
|
||||
action: 'resetCharacter'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -224,13 +230,6 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
|||
async _preparePartContext(partId, context, options) {
|
||||
context = await super._preparePartContext(partId, context, options);
|
||||
switch (partId) {
|
||||
case 'header':
|
||||
const { playerCanEditSheet, levelupAuto } = game.settings.get(
|
||||
CONFIG.DH.id,
|
||||
CONFIG.DH.SETTINGS.gameSettings.Automation
|
||||
);
|
||||
context.showSettings = game.user.isGM || !levelupAuto || (levelupAuto && playerCanEditSheet);
|
||||
break;
|
||||
case 'loadout':
|
||||
await this._prepareLoadoutContext(context, options);
|
||||
break;
|
||||
|
|
@ -679,12 +678,19 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
|||
new LevelupViewMode(this.document).render({ force: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the character data and removes all embedded documents.
|
||||
*/
|
||||
static async #resetCharacter() {
|
||||
new game.system.api.applications.dialogs.CharacterResetDialog(this.document).render({ force: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the Death Move interface for the character.
|
||||
* @type {ApplicationClickAction}
|
||||
*/
|
||||
static async #makeDeathMove() {
|
||||
await new DhpDeathMove(this.document).render({ force: true });
|
||||
await new DhDeathMove(this.document).render({ force: true });
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -725,8 +731,10 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
|||
headerTitle: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
|
||||
ability: abilityLabel
|
||||
}),
|
||||
effects: await game.system.api.data.actions.actionsTypes.base.getEffects(this.document),
|
||||
roll: {
|
||||
trait: button.dataset.attribute
|
||||
trait: button.dataset.attribute,
|
||||
type: 'trait'
|
||||
},
|
||||
hasRoll: true,
|
||||
actionType: 'action',
|
||||
|
|
@ -736,11 +744,12 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
|||
})
|
||||
};
|
||||
const result = await this.document.diceRoll(config);
|
||||
if (!result) return;
|
||||
|
||||
/* This could be avoided by baking config.costs into config.resourceUpdates. Didn't feel like messing with it at the time */
|
||||
const costResources = result.costs
|
||||
.filter(x => x.enabled)
|
||||
.map(cost => ({ ...cost, value: -cost.value, total: -cost.total }));
|
||||
const costResources =
|
||||
result.costs?.filter(x => x.enabled).map(cost => ({ ...cost, value: -cost.value, total: -cost.total })) ||
|
||||
{};
|
||||
config.resourceUpdates.addResources(costResources);
|
||||
await config.resourceUpdates.updateResources();
|
||||
}
|
||||
|
|
@ -840,7 +849,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
|||
static async #toggleVault(_event, button) {
|
||||
const doc = await getDocFromElement(button);
|
||||
const { available } = this.document.system.loadoutSlot;
|
||||
if (doc.system.inVault && !available) {
|
||||
if (doc.system.inVault && !available && !doc.system.loadoutIgnore) {
|
||||
return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.loadoutMaxReached'));
|
||||
}
|
||||
|
||||
|
|
@ -971,6 +980,18 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
|||
return this._onSidebarDrop(event, item);
|
||||
}
|
||||
|
||||
const setupCriticalItemTypes = ['class', 'subclass', 'ancestry', 'community'];
|
||||
if (this.document.system.needsCharacterSetup && setupCriticalItemTypes.includes(item.type)) {
|
||||
const confirmed = await foundry.applications.api.DialogV2.confirm({
|
||||
window: {
|
||||
title: game.i18n.localize('DAGGERHEART.APPLICATIONS.CharacterCreation.setupSkipTitle')
|
||||
},
|
||||
content: game.i18n.localize('DAGGERHEART.APPLICATIONS.CharacterCreation.setupSkipContent')
|
||||
});
|
||||
|
||||
if (!confirmed) return;
|
||||
}
|
||||
|
||||
if (this.document.uuid === item.parent?.uuid) {
|
||||
return super._onDropItem(event, item);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,15 +38,6 @@ export default class DhCompanionSheet extends DHBaseActorSheet {
|
|||
}
|
||||
};
|
||||
|
||||
/** @inheritDoc */
|
||||
async _onRender(context, options) {
|
||||
await super._onRender(context, options);
|
||||
|
||||
this.element
|
||||
.querySelector('.level-value')
|
||||
?.addEventListener('change', event => this.document.updateLevel(Number(event.currentTarget.value)));
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Application Clicks Actions */
|
||||
/* -------------------------------------------- */
|
||||
|
|
@ -71,10 +62,10 @@ export default class DhCompanionSheet extends DHBaseActorSheet {
|
|||
title: `${game.i18n.localize('DAGGERHEART.GENERAL.Roll.action')}: ${this.actor.name}`,
|
||||
headerTitle: `Companion ${game.i18n.localize('DAGGERHEART.GENERAL.Roll.action')}`,
|
||||
roll: {
|
||||
trait: partner.system.spellcastModifierTrait?.key
|
||||
trait: partner.system.spellcastModifierTrait?.key,
|
||||
companionRoll: true
|
||||
},
|
||||
hasRoll: true,
|
||||
data: partner.getRollData()
|
||||
hasRoll: true
|
||||
};
|
||||
|
||||
const result = await partner.diceRoll(config);
|
||||
|
|
|
|||
|
|
@ -505,6 +505,10 @@ export default function DHApplicationMixin(Base) {
|
|||
const doc = await getDocFromElement(target),
|
||||
action = doc?.system?.attack ?? doc;
|
||||
const config = action.prepareConfig(event);
|
||||
config.effects = await game.system.api.data.actions.actionsTypes.base.getEffects(
|
||||
this.document,
|
||||
doc
|
||||
);
|
||||
config.hasRoll = false;
|
||||
return action && action.workflow.get('damage').execute(config, null, true);
|
||||
}
|
||||
|
|
@ -629,7 +633,7 @@ export default function DHApplicationMixin(Base) {
|
|||
{
|
||||
relativeTo: isAction ? doc.parent : doc,
|
||||
rollData: doc.getRollData?.(),
|
||||
secrets: isAction ? doc.parent.isOwner : doc.isOwner
|
||||
secrets: isAction ? doc.parent.parent.isOwner : doc.isOwner
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
|||
1
module/applications/sheets/rollTables/_module.mjs
Normal file
1
module/applications/sheets/rollTables/_module.mjs
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default as RollTableSheet } from './rollTable.mjs';
|
||||
191
module/applications/sheets/rollTables/rollTable.mjs
Normal file
191
module/applications/sheets/rollTables/rollTable.mjs
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
export default class DhRollTableSheet extends foundry.applications.sheets.RollTableSheet {
|
||||
static DEFAULT_OPTIONS = {
|
||||
...super.DEFAULT_OPTIONS,
|
||||
actions: {
|
||||
changeMode: DhRollTableSheet.#onChangeMode,
|
||||
drawResult: DhRollTableSheet.#onDrawResult,
|
||||
resetResults: DhRollTableSheet.#onResetResults,
|
||||
addFormula: DhRollTableSheet.#addFormula,
|
||||
removeFormula: DhRollTableSheet.#removeFormula
|
||||
}
|
||||
};
|
||||
|
||||
static buildParts() {
|
||||
const { footer, header, sheet, results, ...parts } = super.PARTS;
|
||||
return {
|
||||
sheet: {
|
||||
...sheet,
|
||||
template: 'systems/daggerheart/templates/sheets/rollTable/sheet.hbs'
|
||||
},
|
||||
header: { template: 'systems/daggerheart/templates/sheets/rollTable/header.hbs' },
|
||||
...parts,
|
||||
results: {
|
||||
template: 'systems/daggerheart/templates/sheets/rollTable/results.hbs',
|
||||
templates: ['templates/sheets/roll-table/result-details.hbs'],
|
||||
scrollable: ['table[data-results] tbody']
|
||||
},
|
||||
summary: { template: 'systems/daggerheart/templates/sheets/rollTable/summary.hbs' },
|
||||
footer
|
||||
};
|
||||
}
|
||||
|
||||
static PARTS = DhRollTableSheet.buildParts();
|
||||
|
||||
async _preRender(context, options) {
|
||||
await super._preRender(context, options);
|
||||
|
||||
if (!options.internalRefresh)
|
||||
this.daggerheartFlag = new game.system.api.data.DhRollTable(this.document.flags.daggerheart);
|
||||
}
|
||||
|
||||
/* root PART has a blank element on _attachPartListeners, so it cannot be used to set the eventListeners for the view mode */
|
||||
async _onRender(context, options) {
|
||||
super._onRender(context, options);
|
||||
|
||||
for (const element of this.element.querySelectorAll('.system-update-field'))
|
||||
element.addEventListener('change', this.updateSystemField.bind(this));
|
||||
}
|
||||
|
||||
async _preparePartContext(partId, context, options) {
|
||||
context = await super._preparePartContext(partId, context, options);
|
||||
|
||||
switch (partId) {
|
||||
case 'sheet':
|
||||
context.altFormula = this.daggerheartFlag.altFormula;
|
||||
context.usesAltFormula = Object.keys(this.daggerheartFlag.altFormula).length > 0;
|
||||
context.altFormulaOptions = {
|
||||
'': { name: this.daggerheartFlag.formulaName },
|
||||
...this.daggerheartFlag.altFormula
|
||||
};
|
||||
context.activeAltFormula = this.daggerheartFlag.activeAltFormula;
|
||||
context.selectedFormula = this.daggerheartFlag.getActiveFormula(this.document.formula);
|
||||
context.results = this.getExtendedResults(context.results);
|
||||
break;
|
||||
case 'header':
|
||||
context.altFormula = this.daggerheartFlag.altFormula;
|
||||
context.usesAltFormula = Object.keys(this.daggerheartFlag.altFormula).length > 0;
|
||||
context.altFormulaOptions = {
|
||||
'': { name: this.daggerheartFlag.formulaName },
|
||||
...this.daggerheartFlag.altFormula
|
||||
};
|
||||
context.activeAltFormula = this.daggerheartFlag.activeAltFormula;
|
||||
break;
|
||||
case 'summary':
|
||||
context.systemFields = this.daggerheartFlag.schema.fields;
|
||||
context.altFormula = this.daggerheartFlag.altFormula;
|
||||
context.formulaName = this.daggerheartFlag.formulaName;
|
||||
break;
|
||||
case 'results':
|
||||
context.results = this.getExtendedResults(context.results);
|
||||
break;
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
getExtendedResults(results) {
|
||||
const bodyDarkMode = document.body.classList.contains('theme-dark');
|
||||
const elementLightMode = this.element.classList.contains('theme-light');
|
||||
const elementDarkMode = this.element.classList.contains('theme-dark');
|
||||
const isDarkMode = elementDarkMode || (!elementLightMode && bodyDarkMode);
|
||||
|
||||
return results.map(x => ({
|
||||
...x,
|
||||
displayImg: isDarkMode && x.img === 'icons/svg/d20-black.svg' ? 'icons/svg/d20.svg' : x.img
|
||||
}));
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Flag SystemData update methods */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
async updateSystemField(event) {
|
||||
const { dataset, value } = event.target;
|
||||
await this.daggerheartFlag.updateSource({ [dataset.path]: value });
|
||||
this.render({ internalRefresh: true });
|
||||
}
|
||||
|
||||
getSystemFlagUpdate() {
|
||||
const deleteUpdate = Object.keys(this.document._source.flags.daggerheart?.altFormula ?? {}).reduce(
|
||||
(acc, formulaKey) => {
|
||||
if (!this.daggerheartFlag.altFormula[formulaKey]) acc.altFormula[`-=${formulaKey}`] = null;
|
||||
|
||||
return acc;
|
||||
},
|
||||
{ altFormula: {} }
|
||||
);
|
||||
|
||||
return { ['flags.daggerheart']: foundry.utils.mergeObject(this.daggerheartFlag.toObject(), deleteUpdate) };
|
||||
}
|
||||
|
||||
static async #addFormula() {
|
||||
await this.daggerheartFlag.updateSource({
|
||||
[`altFormula.${foundry.utils.randomID()}`]: game.system.api.data.DhRollTable.getDefaultFormula()
|
||||
});
|
||||
this.render({ internalRefresh: true });
|
||||
}
|
||||
|
||||
static async #removeFormula(_event, target) {
|
||||
await this.daggerheartFlag.updateSource({
|
||||
[`altFormula.-=${target.dataset.key}`]: null
|
||||
});
|
||||
this.render({ internalRefresh: true });
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Extended RollTable methods */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Alternate between view and edit modes.
|
||||
* @this {RollTableSheet}
|
||||
* @type {ApplicationClickAction}
|
||||
*/
|
||||
static async #onChangeMode() {
|
||||
this.mode = this.isEditMode ? 'view' : 'edit';
|
||||
await this.document.update(this.getSystemFlagUpdate());
|
||||
await this.render({ internalRefresh: true });
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
async _processSubmitData(event, form, submitData, options) {
|
||||
/* RollTable sends an empty dummy event when swapping from view/edit first time */
|
||||
if (Object.keys(submitData).length) {
|
||||
if (!submitData.flags) submitData.flags = { daggerheart: {} };
|
||||
submitData.flags.daggerheart = this.getSystemFlagUpdate();
|
||||
}
|
||||
|
||||
super._processSubmitData(event, form, submitData, options);
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
static async #onResetResults() {
|
||||
await this.document.update(this.getSystemFlagUpdate());
|
||||
await this.document.resetResults();
|
||||
}
|
||||
|
||||
/**
|
||||
* Roll and draw a TableResult.
|
||||
* @this {RollTableSheet}
|
||||
* @type {ApplicationClickAction}
|
||||
*/
|
||||
static async #onDrawResult(_event, button) {
|
||||
if (this.form) await this.submit({ operation: { render: false } });
|
||||
button.disabled = true;
|
||||
const table = this.document;
|
||||
|
||||
await this.document.update(this.getSystemFlagUpdate());
|
||||
|
||||
/* Sending in the currently selectd activeFormula to table.roll to use as the formula */
|
||||
const selectedFormula = this.daggerheartFlag.getActiveFormula(this.document.formula);
|
||||
const tableRoll = await table.roll({ selectedFormula });
|
||||
const draws = table.getResultsForRoll(tableRoll.roll.total);
|
||||
if (draws.length > 0) {
|
||||
if (game.settings.get('core', 'animateRollTable')) await this._animateRoll(draws);
|
||||
await table.draw(tableRoll);
|
||||
}
|
||||
|
||||
// Reenable the button if drawing with replacement since the draw won't trigger a sheet re-render
|
||||
if (table.replacement) button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -25,7 +25,7 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract
|
|||
|
||||
/** @override */
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ['dh-style'],
|
||||
classes: ['dh-style', 'directory'],
|
||||
window: {
|
||||
title: 'SIDEBAR.TabSettings'
|
||||
},
|
||||
|
|
|
|||
|
|
@ -81,6 +81,9 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
|
|||
html.querySelectorAll('.group-roll-header-expand-section').forEach(element =>
|
||||
element.addEventListener('click', this.groupRollExpandSection)
|
||||
);
|
||||
html.querySelectorAll('.risk-it-all-button').forEach(element =>
|
||||
element.addEventListener('click', event => this.riskItAllClearStressAndHitPoints(event, data))
|
||||
);
|
||||
};
|
||||
|
||||
setupHooks() {
|
||||
|
|
@ -92,6 +95,21 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
|
|||
super.close(options);
|
||||
}
|
||||
|
||||
/** Ensure the chat theme inherits the interface theme */
|
||||
_replaceHTML(result, content, options) {
|
||||
const themedElement = result.log?.querySelector('.chat-log');
|
||||
themedElement?.classList.remove('themed', 'theme-light', 'theme-dark');
|
||||
super._replaceHTML(result, content, options);
|
||||
}
|
||||
|
||||
/** Remove chat log theme from notifications area */
|
||||
async _onFirstRender(result, content) {
|
||||
await super._onFirstRender(result, content);
|
||||
document
|
||||
.querySelector('#chat-notifications .chat-log')
|
||||
?.classList.remove('themed', 'theme-light', 'theme-dark');
|
||||
}
|
||||
|
||||
async onRollSimple(event, message) {
|
||||
const buttonType = event.target.dataset.type ?? 'damage',
|
||||
total = message.rolls.reduce((a, c) => a + Roll.fromJSON(c).total, 0),
|
||||
|
|
@ -135,7 +153,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
|
|||
async actionUseButton(event, message) {
|
||||
const { moveIndex, actionIndex, movePath } = event.currentTarget.dataset;
|
||||
const targetUuid = event.currentTarget.closest('.action-use-button-parent').querySelector('select')?.value;
|
||||
const parent = await foundry.utils.fromUuid(targetUuid || message.system.actor)
|
||||
const parent = await foundry.utils.fromUuid(targetUuid || message.system.actor);
|
||||
|
||||
const actionType = message.system.moves[moveIndex].actions[actionIndex];
|
||||
const cls = game.system.api.models.actions.actionsTypes[actionType.type];
|
||||
|
|
@ -370,4 +388,10 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
|
|||
});
|
||||
event.target.closest('.group-roll-section').querySelector('.group-roll-content').classList.toggle('closed');
|
||||
}
|
||||
|
||||
async riskItAllClearStressAndHitPoints(event, data) {
|
||||
const resourceValue = event.target.dataset.resourceValue;
|
||||
const actor = game.actors.get(event.target.dataset.actorId);
|
||||
new game.system.api.applications.dialogs.RiskItAllDialog(actor, resourceValue).render({ force: true });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,8 +42,8 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
|
|||
this.combats
|
||||
.find(x => x.active)
|
||||
?.system?.extendedBattleToggles?.reduce((acc, toggle) => (acc ?? 0) + toggle.category, null) ?? null;
|
||||
const maxBP = CONFIG.DH.ENCOUNTER.BaseBPPerEncounter(context.characters.length) + modifierBP;
|
||||
const currentBP = AdversaryBPPerEncounter(context.adversaries, context.characters);
|
||||
const maxBP = CONFIG.DH.ENCOUNTER.BaseBPPerEncounter(context.allCharacters.length) + modifierBP;
|
||||
const currentBP = AdversaryBPPerEncounter(context.adversaries, context.allCharacters);
|
||||
|
||||
Object.assign(context, {
|
||||
fear: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear),
|
||||
|
|
@ -73,9 +73,8 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
|
|||
Object.assign(context, {
|
||||
actionTokens: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).actionTokens,
|
||||
adversaries,
|
||||
characters: characters
|
||||
?.filter(x => !x.isNPC)
|
||||
.filter(x => !spotlightQueueEnabled || x.system.spotlight.requestOrderIndex == 0),
|
||||
allCharacters: characters,
|
||||
characters: characters.filter(x => !spotlightQueueEnabled || x.system.spotlight.requestOrderIndex == 0),
|
||||
spotlightRequests
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,6 +76,8 @@ export default class DhEffectsDisplay extends HandlebarsApplicationMixin(Applica
|
|||
};
|
||||
|
||||
toggleHidden(token, focused) {
|
||||
if (!this.element) return;
|
||||
|
||||
const effects = DhEffectsDisplay.getTokenEffects(focused ? token : null);
|
||||
this.element.hidden = effects.length === 0;
|
||||
|
||||
|
|
|
|||
|
|
@ -230,6 +230,14 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
|||
result.flatMap(r => r),
|
||||
'name'
|
||||
);
|
||||
|
||||
/* If any noticeable slowdown occurs, consider replacing with enriching description on clicking to expand descriptions */
|
||||
for (const item of this.items) {
|
||||
item.system.enrichedDescription =
|
||||
(await item.system.getEnrichedDescription?.()) ??
|
||||
(await foundry.applications.ux.TextEditor.implementation.enrichHTML(item.description));
|
||||
}
|
||||
|
||||
this.fieldFilter = this._createFieldFilter();
|
||||
|
||||
if (this.presets?.filter) {
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export default class DhSceneNavigation extends foundry.applications.ui.SceneNavi
|
|||
const environments = daggerheartInfo.sceneEnvironments.filter(
|
||||
x => x && x.testUserPermission(game.user, 'LIMITED')
|
||||
);
|
||||
const hasEnvironments = environments.length > 0;
|
||||
const hasEnvironments = environments.length > 0 && x.isView;
|
||||
return {
|
||||
...x,
|
||||
hasEnvironments,
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
export * as placeables from './placeables/_module.mjs';
|
||||
export { default as DhTokenLayer } from './tokens.mjs';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,12 @@
|
|||
export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
|
||||
/** @inheritdoc */
|
||||
async _draw(options) {
|
||||
await super._draw(options);
|
||||
|
||||
if (this.document.flags.daggerheart?.createPlacement)
|
||||
this.previewHelp ||= this.addChild(this.#drawPreviewHelp());
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
async _drawEffects() {
|
||||
this.effects.renderable = false;
|
||||
|
|
@ -34,7 +42,7 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
|
|||
this.renderFlags.set({ refreshEffects: true });
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Returns the distance from this token to another token object.
|
||||
* This value is corrected to handle alternate token sizes and other grid types
|
||||
* according to the diagonal rules.
|
||||
|
|
@ -47,11 +55,11 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
|
|||
const destinationPoint = target.center;
|
||||
|
||||
// Compute for gridless. This version returns circular edge to edge + grid distance,
|
||||
// so that tokens that are touching return 5.
|
||||
// so that tokens that are touching return 5.
|
||||
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
|
||||
const boundsCorrection = canvas.grid.distance / canvas.grid.size;
|
||||
const originRadius = this.bounds.width * boundsCorrection / 2;
|
||||
const targetRadius = target.bounds.width * boundsCorrection / 2;
|
||||
const originRadius = (this.bounds.width * boundsCorrection) / 2;
|
||||
const targetRadius = (target.bounds.width * boundsCorrection) / 2;
|
||||
const distance = canvas.grid.measurePath([originPoint, destinationPoint]).distance;
|
||||
return distance - originRadius - targetRadius + canvas.grid.distance;
|
||||
}
|
||||
|
|
@ -61,11 +69,11 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
|
|||
const targetEdge = this.#getEdgeBoundary(target.bounds, originPoint, destinationPoint);
|
||||
const adjustedOriginPoint = canvas.grid.getTopLeftPoint({
|
||||
x: originEdge.x + Math.sign(originPoint.x - originEdge.x),
|
||||
y: originEdge.y + Math.sign(originPoint.y - originEdge.y)
|
||||
y: originEdge.y + Math.sign(originPoint.y - originEdge.y)
|
||||
});
|
||||
const adjustDestinationPoint = canvas.grid.getTopLeftPoint({
|
||||
x: targetEdge.x + Math.sign(destinationPoint.x - targetEdge.x),
|
||||
y: targetEdge.y + Math.sign(destinationPoint.y - targetEdge.y)
|
||||
y: targetEdge.y + Math.sign(destinationPoint.y - targetEdge.y)
|
||||
});
|
||||
return canvas.grid.measurePath([adjustedOriginPoint, adjustDestinationPoint]).distance;
|
||||
}
|
||||
|
|
@ -94,7 +102,7 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
|
|||
|
||||
/** Tests if the token is at least adjacent with another, with some leeway for diagonals */
|
||||
isAdjacentWith(token) {
|
||||
return this.distanceTo(token) <= (canvas.grid.distance * 1.5);
|
||||
return this.distanceTo(token) <= canvas.grid.distance * 1.5;
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
|
|
@ -132,4 +140,25 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
|
|||
bar.position.set(0, posY);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a helptext for previews as a text object
|
||||
* @returns {PreciseText} The Text object for the preview helper
|
||||
*/
|
||||
#drawPreviewHelp() {
|
||||
const { uiScale } = canvas.dimensions;
|
||||
|
||||
const textStyle = CONFIG.canvasTextStyle.clone();
|
||||
textStyle.fontSize = 18;
|
||||
textStyle.wordWrapWidth = this.w * 2.5;
|
||||
textStyle.fontStyle = 'italic';
|
||||
|
||||
const helpText = new foundry.canvas.containers.PreciseText(
|
||||
`(${game.i18n.localize('DAGGERHEART.UI.Tooltip.previewTokenHelp')})`,
|
||||
textStyle
|
||||
);
|
||||
helpText.anchor.set(helpText.width / 900, 1);
|
||||
helpText.scale.set(uiScale, uiScale);
|
||||
return helpText;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
module/canvas/tokens.mjs
Normal file
16
module/canvas/tokens.mjs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
export default class DhTokenLayer extends foundry.canvas.layers.TokenLayer {
|
||||
async _createPreview(createData, options) {
|
||||
if (options.actor) {
|
||||
const tokenSizes = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).tokenSizes;
|
||||
if (options.actor?.system.metadata.usesSize) {
|
||||
const tokenSize = tokenSizes[options.actor.system.size];
|
||||
if (tokenSize && options.actor.system.size !== CONFIG.DH.ACTOR.tokenSize.custom.id) {
|
||||
createData.width = tokenSize;
|
||||
createData.height = tokenSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return super._createPreview(createData, options);
|
||||
}
|
||||
}
|
||||
|
|
@ -171,7 +171,7 @@ export const defeatedConditions = () => {
|
|||
acc[key] = {
|
||||
...choice,
|
||||
img: defeated[`${choice.id}Icon`],
|
||||
description: `DAGGERHEART.CONFIG.Condition.${choice.id}.description`
|
||||
description: game.i18n.localize(`DAGGERHEART.CONFIG.Condition.${choice.id}.description`)
|
||||
};
|
||||
|
||||
return acc;
|
||||
|
|
@ -179,6 +179,10 @@ export const defeatedConditions = () => {
|
|||
};
|
||||
|
||||
export const defeatedConditionChoices = {
|
||||
deathMove: {
|
||||
id: 'deathMove',
|
||||
name: 'DAGGERHEART.CONFIG.Condition.deathMove.name'
|
||||
},
|
||||
defeated: {
|
||||
id: 'defeated',
|
||||
name: 'DAGGERHEART.CONFIG.Condition.defeated.name'
|
||||
|
|
@ -496,6 +500,8 @@ export const diceTypes = {
|
|||
d20: 'd20'
|
||||
};
|
||||
|
||||
export const dieFaces = [4, 6, 8, 10, 12, 20];
|
||||
|
||||
export const multiplierTypes = {
|
||||
prof: 'Proficiency',
|
||||
cast: 'Spellcast',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
export { default as DhCombat } from './combat.mjs';
|
||||
export { default as DhCombatant } from './combatant.mjs';
|
||||
export { default as DhTagTeamRoll } from './tagTeamRoll.mjs';
|
||||
export { default as DhRollTable } from './rollTable.mjs';
|
||||
export { default as RegisteredTriggers } from './registeredTriggers.mjs';
|
||||
|
||||
export * as countdowns from './countdowns.mjs';
|
||||
export * as actions from './action/_module.mjs';
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ export default class DHAttackAction extends DHDamageAction {
|
|||
|
||||
async use(event, options) {
|
||||
const result = await super.use(event, options);
|
||||
if (!result.message) return;
|
||||
|
||||
if (result.message.system.action.roll?.type === 'attack') {
|
||||
const { updateCountdowns } = game.system.api.applications.ui.DhCountdowns;
|
||||
|
|
|
|||
|
|
@ -166,7 +166,6 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
|
|||
*/
|
||||
getRollData(data = {}) {
|
||||
const actorData = this.actor ? this.actor.getRollData(false) : {};
|
||||
|
||||
actorData.result = data.roll?.total ?? 1;
|
||||
actorData.scale = data.costs?.length // Right now only return the first scalable cost.
|
||||
? (data.costs.find(c => c.scalable)?.total ?? 1)
|
||||
|
|
@ -199,6 +198,8 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
|
|||
let config = this.prepareConfig(event);
|
||||
if (!config) return;
|
||||
|
||||
config.effects = await game.system.api.data.actions.actionsTypes.base.getEffects(this.actor, this.item);
|
||||
|
||||
if (Hooks.call(`${CONFIG.DH.id}.preUseAction`, this, config) === false) return;
|
||||
|
||||
// Display configuration window if necessary
|
||||
|
|
@ -240,6 +241,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
|
|||
hasHealing: this.hasHealing,
|
||||
hasEffect: this.hasEffect,
|
||||
hasSave: this.hasSave,
|
||||
onSave: this.save?.damageMod,
|
||||
isDirect: !!this.damage?.direct,
|
||||
selectedRollMode: game.settings.get('core', 'rollMode'),
|
||||
data: this.getRollData(),
|
||||
|
|
@ -265,6 +267,28 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
|
|||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the all potentially applicable effects on the actor
|
||||
* @param {DHActor} actor The actor performing the action
|
||||
* @param {DHItem|DhActor} effectParent The parent of the effect
|
||||
* @returns {DhActiveEffect[]}
|
||||
*/
|
||||
static async getEffects(actor, effectParent) {
|
||||
if (!actor) return [];
|
||||
|
||||
return Array.from(await actor.allApplicableEffects()).filter(effect => {
|
||||
/* Effects on weapons only ever apply for the weapon itself */
|
||||
if (effect.parent.type === 'weapon') {
|
||||
/* Unless they're secondary - then they apply only to other primary weapons */
|
||||
if (effect.parent.system.secondary) {
|
||||
if (effectParent?.type !== 'weapon' || effectParent?.system.secondary) return false;
|
||||
} else if (effectParent?.id !== effect.parent.id) return false;
|
||||
}
|
||||
|
||||
return !effect.isSuppressed;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Method used to know if a configuration dialog must be shown or not when there is no roll.
|
||||
* @param {*} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
|
||||
|
|
@ -353,14 +377,14 @@ export class ResourceUpdateMap extends Map {
|
|||
if (!resource.key) continue;
|
||||
|
||||
const existing = this.get(resource.key);
|
||||
if (existing) {
|
||||
if (!existing || resource.clear) {
|
||||
this.set(resource.key, resource);
|
||||
} else if (!existing?.clear) {
|
||||
this.set(resource.key, {
|
||||
...existing,
|
||||
value: existing.value + (resource.value ?? 0),
|
||||
total: existing.total + (resource.total ?? 0)
|
||||
});
|
||||
} else {
|
||||
this.set(resource.key, resource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,5 @@
|
|||
import DHBaseAction from './baseAction.mjs';
|
||||
|
||||
export default class DHSummonAction extends DHBaseAction {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
...super.defineSchema(),
|
||||
documentUUID: new fields.DocumentUUIDField({ type: 'Actor' })
|
||||
};
|
||||
}
|
||||
|
||||
async trigger(event, ...args) {
|
||||
if (!this.canSummon || !canvas.scene) return;
|
||||
}
|
||||
|
||||
get canSummon() {
|
||||
return game.user.can('TOKEN_CREATE');
|
||||
}
|
||||
static extraSchemas = [...super.extraSchemas, 'summon'];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export default class BeastformEffect extends BaseEffect {
|
|||
base64: false
|
||||
}),
|
||||
tokenSize: new fields.SchemaField({
|
||||
scale: new fields.NumberField({ nullable: false, initial: 1 }),
|
||||
height: new fields.NumberField({ integer: false, nullable: true }),
|
||||
width: new fields.NumberField({ integer: false, nullable: true })
|
||||
})
|
||||
|
|
@ -55,7 +56,9 @@ export default class BeastformEffect extends BaseEffect {
|
|||
const update = {
|
||||
...baseUpdate,
|
||||
texture: {
|
||||
src: this.characterTokenData.tokenImg
|
||||
src: this.characterTokenData.tokenImg,
|
||||
scaleX: this.characterTokenData.tokenSize.scale,
|
||||
scaleY: this.characterTokenData.tokenSize.scale
|
||||
},
|
||||
ring: {
|
||||
enabled: this.characterTokenData.usesDynamicToken,
|
||||
|
|
@ -86,7 +89,9 @@ export default class BeastformEffect extends BaseEffect {
|
|||
y,
|
||||
'texture': {
|
||||
enabled: this.characterTokenData.usesDynamicToken,
|
||||
src: token.flags.daggerheart?.beastformTokenImg ?? this.characterTokenData.tokenImg
|
||||
src: token.flags.daggerheart?.beastformTokenImg ?? this.characterTokenData.tokenImg,
|
||||
scaleX: this.characterTokenData.tokenSize.scale,
|
||||
scaleY: this.characterTokenData.tokenSize.scale
|
||||
},
|
||||
'ring': {
|
||||
subject: {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ const resistanceField = (resistanceLabel, immunityLabel, reductionLabel) =>
|
|||
});
|
||||
|
||||
/* Common rules applying to Characters and Adversaries */
|
||||
export const commonActorRules = (extendedData = { damageReduction: {} }) => ({
|
||||
export const commonActorRules = (extendedData = { damageReduction: {}, attack: { damage: {} } }) => ({
|
||||
conditionImmunities: new fields.SchemaField({
|
||||
hidden: new fields.BooleanField({ initial: false }),
|
||||
restrained: new fields.BooleanField({ initial: false }),
|
||||
|
|
@ -41,7 +41,23 @@ export const commonActorRules = (extendedData = { damageReduction: {} }) => ({
|
|||
magical: new fields.NumberField({ initial: 0, min: 0 }),
|
||||
physical: new fields.NumberField({ initial: 0, min: 0 })
|
||||
}),
|
||||
...extendedData.damageReduction
|
||||
...(extendedData.damageReduction ?? {})
|
||||
}),
|
||||
attack: new fields.SchemaField({
|
||||
...extendedData.attack,
|
||||
damage: new fields.SchemaField({
|
||||
hpDamageMultiplier: new fields.NumberField({
|
||||
required: true,
|
||||
nullable: false,
|
||||
initial: 1
|
||||
}),
|
||||
hpDamageTakenMultiplier: new fields.NumberField({
|
||||
required: true,
|
||||
nullable: false,
|
||||
initial: 1
|
||||
}),
|
||||
...(extendedData.attack?.damage ?? {})
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,14 @@ export default class DhCharacter extends BaseDataActor {
|
|||
'DAGGERHEART.ACTORS.Character.maxHPBonus'
|
||||
),
|
||||
stress: resourceField(6, 0, 'DAGGERHEART.GENERAL.stress', true),
|
||||
hope: resourceField(6, 2, 'DAGGERHEART.GENERAL.hope')
|
||||
hope: new fields.SchemaField({
|
||||
value: new fields.NumberField({
|
||||
initial: 2,
|
||||
min: 0,
|
||||
integer: true,
|
||||
label: 'DAGGERHEART.GENERAL.hope'
|
||||
})
|
||||
})
|
||||
}),
|
||||
traits: new fields.SchemaField({
|
||||
agility: attributeField('DAGGERHEART.CONFIG.Traits.agility.name'),
|
||||
|
|
@ -79,12 +86,7 @@ export default class DhCharacter extends BaseDataActor {
|
|||
bags: new fields.NumberField({ initial: 0, integer: true }),
|
||||
chests: new fields.NumberField({ initial: 0, integer: true })
|
||||
}),
|
||||
scars: new fields.TypedObjectField(
|
||||
new fields.SchemaField({
|
||||
name: new fields.StringField({}),
|
||||
description: new fields.StringField()
|
||||
})
|
||||
),
|
||||
scars: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.GENERAL.scars' }),
|
||||
biography: new fields.SchemaField({
|
||||
background: new fields.HTMLField(),
|
||||
connections: new fields.HTMLField(),
|
||||
|
|
@ -252,38 +254,59 @@ export default class DhCharacter extends BaseDataActor {
|
|||
hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.increasePerArmorMark.hint'
|
||||
}),
|
||||
disabledArmor: new fields.BooleanField({ intial: false })
|
||||
},
|
||||
attack: {
|
||||
damage: {
|
||||
diceIndex: new fields.NumberField({
|
||||
integer: true,
|
||||
min: 0,
|
||||
max: 5,
|
||||
initial: 0,
|
||||
label: 'DAGGERHEART.GENERAL.Rules.attack.damage.dice.label',
|
||||
hint: 'DAGGERHEART.GENERAL.Rules.attack.damage.dice.hint'
|
||||
}),
|
||||
bonus: new fields.NumberField({
|
||||
required: true,
|
||||
initial: 0,
|
||||
min: 0,
|
||||
label: 'DAGGERHEART.GENERAL.Rules.attack.damage.bonus.label'
|
||||
})
|
||||
},
|
||||
roll: new fields.SchemaField({
|
||||
trait: new fields.StringField({
|
||||
required: true,
|
||||
choices: CONFIG.DH.ACTOR.abilities,
|
||||
nullable: true,
|
||||
initial: null,
|
||||
label: 'DAGGERHEART.GENERAL.Rules.attack.roll.trait.label'
|
||||
})
|
||||
})
|
||||
}
|
||||
}),
|
||||
attack: new fields.SchemaField({
|
||||
damage: new fields.SchemaField({
|
||||
diceIndex: new fields.NumberField({
|
||||
integer: true,
|
||||
min: 0,
|
||||
max: 5,
|
||||
initial: 0,
|
||||
label: 'DAGGERHEART.GENERAL.Rules.attack.damage.dice.label',
|
||||
hint: 'DAGGERHEART.GENERAL.Rules.attack.damage.dice.hint'
|
||||
}),
|
||||
bonus: new fields.NumberField({
|
||||
required: true,
|
||||
initial: 0,
|
||||
min: 0,
|
||||
label: 'DAGGERHEART.GENERAL.Rules.attack.damage.bonus.label'
|
||||
})
|
||||
dualityRoll: new fields.SchemaField({
|
||||
defaultHopeDice: new fields.NumberField({
|
||||
nullable: false,
|
||||
required: true,
|
||||
integer: true,
|
||||
choices: CONFIG.DH.GENERAL.dieFaces,
|
||||
initial: 12,
|
||||
label: 'DAGGERHEART.ACTORS.Character.defaultHopeDice'
|
||||
}),
|
||||
roll: new fields.SchemaField({
|
||||
trait: new fields.StringField({
|
||||
required: true,
|
||||
choices: CONFIG.DH.ACTOR.abilities,
|
||||
nullable: true,
|
||||
initial: null,
|
||||
label: 'DAGGERHEART.GENERAL.Rules.attack.roll.trait.label'
|
||||
})
|
||||
defaultFearDice: new fields.NumberField({
|
||||
nullable: false,
|
||||
required: true,
|
||||
integer: true,
|
||||
choices: CONFIG.DH.GENERAL.dieFaces,
|
||||
initial: 12,
|
||||
label: 'DAGGERHEART.ACTORS.Character.defaultFearDice'
|
||||
})
|
||||
}),
|
||||
runeWard: new fields.BooleanField({ initial: false }),
|
||||
burden: new fields.SchemaField({
|
||||
ignore: new fields.BooleanField()
|
||||
}),
|
||||
roll: new fields.SchemaField({
|
||||
guaranteedCritical: new fields.BooleanField()
|
||||
})
|
||||
}),
|
||||
sidebarFavorites: new ForeignDocumentUUIDArrayField({ type: 'Item' })
|
||||
|
|
@ -347,7 +370,7 @@ export default class DhCharacter extends BaseDataActor {
|
|||
const modifiers = subClasses
|
||||
?.map(sc => ({ ...this.traits[sc.system.spellcastingTrait], key: sc.system.spellcastingTrait }))
|
||||
.filter(x => x);
|
||||
return modifiers.sort((a, b) => a.value - b.value)[0];
|
||||
return modifiers.sort((a, b) => (b.value ?? 0) - (a.value ?? 0))[0];
|
||||
}
|
||||
|
||||
get spellcastModifier() {
|
||||
|
|
@ -528,7 +551,18 @@ export default class DhCharacter extends BaseDataActor {
|
|||
}
|
||||
|
||||
get deathMoveViable() {
|
||||
return this.resources.hitPoints.max > 0 && this.resources.hitPoints.value >= this.resources.hitPoints.max;
|
||||
const { characterDefault } = game.settings.get(
|
||||
CONFIG.DH.id,
|
||||
CONFIG.DH.SETTINGS.gameSettings.Automation
|
||||
).defeated;
|
||||
const deathMoveOutcomeStatuses = Object.keys(CONFIG.DH.GENERAL.defeatedConditionChoices).filter(
|
||||
key => key !== characterDefault
|
||||
);
|
||||
const deathMoveNotResolved = this.parent.statuses.every(status => !deathMoveOutcomeStatuses.includes(status));
|
||||
|
||||
const allHitPointsMarked =
|
||||
this.resources.hitPoints.max > 0 && this.resources.hitPoints.value >= this.resources.hitPoints.max;
|
||||
return deathMoveNotResolved && allHitPointsMarked;
|
||||
}
|
||||
|
||||
get armorApplicableDamageTypes() {
|
||||
|
|
@ -626,8 +660,15 @@ export default class DhCharacter extends BaseDataActor {
|
|||
? armor.system.baseThresholds.severe + this.levelData.level.current
|
||||
: this.levelData.level.current * 2
|
||||
};
|
||||
this.resources.hope.max -= Object.keys(this.scars).length;
|
||||
|
||||
const globalHopeMax = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).maxHope;
|
||||
this.resources.hope.max = globalHopeMax - this.scars;
|
||||
this.resources.hitPoints.max += this.class.value?.system?.hitPoints ?? 0;
|
||||
|
||||
/* Companion Related Data */
|
||||
this.companionData = {
|
||||
levelupChoices: this.levelData.level.current - 1
|
||||
};
|
||||
}
|
||||
|
||||
prepareDerivedData() {
|
||||
|
|
@ -683,6 +724,30 @@ export default class DhCharacter extends BaseDataActor {
|
|||
changes.system.experiences[experience].core = true;
|
||||
}
|
||||
}
|
||||
|
||||
/* Scars can alter the amount of current hope */
|
||||
if (changes.system?.scars) {
|
||||
const diff = this.system.scars - changes.system.scars;
|
||||
const newHopeMax = this.system.resources.hope.max + diff;
|
||||
const newHopeValue = Math.min(newHopeMax, this.system.resources.hope.value);
|
||||
if (newHopeValue != this.system.resources.hope.value) {
|
||||
if (!changes.system.resources) changes.system.resources = { hope: { value: 0 } };
|
||||
changes.system.resources.hope = {
|
||||
...changes.system.resources.hope,
|
||||
value: changes.system.resources.hope.value + newHopeValue
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/* Force companion data prep */
|
||||
if (this.companion) {
|
||||
if (
|
||||
changes.system?.levelData?.level?.current !== undefined &&
|
||||
changes.system.levelData.level.current !== this._source.levelData.level.current
|
||||
) {
|
||||
this.companion.update(this.companion.toObject(), { diff: false, recursive: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async _preDelete() {
|
||||
|
|
@ -698,4 +763,11 @@ export default class DhCharacter extends BaseDataActor {
|
|||
t => !!t
|
||||
);
|
||||
}
|
||||
|
||||
static migrateData(source) {
|
||||
if (typeof source.scars === 'object') source.scars = 0;
|
||||
if (source.resources?.hope?.max) source.scars = Math.max(6 - source.resources.hope.max, 0);
|
||||
|
||||
return super.migrateData(source);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -108,7 +108,11 @@ export default class DhCompanion extends BaseDataActor {
|
|||
get proficiency() {
|
||||
return this.partner?.system?.proficiency ?? 1;
|
||||
}
|
||||
|
||||
|
||||
get canLevelUp() {
|
||||
return this.levelupChoicesLeft > 0;
|
||||
}
|
||||
|
||||
isItemValid() {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -127,7 +131,7 @@ export default class DhCompanion extends BaseDataActor {
|
|||
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);
|
||||
this.attack.range = adjustRange(this.attack.range).id;
|
||||
}
|
||||
break;
|
||||
case 'stress':
|
||||
|
|
@ -147,6 +151,17 @@ export default class DhCompanion extends BaseDataActor {
|
|||
}
|
||||
}
|
||||
|
||||
prepareDerivedData() {
|
||||
/* Partner Related Setup */
|
||||
if (this.partner) {
|
||||
this.levelData.level.changed = this.partner.system.levelData.level.current;
|
||||
this.levelupChoicesLeft = Object.values(this.levelData.levelups).reduce((acc, curr) => {
|
||||
acc = Math.max(acc - curr.selections.length, 0);
|
||||
return acc;
|
||||
}, this.partner.system.companionData.levelupChoices);
|
||||
}
|
||||
}
|
||||
|
||||
async _preUpdate(changes, options, userId) {
|
||||
const allowed = await super._preUpdate(changes, options, userId);
|
||||
if (allowed === false) return;
|
||||
|
|
@ -162,6 +177,16 @@ export default class DhCompanion extends BaseDataActor {
|
|||
changes.system.experiences[experience].core = true;
|
||||
}
|
||||
}
|
||||
|
||||
/* Force partner data prep */
|
||||
if (this.partner) {
|
||||
if (
|
||||
changes.system?.levelData?.level?.current !== undefined &&
|
||||
changes.system.levelData.level.current !== this._source.levelData.level.current
|
||||
) {
|
||||
this.partner.update(this.partner.toObject(), { diff: false, recursive: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async _preDelete() {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ export const config = {
|
|||
adversaryRoll: DHActorRoll,
|
||||
damageRoll: DHActorRoll,
|
||||
dualityRoll: DHActorRoll,
|
||||
fateRoll: DHActorRoll,
|
||||
groupRoll: DHGroupRoll,
|
||||
systemMessage: DHSystemMessage
|
||||
};
|
||||
|
|
|
|||
370
module/data/companionLevelup.mjs
Normal file
370
module/data/companionLevelup.mjs
Normal file
|
|
@ -0,0 +1,370 @@
|
|||
import { abilities } from '../config/actorConfig.mjs';
|
||||
import { chunkify } from '../helpers/utils.mjs';
|
||||
import { LevelOptionType } from './levelTier.mjs';
|
||||
|
||||
export class DhCompanionLevelup extends foundry.abstract.DataModel {
|
||||
static initializeData(levelTierData, pcLevelData, origChoicesLeft) {
|
||||
let choicesLeft = origChoicesLeft;
|
||||
|
||||
const { current, changed } = pcLevelData.level;
|
||||
const bonusChoicesOnly = current === changed;
|
||||
const startLevel = bonusChoicesOnly ? current : current + 1;
|
||||
const endLevel = bonusChoicesOnly ? startLevel : changed;
|
||||
|
||||
const tiers = {};
|
||||
const levels = {};
|
||||
const tierKeys = Object.keys(levelTierData.tiers);
|
||||
tierKeys.forEach(key => {
|
||||
const tier = levelTierData.tiers[key];
|
||||
const belongingLevels = [];
|
||||
for (var i = tier.levels.start; i <= tier.levels.end; i++) {
|
||||
if (i <= endLevel) {
|
||||
const initialAchievements = i === tier.levels.start ? tier.initialAchievements : {};
|
||||
const experiences = initialAchievements.experience
|
||||
? [...Array(initialAchievements.experience.nr).keys()].reduce((acc, _) => {
|
||||
acc[foundry.utils.randomID()] = {
|
||||
name: '',
|
||||
modifier: initialAchievements.experience.modifier
|
||||
};
|
||||
return acc;
|
||||
}, {})
|
||||
: {};
|
||||
|
||||
const currentChoices = pcLevelData.levelups[i]?.selections?.length;
|
||||
const maxSelections =
|
||||
i === endLevel
|
||||
? choicesLeft + (currentChoices ?? 0)
|
||||
: (currentChoices ?? tier.maxSelections[i]);
|
||||
if (!pcLevelData.levelups[i]) choicesLeft -= maxSelections;
|
||||
|
||||
levels[i] = DhLevelupLevel.initializeData(pcLevelData.levelups[i], maxSelections, {
|
||||
...initialAchievements,
|
||||
experiences,
|
||||
domainCards: {}
|
||||
});
|
||||
}
|
||||
|
||||
belongingLevels.push(i);
|
||||
}
|
||||
|
||||
/* Improve. Temporary handling for Companion new experiences */
|
||||
Object.keys(tier.extraAchievements ?? {}).forEach(key => {
|
||||
const level = Number(key);
|
||||
if (level >= startLevel && level <= endLevel) {
|
||||
const levelExtras = tier.extraAchievements[level];
|
||||
if (levelExtras.experience) {
|
||||
levels[level].achievements.experiences[foundry.utils.randomID()] = {
|
||||
name: '',
|
||||
modifier: levelExtras.experience.modifier
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
tiers[key] = {
|
||||
name: tier.name,
|
||||
belongingLevels: belongingLevels,
|
||||
options: Object.keys(tier.options).reduce((acc, key) => {
|
||||
acc[key] = tier.options[key].toObject?.() ?? tier.options[key];
|
||||
return acc;
|
||||
}, {})
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
tiers,
|
||||
levels,
|
||||
startLevel,
|
||||
currentLevel: startLevel,
|
||||
endLevel
|
||||
};
|
||||
}
|
||||
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
|
||||
return {
|
||||
tiers: new fields.TypedObjectField(
|
||||
new fields.SchemaField({
|
||||
name: new fields.StringField({ required: true }),
|
||||
belongingLevels: new fields.ArrayField(new fields.NumberField({ required: true, integer: true })),
|
||||
options: new fields.TypedObjectField(
|
||||
new fields.SchemaField({
|
||||
label: new fields.StringField({ required: true }),
|
||||
checkboxSelections: new fields.NumberField({ required: true, integer: true }),
|
||||
minCost: new fields.NumberField({ required: true, integer: true }),
|
||||
type: new fields.StringField({ required: true, choices: LevelOptionType }),
|
||||
value: new fields.NumberField({ integer: true }),
|
||||
amount: new fields.NumberField({ integer: true })
|
||||
})
|
||||
)
|
||||
})
|
||||
),
|
||||
levels: new fields.TypedObjectField(new fields.EmbeddedDataField(DhLevelupLevel)),
|
||||
startLevel: new fields.NumberField({ required: true, integer: true }),
|
||||
currentLevel: new fields.NumberField({ required: true, integer: true }),
|
||||
endLevel: new fields.NumberField({ required: true, integer: true })
|
||||
};
|
||||
}
|
||||
|
||||
#levelFinished(levelKey) {
|
||||
const allSelectionsMade = this.levels[levelKey].nrSelections.available === 0;
|
||||
const allChoicesMade = Object.keys(this.levels[levelKey].choices).every(choiceKey => {
|
||||
const choice = this.levels[levelKey].choices[choiceKey];
|
||||
return Object.values(choice).every(checkbox => {
|
||||
switch (choiceKey) {
|
||||
case 'trait':
|
||||
case 'experience':
|
||||
case 'domainCard':
|
||||
case 'subclass':
|
||||
case 'vicious':
|
||||
return checkbox.data.length === (checkbox.amount ?? 1);
|
||||
case 'multiclass':
|
||||
const classSelected = checkbox.data.length === 1;
|
||||
const domainSelected = checkbox.secondaryData.domain;
|
||||
const subclassSelected = checkbox.secondaryData.subclass;
|
||||
return classSelected && domainSelected && subclassSelected;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
});
|
||||
});
|
||||
const experiencesSelected = !this.levels[levelKey].achievements.experiences
|
||||
? true
|
||||
: Object.values(this.levels[levelKey].achievements.experiences).every(exp => exp.name);
|
||||
const domainCardsSelected = Object.values(this.levels[levelKey].achievements.domainCards)
|
||||
.filter(x => x.level <= this.endLevel)
|
||||
.every(card => card.uuid);
|
||||
const allAchievementsSelected = experiencesSelected && domainCardsSelected;
|
||||
|
||||
return allSelectionsMade && allChoicesMade && allAchievementsSelected;
|
||||
}
|
||||
|
||||
get currentLevelFinished() {
|
||||
return this.#levelFinished(this.currentLevel);
|
||||
}
|
||||
|
||||
get allLevelsFinished() {
|
||||
return Object.keys(this.levels)
|
||||
.filter(level => Number(level) >= this.startLevel)
|
||||
.every(this.#levelFinished.bind(this));
|
||||
}
|
||||
|
||||
get unmarkedTraits() {
|
||||
const possibleLevels = Object.values(this.tiers).reduce((acc, tier) => {
|
||||
if (tier.belongingLevels.includes(this.currentLevel)) acc = tier.belongingLevels;
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return Object.keys(this.levels)
|
||||
.filter(key => possibleLevels.some(x => x === Number(key)))
|
||||
.reduce(
|
||||
(acc, levelKey) => {
|
||||
const level = this.levels[levelKey];
|
||||
Object.values(level.choices).forEach(choice =>
|
||||
Object.values(choice).forEach(checkbox => {
|
||||
if (
|
||||
checkbox.type === 'trait' &&
|
||||
checkbox.data.length > 0 &&
|
||||
Number(levelKey) !== this.currentLevel
|
||||
) {
|
||||
checkbox.data.forEach(data => delete acc[data]);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return acc;
|
||||
},
|
||||
{ ...abilities }
|
||||
);
|
||||
}
|
||||
|
||||
get classUpgradeChoices() {
|
||||
let subclasses = [];
|
||||
let multiclass = null;
|
||||
Object.keys(this.levels).forEach(levelKey => {
|
||||
const level = this.levels[levelKey];
|
||||
Object.values(level.choices).forEach(choice => {
|
||||
Object.values(choice).forEach(checkbox => {
|
||||
if (checkbox.type === 'multiclass') {
|
||||
multiclass = {
|
||||
class: checkbox.data.length > 0 ? checkbox.data[0] : null,
|
||||
domain: checkbox.secondaryData.domain ?? null,
|
||||
subclass: checkbox.secondaryData.subclass ?? null,
|
||||
tier: checkbox.tier,
|
||||
level: levelKey
|
||||
};
|
||||
}
|
||||
if (checkbox.type === 'subclass') {
|
||||
subclasses.push({
|
||||
tier: checkbox.tier,
|
||||
level: levelKey
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
return { subclasses, multiclass };
|
||||
}
|
||||
|
||||
get tiersForRendering() {
|
||||
const tierKeys = Object.keys(this.tiers);
|
||||
const selections = Object.keys(this.levels).reduce(
|
||||
(acc, key) => {
|
||||
const level = this.levels[key];
|
||||
Object.keys(level.choices).forEach(optionKey => {
|
||||
const choice = level.choices[optionKey];
|
||||
Object.keys(choice).forEach(checkboxNr => {
|
||||
const checkbox = choice[checkboxNr];
|
||||
if (!acc[checkbox.tier][optionKey]) acc[checkbox.tier][optionKey] = {};
|
||||
Object.keys(choice).forEach(checkboxNr => {
|
||||
acc[checkbox.tier][optionKey][checkboxNr] = { ...checkbox, level: Number(key) };
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return acc;
|
||||
},
|
||||
tierKeys.reduce((acc, key) => {
|
||||
acc[key] = {};
|
||||
return acc;
|
||||
}, {})
|
||||
);
|
||||
|
||||
const { multiclass, subclasses } = this.classUpgradeChoices;
|
||||
return tierKeys.map((tierKey, tierIndex) => {
|
||||
const tier = this.tiers[tierKey];
|
||||
const multiclassInTier = multiclass?.tier === Number(tierKey);
|
||||
const subclassInTier = subclasses.some(x => x.tier === Number(tierKey));
|
||||
|
||||
return {
|
||||
name: game.i18n.localize(tier.name),
|
||||
active: this.currentLevel >= Math.min(...tier.belongingLevels),
|
||||
groups: Object.keys(tier.options).map(optionKey => {
|
||||
const option = tier.options[optionKey];
|
||||
|
||||
const checkboxes = [...Array(option.checkboxSelections).keys()].flatMap(index => {
|
||||
const checkboxNr = index + 1;
|
||||
const checkboxData = selections[tierKey]?.[optionKey]?.[checkboxNr];
|
||||
const checkbox = { ...option, checkboxNr, tier: tierKey };
|
||||
|
||||
if (checkboxData) {
|
||||
checkbox.level = checkboxData.level;
|
||||
checkbox.selected = true;
|
||||
checkbox.disabled = checkbox.level !== this.currentLevel;
|
||||
}
|
||||
|
||||
if (optionKey === 'multiclass') {
|
||||
if ((multiclass && !multiclassInTier) || subclassInTier) {
|
||||
checkbox.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (optionKey === 'subclass' && multiclassInTier) {
|
||||
checkbox.disabled = true;
|
||||
}
|
||||
|
||||
return checkbox;
|
||||
});
|
||||
|
||||
let label = game.i18n.localize(option.label);
|
||||
if (optionKey === 'domainCard') {
|
||||
const maxLevel = tier.belongingLevels[tier.belongingLevels.length - 1];
|
||||
label = game.i18n.format(option.label, { maxLevel });
|
||||
}
|
||||
|
||||
return {
|
||||
label: label,
|
||||
checkboxGroups: chunkify(checkboxes, option.minCost, chunkedBoxes => {
|
||||
const anySelected = chunkedBoxes.some(x => x.selected);
|
||||
const anyDisabled = chunkedBoxes.some(x => x.disabled);
|
||||
return {
|
||||
multi: option.minCost > 1,
|
||||
checkboxes: chunkedBoxes.map(x => ({
|
||||
...x,
|
||||
selected: anySelected,
|
||||
disabled: anyDisabled
|
||||
}))
|
||||
};
|
||||
})
|
||||
};
|
||||
})
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class DhLevelupLevel extends foundry.abstract.DataModel {
|
||||
static initializeData(levelData = { selections: [] }, maxSelections, achievements) {
|
||||
return {
|
||||
maxSelections: maxSelections,
|
||||
achievements: {
|
||||
experiences: levelData.achievements?.experiences ?? achievements.experiences ?? {},
|
||||
domainCards: levelData.achievements?.domainCards
|
||||
? levelData.achievements.domainCards.reduce((acc, card, index) => {
|
||||
acc[index] = { ...card };
|
||||
return acc;
|
||||
}, {})
|
||||
: (achievements.domainCards ?? {}),
|
||||
proficiency: levelData.achievements?.proficiency ?? achievements.proficiency ?? null
|
||||
},
|
||||
choices: levelData.selections.reduce((acc, data) => {
|
||||
if (!acc[data.optionKey]) acc[data.optionKey] = {};
|
||||
acc[data.optionKey][data.checkboxNr] = { ...data };
|
||||
|
||||
return acc;
|
||||
}, {})
|
||||
};
|
||||
}
|
||||
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
|
||||
return {
|
||||
maxSelections: new fields.NumberField({ required: true, integer: true }),
|
||||
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.TypedObjectField(
|
||||
new fields.SchemaField({
|
||||
uuid: new fields.StringField({ required: true, nullable: true, initial: null }),
|
||||
itemUuid: new fields.StringField({ required: true }),
|
||||
level: new fields.NumberField({ required: true, integer: true })
|
||||
})
|
||||
),
|
||||
proficiency: new fields.NumberField({ integer: true })
|
||||
}),
|
||||
choices: new fields.TypedObjectField(
|
||||
new fields.TypedObjectField(
|
||||
new fields.SchemaField({
|
||||
tier: new fields.NumberField({ required: true, integer: true }),
|
||||
minCost: new fields.NumberField({ required: true, integer: true }),
|
||||
amount: new fields.NumberField({ integer: true }),
|
||||
value: new fields.StringField(),
|
||||
data: new fields.ArrayField(new fields.StringField()),
|
||||
secondaryData: new fields.TypedObjectField(new fields.StringField()),
|
||||
type: new fields.StringField({ required: true })
|
||||
})
|
||||
)
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
get nrSelections() {
|
||||
const selections = Object.keys(this.choices).reduce((acc, choiceKey) => {
|
||||
const choice = this.choices[choiceKey];
|
||||
acc += Object.values(choice).reduce((acc, x) => acc + x.minCost, 0);
|
||||
|
||||
return acc;
|
||||
}, 0);
|
||||
|
||||
return {
|
||||
selections: selections,
|
||||
available: this.maxSelections - selections
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -9,3 +9,4 @@ export { default as BeastformField } from './beastformField.mjs';
|
|||
export { default as DamageField } from './damageField.mjs';
|
||||
export { default as RollField } from './rollField.mjs';
|
||||
export { default as MacroField } from './macroField.mjs';
|
||||
export { default as SummonField } from './summonField.mjs';
|
||||
|
|
|
|||
|
|
@ -105,12 +105,22 @@ export default class DamageField extends fields.SchemaField {
|
|||
damagePromises.push(
|
||||
actor.takeHealing(config.damage).then(updates => targetDamage.push({ token, updates }))
|
||||
);
|
||||
else
|
||||
else {
|
||||
const configDamage = foundry.utils.deepClone(config.damage);
|
||||
const hpDamageMultiplier = config.actionActor?.system.rules.attack.damage.hpDamageMultiplier ?? 1;
|
||||
const hpDamageTakenMultiplier = actor.system.rules.attack.damage.hpDamageTakenMultiplier;
|
||||
if (configDamage.hitPoints) {
|
||||
for (const part of configDamage.hitPoints.parts) {
|
||||
part.total = Math.ceil(part.total * hpDamageMultiplier * hpDamageTakenMultiplier);
|
||||
}
|
||||
}
|
||||
|
||||
damagePromises.push(
|
||||
actor
|
||||
.takeDamage(config.damage, config.isDirect)
|
||||
.takeDamage(configDamage, config.isDirect)
|
||||
.then(updates => targetDamage.push({ token, updates }))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Promise.all(damagePromises).then(async _ => {
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ export class DHActionRollData extends foundry.abstract.DataModel {
|
|||
if (this.type === CONFIG.DH.GENERAL.rollTypes.attack.id)
|
||||
modifiers.push({
|
||||
label: 'Bonus to Hit',
|
||||
value: this.bonus ?? this.parent.actor.system.attack.roll.bonus
|
||||
value: this.bonus ?? this.parent.actor.system.attack.roll.bonus ?? 0
|
||||
});
|
||||
break;
|
||||
default:
|
||||
|
|
|
|||
89
module/data/fields/action/summonField.mjs
Normal file
89
module/data/fields/action/summonField.mjs
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import FormulaField from '../formulaField.mjs';
|
||||
|
||||
const fields = foundry.data.fields;
|
||||
|
||||
export default class DHSummonField extends fields.ArrayField {
|
||||
/**
|
||||
* Action Workflow order
|
||||
*/
|
||||
static order = 120;
|
||||
|
||||
constructor(options = {}, context = {}) {
|
||||
const summonFields = new fields.SchemaField({
|
||||
actorUUID: new fields.DocumentUUIDField({
|
||||
type: 'Actor',
|
||||
required: true
|
||||
}),
|
||||
count: new FormulaField({
|
||||
required: true,
|
||||
default: '1'
|
||||
})
|
||||
});
|
||||
super(summonFields, options, context);
|
||||
}
|
||||
|
||||
static async execute() {
|
||||
if (!canvas.scene) {
|
||||
ui.notifications.warn(game.i18n.localize('DAGGERHEART.ACTIONS.TYPES.summon.error'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.summon.length === 0) {
|
||||
ui.notifications.warn('No actors configured for this Summon action.');
|
||||
return;
|
||||
}
|
||||
|
||||
const rolls = [];
|
||||
const summonData = [];
|
||||
for (const summon of this.summon) {
|
||||
let count = summon.count;
|
||||
const roll = new Roll(summon.count);
|
||||
if (!roll.isDeterministic) {
|
||||
await roll.evaluate();
|
||||
if (game.modules.get('dice-so-nice')?.active) rolls.push(roll);
|
||||
count = roll.total;
|
||||
}
|
||||
|
||||
const actor = DHSummonField.getWorldActor(await foundry.utils.fromUuid(summon.actorUUID));
|
||||
/* Extending summon data in memory so it's available in actionField.toChat. Think it's harmless, but ugly. Could maybe find a better way. */
|
||||
summon.rolledCount = count;
|
||||
summon.actor = actor.toObject();
|
||||
|
||||
summonData.push({ actor, count: count });
|
||||
}
|
||||
|
||||
if (rolls.length) await Promise.all(rolls.map(roll => game.dice3d.showForRoll(roll, game.user, true)));
|
||||
|
||||
this.actor.sheet?.minimize();
|
||||
DHSummonField.handleSummon(summonData, this.actor);
|
||||
}
|
||||
|
||||
/* Check for any available instances of the actor present in the world if we're missing artwork in the compendium */
|
||||
static getWorldActor(baseActor) {
|
||||
const dataType = game.system.api.data.actors[`Dh${baseActor.type.capitalize()}`];
|
||||
if (baseActor.inCompendium && dataType && baseActor.img === dataType.DEFAULT_ICON) {
|
||||
const worldActorCopy = game.actors.find(x => x.name === baseActor.name);
|
||||
return worldActorCopy ?? baseActor;
|
||||
}
|
||||
|
||||
return baseActor;
|
||||
}
|
||||
|
||||
static async handleSummon(summonData, actionActor, summonIndex = 0) {
|
||||
const summon = summonData[summonIndex];
|
||||
const result = await CONFIG.ux.TokenManager.createPreviewAsync(summon.actor, {
|
||||
name: `${summon.actor.prototypeToken.name}${summon.count > 1 ? ` (${summon.count}x)` : ''}`
|
||||
});
|
||||
|
||||
if (!result) return actionActor.sheet?.maximize();
|
||||
summon.actor = result.actor;
|
||||
|
||||
summon.count--;
|
||||
if (summon.count <= 0) {
|
||||
summonIndex++;
|
||||
if (summonIndex === summonData.length) return actionActor.sheet?.maximize();
|
||||
}
|
||||
|
||||
DHSummonField.handleSummon(summonData, actionActor, summonIndex);
|
||||
}
|
||||
}
|
||||
|
|
@ -267,7 +267,8 @@ export function ActionMixin(Base) {
|
|||
action: {
|
||||
name: this.name,
|
||||
img: this.baseAction ? this.parent.parent.img : this.img,
|
||||
tags: this.tags ? this.tags : ['Spell', 'Arcana', 'Lv 10']
|
||||
tags: this.tags ? this.tags : ['Spell', 'Arcana', 'Lv 10'],
|
||||
summon: this.summon
|
||||
},
|
||||
itemOrigin: this.item,
|
||||
description: this.description || (this.item instanceof Item ? this.item.system.description : '')
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
|
|||
return await foundry.applications.ux.TextEditor.implementation.enrichHTML(fullDescription, {
|
||||
relativeTo: this,
|
||||
rollData: this.getRollData(),
|
||||
secrets: this.isOwner
|
||||
secrets: this.parent.isOwner
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -164,26 +164,7 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
|
|||
|
||||
prepareBaseData() {
|
||||
super.prepareBaseData();
|
||||
|
||||
for (const action of this.actions ?? []) {
|
||||
if (!action.actor) continue;
|
||||
|
||||
const actionsToRegister = [];
|
||||
for (let i = 0; i < action.triggers.length; i++) {
|
||||
const trigger = action.triggers[i];
|
||||
const { args } = CONFIG.DH.TRIGGER.triggers[trigger.trigger];
|
||||
const fn = new foundry.utils.AsyncFunction(...args, `{${trigger.command}\n}`);
|
||||
actionsToRegister.push(fn.bind(action));
|
||||
if (i === action.triggers.length - 1)
|
||||
game.system.registeredTriggers.registerTriggers(
|
||||
trigger.trigger,
|
||||
action.actor?.uuid,
|
||||
trigger.triggeringActorType,
|
||||
this.parent.uuid,
|
||||
actionsToRegister
|
||||
);
|
||||
}
|
||||
}
|
||||
game.system.registeredTriggers.registerItemTriggers(this.parent);
|
||||
}
|
||||
|
||||
async _preCreate(data, options, user) {
|
||||
|
|
@ -246,6 +227,28 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
|
|||
const armorData = getScrollTextData(this.parent.parent.system.resources, changed.system.marks, 'armor');
|
||||
options.scrollingTextData = [armorData];
|
||||
}
|
||||
|
||||
if (changed.system?.actions) {
|
||||
const triggersToRemove = Object.keys(changed.system.actions).reduce((acc, key) => {
|
||||
if (!changed.system.actions[key]) {
|
||||
const strippedKey = key.replace('-=', '');
|
||||
acc.push(...this.actions.get(strippedKey).triggers.map(x => x.trigger));
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
game.system.registeredTriggers.unregisterTriggers(triggersToRemove, this.parent.uuid);
|
||||
|
||||
if (this.parent.parent && !(this.parent.parent.token instanceof game.system.api.documents.DhToken)) {
|
||||
for (const token of this.parent.parent.getActiveTokens()) {
|
||||
game.system.registeredTriggers.unregisterTriggers(
|
||||
triggersToRemove,
|
||||
`${token.document.uuid}.${this.parent.uuid}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onUpdate(changed, options, userId) {
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ export default class DHBeastform extends BaseDataItem {
|
|||
choices: CONFIG.DH.ACTOR.tokenSize,
|
||||
initial: CONFIG.DH.ACTOR.tokenSize.custom.id
|
||||
}),
|
||||
scale: new fields.NumberField({ nullable: false, min: 0.2, max: 3, step: 0.05, initial: 1 }),
|
||||
height: new fields.NumberField({ integer: true, min: 1, initial: null, nullable: true }),
|
||||
width: new fields.NumberField({ integer: true, min: 1, initial: null, nullable: true })
|
||||
}),
|
||||
|
|
@ -184,6 +185,7 @@ export default class DHBeastform extends BaseDataItem {
|
|||
tokenImg: this.parent.parent.prototypeToken.texture.src,
|
||||
tokenRingImg: this.parent.parent.prototypeToken.ring.subject.texture,
|
||||
tokenSize: {
|
||||
scale: this.parent.parent.prototypeToken.texture.scaleX,
|
||||
height: this.parent.parent.prototypeToken.height,
|
||||
width: this.parent.parent.prototypeToken.width
|
||||
}
|
||||
|
|
@ -209,7 +211,9 @@ export default class DHBeastform extends BaseDataItem {
|
|||
height,
|
||||
width,
|
||||
texture: {
|
||||
src: this.tokenImg
|
||||
src: this.tokenImg,
|
||||
scaleX: this.tokenSize.scale,
|
||||
scaleY: this.tokenSize.scale
|
||||
},
|
||||
ring: {
|
||||
subject: {
|
||||
|
|
|
|||
|
|
@ -29,7 +29,21 @@ export default class DHDomainCard extends BaseDataItem {
|
|||
required: true,
|
||||
initial: CONFIG.DH.DOMAIN.cardTypes.ability.id
|
||||
}),
|
||||
inVault: new fields.BooleanField({ initial: false })
|
||||
inVault: new fields.BooleanField({ initial: false }),
|
||||
vaultActive: new fields.BooleanField({
|
||||
required: true,
|
||||
nullable: false,
|
||||
initial: false
|
||||
}),
|
||||
loadoutIgnore: new fields.BooleanField({
|
||||
required: true,
|
||||
nullable: false,
|
||||
initial: false
|
||||
}),
|
||||
domainTouched: new fields.NumberField({
|
||||
nullable: true,
|
||||
initial: null
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -38,6 +52,19 @@ export default class DHDomainCard extends BaseDataItem {
|
|||
return game.i18n.localize(allDomainData[this.domain].label);
|
||||
}
|
||||
|
||||
get isVaultSupressed() {
|
||||
return this.inVault && !this.vaultActive;
|
||||
}
|
||||
|
||||
get isDomainTouchedSuppressed() {
|
||||
if (!this.parent.system.domainTouched || this.parent.parent?.type !== 'character') return false;
|
||||
|
||||
const matchingDomainCards = this.parent.parent.items.filter(
|
||||
item => !item.system.inVault && item.system.domain === this.parent.system.domain
|
||||
).length;
|
||||
return matchingDomainCards < this.parent.system.domainTouched;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**@override */
|
||||
|
|
|
|||
167
module/data/registeredTriggers.mjs
Normal file
167
module/data/registeredTriggers.mjs
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
export default class RegisteredTriggers extends Map {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
registerTriggers(triggers, actor, uuid) {
|
||||
for (const triggerKey of Object.keys(CONFIG.DH.TRIGGER.triggers)) {
|
||||
const match = triggers[triggerKey];
|
||||
const existingTrigger = this.get(triggerKey);
|
||||
|
||||
if (!match) {
|
||||
if (existingTrigger?.get(uuid)) this.get(triggerKey).delete(uuid);
|
||||
} else {
|
||||
const { trigger, triggeringActorType, commands } = match;
|
||||
|
||||
if (!existingTrigger) this.set(trigger, new Map());
|
||||
this.get(trigger).set(uuid, { actor, triggeringActorType, commands });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerItemTriggers(item, registerOverride) {
|
||||
if (!item.actor || !item._stats.createdTime) return;
|
||||
for (const action of item.system.actions ?? []) {
|
||||
if (!action.actor) continue;
|
||||
|
||||
/* Non actor-linked should only prep synthetic actors so they're not registering triggers unless they're on the canvas */
|
||||
if (
|
||||
!registerOverride &&
|
||||
!action.actor.prototypeToken.actorLink &&
|
||||
(!(action.actor.parent instanceof game.system.api.documents.DhToken) || !action.actor.parent?.uuid)
|
||||
)
|
||||
continue;
|
||||
|
||||
const triggers = {};
|
||||
for (const trigger of action.triggers) {
|
||||
const { args } = CONFIG.DH.TRIGGER.triggers[trigger.trigger];
|
||||
const fn = new foundry.utils.AsyncFunction(...args, `{${trigger.command}\n}`);
|
||||
|
||||
if (!triggers[trigger.trigger])
|
||||
triggers[trigger.trigger] = {
|
||||
trigger: trigger.trigger,
|
||||
triggeringActorType: trigger.triggeringActorType,
|
||||
commands: []
|
||||
};
|
||||
triggers[trigger.trigger].commands.push(fn.bind(action));
|
||||
}
|
||||
|
||||
this.registerTriggers(triggers, action.actor?.uuid, item.uuid);
|
||||
}
|
||||
}
|
||||
|
||||
unregisterTriggers(triggerKeys, uuid) {
|
||||
for (const triggerKey of triggerKeys) {
|
||||
const existingTrigger = this.get(triggerKey);
|
||||
if (!existingTrigger) return;
|
||||
|
||||
existingTrigger.delete(uuid);
|
||||
}
|
||||
}
|
||||
|
||||
unregisterItemTriggers(items) {
|
||||
for (const item of items) {
|
||||
if (!item.system.actions?.size) continue;
|
||||
|
||||
const triggers = (item.system.actions ?? []).reduce((acc, action) => {
|
||||
acc.push(...action.triggers.map(x => x.trigger));
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
this.unregisterTriggers(triggers, item.uuid);
|
||||
}
|
||||
}
|
||||
|
||||
unregisterSceneEnvironmentTriggers(flagSystemData) {
|
||||
const sceneData = new game.system.api.data.scenes.DHScene(flagSystemData);
|
||||
for (const environment of sceneData.sceneEnvironments) {
|
||||
if (environment.pack) continue;
|
||||
this.unregisterItemTriggers(environment.system.features);
|
||||
}
|
||||
}
|
||||
|
||||
unregisterSceneTriggers(scene) {
|
||||
this.unregisterSceneEnvironmentTriggers(scene.flags.daggerheart);
|
||||
|
||||
for (const triggerKey of Object.keys(CONFIG.DH.TRIGGER.triggers)) {
|
||||
const existingTrigger = this.get(triggerKey);
|
||||
if (!existingTrigger) continue;
|
||||
|
||||
const filtered = new Map();
|
||||
for (const [uuid, data] of existingTrigger.entries()) {
|
||||
if (!uuid.startsWith(scene.uuid)) filtered.set(uuid, data);
|
||||
}
|
||||
this.set(triggerKey, filtered);
|
||||
}
|
||||
}
|
||||
|
||||
registerSceneEnvironmentTriggers(flagSystemData) {
|
||||
const sceneData = new game.system.api.data.scenes.DHScene(flagSystemData);
|
||||
for (const environment of sceneData.sceneEnvironments) {
|
||||
for (const feature of environment.system.features) {
|
||||
if (feature) this.registerItemTriggers(feature, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerSceneTriggers(scene) {
|
||||
this.registerSceneEnvironmentTriggers(scene.flags.daggerheart);
|
||||
|
||||
for (const actor of scene.tokens.filter(x => x.actor).map(x => x.actor)) {
|
||||
if (actor.prototypeToken.actorLink) continue;
|
||||
|
||||
for (const item of actor.items) {
|
||||
this.registerItemTriggers(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async runTrigger(trigger, currentActor, ...args) {
|
||||
const updates = [];
|
||||
const triggerSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).triggers;
|
||||
if (!triggerSettings.enabled) return updates;
|
||||
|
||||
const dualityTrigger = this.get(trigger);
|
||||
if (dualityTrigger?.size) {
|
||||
const triggerActors = ['character', 'adversary', 'environment'];
|
||||
for (let [itemUuid, { actor: actorUuid, triggeringActorType, commands }] of dualityTrigger.entries()) {
|
||||
const actor = await foundry.utils.fromUuid(actorUuid);
|
||||
if (!actor || !triggerActors.includes(actor.type)) continue;
|
||||
|
||||
const triggerData = CONFIG.DH.TRIGGER.triggers[trigger];
|
||||
if (triggerData.usesActor && triggeringActorType !== 'any') {
|
||||
if (triggeringActorType === 'self' && currentActor?.uuid !== actorUuid) continue;
|
||||
else if (triggeringActorType === 'other' && currentActor?.uuid === actorUuid) continue;
|
||||
}
|
||||
|
||||
for (const command of commands) {
|
||||
try {
|
||||
if (CONFIG.debug.triggers) {
|
||||
const item = await foundry.utils.fromUuid(itemUuid);
|
||||
console.log(
|
||||
game.i18n.format('DAGGERHEART.UI.ConsoleLogs.triggerRun', {
|
||||
actor: actor.name ?? '<Missing Actor>',
|
||||
item: item?.name ?? '<Missing Item>',
|
||||
trigger: game.i18n.localize(triggerData.label)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const result = await command(...args);
|
||||
if (result?.updates?.length) updates.push(...result.updates);
|
||||
} catch (_) {
|
||||
const triggerName = game.i18n.localize(triggerData.label);
|
||||
ui.notifications.error(
|
||||
game.i18n.format('DAGGERHEART.CONFIG.Triggers.triggerError', {
|
||||
trigger: triggerName,
|
||||
actor: currentActor?.name
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updates;
|
||||
}
|
||||
}
|
||||
38
module/data/rollTable.mjs
Normal file
38
module/data/rollTable.mjs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import FormulaField from './fields/formulaField.mjs';
|
||||
|
||||
//Extra definitions for RollTable
|
||||
export default class DhRollTable extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
|
||||
return {
|
||||
formulaName: new fields.StringField({
|
||||
required: true,
|
||||
nullable: false,
|
||||
initial: 'Roll Formula',
|
||||
label: 'DAGGERHEART.ROLLTABLES.FIELDS.formulaName.label'
|
||||
}),
|
||||
altFormula: new fields.TypedObjectField(
|
||||
new fields.SchemaField({
|
||||
name: new fields.StringField({
|
||||
required: true,
|
||||
nullable: false,
|
||||
initial: 'Roll Formula',
|
||||
label: 'DAGGERHEART.ROLLTABLES.FIELDS.formulaName.label'
|
||||
}),
|
||||
formula: new FormulaField({ label: 'Formula Roll', initial: '1d20' })
|
||||
})
|
||||
),
|
||||
activeAltFormula: new fields.StringField({ nullable: true, initial: null })
|
||||
};
|
||||
}
|
||||
|
||||
getActiveFormula(baseFormula) {
|
||||
return this.activeAltFormula ? (this.altFormula[this.activeAltFormula]?.formula ?? baseFormula) : baseFormula;
|
||||
}
|
||||
|
||||
static getDefaultFormula = () => ({
|
||||
name: game.i18n.localize('Roll Formula'),
|
||||
formula: '1d20'
|
||||
});
|
||||
}
|
||||
|
|
@ -55,15 +55,10 @@ export default class DhAutomation extends foundry.abstract.DataModel {
|
|||
initial: true,
|
||||
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.resourceScrollTexts.label'
|
||||
}),
|
||||
playerCanEditSheet: new fields.BooleanField({
|
||||
required: true,
|
||||
initial: false,
|
||||
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.playerCanEditSheet.label'
|
||||
}),
|
||||
defeated: new fields.SchemaField({
|
||||
enabled: new fields.BooleanField({
|
||||
required: true,
|
||||
initial: false,
|
||||
initial: true,
|
||||
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.enabled.label'
|
||||
}),
|
||||
overlay: new fields.BooleanField({
|
||||
|
|
@ -74,7 +69,7 @@ export default class DhAutomation extends foundry.abstract.DataModel {
|
|||
characterDefault: new fields.StringField({
|
||||
required: true,
|
||||
choices: CONFIG.DH.GENERAL.defeatedConditionChoices,
|
||||
initial: CONFIG.DH.GENERAL.defeatedConditionChoices.unconscious.id,
|
||||
initial: CONFIG.DH.GENERAL.defeatedConditionChoices.deathMove.id,
|
||||
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.characterDefault.label'
|
||||
}),
|
||||
adversaryDefault: new fields.StringField({
|
||||
|
|
@ -89,23 +84,29 @@ export default class DhAutomation extends foundry.abstract.DataModel {
|
|||
initial: CONFIG.DH.GENERAL.defeatedConditionChoices.defeated.id,
|
||||
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.companionDefault.label'
|
||||
}),
|
||||
deathMoveIcon: new fields.FilePathField({
|
||||
initial: 'icons/magic/life/heart-cross-purple-orange.webp',
|
||||
categories: ['IMAGE'],
|
||||
base64: false,
|
||||
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.deathMove.label'
|
||||
}),
|
||||
deadIcon: new fields.FilePathField({
|
||||
initial: 'icons/magic/death/grave-tombstone-glow-teal.webp',
|
||||
categories: ['IMAGE'],
|
||||
base64: false,
|
||||
label: 'Dead'
|
||||
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.dead.label'
|
||||
}),
|
||||
defeatedIcon: new fields.FilePathField({
|
||||
initial: 'icons/magic/control/fear-fright-mask-orange.webp',
|
||||
categories: ['IMAGE'],
|
||||
base64: false,
|
||||
label: 'Defeated'
|
||||
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.defeated.label'
|
||||
}),
|
||||
unconsciousIcon: new fields.FilePathField({
|
||||
initial: 'icons/magic/control/sleep-bubble-purple.webp',
|
||||
categories: ['IMAGE'],
|
||||
base64: false,
|
||||
label: 'Unconcious'
|
||||
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.unconscious.label'
|
||||
})
|
||||
}),
|
||||
roll: new fields.SchemaField({
|
||||
|
|
|
|||
|
|
@ -23,6 +23,13 @@ export default class DhHomebrew extends foundry.abstract.DataModel {
|
|||
initial: 12,
|
||||
label: 'DAGGERHEART.SETTINGS.Homebrew.FIELDS.maxFear.label'
|
||||
}),
|
||||
maxHope: new fields.NumberField({
|
||||
required: true,
|
||||
integer: true,
|
||||
min: 0,
|
||||
initial: 6,
|
||||
label: 'DAGGERHEART.SETTINGS.Homebrew.FIELDS.maxHope.label'
|
||||
}),
|
||||
maxLoadout: new fields.NumberField({
|
||||
required: true,
|
||||
integer: true,
|
||||
|
|
|
|||
|
|
@ -3,3 +3,4 @@ export { default as D20Roll } from './d20Roll.mjs';
|
|||
export { default as DamageRoll } from './damageRoll.mjs';
|
||||
export { default as DHRoll } from './dhRoll.mjs';
|
||||
export { default as DualityRoll } from './dualityRoll.mjs';
|
||||
export { default as FateRoll } from './fateRoll.mjs';
|
||||
|
|
|
|||
|
|
@ -35,7 +35,9 @@ export default class D20Roll extends DHRoll {
|
|||
|
||||
get isCritical() {
|
||||
if (!this.d20._evaluated) return;
|
||||
return this.d20.total >= this.data.system.criticalThreshold;
|
||||
|
||||
const criticalThreshold = this.options.actionType === 'reaction' ? 20 : this.data.system.criticalThreshold;
|
||||
return this.d20.total >= criticalThreshold;
|
||||
}
|
||||
|
||||
get hasAdvantage() {
|
||||
|
|
@ -97,11 +99,14 @@ export default class D20Roll extends DHRoll {
|
|||
|
||||
this.options.roll.modifiers = this.applyBaseBonus();
|
||||
|
||||
const actorExperiences = this.options.roll.companionRoll
|
||||
? (this.options.data?.companion?.system.experiences ?? {})
|
||||
: (this.options.data.system?.experiences ?? {});
|
||||
this.options.experiences?.forEach(m => {
|
||||
if (this.options.data.system?.experiences?.[m])
|
||||
if (actorExperiences[m])
|
||||
this.options.roll.modifiers.push({
|
||||
label: this.options.data.system.experiences[m].name,
|
||||
value: this.options.data.system.experiences[m].value
|
||||
label: actorExperiences[m].name,
|
||||
value: actorExperiences[m].value
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -127,15 +132,55 @@ export default class D20Roll extends DHRoll {
|
|||
const modifiers = foundry.utils.deepClone(this.options.roll.baseModifiers) ?? [];
|
||||
|
||||
modifiers.push(
|
||||
...this.getBonus(`roll.${this.options.actionType}`, `${this.options.actionType?.capitalize()} Bonus`)
|
||||
);
|
||||
modifiers.push(
|
||||
...this.getBonus(`roll.${this.options.roll.type}`, `${this.options.roll.type?.capitalize()} Bonus`)
|
||||
...this.getBonus(
|
||||
`system.bonuses.roll.${this.options.actionType}`,
|
||||
`${this.options.actionType?.capitalize()} Bonus`
|
||||
)
|
||||
);
|
||||
|
||||
if (this.options.roll.type !== CONFIG.DH.GENERAL.rollTypes.attack.id) {
|
||||
modifiers.push(
|
||||
...this.getBonus(
|
||||
`system.bonuses.roll.${this.options.roll.type}`,
|
||||
`${this.options.roll.type?.capitalize()} Bonus`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
this.options.roll.type === CONFIG.DH.GENERAL.rollTypes.attack.id ||
|
||||
(this.options.roll.type === CONFIG.DH.GENERAL.rollTypes.spellcast.id && this.options.hasDamage)
|
||||
) {
|
||||
modifiers.push(
|
||||
...this.getBonus(`system.bonuses.roll.attack`, `${this.options.roll.type?.capitalize()} Bonus`)
|
||||
);
|
||||
}
|
||||
|
||||
return modifiers;
|
||||
}
|
||||
|
||||
getActionChangeKeys() {
|
||||
const changeKeys = new Set([`system.bonuses.roll.${this.options.actionType}`]);
|
||||
|
||||
if (this.options.roll.type !== CONFIG.DH.GENERAL.rollTypes.attack.id) {
|
||||
changeKeys.add(`system.bonuses.roll.${this.options.roll.type}`);
|
||||
}
|
||||
|
||||
if (
|
||||
this.options.roll.type === CONFIG.DH.GENERAL.rollTypes.attack.id ||
|
||||
(this.options.roll.type === CONFIG.DH.GENERAL.rollTypes.spellcast.id && this.options.hasDamage)
|
||||
) {
|
||||
changeKeys.add(`system.bonuses.roll.attack`);
|
||||
}
|
||||
|
||||
if (this.options.roll.trait && this.data.traits?.[this.options.roll.trait]) {
|
||||
if (this.options.roll.type !== CONFIG.DH.GENERAL.rollTypes.spellcast.id)
|
||||
changeKeys.add('system.bonuses.roll.trait');
|
||||
}
|
||||
|
||||
return changeKeys;
|
||||
}
|
||||
|
||||
static postEvaluate(roll, config = {}) {
|
||||
const data = super.postEvaluate(roll, config);
|
||||
data.type = config.actionType;
|
||||
|
|
|
|||
|
|
@ -93,7 +93,6 @@ export default class DamageRoll extends DHRoll {
|
|||
type = this.options.messageType ?? (this.options.hasHealing ? 'healing' : 'damage'),
|
||||
options = part ?? this.options;
|
||||
|
||||
modifiers.push(...this.getBonus(`${type}`, `${type.capitalize()} Bonus`));
|
||||
if (!this.options.hasHealing) {
|
||||
options.damageTypes?.forEach(t => {
|
||||
modifiers.push(...this.getBonus(`${type}.${t}`, `${t.capitalize()} ${type.capitalize()} Bonus`));
|
||||
|
|
@ -108,6 +107,31 @@ export default class DamageRoll extends DHRoll {
|
|||
return modifiers;
|
||||
}
|
||||
|
||||
getActionChangeKeys() {
|
||||
const type = this.options.messageType ?? (this.options.hasHealing ? 'healing' : 'damage');
|
||||
const changeKeys = [];
|
||||
|
||||
for (const roll of this.options.roll) {
|
||||
for (const damageType of roll.damageTypes?.values?.() ?? []) {
|
||||
changeKeys.push(`system.bonuses.${type}.${damageType}`);
|
||||
}
|
||||
}
|
||||
|
||||
const item = this.data.parent?.items?.get(this.options.source.item);
|
||||
if (item) {
|
||||
switch (item.type) {
|
||||
case 'weapon':
|
||||
if (!this.options.hasHealing)
|
||||
['primaryWeapon', 'secondaryWeapon'].forEach(w =>
|
||||
changeKeys.push(`system.bonuses.damage.${w}`)
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return changeKeys;
|
||||
}
|
||||
|
||||
constructFormula(config) {
|
||||
this.options.roll.forEach((part, index) => {
|
||||
part.roll = new Roll(Roll.replaceFormulaData(part.formula, config.data));
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs';
|
|||
export default class DHRoll extends Roll {
|
||||
baseTerms = [];
|
||||
constructor(formula, data = {}, options = {}) {
|
||||
super(formula, data, options);
|
||||
super(formula, data, foundry.utils.mergeObject(options, { roll: [] }, { overwrite: false }));
|
||||
options.bonusEffects = this.bonusEffectBuilder();
|
||||
if (!this.data || !Object.keys(this.data).length) this.data = options.data;
|
||||
}
|
||||
|
||||
|
|
@ -164,12 +165,18 @@ export default class DHRoll extends Roll {
|
|||
new foundry.dice.terms.OperatorTerm({ operator: '+' }),
|
||||
...this.constructor.parse(modifier.join(' + '), this.options.data)
|
||||
];
|
||||
} else {
|
||||
} else if (Number.isNumeric(modifier)) {
|
||||
const numTerm = modifier < 0 ? '-' : '+';
|
||||
return [
|
||||
new foundry.dice.terms.OperatorTerm({ operator: numTerm }),
|
||||
new foundry.dice.terms.NumericTerm({ number: Math.abs(modifier) })
|
||||
];
|
||||
} else {
|
||||
const numTerm = modifier < 0 ? '-' : '+';
|
||||
return [
|
||||
new foundry.dice.terms.OperatorTerm({ operator: numTerm }),
|
||||
...this.constructor.parse(modifier, this.options.data)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -185,18 +192,20 @@ export default class DHRoll extends Roll {
|
|||
}
|
||||
|
||||
getBonus(path, label) {
|
||||
const bonus = foundry.utils.getProperty(this.data.bonuses, path),
|
||||
modifiers = [];
|
||||
if (bonus?.bonus)
|
||||
modifiers.push({
|
||||
label: label,
|
||||
value: bonus?.bonus
|
||||
});
|
||||
if (bonus?.dice?.length)
|
||||
modifiers.push({
|
||||
label: label,
|
||||
value: bonus?.dice
|
||||
});
|
||||
const modifiers = [];
|
||||
for (const effect of Object.values(this.options.bonusEffects)) {
|
||||
if (!effect.selected) continue;
|
||||
for (const change of effect.changes) {
|
||||
if (!change.key.includes(path)) continue;
|
||||
const changeValue = game.system.api.documents.DhActiveEffect.getChangeValue(
|
||||
this.data,
|
||||
change,
|
||||
effect.origEffect
|
||||
);
|
||||
modifiers.push({ label: label, value: changeValue });
|
||||
}
|
||||
}
|
||||
|
||||
return modifiers;
|
||||
}
|
||||
|
||||
|
|
@ -235,4 +244,28 @@ export default class DHRoll extends Roll {
|
|||
static temporaryModifierBuilder(config) {
|
||||
return {};
|
||||
}
|
||||
|
||||
bonusEffectBuilder() {
|
||||
const changeKeys = this.getActionChangeKeys();
|
||||
return (
|
||||
this.options.effects?.reduce((acc, effect) => {
|
||||
if (effect.changes.some(x => changeKeys.some(key => x.key.includes(key)))) {
|
||||
acc[effect.id] = {
|
||||
id: effect.id,
|
||||
name: effect.name,
|
||||
description: effect.description,
|
||||
changes: effect.changes,
|
||||
origEffect: effect,
|
||||
selected: !effect.disabled
|
||||
};
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {}) ?? []
|
||||
);
|
||||
}
|
||||
|
||||
getActionChangeKeys() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export default class DualityRoll extends D20Roll {
|
|||
constructor(formula, data = {}, options = {}) {
|
||||
super(formula, data, options);
|
||||
this.rallyChoices = this.setRallyChoices();
|
||||
this.guaranteedCritical = options.guaranteedCritical;
|
||||
}
|
||||
|
||||
static messageType = 'dualityRoll';
|
||||
|
|
@ -25,29 +26,23 @@ export default class DualityRoll extends D20Roll {
|
|||
}
|
||||
|
||||
get dHope() {
|
||||
// if ( !(this.terms[0] instanceof foundry.dice.terms.Die) ) return;
|
||||
if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice();
|
||||
return this.dice[0];
|
||||
// return this.#hopeDice;
|
||||
}
|
||||
|
||||
set dHope(faces) {
|
||||
if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice();
|
||||
this.terms[0].faces = this.getFaces(faces);
|
||||
// this.#hopeDice = `d${face}`;
|
||||
this.dice[0].faces = this.getFaces(faces);
|
||||
}
|
||||
|
||||
get dFear() {
|
||||
// if ( !(this.terms[1] instanceof foundry.dice.terms.Die) ) return;
|
||||
if (!(this.dice[1] instanceof foundry.dice.terms.Die)) this.createBaseDice();
|
||||
return this.dice[1];
|
||||
// return this.#fearDice;
|
||||
}
|
||||
|
||||
set dFear(faces) {
|
||||
if (!(this.dice[1] instanceof foundry.dice.terms.Die)) this.createBaseDice();
|
||||
this.dice[1].faces = this.getFaces(faces);
|
||||
// this.#fearDice = `d${face}`;
|
||||
}
|
||||
|
||||
get dAdvantage() {
|
||||
|
|
@ -90,26 +85,29 @@ export default class DualityRoll extends D20Roll {
|
|||
}
|
||||
|
||||
get isCritical() {
|
||||
if (this.guaranteedCritical) return true;
|
||||
if (!this.dHope._evaluated || !this.dFear._evaluated) return;
|
||||
return this.dHope.total === this.dFear.total;
|
||||
}
|
||||
|
||||
get withHope() {
|
||||
if (!this._evaluated) return;
|
||||
if (!this._evaluated || this.guaranteedCritical) return;
|
||||
return this.dHope.total > this.dFear.total;
|
||||
}
|
||||
|
||||
get withFear() {
|
||||
if (!this._evaluated) return;
|
||||
if (!this._evaluated || this.guaranteedCritical) return;
|
||||
return this.dHope.total < this.dFear.total;
|
||||
}
|
||||
|
||||
get totalLabel() {
|
||||
const label = this.withHope
|
||||
? 'DAGGERHEART.GENERAL.hope'
|
||||
: this.withFear
|
||||
? 'DAGGERHEART.GENERAL.fear'
|
||||
: 'DAGGERHEART.GENERAL.criticalSuccess';
|
||||
const label = this.guaranteedCritical
|
||||
? 'DAGGERHEART.GENERAL.guaranteedCriticalSuccess'
|
||||
: this.isCritical
|
||||
? 'DAGGERHEART.GENERAL.criticalSuccess'
|
||||
: this.withHope
|
||||
? 'DAGGERHEART.GENERAL.hope'
|
||||
: 'DAGGERHEART.GENERAL.fear';
|
||||
|
||||
return game.i18n.localize(label);
|
||||
}
|
||||
|
|
@ -130,9 +128,14 @@ export default class DualityRoll extends D20Roll {
|
|||
this.terms = [this.terms[0], this.terms[1], this.terms[2]];
|
||||
return;
|
||||
}
|
||||
this.terms[0] = new foundry.dice.terms.Die({ faces: 12 });
|
||||
|
||||
this.terms[0] = new foundry.dice.terms.Die({
|
||||
faces: this.data.rules.dualityRoll?.defaultHopeDice ?? 12
|
||||
});
|
||||
this.terms[1] = new foundry.dice.terms.OperatorTerm({ operator: '+' });
|
||||
this.terms[2] = new foundry.dice.terms.Die({ faces: 12 });
|
||||
this.terms[2] = new foundry.dice.terms.Die({
|
||||
faces: this.data.rules.dualityRoll?.defaultFearDice ?? 12
|
||||
});
|
||||
}
|
||||
|
||||
applyAdvantage() {
|
||||
|
|
@ -173,6 +176,49 @@ export default class DualityRoll extends D20Roll {
|
|||
return modifiers;
|
||||
}
|
||||
|
||||
static async buildConfigure(config = {}, message = {}) {
|
||||
config.dialog ??= {};
|
||||
config.guaranteedCritical = config.data?.parent?.appliedEffects.reduce((a, c) => {
|
||||
const change = c.changes.find(ch => ch.key === 'system.rules.roll.guaranteedCritical');
|
||||
if (change) a = true;
|
||||
return a;
|
||||
}, false);
|
||||
|
||||
if (config.guaranteedCritical) {
|
||||
config.dialog.configure = false;
|
||||
}
|
||||
|
||||
return super.buildConfigure(config, message);
|
||||
}
|
||||
|
||||
getActionChangeKeys() {
|
||||
const changeKeys = new Set([`system.bonuses.roll.${this.options.actionType}`]);
|
||||
|
||||
if (this.options.roll.type !== CONFIG.DH.GENERAL.rollTypes.attack.id) {
|
||||
changeKeys.add(`system.bonuses.roll.${this.options.roll.type}`);
|
||||
}
|
||||
|
||||
if (
|
||||
this.options.roll.type === CONFIG.DH.GENERAL.rollTypes.attack.id ||
|
||||
(this.options.roll.type === CONFIG.DH.GENERAL.rollTypes.spellcast.id && this.options.hasDamage)
|
||||
) {
|
||||
changeKeys.add(`system.bonuses.roll.attack`);
|
||||
}
|
||||
|
||||
if (this.options.roll.trait && this.data.traits?.[this.options.roll.trait]) {
|
||||
if (this.options.roll.type !== CONFIG.DH.GENERAL.rollTypes.spellcast.id)
|
||||
changeKeys.add('system.bonuses.roll.trait');
|
||||
}
|
||||
|
||||
const weapons = ['primaryWeapon', 'secondaryWeapon'];
|
||||
weapons.forEach(w => {
|
||||
if (this.options.source.item && this.options.source.item === this.data[w]?.id)
|
||||
changeKeys.add(`system.bonuses.roll.${w}`);
|
||||
});
|
||||
|
||||
return changeKeys;
|
||||
}
|
||||
|
||||
static async buildEvaluate(roll, config = {}, message = {}) {
|
||||
await super.buildEvaluate(roll, config, message);
|
||||
|
||||
|
|
@ -190,7 +236,7 @@ export default class DualityRoll extends D20Roll {
|
|||
|
||||
data.hope = {
|
||||
dice: roll.dHope.denomination,
|
||||
value: roll.dHope.total,
|
||||
value: this.guaranteedCritical ? 0 : roll.dHope.total,
|
||||
rerolled: {
|
||||
any: roll.dHope.results.some(x => x.rerolled),
|
||||
rerolls: roll.dHope.results.filter(x => x.rerolled)
|
||||
|
|
@ -198,7 +244,7 @@ export default class DualityRoll extends D20Roll {
|
|||
};
|
||||
data.fear = {
|
||||
dice: roll.dFear.denomination,
|
||||
value: roll.dFear.total,
|
||||
value: this.guaranteedCritical ? 0 : roll.dFear.total,
|
||||
rerolled: {
|
||||
any: roll.dFear.results.some(x => x.rerolled),
|
||||
rerolls: roll.dFear.results.filter(x => x.rerolled)
|
||||
|
|
@ -210,7 +256,7 @@ export default class DualityRoll extends D20Roll {
|
|||
};
|
||||
data.result = {
|
||||
duality: roll.withHope ? 1 : roll.withFear ? -1 : 0,
|
||||
total: roll.dHope.total + roll.dFear.total,
|
||||
total: this.guaranteedCritical ? 0 : roll.dHope.total + roll.dFear.total,
|
||||
label: roll.totalLabel
|
||||
};
|
||||
|
||||
|
|
@ -228,6 +274,8 @@ export default class DualityRoll extends D20Roll {
|
|||
}
|
||||
|
||||
static async handleTriggers(roll, config) {
|
||||
if (!config.source?.actor || config.skips?.triggers) return;
|
||||
|
||||
const updates = [];
|
||||
const dualityUpdates = await game.system.registeredTriggers.runTrigger(
|
||||
CONFIG.DH.TRIGGER.triggers.dualityRoll.id,
|
||||
|
|
|
|||
85
module/dice/fateRoll.mjs
Normal file
85
module/dice/fateRoll.mjs
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs';
|
||||
import D20Roll from './d20Roll.mjs';
|
||||
import { setDiceSoNiceForHopeFateRoll, setDiceSoNiceForFearFateRoll } from '../helpers/utils.mjs';
|
||||
|
||||
export default class FateRoll extends D20Roll {
|
||||
constructor(formula, data = {}, options = {}) {
|
||||
super(formula, data, options);
|
||||
}
|
||||
|
||||
static messageType = 'fateRoll';
|
||||
|
||||
static DefaultDialog = D20RollDialog;
|
||||
|
||||
get title() {
|
||||
return game.i18n.localize(`DAGGERHEART.GENERAL.fateRoll`);
|
||||
}
|
||||
|
||||
get dHope() {
|
||||
if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice();
|
||||
return this.dice[0];
|
||||
}
|
||||
|
||||
set dHope(faces) {
|
||||
if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice();
|
||||
this.dice[0].faces = this.getFaces(faces);
|
||||
}
|
||||
|
||||
get dFear() {
|
||||
if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice();
|
||||
return this.dice[0];
|
||||
}
|
||||
|
||||
set dFear(faces) {
|
||||
if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice();
|
||||
this.dice[0].faces = this.getFaces(faces);
|
||||
}
|
||||
|
||||
get isCritical() {
|
||||
return false;
|
||||
}
|
||||
|
||||
get fateDie() {
|
||||
return this.data.fateType;
|
||||
}
|
||||
|
||||
static getHooks(hooks) {
|
||||
return [...(hooks ?? []), 'Fate'];
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
static fromData(data) {
|
||||
data.terms[0].class = foundry.dice.terms.Die.name;
|
||||
return super.fromData(data);
|
||||
}
|
||||
|
||||
createBaseDice() {
|
||||
if (this.dice[0] instanceof foundry.dice.terms.Die) {
|
||||
this.terms = [this.terms[0]];
|
||||
return;
|
||||
}
|
||||
this.terms[0] = new foundry.dice.terms.Die({ faces: 12 });
|
||||
}
|
||||
|
||||
static async buildEvaluate(roll, config = {}, message = {}) {
|
||||
await super.buildEvaluate(roll, config, message);
|
||||
|
||||
if (roll.fateDie === 'Hope') {
|
||||
await setDiceSoNiceForHopeFateRoll(roll, config.roll.fate.dice);
|
||||
} else {
|
||||
await setDiceSoNiceForFearFateRoll(roll, config.roll.fate.dice);
|
||||
}
|
||||
}
|
||||
|
||||
static postEvaluate(roll, config = {}) {
|
||||
const data = super.postEvaluate(roll, config);
|
||||
|
||||
data.fate = {
|
||||
dice: roll.fateDie === 'Hope' ? roll.dHope.denomination : roll.dFear.denomination,
|
||||
value: roll.fateDie === 'Hope' ? roll.dHope.total : roll.dFear.total,
|
||||
fateDie: roll.fateDie
|
||||
};
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,9 @@ export { default as DhpCombat } from './combat.mjs';
|
|||
export { default as DHCombatant } from './combatant.mjs';
|
||||
export { default as DhActiveEffect } from './activeEffect.mjs';
|
||||
export { default as DhChatMessage } from './chatMessage.mjs';
|
||||
export { default as DhRollTable } from './rollTable.mjs';
|
||||
export { default as DhScene } from './scene.mjs';
|
||||
export { default as DhToken } from './token.mjs';
|
||||
export { default as DhTooltipManager } from './tooltipManager.mjs';
|
||||
export { default as DhTemplateManager } from './templateManager.mjs';
|
||||
export { default as DhTokenManager } from './tokenManager.mjs';
|
||||
|
|
|
|||
|
|
@ -20,7 +20,10 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
|
|||
}
|
||||
|
||||
if (this.parent?.type === 'domainCard') {
|
||||
return this.parent.system.inVault;
|
||||
const isVaultSupressed = this.parent.system.isVaultSupressed;
|
||||
const domainTouchedSupressed = this.parent.system.isDomainTouchedSuppressed;
|
||||
|
||||
return isVaultSupressed || domainTouchedSupressed;
|
||||
}
|
||||
|
||||
return super.isSuppressed;
|
||||
|
|
@ -106,23 +109,29 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
|
|||
|
||||
/**@inheritdoc*/
|
||||
static applyField(model, change, field) {
|
||||
const isOriginTarget = change.value.toLowerCase().includes('origin.@');
|
||||
change.value = DhActiveEffect.getChangeValue(model, change, change.effect);
|
||||
super.applyField(model, change, field);
|
||||
}
|
||||
|
||||
/** */
|
||||
static getChangeValue(model, change, effect) {
|
||||
let value = change.value;
|
||||
const isOriginTarget = value.toLowerCase().includes('origin.@');
|
||||
let parseModel = model;
|
||||
if (isOriginTarget && change.effect.origin) {
|
||||
change.value = change.value.replaceAll(/origin\.@/gi, '@');
|
||||
if (isOriginTarget && effect.origin) {
|
||||
value = change.value.replaceAll(/origin\.@/gi, '@');
|
||||
try {
|
||||
const effect = foundry.utils.fromUuidSync(change.effect.origin);
|
||||
const originEffect = foundry.utils.fromUuidSync(effect.origin);
|
||||
const doc =
|
||||
effect.parent?.parent instanceof game.system.api.documents.DhpActor
|
||||
? effect.parent
|
||||
: effect.parent.parent;
|
||||
originEffect.parent?.parent instanceof game.system.api.documents.DhpActor
|
||||
? originEffect.parent
|
||||
: originEffect.parent.parent;
|
||||
if (doc) parseModel = doc;
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
const evalValue = this.effectSafeEval(itemAbleRollParse(change.value, parseModel, change.effect.parent));
|
||||
change.value = evalValue ?? change.value;
|
||||
super.applyField(model, change, field);
|
||||
const evalValue = this.effectSafeEval(itemAbleRollParse(value, parseModel, effect.parent));
|
||||
return evalValue ?? value;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -104,6 +104,16 @@ export default class DhpActor extends Actor {
|
|||
}
|
||||
}
|
||||
|
||||
async _preDelete() {
|
||||
if (this.prototypeToken.actorLink) {
|
||||
game.system.registeredTriggers.unregisterItemTriggers(this.items);
|
||||
} else {
|
||||
for (const token of this.getActiveTokens()) {
|
||||
game.system.registeredTriggers.unregisterItemTriggers(token.actor.items);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onDelete(options, userId) {
|
||||
super._onDelete(options, userId);
|
||||
for (const party of this.parties) {
|
||||
|
|
@ -231,6 +241,11 @@ export default class DhpActor extends Actor {
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (this.system.companion) {
|
||||
this.system.companion.updateLevel(usedLevel);
|
||||
}
|
||||
|
||||
this.sheet.render();
|
||||
}
|
||||
}
|
||||
|
|
@ -597,7 +612,7 @@ export default class DhpActor extends Actor {
|
|||
if (!updates.length) return;
|
||||
|
||||
const hpDamage = updates.find(u => u.key === CONFIG.DH.GENERAL.healingTypes.hitPoints.id);
|
||||
if (hpDamage) {
|
||||
if (hpDamage?.value) {
|
||||
hpDamage.value = this.convertDamageToThreshold(hpDamage.value);
|
||||
if (
|
||||
this.type === 'character' &&
|
||||
|
|
@ -754,16 +769,24 @@ export default class DhpActor extends Actor {
|
|||
};
|
||||
}
|
||||
} else {
|
||||
const valueFunc = (base, resource, baseMax) => {
|
||||
if (resource.clear) return baseMax && base.inverted ? baseMax : 0;
|
||||
|
||||
return (base.value ?? base) + resource.value;
|
||||
};
|
||||
switch (r.key) {
|
||||
case 'fear':
|
||||
ui.resources.updateFear(
|
||||
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear) + r.value
|
||||
valueFunc(
|
||||
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear),
|
||||
r
|
||||
)
|
||||
);
|
||||
break;
|
||||
case 'armor':
|
||||
if (this.system.armor?.system?.marks) {
|
||||
updates.armor.resources['system.marks.value'] = Math.max(
|
||||
Math.min(this.system.armor.system.marks.value + r.value, this.system.armorScore),
|
||||
Math.min(valueFunc(this.system.armor.system.marks, r), this.system.armorScore),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
|
@ -772,7 +795,7 @@ export default class DhpActor extends Actor {
|
|||
if (this.system.resources?.[r.key]) {
|
||||
updates.actor.resources[`system.resources.${r.key}.value`] = Math.max(
|
||||
Math.min(
|
||||
this.system.resources[r.key].value + r.value,
|
||||
valueFunc(this.system.resources[r.key], r, this.system.resources[r.key].max),
|
||||
this.system.resources[r.key].max
|
||||
),
|
||||
0
|
||||
|
|
@ -831,8 +854,8 @@ export default class DhpActor extends Actor {
|
|||
|
||||
async toggleDefeated(defeatedState) {
|
||||
const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).defeated;
|
||||
const { unconscious, defeated, dead } = CONFIG.DH.GENERAL.conditions();
|
||||
const defeatedConditions = new Set([unconscious.id, defeated.id, dead.id]);
|
||||
const { deathMove, unconscious, defeated, dead } = CONFIG.DH.GENERAL.conditions();
|
||||
const defeatedConditions = new Set([deathMove.id, unconscious.id, defeated.id, dead.id]);
|
||||
if (!defeatedState) {
|
||||
for (let defeatedId of defeatedConditions) {
|
||||
await this.toggleStatusEffect(defeatedId, { overlay: settings.overlay, active: defeatedState });
|
||||
|
|
@ -846,6 +869,18 @@ export default class DhpActor extends Actor {
|
|||
}
|
||||
}
|
||||
|
||||
async setDeathMoveDefeated(defeatedIconId) {
|
||||
const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).defeated;
|
||||
const actorDefault = settings[`${this.type}Default`];
|
||||
if (!settings.enabled || !settings.enabled || !actorDefault || actorDefault === defeatedIconId) return;
|
||||
|
||||
for (let defeatedId of Object.keys(CONFIG.DH.GENERAL.defeatedConditionChoices)) {
|
||||
await this.toggleStatusEffect(defeatedId, { overlay: settings.overlay, active: false });
|
||||
}
|
||||
|
||||
if (defeatedIconId) await this.toggleStatusEffect(defeatedIconId, { overlay: settings.overlay, active: true });
|
||||
}
|
||||
|
||||
queueScrollText(scrollingTextData) {
|
||||
this.#scrollTextQueue.push(...scrollingTextData.map(data => () => createScrollText(this, data)));
|
||||
if (!this.#scrollTextInterval) {
|
||||
|
|
|
|||
|
|
@ -87,6 +87,15 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
|
|||
break;
|
||||
}
|
||||
}
|
||||
if (this.type === 'fateRoll') {
|
||||
html.classList.add('fate');
|
||||
if (this.system.roll?.fate.fateDie == 'Hope') {
|
||||
html.classList.add('hope');
|
||||
}
|
||||
if (this.system.roll?.fate.fateDie == 'Fear') {
|
||||
html.classList.add('fear');
|
||||
}
|
||||
}
|
||||
|
||||
const autoExpandRoll = game.settings.get(
|
||||
CONFIG.DH.id,
|
||||
|
|
@ -157,7 +166,12 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
|
|||
event.stopPropagation();
|
||||
const config = foundry.utils.deepClone(this.system);
|
||||
config.event = event;
|
||||
await this.system.action?.workflow.get('damage')?.execute(config, this._id, true);
|
||||
if (this.system.action) {
|
||||
const actor = await foundry.utils.fromUuid(config.source.actor);
|
||||
const item = actor?.items.get(config.source.item) ?? null;
|
||||
config.effects = await game.system.api.data.actions.actionsTypes.base.getEffects(actor, item);
|
||||
await this.system.action.workflow.get('damage')?.execute(config, this._id, true);
|
||||
}
|
||||
|
||||
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
|
||||
await game.socket.emit(`system.${CONFIG.DH.id}`, {
|
||||
|
|
@ -174,7 +188,7 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
|
|||
config = foundry.utils.deepClone(this.system);
|
||||
config.event = event;
|
||||
|
||||
if (this.system.onSave) {
|
||||
if (config.hasSave) {
|
||||
const pendingingSaves = targets.filter(t => t.saved.success === null);
|
||||
if (pendingingSaves.length) {
|
||||
const confirm = await foundry.applications.api.DialogV2.confirm({
|
||||
|
|
@ -189,7 +203,16 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
|
|||
return ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelectedOrPerm'));
|
||||
|
||||
this.consumeOnSuccess();
|
||||
this.system.action?.workflow.get('applyDamage')?.execute(config, targets, true);
|
||||
if (this.system.action) this.system.action.workflow.get('applyDamage')?.execute(config, targets, true);
|
||||
else {
|
||||
for (const target of targets) {
|
||||
const actor = await foundry.utils.fromUuid(target.actorId);
|
||||
if (!actor) continue;
|
||||
|
||||
if (this.system.hasHealing) actor.takeHealing(this.system.damage);
|
||||
else actor.takeDamage(this.system.damage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async onRollSave(event) {
|
||||
|
|
|
|||
1
module/documents/collections/_module.mjs
Normal file
1
module/documents/collections/_module.mjs
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default as DhActorCollection } from './actorCollection.mjs';
|
||||
14
module/documents/collections/actorCollection.mjs
Normal file
14
module/documents/collections/actorCollection.mjs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
export default class DhActorCollection extends foundry.documents.collections.Actors {
|
||||
/** Ensure companions are initialized after all other subtypes. */
|
||||
_initialize() {
|
||||
super._initialize();
|
||||
const companions = [];
|
||||
for (const actor of this.values()) {
|
||||
if (actor.type === 'companion') companions.push(actor);
|
||||
}
|
||||
for (const actor of companions) {
|
||||
this.delete(actor.id);
|
||||
this.set(actor.id, actor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -31,7 +31,7 @@ export default class DHItem extends foundry.documents.Item {
|
|||
static async createDocuments(sources, operation) {
|
||||
// Ensure that items being created are valid to the actor its being added to
|
||||
const actor = operation.parent;
|
||||
sources = actor?.system?.isItemValid ? sources.filter((s) => actor.system.isItemValid(s)) : sources;
|
||||
sources = actor?.system?.isItemValid ? sources.filter(s => actor.system.isItemValid(s)) : sources;
|
||||
return super.createDocuments(sources, operation);
|
||||
}
|
||||
|
||||
|
|
@ -146,6 +146,16 @@ export default class DHItem extends foundry.documents.Item {
|
|||
/* -------------------------------------------- */
|
||||
|
||||
async use(event) {
|
||||
/* DomainCard check. Can be expanded or made neater */
|
||||
if (this.system.isDomainTouchedSuppressed) {
|
||||
return ui.notifications.warn(
|
||||
game.i18n.format('DAGGERHEART.UI.Notifications.domainTouchRequirement', {
|
||||
nr: this.domainTouched,
|
||||
domain: game.i18n.localize(CONFIG.DH.DOMAIN.allDomains()[this.domain].label)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const actions = new Set(this.system.actionsList);
|
||||
if (actions?.size) {
|
||||
let action = actions.first();
|
||||
|
|
@ -198,4 +208,23 @@ export default class DHItem extends foundry.documents.Item {
|
|||
|
||||
cls.create(msg);
|
||||
}
|
||||
|
||||
deleteTriggers() {
|
||||
const actions = Array.from(this.system.actions ?? []);
|
||||
if (!actions.length) return;
|
||||
|
||||
const triggerKeys = actions.flatMap(action => action.triggers.map(x => x.trigger));
|
||||
|
||||
game.system.registeredTriggers.unregisterTriggers(triggerKeys, this.uuid);
|
||||
|
||||
if (this.actor && !(this.actor.parent instanceof game.system.api.documents.DhToken)) {
|
||||
for (const token of this.actor.getActiveTokens()) {
|
||||
game.system.registeredTriggers.unregisterTriggers(triggerKeys, `${token.document.uuid}.${this.uuid}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async _preDelete() {
|
||||
this.deleteTriggers();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
122
module/documents/rollTable.mjs
Normal file
122
module/documents/rollTable.mjs
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
export default class DhRollTable extends foundry.documents.RollTable {
|
||||
async roll({ selectedFormula, roll, recursive = true, _depth = 0 } = {}) {
|
||||
// Prevent excessive recursion
|
||||
if (_depth > 5) {
|
||||
throw new Error(`Maximum recursion depth exceeded when attempting to draw from RollTable ${this.id}`);
|
||||
}
|
||||
|
||||
const formula = selectedFormula ?? this.formula;
|
||||
|
||||
// If there is no formula, automatically calculate an even distribution
|
||||
if (!this.formula) {
|
||||
await this.normalize();
|
||||
}
|
||||
|
||||
// Reference the provided roll formula
|
||||
roll = roll instanceof Roll ? roll : Roll.create(formula);
|
||||
let results = [];
|
||||
|
||||
// Ensure that at least one non-drawn result remains
|
||||
const available = this.results.filter(r => !r.drawn);
|
||||
if (!available.length) {
|
||||
ui.notifications.warn(game.i18n.localize('TABLE.NoAvailableResults'));
|
||||
return { roll, results };
|
||||
}
|
||||
|
||||
// Ensure that results are available within the minimum/maximum range
|
||||
const minRoll = (await roll.reroll({ minimize: true })).total;
|
||||
const maxRoll = (await roll.reroll({ maximize: true })).total;
|
||||
const availableRange = available.reduce(
|
||||
(range, result) => {
|
||||
const r = result.range;
|
||||
if (!range[0] || r[0] < range[0]) range[0] = r[0];
|
||||
if (!range[1] || r[1] > range[1]) range[1] = r[1];
|
||||
return range;
|
||||
},
|
||||
[null, null]
|
||||
);
|
||||
if (availableRange[0] > maxRoll || availableRange[1] < minRoll) {
|
||||
ui.notifications.warn('No results can possibly be drawn from this table and formula.');
|
||||
return { roll, results };
|
||||
}
|
||||
|
||||
// Continue rolling until one or more results are recovered
|
||||
let iter = 0;
|
||||
while (!results.length) {
|
||||
if (iter >= 10000) {
|
||||
ui.notifications.error(
|
||||
`Failed to draw an available entry from Table ${this.name}, maximum iteration reached`
|
||||
);
|
||||
break;
|
||||
}
|
||||
roll = await roll.reroll();
|
||||
results = this.getResultsForRoll(roll.total);
|
||||
iter++;
|
||||
}
|
||||
|
||||
// Draw results recursively from any inner Roll Tables
|
||||
if (recursive) {
|
||||
const inner = [];
|
||||
for (const result of results) {
|
||||
const { type, documentUuid } = result;
|
||||
const documentName = foundry.utils.parseUuid(documentUuid)?.type;
|
||||
if (type === 'document' && documentName === 'RollTable') {
|
||||
const innerTable = await fromUuid(documentUuid);
|
||||
if (innerTable) {
|
||||
const innerRoll = await innerTable.roll({ _depth: _depth + 1 });
|
||||
inner.push(...innerRoll.results);
|
||||
}
|
||||
} else inner.push(result);
|
||||
}
|
||||
results = inner;
|
||||
}
|
||||
|
||||
// Return the Roll and the results
|
||||
return { roll, results };
|
||||
}
|
||||
|
||||
async toMessage(results, { roll, messageData = {}, messageOptions = {} } = {}) {
|
||||
messageOptions.rollMode ??= game.settings.get('core', 'rollMode');
|
||||
|
||||
// Construct chat data
|
||||
messageData = foundry.utils.mergeObject(
|
||||
{
|
||||
author: game.user.id,
|
||||
speaker: foundry.documents.ChatMessage.implementation.getSpeaker(),
|
||||
rolls: [],
|
||||
sound: roll ? CONFIG.sounds.dice : null,
|
||||
flags: { 'core.RollTable': this.id }
|
||||
},
|
||||
messageData
|
||||
);
|
||||
if (roll) messageData.rolls.push(roll);
|
||||
|
||||
// Render the chat card which combines the dice roll with the drawn results
|
||||
const detailsPromises = await Promise.allSettled(results.map(r => r.getHTML()));
|
||||
const flavorKey = `TABLE.DrawFlavor${results.length > 1 ? 'Plural' : ''}`;
|
||||
const flavor = game.i18n.format(flavorKey, {
|
||||
number: results.length,
|
||||
name: foundry.utils.escapeHTML(this.name)
|
||||
});
|
||||
messageData.content = await foundry.applications.handlebars.renderTemplate(CONFIG.RollTable.resultTemplate, {
|
||||
description: await TextEditor.implementation.enrichHTML(this.description, {
|
||||
documents: true,
|
||||
secrets: this.isOwner
|
||||
}),
|
||||
flavor: flavor,
|
||||
results: results.map((result, i) => {
|
||||
const r = result.toObject(false);
|
||||
r.details = detailsPromises[i].value ?? '';
|
||||
const useTableIcon =
|
||||
result.icon === CONFIG.RollTable.resultIcon && this.img !== this.constructor.DEFAULT_ICON;
|
||||
r.icon = useTableIcon ? this.img : result.icon;
|
||||
return r;
|
||||
}),
|
||||
rollHTML: this.displayRoll && roll ? await roll.render() : null,
|
||||
table: this
|
||||
});
|
||||
|
||||
// Create the chat message
|
||||
return foundry.documents.ChatMessage.implementation.create(messageData, messageOptions);
|
||||
}
|
||||
}
|
||||
|
|
@ -51,6 +51,27 @@ export default class DhScene extends Scene {
|
|||
}
|
||||
}
|
||||
|
||||
async _preUpdate(changes, options, user) {
|
||||
const allowed = await super._preUpdate(changes, options, user);
|
||||
if (allowed === false) return false;
|
||||
|
||||
if (changes.flags?.daggerheart) {
|
||||
if (this._source.flags.daggerheart) {
|
||||
const unregisterTriggerData = (this._source.flags.daggerheart.sceneEnvironments ?? []).reduce(
|
||||
(acc, env) => {
|
||||
if (!changes.flags.daggerheart.sceneEnvironments.includes(env)) acc.sceneEnvironments.push(env);
|
||||
|
||||
return acc;
|
||||
},
|
||||
{ ...this._source.flags.daggerheart, sceneEnvironments: [] }
|
||||
);
|
||||
game.system.registeredTriggers.unregisterSceneEnvironmentTriggers(unregisterTriggerData);
|
||||
}
|
||||
|
||||
game.system.registeredTriggers.registerSceneEnvironmentTriggers(changes.flags.daggerheart);
|
||||
}
|
||||
}
|
||||
|
||||
_onDelete(options, userId) {
|
||||
super._onDelete(options, userId);
|
||||
|
||||
|
|
|
|||
|
|
@ -536,4 +536,10 @@ export default class DHToken extends CONFIG.Token.documentClass {
|
|||
};
|
||||
}
|
||||
//#endregion
|
||||
|
||||
async _preDelete() {
|
||||
if (this.actor && !this.actor.prototypeToken?.actorLink) {
|
||||
game.system.registeredTriggers.unregisterItemTriggers(this.actor.items);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
104
module/documents/tokenManager.mjs
Normal file
104
module/documents/tokenManager.mjs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
/**
|
||||
* A singleton class that handles preview tokens.
|
||||
*/
|
||||
|
||||
export default class DhTokenManager {
|
||||
#activePreview;
|
||||
#actor;
|
||||
#resolve;
|
||||
|
||||
/**
|
||||
* Create a template preview, deactivating any existing ones.
|
||||
* @param {object} data
|
||||
*/
|
||||
async createPreview(actor, tokenData) {
|
||||
this.#actor = actor;
|
||||
const token = await canvas.tokens._createPreview(
|
||||
{
|
||||
...actor.prototypeToken,
|
||||
displayName: 50,
|
||||
...tokenData
|
||||
},
|
||||
{ renderSheet: false, actor }
|
||||
);
|
||||
|
||||
this.#activePreview = {
|
||||
document: token.document,
|
||||
object: token,
|
||||
origin: { x: token.document.x, y: token.document.y }
|
||||
};
|
||||
|
||||
this.#activePreview.events = {
|
||||
contextmenu: this.#cancelTemplate.bind(this),
|
||||
mousedown: this.#confirmTemplate.bind(this),
|
||||
mousemove: this.#onDragMouseMove.bind(this)
|
||||
};
|
||||
|
||||
canvas.stage.on('mousemove', this.#activePreview.events.mousemove);
|
||||
canvas.stage.on('mousedown', this.#activePreview.events.mousedown);
|
||||
canvas.app.view.addEventListener('contextmenu', this.#activePreview.events.contextmenu);
|
||||
}
|
||||
|
||||
/* Currently intended for using as a preview of where to create a token. (note the flag) */
|
||||
async createPreviewAsync(actor, tokenData = {}) {
|
||||
return new Promise(resolve => {
|
||||
this.#resolve = resolve;
|
||||
this.createPreview(actor, { ...tokenData, flags: { daggerheart: { createPlacement: true } } });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the movement of the token preview on mousedrag.
|
||||
* @param {mousemove Event} event
|
||||
*/
|
||||
#onDragMouseMove(event) {
|
||||
event.stopPropagation();
|
||||
const { moveTime, object } = this.#activePreview;
|
||||
const update = {};
|
||||
|
||||
const now = Date.now();
|
||||
if (now - (moveTime || 0) <= 16) return;
|
||||
this.#activePreview.moveTime = now;
|
||||
|
||||
let cursor = event.getLocalPosition(canvas.templates);
|
||||
|
||||
Object.assign(update, canvas.grid.getTopLeftPoint(cursor));
|
||||
|
||||
object.document.updateSource(update);
|
||||
object.renderFlags.set({ refresh: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the preview token on right-click.
|
||||
* @param {contextmenu Event} event
|
||||
*/
|
||||
#cancelTemplate(_event, resolved) {
|
||||
const { mousemove, mousedown, contextmenu } = this.#activePreview.events;
|
||||
this.#activePreview.object.destroy();
|
||||
|
||||
canvas.stage.off('mousemove', mousemove);
|
||||
canvas.stage.off('mousedown', mousedown);
|
||||
canvas.app.view.removeEventListener('contextmenu', contextmenu);
|
||||
if (this.#resolve && !resolved) this.#resolve(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a real Actor and token at the preview location and cancels the preview.
|
||||
* @param {click Event} event
|
||||
*/
|
||||
async #confirmTemplate(event) {
|
||||
event.stopPropagation();
|
||||
this.#cancelTemplate(event, true);
|
||||
|
||||
const actor = this.#actor.inCompendium
|
||||
? await game.system.api.documents.DhpActor.create(this.#actor.toObject())
|
||||
: this.#actor;
|
||||
const tokenData = await actor.getTokenDocument();
|
||||
const result = await canvas.scene.createEmbeddedDocuments('Token', [
|
||||
{ ...tokenData, x: this.#activePreview.document.x, y: this.#activePreview.document.y }
|
||||
]);
|
||||
|
||||
this.#activePreview = undefined;
|
||||
if (this.#resolve && result.length) this.#resolve(result[0]);
|
||||
}
|
||||
}
|
||||
|
|
@ -67,7 +67,7 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
|
|||
if (item) {
|
||||
const isAction = item instanceof game.system.api.models.actions.actionsTypes.base;
|
||||
const isEffect = item instanceof ActiveEffect;
|
||||
await this.enrichText(item, isAction || isEffect);
|
||||
await this.enrichText(item);
|
||||
|
||||
const type = isAction ? 'action' : isEffect ? 'effect' : item.type;
|
||||
html = await foundry.applications.handlebars.renderTemplate(
|
||||
|
|
@ -202,10 +202,20 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
|
|||
}
|
||||
}
|
||||
|
||||
async enrichText(item, flatStructure) {
|
||||
async enrichText(item) {
|
||||
const { TextEditor } = foundry.applications.ux;
|
||||
|
||||
if (item.system?.metadata?.hasDescription) {
|
||||
const enrichedValue =
|
||||
(await item.system?.getEnrichedDescription?.()) ??
|
||||
(await TextEditor.enrichHTML(item.system.description));
|
||||
foundry.utils.setProperty(item, 'system.enrichedDescription', enrichedValue);
|
||||
} else if (item.description) {
|
||||
const enrichedValue = await TextEditor.enrichHTML(item.description);
|
||||
foundry.utils.setProperty(item, 'enrichedDescription', enrichedValue);
|
||||
}
|
||||
|
||||
const enrichPaths = [
|
||||
{ path: flatStructure ? '' : 'system', name: 'description' },
|
||||
{ path: 'system', name: 'features' },
|
||||
{ path: 'system', name: 'actions' },
|
||||
{ path: 'system', name: 'customActions' }
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { abilities } from '../config/actorConfig.mjs';
|
|||
import { getCommandTarget, rollCommandToJSON } from '../helpers/utils.mjs';
|
||||
|
||||
export default function DhDualityRollEnricher(match, _options) {
|
||||
const roll = rollCommandToJSON(match[1], match[0]);
|
||||
const roll = rollCommandToJSON(match[0]);
|
||||
if (!roll) return match[0];
|
||||
|
||||
return getDualityMessage(roll.result, roll.flavor);
|
||||
|
|
@ -47,6 +47,7 @@ function getDualityMessage(roll, flavor) {
|
|||
${roll?.trait && abilities[roll.trait] ? `data-trait="${roll.trait}"` : ''}
|
||||
${roll?.advantage ? 'data-advantage="true"' : ''}
|
||||
${roll?.disadvantage ? 'data-disadvantage="true"' : ''}
|
||||
${roll?.grantResources ? 'data-grant-resources="true"' : ''}
|
||||
>
|
||||
${roll?.reaction ? '<i class="fa-solid fa-reply"></i>' : '<i class="fa-solid fa-circle-half-stroke"></i>'}
|
||||
${label}
|
||||
|
|
@ -63,7 +64,8 @@ export const renderDualityButton = async event => {
|
|||
traitValue = button.dataset.trait?.toLowerCase(),
|
||||
target = getCommandTarget({ allowNull: true }),
|
||||
difficulty = button.dataset.difficulty,
|
||||
advantage = button.dataset.advantage ? Number(button.dataset.advantage) : undefined;
|
||||
advantage = button.dataset.advantage ? Number(button.dataset.advantage) : undefined,
|
||||
grantResources = Boolean(button.dataset?.grantResources);
|
||||
|
||||
await enrichedDualityRoll(
|
||||
{
|
||||
|
|
@ -73,36 +75,45 @@ export const renderDualityButton = async event => {
|
|||
difficulty,
|
||||
title: button.dataset.title,
|
||||
label: button.dataset.label,
|
||||
advantage
|
||||
advantage,
|
||||
grantResources
|
||||
},
|
||||
event
|
||||
);
|
||||
};
|
||||
|
||||
export const enrichedDualityRoll = async (
|
||||
{ reaction, traitValue, target, difficulty, title, label, advantage },
|
||||
{ reaction, traitValue, target, difficulty, title, label, advantage, grantResources, customConfig },
|
||||
event
|
||||
) => {
|
||||
const config = {
|
||||
event: event ?? {},
|
||||
title: title,
|
||||
headerTitle: label,
|
||||
roll: {
|
||||
trait: traitValue && target ? traitValue : null,
|
||||
label: label,
|
||||
difficulty: difficulty,
|
||||
advantage,
|
||||
type: reaction ? 'reaction' : null
|
||||
},
|
||||
skips: {
|
||||
resources: !grantResources,
|
||||
triggers: !grantResources
|
||||
},
|
||||
type: 'trait',
|
||||
hasRoll: true
|
||||
hasRoll: true,
|
||||
...(customConfig ?? {})
|
||||
};
|
||||
|
||||
if (target) {
|
||||
await target.diceRoll(config);
|
||||
const result = await target.diceRoll(config);
|
||||
if (!result) return;
|
||||
result.resourceUpdates.updateResources();
|
||||
} else {
|
||||
// For no target, call DualityRoll directly with basic data
|
||||
config.data = { experiences: {}, traits: {} };
|
||||
config.data = { experiences: {}, traits: {}, rules: {} };
|
||||
config.source = { actor: null };
|
||||
await CONFIG.Dice.daggerheart.DualityRoll.build(config);
|
||||
}
|
||||
return config;
|
||||
};
|
||||
|
|
|
|||
80
module/enrichers/FateRollEnricher.mjs
Normal file
80
module/enrichers/FateRollEnricher.mjs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { getCommandTarget, rollCommandToJSON } from '../helpers/utils.mjs';
|
||||
|
||||
export default function DhFateRollEnricher(match, _options) {
|
||||
const roll = rollCommandToJSON(match[0]);
|
||||
if (!roll) return match[0];
|
||||
|
||||
return getFateMessage(roll.result, roll?.flavor);
|
||||
}
|
||||
|
||||
export function getFateTypeData(fateTypeValue) {
|
||||
const value = fateTypeValue ? fateTypeValue.capitalize() : 'Hope';
|
||||
const lowercased = fateTypeValue?.toLowerCase?.() ?? 'hope';
|
||||
switch (lowercased) {
|
||||
case 'hope':
|
||||
case 'fear':
|
||||
return { value, label: game.i18n.localize(`DAGGERHEART.GENERAL.${lowercased}`) };
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getFateMessage(roll, flavor) {
|
||||
const fateTypeData = getFateTypeData(roll?.type);
|
||||
|
||||
if (!fateTypeData)
|
||||
return ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateTypeParsing'));
|
||||
|
||||
const { value: fateType, label: fateTypeLabel } = fateTypeData;
|
||||
const title = flavor ?? game.i18n.localize('DAGGERHEART.GENERAL.fateRoll');
|
||||
|
||||
const fateElement = document.createElement('span');
|
||||
fateElement.innerHTML = `
|
||||
<button type="button" class="fate-roll-button${roll?.inline ? ' inline' : ''}"
|
||||
data-title="${title}"
|
||||
data-label="${fateTypeLabel}"
|
||||
data-fateType="${fateType}"
|
||||
>
|
||||
${title}
|
||||
</button>
|
||||
`;
|
||||
|
||||
return fateElement;
|
||||
}
|
||||
|
||||
export const renderFateButton = async event => {
|
||||
const button = event.currentTarget,
|
||||
target = getCommandTarget({ allowNull: true });
|
||||
|
||||
const fateTypeData = getFateTypeData(button.dataset?.fatetype);
|
||||
|
||||
if (!fateTypeData) ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateTypeParsing'));
|
||||
const { value: fateType, label: fateTypeLabel } = fateTypeData;
|
||||
|
||||
await enrichedFateRoll(
|
||||
{
|
||||
target,
|
||||
title: button.dataset.title,
|
||||
label: button.dataset.label,
|
||||
fateType: fateType
|
||||
},
|
||||
event
|
||||
);
|
||||
};
|
||||
|
||||
export const enrichedFateRoll = async ({ target, title, label, fateType }, event) => {
|
||||
const config = {
|
||||
event: event ?? {},
|
||||
title: title,
|
||||
headerTitle: label,
|
||||
roll: {},
|
||||
hasRoll: true,
|
||||
fateType: fateType,
|
||||
skips: { reaction: true }
|
||||
};
|
||||
|
||||
config.data = { experiences: {}, traits: {}, fateType: fateType };
|
||||
config.source = { actor: target?.uuid };
|
||||
await CONFIG.Dice.daggerheart.FateRoll.build(config);
|
||||
return config;
|
||||
};
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
import { default as DhDamageEnricher, renderDamageButton } from './DamageEnricher.mjs';
|
||||
import { default as DhDualityRollEnricher, renderDualityButton } from './DualityRollEnricher.mjs';
|
||||
import { default as DhFateRollEnricher, renderFateButton } from './FateRollEnricher.mjs';
|
||||
import { default as DhEffectEnricher } from './EffectEnricher.mjs';
|
||||
import { default as DhTemplateEnricher, renderMeasuredTemplate } from './TemplateEnricher.mjs';
|
||||
import { default as DhLookupEnricher } from './LookupEnricher.mjs';
|
||||
|
||||
export { DhDamageEnricher, DhDualityRollEnricher, DhEffectEnricher, DhTemplateEnricher };
|
||||
export { DhDamageEnricher, DhDualityRollEnricher, DhEffectEnricher, DhTemplateEnricher, DhFateRollEnricher };
|
||||
|
||||
export const enricherConfig = [
|
||||
{
|
||||
|
|
@ -15,6 +16,10 @@ export const enricherConfig = [
|
|||
pattern: /\[\[\/dr\s?(.*?)\]\]({[^}]*})?/g,
|
||||
enricher: DhDualityRollEnricher
|
||||
},
|
||||
{
|
||||
pattern: /\[\[\/fr\s?(.*?)\]\]({[^}]*})?/g,
|
||||
enricher: DhFateRollEnricher
|
||||
},
|
||||
{
|
||||
pattern: /@Effect\[([^\[\]]*)\]({[^}]*})?/g,
|
||||
enricher: DhEffectEnricher
|
||||
|
|
@ -38,6 +43,10 @@ export const enricherRenderSetup = element => {
|
|||
.querySelectorAll('.duality-roll-button')
|
||||
.forEach(element => element.addEventListener('click', renderDualityButton));
|
||||
|
||||
element
|
||||
.querySelectorAll('.fate-roll-button')
|
||||
.forEach(element => element.addEventListener('click', renderFateButton));
|
||||
|
||||
element
|
||||
.querySelectorAll('.measured-template-button')
|
||||
.forEach(element => element.addEventListener('click', renderMeasuredTemplate));
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import { diceTypes, getDiceSoNicePresets, range } from '../config/generalConfig.mjs';
|
||||
import { diceTypes, getDiceSoNicePresets, getDiceSoNicePreset, range } from '../config/generalConfig.mjs';
|
||||
import Tagify from '@yaireo/tagify';
|
||||
|
||||
export const capitalize = string => {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
};
|
||||
|
||||
export function rollCommandToJSON(text, raw) {
|
||||
export function rollCommandToJSON(text) {
|
||||
if (!text) return {};
|
||||
|
||||
const flavorMatch = raw?.match(/{(.*)}$/);
|
||||
const flavorMatch = text?.match(/{(.*)}$/);
|
||||
const flavor = flavorMatch ? flavorMatch[1] : null;
|
||||
|
||||
// Match key="quoted string" OR key=unquotedValue
|
||||
|
|
@ -31,7 +31,7 @@ export function rollCommandToJSON(text, raw) {
|
|||
}
|
||||
result[key] = value;
|
||||
}
|
||||
return Object.keys(result).length > 0 ? { result, flavor } : null;
|
||||
return { result, flavor };
|
||||
}
|
||||
|
||||
export const getCommandTarget = (options = {}) => {
|
||||
|
|
@ -69,6 +69,20 @@ export const setDiceSoNiceForDualityRoll = async (rollResult, advantageState, ho
|
|||
}
|
||||
};
|
||||
|
||||
export const setDiceSoNiceForHopeFateRoll = async (rollResult, hopeFaces) => {
|
||||
if (!game.modules.get('dice-so-nice')?.active) return;
|
||||
const { diceSoNice } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance);
|
||||
const diceSoNicePresets = await getDiceSoNicePreset(diceSoNice.hope, hopeFaces);
|
||||
rollResult.dice[0].options = diceSoNicePresets;
|
||||
};
|
||||
|
||||
export const setDiceSoNiceForFearFateRoll = async (rollResult, fearFaces) => {
|
||||
if (!game.modules.get('dice-so-nice')?.active) return;
|
||||
const { diceSoNice } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance);
|
||||
const diceSoNicePresets = await getDiceSoNicePreset(diceSoNice.fear, fearFaces);
|
||||
rollResult.dice[0].options = diceSoNicePresets;
|
||||
};
|
||||
|
||||
export const chunkify = (array, chunkSize, mappingFunc) => {
|
||||
var chunkifiedArray = [];
|
||||
for (let i = 0; i < array.length; i += chunkSize) {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ export const preloadHandlebarsTemplates = async function () {
|
|||
'systems/daggerheart/templates/actionTypes/effect.hbs',
|
||||
'systems/daggerheart/templates/actionTypes/beastform.hbs',
|
||||
'systems/daggerheart/templates/actionTypes/countdown.hbs',
|
||||
'systems/daggerheart/templates/actionTypes/summon.hbs',
|
||||
'systems/daggerheart/templates/settings/components/settings-item-line.hbs',
|
||||
'systems/daggerheart/templates/ui/tooltip/parts/tooltipChips.hbs',
|
||||
'systems/daggerheart/templates/ui/tooltip/parts/tooltipTags.hbs',
|
||||
|
|
|
|||
|
|
@ -210,6 +210,42 @@ export async function runMigrations() {
|
|||
|
||||
lastMigrationVersion = '1.2.7';
|
||||
}
|
||||
|
||||
if (foundry.utils.isNewerVersion('1.5.5', lastMigrationVersion)) {
|
||||
/* Clear out Environments that were added directly from compendium */
|
||||
for (const scene of game.scenes) {
|
||||
if (!scene.flags.daggerheart) continue;
|
||||
const systemData = new game.system.api.data.scenes.DHScene(scene.flags.daggerheart);
|
||||
const sceneEnvironments = systemData.sceneEnvironments;
|
||||
|
||||
const newEnvironments = sceneEnvironments.filter(x => !x?.pack);
|
||||
if (newEnvironments.length !== sceneEnvironments.length)
|
||||
await scene.update({ 'flags.daggerheart.sceneEnvironments': newEnvironments });
|
||||
}
|
||||
|
||||
ui.nav.render(true);
|
||||
|
||||
lastMigrationVersion = '1.5.5';
|
||||
}
|
||||
|
||||
if (foundry.utils.isNewerVersion('1.6.0', lastMigrationVersion)) {
|
||||
/* Delevel any companions that are higher level than their partner character */
|
||||
for (const companion of game.actors.filter(x => x.type === 'companion')) {
|
||||
if (companion.system.levelData.level.current <= 1) continue;
|
||||
|
||||
if (!companion.system.partner) {
|
||||
await companion.updateLevel(1);
|
||||
} else {
|
||||
const endLevel = companion.system.partner.system.levelData.level.current;
|
||||
if (endLevel < companion.system.levelData.level.current) {
|
||||
companion.system.levelData.level.changed = companion.system.levelData.level.current;
|
||||
await companion.updateLevel(endLevel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lastMigrationVersion = '1.6.0';
|
||||
}
|
||||
//#endregion
|
||||
|
||||
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LastMigrationVersion, lastMigrationVersion);
|
||||
|
|
|
|||
|
|
@ -93,10 +93,6 @@ export const registerSocketHooks = () => {
|
|||
}
|
||||
}
|
||||
});
|
||||
Hooks.on(socketEvent.RefreshDocument, async data => {
|
||||
const document = await foundry.utils.fromUuid(data.uuid);
|
||||
document.sheet.render();
|
||||
});
|
||||
};
|
||||
|
||||
export const registerUserQueries = () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue