mirror of
https://github.com/Foundryborne/daggerheart.git
synced 2026-01-12 03:31:07 +01:00
[Feature] Item Resource Support (#328)
* Initial * Resource setup finished * Fixed so that costs can be used * Corrected standard resources * Actions can only use item resources from their parent item * Fixed up dice * Fixed resource dice positioning * Fixed parsing of resource.max * Fixed styling on settings tab * Added manual input for Dice Resources * Lightmode fixes * Fixed Feature spellcasting modifier * Bugfix for item input to resourceDiceDialog * Item fix for TokenInput * PR Fixes
This commit is contained in:
parent
eefa116d9a
commit
4be3e6179c
53 changed files with 972 additions and 329 deletions
|
|
@ -7,3 +7,4 @@ export { default as DamageSelectionDialog } from './damageSelectionDialog.mjs';
|
|||
export { default as DeathMove } from './deathMove.mjs';
|
||||
export { default as Downtime } from './downtime.mjs';
|
||||
export { default as OwnershipSelection } from './ownershipSelection.mjs';
|
||||
export { default as ResourceDiceDialog } from './resourceDiceDialog.mjs';
|
||||
|
|
|
|||
|
|
@ -66,7 +66,12 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
|
|||
context.canRoll = true;
|
||||
if (this.config.costs?.length) {
|
||||
const updatedCosts = this.action.calcCosts(this.config.costs);
|
||||
context.costs = updatedCosts;
|
||||
context.costs = updatedCosts.map(x => ({
|
||||
...x,
|
||||
label: x.keyIsID
|
||||
? this.action.parent.parent.name
|
||||
: game.i18n.localize(CONFIG.DH.GENERAL.abilityCosts[x.key].label)
|
||||
}));
|
||||
context.canRoll = this.action.hasCost(updatedCosts);
|
||||
this.config.data.scale = this.config.costs[0].total;
|
||||
}
|
||||
|
|
@ -74,7 +79,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
|
|||
context.uses = this.action.calcUses(this.config.uses);
|
||||
context.canRoll = context.canRoll && this.action.hasUses(context.uses);
|
||||
}
|
||||
if(this.roll) {
|
||||
if (this.roll) {
|
||||
context.roll = this.roll;
|
||||
context.rollType = this.roll?.constructor.name;
|
||||
context.experiences = Object.keys(this.config.data.experiences).map(id => ({
|
||||
|
|
|
|||
99
module/applications/dialogs/resourceDiceDialog.mjs
Normal file
99
module/applications/dialogs/resourceDiceDialog.mjs
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import { itemAbleRollParse } from '../../helpers/utils.mjs';
|
||||
|
||||
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
|
||||
|
||||
export default class ResourceDiceDialog extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||
constructor(item, actor, options = {}) {
|
||||
super(options);
|
||||
|
||||
this.item = item;
|
||||
this.actor = actor;
|
||||
this.diceStates = foundry.utils.deepClone(item.system.resource.diceStates);
|
||||
}
|
||||
|
||||
static DEFAULT_OPTIONS = {
|
||||
tag: 'form',
|
||||
classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'resource-dice'],
|
||||
window: {
|
||||
icon: 'fa-solid fa-dice'
|
||||
},
|
||||
actions: {
|
||||
rerollDice: this.rerollDice,
|
||||
save: this.save
|
||||
},
|
||||
form: {
|
||||
handler: this.updateResourceDice,
|
||||
submitOnChange: true,
|
||||
submitOnClose: false
|
||||
}
|
||||
};
|
||||
|
||||
/** @override */
|
||||
static PARTS = {
|
||||
resourceDice: {
|
||||
id: 'resourceDice',
|
||||
template: 'systems/daggerheart/templates/dialogs/dice-roll/resourceDice.hbs'
|
||||
}
|
||||
};
|
||||
|
||||
get title() {
|
||||
return game.i18n.format('DAGGERHEART.APPLICATIONS.ResourceDice.title', { name: this.item.name });
|
||||
}
|
||||
|
||||
async _prepareContext(_options) {
|
||||
const context = await super._prepareContext(_options);
|
||||
context.item = this.item;
|
||||
context.actor = this.actor;
|
||||
context.diceStates = this.diceStates;
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
static async updateResourceDice(event, _, formData) {
|
||||
const { diceStates } = foundry.utils.expandObject(formData.object);
|
||||
this.diceStates = Object.keys(diceStates).reduce((acc, key) => {
|
||||
const resourceState = this.item.system.resource.diceStates[key];
|
||||
acc[key] = { ...diceStates[key], used: Boolean(resourceState?.used) };
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
this.render();
|
||||
}
|
||||
|
||||
static async save() {
|
||||
this.rollValues = Object.values(this.diceStates);
|
||||
this.close();
|
||||
}
|
||||
|
||||
static async rerollDice() {
|
||||
const max = itemAbleRollParse(this.item.system.resource.max, this.actor, this.item);
|
||||
const diceFormula = `${max}d${this.item.system.resource.dieFaces}`;
|
||||
const roll = await new Roll(diceFormula).evaluate();
|
||||
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.resetUsed = true;
|
||||
|
||||
const cls = getDocumentClass('ChatMessage');
|
||||
const msg = new cls({
|
||||
user: game.user.id,
|
||||
content: await foundry.applications.handlebars.renderTemplate(
|
||||
'systems/daggerheart/templates/ui/chat/resource-roll.hbs',
|
||||
{
|
||||
user: this.actor.name,
|
||||
name: this.item.name
|
||||
}
|
||||
)
|
||||
});
|
||||
|
||||
cls.create(msg.toObject());
|
||||
this.close();
|
||||
}
|
||||
|
||||
static async create(item, actor, options = {}) {
|
||||
return new Promise(resolve => {
|
||||
const app = new this(item, actor, options);
|
||||
app.addEventListener('close', () => resolve(app.rollValues), { once: true });
|
||||
app.render({ force: true });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -107,6 +107,7 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
|
|||
context.hasBaseDamage = !!this.action.parent.attack;
|
||||
context.getRealIndex = this.getRealIndex.bind(this);
|
||||
context.getEffectDetails = this.getEffectDetails.bind(this);
|
||||
context.costOptions = this.getCostOptions();
|
||||
context.disableOption = this.disableOption.bind(this);
|
||||
context.isNPC = this.action.actor && this.action.actor.type !== 'character';
|
||||
context.hasRoll = this.action.hasRoll;
|
||||
|
|
@ -125,8 +126,21 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
|
|||
this.render(true);
|
||||
}
|
||||
|
||||
disableOption(index, options, choices) {
|
||||
const filtered = foundry.utils.deepClone(options);
|
||||
getCostOptions() {
|
||||
const options = foundry.utils.deepClone(CONFIG.DH.GENERAL.abilityCosts);
|
||||
const resource = this.action.parent.resource;
|
||||
if (resource) {
|
||||
options[this.action.parent.parent.id] = {
|
||||
label: this.action.parent.parent.name,
|
||||
group: 'TYPES.Actor.character'
|
||||
};
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
disableOption(index, costOptions, choices) {
|
||||
const filtered = foundry.utils.deepClone(costOptions);
|
||||
Object.keys(filtered).forEach(o => {
|
||||
if (choices.find((c, idx) => c.type === o && index !== idx)) filtered[o].disabled = true;
|
||||
});
|
||||
|
|
@ -142,11 +156,19 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
|
|||
return this.action.item.effects.get(id);
|
||||
}
|
||||
|
||||
_prepareSubmitData(event, formData) {
|
||||
_prepareSubmitData(_event, formData) {
|
||||
const submitData = foundry.utils.expandObject(formData.object);
|
||||
for (const keyPath of this.constructor.CLEAN_ARRAYS) {
|
||||
const data = foundry.utils.getProperty(submitData, keyPath);
|
||||
if (data) foundry.utils.setProperty(submitData, keyPath, Object.values(data));
|
||||
const dataValues = data ? Object.values(data) : [];
|
||||
if (keyPath === 'cost') {
|
||||
for (var value of dataValues) {
|
||||
const item = this.action.parent.parent.id === value.key;
|
||||
value.keyIsID = Boolean(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (data) foundry.utils.setProperty(submitData, keyPath, dataValues);
|
||||
}
|
||||
return submitData;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { abilities } from '../../../config/actorConfig.mjs';
|
|||
import DhCharacterlevelUp from '../../levelup/characterLevelup.mjs';
|
||||
import DhCharacterCreation from '../../characterCreation/characterCreation.mjs';
|
||||
import FilterMenu from '../../ux/filter-menu.mjs';
|
||||
import { itemAbleRollParse } from '../../../helpers/utils.mjs';
|
||||
|
||||
/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */
|
||||
|
||||
|
|
@ -25,6 +26,8 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
|||
toggleEquipItem: CharacterSheet.#toggleEquipItem,
|
||||
useItem: this.useItem, //TODO Fix this
|
||||
useAction: this.useAction,
|
||||
toggleResourceDice: this.toggleResourceDice,
|
||||
handleResourceDice: this.handleResourceDice,
|
||||
toChat: this.toChat
|
||||
},
|
||||
window: {
|
||||
|
|
@ -91,6 +94,17 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
|||
}
|
||||
};
|
||||
|
||||
_attachPartListeners(partId, htmlElement, options) {
|
||||
super._attachPartListeners(partId, htmlElement, options);
|
||||
|
||||
htmlElement.querySelectorAll('.inventory-item-resource').forEach(element => {
|
||||
element.addEventListener('change', this.updateItemResource.bind(this));
|
||||
});
|
||||
htmlElement.querySelectorAll('.inventory-item-quantity').forEach(element => {
|
||||
element.addEventListener('change', this.updateItemQuantity.bind(this));
|
||||
});
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
async _onRender(context, options) {
|
||||
await super._onRender(context, options);
|
||||
|
|
@ -480,6 +494,27 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
|||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Application Listener Actions */
|
||||
/* -------------------------------------------- */
|
||||
async updateItemResource(event) {
|
||||
const item = this.getItem(event.currentTarget);
|
||||
if (!item) return;
|
||||
|
||||
const max = item.system.resource.max ? itemAbleRollParse(item.system.resource.max, this.document, item) : null;
|
||||
const value = max ? Math.min(Number(event.currentTarget.value), max) : event.currentTarget.value;
|
||||
await item.update({ 'system.resource.value': value });
|
||||
this.render();
|
||||
}
|
||||
|
||||
async updateItemQuantity(event) {
|
||||
const item = this.getItem(event.currentTarget);
|
||||
if (!item) return;
|
||||
|
||||
await item.update({ 'system.quantity': event.currentTarget.value });
|
||||
this.render();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Application Clicks Actions */
|
||||
/* -------------------------------------------- */
|
||||
|
|
@ -640,6 +675,41 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
|||
action.use(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the used state of a resource dice.
|
||||
* @type {ApplicationClickAction}
|
||||
*/
|
||||
static async toggleResourceDice(event) {
|
||||
const target = event.target.closest('.item-resource');
|
||||
const item = this.getItem(event);
|
||||
if (!item) return;
|
||||
|
||||
const diceState = item.system.resource.diceStates[target.dataset.dice];
|
||||
await item.update({
|
||||
[`system.resource.diceStates.${target.dataset.dice}.used`]: diceState?.used ? !diceState.used : true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the roll values of resource dice.
|
||||
* @type {ApplicationClickAction}
|
||||
*/
|
||||
static async handleResourceDice(event) {
|
||||
const item = this.getItem(event);
|
||||
if (!item) return;
|
||||
|
||||
const rollValues = await game.system.api.applications.dialogs.ResourceDiceDialog.create(item, this.document);
|
||||
if (!rollValues) return;
|
||||
|
||||
await item.update({
|
||||
'system.resource.diceStates': rollValues.reduce((acc, state, index) => {
|
||||
acc[index] = { value: state.value, used: state.used };
|
||||
return acc;
|
||||
}, {})
|
||||
});
|
||||
this.render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send item to Chat
|
||||
* @type {ApplicationClickAction}
|
||||
|
|
@ -672,14 +742,14 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
|||
|
||||
async _onDragStart(event) {
|
||||
const item = this.getItem(event);
|
||||
|
||||
|
||||
const dragData = {
|
||||
type: item.documentName,
|
||||
uuid: item.uuid
|
||||
};
|
||||
|
||||
|
||||
event.dataTransfer.setData('text/plain', JSON.stringify(dragData));
|
||||
|
||||
|
||||
super._onDragStart(event);
|
||||
}
|
||||
|
||||
|
|
@ -687,7 +757,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
|||
// Prevent event bubbling to avoid duplicate handling
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
|
||||
super._onDrop(event);
|
||||
this._onDropItem(event, TextEditor.getDragEventData(event));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,9 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
|
|||
removeAction: DHBaseItemSheet.#removeAction,
|
||||
addFeature: DHBaseItemSheet.#addFeature,
|
||||
editFeature: DHBaseItemSheet.#editFeature,
|
||||
removeFeature: DHBaseItemSheet.#removeFeature
|
||||
removeFeature: DHBaseItemSheet.#removeFeature,
|
||||
addResource: DHBaseItemSheet.#addResource,
|
||||
removeResource: DHBaseItemSheet.#removeResource
|
||||
},
|
||||
dragDrop: [
|
||||
{ dragSelector: null, dropSelector: '.tab.features .drop-section' },
|
||||
|
|
@ -215,6 +217,26 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a resource to the item.
|
||||
* @type {ApplicationClickAction}
|
||||
*/
|
||||
static async #addResource() {
|
||||
await this.document.update({
|
||||
'system.resource': { type: 'simple', value: 0 }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the resource from the item.
|
||||
* @type {ApplicationClickAction}
|
||||
*/
|
||||
static async #removeResource() {
|
||||
await this.document.update({
|
||||
'system.resource': null
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Application Drag/Drop */
|
||||
/* -------------------------------------------- */
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
export default function ItemAttachmentSheet(Base) {
|
||||
return class extends Base {
|
||||
static DEFAULT_OPTIONS = {
|
||||
|
|
@ -25,10 +24,7 @@ export default function ItemAttachmentSheet(Base) {
|
|||
...super.TABS,
|
||||
primary: {
|
||||
...super.TABS?.primary,
|
||||
tabs: [
|
||||
...(super.TABS?.primary?.tabs || []),
|
||||
{ id: 'attachments' }
|
||||
],
|
||||
tabs: [...(super.TABS?.primary?.tabs || []), { id: 'attachments' }],
|
||||
initial: super.TABS?.primary?.initial || 'description',
|
||||
labelPrefix: super.TABS?.primary?.labelPrefix || 'DAGGERHEART.GENERAL.Tabs'
|
||||
}
|
||||
|
|
@ -46,29 +42,28 @@ export default function ItemAttachmentSheet(Base) {
|
|||
|
||||
async _onDrop(event) {
|
||||
const data = TextEditor.getDragEventData(event);
|
||||
|
||||
|
||||
const attachmentsSection = event.target.closest('.attachments-section');
|
||||
if (!attachmentsSection) return super._onDrop(event);
|
||||
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
|
||||
const item = await Item.implementation.fromDropData(data);
|
||||
if (!item) return;
|
||||
|
||||
|
||||
// Call the data model's public method
|
||||
await this.document.system.addAttachment(item);
|
||||
}
|
||||
|
||||
|
||||
static async #removeAttachment(event, target) {
|
||||
// Call the data model's public method
|
||||
await this.document.system.removeAttachment(target.dataset.uuid);
|
||||
}
|
||||
}
|
||||
|
||||
async _preparePartContext(partId, context) {
|
||||
await super._preparePartContext(partId, context);
|
||||
|
||||
|
||||
if (partId === 'attachments') {
|
||||
// Keep this simple UI preparation in the mixin
|
||||
const attachedUUIDs = this.document.system.attached;
|
||||
|
|
@ -83,8 +78,8 @@ export default function ItemAttachmentSheet(Base) {
|
|||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return context;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export default class ArmorSheet extends ItemAttachmentSheet(DHBaseItemSheet) {
|
|||
template: 'systems/daggerheart/templates/sheets/items/armor/settings.hbs',
|
||||
scrollable: ['.settings']
|
||||
},
|
||||
...super.PARTS,
|
||||
...super.PARTS
|
||||
};
|
||||
|
||||
/**@inheritdoc */
|
||||
|
|
|
|||
|
|
@ -85,6 +85,16 @@ export default class ClassSheet extends DHBaseItemSheet {
|
|||
await this.document.update({
|
||||
'system.subclasses': [...this.document.system.subclasses.map(x => x.uuid), item.uuid]
|
||||
});
|
||||
} else if (item.type === 'feature') {
|
||||
if (target.classList.contains('hope-feature')) {
|
||||
await this.document.update({
|
||||
'system.hopeFeatures': [...this.document.system.hopeFeatures.map(x => x.uuid), item.uuid]
|
||||
});
|
||||
} else if (target.classList.contains('class-feature')) {
|
||||
await this.document.update({
|
||||
'system.classFeatures': [...this.document.system.classFeatures.map(x => x.uuid), item.uuid]
|
||||
});
|
||||
}
|
||||
} else if (item.type === 'weapon') {
|
||||
if (target.classList.contains('primary-weapon-section')) {
|
||||
if (!this.document.system.characterGuide.suggestedPrimaryWeapon && !item.system.secondary)
|
||||
|
|
@ -144,7 +154,7 @@ export default class ClassSheet extends DHBaseItemSheet {
|
|||
static async #removeItemFromCollection(_event, element) {
|
||||
const { uuid, target } = element.dataset;
|
||||
const prop = foundry.utils.getProperty(this.document.system, target);
|
||||
await this.document.update({ [target]: prop.filter(i => i.uuid !== uuid) });
|
||||
await this.document.update({ [`system.${target}`]: prop.filter(i => i.uuid !== uuid) });
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export default class FeatureSheet extends DHBaseItemSheet {
|
|||
header: { template: 'systems/daggerheart/templates/sheets/items/feature/header.hbs' },
|
||||
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
|
||||
description: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-description.hbs' },
|
||||
settings: { template: 'systems/daggerheart/templates/sheets/items/feature/settings.hbs' },
|
||||
actions: {
|
||||
template: 'systems/daggerheart/templates/sheets/global/tabs/tab-actions.hbs',
|
||||
scrollable: ['.actions']
|
||||
|
|
@ -34,7 +35,7 @@ export default class FeatureSheet extends DHBaseItemSheet {
|
|||
/**@override */
|
||||
static TABS = {
|
||||
primary: {
|
||||
tabs: [{ id: 'description' }, { id: 'actions' }, { id: 'effects' }],
|
||||
tabs: [{ id: 'description' }, { id: 'settings' }, { id: 'actions' }, { id: 'effects' }],
|
||||
initial: 'description',
|
||||
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
|
||||
}
|
||||
|
|
@ -67,7 +68,6 @@ export default class FeatureSheet extends DHBaseItemSheet {
|
|||
}
|
||||
),
|
||||
title = game.i18n.localize('DAGGERHEART.CONFIG.SelectAction.selectType');
|
||||
console.log(this.document);
|
||||
|
||||
return foundry.applications.api.DialogV2.prompt({
|
||||
window: { title },
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export default class WeaponSheet extends ItemAttachmentSheet(DHBaseItemSheet) {
|
|||
template: 'systems/daggerheart/templates/sheets/items/weapon/settings.hbs',
|
||||
scrollable: ['.settings']
|
||||
},
|
||||
...super.PARTS,
|
||||
...super.PARTS
|
||||
};
|
||||
|
||||
/**@inheritdoc */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue