Feature/165 action save (#231)

* Roll All Save button + Merge Attack & Roll Chat message

* Fix conflicts again
This commit is contained in:
Dapoulp 2025-07-01 19:04:53 +02:00 committed by GitHub
parent b7e4169079
commit 8b834036fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 369 additions and 140 deletions

View file

@ -1,3 +1,4 @@
import DHDamageRoll from '../data/chat-message/damageRoll.mjs';
import D20RollDialog from '../dialogs/d20RollDialog.mjs';
import DamageDialog from '../dialogs/damageDialog.mjs';
@ -53,12 +54,13 @@ export class DHRoll extends Roll {
}
static async buildPost(roll, config, message) {
console.log(config)
for (const hook of config.hooks) {
if (Hooks.call(`${SYSTEM.id}.postRoll${hook.capitalize()}`, config, message) === false) return null;
}
// Create Chat Message
if (message.data) {
if (config.source?.message) {
} else {
const messageData = {};
config.message = await this.toMessage(roll, config);
@ -393,7 +395,6 @@ export class DualityRoll extends D20Roll {
total: roll.dHope.total + roll.dFear.total,
label: roll.totalLabel
};
console.log(roll, config);
}
}
@ -409,5 +410,9 @@ export class DamageRoll extends DHRoll {
static async postEvaluate(roll, config = {}) {
super.postEvaluate(roll, config);
config.roll.type = config.type;
if(config.source?.message) {
const chatMessage = ui.chat.collection.get(config.source.message);
chatMessage.update({'system.damage': config});
}
}
}

View file

@ -342,39 +342,53 @@ export const refreshTypes = {
export const abilityCosts = {
hope: {
id: 'hope',
label: 'Hope'
label: 'Hope',
group: 'TYPES.Actor.character'
},
stress: {
id: 'stress',
label: 'DAGGERHEART.HealingType.Stress.Name'
label: 'DAGGERHEART.HealingType.Stress.Name',
group: 'TYPES.Actor.character'
},
armor: {
id: 'armor',
label: 'Armor Stack'
label: 'Armor Stack',
group: 'TYPES.Actor.character'
},
hp: {
id: 'hp',
label: 'DAGGERHEART.HealingType.HitPoints.Name'
label: 'DAGGERHEART.HealingType.HitPoints.Name',
group: 'TYPES.Actor.character'
},
prayer: {
id: 'prayer',
label: 'Prayer Dice'
label: 'Prayer Dice',
group: 'TYPES.Actor.character'
},
favor: {
id: 'favor',
label: 'Favor Points'
label: 'Favor Points',
group: 'TYPES.Actor.character'
},
slayer: {
id: 'slayer',
label: 'Slayer Dice'
label: 'Slayer Dice',
group: 'TYPES.Actor.character'
},
tide: {
id: 'tide',
label: 'Tide'
label: 'Tide',
group: 'TYPES.Actor.character'
},
chaos: {
id: 'chaos',
label: 'Chaos'
label: 'Chaos',
group: 'TYPES.Actor.character'
},
fear: {
id: 'fear',
label: 'Fear',
group: 'TYPES.Actor.adversary'
}
};

View file

@ -320,14 +320,14 @@ export class DHBaseAction extends foundry.abstract.DataModel {
prepareTarget() {
let targets;
if (this.target?.type === SYSTEM.ACTIONS.targetTypes.self.id)
targets = this.formatTarget(this.actor.token ?? this.actor.prototypeToken);
targets = this.constructor.formatTarget(this.actor.token ?? this.actor.prototypeToken);
targets = Array.from(game.user.targets);
// foundry.CONST.TOKEN_DISPOSITIONS.FRIENDLY
if (this.target?.type && this.target.type !== SYSTEM.ACTIONS.targetTypes.any.id) {
targets = targets.filter(t => this.isTargetFriendly(t));
if (this.target.amount && targets.length > this.target.amount) targets = [];
}
targets = targets.map(t => this.formatTarget(t));
targets = targets.map(t => this.constructor.formatTarget(t));
return targets;
}
@ -400,11 +400,13 @@ export class DHBaseAction extends foundry.abstract.DataModel {
}
hasCost(costs) {
const realCosts = this.getRealCosts(costs);
return realCosts.reduce(
(a, c) => a && this.actor.system.resources[c.type]?.value >= (c.total ?? c.value),
true
);
const realCosts = this.getRealCosts(costs),
hasFearCost = realCosts.findIndex(c => c.type === 'fear');
if(hasFearCost > -1) {
const fearCost = realCosts.splice(hasFearCost, 1);
if(!game.user.isGM || fearCost[0].total > game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear)) return false;
}
return realCosts.reduce((a, c) => a && this.actor.system.resources[c.type]?.value >= (c.total ?? c.value), true);
}
/* COST */
@ -435,7 +437,7 @@ export class DHBaseAction extends foundry.abstract.DataModel {
);
}
formatTarget(actor) {
static formatTarget(actor) {
return {
id: actor.id,
actorId: actor.actor.uuid,
@ -452,10 +454,11 @@ export class DHBaseAction extends foundry.abstract.DataModel {
/* RANGE */
/* EFFECTS */
async applyEffects(event, data, force = false) {
if (!this.effects?.length || !data.system.targets.length) return;
async applyEffects(event, data, targets) {
targets ??= data.system.targets;
if (!this.effects?.length || !targets.length) return;
let effects = this.effects;
data.system.targets.forEach(async token => {
targets.forEach(async token => {
if (!token.hit && !force) return;
if (this.hasSave && token.saved.success === true) {
effects = this.effects.filter(e => e.onSave === true);
@ -495,25 +498,19 @@ export class DHBaseAction extends foundry.abstract.DataModel {
/* SAVE */
async rollSave(target, event, message) {
if (!target?.actor) return;
target.actor
.diceRoll({
event,
title: 'Roll Save',
roll: {
trait: this.save.trait,
difficulty: this.save.difficulty,
type: 'reaction'
},
data: target.actor.getRollData()
})
.then(async result => {
if (result)
this.updateChatMessage(message, target.id, {
result: result.roll.total,
success: result.roll.success
});
});
if(!target?.actor) return;
return target.actor.diceRoll({
event,
title: 'Roll Save',
roll: {
trait: this.save.trait,
difficulty: this.save.difficulty,
type: "reaction"
},
data: target.actor.getRollData()
}).then(async (result) => {
if(result) this.updateChatMessage(message, target.id, {result: result.roll.total, success: result.roll.success});
})
}
async updateChatMessage(message, targetId, changes, chain = true) {
@ -539,13 +536,6 @@ export class DHBaseAction extends foundry.abstract.DataModel {
export class DHDamageAction extends DHBaseAction {
static extraSchemas = ['damage', 'target', 'effects'];
/* async use(event, ...args) {
const config = await super.use(event, args);
if (!config || ['error', 'warning'].includes(config.type)) return;
if (!this.directDamage) return;
return await this.rollDamage(event, config);
} */
getFormulaValue(part, data) {
let formulaValue = part.value;
if (this.hasRoll && part.resultBased && data.system.roll.result.duality === -1) return part.valueAlt;

View file

@ -1,7 +1,9 @@
import { DHBaseAction } from "../action/action.mjs";
const fields = foundry.data.fields;
export default class DHAdversaryRoll extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
title: new fields.StringField(),
roll: new fields.DataField(),
@ -20,6 +22,7 @@ export default class DHAdversaryRoll extends foundry.abstract.TypeDataModel {
})
})
),
targetSelection: new fields.BooleanField({ initial: true }),
hasDamage: new fields.BooleanField({ initial: false }),
hasHealing: new fields.BooleanField({ initial: false }),
hasEffect: new fields.BooleanField({ initial: false }),
@ -28,11 +31,17 @@ export default class DHAdversaryRoll extends foundry.abstract.TypeDataModel {
actor: new fields.StringField(),
item: new fields.StringField(),
action: new fields.StringField()
})
}),
damage: new fields.ObjectField()
};
}
get messageTemplate() {
return 'systems/daggerheart/templates/chat/adversary-roll.hbs';
}
prepareDerivedData() {
this.hasHitTarget = this.targets.filter(t => t.hit === true).length > 0;
this.currentTargets = this.targetSelection !== true ? Array.from(game.user.targets).map(t => DHBaseAction.formatTarget(t)) : this.targets;
}
}

View file

@ -19,6 +19,7 @@ export default class DHDamageRoll extends foundry.abstract.TypeDataModel {
})
})
),
targetSelection: new fields.BooleanField({ initial: true }),
hasSave: new fields.BooleanField({ initial: false }),
onSave: new fields.StringField(),
source: new fields.SchemaField({
@ -34,4 +35,9 @@ export default class DHDamageRoll extends foundry.abstract.TypeDataModel {
get messageTemplate() {
return `systems/daggerheart/templates/chat/${this.messageType}-roll.hbs`;
}
prepareDerivedData() {
this.hasHitTarget = this.targets.filter(t => t.hit === true).length > 0;
this.currentTargets = this.targetSelection !== true ? Array.from(game.user.targets).map(t => DHBaseAction.formatTarget(t)) : this.targets;
}
}

View file

@ -1,44 +1,6 @@
const fields = foundry.data.fields;
export default class DHDualityRoll extends foundry.abstract.TypeDataModel {
static dualityResult = {
hope: 1,
fear: 2,
critical: 3
};
static defineSchema() {
return {
title: new fields.StringField(),
roll: new fields.DataField({}),
targets: new fields.ArrayField(
new fields.SchemaField({
id: new fields.StringField({}),
actorId: new fields.StringField({}),
name: new fields.StringField({}),
img: new fields.StringField({}),
difficulty: new fields.NumberField({ integer: true, nullable: true }),
evasion: new fields.NumberField({ integer: true }),
hit: new fields.BooleanField({ initial: false }),
saved: new fields.SchemaField({
result: new fields.NumberField(),
success: new fields.BooleanField({ nullable: true, initial: null })
})
})
),
costs: new fields.ArrayField(new fields.ObjectField()),
hasDamage: new fields.BooleanField({ initial: false }),
hasHealing: new fields.BooleanField({ initial: false }),
hasEffect: new fields.BooleanField({ initial: false }),
hasSave: new fields.BooleanField({ initial: false }),
source: new fields.SchemaField({
actor: new fields.StringField(),
item: new fields.StringField(),
action: new fields.StringField()
})
};
}
import DHAdversaryRoll from "./adversaryRoll.mjs";
export default class DHDualityRoll extends DHAdversaryRoll {
get messageTemplate() {
return 'systems/daggerheart/templates/chat/duality-roll.hbs';
}

View file

View file

@ -484,6 +484,9 @@ export default class DhpActor extends Actor {
let updates = { actor: { target: this, resources: {} }, armor: { target: this.system.armor, resources: {} } };
resources.forEach(r => {
switch (r.type) {
case 'fear':
ui.resources.updateFear(game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear) + r.value);
break;
case 'armorStack':
updates.armor.resources['system.marks.value'] = Math.max(
Math.min(this.system.armor.system.marks.value + r.value, this.system.armorScore),
@ -502,7 +505,6 @@ export default class DhpActor extends Actor {
}
});
Object.values(updates).forEach(async u => {
console.log(updates, u);
if (Object.keys(u.resources).length > 0) {
if (game.user.isGM) {
await u.target.update(u.resources);

View file

@ -25,6 +25,9 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
html.querySelectorAll('.target-save-container').forEach(element =>
element.addEventListener('click', event => this.onRollSave(event, data.message))
);
html.querySelectorAll('.roll-all-save-button').forEach(element =>
element.addEventListener('click', event => this.onRollAllSave(event, data.message))
);
html.querySelectorAll('.duality-action-effect').forEach(element =>
element.addEventListener('click', event => this.onApplyEffect(event, data.message))
);
@ -33,6 +36,9 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
element.addEventListener('mouseleave', this.unhoverTarget);
element.addEventListener('click', this.clickTarget);
});
html.querySelectorAll('.button-target-selection').forEach(element => {
element.addEventListener('click', event => this.onTargetSelection(event, data.message))
});
html.querySelectorAll('.damage-button').forEach(element =>
element.addEventListener('click', event => this.onDamage(event, data.message))
);
@ -107,7 +113,6 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
tokenId = event.target.closest('[data-token]')?.dataset.token,
token = game.canvas.tokens.get(tokenId);
if (!token?.actor || !token.isOwner) return true;
console.log(token.actor.canUserModify(game.user, 'update'));
if (message.system.source.item && message.system.source.action) {
const action = this.getAction(actor, message.system.source.item, message.system.source.action);
if (!action || !action?.hasSave) return;
@ -115,6 +120,14 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
}
};
onRollAllSave = async (event, message) => {
event.stopPropagation();
const targets = event.target.parentElement.querySelectorAll('.target-section > [data-token] .target-save-container');
targets.forEach((el) => {
el.dispatchEvent(new PointerEvent("click", { shiftKey: true}))
})
}
onApplyEffect = async (event, message) => {
event.stopPropagation();
const actor = await this.getActor(message.system.source.actor);
@ -122,10 +135,30 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
if (message.system.source.item && message.system.source.action) {
const action = this.getAction(actor, message.system.source.item, message.system.source.action);
if (!action || !action?.applyEffects) return;
await action.applyEffects(event, message);
const { isHit, targets } = this.getTargetList(event, message);
if (targets.length === 0)
ui.notifications.info(game.i18n.localize('DAGGERHEART.Notification.Info.NoTargetsSelected'));
await action.applyEffects(event, message, targets);
}
};
onTargetSelection = async (event, message) => {
event.stopPropagation();
const targetSelection = Boolean(event.target.dataset.targetHit),
msg = ui.chat.collection.get(message._id);
if(msg.system.targetSelection === targetSelection) return;
if(targetSelection !== true && !Array.from(game.user.targets).length) return ui.notifications.info(game.i18n.localize('DAGGERHEART.Notification.Info.NoTargetsSelected'));
msg.system.targetSelection = targetSelection;
msg.system.prepareDerivedData();
ui.chat.updateMessage(msg);
}
getTargetList = (event, message) => {
const targetSelection = event.target.closest('.message-content').querySelector('.button-target-selection.target-selected'),
isHit = Boolean(targetSelection.dataset.targetHit);
return {isHit, targets: isHit ? message.system.targets.filter(t => t.hit === true).map(target => game.canvas.tokens.get(target.id)) : Array.from(game.user.targets)};
}
hoverTarget = event => {
event.stopPropagation();
const token = canvas.tokens.get(event.currentTarget.dataset.token);
@ -150,15 +183,11 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
onDamage = async (event, message) => {
event.stopPropagation();
const targets = event.currentTarget.dataset.targetHit
? message.system.targets.map(target => game.canvas.tokens.get(target.id))
: Array.from(game.user.targets);
const { isHit, targets } = this.getTargetList(event, message);
if (message.system.onSave && event.currentTarget.dataset.targetHit) {
const pendingingSaves = message.system.targets.filter(
target => target.hit && target.saved.success === null
);
if (pendingingSaves.length) {
if(message.system.onSave && isHit) {
const pendingingSaves = message.system.targets.filter(target => target.hit && target.saved.success === null);
if(pendingingSaves.length) {
const confirm = await foundry.applications.api.DialogV2.confirm({
window: { title: 'Pending Reaction Rolls found' },
content: `<p>Some Tokens still need to roll their Reaction Roll.</p><p>Are you sure you want to continue ?</p><p><i>Undone reaction rolls will be considered as failed</i></p>`