DIvers rework de CSS/LESS et améliorations de messages/layout
This commit is contained in:
+333
-60
@@ -1,3 +1,5 @@
|
||||
import { LesOubliesUtility } from "./les-oublies-utility.js"
|
||||
|
||||
const PRIME_DEFINITIONS = [
|
||||
{
|
||||
id: "none",
|
||||
@@ -187,6 +189,18 @@ const MOVEMENT_DIFFICULTIES = [
|
||||
{ value: -3, label: "Faire un détour (-3)" },
|
||||
]
|
||||
|
||||
const TEST_DIFFICULTIES = [
|
||||
{ value: 12, label: "Exceptionnellement facile (+12)" },
|
||||
{ value: 9, label: "Très facile (+9)" },
|
||||
{ value: 6, label: "Facile (+6)" },
|
||||
{ value: 3, label: "Avantageuse (+3)" },
|
||||
{ value: 0, label: "Normale (+0)" },
|
||||
{ value: -3, label: "Difficile (-3)" },
|
||||
{ value: -6, label: "Très difficile (-6)" },
|
||||
{ value: -9, label: "Extrêmement difficile (-9)" },
|
||||
{ value: -12, label: "Presque impossible (-12)" },
|
||||
]
|
||||
|
||||
const HARVEST_SIDE_EFFECTS = {
|
||||
1: "La main du personnage tremble plus ou moins violemment.",
|
||||
2: "Le personnage n'arrive à trouver ni repos ni sommeil.",
|
||||
@@ -250,6 +264,8 @@ const PRESET_ACTIONS = {
|
||||
}
|
||||
|
||||
export class LesOubliesRolls {
|
||||
static #actorLocks = new Map()
|
||||
|
||||
static async openTestDialog(actor, preset = {}) {
|
||||
const data = await this.#promptTestOptions(actor, preset)
|
||||
if (!data || typeof data !== "object") return null
|
||||
@@ -284,7 +300,18 @@ export class LesOubliesRolls {
|
||||
static async openConfrontationDialog(actor, preset = {}) {
|
||||
const data = await this.#promptConfrontationOptions(actor, preset)
|
||||
if (!data || typeof data !== "object") return null
|
||||
return this.#createConfrontationMessage(actor, data, preset.actionData ?? null)
|
||||
const defenderActor = this.#resolveDialogTargetActor(data.defenderActorId, preset.targetActor ?? this.#getTargetActor())
|
||||
return this.#createConfrontationMessage(actor, {
|
||||
...data,
|
||||
defenderLabel: defenderActor?.name ?? data.defenderLabel,
|
||||
defenderScore: defenderActor
|
||||
? this.#getSkillScoreWithAlternatives(defenderActor, data.defenderSkill)
|
||||
: Number(data.defenderScore ?? 0),
|
||||
defenderSongesValue: defenderActor ? Number(defenderActor.system.songes?.value ?? 0) : Number(data.defenderSongesValue ?? 0),
|
||||
defenderSongesPoints: defenderActor ? Number(defenderActor.system.songes?.points ?? 0) : Number(data.defenderSongesPoints ?? 0),
|
||||
defenderCauchemarValue: defenderActor ? Number(defenderActor.system.cauchemar?.value ?? 0) : Number(data.defenderCauchemarValue ?? 0),
|
||||
defenderCauchemarPoints: defenderActor ? Number(defenderActor.system.cauchemar?.points ?? 0) : Number(data.defenderCauchemarPoints ?? 0),
|
||||
}, preset.actionData ?? null)
|
||||
}
|
||||
|
||||
static async openAttackDialog(actor, { itemId = null, mode = null } = {}) {
|
||||
@@ -297,6 +324,11 @@ export class LesOubliesRolls {
|
||||
targetActor,
|
||||
})
|
||||
if (!data) return null
|
||||
const defenderActor = this.#resolveDialogTargetActor(data.defenderActorId, targetActor)
|
||||
if (!defenderActor) {
|
||||
ui.notifications.info("Aucune cible sélectionnée : choisissez un adversaire avant de lancer une attaque.")
|
||||
return null
|
||||
}
|
||||
|
||||
const modifiers = this.#resolveModifierSelection(data.primeId, data.penaltyId, attackMode === "ranged" ? "rangedAttack" : "meleeAttack")
|
||||
const reactionOptions = this.#getAttackReactionOptions(data.attackerSkill)
|
||||
@@ -311,17 +343,17 @@ export class LesOubliesRolls {
|
||||
modifiers,
|
||||
targetLabel: data.defenderLabel,
|
||||
notes: data.notes?.trim() || "",
|
||||
targetActor,
|
||||
applyToTarget: Boolean(data.applyToTarget && targetActor),
|
||||
targetActor: defenderActor,
|
||||
applyToTarget: Boolean(data.applyToTarget && defenderActor),
|
||||
damageRequest: {
|
||||
actor,
|
||||
weapon,
|
||||
baseDamage: Number(data.baseDamage ?? 0),
|
||||
baseLabel: String(data.baseDamageLabel || weapon?.system?.damage || data.baseDamage || "0"),
|
||||
targetProtection: Number(data.targetProtection ?? 0),
|
||||
targetLabel: String(data.defenderLabel || targetActor?.name || game.i18n.localize("LESOUBLIES.rolls.defender")),
|
||||
targetActor,
|
||||
applyToTarget: Boolean(data.applyToTarget && targetActor),
|
||||
targetLabel: String(data.defenderLabel || defenderActor?.name || game.i18n.localize("LESOUBLIES.rolls.defender")),
|
||||
targetActor: defenderActor,
|
||||
applyToTarget: Boolean(data.applyToTarget && defenderActor),
|
||||
modifiers,
|
||||
},
|
||||
extraContext: {
|
||||
@@ -339,17 +371,17 @@ export class LesOubliesRolls {
|
||||
attackerExtraDie: data.attackerExtraDie,
|
||||
attackerFinalModifier: modifiers.summary.finalModifier,
|
||||
defenderLabel: data.defenderLabel,
|
||||
defenderScore: targetActor
|
||||
? this.#getSkillScoreWithAlternatives(targetActor, data.defenderSkill)
|
||||
defenderScore: defenderActor
|
||||
? this.#getSkillScoreWithAlternatives(defenderActor, data.defenderSkill)
|
||||
: Number(data.defenderScore ?? 0),
|
||||
defenderDifficulty: Number(data.defenderDifficulty ?? 0),
|
||||
defenderRollMode: data.defenderRollMode,
|
||||
defenderExtraDie: data.defenderExtraDie,
|
||||
defenderFinalModifier: modifiers.summary.opponentFinalModifier,
|
||||
defenderSongesValue: targetActor ? Number(targetActor.system.songes?.value ?? 0) : Number(data.defenderSongesValue ?? 0),
|
||||
defenderSongesPoints: targetActor ? Number(targetActor.system.songes?.points ?? 0) : Number(data.defenderSongesPoints ?? 0),
|
||||
defenderCauchemarValue: targetActor ? Number(targetActor.system.cauchemar?.value ?? 0) : Number(data.defenderCauchemarValue ?? 0),
|
||||
defenderCauchemarPoints: targetActor ? Number(targetActor.system.cauchemar?.points ?? 0) : Number(data.defenderCauchemarPoints ?? 0),
|
||||
defenderSongesValue: defenderActor ? Number(defenderActor.system.songes?.value ?? 0) : Number(data.defenderSongesValue ?? 0),
|
||||
defenderSongesPoints: defenderActor ? Number(defenderActor.system.songes?.points ?? 0) : Number(data.defenderSongesPoints ?? 0),
|
||||
defenderCauchemarValue: defenderActor ? Number(defenderActor.system.cauchemar?.value ?? 0) : Number(data.defenderCauchemarValue ?? 0),
|
||||
defenderCauchemarPoints: defenderActor ? Number(defenderActor.system.cauchemar?.points ?? 0) : Number(data.defenderCauchemarPoints ?? 0),
|
||||
}, actionData)
|
||||
}
|
||||
|
||||
@@ -407,33 +439,40 @@ export class LesOubliesRolls {
|
||||
|
||||
const data = await this.#promptSpellOptions(actor, spell)
|
||||
if (!data) return null
|
||||
|
||||
const skill = actor.getCompetenceByKey?.(spell.system.skillKey) ?? null
|
||||
const skillBase = Number(skill?.system?.base ?? 0)
|
||||
if (skillBase < 1) {
|
||||
ui.notifications.warn(`Il faut au moins une base de 1 en ${this.#getSkillLabel(spell.system.skillKey)} pour activer ce sortilège.`)
|
||||
return null
|
||||
}
|
||||
|
||||
const métierMatch = this.#actorMatchesSpellGrant(actor, spell)
|
||||
const surcharge = !métierMatch && data.applyMetierSurcharge
|
||||
const effectiveCost = Number(data.actualCost ?? 0) * (surcharge ? 2 : 1)
|
||||
const paymentMode = String(data.paymentMode || "points")
|
||||
if (paymentMode === "points") {
|
||||
const resource = spell.system.polarity || "songes"
|
||||
if (Number(actor.system?.[resource]?.points ?? 0) < effectiveCost) {
|
||||
ui.notifications.warn(game.i18n.format("LESOUBLIES.rolls.notEnoughResource", {
|
||||
resource: resource === "songes" ? game.i18n.localize("LESOUBLIES.ui.songes") : game.i18n.localize("LESOUBLIES.ui.cauchemar"),
|
||||
actor: actor.name,
|
||||
}))
|
||||
const activation = await this.#withActorLock(`spell:${actor.id}`, async () => {
|
||||
const skill = actor.getCompetenceByKey?.(spell.system.skillKey) ?? null
|
||||
const skillBase = Number(skill?.system?.base ?? 0)
|
||||
if (skillBase < 1) {
|
||||
ui.notifications.warn(`Il faut au moins une base de 1 en ${this.#getSkillLabel(spell.system.skillKey)} pour activer ce sortilège.`)
|
||||
return null
|
||||
}
|
||||
if (effectiveCost > 0) {
|
||||
await actor.update({
|
||||
[`system.${resource}.points`]: Math.max(Number(actor.system?.[resource]?.points ?? 0) - effectiveCost, 0),
|
||||
})
|
||||
|
||||
const métierMatch = this.#actorMatchesSpellGrant(actor, spell)
|
||||
const surcharge = !métierMatch
|
||||
const effectiveCost = Number(data.actualCost ?? 0) * (surcharge ? 2 : 1)
|
||||
const paymentMode = String(data.paymentMode || "points")
|
||||
if (paymentMode === "points") {
|
||||
const resource = spell.system.polarity || "songes"
|
||||
const available = Number(actor.system?.[resource]?.points ?? 0)
|
||||
if (available < effectiveCost) {
|
||||
ui.notifications.warn(game.i18n.format("LESOUBLIES.rolls.notEnoughResourceDetailed", {
|
||||
resource: resource === "songes" ? game.i18n.localize("LESOUBLIES.ui.songes") : game.i18n.localize("LESOUBLIES.ui.cauchemar"),
|
||||
actor: actor.name,
|
||||
required: effectiveCost,
|
||||
available,
|
||||
}))
|
||||
return null
|
||||
}
|
||||
if (effectiveCost > 0) {
|
||||
await actor.update({
|
||||
[`system.${resource}.points`]: Math.max(available - effectiveCost, 0),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { métierMatch, surcharge, effectiveCost, paymentMode }
|
||||
})
|
||||
if (!activation) return null
|
||||
|
||||
const content = await foundry.applications.handlebars.renderTemplate(
|
||||
"systems/fvtt-les-oublies/templates/chat-spell-activation.hbs",
|
||||
@@ -442,14 +481,14 @@ export class LesOubliesRolls {
|
||||
spell,
|
||||
activation: {
|
||||
targetLabel: data.targetLabel?.trim() || "Sans cible précisée",
|
||||
paymentMode,
|
||||
paymentMode: activation.paymentMode,
|
||||
actualCost: Number(data.actualCost ?? 0),
|
||||
effectiveCost,
|
||||
costLabel: paymentMode === "points"
|
||||
? `${effectiveCost} point${effectiveCost > 1 ? "s" : ""} de ${spell.system.polarity === "cauchemar" ? "Cauchemar" : "Songes"}`
|
||||
: `${effectiveCost} fil${effectiveCost > 1 ? "s" : ""} de ${spell.system.polarity === "cauchemar" ? "Cauchemar" : "Songes"}`,
|
||||
métierMatch,
|
||||
surcharge,
|
||||
effectiveCost: activation.effectiveCost,
|
||||
costLabel: activation.paymentMode === "points"
|
||||
? `${activation.effectiveCost} point${activation.effectiveCost > 1 ? "s" : ""} de ${spell.system.polarity === "cauchemar" ? "Cauchemar" : "Songes"}`
|
||||
: `${activation.effectiveCost} fil${activation.effectiveCost > 1 ? "s" : ""} de ${spell.system.polarity === "cauchemar" ? "Cauchemar" : "Songes"}`,
|
||||
métierMatch: activation.métierMatch,
|
||||
surcharge: activation.surcharge,
|
||||
notes: data.notes?.trim() || "",
|
||||
},
|
||||
},
|
||||
@@ -495,6 +534,7 @@ export class LesOubliesRolls {
|
||||
const targetActor = this.#getTargetActor()
|
||||
const data = await this.#promptPresetConfrontationOptions(actor, preset, targetActor)
|
||||
if (!data) return null
|
||||
const defenderActor = this.#resolveDialogTargetActor(data.defenderActorId, targetActor)
|
||||
|
||||
const modifiers = this.#resolveModifierSelection(data.primeId, data.penaltyId, actionKey)
|
||||
const actionData = {
|
||||
@@ -504,8 +544,8 @@ export class LesOubliesRolls {
|
||||
hint: preset.hint,
|
||||
modifiers,
|
||||
notes: data.notes?.trim() || "",
|
||||
targetLabel: data.defenderLabel,
|
||||
targetActor,
|
||||
targetLabel: defenderActor?.name ?? data.defenderLabel,
|
||||
targetActor: defenderActor,
|
||||
applyToTarget: false,
|
||||
outcome: this.#buildPresetOutcome(actionKey, data),
|
||||
}
|
||||
@@ -518,18 +558,18 @@ export class LesOubliesRolls {
|
||||
attackerRollMode: data.attackerRollMode,
|
||||
attackerExtraDie: data.attackerExtraDie,
|
||||
attackerFinalModifier: modifiers.summary.finalModifier,
|
||||
defenderLabel: data.defenderLabel,
|
||||
defenderScore: targetActor
|
||||
? this.#getSkillScoreWithAlternatives(targetActor, preset.defenderSkillKey)
|
||||
defenderLabel: defenderActor?.name ?? data.defenderLabel,
|
||||
defenderScore: defenderActor
|
||||
? this.#getSkillScoreWithAlternatives(defenderActor, preset.defenderSkillKey)
|
||||
: Number(data.defenderScore ?? 0),
|
||||
defenderDifficulty: Number(data.defenderDifficulty ?? 0),
|
||||
defenderRollMode: data.defenderRollMode,
|
||||
defenderExtraDie: data.defenderExtraDie,
|
||||
defenderFinalModifier: modifiers.summary.opponentFinalModifier,
|
||||
defenderSongesValue: targetActor ? Number(targetActor.system.songes?.value ?? 0) : Number(data.defenderSongesValue ?? 0),
|
||||
defenderSongesPoints: targetActor ? Number(targetActor.system.songes?.points ?? 0) : Number(data.defenderSongesPoints ?? 0),
|
||||
defenderCauchemarValue: targetActor ? Number(targetActor.system.cauchemar?.value ?? 0) : Number(data.defenderCauchemarValue ?? 0),
|
||||
defenderCauchemarPoints: targetActor ? Number(targetActor.system.cauchemar?.points ?? 0) : Number(data.defenderCauchemarPoints ?? 0),
|
||||
defenderSongesValue: defenderActor ? Number(defenderActor.system.songes?.value ?? 0) : Number(data.defenderSongesValue ?? 0),
|
||||
defenderSongesPoints: defenderActor ? Number(defenderActor.system.songes?.points ?? 0) : Number(data.defenderSongesPoints ?? 0),
|
||||
defenderCauchemarValue: defenderActor ? Number(defenderActor.system.cauchemar?.value ?? 0) : Number(data.defenderCauchemarValue ?? 0),
|
||||
defenderCauchemarPoints: defenderActor ? Number(defenderActor.system.cauchemar?.points ?? 0) : Number(data.defenderCauchemarPoints ?? 0),
|
||||
}, actionData)
|
||||
}
|
||||
|
||||
@@ -761,17 +801,19 @@ export class LesOubliesRolls {
|
||||
}
|
||||
|
||||
static async #promptTestOptions(actor, preset = {}) {
|
||||
const difficulty = Number(preset.difficulty ?? 0)
|
||||
const content = await foundry.applications.handlebars.renderTemplate(
|
||||
"systems/fvtt-les-oublies/templates/dialog-roll-test.hbs",
|
||||
{
|
||||
actor,
|
||||
rollModes: this.getRollModes(),
|
||||
extraDieModes: this.getExtraDieModes(),
|
||||
difficultyOptions: this.#getDifficultyOptions(TEST_DIFFICULTIES, difficulty),
|
||||
resources: this.#getDialogResources(actor),
|
||||
values: {
|
||||
label: preset.label ?? "",
|
||||
score: Number(preset.score ?? 0),
|
||||
difficulty: Number(preset.difficulty ?? 0),
|
||||
difficulty,
|
||||
rollMode: preset.rollMode ?? this.getDefaultRollMode(actor),
|
||||
extraDie: preset.extraDie ?? "",
|
||||
},
|
||||
@@ -816,10 +858,15 @@ export class LesOubliesRolls {
|
||||
|
||||
static async #promptConfrontationOptions(actor, preset = {}) {
|
||||
const targetActor = preset.targetActor ?? this.#getTargetActor()
|
||||
const defenderSkill = preset.defenderSkill ?? "esquive"
|
||||
const content = await foundry.applications.handlebars.renderTemplate(
|
||||
"systems/fvtt-les-oublies/templates/dialog-roll-confrontation.hbs",
|
||||
{
|
||||
actor,
|
||||
targetActor,
|
||||
targetStatus: this.#getConfrontationTargetStatus(targetActor),
|
||||
targetOptions: this.#getConfrontationTargetOptions(actor, targetActor),
|
||||
defenderSkillOptions: this.#getConfrontationSkillOptions(),
|
||||
rollModes: this.getRollModes(),
|
||||
extraDieModes: this.getExtraDieModes(),
|
||||
defaultRollMode: this.getDefaultRollMode(actor),
|
||||
@@ -837,7 +884,11 @@ export class LesOubliesRolls {
|
||||
attackerRollMode: preset.attackerRollMode ?? this.getDefaultRollMode(actor),
|
||||
attackerExtraDie: preset.attackerExtraDie ?? "",
|
||||
defenderLabel: targetActor?.name ?? preset.defenderLabel ?? game.i18n.localize("LESOUBLIES.rolls.defender"),
|
||||
defenderScore: Number(preset.defenderScore ?? 0),
|
||||
defenderActorId: targetActor?.id ?? "",
|
||||
defenderSkill,
|
||||
defenderScore: targetActor
|
||||
? this.#getSkillScoreWithAlternatives(targetActor, defenderSkill)
|
||||
: Number(preset.defenderScore ?? 0),
|
||||
defenderDifficulty: Number(preset.defenderDifficulty ?? 0),
|
||||
defenderRollMode: preset.defenderRollMode ?? this.getDefaultRollMode(targetActor ?? actor),
|
||||
defenderExtraDie: preset.defenderExtraDie ?? "",
|
||||
@@ -851,6 +902,13 @@ export class LesOubliesRolls {
|
||||
title: game.i18n.localize("LESOUBLIES.rolls.dialogs.confrontationTitle"),
|
||||
},
|
||||
content,
|
||||
render: (_event, dialog) => {
|
||||
this.#bindConfrontationTargetSelection(dialog, {
|
||||
actor,
|
||||
fallbackTargetActor: targetActor,
|
||||
skillFieldName: "defenderSkill",
|
||||
})
|
||||
},
|
||||
buttons: [
|
||||
{
|
||||
action: "roll",
|
||||
@@ -867,6 +925,8 @@ export class LesOubliesRolls {
|
||||
attackerDifficulty: Number(data.attackerDifficulty ?? 0),
|
||||
attackerRollMode: String(data.attackerRollMode || this.getDefaultRollMode(actor)),
|
||||
attackerExtraDie: String(data.attackerExtraDie || ""),
|
||||
defenderActorId: String(data.defenderActorId || ""),
|
||||
defenderSkill: String(data.defenderSkill || defenderSkill),
|
||||
defenderLabel: String(data.defenderLabel || targetActor?.name || game.i18n.localize("LESOUBLIES.rolls.defender")).trim(),
|
||||
defenderScore: Number(data.defenderScore ?? 0),
|
||||
defenderDifficulty: Number(data.defenderDifficulty ?? 0),
|
||||
@@ -892,7 +952,7 @@ export class LesOubliesRolls {
|
||||
const baseDamage = this.#getWeaponBaseDamage(actor, weapon)
|
||||
const baseDamageLabel = weapon?.system?.damage || String(baseDamage)
|
||||
const content = await foundry.applications.handlebars.renderTemplate(
|
||||
"systems/fvtt-les-oublies/templates/dialog-roll-attack.hbs",
|
||||
"systems/fvtt-les-oublies/templates/dialog-roll-attack-v2.hbs",
|
||||
{
|
||||
actor,
|
||||
weapon,
|
||||
@@ -905,6 +965,10 @@ export class LesOubliesRolls {
|
||||
cauchemarPoints: 0,
|
||||
},
|
||||
targetActor,
|
||||
targetStatus: this.#getConfrontationTargetStatus(targetActor, { requireTarget: true }),
|
||||
targetOptions: this.#getConfrontationTargetOptions(actor, targetActor).map((entry, index) => (
|
||||
index === 0 ? { ...entry, label: "— Sélectionner un adversaire —" } : entry
|
||||
)),
|
||||
rollModes: this.getRollModes(),
|
||||
extraDieModes: this.getExtraDieModes(),
|
||||
attackSkills: this.#getAttackSkillOptions(attackMode),
|
||||
@@ -917,6 +981,7 @@ export class LesOubliesRolls {
|
||||
attackerRollMode: this.getDefaultRollMode(actor),
|
||||
attackerExtraDie: "",
|
||||
defenderLabel: targetActor?.name ?? game.i18n.localize("LESOUBLIES.rolls.defender"),
|
||||
defenderActorId: targetActor?.id ?? "",
|
||||
defenderSkill: attackMode === "ranged" ? "esquive" : "esquive",
|
||||
defenderScore: 0,
|
||||
defenderDifficulty: 0,
|
||||
@@ -940,6 +1005,14 @@ export class LesOubliesRolls {
|
||||
title: attackMode === "ranged" ? "Attaque à distance" : "Attaque de mêlée",
|
||||
},
|
||||
content,
|
||||
render: (_event, dialog) => {
|
||||
this.#bindConfrontationTargetSelection(dialog, {
|
||||
actor,
|
||||
fallbackTargetActor: targetActor,
|
||||
skillFieldName: "defenderSkill",
|
||||
requireTarget: true,
|
||||
})
|
||||
},
|
||||
buttons: [
|
||||
{
|
||||
action: "roll",
|
||||
@@ -949,12 +1022,19 @@ export class LesOubliesRolls {
|
||||
const form = this.#getDialogElement(dialog)?.querySelector("form")
|
||||
if (!form) return null
|
||||
const data = this.#formToObject(form)
|
||||
const defenderActor = this.#resolveDialogTargetActor(data.defenderActorId, targetActor)
|
||||
if (!defenderActor) {
|
||||
ui.notifications.info("Aucune cible sélectionnée : choisissez un adversaire avant de lancer une attaque.")
|
||||
dialog.close()
|
||||
return null
|
||||
}
|
||||
const difficultyPreset = Number(data.difficultyPreset ?? 0)
|
||||
const customDifficulty = Number(data.customDifficulty ?? 0)
|
||||
return {
|
||||
attackerSkill: String(data.attackerSkill || (attackMode === "ranged" ? "tir" : "melee")),
|
||||
attackerRollMode: String(data.attackerRollMode || this.getDefaultRollMode(actor)),
|
||||
attackerExtraDie: String(data.attackerExtraDie || ""),
|
||||
defenderActorId: String(data.defenderActorId || ""),
|
||||
defenderLabel: String(data.defenderLabel || targetActor?.name || game.i18n.localize("LESOUBLIES.rolls.defender")).trim(),
|
||||
defenderSkill: String(data.defenderSkill || "esquive"),
|
||||
defenderScore: Number(data.defenderScore ?? 0),
|
||||
@@ -1044,17 +1124,22 @@ export class LesOubliesRolls {
|
||||
}
|
||||
|
||||
static async #promptSpellOptions(actor, spell) {
|
||||
const isMetierMatch = this.#actorMatchesSpellGrant(actor, spell)
|
||||
const effectiveCost = Number(spell.system.cost ?? 0) * (isMetierMatch ? 1 : 2)
|
||||
const polarityLabel = spell.system.polarity === "cauchemar"
|
||||
? game.i18n.localize("LESOUBLIES.ui.cauchemar")
|
||||
: game.i18n.localize("LESOUBLIES.ui.songes")
|
||||
const content = await foundry.applications.handlebars.renderTemplate(
|
||||
"systems/fvtt-les-oublies/templates/dialog-spell-activation.hbs",
|
||||
{
|
||||
actor,
|
||||
spell,
|
||||
resources: this.#getDialogResources(actor),
|
||||
isMetierMatch: this.#actorMatchesSpellGrant(actor, spell),
|
||||
isMetierMatch,
|
||||
effectiveCostLabel: `${effectiveCost} point${effectiveCost > 1 ? "s" : ""} de ${polarityLabel}`,
|
||||
values: {
|
||||
actualCost: Number(spell.system.cost ?? 0),
|
||||
paymentMode: "points",
|
||||
applyMetierSurcharge: true,
|
||||
targetLabel: "",
|
||||
notes: "",
|
||||
},
|
||||
@@ -1078,7 +1163,6 @@ export class LesOubliesRolls {
|
||||
return {
|
||||
actualCost: Number(data.actualCost ?? spell.system.cost ?? 0),
|
||||
paymentMode: String(data.paymentMode || "points"),
|
||||
applyMetierSurcharge: data.applyMetierSurcharge === "on",
|
||||
targetLabel: String(data.targetLabel || ""),
|
||||
notes: String(data.notes || ""),
|
||||
}
|
||||
@@ -1164,6 +1248,9 @@ export class LesOubliesRolls {
|
||||
title: preset.title,
|
||||
hint: preset.hint,
|
||||
targetActor,
|
||||
targetStatus: this.#getConfrontationTargetStatus(targetActor),
|
||||
defenderSkillLabel: this.#getSkillLabel(preset.defenderSkillKey),
|
||||
targetOptions: this.#getConfrontationTargetOptions(actor, targetActor),
|
||||
rollModes: this.getRollModes(),
|
||||
extraDieModes: this.getExtraDieModes(),
|
||||
attackerResources: this.#getDialogResources(actor),
|
||||
@@ -1178,12 +1265,15 @@ export class LesOubliesRolls {
|
||||
values: {
|
||||
attackerDifficulty: Number(preset.difficulty ?? 0),
|
||||
defenderLabel: targetActor?.name ?? game.i18n.localize("LESOUBLIES.rolls.defender"),
|
||||
defenderActorId: targetActor?.id ?? "",
|
||||
defenderDifficulty: 0,
|
||||
attackerRollMode: this.getDefaultRollMode(actor),
|
||||
attackerExtraDie: "",
|
||||
defenderRollMode: this.getDefaultRollMode(targetActor ?? actor),
|
||||
defenderExtraDie: "",
|
||||
defenderScore: 0,
|
||||
defenderScore: targetActor
|
||||
? this.#getSkillScoreWithAlternatives(targetActor, preset.defenderSkillKey)
|
||||
: 0,
|
||||
primeId: "none",
|
||||
penaltyId: "none",
|
||||
outcomeChoice: "",
|
||||
@@ -1201,6 +1291,13 @@ export class LesOubliesRolls {
|
||||
title: preset.title,
|
||||
},
|
||||
content,
|
||||
render: (_event, dialog) => {
|
||||
this.#bindConfrontationTargetSelection(dialog, {
|
||||
actor,
|
||||
fallbackTargetActor: targetActor,
|
||||
skillKey: preset.defenderSkillKey,
|
||||
})
|
||||
},
|
||||
buttons: [
|
||||
{
|
||||
action: "roll",
|
||||
@@ -1212,6 +1309,7 @@ export class LesOubliesRolls {
|
||||
const data = this.#formToObject(form)
|
||||
return {
|
||||
attackerDifficulty: Number(data.attackerDifficulty ?? preset.difficulty ?? 0),
|
||||
defenderActorId: String(data.defenderActorId || ""),
|
||||
defenderLabel: String(data.defenderLabel || targetActor?.name || game.i18n.localize("LESOUBLIES.rolls.defender")).trim(),
|
||||
defenderDifficulty: Number(data.defenderDifficulty ?? 0),
|
||||
attackerRollMode: String(data.attackerRollMode || this.getDefaultRollMode(actor)),
|
||||
@@ -1498,7 +1596,7 @@ export class LesOubliesRolls {
|
||||
|
||||
static #getWeaponAttackMode(weapon) {
|
||||
const category = String(weapon?.system?.category || "").toLowerCase()
|
||||
if (["distance", "ranged", "tir", "projectile"].some((keyword) => category.includes(keyword))) return "ranged"
|
||||
if (["distance", "ranged", "tir", "projectile", "jet"].some((keyword) => category.includes(keyword))) return "ranged"
|
||||
return "melee"
|
||||
}
|
||||
|
||||
@@ -1528,6 +1626,166 @@ export class LesOubliesRolls {
|
||||
]
|
||||
}
|
||||
|
||||
static #getConfrontationTargetOptions(actor, selectedActor = null) {
|
||||
const choices = LesOubliesUtility.sortByName(
|
||||
game.actors.filter((candidate) => (
|
||||
["creature", "personnage"].includes(candidate.type)
|
||||
&& candidate.id !== actor?.id
|
||||
)),
|
||||
).map((candidate) => ({
|
||||
value: candidate.id,
|
||||
label: `${candidate.name} — ${game.i18n.localize(`TYPES.Actor.${candidate.type}`)}`,
|
||||
}))
|
||||
|
||||
return [
|
||||
{ value: "", label: "Saisie manuelle" },
|
||||
...LesOubliesUtility.ensureChoice(
|
||||
choices,
|
||||
selectedActor?.id,
|
||||
selectedActor ? `${selectedActor.name} — ${game.i18n.localize(`TYPES.Actor.${selectedActor.type}`)}` : null,
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
static #getConfrontationSkillOptions() {
|
||||
const skills = CONFIG.LESOUBLIES?.config?.skills ?? CONFIG.LESOUBLIES?.skills ?? {}
|
||||
return Object.entries(skills)
|
||||
.map(([value, data]) => ({
|
||||
value,
|
||||
label: data.label ?? value,
|
||||
}))
|
||||
.sort((left, right) => left.label.localeCompare(right.label, "fr"))
|
||||
}
|
||||
|
||||
static #resolveDialogTargetActor(actorId, fallbackTargetActor = null) {
|
||||
if (actorId !== undefined && actorId !== null && actorId !== "") {
|
||||
return game.actors.get(String(actorId)) ?? null
|
||||
}
|
||||
if (actorId === "") return null
|
||||
return fallbackTargetActor ?? null
|
||||
}
|
||||
|
||||
static #getConfrontationTargetStatus(targetActor = null, { requireTarget = false } = {}) {
|
||||
if (!targetActor) {
|
||||
return {
|
||||
message: requireTarget
|
||||
? "Aucune cible n'est actuellement sélectionnée. Sélectionnez un adversaire dans la liste ci-dessous pour lancer l'attaque."
|
||||
: "Aucune cible n'est actuellement sélectionnée. Choisissez un adversaire dans la liste ci-dessous ou conservez la saisie manuelle.",
|
||||
state: "empty",
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
message: `Adversaire sélectionné : ${targetActor.name}. Ses valeurs de confrontation sont utilisées automatiquement.`,
|
||||
state: "selected",
|
||||
}
|
||||
}
|
||||
|
||||
static #bindConfrontationTargetSelection(dialog, {
|
||||
actor,
|
||||
fallbackTargetActor = null,
|
||||
skillFieldName = null,
|
||||
skillKey = null,
|
||||
requireTarget = false,
|
||||
} = {}) {
|
||||
const root = this.#getDialogElement(dialog)
|
||||
const form = root?.querySelector("form")
|
||||
if (!form) return
|
||||
|
||||
const actorField = form.elements.namedItem("defenderActorId")
|
||||
if (!(actorField instanceof HTMLSelectElement)) return
|
||||
|
||||
const labelField = form.elements.namedItem("defenderLabel")
|
||||
const scoreField = form.elements.namedItem("defenderScore")
|
||||
const rollModeField = form.elements.namedItem("defenderRollMode")
|
||||
const songesValueField = form.elements.namedItem("defenderSongesValue")
|
||||
const songesPointsField = form.elements.namedItem("defenderSongesPoints")
|
||||
const cauchemarValueField = form.elements.namedItem("defenderCauchemarValue")
|
||||
const cauchemarPointsField = form.elements.namedItem("defenderCauchemarPoints")
|
||||
const skillField = skillFieldName ? form.elements.namedItem(skillFieldName) : null
|
||||
const targetStatusField = root.querySelector("[data-target-status]")
|
||||
|
||||
const defaultLabel = game.i18n.localize("LESOUBLIES.rolls.defender")
|
||||
const getSelectedSkill = () => {
|
||||
if (skillKey) return skillKey
|
||||
if (skillField instanceof HTMLSelectElement) return String(skillField.value || "melee")
|
||||
return "melee"
|
||||
}
|
||||
|
||||
const updateTargetFields = ({ preserveRollMode = false } = {}) => {
|
||||
const targetActor = this.#resolveDialogTargetActor(actorField.value, fallbackTargetActor)
|
||||
const hasActor = Boolean(targetActor)
|
||||
const currentSkill = getSelectedSkill()
|
||||
|
||||
if (targetStatusField instanceof HTMLElement) {
|
||||
const targetStatus = this.#getConfrontationTargetStatus(targetActor, { requireTarget })
|
||||
targetStatusField.textContent = targetStatus.message
|
||||
targetStatusField.dataset.state = targetStatus.state
|
||||
}
|
||||
|
||||
if (labelField instanceof HTMLInputElement) {
|
||||
labelField.value = hasActor ? targetActor.name : (labelField.value || defaultLabel)
|
||||
labelField.readOnly = hasActor
|
||||
}
|
||||
|
||||
if (scoreField instanceof HTMLInputElement) {
|
||||
if (hasActor) {
|
||||
scoreField.value = String(this.#getSkillScoreWithAlternatives(targetActor, currentSkill))
|
||||
}
|
||||
scoreField.readOnly = hasActor
|
||||
}
|
||||
|
||||
if (rollModeField instanceof HTMLSelectElement && hasActor && !preserveRollMode) {
|
||||
rollModeField.value = this.getDefaultRollMode(targetActor)
|
||||
}
|
||||
|
||||
const resourceValues = hasActor
|
||||
? {
|
||||
songesValue: Number(targetActor.system.songes?.value ?? 0),
|
||||
songesPoints: Number(targetActor.system.songes?.points ?? 0),
|
||||
cauchemarValue: Number(targetActor.system.cauchemar?.value ?? 0),
|
||||
cauchemarPoints: Number(targetActor.system.cauchemar?.points ?? 0),
|
||||
}
|
||||
: null
|
||||
|
||||
const bindNumericField = (field, value) => {
|
||||
if (!(field instanceof HTMLInputElement)) return
|
||||
if (resourceValues) field.value = String(value)
|
||||
field.readOnly = hasActor
|
||||
}
|
||||
|
||||
bindNumericField(songesValueField, resourceValues?.songesValue ?? 0)
|
||||
bindNumericField(songesPointsField, resourceValues?.songesPoints ?? 0)
|
||||
bindNumericField(cauchemarValueField, resourceValues?.cauchemarValue ?? 0)
|
||||
bindNumericField(cauchemarPointsField, resourceValues?.cauchemarPoints ?? 0)
|
||||
}
|
||||
|
||||
actorField.addEventListener("change", () => updateTargetFields())
|
||||
if (skillField instanceof HTMLSelectElement) {
|
||||
skillField.addEventListener("change", () => updateTargetFields({ preserveRollMode: true }))
|
||||
}
|
||||
updateTargetFields()
|
||||
}
|
||||
|
||||
static async #withActorLock(lockKey, callback) {
|
||||
const previous = this.#actorLocks.get(lockKey) ?? Promise.resolve()
|
||||
let release
|
||||
const current = new Promise((resolve) => {
|
||||
release = resolve
|
||||
})
|
||||
const queued = previous.finally(() => current)
|
||||
this.#actorLocks.set(lockKey, queued)
|
||||
await previous
|
||||
try {
|
||||
return await callback()
|
||||
} finally {
|
||||
release()
|
||||
if (this.#actorLocks.get(lockKey) === queued) {
|
||||
this.#actorLocks.delete(lockKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static #getModifierOptions(type, actionType) {
|
||||
const source = type === "prime" ? PRIME_DEFINITIONS : PENALTY_DEFINITIONS
|
||||
return source
|
||||
@@ -1677,6 +1935,21 @@ export class LesOubliesRolls {
|
||||
return parts.length ? parts.join(" ") : "0"
|
||||
}
|
||||
|
||||
static #getDifficultyOptions(options, selectedValue = 0) {
|
||||
const normalizedValue = Number(selectedValue ?? 0)
|
||||
const entries = options.map((entry) => ({
|
||||
value: Number(entry.value ?? 0),
|
||||
label: entry.label,
|
||||
}))
|
||||
if (!entries.some((entry) => entry.value === normalizedValue)) {
|
||||
entries.push({
|
||||
value: normalizedValue,
|
||||
label: `Personnalisée (${normalizedValue > 0 ? "+" : ""}${normalizedValue})`,
|
||||
})
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
static #getConfrontationOutcome(attacker, defender) {
|
||||
const attackerSuccess = attacker.success
|
||||
const defenderSuccess = defender.success
|
||||
|
||||
Reference in New Issue
Block a user