mirror of
https://github.com/Foundryborne/daggerheart.git
synced 2026-01-11 19:25:21 +01:00
359 lines
13 KiB
JavaScript
359 lines
13 KiB
JavaScript
import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs';
|
|
import D20Roll from './d20Roll.mjs';
|
|
import { setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs';
|
|
import { getDiceSoNicePresets } from '../config/generalConfig.mjs';
|
|
import { ResourceUpdateMap } from '../data/action/baseAction.mjs';
|
|
|
|
export default class DualityRoll extends D20Roll {
|
|
_advantageFaces = 6;
|
|
_advantageNumber = 1;
|
|
_rallyIndex;
|
|
|
|
constructor(formula, data = {}, options = {}) {
|
|
super(formula, data, options);
|
|
this.rallyChoices = this.setRallyChoices();
|
|
}
|
|
|
|
static messageType = 'dualityRoll';
|
|
|
|
static DefaultDialog = D20RollDialog;
|
|
|
|
get title() {
|
|
return game.i18n.localize(
|
|
`DAGGERHEART.GENERAL.${this.options?.actionType === 'reaction' ? 'reactionRoll' : 'dualityRoll'}`
|
|
);
|
|
}
|
|
|
|
get dHope() {
|
|
// if ( !(this.terms[0] instanceof foundry.dice.terms.Die) ) return;
|
|
if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice();
|
|
return this.dice[0];
|
|
// return this.#hopeDice;
|
|
}
|
|
|
|
set dHope(faces) {
|
|
if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice();
|
|
this.terms[0].faces = this.getFaces(faces);
|
|
// this.#hopeDice = `d${face}`;
|
|
}
|
|
|
|
get dFear() {
|
|
// if ( !(this.terms[1] instanceof foundry.dice.terms.Die) ) return;
|
|
if (!(this.dice[1] instanceof foundry.dice.terms.Die)) this.createBaseDice();
|
|
return this.dice[1];
|
|
// return this.#fearDice;
|
|
}
|
|
|
|
set dFear(faces) {
|
|
if (!(this.dice[1] instanceof foundry.dice.terms.Die)) this.createBaseDice();
|
|
this.dice[1].faces = this.getFaces(faces);
|
|
// this.#fearDice = `d${face}`;
|
|
}
|
|
|
|
get dAdvantage() {
|
|
return this.dice[2];
|
|
}
|
|
|
|
get advantageFaces() {
|
|
return this._advantageFaces;
|
|
}
|
|
|
|
set advantageFaces(faces) {
|
|
this._advantageFaces = this.getFaces(faces);
|
|
}
|
|
|
|
get advantageNumber() {
|
|
return this._advantageNumber;
|
|
}
|
|
|
|
set advantageNumber(value) {
|
|
this._advantageNumber = Number(value);
|
|
}
|
|
|
|
setRallyChoices() {
|
|
return this.data?.parent?.appliedEffects.reduce((a, c) => {
|
|
const change = c.changes.find(ch => ch.key === 'system.bonuses.rally');
|
|
if (change) a.push({ value: c.id, label: change.value });
|
|
return a;
|
|
}, []);
|
|
}
|
|
|
|
get dRally() {
|
|
if (!this.rallyFaces) return null;
|
|
if (this.hasDisadvantage || this.hasAdvantage) return this.dice[3];
|
|
else return this.dice[2];
|
|
}
|
|
|
|
get rallyFaces() {
|
|
const rallyChoice = this.rallyChoices?.find(r => r.value === this._rallyIndex)?.label;
|
|
return rallyChoice ? this.getFaces(rallyChoice) : null;
|
|
}
|
|
|
|
get isCritical() {
|
|
if (!this.dHope._evaluated || !this.dFear._evaluated) return;
|
|
return this.dHope.total === this.dFear.total;
|
|
}
|
|
|
|
get withHope() {
|
|
if (!this._evaluated) return;
|
|
return this.dHope.total > this.dFear.total;
|
|
}
|
|
|
|
get withFear() {
|
|
if (!this._evaluated) return;
|
|
return this.dHope.total < this.dFear.total;
|
|
}
|
|
|
|
get totalLabel() {
|
|
const label = this.withHope
|
|
? 'DAGGERHEART.GENERAL.hope'
|
|
: this.withFear
|
|
? 'DAGGERHEART.GENERAL.fear'
|
|
: 'DAGGERHEART.GENERAL.criticalSuccess';
|
|
|
|
return game.i18n.localize(label);
|
|
}
|
|
|
|
static getHooks(hooks) {
|
|
return [...(hooks ?? []), 'Duality'];
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
static fromData(data) {
|
|
data.terms[0].class = foundry.dice.terms.Die.name;
|
|
data.terms[2].class = foundry.dice.terms.Die.name;
|
|
return super.fromData(data);
|
|
}
|
|
|
|
createBaseDice() {
|
|
if (this.dice[0] instanceof foundry.dice.terms.Die && this.dice[1] instanceof foundry.dice.terms.Die) {
|
|
this.terms = [this.terms[0], this.terms[1], this.terms[2]];
|
|
return;
|
|
}
|
|
this.terms[0] = new foundry.dice.terms.Die({ faces: 12 });
|
|
this.terms[1] = new foundry.dice.terms.OperatorTerm({ operator: '+' });
|
|
this.terms[2] = new foundry.dice.terms.Die({ faces: 12 });
|
|
}
|
|
|
|
applyAdvantage() {
|
|
if (this.hasAdvantage || this.hasDisadvantage) {
|
|
const dieFaces = this.advantageFaces,
|
|
advDie = new foundry.dice.terms.Die({ faces: dieFaces, number: this.advantageNumber });
|
|
if (this.advantageNumber > 1) advDie.modifiers = ['kh'];
|
|
this.terms.push(
|
|
new foundry.dice.terms.OperatorTerm({ operator: this.hasDisadvantage ? '-' : '+' }),
|
|
advDie
|
|
);
|
|
}
|
|
if (this.rallyFaces)
|
|
this.terms.push(
|
|
new foundry.dice.terms.OperatorTerm({ operator: this.hasDisadvantage ? '-' : '+' }),
|
|
new foundry.dice.terms.Die({ faces: this.rallyFaces })
|
|
);
|
|
}
|
|
|
|
applyBaseBonus() {
|
|
const modifiers = super.applyBaseBonus();
|
|
|
|
if (this.options.roll.trait && this.data.traits?.[this.options.roll.trait])
|
|
modifiers.unshift({
|
|
label:
|
|
this.options.roll.type === CONFIG.DH.GENERAL.rollTypes.spellcast.id
|
|
? 'DAGGERHEART.CONFIG.RollTypes.spellcast.name'
|
|
: `DAGGERHEART.CONFIG.Traits.${this.options.roll.trait}.name`,
|
|
value: this.data.traits[this.options.roll.trait].value
|
|
});
|
|
|
|
const weapons = ['primaryWeapon', 'secondaryWeapon'];
|
|
weapons.forEach(w => {
|
|
if (this.options.source.item && this.options.source.item === this.data[w]?.id)
|
|
modifiers.push(...this.getBonus(`roll.${w}`, 'Weapon Bonus'));
|
|
});
|
|
|
|
return modifiers;
|
|
}
|
|
|
|
static async buildEvaluate(roll, config = {}, message = {}) {
|
|
await super.buildEvaluate(roll, config, message);
|
|
|
|
await setDiceSoNiceForDualityRoll(
|
|
roll,
|
|
config.roll.advantage.type,
|
|
config.roll.hope.dice,
|
|
config.roll.fear.dice,
|
|
config.roll.advantage.dice
|
|
);
|
|
}
|
|
|
|
static postEvaluate(roll, config = {}) {
|
|
const data = super.postEvaluate(roll, config);
|
|
|
|
data.hope = {
|
|
dice: roll.dHope.denomination,
|
|
value: roll.dHope.total,
|
|
rerolled: {
|
|
any: roll.dHope.results.some(x => x.rerolled),
|
|
rerolls: roll.dHope.results.filter(x => x.rerolled)
|
|
}
|
|
};
|
|
data.fear = {
|
|
dice: roll.dFear.denomination,
|
|
value: roll.dFear.total,
|
|
rerolled: {
|
|
any: roll.dFear.results.some(x => x.rerolled),
|
|
rerolls: roll.dFear.results.filter(x => x.rerolled)
|
|
}
|
|
};
|
|
data.rally = {
|
|
dice: roll.dRally?.denomination,
|
|
value: roll.dRally?.total
|
|
};
|
|
data.result = {
|
|
duality: roll.withHope ? 1 : roll.withFear ? -1 : 0,
|
|
total: roll.dHope.total + roll.dFear.total,
|
|
label: roll.totalLabel
|
|
};
|
|
|
|
if (roll._rallyIndex && roll.data?.parent)
|
|
roll.data.parent.deleteEmbeddedDocuments('ActiveEffect', [roll._rallyIndex]);
|
|
|
|
return data;
|
|
}
|
|
|
|
static async buildPost(roll, config, message) {
|
|
await super.buildPost(roll, config, message);
|
|
|
|
await DualityRoll.dualityUpdate(config);
|
|
}
|
|
|
|
static async addDualityResourceUpdates(config) {
|
|
const automationSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation);
|
|
const hopeFearAutomation = automationSettings.hopeFear;
|
|
if (
|
|
!config.source?.actor ||
|
|
(game.user.isGM ? !hopeFearAutomation.gm : !hopeFearAutomation.players) ||
|
|
config.actionType === 'reaction' ||
|
|
config.tagTeamSelected ||
|
|
config.skips?.resources
|
|
)
|
|
return;
|
|
const actor = await fromUuid(config.source.actor);
|
|
let updates = [];
|
|
if (!actor) return;
|
|
|
|
if (config.rerolledRoll) {
|
|
if (config.roll.result.duality != config.rerolledRoll.result.duality) {
|
|
const hope =
|
|
(config.roll.isCritical || config.roll.result.duality === 1 ? 1 : 0) -
|
|
(config.rerolledRoll.isCritical || config.rerolledRoll.result.duality === 1 ? 1 : 0);
|
|
const stress = (config.roll.isCritical ? 1 : 0) - (config.rerolledRoll.isCritical ? 1 : 0);
|
|
const fear =
|
|
(config.roll.result.duality === -1 ? 1 : 0) - (config.rerolledRoll.result.duality === -1 ? 1 : 0);
|
|
|
|
if (hope !== 0) updates.push({ key: 'hope', value: hope, total: -1 * hope, enabled: true });
|
|
if (stress !== 0) updates.push({ key: 'stress', value: -1 * stress, total: stress, enabled: true });
|
|
if (fear !== 0) updates.push({ key: 'fear', value: fear, total: -1 * fear, enabled: true });
|
|
}
|
|
} else {
|
|
if (config.roll.isCritical || config.roll.result.duality === 1)
|
|
updates.push({ key: 'hope', value: 1, total: -1, enabled: true });
|
|
if (config.roll.isCritical) updates.push({ key: 'stress', value: -1, total: 1, enabled: true });
|
|
if (config.roll.result.duality === -1) updates.push({ key: 'fear', value: 1, total: -1, enabled: true });
|
|
}
|
|
|
|
if (updates.length) {
|
|
// const target = actor.system.partner ?? actor;
|
|
if (!['dead', 'defeated', 'unconscious'].some(x => actor.statuses.has(x))) {
|
|
config.resourceUpdates.addResources(updates);
|
|
}
|
|
}
|
|
}
|
|
|
|
static async dualityUpdate(config) {
|
|
const automationSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation);
|
|
if (
|
|
automationSettings.countdownAutomation &&
|
|
config.actionType !== 'reaction' &&
|
|
!config.tagTeamSelected &&
|
|
!config.skips?.updateCountdowns
|
|
) {
|
|
const { updateCountdowns } = game.system.api.applications.ui.DhCountdowns;
|
|
|
|
if (config.roll.result.duality === -1) {
|
|
await updateCountdowns(
|
|
CONFIG.DH.GENERAL.countdownProgressionTypes.actionRoll.id,
|
|
CONFIG.DH.GENERAL.countdownProgressionTypes.fear.id
|
|
);
|
|
} else {
|
|
await updateCountdowns(CONFIG.DH.GENERAL.countdownProgressionTypes.actionRoll.id);
|
|
}
|
|
}
|
|
|
|
await DualityRoll.addDualityResourceUpdates(config);
|
|
|
|
if (!config.roll.hasOwnProperty('success') && !config.targets?.length) return;
|
|
|
|
const rollResult = config.roll.success || config.targets?.some(t => t.hit),
|
|
looseSpotlight = !rollResult || config.roll.result.duality === -1;
|
|
|
|
if (looseSpotlight && game.combat?.active) {
|
|
const currentCombatant = game.combat.combatants.get(game.combat.current?.combatantId);
|
|
if (currentCombatant?.actorId == config.data.id) ui.combat.setCombatantSpotlight(currentCombatant.id);
|
|
}
|
|
}
|
|
|
|
static async reroll(rollString, target, message) {
|
|
let parsedRoll = game.system.api.dice.DualityRoll.fromData({ ...rollString, evaluated: false });
|
|
const term = parsedRoll.terms[target.dataset.dieIndex];
|
|
await term.reroll(`/r1=${term.total}`);
|
|
if (game.modules.get('dice-so-nice')?.active) {
|
|
const diceSoNiceRoll = {
|
|
_evaluated: true,
|
|
dice: [
|
|
new foundry.dice.terms.Die({
|
|
...term,
|
|
faces: term._faces,
|
|
results: term.results.filter(x => !x.rerolled)
|
|
})
|
|
],
|
|
options: { appearance: {} }
|
|
};
|
|
|
|
const diceSoNicePresets = await getDiceSoNicePresets(`d${term._faces}`, `d${term._faces}`);
|
|
const type = target.dataset.type;
|
|
if (diceSoNicePresets[type]) {
|
|
diceSoNiceRoll.dice[0].options = diceSoNicePresets[type];
|
|
}
|
|
|
|
await game.dice3d.showForRoll(diceSoNiceRoll, game.user, true);
|
|
}
|
|
|
|
await parsedRoll.evaluate();
|
|
|
|
const newRoll = game.system.api.dice.DualityRoll.postEvaluate(parsedRoll, {
|
|
targets: message.system.targets,
|
|
roll: {
|
|
advantage: message.system.roll.advantage?.type,
|
|
difficulty: message.system.roll.difficulty ? Number(message.system.roll.difficulty) : null
|
|
}
|
|
});
|
|
newRoll.extra = newRoll.extra.slice(2);
|
|
|
|
const tagTeamSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
|
|
|
|
const actor = message.system.source.actor ? await foundry.utils.fromUuid(message.system.source.actor) : null;
|
|
const config = {
|
|
source: { actor: message.system.source.actor ?? '' },
|
|
targets: message.system.targets,
|
|
tagTeamSelected: Object.values(tagTeamSettings.members).some(x => x.messageId === message._id),
|
|
roll: newRoll,
|
|
rerolledRoll: message.system.roll,
|
|
resourceUpdates: new ResourceUpdateMap(actor)
|
|
};
|
|
|
|
await DualityRoll.addDualityResourceUpdates(config);
|
|
await config.resourceUpdates.updateResources();
|
|
|
|
return { newRoll, parsedRoll };
|
|
}
|
|
}
|