diff --git a/lang/en.json b/lang/en.json
index 5c0c7470..7b8b45c7 100755
--- a/lang/en.json
+++ b/lang/en.json
@@ -213,6 +213,9 @@
"headerTitle": "Adversary Reaction Roll"
}
},
+ "Base": {
+ "CannotAddType": "Cannot add {itemType} items to {actorType} actors."
+ },
"Character": {
"advantageSources": {
"label": "Advantage Sources",
@@ -2878,6 +2881,10 @@
}
},
"Keybindings": {
+ "partySheet": {
+ "name": "Toggle Party Sheet",
+ "hint": "Open or close the active party's sheet"
+ },
"spotlight": {
"name": "Spotlight Combatant",
"hint": "Move the spotlight to a hovered or selected token that's present in an active encounter"
diff --git a/module/applications/dialogs/d20RollDialog.mjs b/module/applications/dialogs/d20RollDialog.mjs
index 64fa168a..067aa473 100644
--- a/module/applications/dialogs/d20RollDialog.mjs
+++ b/module/applications/dialogs/d20RollDialog.mjs
@@ -123,6 +123,10 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
context.advantage = this.config.roll?.advantage;
context.disadvantage = this.config.roll?.disadvantage;
context.diceOptions = CONFIG.DH.GENERAL.diceTypes;
+ context.dieFaces = CONFIG.DH.GENERAL.dieFaces.reduce((acc, face) => {
+ acc[face] = `d${face}`;
+ return acc;
+ }, {});
context.isLite = this.config.roll?.lite;
context.extraFormula = this.config.extraFormula;
context.formula = this.roll.constructFormula(this.config);
@@ -152,9 +156,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
}
if (this.config.uses) this.config.uses = foundry.utils.mergeObject(this.config.uses, rest.uses);
if (rest.roll?.dice) {
- Object.entries(rest.roll.dice).forEach(([key, value]) => {
- this.roll[key] = value;
- });
+ this.roll = foundry.utils.mergeObject(this.roll, rest.roll.dice);
}
if (rest.hasOwnProperty('trait')) {
this.config.roll.trait = rest.trait;
@@ -173,6 +175,15 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
this.disadvantage = advantage === -1;
this.config.roll.advantage = this.config.roll.advantage === advantage ? 0 : advantage;
+
+ if (this.config.roll.advantage === 1 && this.config.data.rules.roll.defaultAdvantageDice) {
+ const faces = Number.parseInt(this.config.data.rules.roll.defaultAdvantageDice);
+ this.roll.advantageFaces = Number.isNaN(faces) ? this.roll.advantageFaces : faces;
+ } else if (this.config.roll.advantage === -1 && this.config.data.rules.roll.defaultDisadvantageDice) {
+ const faces = Number.parseInt(this.config.data.rules.roll.defaultDisadvantageDice);
+ this.roll.advantageFaces = Number.isNaN(faces) ? this.roll.advantageFaces : faces;
+ }
+
this.render();
}
diff --git a/module/applications/dialogs/itemTransfer.mjs b/module/applications/dialogs/itemTransfer.mjs
index ad3cf103..42e3a727 100644
--- a/module/applications/dialogs/itemTransfer.mjs
+++ b/module/applications/dialogs/itemTransfer.mjs
@@ -38,13 +38,15 @@ export default class ItemTransferDialog extends HandlebarsApplicationMixin(Appli
originActor ??= item?.actor;
const homebrewKey = CONFIG.DH.SETTINGS.gameSettings.Homebrew;
const currencySetting = game.settings.get(CONFIG.DH.id, homebrewKey).currency?.[currency] ?? null;
+ const max = item?.system.quantity ?? originActor.system.gold[currency] ?? 0;
return {
originActor,
targetActor,
itemImage: item?.img,
currencyIcon: currencySetting?.icon,
- max: item?.system.quantity ?? originActor.system.gold[currency] ?? 0,
+ max,
+ initial: targetActor.system.metadata.quantifiable?.includes(item.type) ? max : 1,
title: item?.name ?? currencySetting?.label
};
}
diff --git a/module/applications/sheets/api/base-actor.mjs b/module/applications/sheets/api/base-actor.mjs
index 4a550d72..e23a4426 100644
--- a/module/applications/sheets/api/base-actor.mjs
+++ b/module/applications/sheets/api/base-actor.mjs
@@ -73,7 +73,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
.hideAttribution;
// Prepare inventory data
- if (['party', 'character'].includes(this.document.type)) {
+ if (this.document.system.metadata.hasInventory) {
context.inventory = {
currencies: {},
weapons: this.document.itemTypes.weapon.sort((a, b) => a.sort - b.sort),
@@ -283,11 +283,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
async _onDropItem(event, item) {
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
const originActor = item.actor;
- if (
- item.actor?.uuid === this.document.uuid ||
- !originActor ||
- !['character', 'party'].includes(this.document.type)
- ) {
+ if (!originActor || originActor.uuid === this.document.uuid || !this.document.system.metadata.hasInventory) {
return super._onDropItem(event, item);
}
@@ -302,47 +298,79 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
);
}
- if (item.system.metadata.isQuantifiable) {
- const actorItem = originActor.items.get(data.originId);
- const quantityTransfered = await game.system.api.applications.dialogs.ItemTransferDialog.configure({
+ // Perform the actual transfer, showing a dialog when doing it
+ const availableQuantity = Math.max(1, item.system.quantity);
+ const actorItem = originActor.items.get(data.originId) ?? item;
+ if (availableQuantity > 1) {
+ const quantityTransferred = await game.system.api.applications.dialogs.ItemTransferDialog.configure({
item,
targetActor: this.document
});
-
- if (quantityTransfered) {
- const existingItem = this.document.items.find(x => itemIsIdentical(x, item));
- if (existingItem) {
- await existingItem.update({
- 'system.quantity': existingItem.system.quantity + quantityTransfered
- });
- } else {
- const createData = item.toObject();
- await this.document.createEmbeddedDocuments('Item', [
- {
- ...createData,
- system: {
- ...createData.system,
- quantity: quantityTransfered
- }
- }
- ]);
- }
-
- if (quantityTransfered === actorItem.system.quantity) {
- await originActor.deleteEmbeddedDocuments('Item', [data.originId]);
- } else {
- await actorItem.update({
- 'system.quantity': actorItem.system.quantity - quantityTransfered
- });
- }
- }
+ return this.#transferItem(actorItem, quantityTransferred);
} else {
- await this.document.createEmbeddedDocuments('Item', [item.toObject()]);
- await originActor.deleteEmbeddedDocuments('Item', [data.originId]);
+ return this.#transferItem(actorItem, availableQuantity);
}
}
}
+ /**
+ * Helper to perform the actual transfer of an item to this actor, including stack/unstack logic based on target quantifiability.
+ * Make sure item is the actor item before calling this method or there will be issues
+ */
+ async #transferItem(item, quantity) {
+ const originActor = item.actor;
+ const targetActor = this.document;
+ const allowStacking = targetActor.system.metadata.quantifiable?.includes(item.type);
+
+ const batch = [];
+
+ // First add/update the item to the target actor
+ const existing = allowStacking ? targetActor.items.find(x => itemIsIdentical(x, item)) : null;
+ if (existing) {
+ batch.push({
+ action: 'update',
+ documentName: 'Item',
+ parent: targetActor,
+ updates: [{ '_id': existing.id, 'system.quantity': existing.system.quantity + quantity }]
+ });
+ } else {
+ const itemsToCreate = [];
+ if (allowStacking) {
+ itemsToCreate.push(foundry.utils.mergeObject(item.toObject(true), { system: { quantity } }));
+ } else {
+ const createData = new Array(Math.max(1, quantity))
+ .fill(0)
+ .map(() => foundry.utils.mergeObject(item.toObject(), { system: { quantity: 1 } }));
+ itemsToCreate.push(...createData);
+ }
+ batch.push({
+ action: 'create',
+ documentName: 'Item',
+ parent: targetActor,
+ data: itemsToCreate
+ });
+ }
+
+ // Remove the item from the original actor (by either deleting it, or updating its quantity)
+ if (quantity >= item.system.quantity) {
+ batch.push({
+ action: 'delete',
+ documentName: 'Item',
+ parent: originActor,
+ ids: [item.id]
+ });
+ } else {
+ batch.push({
+ action: 'update',
+ documentName: 'Item',
+ parent: originActor,
+ updates: [{ '_id': item.id, 'system.quantity': item.system.quantity - quantity }]
+ });
+ }
+
+ return foundry.documents.modifyBatch(batch);
+ }
+
/**
* On dragStart on the item.
* @param {DragEvent} event - The drag event
diff --git a/module/applications/sheets/items/ancestry.mjs b/module/applications/sheets/items/ancestry.mjs
index 8c0a5620..cf040e02 100644
--- a/module/applications/sheets/items/ancestry.mjs
+++ b/module/applications/sheets/items/ancestry.mjs
@@ -31,11 +31,12 @@ export default class AncestrySheet extends DHHeritageSheet {
if (data.type === 'ActiveEffect') return super._onDrop(event);
const target = event.target.closest('fieldset.drop-section');
- const typeField =
- this.document.system[target.dataset.type === 'primary' ? 'primaryFeature' : 'secondaryFeature'];
-
- if (!typeField) {
- super._onDrop(event);
+ if (target) {
+ const typeField =
+ this.document.system[target.dataset.type === 'primary' ? 'primaryFeature' : 'secondaryFeature'];
+ if (!typeField) {
+ super._onDrop(event);
+ }
}
}
}
diff --git a/module/config/settingsConfig.mjs b/module/config/settingsConfig.mjs
index 12e8536e..6dae29aa 100644
--- a/module/config/settingsConfig.mjs
+++ b/module/config/settingsConfig.mjs
@@ -1,5 +1,6 @@
export const keybindings = {
- spotlight: 'DHSpotlight'
+ spotlight: 'DHSpotlight',
+ partySheet: 'DHPartySheet'
};
export const menu = {
diff --git a/module/data/actor/adversary.mjs b/module/data/actor/adversary.mjs
index 73673896..d30e090a 100644
--- a/module/data/actor/adversary.mjs
+++ b/module/data/actor/adversary.mjs
@@ -133,7 +133,7 @@ export default class DhpAdversary extends DhCreature {
}
isItemValid(source) {
- return source.type === 'feature';
+ return super.isItemValid(source) || source.type === 'feature';
}
async _preUpdate(changes, options, user) {
diff --git a/module/data/actor/base.mjs b/module/data/actor/base.mjs
index 89ba5db2..13ff0a38 100644
--- a/module/data/actor/base.mjs
+++ b/module/data/actor/base.mjs
@@ -107,7 +107,8 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel {
hasResistances: true,
hasAttribution: false,
hasLimitedView: true,
- usesSize: false
+ usesSize: false,
+ hasInventory: false
};
}
@@ -168,6 +169,11 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel {
/* -------------------------------------------- */
+ isItemValid(source) {
+ const inventoryTypes = ['weapon', 'armor', 'consumable', 'loot'];
+ return this.metadata.hasInventory && inventoryTypes.includes(source.type);
+ }
+
/**
* Obtain a data object used to evaluate any dice rolls associated with this Item Type
* @param {object} [options] - Options which modify the getRollData method.
diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs
index 0e1e2259..bf3d7bb8 100644
--- a/module/data/actor/character.mjs
+++ b/module/data/actor/character.mjs
@@ -18,7 +18,9 @@ export default class DhCharacter extends DhCreature {
label: 'TYPES.Actor.character',
type: 'character',
settingSheet: DHCharacterSettings,
- isNPC: false
+ isNPC: false,
+ hasInventory: true,
+ quantifiable: ["loot", "consumable"]
});
}
@@ -284,7 +286,23 @@ export default class DhCharacter extends DhCreature {
guaranteedCritical: new fields.BooleanField({
label: 'DAGGERHEART.ACTORS.Character.roll.guaranteedCritical.label',
hint: 'DAGGERHEART.ACTORS.Character.roll.guaranteedCritical.hint'
- })
+ }),
+ defaultAdvantageDice: new fields.NumberField({
+ nullable: true,
+ required: true,
+ integer: true,
+ choices: CONFIG.DH.GENERAL.dieFaces,
+ initial: null,
+ label: 'DAGGERHEART.ACTORS.Character.defaultAdvantageDice'
+ }),
+ defaultDisadvantageDice: new fields.NumberField({
+ nullable: true,
+ required: true,
+ integer: true,
+ choices: CONFIG.DH.GENERAL.dieFaces,
+ initial: null,
+ label: 'DAGGERHEART.ACTORS.Character.defaultDisadvantageDice'
+ }),
})
})
};
@@ -429,6 +447,11 @@ export default class DhCharacter extends DhCreature {
return attack;
}
+ /* All items are valid on characters */
+ isItemValid() {
+ return true;
+ }
+
/** @inheritDoc */
isItemAvailable(item) {
if (!super.isItemAvailable(this)) return false;
diff --git a/module/data/actor/companion.mjs b/module/data/actor/companion.mjs
index 81d0aa8a..a1fa1429 100644
--- a/module/data/actor/companion.mjs
+++ b/module/data/actor/companion.mjs
@@ -61,6 +61,24 @@ export default class DhCompanion extends DhCreature {
initial: false,
label: 'DAGGERHEART.GENERAL.Rules.conditionImmunities.vulnerable'
})
+ }),
+ roll: new fields.SchemaField({
+ defaultAdvantageDice: new fields.NumberField({
+ nullable: true,
+ required: true,
+ integer: true,
+ choices: CONFIG.DH.GENERAL.dieFaces,
+ initial: null,
+ label: 'DAGGERHEART.ACTORS.Character.defaultAdvantageDice'
+ }),
+ defaultDisadvantageDice: new fields.NumberField({
+ nullable: true,
+ required: true,
+ integer: true,
+ choices: CONFIG.DH.GENERAL.dieFaces,
+ initial: null,
+ label: 'DAGGERHEART.ACTORS.Character.defaultDisadvantageDice'
+ }),
})
}),
attack: new ActionField({
@@ -118,10 +136,6 @@ export default class DhCompanion extends DhCreature {
return this.levelupChoicesLeft > 0;
}
- isItemValid() {
- return false;
- }
-
prepareBaseData() {
super.prepareBaseData();
this.attack.roll.bonus = this.partner?.system?.spellcastModifier ?? 0;
diff --git a/module/data/actor/environment.mjs b/module/data/actor/environment.mjs
index e06f038c..e037e004 100644
--- a/module/data/actor/environment.mjs
+++ b/module/data/actor/environment.mjs
@@ -56,7 +56,7 @@ export default class DhEnvironment extends BaseDataActor {
}
isItemValid(source) {
- return source.type === 'feature';
+ return super.isItemValid(source) || source.type === 'feature';
}
_onUpdate(changes, options, userId) {
diff --git a/module/data/actor/party.mjs b/module/data/actor/party.mjs
index ea09c622..6ccf8852 100644
--- a/module/data/actor/party.mjs
+++ b/module/data/actor/party.mjs
@@ -5,6 +5,14 @@ import GroupRollData from '../groupRollData.mjs';
import { GoldField } from '../fields/actorField.mjs';
export default class DhParty extends BaseDataActor {
+ /** @inheritdoc */
+ static get metadata() {
+ return foundry.utils.mergeObject(super.metadata, {
+ hasInventory: true,
+ quantifiable: ["weapon", "armor", "loot", "consumable"]
+ });
+ }
+
/**@inheritdoc */
static defineSchema() {
const fields = foundry.data.fields;
@@ -29,10 +37,6 @@ export default class DhParty extends BaseDataActor {
/* -------------------------------------------- */
- isItemValid(source) {
- return ['weapon', 'armor', 'consumable', 'loot'].includes(source.type);
- }
-
prepareBaseData() {
super.prepareBaseData();
diff --git a/module/data/item/base.mjs b/module/data/item/base.mjs
index 21a11149..72718c5e 100644
--- a/module/data/item/base.mjs
+++ b/module/data/item/base.mjs
@@ -4,7 +4,6 @@
* @property {string} label - A localizable label used on application.
* @property {string} type - The system type that this data model represents.
* @property {boolean} hasDescription - Indicates whether items of this type have description field
- * @property {boolean} isQuantifiable - Indicates whether items of this type have quantity field
* @property {boolean} isInventoryItem- Indicates whether items of this type is a Inventory Item
*/
@@ -24,7 +23,6 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
type: 'base',
hasDescription: false,
hasResource: false,
- isQuantifiable: false,
isInventoryItem: false,
hasActions: false,
hasAttribution: true
@@ -83,7 +81,7 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
);
}
- if (this.metadata.isQuantifiable)
+ if (this.metadata.isInventoryItem)
schema.quantity = new fields.NumberField({ integer: true, initial: 1, min: 0, required: true });
if (this.metadata.hasActions) schema.actions = new ActionsField();
diff --git a/module/data/item/consumable.mjs b/module/data/item/consumable.mjs
index ab527967..e83a1a53 100644
--- a/module/data/item/consumable.mjs
+++ b/module/data/item/consumable.mjs
@@ -7,7 +7,6 @@ export default class DHConsumable extends BaseDataItem {
label: 'TYPES.Item.consumable',
type: 'consumable',
hasDescription: true,
- isQuantifiable: true,
isInventoryItem: true,
hasActions: true
});
diff --git a/module/data/item/loot.mjs b/module/data/item/loot.mjs
index cdb0855e..d4092934 100644
--- a/module/data/item/loot.mjs
+++ b/module/data/item/loot.mjs
@@ -7,7 +7,6 @@ export default class DHLoot extends BaseDataItem {
label: 'TYPES.Item.loot',
type: 'loot',
hasDescription: true,
- isQuantifiable: true,
isInventoryItem: true,
hasActions: true
});
diff --git a/module/dice/dualityRoll.mjs b/module/dice/dualityRoll.mjs
index f9a06d37..2448a16d 100644
--- a/module/dice/dualityRoll.mjs
+++ b/module/dice/dualityRoll.mjs
@@ -3,7 +3,6 @@ import D20Roll from './d20Roll.mjs';
import { parseRallyDice, setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs';
export default class DualityRoll extends D20Roll {
- _advantageFaces = 6;
_advantageNumber = 1;
_rallyIndex;
@@ -11,6 +10,9 @@ export default class DualityRoll extends D20Roll {
super(formula, data, options);
this.rallyChoices = this.setRallyChoices();
this.guaranteedCritical = options.guaranteedCritical;
+
+ const advantageFaces = data.rules?.roll?.defaultAdvantageDice ? Number.parseInt(data.rules.roll.defaultAdvantageDice) : 6
+ this.advantageFaces = Number.isNaN(advantageFaces) ? 6 : advantageFaces;
}
static messageType = 'dualityRoll';
@@ -51,14 +53,6 @@ export default class DualityRoll extends D20Roll {
return this.dice[2] instanceof game.system.api.dice.diceTypes.DisadvantageDie ? this.dice[2] : null;
}
- get advantageFaces() {
- return this._advantageFaces;
- }
-
- set advantageFaces(faces) {
- this._advantageFaces = this.getFaces(faces);
- }
-
get advantageNumber() {
return this._advantageNumber;
}
diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs
index 8ae2e062..4e20db0e 100644
--- a/module/documents/actor.mjs
+++ b/module/documents/actor.mjs
@@ -113,7 +113,7 @@ export default class DhpActor extends Actor {
_onUpdate(changes, options, userId) {
super._onUpdate(changes, options, userId);
for (const party of this.parties) {
- party.render();
+ party.render({ parts: ['partyMembers'] });
}
}
@@ -132,7 +132,7 @@ export default class DhpActor extends Actor {
_onDelete(options, userId) {
super._onDelete(options, userId);
for (const party of this.parties) {
- party.render();
+ party.render({ parts: ['partyMembers'] });
}
}
diff --git a/module/documents/item.mjs b/module/documents/item.mjs
index d1a618c7..a8b41b05 100644
--- a/module/documents/item.mjs
+++ b/module/documents/item.mjs
@@ -31,8 +31,13 @@ export default class DHItem extends foundry.documents.Item {
static async createDocuments(sources, operation) {
// Ensure that items being created are valid to the actor its being added to
const actor = operation.parent;
- sources = actor?.system?.isItemValid ? sources.filter(s => actor.system.isItemValid(s)) : sources;
- return super.createDocuments(sources, operation);
+ const filtered = actor ? sources.filter(s => actor.system.isItemValid(s)) : sources;
+ if (actor && filtered.length === 0 && sources.length > 0) {
+ const itemType = _loc(`TYPES.Item.${sources[0].type}`);
+ const actorType = _loc(`TYPES.Actor.${actor.type}`);
+ ui.notifications.error('DAGGERHEART.ACTORS.Base.CannotAddType', { format: { itemType, actorType } });
+ }
+ return super.createDocuments(filtered, operation);
}
/* -------------------------------------------- */
diff --git a/module/systemRegistration/settings.mjs b/module/systemRegistration/settings.mjs
index ae78e23b..69bd1730 100644
--- a/module/systemRegistration/settings.mjs
+++ b/module/systemRegistration/settings.mjs
@@ -52,6 +52,27 @@ export const registerKeyBindings = () => {
reservedModifiers: [],
precedence: CONST.KEYBINDING_PRECEDENCE.NORMAL
});
+
+ 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" }],
+ onDown: () => {
+ const controlled = canvas.ready ? canvas.tokens.controlled : [];
+ const selectedParty = controlled.find((c) => c.actor?.type === 'party')?.actor;
+ const party = selectedParty ?? game.actors.party;
+ if (!party) return;
+
+ const sheet = party.sheet;
+ if (!sheet.rendered) {
+ sheet.render(true);
+ } else if (sheet.minimized) {
+ sheet.maximize();
+ } else {
+ sheet.close();
+ }
+ }
+ });
};
const registerMenuSettings = () => {
diff --git a/templates/dialogs/dice-roll/rollSelection.hbs b/templates/dialogs/dice-roll/rollSelection.hbs
index 2deccaf2..2c1a21b6 100644
--- a/templates/dialogs/dice-roll/rollSelection.hbs
+++ b/templates/dialogs/dice-roll/rollSelection.hbs
@@ -157,8 +157,8 @@
{{/times}}
-