Merged with main

This commit is contained in:
WBHarry 2025-07-29 23:08:12 +02:00
commit 5a746190c6
28 changed files with 281 additions and 134 deletions

View file

@ -19,7 +19,6 @@ import {
} from './module/systemRegistration/_module.mjs'; } from './module/systemRegistration/_module.mjs';
import { placeables } from './module/canvas/_module.mjs'; import { placeables } from './module/canvas/_module.mjs';
import { registerRollDiceHooks } from './module/dice/dhRoll.mjs'; import { registerRollDiceHooks } from './module/dice/dhRoll.mjs';
import { registerDHActorHooks } from './module/documents/actor.mjs';
import './node_modules/@yaireo/tagify/dist/tagify.css'; import './node_modules/@yaireo/tagify/dist/tagify.css';
Hooks.once('init', () => { Hooks.once('init', () => {
@ -169,7 +168,7 @@ Hooks.on('ready', () => {
registerCountdownHooks(); registerCountdownHooks();
socketRegistration.registerSocketHooks(); socketRegistration.registerSocketHooks();
registerRollDiceHooks(); registerRollDiceHooks();
registerDHActorHooks(); socketRegistration.registerUserQueries();
}); });
Hooks.once('dicesoniceready', () => {}); Hooks.once('dicesoniceready', () => {});

View file

@ -453,6 +453,9 @@
"title": "Ownership Selection - {name}", "title": "Ownership Selection - {name}",
"default": "Default Ownership" "default": "Default Ownership"
}, },
"ReactionRoll": {
"title": "Reaction Roll: {trait}"
},
"ResourceDice": { "ResourceDice": {
"title": "{name} Resource", "title": "{name} Resource",
"rerollDice": "Reroll Dice" "rerollDice": "Reroll Dice"
@ -849,7 +852,7 @@
"name": "Hope", "name": "Hope",
"abbreviation": "HO" "abbreviation": "HO"
}, },
"armorStack": { "armorSlot": {
"name": "Armor Slot", "name": "Armor Slot",
"abbreviation": "AS" "abbreviation": "AS"
}, },
@ -1824,6 +1827,7 @@
"imagePath": "Image Path", "imagePath": "Image Path",
"inactiveEffects": "Inactive Effects", "inactiveEffects": "Inactive Effects",
"inventory": "Inventory", "inventory": "Inventory",
"itemResource": "Item Resource",
"level": "Level", "level": "Level",
"levelUp": "Level Up", "levelUp": "Level Up",
"loadout": "Loadout", "loadout": "Loadout",

View file

@ -1,6 +1,6 @@
import { damageKeyToNumber, getDamageLabel } from '../../helpers/utils.mjs'; import { damageKeyToNumber, getDamageLabel } from '../../helpers/utils.mjs';
const { DialogV2, ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export default class DamageReductionDialog extends HandlebarsApplicationMixin(ApplicationV2) { export default class DamageReductionDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(resolve, reject, actor, damage, damageType) { constructor(resolve, reject, actor, damage, damageType) {
@ -53,10 +53,6 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
); );
} }
get title() {
return game.i18n.localize('DAGGERHEART.APPLICATIONS.DamageReduction.title');
}
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
tag: 'form', tag: 'form',
classes: ['daggerheart', 'views', 'damage-reduction'], classes: ['daggerheart', 'views', 'damage-reduction'],
@ -229,7 +225,7 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
await super.close({}); await super.close({});
} }
static async armorStackQuery({ actorId, damage, type }) { static async armorSlotQuery({ actorId, damage, type }) {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
const actor = await fromUuid(actorId); const actor = await fromUuid(actorId);
if (!actor || !actor?.isOwner) reject(); if (!actor || !actor?.isOwner) reject();

View file

@ -108,9 +108,11 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
context.hasBaseDamage = !!this.action.parent.attack; context.hasBaseDamage = !!this.action.parent.attack;
context.getEffectDetails = this.getEffectDetails.bind(this); context.getEffectDetails = this.getEffectDetails.bind(this);
context.costOptions = this.getCostOptions(); context.costOptions = this.getCostOptions();
context.getRollTypeOptions = this.getRollTypeOptions();
context.disableOption = this.disableOption.bind(this); context.disableOption = this.disableOption.bind(this);
context.isNPC = this.action.actor?.isNPC; context.isNPC = this.action.actor?.isNPC;
context.baseSaveDifficulty = this.action.actor?.baseSaveDifficulty; context.baseSaveDifficulty = this.action.actor?.baseSaveDifficulty;
context.baseAttackBonus = this.action.actor?.system.attack?.roll.bonus;
context.hasRoll = this.action.hasRoll; context.hasRoll = this.action.hasRoll;
const settingsTiers = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers; const settingsTiers = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers;
@ -131,14 +133,23 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
const resource = this.action.parent.resource; const resource = this.action.parent.resource;
if (resource) { if (resource) {
options[this.action.parent.parent.id] = { options[this.action.parent.parent.id] = {
label: this.action.parent.parent.name, label: 'DAGGERHEART.GENERAL.itemResource',
group: 'TYPES.Actor.character' group: 'Global'
}; };
} }
return options; return options;
} }
getRollTypeOptions() {
const types = foundry.utils.deepClone(CONFIG.DH.GENERAL.rollTypes);
if (!this.action.actor) return types;
Object.values(types).forEach(t => {
if (this.action.actor.type !== 'character' && t.playerOnly) delete types[t.id];
});
return types;
}
disableOption(index, costOptions, choices) { disableOption(index, costOptions, choices) {
const filtered = foundry.utils.deepClone(costOptions); const filtered = foundry.utils.deepClone(costOptions);
Object.keys(filtered).forEach(o => { Object.keys(filtered).forEach(o => {

View file

@ -260,7 +260,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
icon: 'fa-solid fa-arrow-up', icon: 'fa-solid fa-arrow-up',
condition: target => { condition: target => {
const doc = getDocFromElementSync(target); const doc = getDocFromElementSync(target);
return doc && system.inVault; return doc && doc.system.inVault;
}, },
callback: async target => { callback: async target => {
const doc = await getDocFromElement(target); const doc = await getDocFromElement(target);

View file

@ -1,3 +1,5 @@
import { emitAsGM, GMUpdateEvent } from '../../systemRegistration/socket.mjs';
export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLog { export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLog {
constructor(options) { constructor(options) {
super(options); super(options);
@ -98,17 +100,44 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
if (message.system.source.item && message.system.source.action) { if (message.system.source.item && message.system.source.action) {
const action = this.getAction(actor, 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; if (!action || !action?.hasSave) return;
action.rollSave(token, event, message); action.rollSave(token.actor, event, message).then(result =>
emitAsGM(
GMUpdateEvent.UpdateSaveMessage,
action.updateSaveMessage.bind(action, result, message, token.id),
{
action: action.uuid,
message: message._id,
token: token.id,
result
}
)
);
} }
} }
onRollAllSave(event, _message) { async onRollAllSave(event, message) {
event.stopPropagation(); event.stopPropagation();
if (!game.user.isGM) return;
const targets = event.target.parentElement.querySelectorAll( const targets = event.target.parentElement.querySelectorAll(
'.target-section > [data-token] .target-save-container' '.target-section > [data-token] .target-save-container'
); );
targets.forEach(el => { const actor = await this.getActor(message.system.source.actor),
el.dispatchEvent(new PointerEvent('click', { shiftKey: true })); action = this.getAction(actor, message.system.source.item, message.system.source.action);
targets.forEach(async el => {
const tokenId = el.closest('[data-token]')?.dataset.token,
token = game.canvas.tokens.get(tokenId);
if (!token.actor) return;
if (game.user === token.actor.owner) el.dispatchEvent(new PointerEvent('click', { shiftKey: true }));
else {
token.actor.owner
.query('reactionRoll', {
actionId: action.uuid,
actorId: token.actor.uuid,
event,
message
})
.then(result => action.updateSaveMessage(result, message, token.id));
}
}); });
} }
@ -146,7 +175,9 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
return { return {
isHit, isHit,
targets: isHit targets: isHit
? message.system.targets.filter(t => t.hit === true).map(target => game.canvas.tokens.documentCollection.find(t => t.actor.uuid === target.actorId)) ? message.system.targets
.filter(t => t.hit === true)
.map(target => game.canvas.tokens.documentCollection.find(t => t.actor.uuid === target.actorId))
: Array.from(game.user.targets) : Array.from(game.user.targets)
}; };
} }
@ -208,10 +239,8 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
}); });
} }
if(message.system.hasHealing) if (message.system.hasHealing) target.actor.takeHealing(damages);
target.actor.takeHealing(damages); else target.actor.takeDamage(damages);
else
target.actor.takeDamage(damages);
} }
} }

View file

@ -18,7 +18,7 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
x => x.statuses.size === 1 && x.name === game.i18n.localize(statusMap.get(x.statuses.first()).name) x => x.statuses.size === 1 && x.name === game.i18n.localize(statusMap.get(x.statuses.first()).name)
); );
for (var status of effect.statuses) { for (var status of effect.statuses) {
if (!currentStatusActiveEffects.find(x => x.statuses.includes(status))) { if (!currentStatusActiveEffects.find(x => x.statuses.has(status))) {
const statusData = statusMap.get(status); const statusData = statusMap.get(status);
acc.push({ acc.push({
name: game.i18n.localize(statusData.name), name: game.i18n.localize(statusData.name),

View file

@ -85,10 +85,10 @@ export const healingTypes = {
label: 'DAGGERHEART.CONFIG.HealingType.hope.name', label: 'DAGGERHEART.CONFIG.HealingType.hope.name',
abbreviation: 'DAGGERHEART.CONFIG.HealingType.hope.abbreviation' abbreviation: 'DAGGERHEART.CONFIG.HealingType.hope.abbreviation'
}, },
armorStack: { armorSlot: {
id: 'armorStack', id: 'armorSlot',
label: 'DAGGERHEART.CONFIG.HealingType.armorStack.name', label: 'DAGGERHEART.CONFIG.HealingType.armorSlot.name',
abbreviation: 'DAGGERHEART.CONFIG.HealingType.armorStack.abbreviation' abbreviation: 'DAGGERHEART.CONFIG.HealingType.armorSlot.abbreviation'
}, },
fear: { fear: {
id: 'fear', id: 'fear',
@ -199,7 +199,7 @@ export const defaultRestOptions = {
actionType: 'action', actionType: 'action',
chatDisplay: false, chatDisplay: false,
healing: { healing: {
applyTo: healingTypes.armorStack.id, applyTo: healingTypes.armorSlot.id,
value: { value: {
custom: { custom: {
enabled: true, enabled: true,
@ -287,7 +287,7 @@ export const defaultRestOptions = {
actionType: 'action', actionType: 'action',
chatDisplay: false, chatDisplay: false,
healing: { healing: {
applyTo: healingTypes.armorStack.id, applyTo: healingTypes.armorSlot.id,
value: { value: {
custom: { custom: {
enabled: true, enabled: true,
@ -425,8 +425,8 @@ export const refreshTypes = {
}; };
export const abilityCosts = { export const abilityCosts = {
hp: { hitPoints: {
id: 'hp', id: 'hitPoints',
label: 'DAGGERHEART.CONFIG.HealingType.hitPoints.name', label: 'DAGGERHEART.CONFIG.HealingType.hitPoints.name',
group: 'Global' group: 'Global'
}, },
@ -473,11 +473,13 @@ export const rollTypes = {
}, },
spellcast: { spellcast: {
id: 'spellcast', id: 'spellcast',
label: 'DAGGERHEART.CONFIG.RollTypes.spellcast.name' label: 'DAGGERHEART.CONFIG.RollTypes.spellcast.name',
playerOnly: true
}, },
trait: { trait: {
id: 'trait', id: 'trait',
label: 'DAGGERHEART.CONFIG.RollTypes.trait.name' label: 'DAGGERHEART.CONFIG.RollTypes.trait.name',
playerOnly: true
}, },
diceSet: { diceSet: {
id: 'diceSet', id: 'diceSet',

View file

@ -661,7 +661,7 @@ export const weaponFeatures = {
}, },
cost: [ cost: [
{ {
key: 'armorStack', type: 'armorSlot',
value: 1 value: 1
} }
], ],

View file

@ -185,13 +185,11 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
prepareRoll() { prepareRoll() {
const roll = { const roll = {
modifiers: this.modifiers, baseModifiers: this.roll.getModifier(),
trait: this.roll?.trait,
label: 'Attack', label: 'Attack',
type: this.actionType, type: this.actionType,
difficulty: this.roll?.difficulty, difficulty: this.roll?.difficulty,
formula: this.roll.getFormula(), formula: this.roll.getFormula(),
bonus: this.roll.bonus,
advantage: CONFIG.DH.ACTIONS.advantageState[this.roll.advState].value advantage: CONFIG.DH.ACTIONS.advantageState[this.roll.advState].value
}; };
if (this.roll?.type === 'diceSet') roll.lite = true; if (this.roll?.type === 'diceSet') roll.lite = true;
@ -205,6 +203,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
async consume(config) { async consume(config) {
const usefulResources = foundry.utils.deepClone(this.actor.system.resources); const usefulResources = foundry.utils.deepClone(this.actor.system.resources);
for (var cost of config.costs) { for (var cost of config.costs) {
if (cost.keyIsID) { if (cost.keyIsID) {
usefulResources[cost.key] = { usefulResources[cost.key] = {
@ -225,19 +224,15 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
keyIsID: resource.keyIsID keyIsID: resource.keyIsID
}; };
}); });
console.log(resources);
await this.actor.modifyResource(resources); await this.actor.modifyResource(resources);
if (config.uses?.enabled) { if (config.uses?.enabled) this.update({ 'uses.value': this.uses.value + 1 });
const newActions = foundry.utils.getProperty(this.item.system, this.systemPath).map(x => x.toObject());
newActions[this.index].uses.value++;
await this.item.update({ [`system.${this.systemPath}`]: newActions });
}
} }
/* */ /* */
/* ROLL */ /* ROLL */
get hasRoll() { get hasRoll() {
return !!this.roll?.type || !!this.roll?.bonus; return !!this.roll?.type;
} }
get modifiers() { get modifiers() {
@ -299,26 +294,37 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
/* EFFECTS */ /* EFFECTS */
/* SAVE */ /* SAVE */
async rollSave(target, event, message) { async rollSave(actor, event, message) {
if (!target?.actor) return; if (!actor) return;
return target.actor return actor.diceRoll({
.diceRoll({ event,
event, title: 'Roll Save',
title: 'Roll Save', roll: {
roll: { trait: this.save.trait,
trait: this.save.trait, difficulty: this.save.difficulty ?? this.actor?.baseSaveDifficulty,
difficulty: this.save.difficulty ?? this.actor?.baseSaveDifficulty, type: 'reaction'
type: 'reaction' },
}, data: actor.getRollData()
data: target.actor.getRollData() });
}) }
.then(async result => {
if (result) updateSaveMessage(result, message, targetId) {
this.updateChatMessage(message, target.id, { const updateMsg = this.updateChatMessage.bind(this, message, targetId, {
result: result.roll.total, result: result.roll.total,
success: result.roll.success success: result.roll.success
}); });
}); if (game.modules.get('dice-so-nice')?.active)
game.dice3d.waitFor3DAnimationByMessageID(result.message.id ?? result.message._id).then(() => updateMsg());
else updateMsg();
}
static rollSaveQuery({ actionId, actorId, event, message }) {
return new Promise(async (resolve, reject) => {
const actor = await fromUuid(actorId),
action = await fromUuid(actionId);
if (!actor || !actor?.isOwner) reject();
action.rollSave(actor, event, message).then(result => resolve(result));
});
} }
/* SAVE */ /* SAVE */
@ -333,7 +339,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
if (chain) { if (chain) {
if (message.system.source.message) if (message.system.source.message)
this.updateChatMessage(ui.chat.collection.get(message.system.source.message), targetId, changes, false); this.updateChatMessage(ui.chat.collection.get(message.system.source.message), targetId, changes, false);
const relatedChatMessages = ui.chat.collection.filter(c => c.system.source.message === message._id); const relatedChatMessages = ui.chat.collection.filter(c => c.system.source?.message === message._id);
relatedChatMessages.forEach(c => { relatedChatMessages.forEach(c => {
this.updateChatMessage(c, targetId, changes, false); this.updateChatMessage(c, targetId, changes, false);
}); });

View file

@ -5,12 +5,14 @@ const resistanceField = (resistanceLabel, immunityLabel, reductionLabel) =>
resistance: new foundry.data.fields.BooleanField({ resistance: new foundry.data.fields.BooleanField({
initial: false, initial: false,
label: `${resistanceLabel}.label`, label: `${resistanceLabel}.label`,
hint: `${resistanceLabel}.hint` hint: `${resistanceLabel}.hint`,
isAttributeChoice: true
}), }),
immunity: new foundry.data.fields.BooleanField({ immunity: new foundry.data.fields.BooleanField({
initial: false, initial: false,
label: `${immunityLabel}.label`, label: `${immunityLabel}.label`,
hint: `${immunityLabel}.hint` hint: `${immunityLabel}.hint`,
isAttributeChoice: true
}), }),
reduction: new foundry.data.fields.NumberField({ reduction: new foundry.data.fields.NumberField({
integer: true, integer: true,

View file

@ -532,7 +532,7 @@ export default class DhCharacter extends BaseDataActor {
this.evasion += selection.value; this.evasion += selection.value;
break; break;
case 'proficiency': case 'proficiency':
this.proficiency = selection.value; this.proficiency += selection.value;
break; break;
case 'experience': case 'experience':
Object.keys(this.experiences).forEach(key => { Object.keys(this.experiences).forEach(key => {
@ -563,6 +563,12 @@ export default class DhCharacter extends BaseDataActor {
this.resources.hope.value = Math.min(baseHope, this.resources.hope.max); this.resources.hope.value = Math.min(baseHope, this.resources.hope.max);
this.attack.roll.trait = this.rules.attack.roll.trait ?? this.attack.roll.trait; this.attack.roll.trait = this.rules.attack.roll.trait ?? this.attack.roll.trait;
this.resources.armor = {
value: this.armor.system.marks.value,
max: this.armorScore,
isReversed: true
};
this.attack.damage.parts[0].value.custom.formula = `@prof${this.basicAttackDamageDice}${this.rules.attack.damage.bonus ? ` + ${this.rules.attack.damage.bonus}` : ''}`; this.attack.damage.parts[0].value.custom.formula = `@prof${this.basicAttackDamageDice}${this.rules.attack.damage.bonus ? ` + ${this.rules.attack.damage.bonus}` : ''}`;
} }

View file

@ -26,11 +26,20 @@ export default class CostField extends fields.ArrayField {
} }
static calcCosts(costs) { static calcCosts(costs) {
console.log(costs, CostField.getResources.call(this, costs));
const resources = CostField.getResources.call(this, costs);
return costs.map(c => { return costs.map(c => {
c.scale = c.scale ?? 1; c.scale = c.scale ?? 1;
c.step = c.step ?? 1; c.step = c.step ?? 1;
c.total = c.value * c.scale * c.step; c.total = c.value + (c.scale - 1) * c.step;
c.enabled = c.hasOwnProperty('enabled') ? c.enabled : true; c.enabled = c.hasOwnProperty('enabled') ? c.enabled : true;
c.max =
c.key === 'fear'
? game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear)
: resources[c.key].isReversed
? resources[c.key].max
: resources[c.key].value;
if (c.scalable) c.maxStep = Math.floor(c.max / c.step);
return c; return c;
}); });
} }
@ -51,9 +60,11 @@ export default class CostField extends fields.ArrayField {
const resources = CostField.getResources.call(this, realCosts); const resources = CostField.getResources.call(this, realCosts);
return realCosts.reduce( return realCosts.reduce(
(a, c) => (a, c) =>
a && resources[c.key].isReversed !resources[c.key]
? resources[c.key].value + (c.total ?? c.value) <= resources[c.key].max ? a
: resources[c.key]?.value >= (c.total ?? c.value), : a && resources[c.key].isReversed
? resources[c.key].value + (c.total ?? c.value) <= resources[c.key].max
: resources[c.key]?.value >= (c.total ?? c.value),
true true
); );
} }
@ -61,10 +72,11 @@ export default class CostField extends fields.ArrayField {
static getResources(costs) { static getResources(costs) {
const actorResources = this.actor.system.resources; const actorResources = this.actor.system.resources;
const itemResources = {}; const itemResources = {};
for (var itemResource of costs) { for (let itemResource of costs) {
if (itemResource.keyIsID) { if (itemResource.keyIsID) {
itemResources[itemResource.key] = { itemResources[itemResource.key] = {
value: this.parent.resource.value ?? 0 value: this.parent.resource.value ?? 0,
max: CostField.formatMax.call(this, this.parent?.resource?.max)
}; };
} }
} }
@ -79,4 +91,13 @@ export default class CostField extends fields.ArrayField {
const realCosts = costs?.length ? costs.filter(c => c.enabled) : []; const realCosts = costs?.length ? costs.filter(c => c.enabled) : [];
return realCosts; return realCosts;
} }
static formatMax(max) {
max ??= 0;
if (isNaN(max)) {
const roll = Roll.replaceFormulaData(max, this.getRollData());
max = roll.total;
}
return Number(max);
}
} }

View file

@ -66,6 +66,43 @@ export class DHActionRollData extends foundry.abstract.DataModel {
} }
return formula; return formula;
} }
getModifier() {
const modifiers = [];
if (!this.parent?.actor) return modifiers;
switch (this.parent.actor.type) {
case 'character':
const trait =
this.useDefault || !this.trait
? (this.parent.item.system.attack.roll.trait ?? 'agility')
: this.trait;
if (
this.type === CONFIG.DH.GENERAL.rollTypes.attack.id ||
this.type === CONFIG.DH.GENERAL.rollTypes.trait.id
)
modifiers.push({
label: `DAGGERHEART.CONFIG.Traits.${trait}.name`,
value: this.parent.actor.system.traits[trait].value
});
else if (this.type === CONFIG.DH.GENERAL.rollTypes.spellcast.id)
modifiers.push({
label: `DAGGERHEART.CONFIG.RollTypes.spellcast.name`,
value: this.parent.actor.system.spellcastModifier
});
break;
case 'companion':
case 'adversary':
if (this.type === CONFIG.DH.GENERAL.rollTypes.attack.id)
modifiers.push({
label: 'Bonus to Hit',
value: this.bonus ?? this.parent.actor.system.attack.roll.bonus
});
break;
default:
break;
}
return modifiers;
}
} }
export default class RollField extends fields.EmbeddedDataField { export default class RollField extends fields.EmbeddedDataField {

View file

@ -1,10 +1,12 @@
import FormulaField from '../formulaField.mjs';
const fields = foundry.data.fields; const fields = foundry.data.fields;
export default class UsesField extends fields.SchemaField { export default class UsesField extends fields.SchemaField {
constructor(options = {}, context = {}) { constructor(options = {}, context = {}) {
const usesFields = { const usesFields = {
value: new fields.NumberField({ nullable: true, initial: null }), value: new fields.NumberField({ nullable: true, initial: null }),
max: new fields.NumberField({ nullable: true, initial: null }), max: new FormulaField({ nullable: true, initial: null, deterministic: true }),
recovery: new fields.StringField({ recovery: new fields.StringField({
choices: CONFIG.DH.GENERAL.refreshTypes, choices: CONFIG.DH.GENERAL.refreshTypes,
initial: null, initial: null,
@ -33,6 +35,11 @@ export default class UsesField extends fields.SchemaField {
static hasUses(uses) { static hasUses(uses) {
if (!uses) return true; if (!uses) return true;
return (uses.hasOwnProperty('enabled') && !uses.enabled) || uses.value + 1 <= uses.max; let max = uses.max ?? 0;
if (isNaN(max)) {
const roll = new Roll(Roll.replaceFormulaData(uses.max, this.getRollData())).evaluateSync();
max = roll.total;
}
return (uses.hasOwnProperty('enabled') && !uses.enabled) || uses.value + 1 <= max;
} }
} }

View file

@ -10,6 +10,7 @@
import { addLinkedItemsDiff, updateLinkedItemApps } from '../../helpers/utils.mjs'; import { addLinkedItemsDiff, updateLinkedItemApps } from '../../helpers/utils.mjs';
import { ActionsField } from '../fields/actionField.mjs'; import { ActionsField } from '../fields/actionField.mjs';
import FormulaField from '../fields/formulaField.mjs';
const fields = foundry.data.fields; const fields = foundry.data.fields;
@ -48,7 +49,7 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
initial: CONFIG.DH.ITEM.itemResourceTypes.simple initial: CONFIG.DH.ITEM.itemResourceTypes.simple
}), }),
value: new fields.NumberField({ integer: true, min: 0, initial: 0 }), value: new fields.NumberField({ integer: true, min: 0, initial: 0 }),
max: new fields.StringField({ nullable: true, initial: null }), max: new FormulaField({ nullable: true, initial: null, deterministic: true }),
icon: new fields.StringField(), icon: new fields.StringField(),
recovery: new fields.StringField({ recovery: new fields.StringField({
choices: CONFIG.DH.GENERAL.refreshTypes, choices: CONFIG.DH.GENERAL.refreshTypes,

View file

@ -124,13 +124,7 @@ export default class D20Roll extends DHRoll {
} }
applyBaseBonus() { applyBaseBonus() {
const modifiers = []; const modifiers = foundry.utils.deepClone(this.options.roll.baseModifiers) ?? [];
if (this.options.roll.bonus)
modifiers.push({
label: 'Bonus to Hit',
value: this.options.roll.bonus
});
modifiers.push(...this.getBonus(`roll.${this.options.type}`, `${this.options.type?.capitalize()} Bonus`)); modifiers.push(...this.getBonus(`roll.${this.options.type}`, `${this.options.type?.capitalize()} Bonus`));
modifiers.push( modifiers.push(

View file

@ -12,7 +12,7 @@ export default class DamageRoll extends DHRoll {
static async buildEvaluate(roll, config = {}, message = {}) { static async buildEvaluate(roll, config = {}, message = {}) {
if (config.evaluate !== false) { if (config.evaluate !== false) {
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) await roll.roll.evaluate();
} }
roll._evaluated = true; roll._evaluated = true;
@ -87,7 +87,7 @@ export default class DamageRoll extends DHRoll {
options = part ?? this.options; options = part ?? this.options;
modifiers.push(...this.getBonus(`${type}`, `${type.capitalize()} Bonus`)); modifiers.push(...this.getBonus(`${type}`, `${type.capitalize()} Bonus`));
if(!this.options.isHealing) { if (!this.options.isHealing) {
options.damageTypes?.forEach(t => { options.damageTypes?.forEach(t => {
modifiers.push(...this.getBonus(`${type}.${t}`, `${t.capitalize()} ${type.capitalize()} Bonus`)); modifiers.push(...this.getBonus(`${type}.${t}`, `${t.capitalize()} ${type.capitalize()} Bonus`));
}); });

View file

@ -193,10 +193,12 @@ export const registerRollDiceHooks = () => {
if (config.roll.isCritical) updates.push({ key: 'stress', value: -1 }); if (config.roll.isCritical) updates.push({ key: 'stress', value: -1 });
if (config.roll.result.duality === -1) updates.push({ key: 'fear', value: 1 }); if (config.roll.result.duality === -1) updates.push({ key: 'fear', value: 1 });
if (config.rerolledRoll.isCritical || config.rerolledRoll.result.duality === 1) if (config.rerolledRoll) {
updates.push({ key: 'hope', value: -1 }); if (config.rerolledRoll.isCritical || config.rerolledRoll.result.duality === 1)
if (config.rerolledRoll.isCritical) updates.push({ key: 'stress', value: 1 }); updates.push({ key: 'hope', value: -1 });
if (config.rerolledRoll.result.duality === -1) updates.push({ key: 'fear', value: -1 }); if (config.rerolledRoll.isCritical) updates.push({ key: 'stress', value: 1 });
if (config.rerolledRoll.result.duality === -1) updates.push({ key: 'fear', value: -1 });
}
if (updates.length) { if (updates.length) {
const target = actor.system.partner ?? actor; const target = actor.system.partner ?? actor;

View file

@ -1,5 +1,4 @@
import { emitAsGM, GMUpdateEvent } from '../systemRegistration/socket.mjs'; import { emitAsGM, GMUpdateEvent } from '../systemRegistration/socket.mjs';
import DamageReductionDialog from '../applications/dialogs/damageReductionDialog.mjs';
import { LevelOptionType } from '../data/levelTier.mjs'; import { LevelOptionType } from '../data/levelTier.mjs';
import DHFeature from '../data/item/feature.mjs'; import DHFeature from '../data/item/feature.mjs';
import { damageKeyToNumber } from '../helpers/utils.mjs'; import { damageKeyToNumber } from '../helpers/utils.mjs';
@ -482,16 +481,22 @@ export default class DhpActor extends Actor {
this.system.armor && this.system.armor &&
this.#canReduceDamage(hpDamage.value, hpDamage.damageTypes) this.#canReduceDamage(hpDamage.value, hpDamage.damageTypes)
) { ) {
const armorStackResult = await this.owner.query('armorStack', { const armorSlotResult = await this.owner.query(
actorId: this.uuid, 'armorSlot',
damage: hpDamage.value, {
type: [...hpDamage.damageTypes] actorId: this.uuid,
}); damage: hpDamage.value,
if (armorStackResult) { type: [...hpDamage.damageTypes]
const { modifiedDamage, armorSpent, stressSpent } = armorStackResult; },
{
timeout: 30000
}
);
if (armorSlotResult) {
const { modifiedDamage, armorSpent, stressSpent } = armorSlotResult;
updates.find(u => u.key === 'hitPoints').value = modifiedDamage; updates.find(u => u.key === 'hitPoints').value = modifiedDamage;
updates.push( updates.push(
...(armorSpent ? [{ value: armorSpent, key: 'armorStack' }] : []), ...(armorSpent ? [{ value: armorSpent, key: 'armor' }] : []),
...(stressSpent ? [{ value: stressSpent, key: 'stress' }] : []) ...(stressSpent ? [{ value: stressSpent, key: 'stress' }] : [])
); );
} }
@ -566,6 +571,7 @@ export default class DhpActor extends Actor {
armor: { target: this.system.armor, resources: {} }, armor: { target: this.system.armor, resources: {} },
items: {} items: {}
}; };
resources.forEach(r => { resources.forEach(r => {
if (r.keyIsID) { if (r.keyIsID) {
updates.items[r.key] = { updates.items[r.key] = {
@ -581,7 +587,7 @@ export default class DhpActor extends Actor {
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear) + r.value game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear) + r.value
); );
break; break;
case 'armorStack': case 'armor':
updates.armor.resources['system.marks.value'] = Math.max( updates.armor.resources['system.marks.value'] = Math.max(
Math.min(this.system.armor.system.marks.value + r.value, this.system.armorScore), Math.min(this.system.armor.system.marks.value + r.value, this.system.armorScore),
0 0
@ -638,7 +644,3 @@ export default class DhpActor extends Actor {
}); });
} }
} }
export const registerDHActorHooks = () => {
CONFIG.queries.armorStack = DamageReductionDialog.armorStackQuery;
};

View file

@ -52,6 +52,8 @@ export default class DHToken extends TokenDocument {
for (const [name, field] of Object.entries(schema.fields)) { for (const [name, field] of Object.entries(schema.fields)) {
const p = _path.concat([name]); const p = _path.concat([name]);
if (field instanceof foundry.data.fields.NumberField) attributes.value.push(p); if (field instanceof foundry.data.fields.NumberField) attributes.value.push(p);
if (field instanceof foundry.data.fields.BooleanField && field.options.isAttributeChoice)
attributes.value.push(p);
if (field instanceof foundry.data.fields.StringField) attributes.value.push(p); if (field instanceof foundry.data.fields.StringField) attributes.value.push(p);
if (field instanceof foundry.data.fields.ArrayField) attributes.value.push(p); if (field instanceof foundry.data.fields.ArrayField) attributes.value.push(p);
const isSchema = field instanceof foundry.data.fields.SchemaField; const isSchema = field instanceof foundry.data.fields.SchemaField;

View file

@ -7,6 +7,7 @@ export default class RegisterHandlebarsHelpers {
includes: this.includes, includes: this.includes,
times: this.times, times: this.times,
damageFormula: this.damageFormula, damageFormula: this.damageFormula,
formulaValue: this.formulaValue,
damageSymbols: this.damageSymbols, damageSymbols: this.damageSymbols,
rollParsed: this.rollParsed, rollParsed: this.rollParsed,
hasProperty: foundry.utils.hasProperty, hasProperty: foundry.utils.hasProperty,
@ -39,6 +40,15 @@ export default class RegisterHandlebarsHelpers {
return instances.join(traitTotal > 0 ? ' + ' : ' - '); return instances.join(traitTotal > 0 ? ' + ' : ' - ');
} }
static formulaValue(formula, item) {
if (isNaN(formula)) {
const data = item.getRollData.bind(item)(),
roll = new Roll(Roll.replaceFormulaData(formula, data)).evaluateSync();
formula = roll.total;
}
return formula;
}
static damageSymbols(damageParts) { static damageSymbols(damageParts) {
const symbols = [...new Set(damageParts.reduce((a, c) => a.concat([...c.type]), []))].map( const symbols = [...new Set(damageParts.reduce((a, c) => a.concat([...c.type]), []))].map(
p => CONFIG.DH.GENERAL.damageTypes[p].icon p => CONFIG.DH.GENERAL.damageTypes[p].icon

View file

@ -1,3 +1,5 @@
import DamageReductionDialog from '../applications/dialogs/damageReductionDialog.mjs';
export function handleSocketEvent({ action = null, data = {} } = {}) { export function handleSocketEvent({ action = null, data = {} } = {}) {
switch (action) { switch (action) {
case socketEvent.GMUpdate: case socketEvent.GMUpdate:
@ -21,7 +23,8 @@ export const socketEvent = {
export const GMUpdateEvent = { export const GMUpdateEvent = {
UpdateDocument: 'DhGMUpdateDocument', UpdateDocument: 'DhGMUpdateDocument',
UpdateSetting: 'DhGMUpdateSetting', UpdateSetting: 'DhGMUpdateSetting',
UpdateFear: 'DhGMUpdateFear' UpdateFear: 'DhGMUpdateFear',
UpdateSaveMessage: 'DhGMUpdateSaveMessage'
}; };
export const RefreshType = { export const RefreshType = {
@ -53,8 +56,12 @@ export const registerSocketHooks = () => {
) )
) )
); );
/* Hooks.callAll(socketEvent.DhpFearUpdate); break;
await game.socket.emit(`system.${CONFIG.DH.id}`, { action: socketEvent.DhpFearUpdate }); */ case GMUpdateEvent.UpdateSaveMessage:
const action = await fromUuid(data.update.action),
message = game.messages.get(data.update.message);
if (!action || !message) return;
action.updateSaveMessage(data.update.result, message, data.update.token);
break; break;
} }
@ -69,6 +76,11 @@ export const registerSocketHooks = () => {
}); });
}; };
export const registerUserQueries = () => {
CONFIG.queries.armorSlot = DamageReductionDialog.armorSlotQuery;
CONFIG.queries.reactionRoll = game.system.api.models.actions.actionsTypes.base.rollSaveQuery;
};
export const emitAsGM = async (eventName, callback, update, uuid = null) => { export const emitAsGM = async (eventName, callback, update, uuid = null) => {
if (!game.user.isGM) { if (!game.user.isGM) {
return await game.socket.emit(`system.${CONFIG.DH.id}`, { return await game.socket.emit(`system.${CONFIG.DH.id}`, {

View file

@ -6,7 +6,7 @@
{{#each source as |cost index|}} {{#each source as |cost index|}}
<div class="nest-inputs"> <div class="nest-inputs">
{{formField ../fields.scalable label="Scalable" value=cost.scalable name=(concat "cost." index ".scalable") classes="checkbox"}} {{formField ../fields.scalable label="Scalable" value=cost.scalable name=(concat "cost." index ".scalable") classes="checkbox"}}
{{formField ../fields.key choices=(@root.disableOption index @root.costOptions ../source) label="Resource" value=cost.key name=(concat "cost." index ".key") localize=true}} {{formField ../fields.key choices=(@root.disableOption index @root.costOptions ../source) label="Resource" value=cost.key name=(concat "cost." index ".key") localize=true blank=false}}
{{formField ../fields.value label="Amount" value=cost.value name=(concat "cost." index ".value")}} {{formField ../fields.value label="Amount" value=cost.value name=(concat "cost." index ".value")}}
{{formField ../fields.step label="Step" value=cost.step name=(concat "cost." index ".step") disabled=(not cost.scalable)}} {{formField ../fields.step label="Step" value=cost.step name=(concat "cost." index ".step") disabled=(not cost.scalable)}}
<a class="btn" data-tooltip="{{localize "CONTROLS.CommonDelete"}}" data-action="removeElement" data-index="{{index}}"><i class="fas fa-trash"></i></a> <a class="btn" data-tooltip="{{localize "CONTROLS.CommonDelete"}}" data-action="removeElement" data-index="{{index}}"><i class="fas fa-trash"></i></a>

View file

@ -3,25 +3,27 @@
Roll Roll
{{#if @root.hasBaseDamage}}{{formInput fields.useDefault name="roll.useDefault" value=source.useDefault dataset=(object tooltip="Use default Item values" tooltipDirection="UP")}}{{/if}} {{#if @root.hasBaseDamage}}{{formInput fields.useDefault name="roll.useDefault" value=source.useDefault dataset=(object tooltip="Use default Item values" tooltipDirection="UP")}}{{/if}}
</legend> </legend>
{{#if @root.isNPC}}
{{formField fields.bonus label="Bonus" name="roll.bonus" value=source.bonus}} {{formField fields.type label="Type" name="roll.type" value=source.type localize=true choices=@root.getRollTypeOptions}}
{{formField fields.advState label= "Advantage State" name="roll.advState" value=source.advState localize=true}} {{#if (eq source.type "diceSet")}}
<div class="nest-inputs">
{{formField fields.diceRolling.fields.multiplier name="roll.diceRolling.multiplier" value=source.diceRolling.multiplier localize=true}}
{{#if (eq source.diceRolling.multiplier 'flat')}}{{formField fields.diceRolling.fields.flatMultiplier value=source.diceRolling.flatMultiplier name="roll.diceRolling.flatMultiplier" localize=true }}{{/if}}
{{formField fields.diceRolling.fields.dice name="roll.diceRolling.dice" value=source.diceRolling.dice localize=true}}
{{formField fields.diceRolling.fields.compare name="roll.diceRolling.compare" value=source.diceRolling.compare localize=true blank=""}}
{{formField fields.diceRolling.fields.treshold name="roll.diceRolling.treshold" value=source.diceRolling.treshold localize=true}}
</div>
{{else}} {{else}}
{{formField fields.type label="Type" name="roll.type" value=source.type localize=true}} <div class="nest-inputs">
{{#if (eq source.type "diceSet")}} {{#unless (eq source.type 'spellcast')}}
<div class="nest-inputs"> {{#if @root.isNPC}}
{{formField fields.diceRolling.fields.multiplier name="roll.diceRolling.multiplier" value=source.diceRolling.multiplier localize=true}} {{formField fields.bonus label="Bonus" name="roll.bonus" value=source.bonus placeholder=@root.baseAttackBonus disabled=(not source.type)}}
{{#if (eq source.diceRolling.multiplier 'flat')}}{{formField fields.diceRolling.fields.flatMultiplier value=source.diceRolling.flatMultiplier name="roll.diceRolling.flatMultiplier" localize=true }}{{/if}} {{else}}
{{formField fields.diceRolling.fields.dice name="roll.diceRolling.dice" value=source.diceRolling.dice localize=true}} {{formField fields.trait label="Trait" name="roll.trait" value=source.trait localize=true disabled=(not source.type)}}
{{formField fields.diceRolling.fields.compare name="roll.diceRolling.compare" value=source.diceRolling.compare localize=true blank=""}} {{/if}}
{{formField fields.diceRolling.fields.treshold name="roll.diceRolling.treshold" value=source.diceRolling.treshold localize=true}} {{/unless}}
</div> {{formField fields.difficulty label="Difficulty" name="roll.difficulty" value=source.difficulty disabled=(not source.type)}}
{{else}} {{formField fields.advState label= "Advantage State" name="roll.advState" value=source.advState localize=true disabled=(not source.type)}}
<div class="nest-inputs"> </div>
{{formField fields.trait label="Trait" name="roll.trait" value=source.trait localize=true disabled=(not source.type)}}
{{formField fields.difficulty label="Difficulty" name="roll.difficulty" value=source.difficulty disabled=(not source.type)}}
{{formField fields.advState label= "Advantage State" name="roll.advState" value=source.advState localize=true}}
</div>
{{/if}}
{{/if}} {{/if}}
</fieldset> </fieldset>

View file

@ -8,8 +8,8 @@
<input name="uses.enabled" type="checkbox"{{#if uses.enabled}} checked{{/if}}> <input name="uses.enabled" type="checkbox"{{#if uses.enabled}} checked{{/if}}>
<label for="uses.enabled">Uses</label> <label for="uses.enabled">Uses</label>
</div> </div>
</div> </div>{{log @root}}
<label class="modifier-label">{{uses.value}}/{{uses.max}}</label> <label class="modifier-label">{{uses.value}}/{{formulaValue uses.max @root.rollConfig.data}}</label>
</li> </li>
{{/if}} {{/if}}
{{#each costs as | cost index |}} {{#each costs as | cost index |}}
@ -20,10 +20,10 @@
<label>{{label}}</label> <label>{{label}}</label>
</div> </div>
</div> </div>
{{#if scalable}} {{#if (and scalable (gt maxStep 1))}}
<input type="range" value="{{scale}}" min="1" max="10" step="{{step}}" name="costs.{{index}}.scale"> <input type="range" value="{{scale}}" min="1" max="{{maxStep}}" step="1" name="costs.{{index}}.scale">
{{/if}} {{/if}}
<label class="modifier-label">{{total}}/10</label> <label class="modifier-label">{{total}}/{{max}}</label>
</li> </li>
{{/each}} {{/each}}
</ul> </ul>

View file

@ -0,0 +1,3 @@
<div>
Reaction Roll
</div>

View file

@ -2,7 +2,6 @@
<h2 class="tooltip-title">{{item.name}}</h2> <h2 class="tooltip-title">{{item.name}}</h2>
<img class="tooltip-image" src="{{item.img}}" /> <img class="tooltip-image" src="{{item.img}}" />
<div class="tooltip-description">{{{description}}}</div> <div class="tooltip-description">{{{description}}}</div>
{{#if item.uses.max}} {{#if item.uses.max}}
<h4 class="tooltip-sub-title">{{localize "DAGGERHEART.GENERAL.uses"}}</h4> <h4 class="tooltip-sub-title">{{localize "DAGGERHEART.GENERAL.uses"}}</h4>
<div class="tooltip-information-section triple spaced"> <div class="tooltip-information-section triple spaced">
@ -12,7 +11,7 @@
</div> </div>
<div class="tooltip-information"> <div class="tooltip-information">
<label>{{localize "DAGGERHEART.GENERAL.max"}}</label> <label>{{localize "DAGGERHEART.GENERAL.max"}}</label>
<div>{{item.uses.max}}</div> <div>{{formulaValue item.uses.max item}}</div>
</div> </div>
<div class="tooltip-information"> <div class="tooltip-information">
<label>{{localize "DAGGERHEART.GENERAL.recovery"}}</label> <label>{{localize "DAGGERHEART.GENERAL.recovery"}}</label>