DIvers rework de CSS/LESS et améliorations de messages/layout

This commit is contained in:
2026-05-03 20:20:30 +02:00
parent 4f8735f86f
commit 267f992874
113 changed files with 11565 additions and 843 deletions
+333 -60
View File
@@ -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