Files
l5rx-chiaroscuro/system/scripts/dice/chiaroscuro-dice-dialog.js

335 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Chiaroscuro Dice Dialog
*
* d6 pool system: ring value × multiplier d6, sum vs difficulty.
* Multiplier: ×1 base, ×2 if aspect or assistance, ×3 if both.
* Parangon passives adjust individual die results before summing.
*/
export class ChiaroscuroDiceDialog extends FormApplication {
/**
* Current Actor
* @type {ActorL5r5e}
* @private
*/
_actor = null;
/**
* Payload Object
*/
object = {
ring: { id: "void", value: 1 },
skill: { id: "", name: "", bonus: 0, rank: "0" },
difficulty: { id: "moyenne", value: 10 },
modifier: 0,
useAspectPoint: false,
aspectType: "solar",
useAssistance: false,
};
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
id: "l5r5e-chiaroscuro-dice-dialog",
classes: ["l5r5e", "chiaroscuro-dice-dialog"],
template: CONFIG.l5r5e.paths.templates + "dice/chiaroscuro-dice-dialog.html",
title: game.i18n.localize("chiaroscuro.dice.title"),
width: 440,
height: "auto",
});
}
get id() {
return `l5r5e-chiaroscuro-dice-dialog-${this._actor?.id ?? "no-actor"}`;
}
get title() {
return game.i18n.localize("chiaroscuro.dice.title") + (this._actor ? " — " + this._actor.name : "");
}
/**
* Total dice to roll (ring value × multiplier)
* @return {number}
*/
get totalDice() {
const base = this.object.ring.value;
const both = this.object.useAspectPoint && this.object.useAssistance;
const either = this.object.useAspectPoint || this.object.useAssistance;
return base * (both ? 3 : either ? 2 : 1);
}
/**
* @param options actor, actorId, ringId, skillId
*/
constructor(options = {}) {
super({}, options);
// Resolve actor
[
options?.actor,
game.actors.get(options?.actorId),
canvas.tokens.controlled[0]?.actor,
game.user.character,
].forEach((actor) => {
if (!this._actor && actor instanceof Actor && actor.isOwner) {
this._actor = actor;
}
});
// Default ring: options > actor default_ring > void
const ringId = options.ringId ?? this._actor?.system?.default_ring ?? "void";
this.ringId = ringId;
// Skill
if (options.skillId) {
this.skillId = options.skillId;
}
}
/**
* Set ring (id + value from actor)
* @param {string} ringId
*/
set ringId(ringId) {
this.object.ring.id = CONFIG.l5r5e.stances.includes(ringId) ? ringId : "void";
this.object.ring.value = this._actor?.system?.rings?.[this.object.ring.id] || 1;
// Auto-derive aspect type from ring (fire/earth → solar, air/water → lunar; void = manual)
if (this.object.ring.id !== "void") {
this.object.aspectType = ["fire", "earth"].includes(this.object.ring.id) ? "solar" : "lunar";
}
}
/**
* Set skill (id, name, rank, bonus)
* @param {string} skillId
*/
set skillId(skillId) {
if (!skillId) return;
const catId = CONFIG.l5r5e.skills.get(skillId.toLowerCase().trim());
const rank = this._actor?.system?.skills?.[catId]?.[skillId] ?? "0";
this.object.skill = {
...this.object.skill,
id: skillId,
name: catId ? game.i18n.localize(`l5r5e.skills.${catId}.${skillId}`) : skillId,
rank,
bonus: CONFIG.l5r5e.skillRanks?.[rank]?.bonus ?? 0,
};
}
async getData(options = null) {
const difficultiesList = Object.entries(CONFIG.l5r5e.difficulties).map(([id, value]) => ({
id,
label: game.i18n.localize(`chiaroscuro.difficulties.${id}`),
value,
}));
const aspectsList = [
{ id: "solar", label: game.i18n.localize("chiaroscuro.aspects.solar") },
{ id: "lunar", label: game.i18n.localize("chiaroscuro.aspects.lunar") },
];
return {
...(await super.getData(options)),
actor: this._actor,
data: this.object,
totalDice: this.totalDice,
ringsList: game.l5r5e.HelpersL5r5e.getRingsList(this._actor),
difficultiesList,
aspectsList,
isVoidRing: this.object.ring.id === "void",
quickInfo: this._actor?.system?.quick_info ?? "",
};
}
activateListeners(html) {
super.activateListeners(html);
// Ring selector
html.find(".ring-selection-chi").on("click", async (event) => {
event.preventDefault();
event.stopPropagation();
this.ringId = event.currentTarget.dataset.ringid;
this.render(false);
});
// Difficulty select
html.find("select[name='difficulty.id']").on("change", (event) => {
this.object.difficulty.id = event.target.value;
this.object.difficulty.value = CONFIG.l5r5e.difficulties[this.object.difficulty.id];
this.render(false);
});
// Flat modifier
html.find("input[name='modifier']").on("change", (event) => {
this.object.modifier = parseInt(event.target.value) || 0;
});
// Aspect point checkbox
html.find("#use_aspect_point").on("change", (event) => {
this.object.useAspectPoint = event.target.checked;
this.render(false);
});
// Aspect type select (solar / lunar)
html.find("select[name='aspectType']").on("change", (event) => {
this.object.aspectType = event.target.value;
});
// Assistance checkbox
html.find("#use_assistance").on("change", (event) => {
this.object.useAssistance = event.target.checked;
this.render(false);
});
// Roll button — explicit submit trigger
html.find("button[name='roll']").on("click", (event) => {
event.preventDefault();
this._onSubmit(event);
});
}
async _updateObject(event, formData) {
const nbDice = this.totalDice;
const skillRank = this.object.skill.rank;
const skillBonus = this.object.skill.bonus;
const flatModifier = this.object.modifier;
const difficultyObj = this.object.difficulty;
const difficultyValue = difficultyObj.value;
// Roll the dice using FoundryVTT Roll API
const roll = await new Roll(`${nbDice}d6`).evaluate();
const rawResults = roll.dice[0].results.map((r) => r.result);
// Apply parangon passive adjustments
const adjustedResults = rawResults.map((r) => this._applyParangon(r, skillRank));
const diceAdjustedFlags = rawResults.map((r, i) => adjustedResults[i] !== r);
const wasAdjusted = diceAdjustedFlags.some(Boolean);
// Compute total
const rawSum = adjustedResults.reduce((a, b) => a + b, 0);
const total = rawSum + skillBonus + flatModifier;
const success = total >= difficultyValue;
const bonus = success ? total - difficultyValue : 0;
// Update aspect gauge after roll
if (this._actor && this.object.useAspectPoint) {
await this._updateAspectGauge();
}
// Post chat message
await this._sendChatMessage({
nbDice,
rawResults,
adjustedResults,
diceAdjustedFlags,
wasAdjusted,
rawSum,
total,
skillBonus,
flatModifier,
difficulty: difficultyObj,
success,
bonus,
});
return this.close();
}
/**
* Apply parangon rank passive: replace low die results with higher value.
* parangon1: 1 → 2
* parangon2: 12 → 3
* parangon3: 13 → 4
* @param {number} result
* @param {string} rank
* @return {number}
*/
_applyParangon(result, rank) {
if (rank === "parangon3" && result <= 3) return 4;
if (rank === "parangon2" && result <= 2) return 3;
if (rank === "parangon1" && result <= 1) return 2;
return result;
}
/**
* Update the aspect gauge on the actor after an aspect point roll.
* Gauge positive = solar side, negative = lunar side.
* ±5 → apply Déséquilibre. ±10 → full reset.
*/
async _updateAspectGauge() {
// Support both single-nested (system.aspects) and double-nested (system.aspects.aspects)
const aspectsPath = this._actor.system.aspects?.aspects !== undefined
? "system.aspects.aspects"
: "system.aspects";
const aspects = foundry.utils.getProperty(this._actor, aspectsPath) ?? {};
const gaugeDirection = this.object.aspectType === "solar" ? 1 : -1;
const newGauge = (aspects.gauge ?? 0) + gaugeDirection;
if (Math.abs(newGauge) >= 10) {
// Full reset
await this._actor.update({
[`${aspectsPath}.gauge`]: 0,
[`${aspectsPath}.solar`]: 0,
[`${aspectsPath}.lunar`]: 0,
});
// Remove all desequilibre conditions
const toRemove = this._actor.items
.filter((i) => i.type === "etat" && ["desequilibre_solaire", "desequilibre_lunaire"].includes(i.system?.condition_type))
.map((i) => i.id);
if (toRemove.length) {
await this._actor.deleteEmbeddedDocuments("Item", toRemove);
}
} else {
await this._actor.update({ [`${aspectsPath}.gauge`]: newGauge });
if (Math.abs(newGauge) >= 5) {
// Apply opposing desequilibre
const condType = this.object.aspectType === "solar" ? "desequilibre_lunaire" : "desequilibre_solaire";
const existing = this._actor.items.find(
(i) => i.type === "etat" && i.system?.condition_type === condType
);
if (!existing) {
await this._actor.createEmbeddedDocuments("Item", [
{
type: "etat",
name: game.i18n.localize(`chiaroscuro.aspects.${condType}`),
system: { condition_type: condType },
},
]);
}
}
}
}
/**
* Create and send the chat message.
*/
async _sendChatMessage(rollData) {
const content = await foundry.applications.handlebars.renderTemplate(
CONFIG.l5r5e.paths.templates + "dice/chiaroscuro-chat-roll.html",
{
actor: this._actor,
profileImg: this._actor?.img ?? "icons/svg/mystery-man.svg",
ring: this.object.ring,
skill: this.object.skill,
difficulty: this.object.difficulty,
useAspectPoint: this.object.useAspectPoint,
aspectType: this.object.aspectType,
useAssistance: this.object.useAssistance,
modifier: this.object.modifier,
quickInfo: this._actor?.system?.quick_info ?? "",
...rollData,
}
);
return ChatMessage.implementation.create({
user: game.user.id,
speaker: {
actor: this._actor?.id ?? null,
alias: this._actor?.name ?? null,
},
content,
sound: CONFIG.sounds.dice,
});
}
}