Merge branch 'development' into feature-rollTableSheet

This commit is contained in:
WBHarry 2026-01-17 17:41:40 +01:00
commit 48b76e2961
42 changed files with 1166 additions and 260 deletions

View file

@ -14,3 +14,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';

View file

@ -123,7 +123,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;
}

View file

@ -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,107 @@ 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;
if (config.roll.fate.value <= this.actor.system.levelData.level.current) {
// apply scarring - for now directly apply - later add a button.
const newScarAmount = this.actor.system.scars + 1;
await this.actor.update({
system: {
scars: newScarAmount
}
});
if (newScarAmount >= this.actor.system.resources.hope.max) {
return game.i18n.format('DAGGERHEART.UI.Chat.deathMove.journeysEnd', { scars: newScarAmount });
}
return game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.gainScar');
}
return game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.avoidScar');
}
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,
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) {
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'
}
]
}
]);
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 +152,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 +204,5 @@ export default class DhpDeathMove extends HandlebarsApplicationMixin(Application
};
cls.create(msg);
this.close();
}
}

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

View file

@ -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';
@ -696,7 +696,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
* @type {ApplicationClickAction}
*/
static async #makeDeathMove() {
await new DhpDeathMove(this.document).render({ force: true });
await new DhDeathMove(this.document).render({ force: true });
}
/**
@ -753,9 +753,8 @@ export default class CharacterSheet extends DHBaseActorSheet {
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();
}

View file

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