Corrections sur factions, aspects, degats et fiches PNJs

This commit is contained in:
2026-04-11 15:02:46 +02:00
parent 36516c3b08
commit 3358dea306
44 changed files with 2308 additions and 148 deletions

View File

@@ -1,6 +1,14 @@
/**
* fvtt-celestopol.mjs — Point d'entrée principal du système Célestopol 1922
* FoundryVTT v13+ / DataModels / ApplicationV2
* 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 20252026 LeRatierBretonnien
* @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
*/
import { SYSTEM, SYSTEM_ID, ASCII } from "./module/config/system.mjs"
@@ -30,6 +38,9 @@ import {
CelestopolArmureSheet,
} from "./module/applications/_module.mjs"
const DAMAGE_APPLICATION_FLAG = "damageApplication"
const FACTION_ASPECT_STATE_SETTING = "factionAspectState"
/* ─── Init hook ──────────────────────────────────────────────────────────── */
Hooks.once("init", () => {
@@ -52,6 +63,11 @@ Hooks.once("init", () => {
game.celestopol = {
SYSTEM,
rollMoonStandalone: (actor = null) => CelestopolRoll.rollMoonStandalone(actor),
manageFactionAspects: (actor = null) => _manageFactionAspects(actor),
getFactionAspectState: () => _getFactionAspectState(),
getFactionAspectSummary: (actor = null) => _getFactionAspectSummary(actor),
getFactionDisplayLabel: (value) => _getFactionDisplayLabel(value),
normalizeFactionId: (value) => _normalizeFactionId(value),
}
// ── DataModels ──────────────────────────────────────────────────────────
@@ -148,6 +164,16 @@ Hooks.once("ready", () => {
game.socket.on(`system.${SYSTEM_ID}`, _onSocketMessage)
}
Hooks.on("renderChatMessageHTML", (message, html) => {
_activateChatCardListeners(message, html)
})
Hooks.on("updateChatMessage", (message, changed) => {
if (foundry.utils.hasProperty(changed, `flags.${SYSTEM_ID}.${DAMAGE_APPLICATION_FLAG}`)) {
_updateRenderedChatMessageState(message)
}
})
_activateExistingChatCards()
// Migration : supprime les items de types obsolètes (ex: "attribute")
if (game.user.isGM) {
_migrateObsoleteItems()
@@ -317,6 +343,12 @@ function _registerSettings() {
type: Boolean,
default: false,
})
game.settings.register(SYSTEM_ID, FACTION_ASPECT_STATE_SETTING, {
scope: "world",
config: false,
type: Object,
default: _getDefaultFactionAspectState(),
})
}
/* ─── Template preload ───────────────────────────────────────────────────── */
@@ -349,17 +381,702 @@ function _preloadTemplates() {
/* ─── Socket handler ─────────────────────────────────────────────────────── */
function _onSocketMessage(data) {
if (!game.user.isGM) return
async function _onSocketMessage(data) {
const activeGM = game.users.activeGM
if (!game.user.isGM || (activeGM && activeGM.id !== game.user.id)) return
switch (data.type) {
case "applyWound": {
const actor = game.actors.get(data.actorId)
if (actor) actor.update({ "system.blessures.lvl": data.level })
if (actor) await actor.update({ "system.blessures.lvl": data.level })
break
}
case "applyWeaponDamage": {
await _applyWeaponDamage(data)
break
}
}
}
function _getChatHtmlRoot(html) {
if (html instanceof HTMLElement) return html
if (html?.[0] instanceof HTMLElement) return html[0]
if (html?.element instanceof HTMLElement) return html.element
if (html?.element?.[0] instanceof HTMLElement) return html.element[0]
return null
}
function _activateExistingChatCards() {
document.querySelectorAll(".message[data-message-id]").forEach(messageEl => {
const messageId = messageEl.dataset.messageId
const message = game.messages.get(messageId)
const root = messageEl.querySelector(".celestopol.chat-roll")
if (!message || !root) return
_activateChatCardListeners(message, root)
})
}
function _activateChatCardListeners(message, html) {
const root = _getChatHtmlRoot(html)
if (!root) return
_renderWeaponDamageState(message, root)
root.querySelectorAll('[data-action="apply-weapon-damage"]').forEach(button => {
if (button.dataset.bound === "true") return
button.dataset.bound = "true"
button.addEventListener("click", event => _onApplyWeaponDamageClick(event, message))
})
}
async function _onApplyWeaponDamageClick(event, message) {
event.preventDefault()
const button = event.currentTarget
const card = button.closest(".celestopol.chat-roll")
const select = button.closest(".weapon-damage-actions")?.querySelector('select[name="targetActorId"]')
const actorId = button.dataset.actorId || select?.value || ""
const incomingWounds = Number.parseInt(button.dataset.incomingWounds ?? "", 10)
const currentState = _getDamageApplicationState(message)
if (currentState?.applied) {
if (card) _renderWeaponDamageState(message, card)
return
}
if (!actorId) {
ui.notifications.warn(game.i18n.localize("CELESTOPOL.Combat.selectCharacterFirst"))
return
}
if (!Number.isFinite(incomingWounds) || incomingWounds < 0) return
if (card) _renderPendingWeaponDamageState(card)
button.disabled = true
await _requestWeaponDamageApplication({
actorId,
incomingWounds,
chatMessageId: message?.id ?? null,
})
}
async function _requestWeaponDamageApplication({ actorId, incomingWounds, chatMessageId = null }) {
if (game.user.isGM) {
return _applyWeaponDamage({ actorId, incomingWounds, chatMessageId })
}
if (!game.socket) return
game.socket.emit(`system.${SYSTEM_ID}`, {
type: "applyWeaponDamage",
actorId,
incomingWounds,
chatMessageId,
})
ui.notifications.info(game.i18n.localize("CELESTOPOL.Combat.damageRequestSent"))
}
function _getDamageApplicationState(message) {
return message?.getFlag(SYSTEM_ID, DAMAGE_APPLICATION_FLAG) ?? null
}
function _updateRenderedChatMessageState(message) {
const root = document.querySelector(`.message[data-message-id="${message.id}"] .celestopol.chat-roll`)
if (!root) return
_renderWeaponDamageState(message, root)
}
function _removeDamageStatus(root) {
root.querySelector(".damage-application-status")?.remove()
root.querySelector(".weapon-damage-summary")?.classList.remove("is-applied", "is-pending")
}
function _setDamageStatus(root, { text, cssClass = "" }) {
const summary = root.querySelector(".weapon-damage-summary")
if (!summary) return
_removeDamageStatus(root)
summary.classList.add(cssClass)
const status = document.createElement("div")
status.className = `damage-application-status ${cssClass}`.trim()
status.textContent = text
summary.append(status)
}
function _renderPendingWeaponDamageState(root) {
const button = root.querySelector('[data-action="apply-weapon-damage"]')
const select = root.querySelector('select[name="targetActorId"]')
if (button) {
button.disabled = true
button.textContent = game.i18n.localize("CELESTOPOL.Combat.damageApplying")
}
if (select) select.disabled = true
_setDamageStatus(root, {
text: game.i18n.localize("CELESTOPOL.Combat.damageApplyingNotice"),
cssClass: "is-pending",
})
}
function _renderWeaponDamageState(message, root) {
const button = root.querySelector('[data-action="apply-weapon-damage"]')
const select = root.querySelector('select[name="targetActorId"]')
const state = _getDamageApplicationState(message)
if (!state?.applied) {
if (button) button.textContent = game.i18n.localize("CELESTOPOL.Combat.applyDamage")
if (button) button.disabled = false
if (select) select.disabled = false
_removeDamageStatus(root)
return
}
if (button) {
button.disabled = true
button.textContent = game.i18n.localize("CELESTOPOL.Combat.damageAppliedDone")
}
if (select) select.disabled = true
const text = state.appliedWounds > 0
? game.i18n.format("CELESTOPOL.Combat.damageAppliedCard", {
actor: state.actorName,
wounds: state.appliedWounds,
armor: state.armorProtection,
})
: game.i18n.format("CELESTOPOL.Combat.damageNoEffectCard", {
actor: state.actorName,
armor: state.armorProtection,
})
_setDamageStatus(root, { text, cssClass: "is-applied" })
}
async function _markChatMessageDamageApplied(chatMessageId, data) {
if (!chatMessageId) return
const message = game.messages.get(chatMessageId)
if (!message) return
await message.setFlag(SYSTEM_ID, DAMAGE_APPLICATION_FLAG, {
applied: true,
...data,
})
_updateRenderedChatMessageState(message)
}
async function _applyWeaponDamage({ actorId, incomingWounds, chatMessageId = null }) {
const actor = game.actors.get(actorId)
if (!actor) return null
const message = chatMessageId ? game.messages.get(chatMessageId) : null
if (_getDamageApplicationState(message)?.applied) {
return null
}
const armorProtection = CelestopolRoll.getActorArmorProtection(actor)
const appliedWounds = Math.max(0, incomingWounds - armorProtection)
const currentWounds = actor.system?.blessures?.lvl ?? 0
const nextWounds = Math.min(8, currentWounds + appliedWounds)
if (appliedWounds > 0 && nextWounds !== currentWounds) {
await actor.update({ "system.blessures.lvl": nextWounds })
}
await _markChatMessageDamageApplied(chatMessageId, {
actorId,
actorName: actor.name,
appliedWounds,
armorProtection,
})
if (appliedWounds > 0) {
ui.notifications.info(game.i18n.format("CELESTOPOL.Combat.damageAppliedNotify", {
actor: actor.name,
wounds: appliedWounds,
armor: armorProtection,
}))
} else {
ui.notifications.info(game.i18n.format("CELESTOPOL.Combat.damageNoEffectNotify", {
actor: actor.name,
armor: armorProtection,
}))
}
return { actorName: actor.name, appliedWounds, armorProtection }
}
function _getDefaultFactionAspectState() {
return {
pointsMax: 8,
activatedAspects: [],
customCell: {
enabled: false,
mode: "replace",
name: "",
aspectIds: [],
},
}
}
function _normalizeFactionId(value) {
const raw = `${value ?? ""}`.trim()
if (!raw) return ""
const direct = raw.toLowerCase()
if (SYSTEM.FACTIONS[direct]) return direct
for (const [id, faction] of Object.entries(SYSTEM.FACTIONS)) {
if (game.i18n.localize(faction.label).trim().toLowerCase() === direct) return id
}
return ""
}
function _getFactionDisplayLabel(value) {
const factionId = _normalizeFactionId(value)
if (!factionId) return `${value ?? ""}`.trim()
return game.i18n.localize(SYSTEM.FACTIONS[factionId].label)
}
function _sanitizeFactionAspectState(state = {}) {
const base = foundry.utils.mergeObject(_getDefaultFactionAspectState(), foundry.utils.deepClone(state), {
inplace: false,
insertKeys: true,
insertValues: true,
overwrite: true,
recursive: true,
})
base.pointsMax = Math.max(0, Number.parseInt(base.pointsMax ?? 8, 10) || 0)
base.customCell.enabled = Boolean(base.customCell?.enabled)
base.customCell.mode = base.customCell?.mode === "extend" ? "extend" : "replace"
base.customCell.name = `${base.customCell?.name ?? ""}`.trim()
base.customCell.aspectIds = Array.from(new Set((base.customCell?.aspectIds ?? [])
.filter(id => SYSTEM.FACTION_ASPECTS[id])))
base.activatedAspects = (base.activatedAspects ?? [])
.map(entry => {
const id = `${entry?.id ?? ""}`.trim()
const aspect = SYSTEM.FACTION_ASPECTS[id]
if (!aspect) return null
const value = Math.max(1, Math.min(4, Number.parseInt(entry.value ?? 1, 10) || 1))
return {
id,
value,
label: game.i18n.localize(aspect.label),
}
})
.filter(Boolean)
return base
}
function _getFactionAspectState() {
const stored = game.settings.get(SYSTEM_ID, FACTION_ASPECT_STATE_SETTING) ?? {}
return _sanitizeFactionAspectState(stored)
}
async function _setFactionAspectState(state) {
const cleanState = _sanitizeFactionAspectState(state)
await game.settings.set(SYSTEM_ID, FACTION_ASPECT_STATE_SETTING, cleanState)
_refreshFactionAspectSheets()
return cleanState
}
function _refreshFactionAspectSheets() {
for (const actor of game.actors.contents) {
if (actor.type !== "character") continue
if (!actor.sheet?.rendered) continue
actor.sheet.render(true)
}
}
function _getRepresentedFactionIds() {
return Array.from(new Set(
game.actors.contents
.filter(actor => actor.type === "character")
.map(actor => _normalizeFactionId(actor.system?.faction))
.filter(Boolean)
))
}
function _getFactionAspectSourceData(state = _getFactionAspectState()) {
const representedFactionIds = _getRepresentedFactionIds()
const sourceFactionIds = state.customCell.enabled && state.customCell.mode === "replace"
? []
: representedFactionIds
const sourceAspectIds = new Set()
const officialSourceLabels = []
const sourceLabels = []
for (const factionId of sourceFactionIds) {
const label = game.i18n.localize(SYSTEM.FACTIONS[factionId].label)
officialSourceLabels.push(label)
sourceLabels.push(label)
for (const aspectId of SYSTEM.FACTION_ASPECTS_BY_FACTION[factionId] ?? []) {
sourceAspectIds.add(aspectId)
}
}
let customCellLabel = ""
if (state.customCell.enabled) {
customCellLabel = state.customCell.name || game.i18n.localize("CELESTOPOL.FactionAspect.customCell")
sourceLabels.push(customCellLabel)
for (const aspectId of state.customCell.aspectIds ?? []) {
sourceAspectIds.add(aspectId)
}
}
return {
representedFactionIds,
officialSourceLabels,
availableAspectIds: Array.from(sourceAspectIds),
sourceLabels,
customCellLabel,
}
}
function _getActorAvailableFactionAspectIds(actor, state = _getFactionAspectState()) {
const ids = new Set()
const factionId = _normalizeFactionId(actor?.system?.faction)
if (state.customCell.enabled && state.customCell.mode === "replace") {
for (const aspectId of state.customCell.aspectIds ?? []) ids.add(aspectId)
return ids
}
if (factionId) {
for (const aspectId of SYSTEM.FACTION_ASPECTS_BY_FACTION[factionId] ?? []) ids.add(aspectId)
}
if (state.customCell.enabled && state.customCell.mode === "extend") {
for (const aspectId of state.customCell.aspectIds ?? []) ids.add(aspectId)
}
return ids
}
function _getFactionAspectSummary(actor = null) {
const state = _getFactionAspectState()
const sourceData = _getFactionAspectSourceData(state)
const primaryFactionId = actor ? _normalizeFactionId(actor.system?.faction) : ""
const actorAvailableIds = actor ? _getActorAvailableFactionAspectIds(actor, state) : new Set(sourceData.availableAspectIds)
const pointsSpent = state.activatedAspects.reduce((sum, aspect) => sum + aspect.value, 0)
const pointsRemaining = Math.max(0, state.pointsMax - pointsSpent)
return {
...state,
pointsSpent,
pointsRemaining,
sourceLabels: sourceData.sourceLabels,
officialSourceLabels: sourceData.officialSourceLabels,
customCellLabel: sourceData.customCellLabel,
hasOfficialSources: sourceData.officialSourceLabels.length > 0,
needsSourceConfiguration: !sourceData.officialSourceLabels.length && !state.customCell.enabled,
representedFactions: sourceData.representedFactionIds.map(id => ({
id,
label: game.i18n.localize(SYSTEM.FACTIONS[id].label),
})),
primaryFactionId,
primaryFactionLabel: primaryFactionId ? game.i18n.localize(SYSTEM.FACTIONS[primaryFactionId].label) : _getFactionDisplayLabel(actor?.system?.faction),
availableAspectChoices: state.activatedAspects.map(aspect => ({
id: aspect.id,
value: aspect.value,
label: aspect.label,
})),
activatableAspectChoices: sourceData.availableAspectIds
.filter(id => !state.activatedAspects.some(aspect => aspect.id === id))
.map(id => ({
id,
label: game.i18n.localize(SYSTEM.FACTION_ASPECTS[id].label),
})),
availableAspectLabels: sourceData.availableAspectIds.map(id => game.i18n.localize(SYSTEM.FACTION_ASPECTS[id].label)),
activatedAspects: state.activatedAspects.map(aspect => ({
...aspect,
relevantToActor: actor ? actorAvailableIds.has(aspect.id) : true,
})),
}
}
function _parseFactionAspectManagerForm(form) {
return {
pointsMax: Math.max(0, Number.parseInt(form.querySelector('[name="pointsMax"]')?.value ?? 8, 10) || 0),
customCellEnabled: form.querySelector('[name="customCellEnabled"]')?.checked ?? false,
customCellMode: form.querySelector('[name="customCellMode"]')?.value === "extend" ? "extend" : "replace",
customCellName: `${form.querySelector('[name="customCellName"]')?.value ?? ""}`.trim(),
customCellAspectIds: Array.from(form.querySelectorAll('input[name="customCellAspectIds"]:checked')).map(input => input.value),
activateAspectId: `${form.querySelector('[name="activateAspectId"]')?.value ?? ""}`.trim(),
activateAspectValue: Math.max(1, Math.min(4, Number.parseInt(form.querySelector('[name="activateAspectValue"]')?.value ?? 1, 10) || 1)),
removeAspectId: `${form.querySelector('[name="removeAspectId"]')?.value ?? ""}`.trim(),
}
}
function _renderFactionAspectManagerContent(summary) {
const i18n = game.i18n
const hint = (text) => ` <span class="faction-aspect-help-tip" title="${foundry.utils.escapeHTML(text)}">?</span>`
const checkedAspectIds = new Set(summary.customCell.aspectIds ?? [])
const customAspectCheckboxes = Object.values(SYSTEM.FACTION_ASPECTS).map(aspect => `
<label class="faction-aspect-cell-option">
<input type="checkbox" name="customCellAspectIds" value="${aspect.id}" ${checkedAspectIds.has(aspect.id) ? "checked" : ""}>
<span>${i18n.localize(aspect.label)}</span>
</label>
`).join("")
const activatableOptions = summary.activatableAspectChoices.length
? summary.activatableAspectChoices.map(aspect => `<option value="${aspect.id}">${aspect.label}</option>`).join("")
: `<option value="">${i18n.localize("CELESTOPOL.FactionAspect.noAspectAvailable")}</option>`
const activatedRows = summary.activatedAspects.length
? summary.activatedAspects.map(aspect => `
<div class="faction-aspect-active-row ${aspect.relevantToActor ? "is-relevant" : ""}">
<span class="faction-aspect-active-name">${aspect.label}</span>
<span class="faction-aspect-active-value">+${aspect.value}</span>
</div>
`).join("")
: `<div class="faction-aspect-empty">${i18n.localize("CELESTOPOL.FactionAspect.noneActive")}</div>`
const sourceLabels = summary.sourceLabels.length
? summary.sourceLabels.join(" • ")
: i18n.localize("CELESTOPOL.FactionAspect.noSource")
const availableAspectList = summary.availableAspectLabels.length
? summary.availableAspectLabels.map(label => `<span class="faction-aspect-tag">${label}</span>`).join("")
: `<div class="faction-aspect-empty">${i18n.localize("CELESTOPOL.FactionAspect.noAspectAvailable")}</div>`
const removeOptions = summary.activatedAspects.length
? summary.activatedAspects.map(aspect => `<option value="${aspect.id}">${aspect.label} (+${aspect.value})</option>`).join("")
: `<option value="">${i18n.localize("CELESTOPOL.FactionAspect.noneActive")}</option>`
const officialSourcesBlock = summary.hasOfficialSources
? `
<div class="faction-aspect-source-list">
${summary.officialSourceLabels.map(label => `<span class="faction-aspect-tag">${label}</span>`).join("")}
</div>
`
: `<div class="faction-aspect-warning">${i18n.localize("CELESTOPOL.FactionAspect.officialSourcesEmpty")}</div>`
const customCellOpen = summary.customCell.enabled ? "open" : ""
return `
<form class="cel-dialog-form faction-aspect-manager">
<div class="faction-aspect-box">
<div class="faction-aspect-box-title">${i18n.localize("CELESTOPOL.FactionAspect.managerState")}</div>
<div class="faction-aspect-points">
<span class="faction-aspect-point-card"><strong>${i18n.localize("CELESTOPOL.FactionAspect.pointsMax")}</strong><em>${summary.pointsMax}</em></span>
<span class="faction-aspect-point-card"><strong>${i18n.localize("CELESTOPOL.FactionAspect.pointsSpent")}</strong><em>${summary.pointsSpent}</em></span>
<span class="faction-aspect-point-card"><strong>${i18n.localize("CELESTOPOL.FactionAspect.pointsRemaining")}</strong><em>${summary.pointsRemaining}</em></span>
</div>
${game.user.isGM ? `
<div class="form-group faction-aspect-pool-group">
<label>${i18n.localize("CELESTOPOL.FactionAspect.globalPoolLabel")}${hint(i18n.localize("CELESTOPOL.FactionAspect.globalPoolHint"))}</label>
<input type="number" name="pointsMax" min="0" value="${summary.pointsMax}">
</div>
` : ""}
<div class="faction-aspect-source-line"><strong>${i18n.localize("CELESTOPOL.FactionAspect.sources")}</strong> ${sourceLabels}</div>
</div>
<div class="faction-aspect-box">
<div class="faction-aspect-box-title">${i18n.localize("CELESTOPOL.FactionAspect.officialSourcesTitle")}${hint(i18n.localize("CELESTOPOL.FactionAspect.officialSourcesHint"))}</div>
${officialSourcesBlock}
</div>
<div class="faction-aspect-box">
<div class="faction-aspect-box-title">${i18n.localize("CELESTOPOL.FactionAspect.availablePoolTitle")}${hint(i18n.localize("CELESTOPOL.FactionAspect.availablePoolHint"))}</div>
<div class="faction-aspect-tag-list">${availableAspectList}</div>
</div>
<div class="faction-aspect-box">
<div class="faction-aspect-box-title">${i18n.localize("CELESTOPOL.FactionAspect.managerSettings")}</div>
<details class="faction-aspect-advanced" ${customCellOpen}>
<summary>${i18n.localize("CELESTOPOL.FactionAspect.customCellSection")}${hint(i18n.localize("CELESTOPOL.FactionAspect.customCellHint"))}</summary>
<div class="form-group faction-aspect-checkbox-line">
<label>
<input type="checkbox" name="customCellEnabled" ${summary.customCell.enabled ? "checked" : ""}>
${i18n.localize("CELESTOPOL.FactionAspect.customCellEnabled")}
</label>
</div>
<div class="form-group">
<label>${i18n.localize("CELESTOPOL.FactionAspect.customCellName")}</label>
<input type="text" name="customCellName" value="${foundry.utils.escapeHTML(summary.customCell.name ?? "")}" placeholder="${i18n.localize("CELESTOPOL.FactionAspect.customCell")}">
</div>
<div class="form-group">
<label>${i18n.localize("CELESTOPOL.FactionAspect.customCellMode")}</label>
<select name="customCellMode">
<option value="replace" ${summary.customCell.mode === "replace" ? "selected" : ""}>${i18n.localize("CELESTOPOL.FactionAspect.modeReplace")}</option>
<option value="extend" ${summary.customCell.mode === "extend" ? "selected" : ""}>${i18n.localize("CELESTOPOL.FactionAspect.modeExtend")}</option>
</select>
</div>
<div class="faction-aspect-cell-grid">${customAspectCheckboxes}</div>
</details>
</div>
<div class="faction-aspect-box">
<div class="faction-aspect-box-title">${i18n.localize("CELESTOPOL.FactionAspect.activateTitle")}${hint(i18n.localize("CELESTOPOL.FactionAspect.activateHint"))}</div>
<div class="form-group">
<label>${i18n.localize("CELESTOPOL.FactionAspect.activateAspect")}</label>
<select name="activateAspectId">${activatableOptions}</select>
</div>
<div class="form-group">
<label>${i18n.localize("CELESTOPOL.FactionAspect.activateValue")}</label>
<select name="activateAspectValue">
<option value="1">+1</option>
<option value="2">+2</option>
<option value="3">+3</option>
<option value="4">+4</option>
</select>
</div>
</div>
<div class="faction-aspect-box">
<div class="faction-aspect-box-title">${i18n.localize("CELESTOPOL.FactionAspect.activeTitle")}</div>
<div class="faction-aspect-active-list">${activatedRows}</div>
<div class="faction-aspect-remove-block">
<div class="form-group">
<label>${i18n.localize("CELESTOPOL.FactionAspect.removeAspect")}</label>
<select name="removeAspectId">${removeOptions}</select>
</div>
</div>
</div>
</form>
`
}
async function _saveFactionAspectManagerSettings(formData, currentState) {
if (!game.user.isGM) {
formData.pointsMax = currentState.pointsMax
}
const activatedCost = currentState.activatedAspects.reduce((sum, aspect) => sum + aspect.value, 0)
if (formData.pointsMax < activatedCost) {
ui.notifications.warn(game.i18n.localize("CELESTOPOL.FactionAspect.pointsBelowSpent"))
return null
}
if (formData.customCellEnabled) {
const count = formData.customCellAspectIds.length
if (count < 4 || count > 8) {
ui.notifications.warn(game.i18n.localize("CELESTOPOL.FactionAspect.customCellAspectCount"))
return null
}
}
return _setFactionAspectState({
...currentState,
pointsMax: formData.pointsMax,
customCell: {
enabled: formData.customCellEnabled,
mode: formData.customCellMode,
name: formData.customCellName,
aspectIds: formData.customCellAspectIds,
},
})
}
async function _manageFactionAspects(actor = null) {
const summary = _getFactionAspectSummary(actor)
const result = await foundry.applications.api.DialogV2.wait({
window: { title: game.i18n.localize("CELESTOPOL.FactionAspect.managerTitle") },
classes: ["fvtt-celestopol", "faction-aspect-dialog"],
content: _renderFactionAspectManagerContent(summary),
buttons: [
{
action: "save",
label: game.i18n.localize("CELESTOPOL.FactionAspect.save"),
callback: (_event, button) => ({ action: "save", ..._parseFactionAspectManagerForm(button.form) }),
},
{
action: "activate",
label: game.i18n.localize("CELESTOPOL.FactionAspect.activateButton"),
callback: (_event, button) => ({ action: "activate", ..._parseFactionAspectManagerForm(button.form) }),
},
{
action: "remove",
label: game.i18n.localize("CELESTOPOL.FactionAspect.removeButton"),
callback: (_event, button) => ({ action: "remove", ..._parseFactionAspectManagerForm(button.form) }),
},
{
action: "reset",
label: game.i18n.localize("CELESTOPOL.FactionAspect.resetScenario"),
callback: () => ({ action: "reset" }),
},
],
rejectClose: false,
})
if (!result?.action) return null
if (result.action === "save") {
const updatedState = await _saveFactionAspectManagerSettings(result, _getFactionAspectState())
if (updatedState) ui.notifications.info(game.i18n.localize("CELESTOPOL.FactionAspect.saved"))
return null
}
if (result.action === "reset") {
const currentState = _getFactionAspectState()
await _setFactionAspectState({ ...currentState, activatedAspects: [] })
ui.notifications.info(game.i18n.localize("CELESTOPOL.FactionAspect.resetDone"))
return null
}
if (result.action === "activate") {
const savedState = await _saveFactionAspectManagerSettings(result, _getFactionAspectState())
if (!savedState) return null
const aspectId = result.activateAspectId
const aspectValue = result.activateAspectValue
const availableIds = new Set(_getFactionAspectSourceData(savedState).availableAspectIds)
const alreadyActive = savedState.activatedAspects.some(aspect => aspect.id === aspectId)
const currentSpent = savedState.activatedAspects.reduce((sum, aspect) => sum + aspect.value, 0)
if (!aspectId || !SYSTEM.FACTION_ASPECTS[aspectId] || !availableIds.has(aspectId)) {
ui.notifications.warn(game.i18n.localize("CELESTOPOL.FactionAspect.invalidAspect"))
return null
}
if (alreadyActive) {
ui.notifications.warn(game.i18n.localize("CELESTOPOL.FactionAspect.alreadyActive"))
return null
}
if ((currentSpent + aspectValue) > savedState.pointsMax) {
ui.notifications.warn(game.i18n.localize("CELESTOPOL.FactionAspect.notEnoughPoints"))
return null
}
const aspectLabel = game.i18n.localize(SYSTEM.FACTION_ASPECTS[aspectId].label)
await _setFactionAspectState({
...savedState,
activatedAspects: [
...savedState.activatedAspects,
{ id: aspectId, value: aspectValue, label: aspectLabel },
],
})
ui.notifications.info(game.i18n.format("CELESTOPOL.FactionAspect.activated", {
aspect: aspectLabel,
value: aspectValue,
}))
}
if (result.action === "remove") {
const savedState = await _saveFactionAspectManagerSettings(result, _getFactionAspectState())
if (!savedState) return null
const aspectId = result.removeAspectId
const removedAspect = savedState.activatedAspects.find(aspect => aspect.id === aspectId)
if (!removedAspect) {
ui.notifications.warn(game.i18n.localize("CELESTOPOL.FactionAspect.invalidRemove"))
return null
}
await _setFactionAspectState({
...savedState,
activatedAspects: savedState.activatedAspects.filter(aspect => aspect.id !== aspectId),
})
ui.notifications.info(game.i18n.format("CELESTOPOL.FactionAspect.removed", {
aspect: removedAspect.label,
value: removedAspect.value,
}))
}
return null
}
/* ─── Import initial des anomalies du compendium dans le monde ─────────── */
/**