merged with main

This commit is contained in:
WBHarry 2025-06-29 23:05:14 +02:00
commit 0aa08deaa3
41 changed files with 805 additions and 446 deletions

View file

@ -1,48 +0,0 @@
export default function DhpApplicationMixin(Base) {
return class DhpSheet extends Base {
static applicationType = 'sheets';
static documentType = '';
static get defaultOptions() {
return Object.assign(super.defaultOptions, {
classes: ['daggerheart', 'sheet', this.documentType],
template: `systems/${SYSTEM.id}/templates/${this.applicationType}/${this.documentType}.hbs`,
height: 'auto',
submitOnChange: true,
submitOnClose: false,
width: 450
});
}
/** @override */
get title() {
const { documentName, type, name } = this.object;
// const typeLabel = game.i18n.localize(CONFIG[documentName].typeLabels[type]);
const typeLabel = documentName;
return `[${typeLabel}] ${name}`;
}
// async _renderOuter() {
// const html = await super._renderOuter();
// // const overlaySrc = "systems/amia/assets/ThePrimordial.png";
// const overlay = `<div class="outer-render"></div>`
// $(html).find('.window-header').prepend(overlay);
// return html;
// }
activateListeners(html) {
super.activateListeners(html);
html.on('click', '[data-action]', this.#onClickAction.bind(this));
}
async #onClickAction(event) {
event.preventDefault();
const button = event.currentTarget;
const action = button.dataset.action;
return this._handleAction(action, event, button);
}
async _handleAction(action, event, button) {}
};
}

View file

@ -11,6 +11,10 @@ export class DHRoll extends Roll {
super(formula, data, options);
}
static messageType = 'adversaryRoll';
static DefaultDialog = D20RollDialog;
static async build(config = {}, message = {}) {
const roll = await this.buildConfigure(config, message);
if (!roll) return;
@ -28,19 +32,18 @@ export class DHRoll extends Roll {
this.applyKeybindings(config);
let roll = new this(config.roll.formula, config.data, config);
if (config.dialog.configure !== false) {
// Open Roll Dialog
const DialogClass = config.dialog?.class ?? this.DefaultDialog;
config = await DialogClass.configure(config, message);
if (!config) return;
const configDialog = await DialogClass.configure(roll, config, message);
if (!configDialog) return;
}
let roll = new this(config.formula, config.data, config);
for (const hook of config.hooks) {
if (Hooks.call(`${SYSTEM.id}.post${hook.capitalize()}RollConfiguration`, roll, config, message) === false)
return [];
}
return roll;
}
@ -62,7 +65,20 @@ export class DHRoll extends Roll {
}
}
static async postEvaluate(roll, config = {}) {}
static postEvaluate(roll, config = {}) {
if (!config.roll) config.roll = {};
config.roll.total = roll.total;
config.roll.formula = roll.formula;
config.roll.dice = [];
roll.dice.forEach(d => {
config.roll.dice.push({
dice: d.denomination,
total: d.total,
formula: d.formula,
results: d.results
});
});
}
static async toMessage(roll, config) {
const cls = getDocumentClass('ChatMessage'),
@ -77,15 +93,16 @@ export class DHRoll extends Roll {
}
static applyKeybindings(config) {
config.dialog.configure ??= true;
config.dialog.configure ??= !(config.event.shiftKey || config.event.altKey || config.event.ctrlKey);
}
constructFormula(config) {
// const formula = Roll.replaceFormulaData(this.options.roll.formula, config.data);
this.terms = Roll.parse(this.options.roll.formula, config.data);
return (this._formula = this.constructor.getFormula(this.terms));
}
}
// DHopeDie
// DFearDie
// DualityDie
// D20Die
export class DualityDie extends foundry.dice.terms.Die {
constructor({ number = 1, faces = 12, ...args } = {}) {
super({ number, faces, ...args });
@ -95,10 +112,11 @@ export class DualityDie extends foundry.dice.terms.Die {
export class D20Roll extends DHRoll {
constructor(formula, data = {}, options = {}) {
super(formula, data, options);
this.createBaseDice();
this.configureModifiers();
// this.createBaseDice();
// this.configureModifiers();
this._formula = this.resetFormula();
// this._formula = this.resetFormula();
this.constructFormula();
}
static ADV_MODE = {
@ -165,7 +183,7 @@ export class D20Roll extends DHRoll {
applyAdvantage() {
this.d20.modifiers.findSplice(m => ['kh', 'kl'].includes(m));
if (!this.hasAdvantage && !this.hasAdvantage) this.number = 1;
if (!this.hasAdvantage && !this.hasDisadvantage) this.number = 1;
else {
this.d20.number = 2;
this.d20.modifiers.push(this.hasAdvantage ? 'kh' : 'kl');
@ -175,62 +193,65 @@ export class D20Roll extends DHRoll {
// Trait bonus != Adversary
configureModifiers() {
this.applyAdvantage();
// this.options.roll.modifiers = [];
this.applyBaseBonus();
this.options.experiences?.forEach(m => {
if (this.options.data.experiences?.[m])
this.options.roll.modifiers.push({
label: this.options.data.experiences[m].description,
value: this.options.data.experiences[m].total
label: this.options.data.experiences[m].name,
value: this.options.data.experiences[m].total ?? this.options.data.experiences[m].value
});
});
this.options.roll.modifiers?.forEach(m => {
this.terms.push(...this.formatModifier(m.value));
});
if (this.options.extraFormula)
if (this.options.extraFormula) {
this.terms.push(
new foundry.dice.terms.OperatorTerm({ operator: '+' }),
...this.constructor.parse(this.options.extraFormula, this.getRollData())
);
}
// this.resetFormula();
}
applyBaseBonus() {
if (this.options.type === 'attack')
this.terms.push(...this.formatModifier(this.options.data.attack.roll.bonus));
constructFormula(config) {
this.terms = [];
this.createBaseDice();
this.configureModifiers();
this.resetFormula();
return this._formula;
}
static async postEvaluate(roll, config = {}) {
applyBaseBonus() {
this.options.roll.modifiers = [
{
label: 'Bonus to Hit',
value: Roll.replaceFormulaData('@attackBonus', this.data)
}
];
}
static postEvaluate(roll, config = {}) {
super.postEvaluate(roll, config);
if (config.targets?.length) {
config.targets.forEach(target => {
const difficulty = config.roll.difficulty ?? target.difficulty ?? target.evasion;
target.hit = this.isCritical || roll.total >= difficulty;
});
} else if (config.roll.difficulty) config.roll.success = roll.isCritical || roll.total >= config.roll.difficulty;
config.roll.total = roll.total;
config.roll.formula = roll.formula;
} else if (config.roll.difficulty)
config.roll.success = roll.isCritical || roll.total >= config.roll.difficulty;
config.roll.advantage = {
type: config.advantage,
dice: roll.dAdvantage?.denomination,
value: roll.dAdvantage?.total
};
config.roll.modifierTotal = config.roll.modifiers.reduce((a, c) => a + c.value, 0);
config.roll.dice = [];
roll.dice.forEach(d => {
config.roll.dice.push({
dice: d.denomination,
total: d.total,
formula: d.formula,
results: d.results
});
});
config.roll.modifierTotal = config.roll.modifiers.reduce((a, c) => a + Number(c.value), 0);
}
getRollData() {
return this.options.data();
return this.options.data;
}
formatModifier(modifier) {
@ -332,7 +353,7 @@ export class DualityRoll extends D20Roll {
bardRallyFaces = this.hasBarRally,
advDie = new foundry.dice.terms.Die({ faces: dieFaces });
if (this.hasAdvantage || this.hasDisadvantage || bardRallyFaces)
this.terms.push(new foundry.dice.terms.OperatorTerm({ operator: '+' }));
this.terms.push(new foundry.dice.terms.OperatorTerm({ operator: this.hasDisadvantage ? '-' : '+' }));
if (bardRallyFaces) {
const rallyDie = new foundry.dice.terms.Die({ faces: bardRallyFaces });
if (this.hasAdvantage) {
@ -349,15 +370,15 @@ export class DualityRoll extends D20Roll {
}
applyBaseBonus() {
if (!this.options.roll.modifiers) this.options.roll.modifiers = [];
if (this.options.roll?.trait)
this.options.roll.modifiers.push({
this.options.roll.modifiers = [
{
label: `DAGGERHEART.Abilities.${this.options.roll.trait}.name`,
value: this.options.data.traits[this.options.roll.trait].total
});
value: Roll.replaceFormulaData(`@traits.${this.options.roll.trait}.total`, this.data)
}
];
}
static async postEvaluate(roll, config = {}) {
static postEvaluate(roll, config = {}) {
super.postEvaluate(roll, config);
config.roll.hope = {
dice: roll.dHope.denomination,
@ -372,6 +393,7 @@ export class DualityRoll extends D20Roll {
total: roll.dHope.total + roll.dFear.total,
label: roll.totalLabel
};
console.log(roll, config);
}
}
@ -385,19 +407,7 @@ export class DamageRoll extends DHRoll {
static DefaultDialog = DamageDialog;
static async postEvaluate(roll, config = {}) {
config.roll = {
total: roll.total,
formula: roll.formula,
type: config.type
};
config.roll.dice = [];
roll.dice.forEach(d => {
config.roll.dice.push({
dice: d.denomination,
total: d.total,
formula: d.formula,
results: d.results
});
});
super.postEvaluate(roll, config);
config.roll.type = config.type;
}
}

View file

@ -24,14 +24,16 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
editItem: this.editItem,
removeItem: this.removeItem,
resetMoves: this.resetMoves,
save: this.save
save: this.save,
reset: this.reset
},
form: { handler: this.updateData, submitOnChange: true }
};
static PARTS = {
main: {
template: 'systems/daggerheart/templates/settings/homebrew-settings.hbs'
template: 'systems/daggerheart/templates/settings/homebrew-settings.hbs',
scrollable: ['']
}
};
@ -154,4 +156,27 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Homebrew, this.settings.toObject());
this.close();
}
static async reset() {
const resetSettings = new DhHomebrew();
let localizedSettings = this.localizeObject(resetSettings);
this.settings.updateSource(localizedSettings);
this.render();
}
localizeObject(obj) {
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
const value = obj[key];
if (typeof value === 'object' && value !== null) {
obj[key] = this.localizeObject(value);
} else {
if (typeof value === 'string' && value.startsWith('DAGGERHEART.')) {
obj[key] = game.i18n.localize(value);
}
}
}
}
return obj;
}
}

View file

@ -46,19 +46,18 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
switch (partId) {
case 'description':
const value = foundry.utils.getProperty(this.document, "system.description") ?? "";
const value = foundry.utils.getProperty(this.document, 'system.description') ?? '';
context.enrichedDescription = await TextEditor.enrichHTML(value, {
relativeTo: this.item,
rollData: this.item.getRollData(),
secrets: this.item.isOwner
})
});
break;
}
return context;
}
/* -------------------------------------------- */
/* Application Clicks Actions */
/* -------------------------------------------- */
@ -70,26 +69,18 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
*/
static async selectActionType() {
const content = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/views/actionType.hbs',
{ types: SYSTEM.ACTIONS.actionTypes }
),
title = 'Select Action Type', //useless var
type = 'form',
data = {}; //useless var
//TODO: use DialogV2
return Dialog.prompt({
title,
label: title,
'systems/daggerheart/templates/views/actionType.hbs',
{ types: SYSTEM.ACTIONS.actionTypes }
),
title = 'Select Action Type';
return foundry.applications.api.DialogV2.prompt({
window: { title },
content,
type, //this prop is useless
callback: html => {
const form = html[0].querySelector('form'),
fd = new foundry.applications.ux.FormDataExtended(form);
foundry.utils.mergeObject(data, fd.object, { inplace: true });
// if (!data.name?.trim()) data.name = game.i18n.localize(SYSTEM.ACTIONS.actionTypes[data.type].name);
return data;
},
rejectClose: false
ok: {
label: title,
callback: (event, button, dialog) => button.form.elements.type.value
}
});
}
@ -100,13 +91,14 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
*/
static async #addAction(_event, _button) {
const actionType = await DHBaseItemSheet.selectActionType();
if (!actionType) return;
try {
const cls = actionsTypes[actionType?.type] ?? actionsTypes.attack,
const cls = actionsTypes[actionType] ?? actionsTypes.attack,
action = new cls(
{
_id: foundry.utils.randomID(),
type: actionType.type,
name: game.i18n.localize(SYSTEM.ACTIONS.actionTypes[actionType.type].name),
type: actionType,
name: game.i18n.localize(SYSTEM.ACTIONS.actionTypes[actionType].name),
...cls.getSourceConfig(this.document)
},
{
@ -141,9 +133,7 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
event.stopPropagation();
const actionIndex = button.closest('[data-index]').dataset.index;
await this.document.update({
'system.actions': this.document.system.actions.filter(
(_, index) => index !== Number.parseInt(actionIndex)
)
'system.actions': this.document.system.actions.filter((_, index) => index !== Number.parseInt(actionIndex))
});
}
}

View file

@ -56,7 +56,6 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
resizable: true
},
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false
},
@ -218,6 +217,15 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
this._createContextMenues();
}
/** @inheritDoc */
async _onRender(context, options) {
await super._onRender(context, options);
this._createSearchFilter();
}
/* -------------------------------------------- */
_createContextMenues() {
const allOptions = {
useItem: {
@ -332,12 +340,123 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
return acc;
}, {});
context.inventory = {
currency: {
title: game.i18n.localize('DAGGERHEART.Sheets.PC.Gold.Title'),
coins: game.i18n.localize('DAGGERHEART.Sheets.PC.Gold.Coins'),
handfulls: game.i18n.localize('DAGGERHEART.Sheets.PC.Gold.Handfulls'),
bags: game.i18n.localize('DAGGERHEART.Sheets.PC.Gold.Bags'),
chests: game.i18n.localize('DAGGERHEART.Sheets.PC.Gold.Chests')
}
};
const homebrewCurrency = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Homebrew).currency;
if (homebrewCurrency.enabled) {
context.inventory.currency = homebrewCurrency;
}
if (context.inventory.length === 0) {
context.inventory = Array(1).fill(Array(5).fill([]));
}
return context;
}
static async updateForm(event, _, formData) {
await this.document.update(formData.object);
this.render();
/* -------------------------------------------- */
/* Search Filter */
/* -------------------------------------------- */
/**
* The currently active search filter.
* @type {foundry.applications.ux.SearchFilter}
*/
#search = {};
/**
* Track which item IDs are currently displayed due to a search filter.
* @type {{ inventory: Set<string>, loadout: Set<string> }}
*/
#filteredItems = {
inventory: new Set(),
loadout: new Set()
};
/**
* Create and initialize search filter instances for the inventory and loadout sections.
*
* Sets up two {@link foundry.applications.ux.SearchFilter} instances:
* - One for the inventory, which filters items in the inventory grid.
* - One for the loadout, which filters items in the loadout/card grid.
* @private
*/
_createSearchFilter() {
//Filters could be a application option if needed
const filters = [
{
key: 'inventory',
input: 'input[type="search"].search-inventory',
content: '[data-application-part="inventory"] .items-section',
callback: this._onSearchFilterInventory.bind(this)
},
{
key: 'loadout',
input: 'input[type="search"].search-loadout',
content: '[data-application-part="loadout"] .items-section',
callback: this._onSearchFilterCard.bind(this)
}
];
for (const { key, input, content, callback } of filters) {
const filter = new foundry.applications.ux.SearchFilter({
inputSelector: input,
contentSelector: content,
callback
});
filter.bind(this.element);
this.#search[key] = filter;
}
}
/**
* Handle invetory items search and filtering.
* @param {KeyboardEvent} event The keyboard input event.
* @param {string} query The input search string.
* @param {RegExp} rgx The regular expression query that should be matched against.
* @param {HTMLElement} html The container to filter items from.
* @protected
*/
_onSearchFilterInventory(event, query, rgx, html) {
this.#filteredItems.inventory.clear();
for (const ul of html.querySelectorAll('.items-list')) {
for (const li of ul.querySelectorAll('.inventory-item')) {
const item = this.document.items.get(li.dataset.itemId);
const match = !query || foundry.applications.ux.SearchFilter.testQuery(rgx, item.name);
if (match) this.#filteredItems.inventory.add(item.id);
li.hidden = !match;
}
}
}
/**
* Handle card items search and filtering.
* @param {KeyboardEvent} event The keyboard input event.
* @param {string} query The input search string.
* @param {RegExp} rgx The regular expression query that should be matched against.
* @param {HTMLElement} html The container to filter items from.
* @protected
*/
_onSearchFilterCard(event, query, rgx, html) {
this.#filteredItems.loadout.clear();
const elements = html.querySelectorAll('.items-list .inventory-item, .card-list .card-item');
for (const li of elements) {
const item = this.document.items.get(li.dataset.itemId);
const match = !query || foundry.applications.ux.SearchFilter.testQuery(rgx, item.name);
if (match) this.#filteredItems.loadout.add(item.id);
li.hidden = !match;
}
}
static async rollAttribute(event, button) {
@ -543,7 +662,7 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
}
static async makeDeathMove() {
if (this.document.system.resources.hitPoints.value === this.document.system.resources.hitPoints.max) {
if (this.document.system.resources.hitPoints.value >= this.document.system.resources.hitPoints.maxTotal) {
await new DhpDeathMove(this.document).render(true);
}
}