daggerheart/module/applications/ui/chatLog.mjs
Chris Ryan 9d75157e17
[Feature] Death moves and Fate rolls (#1463)
* Update the death move descriptions

* Renamed to DhDeathMove

* Partial Fate Roll creation and Fate Roll Enricher (/fr)

* Hide stuff not required for fate roll

* Hide formula display; code removal; start to add Fear die as a choice for Fate roll

* Fix chat message display; start moving towards supporting Hope and Fear for Fate roll

* /fr now supports type=X, where X is Hope or Fear, if not supplied, defaults to Hope

* Fixed DSN rolling; removed console messages; chat message clean up

* Add localisation entry

* Trying to sort out the button for the fate roll

* Style the fate message based on Hope/Fear colors.

* Partial improvement on the fate template buttons - chat display is correct, but the roll dialog is wrong

* Fixed enricher button; localization fixes; debug cleanup

* Error checking for the fate type parsing in all potential problem locations

* Added localization for the fate type parsing error

* Start on Avoid Death death move

* debug stuff

* More death moves setup/testing

* Avoid fate scars update in place, with scars migrating to an integer value.

* Remove some debug code; add Blaze Of Glory shell

* Start on Guaranteed Critical for Blaze of Glory

* Partial implementation of Blaze of Glory

* Dice/critical checks/tests

* Moved detection of guaranteed critical to before the roll dialog is created, so it can be skipped; removed debug code

* Remove debug

* Update Blaze of Glory effect description

* Risk It All - critical roll - clear all stress and HP

* Auto remove all marked stress and HP for Risk It All, if Hope value rolled covers it.

* Display the Death Move description in chat expanded if the appropriate config setting is on

* Made the Blaze of Glory ActiveEffect image use configured version

* Update the current Hope value if the scar value change affects it

* Scars management in the Character details editor

* Separate less file for the Death Moves instead of reusing Downtime

* Added result messages to the Death Move chat output and removed debug statements

* Some localization, style and smaller changes

* Fixed RiskItAll resource handling method

* Risk It All success chat message start

* [Add] Hope/Scar Interplay (#1531)

* Migrated character.maxHope to homebrew settings

* Added a visual for scars

* .

* .

* Pass the hope value in the button data; skeleton risk it all dialog to fill out.

* Start on risk it dialog

* More dialog stuff

* Remove non-existent field

* Dialog templating and logic

* .

* Ensure effect is Applied to Actor (#1547)

Co-authored-by: Chris Ryan <chrisr@blackhole>

* [Fix] 1548 - Standalone Item Add Actions (#1549)

* Fixed so that items not on an actor don't error out on creating actions

* Fixed deletion of items error

* Raised version

* Fix the sliders to do the correct maximums

* Pass the actor id through the button; fix /dr and /fr flavor text

* Remove debug message

---------

Co-authored-by: Chris Ryan <chrisr@blackhole>
Co-authored-by: WBHarry <williambjrklund@gmail.com>
Co-authored-by: WBHarry <89362246+WBHarry@users.noreply.github.com>
2026-01-17 15:11:50 +01:00

397 lines
16 KiB
JavaScript

import { abilities } from '../../config/actorConfig.mjs';
import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLog {
constructor(options) {
super(options);
this.targetTemplate = {
activeLayer: undefined,
document: undefined,
object: undefined,
minimizedSheets: [],
config: undefined,
targets: undefined
};
this.setupHooks();
}
/** @inheritDoc */
static DEFAULT_OPTIONS = {
classes: ['daggerheart']
};
_getEntryContextOptions() {
return [
...super._getEntryContextOptions(),
// {
// name: 'Reroll',
// icon: '<i class="fa-solid fa-dice"></i>',
// condition: li => {
// const message = game.messages.get(li.dataset.messageId);
// return (game.user.isGM || message.isAuthor) && message.rolls.length > 0;
// },
// callback: li => {
// const message = game.messages.get(li.dataset.messageId);
// new game.system.api.applications.dialogs.RerollDialog(message).render({ force: true });
// }
// },
{
name: game.i18n.localize('DAGGERHEART.UI.ChatLog.rerollDamage'),
icon: '<i class="fa-solid fa-dice"></i>',
condition: li => {
const message = game.messages.get(li.dataset.messageId);
const hasRolledDamage = message.system.hasDamage
? Object.keys(message.system.damage).length > 0
: false;
return (game.user.isGM || message.isAuthor) && hasRolledDamage;
},
callback: li => {
const message = game.messages.get(li.dataset.messageId);
new game.system.api.applications.dialogs.RerollDamageDialog(message).render({ force: true });
}
}
];
}
addChatListeners = async (document, html, data) => {
const message = data?.message ?? document.toObject(false);
html.querySelectorAll('.simple-roll-button').forEach(element =>
element.addEventListener('click', event => this.onRollSimple(event, message))
);
html.querySelectorAll('.ability-use-button').forEach(element =>
element.addEventListener('click', event => this.abilityUseButton(event, message))
);
html.querySelectorAll('.action-use-button').forEach(element =>
element.addEventListener('click', event => this.actionUseButton(event, message))
);
html.querySelectorAll('.reroll-button').forEach(element =>
element.addEventListener('click', event => this.rerollEvent(event, message))
);
html.querySelectorAll('.group-roll-button').forEach(element =>
element.addEventListener('click', event => this.groupRollButton(event, message))
);
html.querySelectorAll('.group-roll-reroll').forEach(element =>
element.addEventListener('click', event => this.groupRollReroll(event, message))
);
html.querySelectorAll('.group-roll-success').forEach(element =>
element.addEventListener('click', event => this.groupRollSuccessEvent(event, message))
);
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() {
Hooks.on('renderChatMessageHTML', this.addChatListeners.bind());
}
close(options) {
Hooks.off('renderChatMessageHTML', this.addChatListeners);
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),
damages = {
hitPoints: {
parts: [
{
applyTo: 'hitPoints',
damageTypes: [],
total
}
]
}
},
targets = Array.from(game.user.targets);
if (targets.length === 0)
return ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelected'));
targets.forEach(target => {
if (buttonType === 'healing') target.actor.takeHealing(damages);
else target.actor.takeDamage(damages);
});
}
async abilityUseButton(event, message) {
event.stopPropagation();
const item = await foundry.utils.fromUuid(message.system.origin);
const action =
item.system.attack?.id === event.currentTarget.id
? item.system.attack
: item.system.actions.get(event.currentTarget.id);
if (event.currentTarget.dataset.directDamage) {
const config = action.prepareConfig(event);
config.hasRoll = false;
action.workflow.get('damage').execute(config, null, true);
} else action.use(event);
}
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 actionType = message.system.moves[moveIndex].actions[actionIndex];
const cls = game.system.api.models.actions.actionsTypes[actionType.type];
const action = new cls(
{
...actionType,
_id: foundry.utils.randomID(),
name: game.i18n.localize(actionType.name),
originItem: {
type: CONFIG.DH.ITEM.originItemType.restMove,
itemPath: movePath,
actionIndex: actionIndex
},
targetUuid: targetUuid
},
{ parent: parent.system }
);
action.use(event);
}
async rerollEvent(event, message) {
event.stopPropagation();
if (!event.shiftKey) {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.localize('DAGGERHEART.UI.Chat.reroll.confirmTitle')
},
content: game.i18n.localize('DAGGERHEART.UI.Chat.reroll.confirmText')
});
if (!confirmed) return;
}
const target = event.target.closest('[data-die-index]');
if (target.dataset.type === 'damage') {
game.system.api.dice.DamageRoll.reroll(target, message);
} else {
let originalRoll_parsed = message.rolls.map(roll => JSON.parse(roll))[0];
const rollClass =
game.system.api.dice[
message.type === 'dualityRoll'
? 'DualityRoll'
: target.dataset.type === 'damage'
? 'DHRoll'
: 'D20Roll'
];
if (!game.modules.get('dice-so-nice')?.active) foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice });
const { newRoll, parsedRoll } = await rollClass.reroll(originalRoll_parsed, target, message);
await game.messages.get(message._id).update({
'system.roll': newRoll,
'rolls': [parsedRoll]
});
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: {
refreshType: RefreshType.TagTeamRoll
}
});
}
}
async groupRollButton(event, message) {
const path = event.currentTarget.dataset.path;
const isLeader = path === 'leader';
const { actor: actorData, trait } = foundry.utils.getProperty(message.system, path);
const actor = game.actors.get(actorData._id);
if (!actor) {
return ui.notifications.error(
game.i18n.format('DAGGERHEART.UI.Notifications.documentIsMissing', {
documentType: game.i18n.localize('TYPES.Actor.character')
})
);
}
if (!actor.testUserPermission(game.user, 'OWNER')) {
return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.noActorOwnership'));
}
const traitLabel = game.i18n.localize(abilities[trait].label);
const config = {
event: event,
title: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${actor.name}`,
headerTitle: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: traitLabel
}),
roll: {
trait: trait,
advantage: 0,
modifiers: [{ label: traitLabel, value: actor.system.traits[trait].value }]
},
hasRoll: true,
skips: {
createMessage: true,
resources: !isLeader,
updateCountdowns: !isLeader
}
};
const result = await actor.diceRoll({
...config,
headerTitle: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${actor.name}`,
title: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: traitLabel
})
});
if (!result) return;
const newMessageData = foundry.utils.deepClone(message.system);
foundry.utils.setProperty(newMessageData, `${path}.result`, result.roll);
const renderData = { system: new game.system.api.models.chatMessages.config.groupRoll(newMessageData) };
const updatedContent = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/groupRoll.hbs',
{ ...renderData, user: game.user }
);
const mess = game.messages.get(message._id);
await emitAsGM(
GMUpdateEvent.UpdateDocument,
mess.update.bind(mess),
{
...renderData,
content: updatedContent
},
mess.uuid
);
}
async groupRollReroll(event, message) {
const path = event.currentTarget.dataset.path;
const { actor: actorData, trait } = foundry.utils.getProperty(message.system, path);
const actor = game.actors.get(actorData._id);
if (!actor.testUserPermission(game.user, 'OWNER')) {
return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.noActorOwnership'));
}
const traitLabel = game.i18n.localize(abilities[trait].label);
const config = {
event: event,
title: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${actor.name}`,
headerTitle: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: traitLabel
}),
roll: {
trait: trait,
advantage: 0,
modifiers: [{ label: traitLabel, value: actor.system.traits[trait].value }]
},
hasRoll: true,
skips: {
createMessage: true,
updateCountdowns: true
}
};
const result = await actor.diceRoll({
...config,
headerTitle: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${actor.name}`,
title: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: traitLabel
})
});
const newMessageData = foundry.utils.deepClone(message.system);
foundry.utils.setProperty(newMessageData, `${path}.result`, { ...result.roll, rerolled: true });
const renderData = { system: new game.system.api.models.chatMessages.config.groupRoll(newMessageData) };
const updatedContent = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/groupRoll.hbs',
{ ...renderData, user: game.user }
);
const mess = game.messages.get(message._id);
await emitAsGM(
GMUpdateEvent.UpdateDocument,
mess.update.bind(mess),
{
...renderData,
content: updatedContent
},
mess.uuid
);
}
async groupRollSuccessEvent(event, message) {
if (!game.user.isGM) {
return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.gmOnly'));
}
const { path, success } = event.currentTarget.dataset;
const { actor: actorData } = foundry.utils.getProperty(message.system, path);
const actor = game.actors.get(actorData._id);
if (!actor.testUserPermission(game.user, 'OWNER')) {
return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.noActorOwnership'));
}
const newMessageData = foundry.utils.deepClone(message.system);
foundry.utils.setProperty(newMessageData, `${path}.manualSuccess`, Boolean(success));
const renderData = { system: new game.system.api.models.chatMessages.config.groupRoll(newMessageData) };
const updatedContent = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/groupRoll.hbs',
{ ...renderData, user: game.user }
);
const mess = game.messages.get(message._id);
await emitAsGM(
GMUpdateEvent.UpdateDocument,
mess.update.bind(mess),
{
...renderData,
content: updatedContent
},
mess.uuid
);
}
async groupRollExpandSection(event) {
event.target
.closest('.group-roll-header-expand-section')
.querySelectorAll('i')
.forEach(element => {
element.classList.toggle('fa-angle-up');
element.classList.toggle('fa-angle-down');
});
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 });
}
}