This commit is contained in:
@@ -361,6 +361,259 @@ function _registerHandlebarsHelpers() {
|
||||
})
|
||||
}
|
||||
|
||||
/* ─── Migration depuis l'ancien système ─────────────────────────────────── */
|
||||
|
||||
/** Mapping des types d'anomalies ancien→nouveau */
|
||||
const _OLD_ANOMALY_TYPES = [
|
||||
"none", "entropie", "communicationaveclesmorts", "illusion",
|
||||
"suggestion", "tarotdivinatoire", "telekinesie", "telepathie", "voyageastral",
|
||||
]
|
||||
|
||||
const _OLD_SKILLS = {
|
||||
ame: ["artifice", "attraction", "coercition", "faveur"],
|
||||
corps: ["echauffouree", "effacement", "mobilite", "prouesse"],
|
||||
coeur: ["appreciation", "arts", "inspiration", "traque"],
|
||||
esprit: ["instruction", "mtechnologique", "raisonnement", "traitement"],
|
||||
}
|
||||
|
||||
/** Infère le type d'anomalie depuis le nom de l'item (ex: "Entropie 1" → "entropie"). */
|
||||
function _anomalyTypeFromName(name) {
|
||||
let n = name.toLowerCase().replace(/\s*\d+\s*$/, "").replace(/\s+/g, "")
|
||||
const accents = [["é","e"],["è","e"],["ê","e"],["ë","e"],["à","a"],["â","a"],
|
||||
["î","i"],["ï","i"],["ô","o"],["û","u"],["ù","u"],["ü","u"],["ç","c"]]
|
||||
for (const [s,d] of accents) n = n.replaceAll(s, d)
|
||||
return _OLD_ANOMALY_TYPES.find(k => k !== "none" && (k.includes(n) || n.includes(k))) ?? "none"
|
||||
}
|
||||
|
||||
/** Convertit les stats d'un PJ (skill → stats). */
|
||||
function _convertStatsCharacter(oldSkill) {
|
||||
const stats = {}
|
||||
for (const stat of ["ame","corps","coeur","esprit"]) {
|
||||
const os = oldSkill[stat] ?? {}
|
||||
const ns = { label: stat, res: Number(os.res ?? 0) }
|
||||
for (const sk of _OLD_SKILLS[stat]) {
|
||||
const val = Number(os[sk]?.value ?? 0) || 0
|
||||
ns[sk] = { label: sk, value: val }
|
||||
}
|
||||
stats[stat] = ns
|
||||
}
|
||||
return stats
|
||||
}
|
||||
|
||||
/** Convertit les stats d'un PNJ (skill → stats avec actuel). */
|
||||
function _convertStatsNPC(oldSkill) {
|
||||
const stats = {}
|
||||
for (const stat of ["ame","corps","coeur","esprit"]) {
|
||||
const res = Number(oldSkill[stat]?.res ?? 0)
|
||||
stats[stat] = { label: stat, res, actuel: res }
|
||||
}
|
||||
return stats
|
||||
}
|
||||
|
||||
/** Convertit les attributs du format plat vers {value, max}. */
|
||||
function _convertAttributs(a = {}) {
|
||||
return {
|
||||
entregent: { value: Number(a.entregent ?? 0), max: Number(a.entregentmax ?? 0) },
|
||||
fortune: { value: Number(a.fortune ?? 0), max: Number(a.fortunemax ?? 0) },
|
||||
reve: { value: Number(a.reve ?? 0), max: Number(a.revemax ?? 0) },
|
||||
vision: { value: Number(a.vision ?? 0), max: Number(a.visionmax ?? 0) },
|
||||
}
|
||||
}
|
||||
|
||||
/** Convertit un item de l'ancien format. Retourne null si ignoré. */
|
||||
function _convertItem(item, warnings) {
|
||||
const os = item.system ?? {}
|
||||
const name = item.name ?? "Sans nom"
|
||||
const base = {
|
||||
name,
|
||||
img: item.img ?? "icons/svg/item-bag.svg",
|
||||
effects: [],
|
||||
flags: {},
|
||||
sort: 0,
|
||||
}
|
||||
|
||||
if (item.type === "anomaly") {
|
||||
const level = Math.max(1, Math.min(4, Number(os.value ?? 1) || 1))
|
||||
const subtype = _anomalyTypeFromName(name)
|
||||
if (subtype === "none")
|
||||
warnings.push(`Impossible de déterminer le type d'anomalie depuis "${name}"`)
|
||||
return { ...base, type: "anomaly", system: {
|
||||
subtype, level, usesRemaining: level,
|
||||
technique: os.technique ?? "", narratif: os.narratif ?? "",
|
||||
}}
|
||||
}
|
||||
|
||||
if (item.type === "aspect") {
|
||||
return { ...base, type: "aspect", system: {
|
||||
valeur: Number(os.value ?? 0) || 0,
|
||||
description: os.narratif ?? os.technique ?? "",
|
||||
}}
|
||||
}
|
||||
|
||||
if (item.type === "item") {
|
||||
const sub = (os.subtype ?? "").toLowerCase()
|
||||
if (sub === "weapon") {
|
||||
if (os.damage) warnings.push(`"${name}" : dégâts "${os.damage}" à saisir manuellement`)
|
||||
return { ...base, type: "weapon", system: {
|
||||
type: "melee", degats: "0", portee: "contact", equipped: false,
|
||||
description: os.technique ?? os.narratif ?? "",
|
||||
}}
|
||||
}
|
||||
if (sub === "armor") {
|
||||
if (os.protection) warnings.push(`"${name}" : protection "${os.protection}" à saisir manuellement`)
|
||||
return { ...base, type: "armure", system: {
|
||||
protection: 1, malus: 1, equipped: false,
|
||||
description: os.technique ?? os.narratif ?? "",
|
||||
}}
|
||||
}
|
||||
return { ...base, type: "equipment", system: {
|
||||
description: os.technique ?? os.narratif ?? "",
|
||||
}}
|
||||
}
|
||||
|
||||
warnings.push(`Type d'item inconnu "${item.type}" pour "${name}", ignoré`)
|
||||
return null
|
||||
}
|
||||
|
||||
/** Convertit un acteur complet de l'ancien format vers le nouveau. */
|
||||
function _convertOldActor(old) {
|
||||
const warnings = []
|
||||
const os = old.system ?? {}
|
||||
const osk = os.skill ?? {}
|
||||
const type = old.type ?? "character"
|
||||
|
||||
// Anomalie : résolution depuis l'index stocké dans os.anomaly
|
||||
const anomalyIdx = Number(os.anomaly ?? 0)
|
||||
const anomalyTypes = osk.anomalytypes ?? _OLD_ANOMALY_TYPES.map(k => `CEL1922.opt.${k}`)
|
||||
const rawAnomalyKey = (anomalyTypes[anomalyIdx] ?? "").split(".").pop() ?? "none"
|
||||
const anomalyType = _OLD_ANOMALY_TYPES.includes(rawAnomalyKey) ? rawAnomalyKey : "none"
|
||||
|
||||
const sys = {
|
||||
concept: os.concept ?? "",
|
||||
metier: os.metier ?? os.concept ?? "",
|
||||
faction: os.faction ?? "",
|
||||
blessures: { lvl: Number(os.blessures?.lvl ?? 0) },
|
||||
destin: { lvl: Number(os.destin?.lvl ?? 0) },
|
||||
spleen: { lvl: Number(os.spleen?.lvl ?? 0) },
|
||||
}
|
||||
|
||||
if (type === "character") {
|
||||
sys.stats = _convertStatsCharacter(osk)
|
||||
sys.anomaly = { type: anomalyType, value: Number(os.anomalyval ?? 0) }
|
||||
sys.attributs = _convertAttributs(os.attributs)
|
||||
sys.historique = os.description ?? ""
|
||||
sys.descriptionPhysique = ""
|
||||
sys.descriptionPsychologique = ""
|
||||
sys.initiative = 0
|
||||
sys.factions = os.factions ?? {}
|
||||
} else {
|
||||
sys.stats = _convertStatsNPC(osk)
|
||||
sys.npcType = os.npcType ?? "standard"
|
||||
sys.historique = os.description ?? ""
|
||||
}
|
||||
|
||||
const items = (old.items ?? []).map(it => _convertItem(it, warnings)).filter(Boolean)
|
||||
|
||||
const actor = {
|
||||
name: old.name ?? "Personnage sans nom",
|
||||
type,
|
||||
img: old.img ?? "icons/svg/mystery-man.svg",
|
||||
system: sys,
|
||||
items,
|
||||
effects: [],
|
||||
folder: null,
|
||||
flags: {},
|
||||
prototypeToken: old.prototypeToken ?? {},
|
||||
}
|
||||
|
||||
return { actor, warnings }
|
||||
}
|
||||
|
||||
/** Ouvre la dialog DialogV2 de migration et gère l'import de l'acteur. */
|
||||
async function _openMigrationDialog() {
|
||||
const content = `
|
||||
<div class="cel-migration-dialog" style="padding:0.5em 0">
|
||||
<p style="margin-bottom:0.8em">${game.i18n.localize("CELESTOPOL.Migration.instructions")}</p>
|
||||
<div style="display:flex;gap:0.5em;align-items:center">
|
||||
<label style="font-weight:bold;white-space:nowrap">
|
||||
${game.i18n.localize("CELESTOPOL.Migration.fileLabel")}
|
||||
</label>
|
||||
<input type="file" id="cel-migration-file" accept=".json" style="flex:1">
|
||||
</div>
|
||||
</div>`
|
||||
|
||||
await foundry.applications.api.DialogV2.wait({
|
||||
window: { title: game.i18n.localize("CELESTOPOL.Migration.title") },
|
||||
classes: ["fvtt-celestopol"],
|
||||
content,
|
||||
buttons: [
|
||||
{
|
||||
label: game.i18n.localize("CELESTOPOL.Migration.importBtn"),
|
||||
icon: "fas fa-file-import",
|
||||
action: "import",
|
||||
default: true,
|
||||
callback: (_event, _btn, dialog) => {
|
||||
const fileInput = dialog.element.querySelector("#cel-migration-file")
|
||||
return fileInput?.files?.[0] ?? null
|
||||
},
|
||||
},
|
||||
{
|
||||
label: game.i18n.localize("Cancel"),
|
||||
icon: "fas fa-times",
|
||||
action: "cancel",
|
||||
callback: () => null,
|
||||
},
|
||||
],
|
||||
submit: async (result) => {
|
||||
if (!result) return
|
||||
const text = await result.text()
|
||||
let old
|
||||
try { old = JSON.parse(text) }
|
||||
catch {
|
||||
ui.notifications.error(game.i18n.localize("CELESTOPOL.Migration.errorParse"))
|
||||
return
|
||||
}
|
||||
|
||||
const { actor, warnings } = _convertOldActor(old)
|
||||
|
||||
try {
|
||||
const created = await Actor.create(actor)
|
||||
ui.notifications.info(game.i18n.format("CELESTOPOL.Migration.success", {
|
||||
name: created.name, count: actor.items.length,
|
||||
}))
|
||||
if (warnings.length) {
|
||||
console.warn(`Célestopol Migration | ${created.name} — avertissements :`)
|
||||
warnings.forEach(w => console.warn(" ⚠", w))
|
||||
ui.notifications.warn(game.i18n.format("CELESTOPOL.Migration.warnings", { count: warnings.length }))
|
||||
}
|
||||
} catch(err) {
|
||||
console.error("Célestopol Migration | Échec de création de l'acteur :", err)
|
||||
ui.notifications.error(game.i18n.localize("CELESTOPOL.Migration.errorCreate"))
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Classe de menu de paramètre pour la migration.
|
||||
* Étend ApplicationV2 (requis par registerMenu) mais surcharge render()
|
||||
* pour déclencher directement la dialog de migration.
|
||||
*/
|
||||
class CelestopolMigrationMenu extends foundry.applications.api.ApplicationV2 {
|
||||
static DEFAULT_OPTIONS = {
|
||||
id: "celestopol-migration",
|
||||
window: { title: "CELESTOPOL.Migration.title" },
|
||||
classes: ["fvtt-celestopol"],
|
||||
position: { width: 480 },
|
||||
}
|
||||
|
||||
/** Surcharge : ignore le rendu AppV2 et ouvre directement la dialog. */
|
||||
async render(...args) {
|
||||
await _openMigrationDialog()
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Settings ───────────────────────────────────────────────────────────── */
|
||||
|
||||
function _registerSettings() {
|
||||
@@ -397,6 +650,16 @@ function _registerSettings() {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
})
|
||||
|
||||
// Entrée de menu : migration depuis l'ancien système
|
||||
game.settings.registerMenu(SYSTEM_ID, "migrateOldSystem", {
|
||||
name: "CELESTOPOL.Setting.migrateOldSystem.name",
|
||||
hint: "CELESTOPOL.Setting.migrateOldSystem.hint",
|
||||
label: "CELESTOPOL.Setting.migrateOldSystem.label",
|
||||
icon: "fas fa-file-import",
|
||||
type: CelestopolMigrationMenu,
|
||||
restricted: true,
|
||||
})
|
||||
}
|
||||
|
||||
async function _createWelcomeChatMessage() {
|
||||
|
||||
Reference in New Issue
Block a user