[Feature] Reload Check (#2051)
Some checks are pending
Project CI / build (24.x) (push) Waiting to run

This commit is contained in:
WBHarry 2026-07-21 02:08:52 +02:00 committed by GitHub
parent 8d68166e4c
commit 26bcc2dddc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 223 additions and 14 deletions

View file

@ -126,6 +126,10 @@
"damageOnSave": "Damage on Save", "damageOnSave": "Damage on Save",
"useDefaultItemValues": "Use default Item values" "useDefaultItemValues": "Use default Item values"
}, },
"Reload": {
"checkReload": "Check Reload",
"reloadRequired": "Reload Required!"
},
"RollField": { "RollField": {
"diceRolling": { "diceRolling": {
"compare": "Should be", "compare": "Should be",
@ -1322,6 +1326,11 @@
"short": "V. Far" "short": "V. Far"
} }
}, },
"ReloadChoices": {
"off": { "label": "Don't Use" },
"button": { "label": "Use button" },
"auto": { "label": "Automatic" }
},
"RollTypes": { "RollTypes": {
"trait": { "trait": {
"name": "Trait" "name": "Trait"
@ -2221,7 +2230,9 @@
}, },
"Resource": { "Resource": {
"single": "Resource", "single": "Resource",
"plural": "Resources" "plural": "Resources",
"unloaded": "The weapon is not loaded",
"loaded": "The weapon is loaded"
}, },
"Roll": { "Roll": {
"attack": "Attack Roll", "attack": "Attack Roll",
@ -2797,6 +2808,10 @@
"hint": "Effects with defined range dependency will automatically turn on/off depending on range" "hint": "Effects with defined range dependency will automatically turn on/off depending on range"
} }
}, },
"reload": {
"label": "Reload Checking",
"hint": "If the system should present a button for checking if the character will need to reload or do it automatically"
},
"resourceScrollTexts": { "resourceScrollTexts": {
"label": "Show Resource Change Scrolltexts", "label": "Show Resource Change Scrolltexts",
"hint": "When a character is damaged, uses armor etc, a scrolling text will briefly appear by the token to signify this." "hint": "When a character is damaged, uses armor etc, a scrolling text will briefly appear by the token to signify this."
@ -3258,7 +3273,8 @@
"knowTheTide": "Know The Tide gained a token", "knowTheTide": "Know The Tide gained a token",
"lackingItemTransferPermission": "User {user} lacks owner permission needed to transfer items to {target}", "lackingItemTransferPermission": "User {user} lacks owner permission needed to transfer items to {target}",
"noTokenTargeted": "No token is targeted", "noTokenTargeted": "No token is targeted",
"behaviorRegionRequiresGM": "Creating a Region with an attached Behavior requires an online GM" "behaviorRegionRequiresGM": "Creating a Region with an attached Behavior requires an online GM",
"reloadRequired": "The {weapon} must be reloaded to be used!"
}, },
"Progress": { "Progress": {
"migrationLabel": "Performing system migration. Please wait and do not close Foundry." "migrationLabel": "Performing system migration. Please wait and do not close Foundry."

View file

@ -3,7 +3,7 @@ import DhDeathMove from '../../dialogs/deathMove.mjs';
import { CharacterLevelup, LevelupViewMode } from '../../levelup/_module.mjs'; import { CharacterLevelup, LevelupViewMode } from '../../levelup/_module.mjs';
import DhCharacterCreation from '../../characterCreation/characterCreation.mjs'; import DhCharacterCreation from '../../characterCreation/characterCreation.mjs';
import FilterMenu from '../../ux/filter-menu.mjs'; import FilterMenu from '../../ux/filter-menu.mjs';
import { getArmorSources, getDocFromElement, getDocFromElementSync, sortBy } from '../../../helpers/utils.mjs'; import { getArmorSources, getDocFromElement, getDocFromElementSync, itemAbleRollParse, sortBy } from '../../../helpers/utils.mjs';
/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */ /**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */
@ -29,6 +29,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
toggleResourceDice: CharacterSheet.#toggleResourceDice, toggleResourceDice: CharacterSheet.#toggleResourceDice,
handleResourceDice: CharacterSheet.#handleResourceDice, handleResourceDice: CharacterSheet.#handleResourceDice,
advanceResourceDie: CharacterSheet.#advanceResourceDie, advanceResourceDie: CharacterSheet.#advanceResourceDie,
toggleItemReload: CharacterSheet.#onToggleItemReload,
cancelBeastform: CharacterSheet.#cancelBeastform, cancelBeastform: CharacterSheet.#cancelBeastform,
toggleResourceManagement: CharacterSheet.#toggleResourceManagement, toggleResourceManagement: CharacterSheet.#toggleResourceManagement,
useDowntime: this.useDowntime, useDowntime: this.useDowntime,
@ -954,11 +955,21 @@ export default class CharacterSheet extends DHBaseActorSheet {
}); });
} }
/** */
static #advanceResourceDie(_, target) { static #advanceResourceDie(_, target) {
this.updateResourceDie(target, true); this.updateResourceDie(target, true);
} }
static async #onToggleItemReload(_, target) {
const item = await getDocFromElement(target);
if (!item || !item.system.resource?.max)
return;
await item.update({
'system.resource.value': item.system.needsReload ?
itemAbleRollParse(item.system.resource.max, this.document, item) : 0
})
}
lowerResourceDie(event) { lowerResourceDie(event) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();

View file

@ -152,6 +152,9 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
html.querySelectorAll('.risk-it-all-button').forEach(element => html.querySelectorAll('.risk-it-all-button').forEach(element =>
element.addEventListener('click', event => this.riskItAllClearStressAndHitPoints(event, data)) element.addEventListener('click', event => this.riskItAllClearStressAndHitPoints(event, data))
); );
for (const element of html.querySelectorAll('.roll-reload-check')) {
element.addEventListener('click', event => this.onRollReloadCheck(event, message));
}
}; };
setupHooks() { setupHooks() {
@ -275,4 +278,10 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
const actor = game.actors.get(event.target.dataset.actorId); const actor = game.actors.get(event.target.dataset.actorId);
new game.system.api.applications.dialogs.RiskItAllDialog(actor, resourceValue).render({ force: true }); new game.system.api.applications.dialogs.RiskItAllDialog(actor, resourceValue).render({ force: true });
} }
async onRollReloadCheck(_event, messageData) {
const message = game.messages.get(messageData._id);
const needsReload = await message.system.action.handleReload?.({ awaitRoll: true });
await message.update({ 'system.needsReload': needsReload });
}
} }

View file

@ -62,3 +62,18 @@ export const actionAutomationChoices = {
label: 'DAGGERHEART.CONFIG.ActionAutomationChoices.always' label: 'DAGGERHEART.CONFIG.ActionAutomationChoices.always'
} }
}; };
export const reloadChoices = {
off: {
id: 'off',
label: 'DAGGERHEART.CONFIG.ReloadChoices.off.label'
},
button: {
id: 'button',
label: 'DAGGERHEART.CONFIG.ReloadChoices.button.label'
},
auto: {
id: 'auto',
label: 'DAGGERHEART.CONFIG.ReloadChoices.auto.label'
}
};

View file

@ -52,6 +52,10 @@ export default class DHAttackAction extends DHDamageAction {
} }
async use(event, options) { async use(event, options) {
if (this.item?.system.needsReload) {
return ui.notifications.error(_loc('DAGGERHEART.UI.Notifications.reloadRequired', { weapon: this.item.name }));
}
const result = await super.use(event, options); const result = await super.use(event, options);
if (result?.message?.system.action?.roll?.type === 'attack') { if (result?.message?.system.action?.roll?.type === 'attack') {
@ -62,6 +66,23 @@ export default class DHAttackAction extends DHDamageAction {
return result; return result;
} }
async handleReload(options = { awaitRoll: false }) {
const roll = await new Roll('1d6').evaluate();
if (game.modules.get('dice-so-nice')?.active) {
if (options.awaitRoll)
await game.dice3d.showForRoll(roll, game.user, true);
else
game.dice3d.showForRoll(roll, game.user, true);
}
const needsToReload = roll.total === 1;
if (needsToReload) {
this.item.update({ 'system.resource.value': 0 });
}
return needsToReload;
}
/** /**
* Generate a localized label array for this item subtype. * Generate a localized label array for this item subtype.
* @returns {(string | { value: string, icons: string[] })[]} An array of localized strings and damage label objects. * @returns {(string | { value: string, icons: string[] })[]} An array of localized strings and damage label objects.

View file

@ -42,6 +42,7 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
hasEffect: new fields.BooleanField({ initial: false }), hasEffect: new fields.BooleanField({ initial: false }),
hasSave: new fields.BooleanField({ initial: false }), hasSave: new fields.BooleanField({ initial: false }),
hasTarget: new fields.BooleanField({ initial: false }), hasTarget: new fields.BooleanField({ initial: false }),
needsReload: new fields.BooleanField({ initial: false }),
isDirect: new fields.BooleanField({ initial: false }), isDirect: new fields.BooleanField({ initial: false }),
onSave: new fields.StringField(), onSave: new fields.StringField(),
source: new fields.SchemaField({ source: new fields.SchemaField({

View file

@ -116,6 +116,14 @@ export default class DHWeapon extends AttachableItem {
return this.weaponFeatures; return this.weaponFeatures;
} }
get hasReload() {
return Boolean(this.weaponFeatures.find(x => x.value === 'reloading'));
}
get needsReload() {
return this.hasReload && this.resource.value === 0;
}
/**@inheritdoc */ /**@inheritdoc */
async getDescriptionData() { async getDescriptionData() {
const baseDescription = this.description; const baseDescription = this.description;

View file

@ -196,6 +196,13 @@ export default class DhAutomation extends foundry.abstract.DataModel {
}) })
}) })
}), }),
reload: new fields.StringField({
required: true,
choices: CONFIG.DH.SETTINGS.reloadChoices,
initial: CONFIG.DH.SETTINGS.reloadChoices.button.id,
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.reload.label',
hint: 'DAGGERHEART.SETTINGS.Automation.FIELDS.reload.hint'
}),
autoExpireActiveEffects: new fields.BooleanField({ autoExpireActiveEffects: new fields.BooleanField({
required: true, required: true,
initial: true, initial: true,

View file

@ -117,7 +117,11 @@ export default class DHRoll extends BaseRoll {
static async toMessage(roll, config) { static async toMessage(roll, config) {
const item = config.data.parent?.items?.get?.(config.source.item) ?? null; const item = config.data.parent?.items?.get?.(config.source.item) ?? null;
const action = item ? item.system.actions.get(config.source.action) : null; const actions = item ? [
...item.system.actions,
...(item.system.attack?.id === config.source.action ? [item.system.attack] : [])
] : [];
const action = actions.find(x => x.id === config.source.action);
let actionDescription = null; let actionDescription = null;
if (action?.chatDisplay) { if (action?.chatDisplay) {
actionDescription = action actionDescription = action
@ -129,6 +133,14 @@ export default class DHRoll extends BaseRoll {
config.actionChatMessageHandled = true; config.actionChatMessageHandled = true;
} }
const reloadSetting =
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).reload;
const useReload =
item?.system.hasReload &&
action?.type === 'attack' &&
reloadSetting === CONFIG.DH.SETTINGS.reloadChoices.auto.id;
const needsReload = useReload ? await action?.handleReload?.() : false;
const cls = getDocumentClass('ChatMessage'), const cls = getDocumentClass('ChatMessage'),
msgData = { msgData = {
type: this.messageType, type: this.messageType,
@ -136,7 +148,7 @@ export default class DHRoll extends BaseRoll {
title: roll.title, title: roll.title,
speaker: cls.getSpeaker({ actor: roll.data?.parent }), speaker: cls.getSpeaker({ actor: roll.data?.parent }),
sound: config.mute ? null : CONFIG.sounds.dice, sound: config.mute ? null : CONFIG.sounds.dice,
system: { ...config, actionDescription }, system: { ...config, actionDescription, needsReload },
rolls: [roll] rolls: [roll]
}; };
@ -158,14 +170,17 @@ export default class DHRoll extends BaseRoll {
if (!this._evaluated) return; if (!this._evaluated) return;
const metagamingSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Metagaming); const metagamingSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Metagaming);
const automationSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation);
const chatData = await this._prepareChatRenderContext({ flavor, isPrivate, ...options }); const chatData = await this._prepareChatRenderContext({ flavor, isPrivate, ...options });
return foundry.applications.handlebars.renderTemplate(template, { return foundry.applications.handlebars.renderTemplate(template, {
roll: this, roll: this,
...chatData, ...chatData,
action: chatData.action,
parent: chatData.parent, parent: chatData.parent,
targetMode: chatData.targetMode, targetMode: chatData.targetMode,
areas: chatData.action?.areas, areas: chatData.action?.areas,
metagamingSettings metagamingSettings,
automationSettings
}); });
} }

View file

@ -128,6 +128,15 @@
"source": "Daggerheart SRD", "source": "Daggerheart SRD",
"page": 48, "page": 48,
"artist": "" "artist": ""
},
"resource": {
"type": "simple",
"value": 1,
"max": "1",
"recovery": null,
"progression": "decreasing",
"dieFaces": "d4",
"icon": "fa-solid fa-gun"
} }
}, },
"effects": [], "effects": [],

View file

@ -128,6 +128,15 @@
"source": "Daggerheart SRD", "source": "Daggerheart SRD",
"page": 46, "page": 46,
"artist": "" "artist": ""
},
"resource": {
"type": "simple",
"value": 1,
"max": "1",
"recovery": null,
"progression": "decreasing",
"dieFaces": "d4",
"icon": "fa-solid fa-gun"
} }
}, },
"effects": [], "effects": [],

View file

@ -128,6 +128,15 @@
"source": "Daggerheart SRD", "source": "Daggerheart SRD",
"page": 50, "page": 50,
"artist": "" "artist": ""
},
"resource": {
"type": "simple",
"value": 1,
"max": "1",
"recovery": null,
"progression": "decreasing",
"dieFaces": "d4",
"icon": "fa-solid fa-gun"
} }
}, },
"effects": [], "effects": [],

View file

@ -128,6 +128,15 @@
"source": "Daggerheart SRD", "source": "Daggerheart SRD",
"page": 49, "page": 49,
"artist": "" "artist": ""
},
"resource": {
"type": "simple",
"value": 1,
"max": "1",
"recovery": null,
"progression": "decreasing",
"dieFaces": "d4",
"icon": "fa-solid fa-gun"
} }
}, },
"effects": [], "effects": [],

View file

@ -128,6 +128,15 @@
"source": "Daggerheart SRD", "source": "Daggerheart SRD",
"page": 51, "page": 51,
"artist": "" "artist": ""
},
"resource": {
"type": "simple",
"value": 1,
"max": "1",
"recovery": null,
"progression": "decreasing",
"dieFaces": "d4",
"icon": "fa-solid fa-gun"
} }
}, },
"effects": [], "effects": [],

View file

@ -144,6 +144,10 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; gap: 4px;
.unloaded {
opacity: 0.5;
}
} }
} }

View file

@ -133,6 +133,11 @@
height: 40px; height: 40px;
flex: 1 1 calc(50% - 5px); flex: 1 1 calc(50% - 5px);
span {
font-family: @font-body;
font-weight: 700;
}
&:nth-last-child(1):nth-child(odd) { &:nth-last-child(1):nth-child(odd) {
flex-basis: 100%; flex-basis: 100%;
} }

View file

@ -46,9 +46,14 @@
padding: 0 8px; padding: 0 8px;
button { button {
height: 40px; height: 36px;
flex: 1 1 calc(50% - 5px); flex: 1 1 calc(50% - 5px);
span {
font-family: @font-body;
font-weight: 700;
}
&:nth-last-child(1):nth-child(odd) { &:nth-last-child(1):nth-child(odd) {
flex-basis: 100%; flex-basis: 100%;
} }

View file

@ -271,6 +271,28 @@
} }
} }
.roll-reload-container {
text-align: center;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
.reload-warning {
display: flex;
align-items: center;
border-radius: 5px;
width: fit-content;
gap: 5px;
cursor: pointer;
padding: 5px;
transition: all 0.3s ease;
color: @beige;
background: @red-40;
outline: 1px solid @red;
}
}
.roll-part-extra { .roll-part-extra {
display: flex; display: flex;
justify-content: center; justify-content: center;
@ -622,15 +644,19 @@
.roll-buttons { .roll-buttons {
display: flex; display: flex;
flex-wrap: wrap;
gap: 5px; gap: 5px;
width: 100%;
margin-top: 8px; margin-top: 8px;
button { button {
height: 32px; height: 36px;
flex: 1; flex: 1 1 calc(50% - 5px);
font-family: @font-body;
font-weight: 700;
&.end-button { &:nth-last-child(1):nth-child(odd) {
flex: 0; flex-basis: 100%;
} }
} }
} }

View file

@ -18,6 +18,7 @@
<p class="hint">{{localize (concat "DAGGERHEART.SETTINGS.Automation.FIELDS.roll." field.name ".hint")}}</p> <p class="hint">{{localize (concat "DAGGERHEART.SETTINGS.Automation.FIELDS.roll." field.name ".hint")}}</p>
</div> </div>
{{/each}} {{/each}}
{{formGroup settingFields.schema.fields.reload value=settingFields.reload localize=true}}
</fieldset> </fieldset>
<fieldset> <fieldset>

View file

@ -1,7 +1,13 @@
{{#if (eq item.system.resource.type 'simple')}} {{#if (eq item.system.resource.type 'simple')}}
<div class="item-resource"> <div class="item-resource">
<i class="{{#if item.system.resource.icon}}{{item.system.resource.icon}}{{else}}fa-solid fa-hashtag{{/if}}"></i> {{#if item.system.hasReload}}
<input type="number" id="{{item.uuid}}-resource" class="inventory-item-resource" value="{{item.system.resource.value}}" min="0" max="{{rollParsed item.system.resource.max item.actor item true}}" /> <a data-action="toggleItemReload" title="{{localize (ifThen item.system.needsReload 'DAGGERHEART.GENERAL.Resource.unloaded' 'DAGGERHEART.GENERAL.Resource.loaded')}}">
<i class="fa-solid fa-gun {{#if item.system.needsReload}}unloaded{{/if}}"></i>
</a>
{{else}}
<i class="{{#if item.system.resource.icon}}{{item.system.resource.icon}}{{else}}fa-solid fa-hashtag{{/if}}"></i>
<input type="number" id="{{item.uuid}}-resource" class="inventory-item-resource" value="{{item.system.resource.value}}" min="0" max="{{rollParsed item.system.resource.max item.actor item true}}" />
{{/if}}
</div> </div>
{{else if (eq item.system.resource.type 'diceValue')}} {{else if (eq item.system.resource.type 'diceValue')}}
<div class="item-resources"> <div class="item-resources">

View file

@ -1,4 +1,7 @@
<div class="roll-buttons"> <div class="roll-buttons">
{{#if (eq automationSettings.reload 'button')}}
<button class="roll-reload-check" {{#if needsReload}}disabled{{/if}}>{{localize "DAGGERHEART.ACTIONS.Reload.checkReload"}}</button>
{{/if}}
{{#if areas.length}}<button class="action-areas end-button"><i class="fa-solid fa-crosshairs"></i></button>{{/if}} {{#if areas.length}}<button class="action-areas end-button"><i class="fa-solid fa-crosshairs"></i></button>{{/if}}
{{#if hasDamage}} {{#if hasDamage}}
{{#if damage.active}} {{#if damage.active}}

View file

@ -1,5 +1,16 @@
<div class="chat-roll"> <div class="chat-roll">
<div class="roll-part-title"><span>{{title}}</span></div> <div class="roll-part-title"><span>{{title}}</span></div>
{{#if (eq action.type 'attack')}}
{{#if needsReload}}
<div class="roll-reload-container">
{{#if needsReload}}
<p class='reload-warning'>{{localize "DAGGERHEART.ACTIONS.Reload.reloadRequired"}}</p>
{{/if}}
</div>
{{/if}}
{{/if}}
{{#if actionDescription}}{{> 'systems/daggerheart/templates/ui/chat/parts/description-part.hbs'}}{{/if}} {{#if actionDescription}}{{> 'systems/daggerheart/templates/ui/chat/parts/description-part.hbs'}}{{/if}}
{{#if hasRoll}} {{#if hasRoll}}
<div class="roll-part-header"><span>{{localize "Result"}}</span></div> <div class="roll-part-header"><span>{{localize "Result"}}</span></div>