Import des persos du système précédent
This commit is contained in:
Vendored
+448
@@ -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,
|
||||
|
||||
Vendored
+3
-3
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user