Import de personnages du précédent système
Release Creation / build (release) Successful in 1m21s

This commit is contained in:
2026-04-27 22:29:49 +02:00
parent 58478d56ea
commit 8123b53f75
32 changed files with 3846 additions and 84 deletions
+263
View File
@@ -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() {