Merged with v14-Dev

This commit is contained in:
WBHarry 2026-03-31 17:33:18 +02:00
commit 8d84b8da48
70 changed files with 1076 additions and 936 deletions

View file

@ -10,6 +10,12 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
this.selected = null;
this.evolved = { form: null };
this.hybrid = { forms: {}, advantages: {}, features: {} };
this.modifications = {
traitBonuses: configData.modifications.traitBonuses.map(x => ({
trait: null,
bonus: x.bonus
}))
};
this._dragDrop = this._createDragDropHandlers();
}
@ -28,6 +34,7 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
selectBeastform: this.selectBeastform,
toggleHybridFeature: this.toggleHybridFeature,
toggleHybridAdvantage: this.toggleHybridAdvantage,
toggleTraitBonus: this.toggleTraitBonus,
submitBeastform: this.submitBeastform
},
form: {
@ -48,6 +55,7 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
tabs: { template: 'systems/daggerheart/templates/dialogs/beastform/tabs.hbs' },
beastformTier: { template: 'systems/daggerheart/templates/dialogs/beastform/beastformTier.hbs' },
advanced: { template: 'systems/daggerheart/templates/dialogs/beastform/advanced.hbs' },
modifications: { template: 'systems/daggerheart/templates/dialogs/beastform/modifications.hbs' },
footer: { template: 'systems/daggerheart/templates/dialogs/beastform/footer.hbs' }
};
@ -146,6 +154,9 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
{}
);
context.modifications = this.modifications;
context.traits = CONFIG.DH.ACTOR.abilities;
context.tier = beastformTiers[this.tabGroups.primary];
context.tierKey = this.tabGroups.primary;
@ -155,6 +166,9 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
}
canSubmit() {
const modificationsFinished = this.modifications.traitBonuses.every(x => x.trait);
if (!modificationsFinished) return false;
if (this.selected) {
switch (this.selected.system.beastformType) {
case 'normal':
@ -261,6 +275,13 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
this.render();
}
static toggleTraitBonus(_, button) {
const { index, trait } = button.dataset;
this.modifications.traitBonuses[index].trait =
this.modifications.traitBonuses[index].trait === trait ? null : trait;
this.render();
}
static async submitBeastform() {
await this.close({ submitted: true });
}
@ -292,6 +313,23 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
}
}
const beastformEffect = selected.effects.find(x => x.type === 'beastform');
for (const traitBonus of app.modifications.traitBonuses) {
const existingChange = beastformEffect.changes.find(
x => x.key === `system.traits.${traitBonus.trait}.value`
);
if (existingChange) {
existingChange.value = Number.parseInt(existingChange.value) + traitBonus.bonus;
} else {
beastformEffect.changes.push({
key: `system.traits.${traitBonus.trait}.value`,
mode: 2,
priority: null,
value: traitBonus.bonus
});
}
}
resolve({
selected: selected,
evolved: { ...app.evolved, form: evolved },

View file

@ -200,6 +200,7 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
partContext.members[partId] = {
...data,
roll: data.roll,
isEditable: actor.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER),
key: partId,
readyToRoll: Boolean(data.rollChoice),
@ -448,23 +449,19 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
const { member, diceType } = button.dataset;
const memberData = this.party.system.tagTeam.members[member];
const dieIndex = diceType === 'hope' ? 0 : diceType === 'fear' ? 2 : 4;
const { parsedRoll, newRoll } = await game.system.api.dice.DualityRoll.reroll(
memberData.rollData,
dieIndex,
diceType
);
const rollData = parsedRoll.toJSON();
const dieIndex = diceType === 'hope' ? 0 : diceType === 'fear' ? 1 : 2;
const newRoll = game.system.api.dice.DualityRoll.fromData(memberData.rollData);
const dice = newRoll.dice[dieIndex];
await dice.reroll(`/r1=${dice.total}`, {
liveRoll: {
roll: newRoll,
isReaction: true
}
});
const rollData = newRoll.toJSON();
this.updatePartyData(
{
[`system.tagTeam.members.${member}.rollData`]: {
...rollData,
options: {
...rollData.options,
roll: newRoll
}
}
[`system.tagTeam.members.${member}.rollData`]: rollData
},
this.getUpdatingParts(button)
);
@ -699,7 +696,9 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
const error = this.checkInitiatorHopeError(this.party.system.tagTeam.initiator);
if (error) return error;
const mainRoll = (await this.getJoinedRoll()).rollData;
const joinedRoll = await this.getJoinedRoll();
const mainRoll = joinedRoll.rollData;
const finalRoll = foundry.utils.deepClone(joinedRoll.roll);
const mainActor = this.party.system.partyMembers.find(x => x.uuid === mainRoll.options.source.actor);
mainRoll.options.title = game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.chatMessageRollTitle');
@ -710,7 +709,7 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
title: game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.title'),
speaker: cls.getSpeaker({ actor: mainActor }),
system: mainRoll.options,
rolls: [mainRoll],
rolls: [JSON.stringify(joinedRoll.roll)],
sound: null,
flags: { core: { RollTable: true } }
};
@ -722,7 +721,7 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
const fearUpdate = { key: 'fear', value: null, total: null, enabled: true };
for (let memberId in tagTeamData.members) {
const resourceUpdates = [];
const rollGivesHope = mainRoll.options.roll.isCritical || mainRoll.options.roll.result.duality === 1;
const rollGivesHope = finalRoll.isCritical || finalRoll.withHope;
if (memberId === tagTeamData.initiator.memberId) {
const value = tagTeamData.initiator.cost
? rollGivesHope
@ -733,9 +732,8 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
} else if (rollGivesHope) {
resourceUpdates.push({ key: 'hope', value: 1, total: -1, enabled: true });
}
if (mainRoll.options.roll.isCritical)
resourceUpdates.push({ key: 'stress', value: -1, total: 1, enabled: true });
if (mainRoll.options.roll.result.duality === -1) {
if (finalRoll.isCritical) resourceUpdates.push({ key: 'stress', value: -1, total: 1, enabled: true });
if (finalRoll.withFear) {
fearUpdate.value = fearUpdate.value === null ? 1 : fearUpdate.value + 1;
fearUpdate.total = fearUpdate.total === null ? -1 : fearUpdate.total - 1;
}

View file

@ -36,7 +36,9 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
editDoc: this.editDoc,
addTrigger: this.addTrigger,
removeTrigger: this.removeTrigger,
expandTrigger: this.expandTrigger
expandTrigger: this.expandTrigger,
addBeastformTraitBonus: this.addBeastformTraitBonus,
removeBeastformTraitBonus: this.removeBeastformTraitBonus
},
form: {
handler: this.updateForm,
@ -412,6 +414,21 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
}
}
static async addBeastformTraitBonus() {
const data = this.action.toObject();
data.beastform.modifications.traitBonuses = [
...data.beastform.modifications.traitBonuses,
this.action.schema.fields.beastform.fields.modifications.fields.traitBonuses.element.getInitialValue()
];
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
static async removeBeastformTraitBonus(_event, button) {
const data = this.action.toObject();
data.beastform.modifications.traitBonuses.splice(button.dataset.index, 1);
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
updateSummonCount(event) {
event.stopPropagation();
const wrapper = event.target.closest('.summon-count-wrapper');

View file

@ -79,8 +79,6 @@ export default function DHApplicationMixin(Base) {
*/
constructor(options = {}) {
super(options);
this._setupDragDrop();
}
/**
@ -175,9 +173,6 @@ export default function DHApplicationMixin(Base) {
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
/* Core dragDrop from ActorDocument is always only 1. Possible we could refactor our own */
if (Array.isArray(this._dragDrop)) this._dragDrop.forEach(d => d.bind(htmlElement));
// Handle delta inputs
for (const deltaInput of htmlElement.querySelectorAll('input[data-allow-delta]')) {
deltaInput.dataset.numValue = deltaInput.value;
@ -289,6 +284,16 @@ export default function DHApplicationMixin(Base) {
async _onRender(context, options) {
await super._onRender(context, options);
this._createTagifyElements(this.options.tagifyConfigs);
for (const d of this.options.dragDrop) {
new foundry.applications.ux.DragDrop.implementation({
...d,
callbacks: {
dragstart: this._onDragStart.bind(this),
drop: this._onDrop.bind(this)
}
}).bind(this.element);
}
}
/* -------------------------------------------- */
@ -349,26 +354,6 @@ export default function DHApplicationMixin(Base) {
/* Drag and Drop */
/* -------------------------------------------- */
/**
* Creates drag-drop handlers from the configured options.
* @returns {foundry.applications.ux.DragDrop[]}
* @private
*/
_setupDragDrop() {
if (this._dragDrop) {
this._dragDrop.callbacks.dragStart = this._onDragStart;
this._dragDrop.callback.drop = this._onDrop;
} else {
this._dragDrop = this.options.dragDrop.map(d => {
d.callbacks = {
dragstart: this._onDragStart.bind(this),
drop: this._onDrop.bind(this)
};
return new foundry.applications.ux.DragDrop.implementation(d);
});
}
}
/**
* Handle dragStart event.
* @param {DragEvent} event

View file

@ -228,7 +228,6 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
'systems/daggerheart/templates/ui/chat/action.hbs',
systemData
),
title: game.i18n.localize('DAGGERHEART.ACTIONS.Config.displayInChat'),
speaker: cls.getSpeaker(),
flags: {
daggerheart: {

View file

@ -31,7 +31,8 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract
},
actions: {
selectRefreshable: DaggerheartMenu.#selectRefreshable,
refreshActors: DaggerheartMenu.#refreshActors
refreshActors: DaggerheartMenu.#refreshActors,
createFallCollisionDamage: DaggerheartMenu.#createFallCollisionDamage
}
};
@ -50,6 +51,7 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract
const context = await super._prepareContext(options);
context.refreshables = this.refreshSelections;
context.disableRefresh = Object.values(this.refreshSelections).every(x => !x.selected);
context.fallAndCollision = CONFIG.DH.GENERAL.fallAndCollisionDamage;
return context;
}
@ -71,4 +73,22 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract
this.refreshSelections = DaggerheartMenu.defaultRefreshSelections();
this.render();
}
static async #createFallCollisionDamage(_event, button) {
const data = CONFIG.DH.GENERAL.fallAndCollisionDamage[button.dataset.key];
const roll = new Roll(data.damageFormula);
await roll.evaluate();
/* class BaseRoll needed to get rendered by foundryRoll.hbs */
const rollJSON = roll.toJSON();
rollJSON.class = 'BaseRoll';
foundry.documents.ChatMessage.implementation.create({
title: game.i18n.localize(data.chatTitle),
author: game.user.id,
speaker: foundry.documents.ChatMessage.implementation.getSpeaker(),
rolls: [rollJSON],
sound: CONFIG.sounds.dice
});
}
}

View file

@ -1,5 +1,8 @@
import { abilities } from '../../config/actorConfig.mjs';
import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
import { enrichedDualityRoll } from '../../enrichers/DualityRollEnricher.mjs';
import { enrichedFateRoll, getFateTypeData } from '../../enrichers/FateRollEnricher.mjs';
import { getCommandTarget, rollCommandToJSON } from '../../helpers/utils.mjs';
import { emitAsGM, GMUpdateEvent } from '../../systemRegistration/socket.mjs';
export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLog {
constructor(options) {
@ -21,6 +24,84 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
classes: ['daggerheart']
};
static CHAT_COMMANDS = {
...super.CHAT_COMMANDS,
dr: {
rgx: /^(?:\/dr)((?:\s)[^]*)?/,
fn: (_, match) => {
const argString = match[1]?.trim();
const result = argString ? rollCommandToJSON(argString) : { result: {} };
if (!result) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.dualityParsing'));
return false;
}
const { result: rollCommand, flavor } = result;
const reaction = rollCommand.reaction;
const traitValue = rollCommand.trait?.toLowerCase();
const advantage = rollCommand.advantage
? CONFIG.DH.ACTIONS.advantageState.advantage.value
: rollCommand.disadvantage
? CONFIG.DH.ACTIONS.advantageState.disadvantage.value
: undefined;
const difficulty = rollCommand.difficulty;
const grantResources = rollCommand.grantResources;
const target = getCommandTarget({ allowNull: true });
const title =
(flavor ?? traitValue)
? game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: game.i18n.localize(SYSTEM.ACTOR.abilities[traitValue].label)
})
: game.i18n.localize('DAGGERHEART.GENERAL.duality');
enrichedDualityRoll({
reaction,
traitValue,
target,
difficulty,
title,
label: game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll'),
actionType: null,
advantage,
grantResources
});
return false;
}
},
fr: {
rgx: /^(?:\/fr)((?:\s)[^]*)?/,
fn: (_, match) => {
const argString = match[1]?.trim();
const result = argString ? rollCommandToJSON(argString) : { result: {} };
if (!result) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateParsing'));
return false;
}
const { result: rollCommand, flavor } = result;
const fateTypeData = getFateTypeData(rollCommand?.type);
if (!fateTypeData)
return ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateTypeParsing'));
const { value: fateType, label: fateTypeLabel } = fateTypeData;
const target = getCommandTarget({ allowNull: true });
const title = flavor ?? game.i18n.localize('DAGGERHEART.GENERAL.fateRoll');
enrichedFateRoll({
target,
title,
label: fateTypeLabel,
fateType
});
return false;
}
}
};
_getEntryContextOptions() {
return [
...super._getEntryContextOptions(),
@ -175,7 +256,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
action.use(event);
}
async rerollEvent(event, message) {
async rerollEvent(event, messageData) {
event.stopPropagation();
if (!event.shiftKey) {
const confirmed = await foundry.applications.api.DialogV2.confirm({
@ -187,6 +268,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
if (!confirmed) return;
}
const message = game.messages.get(messageData._id);
const target = event.target.closest('[data-die-index]');
if (target.dataset.type === 'damage') {
@ -209,27 +291,16 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
}
});
} 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.dataset.dieIndex,
target.dataset.type
);
await game.messages.get(message._id).update({
'system.roll': newRoll,
'rolls': [parsedRoll]
const rerollDice = message.system.roll.dice[target.dataset.dieIndex];
await rerollDice.reroll(`/r1=${rerollDice.total}`, {
liveRoll: {
roll: message.system.roll,
actor: message.system.actionActor,
isReaction: message.system.roll.options.actionType === 'reaction'
}
});
await message.update({
rolls: [message.system.roll.toJSON()]
});
}
}

View file

@ -1,5 +1,6 @@
import { AdversaryBPPerEncounter } from '../../config/encounterConfig.mjs';
import { expireActiveEffects } from '../../helpers/utils.mjs';
import { clearPreviousSpotlight } from '../../macros/spotlightCombatant.mjs';
export default class DhCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker {
static DEFAULT_OPTIONS = {
@ -150,13 +151,13 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
}
async setCombatantSpotlight(combatantId) {
const combatant = this.viewed.combatants.get(combatantId);
const update = {
system: {
'spotlight.requesting': false,
'spotlight.requestOrderIndex': 0
}
};
const combatant = this.viewed.combatants.get(combatantId);
const toggleTurn = this.viewed.combatants.contents
.sort(this.viewed._sortCombatants)
@ -187,6 +188,14 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
round: this.viewed.round + 1
});
await combatant.update(update);
if (combatant.token) clearPreviousSpotlight();
}
async clearTurn() {
await this.viewed.update({
turn: null,
round: this.viewed.round + 1
});
}
static async requestSpotlight(_, target) {