Embedding Duality Rolls (#52)

* Added DualityRoll direct rolls in chat
* Added button render to renderJournalEntryPageProseMirrorSheet and renderHandlebarsApplication
* Hope and Fear dice totals are now properly added together
* Added Colorful/Normal DualityRoll color settings
This commit is contained in:
WBHarry 2025-05-26 16:34:32 +02:00 committed by GitHub
parent cf51153432
commit d1a0a9ab24
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 1192 additions and 1264 deletions

View file

@ -1,3 +1,6 @@
import { DualityRollColor } from '../config/settingsConfig.mjs';
import DhpDualityRoll from '../data/dualityRoll.mjs';
export default class DhpChatMesssage extends ChatMessage {
async renderHTML() {
if (
@ -9,6 +12,20 @@ export default class DhpChatMesssage extends ChatMessage {
this.content = await foundry.applications.handlebars.renderTemplate(this.content, this.system);
}
return super.renderHTML();
/* We can change to fully implementing the renderHTML function if needed, instead of augmenting it. */
const html = await super.renderHTML();
if (
this.type === 'dualityRoll' &&
game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.DualityRollColor) ===
DualityRollColor.colorful.value
) {
html.classList.add('duality');
const dualityResult = this.system.dualityResult;
if (dualityResult === DhpDualityRoll.dualityResult.hope) html.classList.add('hope');
else if (dualityResult === DhpDualityRoll.dualityResult.fear) html.classList.add('fear');
else html.classList.add('critical');
}
return html;
}
}

View file

@ -113,7 +113,9 @@ export default class DamageSelectionDialog extends HandlebarsApplicationMixin(Ap
}
}
static rollDamage() {
static rollDamage(event) {
event.preventDefault();
this.resolve({
rollString: this.getRollString(),
bonusDamage: this.data.bonusDamage,

View file

@ -1,3 +1,5 @@
import { DualityRollColor } from '../config/settingsConfig.mjs';
class DhpAutomationSettings extends FormApplication {
constructor(object = {}, options = {}) {
super(object, options);
@ -213,6 +215,16 @@ export const registerDHPSettings = () => {
}
});
game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.DualityRollColor, {
name: game.i18n.localize('DAGGERHEART.Settings.DualityRollColor.Name'),
hint: game.i18n.localize('DAGGERHEART.Settings.DualityRollColor.Hint'),
scope: 'world',
config: true,
type: Number,
choices: Object.values(DualityRollColor),
default: DualityRollColor.colorful.value
});
game.settings.registerMenu(SYSTEM.id, SYSTEM.SETTINGS.menu.Automation.Name, {
name: game.i18n.localize('DAGGERHEART.Settings.Menu.Automation.Name'),
label: game.i18n.localize('DAGGERHEART.Settings.Menu.Automation.Label'),

File diff suppressed because it is too large Load diff

View file

@ -24,5 +24,17 @@ export const gameSettings = {
General: {
AbilityArray: 'AbilityArray',
RangeMeasurement: 'RangeMeasurement'
},
DualityRollColor: 'DualityRollColor'
};
export const DualityRollColor = {
colorful: {
value: 0,
label: 'DAGGERHEART.Settings.DualityRollColor.Options.Colorful'
},
normal: {
value: 1,
label: 'DAGGERHEART.Settings.DualityRollColor.Options.Normal'
}
};

View file

@ -1,3 +1,5 @@
import { DualityRollColor } from '../config/settingsConfig.mjs';
const fields = foundry.data.fields;
const diceField = () =>
new fields.SchemaField({
@ -6,6 +8,12 @@ const diceField = () =>
});
export default class DhpDualityRoll extends foundry.abstract.TypeDataModel {
static dualityResult = {
hope: 1,
fear: 2,
critical: 3
};
static defineSchema() {
return {
title: new fields.StringField(),
@ -57,17 +65,32 @@ export default class DhpDualityRoll extends foundry.abstract.TypeDataModel {
}
get total() {
const modifiers = this.modifiers.reduce((acc, x) => acc + x.value, 0);
const advantage = this.advantage.value
? this.advantage.value
: this.disadvantage.value
? -this.disadvantage.value
: 0;
return this.highestRoll + advantage + modifiers;
return this.diceTotal + advantage + this.modifierTotal.value;
}
get highestRoll() {
return Math.max(this.hope.value, this.fear.value);
get diceTotal() {
return this.hope.value + this.fear.value;
}
get modifierTotal() {
const total = this.modifiers.reduce((acc, x) => acc + x.value, 0);
return {
value: total,
label: total > 0 ? `+${total}` : total < 0 ? `-${total}` : ''
};
}
get dualityResult() {
return this.hope.value > this.fear.value
? this.constructor.dualityResult.hope
: this.fear.value > this.hope.value
? this.constructor.dualityResult.fear
: this.constructor.dualityResult.critical;
}
get totalLabel() {
@ -81,6 +104,13 @@ export default class DhpDualityRoll extends foundry.abstract.TypeDataModel {
return game.i18n.localize(label);
}
get colorful() {
return (
game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.DualityRollColor) ===
DualityRollColor.colorful.value
);
}
prepareDerivedData() {
const total = this.total;
@ -92,89 +122,3 @@ export default class DhpDualityRoll extends foundry.abstract.TypeDataModel {
});
}
}
//V1.3
// const fields = foundry.data.fields;
// const diceField = () => new fields.SchemaField({
// dice: new fields.StringField({}),
// value: new fields.NumberField({ integer: true}),
// });
// export default class DhpDualityRoll extends foundry.abstract.TypeDataModel {
// static defineSchema() {
// return {
// roll: new fields.StringField({}),
// modifiers: new fields.ArrayField(new fields.SchemaField({
// value: new fields.NumberField({ integer: true }),
// label: new fields.StringField({}),
// title: new fields.StringField({}),
// })),
// hope: diceField(),
// fear: diceField(),
// advantage: diceField(),
// disadvantage: diceField(),
// advantageSelected: new fields.NumberField({ initial: 0 }),
// targets: new fields.ArrayField(new fields.SchemaField({
// id: new fields.StringField({}),
// name: new fields.StringField({}),
// img: new fields.StringField({}),
// difficulty: new fields.NumberField({ integer: true, nullable: true }),
// evasion: new fields.NumberField({ integer: true }),
// hit: new fields.BooleanField({ initial: false }),
// })),
// damage: new fields.SchemaField({
// value: new fields.StringField({}),
// type: new fields.StringField({ choices: Object.keys(SYSTEM.GENERAL.damageTypes), integer: false }),
// bonusDamage: new fields.ArrayField(new fields.SchemaField({
// value: new fields.StringField({}),
// type: new fields.StringField({ choices: Object.keys(SYSTEM.GENERAL.damageTypes), integer: false }),
// initiallySelected: new fields.BooleanField(),
// appliesOn: new fields.StringField({ choices: Object.keys(SYSTEM.EFFECTS.applyLocations) }, { nullable: true, initial: null }),
// description: new fields.StringField({}),
// hopeIncrease: new fields.StringField({ nullable: true })
// }), { nullable: true, initial: null })
// })
// }
// }
// get total() {
// const modifiers = this.modifiers.reduce((acc, x) => acc+x.value, 0);
// const regular = {
// normal: this.disadvantage.value ? Math.min(this.disadvantage.value, this.hope.value) + this.fear.value + modifiers : this.hope.value + this.fear.value + modifiers,
// alternate: this.advantage.value ? this.advantage.value + this.fear.value + modifiers : null,
// };
// const advantageSolve = this.advantageSelected === 0 ? null : {
// normal: this.advantageSelected === 1 ? this.hope.value + this.fear.value + modifiers : this.advantage.value + this.fear.value + modifiers,
// alternate: null,
// };
// return advantageSolve ?? regular;
// }
// get totalLabel() {
// if(this.advantage.value && this.advantageSelected === 0) return game.i18n.localize("DAGGERHEART.Chat.DualityRoll.AdvantageChooseTitle");
// const hope = !this.advantage.value || this.advantageSelected === 1 ? this.hope.value : this.advantage.value;
// const label = hope > this.fear.value ? "DAGGERHEART.General.Hope" : this.fear.value > hope ? "DAGGERHEART.General.Fear" : "DAGGERHEART.General.CriticalSuccess";
// return game.i18n.localize(label);
// }
// get dualityDiceStates() {
// return {
// hope: this.hope.value > this.fear.value ? 'hope' : this.fear.value > this.hope.value ? 'fear' : 'critical',
// alternate: this.advantage.value > this.fear.value ? 'hope' : this.fear.value > this.advantage.value ? 'fear' : 'critical',
// }
// }
// prepareDerivedData(){
// const total = this.total;
// if(total.alternate) return false;
// this.targets.forEach(target => {
// target.hit = target.difficulty ? total.normal >= target.difficulty : total.normal >= target.evasion;
// });
// }
// }

View file

@ -119,7 +119,10 @@ export default class DhpActor extends Actor {
const modifiers = [
{
value: modifier.value ? Number.parseInt(modifier.value) : 0,
label: modifier.value >= 0 ? `+${modifier.value}` : `-${modifier.value}`,
label:
modifier.value >= 0
? `${modifier.title} +${modifier.value}`
: `${modifier.title} -${modifier.value}`,
title: modifier.title
}
];

View file

@ -0,0 +1,36 @@
import { abilities } from '../config/actorConfig.mjs';
import { rollCommandToJSON } from '../helpers/utils.mjs';
export function dualityRollEnricher(match, _options) {
const roll = rollCommandToJSON(match[1]);
if (!roll) return match[0];
return getDualityMessage(roll);
}
export function getDualityMessage(roll) {
const attributeLabel =
roll.attribute && abilities[roll.attribute]
? game.i18n.format('DAGGERHEART.General.Check', {
check: game.i18n.localize(abilities[roll.attribute].label)
})
: null;
const label = attributeLabel ?? game.i18n.localize('DAGGERHEART.General.Duality');
const dualityElement = document.createElement('span');
dualityElement.innerHTML = `
<button class="duality-roll-button"
data-label="${label}"
data-hope="${roll.hope ?? 'd12'}"
data-fear="${roll.fear ?? 'd12'}"
${roll.attribute && abilities[roll.attribute] ? `data-attribute="${roll.attribute}"` : ''}
${roll.advantage ? 'data-advantage="true"' : ''}
${roll.disadvantage ? 'data-disadvantage="true"' : ''}
>
<i class="fa-solid fa-circle-half-stroke"></i>
${label}
</button>
`;
return dualityElement;
}

View file

@ -22,17 +22,6 @@ const getCompendiumOptions = async compendium => {
};
export const getWidthOfText = (txt, fontsize, allCaps, bold) => {
// if(getWidthOfText.e === undefined){
// getWidthOfText.e = document.createElement('span');
// getWidthOfText.e.style.display = "none";
// document.body.appendChild(getWidthOfText.e);
// }
// if(getWidthOfText.e.style.fontSize !== fontsize)
// getWidthOfText.e.style.fontSize = fontsize;
// if(getWidthOfText.e.style.fontFamily !== 'Signika, sans-serif')
// getWidthOfText.e.style.fontFamily = 'Signika, sans-serif';
// getWidthOfText.e.innerText = txt;
// return getWidthOfText.e.offsetWidth;
const text = allCaps ? txt.toUpperCase() : txt;
if (getWidthOfText.c === undefined) {
getWidthOfText.c = document.createElement('canvas');
@ -82,3 +71,50 @@ export const generateId = (title, length) => {
.join('');
return Number.isNumeric(length) ? id.slice(0, length).padEnd(length, '0') : id;
};
export function rollCommandToJSON(text) {
if (!text) return {};
// Match key="quoted string" OR key=unquotedValue
const PAIR_RE = /(\w+)=("(?:[^"\\]|\\.)*"|\S+)/g;
const result = {};
for (const [, key, raw] of text.matchAll(PAIR_RE)) {
let value;
if (raw.startsWith('"') && raw.endsWith('"')) {
// Strip the surrounding quotes, un-escape any \" sequences
value = raw.slice(1, -1).replace(/\\"/g, '"');
} else if (/^(true|false)$/i.test(raw)) {
// Boolean
value = raw.toLowerCase() === 'true';
} else if (!Number.isNaN(Number(raw))) {
// Numeric
value = Number(raw);
} else {
// Fallback to string
value = raw;
}
result[key] = value;
}
return Object.keys(result).length > 0 ? result : null;
}
export const getCommandTarget = () => {
let target = game.canvas.tokens.controlled.length > 0 ? game.canvas.tokens.controlled[0].actor : null;
if (!game.user.isGM) {
target = game.user.character;
if (!target) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.Notification.Error.NoAssignedPlayerCharacter'));
return null;
}
}
if (!target) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.Notification.Error.NoSelectedToken'));
return null;
}
if (target.type !== 'pc') {
ui.notifications.error(game.i18n.localize('DAGGERHEART.Notification.Error.OnlyUseableByPC'));
return null;
}
return target;
};

View file

@ -14,7 +14,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
}
addChatListeners = async (app, html, data) => {
html.querySelectorAll('.roll-damage-button').forEach(element =>
html.querySelectorAll('.duality-action').forEach(element =>
element.addEventListener('click', event => this.onRollDamage(event, data.message))
);
html.querySelectorAll('.target-container').forEach(element => {