Merged with main

This commit is contained in:
WBHarry 2025-07-13 03:12:43 +02:00
commit 58f96a36c9
44 changed files with 564 additions and 165 deletions

View file

@ -393,6 +393,7 @@
"rerollDice": "Reroll Dice"
}
},
"CONFIG": {
"ActionType": {
"passive": "Passive",
@ -966,6 +967,10 @@
"stress": {
"name": "Stress"
}
},
"Attachments": {
"attachHint": "Drop items here to attach them",
"transferHint": "If checked, this effect will be applied to any actor that owns this Effect's parent Item. The effect is always applied if this Item is attached to another one."
}
},
"GENERAL": {
@ -995,6 +1000,11 @@
"minor": "Minor",
"none": "None"
},
"DamageResistance": {
"none": "None",
"resistance": "Resistance",
"immunity": "Immunity"
},
"DamageThresholds": {
"title": "Damage Thresholds",
"minor": "Minor",
@ -1100,7 +1110,8 @@
"optional": "Optional",
"recovery": "Recovery",
"setup": "Setup",
"equipment": "Equipment"
"equipment": "Equipment",
"attachments": "Attachments"
},
"Tiers": {
"singular": "Tier",

View file

@ -11,7 +11,7 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
this.actor = actor;
this.damage = damage;
const canApplyArmor = actor.system.armorApplicableDamageTypes[damageType];
const canApplyArmor = damageType.every(t => actor.system.armorApplicableDamageTypes[t] === true);
const maxArmorMarks = canApplyArmor
? Math.min(
actor.system.armorScore - actor.system.armor.system.marks.value,

View file

@ -56,10 +56,6 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
id: 'effect',
template: 'systems/daggerheart/templates/sheets-settings/action-settings/effect.hbs'
}
/* form: {
id: 'action',
template: 'systems/daggerheart/templates/config/action.hbs'
} */
};
static TABS = {
@ -183,7 +179,7 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
container = foundry.utils.getProperty(this.action.parent, this.action.systemPath);
let newActions;
if (Array.isArray(container)) {
newActions = foundry.utils.getProperty(this.action.parent, this.action.systemPath).map(x => x.toObject()); // Find better way
newActions = foundry.utils.getProperty(this.action.parent, this.action.systemPath).map(x => x.toObject());
if (!newActions.findSplice(x => x._id === data._id, data)) newActions.push(data);
} else newActions = data;

View file

@ -32,7 +32,12 @@ export default class CharacterSheet extends DHBaseActorSheet {
window: {
resizable: true
},
dragDrop: [],
dragDrop: [
{
dragSelector: '[data-item-id][draggable="true"]',
dropSelector: null
}
],
contextMenus: [
{
handler: CharacterSheet._getContextMenuOptions,
@ -739,11 +744,24 @@ export default class CharacterSheet extends DHBaseActorSheet {
}
}
async _onDragStart(_, event) {
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);
}
async _onDrop(event) {
// Prevent event bubbling to avoid duplicate handling
event.preventDefault();
event.stopPropagation();
super._onDrop(event);
this._onDropItem(event, TextEditor.getDragEventData(event));
}

View file

@ -1,5 +1,6 @@
export { default as DHApplicationMixin } from './application-mixin.mjs';
export { default as DHBaseItemSheet } from './base-item.mjs';
export { default as DHHeritageSheet } from './heritage-sheet.mjs';
export { default as DHItemAttachmentSheet } from './item-attachment-sheet.mjs';
export { default as DHBaseActorSheet } from './base-actor.mjs';
export { default as DHBaseActorSettings } from './actor-setting.mjs';

View file

@ -0,0 +1,85 @@
export default function ItemAttachmentSheet(Base) {
return class extends Base {
static DEFAULT_OPTIONS = {
...super.DEFAULT_OPTIONS,
dragDrop: [
...(super.DEFAULT_OPTIONS.dragDrop || []),
{ dragSelector: null, dropSelector: '.attachments-section' }
],
actions: {
...super.DEFAULT_OPTIONS.actions,
removeAttachment: this.#removeAttachment
}
};
static PARTS = {
...super.PARTS,
attachments: {
template: 'systems/daggerheart/templates/sheets/global/tabs/tab-attachments.hbs',
scrollable: ['.attachments']
}
};
static TABS = {
...super.TABS,
primary: {
...super.TABS?.primary,
tabs: [...(super.TABS?.primary?.tabs || []), { id: 'attachments' }],
initial: super.TABS?.primary?.initial || 'description',
labelPrefix: super.TABS?.primary?.labelPrefix || 'DAGGERHEART.GENERAL.Tabs'
}
};
async _preparePartContext(partId, context) {
await super._preparePartContext(partId, context);
if (partId === 'attachments') {
context.attachedItems = await prepareAttachmentContext(this.document);
}
return context;
}
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;
context.attachedItems = await Promise.all(
attachedUUIDs.map(async uuid => {
const item = await fromUuid(uuid);
return {
uuid: uuid,
name: item?.name || 'Unknown Item',
img: item?.img || 'icons/svg/item-bag.svg'
};
})
);
}
return context;
}
};
}

View file

@ -1,10 +1,10 @@
import DHBaseItemSheet from '../api/base-item.mjs';
import ItemAttachmentSheet from '../api/item-attachment-sheet.mjs';
export default class ArmorSheet extends DHBaseItemSheet {
export default class ArmorSheet extends ItemAttachmentSheet(DHBaseItemSheet) {
/**@inheritdoc */
static DEFAULT_OPTIONS = {
classes: ['armor'],
dragDrop: [{ dragSelector: null, dropSelector: null }],
tagifyConfigs: [
{
selector: '.features-input',
@ -26,7 +26,8 @@ export default class ArmorSheet extends DHBaseItemSheet {
settings: {
template: 'systems/daggerheart/templates/sheets/items/armor/settings.hbs',
scrollable: ['.settings']
}
},
...super.PARTS
};
/**@inheritdoc */

View file

@ -1,6 +1,7 @@
import DHBaseItemSheet from '../api/base-item.mjs';
import ItemAttachmentSheet from '../api/item-attachment-sheet.mjs';
export default class WeaponSheet extends DHBaseItemSheet {
export default class WeaponSheet extends ItemAttachmentSheet(DHBaseItemSheet) {
/**@inheritdoc */
static DEFAULT_OPTIONS = {
classes: ['weapon'],
@ -25,12 +26,13 @@ export default class WeaponSheet extends DHBaseItemSheet {
settings: {
template: 'systems/daggerheart/templates/sheets/items/weapon/settings.hbs',
scrollable: ['.settings']
}
},
...super.PARTS
};
/**@inheritdoc */
async _preparePartContext(partId, context) {
super._preparePartContext(partId, context);
await super._preparePartContext(partId, context);
switch (partId) {
case 'settings':
context.features = this.document.system.weaponFeatures.map(x => x.value);

View file

@ -215,7 +215,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
if (message.system.onSave && message.system.targets.find(t => t.id === target.id)?.saved?.success === true)
damage = Math.ceil(damage * (CONFIG.DH.ACTIONS.damageOnSave[message.system.onSave]?.mod ?? 1));
target.actor.takeDamage(damage, message.system.roll.type);
target.actor.takeDamage(damage, message.system.damage.damageType);
}
};

View file

@ -7,3 +7,5 @@ export const encounterCountdown = {
simple: 'countdown-encounter-simple',
position: 'countdown-encounter-position'
};
export const itemAttachmentSource = 'attachmentSource';

View file

@ -59,13 +59,13 @@ export const damageTypes = {
id: 'physical',
label: 'DAGGERHEART.CONFIG.DamageType.physical.name',
abbreviation: 'DAGGERHEART.CONFIG.DamageType.physical.abbreviation',
icon: ['fa-hand-fist']
icon: 'fa-hand-fist'
},
magical: {
id: 'magical',
label: 'DAGGERHEART.CONFIG.DamageType.magical.name',
abbreviation: 'DAGGERHEART.CONFIG.DamageType.magical.abbreviation',
icon: ['fa-wand-sparkles']
icon: 'fa-wand-sparkles'
}
};

View file

@ -395,7 +395,7 @@ export const armorFeatures = {
img: 'icons/magic/defensive/barrier-shield-dome-pink.webp',
changes: [
{
key: 'system.bonuses.damageReduction.magical',
key: 'system.resistance.magical.reduction',
mode: 2,
value: '@system.armorScore'
}

View file

@ -76,11 +76,7 @@ export class DHActionDiceData extends foundry.abstract.DataModel {
};
}
getFormula(actor) {
/* const multiplier = this.multiplier === 'flat' ? this.flatMultiplier : actor.system[this.multiplier]?.total;
return this.custom.enabled
? this.custom.formula
: `${multiplier ?? 1}${this.dice}${this.bonus ? (this.bonus < 0 ? ` - ${Math.abs(this.bonus)}` : ` + ${this.bonus}`) : ''}`; */
getFormula() {
const multiplier = this.multiplier === 'flat' ? this.flatMultiplier : `@${this.multiplier}`,
bonus = this.bonus ? (this.bonus < 0 ? ` - ${Math.abs(this.bonus)}` : ` + ${this.bonus}`) : '';
return this.custom.enabled ? this.custom.formula : `${multiplier ?? 1}${this.dice}${bonus}`;
@ -93,7 +89,6 @@ export class DHDamageField extends fields.SchemaField {
parts: new fields.ArrayField(new fields.EmbeddedDataField(DHDamageData)),
includeBase: new fields.BooleanField({ initial: false })
};
// if (hasBase) damageFields.includeBase = new fields.BooleanField({ initial: true });
super(damageFields, options, context);
}
}
@ -102,15 +97,19 @@ export class DHDamageData extends foundry.abstract.DataModel {
/** @override */
static defineSchema() {
return {
// ...super.defineSchema(),
base: new fields.BooleanField({ initial: false, readonly: true, label: 'Base' }),
type: new fields.StringField({
choices: CONFIG.DH.GENERAL.damageTypes,
initial: 'physical',
label: 'Type',
nullable: false,
required: true
}),
type: new fields.SetField(
new fields.StringField({
choices: CONFIG.DH.GENERAL.damageTypes,
initial: 'physical',
nullable: false,
required: true
}),
{
label: 'Type',
initial: 'physical'
}
),
resultBased: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.ACTIONS.Settings.resultBased.label'

View file

@ -180,16 +180,9 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
getRollData(data = {}) {
const actorData = this.actor.getRollData(false);
// Remove when included directly in Actor getRollData
actorData.prof = actorData.proficiency?.total ?? 1;
actorData.cast = actorData.spellcast?.total ?? 1;
// Add Roll results to RollDatas
actorData.result = data.roll?.total ?? 1;
/* actorData.scale = data.costs?.length
? data.costs.reduce((a, c) => {
a[c.type] = c.value;
return a;
}, {})
: 1; */
actorData.scale = data.costs?.length // Right now only return the first scalable cost.
? (data.costs.find(c => c.scalable)?.total ?? 1)
: 1;

View file

@ -10,7 +10,10 @@ export default class DHDamageAction extends DHBaseAction {
}
async rollDamage(event, data) {
let formula = this.damage.parts.map(p => this.getFormulaValue(p, data).getFormula(this.actor)).join(' + ');
let formula = this.damage.parts.map(p => this.getFormulaValue(p, data).getFormula(this.actor)).join(' + '),
damageTypes = [...new Set(this.damage.parts.reduce((a, c) => a.concat([...c.type]), []))];
damageTypes = !damageTypes.length ? ['physical'] : damageTypes;
if (!formula || formula == '') return;
let roll = { formula: formula, total: formula },
@ -25,6 +28,7 @@ export default class DHDamageAction extends DHBaseAction {
hasSave: this.hasSave,
isCritical: data.system?.roll?.isCritical ?? false,
source: data.system?.source,
damageTypes,
event
};
if (this.hasSave) config.onSave = this.save.damageMod;

View file

@ -5,6 +5,7 @@ import BaseDataActor from './base.mjs';
const resourceField = () =>
new foundry.data.fields.SchemaField({
value: new foundry.data.fields.NumberField({ initial: 0, integer: true }),
bonus: new foundry.data.fields.NumberField({ initial: 0, integer: true }),
max: new foundry.data.fields.NumberField({ initial: 0, integer: true })
});
@ -22,6 +23,7 @@ export default class DhpAdversary extends BaseDataActor {
static defineSchema() {
const fields = foundry.data.fields;
return {
...super.defineSchema(),
tier: new fields.StringField({
required: true,
choices: CONFIG.DH.GENERAL.tiers,
@ -32,7 +34,6 @@ export default class DhpAdversary extends BaseDataActor {
choices: CONFIG.DH.ACTOR.adversaryTypes,
initial: CONFIG.DH.ACTOR.adversaryTypes.standard.id
}),
description: new fields.StringField(),
motivesAndTactics: new fields.StringField(),
notes: new fields.HTMLField(),
difficulty: new fields.NumberField({ required: true, initial: 1, integer: true }),
@ -63,6 +64,7 @@ export default class DhpAdversary extends BaseDataActor {
damage: {
parts: [
{
type: ['physical'],
value: {
multiplier: 'flat'
}
@ -93,4 +95,9 @@ export default class DhpAdversary extends BaseDataActor {
get features() {
return this.parent.items.filter(x => x.type === 'feature');
}
prepareDerivedData() {
this.resources.hitPoints.maxTotal = this.resources.hitPoints.max + this.resources.hitPoints.bonus;
this.resources.stress.maxTotal = this.resources.stress.max + this.resources.stress.bonus;
}
}

View file

@ -1,4 +1,11 @@
import DHBaseActorSettings from "../../applications/sheets/api/actor-setting.mjs";
import DHBaseActorSettings from '../../applications/sheets/api/actor-setting.mjs';
const resistanceField = () =>
new foundry.data.fields.SchemaField({
resistance: new foundry.data.fields.BooleanField({ initial: false }),
immunity: new foundry.data.fields.BooleanField({ initial: false }),
reduction: new foundry.data.fields.NumberField({ integer: true, initial: 0 })
});
/**
* Describes metadata about the actor data model type
@ -16,6 +23,7 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel {
type: 'base',
isNPC: true,
settingSheet: null,
hasResistances: true
};
}
@ -27,10 +35,15 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel {
/** @inheritDoc */
static defineSchema() {
const fields = foundry.data.fields;
const schema = {};
return {
description: new fields.HTMLField({ required: true, nullable: true })
};
if (this.metadata.isNPC) schema.description = new fields.HTMLField({ required: true, nullable: true });
if (this.metadata.hasResistances)
schema.resistance = new fields.SchemaField({
physical: resistanceField(),
magical: resistanceField()
});
return schema;
}
/**

View file

@ -36,6 +36,7 @@ export default class DhCharacter extends BaseDataActor {
const fields = foundry.data.fields;
return {
...super.defineSchema(),
resources: new fields.SchemaField({
hitPoints: new fields.SchemaField({
value: new foundry.data.fields.NumberField({ initial: 0, integer: true }),
@ -98,10 +99,6 @@ export default class DhCharacter extends BaseDataActor {
levelData: new fields.EmbeddedDataField(DhLevelData),
bonuses: new fields.SchemaField({
armorScore: new fields.NumberField({ integer: true, initial: 0 }),
damageReduction: new fields.SchemaField({
physical: new fields.NumberField({ integer: true, initial: 0 }),
magical: new fields.NumberField({ integer: true, initial: 0 })
}),
damageThresholds: new fields.SchemaField({
severe: new fields.NumberField({ integer: true, initial: 0 }),
major: new fields.NumberField({ integer: true, initial: 0 })

View file

@ -20,6 +20,7 @@ export default class DhCompanion extends BaseDataActor {
const fields = foundry.data.fields;
return {
...super.defineSchema(),
partner: new ForeignDocumentUUIDField({ type: 'Actor' }),
resources: new fields.SchemaField({
stress: new fields.SchemaField({
@ -66,6 +67,7 @@ export default class DhCompanion extends BaseDataActor {
damage: {
parts: [
{
type: ['physical'],
value: {
dice: 'd6',
multiplier: 'prof'

View file

@ -9,20 +9,21 @@ export default class DhEnvironment extends BaseDataActor {
return foundry.utils.mergeObject(super.metadata, {
label: 'TYPES.Actor.environment',
type: 'environment',
settingSheet: DHEnvironmentSettings
settingSheet: DHEnvironmentSettings,
hasResistances: false
});
}
static defineSchema() {
const fields = foundry.data.fields;
return {
...super.defineSchema(),
tier: new fields.StringField({
required: true,
choices: CONFIG.DH.GENERAL.tiers,
initial: CONFIG.DH.GENERAL.tiers.tier1.id
}),
type: new fields.StringField({ choices: CONFIG.DH.ACTOR.environmentTypes }),
description: new fields.StringField(),
impulses: new fields.StringField(),
difficulty: new fields.NumberField({ required: true, initial: 11, integer: true }),
potentialAdversaries: new fields.TypedObjectField(

View file

@ -1,5 +1,6 @@
import DHAncestry from './ancestry.mjs';
import DHArmor from './armor.mjs';
import DHAttachableItem from './attachableItem.mjs';
import DHClass from './class.mjs';
import DHCommunity from './community.mjs';
import DHConsumable from './consumable.mjs';
@ -13,6 +14,7 @@ import DHBeastform from './beastform.mjs';
export {
DHAncestry,
DHArmor,
DHAttachableItem,
DHClass,
DHCommunity,
DHConsumable,
@ -27,6 +29,7 @@ export {
export const config = {
ancestry: DHAncestry,
armor: DHArmor,
attachableItem: DHAttachableItem,
class: DHClass,
community: DHCommunity,
consumable: DHConsumable,

View file

@ -1,8 +1,9 @@
import BaseDataItem from './base.mjs';
import AttachableItem from './attachableItem.mjs';
import ActionField from '../fields/actionField.mjs';
import { armorFeatures } from '../../config/itemConfig.mjs';
import { actionsTypes } from '../action/_module.mjs';
export default class DHArmor extends BaseDataItem {
export default class DHArmor extends AttachableItem {
/** @inheritDoc */
static get metadata() {
return foundry.utils.mergeObject(super.metadata, {

View file

@ -0,0 +1,160 @@
import BaseDataItem from './base.mjs';
export default class AttachableItem extends BaseDataItem {
static defineSchema() {
const fields = foundry.data.fields;
return {
...super.defineSchema(),
attached: new fields.ArrayField(new fields.DocumentUUIDField({ type: 'Item', nullable: true }))
};
}
async _preUpdate(changes, options, user) {
const allowed = await super._preUpdate(changes, options, user);
if (allowed === false) return false;
// Handle equipped status changes for attachment effects
if (changes.system?.equipped !== undefined && changes.system.equipped !== this.equipped) {
await this.#handleAttachmentEffectsOnEquipChange(changes.system.equipped);
}
}
async #handleAttachmentEffectsOnEquipChange(newEquippedStatus) {
const actor = this.parent.parent?.type === 'character' ? this.parent.parent : this.parent.parent?.parent;
const parentType = this.parent.type;
if (!actor || !this.attached?.length) {
return;
}
if (newEquippedStatus) {
// Item is being equipped - add attachment effects
for (const attachedUuid of this.attached) {
const attachedItem = await fromUuid(attachedUuid);
if (attachedItem && attachedItem.effects.size > 0) {
await this.#copyAttachmentEffectsToActor({
attachedItem,
attachedUuid,
parentType
});
}
}
} else {
// Item is being unequipped - remove attachment effects
await this.#removeAllAttachmentEffects(parentType);
}
}
async #copyAttachmentEffectsToActor({ attachedItem, attachedUuid, parentType }) {
const actor = this.parent.parent;
if (!actor || !attachedItem.effects.size > 0 || !this.equipped) {
return [];
}
const effectsToCreate = [];
for (const effect of attachedItem.effects) {
const effectData = effect.toObject();
effectData.origin = `${this.parent.uuid}:${attachedUuid}`;
const attachmentSource = {
itemUuid: attachedUuid,
originalEffectId: effect.id
};
attachmentSource[`${parentType}Uuid`] = this.parent.uuid;
effectData.flags = {
...effectData.flags,
[CONFIG.DH.id]: {
...effectData.flags?.[CONFIG.DH.id],
[CONFIG.DH.FLAGS.itemAttachmentSource]: attachmentSource
}
};
effectsToCreate.push(effectData);
}
if (effectsToCreate.length > 0) {
return await actor.createEmbeddedDocuments('ActiveEffect', effectsToCreate);
}
return [];
}
async #removeAllAttachmentEffects(parentType) {
const actor = this.parent.parent;
if (!actor) return;
const parentUuidProperty = `${parentType}Uuid`;
const effectsToRemove = actor.effects.filter(effect => {
const attachmentSource = effect.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.itemAttachmentSource);
return attachmentSource && attachmentSource[parentUuidProperty] === this.parent.uuid;
});
if (effectsToRemove.length > 0) {
await actor.deleteEmbeddedDocuments(
'ActiveEffect',
effectsToRemove.map(e => e.id)
);
}
}
/**
* Public method for adding an attachment
*/
async addAttachment(droppedItem) {
const newUUID = droppedItem.uuid;
if (this.attached.includes(newUUID)) {
ui.notifications.warn(`${droppedItem.name} is already attached to this ${this.parent.type}.`);
return;
}
const updatedAttached = [...this.attached, newUUID];
await this.parent.update({
'system.attached': updatedAttached
});
// Copy effects if equipped
if (this.equipped && droppedItem.effects.size > 0) {
await this.#copyAttachmentEffectsToActor({
attachedItem: droppedItem,
attachedUuid: newUUID,
parentType: this.parent.type
});
}
}
/**
* Public method for removing an attachment
*/
async removeAttachment(attachedUuid) {
await this.parent.update({
'system.attached': this.attached.filter(uuid => uuid !== attachedUuid)
});
// Remove effects
await this.#removeAttachmentEffects(attachedUuid);
}
async #removeAttachmentEffects(attachedUuid) {
const actor = this.parent.parent;
if (!actor) return;
const parentType = this.parent.type;
const parentUuidProperty = `${parentType}Uuid`;
const effectsToRemove = actor.effects.filter(effect => {
const attachmentSource = effect.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.itemAttachmentSource);
return (
attachmentSource &&
attachmentSource[parentUuidProperty] === this.parent.uuid &&
attachmentSource.itemUuid === attachedUuid
);
});
if (effectsToRemove.length > 0) {
await actor.deleteEmbeddedDocuments(
'ActiveEffect',
effectsToRemove.map(e => e.id)
);
}
}
}

View file

@ -1,8 +1,8 @@
import BaseDataItem from './base.mjs';
import AttachableItem from './attachableItem.mjs';
import { actionsTypes } from '../action/_module.mjs';
import ActionField from '../fields/actionField.mjs';
export default class DHWeapon extends BaseDataItem {
export default class DHWeapon extends AttachableItem {
/** @inheritDoc */
static get metadata() {
return foundry.utils.mergeObject(super.metadata, {
@ -56,6 +56,7 @@ export default class DHWeapon extends BaseDataItem {
damage: {
parts: [
{
type: ['physical'],
value: {
multiplier: 'prof',
dice: 'd8'

View file

@ -14,6 +14,10 @@ export default class DamageRoll extends DHRoll {
super.postEvaluate(roll, config);
config.roll.type = config.type;
config.roll.modifierTotal = this.calculateTotalModifiers(roll);
}
static async buildPost(roll, config, message) {
await super.buildPost(roll, config, message);
if (config.source?.message) {
const chatMessage = ui.chat.collection.get(config.source.message);
chatMessage.update({ 'system.damage': config });

View file

@ -56,8 +56,8 @@ export default class DHRoll extends Roll {
// Create Chat Message
if (config.source?.message) {
if (game.modules.get('dice-so-nice')?.active) await game.dice3d.showForRoll(roll, game.user, true);
} else {
const messageData = {};
config.message = await this.toMessage(roll, config);
}
}

View file

@ -1,16 +1,44 @@
export default class DhActiveEffect extends ActiveEffect {
get isSuppressed() {
if (['weapon', 'armor'].includes(this.parent.type)) {
// If this is a copied effect from an attachment, never suppress it
// (These effects have attachmentSource metadata)
if (this.flags?.daggerheart?.attachmentSource) {
return false;
}
// Then apply the standard suppression rules
if (['weapon', 'armor'].includes(this.parent?.type)) {
return !this.parent.system.equipped;
}
if (this.parent.type === 'domainCard') {
if (this.parent?.type === 'domainCard') {
return this.parent.system.inVault;
}
return super.isSuppressed;
}
/**
* Check if the parent item is currently attached to another item
* @returns {boolean}
*/
get isAttached() {
if (!this.parent || !this.parent.parent) return false;
// Check if this item's UUID is in any actor's armor or weapon attachment lists
const actor = this.parent.parent;
if (!actor || !actor.items) return false;
return actor.items.some(item => {
return (
(item.type === 'armor' || item.type === 'weapon') &&
item.system?.attached &&
Array.isArray(item.system.attached) &&
item.system.attached.includes(this.parent.uuid)
);
});
}
async _preCreate(data, options, user) {
const update = {};
if (!data.img) {

View file

@ -370,7 +370,10 @@ export default class DhpActor extends Actor {
}
getRollData() {
return this.system;
const rollData = super.getRollData();
rollData.prof = this.system.proficiency?.total ?? 1;
rollData.cast = this.system.spellcast?.total ?? 1;
return rollData;
}
formatRollModifier(roll) {
@ -462,7 +465,7 @@ export default class DhpActor extends Actor {
const canUseArmor =
this.system.armor &&
this.system.armor.system.marks.value < this.system.armorScore &&
this.system.armorApplicableDamageTypes[type];
type.every(t => this.system.armorApplicableDamageTypes[t] === true);
const canUseStress = Object.keys(this.system.rules.damageReduction.stressDamageReduction).reduce((acc, x) => {
const rule = this.system.rules.damageReduction.stressDamageReduction[x];
if (damageKeyToNumber(x) <= hpDamage) return acc || (rule.enabled && availableStress >= rule.cost);
@ -480,11 +483,9 @@ export default class DhpActor extends Actor {
return;
}
const flatReduction = this.system.bonuses.damageReduction[type];
const damage = Math.max(baseDamage - (flatReduction ?? 0), 0);
const hpDamage = this.convertDamageToThreshold(damage);
type = !Array.isArray(type) ? [type] : type;
if (Hooks.call(`${CONFIG.DH.id}.postDamageTreshold`, this, hpDamage, damage, type) === false) return null;
const hpDamage = this.calculateDamage(baseDamage, type);
if (!hpDamage) return;
@ -511,6 +512,38 @@ export default class DhpActor extends Actor {
if (Hooks.call(`${CONFIG.DH.id}.postTakeDamage`, this, damage, type) === false) return null;
}
calculateDamage(baseDamage, type) {
if (Hooks.call(`${CONFIG.DH.id}.preCalculateDamage`, this, baseDamage, type) === false) return null;
/* if(this.system.resistance[type]?.immunity) return 0;
if(this.system.resistance[type]?.resistance) baseDamage = Math.ceil(baseDamage / 2); */
if (this.canResist(type, 'immunity')) return 0;
if (this.canResist(type, 'resistance')) baseDamage = Math.ceil(baseDamage / 2);
// const flatReduction = this.system.resistance[type].reduction;
const flatReduction = this.getDamageTypeReduction(type);
const damage = Math.max(baseDamage - (flatReduction ?? 0), 0);
const hpDamage = this.convertDamageToThreshold(damage);
if (Hooks.call(`${CONFIG.DH.id}.postCalculateDamage`, this, baseDamage, type) === false) return null;
return hpDamage;
}
canResist(type, resistance) {
if (!type) return 0;
return type.every(t => this.system.resistance[t]?.[resistance] === true);
}
getDamageTypeReduction(type) {
if (!type) return 0;
const reduction = Object.entries(this.system.resistance).reduce(
(a, [index, value]) => (type.includes(index) ? Math.min(value.reduction, a) : a),
Infinity
);
return reduction === Infinity ? 0 : reduction;
}
async takeHealing(resources) {
resources.forEach(r => (r.value *= -1));
await this.modifyResource(resources);
@ -577,18 +610,6 @@ export default class DhpActor extends Actor {
u.resources,
u.target.uuid
);
/* if (game.user.isGM) {
await u.target.update(u.resources);
} else {
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.GMUpdate,
data: {
action: GMUpdateEvent.UpdateDocument,
uuid: u.target.uuid,
update: u.resources
}
});
} */
}
}
});

View file

@ -236,16 +236,7 @@ Roll.replaceFormulaData = function (formula, data = {}, { missing, warn = false
};
export const getDamageKey = damage => {
switch (damage) {
case 3:
return 'severe';
case 2:
return 'major';
case 1:
return 'minor';
case 0:
return 'none';
}
return ['none', 'minor', 'major', 'severe'][damage];
};
export const getDamageLabel = damage => {
@ -253,16 +244,12 @@ export const getDamageLabel = damage => {
};
export const damageKeyToNumber = key => {
switch (key) {
case 'severe':
return 3;
case 'major':
return 2;
case 'minor':
return 1;
case 'none':
return 0;
}
return {
none: 0,
minor: 1,
major: 2,
severe: 3
}[key];
};
export default function constructHTMLButton({

View file

@ -104,6 +104,40 @@
}
}
multi-select {
position: relative;
height: 34px;
.tags {
justify-content: flex-start;
margin: 5px;
height: inherit;
.tag {
box-shadow: 0 0 0 1.1em #e5e5e5 inset;
vertical-align: top;
box-sizing: border-box;
max-width: 100%;
padding: 0.3em 0 0.3em 0.5em;
color: black;
border-radius: 3px;
white-space: nowrap;
transition: 0.13s ease-out;
height: 22px;
font-size: 0.9rem;
gap: 0.5em;
z-index: 1;
.remove {
font-size: 10px;
margin-inline: auto 4.6666666667px;
}
}
}
select {
position: absolute;
height: inherit;
outline: initial;
}
}
p {
margin: 0;
}

View file

@ -12,3 +12,4 @@
@import './inventory-fieldset-items.less';
@import './prose-mirror.less';
@import './filter-menu.less';
@import './tab-attachments.less';

View file

@ -0,0 +1,7 @@
.daggerheart.dh-style {
.tab.attachments {
.attached-items {
width: 100%;
}
}
}

View file

@ -9,6 +9,7 @@
gap: 15px 0;
height: 100%;
width: 100%;
padding-bottom: 0;
.adversary-sidebar-sheet {
grid-row: 1 / span 2;

View file

@ -110,10 +110,11 @@
justify-content: space-evenly;
.status-bar {
display: flex;
justify-content: center;
position: relative;
width: 100px;
height: 40px;
justify-items: center;
.status-label {
position: relative;

View file

@ -9,6 +9,7 @@
gap: 15px 0;
height: 100%;
width: 100%;
padding-bottom: 0;
overflow: auto;
.character-sidebar-sheet {

View file

@ -70,10 +70,11 @@
justify-content: space-evenly;
.status-bar {
display: flex;
justify-content: center;
position: relative;
width: 100px;
height: 40px;
justify-items: center;
.status-label {
position: relative;

View file

@ -45,7 +45,9 @@
justify-content: center;
.status-number {
justify-items: center;
display: flex;
flex-direction: column;
align-items: center;
.status-value {
position: relative;
@ -85,6 +87,8 @@
}
.status-bar {
display: flex;
justify-content: center;
position: relative;
width: 100px;
height: 40px;

View file

@ -7,7 +7,7 @@
{{formGroup fields.origin value=source.origin rootId=rootId disabled=true}}
{{/if}}
{{#if isItemEffect}}
{{formGroup fields.transfer value=source.transfer rootId=rootId label=legacyTransfer.label hint=legacyTransfer.hint}}
{{formGroup fields.transfer value=source.transfer rootId=rootId label=legacyTransfer.label hint=(localize "DAGGERHEART.EFFECTS.Attachments.transferHint")}}
{{/if}}
{{formGroup fields.statuses value=source.statuses options=statuses rootId=rootId classes="statuses"}}

View file

@ -10,12 +10,12 @@
<div class='status-value'>
<p><input class="bar-input" name="system.resources.hitPoints.value" value="{{source.system.resources.hitPoints.value}}" type="number"></p>
<p>/</p>
<p class="bar-label">{{source.system.resources.hitPoints.max}}</p>
<p class="bar-label">{{source.system.resources.hitPoints.maxTotal}}</p>
</div>
<progress
class='progress-bar'
value='{{source.system.resources.hitPoints.value}}'
max='{{source.system.resources.hitPoints.max}}'
max='{{source.system.resources.hitPoints.maxTotal}}'
></progress>
<div class="status-label">
<h4>HP</h4>
@ -26,12 +26,12 @@
<div class='status-value'>
<p><input class="bar-input" name="system.resources.stress.value" value="{{source.system.resources.stress.value}}" type="number"></p>
<p>/</p>
<p class="bar-label">{{source.system.resources.stress.max}}</p>
<p class="bar-label">{{source.system.resources.stress.maxTotal}}</p>
</div>
<progress
class='progress-bar stress-color'
value='{{source.system.resources.stress.value}}'
max='{{source.system.resources.stress.max}}'
max='{{source.system.resources.stress.maxTotal}}'
></progress>
<div class="status-label">
<h4>Stress</h4>

View file

@ -1,66 +1,45 @@
<li class="inventory-item" data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}" data-type="{{type}}">
<li class="inventory-item" data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}" data-type="{{type}}" draggable="true">
<img src="{{item.img}}" class="item-img {{#if isActor}}actor-img{{/if}}" data-action="useItem" {{#if (not noTooltip)}}data-tooltip="{{concat "#item#" item.uuid}}"{{/if}} />
<div class="item-label-wrapper">
<div class="item-label {{#unless (and (not isSidebar) (or item.system.resource item.system.quantity))}}fullWidth{{/unless}}">
{{#if isCompanion}}
<a class="item-name" data-action="attackRoll">{{item.name}}</a>
{{else}}
<div class="item-name">{{item.name}}</div>
{{/if}}
{{#if (eq type 'weapon')}}
<div class="item-tags">
{{#if isSidebar}}
<div class="item-labels">
<div class="label">
{{!-- {{localize (concat 'DAGGERHEART.CONFIG.Traits.' item.system.attack.roll.trait '.short')}} --}}
{{localize (concat 'DAGGERHEART.CONFIG.Range.' item.system.attack.range '.short')}}
<span> - </span>
{{item.system.attack.damage.parts.0.value.dice}}{{#if item.system.attack.damage.parts.0.value.bonus}} + {{item.system.attack.damage.parts.0.value.bonus}}{{/if}}
{{!-- ({{localize (concat 'DAGGERHEART.CONFIG.DamageType.' item.system.attack.damage.parts.0.type '.abbreviation')}}) --}}
{{#with (lookup @root.config.GENERAL.damageTypes item.system.attack.damage.parts.0.type)}}
{{#each icon}}
<i class="fa-solid {{this}}"></i>
{{/each}}
{{/with}}
</div>
</div>
{{else}}
<div class="tag">
{{localize (concat 'DAGGERHEART.CONFIG.Traits.' item.system.attack.roll.trait '.name')}}
</div>
<div class="tag">
{{localize (concat 'DAGGERHEART.CONFIG.Range.' item.system.attack.range '.name')}}
</div>
<div class="tag">
{{item.system.attack.damage.parts.0.value.dice}}{{#if item.system.attack.damage.parts.0.value.bonus}} + {{item.system.attack.damage.parts.0.value.bonus}}{{/if}}
({{localize (concat 'DAGGERHEART.CONFIG.DamageType.' item.system.attack.damage.parts.0.type '.abbreviation')}})
</div>
<div class="tag">
{{localize (concat 'DAGGERHEART.CONFIG.Burden.' item.system.burden)}}
</div>
{{/if}}
</div>
{{/if}}
{{#if (eq type 'armor')}}
<div class="item-label">
{{#if isCompanion}}
<a class="item-name" data-action="attackRoll">{{item.name}}</a>
{{else}}
<div class="item-name">{{item.name}}</div>
{{/if}}
{{#if (eq type 'weapon')}}
<div class="item-tags">
{{#if isSidebar}}
<div class="item-labels">
<div class="label">
{{localize "DAGGERHEART.ITEMS.Armor.baseScore"}}:
{{item.system.baseScore}}
{{!-- {{localize (concat 'DAGGERHEART.CONFIG.Traits.' item.system.attack.roll.trait '.short')}} --}}
{{localize (concat 'DAGGERHEART.CONFIG.Range.' item.system.attack.range '.short')}}
<span> - </span>
{{item.system.attack.damage.parts.0.value.dice}}{{#if item.system.attack.damage.parts.0.value.bonus}} + {{item.system.attack.damage.parts.0.value.bonus}}{{/if}}
{{!-- ({{localize (concat 'DAGGERHEART.CONFIG.DamageType.' item.system.attack.damage.parts.0.type '.abbreviation')}}) --}}
{{#each item.system.attack.damage.parts.0.type as | type | }}
{{#with (lookup @root.config.GENERAL.damageTypes type)}}
<i class="fa-solid {{icon}}"></i>
{{/with}}
{{/each}}
</div>
</div>
{{else}}
<div class="item-tags">
<div class="tag">
{{localize "DAGGERHEART.ITEMS.Armor.baseScore"}}:
{{item.system.baseScore}}
</div>
<div class="tag">
{{localize "DAGGERHEART.ITEMS.Armor.baseThresholds.base"}}:
{{item.system.baseThresholds.major}}
<span>/</span>
{{item.system.baseThresholds.severe}}
</div>
<div class="tag">
{{localize (concat 'DAGGERHEART.CONFIG.Traits.' item.system.attack.roll.trait '.name')}}
</div>
<div class="tag">
{{localize (concat 'DAGGERHEART.CONFIG.Range.' item.system.attack.range '.name')}}
</div>
<div class="tag">
{{item.system.attack.damage.parts.0.value.dice}}{{#if item.system.attack.damage.parts.0.value.bonus}} + {{item.system.attack.damage.parts.0.value.bonus}}{{/if}}
(
{{#each item.system.attack.damage.parts.0.type}}
{{localize (concat 'DAGGERHEART.CONFIG.DamageType.' this '.abbreviation')}}
{{/each}}
)
</div>
<div class="tag">
{{localize (concat 'DAGGERHEART.CONFIG.Burden.' item.system.burden)}}
</div>
{{/if}}
{{/if}}

View file

@ -0,0 +1,29 @@
<section
class='tab {{tabs.attachments.cssClass}} {{tabs.attachments.id}}'
data-tab='{{tabs.attachments.id}}'
data-group='{{tabs.attachments.group}}'
>
<fieldset class="one-column drop-section attachments-section">
<legend>{{localize tabs.attachments.label}}</legend>
{{#if attachedItems}}
<div class="attached-items">
{{#each attachedItems as |item|}}
<div class="inventory-item attached-item" data-uuid="{{item.uuid}}">
<img src="{{item.img}}" alt="{{item.name}}" class="item-img">
<div class="item-label">
<div class="item-name">{{item.name}}</div>
</div>
<div class="controls">
<a data-action="removeAttachment" data-uuid="{{item.uuid}}"><i class="fa-solid fa-trash remove-attachment"></i></a>
</div>
</div>
{{/each}}
</div>
{{/if}}
<div class="drop-area" data-drop-type="Item" style="width: 100%;">
<span>{{localize "DAGGERHEART.EFFECTS.Attachments.attachHint"}}</span>
</div>
</fieldset>
</section>

View file

@ -15,7 +15,11 @@
{{localize (concat 'DAGGERHEART.CONFIG.Range.' source.system.attack.range '.name')}}
<span>-</span>
{{source.system.attack.damage.parts.0.value.dice}}{{#if source.system.attack.damage.parts.0.value.bonus}} + {{source.system.attack.damage.parts.0.value.bonus}}{{/if}}
({{localize (concat 'DAGGERHEART.CONFIG.DamageType.' source.system.attack.damage.parts.0.type '.abbreviation')}})
(
{{#each source.system.attack.damage.parts.0.type}}
{{localize (concat 'DAGGERHEART.CONFIG.DamageType.' this '.abbreviation')}}
{{/each}}
)
<span>-</span>
{{localize (concat 'DAGGERHEART.CONFIG.Burden.' source.system.burden)}}
</h3>