Merged with development

This commit is contained in:
WBHarry 2025-09-06 19:14:02 +02:00
commit 0712ea70ae
24 changed files with 587 additions and 368 deletions

View file

@ -3,43 +3,48 @@ import { getDiceSoNicePreset } from '../../config/generalConfig.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
/**
* @import {ApplicationClickAction} from "@client/applications/_types.mjs"
*/
export default class DHAppearanceSettings extends HandlebarsApplicationMixin(ApplicationV2) {
constructor() {
super({});
this.settings = new DhAppearance(
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance).toObject()
);
}
get title() {
return game.i18n.localize('DAGGERHEART.SETTINGS.Menu.title');
}
/**@inheritdoc */
static DEFAULT_OPTIONS = {
tag: 'form',
id: 'daggerheart-appearance-settings',
classes: ['daggerheart', 'dialog', 'dh-style', 'setting'],
position: { width: '600', height: 'auto' },
window: {
title: 'DAGGERHEART.SETTINGS.Menu.title',
icon: 'fa-solid fa-gears'
},
actions: {
reset: this.reset,
save: this.save,
preview: this.preview
reset: DHAppearanceSettings.#onReset,
preview: DHAppearanceSettings.#onPreview
},
form: { handler: this.updateData, submitOnChange: true }
form: {
closeOnSubmit: true,
handler: DHAppearanceSettings.#onSubmit
}
};
static PARTS = {
main: {
template: 'systems/daggerheart/templates/settings/appearance-settings.hbs'
}
header: { template: 'systems/daggerheart/templates/settings/appearance-settings/header.hbs' },
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
main: { template: 'systems/daggerheart/templates/settings/appearance-settings/main.hbs' },
diceSoNice: { template: 'systems/daggerheart/templates/settings/appearance-settings/diceSoNice.hbs' },
footer: { template: 'templates/generic/form-footer.hbs' }
};
/** @inheritdoc */
static TABS = {
general: {
tabs: [
{ id: 'main', label: 'DAGGERHEART.GENERAL.Tabs.general' },
{ id: 'diceSoNice', label: 'DAGGERHEART.SETTINGS.Menu.appearance.diceSoNice.title' }
],
initial: 'main'
},
diceSoNice: {
tabs: [
{ id: 'hope', label: 'DAGGERHEART.GENERAL.hope' },
@ -51,79 +56,149 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
}
};
changeTab(tab, group, options) {
super.changeTab(tab, group, options);
/**@type {DhAppearance}*/
setting;
this.render();
static #localized = false;
/** @inheritDoc */
async _preFirstRender(_context, _options) {
await super._preFirstRender(_context, _options);
if (!DHAppearanceSettings.#localized) {
foundry.helpers.Localization.localizeDataModel(this.setting.constructor);
DHAppearanceSettings.#localized = true;
}
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.settingFields = this.settings;
context.showDiceSoNice = game.modules.get('dice-so-nice')?.active;
if (game.dice3d) {
context.diceSoNiceTextures = game.dice3d.exports.TEXTURELIST;
context.diceSoNiceColorsets = game.dice3d.exports.COLORSETS;
context.diceSoNiceMaterials = Object.keys(game.dice3d.DiceFactory.material_options).map(key => ({
key: key,
name: `DICESONICE.Material${key.capitalize()}`
}));
context.diceSoNiceSystems = [];
for (const [key, system] of game.dice3d.DiceFactory.systems.entries()) {
context.diceSoNiceSystems.push({ key, name: system.name });
}
/** @inheritdoc */
_configureRenderParts(options) {
const parts = super._configureRenderParts(options);
if (!game.modules.get('dice-so-nice')?.active) {
delete parts.diceSoNice;
delete parts.tabs;
}
return parts;
}
context.diceTab = {
key: this.tabGroups.diceSoNice,
source: this.settings._source.diceSoNice[this.tabGroups.diceSoNice],
fields: this.settings.schema.fields.diceSoNice.fields[this.tabGroups.diceSoNice].fields
};
/**@inheritdoc */
async _prepareContext(options) {
const context = await super._prepareContext(options);
if (options.isFirstRender)
this.setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance);
context.setting = this.setting;
context.fields = this.setting.schema.fields;
context.tabs = this._prepareTabs('general');
context.dsnTabs = this._prepareTabs('diceSoNice');
return context;
}
static async updateData(event, element, formData) {
const updatedSettings = foundry.utils.expandObject(formData.object);
await this.settings.updateSource(updatedSettings);
this.render();
/**@inheritdoc */
async _preparePartContext(partId, context, options) {
const partContext = await super._preparePartContext(partId, context, options);
if (partId in context.tabs) partContext.tab = partContext.tabs[partId];
switch (partId) {
case 'diceSoNice':
await this.prepareDiceSoNiceContext(partContext);
break;
case 'footer':
partContext.buttons = [
{ type: 'button', action: 'reset', icon: 'fa-solid fa-arrow-rotate-left', label: 'Reset' },
{ type: 'submit', icon: 'fa-solid fa-floppy-disk', label: 'Save Changes' }
];
break;
}
return partContext;
}
static async preview() {
const source = this.settings._source.diceSoNice[this.tabGroups.diceSoNice];
let faces = 'd12';
switch (this.tabGroups.diceSoNice) {
case 'advantage':
case 'disadvantage':
faces = 'd6';
}
const preset = await getDiceSoNicePreset(source, faces);
const diceSoNiceRoll = await new Roll(`1${faces}`).evaluate();
/**
* Prepare render context for the DSN part.
* @param {ApplicationRenderContext} context
* @returns {Promise<void>}
* @protected
*/
async prepareDiceSoNiceContext(context) {
context.diceSoNiceTextures = Object.entries(game.dice3d.exports.TEXTURELIST).reduce(
(acc, [k, v]) => ({
...acc,
[k]: v.name
}),
{}
);
context.diceSoNiceColorsets = Object.values(game.dice3d.exports.COLORSETS).reduce(
(acc, v) => ({
...acc,
[v.id]: v.description
}),
{}
);
context.diceSoNiceMaterials = Object.keys(game.dice3d.DiceFactory.material_options).reduce(
(acc, key) => ({
...acc,
[key]: `DICESONICE.Material${key.capitalize()}`
}),
{}
);
context.diceSoNiceSystems = Object.fromEntries(
[...game.dice3d.DiceFactory.systems].map(([k, v]) => [k, v.name])
);
foundry.utils.mergeObject(
context.dsnTabs,
['hope', 'fear', 'advantage', 'disadvantage'].reduce(
(acc, key) => ({
...acc,
[key]: {
values: this.setting.diceSoNice[key],
fields: this.setting.schema.getField(`diceSoNice.${key}`).fields
}
}),
{}
)
);
}
/**
* Submit the configuration form.
* @this {DHAppearanceSettings}
* @param {SubmitEvent} event
* @param {HTMLFormElement} form
* @param {foundry.applications.ux.FormDataExtended} formData
* @returns {Promise<void>}
*/
static async #onSubmit(event, form, formData) {
const data = this.setting.schema.clean(foundry.utils.expandObject(formData.object));
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance, data);
}
/* -------------------------------------------- */
/**
* Submit the configuration form.
* @this {DHAppearanceSettings}
* @type {ApplicationClickAction}
*/
static async #onPreview(_, target) {
const formData = new foundry.applications.ux.FormDataExtended(target.closest('form'));
const { diceSoNice } = foundry.utils.expandObject(formData.object);
const { key } = target.dataset;
const faces = ['advantage', 'disadvantage'].includes(key) ? 'd6' : 'd12';
const preset = await getDiceSoNicePreset(diceSoNice[key], faces);
const diceSoNiceRoll = await new foundry.dice.Roll(`1${faces}`).evaluate();
diceSoNiceRoll.dice[0].options.appearance = preset.appearance;
diceSoNiceRoll.dice[0].options.modelFile = preset.modelFile;
await game.dice3d.showForRoll(diceSoNiceRoll, game.user, false);
}
static async reset() {
this.settings = new DhAppearance();
this.render();
}
static async save() {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance, this.settings.toObject());
this.close();
}
_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;
/**
* Reset the form back to default values.
* @this {DHAppearanceSettings}
* @type {ApplicationClickAction}
*/
static async #onReset() {
this.setting = new this.setting.constructor();
this.render({ force: false });
}
}

View file

@ -46,6 +46,10 @@ export default class ClassSheet extends DHBaseItemSheet {
template: 'systems/daggerheart/templates/sheets/items/class/settings.hbs',
scrollable: ['.settings']
},
questions: {
template: 'systems/daggerheart/templates/sheets/items/class/questions.hbs',
scrollable: ['.questions']
},
effects: {
template: 'systems/daggerheart/templates/sheets/global/tabs/tab-effects.hbs',
scrollable: ['.effects']
@ -55,7 +59,13 @@ export default class ClassSheet extends DHBaseItemSheet {
/** @inheritdoc */
static TABS = {
primary: {
tabs: [{ id: 'description' }, { id: 'features' }, { id: 'settings' }, { id: 'effects' }],
tabs: [
{ id: 'description' },
{ id: 'features' },
{ id: 'settings' },
{ id: 'questions' },
{ id: 'effects' }
],
initial: 'description',
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
}

View file

@ -55,9 +55,12 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
}
get action() {
const actionItem = this.actionItem;
if (!actionItem || !this.source.action) return null;
return actionItem.system.actionsList?.find(a => a.id === this.source.action);
const actionActor = this.actionActor,
actionItem = this.actionItem;
if (!this.source.action) return null;
if (actionItem) return actionItem.system.actionsList?.find(a => a.id === this.source.action);
else if (actionActor?.system.attack?._id === this.source.action) return actionActor.system.attack;
return null;
}
get targetMode() {

View file

@ -30,8 +30,13 @@ export default class DamageField extends fields.SchemaField {
* @param {boolean} [force=false] If the method should be executed outside of Action workflow, for ChatMessage button for example.
*/
static async execute(config, messageId = null, force = false) {
if(!this.hasDamage && !this.hasHealing) return;
if((this.hasRoll && DamageField.getAutomation() === CONFIG.DH.SETTINGS.actionAutomationChoices.never.id) && !force) return;
if (!this.hasDamage && !this.hasHealing) return;
if (
this.hasRoll &&
DamageField.getAutomation() === CONFIG.DH.SETTINGS.actionAutomationChoices.never.id &&
!force
)
return;
let formulas = this.damage.parts.map(p => ({
formula: DamageField.getFormulaValue.call(this, p, config).getFormula(this.actor),
@ -51,17 +56,18 @@ export default class DamageField extends fields.SchemaField {
};
delete damageConfig.evaluate;
if(DamageField.getAutomation() === CONFIG.DH.SETTINGS.actionAutomationChoices.always.id) damageConfig.dialog.configure = false;
if (DamageField.getAutomation() === CONFIG.DH.SETTINGS.actionAutomationChoices.always.id)
damageConfig.dialog.configure = false;
if (config.hasSave) config.onSave = damageConfig.onSave = this.save.damageMod;
damageConfig.source.message = config.message?._id ?? messageId;
damageConfig.directDamage = !!damageConfig.source?.message;
if(damageConfig.source?.message && game.modules.get('dice-so-nice')?.active)
await game.dice3d.waitFor3DAnimationByMessageID(damageConfig.source.message);
// if(damageConfig.source?.message && game.modules.get('dice-so-nice')?.active)
// await game.dice3d.waitFor3DAnimationByMessageID(damageConfig.source.message);
const damageResult = await CONFIG.Dice.daggerheart.DamageRoll.build(damageConfig);
if(!damageResult) return false;
if (!damageResult) return false;
config.damage = damageResult.damage;
config.message ??= damageConfig.message;
}
@ -70,19 +76,15 @@ export default class DamageField extends fields.SchemaField {
* Apply Damage/Healing Action Worflow part.
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
* @param {*[]} targets Arrays of targets to bypass pre-selected ones.
* @param {boolean} force If the method should be executed outside of Action workflow, for ChatMessage button for example.
* @param {boolean} force If the method should be executed outside of Action workflow, for ChatMessage button for example.
*/
static async applyDamage(config, targets = null, force = false) {
targets ??= config.targets.filter(target => target.hit);
if(!config.damage || !targets?.length || (!DamageField.getApplyAutomation() && !force)) return;
if (!config.damage || !targets?.length || (!DamageField.getApplyAutomation() && !force)) return;
for (let target of targets) {
const actor = fromUuidSync(target.actorId);
if(!actor) continue;
if (
!config.hasHealing &&
config.onSave &&
target.saved?.success === true
) {
if (!actor) continue;
if (!config.hasHealing && config.onSave && target.saved?.success === true) {
const mod = CONFIG.DH.ACTIONS.damageOnSave[config.onSave]?.mod ?? 1;
Object.entries(config.damage).forEach(([k, v]) => {
v.total = 0;
@ -97,17 +99,17 @@ export default class DamageField extends fields.SchemaField {
else actor.takeDamage(config.damage, config.isDirect);
}
}
/**
* Return value or valueAlt from damage part
* Must be called within Action context or similar.
* @param {object} part Damage Part
* @param {object} part Damage Part
* @param {object} data Action getRollData
* @returns Formula value object
*/
static getFormulaValue(part, data) {
let formulaValue = part.value;
if (data.hasRoll && part.resultBased && data.roll.result.duality === -1) return part.valueAlt;
const isAdversary = this.actor.type === 'adversary';
@ -123,8 +125,8 @@ export default class DamageField extends fields.SchemaField {
* Prepare formulas for Damage Roll
* Must be called within Action context or similar.
* @param {object[]} formulas Array of formatted formulas object
* @param {object} data Action getRollData
* @returns
* @param {object} data Action getRollData
* @returns
*/
static formatFormulas(formulas, data) {
const formattedFormulas = [];
@ -145,16 +147,25 @@ export default class DamageField extends fields.SchemaField {
* @returns {string} Id from settingsConfig.mjs actionAutomationChoices
*/
static getAutomation() {
return (game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damage.gm) || (!game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damage.players)
return (
(game.user.isGM &&
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damage.gm) ||
(!game.user.isGM &&
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damage.players)
);
}
/**
* Return the automation setting for applyDamage method for current user role
* @returns {boolean} If applyDamage should be triggered automatically
*/
static getApplyAutomation() {
return (game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damageApply.gm) || (!game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damageApply.players)
return (
(game.user.isGM &&
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damageApply.gm) ||
(!game.user.isGM &&
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damageApply.players)
);
}
}

View file

@ -49,6 +49,8 @@ export default class DHClass extends BaseDataItem {
suggestedSecondaryWeapon: new ForeignDocumentUUIDField({ type: 'Item' }),
suggestedArmor: new ForeignDocumentUUIDField({ type: 'Item' })
}),
backgroundQuestions: new fields.ArrayField(new fields.StringField(), { initial: ['', '', ''] }),
connections: new fields.ArrayField(new fields.StringField(), { initial: ['', '', ''] }),
isMulticlass: new fields.BooleanField({ initial: false })
};
}
@ -96,6 +98,20 @@ export default class DHClass extends BaseDataItem {
}
}
}
if (!data.system.isMulticlass) {
const addQuestions = (base, questions) => {
return `${base}${questions.map(q => `<p><strong>${q}</strong></p>`).join('<br/>')}`;
};
const backgroundQuestions = data.system.backgroundQuestions.filter(x => x);
const connections = data.system.connections.filter(x => x);
await this.actor.update({
'system.biography': {
background: addQuestions(this.actor.system.biography.background, backgroundQuestions),
connections: addQuestions(this.actor.system.biography.connections, connections)
}
});
}
}
const allowed = await super._preCreate(data, options, user);

View file

@ -1,100 +1,46 @@
import { fearDisplay } from '../../config/generalConfig.mjs';
export default class DhAppearance extends foundry.abstract.DataModel {
static LOCALIZATION_PREFIXES = ['DAGGERHEART.SETTINGS.Appearance'];
static defineSchema() {
const fields = foundry.data.fields;
const { StringField, ColorField, BooleanField, SchemaField } = foundry.data.fields;
// helper to create dice style schema
const diceStyle = ({ fg, bg, outline, edge }) =>
new SchemaField({
foreground: new ColorField({ required: true, initial: fg }),
background: new ColorField({ required: true, initial: bg }),
outline: new ColorField({ required: true, initial: outline }),
edge: new ColorField({ required: true, initial: edge }),
texture: new StringField({ initial: 'astralsea', required: true, blank: false }),
colorset: new StringField({ initial: 'inspired', required: true, blank: false }),
material: new StringField({ initial: 'metal', required: true, blank: false }),
system: new StringField({ initial: 'standard', required: true, blank: false })
});
return {
displayFear: new fields.StringField({
displayFear: new StringField({
required: true,
choices: fearDisplay,
initial: fearDisplay.token.value,
label: 'DAGGERHEART.SETTINGS.Appearance.FIELDS.displayFear.label'
choices: CONFIG.DH.GENERAL.fearDisplay,
initial: CONFIG.DH.GENERAL.fearDisplay.token.value
}),
diceSoNice: new fields.SchemaField({
hope: new fields.SchemaField({
foreground: new fields.ColorField({ required: true, initial: '#ffffff' }),
background: new fields.ColorField({ required: true, initial: '#ffe760' }),
outline: new fields.ColorField({ required: true, initial: '#000000' }),
edge: new fields.ColorField({ required: true, initial: '#ffffff' }),
texture: new fields.StringField({ initial: 'astralsea' }),
colorset: new fields.StringField({ initial: 'inspired' }),
material: new fields.StringField({ initial: 'metal' }),
system: new fields.StringField({ initial: 'standard' })
}),
fear: new fields.SchemaField({
foreground: new fields.ColorField({ required: true, initial: '#000000' }),
background: new fields.ColorField({ required: true, initial: '#0032b1' }),
outline: new fields.ColorField({ required: true, initial: '#ffffff' }),
edge: new fields.ColorField({ required: true, initial: '#000000' }),
texture: new fields.StringField({ initial: 'astralsea' }),
colorset: new fields.StringField({ initial: 'inspired' }),
material: new fields.StringField({ initial: 'metal' }),
system: new fields.StringField({ initial: 'standard' })
}),
advantage: new fields.SchemaField({
foreground: new fields.ColorField({ required: true, initial: '#ffffff' }),
background: new fields.ColorField({ required: true, initial: '#008000' }),
outline: new fields.ColorField({ required: true, initial: '#000000' }),
edge: new fields.ColorField({ required: true, initial: '#ffffff' }),
texture: new fields.StringField({ initial: 'astralsea' }),
colorset: new fields.StringField({ initial: 'inspired' }),
material: new fields.StringField({ initial: 'metal' }),
system: new fields.StringField({ initial: 'standard' })
}),
disadvantage: new fields.SchemaField({
foreground: new fields.ColorField({ required: true, initial: '#000000' }),
background: new fields.ColorField({ required: true, initial: '#b30000' }),
outline: new fields.ColorField({ required: true, initial: '#ffffff' }),
edge: new fields.ColorField({ required: true, initial: '#000000' }),
texture: new fields.StringField({ initial: 'astralsea' }),
colorset: new fields.StringField({ initial: 'inspired' }),
material: new fields.StringField({ initial: 'metal' }),
system: new fields.StringField({ initial: 'standard' })
})
diceSoNice: new SchemaField({
hope: diceStyle({ fg: '#ffffff', bg: '#ffe760', outline: '#000000', edge: '#ffffff' }),
fear: diceStyle({ fg: '#000000', bg: '#0032b1', outline: '#ffffff', edge: '#000000' }),
advantage: diceStyle({ fg: '#ffffff', bg: '#008000', outline: '#000000', edge: '#ffffff' }),
disadvantage: diceStyle({ fg: '#000000', bg: '#b30000', outline: '#ffffff', edge: '#000000' })
}),
showGenericStatusEffects: new fields.BooleanField({
initial: true,
label: 'DAGGERHEART.SETTINGS.Appearance.FIELDS.showGenericStatusEffects.label'
extendCharacterDescriptions: new BooleanField(),
extendAdversaryDescriptions: new BooleanField(),
extendEnvironmentDescriptions: new BooleanField(),
extendItemDescriptions: new BooleanField(),
expandRollMessage: new SchemaField({
desc: new BooleanField(),
roll: new BooleanField(),
damage: new BooleanField(),
target: new BooleanField()
}),
extendCharacterDescriptions: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.SETTINGS.Appearance.FIELDS.extendCharacterDescriptions.label'
}),
extendAdversaryDescriptions: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.SETTINGS.Appearance.FIELDS.extendAdversaryDescriptions.label'
}),
extendEnvironmentDescriptions: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.SETTINGS.Appearance.FIELDS.extendEnvironmentDescriptions.label'
}),
extendItemDescriptions: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.SETTINGS.Appearance.FIELDS.extendItemDescriptions.label'
}),
expandRollMessage: new fields.SchemaField({
desc: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.SETTINGS.Appearance.FIELDS.expandRollMessageDesc.label'
}),
roll: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.SETTINGS.Appearance.FIELDS.expandRollMessageRoll.label'
}),
damage: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.SETTINGS.Appearance.FIELDS.expandRollMessageDamage.label'
}),
target: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.SETTINGS.Appearance.FIELDS.expandRollMessageTarget.label'
})
}),
hideAttribution: new fields.BooleanField({
required: true,
initial: false,
label: 'DAGGERHEART.SETTINGS.Appearance.FIELDS.hideAttribution.label'
})
hideAttribution: new BooleanField(),
showGenericStatusEffects: new BooleanField({ initial: true })
};
}
}

View file

@ -72,7 +72,7 @@ const registerMenus = () => {
});
game.settings.registerMenu(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance, {
name: game.i18n.localize('DAGGERHEART.SETTINGS.Menu.appearance.title'),
name: game.i18n.localize('DAGGERHEART.SETTINGS.Menu.appearance.label'),
label: game.i18n.localize('DAGGERHEART.SETTINGS.Menu.appearance.label'),
hint: game.i18n.localize('DAGGERHEART.SETTINGS.Menu.appearance.hint'),
icon: 'fa-solid fa-palette',