Merged with main

This commit is contained in:
WBHarry 2026-05-20 23:36:02 +02:00
commit a6e9f2cac2
10 changed files with 120 additions and 73 deletions

View file

@ -1,5 +1,3 @@
import { slugify } from '../../helpers/utils.mjs';
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export default class CompendiumBrowserSettings extends HandlebarsApplicationMixin(ApplicationV2) { export default class CompendiumBrowserSettings extends HandlebarsApplicationMixin(ApplicationV2) {
@ -55,9 +53,9 @@ export default class CompendiumBrowserSettings extends HandlebarsApplicationMixi
const { type, label, packageType, packageName: basePackageName, id: baseId } = pack.metadata; const { type, label, packageType, packageName: basePackageName, id: baseId } = pack.metadata;
if (!CompendiumBrowserSettings.#browserPackTypes.includes(type)) return acc; if (!CompendiumBrowserSettings.#browserPackTypes.includes(type)) return acc;
const id = slugify(baseId); const id = baseId.slugify();
const isWorldPack = packageType === 'world'; const isWorldPack = packageType === 'world';
const packageName = isWorldPack ? 'world' : slugify(basePackageName); const packageName = isWorldPack ? 'world' : basePackageName.slugify();
const sourceChecked = const sourceChecked =
!excludedSourceData[packageName] || !excludedSourceData[packageName] ||
!excludedSourceData[packageName].excludedDocumentTypes.includes(type); !excludedSourceData[packageName].excludedDocumentTypes.includes(type);

View file

@ -1,6 +1,5 @@
import { DhHomebrew } from '../../data/settings/_module.mjs'; import { DhHomebrew } from '../../data/settings/_module.mjs';
import { Resource } from '../../data/settings/Homebrew.mjs'; import { Resource } from '../../data/settings/Homebrew.mjs';
import { slugify } from '../../helpers/utils.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -403,12 +402,12 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
const domainName = button.form.elements.domainName.value; const domainName = button.form.elements.domainName.value;
if (!domainName) return; if (!domainName) return;
const newSlug = slugify(domainName); const newSlug = domainName.slugify();
const existingDomains = [ const existingDomains = [
...Object.values(this.settings.domains), ...Object.values(this.settings.domains),
...Object.values(CONFIG.DH.DOMAIN.domains) ...Object.values(CONFIG.DH.DOMAIN.domains)
]; ];
if (existingDomains.find(x => slugify(game.i18n.localize(x.label)) === newSlug)) { if (existingDomains.find(x => x.id === newSlug)) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.domains.duplicateDomain')); ui.notifications.warn(game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.domains.duplicateDomain'));
return; return;
} }
@ -529,7 +528,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
const identifier = button.form.elements.identifier.value; const identifier = button.form.elements.identifier.value;
if (!identifier) return; if (!identifier) return;
const sluggedIdentifier = slugify(identifier); const sluggedIdentifier = identifier.slugify();
await this.settings.updateSource({ await this.settings.updateSource({
[`resources.${actorType}.resources.${sluggedIdentifier}`]: Resource.getDefaultResourceData(identifier) [`resources.${actorType}.resources.${sluggedIdentifier}`]: Resource.getDefaultResourceData(identifier)

View file

@ -57,6 +57,14 @@ export default class CharacterSheet extends DHBaseActorSheet {
} }
], ],
contextMenus: [ contextMenus: [
{
handler: CharacterSheet.#getCreationMainContextOptions,
selector: '.character-details [data-action="editDoc"]',
options: {
parentClassHooks: false,
fixed: true
}
},
{ {
handler: CharacterSheet.#getDomainCardContextOptions, handler: CharacterSheet.#getDomainCardContextOptions,
selector: '[data-item-uuid][data-type="domainCard"]', selector: '[data-item-uuid][data-type="domainCard"]',
@ -319,6 +327,56 @@ export default class CharacterSheet extends DHBaseActorSheet {
/* Context Menu */ /* 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);
},
callback: async (target, event) => {
const doc = await getDocFromElement(target);
if (event.shiftKey) return doc.delete();
else return doc.deleteDialog();
}
}
];
}
/** /**
* Get the set of ContextMenu options for DomainCards. * 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 * @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance
@ -718,7 +776,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
? { ? {
'system.linkedClass.uuid': { 'system.linkedClass.uuid': {
key: 'system.linkedClass.uuid', key: 'system.linkedClass.uuid',
value: this.document.system.class.value._stats.compendiumSource value: this.document.system.class.value?._stats.compendiumSource
} }
} }
: undefined, : undefined,

View file

@ -443,10 +443,12 @@ export const typeConfig = {
const list = []; const list = [];
for (const item of items.filter(item => item.system.linkedClass)) { for (const item of items.filter(item => item.system.linkedClass)) {
const linkedClass = await foundry.utils.fromUuid(item.system.linkedClass); const linkedClass = await foundry.utils.fromUuid(item.system.linkedClass);
list.push({ if (linkedClass) {
value: linkedClass.uuid, list.push({
label: linkedClass.name value: linkedClass.uuid,
}); label: linkedClass.name
});
}
} }
return list.reduce((a, c) => { return list.reduce((a, c) => {

View file

@ -1,5 +1,3 @@
import { slugify } from '../helpers/utils.mjs';
export default class CompendiumBrowserSettings extends foundry.abstract.DataModel { export default class CompendiumBrowserSettings extends foundry.abstract.DataModel {
static defineSchema() { static defineSchema() {
const fields = foundry.data.fields; const fields = foundry.data.fields;
@ -26,11 +24,11 @@ export default class CompendiumBrowserSettings extends foundry.abstract.DataMode
const pack = game.packs.get(item.pack); const pack = game.packs.get(item.pack);
if (!pack) return false; if (!pack) return false;
const packageName = pack.metadata.packageType === 'world' ? 'world' : slugify(pack.metadata.packageName); const packageName = pack.metadata.packageType === 'world' ? 'world' : pack.metadata.packageName.slugify();
const excludedSourceData = this.excludedSources[packageName]; const excludedSourceData = this.excludedSources[packageName];
if (excludedSourceData && excludedSourceData.excludedDocumentTypes.includes(pack.metadata.type)) return true; if (excludedSourceData && excludedSourceData.excludedDocumentTypes.includes(pack.metadata.type)) return true;
const packName = slugify(item.pack); const packName = item.pack.slugify();
const excludedPackData = this.excludedPacks[packName]; const excludedPackData = this.excludedPacks[packName];
if (excludedPackData && excludedPackData.excludedDocumentTypes.includes(pack.metadata.type)) return true; if (excludedPackData && excludedPackData.excludedDocumentTypes.includes(pack.metadata.type)) return true;

View file

@ -56,38 +56,30 @@ export default class DHSubclass extends BaseDataItem {
if (allowed === false) return; if (allowed === false) return;
if (this.actor?.type === 'character') { if (this.actor?.type === 'character') {
const dataUuid = data.uuid ?? data._stats.compendiumSource ?? `Item.${data._id}`; const { value: actorClass, subclass: existingSubclass } = this.actor.system.class;
if (this.actor.system.class.subclass) { const { value: multiclass, subclass: existingMultisubclass } = this.actor.system.multiclass;
if (this.actor.system.multiclass.subclass) { if (!actorClass && !multiclass) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.subclassesAlreadyPresent')); ui.notifications.warn('DAGGERHEART.UI.Notifications.missingClass', { localize: true });
return false; return false;
} else { }
const multiclass = this.actor.items.find(x => x.type === 'class' && x.system.isMulticlass); if (existingSubclass && existingMultisubclass) {
if (!multiclass) { ui.notifications.warn('DAGGERHEART.UI.Notifications.subclassesAlreadyPresent', { localize: true });
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.missingMulticlass')); return false;
return false; }
} if (existingSubclass && !multiclass) {
ui.notifications.warn('DAGGERHEART.UI.Notifications.missingMulticlass', { localize: true });
return false;
}
if (multiclass.system.subclasses.every(x => x.uuid !== dataUuid)) { const match = [multiclass, actorClass].find(
ui.notifications.error( c => c && (c._stats.compendiumSource ?? c.uuid) === this.linkedClass
game.i18n.localize('DAGGERHEART.UI.Notifications.subclassNotInMulticlass') );
); if (!match) {
return false; const key = multiclass ? 'subclassNotInMulticlass' : 'subclassNotInClass';
} ui.notifications.warn(`DAGGERHEART.UI.Notifications.${key}`, { localize: true });
return false;
await this.updateSource({ isMulticlass: true }); } else if (match.system.isMulticlass) {
} await this.updateSource({ isMulticlass: true });
} else {
const actorClass = this.actor.items.find(x => x.type === 'class' && !x.system.isMulticlass);
if (!actorClass) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.missingClass'));
return false;
}
if ((await actorClass.system.fetchSubclasses()).every(x => x.uuid !== dataUuid)) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.subclassNotInClass'));
return false;
}
} }
} }
} }

View file

@ -153,10 +153,13 @@ export default class DhpActor extends Actor {
async updateLevel(newLevel) { async updateLevel(newLevel) {
if (!['character', 'companion'].includes(this.type) || newLevel === this.system.levelData.level.changed) return; if (!['character', 'companion'].includes(this.type) || newLevel === this.system.levelData.level.changed) return;
const tiers = Object.values(game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers);
const maxLevel = tiers.reduce((acc, tier) => Math.max(acc, tier.levels.end), 0);
const multiclassMinLevel = Math.min(
maxLevel,
...tiers.filter(t => t.options.multiclass).map(t => t.levels.start)
);
if (newLevel > this.system.levelData.level.current) { if (newLevel > this.system.levelData.level.current) {
const maxLevel = Object.values(
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers
).reduce((acc, tier) => Math.max(acc, tier.levels.end), 0);
if (newLevel > maxLevel) { if (newLevel > maxLevel) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.tooHighLevel')); ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.tooHighLevel'));
} }
@ -231,18 +234,19 @@ export default class DhpActor extends Actor {
this.system.multiclass.subclass.update({ 'system.featureState': subclassFeatureState.multiclass }); this.system.multiclass.subclass.update({ 'system.featureState': subclassFeatureState.multiclass });
} }
if (multiclass) { // Remove multiclass if we're removing a multiclass feature or if we're below the multiclass minimum level
const multiclassItem = this.items.find(x => x.uuid === multiclass.itemUuid); // Multclasses cannot be manually removed on the sheet, so this allows recovering in the case of errors
const multiclassFeatures = this.items.filter( if (multiclass || newLevel < multiclassMinLevel) {
x => x.system.originItemType === 'class' && x.system.multiclassOrigin const multiclassItems = this.items.filter(
); x =>
const subclassFeatures = this.items.filter( x.uuid === multiclass?.itemUuid ||
x => x.system.originItemType === 'subclass' && x.system.multiclassOrigin x.system.isMulticlass ||
(['class', 'subclass'].includes(x.system.originItemType) && x.system.multiclassOrigin)
); );
this.deleteEmbeddedDocuments( this.deleteEmbeddedDocuments(
'Item', 'Item',
[multiclassItem, ...multiclassFeatures, ...subclassFeatures].map(x => x.id) multiclassItems.map(x => x.id)
); );
this.update({ this.update({
@ -281,6 +285,7 @@ export default class DhpActor extends Actor {
async levelUp(levelupData) { async levelUp(levelupData) {
const levelupAuto = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).levelupAuto; const levelupAuto = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).levelupAuto;
const getStatsWithSource = document => ({ ...(document._stats ?? {}), compendiumSource: document.uuid });
const levelups = {}; const levelups = {};
for (var levelKey of Object.keys(levelupData)) { for (var levelKey of Object.keys(levelupData)) {
@ -393,8 +398,8 @@ export default class DhpActor extends Actor {
const embeddedItem = await this.createEmbeddedDocuments('Item', [ const embeddedItem = await this.createEmbeddedDocuments('Item', [
{ {
...multiclassData, ...multiclassData,
uuid: multiclassItem.uuid, uuid: multiclassItem.uuid, // todo: replace with setting an id and using keepId
_stats: multiclassItem._stats, _stats: getStatsWithSource(multiclassItem),
system: { system: {
...multiclassData.system, ...multiclassData.system,
features: multiclassData.system.features.filter(x => x.type !== 'hope'), features: multiclassData.system.features.filter(x => x.type !== 'hope'),
@ -407,8 +412,8 @@ export default class DhpActor extends Actor {
await this.createEmbeddedDocuments('Item', [ await this.createEmbeddedDocuments('Item', [
{ {
...subclassData, ...subclassData,
uuid: subclassItem.uuid, uuid: subclassItem.uuid, // todo: replace with setting an id and using keepId
_stats: subclassItem._stats, _stats: getStatsWithSource(subclassItem),
system: { system: {
...subclassData.system, ...subclassData.system,
isMulticlass: true isMulticlass: true
@ -428,8 +433,8 @@ export default class DhpActor extends Actor {
const embeddedItem = await this.createEmbeddedDocuments('Item', [ const embeddedItem = await this.createEmbeddedDocuments('Item', [
{ {
...cardData, ...cardData,
uuid: cardItem.uuid, uuid: cardItem.uuid, // todo: replace with setting an id and using keepId
_stats: cardItem._stats, _stats: getStatsWithSource(cardItem),
system: { system: {
...cardData.system, ...cardData.system,
inVault: true inVault: true
@ -450,8 +455,7 @@ export default class DhpActor extends Actor {
const embeddedItem = await this.createEmbeddedDocuments('Item', [ const embeddedItem = await this.createEmbeddedDocuments('Item', [
{ {
...cardData, ...cardData,
uuid: cardItem.uuid, _stats: getStatsWithSource(cardItem),
_stats: cardItem._stats,
system: { system: {
...cardData.system, ...cardData.system,
inVault: true inVault: true

View file

@ -452,10 +452,6 @@ export async function createEmbeddedItemsWithEffects(actor, baseData) {
await actor.createEmbeddedDocuments('Item', effectData); await actor.createEmbeddedDocuments('Item', effectData);
} }
export const slugify = name => {
return name.toLowerCase().replaceAll(' ', '-').replaceAll('.', '_');
};
export function shuffleArray(array) { export function shuffleArray(array) {
let currentIndex = array.length; let currentIndex = array.length;
while (currentIndex != 0) { while (currentIndex != 0) {

View file

@ -64,7 +64,7 @@
{{/if}} {{/if}}
</div> </div>
{{#if document.system.multiclass.value}} {{#if (or document.system.multiclass.value document.system.multiclass.subclass)}}
<div class="multiclass"> <div class="multiclass">
{{#if document.system.multiclass.value}} {{#if document.system.multiclass.value}}
<span data-action="editDoc"data-item-uuid="{{document.system.multiclass.value.uuid}}">{{document.system.multiclass.value.name}}</span> <span data-action="editDoc"data-item-uuid="{{document.system.multiclass.value.uuid}}">{{document.system.multiclass.value.name}}</span>

View file

@ -6,7 +6,7 @@
{{#if roll.isCritical}} {{#if roll.isCritical}}
<span>{{localize "DAGGERHEART.GENERAL.criticalShort"}}</span> <span>{{localize "DAGGERHEART.GENERAL.criticalShort"}}</span>
{{else}} {{else}}
{{#if (and roll.dHope (not (eq roll.type "reaction")))}} {{#if (and roll.dHope (not (eq roll.options.roll.type "reaction")))}}
<span>{{localize "DAGGERHEART.GENERAL.withThing" thing=roll.totalLabel}}</span> <span>{{localize "DAGGERHEART.GENERAL.withThing" thing=roll.totalLabel}}</span>
{{/if}} {{/if}}
{{/if}} {{/if}}