Merge branch 'main' into feature/action-areas

This commit is contained in:
Carlos Fernandez 2026-04-20 20:19:58 -04:00
commit f348b64aae
53 changed files with 1240 additions and 288 deletions

View file

@ -259,8 +259,9 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
const resetValue = increasing
? 0
: feature.system.resource.max
? Roll.replaceFormulaData(feature.system.resource.max, this.actor)
? new Roll(Roll.replaceFormulaData(feature.system.resource.max, this.actor)).evaluateSync().total
: 0;
await feature.update({ 'system.resource.value': resetValue });
}

View file

@ -12,8 +12,6 @@ export default class CharacterSheet extends DHBaseActorSheet {
static DEFAULT_OPTIONS = {
classes: ['character'],
position: { width: 850, height: 800 },
/* Foundry adds disabled to all buttons and inputs if editPermission is missing. This is not desired. */
editPermission: CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER,
actions: {
toggleVault: CharacterSheet.#toggleVault,
rollAttribute: CharacterSheet.#rollAttribute,
@ -68,7 +66,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
}
},
{
handler: CharacterSheet.#getEquipamentContextOptions,
handler: CharacterSheet.#getEquipmentContextOptions,
selector: '[data-item-uuid][data-type="armor"], [data-item-uuid][data-type="weapon"]',
options: {
parentClassHooks: false,
@ -170,6 +168,16 @@ export default class CharacterSheet extends DHBaseActorSheet {
return applicationOptions;
}
/** @inheritdoc */
_toggleDisabled(disabled) {
// Overriden to only disable text inputs by default.
// Everything else is done by checking @root.editable in the sheet
const form = this.form;
for (const input of form.querySelectorAll('input:not([type=search]), .editor.prosemirror')) {
input.disabled = disabled;
}
}
/** @inheritDoc */
async _onRender(context, options) {
await super._onRender(context, options);
@ -315,11 +323,11 @@ export default class CharacterSheet extends DHBaseActorSheet {
/**@type {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} */
const options = [
{
name: 'toLoadout',
label: 'toLoadout',
icon: 'fa-solid fa-arrow-up',
condition: target => {
visible: target => {
const doc = getDocFromElementSync(target);
return doc && doc.system.inVault;
return doc?.isOwner && doc.system.inVault;
},
callback: async target => {
const doc = await getDocFromElement(target);
@ -329,11 +337,11 @@ export default class CharacterSheet extends DHBaseActorSheet {
}
},
{
name: 'recall',
label: 'recall',
icon: 'fa-solid fa-bolt-lightning',
condition: target => {
visible: target => {
const doc = getDocFromElementSync(target);
return doc && doc.system.inVault;
return doc?.isOwner && doc.system.inVault;
},
callback: async (target, event) => {
const doc = await getDocFromElement(target);
@ -368,17 +376,17 @@ export default class CharacterSheet extends DHBaseActorSheet {
}
},
{
name: 'toVault',
label: 'toVault',
icon: 'fa-solid fa-arrow-down',
condition: target => {
visible: target => {
const doc = getDocFromElementSync(target);
return doc && !doc.system.inVault;
return doc?.isOwner && !doc.system.inVault;
},
callback: async target => (await getDocFromElement(target)).update({ 'system.inVault': true })
}
].map(option => ({
...option,
name: `DAGGERHEART.APPLICATIONS.ContextMenu.${option.name}`,
label: `DAGGERHEART.APPLICATIONS.ContextMenu.${option.label}`,
icon: `<i class="${option.icon}"></i>`
}));
@ -391,29 +399,29 @@ export default class CharacterSheet extends DHBaseActorSheet {
* @this {CharacterSheet}
* @protected
*/
static #getEquipamentContextOptions() {
static #getEquipmentContextOptions() {
const options = [
{
name: 'equip',
label: 'equip',
icon: 'fa-solid fa-hands',
condition: target => {
visible: target => {
const doc = getDocFromElementSync(target);
return doc && !doc.system.equipped;
return doc.isOwner && doc && !doc.system.equipped;
},
callback: (target, event) => CharacterSheet.#toggleEquipItem.call(this, event, target)
},
{
name: 'unequip',
label: 'unequip',
icon: 'fa-solid fa-hands',
condition: target => {
visible: target => {
const doc = getDocFromElementSync(target);
return doc && doc.system.equipped;
return doc.isOwner && doc && doc.system.equipped;
},
callback: (target, event) => CharacterSheet.#toggleEquipItem.call(this, event, target)
}
].map(option => ({
...option,
name: `DAGGERHEART.APPLICATIONS.ContextMenu.${option.name}`,
label: `DAGGERHEART.APPLICATIONS.ContextMenu.${option.label}`,
icon: `<i class="${option.icon}"></i>`
}));

View file

@ -418,18 +418,18 @@ export default function DHApplicationMixin(Base) {
/**@type {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} */
const options = [
{
name: 'disableEffect',
label: 'disableEffect',
icon: 'fa-solid fa-lightbulb',
condition: element => {
visible: element => {
const target = element.closest('[data-item-uuid]');
return !target.dataset.disabled && target.dataset.itemType !== 'beastform';
},
callback: async target => (await getDocFromElement(target)).update({ disabled: true })
},
{
name: 'enableEffect',
label: 'enableEffect',
icon: 'fa-regular fa-lightbulb',
condition: element => {
visible: element => {
const target = element.closest('[data-item-uuid]');
return target.dataset.disabled && target.dataset.itemType !== 'beastform';
},
@ -437,7 +437,7 @@ export default function DHApplicationMixin(Base) {
}
].map(option => ({
...option,
name: `DAGGERHEART.APPLICATIONS.ContextMenu.${option.name}`,
label: `DAGGERHEART.APPLICATIONS.ContextMenu.${option.label}`,
icon: `<i class="${option.icon}"></i>`
}));
@ -468,14 +468,14 @@ export default function DHApplicationMixin(Base) {
_getContextMenuCommonOptions({ usable = false, toChat = false, deletable = true }) {
const options = [
{
name: 'CONTROLS.CommonEdit',
label: 'CONTROLS.CommonEdit',
icon: 'fa-solid fa-pen-to-square',
condition: target => {
visible: target => {
const { dataset } = target.closest('[data-item-uuid]');
const doc = getDocFromElementSync(target);
return (
(!dataset.noCompendiumEdit && !doc) ||
(doc && (!doc?.hasOwnProperty('systemPath') || doc?.inCollection))
(doc?.isOwner && (!doc?.hasOwnProperty('systemPath') || doc?.inCollection))
);
},
callback: async target => (await getDocFromElement(target)).sheet.render({ force: true })
@ -484,14 +484,14 @@ export default function DHApplicationMixin(Base) {
if (usable) {
options.unshift({
name: 'DAGGERHEART.GENERAL.damage',
label: 'DAGGERHEART.GENERAL.damage',
icon: 'fa-solid fa-explosion',
condition: target => {
visible: target => {
const doc = getDocFromElementSync(target);
return (
const hasDamage =
!foundry.utils.isEmpty(doc?.system?.attack?.damage.parts) ||
!foundry.utils.isEmpty(doc?.damage?.parts)
);
!foundry.utils.isEmpty(doc?.damage?.parts);
return doc?.isOwner && hasDamage;
},
callback: async (target, event) => {
const doc = await getDocFromElement(target),
@ -507,11 +507,11 @@ export default function DHApplicationMixin(Base) {
});
options.unshift({
name: 'DAGGERHEART.APPLICATIONS.ContextMenu.useItem',
label: 'DAGGERHEART.APPLICATIONS.ContextMenu.useItem',
icon: 'fa-solid fa-burst',
condition: target => {
visible: target => {
const doc = getDocFromElementSync(target);
return doc && !(doc.type === 'domainCard' && doc.system.inVault);
return doc?.isOwner && !(doc.type === 'domainCard' && doc.system.inVault);
},
callback: async (target, event) => (await getDocFromElement(target)).use(event)
});
@ -519,18 +519,19 @@ export default function DHApplicationMixin(Base) {
if (toChat)
options.push({
name: 'DAGGERHEART.APPLICATIONS.ContextMenu.sendToChat',
label: 'DAGGERHEART.APPLICATIONS.ContextMenu.sendToChat',
icon: 'fa-solid fa-message',
callback: async target => (await getDocFromElement(target)).toChat(this.document.uuid)
});
if (deletable)
options.push({
name: 'CONTROLS.CommonDelete',
label: 'CONTROLS.CommonDelete',
icon: 'fa-solid fa-trash',
condition: element => {
visible: element => {
const target = element.closest('[data-item-uuid]');
return target.dataset.itemType !== 'beastform';
const doc = getDocFromElementSync(target);
return doc?.isOwner && target.dataset.itemType !== 'beastform';
},
callback: async (target, event) => {
const doc = await getDocFromElement(target);

View file

@ -31,7 +31,7 @@ export default class FeatureSheet extends DHBaseItemSheet {
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
}
};
//Might be wrong location but testing out if here is okay.
//Might be wrong location but testing out if here is okay.
/**@override */
async _prepareContext(options) {
const context = await super._prepareContext(options);

View file

@ -48,9 +48,9 @@ export default class DhActorDirectory extends foundry.applications.sidebar.tabs.
const options = super._getEntryContextOptions();
options.push(
{
name: 'DAGGERHEART.UI.Sidebar.actorDirectory.duplicateToNewTier',
label: 'DAGGERHEART.UI.Sidebar.actorDirectory.duplicateToNewTier',
icon: `<i class="fa-solid fa-arrow-trend-up" inert></i>`,
condition: li => {
visible: li => {
const actor = game.actors.get(li.dataset.entryId);
return actor?.type === 'adversary' && actor.system.type !== 'social';
},
@ -92,9 +92,9 @@ export default class DhActorDirectory extends foundry.applications.sidebar.tabs.
}
},
{
name: 'DAGGERHEART.UI.Sidebar.actorDirectory.activateParty',
label: 'DAGGERHEART.UI.Sidebar.actorDirectory.activateParty',
icon: `<i class="fa-regular fa-square"></i>`,
condition: li => {
visible: li => {
const actor = game.actors.get(li.dataset.entryId);
return actor && actor.type === 'party' && !actor.system.active;
},

View file

@ -103,23 +103,10 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
_getEntryContextOptions() {
return [
...super._getEntryContextOptions(),
// {
// name: 'Reroll',
// icon: '<i class="fa-solid fa-dice"></i>',
// condition: li => {
// const message = game.messages.get(li.dataset.messageId);
// return (game.user.isGM || message.isAuthor) && message.rolls.length > 0;
// },
// callback: li => {
// const message = game.messages.get(li.dataset.messageId);
// new game.system.api.applications.dialogs.RerollDialog(message).render({ force: true });
// }
// },
{
name: game.i18n.localize('DAGGERHEART.UI.ChatLog.rerollDamage'),
label: 'DAGGERHEART.UI.ChatLog.rerollDamage',
icon: '<i class="fa-solid fa-dice"></i>',
condition: li => {
visible: li => {
const message = game.messages.get(li.dataset.messageId);
const hasRolledDamage = message.system.hasDamage
? Object.keys(message.system.damage).length > 0

View file

@ -84,15 +84,15 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
_getCombatContextOptions() {
return [
{
name: 'COMBAT.ClearMovementHistories',
label: 'COMBAT.ClearMovementHistories',
icon: '<i class="fa-solid fa-shoe-prints"></i>',
condition: () => game.user.isGM && this.viewed?.combatants.size > 0,
visible: () => game.user.isGM && this.viewed?.combatants.size > 0,
callback: () => this.viewed.clearMovementHistories()
},
{
name: 'COMBAT.Delete',
label: 'COMBAT.Delete',
icon: '<i class="fa-solid fa-trash"></i>',
condition: () => game.user.isGM && !!this.viewed,
visible: () => game.user.isGM && !!this.viewed,
callback: () => this.viewed.endCombat()
}
];

View file

@ -75,12 +75,17 @@ export const typeConfig = {
{
key: 'type',
label: 'DAGGERHEART.GENERAL.type',
format: type => type ? `TYPES.Item.${type}` : '-'
format: type => (type ? `TYPES.Item.${type}` : '-')
},
{
key: 'system.secondary',
label: 'DAGGERHEART.UI.ItemBrowser.subtype',
format: isSecondary => (isSecondary ? 'DAGGERHEART.ITEMS.Weapon.secondaryWeapon.short' : isSecondary === false ? 'DAGGERHEART.ITEMS.Weapon.primaryWeapon.short' : '-')
format: isSecondary =>
isSecondary
? 'DAGGERHEART.ITEMS.Weapon.secondaryWeapon.short'
: isSecondary === false
? 'DAGGERHEART.ITEMS.Weapon.primaryWeapon.short'
: '-'
},
{
key: 'system.tier',
@ -260,12 +265,12 @@ export const typeConfig = {
{
key: 'system.type',
label: 'DAGGERHEART.GENERAL.type',
format: type => type ? `DAGGERHEART.CONFIG.DomainCardTypes.${type}` : '-'
format: type => (type ? `DAGGERHEART.CONFIG.DomainCardTypes.${type}` : '-')
},
{
key: 'system.domain',
label: 'DAGGERHEART.GENERAL.Domain.single',
format: domain => domain ? CONFIG.DH.DOMAIN.allDomains()[domain].label : '-'
format: domain => (domain ? CONFIG.DH.DOMAIN.allDomains()[domain].label : '-')
},
{
key: 'system.level',

View file

@ -42,7 +42,7 @@ export const gameSettings = {
SpotlightRequestQueue: 'SpotlightRequestQueue',
CompendiumBrowserSettings: 'CompendiumBrowserSettings',
SpotlightTracker: 'SpotlightTracker',
ActiveParty: 'ActiveParty',
ActiveParty: 'ActiveParty'
};
export const actionAutomationChoices = {

View file

@ -13,7 +13,7 @@ export default class DHAttackAction extends DHDamageAction {
if (!!this.item?.system?.attack) {
if (this.damage.includeBase) {
const baseDamage = this.getParentDamage();
this.damage.parts.unshift(new DHDamageData(baseDamage));
this.damage.parts.hitPoints = new DHDamageData(baseDamage);
}
if (this.roll.useDefault) {
this.roll.trait = this.item.system.attack.roll.trait;

View file

@ -110,6 +110,11 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
return this._id;
}
/** Returns true if the current user is the owner of the containing item */
get isOwner() {
return this.item?.isOwner ?? true;
}
/**
* Return Item the action is attached too.
*/
@ -143,6 +148,12 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
: null;
}
/** Returns true if the action is usable */
get usable() {
const actor = this.actor;
return this.isOwner && actor?.type === 'character';
}
static getRollType(parent) {
return 'trait';
}

View file

@ -20,7 +20,7 @@ export default class DhCharacter extends DhCreature {
settingSheet: DHCharacterSettings,
isNPC: false,
hasInventory: true,
quantifiable: ["loot", "consumable"]
quantifiable: ['loot', 'consumable']
});
}
@ -302,7 +302,7 @@ export default class DhCharacter extends DhCreature {
choices: CONFIG.DH.GENERAL.dieFaces,
initial: null,
label: 'DAGGERHEART.ACTORS.Character.defaultDisadvantageDice'
}),
})
})
})
};
@ -449,7 +449,7 @@ export default class DhCharacter extends DhCreature {
/* All items are valid on characters */
isItemValid() {
return true;
return true;
}
/** @inheritDoc */

View file

@ -78,7 +78,7 @@ export default class DhCompanion extends DhCreature {
choices: CONFIG.DH.GENERAL.dieFaces,
initial: null,
label: 'DAGGERHEART.ACTORS.Character.defaultDisadvantageDice'
}),
})
})
}),
attack: new ActionField({

View file

@ -9,7 +9,7 @@ export default class DhParty extends BaseDataActor {
static get metadata() {
return foundry.utils.mergeObject(super.metadata, {
hasInventory: true,
quantifiable: ["weapon", "armor", "loot", "consumable"]
quantifiable: ['weapon', 'armor', 'loot', 'consumable']
});
}

View file

@ -40,9 +40,7 @@ export default class DHSummonField extends fields.ArrayField {
const roll = new Roll(itemAbleRollParse(summon.count, this.actor, this.item));
await roll.evaluate();
const count = roll.total;
if (!roll.isDeterministic && game.modules.get('dice-so-nice')?.active)
rolls.push(roll);
if (!roll.isDeterministic && game.modules.get('dice-so-nice')?.active) rolls.push(roll);
const actor = await DHSummonField.getWorldActor(await foundry.utils.fromUuid(summon.actorUUID));
/* Extending summon data in memory so it's available in actionField.toChat. Think it's harmless, but ugly. Could maybe find a better way. */

View file

@ -108,6 +108,8 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
}
get actionsList() {
// No actions on non-characters
if (this.actor && this.actor.type !== 'character') return [];
return this.actions;
}

View file

@ -99,7 +99,9 @@ export default class DHWeapon extends AttachableItem {
/* -------------------------------------------- */
get actionsList() {
return [this.attack, ...this.actions];
// No actions on non-characters
if (this.actor && this.actor.type !== 'character') return [];
return [this.attack, ...super.actionsList];
}
get customActions() {

View file

@ -11,7 +11,9 @@ export default class DualityRoll extends D20Roll {
this.rallyChoices = this.setRallyChoices();
this.guaranteedCritical = options.guaranteedCritical;
const advantageFaces = data.rules?.roll?.defaultAdvantageDice ? Number.parseInt(data.rules.roll.defaultAdvantageDice) : 6
const advantageFaces = data.rules?.roll?.defaultAdvantageDice
? Number.parseInt(data.rules.roll.defaultAdvantageDice)
: 6;
this.advantageFaces = Number.isNaN(advantageFaces) ? 6 : advantageFaces;
}

View file

@ -200,7 +200,6 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
static effectSafeEval(expression) {
let result;
try {
// eslint-disable-next-line no-new-func
const evl = new Function('sandbox', `with (sandbox) { return ${expression}}`);
result = evl(Roll.MATH_PROXY);
} catch (err) {

View file

@ -602,7 +602,7 @@ export default class DhpActor extends Actor {
rollData.system = this.system.getRollData();
rollData.prof = this.system.proficiency ?? 1;
rollData.cast = this.system.spellcastModifier ?? 1;
return rollData;
}

View file

@ -3,7 +3,7 @@ export default class DhActorCollection extends foundry.documents.collections.Act
get party() {
const id = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.ActiveParty);
const actor = game.actors.get(id);
return actor?.type === "party" ? actor : null;
return actor?.type === 'party' ? actor : null;
}
/** Ensure companions are initialized after all other subtypes. */

View file

@ -76,6 +76,13 @@ export default class DHItem extends foundry.documents.Item {
return this.system.metadata.isInventoryItem ?? false;
}
/** Returns true if the item can be used */
get usable() {
const actor = this.actor;
const actionsList = this.system.actionsList;
return this.isOwner && actor?.type === 'character' && (actionsList?.size || actionsList?.length);
}
/** @inheritdoc */
static async createDialog(data = {}, createOptions = {}, options = {}) {
const { folders, types, template, context = {}, ...dialogOptions } = options;

View file

@ -56,10 +56,10 @@ export const registerKeyBindings = () => {
game.keybindings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.keybindings.partySheet, {
name: _loc('DAGGERHEART.SETTINGS.Keybindings.partySheet.name'),
hint: _loc('DAGGERHEART.SETTINGS.Keybindings.partySheet.hint'),
editable: [{ key: "KeyP" }],
editable: [{ key: 'KeyP' }],
onDown: () => {
const controlled = canvas.ready ? canvas.tokens.controlled : [];
const selectedParty = controlled.find((c) => c.actor?.type === 'party')?.actor;
const selectedParty = controlled.find(c => c.actor?.type === 'party')?.actor;
const party = selectedParty ?? game.actors.party;
if (!party) return;
@ -215,6 +215,6 @@ const registerNonConfigSettings = () => {
scope: 'world',
config: false,
type: String,
default: null,
default: null
});
};