Initial rework

This commit is contained in:
WBHarry 2026-07-11 20:19:25 +02:00
parent 0fbfe388b0
commit 92787329c3
8 changed files with 136 additions and 120 deletions

View file

@ -183,13 +183,8 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
total = message.rolls.reduce((a, c) => a + Roll.fromJSON(c).total, 0), total = message.rolls.reduce((a, c) => a + Roll.fromJSON(c).total, 0),
damages = { damages = {
hitPoints: { hitPoints: {
parts: [ damageTypes: [],
{ roll: { total }
applyTo: 'hitPoints',
damageTypes: [],
total
}
]
} }
}, },
targets = Array.from(game.user.targets); targets = Array.from(game.user.targets);
@ -259,23 +254,14 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
const target = event.target.closest('[data-die-index]'); const target = event.target.closest('[data-die-index]');
if (target.dataset.type === 'damage') { if (target.dataset.type === 'damage') {
const { damageType, part, dice, result } = target.dataset; const { damageType, dice } = target.dataset;
const damagePart = message.system.damage[damageType].parts[part]; await message.system.damage.rerollDamageDice(damageType, dice);
const { parsedRoll, rerolledDice } = await game.system.api.dice.DamageRoll.reroll(damagePart, dice, result); await message.update({
const damageParts = message.system.damage[damageType].parts.map((damagePart, index) => { 'system.damage.types': {
if (index !== Number(part)) return damagePart; [damageType]: {
return { roll: message.system.damage.types[damageType].roll.toJSON()
...damagePart, }
total: parsedRoll.total, }
dice: rerolledDice
};
});
const updateMessage = game.messages.get(message._id);
await updateMessage.update({
[`system.damage.${damageType}`]: {
total: parsedRoll.total,
parts: damageParts
}
}); });
} else { } else {
const rerollDice = message.system.roll.dice[target.dataset.dieIndex]; const rerollDice = message.system.roll.dice[target.dataset.dieIndex];

View file

@ -29,6 +29,60 @@ export const originItemField = () =>
actionIndex: new fields.StringField() actionIndex: new fields.StringField()
}); });
class ChatMessageRollDamage extends foundry.abstract.DataModel {
static defineSchema() {
return {
types: new fields.TypedObjectField(new fields.SchemaField({
roll: new fields.JSONField({validate: ChatMessageRollDamage.#validateRoll}),
damageTypes: new fields.ArrayField(new fields.StringField({ choices: CONFIG.DH.GENERAL.damageTypes })),
type: new fields.StringField()
}))
};
}
get active() {
return Boolean(Object.keys(this.types).length);
}
static #validateRoll(rollJSON) {
const roll = JSON.parse(rollJSON);
if (!roll.evaluated) throw new Error('Roll objects added to ChatMessage documents must be evaluated');
}
prepareRolls() {
for (const key of Object.keys(this.types)) {
const type = this.types[key];
try {
const roll = Roll.fromData(type.roll);
type.roll = roll;
type.roll.modifierTotal = CONFIG.Dice.daggerheart.DHRoll.calculateTotalModifiers(roll);
} catch {}
}
}
async rerollDamageDice(damageType, dice) {
const reroll = this.types[damageType].roll;
const rerollDice = reroll.dice[dice];
await rerollDice.reroll(`/r1=${rerollDice.total}`);
await reroll._evaluate();
const result = rerollDice.results.find(x => x.active);
if (result) {
const fakeRoll = {
_evaluated: true,
dice: [new foundry.dice.terms.Die({
...rerollDice,
results: [result],
total: result.value,
faces: rerollDice.faces
})],
options: { appearance: {} }
};
await triggerChatRollFx([fakeRoll]);
}
}
}
export default class DHActorRoll extends foundry.abstract.TypeDataModel { export default class DHActorRoll extends foundry.abstract.TypeDataModel {
static defineSchema() { static defineSchema() {
return { return {
@ -49,7 +103,7 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
originItem: originItemField(), originItem: originItemField(),
action: new fields.StringField() action: new fields.StringField()
}), }),
damage: new fields.ObjectField(), damage: new fields.EmbeddedDataField(ChatMessageRollDamage),
damageOptions: new fields.ObjectField(), damageOptions: new fields.ObjectField(),
costs: new fields.ArrayField(new fields.ObjectField()), costs: new fields.ArrayField(new fields.ObjectField()),
successConsumed: new fields.BooleanField({ initial: false }) successConsumed: new fields.BooleanField({ initial: false })
@ -134,28 +188,36 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
/* TODO: Change how damage data is stored somehow to enable better rerolling */ /* TODO: Change how damage data is stored somehow to enable better rerolling */
async getRerolledDamage() { async getRerolledDamage() {
if (!this.damage) return; if (!this.damage.active) return;
const rerolls = []; const rerolls = [];
const update = { system: { damage: {} } }; const update = { system: { damage: { types: {} } } };
for (const partKey in this.damage) { for (const key of Object.keys(this.damage.types)) {
const part = this.damage[partKey]; const type = this.damage.types[key];
const testRoll = Roll.fromData(part.parts[0].roll); const reroll = await type.roll.reroll();
const rerolled = await testRoll.reroll(); rerolls.push(reroll);
rerolls.push(rerolled); update.system.damage.types[key] = { roll: reroll.toJSON() };
if (!update.system.damage[partKey]) update.system.damage[partKey] = { parts: [part.parts[0]] };
const partData = update.system.damage[partKey].parts[0];
update.system.damage[partKey].total = rerolled.total;
partData.modifierTotal = rerolled.terms.reduce((acc, x) => {
if (x.isDeterministic && !x.operator) acc += x.total;
return acc;
}, 0);
partData.dice = rerolled.dice.map(d => ({ ...d.toJSON(), dice: d.denomination }));
partData.total = rerolled.total;
partData.roll = rerolled.toJSON();
} }
// const update = { system: { damage: {} } };
// for (const partKey in this.damage) {
// const part = this.damage[partKey];
// const testRoll = Roll.fromData(part.parts[0].roll);
// const rerolled = await testRoll.reroll();
// rerolls.push(rerolled);
// if (!update.system.damage[partKey]) update.system.damage[partKey] = { parts: [part.parts[0]] };
// const partData = update.system.damage[partKey].parts[0];
// update.system.damage[partKey].total = rerolled.total;
// partData.modifierTotal = rerolled.terms.reduce((acc, x) => {
// if (x.isDeterministic && !x.operator) acc += x.total;
// return acc;
// }, 0);
// partData.dice = rerolled.dice.map(d => ({ ...d.toJSON(), dice: d.denomination }));
// partData.total = rerolled.total;
// partData.roll = rerolled.toJSON();
// }
await triggerChatRollFx(rerolls); await triggerChatRollFx(rerolls);
return update; return update;
@ -175,6 +237,8 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
} }
prepareDerivedData() { prepareDerivedData() {
this.damage.prepareRolls();
if (this.hasTarget) { if (this.hasTarget) {
this.hasHitTarget = this.targets.filter(t => t.hit === true).length > 0; this.hasHitTarget = this.targets.filter(t => t.hit === true).length > 0;
this.currentTargets = this.getTargetList(); this.currentTargets = this.getTargetList();

View file

@ -111,16 +111,17 @@ export default class DamageField extends fields.SchemaField {
: actor.prototypeToken; : actor.prototypeToken;
if (config.hasHealing) if (config.hasHealing)
damagePromises.push( damagePromises.push(
actor.takeHealing(config.damage).then(updates => targetDamage.push({ token, updates })) actor.takeHealing(config.damage.types).then(updates => targetDamage.push({ token, updates }))
); );
else { else {
const configDamage = foundry.utils.deepClone(config.damage); const configDamage = foundry.utils.deepClone(config.damage.types);
const hpDamageMultiplier = config.actionActor?.system.rules?.attack?.damage?.hpDamageMultiplier ?? 1; const hpDamageMultiplier = config.actionActor?.system.rules?.attack?.damage?.hpDamageMultiplier ?? 1;
const hpDamageTakenMultiplier = actor.system.rules?.attack?.damage?.hpDamageTakenMultiplier; const hpDamageTakenMultiplier = actor.system.rules?.attack?.damage?.hpDamageTakenMultiplier;
if (configDamage.hitPoints) { if (configDamage.hitPoints) {
for (const part of configDamage.hitPoints.parts) { configDamage.hitPoints.roll = configDamage.hitPoints.roll.toJSON();
part.total = Math.ceil(part.total * hpDamageMultiplier * hpDamageTakenMultiplier); configDamage.hitPoints.roll.total = Math.ceil(
} configDamage.hitPoints.roll.total * hpDamageMultiplier * hpDamageTakenMultiplier
);
} }
damagePromises.push( damagePromises.push(

View file

@ -14,35 +14,17 @@ export default class DamageRoll extends DHRoll {
static DefaultDialog = DamageDialog; static DefaultDialog = DamageDialog;
/** @inheritdoc */ /** @inheritdoc */
static async buildEvaluate(roll, config = {}, message = {}) { static async buildEvaluate(roll, config = {}) {
if (config.dialog.configure === false) roll.constructFormula(config); if (config.dialog.configure === false) roll.constructFormula(config);
for (const roll of config.roll) await roll.roll.evaluate();
for (const roll of config.roll) {
roll._evaluated = true; await roll.roll.evaluate();
config.damage.types[roll.applyTo] = {
const parts = []; roll: roll.roll,
for (const rollData of config.roll) { damageTypes: roll.damageTypes ?? [],
const roll = rollData.roll; type: config.type
parts.push({ };
...rollData,
...roll.options.roll,
total: roll.total,
formula: roll.formula,
dice: roll.dice.map(d => ({
dice: d.denomination,
total: d.total,
formula: d.formula,
results: d.results
})),
damageTypes: [...(rollData.damageTypes ?? [])],
roll,
type: config.type,
modifierTotal: this.calculateTotalModifiers(roll)
});
rollData.roll = JSON.stringify(roll.toJSON());
} }
config.damage = this.unifyDamageRoll(parts);
} }
static async buildPost(roll, config, message) { static async buildPost(roll, config, message) {
@ -54,7 +36,7 @@ export default class DamageRoll extends DHRoll {
if (game.modules.get('dice-so-nice')?.active) { if (game.modules.get('dice-so-nice')?.active) {
config.mute = true; config.mute = true;
const pool = foundry.dice.terms.PoolTerm.fromRolls( const pool = foundry.dice.terms.PoolTerm.fromRolls(
Object.values(config.damage).flatMap(r => r.parts.map(p => p.roll)) Object.values(config.damage.types).map(x => x.roll)
); );
diceRolls.push(Roll.fromTerms([pool])); diceRolls.push(Roll.fromTerms([pool]));
} }
@ -66,22 +48,13 @@ export default class DamageRoll extends DHRoll {
await super.buildPost(roll, config, message); await super.buildPost(roll, config, message);
if (config.source?.message) { if (config.source?.message) {
chatMessage.update({ 'system.damage': config.damage }); chatMessage.update({ 'system.damage': {
...config.damage.toObject(),
types: config.damage.types
}});
} }
} }
static unifyDamageRoll(rolls) {
const unified = {};
rolls.forEach(r => {
const resource = unified[r.applyTo] ?? { formula: '', total: 0, parts: [] };
resource.formula += `${resource.formula !== '' ? ' + ' : ''}${r.formula}`;
resource.total += r.total;
resource.parts.push(r);
unified[r.applyTo] = resource;
});
return unified;
}
static formatGlobal(rolls) { static formatGlobal(rolls) {
let formula, total; let formula, total;
const applyTo = new Set(rolls.flatMap(r => r.applyTo)); const applyTo = new Set(rolls.flatMap(r => r.applyTo));

View file

@ -409,8 +409,4 @@ export default class DualityRoll extends D20Roll {
return rerolled; return rerolled;
} }
fromJSON(json) {
return super.fromJSON(json);
}
} }

View file

@ -659,15 +659,13 @@ export default class DhpActor extends Actor {
const updates = []; const updates = [];
Object.entries(damages).forEach(([key, damage]) => { Object.entries(damages).forEach(([key, damage]) => {
damage.parts.forEach(part => { if (key === CONFIG.DH.GENERAL.healingTypes.hitPoints.id)
if (part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) damage.roll.total = this.calculateDamage(damage.roll.total, damage.damageTypes);
part.total = this.calculateDamage(part.total, part.damageTypes); const update = updates.find(u => u.key === key);
const update = updates.find(u => u.key === key); if (update) {
if (update) { update.value += damage.roll.total;
update.value += part.total; update.damageTypes.add(...new Set(damage.damageTypes));
update.damageTypes.add(...new Set(part.damageTypes)); } else updates.push({ value: damage.roll.total, key, damageTypes: new Set(damage.damageTypes) });
} else updates.push({ value: part.total, key, damageTypes: new Set(part.damageTypes) });
});
}); });
if (Hooks.call(`${CONFIG.DH.id}.postCalculateDamage`, this, damages) === false) return null; if (Hooks.call(`${CONFIG.DH.id}.postCalculateDamage`, this, damages) === false) return null;
@ -772,11 +770,9 @@ export default class DhpActor extends Actor {
const updates = []; const updates = [];
Object.entries(healings).forEach(([key, healing]) => { Object.entries(healings).forEach(([key, healing]) => {
healing.parts.forEach(part => { const update = updates.find(u => u.key === key);
const update = updates.find(u => u.key === key); if (update) update.value += healing.roll.total;
if (update) update.value += part.total; else updates.push({ value: healing.roll.total, key });
else updates.push({ value: part.total, key });
});
}); });
updates.forEach( updates.forEach(

View file

@ -1,18 +1,18 @@
<div class="roll-buttons"> <div class="roll-buttons">
{{#if areas.length}}<button class="action-areas end-button"><i class="fa-solid fa-crosshairs"></i></button>{{/if}} {{#if areas.length}}<button class="action-areas end-button"><i class="fa-solid fa-crosshairs"></i></button>{{/if}}
{{#if hasDamage}} {{#if hasDamage}}
{{#unless (empty damage)}} {{#if damage.active}}
<button class="duality-action damage-button">{{localize "DAGGERHEART.UI.Chat.damageRoll.dealDamage"}}</button> <button class="duality-action damage-button">{{localize "DAGGERHEART.UI.Chat.damageRoll.dealDamage"}}</button>
{{else}} {{else}}
<button class="duality-action duality-action-damage">{{localize "DAGGERHEART.UI.Chat.attackRoll.rollDamage"}}</button> <button class="duality-action duality-action-damage">{{localize "DAGGERHEART.UI.Chat.attackRoll.rollDamage"}}</button>
{{/unless}} {{/if}}
{{/if}} {{/if}}
{{#if hasHealing}} {{#if hasHealing}}
{{#unless (empty damage)}} {{#if damage.active}}
<button class="duality-action damage-button">{{localize "DAGGERHEART.UI.Chat.healingRoll.applyHealing"}}</button> <button class="duality-action damage-button">{{localize "DAGGERHEART.UI.Chat.healingRoll.applyHealing"}}</button>
{{else}} {{else}}
<button class="duality-action duality-action-damage">{{localize "DAGGERHEART.UI.Chat.attackRoll.rollHealing"}}</button> <button class="duality-action duality-action-damage">{{localize "DAGGERHEART.UI.Chat.attackRoll.rollHealing"}}</button>
{{/unless}} {{/if}}
{{/if}} {{/if}}
{{#if (and hasEffect)}}<button class="duality-action-effect">{{localize "DAGGERHEART.UI.Chat.attackRoll.applyEffect"}}</button>{{/if}} {{#if (and hasEffect)}}<button class="duality-action-effect">{{localize "DAGGERHEART.UI.Chat.attackRoll.applyEffect"}}</button>{{/if}}
</div> </div>

View file

@ -1,4 +1,4 @@
<div class="roll-part damage-section dice-roll" data-action="expandRoll"{{#if (empty damage)}} hidden{{/if}}> <div class="roll-part damage-section dice-roll" data-action="expandRoll"{{#unless damage.active}} hidden{{/unless}}>
<div class="roll-part-header"> <div class="roll-part-header">
<div> <div>
{{#if hasHealing}} {{#if hasHealing}}
@ -10,20 +10,20 @@
</div> </div>
<div class="roll-part-extra on-reduced"> <div class="roll-part-extra on-reduced">
<div class="wrapper"> <div class="wrapper">
{{#each damage as | roll index | }} {{#each damage.types as | data index | }}
<div class="roll-formula">{{localize (concat 'DAGGERHEART.CONFIG.HealingType.' index '.inChatRoll')}}: {{total}}</div> <div class="roll-formula">{{localize (concat 'DAGGERHEART.CONFIG.HealingType.' index '.inChatRoll')}}: {{data.roll.total}}</div>
{{/each}} {{/each}}
</div> </div>
</div> </div>
<div class="roll-part-content dice-result"> <div class="roll-part-content dice-result">
<div class="dice-tooltip"> <div class="dice-tooltip">
<div class="wrapper"> <div class="wrapper">
{{#each damage as | roll index | }} {{#each damage.types as | data index | }}
<fieldset> <fieldset>
<legend> <legend>
{{#if ../hasHealing}}{{localize (concat 'DAGGERHEART.CONFIG.HealingType.' index '.name')}}{{else}}{{localize (concat 'DAGGERHEART.CONFIG.HealingType.' index '.inChatRoll')}}{{/if}} <div class="roll-formula">{{localize "DAGGERHEART.GENERAL.total"}}: {{roll.total}}</div>{{#if (and (eq index "hitPoints") ../isDirect)}} <div class="roll-formula">{{localize "DAGGERHEART.CONFIG.DamageType.direct.short"}}</div>{{/if}} {{#if ../hasHealing}}{{localize (concat 'DAGGERHEART.CONFIG.HealingType.' index '.name')}}{{else}}{{localize (concat 'DAGGERHEART.CONFIG.HealingType.' index '.inChatRoll')}}{{/if}} <div class="roll-formula">{{localize "DAGGERHEART.GENERAL.total"}}: {{roll.total}}</div>{{#if (and (eq index "hitPoints") ../isDirect)}} <div class="roll-formula">{{localize "DAGGERHEART.CONFIG.DamageType.direct.short"}}</div>{{/if}}
</legend> </legend>
{{#each roll.parts}} {{#with data.roll}}
{{#if (and (not @root.hasHealing) damageTypes.length)}} {{#if (and (not @root.hasHealing) damageTypes.length)}}
<label class="roll-part-header"><span> <label class="roll-part-header"><span>
{{#each damageTypes}} {{#each damageTypes}}
@ -36,17 +36,17 @@
{{#if dice.length}} {{#if dice.length}}
{{#each dice}} {{#each dice}}
{{#each results}} {{#each results}}
{{#unless discarded}} {{#if active}}
<div class="roll-die{{#unless @../first}} has-plus{{/unless}}"> <div class="roll-die{{#unless @../first}} has-plus{{/unless}}">
<div <div
class="dice reroll-button {{../dice}}" class="dice reroll-button {{../dice}}"
data-die-index="0" data-type="damage" data-damage-type="{{@../../../key}}" data-part="{{@../../key}}" data-dice="{{@../key}}" data-result="{{@key}}" data-die-index="0" data-type="damage" data-damage-type="{{@../../key}}" data-dice="{{@../key}}"
> >
{{#if hasRerolls}}<i class="fa-solid fa-dice dice-rerolled" data-tooltip="{{localize "DAGGERHEART.GENERAL.rerolled"}}"></i>{{/if}} {{#if hasRerolls}}<i class="fa-solid fa-dice dice-rerolled" data-tooltip="{{localize "DAGGERHEART.GENERAL.rerolled"}}"></i>{{/if}}
{{result}} {{result}}
</div> </div>
</div> </div>
{{/unless}} {{/if}}
{{/each}} {{/each}}
{{/each}} {{/each}}
{{#if modifierTotal}} {{#if modifierTotal}}
@ -60,7 +60,7 @@
</div> </div>
{{/if}} {{/if}}
</div> </div>
{{/each}} {{/with}}
</fieldset> </fieldset>
{{/each}} {{/each}}
</div> </div>