Merged with main

This commit is contained in:
WBHarry 2025-07-17 19:42:49 +02:00
commit 38f7390d87
18 changed files with 233 additions and 18 deletions

View file

@ -140,6 +140,7 @@ Hooks.once('init', () => {
CONFIG.Combat.documentClass = documents.DhpCombat; CONFIG.Combat.documentClass = documents.DhpCombat;
CONFIG.ui.combat = applications.ui.DhCombatTracker; CONFIG.ui.combat = applications.ui.DhCombatTracker;
CONFIG.ui.chat = applications.ui.DhChatLog; CONFIG.ui.chat = applications.ui.DhChatLog;
CONFIG.ui.hotbar = applications.ui.DhHotbar;
CONFIG.Token.rulerClass = placeables.DhTokenRuler; CONFIG.Token.rulerClass = placeables.DhTokenRuler;
CONFIG.ui.resources = applications.ui.DhFearTracker; CONFIG.ui.resources = applications.ui.DhFearTracker;

View file

@ -1538,6 +1538,10 @@
"notEnoughStress": "You don't have enough stress", "notEnoughStress": "You don't have enough stress",
"damageIgnore": "{character} did not take damage", "damageIgnore": "{character} did not take damage",
"featureIsMissing": "Feature is missing", "featureIsMissing": "Feature is missing",
"actionIsMissing": "Action is missing",
"attackIsMissing": "Attack is missing",
"unownedActionMacro": "Cannot make a Use macro for an Action not on your character",
"unownedAttackMacro": "Cannot make a Use macro for an Attack that doesn't belong to one of your characters",
"featureNotHope": "This feature is used as something else than a Hope feature and cannot be used here.", "featureNotHope": "This feature is used as something else than a Hope feature and cannot be used here.",
"featureNotClass": "This feature is used as something else than a Class feature and cannot be used here.", "featureNotClass": "This feature is used as something else than a Class feature and cannot be used here.",
"featureNotPrimary": "This feature is used as something else than a Primary feature and cannot be used here.", "featureNotPrimary": "This feature is used as something else than a Primary feature and cannot be used here.",

View file

@ -67,7 +67,7 @@ export default class ResourceDiceDialog extends HandlebarsApplicationMixin(Appli
static async rerollDice() { static async rerollDice() {
const max = itemAbleRollParse(this.item.system.resource.max, this.actor, this.item); const max = itemAbleRollParse(this.item.system.resource.max, this.actor, this.item);
const diceFormula = `${max}d${this.item.system.resource.dieFaces}`; const diceFormula = `${max}${this.item.system.resource.dieFaces}`;
const roll = await new Roll(diceFormula).evaluate(); const roll = await new Roll(diceFormula).evaluate();
if (game.modules.get('dice-so-nice')?.active) await game.dice3d.showForRoll(roll, game.user, true); if (game.modules.get('dice-so-nice')?.active) await game.dice3d.showForRoll(roll, game.user, true);
this.rollValues = roll.terms[0].results.map(x => ({ value: x.result, used: false })); this.rollValues = roll.terms[0].results.map(x => ({ value: x.result, used: false }));

View file

@ -23,7 +23,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
actions: { actions: {
openSettings: DHBaseActorSheet.#openSettings openSettings: DHBaseActorSheet.#openSettings
}, },
dragDrop: [] dragDrop: [{ dragSelector: '.inventory-item[data-type="attack"]', dropSelector: null }]
}; };
/**@type {typeof DHBaseActorSettings}*/ /**@type {typeof DHBaseActorSettings}*/
@ -49,4 +49,27 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
static async #openSettings() { static async #openSettings() {
await this.settingSheet.render({ force: true }); await this.settingSheet.render({ force: true });
} }
/* -------------------------------------------- */
/* Application Drag/Drop */
/* -------------------------------------------- */
/**
* On dragStart on the item.
* @param {DragEvent} event - The drag event
*/
async _onDragStart(event) {
const attackItem = event.currentTarget.closest('.inventory-item[data-type="attack"]');
if (attackItem) {
const attackData = {
type: 'Attack',
actorUuid: this.document.uuid,
img: this.document.system.attack.img,
fromInternal: true
};
event.dataTransfer.setData('text/plain', JSON.stringify(attackData));
event.dataTransfer.setDragImage(attackItem.querySelector('img'), 60, 0);
}
}
} }

View file

@ -30,7 +30,8 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
}, },
dragDrop: [ dragDrop: [
{ dragSelector: null, dropSelector: '.tab.features .drop-section' }, { dragSelector: null, dropSelector: '.tab.features .drop-section' },
{ dragSelector: '.feature-item', dropSelector: null } { dragSelector: '.feature-item', dropSelector: null },
{ dragSelector: '.action-item', dropSelector: null }
] ]
}; };
@ -258,6 +259,23 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
const featureData = { type: 'Item', data: { ...feature.toObject(), _id: null }, fromInternal: true }; const featureData = { type: 'Item', data: { ...feature.toObject(), _id: null }, fromInternal: true };
event.dataTransfer.setData('text/plain', JSON.stringify(featureData)); event.dataTransfer.setData('text/plain', JSON.stringify(featureData));
event.dataTransfer.setDragImage(featureItem.querySelector('img'), 60, 0); event.dataTransfer.setDragImage(featureItem.querySelector('img'), 60, 0);
} else {
const actionItem = event.currentTarget.closest('.action-item');
if (actionItem) {
const action = this.document.system.actions[actionItem.dataset.index];
if (!action) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.actionIsMissing'));
return;
}
const actionData = {
type: 'Action',
data: { ...action.toObject(), id: action.id, itemUuid: this.document.uuid },
fromInternal: true
};
event.dataTransfer.setData('text/plain', JSON.stringify(actionData));
event.dataTransfer.setDragImage(actionItem.querySelector('img'), 60, 0);
}
} }
} }

View file

@ -2,3 +2,4 @@ export { default as DhChatLog } from './chatLog.mjs';
export { default as DhCombatTracker } from './combatTracker.mjs'; export { default as DhCombatTracker } from './combatTracker.mjs';
export * as DhCountdowns from './countdowns.mjs'; export * as DhCountdowns from './countdowns.mjs';
export { default as DhFearTracker } from './fearTracker.mjs'; export { default as DhFearTracker } from './fearTracker.mjs';
export { default as DhHotbar } from './hotbar.mjs';

View file

@ -0,0 +1,129 @@
export default class DhHotbar extends foundry.applications.ui.Hotbar {
constructor(options) {
super(options);
this.setupHooks();
}
static async useItem(uuid) {
const item = await fromUuid(uuid);
if (!item) {
return ui.notifications.warn('WARNING.ObjectDoesNotExist', {
format: {
name: game.i18n.localize('Document'),
identifier: uuid
}
});
}
await item.use({});
}
static async useAction(itemUuid, actionId) {
const item = await foundry.utils.fromUuid(itemUuid);
if (!item) {
return ui.notifications.warn('WARNING.ObjectDoesNotExist', {
format: {
name: game.i18n.localize('Document'),
identifier: itemUuid
}
});
}
const action = item.system.actions.find(x => x.id === actionId);
if (!action) {
return ui.notifications.warn('DAGGERHEART.UI.Notifications.actionIsMissing');
}
await action.use({});
}
static async useAttack(actorUuid) {
const actor = await foundry.utils.fromUuid(actorUuid);
if (!actor) {
return ui.notifications.warn('WARNING.ObjectDoesNotExist', {
format: {
name: game.i18n.localize('Document'),
identifier: actorUuid
}
});
}
const attack = actor.system.attack;
if (!attack) {
return ui.notifications.warn('DAGGERHEART.UI.Notifications.attackIsMissing');
}
await attack.use({});
}
setupHooks() {
Hooks.on('hotbarDrop', (bar, data, slot) => {
if (data.type === 'Item') {
const item = foundry.utils.fromUuidSync(data.uuid);
if (item.uuid.startsWith('Compendium') || !item.isOwned || !item.isOwner) return true;
switch (item.type) {
case 'ancestry':
case 'community':
case 'class':
case 'subclass':
return true;
default:
this.createItemMacro(item, slot);
return false;
}
} else if (data.type === 'Action') {
const item = foundry.utils.fromUuidSync(data.data.itemUuid);
if (item.uuid.startsWith('Compendium')) return true;
if (!item.isOwned || !item.isOwner) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.unownedActionMacro'));
return false;
}
this.createActionMacro(data, slot);
return false;
} else if (data.type === 'Attack') {
const actor = foundry.utils.fromUuidSync(data.actorUuid);
if (actor.uuid.startsWith('Compendium')) return true;
if (!actor.isOwner) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.unownedAttackMacro'));
return false;
}
this.createAttackMacro(data, slot);
return false;
}
});
}
async createItemMacro(data, slot) {
const macro = await Macro.implementation.create({
name: `${game.i18n.localize('Display')} ${name}`,
type: CONST.MACRO_TYPES.SCRIPT,
img: data.img,
command: `await game.system.api.applications.ui.DhHotbar.useItem("${data.uuid}");`
});
await game.user.assignHotbarMacro(macro, slot);
}
async createActionMacro(data, slot) {
const macro = await Macro.implementation.create({
name: `${game.i18n.localize('Display')} ${name}`,
type: CONST.MACRO_TYPES.SCRIPT,
img: data.data.img,
command: `await game.system.api.applications.ui.DhHotbar.useAction("${data.data.itemUuid}", "${data.data.id}");`
});
await game.user.assignHotbarMacro(macro, slot);
}
async createAttackMacro(data, slot) {
const macro = await Macro.implementation.create({
name: `${game.i18n.localize('Display')} ${name}`,
type: CONST.MACRO_TYPES.SCRIPT,
img: data.img,
command: `await game.system.api.applications.ui.DhHotbar.useAttack("${data.actorUuid}");`
});
await game.user.assignHotbarMacro(macro, slot);
}
}

View file

@ -87,6 +87,8 @@ export default class DhCharacter extends BaseDataActor {
value: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }), value: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }),
subclass: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }) subclass: new ForeignDocumentUUIDField({ type: 'Item', nullable: true })
}), }),
advantageSources: new fields.ArrayField(new fields.StringField()),
disadvantageSources: new fields.ArrayField(new fields.StringField()),
levelData: new fields.EmbeddedDataField(DhLevelData), levelData: new fields.EmbeddedDataField(DhLevelData),
bonuses: new fields.SchemaField({ bonuses: new fields.SchemaField({
roll: new fields.SchemaField({ roll: new fields.SchemaField({
@ -247,13 +249,13 @@ export default class DhCharacter extends BaseDataActor {
features = []; features = [];
for (let item of this.parent.items) { for (let item of this.parent.items) {
if (item.system.type === CONFIG.DH.ITEM.featureTypes.ancestry.id) { if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.ancestry.id) {
ancestryFeatures.push(item); ancestryFeatures.push(item);
} else if (item.system.type === CONFIG.DH.ITEM.featureTypes.community.id) { } else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.community.id) {
communityFeatures.push(item); communityFeatures.push(item);
} else if (item.system.type === CONFIG.DH.ITEM.featureTypes.class.id) { } else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.class.id) {
classFeatures.push(item); classFeatures.push(item);
} else if (item.system.type === CONFIG.DH.ITEM.featureTypes.subclass.id) { } else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.subclass.id) {
const subclassState = this.class.subclass.system.featureState; const subclassState = this.class.subclass.system.featureState;
const subType = item.system.subType; const subType = item.system.subType;
if ( if (
@ -263,7 +265,7 @@ export default class DhCharacter extends BaseDataActor {
) { ) {
subclassFeatures.push(item); subclassFeatures.push(item);
} }
} else if (item.system.type === CONFIG.DH.ITEM.featureTypes.companion.id) { } else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.companion.id) {
companionFeatures.push(item); companionFeatures.push(item);
} else if (item.type === 'feature' && !item.system.type) { } else if (item.type === 'feature' && !item.system.type) {
features.push(item); features.push(item);

View file

@ -135,8 +135,8 @@ export const registerCountdownHooks = () => {
if (application) { if (application) {
foundry.applications.instances.get(application)?.render(); foundry.applications.instances.get(application)?.render();
} else { } else {
foundry.applications.instances.get('narrative-countdowns').render(); foundry.applications.instances.get('narrative-countdowns')?.render();
foundry.applications.instances.get('encounter-countdowns').render(); foundry.applications.instances.get('encounter-countdowns')?.render();
} }
return false; return false;

View file

@ -53,11 +53,14 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
}), }),
diceStates: new fields.TypedObjectField( diceStates: new fields.TypedObjectField(
new fields.SchemaField({ new fields.SchemaField({
value: new fields.NumberField({ integer: true, nullable: true, initial: null }), value: new fields.NumberField({ integer: true, initial: 1, min: 1 }),
used: new fields.BooleanField({ initial: false }) used: new fields.BooleanField({ initial: false })
}) })
), ),
dieFaces: new fields.StringField({ initial: '4' }) dieFaces: new fields.StringField({
choices: CONFIG.DH.GENERAL.diceTypes,
initial: CONFIG.DH.GENERAL.diceTypes.d4
})
}, },
{ nullable: true, initial: null } { nullable: true, initial: null }
); );

View file

@ -69,6 +69,7 @@ export default class DHBeastform extends BaseDataItem {
const beastformEffect = this.parent.effects.find(x => x.type === 'beastform'); const beastformEffect = this.parent.effects.find(x => x.type === 'beastform');
await beastformEffect.updateSource({ await beastformEffect.updateSource({
changes: [...beastformEffect.changes, { key: 'system.advantageSources', mode: 2, value: this.advantageOn }],
system: { system: {
characterTokenData: { characterTokenData: {
tokenImg: this.parent.parent.prototypeToken.texture.src, tokenImg: this.parent.parent.prototypeToken.texture.src,

View file

@ -21,6 +21,24 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
this.tooltip.innerHTML = html; this.tooltip.innerHTML = html;
options.direction = this._determineItemTooltipDirection(element); options.direction = this._determineItemTooltipDirection(element);
} }
} else {
const isAdvantage = element.dataset.tooltip?.startsWith('#advantage#');
const isDisadvantage = element.dataset.tooltip?.startsWith('#disadvantage#');
if (isAdvantage || isDisadvantage) {
const actorUuid = element.dataset.tooltip.slice(isAdvantage ? 11 : 14);
const actor = await foundry.utils.fromUuid(actorUuid);
if (actor) {
html = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/advantage.hbs`,
{
sources: isAdvantage ? actor.system.advantageSources : actor.system.disadvantageSources
}
);
this.tooltip.innerHTML = html;
}
}
} }
super.activate(element, { ...options, html: html }); super.activate(element, { ...options, html: html });

View file

@ -100,6 +100,10 @@
font-size: 14px; font-size: 14px;
line-height: 17px; line-height: 17px;
} }
.advantage-chip-tooltip {
pointer-events: all;
}
} }
.advantage-chip { .advantage-chip {

View file

@ -3,14 +3,14 @@
{{#times (rollParsed item.system.resource.max actor item numerical=true)}} {{#times (rollParsed item.system.resource.max actor item numerical=true)}}
{{#with (ifThen (lookup ../diceStates this) (lookup ../diceStates this) this) as | state |}} {{#with (ifThen (lookup ../diceStates this) (lookup ../diceStates this) this) as | state |}}
<div class="resource-item" data-dice="{{#if ../../this}}{{../this}}{{else}}{{state}}{{/if}}"> <div class="resource-item" data-dice="{{#if ../../this}}{{../this}}{{else}}{{state}}{{/if}}">
<input type="number" data-dtype="Number" name={{concat "diceStates." (ifThen ../../this ../this state) ".value" }} value="{{state.value}}" /> <input type="number" data-dtype="Number" name={{concat "diceStates." (ifThen ../../this ../this state) ".value" }} value="{{state.value}}" min="1" />
<img src="{{concat "systems/daggerheart/assets/icons/dice/hope/d" (ifThen ../../item.system.resource.dieFaces ../../item.system.resource.dieFaces ../item.system.resource.dieFaces) ".svg"}}" /> <img src="{{concat "systems/daggerheart/assets/icons/dice/hope/" (ifThen ../../item.system.resource.dieFaces ../../item.system.resource.dieFaces ../item.system.resource.dieFaces) ".svg"}}" />
</div> </div>
{{/with}} {{/with}}
{{/times}} {{/times}}
</div> </div>
<footer> <footer>
<button data-action="save">{{localize 'Save'}}</button> <button type="button" data-action="save">{{localize 'Save'}}</button>
<button data-action="rerollDice">{{localize "DAGGERHEART.APPLICATIONS.ResourceDice.rerollDice"}}</button> <button type="button" data-action="rerollDice">{{localize "DAGGERHEART.APPLICATIONS.ResourceDice.rerollDice"}}</button>
</footer> </footer>
</section> </section>

View file

@ -98,6 +98,9 @@
<span><i class="fa-regular fa-circle"></i></span> <span><i class="fa-regular fa-circle"></i></span>
{{/if}} {{/if}}
<span class="label">{{localize "DAGGERHEART.GENERAL.Advantage.full"}}</span> <span class="label">{{localize "DAGGERHEART.GENERAL.Advantage.full"}}</span>
{{#if @root.rollConfig.data.advantageSources.length}}
<span class="advantage-chip-tooltip" data-tooltip="{{concat "#advantage#" @root.rollConfig.source.actor}}"><i class="fa-solid fa-circle-info"></i></span>
{{/if}}
</button> </button>
<button class="disadvantage-chip flex1 {{#if (eq advantage -1)}}selected{{/if}}" data-action="updateIsAdvantage" data-advantage="-1"> <button class="disadvantage-chip flex1 {{#if (eq advantage -1)}}selected{{/if}}" data-action="updateIsAdvantage" data-advantage="-1">
{{#if (eq advantage -1)}} {{#if (eq advantage -1)}}
@ -106,6 +109,9 @@
<span><i class="fa-regular fa-circle"></i></span> <span><i class="fa-regular fa-circle"></i></span>
{{/if}} {{/if}}
<span class="label">{{localize "DAGGERHEART.GENERAL.Disadvantage.full"}}</span> <span class="label">{{localize "DAGGERHEART.GENERAL.Disadvantage.full"}}</span>
{{#if @root.rollConfig.data.disadvantageSources.length}}
<span class="advantage-chip-tooltip" data-tooltip="{{concat "#disadvantage#" @root.rollConfig.source.actor}}"><i class="fa-solid fa-circle-info"></i></span>
{{/if}}
</button> </button>
</div> </div>
{{#unless (eq @root.rollType 'D20Roll')}} {{#unless (eq @root.rollType 'D20Roll')}}

View file

@ -10,7 +10,7 @@
<a class="item-resource" data-action="toggleResourceDice" data-dice="{{#if ../../this}}{{../this}}{{else}}{{state}}{{/if}}"> <a class="item-resource" data-action="toggleResourceDice" data-dice="{{#if ../../this}}{{../this}}{{else}}{{state}}{{/if}}">
<div class="item-dice-resource"> <div class="item-dice-resource">
<label>{{ifThen state.value state.value '?'}}</label> <label>{{ifThen state.value state.value '?'}}</label>
<img src="{{concat "systems/daggerheart/assets/icons/dice/hope/d" (ifThen ../../item.system.resource.dieFaces ../../item.system.resource.dieFaces ../item.system.resource.dieFaces) ".svg"}}" /> <img src="{{concat "systems/daggerheart/assets/icons/dice/hope/" (ifThen ../../item.system.resource.dieFaces ../../item.system.resource.dieFaces ../item.system.resource.dieFaces) ".svg"}}" />
{{#if state.used}}<i class="fa-solid fa-x"></i>{{/if}} {{#if state.used}}<i class="fa-solid fa-x"></i>{{/if}}
</div> </div>
</a> </a>

View file

@ -19,7 +19,7 @@
{{formGroup systemFields.resource.fields.value value=source.system.resource.value localize=true}} {{formGroup systemFields.resource.fields.value value=source.system.resource.value localize=true}}
{{formGroup systemFields.resource.fields.max value=source.system.resource.max localize=true}} {{formGroup systemFields.resource.fields.max value=source.system.resource.max localize=true}}
{{else}} {{else}}
{{formGroup systemFields.resource.fields.dieFaces value=source.system.resource.dieFaces localize=true}} {{formGroup systemFields.resource.fields.dieFaces value=source.system.resource.dieFaces localize=true blank=false}}
{{formGroup systemFields.resource.fields.max value=source.system.resource.max label="DAGGERHEART.ITEMS.FIELDS.resource.amount.label" localize=true}} {{formGroup systemFields.resource.fields.max value=source.system.resource.max label="DAGGERHEART.ITEMS.FIELDS.resource.amount.label" localize=true}}
{{/if}} {{/if}}
</div> </div>

View file

@ -0,0 +1,5 @@
<div class="daggerheart dh-style tooltip">
{{#each sources as | source |}}
<div>{{{source}}}</div>
{{/each}}
</div>