Import des persos du système précédent
This commit is contained in:
@@ -12,12 +12,22 @@
|
||||
*/
|
||||
|
||||
import { SYSTEM_ID } from "./constants.js"
|
||||
import { CDEMigrationApp } from "../ui/apps/migration-app.js"
|
||||
|
||||
/**
|
||||
* Register all world/client settings for the system.
|
||||
* Called during the "init" hook before sheets and data-models are set up.
|
||||
*/
|
||||
export function registerSettings() {
|
||||
game.settings.registerMenu(SYSTEM_ID, "migrationTool", {
|
||||
name: "CDE.MigrationTitle",
|
||||
label: "CDE.MigrationMenuLabel",
|
||||
hint: "CDE.MigrationMenuHint",
|
||||
icon: "fas fa-file-import",
|
||||
type: CDEMigrationApp,
|
||||
restricted: true,
|
||||
})
|
||||
|
||||
game.settings.register(SYSTEM_ID, "loksyuData", {
|
||||
scope: "world",
|
||||
config: false,
|
||||
|
||||
@@ -0,0 +1,412 @@
|
||||
/**
|
||||
* Chroniques de l'Étrange — Système FoundryVTT
|
||||
*
|
||||
* Chroniques de l'Étrange 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 2024–2026 LeRatierBretonnien
|
||||
* @license CC BY-NC-SA 4.0 – https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
*/
|
||||
|
||||
/**
|
||||
* Migrates actor JSON from the legacy CDE system to the current system format.
|
||||
*
|
||||
* This module is pure logic — it does not interact with Foundry APIs directly.
|
||||
* All transformation is done in-memory; the caller is responsible for creating
|
||||
* Foundry documents from the returned data.
|
||||
*/
|
||||
|
||||
// ── Element label → key ──────────────────────────────────────────────────────
|
||||
|
||||
const ELEMENT_LABEL_TO_KEY = {
|
||||
"métal": "metal",
|
||||
"metal": "metal",
|
||||
"eau": "eau",
|
||||
"terre": "terre",
|
||||
"feu": "feu",
|
||||
"bois": "bois",
|
||||
}
|
||||
|
||||
/** Normalise a French element label to its system key (e.g. "Métal" → "metal"). */
|
||||
function elementKey(label = "") {
|
||||
return ELEMENT_LABEL_TO_KEY[label.toLowerCase().trim()] ?? "metal"
|
||||
}
|
||||
|
||||
// ── Hei polarity label → key ──────────────────────────────────────────────────
|
||||
|
||||
function heiKey(label = "") {
|
||||
const l = label.toLowerCase().trim()
|
||||
if (l === "yin/yang" || l === "yinyang") return "yinyang"
|
||||
if (l === "yang") return "yang"
|
||||
return "yin"
|
||||
}
|
||||
|
||||
// ── Spell discipline inference from speciality name ───────────────────────────
|
||||
|
||||
/** Maps French speciality labels (lowercase, accents stripped) → school key. */
|
||||
const SPECIALITY_TO_DISCIPLINE = {
|
||||
// internalcinnabar
|
||||
"essence": "internalcinnabar",
|
||||
"esprit": "internalcinnabar",
|
||||
"mind": "internalcinnabar",
|
||||
"purification": "internalcinnabar",
|
||||
"manipulation": "internalcinnabar",
|
||||
"aura": "internalcinnabar",
|
||||
// alchemy
|
||||
"acupuncture": "alchemy",
|
||||
"élixirs": "alchemy",
|
||||
"elixirs": "alchemy",
|
||||
"poisons": "alchemy",
|
||||
"arsenal": "alchemy",
|
||||
"potions": "alchemy",
|
||||
// masteryoftheway
|
||||
"malédiction": "masteryoftheway",
|
||||
"malediction": "masteryoftheway",
|
||||
"transfiguration": "masteryoftheway",
|
||||
"nécromancie": "masteryoftheway",
|
||||
"necromancie": "masteryoftheway",
|
||||
"contrôle climatique": "masteryoftheway",
|
||||
"controle climatique": "masteryoftheway",
|
||||
"magie d'or": "masteryoftheway",
|
||||
"magie dor": "masteryoftheway",
|
||||
// exorcism
|
||||
"invocation": "exorcism",
|
||||
"pistage": "exorcism",
|
||||
"traçage": "exorcism",
|
||||
"tracage": "exorcism",
|
||||
"protection": "exorcism",
|
||||
"châtiment": "exorcism",
|
||||
"chatiment": "exorcism",
|
||||
"domination": "exorcism",
|
||||
// geomancy
|
||||
"neutralisation": "geomancy",
|
||||
"divination": "geomancy",
|
||||
"prière terrestre": "geomancy",
|
||||
"priere terrestre": "geomancy",
|
||||
"prière céleste": "geomancy",
|
||||
"priere celeste": "geomancy",
|
||||
"géomancie": "geomancy",
|
||||
"geomancie": "geomancy",
|
||||
"feng shui": "geomancy",
|
||||
"fungseoi": "geomancy",
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to infer the magic school (discipline) from a spell's speciality name.
|
||||
* Falls back to scanning the item name for school keywords if needed.
|
||||
*/
|
||||
function inferDiscipline(specialityName = "", itemName = "") {
|
||||
const key = specialityName.toLowerCase().trim()
|
||||
if (SPECIALITY_TO_DISCIPLINE[key]) return SPECIALITY_TO_DISCIPLINE[key]
|
||||
|
||||
// Fuzzy fallback: check item name for school markers
|
||||
const name = itemName.toLowerCase()
|
||||
if (name.includes("exorcis")) return "exorcism"
|
||||
if (name.includes("géomanci") || name.includes("geomanci")) return "geomancy"
|
||||
if (name.includes("alchimi")) return "alchemy"
|
||||
if (name.includes("cinnabre") || name.includes("interne")) return "internalcinnabar"
|
||||
if (name.includes("maîtrise") || name.includes("maitrise") || name.includes("tao")) return "masteryoftheway"
|
||||
|
||||
return "internalcinnabar"
|
||||
}
|
||||
|
||||
// ── KungFu activation mapping ─────────────────────────────────────────────────
|
||||
|
||||
function mapActivation(oldActivation = "") {
|
||||
const s = oldActivation.toLowerCase()
|
||||
if (s.includes("infligés") || s.includes("infliges")) return "damage-inflicted"
|
||||
if (s.includes("reçus") || s.includes("recus")) return "damage-received"
|
||||
if (s.includes("réaction") || s.includes("reaction")) return "reaction"
|
||||
if (s.includes("dés-fastes") || s.includes("des-fastes") || s.includes("fastes")) return "dice"
|
||||
if (s.includes("aide")) return "action-aid"
|
||||
if (s.includes("attaque") && s.includes("défense")) return "action-attack-defense"
|
||||
if (s.includes("attaque") && s.includes("defense")) return "action-attack-defense"
|
||||
if (s.includes("attaque")) return "action-attack"
|
||||
if (s.includes("défense") || s.includes("defense")) return "action-defense"
|
||||
return "action-attack"
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
const DEFAULT_ACTOR_IMG = "icons/svg/mystery-man.svg"
|
||||
const DEFAULT_ITEM_IMG = "icons/svg/item-bag.svg"
|
||||
|
||||
// ── Item migration ────────────────────────────────────────────────────────────
|
||||
|
||||
function migrateEquipmentItem(oldItem) {
|
||||
const s = oldItem.system ?? {}
|
||||
return {
|
||||
name: oldItem.name,
|
||||
type: "item",
|
||||
img: oldItem.img || DEFAULT_ITEM_IMG,
|
||||
system: {
|
||||
reference: s.reference ?? "",
|
||||
description: s.description ?? "",
|
||||
quantity: Number(s.quantity ?? 1),
|
||||
weight: Number(s.weight ?? 0),
|
||||
notes: s.notes ?? "",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function migrateKungfuItem(oldItem) {
|
||||
const s = oldItem.system ?? {}
|
||||
const techs = s.techniques ?? {}
|
||||
|
||||
const migratedTechs = {}
|
||||
for (const key of ["technique1", "technique2", "technique3"]) {
|
||||
const t = techs[key] ?? {}
|
||||
migratedTechs[key] = {
|
||||
check: Boolean(t.check),
|
||||
name: t.name ?? "",
|
||||
activation: mapActivation(t.activation ?? ""),
|
||||
technique: t.technique ?? "",
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: oldItem.name,
|
||||
type: "kungfu",
|
||||
img: oldItem.img || DEFAULT_ITEM_IMG,
|
||||
system: {
|
||||
reference: s.reference ?? "",
|
||||
description: s.description ?? "",
|
||||
orientation: s.orientation || "yin",
|
||||
aspect: s.aspect || "metal",
|
||||
skill: s.skill || "kungfu",
|
||||
speciality: s.speciality ?? "",
|
||||
style: s.style ?? "",
|
||||
techniques: migratedTechs,
|
||||
notes: s.notes ?? "",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function migrateSpellItem(oldItem) {
|
||||
const s = oldItem.system ?? {}
|
||||
return {
|
||||
name: oldItem.name,
|
||||
type: "spell",
|
||||
img: oldItem.img || DEFAULT_ITEM_IMG,
|
||||
system: {
|
||||
reference: s.reference ?? "",
|
||||
description: s.description ?? "",
|
||||
specialityname: s.specialityname ?? "",
|
||||
associatedelement: elementKey(s.associatedelement ?? ""),
|
||||
heiType: heiKey(s.hei ?? ""),
|
||||
heiCost: Number(s.heiCost ?? 0),
|
||||
difficulty: Number(s.difficulty ?? 0),
|
||||
realizationtimeritual: s.realizationtimeritual ?? "",
|
||||
realizationtimeaccelerated: s.realizationtimeaccelerated ?? "",
|
||||
flashback: s.flashback ?? "",
|
||||
components: s.components ?? "",
|
||||
effects: s.effects ?? "",
|
||||
examples: s.examples ?? "",
|
||||
notes: s.notes ?? "",
|
||||
discipline: inferDiscipline(s.specialityname ?? "", oldItem.name ?? ""),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function migrateSupernaturalItem(oldItem) {
|
||||
const s = oldItem.system ?? {}
|
||||
// Old system stored a nested `supernatural: { reference }` — prefer that reference if top-level is empty
|
||||
const nestedRef = s.supernatural?.reference ?? ""
|
||||
return {
|
||||
name: oldItem.name,
|
||||
type: "supernatural",
|
||||
img: oldItem.img || DEFAULT_ITEM_IMG,
|
||||
system: {
|
||||
reference: s.reference || nestedRef,
|
||||
description: s.description ?? "",
|
||||
notes: s.notes ?? "",
|
||||
heiType: "yin",
|
||||
heiCost: 0,
|
||||
trigger: "",
|
||||
effects: "",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function migrateItem(oldItem) {
|
||||
switch (oldItem.type) {
|
||||
case "item": return migrateEquipmentItem(oldItem)
|
||||
case "kungfu": return migrateKungfuItem(oldItem)
|
||||
case "spell": return migrateSpellItem(oldItem)
|
||||
case "supernatural": return migrateSupernaturalItem(oldItem)
|
||||
default:
|
||||
// Unknown item type: keep as generic equipment
|
||||
return migrateEquipmentItem({ ...oldItem, type: "item" })
|
||||
}
|
||||
}
|
||||
|
||||
// ── Actor migration ───────────────────────────────────────────────────────────
|
||||
|
||||
function migrateCharacter(old) {
|
||||
const s = old.system ?? {}
|
||||
|
||||
// aspects: keep only { chinese, label, value }
|
||||
const aspect = {}
|
||||
for (const [k, v] of Object.entries(s.aspect ?? {})) {
|
||||
aspect[k] = { chinese: v.chinese ?? "", label: v.label ?? "", value: Number(v.value ?? 0) }
|
||||
}
|
||||
|
||||
// skills: keep { label, specialities, value }
|
||||
const skills = {}
|
||||
for (const [k, v] of Object.entries(s.skills ?? {})) {
|
||||
skills[k] = { label: v.label ?? "", specialities: v.specialities ?? "", value: Number(v.value ?? 0) }
|
||||
}
|
||||
|
||||
// resources: keep { label, specialities, value, debt }
|
||||
const resources = {}
|
||||
for (const [k, v] of Object.entries(s.resources ?? {})) {
|
||||
resources[k] = { label: v.label ?? "", specialities: v.specialities ?? "", value: Number(v.value ?? 0), debt: Boolean(v.debt) }
|
||||
}
|
||||
|
||||
// components: keep { value }
|
||||
const component = {}
|
||||
for (const [k, v] of Object.entries(s.component ?? {})) {
|
||||
component[k] = { value: v.value ?? "" }
|
||||
}
|
||||
|
||||
// magics: keep { visible, value, speciality: { [key]: { check } } }
|
||||
const MAGIC_SPECIALITIES = {
|
||||
internalcinnabar: ["essence", "mind", "purification", "manipulation", "aura"],
|
||||
alchemy: ["acupuncture", "elixirs", "poisons", "arsenal", "potions"],
|
||||
masteryoftheway: ["curse", "transfiguration", "necromancy", "climatecontrol", "goldenmagic"],
|
||||
exorcism: ["invocation", "tracking", "protection", "punishment", "domination"],
|
||||
geomancy: ["neutralization", "divination", "earthlyprayer", "heavenlyprayer", "fungseoi"],
|
||||
}
|
||||
const magics = {}
|
||||
for (const [school, specs] of Object.entries(MAGIC_SPECIALITIES)) {
|
||||
const om = s.magics?.[school] ?? {}
|
||||
const speciality = {}
|
||||
for (const spec of specs) {
|
||||
speciality[spec] = { check: Boolean(om.speciality?.[spec]?.check) }
|
||||
}
|
||||
magics[school] = { visible: Boolean(om.visible), value: Number(om.value ?? 0), speciality }
|
||||
}
|
||||
|
||||
// threetreasures: strip `min` from heiyang/heiyin; keep dicelevel as-is
|
||||
const tt = s.threetreasures ?? {}
|
||||
const threetreasures = {
|
||||
heiyang: { value: Number(tt.heiyang?.value ?? 0), max: Number(tt.heiyang?.max ?? 0) },
|
||||
heiyin: { value: Number(tt.heiyin?.value ?? 0), max: Number(tt.heiyin?.max ?? 0) },
|
||||
dicelevel: {
|
||||
level0d: {
|
||||
san: { value: Number(tt.dicelevel?.level0d?.san?.value ?? 0), max: Number(tt.dicelevel?.level0d?.san?.max ?? 0) },
|
||||
zing: { value: Number(tt.dicelevel?.level0d?.zing?.value ?? 0), max: Number(tt.dicelevel?.level0d?.zing?.max ?? 0) },
|
||||
},
|
||||
level1d: {
|
||||
san: { value: Number(tt.dicelevel?.level1d?.san?.value ?? 0), max: Number(tt.dicelevel?.level1d?.san?.max ?? 0) },
|
||||
zing: { value: Number(tt.dicelevel?.level1d?.zing?.value ?? 0), max: Number(tt.dicelevel?.level1d?.zing?.max ?? 0) },
|
||||
},
|
||||
level2d: {
|
||||
san: { value: Number(tt.dicelevel?.level2d?.san?.value ?? 0), max: Number(tt.dicelevel?.level2d?.san?.max ?? 0) },
|
||||
zing: { value: Number(tt.dicelevel?.level2d?.zing?.value ?? 0), max: Number(tt.dicelevel?.level2d?.zing?.max ?? 0) },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// biography (old separate field) merged into description
|
||||
const description = s.description || s.biography || ""
|
||||
|
||||
return {
|
||||
name: old.name,
|
||||
type: "character",
|
||||
img: old.img || DEFAULT_ACTOR_IMG,
|
||||
system: {
|
||||
concept: s.concept ?? "",
|
||||
guardian: parseInt(s.guardian ?? "0") || 0,
|
||||
initiative: Number(s.initiative ?? 1),
|
||||
anti_initiative: Number(s.anti_initiative ?? 24),
|
||||
description,
|
||||
aspect,
|
||||
skills,
|
||||
resources,
|
||||
component,
|
||||
magics,
|
||||
threetreasures,
|
||||
experience: {
|
||||
value: Number(s.experience?.value ?? 0),
|
||||
max: Number(s.experience?.max ?? 0),
|
||||
min: Number(s.experience?.min ?? 0),
|
||||
},
|
||||
},
|
||||
items: (old.items ?? []).map(migrateItem),
|
||||
}
|
||||
}
|
||||
|
||||
function migrateNpc(old) {
|
||||
const s = old.system ?? {}
|
||||
|
||||
const aptitudes = {}
|
||||
for (const [k, v] of Object.entries(s.aptitudes ?? {})) {
|
||||
aptitudes[k] = { value: Number(v.value ?? 0), speciality: v.speciality ?? "" }
|
||||
}
|
||||
|
||||
return {
|
||||
name: old.name,
|
||||
type: "npc",
|
||||
img: old.img || DEFAULT_ACTOR_IMG,
|
||||
system: {
|
||||
type: s.type ?? "",
|
||||
// Old system had separate `levelofthreat`/`powerofnuisance` as numbers
|
||||
// and string copies `threat`/`nuisance` — use the numeric fields
|
||||
threat: Number(s.levelofthreat ?? s.threat ?? 0),
|
||||
nuisance: Number(s.powerofnuisance ?? s.nuisance ?? 0),
|
||||
initiative: Number(s.initiative ?? 1),
|
||||
anti_initiative: Number(s.anti_initiative ?? 24),
|
||||
aptitudes,
|
||||
vitality: {
|
||||
value: Number(s.vitality?.value ?? 0),
|
||||
calcul: Number(s.vitality?.calcul ?? 0),
|
||||
note: s.vitality?.note ?? "",
|
||||
},
|
||||
hei: {
|
||||
value: Number(s.hei?.value ?? 0),
|
||||
calcul: Number(s.hei?.calcul ?? 0),
|
||||
note: s.hei?.note ?? "",
|
||||
},
|
||||
description: s.description ?? "",
|
||||
},
|
||||
items: (old.items ?? []).map(migrateItem),
|
||||
}
|
||||
}
|
||||
|
||||
// ── Public API ────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Migrate a single legacy actor JSON to the current system format.
|
||||
*
|
||||
* @param {object} oldJson Parsed JSON from the old system export
|
||||
* @returns {{ name: string, type: string, img: string, system: object, items: object[] }}
|
||||
* @throws {Error} if the actor type is unrecognised
|
||||
*/
|
||||
export function migrateActor(oldJson) {
|
||||
switch (oldJson.type) {
|
||||
case "character": return migrateCharacter(oldJson)
|
||||
case "npc": return migrateNpc(oldJson)
|
||||
default:
|
||||
throw new Error(`Unknown actor type "${oldJson.type}" in "${oldJson.name}"`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse one or more legacy JSON strings and return migrated actor data.
|
||||
* Accepts a single actor object or an array of actor objects in one file.
|
||||
*
|
||||
* @param {string} jsonText Raw JSON text from a file
|
||||
* @returns {Array<{ name, type, img, system, items }>}
|
||||
*/
|
||||
export function parseLegacyJson(jsonText) {
|
||||
const parsed = JSON.parse(jsonText)
|
||||
if (typeof parsed !== "object" || parsed === null) {
|
||||
throw new Error("Le fichier JSON doit contenir un objet acteur ou un tableau d'acteurs")
|
||||
}
|
||||
const actors = Array.isArray(parsed) ? parsed : [parsed]
|
||||
return actors.map(migrateActor)
|
||||
}
|
||||
@@ -15,3 +15,4 @@ export { CDELoksyuApp } from "./loksyu-app.js"
|
||||
export { CDETinjiApp } from "./tinji-app.js"
|
||||
export { updateLoksyuFromRoll, updateTinjiFromRoll } from "./singletons.js"
|
||||
export { CDEWheelApp } from "./wheel-app.js"
|
||||
export { CDEMigrationApp } from "./migration-app.js"
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* Chroniques de l'Étrange — Système FoundryVTT
|
||||
*
|
||||
* Chroniques de l'Étrange 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 2024–2026 LeRatierBretonnien
|
||||
* @license CC BY-NC-SA 4.0 – https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
*/
|
||||
|
||||
import { parseLegacyJson } from "../../migration/migrator.js"
|
||||
|
||||
const MIGRATION_TEMPLATE = "systems/fvtt-chroniques-de-l-etrange/templates/apps/cde-migration-app.html"
|
||||
|
||||
/**
|
||||
* Dialog for importing legacy CDE actor JSON files into the current system.
|
||||
*
|
||||
* Accessible via the System Settings menu (registerMenu).
|
||||
* Supports multi-file selection and shows a preview table before importing.
|
||||
*/
|
||||
export class CDEMigrationApp extends foundry.applications.api.HandlebarsApplicationMixin(
|
||||
foundry.applications.api.ApplicationV2
|
||||
) {
|
||||
static DEFAULT_OPTIONS = {
|
||||
id: "cde-migration-app",
|
||||
classes: ["cde-migration-app"],
|
||||
tag: "div",
|
||||
window: {
|
||||
title: "CDE.MigrationTitle",
|
||||
icon: "fas fa-file-import",
|
||||
resizable: false,
|
||||
},
|
||||
position: { width: 560, height: "auto" },
|
||||
actions: {
|
||||
clearFiles: CDEMigrationApp.#clearFiles,
|
||||
doImport: CDEMigrationApp.#doImport,
|
||||
},
|
||||
}
|
||||
|
||||
static PARTS = {
|
||||
form: { template: MIGRATION_TEMPLATE },
|
||||
}
|
||||
|
||||
/** @type {Array<{name: string, type: string, img: string, system: object, items: object[], _srcFile: string}>} */
|
||||
#pending = []
|
||||
|
||||
/** @type {string[]} - error messages per file */
|
||||
#errors = []
|
||||
|
||||
async _prepareContext(options) {
|
||||
return {
|
||||
pending: this.#pending,
|
||||
errors: this.#errors,
|
||||
hasPending: this.#pending.length > 0,
|
||||
hasErrors: this.#errors.length > 0,
|
||||
count: this.#pending.length,
|
||||
}
|
||||
}
|
||||
|
||||
/** After render, wire up the file input. */
|
||||
_onRender(context, options) {
|
||||
super._onRender(context, options)
|
||||
const input = this.element.querySelector(".cde-migration-file-input")
|
||||
input?.addEventListener("change", this.#onFileChange.bind(this))
|
||||
|
||||
const dropZone = this.element.querySelector(".cde-migration-drop-zone")
|
||||
if (dropZone) {
|
||||
dropZone.addEventListener("dragover", (e) => { e.preventDefault(); dropZone.classList.add("is-dragover") })
|
||||
dropZone.addEventListener("dragleave", () => dropZone.classList.remove("is-dragover"))
|
||||
dropZone.addEventListener("drop", (e) => {
|
||||
e.preventDefault()
|
||||
dropZone.classList.remove("is-dragover")
|
||||
this.#processFiles(Array.from(e.dataTransfer.files))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async #onFileChange(event) {
|
||||
const files = Array.from(event.target.files ?? [])
|
||||
event.target.value = ""
|
||||
await this.#processFiles(files)
|
||||
}
|
||||
|
||||
async #processFiles(files) {
|
||||
for (const file of files) {
|
||||
if (!file.name.endsWith(".json")) {
|
||||
this.#errors.push(game.i18n.format("CDE.MigrationErrorNotJson", { file: file.name }))
|
||||
continue
|
||||
}
|
||||
try {
|
||||
const text = await file.text()
|
||||
const actors = parseLegacyJson(text)
|
||||
for (const actor of actors) {
|
||||
actor._srcFile = file.name
|
||||
// Avoid duplicates by name
|
||||
if (!this.#pending.some(p => p.name === actor.name)) {
|
||||
this.#pending.push(actor)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
this.#errors.push(game.i18n.format("CDE.MigrationErrorParse", { file: file.name, error: err.message }))
|
||||
}
|
||||
}
|
||||
this.render()
|
||||
}
|
||||
|
||||
static async #clearFiles() {
|
||||
this.#pending = []
|
||||
this.#errors = []
|
||||
this.render()
|
||||
}
|
||||
|
||||
static async #doImport() {
|
||||
if (!this.#pending.length) return
|
||||
|
||||
const created = []
|
||||
const failed = []
|
||||
|
||||
for (const data of this.#pending) {
|
||||
try {
|
||||
const { _srcFile, ...actorData } = data
|
||||
const actor = await Actor.create(actorData)
|
||||
created.push(actor.name)
|
||||
} catch (err) {
|
||||
failed.push(`${data.name}: ${err.message}`)
|
||||
console.error(`CHRONIQUESDELETRANGE | Migration failed for "${data.name}":`, err)
|
||||
}
|
||||
}
|
||||
|
||||
this.#pending = []
|
||||
this.#errors = failed
|
||||
this.render()
|
||||
|
||||
if (created.length) {
|
||||
ui.notifications.info(
|
||||
game.i18n.format("CDE.MigrationSuccess", { count: created.length, names: created.join(", ") })
|
||||
)
|
||||
}
|
||||
if (failed.length) {
|
||||
ui.notifications.warn(
|
||||
game.i18n.format("CDE.MigrationPartialError", { count: failed.length })
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user