648 lines
27 KiB
JavaScript
648 lines
27 KiB
JavaScript
/**
|
||
* Célestopol 1922 — Système FoundryVTT
|
||
*
|
||
* Célestopol 1922 est un jeu de rôle édité par Antre-Monde Éditions.
|
||
* Ce système FoundryVTT est une implémentation indépendante et n'est pas
|
||
* affilié à Antre-Monde Éditions,
|
||
* mais a été réalisé avec l'autorisation d'Antre-Monde Éditions.
|
||
*
|
||
* @author LeRatierBretonnien
|
||
* @copyright 2025–2026 LeRatierBretonnien
|
||
* @license CC BY-NC-SA 4.0 – https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||
*/
|
||
|
||
import { SYSTEM } from "../config/system.mjs"
|
||
|
||
/** Construit la formule de jet à partir du nombre de dés et du modificateur total. */
|
||
function buildFormula(nbDice, totalModifier) {
|
||
if (totalModifier > 0) return `${nbDice}d8 + ${totalModifier}`
|
||
if (totalModifier < 0) return `${nbDice}d8 - ${Math.abs(totalModifier)}`
|
||
return `${nbDice}d8`
|
||
}
|
||
|
||
/**
|
||
* Système de dés de Célestopol 1922.
|
||
*
|
||
* Formule de base : 2d8 + Spécialisation + modificateurs (aspect, blessures, manual)
|
||
* Destin (jauge pleine à 8) : 3d8 + modificateurs, puis reset de la jauge à 0
|
||
* Dé de la Lune (optionnel) : 1d8 séparé → résultat narratif (Triomphe/Brio/Contrecoup/Catastrophe)
|
||
*/
|
||
export class CelestopolRoll extends Roll {
|
||
static CHAT_TEMPLATE = "systems/fvtt-celestopol/templates/chat-message.hbs"
|
||
|
||
get resultType() { return this.options.resultType }
|
||
get isSuccess() { return this.resultType === "success" || this.resultType === "critical-success" }
|
||
get isFailure() { return this.resultType === "failure" || this.resultType === "critical-failure" }
|
||
get isTie() { return this.resultType === "tie" }
|
||
get isCriticalSuccess(){ return this.resultType === "critical-success" }
|
||
get isCriticalFailure(){ return this.resultType === "critical-failure" }
|
||
get actorId() { return this.options.actorId }
|
||
get actorName() { return this.options.actorName }
|
||
get actorImage() { return this.options.actorImage }
|
||
get skillLabel() { return this.options.skillLabel }
|
||
get difficulty() { return this.options.difficulty }
|
||
|
||
/**
|
||
* Convertit le niveau de dégâts d'une arme en nombre de blessures de base.
|
||
* Règle : une attaque réussie inflige toujours 1 blessure, plus le bonus de dégâts.
|
||
* @param {string|number|null} weaponDegats
|
||
* @returns {number|null}
|
||
*/
|
||
static getIncomingWounds(weaponDegats) {
|
||
const raw = `${weaponDegats ?? "0"}`
|
||
const bonus = Number.parseInt(raw, 10)
|
||
if (!Number.isFinite(bonus)) return null
|
||
return Math.max(0, 1 + bonus)
|
||
}
|
||
|
||
/**
|
||
* Retourne la protection totale de l'armure équipée pour un acteur.
|
||
* @param {Actor|null} actor
|
||
* @returns {number}
|
||
*/
|
||
static getActorArmorProtection(actor) {
|
||
if (!actor) return 0
|
||
|
||
if (typeof actor.system?.getArmorMalus === "function") {
|
||
return Math.abs(actor.system.getArmorMalus())
|
||
}
|
||
|
||
const derivedArmorMalus = actor.system?.armorMalus
|
||
if (Number.isFinite(derivedArmorMalus)) {
|
||
return Math.abs(derivedArmorMalus)
|
||
}
|
||
|
||
const armures = actor.itemTypes?.armure ?? []
|
||
return armures
|
||
.filter(a => a.system.equipped)
|
||
.reduce((sum, a) => sum + Math.abs(a.system.protection ?? a.system.malus ?? 0), 0)
|
||
}
|
||
|
||
/**
|
||
* Ouvre le dialogue de configuration du jet via DialogV2 et exécute le jet.
|
||
* @param {object} options
|
||
* @returns {Promise<CelestopolRoll|null>}
|
||
*/
|
||
static async prompt(options = {}) {
|
||
const woundMalus = options.woundMalus ?? 0
|
||
const armorMalus = options.armorMalus ?? 0
|
||
const skillValue = options.skillValue ?? 0
|
||
const woundLevelId = options.woundLevel ?? 0
|
||
const destGaugeFull = options.destGaugeFull ?? false
|
||
const fortuneValue = options.fortuneValue ?? 0
|
||
const isResistance = options.isResistance ?? false
|
||
const isCombat = options.isCombat ?? false
|
||
const isRangedDefense = options.isRangedDefense ?? false
|
||
const weaponType = options.weaponType ?? "melee"
|
||
const weaponName = options.weaponName ?? null
|
||
const weaponDegats = options.weaponDegats ?? "0"
|
||
const availableTargets = options.availableTargets ?? []
|
||
const isRangedAttack = isCombat && !isRangedDefense && weaponType === "distance"
|
||
const woundLabel = woundLevelId > 0
|
||
? game.i18n.localize(SYSTEM.WOUND_LEVELS[woundLevelId]?.label ?? "")
|
||
: null
|
||
|
||
const modifierChoices = SYSTEM.CONTEXT_MODIFIER_CHOICES.map(c => ({
|
||
id: c.id,
|
||
value: c.value,
|
||
label: game.i18n.localize(c.label),
|
||
}))
|
||
const aspectChoices = [-4, -3, -2, -1, 0, 1, 2, 3, 4].map(v => ({
|
||
value: v,
|
||
label: v > 0 ? `+${v}` : `${v}`,
|
||
}))
|
||
const situationChoices = Array.from({ length: 17 }, (_, i) => {
|
||
const v = i - 8
|
||
return { value: v, label: v > 0 ? `+${v}` : `${v}` }
|
||
})
|
||
const rangedModChoices = SYSTEM.RANGED_MODIFIERS.map(m => ({
|
||
id: m.id,
|
||
value: m.value,
|
||
label: game.i18n.localize(m.label),
|
||
}))
|
||
const factionAspectChoices = game.celestopol?.getFactionAspectSummary(options.actorId ? game.actors.get(options.actorId) : null)
|
||
?.availableAspectChoices ?? []
|
||
|
||
const dialogContext = {
|
||
actorName: options.actorName,
|
||
statLabel: options.statLabel,
|
||
skillLabel: options.skillLabel,
|
||
skillValue,
|
||
woundMalus,
|
||
woundLabel,
|
||
isCombat,
|
||
isRangedDefense,
|
||
isRangedAttack,
|
||
weaponType,
|
||
weaponName,
|
||
weaponDegats,
|
||
modifierChoices,
|
||
aspectChoices,
|
||
situationChoices,
|
||
rangedModChoices,
|
||
factionAspectChoices,
|
||
availableTargets,
|
||
fortuneValue,
|
||
armorMalus,
|
||
destGaugeFull,
|
||
defaultRollMoonDie: options.rollMoonDie ?? false,
|
||
isResistance,
|
||
}
|
||
|
||
const content = await foundry.applications.handlebars.renderTemplate(
|
||
"systems/fvtt-celestopol/templates/roll-dialog.hbs",
|
||
dialogContext
|
||
)
|
||
|
||
const skillLocalized = game.i18n.localize(options.skillLabel ?? "")
|
||
const statLocalized = options.statLabel ? game.i18n.localize(options.statLabel) : null
|
||
const title = statLocalized ? `${statLocalized} › ${skillLocalized}` : skillLocalized
|
||
|
||
const rollContext = await foundry.applications.api.DialogV2.wait({
|
||
window: { title },
|
||
classes: ["fvtt-celestopol", "roll-dialog"],
|
||
content,
|
||
render: (_event, dialog) => {
|
||
const wrap = dialog.element.querySelector('.roll-dialog-content')
|
||
if (!wrap) return
|
||
|
||
// Sélection de cible PNJ : masque le champ Corps PNJ (valeur cachée)
|
||
const targetSelect = wrap.querySelector('#targetSelect')
|
||
const corpsPnjRow = wrap.querySelector('#corps-pnj-row')
|
||
const targetConfirmedRow = wrap.querySelector('#target-confirmed-row')
|
||
const targetConfirmedName = wrap.querySelector('#target-confirmed-name')
|
||
|
||
function applyTargetSelection() {
|
||
if (!targetSelect) return
|
||
const selectedOption = targetSelect.options[targetSelect.selectedIndex]
|
||
const val = parseFloat(selectedOption?.dataset.corps ?? "")
|
||
const corpsPnjInput = wrap.querySelector('#corpsPnj')
|
||
if (targetSelect.value && !isNaN(val)) {
|
||
// Cible sélectionnée : masquer la valeur, afficher le nom
|
||
if (corpsPnjRow) corpsPnjRow.style.display = 'none'
|
||
if (targetConfirmedRow) targetConfirmedRow.style.display = ''
|
||
if (targetConfirmedName) targetConfirmedName.textContent = selectedOption?.text ?? ''
|
||
if (corpsPnjInput) {
|
||
corpsPnjInput.value = val
|
||
corpsPnjInput.dispatchEvent(new Event('input'))
|
||
}
|
||
} else {
|
||
// Saisie manuelle
|
||
if (corpsPnjRow) corpsPnjRow.style.display = ''
|
||
if (targetConfirmedRow) targetConfirmedRow.style.display = 'none'
|
||
}
|
||
}
|
||
|
||
if (targetSelect) {
|
||
targetSelect.addEventListener('change', applyTargetSelection)
|
||
applyTargetSelection()
|
||
}
|
||
|
||
function hasMalus(mod, asp, sit, ranged) {
|
||
return woundMalus < 0 || armorMalus < 0 || mod < 0 || asp < 0 || sit < 0 || ranged < 0
|
||
}
|
||
|
||
function update() {
|
||
const rawMod = wrap.querySelector('#modifier')?.value
|
||
const autoSucc = rawMod === "auto"
|
||
const modifier = autoSucc ? 0 : (parseInt(rawMod ?? 0) || 0)
|
||
const aspectMod = parseInt(wrap.querySelector('#aspectModifier')?.value ?? 0) || 0
|
||
const selectedFactionAspect = wrap.querySelector('#factionAspectId')?.selectedOptions?.[0]
|
||
const factionAspectBonus = parseInt(selectedFactionAspect?.dataset.value ?? 0) || 0
|
||
const situMod = parseInt(wrap.querySelector('#situationMod')?.value ?? 0) || 0
|
||
const rangedMod = parseInt(wrap.querySelector('#rangedMod')?.value ?? 0) || 0
|
||
const useDestin = wrap.querySelector('#useDestin')?.checked
|
||
const useFort = wrap.querySelector('#useFortune')?.checked
|
||
const puiser = wrap.querySelector('#puiserRessources')?.checked
|
||
const ndice = useDestin ? 3 : 2
|
||
|
||
// En résistance : pas de "Puiser" possible
|
||
const puiserRow = wrap.querySelector('#puiser-row')
|
||
if (puiserRow) {
|
||
if (!isResistance && hasMalus(modifier, aspectMod, situMod, rangedMod)) {
|
||
puiserRow.style.display = ''
|
||
} else {
|
||
puiserRow.style.display = 'none'
|
||
const cb = wrap.querySelector('#puiserRessources')
|
||
if (cb) cb.checked = false
|
||
}
|
||
}
|
||
|
||
const effWound = puiser ? 0 : woundMalus
|
||
const effMod = puiser ? Math.max(0, modifier) : modifier
|
||
const effAspect = puiser ? Math.max(0, aspectMod) : aspectMod
|
||
const effSit = puiser ? Math.max(0, situMod) : situMod
|
||
const effArmor = puiser ? 0 : armorMalus
|
||
const effRanged = puiser ? Math.max(0, rangedMod) : rangedMod
|
||
const totalMod = skillValue + effWound + effMod + effAspect + factionAspectBonus + effSit + effArmor + effRanged
|
||
|
||
let formula
|
||
if (autoSucc) {
|
||
formula = game.i18n.localize("CELESTOPOL.Roll.autoSuccess")
|
||
} else if (useFort) {
|
||
const fm = totalMod + 8
|
||
formula = `1d8` + (fm > 0 ? ` + ${fm}` : fm < 0 ? ` − ${Math.abs(fm)}` : ``)
|
||
} else {
|
||
formula = `${ndice}d8`
|
||
if (totalMod > 0) formula += ` + ${totalMod}`
|
||
if (totalMod < 0) formula += ` − ${Math.abs(totalMod)}`
|
||
}
|
||
|
||
const previewEl = wrap.querySelector('#preview-formula')
|
||
if (previewEl) previewEl.textContent = formula
|
||
}
|
||
|
||
wrap.querySelectorAll('#modifier, #aspectModifier, #factionAspectId, #situationMod, #rangedMod, #useDestin, #useFortune, #puiserRessources, #corpsPnj')
|
||
.forEach(el => {
|
||
el.addEventListener('change', update)
|
||
el.addEventListener('input', update)
|
||
})
|
||
update()
|
||
},
|
||
buttons: [
|
||
{
|
||
label: game.i18n.localize("CELESTOPOL.Roll.roll"),
|
||
icon: "fa-solid fa-dice",
|
||
callback: (event, button) => {
|
||
return Array.from(button.form.elements).reduce((obj, input) => {
|
||
if (input.name) {
|
||
obj[input.name] = input.type === "checkbox" ? input.checked : input.value
|
||
}
|
||
return obj
|
||
}, {})
|
||
},
|
||
},
|
||
],
|
||
rejectClose: false,
|
||
})
|
||
|
||
if (!rollContext) return null
|
||
|
||
// En combat : Corps PNJ = seuil direct ; sinon seuil fixe = 11
|
||
const corpsPnj = isCombat ? (parseInt(rollContext.corpsPnj ?? 7) || 7) : null
|
||
const difficulty = isCombat ? "combat" : "standard"
|
||
const diffConfig = isCombat
|
||
? { value: corpsPnj, label: "CELESTOPOL.Combat.corpsPnj" }
|
||
: { value: 11, label: "CELESTOPOL.Roll.threshold" }
|
||
const autoSuccess = rollContext.modifier === "auto"
|
||
const modifier = autoSuccess ? 0 : (parseInt(rollContext.modifier ?? 0) || 0)
|
||
const aspectMod = parseInt(rollContext.aspectModifier ?? 0) || 0
|
||
const factionAspectId = typeof rollContext.factionAspectId === "string" ? rollContext.factionAspectId : ""
|
||
const selectedFactionAspect = factionAspectChoices.find(choice => choice.id === factionAspectId) ?? null
|
||
const factionAspectBonus = selectedFactionAspect?.value ?? 0
|
||
const factionAspectLabel = selectedFactionAspect?.label ?? ""
|
||
const situationMod = parseInt(rollContext.situationMod ?? 0) || 0
|
||
const rangedMod = isRangedAttack ? (parseInt(rollContext.rangedMod ?? 0) || 0) : 0
|
||
const isOpposition = !isCombat && (rollContext.isOpposition === true || rollContext.isOpposition === "true")
|
||
const useDestin = destGaugeFull && (rollContext.useDestin === true || rollContext.useDestin === "true")
|
||
const useFortune = fortuneValue > 0 && (rollContext.useFortune === true || rollContext.useFortune === "true")
|
||
const puiserRessources = rollContext.puiserRessources === true || rollContext.puiserRessources === "true"
|
||
const rollMoonDie = rollContext.rollMoonDie === true || rollContext.rollMoonDie === "true"
|
||
const selectedCombatTargetId = typeof rollContext.targetSelect === "string" ? rollContext.targetSelect : ""
|
||
const selectedCombatTarget = selectedCombatTargetId
|
||
? availableTargets.find(t => t.id === selectedCombatTargetId) ?? null
|
||
: null
|
||
const targetActorId = selectedCombatTarget?.id || ""
|
||
const targetActorName = selectedCombatTarget?.name || ""
|
||
|
||
// En résistance : forcer puiser=false, lune=false, fortune=false, destin=false
|
||
const effectivePuiser = isResistance ? false : puiserRessources
|
||
const effectiveMoon = isResistance ? false : rollMoonDie
|
||
|
||
// Puiser dans ses ressources → ignorer tous les malus
|
||
const effectiveWoundMalus = effectivePuiser ? 0 : woundMalus
|
||
const effectiveArmorMalus = effectivePuiser ? 0 : armorMalus
|
||
const effectiveModifier = effectivePuiser ? Math.max(0, modifier) : modifier
|
||
const effectiveAspectMod = effectivePuiser ? Math.max(0, aspectMod) : aspectMod
|
||
const effectiveSituationMod = effectivePuiser ? Math.max(0, situationMod) : situationMod
|
||
const effectiveRangedMod = effectivePuiser ? Math.max(0, rangedMod) : rangedMod
|
||
|
||
// Fortune : 1d8 + 8 ; Destin : 3d8 ; sinon : 2d8
|
||
const nbDice = (!isResistance && useDestin) ? 3 : 2
|
||
const totalModifier = skillValue + effectiveWoundMalus + effectiveAspectMod + effectiveModifier + factionAspectBonus + effectiveSituationMod + effectiveArmorMalus + effectiveRangedMod
|
||
const formula = (!isResistance && useFortune)
|
||
? buildFormula(1, totalModifier + 8)
|
||
: buildFormula(nbDice, totalModifier)
|
||
|
||
// Jet du dé de lune séparé (narratif) — pas en résistance
|
||
let moonDieResult = null
|
||
let moonFace = null
|
||
let moonResultType = null
|
||
if (effectiveMoon) {
|
||
const moonRoll = await new Roll("1d8").evaluate()
|
||
moonDieResult = moonRoll.total
|
||
moonFace = SYSTEM.MOON_DIE_FACES[moonDieResult] ?? null
|
||
moonResultType = moonFace ? SYSTEM.MOON_RESULT_TYPES[moonFace.result] ?? null : null
|
||
}
|
||
|
||
const rollData = {
|
||
...options,
|
||
difficulty,
|
||
difficultyValue: diffConfig.value,
|
||
modifier: effectiveModifier,
|
||
aspectMod: effectiveAspectMod,
|
||
factionAspectId,
|
||
factionAspectLabel,
|
||
factionAspectBonus,
|
||
situationMod: effectiveSituationMod,
|
||
woundMalus: effectiveWoundMalus,
|
||
autoSuccess,
|
||
isResistance,
|
||
isOpposition,
|
||
isCombat,
|
||
isRangedDefense,
|
||
weaponType,
|
||
weaponName,
|
||
weaponDegats,
|
||
targetActorId,
|
||
targetActorName,
|
||
availableTargets,
|
||
rangedMod: effectiveRangedMod,
|
||
useDestin: !isResistance && useDestin,
|
||
useFortune: !isResistance && useFortune,
|
||
puiserRessources: effectivePuiser,
|
||
nbDice: (!isResistance && useFortune) ? 1 : nbDice,
|
||
formula,
|
||
rollMode: rollContext.visibility ?? "publicroll",
|
||
rollMoonDie: effectiveMoon,
|
||
moonDieResult,
|
||
moonFace,
|
||
moonResultType,
|
||
}
|
||
|
||
const roll = new this(formula, {}, rollData)
|
||
await roll.evaluate()
|
||
roll.computeResult()
|
||
|
||
// Test de résistance échoué → cocher automatiquement la prochaine case de blessure
|
||
const actor = game.actors.get(options.actorId)
|
||
if (isResistance && actor && roll.options.resultType === "failure") {
|
||
const nextLvl = (actor.system.blessures.lvl ?? 0) + 1
|
||
if (nextLvl <= 8) {
|
||
await actor.update({ "system.blessures.lvl": nextLvl })
|
||
roll.options.woundTaken = nextLvl
|
||
}
|
||
}
|
||
|
||
// Mêlée échouée OU défense à distance échouée → joueur prend une blessure
|
||
if (isCombat && (weaponType === "melee" || isRangedDefense) && actor && roll.options.resultType === "failure") {
|
||
const nextLvl = (actor.system.blessures.lvl ?? 0) + 1
|
||
if (nextLvl <= 8) {
|
||
await actor.update({ "system.blessures.lvl": nextLvl })
|
||
roll.options.woundTaken = nextLvl
|
||
}
|
||
}
|
||
|
||
await roll.toMessage({}, { rollMode: rollData.rollMode })
|
||
|
||
// Batching de toutes les mises à jour de l'acteur en un seul appel réseau
|
||
if (actor) {
|
||
const updateData = {}
|
||
|
||
if (rollData.useDestin) {
|
||
updateData["system.destin.lvl"] = 0
|
||
}
|
||
|
||
if (rollData.useFortune) {
|
||
const currentFortune = actor.system.attributs.fortune.value ?? 0
|
||
updateData["system.attributs.fortune.value"] = Math.max(0, currentFortune - 1)
|
||
}
|
||
|
||
if (rollData.puiserRessources) {
|
||
const currentSpleen = actor.system.spleen.lvl ?? 0
|
||
if (currentSpleen < 8) {
|
||
updateData["system.spleen.lvl"] = currentSpleen + 1
|
||
}
|
||
}
|
||
|
||
// Mémoriser les préférences (protagonistes uniquement — le modèle NPC n'a pas de champ prefs)
|
||
if (actor.type === "character") {
|
||
updateData["system.prefs.rollMoonDie"] = rollData.rollMoonDie
|
||
updateData["system.prefs.difficulty"] = difficulty
|
||
}
|
||
|
||
await actor.update(updateData)
|
||
}
|
||
|
||
return roll
|
||
}
|
||
|
||
/**
|
||
* Détermine succès/échec selon la marge (total − seuil).
|
||
* Seuil : 11 pour les tests normaux, Corps PNJ pour le combat.
|
||
* Pas de succès/échec critique — seul le Dé de la Lune produit des résultats exceptionnels.
|
||
*/
|
||
computeResult() {
|
||
if (this.options.autoSuccess) {
|
||
this.options.resultType = "success"
|
||
this.options.margin = null
|
||
return
|
||
}
|
||
// En test d'opposition : pas de résultat calculé — le MJ décide
|
||
if (this.options.isOpposition) {
|
||
this.options.resultType = "opposition"
|
||
this.options.margin = null
|
||
return
|
||
}
|
||
const threshold = this.options.isCombat
|
||
? (this.options.difficultyValue ?? 0)
|
||
: 11
|
||
if (threshold === 0) {
|
||
this.options.resultType = "unknown"
|
||
this.options.margin = null
|
||
return
|
||
}
|
||
const margin = this.total - threshold
|
||
this.options.margin = margin
|
||
if (this.options.isCombat && margin === 0) {
|
||
this.options.resultType = "tie"
|
||
} else if (margin >= 0) {
|
||
this.options.resultType = "success"
|
||
} else {
|
||
this.options.resultType = "failure"
|
||
}
|
||
}
|
||
|
||
/** @override */
|
||
async render(chatOptions = {}) {
|
||
const data = await this._getChatCardData(chatOptions.isPrivate)
|
||
return foundry.applications.handlebars.renderTemplate(this.constructor.CHAT_TEMPLATE, data)
|
||
}
|
||
|
||
async _getChatCardData(isPrivate) {
|
||
const statLabel = this.options.statLabel
|
||
const skillLabel = this.options.skillLabel
|
||
const resultType = this.resultType
|
||
const diceResults = this.dice[0]?.results?.map(r => r.result) ?? []
|
||
const diceSum = diceResults.reduce((a, b) => a + b, 0)
|
||
const threshold = this.options.isCombat
|
||
? (this.options.difficultyValue ?? 0)
|
||
: 11
|
||
const margin = this.options.margin
|
||
const woundMalus = this.options.woundMalus ?? 0
|
||
const armorMalus = this.options.armorMalus ?? 0
|
||
const skillValue = this.options.skillValue ?? 0
|
||
const woundLevelId = this.options.woundLevel ?? 0
|
||
const weaponDegats = `${this.options.weaponDegats ?? "0"}`
|
||
const woundLabel = woundLevelId > 0
|
||
? game.i18n.localize(SYSTEM.WOUND_LEVELS[woundLevelId]?.label ?? "")
|
||
: null
|
||
|
||
// Dé de lune
|
||
const moonDieResult = this.options.moonDieResult ?? null
|
||
const moonFace = this.options.moonFace ?? null
|
||
const moonResultType = this.options.moonResultType ?? null
|
||
|
||
const resultClassMap = {
|
||
"success": "success",
|
||
"tie": "tie",
|
||
"failure": "failure",
|
||
"opposition": "opposition",
|
||
"unknown": "",
|
||
}
|
||
|
||
const isOpposition = this.options.isOpposition ?? false
|
||
const isWeaponHit = (this.options.isCombat ?? false) && !(this.options.isRangedDefense ?? false) && this.isSuccess
|
||
const incomingWounds = isWeaponHit ? this.constructor.getIncomingWounds(weaponDegats) : null
|
||
const hasVariableDamage = isWeaponHit && incomingWounds === null
|
||
const targetActorId = this.options.targetActorId ?? ""
|
||
const targetActorName = this.options.targetActorName ?? ""
|
||
const availableTargets = (this.options.availableTargets ?? []).map(target => ({
|
||
...target,
|
||
selected: target.id === targetActorId,
|
||
}))
|
||
const selectedTargetActor = targetActorId ? game.actors.get(targetActorId) : null
|
||
const selectedTargetProtection = selectedTargetActor
|
||
? this.constructor.getActorArmorProtection(selectedTargetActor)
|
||
: null
|
||
const selectedTargetAppliedWounds = (incomingWounds !== null && selectedTargetActor)
|
||
? Math.max(0, incomingWounds - selectedTargetProtection)
|
||
: null
|
||
|
||
// Libellé de difficulté : en combat "Corps PNJ : N", en opposition "vs ?", sinon "Seuil : 11"
|
||
const difficultyLabel = this.options.isCombat
|
||
? `${game.i18n.localize("CELESTOPOL.Combat.corpsPnj")} : ${threshold}`
|
||
: isOpposition
|
||
? `${game.i18n.localize("CELESTOPOL.Roll.oppositionVs")}`
|
||
: `${game.i18n.localize("CELESTOPOL.Roll.threshold")} : 11`
|
||
|
||
return {
|
||
cssClass: [SYSTEM.id, "dice-roll"].join(" "),
|
||
actorId: this.actorId,
|
||
actorName: this.actorName,
|
||
actorImg: this.actorImage,
|
||
statLabel: statLabel ? game.i18n.localize(statLabel) : "",
|
||
skillLabel: skillLabel ? game.i18n.localize(skillLabel) : "",
|
||
formula: this.formula,
|
||
total: this.total,
|
||
diceSum,
|
||
diceResults,
|
||
resultType,
|
||
resultClass: resultClassMap[resultType] ?? "",
|
||
isSuccess: this.isSuccess,
|
||
isFailure: this.isFailure,
|
||
isTie: this.isTie,
|
||
isOpposition,
|
||
difficulty: this.options.difficulty,
|
||
difficultyLabel,
|
||
difficultyValue: isOpposition ? null : threshold,
|
||
margin,
|
||
marginAbs: margin !== null ? Math.abs(margin) : null,
|
||
marginAbove: margin !== null && margin >= 0,
|
||
modifier: this.options.modifier ?? 0,
|
||
autoSuccess: this.options.autoSuccess ?? false,
|
||
aspectMod: this.options.aspectMod ?? 0,
|
||
factionAspectLabel: this.options.factionAspectLabel ?? "",
|
||
factionAspectBonus: this.options.factionAspectBonus ?? 0,
|
||
skillValue,
|
||
useDestin: this.options.useDestin ?? false,
|
||
useFortune: this.options.useFortune ?? false,
|
||
puiserRessources: this.options.puiserRessources ?? false,
|
||
nbDice: this.options.nbDice ?? diceResults.length,
|
||
woundMalus,
|
||
armorMalus,
|
||
woundLabel,
|
||
isResistance: this.options.isResistance ?? false,
|
||
isCombat: this.options.isCombat ?? false,
|
||
weaponName: this.options.weaponName ?? null,
|
||
weaponDegats,
|
||
weaponType: this.options.weaponType ?? null,
|
||
isRangedDefense: this.options.isRangedDefense ?? false,
|
||
woundTaken: this.options.woundTaken ?? null,
|
||
situationMod: this.options.situationMod ?? 0,
|
||
rangedMod: this.options.rangedMod ?? 0,
|
||
hasDamageSummary: isWeaponHit,
|
||
incomingWounds,
|
||
incomingWoundsDisplay: incomingWounds ?? "1 + X",
|
||
hasVariableDamage,
|
||
canApplyWeaponDamage: incomingWounds !== null,
|
||
targetActorId,
|
||
targetActorName,
|
||
selectedTargetProtection,
|
||
selectedTargetAppliedWounds,
|
||
availableTargets,
|
||
// Dé de lune
|
||
hasMoonDie: moonDieResult !== null,
|
||
moonDieResult,
|
||
moonFaceSymbol: moonFace?.symbol ?? "",
|
||
moonFaceLabel: moonFace ? game.i18n.localize(moonFace.label) : "",
|
||
moonResultClass: moonResultType?.cssClass ?? "",
|
||
moonResultLabel: moonResultType ? game.i18n.localize(moonResultType.label) : "",
|
||
moonResultDesc: moonResultType ? game.i18n.localize(moonResultType.desc) : "",
|
||
isPrivate,
|
||
tooltip: isPrivate ? "" : await this.getTooltip(),
|
||
}
|
||
}
|
||
|
||
/** @override */
|
||
async toMessage(messageData = {}, { rollMode, create = true } = {}) {
|
||
const skillLocalized = this.skillLabel ? game.i18n.localize(this.skillLabel) : ""
|
||
const statLocalized = this.options.statLabel
|
||
? game.i18n.localize(this.options.statLabel) : ""
|
||
const flavor = statLocalized
|
||
? `<strong>${statLocalized} › ${skillLocalized}</strong>`
|
||
: `<strong>${skillLocalized}</strong>`
|
||
return super.toMessage({ flavor, ...messageData }, { rollMode })
|
||
}
|
||
|
||
/**
|
||
* Lance le dé de la Lune de façon autonome (hors test de spécialisation).
|
||
* Affiche un carte de chat avec le résultat narratif ET l'interprétation chance.
|
||
* @param {Actor|null} actor Acteur initiateur (pour le speaker du message)
|
||
*/
|
||
static async rollMoonStandalone(actor = null) {
|
||
const roll = await new Roll("1d8").evaluate()
|
||
const result = roll.total
|
||
const face = SYSTEM.MOON_DIE_FACES[result] ?? null
|
||
const resultType = face ? SYSTEM.MOON_RESULT_TYPES[face.result] ?? null : null
|
||
const isGoodFortune = result <= 4
|
||
|
||
const templateData = {
|
||
result,
|
||
moonFaceSymbol: face?.symbol ?? "",
|
||
moonFaceLabel: face ? game.i18n.localize(face.label) : "",
|
||
moonResultLabel: resultType ? game.i18n.localize(resultType.label) : "",
|
||
moonResultDesc: resultType ? game.i18n.localize(resultType.desc) : "",
|
||
moonResultClass: resultType?.cssClass ?? "",
|
||
isGoodFortune,
|
||
actorName: actor?.name ?? null,
|
||
}
|
||
|
||
const content = await foundry.applications.handlebars.renderTemplate(
|
||
"systems/fvtt-celestopol/templates/moon-standalone.hbs",
|
||
templateData
|
||
)
|
||
|
||
const speaker = actor
|
||
? ChatMessage.getSpeaker({ actor })
|
||
: { alias: game.i18n.localize("CELESTOPOL.Moon.standalone") }
|
||
|
||
await ChatMessage.create({
|
||
content,
|
||
speaker,
|
||
rolls: [roll],
|
||
style: CONST.CHAT_MESSAGE_STYLES?.ROLL ?? 5,
|
||
})
|
||
}
|
||
}
|