[Feature] Homebrew Domains (#639)

* Split into tabs

* Finished homebrew settings

* .

* Improved domainremoval cleanup
This commit is contained in:
WBHarry 2025-08-06 13:58:17 +02:00 committed by GitHub
parent d186c62ee5
commit 02958f9574
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 560 additions and 49 deletions

View file

@ -34,7 +34,7 @@ export default class MulticlassChoiceDialog extends HandlebarsApplicationMixin(A
const context = await super._prepareContext(_options);
context.multiclass = this.multiclass;
context.domainChoices = this.multiclass.domains.map(value => {
const domain = CONFIG.DH.DOMAIN.domains[value];
const domain = CONFIG.DH.DOMAIN.allDomains()[value];
return {
value: value,
label: game.i18n.localize(domain.label),

View file

@ -1,6 +1,5 @@
import LevelUpBase from './levelup.mjs';
import { DhLevelup } from '../../data/levelup.mjs';
import { domains } from '../../config/domainConfig.mjs';
import { abilities, subclassFeatureLabels } from '../../config/actorConfig.mjs';
export default class DhCharacterLevelUp extends LevelUpBase {
@ -113,7 +112,7 @@ export default class DhCharacterLevelUp extends LevelUpBase {
: levelBase;
return game.i18n.format('DAGGERHEART.APPLICATIONS.Levelup.selections.emptyDomainCardHint', {
domain: game.i18n.localize(domains[domain.domain].label),
domain: game.i18n.localize(CONFIG.DH.DOMAIN.allDomains()[domain.domain].label),
level: levelMax
});
}),
@ -170,7 +169,7 @@ export default class DhCharacterLevelUp extends LevelUpBase {
uuid: multiclass.uuid,
domains:
multiclass?.system?.domains.map(key => {
const domain = domains[key];
const domain = CONFIG.DH.DOMAIN.allDomains()[key];
const alreadySelected = this.actor.system.class.value.system.domains.includes(key);
return {
@ -315,7 +314,10 @@ export default class DhCharacterLevelUp extends LevelUpBase {
? {
...multiclassItem.toObject(),
domain: checkbox.secondaryData.domain
? game.i18n.localize(domains[checkbox.secondaryData.domain].label)
? game.i18n.localize(
CONFIG.DH.DOMAIN.allDomains()[checkbox.secondaryData.domain]
.label
)
: null,
subclass: subclass ? subclass.name : null
}

View file

@ -1,5 +1,4 @@
import { abilities, subclassFeatureLabels } from '../../config/actorConfig.mjs';
import { domains } from '../../config/domainConfig.mjs';
import { getDeleteKeys, tagifyElement } from '../../helpers/utils.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -253,7 +252,10 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
? {
...multiclassItem.toObject(),
domain: checkbox.secondaryData.domain
? game.i18n.localize(domains[checkbox.secondaryData.domain].label)
? game.i18n.localize(
CONFIG.DH.DOMAIN.allDomains()[checkbox.secondaryData.domain]
.label
)
: null,
subclass: subclass ? subclass.name : null
}
@ -357,10 +359,10 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
experienceIncreaseTagify,
Object.keys(this.actor.system.experiences).reduce((acc, id) => {
const experience = this.actor.system.experiences[id];
acc[id] = { label: experience.name };
acc.push({ id: id, label: experience.name });
return acc;
}, {}),
}, []),
this.tagifyUpdate('experience').bind(this)
);
}

View file

@ -1,4 +1,5 @@
import { DhHomebrew } from '../../data/settings/_module.mjs';
import { slugify } from '../../helpers/utils.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class DhHomebrewSettings extends HandlebarsApplicationMixin(ApplicationV2) {
@ -8,6 +9,10 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
this.settings = new DhHomebrew(
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).toObject()
);
this.selected = {
domain: null
};
}
get title() {
@ -17,7 +22,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
static DEFAULT_OPTIONS = {
tag: 'form',
id: 'daggerheart-homebrew-settings',
classes: ['daggerheart', 'dh-style', 'dialog', 'setting'],
classes: ['daggerheart', 'dh-style', 'dialog', 'setting', 'homebrew-settings'],
position: { width: '600', height: 'auto' },
window: {
icon: 'fa-solid fa-gears'
@ -27,6 +32,9 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
editItem: this.editItem,
removeItem: this.removeItem,
resetMoves: this.resetMoves,
addDomain: this.addDomain,
toggleSelectedDomain: this.toggleSelectedDomain,
deleteDomain: this.deleteDomain,
save: this.save,
reset: this.reset
},
@ -34,9 +42,19 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
};
static PARTS = {
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
settings: { template: 'systems/daggerheart/templates/settings/homebrew-settings/settings.hbs' },
domains: { template: 'systems/daggerheart/templates/settings/homebrew-settings/domains.hbs' },
downtime: { template: 'systems/daggerheart/templates/settings/homebrew-settings/downtime.hbs' },
footer: { template: 'systems/daggerheart/templates/settings/homebrew-settings/footer.hbs' }
};
/** @inheritdoc */
static TABS = {
main: {
template: 'systems/daggerheart/templates/settings/homebrew-settings.hbs',
scrollable: ['']
tabs: [{ id: 'settings' }, { id: 'domains' }, { id: 'downtime' }],
initial: 'settings',
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
}
};
@ -47,7 +65,26 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
return context;
}
static async updateData(event, element, formData) {
async _preparePartContext(partId, context) {
await super._preparePartContext(partId, context);
switch (partId) {
case 'domains':
const selectedDomain = this.selected.domain ? this.settings.domains[this.selected.domain] : null;
const enrichedDescription = selectedDomain
? await foundry.applications.ux.TextEditor.implementation.enrichHTML(selectedDomain.description)
: null;
if (enrichedDescription !== null) context.selectedDomain = { ...selectedDomain, enrichedDescription };
context.configDomains = CONFIG.DH.DOMAIN.domains;
context.homebrewDomains = this.settings.domains;
break;
}
return context;
}
static async updateData(_event, _element, formData) {
const updatedSettings = foundry.utils.expandObject(formData.object);
await this.settings.updateSource({
@ -163,6 +200,107 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
this.render();
}
static async addDomain(event) {
event.preventDefault();
const content = new foundry.data.fields.StringField({
label: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.domains.newDomainInputLabel'),
hint: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.domains.newDomainInputHint'),
required: true
}).toFormGroup({}, { name: 'domainName', localize: true }).outerHTML;
async function callback(_, button) {
const domainName = button.form.elements.domainName.value;
if (!domainName) return;
const newSlug = slugify(domainName);
const existingDomains = [
...Object.values(this.settings.domains),
...Object.values(CONFIG.DH.DOMAIN.domains)
];
if (existingDomains.find(x => slugify(game.i18n.localize(x.label)) === newSlug)) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.domains.duplicateDomain'));
return;
}
this.settings.updateSource({
[`domains.${newSlug}`]: {
id: newSlug,
label: domainName,
src: 'icons/svg/portal.svg'
}
});
this.selected.domain = newSlug;
this.render();
}
foundry.applications.api.DialogV2.prompt({
content: content,
rejectClose: false,
modal: true,
ok: { callback: callback.bind(this) },
window: {
title: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.domains.newDomainInputTitle')
},
position: { width: 400 }
});
}
static toggleSelectedDomain(_, target) {
this.selected.domain = this.selected.domain === target.id ? null : target.id;
this.render();
}
static async deleteDomain() {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.domains.deleteDomain')
},
content: game.i18n.format('DAGGERHEART.SETTINGS.Homebrew.domains.deleteDomainText', {
name: this.settings.domains[this.selected.domain].label
})
});
if (!confirmed) return;
await this.settings.updateSource({
[`domains.-=${this.selected.domain}`]: null
});
const currentSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew);
if (currentSettings.domains[this.selected.domain]) {
await currentSettings.updateSource({ [`domains.-=${this.selected.domain}`]: null });
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, currentSettings);
}
const updateClasses = game.items.filter(x => x.type === 'class');
for (let actor of game.actors) {
updateClasses.push(...actor.items.filter(x => x.type === 'class'));
}
for (let c of updateClasses) {
if (c.system.domains.includes(this.selected.domain)) {
const newDomains =
c.system.domains.length === 1
? [CONFIG.DH.DOMAIN.domains.arcana.id]
: c.system.domains.filter(x => x !== this.selected.domain);
await c.update({ 'system.domains': newDomains });
}
c.sheet.render();
}
const updateDomainCards = game.items.filter(
x => x.type === 'domainCard' && x.system.domain === this.selected.domain
);
for (let d of updateDomainCards) {
await d.update({ 'system.domain': CONFIG.DH.DOMAIN.domains.arcana.id });
d.sheet.render();
}
this.selected.domain = null;
this.render();
}
static async save() {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject());
this.close();
@ -200,4 +338,13 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
}
return obj;
}
_getTabs(tabs) {
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] ? this.tabGroups[v.group] === v.id : v.active;
v.cssClass = v.active ? 'active' : '';
}
return tabs;
}
}

View file

@ -14,7 +14,7 @@ export default class ClassSheet extends DHBaseItemSheet {
tagifyConfigs: [
{
selector: '.domain-input',
options: () => CONFIG.DH.DOMAIN.domains,
options: () => CONFIG.DH.DOMAIN.orderedDomains(),
callback: ClassSheet.#onDomainSelect,
tagifyOptions: {
maxTags: () => game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).maxDomains

View file

@ -34,4 +34,12 @@ export default class DomainCardSheet extends DHBaseItemSheet {
scrollable: ['.effects']
}
};
async _prepareContext(options) {
const context = await super._prepareContext(options);
context.domain = CONFIG.DH.DOMAIN.allDomains()[this.document.system.domain];
context.domainChoices = CONFIG.DH.DOMAIN.orderedDomains();
return context;
}
}

View file

@ -219,7 +219,7 @@ export default class FilterMenu extends foundry.applications.ux.ContextMenu {
}
}));
const domainFilter = Object.values(CONFIG.DH.DOMAIN.domains).map(({ id, label }) => ({
const domainFilter = Object.values(CONFIG.DH.DOMAIN.allDomains()).map(({ id, label }) => ({
group: game.i18n.localize('DAGGERHEART.GENERAL.Domain.single'),
name: game.i18n.localize(label),
filter: {

View file

@ -55,8 +55,17 @@ export const domains = {
}
};
export const classDomainMap = {
rogue: [domains.midnight, domains.grace]
export const allDomains = () => ({
...domains,
...game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).domains
});
export const orderedDomains = () => {
const all = {
...domains,
...game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).domains
};
return Object.values(all).sort((a, b) => game.i18n.localize(a.label).localeCompare(game.i18n.localize(b.label)));
};
export const subclassMap = {

View file

@ -18,7 +18,7 @@ export default class DHDomainCard extends BaseDataItem {
return {
...super.defineSchema(),
domain: new fields.StringField({
choices: CONFIG.DH.DOMAIN.domains,
choices: CONFIG.DH.DOMAIN.allDomains,
required: true,
initial: CONFIG.DH.DOMAIN.domains.arcana.id
}),

View file

@ -2,8 +2,6 @@ import { defaultRestOptions } from '../../config/generalConfig.mjs';
import { ActionsField } from '../fields/actionField.mjs';
export default class DhHomebrew extends foundry.abstract.DataModel {
static LOCALIZATION_PREFIXES = ['DAGGERHEART.SETTINGS.Homebrew']; // Doesn't work for some reason
static defineSchema() {
const fields = foundry.data.fields;
return {
@ -98,7 +96,19 @@ export default class DhHomebrew extends foundry.abstract.DataModel {
{ initial: defaultRestOptions.shortRest() }
)
})
})
}),
domains: new fields.TypedObjectField(
new fields.SchemaField({
id: new fields.StringField({ required: true }),
label: new fields.StringField({ required: true, initial: '', label: 'DAGGERHEART.GENERAL.label' }),
src: new fields.FilePathField({
categories: ['IMAGE'],
base64: false,
label: 'Image'
}),
description: new fields.HTMLField()
})
)
};
}
}

View file

@ -83,15 +83,16 @@ export const chunkify = (array, chunkSize, mappingFunc) => {
return chunkifiedArray;
};
export const tagifyElement = (element, options, onChange, tagifyOptions = {}) => {
export const tagifyElement = (element, baseOptions, onChange, tagifyOptions = {}) => {
const { maxTags } = tagifyOptions;
const options = typeof baseOptions === 'object' ? Object.values(baseOptions) : baseOptions;
const tagifyElement = new Tagify(element, {
tagTextProp: 'name',
enforceWhitelist: true,
whitelist: Object.keys(options).map(key => {
const option = options[key];
whitelist: options.map(option => {
return {
value: key,
value: option.id,
name: game.i18n.localize(option.label),
src: option.src,
description: option.description
@ -100,7 +101,7 @@ export const tagifyElement = (element, options, onChange, tagifyOptions = {}) =>
maxTags: typeof maxTags === 'function' ? maxTags() : maxTags,
dropdown: {
mapValueTo: 'name',
searchKeys: ['name'],
searchKeys: ['value'],
enabled: 0,
maxItems: 100,
closeOnSelect: true,
@ -369,3 +370,7 @@ export async function createEmbeddedItemWithEffects(actor, baseData, update) {
return doc;
}
export const slugify = name => {
return name.toLowerCase().replaceAll(' ', '-').replaceAll('.', '');
};