Merged with main

This commit is contained in:
WBHarry 2026-06-05 16:00:09 +02:00
commit d78927d0c2
275 changed files with 5076 additions and 4132 deletions

View file

@ -2,4 +2,5 @@ export { default as Adversary } from './adversary.mjs';
export { default as Character } from './character.mjs';
export { default as Companion } from './companion.mjs';
export { default as Environment } from './environment.mjs';
export { default as NPC } from './npc.mjs';
export { default as Party } from './party.mjs';

View file

@ -31,6 +31,16 @@ export default class AdversarySheet extends DHBaseActorSheet {
dragSelector: '[data-item-id][draggable="true"], [data-item-id] [draggable="true"]',
dropSelector: null
}
],
contextMenus: [
{
handler: DHBaseActorSheet.getBaseAttackContextOptions,
selector: '[data-item-uuid][data-type="attack"]',
options: {
parentClassHooks: false,
fixed: true
}
}
]
};

View file

@ -3,7 +3,7 @@ import DhDeathMove from '../../dialogs/deathMove.mjs';
import { CharacterLevelup, LevelupViewMode } from '../../levelup/_module.mjs';
import DhCharacterCreation from '../../characterCreation/characterCreation.mjs';
import FilterMenu from '../../ux/filter-menu.mjs';
import { getArmorSources, getDocFromElement, getDocFromElementSync } from '../../../helpers/utils.mjs';
import { getArmorSources, getDocFromElement, getDocFromElementSync, sortBy } from '../../../helpers/utils.mjs';
/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */
@ -57,6 +57,22 @@ export default class CharacterSheet extends DHBaseActorSheet {
}
],
contextMenus: [
{
handler: CharacterSheet.#getCreationMainContextOptions,
selector: '.character-details [data-action="editDoc"]',
options: {
parentClassHooks: false,
fixed: true
}
},
{
handler: DHBaseActorSheet.getBaseAttackContextOptions,
selector: '[data-item-uuid][data-type="attack"]',
options: {
parentClassHooks: false,
fixed: true
}
},
{
handler: CharacterSheet.#getDomainCardContextOptions,
selector: '[data-item-uuid][data-type="domainCard"]',
@ -176,6 +192,9 @@ export default class CharacterSheet extends DHBaseActorSheet {
for (const input of form.querySelectorAll('input:not([type=search]), .editor.prosemirror')) {
input.disabled = disabled;
}
for (const element of form.querySelectorAll('.input[contenteditable]')) {
element.classList.toggle('disabled', disabled);
}
}
/** @inheritDoc */
@ -209,8 +228,9 @@ export default class CharacterSheet extends DHBaseActorSheet {
context.attributes = Object.keys(this.document.system.traits).reduce((acc, key) => {
acc[key] = {
...this.document.system.traits[key],
name: game.i18n.localize(CONFIG.DH.ACTOR.abilities[key].name),
verbs: CONFIG.DH.ACTOR.abilities[key].verbs.map(x => game.i18n.localize(x))
label: _loc(CONFIG.DH.ACTOR.abilities[key].label),
verbs: CONFIG.DH.ACTOR.abilities[key].verbs.map(x => game.i18n.localize(x)),
isSpellcasting: this.document.system.spellcastModifierTrait?.key === key
};
return acc;
@ -226,6 +246,11 @@ export default class CharacterSheet extends DHBaseActorSheet {
context.resources.stress.emptyPips =
context.resources.stress.max < maxResource ? maxResource - context.resources.stress.max : 0;
context.equippedItems = sortBy(
this.document.items.filter(i => i.system.equipped && (i.type === 'weapon' || i.usable)),
i => (i.type === 'weapon' ? (i.system.secondary ? 1 : 0) : 2)
);
context.beastformActive = this.document.effects.find(x => x.type === 'beastform');
return context;
@ -313,6 +338,56 @@ export default class CharacterSheet extends DHBaseActorSheet {
/* Context Menu */
/* -------------------------------------------- */
static #getCreationMainContextOptions() {
/** Returns true if the item is managed by the level up wizard. Such items shouldn't allow things like manual removal */
function isItemWizardManaged(item) {
const actor = item?.actor;
if (!actor) return false;
// If levelup automation is off in general or for this character, all items are unmanaged
// This is disabled until we have proper granted feature removal, for now this feature is to correct errors
// const levelupAuto = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).levelupAuto;
// if (!levelupAuto) return false;
// Core items aren't part of levelup data. TODO: add some way to flag a specific character as no auto leveling
const classPair = actor.system.class;
const coreItems = [actor.system.ancestry, actor.system.community, classPair?.value, classPair?.subclass];
if (coreItems.includes(item)) return true;
const levelups = Object.values(actor.system.levelData?.levelups) ?? [];
const uuid = item.uuid;
const sourceUuid = item._stats.compendiumSource; // on older characters this may be missing
return levelups.some(data => {
if (item.type === 'subclass') {
const selectedSubclasses = data.selections.map(s => s.secondaryData?.subclass).filter(s => !!s);
return sourceUuid
? selectedSubclasses.includes(sourceUuid)
: selectedSubclasses.length && item.system.isMulticlass;
}
const matchesCard = data.achievements.domainCards.some(i => i.itemUuid === uuid);
const matchesSelection = data.selections.some(s => s.itemUuid === uuid);
return matchesCard || matchesSelection;
});
}
return [
{
label: 'CONTROLS.CommonDelete',
icon: 'fa-solid fa-trash',
visible: target => {
const doc = getDocFromElementSync(target);
return doc?.isOwner && !isItemWizardManaged(doc);
},
onClick: async (event, target) => {
const doc = await getDocFromElement(target);
if (event.shiftKey) return doc.delete();
else return doc.deleteDialog();
}
}
];
}
/**
* Get the set of ContextMenu options for DomainCards.
* @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance
@ -329,7 +404,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
const doc = getDocFromElementSync(target);
return doc?.isOwner && doc.system.inVault;
},
callback: async target => {
onClick: async (_, target) => {
const doc = await getDocFromElement(target);
const actorLoadout = doc.actor.system.loadoutSlot;
if (actorLoadout.available) return doc.update({ 'system.inVault': false });
@ -343,7 +418,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
const doc = getDocFromElementSync(target);
return doc?.isOwner && doc.system.inVault;
},
callback: async (target, event) => {
onClick: async (event, target) => {
const doc = await getDocFromElement(target);
const actorLoadout = doc.actor.system.loadoutSlot;
if (!actorLoadout.available) {
@ -382,7 +457,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
const doc = getDocFromElementSync(target);
return doc?.isOwner && !doc.system.inVault;
},
callback: async target => (await getDocFromElement(target)).update({ 'system.inVault': true })
onClick: async (_, target) => (await getDocFromElement(target)).update({ 'system.inVault': true })
}
].map(option => ({
...option,
@ -408,7 +483,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
const doc = getDocFromElementSync(target);
return doc.isOwner && doc && !doc.system.equipped;
},
callback: (target, event) => CharacterSheet.#toggleEquipItem.call(this, event, target)
onClick: (event, target) => CharacterSheet.#toggleEquipItem.call(this, event, target)
},
{
label: 'unequip',
@ -417,7 +492,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
const doc = getDocFromElementSync(target);
return doc.isOwner && doc && doc.system.equipped;
},
callback: (target, event) => CharacterSheet.#toggleEquipItem.call(this, event, target)
onClick: (event, target) => CharacterSheet.#toggleEquipItem.call(this, event, target)
}
].map(option => ({
...option,
@ -712,7 +787,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
? {
'system.linkedClass.uuid': {
key: 'system.linkedClass.uuid',
value: this.document.system.class.value._stats.compendiumSource
value: this.document.system.class.value?._stats.compendiumSource
}
}
: undefined,
@ -978,7 +1053,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
game.tooltip.activate(target, {
html,
locked: true,
cssClass: 'bordered-tooltip',
cssClass: 'bordered-tooltip dh-style',
direction: 'DOWN'
});
@ -1074,7 +1149,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
game.tooltip.activate(target, {
html,
locked: true,
cssClass: 'bordered-tooltip',
cssClass: 'bordered-tooltip dh-style',
direction: 'DOWN',
noOffset: true
});

View file

@ -11,7 +11,17 @@ export default class DhCompanionSheet extends DHBaseActorSheet {
toggleStress: DhCompanionSheet.#toggleStress,
actionRoll: DhCompanionSheet.#actionRoll,
levelManagement: DhCompanionSheet.#levelManagement
}
},
contextMenus: [
{
handler: DHBaseActorSheet.getBaseAttackContextOptions,
selector: '[data-item-uuid][data-type="attack"]',
options: {
parentClassHooks: false,
fixed: true
}
}
]
};
static PARTS = {

View file

@ -0,0 +1,136 @@
import DHBaseActorSheet from '../api/base-actor.mjs';
export default class NPCSheet extends DHBaseActorSheet {
/** @inheritDoc */
static DEFAULT_OPTIONS = {
classes: ['npc'],
position: { width: 660, height: 600 },
window: { resizable: true },
actions: {},
window: {
resizable: true,
controls: [
{
icon: 'fa-solid fa-signature',
label: 'DAGGERHEART.UI.Tooltip.configureAttribution',
action: 'editAttribution'
}
]
},
dragDrop: [
{
dragSelector: '[data-item-id][draggable="true"], [data-item-id] [draggable="true"]',
dropSelector: null
}
]
};
static PARTS = {
header: { template: 'systems/daggerheart/templates/sheets/actors/npc/header.hbs' },
tabs: { template: 'systems/daggerheart/templates/sheets/actors/npc/navigation.hbs' },
features: {
template: 'systems/daggerheart/templates/sheets/actors/npc/features.hbs',
scrollable: ['.feature-section']
},
notes: {
template: 'systems/daggerheart/templates/sheets/actors/npc/notes.hbs'
}
};
/** @inheritdoc */
static TABS = {
primary: {
tabs: [{ id: 'notes' }, { id: 'features' }],
initial: 'notes',
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
}
};
/** @inheritdoc */
_prepareTabs(group) {
const result = super._prepareTabs(group);
if (group === 'primary') {
result.features.empty = this.document.system.features.length === 0;
}
return result;
}
/** @inheritdoc */
async _preparePartContext(partId, context, options) {
context = await super._preparePartContext(partId, context, options);
switch (partId) {
case 'header':
await this._prepareHeaderContext(context, options);
break;
case 'features':
await this._prepareFeaturesContext(context, options);
break;
case 'notes':
await this._prepareNotesContext(context, options);
break;
}
return context;
}
/**
* Prepare render context for the Header part.
* @param {ApplicationRenderContext} context
* @param {ApplicationRenderOptions} options
* @returns {Promise<void>}
* @protected
*/
async _prepareHeaderContext(context, _options) {
const { system } = this.document;
const { TextEditor } = foundry.applications.ux;
context.description = await TextEditor.implementation.enrichHTML(system.description, {
secrets: this.document.isOwner,
relativeTo: this.document
});
}
/**
* Prepare render context for the Features part.
* @param {ApplicationRenderContext} context
* @param {ApplicationRenderOptions} options
* @returns {Promise<void>}
* @protected
*/
async _prepareFeaturesContext(context, _options) {
const featureForms = ['passive', 'action', 'reaction'];
context.features = this.document.system.features.sort((a, b) =>
a.system.featureForm !== b.system.featureForm
? featureForms.indexOf(a.system.featureForm) - featureForms.indexOf(b.system.featureForm)
: a.sort - b.sort
);
}
/**
* Prepare render context for the Biography part.
* @param {ApplicationRenderContext} context
* @param {ApplicationRenderOptions} options
* @returns {Promise<void>}
* @protected
*/
async _prepareNotesContext(context, _options) {
const { system } = this.document;
const { TextEditor } = foundry.applications.ux;
const paths = {
notes: 'notes'
};
for (const [key, path] of Object.entries(paths)) {
const value = foundry.utils.getProperty(system, path);
context[key] = {
field: system.schema.getField(path),
value,
enriched: await TextEditor.implementation.enrichHTML(value, {
secrets: this.document.isOwner,
relativeTo: this.document
})
};
}
}
}

View file

@ -26,7 +26,6 @@ export default class Party extends DHBaseActorSheet {
actions: {
openDocument: Party.#openDocument,
deletePartyMember: Party.#deletePartyMember,
deleteItem: Party.#deleteItem,
toggleHope: Party.#toggleHope,
toggleHitPoints: Party.#toggleHitPoints,
toggleStress: Party.#toggleStress,
@ -44,12 +43,10 @@ export default class Party extends DHBaseActorSheet {
static PARTS = {
header: { template: 'systems/daggerheart/templates/sheets/actors/party/header.hbs' },
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
partyMembers: { template: 'systems/daggerheart/templates/sheets/actors/party/party-members.hbs' },
/* NOT YET IMPLEMENTED */
// projects: {
// template: 'systems/daggerheart/templates/sheets/actors/party/projects.hbs',
// scrollable: ['']
// },
partyMembers: {
template: 'systems/daggerheart/templates/sheets/actors/party/party-members.hbs',
scrollable: ['']
},
inventory: {
template: 'systems/daggerheart/templates/sheets/actors/party/inventory.hbs',
scrollable: ['.tab.inventory .items-section']
@ -60,19 +57,13 @@ export default class Party extends DHBaseActorSheet {
/** @inheritdoc */
static TABS = {
primary: {
tabs: [
{ id: 'partyMembers' },
/* NOT YET IMPLEMENTED */
// { id: 'projects' },
{ id: 'inventory' },
{ id: 'notes' }
],
tabs: [{ id: 'partyMembers' }, { id: 'inventory' }, { id: 'notes' }],
initial: 'partyMembers',
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
}
};
static ALLOWED_ACTOR_TYPES = ['character', 'companion', 'adversary'];
static ALLOWED_ACTOR_TYPES = ['character', 'companion', 'adversary', 'npc'];
static DICE_ROLL_ACTOR_TYPES = ['character'];
async _onRender(context, options) {
@ -85,6 +76,14 @@ export default class Party extends DHBaseActorSheet {
/* Prepare Context */
/* -------------------------------------------- */
async _prepareContext(options) {
const context = await super._prepareContext(options);
const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Metagaming);
context.showStats =
settings.hidePartyStats === 'never' || (settings.hidePartyStats === 'players' && game.user.isGM);
return context;
}
async _preparePartContext(partId, context, options) {
context = await super._preparePartContext(partId, context, options);
switch (partId) {
@ -498,23 +497,4 @@ export default class Party extends DHBaseActorSheet {
const newMembersList = currentMembers.filter(uuid => uuid !== doc.uuid);
await this.document.update({ 'system.partyMembers': newMembersList });
}
static async #deleteItem(event, target) {
const doc = await getDocFromElement(target.closest('.inventory-item'));
if (!event.shiftKey) {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', {
type: game.i18n.localize('TYPES.Actor.party'),
name: doc.name
})
},
content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: doc.name })
});
if (!confirmed) return;
}
this.document.deleteEmbeddedDocuments('Item', [doc.id]);
}
}

View file

@ -89,7 +89,7 @@ export default function DHApplicationMixin(Base) {
classes: ['daggerheart', 'sheet', 'dh-style'],
actions: {
triggerContextMenu: DHSheetV2.#triggerContextMenu,
createDoc: DHSheetV2.#createDoc,
createDoc: DHSheetV2.#onCreateDoc,
editDoc: DHSheetV2.#editDoc,
deleteDoc: DHSheetV2.#deleteDoc,
toChat: DHSheetV2.#toChat,
@ -97,8 +97,8 @@ export default function DHApplicationMixin(Base) {
viewItem: DHSheetV2.#viewItem,
toggleEffect: DHSheetV2.#toggleEffect,
toggleExtended: DHSheetV2.#toggleExtended,
addNewItem: DHSheetV2.#addNewItem,
browseItem: DHSheetV2.#browseItem,
addNewItem: DHSheetV2.#onAddNewItem,
browseItem: DHSheetV2.#onBrowseItem,
editAttribution: DHSheetV2.#editAttribution,
configureLevelUpOptions: DHSheetV2.#configureLevelUpOptions
},
@ -439,7 +439,7 @@ export default function DHApplicationMixin(Base) {
const target = element.closest('[data-item-uuid]');
return !target.dataset.disabled && target.dataset.itemType !== 'beastform';
},
callback: async target => (await getDocFromElement(target)).update({ disabled: true })
onClick: async (_, target) => (await getDocFromElement(target)).update({ disabled: true })
},
{
label: 'enableEffect',
@ -448,7 +448,7 @@ export default function DHApplicationMixin(Base) {
const target = element.closest('[data-item-uuid]');
return target.dataset.disabled && target.dataset.itemType !== 'beastform';
},
callback: async target => (await getDocFromElement(target)).update({ disabled: false })
onClick: async (_, target) => (await getDocFromElement(target)).update({ disabled: false })
}
].map(option => ({
...option,
@ -493,7 +493,9 @@ export default function DHApplicationMixin(Base) {
(doc?.isOwner && (!doc?.hasOwnProperty('systemPath') || doc?.inCollection))
);
},
callback: async target => (await getDocFromElement(target)).sheet.render({ force: true })
onClick: async (_, target) => {
return (await getDocFromElement(target)).sheet.render({ force: true });
}
}
];
@ -508,7 +510,7 @@ export default function DHApplicationMixin(Base) {
!foundry.utils.isEmpty(doc?.damage?.parts);
return doc?.isOwner && hasDamage;
},
callback: async (target, event) => {
onClick: async (event, target) => {
const doc = await getDocFromElement(target),
action = doc?.system?.attack ?? doc;
const config = action.prepareConfig(event);
@ -528,7 +530,7 @@ export default function DHApplicationMixin(Base) {
const doc = getDocFromElementSync(target);
return doc?.isOwner && !(doc.type === 'domainCard' && doc.system.inVault);
},
callback: async (target, event) => (await getDocFromElement(target)).use(event)
onClick: async (event, target) => (await getDocFromElement(target)).use(event)
});
}
@ -536,7 +538,7 @@ export default function DHApplicationMixin(Base) {
options.push({
label: 'DAGGERHEART.APPLICATIONS.ContextMenu.sendToChat',
icon: 'fa-solid fa-message',
callback: async target => (await getDocFromElement(target)).toChat(this.document.uuid)
onClick: async (_, target) => (await getDocFromElement(target)).toChat(this.document.uuid)
});
if (deletable)
@ -546,9 +548,9 @@ export default function DHApplicationMixin(Base) {
visible: element => {
const target = element.closest('[data-item-uuid]');
const doc = getDocFromElementSync(target);
return doc?.isOwner && target.dataset.itemType !== 'beastform';
return doc?.isOwner !== false && target.dataset.itemType !== 'beastform';
},
callback: async (target, event) => {
onClick: async (event, target) => {
const doc = await getDocFromElement(target);
if (event.shiftKey) return doc.delete();
else return doc.deleteDialog();
@ -654,7 +656,7 @@ export default function DHApplicationMixin(Base) {
/* Application Clicks Actions */
/* -------------------------------------------- */
static async #addNewItem(event, target) {
static async #onAddNewItem(event, target) {
const createChoice = await foundry.applications.api.DialogV2.wait({
classes: ['dh-style', 'two-big-buttons'],
buttons: [
@ -673,11 +675,11 @@ export default function DHApplicationMixin(Base) {
if (!createChoice) return;
if (createChoice === 'browse') return DHSheetV2.#browseItem.call(this, event, target);
else return DHSheetV2.#createDoc.call(this, event, target);
if (createChoice === 'browse') return DHSheetV2.#onBrowseItem.call(this, event, target);
else return DHSheetV2.#onCreateDoc.call(this, event, target);
}
static async #browseItem(event, target) {
static async #onBrowseItem(_event, target) {
const type = target.dataset.compendium ?? target.dataset.type;
const presets = {
@ -732,7 +734,7 @@ export default function DHApplicationMixin(Base) {
* Create an embedded document.
* @type {ApplicationClickAction}
*/
static async #createDoc(event, target) {
static async #onCreateDoc(event, target) {
const { documentClass, type, inVault, disabled } = target.dataset;
const parentIsItem = this.document.documentName === 'Item';
const featureOnCharacter = this.document.parent?.type === 'character' && type === 'feature';
@ -760,11 +762,15 @@ export default function DHApplicationMixin(Base) {
type,
system: systemData
};
if (inVault) data['system.inVault'] = true;
if (disabled) data.disabled = true;
if (type === 'domainCard' && parent?.system.domains?.length) {
data.system.domain = parent.system.domains[0];
if (type === 'domainCard') {
if (parent?.system.domains?.length) data.system.domain = parent.system.domains[0];
if (inVault) data.system.inVault = true;
} else if (type === 'weapon') {
// Passing an empty system object to weapon causes validation failure due to attack action initialization
// todo: determine why, fix it at its source, then remove this fallback
delete data.system;
}
const doc = await cls.create(data, { parent, renderSheet: !event.shiftKey });

View file

@ -166,6 +166,15 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
}
}
/** Add support for input content editables */
_toggleDisabled(disabled) {
super._toggleDisabled(disabled);
const form = this.form;
for (const element of form.querySelectorAll('.input[contenteditable]')) {
element.classList.toggle('disabled', disabled);
}
}
/* -------------------------------------------- */
/* Context Menu */
/* -------------------------------------------- */
@ -180,6 +189,43 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
return this._getContextMenuCommonOptions.call(this, { usable: true, toChat: true });
}
/**
* Get the set of ContextMenu options for the base attack.
* @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance
* @this {CharacterSheet}
* @protected
*/
static getBaseAttackContextOptions() {
/**@type {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} */
return [
{
label: 'DAGGERHEART.CONFIG.RollTypes.attack.name',
icon: 'fa-solid fa-burst',
onClick: async (event, target) => (await getDocFromElement(target)).use(event)
},
{
label: 'DAGGERHEART.GENERAL.damage',
icon: 'fa-solid fa-explosion',
onClick: async (event, target) => {
const doc = await getDocFromElement(target),
action = doc?.system?.attack ?? doc;
const config = action.prepareConfig(event);
config.effects = await game.system.api.data.actions.actionsTypes.base.getEffects(
this.document,
doc
);
config.hasRoll = false;
return action && action.workflow.get('damage').execute(config, null, true);
}
},
{
label: 'DAGGERHEART.APPLICATIONS.ContextMenu.sendToChat',
icon: 'fa-solid fa-message',
onClick: async (_, target) => (await getDocFromElement(target)).toChat(this.document.uuid)
}
];
}
/* -------------------------------------------- */
/* Application Listener Actions */
/* -------------------------------------------- */

View file

@ -126,7 +126,7 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
options.push({
name: 'CONTROLS.CommonDelete',
icon: '<i class="fa-solid fa-trash"></i>',
callback: async target => {
onClick: async (_, target) => {
const feature = await getDocFromElement(target);
if (!feature) return;
const confirmed = await foundry.applications.api.DialogV2.confirm({

View file

@ -104,9 +104,10 @@ export default class ClassSheet extends DHBaseItemSheet {
}
/**@inheritdoc */
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
async _prepareContext(options) {
const context = await super._prepareContext(options);
context.domains = this.document.system.domains;
context.subclasses = await this.document.system.fetchSubclasses();
return context;
}
@ -128,20 +129,8 @@ export default class ClassSheet extends DHBaseItemSheet {
const item = await fromUuid(data.uuid);
const itemType = data.type === 'ActiveEffect' ? data.type : item.type;
const target = event.target.closest('fieldset.drop-section');
if (itemType === 'subclass') {
if (item.system.linkedClass) {
return ui.notifications.warn(
game.i18n.format('DAGGERHEART.UI.Notifications.subclassAlreadyLinked', {
name: item.name,
class: this.document.name
})
);
}
await item.update({ 'system.linkedClass': this.document.uuid });
await this.document.update({
'system.subclasses': [...this.document.system.subclasses.map(x => x.uuid), item.uuid]
});
} else if (['feature', 'ActiveEffect'].includes(itemType)) {
if (['feature', 'ActiveEffect'].includes(itemType)) {
super._onDrop(event);
} else if (this.document.parent?.type !== 'character') {
if (itemType === 'weapon') {
@ -200,12 +189,6 @@ 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);
if (target === 'subclasses') {
const subclass = await foundry.utils.fromUuid(uuid);
await subclass?.update({ 'system.linkedClass': null });
}
await this.document.update({ [`system.${target}`]: prop.filter(i => i && i.uuid !== uuid).map(x => x.uuid) });
}

View file

@ -40,4 +40,36 @@ export default class SubclassSheet extends DHBaseItemSheet {
get relatedDocs() {
return this.document.system.features.map(x => x.item);
}
async _prepareContext(options) {
const context = await super._prepareContext(options);
if (this.document.system.linkedClass) {
const classData = await fromUuid(this.document.system.linkedClass);
context.class = classData ?? {
name: _loc('DAGGERHEART.GENERAL.missingX', { x: _loc('TYPES.Item.class') }),
missing: true
};
}
return context;
}
async _onDrop(event) {
event.stopPropagation();
const data = TextEditor.getDragEventData(event);
const item = await fromUuid(data.uuid);
const itemType = data.type === 'ActiveEffect' ? data.type : item.type;
if (itemType === 'class') {
const uuid = item._stats.compendiumSource ?? item.uuid;
if (this.document.system.linkedClass !== uuid) {
await this.document.update({ 'system.linkedClass': uuid });
// Re-render all class sheets for instant feedback
for (const app of foundry.applications.instances.values()) {
if (app.document?.type === 'class') app.render();
}
}
return;
}
return super._onDrop(event);
}
}