Corrections sur factions, aspects, degats et fiches PNJs
This commit is contained in:
@@ -1,6 +1,14 @@
|
||||
/**
|
||||
* fvtt-celestopol.mjs — Point d'entrée principal du système Célestopol 1922
|
||||
* FoundryVTT v13+ / DataModels / ApplicationV2
|
||||
* 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, 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 ─────────── */
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user