This commit is contained in:
@@ -4443,6 +4443,105 @@ ol.item-list li.item .item-controls a.item-control:hover {
|
|||||||
color: #7d94b8;
|
color: #7d94b8;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
/* Duplicate row highlight */
|
||||||
|
.cde-migration-row-duplicate {
|
||||||
|
background: rgba(212, 175, 55, 0.15);
|
||||||
|
}
|
||||||
|
.cde-migration-duplicate-icon {
|
||||||
|
color: #d4af37;
|
||||||
|
margin-right: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
/* Confirmation bar */
|
||||||
|
.cde-migration-confirm-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid rgba(212, 175, 55, 0.7);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgba(212, 175, 55, 0.1);
|
||||||
|
}
|
||||||
|
.cde-migration-confirm-msg {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #e2e8f4;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.cde-migration-confirm-msg i {
|
||||||
|
color: #d4af37;
|
||||||
|
}
|
||||||
|
.cde-migration-confirm-duplicates {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #e07070;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.cde-migration-confirm-duplicates i {
|
||||||
|
color: #e07070;
|
||||||
|
}
|
||||||
|
.cde-migration-confirm-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.cde-migration-confirm-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 9px 24px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #4a9eff;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: filter 0.15s;
|
||||||
|
}
|
||||||
|
.cde-migration-confirm-btn:hover {
|
||||||
|
filter: brightness(1.15);
|
||||||
|
}
|
||||||
|
.cde-migration-cancel-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 9px 24px;
|
||||||
|
border: 1px solid #1a2436;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: none;
|
||||||
|
color: #7d94b8;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.15s, border-color 0.15s;
|
||||||
|
}
|
||||||
|
.cde-migration-cancel-btn:hover {
|
||||||
|
color: #e04444;
|
||||||
|
border-color: #e04444;
|
||||||
|
}
|
||||||
|
/* Progress section */
|
||||||
|
.cde-migration-progress {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #e2e8f4;
|
||||||
|
}
|
||||||
|
.cde-migration-progress i {
|
||||||
|
color: #4a9eff;
|
||||||
|
}
|
||||||
|
.cde-migration-progress-count {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #4a9eff;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
.cde-welcome-message {
|
.cde-welcome-message {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -4631,6 +4631,112 @@ ol.item-list {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Duplicate row highlight */
|
||||||
|
.cde-migration-row-duplicate {
|
||||||
|
background: fadeout(#d4af37, 85%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cde-migration-duplicate-icon {
|
||||||
|
color: #d4af37;
|
||||||
|
margin-right: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Confirmation bar */
|
||||||
|
.cde-migration-confirm-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid fadeout(#d4af37, 30%);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: fadeout(#d4af37, 90%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cde-migration-confirm-msg {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: @cde-text;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
i { color: #d4af37; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.cde-migration-confirm-duplicates {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #e07070;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
i { color: #e07070; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.cde-migration-confirm-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cde-migration-confirm-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 9px 24px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: @cde-spell;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: filter 0.15s;
|
||||||
|
|
||||||
|
&:hover { filter: brightness(1.15); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.cde-migration-cancel-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 9px 24px;
|
||||||
|
border: 1px solid @cde-border;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: none;
|
||||||
|
color: @cde-muted;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.15s, border-color 0.15s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #e04444;
|
||||||
|
border-color: #e04444;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Progress section */
|
||||||
|
.cde-migration-progress {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: @cde-text;
|
||||||
|
|
||||||
|
i { color: @cde-spell; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.cde-migration-progress-count {
|
||||||
|
font-weight: 700;
|
||||||
|
color: @cde-spell;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Welcome message
|
// Welcome message
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
Vendored
+143
-8
@@ -311,6 +311,89 @@ function migrateSupernaturalItem(oldItem) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
function migrateWeaponItem(oldItem) {
|
||||||
|
const s = oldItem.system ?? {};
|
||||||
|
return {
|
||||||
|
name: oldItem.name,
|
||||||
|
type: "weapon",
|
||||||
|
img: oldItem.img || DEFAULT_ITEM_IMG,
|
||||||
|
system: {
|
||||||
|
reference: s.reference ?? "",
|
||||||
|
description: s.description ?? "",
|
||||||
|
hasSpeciality: Boolean(s.hasSpeciality),
|
||||||
|
weaponType: s.weaponType || "melee",
|
||||||
|
material: s.material ?? "",
|
||||||
|
damageAspect: elementKey(s.damageAspect ?? ""),
|
||||||
|
damageBase: Number(s.damageBase ?? 0),
|
||||||
|
range: s.range || "contact",
|
||||||
|
obtainLevel: Number(s.obtainLevel ?? 0),
|
||||||
|
obtainDifficulty: Number(s.obtainDifficulty ?? 0),
|
||||||
|
quantity: Number(s.quantity ?? 1),
|
||||||
|
notes: s.notes ?? ""
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function migrateArmorItem(oldItem) {
|
||||||
|
const s = oldItem.system ?? {};
|
||||||
|
return {
|
||||||
|
name: oldItem.name,
|
||||||
|
type: "armor",
|
||||||
|
img: oldItem.img || DEFAULT_ITEM_IMG,
|
||||||
|
system: {
|
||||||
|
reference: s.reference ?? "",
|
||||||
|
description: s.description ?? "",
|
||||||
|
protectionValue: Number(s.protectionValue ?? 0),
|
||||||
|
domain: s.domain ?? "",
|
||||||
|
obtainLevel: Number(s.obtainLevel ?? 0),
|
||||||
|
obtainDifficulty: Number(s.obtainDifficulty ?? 0),
|
||||||
|
quantity: Number(s.quantity ?? 1),
|
||||||
|
notes: s.notes ?? ""
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function migrateSanheiItem(oldItem) {
|
||||||
|
const s = oldItem.system ?? {};
|
||||||
|
const props = s.properties ?? {};
|
||||||
|
const propSchema = (p) => ({
|
||||||
|
name: p?.name ?? "",
|
||||||
|
heiCost: Number(p?.heiCost ?? 0),
|
||||||
|
heiType: heiKey(p?.heiType ?? ""),
|
||||||
|
description: p?.description ?? ""
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
name: oldItem.name,
|
||||||
|
type: "sanhei",
|
||||||
|
img: oldItem.img || DEFAULT_ITEM_IMG,
|
||||||
|
system: {
|
||||||
|
reference: s.reference ?? "",
|
||||||
|
description: s.description ?? "",
|
||||||
|
heiType: heiKey(s.heiType ?? ""),
|
||||||
|
properties: {
|
||||||
|
prop1: propSchema(props.prop1),
|
||||||
|
prop2: propSchema(props.prop2),
|
||||||
|
prop3: propSchema(props.prop3)
|
||||||
|
},
|
||||||
|
notes: s.notes ?? ""
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function migrateIngredientItem(oldItem) {
|
||||||
|
const s = oldItem.system ?? {};
|
||||||
|
return {
|
||||||
|
name: oldItem.name,
|
||||||
|
type: "ingredient",
|
||||||
|
img: oldItem.img || DEFAULT_ITEM_IMG,
|
||||||
|
system: {
|
||||||
|
reference: s.reference ?? "",
|
||||||
|
description: s.description ?? "",
|
||||||
|
school: s.school ?? "all",
|
||||||
|
obtainLevel: Number(s.obtainLevel ?? 0),
|
||||||
|
obtainDifficulty: Number(s.obtainDifficulty ?? 0),
|
||||||
|
quantity: Number(s.quantity ?? 1),
|
||||||
|
notes: s.notes ?? ""
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
function migrateItem(oldItem) {
|
function migrateItem(oldItem) {
|
||||||
switch (oldItem.type) {
|
switch (oldItem.type) {
|
||||||
case "item":
|
case "item":
|
||||||
@@ -321,6 +404,14 @@ function migrateItem(oldItem) {
|
|||||||
return migrateSpellItem(oldItem);
|
return migrateSpellItem(oldItem);
|
||||||
case "supernatural":
|
case "supernatural":
|
||||||
return migrateSupernaturalItem(oldItem);
|
return migrateSupernaturalItem(oldItem);
|
||||||
|
case "weapon":
|
||||||
|
return migrateWeaponItem(oldItem);
|
||||||
|
case "armor":
|
||||||
|
return migrateArmorItem(oldItem);
|
||||||
|
case "sanhei":
|
||||||
|
return migrateSanheiItem(oldItem);
|
||||||
|
case "ingredient":
|
||||||
|
return migrateIngredientItem(oldItem);
|
||||||
default:
|
default:
|
||||||
return migrateEquipmentItem({ ...oldItem, type: "item" });
|
return migrateEquipmentItem({ ...oldItem, type: "item" });
|
||||||
}
|
}
|
||||||
@@ -394,6 +485,7 @@ function migrateCharacter(old) {
|
|||||||
resources,
|
resources,
|
||||||
component,
|
component,
|
||||||
magics,
|
magics,
|
||||||
|
magicOrder: [],
|
||||||
threetreasures,
|
threetreasures,
|
||||||
experience: {
|
experience: {
|
||||||
value: Number(s.experience?.value ?? 0),
|
value: Number(s.experience?.value ?? 0),
|
||||||
@@ -471,10 +563,12 @@ var CDEMigrationApp = class _CDEMigrationApp extends foundry.applications.api.Ha
|
|||||||
icon: "fas fa-file-import",
|
icon: "fas fa-file-import",
|
||||||
resizable: false
|
resizable: false
|
||||||
},
|
},
|
||||||
position: { width: 560, height: "auto" },
|
position: { width: 600, height: "auto" },
|
||||||
actions: {
|
actions: {
|
||||||
clearFiles: _CDEMigrationApp.#clearFiles,
|
clearFiles: _CDEMigrationApp.#clearFiles,
|
||||||
doImport: _CDEMigrationApp.#doImport
|
doImport: _CDEMigrationApp.#doImport,
|
||||||
|
confirmImport: _CDEMigrationApp.#confirmImport,
|
||||||
|
cancelImport: _CDEMigrationApp.#cancelImport
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
static PARTS = {
|
static PARTS = {
|
||||||
@@ -484,13 +578,28 @@ var CDEMigrationApp = class _CDEMigrationApp extends foundry.applications.api.Ha
|
|||||||
#pending = [];
|
#pending = [];
|
||||||
/** @type {string[]} - error messages per file */
|
/** @type {string[]} - error messages per file */
|
||||||
#errors = [];
|
#errors = [];
|
||||||
|
/** @type {"idle"|"confirm"|"importing"} */
|
||||||
|
#importState = "idle";
|
||||||
|
/** @type {number} - actors created so far (during importing) */
|
||||||
|
#progress = 0;
|
||||||
async _prepareContext(options) {
|
async _prepareContext(options) {
|
||||||
|
const enrichDuplicate = (a) => ({
|
||||||
|
...a,
|
||||||
|
_duplicate: game.actors?.getName(a.name) !== null
|
||||||
|
});
|
||||||
|
const pending = this.#pending.map(enrichDuplicate);
|
||||||
|
const duplicateCount = pending.filter((a) => a._duplicate).length;
|
||||||
return {
|
return {
|
||||||
pending: this.#pending,
|
pending,
|
||||||
errors: this.#errors,
|
errors: this.#errors,
|
||||||
hasPending: this.#pending.length > 0,
|
hasPending: this.#pending.length > 0,
|
||||||
hasErrors: this.#errors.length > 0,
|
hasErrors: this.#errors.length > 0,
|
||||||
count: this.#pending.length
|
hasDuplicates: duplicateCount > 0,
|
||||||
|
duplicateCount,
|
||||||
|
count: this.#pending.length,
|
||||||
|
importState: this.#importState,
|
||||||
|
progress: this.#progress,
|
||||||
|
total: this.#pending.length
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
/** After render, wire up the file input. */
|
/** After render, wire up the file input. */
|
||||||
@@ -528,37 +637,59 @@ var CDEMigrationApp = class _CDEMigrationApp extends foundry.applications.api.Ha
|
|||||||
const actors = parseLegacyJson(text);
|
const actors = parseLegacyJson(text);
|
||||||
for (const actor of actors) {
|
for (const actor of actors) {
|
||||||
actor._srcFile = file.name;
|
actor._srcFile = file.name;
|
||||||
if (!this.#pending.some((p) => p.name === actor.name)) {
|
if (this.#pending.some((p) => p.name === actor.name)) {
|
||||||
this.#pending.push(actor);
|
this.#errors.push(`\xAB ${actor.name} \xBB ignor\xE9 (nom d\xE9j\xE0 dans la liste d'attente, fichier \xAB ${file.name} \xBB)`);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
this.#pending.push(actor);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.#errors.push(game.i18n.format("CDE.MigrationErrorParse", { file: file.name, error: err.message }));
|
this.#errors.push(game.i18n.format("CDE.MigrationErrorParse", { file: file.name, error: err.message }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.#importState = "idle";
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
static async #clearFiles() {
|
static async #clearFiles() {
|
||||||
this.#pending = [];
|
this.#pending = [];
|
||||||
this.#errors = [];
|
this.#errors = [];
|
||||||
|
this.#importState = "idle";
|
||||||
|
this.#progress = 0;
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
/** First click: switch to confirmation state instead of importing immediately. */
|
||||||
static async #doImport() {
|
static async #doImport() {
|
||||||
if (!this.#pending.length) return;
|
if (!this.#pending.length) return;
|
||||||
|
this.#importState = "confirm";
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
/** Second click: actually perform the import. */
|
||||||
|
static async #confirmImport() {
|
||||||
|
if (!this.#pending.length) return;
|
||||||
|
this.#importState = "importing";
|
||||||
|
this.#progress = 0;
|
||||||
|
this.render();
|
||||||
|
const total = this.#pending.length;
|
||||||
const created = [];
|
const created = [];
|
||||||
const failed = [];
|
const failed = [];
|
||||||
for (const data of this.#pending) {
|
for (let i = 0; i < total; i++) {
|
||||||
|
const data = this.#pending[i];
|
||||||
try {
|
try {
|
||||||
const { _srcFile, ...actorData } = data;
|
const { _srcFile, ...actorData } = data;
|
||||||
const actor = await Actor.create(actorData);
|
const actor = await Actor.create(actorData);
|
||||||
created.push(actor.name);
|
created.push(actor.name);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
failed.push(`${data.name}: ${err.message}`);
|
failed.push(`${data.name}: ${err.message}`);
|
||||||
console.error(`CHRONIQUESDELETRANGE | Migration failed for "${data.name}":`, err);
|
console.error(`CHRONIQUESDELETRANGE | Import failed for "${data.name}":`, err);
|
||||||
}
|
}
|
||||||
|
this.#progress = i + 1;
|
||||||
|
const progEl = this.element?.querySelector(".cde-migration-progress-count");
|
||||||
|
if (progEl) progEl.textContent = `${this.#progress}/${total}`;
|
||||||
}
|
}
|
||||||
this.#pending = [];
|
this.#pending = [];
|
||||||
this.#errors = failed;
|
this.#errors = failed;
|
||||||
|
this.#importState = "idle";
|
||||||
|
this.#progress = 0;
|
||||||
this.render();
|
this.render();
|
||||||
if (created.length) {
|
if (created.length) {
|
||||||
ui.notifications.info(
|
ui.notifications.info(
|
||||||
@@ -571,6 +702,10 @@ var CDEMigrationApp = class _CDEMigrationApp extends foundry.applications.api.Ha
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
static async #cancelImport() {
|
||||||
|
this.#importState = "idle";
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// src/config/settings.js
|
// src/config/settings.js
|
||||||
|
|||||||
Vendored
+2
-2
File diff suppressed because one or more lines are too long
@@ -150,6 +150,11 @@
|
|||||||
"CDE.MigrationPartialError": "{count} personnage(s) n'ont pas pu être importés.",
|
"CDE.MigrationPartialError": "{count} personnage(s) n'ont pas pu être importés.",
|
||||||
"CDE.MigrationErrorNotJson": "Le fichier « {file} » n'est pas un fichier JSON.",
|
"CDE.MigrationErrorNotJson": "Le fichier « {file} » n'est pas un fichier JSON.",
|
||||||
"CDE.MigrationErrorParse": "Erreur lors de la lecture de « {file} » : {error}",
|
"CDE.MigrationErrorParse": "Erreur lors de la lecture de « {file} » : {error}",
|
||||||
|
"CDE.MigrationConfirmAction": "Confirmer l'importation",
|
||||||
|
"CDE.MigrationDuplicate": "Ce nom existe déjà dans le monde",
|
||||||
|
"CDE.MigrationDuplicateCount": "{count} personnage(s) existent déjà dans le monde",
|
||||||
|
"CDE.MigrationImportConfirm": "Vous allez importer {count} personnage(s). Confirmez-vous ?",
|
||||||
|
"CDE.MigrationImporting": "Importation en cours...",
|
||||||
"CDE.InitiativeWheel": "Roue d'Initiative",
|
"CDE.InitiativeWheel": "Roue d'Initiative",
|
||||||
"CDE.InitiativeWheelOpen": "Ouvrir la Roue d'Initiative",
|
"CDE.InitiativeWheelOpen": "Ouvrir la Roue d'Initiative",
|
||||||
"CDE.InitiativeWheelHint": "Roue d'initiative – Les Chroniques de l'Étrange",
|
"CDE.InitiativeWheelHint": "Roue d'initiative – Les Chroniques de l'Étrange",
|
||||||
|
|||||||
@@ -231,12 +231,103 @@ function migrateSupernaturalItem(oldItem) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function migrateWeaponItem(oldItem) {
|
||||||
|
const s = oldItem.system ?? {}
|
||||||
|
return {
|
||||||
|
name: oldItem.name,
|
||||||
|
type: "weapon",
|
||||||
|
img: oldItem.img || DEFAULT_ITEM_IMG,
|
||||||
|
system: {
|
||||||
|
reference: s.reference ?? "",
|
||||||
|
description: s.description ?? "",
|
||||||
|
hasSpeciality: Boolean(s.hasSpeciality),
|
||||||
|
weaponType: s.weaponType || "melee",
|
||||||
|
material: s.material ?? "",
|
||||||
|
damageAspect: elementKey(s.damageAspect ?? ""),
|
||||||
|
damageBase: Number(s.damageBase ?? 0),
|
||||||
|
range: s.range || "contact",
|
||||||
|
obtainLevel: Number(s.obtainLevel ?? 0),
|
||||||
|
obtainDifficulty: Number(s.obtainDifficulty ?? 0),
|
||||||
|
quantity: Number(s.quantity ?? 1),
|
||||||
|
notes: s.notes ?? "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrateArmorItem(oldItem) {
|
||||||
|
const s = oldItem.system ?? {}
|
||||||
|
return {
|
||||||
|
name: oldItem.name,
|
||||||
|
type: "armor",
|
||||||
|
img: oldItem.img || DEFAULT_ITEM_IMG,
|
||||||
|
system: {
|
||||||
|
reference: s.reference ?? "",
|
||||||
|
description: s.description ?? "",
|
||||||
|
protectionValue: Number(s.protectionValue ?? 0),
|
||||||
|
domain: s.domain ?? "",
|
||||||
|
obtainLevel: Number(s.obtainLevel ?? 0),
|
||||||
|
obtainDifficulty: Number(s.obtainDifficulty ?? 0),
|
||||||
|
quantity: Number(s.quantity ?? 1),
|
||||||
|
notes: s.notes ?? "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrateSanheiItem(oldItem) {
|
||||||
|
const s = oldItem.system ?? {}
|
||||||
|
const props = s.properties ?? {}
|
||||||
|
const propSchema = (p) => ({
|
||||||
|
name: p?.name ?? "",
|
||||||
|
heiCost: Number(p?.heiCost ?? 0),
|
||||||
|
heiType: heiKey(p?.heiType ?? ""),
|
||||||
|
description: p?.description ?? "",
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
name: oldItem.name,
|
||||||
|
type: "sanhei",
|
||||||
|
img: oldItem.img || DEFAULT_ITEM_IMG,
|
||||||
|
system: {
|
||||||
|
reference: s.reference ?? "",
|
||||||
|
description: s.description ?? "",
|
||||||
|
heiType: heiKey(s.heiType ?? ""),
|
||||||
|
properties: {
|
||||||
|
prop1: propSchema(props.prop1),
|
||||||
|
prop2: propSchema(props.prop2),
|
||||||
|
prop3: propSchema(props.prop3),
|
||||||
|
},
|
||||||
|
notes: s.notes ?? "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrateIngredientItem(oldItem) {
|
||||||
|
const s = oldItem.system ?? {}
|
||||||
|
return {
|
||||||
|
name: oldItem.name,
|
||||||
|
type: "ingredient",
|
||||||
|
img: oldItem.img || DEFAULT_ITEM_IMG,
|
||||||
|
system: {
|
||||||
|
reference: s.reference ?? "",
|
||||||
|
description: s.description ?? "",
|
||||||
|
school: s.school ?? "all",
|
||||||
|
obtainLevel: Number(s.obtainLevel ?? 0),
|
||||||
|
obtainDifficulty: Number(s.obtainDifficulty ?? 0),
|
||||||
|
quantity: Number(s.quantity ?? 1),
|
||||||
|
notes: s.notes ?? "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function migrateItem(oldItem) {
|
function migrateItem(oldItem) {
|
||||||
switch (oldItem.type) {
|
switch (oldItem.type) {
|
||||||
case "item": return migrateEquipmentItem(oldItem)
|
case "item": return migrateEquipmentItem(oldItem)
|
||||||
case "kungfu": return migrateKungfuItem(oldItem)
|
case "kungfu": return migrateKungfuItem(oldItem)
|
||||||
case "spell": return migrateSpellItem(oldItem)
|
case "spell": return migrateSpellItem(oldItem)
|
||||||
case "supernatural": return migrateSupernaturalItem(oldItem)
|
case "supernatural": return migrateSupernaturalItem(oldItem)
|
||||||
|
case "weapon": return migrateWeaponItem(oldItem)
|
||||||
|
case "armor": return migrateArmorItem(oldItem)
|
||||||
|
case "sanhei": return migrateSanheiItem(oldItem)
|
||||||
|
case "ingredient": return migrateIngredientItem(oldItem)
|
||||||
default:
|
default:
|
||||||
// Unknown item type: keep as generic equipment
|
// Unknown item type: keep as generic equipment
|
||||||
return migrateEquipmentItem({ ...oldItem, type: "item" })
|
return migrateEquipmentItem({ ...oldItem, type: "item" })
|
||||||
@@ -329,6 +420,7 @@ function migrateCharacter(old) {
|
|||||||
resources,
|
resources,
|
||||||
component,
|
component,
|
||||||
magics,
|
magics,
|
||||||
|
magicOrder: [],
|
||||||
threetreasures,
|
threetreasures,
|
||||||
experience: {
|
experience: {
|
||||||
value: Number(s.experience?.value ?? 0),
|
value: Number(s.experience?.value ?? 0),
|
||||||
|
|||||||
@@ -1,16 +1,3 @@
|
|||||||
/**
|
|
||||||
* 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"
|
import { parseLegacyJson } from "../../migration/migrator.js"
|
||||||
|
|
||||||
const MIGRATION_TEMPLATE = "systems/fvtt-chroniques-de-l-etrange/templates/apps/cde-migration-app.html"
|
const MIGRATION_TEMPLATE = "systems/fvtt-chroniques-de-l-etrange/templates/apps/cde-migration-app.html"
|
||||||
@@ -33,10 +20,12 @@ export class CDEMigrationApp extends foundry.applications.api.HandlebarsApplicat
|
|||||||
icon: "fas fa-file-import",
|
icon: "fas fa-file-import",
|
||||||
resizable: false,
|
resizable: false,
|
||||||
},
|
},
|
||||||
position: { width: 560, height: "auto" },
|
position: { width: 600, height: "auto" },
|
||||||
actions: {
|
actions: {
|
||||||
clearFiles: CDEMigrationApp.#clearFiles,
|
clearFiles: CDEMigrationApp.#clearFiles,
|
||||||
doImport: CDEMigrationApp.#doImport,
|
doImport: CDEMigrationApp.#doImport,
|
||||||
|
confirmImport: CDEMigrationApp.#confirmImport,
|
||||||
|
cancelImport: CDEMigrationApp.#cancelImport,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,13 +39,31 @@ export class CDEMigrationApp extends foundry.applications.api.HandlebarsApplicat
|
|||||||
/** @type {string[]} - error messages per file */
|
/** @type {string[]} - error messages per file */
|
||||||
#errors = []
|
#errors = []
|
||||||
|
|
||||||
|
/** @type {"idle"|"confirm"|"importing"} */
|
||||||
|
#importState = "idle"
|
||||||
|
|
||||||
|
/** @type {number} - actors created so far (during importing) */
|
||||||
|
#progress = 0
|
||||||
|
|
||||||
async _prepareContext(options) {
|
async _prepareContext(options) {
|
||||||
|
// Compute _duplicate live from the world each render, to avoid stale flags
|
||||||
|
const enrichDuplicate = (a) => ({
|
||||||
|
...a,
|
||||||
|
_duplicate: game.actors?.getName(a.name) !== null,
|
||||||
|
})
|
||||||
|
const pending = this.#pending.map(enrichDuplicate)
|
||||||
|
const duplicateCount = pending.filter(a => a._duplicate).length
|
||||||
return {
|
return {
|
||||||
pending: this.#pending,
|
pending,
|
||||||
errors: this.#errors,
|
errors: this.#errors,
|
||||||
hasPending: this.#pending.length > 0,
|
hasPending: this.#pending.length > 0,
|
||||||
hasErrors: this.#errors.length > 0,
|
hasErrors: this.#errors.length > 0,
|
||||||
|
hasDuplicates: duplicateCount > 0,
|
||||||
|
duplicateCount,
|
||||||
count: this.#pending.length,
|
count: this.#pending.length,
|
||||||
|
importState: this.#importState,
|
||||||
|
progress: this.#progress,
|
||||||
|
total: this.#pending.length,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,43 +102,68 @@ export class CDEMigrationApp extends foundry.applications.api.HandlebarsApplicat
|
|||||||
const actors = parseLegacyJson(text)
|
const actors = parseLegacyJson(text)
|
||||||
for (const actor of actors) {
|
for (const actor of actors) {
|
||||||
actor._srcFile = file.name
|
actor._srcFile = file.name
|
||||||
// Avoid duplicates by name
|
// Avoid duplicates-by-name in our pending list
|
||||||
if (!this.#pending.some(p => p.name === actor.name)) {
|
if (this.#pending.some(p => p.name === actor.name)) {
|
||||||
this.#pending.push(actor)
|
this.#errors.push(`« ${actor.name} » ignoré (nom déjà dans la liste d'attente, fichier « ${file.name} »)`)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
this.#pending.push(actor)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.#errors.push(game.i18n.format("CDE.MigrationErrorParse", { file: file.name, error: err.message }))
|
this.#errors.push(game.i18n.format("CDE.MigrationErrorParse", { file: file.name, error: err.message }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.#importState = "idle"
|
||||||
this.render()
|
this.render()
|
||||||
}
|
}
|
||||||
|
|
||||||
static async #clearFiles() {
|
static async #clearFiles() {
|
||||||
this.#pending = []
|
this.#pending = []
|
||||||
this.#errors = []
|
this.#errors = []
|
||||||
|
this.#importState = "idle"
|
||||||
|
this.#progress = 0
|
||||||
this.render()
|
this.render()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** First click: switch to confirmation state instead of importing immediately. */
|
||||||
static async #doImport() {
|
static async #doImport() {
|
||||||
if (!this.#pending.length) return
|
if (!this.#pending.length) return
|
||||||
|
this.#importState = "confirm"
|
||||||
|
this.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Second click: actually perform the import. */
|
||||||
|
static async #confirmImport() {
|
||||||
|
if (!this.#pending.length) return
|
||||||
|
|
||||||
|
this.#importState = "importing"
|
||||||
|
this.#progress = 0
|
||||||
|
this.render()
|
||||||
|
|
||||||
|
const total = this.#pending.length
|
||||||
const created = []
|
const created = []
|
||||||
const failed = []
|
const failed = []
|
||||||
|
|
||||||
for (const data of this.#pending) {
|
for (let i = 0; i < total; i++) {
|
||||||
|
const data = this.#pending[i]
|
||||||
try {
|
try {
|
||||||
const { _srcFile, ...actorData } = data
|
const { _srcFile, ...actorData } = data
|
||||||
const actor = await Actor.create(actorData)
|
const actor = await Actor.create(actorData)
|
||||||
created.push(actor.name)
|
created.push(actor.name)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
failed.push(`${data.name}: ${err.message}`)
|
failed.push(`${data.name}: ${err.message}`)
|
||||||
console.error(`CHRONIQUESDELETRANGE | Migration failed for "${data.name}":`, err)
|
console.error(`CHRONIQUESDELETRANGE | Import failed for "${data.name}":`, err)
|
||||||
}
|
}
|
||||||
|
this.#progress = i + 1
|
||||||
|
// Live-update the progress element in the DOM without full re-render
|
||||||
|
const progEl = this.element?.querySelector(".cde-migration-progress-count")
|
||||||
|
if (progEl) progEl.textContent = `${this.#progress}/${total}`
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#pending = []
|
this.#pending = []
|
||||||
this.#errors = failed
|
this.#errors = failed
|
||||||
|
this.#importState = "idle"
|
||||||
|
this.#progress = 0
|
||||||
this.render()
|
this.render()
|
||||||
|
|
||||||
if (created.length) {
|
if (created.length) {
|
||||||
@@ -145,4 +177,9 @@ export class CDEMigrationApp extends foundry.applications.api.HandlebarsApplicat
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async #cancelImport() {
|
||||||
|
this.#importState = "idle"
|
||||||
|
this.render()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{! Preview table }}
|
{{! Preview table }}
|
||||||
{{#if hasPending}}
|
{{#if (and hasPending (ne importState "importing"))}}
|
||||||
<div class="cde-migration-preview">
|
<div class="cde-migration-preview">
|
||||||
<div class="cde-migration-preview-header">
|
<div class="cde-migration-preview-header">
|
||||||
<span>{{ localize "CDE.MigrationPreviewTitle" }}</span>
|
<span>{{ localize "CDE.MigrationPreviewTitle" }}</span>
|
||||||
@@ -34,9 +34,12 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{{#each pending}}
|
{{#each pending}}
|
||||||
<tr>
|
<tr class="{{#if _duplicate}}cde-migration-row-duplicate{{/if}}">
|
||||||
<td><img src="{{img}}" class="cde-migration-thumb" alt=""></td>
|
<td><img src="{{img}}" class="cde-migration-thumb" alt=""></td>
|
||||||
<td class="cde-migration-name">{{name}}</td>
|
<td class="cde-migration-name">
|
||||||
|
{{#if _duplicate}}<i class="fa-solid fa-triangle-exclamation cde-migration-duplicate-icon" title="{{ localize 'CDE.MigrationDuplicate' }}"></i>{{/if}}
|
||||||
|
{{name}}
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="cde-migration-type-badge cde-migration-type-{{type}}">
|
<span class="cde-migration-type-badge cde-migration-type-{{type}}">
|
||||||
{{#if (eq type "character")}}
|
{{#if (eq type "character")}}
|
||||||
@@ -55,6 +58,41 @@
|
|||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
{{! Duplicate warning banner (confirmation step) }}
|
||||||
|
{{#if (eq importState "confirm")}}
|
||||||
|
<div class="cde-migration-confirm-bar">
|
||||||
|
<p class="cde-migration-confirm-msg">
|
||||||
|
<i class="fa-solid fa-triangle-exclamation"></i>
|
||||||
|
{{ localize "CDE.MigrationImportConfirm" count=count }}
|
||||||
|
</p>
|
||||||
|
{{#if hasDuplicates}}
|
||||||
|
<p class="cde-migration-confirm-duplicates">
|
||||||
|
<i class="fa-solid fa-triangle-exclamation"></i>
|
||||||
|
{{ localize "CDE.MigrationDuplicateCount" count=duplicateCount }}
|
||||||
|
</p>
|
||||||
|
{{/if}}
|
||||||
|
<div class="cde-migration-confirm-actions">
|
||||||
|
<button type="button" class="cde-migration-confirm-btn" data-action="confirmImport">
|
||||||
|
<i class="fa-solid fa-download"></i>
|
||||||
|
{{ localize "CDE.MigrationConfirmAction" }} ({{count}})
|
||||||
|
</button>
|
||||||
|
<button type="button" class="cde-migration-cancel-btn" data-action="cancelImport">
|
||||||
|
<i class="fa-solid fa-xmark"></i>
|
||||||
|
{{ localize "CDE.Cancel" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{! Progress (importing state) }}
|
||||||
|
{{#if (eq importState "importing")}}
|
||||||
|
<div class="cde-migration-progress">
|
||||||
|
<i class="fa-solid fa-spinner fa-spin"></i>
|
||||||
|
<span>{{ localize "CDE.MigrationImporting" }}</span>
|
||||||
|
<span class="cde-migration-progress-count">{{progress}}/{{total}}</span>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
{{! Errors }}
|
{{! Errors }}
|
||||||
{{#if hasErrors}}
|
{{#if hasErrors}}
|
||||||
<ul class="cde-migration-errors">
|
<ul class="cde-migration-errors">
|
||||||
@@ -66,12 +104,12 @@
|
|||||||
|
|
||||||
{{! Action bar }}
|
{{! Action bar }}
|
||||||
<div class="cde-migration-actions">
|
<div class="cde-migration-actions">
|
||||||
{{#if hasPending}}
|
{{#if (and hasPending (eq importState "idle"))}}
|
||||||
<button type="button" class="cde-migration-import-btn" data-action="doImport">
|
<button type="button" class="cde-migration-import-btn" data-action="doImport">
|
||||||
<i class="fa-solid fa-download"></i>
|
<i class="fa-solid fa-download"></i>
|
||||||
{{ localize "CDE.MigrationImport" }} ({{count}})
|
{{ localize "CDE.MigrationImport" }} ({{count}})
|
||||||
</button>
|
</button>
|
||||||
{{else}}
|
{{else if (and (not hasPending) (eq importState "idle"))}}
|
||||||
<p class="cde-migration-hint">{{ localize "CDE.MigrationHint" }}</p>
|
<p class="cde-migration-hint">{{ localize "CDE.MigrationHint" }}</p>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user