Import des persos du système précédent

This commit is contained in:
2026-05-06 21:31:03 +02:00
parent fbfb265570
commit 73a3381d2a
14 changed files with 3296 additions and 3 deletions
+448
View File
@@ -134,8 +134,456 @@ var TEMPLATE_PARTIALS = [
"systems/fvtt-chroniques-de-l-etrange/templates/apps/cde-wheel-app.html"
];
// src/migration/migrator.js
var ELEMENT_LABEL_TO_KEY = {
"m\xE9tal": "metal",
"metal": "metal",
"eau": "eau",
"terre": "terre",
"feu": "feu",
"bois": "bois"
};
function elementKey(label = "") {
return ELEMENT_LABEL_TO_KEY[label.toLowerCase().trim()] ?? "metal";
}
function heiKey(label = "") {
const l = label.toLowerCase().trim();
if (l === "yin/yang" || l === "yinyang") return "yinyang";
if (l === "yang") return "yang";
return "yin";
}
var SPECIALITY_TO_DISCIPLINE = {
// internalcinnabar
"essence": "internalcinnabar",
"esprit": "internalcinnabar",
"mind": "internalcinnabar",
"purification": "internalcinnabar",
"manipulation": "internalcinnabar",
"aura": "internalcinnabar",
// alchemy
"acupuncture": "alchemy",
"\xE9lixirs": "alchemy",
"elixirs": "alchemy",
"poisons": "alchemy",
"arsenal": "alchemy",
"potions": "alchemy",
// masteryoftheway
"mal\xE9diction": "masteryoftheway",
"malediction": "masteryoftheway",
"transfiguration": "masteryoftheway",
"n\xE9cromancie": "masteryoftheway",
"necromancie": "masteryoftheway",
"contr\xF4le climatique": "masteryoftheway",
"controle climatique": "masteryoftheway",
"magie d'or": "masteryoftheway",
"magie dor": "masteryoftheway",
// exorcism
"invocation": "exorcism",
"pistage": "exorcism",
"tra\xE7age": "exorcism",
"tracage": "exorcism",
"protection": "exorcism",
"ch\xE2timent": "exorcism",
"chatiment": "exorcism",
"domination": "exorcism",
// geomancy
"neutralisation": "geomancy",
"divination": "geomancy",
"pri\xE8re terrestre": "geomancy",
"priere terrestre": "geomancy",
"pri\xE8re c\xE9leste": "geomancy",
"priere celeste": "geomancy",
"g\xE9omancie": "geomancy",
"geomancie": "geomancy",
"feng shui": "geomancy",
"fungseoi": "geomancy"
};
function inferDiscipline(specialityName = "", itemName = "") {
const key = specialityName.toLowerCase().trim();
if (SPECIALITY_TO_DISCIPLINE[key]) return SPECIALITY_TO_DISCIPLINE[key];
const name = itemName.toLowerCase();
if (name.includes("exorcis")) return "exorcism";
if (name.includes("g\xE9omanci") || name.includes("geomanci")) return "geomancy";
if (name.includes("alchimi")) return "alchemy";
if (name.includes("cinnabre") || name.includes("interne")) return "internalcinnabar";
if (name.includes("ma\xEEtrise") || name.includes("maitrise") || name.includes("tao")) return "masteryoftheway";
return "internalcinnabar";
}
function mapActivation(oldActivation = "") {
const s = oldActivation.toLowerCase();
if (s.includes("inflig\xE9s") || s.includes("infliges")) return "damage-inflicted";
if (s.includes("re\xE7us") || s.includes("recus")) return "damage-received";
if (s.includes("r\xE9action") || s.includes("reaction")) return "reaction";
if (s.includes("d\xE9s-fastes") || s.includes("des-fastes") || s.includes("fastes")) return "dice";
if (s.includes("aide")) return "action-aid";
if (s.includes("attaque") && s.includes("d\xE9fense")) 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\xE9fense") || s.includes("defense")) return "action-defense";
return "action-attack";
}
var DEFAULT_ACTOR_IMG = "icons/svg/mystery-man.svg";
var DEFAULT_ITEM_IMG = "icons/svg/item-bag.svg";
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 ?? {};
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:
return migrateEquipmentItem({ ...oldItem, type: "item" });
}
}
function migrateCharacter(old) {
const s = old.system ?? {};
const aspect = {};
for (const [k, v] of Object.entries(s.aspect ?? {})) {
aspect[k] = { chinese: v.chinese ?? "", label: v.label ?? "", value: Number(v.value ?? 0) };
}
const skills = {};
for (const [k, v] of Object.entries(s.skills ?? {})) {
skills[k] = { label: v.label ?? "", specialities: v.specialities ?? "", value: Number(v.value ?? 0) };
}
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) };
}
const component = {};
for (const [k, v] of Object.entries(s.component ?? {})) {
component[k] = { value: v.value ?? "" };
}
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 };
}
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) }
}
}
};
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)
};
}
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}"`);
}
}
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);
}
// src/ui/apps/migration-app.js
var MIGRATION_TEMPLATE = "systems/fvtt-chroniques-de-l-etrange/templates/apps/cde-migration-app.html";
var CDEMigrationApp = 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;
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 })
);
}
}
};
// src/config/settings.js
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,
+3 -3
View File
File diff suppressed because one or more lines are too long