8 Commits

Author SHA1 Message Date
uberwald 98c319767e Use socket to manage loksyu
Release Creation / build (release) Successful in 1m3s
2026-06-14 22:55:26 +02:00
uberwald 50038a13f9 Use socket to manage loksyu 2026-06-14 22:54:37 +02:00
uberwald 4cb8e26333 Correction sur police des equipements
Release Creation / build (release) Successful in 1m3s
2026-06-14 22:33:49 +02:00
uberwald faf8c4ca92 Import de masse des PNJ ancien système
Release Creation / build (release) Successful in 1m13s
2026-06-10 22:16:25 +02:00
uberwald 20b41f2cd4 Nouvelle correction sur lancement des sorts
Release Creation / build (release) Successful in 1m4s
2026-06-10 20:34:41 +02:00
uberwald 34b7e32d08 Fix timeout usage
Release Creation / build (release) Successful in 1m7s
2026-06-10 16:30:07 +02:00
uberwald 75f79c1c08 feat(magic): reorder schools, fix Wu Xing aspect & power formula
- `magicOrder` ArrayField + ▲/▼ buttons for manual reordering
- Magic rolls use school's aspect for Wu Xing, not speciality's element
- Spell power: `difficulty × (aspectValue + freePowerLevels)` (not `successes × diff`)
- Prompt replaces `aspectspeciality`/`bonusmalusspeciality`/`heispend` with `freepowerlevels`

fix: code review issues
- combat.js: guard undefined `ids` in rollInitiative
- rolling.js: catch Dice So Nice promise, normalize French→English kungfu aspects
- weapon/armor/ingredient: `{ min: 0 }` on quantity
- character.js/npc.js: catch rollForActor fire-and-forget promises
- roll-actions.js/tinji-app.js: await ChatMessage.create
- sanhei.js: null guard on properties
- spell.js/kungfu.js: fix aspect name comments (French→English)
2026-06-10 15:54:31 +02:00
uberwald 188717c925 Corrections sur degats - again
Release Creation / build (release) Successful in 1m7s
2026-06-09 23:05:00 +02:00
92 changed files with 1369 additions and 500 deletions
+126
View File
@@ -2179,6 +2179,25 @@ section.npc .cde-neon-tabs .item.active {
.cde-magic-toggle:hover i { .cde-magic-toggle:hover i {
color: #e2e8f4; color: #e2e8f4;
} }
.cde-magic-order-btn {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: #7d94b8;
border-radius: 3px;
flex-shrink: 0;
transition: color 0.12s, background 0.12s;
}
.cde-magic-order-btn i {
font-size: 9px;
}
.cde-magic-order-btn:hover {
color: #e2e8f4;
background: rgba(38, 56, 83, 0.25);
}
.cde-magic-specialities { .cde-magic-specialities {
border-top: 1px solid #1a2436; border-top: 1px solid #1a2436;
padding: 4px 0; padding: 4px 0;
@@ -3190,7 +3209,9 @@ ol.item-list li.item h4.item-name {
flex: 1 1 0; flex: 1 1 0;
margin: 0; margin: 0;
font-size: 13px; font-size: 13px;
font-weight: 600;
color: #e2e8f4; color: #e2e8f4;
font-family: "Signika", sans-serif;
} }
ol.item-list li.item .cde-item-stat { ol.item-list li.item .cde-item-stat {
font-family: monospace; font-family: monospace;
@@ -3523,6 +3544,12 @@ ol.item-list li.item .item-controls a.item-control:hover {
color: var(--rr-accent, #e2e8f4); color: var(--rr-accent, #e2e8f4);
text-shadow: 0 0 12px var(--rr-accent, transparent); text-shadow: 0 0 12px var(--rr-accent, transparent);
} }
.cde-roll-result .cde-rr-hero .cde-rr-spell-power .cde-rr-spell-power-formula {
font-size: 10px;
color: #7d94b8;
margin: 2px 0;
white-space: nowrap;
}
.cde-roll-result .cde-rr-hero .cde-rr-spell-power .cde-rr-spell-power-label { .cde-roll-result .cde-rr-hero .cde-rr-spell-power .cde-rr-spell-power-label {
font-size: 9px; font-size: 9px;
font-weight: 700; font-weight: 700;
@@ -4418,6 +4445,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;
+131 -1
View File
@@ -2253,6 +2253,23 @@ section.npc .cde-neon-tabs .item.active { color: @cde-supernatural; borde
&:hover i { color: @cde-text; } &:hover i { color: @cde-text; }
} }
.cde-magic-order-btn {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: @cde-muted;
border-radius: 3px;
flex-shrink: 0;
transition: color 0.12s, background 0.12s;
i { font-size: 9px; }
&:hover { color: @cde-text; background: fade(@cde-border-hi, 25%); }
}
// Specialities list // Specialities list
.cde-magic-specialities { .cde-magic-specialities {
border-top: 1px solid @cde-border; border-top: 1px solid @cde-border;
@@ -3247,7 +3264,7 @@ ol.item-list {
img { border: none; border-radius: 3px; flex-shrink: 0; } img { border: none; border-radius: 3px; flex-shrink: 0; }
h4.item-name { flex: 1 1 0; margin: 0; font-size: 13px; color: @cde-text; } h4.item-name { flex: 1 1 0; margin: 0; font-size: 13px; font-weight: 600; color: @cde-text; font-family: "Signika", sans-serif; }
.cde-item-stat { .cde-item-stat {
font-family: monospace; font-family: monospace;
@@ -3582,6 +3599,13 @@ ol.item-list {
text-shadow: 0 0 12px var(--rr-accent, transparent); text-shadow: 0 0 12px var(--rr-accent, transparent);
} }
.cde-rr-spell-power-formula {
font-size: 10px;
color: @cde-muted;
margin: 2px 0;
white-space: nowrap;
}
.cde-rr-spell-power-label { .cde-rr-spell-power-label {
font-size: 9px; font-size: 9px;
font-weight: 700; font-weight: 700;
@@ -4607,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
// ============================================================ // ============================================================
+390 -102
View File
@@ -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
@@ -661,6 +796,7 @@ function preLocalizeConfig() {
magic.aspectlabel = game.i18n.localize(magic.aspectlabel); magic.aspectlabel = game.i18n.localize(magic.aspectlabel);
Object.values(magic.speciality).forEach((spec) => { Object.values(magic.speciality).forEach((spec) => {
spec.label = game.i18n.localize(spec.label); spec.label = game.i18n.localize(spec.label);
spec.labelelementkey = spec.labelelement;
spec.labelelement = game.i18n.localize(spec.labelelement); spec.labelelement = game.i18n.localize(spec.labelelement);
}); });
}); });
@@ -774,9 +910,8 @@ var CharacterDataModel = class extends foundry.abstract.TypeDataModel {
typeofthrow: numberField(0), typeofthrow: numberField(0),
aspectskill: numberField(0), aspectskill: numberField(0),
bonusmalusskill: numberField(0), bonusmalusskill: numberField(0),
aspectspeciality: numberField(0),
rolldifficulty: numberField(0), rolldifficulty: numberField(0),
bonusmalusspeciality: numberField(0) freepowerlevels: numberField(0)
}) })
}), }),
aspect: new fields.SchemaField({ aspect: new fields.SchemaField({
@@ -815,6 +950,10 @@ var CharacterDataModel = class extends foundry.abstract.TypeDataModel {
nine: componentField(), nine: componentField(),
zero: componentField() zero: componentField()
}), }),
magicOrder: new fields.ArrayField(
new fields.StringField({ required: true, nullable: false, initial: "" }),
{ required: true, initial: [] }
),
magics: new fields.SchemaField({ magics: new fields.SchemaField({
internalcinnabar: magicField(), internalcinnabar: magicField(),
alchemy: magicField(), alchemy: magicField(),
@@ -931,7 +1070,7 @@ var KungfuDataModel = class extends foundry.abstract.TypeDataModel {
orientation: stringField("yin"), orientation: stringField("yin"),
// yin | yang | yinyang // yin | yang | yinyang
aspect: stringField("metal"), aspect: stringField("metal"),
// metal | eau | terre | feu | bois // metal | water | earth | fire | wood
skill: stringField("kungfu"), skill: stringField("kungfu"),
// kungfu | rangedcombat // kungfu | rangedcombat
speciality: stringField(""), speciality: stringField(""),
@@ -957,7 +1096,7 @@ var SpellDataModel = class extends foundry.abstract.TypeDataModel {
description: htmlField(""), description: htmlField(""),
specialityname: stringField(""), specialityname: stringField(""),
associatedelement: stringField("metal"), associatedelement: stringField("metal"),
// metal | eau | terre | feu | bois // metal | water | earth | fire | wood
hei: stringField(""), hei: stringField(""),
realizationtimeritual: stringField(""), realizationtimeritual: stringField(""),
realizationtimeaccelerated: stringField(""), realizationtimeaccelerated: stringField(""),
@@ -999,9 +1138,11 @@ var WeaponDataModel = class extends foundry.abstract.TypeDataModel {
const stringField = (initial = "") => new fields.StringField({ required: true, nullable: false, initial }); const stringField = (initial = "") => new fields.StringField({ required: true, nullable: false, initial });
const htmlField = (initial = "") => new fields.HTMLField({ required: true, nullable: false, initial, textSearch: true }); const htmlField = (initial = "") => new fields.HTMLField({ required: true, nullable: false, initial, textSearch: true });
const intField = (initial = 0, opts = {}) => new fields.NumberField({ required: true, nullable: false, integer: true, initial, ...opts }); const intField = (initial = 0, opts = {}) => new fields.NumberField({ required: true, nullable: false, integer: true, initial, ...opts });
const boolField = (initial = false) => new fields.BooleanField({ required: true, initial });
return { return {
reference: stringField(""), reference: stringField(""),
description: htmlField(""), description: htmlField(""),
hasSpeciality: boolField(false),
weaponType: stringField("melee"), weaponType: stringField("melee"),
material: stringField(""), material: stringField(""),
damageAspect: stringField("metal"), damageAspect: stringField("metal"),
@@ -1010,7 +1151,7 @@ var WeaponDataModel = class extends foundry.abstract.TypeDataModel {
// contact | courte | mediane | longue | extreme // contact | courte | mediane | longue | extreme
obtainLevel: intField(0, { min: 0, max: 5 }), obtainLevel: intField(0, { min: 0, max: 5 }),
obtainDifficulty: intField(0, { min: 0, max: 3 }), obtainDifficulty: intField(0, { min: 0, max: 3 }),
quantity: intField(1), quantity: intField(1, { min: 0 }),
notes: htmlField("") notes: htmlField("")
}; };
} }
@@ -1030,7 +1171,7 @@ var ArmorDataModel = class extends foundry.abstract.TypeDataModel {
domain: stringField(""), domain: stringField(""),
obtainLevel: intField(0, { min: 0, max: 5 }), obtainLevel: intField(0, { min: 0, max: 5 }),
obtainDifficulty: intField(0, { min: 0, max: 3 }), obtainDifficulty: intField(0, { min: 0, max: 3 }),
quantity: intField(1), quantity: intField(1, { min: 0 }),
notes: htmlField("") notes: htmlField("")
}; };
} }
@@ -1076,7 +1217,7 @@ var IngredientDataModel = class extends foundry.abstract.TypeDataModel {
school: stringField("all"), school: stringField("all"),
obtainLevel: intField(0, { min: 0, max: 5 }), obtainLevel: intField(0, { min: 0, max: 5 }),
obtainDifficulty: intField(0, { min: 0, max: 3 }), obtainDifficulty: intField(0, { min: 0, max: 3 }),
quantity: intField(1), quantity: intField(1, { min: 0 }),
notes: htmlField("") notes: htmlField("")
}; };
} }
@@ -1420,6 +1561,7 @@ async function rollInitiativeNPC(actor) {
} }
// src/ui/apps/singletons.js // src/ui/apps/singletons.js
var SOCKET_CHANNEL = `system.${SYSTEM_ID}`;
function getLoksyuData() { function getLoksyuData() {
return game.settings.get(SYSTEM_ID, "loksyuData") ?? { return game.settings.get(SYSTEM_ID, "loksyuData") ?? {
wood: { yin: 0, yang: 0 }, wood: { yin: 0, yang: 0 },
@@ -1429,16 +1571,75 @@ function getLoksyuData() {
water: { yin: 0, yang: 0 } water: { yin: 0, yang: 0 }
}; };
} }
async function setLoksyuData(data) { async function writeLoksyuData(data) {
await game.settings.set(SYSTEM_ID, "loksyuData", data); await game.settings.set(SYSTEM_ID, "loksyuData", data);
Hooks.callAll("cde:loksyuUpdated", data); Hooks.callAll("cde:loksyuUpdated", data);
} }
async function writeTinjiValue(value) {
value = Math.max(0, value);
await game.settings.set(SYSTEM_ID, "tinjiData", value);
Hooks.callAll("cde:tinjiUpdated", value);
}
async function setLoksyuData(data) {
if (game.user.isGM) return writeLoksyuData(data);
game.socket.emit(SOCKET_CHANNEL, { action: "setLoksyuData", data });
}
function getTinjiValue() { function getTinjiValue() {
return game.settings.get(SYSTEM_ID, "tinjiData") ?? 0; return game.settings.get(SYSTEM_ID, "tinjiData") ?? 0;
} }
async function setTinjiValue(value) { async function setTinjiValue(value) {
await game.settings.set(SYSTEM_ID, "tinjiData", Math.max(0, value)); if (game.user.isGM) return writeTinjiValue(value);
Hooks.callAll("cde:tinjiUpdated", Math.max(0, value)); game.socket.emit(SOCKET_CHANNEL, { action: "setTinjiValue", value });
}
function requestLoksyuDraw(aspect, order) {
game.socket.emit(SOCKET_CHANNEL, { action: "loksyuDraw", aspect, order });
}
function requestTinjiSpend() {
game.socket.emit(SOCKET_CHANNEL, { action: "tinjiSpend" });
}
function registerSingletonSocket() {
game.socket.on(SOCKET_CHANNEL, async (payload) => {
if (!game.user.isGM) return;
switch (payload.action) {
case "setLoksyuData":
await writeLoksyuData(payload.data);
break;
case "setTinjiValue":
await writeTinjiValue(payload.value);
break;
case "updateLoksyuFromRoll":
await updateLoksyuFromRoll(payload.activeAspect, payload.faces);
break;
case "updateTinjiFromRoll":
await updateTinjiFromRoll(payload.delta);
break;
case "loksyuDraw": {
const data = getLoksyuData();
const entry = data[payload.aspect] ?? { yin: 0, yang: 0 };
const order = payload.order ?? "yang-first";
if (order === "yin-first") {
if (entry.yin > 0) entry.yin--;
else entry.yang--;
} else if (order === "balanced") {
if (entry.yin > entry.yang) entry.yin--;
else if (entry.yang > entry.yin) entry.yang--;
else if (entry.yang > 0) entry.yang--;
else entry.yin--;
} else {
if (entry.yang > 0) entry.yang--;
else entry.yin--;
}
data[payload.aspect] = entry;
await writeLoksyuData(data);
break;
}
case "tinjiSpend": {
const cur = getTinjiValue();
if (cur > 0) await writeTinjiValue(cur - 1);
break;
}
}
});
} }
async function updateLoksyuFromRoll(activeAspect, faces) { async function updateLoksyuFromRoll(activeAspect, faces) {
const cycle = WU_XING_CYCLE[activeAspect]; const cycle = WU_XING_CYCLE[activeAspect];
@@ -1449,18 +1650,23 @@ async function updateLoksyuFromRoll(activeAspect, faces) {
const yinCount = faces[yinFace] ?? 0; const yinCount = faces[yinFace] ?? 0;
const yangCount = faces[yangFace] ?? 0; const yangCount = faces[yangFace] ?? 0;
if (yinCount === 0 && yangCount === 0) return; if (yinCount === 0 && yangCount === 0) return;
const data = getLoksyuData(); if (game.user.isGM) {
const current = data[lokAspect] ?? { yin: 0, yang: 0 }; const data = getLoksyuData();
data[lokAspect] = { const current = data[lokAspect] ?? { yin: 0, yang: 0 };
yin: (current.yin ?? 0) + yinCount, data[lokAspect] = { yin: (current.yin ?? 0) + yinCount, yang: (current.yang ?? 0) + yangCount };
yang: (current.yang ?? 0) + yangCount await writeLoksyuData(data);
}; } else {
await setLoksyuData(data); game.socket.emit(SOCKET_CHANNEL, { action: "updateLoksyuFromRoll", activeAspect, faces });
}
} }
async function updateTinjiFromRoll(count) { async function updateTinjiFromRoll(count) {
if (!count || count <= 0) return; if (!count || count <= 0) return;
const current = getTinjiValue(); if (game.user.isGM) {
await setTinjiValue(current + count); const current = getTinjiValue();
await writeTinjiValue(current + count);
} else {
game.socket.emit(SOCKET_CHANNEL, { action: "updateTinjiFromRoll", delta: count });
}
} }
// src/ui/rolling.js // src/ui/rolling.js
@@ -1566,20 +1772,16 @@ async function showMagicPrompt(params) {
aspectskill: Number(params.aspectskill ?? 0), aspectskill: Number(params.aspectskill ?? 0),
bonusmalusskill: params.bonusmalusskill ?? 0, bonusmalusskill: params.bonusmalusskill ?? 0,
bonusauspiciousdice: params.bonusauspiciousdice ?? 0, bonusauspiciousdice: params.bonusauspiciousdice ?? 0,
aspectspeciality: Number(params.aspectspeciality ?? 0),
rolldifficulty: params.rolldifficulty ?? 1, rolldifficulty: params.rolldifficulty ?? 1,
bonusmalusspeciality: params.bonusmalusspeciality ?? 0, freepowerlevels: params.freepowerlevels ?? 0,
heispend: params.heispend ?? 0,
typeofthrow: Number(params.typeofthrow ?? 0) typeofthrow: Number(params.typeofthrow ?? 0)
}, },
fields: [ fields: [
"aspectskill", "aspectskill",
"bonusmalusskill", "bonusmalusskill",
"bonusauspiciousdice", "bonusauspiciousdice",
"aspectspeciality",
"rolldifficulty", "rolldifficulty",
"bonusmalusspeciality", "freepowerlevels",
"heispend",
"typeofthrow" "typeofthrow"
] ]
}); });
@@ -1709,7 +1911,9 @@ async function rollForActor(actor, rollKey) {
const kfSkill = kfItem.system.skill ?? "kungfu"; const kfSkill = kfItem.system.skill ?? "kungfu";
numberofdice = sys.skills?.[kfSkill]?.value ?? 0; numberofdice = sys.skills?.[kfSkill]?.value ?? 0;
title = `${kfItem.name} [${game.i18n.localize(sys.skills?.[kfSkill]?.label ?? "CDE.KungFu")}]`; title = `${kfItem.name} [${game.i18n.localize(sys.skills?.[kfSkill]?.label ?? "CDE.KungFu")}]`;
kfDefaultAspect = ASPECT_NAMES.indexOf(kfItem.system.aspect ?? "metal"); const kfAspect = kfItem.system.aspect?.toLowerCase() ?? "metal";
const ASPECT_NORMALIZE = { eau: "water", terre: "earth", feu: "fire", bois: "wood" };
kfDefaultAspect = ASPECT_NAMES.indexOf(ASPECT_NORMALIZE[kfAspect] ?? kfAspect);
if (kfDefaultAspect < 0) kfDefaultAspect = 0; if (kfDefaultAspect < 0) kfDefaultAspect = 0;
break; break;
} }
@@ -1758,7 +1962,8 @@ async function rollForActor(actor, rollKey) {
const wpBonusAusp = Number(wParams.bonusauspiciousdice ?? 0); const wpBonusAusp = Number(wParams.bonusauspiciousdice ?? 0);
const wpThrowMode = Number(wParams.typeofthrow ?? 0); const wpThrowMode = Number(wParams.typeofthrow ?? 0);
const wpDamageBase = wpItem.system.damageBase ?? 0; const wpDamageBase = wpItem.system.damageBase ?? 0;
const wpTotalDice = wpSkillDice + wpAspectDice + wpRangeMalus + wpBonusMalus - wpWoundMalus; const wpSpecialtyBonus = wpItem.system.hasSpeciality ? 1 : 0;
const wpTotalDice = wpSkillDice + wpAspectDice + wpRangeMalus + wpBonusMalus - wpWoundMalus + wpSpecialtyBonus;
if (wpTotalDice <= 0) { if (wpTotalDice <= 0) {
ui.notifications.warn(game.i18n.localize("CDE.Error0")); ui.notifications.warn(game.i18n.localize("CDE.Error0"));
return; return;
@@ -1774,8 +1979,11 @@ async function rollForActor(actor, rollKey) {
if (wpBonusMalus !== 0) wpModParts.push(`${wpBonusMalus > 0 ? "+" : ""}${wpBonusMalus} ${game.i18n.localize("CDE.BonusMalus")}`); if (wpBonusMalus !== 0) wpModParts.push(`${wpBonusMalus > 0 ? "+" : ""}${wpBonusMalus} ${game.i18n.localize("CDE.BonusMalus")}`);
if (wpWoundMalus !== 0) wpModParts.push(`-${wpWoundMalus} ${game.i18n.localize("CDE.WoundMalus")}`); if (wpWoundMalus !== 0) wpModParts.push(`-${wpWoundMalus} ${game.i18n.localize("CDE.WoundMalus")}`);
if (wpBonusAusp !== 0) wpModParts.push(`+${wpBonusAusp} ${game.i18n.localize("CDE.BonusAuspiciousDice")}`); if (wpBonusAusp !== 0) wpModParts.push(`+${wpBonusAusp} ${game.i18n.localize("CDE.BonusAuspiciousDice")}`);
const wpDamageAspectValue = sys.aspect?.[ASPECT_NAMES[wpAspFinal]]?.value ?? 0; const wpDamageAspectRaw = wpItem.system.damageAspect ?? "metal";
const wpDamageAspectLabel = game.i18n.localize(ASPECT_LABELS[ASPECT_NAMES[wpAspFinal]] ?? ""); const wpDamageAspectIdx = WEAPON_ASPECT_INDEX[wpDamageAspectRaw] ?? 0;
const wpDamageAspectName = ASPECT_NAMES[wpDamageAspectIdx];
const wpDamageAspectValue = sys.aspect?.[wpDamageAspectName]?.value ?? 0;
const wpDamageAspectLabel = game.i18n.localize(ASPECT_LABELS[wpDamageAspectName] ?? "");
const wpMsg = await sendResultMessage(actor, { const wpMsg = await sendResultMessage(actor, {
rollLabel: `${wpItem.name}`, rollLabel: `${wpItem.name}`,
aspectName: wpAspectName, aspectName: wpAspectName,
@@ -1807,7 +2015,10 @@ async function rollForActor(actor, rollKey) {
d0: wpFaces[0] d0: wpFaces[0]
}, wpRoll, ROLL_MODES[wpThrowMode] ?? "roll"); }, wpRoll, ROLL_MODES[wpThrowMode] ?? "roll");
if (game.modules.get("dice-so-nice")?.active && wpMsg?.id) { if (game.modules.get("dice-so-nice")?.active && wpMsg?.id) {
await game.dice3d.waitFor3DAnimationByMessageID(wpMsg.id); try {
await game.dice3d.waitFor3DAnimationByMessageID(wpMsg.id);
} catch (_e) {
}
} }
if ((wpResults.loksyudice ?? 0) > 0) await updateLoksyuFromRoll(wpAspectName, wpFaces); if ((wpResults.loksyudice ?? 0) > 0) await updateLoksyuFromRoll(wpAspectName, wpFaces);
if ((wpResults.tinjidice ?? 0) > 0) await updateTinjiFromRoll(wpResults.tinjidice); if ((wpResults.tinjidice ?? 0) > 0) await updateTinjiFromRoll(wpResults.tinjidice);
@@ -1840,14 +2051,6 @@ async function rollForActor(actor, rollKey) {
if (kfDefaultAspect >= 0) { if (kfDefaultAspect >= 0) {
defaultAspect = kfDefaultAspect; defaultAspect = kfDefaultAspect;
} }
let defaultSpecialAspect = 0;
if (isMagicSpecial && specialLibel) {
const specialCfg = MAGICS?.[skillLibel]?.speciality?.[specialLibel];
const aspectName = LABELELEMENT_TO_ASPECT[specialCfg?.labelelement];
if (aspectName) {
defaultSpecialAspect = ASPECT_NAMES.indexOf(aspectName);
}
}
let params; let params;
if (isMagic) { if (isMagic) {
params = await showMagicPrompt({ params = await showMagicPrompt({
@@ -1856,10 +2059,8 @@ async function rollForActor(actor, rollKey) {
aspectskill: defaultAspect, aspectskill: defaultAspect,
bonusmalusskill: 0, bonusmalusskill: 0,
bonusauspiciousdice: 0, bonusauspiciousdice: 0,
aspectspeciality: defaultSpecialAspect,
rolldifficulty: 1, rolldifficulty: 1,
bonusmalusspeciality: 0, freepowerlevels: 0,
heispend: 0,
typeofthrow: typeOfThrow typeofthrow: typeOfThrow
}); });
} else { } else {
@@ -1876,20 +2077,16 @@ async function rollForActor(actor, rollKey) {
} }
if (!params) return; if (!params) return;
let aspectIndex, bonusMalus, bonusAuspicious, throwMode; let aspectIndex, bonusMalus, bonusAuspicious, throwMode;
let spellAspectIndex = null;
let rollDifficulty = 1; let rollDifficulty = 1;
if (isMagic) { if (isMagic) {
const skillAspectIndex = Number(params.aspectskill ?? 0); const skillAspectIndex = Number(params.aspectskill ?? 0);
spellAspectIndex = Number(params.aspectspeciality ?? skillAspectIndex);
aspectIndex = skillAspectIndex; aspectIndex = skillAspectIndex;
bonusMalus = Number(params.bonusmalusskill ?? 0); bonusMalus = Number(params.bonusmalusskill ?? 0);
bonusAuspicious = Number(params.bonusauspiciousdice ?? 0); bonusAuspicious = Number(params.bonusauspiciousdice ?? 0);
rollDifficulty = Math.max(1, Number(params.rolldifficulty ?? 1)); rollDifficulty = Math.max(1, Number(params.rolldifficulty ?? 1));
throwMode = Number(params.typeofthrow ?? 0); throwMode = Number(params.typeofthrow ?? 0);
const aspectDice = sys.aspect?.[ASPECT_NAMES[aspectIndex]]?.value ?? 0; const aspectDice = sys.aspect?.[ASPECT_NAMES[aspectIndex]]?.value ?? 0;
const bonusSpec = Number(params.bonusmalusspeciality ?? 0); numberofdice = numberofdice + aspectDice + bonusMalus + 1;
const heiDice = Number(params.heispend ?? 0);
numberofdice = numberofdice + aspectDice + bonusMalus + 1 + bonusSpec + heiDice;
} else { } else {
aspectIndex = Number(params.aspect ?? 0); aspectIndex = Number(params.aspect ?? 0);
bonusMalus = Number(params.bonusmalus ?? 0); bonusMalus = Number(params.bonusmalus ?? 0);
@@ -1907,22 +2104,33 @@ async function rollForActor(actor, rollKey) {
const roll = new Roll(`${numberofdice}d10`); const roll = new Roll(`${numberofdice}d10`);
await roll.evaluate(); await roll.evaluate();
const rollModeKey = ROLL_MODES[throwMode] ?? "roll"; const rollModeKey = ROLL_MODES[throwMode] ?? "roll";
const wuXingAspectName = spellAspectIndex !== null ? ASPECT_NAMES[spellAspectIndex] : ASPECT_NAMES[aspectIndex]; let spellPower = null;
let spellPowerAspectName = null;
let spellPowerAspectValue = null;
if (isMagic) {
if (isMagicSpecial && specialLibel) {
const specialCfg = MAGICS?.[skillLibel]?.speciality?.[specialLibel];
const elemName = LABELELEMENT_TO_ASPECT[specialCfg?.labelelementkey];
if (elemName) spellPowerAspectName = elemName;
}
if (!spellPowerAspectName) spellPowerAspectName = ASPECT_NAMES[aspectIndex];
spellPowerAspectValue = sys.aspect?.[spellPowerAspectName]?.value ?? 0;
const freePowerLevels = Number(params.freepowerlevels ?? 0);
spellPower = rollDifficulty * (spellPowerAspectValue + freePowerLevels);
}
const wuXingAspectName = ASPECT_NAMES[aspectIndex];
const allResults = roll.dice[0]?.results ?? []; const allResults = roll.dice[0]?.results ?? [];
const faces = countFaces(allResults); const faces = countFaces(allResults);
const results = computeWuXingResults(faces, wuXingAspectName, bonusAuspicious); const results = computeWuXingResults(faces, wuXingAspectName, bonusAuspicious);
if (!results) return; if (!results) return;
const spellPower = isMagic ? results.successesdice * rollDifficulty : null;
const modParts = []; const modParts = [];
if (isMagic) { if (isMagic) {
const bm = Number(params.bonusmalusskill ?? 0); const bm = Number(params.bonusmalusskill ?? 0);
const bs = Number(params.bonusmalusspeciality ?? 0);
const hs = Number(params.heispend ?? 0);
const ba = Number(params.bonusauspiciousdice ?? 0); const ba = Number(params.bonusauspiciousdice ?? 0);
const fp = Number(params.freepowerlevels ?? 0);
if (bm !== 0) modParts.push(`${bm > 0 ? "+" : ""}${bm} ${game.i18n.localize("CDE.BonusMalus")}`); if (bm !== 0) modParts.push(`${bm > 0 ? "+" : ""}${bm} ${game.i18n.localize("CDE.BonusMalus")}`);
if (bs !== 0) modParts.push(`${bs > 0 ? "+" : ""}${bs} ${game.i18n.localize("CDE.SpellBonus")}`);
if (ba !== 0) modParts.push(`+${ba} ${game.i18n.localize("CDE.BonusAuspiciousDice")}`); if (ba !== 0) modParts.push(`+${ba} ${game.i18n.localize("CDE.BonusAuspiciousDice")}`);
if (hs !== 0) modParts.push(`${hs} ${game.i18n.localize("CDE.HeiSpend")}`); if (fp !== 0) modParts.push(`+${fp} ${game.i18n.localize("CDE.FreePowerLevels")}`);
if (rollDifficulty !== 1) modParts.push(`\xD7${rollDifficulty} ${game.i18n.localize("CDE.RollDifficulty")}`); if (rollDifficulty !== 1) modParts.push(`\xD7${rollDifficulty} ${game.i18n.localize("CDE.RollDifficulty")}`);
} else { } else {
const bm = Number(params.bonusmalus ?? 0); const bm = Number(params.bonusmalus ?? 0);
@@ -1942,6 +2150,9 @@ async function rollForActor(actor, rollKey) {
modifiersText: modParts.length ? modParts.join(" \xB7 ") : "", modifiersText: modParts.length ? modParts.join(" \xB7 ") : "",
// Spell power (magic only) // Spell power (magic only)
spellPower, spellPower,
spellPowerAspectLabel: spellPowerAspectName ? game.i18n.localize(ASPECT_LABELS[spellPowerAspectName] ?? "") : "",
spellPowerAspectValue,
spellPowerFreeLevels: isMagic ? Number(params.freepowerlevels ?? 0) : 0,
rollDifficulty: isMagic ? rollDifficulty : null, rollDifficulty: isMagic ? rollDifficulty : null,
// Actor info // Actor info
actorName: actor.name ?? "", actorName: actor.name ?? "",
@@ -1962,7 +2173,10 @@ async function rollForActor(actor, rollKey) {
d0: faces[0] d0: faces[0]
}, roll, rollModeKey); }, roll, rollModeKey);
if (game.modules.get("dice-so-nice")?.active && msg?.id) { if (game.modules.get("dice-so-nice")?.active && msg?.id) {
await game.dice3d.waitFor3DAnimationByMessageID(msg.id); try {
await game.dice3d.waitFor3DAnimationByMessageID(msg.id);
} catch (_e) {
}
} }
if ((results.loksyudice ?? 0) > 0) await updateLoksyuFromRoll(wuXingAspectName, faces); if ((results.loksyudice ?? 0) > 0) await updateLoksyuFromRoll(wuXingAspectName, faces);
if ((results.tinjidice ?? 0) > 0) await updateTinjiFromRoll(results.tinjidice); if ((results.tinjidice ?? 0) > 0) await updateTinjiFromRoll(results.tinjidice);
@@ -2058,9 +2272,13 @@ var CDEBaseActorSheet = class _CDEBaseActorSheet extends HandlebarsApplicationMi
}; };
// src/ui/sheets/actors/character.js // src/ui/sheets/actors/character.js
var CDECharacterSheet = class extends CDEBaseActorSheet { var CDECharacterSheet = class _CDECharacterSheet extends CDEBaseActorSheet {
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
classes: ["character"] classes: ["character"],
actions: {
moveMagicUp: _CDECharacterSheet.#onMoveMagicUp,
moveMagicDown: _CDECharacterSheet.#onMoveMagicDown
}
}; };
static PARTS = { static PARTS = {
main: { template: "systems/fvtt-chroniques-de-l-etrange/templates/actor/cde-character-sheet.html" } main: { template: "systems/fvtt-chroniques-de-l-etrange/templates/actor/cde-character-sheet.html" }
@@ -2083,25 +2301,35 @@ var CDECharacterSheet = class extends CDEBaseActorSheet {
spellsByDiscipline[disc].push(spell); spellsByDiscipline[disc].push(spell);
} }
const systemMagics = context.systemData.magics ?? {}; const systemMagics = context.systemData.magics ?? {};
context.magicsDisplay = Object.fromEntries( const magicEntries = Object.entries(MAGICS).map(([magicKey, magicDef]) => {
Object.entries(MAGICS).map(([magicKey, magicDef]) => { const magicData = systemMagics[magicKey] ?? {};
const magicData = systemMagics[magicKey] ?? {}; return [
return [ magicKey,
magicKey, {
{ value: magicData.value ?? 0,
value: magicData.value ?? 0, visible: magicData.visible ?? false,
visible: magicData.visible ?? false, speciality: Object.fromEntries(
speciality: Object.fromEntries( Object.keys(magicDef.speciality).map((specKey) => [
Object.keys(magicDef.speciality).map((specKey) => [ specKey,
specKey, { check: magicData.speciality?.[specKey]?.check ?? false }
{ check: magicData.speciality?.[specKey]?.check ?? false } ])
]) ),
), grimoire: spellsByDiscipline[magicKey] ?? []
grimoire: spellsByDiscipline[magicKey] ?? [] }
} ];
]; });
}) const order = context.systemData.magicOrder ?? [];
); if (order.length > 0) {
magicEntries.sort((a, b) => {
const ia = order.indexOf(a[0]);
const ib = order.indexOf(b[0]);
if (ia === -1 && ib === -1) return 0;
if (ia === -1) return 1;
if (ib === -1) return -1;
return ia - ib;
});
}
context.magicsDisplay = Object.fromEntries(magicEntries);
return context; return context;
} }
_onRender(context, options) { _onRender(context, options) {
@@ -2185,7 +2413,7 @@ var CDECharacterSheet = class extends CDEBaseActorSheet {
cell.addEventListener("click", (event) => { cell.addEventListener("click", (event) => {
event.preventDefault(); event.preventDefault();
const rollKey = cell.dataset.libelId; const rollKey = cell.dataset.libelId;
if (rollKey) rollForActor(this.document, rollKey); if (rollKey) rollForActor(this.document, rollKey)?.catch((err) => console.error("Roll failed:", err));
}); });
}); });
} }
@@ -2199,6 +2427,26 @@ var CDECharacterSheet = class extends CDEBaseActorSheet {
}).render(true); }).render(true);
}); });
} }
static async #onMoveMagicUp(event, target) {
const key = target.dataset.magicKey;
let order = this.document.system.magicOrder ?? [];
if (!order.length) order = [...Object.keys(MAGICS)];
else order = [...order];
const idx = order.indexOf(key);
if (idx <= 0) return;
[order[idx - 1], order[idx]] = [order[idx], order[idx - 1]];
await this.document.update({ "system.magicOrder": order });
}
static async #onMoveMagicDown(event, target) {
const key = target.dataset.magicKey;
let order = this.document.system.magicOrder ?? [];
if (!order.length) order = [...Object.keys(MAGICS)];
else order = [...order];
const idx = order.indexOf(key);
if (idx === -1 || idx >= order.length - 1) return;
[order[idx], order[idx + 1]] = [order[idx + 1], order[idx]];
await this.document.update({ "system.magicOrder": order });
}
#bindComponentRandomize() { #bindComponentRandomize() {
const btn = this.element?.querySelector("[data-action='randomize-component']"); const btn = this.element?.querySelector("[data-action='randomize-component']");
if (!btn) return; if (!btn) return;
@@ -2268,7 +2516,7 @@ var CDENpcSheet = class extends CDEBaseActorSheet {
cell.addEventListener("click", (event) => { cell.addEventListener("click", (event) => {
event.preventDefault(); event.preventDefault();
const rollKey = cell.dataset.libelId; const rollKey = cell.dataset.libelId;
if (rollKey) rollForActor(this.document, rollKey); if (rollKey) rollForActor(this.document, rollKey)?.catch((err) => console.error("Roll failed:", err));
}); });
}); });
} }
@@ -2455,11 +2703,11 @@ var CDESanheiSheet = class extends CDEBaseItemSheet {
async _prepareContext() { async _prepareContext() {
const context = await super._prepareContext(); const context = await super._prepareContext();
const enrich = (content) => foundry.applications.ux.TextEditor.implementation.enrichHTML(content ?? "", { async: true }); const enrich = (content) => foundry.applications.ux.TextEditor.implementation.enrichHTML(content ?? "", { async: true });
const props = this.document.system.properties; const props = this.document.system.properties ?? {};
context.prop1DescriptionHTML = await enrich(props.prop1.description); context.prop1DescriptionHTML = await enrich(props.prop1?.description);
context.prop2DescriptionHTML = await enrich(props.prop2.description); context.prop2DescriptionHTML = await enrich(props.prop2?.description);
context.prop3DescriptionHTML = await enrich(props.prop3.description); context.prop3DescriptionHTML = await enrich(props.prop3?.description);
context.propFields = this.document.system.schema.fields.properties.fields; context.propFields = this.document.system.schema.fields.properties?.fields;
return context; return context;
} }
}; };
@@ -2502,6 +2750,8 @@ var CDELoksyuApp = class _CDELoksyuApp extends foundry.applications.api.Handleba
}; };
/** @type {Function|null} bound hook handler */ /** @type {Function|null} bound hook handler */
_updateHook = null; _updateHook = null;
/** @type {Function|null} updateSetting hook handler (for socket-propagated writes) */
_settingHook = null;
/** Singleton accessor — open or bring to front */ /** Singleton accessor — open or bring to front */
static open() { static open() {
const existing = Array.from(foundry.applications.instances.values()).find( const existing = Array.from(foundry.applications.instances.values()).find(
@@ -2536,13 +2786,24 @@ var CDELoksyuApp = class _CDELoksyuApp extends foundry.applications.api.Handleba
_onRender(context, options) { _onRender(context, options) {
super._onRender(context, options); super._onRender(context, options);
this.#bindInputs(); this.#bindInputs();
this._updateHook = Hooks.on("cde:loksyuUpdated", () => this.render()); if (!this._updateHook) {
this._updateHook = Hooks.on("cde:loksyuUpdated", () => this.render());
}
if (!this._settingHook) {
this._settingHook = Hooks.on("updateSetting", (setting) => {
if (setting.key === `${SYSTEM_ID}.loksyuData`) this.render();
});
}
} }
_onClose(options) { _onClose(options) {
if (this._updateHook !== null) { if (this._updateHook !== null) {
Hooks.off("cde:loksyuUpdated", this._updateHook); Hooks.off("cde:loksyuUpdated", this._updateHook);
this._updateHook = null; this._updateHook = null;
} }
if (this._settingHook !== null) {
Hooks.off("updateSetting", this._settingHook);
this._settingHook = null;
}
super._onClose(options); super._onClose(options);
} }
#bindInputs() { #bindInputs() {
@@ -2611,6 +2872,8 @@ var CDETinjiApp = class _CDETinjiApp extends foundry.applications.api.Handlebars
}; };
/** @type {Function|null} */ /** @type {Function|null} */
_updateHook = null; _updateHook = null;
/** @type {Function|null} */
_settingHook = null;
static open() { static open() {
const existing = Array.from(foundry.applications.instances.values()).find( const existing = Array.from(foundry.applications.instances.values()).find(
(app2) => app2 instanceof _CDETinjiApp (app2) => app2 instanceof _CDETinjiApp
@@ -2632,13 +2895,24 @@ var CDETinjiApp = class _CDETinjiApp extends foundry.applications.api.Handlebars
_onRender(context, options) { _onRender(context, options) {
super._onRender(context, options); super._onRender(context, options);
this.#bindDirectInput(); this.#bindDirectInput();
this._updateHook = Hooks.on("cde:tinjiUpdated", () => this.render()); if (!this._updateHook) {
this._updateHook = Hooks.on("cde:tinjiUpdated", () => this.render());
}
if (!this._settingHook) {
this._settingHook = Hooks.on("updateSetting", (setting) => {
if (setting.key === `${SYSTEM_ID}.tinjiData`) this.render();
});
}
} }
_onClose(options) { _onClose(options) {
if (this._updateHook !== null) { if (this._updateHook !== null) {
Hooks.off("cde:tinjiUpdated", this._updateHook); Hooks.off("cde:tinjiUpdated", this._updateHook);
this._updateHook = null; this._updateHook = null;
} }
if (this._settingHook !== null) {
Hooks.off("updateSetting", this._settingHook);
this._settingHook = null;
}
super._onClose(options); super._onClose(options);
} }
#bindDirectInput() { #bindDirectInput() {
@@ -2667,7 +2941,7 @@ var CDETinjiApp = class _CDETinjiApp extends foundry.applications.api.Handlebars
return; return;
} }
await setTinjiValue(current - 1); await setTinjiValue(current - 1);
ChatMessage.create({ await ChatMessage.create({
user: game.user.id, user: game.user.id,
content: `<div class="cde-tinji-spend-msg"> content: `<div class="cde-tinji-spend-msg">
<i class="fas fa-star"></i> <i class="fas fa-star"></i>
@@ -2686,7 +2960,7 @@ var CDECombat = class extends Combat {
* for each selected combatant, then sync the result to the Combatant document. * for each selected combatant, then sync the result to the Combatant document.
*/ */
async rollInitiative(ids, options = {}) { async rollInitiative(ids, options = {}) {
const combatantIds = typeof ids === "string" ? [ids] : ids; const combatantIds = ids ? typeof ids === "string" ? [ids] : ids : this.combatants.map((c) => c.id);
for (const id of combatantIds) { for (const id of combatantIds) {
const combatant = this.combatants.get(id); const combatant = this.combatants.get(id);
if (!combatant) continue; if (!combatant) continue;
@@ -2895,8 +3169,7 @@ function refreshRollActions(rollCard, aspect, message) {
const tinji = getTinjiValue(); const tinji = getTinjiValue();
const successAvail = (loksyu[aspect]?.yin ?? 0) + (loksyu[aspect]?.yang ?? 0); const successAvail = (loksyu[aspect]?.yin ?? 0) + (loksyu[aspect]?.yang ?? 0);
const fasteAvail = (loksyu[fasteAspect]?.yin ?? 0) + (loksyu[fasteAspect]?.yang ?? 0); const fasteAvail = (loksyu[fasteAspect]?.yin ?? 0) + (loksyu[fasteAspect]?.yang ?? 0);
const isGM = game.user.isGM; const hasSomething = successAvail > 0 || fasteAvail > 0 || tinji > 0;
const hasSomething = successAvail > 0 || fasteAvail > 0 || isGM && tinji > 0;
if (!hasSomething) return; if (!hasSomething) return;
const aspLabel = game.i18n.localize(ASPECT_LABELS[aspect]); const aspLabel = game.i18n.localize(ASPECT_LABELS[aspect]);
const fasteLabel = game.i18n.localize(ASPECT_LABELS[fasteAspect]); const fasteLabel = game.i18n.localize(ASPECT_LABELS[fasteAspect]);
@@ -2915,7 +3188,7 @@ function refreshRollActions(rollCard, aspect, message) {
<span class="cde-roll-action-count">${fasteAvail}</span> <span class="cde-roll-action-count">${fasteAvail}</span>
</button>`; </button>`;
} }
if (isGM && tinji > 0) { if (tinji > 0) {
btns += `<button class="cde-roll-action-btn cde-roll-action--tinji" data-action="tinji"> btns += `<button class="cde-roll-action-btn cde-roll-action--tinji" data-action="tinji">
<span class="cde-roll-action-tinji-char">\u5929</span> <span class="cde-roll-action-tinji-char">\u5929</span>
<span class="cde-roll-action-label">${game.i18n.localize("CDE.TinJi2")}</span> <span class="cde-roll-action-label">${game.i18n.localize("CDE.TinJi2")}</span>
@@ -2968,7 +3241,11 @@ async function _drawFromLoksyu(message, aspect, type, aspectLabel) {
else entry.yin--; else entry.yin--;
} }
data[aspect] = entry; data[aspect] = entry;
await setLoksyuData(data); if (game.user.isGM) {
await setLoksyuData(data);
} else {
requestLoksyuDraw(aspect, order);
}
const flags = message?.flags?.[SYSTEM_ID]; const flags = message?.flags?.[SYSTEM_ID];
if (flags?.rollResult && message.isOwner) { if (flags?.rollResult && message.isOwner) {
const updated = foundry.utils.deepClone(flags.rollResult); const updated = foundry.utils.deepClone(flags.rollResult);
@@ -2988,7 +3265,7 @@ async function _drawFromLoksyu(message, aspect, type, aspectLabel) {
} }
const remain = entry.yin + entry.yang; const remain = entry.yin + entry.yang;
const typeLabel = type === "success" ? game.i18n.localize("CDE.Successes") : game.i18n.localize("CDE.AuspiciousDie"); const typeLabel = type === "success" ? game.i18n.localize("CDE.Successes") : game.i18n.localize("CDE.AuspiciousDie");
ChatMessage.create({ await ChatMessage.create({
user: game.user.id, user: game.user.id,
content: `<div class="cde-loksyu-draw-msg"> content: `<div class="cde-loksyu-draw-msg">
<div class="cde-loksyu-draw-header"> <div class="cde-loksyu-draw-header">
@@ -3007,14 +3284,17 @@ async function _drawFromLoksyu(message, aspect, type, aspectLabel) {
}); });
} }
async function _spendTinjiPostRoll() { async function _spendTinjiPostRoll() {
if (!game.user.isGM) return;
const current = getTinjiValue(); const current = getTinjiValue();
if (current <= 0) { if (current <= 0) {
ui.notifications.warn(game.i18n.localize("CDE.TinjiEmpty")); ui.notifications.warn(game.i18n.localize("CDE.TinjiEmpty"));
return; return;
} }
await setTinjiValue(current - 1); if (game.user.isGM) {
ChatMessage.create({ await setTinjiValue(current - 1);
} else {
requestTinjiSpend();
}
await ChatMessage.create({
user: game.user.id, user: game.user.id,
content: `<div class="cde-tinji-spend-msg"> content: `<div class="cde-tinji-spend-msg">
<span class="cde-tinji-icon">\u5929</span> <span class="cde-tinji-icon">\u5929</span>
@@ -3080,7 +3360,14 @@ function injectWelcomeActions(_message, html) {
// src/system.js // src/system.js
Hooks.once("i18nInit", preLocalizeConfig); Hooks.once("i18nInit", preLocalizeConfig);
Hooks.once("init", async () => { Hooks.once("init", async () => {
console.info(`CHRONIQUESDELETRANGE | Initializing ${SYSTEM_ID}`); console.log(
"%c\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557\n%c\u2551 Chroniques de l'\xC9trange \u2014 FoundryVTT \u2551\n%c\u2551 Syst\xE8me de jeu par Antre-Monde \xC9ditions \u2551\n%c\u2551 Made by Uberwald - https://www.ubwerwald.me \u2551\n%c\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D",
"color: #d4af37; font-weight: bold;",
"color: #e2e8f4;",
"color: #7d94b8;",
"color: #5a7a9a;",
"color: #d4af37; font-weight: bold;"
);
registerSettings(); registerSettings();
game.system.CONST = { MAGICS, SUBTYPES }; game.system.CONST = { MAGICS, SUBTYPES };
game.cde = { CDELoksyuApp, CDETinjiApp, CDEWheelApp }; game.cde = { CDELoksyuApp, CDETinjiApp, CDEWheelApp };
@@ -3164,6 +3451,7 @@ Hooks.once("ready", async () => {
await migrateIfNeeded(); await migrateIfNeeded();
await loadWelcomeSceneIfNeeded(); await loadWelcomeSceneIfNeeded();
CDEWheelApp.registerHooks(); CDEWheelApp.registerHooks();
registerSingletonSocket();
if (game.user.isGM) showWelcomeMessage(); if (game.user.isGM) showWelcomeMessage();
}); });
Hooks.on("renderChatLog", (_app, html) => { Hooks.on("renderChatLog", (_app, html) => {
+2 -2
View File
File diff suppressed because one or more lines are too long
+13
View File
@@ -34,6 +34,7 @@
"CDE.HeiSpend": "Dépense HEI", "CDE.HeiSpend": "Dépense HEI",
"CDE.SpellBonus": "Bonus Sort", "CDE.SpellBonus": "Bonus Sort",
"CDE.SpellPower": "Puissance du Sort", "CDE.SpellPower": "Puissance du Sort",
"CDE.BonusMalus": "Bonus/Malus",
"CDE.Boss": "Boss", "CDE.Boss": "Boss",
"CDE.Cancel": "Annuler", "CDE.Cancel": "Annuler",
"CDE.CancelChanges": "Annuler les modifications", "CDE.CancelChanges": "Annuler les modifications",
@@ -81,6 +82,7 @@
"CDE.EarthlyPrayer": "Prière Terrestre", "CDE.EarthlyPrayer": "Prière Terrestre",
"CDE.Effects": "Effets", "CDE.Effects": "Effets",
"CDE.Elixirs": "Élixirs", "CDE.Elixirs": "Élixirs",
"CDE.EnablePrompt": "Toujours demander avant de lancer",
"CDE.Error0": "Impossible de poursuivre : le nombre total de vos dés à lancer est inférieur à 1.", "CDE.Error0": "Impossible de poursuivre : le nombre total de vos dés à lancer est inférieur à 1.",
"CDE.Error1": "Impossible de poursuivre : vous ne possédez pas cette Compétence.", "CDE.Error1": "Impossible de poursuivre : vous ne possédez pas cette Compétence.",
"CDE.Error10": "Impossible de poursuivre : vous avez 0 ou moins dans cet Aspect.", "CDE.Error10": "Impossible de poursuivre : vous avez 0 ou moins dans cet Aspect.",
@@ -148,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",
@@ -199,6 +206,7 @@
"CDE.MartialArts": "Arts Martiaux", "CDE.MartialArts": "Arts Martiaux",
"CDE.Masterized": "Vous maîtrisez cette Technique", "CDE.Masterized": "Vous maîtrisez cette Technique",
"CDE.MasteryOfTheWay": "Maîtrise de la Voie", "CDE.MasteryOfTheWay": "Maîtrise de la Voie",
"CDE.Material": "Matériau",
"CDE.Max": "Max", "CDE.Max": "Max",
"CDE.Max-Present-Malus-Present-Max": "Max ● Actuel ● Malus ● Actuel ● Max", "CDE.Max-Present-Malus-Present-Max": "Max ● Actuel ● Malus ● Actuel ● Max",
"CDE.Max-Present-Present-Max": "Max ● Actuel ● Actuel ● Max", "CDE.Max-Present-Present-Max": "Max ● Actuel ● Actuel ● Max",
@@ -217,8 +225,11 @@
"CDE.MsgMagic2": "s'élève à ", "CDE.MsgMagic2": "s'élève à ",
"CDE.MsgMagic3": ". La puissance à invoquer est de ", "CDE.MsgMagic3": ". La puissance à invoquer est de ",
"CDE.MsgMagic4": ", si toutefois le sort est lancé avec succès.", "CDE.MsgMagic4": ", si toutefois le sort est lancé avec succès.",
"CDE.MoveUp": "Monter",
"CDE.MoveDown": "Descendre",
"CDE.NPCName": "Nom du PNJ", "CDE.NPCName": "Nom du PNJ",
"CDE.FatSi": "Fat Si", "CDE.FatSi": "Fat Si",
"CDE.FreePowerLevels": "Niveaux de puissance gratuits",
"CDE.PNJ": "PNJ", "CDE.PNJ": "PNJ",
"CDE.Name": "Nom", "CDE.Name": "Nom",
"CDE.Necromancy": "Nécromancie", "CDE.Necromancy": "Nécromancie",
@@ -280,6 +291,7 @@
"CDE.ResourceValue": "Valeur", "CDE.ResourceValue": "Valeur",
"CDE.Resources": "Ressources", "CDE.Resources": "Ressources",
"CDE.Results": "Résultats :", "CDE.Results": "Résultats :",
"CDE.Roll": "Jet",
"CDE.RollDifficulty": "Difficulté du Jet", "CDE.RollDifficulty": "Difficulté du Jet",
"CDE.SAN-ZING": "SAN ● ZING", "CDE.SAN-ZING": "SAN ● ZING",
"CDE.Sanhei": "Sanhei", "CDE.Sanhei": "Sanhei",
@@ -321,6 +333,7 @@
"CDE.Technique": "Technique", "CDE.Technique": "Technique",
"CDE.Technologies": "Technologies", "CDE.Technologies": "Technologies",
"CDE.Threat": "Niveau de Menace", "CDE.Threat": "Niveau de Menace",
"CDE.ThrowType": "Type de jet",
"CDE.TinJi": "Tin Ji :", "CDE.TinJi": "Tin Ji :",
"CDE.TinJi2": "Tin Ji", "CDE.TinJi2": "Tin Ji",
"CDE.TinJiName": "Nom de la Tin Ji", "CDE.TinJiName": "Nom de la Tin Ji",
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000028 MANIFEST-000052
+7 -7
View File
@@ -1,7 +1,7 @@
2026/06/09-14:17:25.397390 7ff7d5fed6c0 Recovering log #25 2026/06/14-22:49:59.100287 7f29d5fed6c0 Recovering log #50
2026/06/09-14:17:25.407486 7ff7d5fed6c0 Delete type=3 #23 2026/06/14-22:49:59.110340 7f29d5fed6c0 Delete type=3 #48
2026/06/09-14:17:25.407509 7ff7d5fed6c0 Delete type=0 #25 2026/06/14-22:49:59.110396 7f29d5fed6c0 Delete type=0 #50
2026/06/09-14:18:14.027632 7ff7d4feb6c0 Level-0 table #31: started 2026/06/14-22:52:09.917197 7f29d4feb6c0 Level-0 table #55: started
2026/06/09-14:18:14.027648 7ff7d4feb6c0 Level-0 table #31: 0 bytes OK 2026/06/14-22:52:09.917225 7f29d4feb6c0 Level-0 table #55: 0 bytes OK
2026/06/09-14:18:14.034363 7ff7d4feb6c0 Delete type=0 #29 2026/06/14-22:52:09.923646 7f29d4feb6c0 Delete type=0 #53
2026/06/09-14:18:14.046545 7ff7d4feb6c0 Manual compaction at level-0 from '!items!3aig6MWvZCRoWXPW' @ 72057594037927935 : 1 .. '!items!cXaQG1TBE0jzrbNt' @ 0 : 0; will stop at (end) 2026/06/14-22:52:09.923831 7f29d4feb6c0 Manual compaction at level-0 from '!items!3aig6MWvZCRoWXPW' @ 72057594037927935 : 1 .. '!items!cXaQG1TBE0jzrbNt' @ 0 : 0; will stop at (end)
+7 -11
View File
@@ -1,11 +1,7 @@
2026/06/01-22:35:11.880760 7f52c5bfd6c0 Delete type=3 #1 2026/06/14-22:22:39.661219 7f29d6fef6c0 Recovering log #46
2026/06/01-22:35:51.016255 7f52c4bfb6c0 Level-0 table #26: started 2026/06/14-22:22:39.671056 7f29d6fef6c0 Delete type=3 #44
2026/06/01-22:35:51.016270 7f52c4bfb6c0 Level-0 table #26: 0 bytes OK 2026/06/14-22:22:39.671114 7f29d6fef6c0 Delete type=0 #46
2026/06/01-22:35:51.023046 7f52c4bfb6c0 Delete type=0 #24 2026/06/14-22:32:37.952133 7f29d4feb6c0 Level-0 table #51: started
2026/06/01-22:35:51.047956 7f52c4bfb6c0 Manual compaction at level-0 from '!items!3aig6MWvZCRoWXPW' @ 72057594037927935 : 1 .. '!items!cXaQG1TBE0jzrbNt' @ 0 : 0; will stop at '!items!cXaQG1TBE0jzrbNt' @ 8 : 1 2026/06/14-22:32:37.952163 7f29d4feb6c0 Level-0 table #51: 0 bytes OK
2026/06/01-22:35:51.047965 7f52c4bfb6c0 Compacting 1@0 + 0@1 files 2026/06/14-22:32:37.958637 7f29d4feb6c0 Delete type=0 #49
2026/06/01-22:35:51.051139 7f52c4bfb6c0 Generated table #27@0: 4 keys, 1387 bytes 2026/06/14-22:32:37.965966 7f29d4feb6c0 Manual compaction at level-0 from '!items!3aig6MWvZCRoWXPW' @ 72057594037927935 : 1 .. '!items!cXaQG1TBE0jzrbNt' @ 0 : 0; will stop at (end)
2026/06/01-22:35:51.051154 7f52c4bfb6c0 Compacted 1@0 + 0@1 files => 1387 bytes
2026/06/01-22:35:51.057027 7f52c4bfb6c0 compacted to: files[ 0 1 0 0 0 0 0 ]
2026/06/01-22:35:51.057087 7f52c4bfb6c0 Delete type=2 #10
2026/06/01-22:35:51.067136 7f52c4bfb6c0 Manual compaction at level-0 from '!items!cXaQG1TBE0jzrbNt' @ 8 : 1 .. '!items!cXaQG1TBE0jzrbNt' @ 0 : 0; will stop at (end)
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000028 MANIFEST-000052
+7 -7
View File
@@ -1,7 +1,7 @@
2026/06/09-14:17:25.462325 7ff7d5fed6c0 Recovering log #25 2026/06/14-22:49:59.185177 7f29d5fed6c0 Recovering log #50
2026/06/09-14:17:25.472781 7ff7d5fed6c0 Delete type=3 #23 2026/06/14-22:49:59.194849 7f29d5fed6c0 Delete type=3 #48
2026/06/09-14:17:25.472805 7ff7d5fed6c0 Delete type=0 #25 2026/06/14-22:49:59.194912 7f29d5fed6c0 Delete type=0 #50
2026/06/09-14:18:14.058523 7ff7d4feb6c0 Level-0 table #31: started 2026/06/14-22:52:09.979071 7f29d4feb6c0 Level-0 table #55: started
2026/06/09-14:18:14.058534 7ff7d4feb6c0 Level-0 table #31: 0 bytes OK 2026/06/14-22:52:09.979102 7f29d4feb6c0 Level-0 table #55: 0 bytes OK
2026/06/09-14:18:14.065661 7ff7d4feb6c0 Delete type=0 #29 2026/06/14-22:52:09.985673 7f29d4feb6c0 Delete type=0 #53
2026/06/09-14:18:14.071686 7ff7d4feb6c0 Manual compaction at level-0 from '!journal!CDEGuideMain0001' @ 72057594037927935 : 1 .. '!journal.pages!CDEGuideMain0001.wgqIHHVlO9miegn1' @ 0 : 0; will stop at (end) 2026/06/14-22:52:10.010878 7f29d4feb6c0 Manual compaction at level-0 from '!journal!CDEGuideMain0001' @ 72057594037927935 : 1 .. '!journal.pages!CDEGuideMain0001.wgqIHHVlO9miegn1' @ 0 : 0; will stop at (end)
+7 -11
View File
@@ -1,11 +1,7 @@
2026/06/01-22:35:11.987923 7f52c53fc6c0 Delete type=3 #1 2026/06/14-22:22:39.748395 7f29d6fef6c0 Recovering log #46
2026/06/01-22:35:51.085088 7f52c4bfb6c0 Level-0 table #26: started 2026/06/14-22:22:39.759263 7f29d6fef6c0 Delete type=3 #44
2026/06/01-22:35:51.085109 7f52c4bfb6c0 Level-0 table #26: 0 bytes OK 2026/06/14-22:22:39.759326 7f29d6fef6c0 Delete type=0 #46
2026/06/01-22:35:51.091460 7f52c4bfb6c0 Delete type=0 #24 2026/06/14-22:32:38.022572 7f29d4feb6c0 Level-0 table #51: started
2026/06/01-22:35:51.107759 7f52c4bfb6c0 Manual compaction at level-0 from '!journal!CDEGuideMain0001' @ 72057594037927935 : 1 .. '!journal.pages!CDEGuideMain0001.wgqIHHVlO9miegn1' @ 0 : 0; will stop at '!journal.pages!CDEGuideMain0001.wgqIHHVlO9miegn1' @ 17 : 1 2026/06/14-22:32:38.022623 7f29d4feb6c0 Level-0 table #51: 0 bytes OK
2026/06/01-22:35:51.107763 7f52c4bfb6c0 Compacting 1@0 + 0@1 files 2026/06/14-22:32:38.028952 7f29d4feb6c0 Delete type=0 #49
2026/06/01-22:35:51.111831 7f52c4bfb6c0 Generated table #27@0: 21 keys, 18033 bytes 2026/06/14-22:32:38.054477 7f29d4feb6c0 Manual compaction at level-0 from '!journal!CDEGuideMain0001' @ 72057594037927935 : 1 .. '!journal.pages!CDEGuideMain0001.wgqIHHVlO9miegn1' @ 0 : 0; will stop at (end)
2026/06/01-22:35:51.111846 7f52c4bfb6c0 Compacted 1@0 + 0@1 files => 18033 bytes
2026/06/01-22:35:51.117896 7f52c4bfb6c0 compacted to: files[ 0 1 0 0 0 0 0 ]
2026/06/01-22:35:51.117946 7f52c4bfb6c0 Delete type=2 #10
2026/06/01-22:35:51.124817 7f52c4bfb6c0 Manual compaction at level-0 from '!journal.pages!CDEGuideMain0001.wgqIHHVlO9miegn1' @ 17 : 1 .. '!journal.pages!CDEGuideMain0001.wgqIHHVlO9miegn1' @ 0 : 0; will stop at (end)
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000028 MANIFEST-000052
+7 -7
View File
@@ -1,7 +1,7 @@
2026/06/09-14:17:25.419369 7ff7d6fef6c0 Recovering log #25 2026/06/14-22:49:59.128336 7f29d5fed6c0 Recovering log #50
2026/06/09-14:17:25.428427 7ff7d6fef6c0 Delete type=3 #23 2026/06/14-22:49:59.138475 7f29d5fed6c0 Delete type=3 #48
2026/06/09-14:17:25.428447 7ff7d6fef6c0 Delete type=0 #25 2026/06/14-22:49:59.138521 7f29d5fed6c0 Delete type=0 #50
2026/06/09-14:18:14.040287 7ff7d4feb6c0 Level-0 table #31: started 2026/06/14-22:52:09.937223 7f29d4feb6c0 Level-0 table #55: started
2026/06/09-14:18:14.040298 7ff7d4feb6c0 Level-0 table #31: 0 bytes OK 2026/06/14-22:52:09.937263 7f29d4feb6c0 Level-0 table #55: 0 bytes OK
2026/06/09-14:18:14.046472 7ff7d4feb6c0 Delete type=0 #29 2026/06/14-22:52:09.944028 7f29d4feb6c0 Delete type=0 #53
2026/06/09-14:18:14.046553 7ff7d4feb6c0 Manual compaction at level-0 from '!items!0NDBw1YB54q3hLH0' @ 72057594037927935 : 1 .. '!items!ykekdZlirabRobEF' @ 0 : 0; will stop at (end) 2026/06/14-22:52:09.951911 7f29d4feb6c0 Manual compaction at level-0 from '!items!0NDBw1YB54q3hLH0' @ 72057594037927935 : 1 .. '!items!ykekdZlirabRobEF' @ 0 : 0; will stop at (end)
+7 -11
View File
@@ -1,11 +1,7 @@
2026/06/01-22:35:11.914967 7f52c6bff6c0 Delete type=3 #1 2026/06/14-22:22:39.688631 7f29d6fef6c0 Recovering log #46
2026/06/01-22:35:51.023125 7f52c4bfb6c0 Level-0 table #26: started 2026/06/14-22:22:39.699291 7f29d6fef6c0 Delete type=3 #44
2026/06/01-22:35:51.023151 7f52c4bfb6c0 Level-0 table #26: 0 bytes OK 2026/06/14-22:22:39.699355 7f29d6fef6c0 Delete type=0 #46
2026/06/01-22:35:51.028968 7f52c4bfb6c0 Delete type=0 #24 2026/06/14-22:32:37.979476 7f29d4feb6c0 Level-0 table #51: started
2026/06/01-22:35:51.057155 7f52c4bfb6c0 Manual compaction at level-0 from '!items!0NDBw1YB54q3hLH0' @ 72057594037927935 : 1 .. '!items!ykekdZlirabRobEF' @ 0 : 0; will stop at '!items!ykekdZlirabRobEF' @ 108 : 1 2026/06/14-22:32:37.979507 7f29d4feb6c0 Level-0 table #51: 0 bytes OK
2026/06/01-22:35:51.057162 7f52c4bfb6c0 Compacting 1@0 + 0@1 files 2026/06/14-22:32:37.985981 7f29d4feb6c0 Delete type=0 #49
2026/06/01-22:35:51.060536 7f52c4bfb6c0 Generated table #27@0: 54 keys, 8881 bytes 2026/06/14-22:32:37.994186 7f29d4feb6c0 Manual compaction at level-0 from '!items!0NDBw1YB54q3hLH0' @ 72057594037927935 : 1 .. '!items!ykekdZlirabRobEF' @ 0 : 0; will stop at (end)
2026/06/01-22:35:51.060550 7f52c4bfb6c0 Compacted 1@0 + 0@1 files => 8881 bytes
2026/06/01-22:35:51.067003 7f52c4bfb6c0 compacted to: files[ 0 1 0 0 0 0 0 ]
2026/06/01-22:35:51.067054 7f52c4bfb6c0 Delete type=2 #10
2026/06/01-22:35:51.073072 7f52c4bfb6c0 Manual compaction at level-0 from '!items!ykekdZlirabRobEF' @ 108 : 1 .. '!items!ykekdZlirabRobEF' @ 0 : 0; will stop at (end)
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000028 MANIFEST-000052
+7 -7
View File
@@ -1,7 +1,7 @@
2026/06/09-14:17:25.429438 7ff7d67ee6c0 Recovering log #25 2026/06/14-22:49:59.141353 7f29d6fef6c0 Recovering log #50
2026/06/09-14:17:25.439353 7ff7d67ee6c0 Delete type=3 #23 2026/06/14-22:49:59.151373 7f29d6fef6c0 Delete type=3 #48
2026/06/09-14:17:25.439378 7ff7d67ee6c0 Delete type=0 #25 2026/06/14-22:49:59.151426 7f29d6fef6c0 Delete type=0 #50
2026/06/09-14:18:14.021756 7ff7d4feb6c0 Level-0 table #31: started 2026/06/14-22:52:09.944165 7f29d4feb6c0 Level-0 table #55: started
2026/06/09-14:18:14.021767 7ff7d4feb6c0 Level-0 table #31: 0 bytes OK 2026/06/14-22:52:09.944196 7f29d4feb6c0 Level-0 table #55: 0 bytes OK
2026/06/09-14:18:14.027556 7ff7d4feb6c0 Delete type=0 #29 2026/06/14-22:52:09.951751 7f29d4feb6c0 Delete type=0 #53
2026/06/09-14:18:14.046539 7ff7d4feb6c0 Manual compaction at level-0 from '!items!HKq5ANSGiBIdcnki' @ 72057594037927935 : 1 .. '!items!HKq5ANSGiBIdcnki' @ 0 : 0; will stop at (end) 2026/06/14-22:52:09.952015 7f29d4feb6c0 Manual compaction at level-0 from '!items!HKq5ANSGiBIdcnki' @ 72057594037927935 : 1 .. '!items!HKq5ANSGiBIdcnki' @ 0 : 0; will stop at (end)
+7 -11
View File
@@ -1,11 +1,7 @@
2026/06/01-22:35:11.932171 7f52c63fe6c0 Delete type=3 #1 2026/06/14-22:22:39.702477 7f29d5fed6c0 Recovering log #46
2026/06/01-22:35:51.010289 7f52c4bfb6c0 Level-0 table #26: started 2026/06/14-22:22:39.712938 7f29d5fed6c0 Delete type=3 #44
2026/06/01-22:35:51.010313 7f52c4bfb6c0 Level-0 table #26: 0 bytes OK 2026/06/14-22:22:39.712993 7f29d5fed6c0 Delete type=0 #46
2026/06/01-22:35:51.016160 7f52c4bfb6c0 Delete type=0 #24 2026/06/14-22:32:37.986092 7f29d4feb6c0 Level-0 table #51: started
2026/06/01-22:35:51.037980 7f52c4bfb6c0 Manual compaction at level-0 from '!items!HKq5ANSGiBIdcnki' @ 72057594037927935 : 1 .. '!items!HKq5ANSGiBIdcnki' @ 0 : 0; will stop at '!items!HKq5ANSGiBIdcnki' @ 2 : 1 2026/06/14-22:32:37.986120 7f29d4feb6c0 Level-0 table #51: 0 bytes OK
2026/06/01-22:35:51.037986 7f52c4bfb6c0 Compacting 1@0 + 0@1 files 2026/06/14-22:32:37.994045 7f29d4feb6c0 Delete type=0 #49
2026/06/01-22:35:51.041162 7f52c4bfb6c0 Generated table #27@0: 1 keys, 596 bytes 2026/06/14-22:32:37.994361 7f29d4feb6c0 Manual compaction at level-0 from '!items!HKq5ANSGiBIdcnki' @ 72057594037927935 : 1 .. '!items!HKq5ANSGiBIdcnki' @ 0 : 0; will stop at (end)
2026/06/01-22:35:51.041178 7f52c4bfb6c0 Compacted 1@0 + 0@1 files => 596 bytes
2026/06/01-22:35:51.047786 7f52c4bfb6c0 compacted to: files[ 0 1 0 0 0 0 0 ]
2026/06/01-22:35:51.047867 7f52c4bfb6c0 Delete type=2 #10
2026/06/01-22:35:51.067128 7f52c4bfb6c0 Manual compaction at level-0 from '!items!HKq5ANSGiBIdcnki' @ 2 : 1 .. '!items!HKq5ANSGiBIdcnki' @ 0 : 0; will stop at (end)
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000028 MANIFEST-000052
+7 -7
View File
@@ -1,7 +1,7 @@
2026/06/09-14:17:25.355145 7ff7d5fed6c0 Recovering log #25 2026/06/14-22:49:59.045733 7f29d57ec6c0 Recovering log #50
2026/06/09-14:17:25.364080 7ff7d5fed6c0 Delete type=3 #23 2026/06/14-22:49:59.056842 7f29d57ec6c0 Delete type=3 #48
2026/06/09-14:17:25.364100 7ff7d5fed6c0 Delete type=0 #25 2026/06/14-22:49:59.056897 7f29d57ec6c0 Delete type=0 #50
2026/06/09-14:18:14.002973 7ff7d4feb6c0 Level-0 table #31: started 2026/06/14-22:52:09.896535 7f29d4feb6c0 Level-0 table #55: started
2026/06/09-14:18:14.002982 7ff7d4feb6c0 Level-0 table #31: 0 bytes OK 2026/06/14-22:52:09.896595 7f29d4feb6c0 Level-0 table #55: 0 bytes OK
2026/06/09-14:18:14.008712 7ff7d4feb6c0 Delete type=0 #29 2026/06/14-22:52:09.903914 7f29d4feb6c0 Delete type=0 #53
2026/06/09-14:18:14.021671 7ff7d4feb6c0 Manual compaction at level-0 from '!items!2nKXEHLG0fXtSOdy' @ 72057594037927935 : 1 .. '!items!tlIc1bmIAbQeUwj7' @ 0 : 0; will stop at (end) 2026/06/14-22:52:09.923752 7f29d4feb6c0 Manual compaction at level-0 from '!items!2nKXEHLG0fXtSOdy' @ 72057594037927935 : 1 .. '!items!tlIc1bmIAbQeUwj7' @ 0 : 0; will stop at (end)
+7 -11
View File
@@ -1,11 +1,7 @@
2026/06/01-22:35:11.796994 7f52c63fe6c0 Delete type=3 #1 2026/06/14-22:22:39.603745 7f29d67ee6c0 Recovering log #46
2026/06/01-22:35:50.937098 7f52c4bfb6c0 Level-0 table #26: started 2026/06/14-22:22:39.614957 7f29d67ee6c0 Delete type=3 #44
2026/06/01-22:35:50.937158 7f52c4bfb6c0 Level-0 table #26: 0 bytes OK 2026/06/14-22:22:39.615010 7f29d67ee6c0 Delete type=0 #46
2026/06/01-22:35:50.943430 7f52c4bfb6c0 Delete type=0 #24 2026/06/14-22:32:37.939036 7f29d4feb6c0 Level-0 table #51: started
2026/06/01-22:35:50.962798 7f52c4bfb6c0 Manual compaction at level-0 from '!items!2nKXEHLG0fXtSOdy' @ 72057594037927935 : 1 .. '!items!tlIc1bmIAbQeUwj7' @ 0 : 0; will stop at '!items!tlIc1bmIAbQeUwj7' @ 40 : 1 2026/06/14-22:32:37.939102 7f29d4feb6c0 Level-0 table #51: 0 bytes OK
2026/06/01-22:35:50.962805 7f52c4bfb6c0 Compacting 1@0 + 0@1 files 2026/06/14-22:32:37.945655 7f29d4feb6c0 Delete type=0 #49
2026/06/01-22:35:50.966120 7f52c4bfb6c0 Generated table #27@0: 20 keys, 34454 bytes 2026/06/14-22:32:37.965934 7f29d4feb6c0 Manual compaction at level-0 from '!items!2nKXEHLG0fXtSOdy' @ 72057594037927935 : 1 .. '!items!tlIc1bmIAbQeUwj7' @ 0 : 0; will stop at (end)
2026/06/01-22:35:50.966138 7f52c4bfb6c0 Compacted 1@0 + 0@1 files => 34454 bytes
2026/06/01-22:35:50.972309 7f52c4bfb6c0 compacted to: files[ 0 1 0 0 0 0 0 ]
2026/06/01-22:35:50.972597 7f52c4bfb6c0 Delete type=2 #10
2026/06/01-22:35:51.004209 7f52c4bfb6c0 Manual compaction at level-0 from '!items!tlIc1bmIAbQeUwj7' @ 40 : 1 .. '!items!tlIc1bmIAbQeUwj7' @ 0 : 0; will stop at (end)
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000028 MANIFEST-000052
+7 -7
View File
@@ -1,7 +1,7 @@
2026/06/09-14:17:25.440402 7ff7d5fed6c0 Recovering log #25 2026/06/14-22:49:59.154286 7f29d5fed6c0 Recovering log #50
2026/06/09-14:17:25.450255 7ff7d5fed6c0 Delete type=3 #23 2026/06/14-22:49:59.165791 7f29d5fed6c0 Delete type=3 #48
2026/06/09-14:17:25.450286 7ff7d5fed6c0 Delete type=0 #25 2026/06/14-22:49:59.165837 7f29d5fed6c0 Delete type=0 #50
2026/06/09-14:18:14.046641 7ff7d4feb6c0 Level-0 table #31: started 2026/06/14-22:52:09.952102 7f29d4feb6c0 Level-0 table #55: started
2026/06/09-14:18:14.046651 7ff7d4feb6c0 Level-0 table #31: 0 bytes OK 2026/06/14-22:52:09.952142 7f29d4feb6c0 Level-0 table #55: 0 bytes OK
2026/06/09-14:18:14.052649 7ff7d4feb6c0 Delete type=0 #29 2026/06/14-22:52:09.958682 7f29d4feb6c0 Delete type=0 #53
2026/06/09-14:18:14.071674 7ff7d4feb6c0 Manual compaction at level-0 from '!actors!4ZjFZ1HoJV9mJStt' @ 72057594037927935 : 1 .. '!actors!zVpmacwoWEG8YTCQ' @ 0 : 0; will stop at (end) 2026/06/14-22:52:09.978831 7f29d4feb6c0 Manual compaction at level-0 from '!actors!4ZjFZ1HoJV9mJStt' @ 72057594037927935 : 1 .. '!actors!zVpmacwoWEG8YTCQ' @ 0 : 0; will stop at (end)
+7 -11
View File
@@ -1,11 +1,7 @@
2026/06/01-22:35:11.950430 7f52c6bff6c0 Delete type=3 #1 2026/06/14-22:22:39.716564 7f29d6fef6c0 Recovering log #46
2026/06/01-22:35:51.067200 7f52c4bfb6c0 Level-0 table #26: started 2026/06/14-22:22:39.726981 7f29d6fef6c0 Delete type=3 #44
2026/06/01-22:35:51.067214 7f52c4bfb6c0 Level-0 table #26: 0 bytes OK 2026/06/14-22:22:39.727264 7f29d6fef6c0 Delete type=0 #46
2026/06/01-22:35:51.072998 7f52c4bfb6c0 Delete type=0 #24 2026/06/14-22:32:37.994382 7f29d4feb6c0 Level-0 table #51: started
2026/06/01-22:35:51.091558 7f52c4bfb6c0 Manual compaction at level-0 from '!actors!4ZjFZ1HoJV9mJStt' @ 72057594037927935 : 1 .. '!actors!zVpmacwoWEG8YTCQ' @ 0 : 0; will stop at '!actors!zVpmacwoWEG8YTCQ' @ 98 : 1 2026/06/14-22:32:37.994411 7f29d4feb6c0 Level-0 table #51: 0 bytes OK
2026/06/01-22:35:51.091566 7f52c4bfb6c0 Compacting 1@0 + 0@1 files 2026/06/14-22:32:38.000939 7f29d4feb6c0 Delete type=0 #49
2026/06/01-22:35:51.095457 7f52c4bfb6c0 Generated table #27@0: 49 keys, 50053 bytes 2026/06/14-22:32:38.022274 7f29d4feb6c0 Manual compaction at level-0 from '!actors!4ZjFZ1HoJV9mJStt' @ 72057594037927935 : 1 .. '!actors!zVpmacwoWEG8YTCQ' @ 0 : 0; will stop at (end)
2026/06/01-22:35:51.095470 7f52c4bfb6c0 Compacted 1@0 + 0@1 files => 50053 bytes
2026/06/01-22:35:51.101290 7f52c4bfb6c0 compacted to: files[ 0 1 0 0 0 0 0 ]
2026/06/01-22:35:51.101339 7f52c4bfb6c0 Delete type=2 #10
2026/06/01-22:35:51.118034 7f52c4bfb6c0 Manual compaction at level-0 from '!actors!zVpmacwoWEG8YTCQ' @ 98 : 1 .. '!actors!zVpmacwoWEG8YTCQ' @ 0 : 0; will stop at (end)
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000006 MANIFEST-000030
+8 -8
View File
@@ -1,8 +1,8 @@
2026/06/09-14:17:25.451270 7ff7d5fed6c0 Recovering log #4 2026/06/14-22:49:59.169462 7f29d5fed6c0 Recovering log #28
2026/06/09-14:17:25.460590 7ff7d5fed6c0 Delete type=3 #2 2026/06/14-22:49:59.179433 7f29d5fed6c0 Delete type=3 #26
2026/06/09-14:17:25.460605 7ff7d5fed6c0 Delete type=0 #4 2026/06/14-22:49:59.179471 7f29d5fed6c0 Delete type=0 #28
2026/06/09-14:18:14.052676 7ff7d4feb6c0 Level-0 table #9: started 2026/06/14-22:52:09.985789 7f29d4feb6c0 Level-0 table #33: started
2026/06/09-14:18:14.052683 7ff7d4feb6c0 Level-0 table #9: 0 bytes OK 2026/06/14-22:52:09.985816 7f29d4feb6c0 Level-0 table #33: 0 bytes OK
2026/06/09-14:18:14.058481 7ff7d4feb6c0 Delete type=0 #7 2026/06/14-22:52:09.992966 7f29d4feb6c0 Delete type=0 #31
2026/06/09-14:18:14.071681 7ff7d4feb6c0 Manual compaction at level-0 from '!actors!5OGW1fRUn12aNMMV' @ 72057594037927935 : 1 .. '!actors.items!zSWwOmFiFjN4YxC9.w8LXSYQ1eIygtlKV' @ 0 : 0; will stop at (end) 2026/06/14-22:52:10.010894 7f29d4feb6c0 Manual compaction at level-0 from '!actors!5OGW1fRUn12aNMMV' @ 72057594037927935 : 1 .. '!actors.items!zSWwOmFiFjN4YxC9.w8LXSYQ1eIygtlKV' @ 0 : 0; will stop at (end)
2026/06/09-14:18:14.071693 7ff7d4feb6c0 Manual compaction at level-1 from '!actors!5OGW1fRUn12aNMMV' @ 72057594037927935 : 1 .. '!actors.items!zSWwOmFiFjN4YxC9.w8LXSYQ1eIygtlKV' @ 0 : 0; will stop at (end) 2026/06/14-22:52:10.011012 7f29d4feb6c0 Manual compaction at level-1 from '!actors!5OGW1fRUn12aNMMV' @ 72057594037927935 : 1 .. '!actors.items!zSWwOmFiFjN4YxC9.w8LXSYQ1eIygtlKV' @ 0 : 0; will stop at (end)
+8 -5
View File
@@ -1,5 +1,8 @@
2026/06/01-22:35:11.966943 7f52c5bfd6c0 Delete type=3 #1 2026/06/14-22:22:39.731841 7f29d6fef6c0 Recovering log #24
2026/06/01-22:35:51.073080 7f52c4bfb6c0 Level-0 table #5: started 2026/06/14-22:22:39.741957 7f29d6fef6c0 Delete type=3 #22
2026/06/01-22:35:51.079046 7f52c4bfb6c0 Level-0 table #5: 429877 bytes OK 2026/06/14-22:22:39.742028 7f29d6fef6c0 Delete type=0 #24
2026/06/01-22:35:51.084913 7f52c4bfb6c0 Delete type=0 #3 2026/06/14-22:32:38.007843 7f29d4feb6c0 Level-0 table #29: started
2026/06/01-22:35:51.107744 7f52c4bfb6c0 Manual compaction at level-0 from '!actors!5OGW1fRUn12aNMMV' @ 72057594037927935 : 1 .. '!actors.items!zSWwOmFiFjN4YxC9.w8LXSYQ1eIygtlKV' @ 0 : 0; will stop at (end) 2026/06/14-22:32:38.007886 7f29d4feb6c0 Level-0 table #29: 0 bytes OK
2026/06/14-22:32:38.014714 7f29d4feb6c0 Delete type=0 #27
2026/06/14-22:32:38.022308 7f29d4feb6c0 Manual compaction at level-0 from '!actors!5OGW1fRUn12aNMMV' @ 72057594037927935 : 1 .. '!actors.items!zSWwOmFiFjN4YxC9.w8LXSYQ1eIygtlKV' @ 0 : 0; will stop at (end)
2026/06/14-22:32:38.022452 7f29d4feb6c0 Manual compaction at level-1 from '!actors!5OGW1fRUn12aNMMV' @ 72057594037927935 : 1 .. '!actors.items!zSWwOmFiFjN4YxC9.w8LXSYQ1eIygtlKV' @ 0 : 0; will stop at (end)
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000028 MANIFEST-000052
+7 -7
View File
@@ -1,7 +1,7 @@
2026/06/09-14:17:25.408481 7ff7d57ec6c0 Recovering log #25 2026/06/14-22:49:59.113546 7f29d67ee6c0 Recovering log #50
2026/06/09-14:17:25.418586 7ff7d57ec6c0 Delete type=3 #23 2026/06/14-22:49:59.125289 7f29d67ee6c0 Delete type=3 #48
2026/06/09-14:17:25.418602 7ff7d57ec6c0 Delete type=0 #25 2026/06/14-22:49:59.125333 7f29d67ee6c0 Delete type=0 #50
2026/06/09-14:18:14.034422 7ff7d4feb6c0 Level-0 table #31: started 2026/06/14-22:52:09.930699 7f29d4feb6c0 Level-0 table #55: started
2026/06/09-14:18:14.034431 7ff7d4feb6c0 Level-0 table #31: 0 bytes OK 2026/06/14-22:52:09.930727 7f29d4feb6c0 Level-0 table #55: 0 bytes OK
2026/06/09-14:18:14.040246 7ff7d4feb6c0 Delete type=0 #29 2026/06/14-22:52:09.937112 7f29d4feb6c0 Delete type=0 #53
2026/06/09-14:18:14.046549 7ff7d4feb6c0 Manual compaction at level-0 from '!items!DC2kimCi9sWxqhXG' @ 72057594037927935 : 1 .. '!items!qzfAEhmvVxEMzm0k' @ 0 : 0; will stop at (end) 2026/06/14-22:52:09.951899 7f29d4feb6c0 Manual compaction at level-0 from '!items!DC2kimCi9sWxqhXG' @ 72057594037927935 : 1 .. '!items!qzfAEhmvVxEMzm0k' @ 0 : 0; will stop at (end)
+7 -11
View File
@@ -1,11 +1,7 @@
2026/06/01-22:35:11.897294 7f52c63fe6c0 Delete type=3 #1 2026/06/14-22:22:39.674682 7f29d5fed6c0 Recovering log #46
2026/06/01-22:35:51.004437 7f52c4bfb6c0 Level-0 table #26: started 2026/06/14-22:22:39.685769 7f29d5fed6c0 Delete type=3 #44
2026/06/01-22:35:51.004459 7f52c4bfb6c0 Level-0 table #26: 0 bytes OK 2026/06/14-22:22:39.685821 7f29d5fed6c0 Delete type=0 #46
2026/06/01-22:35:51.010228 7f52c4bfb6c0 Delete type=0 #24 2026/06/14-22:32:37.972754 7f29d4feb6c0 Level-0 table #51: started
2026/06/01-22:35:51.029047 7f52c4bfb6c0 Manual compaction at level-0 from '!items!DC2kimCi9sWxqhXG' @ 72057594037927935 : 1 .. '!items!qzfAEhmvVxEMzm0k' @ 0 : 0; will stop at '!items!qzfAEhmvVxEMzm0k' @ 10 : 1 2026/06/14-22:32:37.972783 7f29d4feb6c0 Level-0 table #51: 0 bytes OK
2026/06/01-22:35:51.029057 7f52c4bfb6c0 Compacting 1@0 + 0@1 files 2026/06/14-22:32:37.979353 7f29d4feb6c0 Delete type=0 #49
2026/06/01-22:35:51.032066 7f52c4bfb6c0 Generated table #27@0: 5 keys, 4934 bytes 2026/06/14-22:32:37.994176 7f29d4feb6c0 Manual compaction at level-0 from '!items!DC2kimCi9sWxqhXG' @ 72057594037927935 : 1 .. '!items!qzfAEhmvVxEMzm0k' @ 0 : 0; will stop at (end)
2026/06/01-22:35:51.032071 7f52c4bfb6c0 Compacted 1@0 + 0@1 files => 4934 bytes
2026/06/01-22:35:51.037821 7f52c4bfb6c0 compacted to: files[ 0 1 0 0 0 0 0 ]
2026/06/01-22:35:51.037900 7f52c4bfb6c0 Delete type=2 #10
2026/06/01-22:35:51.067117 7f52c4bfb6c0 Manual compaction at level-0 from '!items!qzfAEhmvVxEMzm0k' @ 10 : 1 .. '!items!qzfAEhmvVxEMzm0k' @ 0 : 0; will stop at (end)
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000047 MANIFEST-000072
+7 -7
View File
@@ -1,7 +1,7 @@
2026/06/09-14:17:25.474281 7ff7d57ec6c0 Recovering log #44 2026/06/14-22:49:59.198719 7f29d57ec6c0 Recovering log #69
2026/06/09-14:17:25.483017 7ff7d57ec6c0 Delete type=3 #42 2026/06/14-22:49:59.210387 7f29d57ec6c0 Delete type=3 #67
2026/06/09-14:17:25.483037 7ff7d57ec6c0 Delete type=0 #44 2026/06/14-22:49:59.210425 7f29d57ec6c0 Delete type=0 #69
2026/06/09-14:18:14.065718 7ff7d4feb6c0 Level-0 table #50: started 2026/06/14-22:52:09.958777 7f29d4feb6c0 Level-0 table #75: started
2026/06/09-14:18:14.065734 7ff7d4feb6c0 Level-0 table #50: 0 bytes OK 2026/06/14-22:52:09.958803 7f29d4feb6c0 Level-0 table #75: 0 bytes OK
2026/06/09-14:18:14.071596 7ff7d4feb6c0 Delete type=0 #48 2026/06/14-22:52:09.965059 7f29d4feb6c0 Delete type=0 #73
2026/06/09-14:18:14.071689 7ff7d4feb6c0 Manual compaction at level-0 from '!scenes!2C6gyZpvPxWlsVZi' @ 72057594037927935 : 1 .. '!scenes.levels!olYe9bhuXwRWQ8j7.defaultLevel0000' @ 0 : 0; will stop at (end) 2026/06/14-22:52:09.978852 7f29d4feb6c0 Manual compaction at level-0 from '!scenes!2C6gyZpvPxWlsVZi' @ 72057594037927935 : 1 .. '!scenes.levels!olYe9bhuXwRWQ8j7.defaultLevel0000' @ 0 : 0; will stop at (end)
+14 -11
View File
@@ -1,11 +1,14 @@
2026/06/01-22:35:12.007471 7f52c63fe6c0 Delete type=3 #1 2026/06/14-22:22:39.763440 7f29d67ee6c0 Recovering log #65
2026/06/01-22:35:51.124825 7f52c4bfb6c0 Level-0 table #45: started 2026/06/14-22:22:39.773982 7f29d67ee6c0 Delete type=3 #63
2026/06/01-22:35:51.124844 7f52c4bfb6c0 Level-0 table #45: 0 bytes OK 2026/06/14-22:22:39.774029 7f29d67ee6c0 Delete type=0 #65
2026/06/01-22:35:51.131445 7f52c4bfb6c0 Delete type=0 #43 2026/06/14-22:32:38.029088 7f29d4feb6c0 Level-0 table #70: started
2026/06/01-22:35:51.145660 7f52c4bfb6c0 Manual compaction at level-0 from '!scenes!2C6gyZpvPxWlsVZi' @ 72057594037927935 : 1 .. '!scenes.levels!olYe9bhuXwRWQ8j7.defaultLevel0000' @ 0 : 0; will stop at '!scenes.levels!olYe9bhuXwRWQ8j7.defaultLevel0000' @ 1 : 1 2026/06/14-22:32:38.032823 7f29d4feb6c0 Level-0 table #70: 3169 bytes OK
2026/06/01-22:35:51.145675 7f52c4bfb6c0 Compacting 1@0 + 0@1 files 2026/06/14-22:32:38.039619 7f29d4feb6c0 Delete type=0 #68
2026/06/01-22:35:51.149105 7f52c4bfb6c0 Generated table #46@0: 8 keys, 3172 bytes 2026/06/14-22:32:38.054502 7f29d4feb6c0 Manual compaction at level-0 from '!scenes!2C6gyZpvPxWlsVZi' @ 72057594037927935 : 1 .. '!scenes.levels!olYe9bhuXwRWQ8j7.defaultLevel0000' @ 0 : 0; will stop at '!scenes.levels!olYe9bhuXwRWQ8j7.defaultLevel0000' @ 29 : 1
2026/06/01-22:35:51.149125 7f52c4bfb6c0 Compacted 1@0 + 0@1 files => 3172 bytes 2026/06/14-22:32:38.054509 7f29d4feb6c0 Compacting 1@0 + 1@1 files
2026/06/01-22:35:51.155673 7f52c4bfb6c0 compacted to: files[ 0 1 0 0 0 0 0 ] 2026/06/14-22:32:38.058254 7f29d4feb6c0 Generated table #71@0: 8 keys, 3169 bytes
2026/06/01-22:35:51.155780 7f52c4bfb6c0 Delete type=2 #5 2026/06/14-22:32:38.058277 7f29d4feb6c0 Compacted 1@0 + 1@1 files => 3169 bytes
2026/06/01-22:35:51.163444 7f52c4bfb6c0 Manual compaction at level-0 from '!scenes.levels!olYe9bhuXwRWQ8j7.defaultLevel0000' @ 1 : 1 .. '!scenes.levels!olYe9bhuXwRWQ8j7.defaultLevel0000' @ 0 : 0; will stop at (end) 2026/06/14-22:32:38.064919 7f29d4feb6c0 compacted to: files[ 0 1 0 0 0 0 0 ]
2026/06/14-22:32:38.065170 7f29d4feb6c0 Delete type=2 #46
2026/06/14-22:32:38.065328 7f29d4feb6c0 Delete type=2 #70
2026/06/14-22:32:38.072993 7f29d4feb6c0 Manual compaction at level-0 from '!scenes.levels!olYe9bhuXwRWQ8j7.defaultLevel0000' @ 29 : 1 .. '!scenes.levels!olYe9bhuXwRWQ8j7.defaultLevel0000' @ 0 : 0; will stop at (end)
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000028 MANIFEST-000052
+7 -7
View File
@@ -1,7 +1,7 @@
2026/06/09-14:17:25.365128 7ff7d67ee6c0 Recovering log #25 2026/06/14-22:49:59.060460 7f29d5fed6c0 Recovering log #50
2026/06/09-14:17:25.375651 7ff7d67ee6c0 Delete type=3 #23 2026/06/14-22:49:59.070829 7f29d5fed6c0 Delete type=3 #48
2026/06/09-14:17:25.375676 7ff7d67ee6c0 Delete type=0 #25 2026/06/14-22:49:59.070872 7f29d5fed6c0 Delete type=0 #50
2026/06/09-14:18:14.008762 7ff7d4feb6c0 Level-0 table #31: started 2026/06/14-22:52:09.904026 7f29d4feb6c0 Level-0 table #55: started
2026/06/09-14:18:14.008772 7ff7d4feb6c0 Level-0 table #31: 0 bytes OK 2026/06/14-22:52:09.904053 7f29d4feb6c0 Level-0 table #55: 0 bytes OK
2026/06/09-14:18:14.014913 7ff7d4feb6c0 Delete type=0 #29 2026/06/14-22:52:09.910627 7f29d4feb6c0 Delete type=0 #53
2026/06/09-14:18:14.021676 7ff7d4feb6c0 Manual compaction at level-0 from '!items!2f51pcvFkcZjaxDk' @ 72057594037927935 : 1 .. '!items!yVN7PZw35iIaBl0H' @ 0 : 0; will stop at (end) 2026/06/14-22:52:09.923769 7f29d4feb6c0 Manual compaction at level-0 from '!items!2f51pcvFkcZjaxDk' @ 72057594037927935 : 1 .. '!items!yVN7PZw35iIaBl0H' @ 0 : 0; will stop at (end)
+7 -11
View File
@@ -1,11 +1,7 @@
2026/06/01-22:35:11.817600 7f52c53fc6c0 Delete type=3 #1 2026/06/14-22:22:39.618536 7f29d57ec6c0 Recovering log #46
2026/06/01-22:35:50.950071 7f52c4bfb6c0 Level-0 table #26: started 2026/06/14-22:22:39.628785 7f29d57ec6c0 Delete type=3 #44
2026/06/01-22:35:50.950101 7f52c4bfb6c0 Level-0 table #26: 0 bytes OK 2026/06/14-22:22:39.628834 7f29d57ec6c0 Delete type=0 #46
2026/06/01-22:35:50.956859 7f52c4bfb6c0 Delete type=0 #24 2026/06/14-22:32:37.966108 7f29d4feb6c0 Level-0 table #51: started
2026/06/01-22:35:50.982784 7f52c4bfb6c0 Manual compaction at level-0 from '!items!2f51pcvFkcZjaxDk' @ 72057594037927935 : 1 .. '!items!yVN7PZw35iIaBl0H' @ 0 : 0; will stop at '!items!yVN7PZw35iIaBl0H' @ 50 : 1 2026/06/14-22:32:37.966141 7f29d4feb6c0 Level-0 table #51: 0 bytes OK
2026/06/01-22:35:50.982795 7f52c4bfb6c0 Compacting 1@0 + 0@1 files 2026/06/14-22:32:37.972646 7f29d4feb6c0 Delete type=0 #49
2026/06/01-22:35:50.988505 7f52c4bfb6c0 Generated table #27@0: 25 keys, 124022 bytes 2026/06/14-22:32:37.994161 7f29d4feb6c0 Manual compaction at level-0 from '!items!2f51pcvFkcZjaxDk' @ 72057594037927935 : 1 .. '!items!yVN7PZw35iIaBl0H' @ 0 : 0; will stop at (end)
2026/06/01-22:35:50.988522 7f52c4bfb6c0 Compacted 1@0 + 0@1 files => 124022 bytes
2026/06/01-22:35:50.994813 7f52c4bfb6c0 compacted to: files[ 0 1 0 0 0 0 0 ]
2026/06/01-22:35:50.994888 7f52c4bfb6c0 Delete type=2 #10
2026/06/01-22:35:51.004229 7f52c4bfb6c0 Manual compaction at level-0 from '!items!yVN7PZw35iIaBl0H' @ 50 : 1 .. '!items!yVN7PZw35iIaBl0H' @ 0 : 0; will stop at (end)
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000028 MANIFEST-000052
+7 -7
View File
@@ -1,7 +1,7 @@
2026/06/09-14:17:25.377225 7ff7d6fef6c0 Recovering log #25 2026/06/14-22:49:59.074203 7f29d57ec6c0 Recovering log #50
2026/06/09-14:17:25.386451 7ff7d6fef6c0 Delete type=3 #23 2026/06/14-22:49:59.085196 7f29d57ec6c0 Delete type=3 #48
2026/06/09-14:17:25.386469 7ff7d6fef6c0 Delete type=0 #25 2026/06/14-22:49:59.085240 7f29d57ec6c0 Delete type=0 #50
2026/06/09-14:18:14.014965 7ff7d4feb6c0 Level-0 table #31: started 2026/06/14-22:52:09.923913 7f29d4feb6c0 Level-0 table #55: started
2026/06/09-14:18:14.014978 7ff7d4feb6c0 Level-0 table #31: 0 bytes OK 2026/06/14-22:52:09.923940 7f29d4feb6c0 Level-0 table #55: 0 bytes OK
2026/06/09-14:18:14.021573 7ff7d4feb6c0 Delete type=0 #29 2026/06/14-22:52:09.930579 7f29d4feb6c0 Delete type=0 #53
2026/06/09-14:18:14.021680 7ff7d4feb6c0 Manual compaction at level-0 from '!items!APN91pQL0NBfZsG7' @ 72057594037927935 : 1 .. '!items!xxZKGqDVxAfr140W' @ 0 : 0; will stop at (end) 2026/06/14-22:52:09.951882 7f29d4feb6c0 Manual compaction at level-0 from '!items!APN91pQL0NBfZsG7' @ 72057594037927935 : 1 .. '!items!xxZKGqDVxAfr140W' @ 0 : 0; will stop at (end)
+7 -11
View File
@@ -1,11 +1,7 @@
2026/06/01-22:35:11.836158 7f52c63fe6c0 Delete type=3 #1 2026/06/14-22:22:39.633149 7f29d6fef6c0 Recovering log #46
2026/06/01-22:35:50.943540 7f52c4bfb6c0 Level-0 table #26: started 2026/06/14-22:22:39.644445 7f29d6fef6c0 Delete type=3 #44
2026/06/01-22:35:50.943578 7f52c4bfb6c0 Level-0 table #26: 0 bytes OK 2026/06/14-22:22:39.644504 7f29d6fef6c0 Delete type=0 #46
2026/06/01-22:35:50.949973 7f52c4bfb6c0 Delete type=0 #24 2026/06/14-22:32:37.945805 7f29d4feb6c0 Level-0 table #51: started
2026/06/01-22:35:50.972693 7f52c4bfb6c0 Manual compaction at level-0 from '!items!APN91pQL0NBfZsG7' @ 72057594037927935 : 1 .. '!items!xxZKGqDVxAfr140W' @ 0 : 0; will stop at '!items!xxZKGqDVxAfr140W' @ 32 : 1 2026/06/14-22:32:37.945837 7f29d4feb6c0 Level-0 table #51: 0 bytes OK
2026/06/01-22:35:50.972702 7f52c4bfb6c0 Compacting 1@0 + 0@1 files 2026/06/14-22:32:37.952016 7f29d4feb6c0 Delete type=0 #49
2026/06/01-22:35:50.976827 7f52c4bfb6c0 Generated table #27@0: 16 keys, 8790 bytes 2026/06/14-22:32:37.965952 7f29d4feb6c0 Manual compaction at level-0 from '!items!APN91pQL0NBfZsG7' @ 72057594037927935 : 1 .. '!items!xxZKGqDVxAfr140W' @ 0 : 0; will stop at (end)
2026/06/01-22:35:50.976844 7f52c4bfb6c0 Compacted 1@0 + 0@1 files => 8790 bytes
2026/06/01-22:35:50.982579 7f52c4bfb6c0 compacted to: files[ 0 1 0 0 0 0 0 ]
2026/06/01-22:35:50.982676 7f52c4bfb6c0 Delete type=2 #10
2026/06/01-22:35:51.004221 7f52c4bfb6c0 Manual compaction at level-0 from '!items!xxZKGqDVxAfr140W' @ 32 : 1 .. '!items!xxZKGqDVxAfr140W' @ 0 : 0; will stop at (end)
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000028 MANIFEST-000052
+7 -7
View File
@@ -1,7 +1,7 @@
2026/06/09-14:17:25.387408 7ff7d57ec6c0 Recovering log #25 2026/06/14-22:49:59.087477 7f29d67ee6c0 Recovering log #50
2026/06/09-14:17:25.396611 7ff7d57ec6c0 Delete type=3 #23 2026/06/14-22:49:59.097614 7f29d67ee6c0 Delete type=3 #48
2026/06/09-14:17:25.396630 7ff7d57ec6c0 Delete type=0 #25 2026/06/14-22:49:59.097669 7f29d67ee6c0 Delete type=0 #50
2026/06/09-14:18:13.995792 7ff7d4feb6c0 Level-0 table #31: started 2026/06/14-22:52:09.910724 7f29d4feb6c0 Level-0 table #55: started
2026/06/09-14:18:13.995843 7ff7d4feb6c0 Level-0 table #31: 0 bytes OK 2026/06/14-22:52:09.910751 7f29d4feb6c0 Level-0 table #55: 0 bytes OK
2026/06/09-14:18:14.002901 7ff7d4feb6c0 Delete type=0 #29 2026/06/14-22:52:09.917084 7f29d4feb6c0 Delete type=0 #53
2026/06/09-14:18:14.021665 7ff7d4feb6c0 Manual compaction at level-0 from '!items!2IYbyCPF9LJojzsj' @ 72057594037927935 : 1 .. '!items!uOpWyMGK3oiUJ1Sl' @ 0 : 0; will stop at (end) 2026/06/14-22:52:09.923779 7f29d4feb6c0 Manual compaction at level-0 from '!items!2IYbyCPF9LJojzsj' @ 72057594037927935 : 1 .. '!items!uOpWyMGK3oiUJ1Sl' @ 0 : 0; will stop at (end)
+7 -11
View File
@@ -1,11 +1,7 @@
2026/06/01-22:35:11.855333 7f52c53fc6c0 Delete type=3 #1 2026/06/14-22:22:39.647097 7f29d67ee6c0 Recovering log #46
2026/06/01-22:35:50.956960 7f52c4bfb6c0 Level-0 table #26: started 2026/06/14-22:22:39.657645 7f29d67ee6c0 Delete type=3 #44
2026/06/01-22:35:50.956991 7f52c4bfb6c0 Level-0 table #26: 0 bytes OK 2026/06/14-22:22:39.657692 7f29d67ee6c0 Delete type=0 #46
2026/06/01-22:35:50.962751 7f52c4bfb6c0 Delete type=0 #24 2026/06/14-22:32:37.958747 7f29d4feb6c0 Level-0 table #51: started
2026/06/01-22:35:50.994960 7f52c4bfb6c0 Manual compaction at level-0 from '!items!2IYbyCPF9LJojzsj' @ 72057594037927935 : 1 .. '!items!uOpWyMGK3oiUJ1Sl' @ 0 : 0; will stop at '!items!uOpWyMGK3oiUJ1Sl' @ 30 : 1 2026/06/14-22:32:37.958775 7f29d4feb6c0 Level-0 table #51: 0 bytes OK
2026/06/01-22:35:50.994966 7f52c4bfb6c0 Compacting 1@0 + 0@1 files 2026/06/14-22:32:37.965805 7f29d4feb6c0 Delete type=0 #49
2026/06/01-22:35:50.998285 7f52c4bfb6c0 Generated table #27@0: 15 keys, 4529 bytes 2026/06/14-22:32:37.966067 7f29d4feb6c0 Manual compaction at level-0 from '!items!2IYbyCPF9LJojzsj' @ 72057594037927935 : 1 .. '!items!uOpWyMGK3oiUJ1Sl' @ 0 : 0; will stop at (end)
2026/06/01-22:35:50.998300 7f52c4bfb6c0 Compacted 1@0 + 0@1 files => 4529 bytes
2026/06/01-22:35:51.004081 7f52c4bfb6c0 compacted to: files[ 0 1 0 0 0 0 0 ]
2026/06/01-22:35:51.004146 7f52c4bfb6c0 Delete type=2 #10
2026/06/01-22:35:51.004357 7f52c4bfb6c0 Manual compaction at level-0 from '!items!uOpWyMGK3oiUJ1Sl' @ 30 : 1 .. '!items!uOpWyMGK3oiUJ1Sl' @ 0 : 0; will stop at (end)
+1
View File
@@ -28,6 +28,7 @@ export function preLocalizeConfig() {
magic.aspectlabel = game.i18n.localize(magic.aspectlabel) magic.aspectlabel = game.i18n.localize(magic.aspectlabel)
Object.values(magic.speciality).forEach((spec) => { Object.values(magic.speciality).forEach((spec) => {
spec.label = game.i18n.localize(spec.label) spec.label = game.i18n.localize(spec.label)
spec.labelelementkey = spec.labelelement
spec.labelelement = game.i18n.localize(spec.labelelement) spec.labelelement = game.i18n.localize(spec.labelelement)
}) })
}) })
+5 -2
View File
@@ -122,9 +122,8 @@ export default class CharacterDataModel extends foundry.abstract.TypeDataModel {
typeofthrow: numberField(0), typeofthrow: numberField(0),
aspectskill: numberField(0), aspectskill: numberField(0),
bonusmalusskill: numberField(0), bonusmalusskill: numberField(0),
aspectspeciality: numberField(0),
rolldifficulty: numberField(0), rolldifficulty: numberField(0),
bonusmalusspeciality: numberField(0), freepowerlevels: numberField(0),
}), }),
}), }),
aspect: new fields.SchemaField({ aspect: new fields.SchemaField({
@@ -163,6 +162,10 @@ export default class CharacterDataModel extends foundry.abstract.TypeDataModel {
nine: componentField(), nine: componentField(),
zero: componentField(), zero: componentField(),
}), }),
magicOrder: new fields.ArrayField(
new fields.StringField({ required: true, nullable: false, initial: "" }),
{ required: true, initial: [] }
),
magics: new fields.SchemaField({ magics: new fields.SchemaField({
internalcinnabar: magicField(), internalcinnabar: magicField(),
alchemy: magicField(), alchemy: magicField(),
+1 -1
View File
@@ -25,7 +25,7 @@ export default class ArmorDataModel extends foundry.abstract.TypeDataModel {
domain: stringField(""), domain: stringField(""),
obtainLevel: intField(0, { min: 0, max: 5 }), obtainLevel: intField(0, { min: 0, max: 5 }),
obtainDifficulty: intField(0, { min: 0, max: 3 }), obtainDifficulty: intField(0, { min: 0, max: 3 }),
quantity: intField(1), quantity: intField(1, { min: 0 }),
notes: htmlField(""), notes: htmlField(""),
} }
} }
+1 -1
View File
@@ -24,7 +24,7 @@ export default class IngredientDataModel extends foundry.abstract.TypeDataModel
school: stringField("all"), school: stringField("all"),
obtainLevel: intField(0, { min: 0, max: 5 }), obtainLevel: intField(0, { min: 0, max: 5 }),
obtainDifficulty: intField(0, { min: 0, max: 3 }), obtainDifficulty: intField(0, { min: 0, max: 3 }),
quantity: intField(1), quantity: intField(1, { min: 0 }),
notes: htmlField(""), notes: htmlField(""),
} }
} }
+1 -1
View File
@@ -30,7 +30,7 @@ export default class KungfuDataModel extends foundry.abstract.TypeDataModel {
reference: stringField(""), reference: stringField(""),
description: htmlField(""), description: htmlField(""),
orientation: stringField("yin"), // yin | yang | yinyang orientation: stringField("yin"), // yin | yang | yinyang
aspect: stringField("metal"), // metal | eau | terre | feu | bois aspect: stringField("metal"), // metal | water | earth | fire | wood
skill: stringField("kungfu"), // kungfu | rangedcombat skill: stringField("kungfu"), // kungfu | rangedcombat
speciality: stringField(""), speciality: stringField(""),
style: stringField(""), style: stringField(""),
+1 -1
View File
@@ -21,7 +21,7 @@ export default class SpellDataModel extends foundry.abstract.TypeDataModel {
reference: stringField(""), reference: stringField(""),
description: htmlField(""), description: htmlField(""),
specialityname: stringField(""), specialityname: stringField(""),
associatedelement: stringField("metal"), // metal | eau | terre | feu | bois associatedelement: stringField("metal"), // metal | water | earth | fire | wood
hei: stringField(""), hei: stringField(""),
realizationtimeritual: stringField(""), realizationtimeritual: stringField(""),
realizationtimeaccelerated: stringField(""), realizationtimeaccelerated: stringField(""),
+3 -1
View File
@@ -17,10 +17,12 @@ export default class WeaponDataModel extends foundry.abstract.TypeDataModel {
const stringField = (initial = "") => new fields.StringField({ required: true, nullable: false, initial }) const stringField = (initial = "") => new fields.StringField({ required: true, nullable: false, initial })
const htmlField = (initial = "") => new fields.HTMLField({ required: true, nullable: false, initial, textSearch: true }) const htmlField = (initial = "") => new fields.HTMLField({ required: true, nullable: false, initial, textSearch: true })
const intField = (initial = 0, opts = {}) => new fields.NumberField({ required: true, nullable: false, integer: true, initial, ...opts }) const intField = (initial = 0, opts = {}) => new fields.NumberField({ required: true, nullable: false, integer: true, initial, ...opts })
const boolField = (initial = false) => new fields.BooleanField({ required: true, initial })
return { return {
reference: stringField(""), reference: stringField(""),
description: htmlField(""), description: htmlField(""),
hasSpeciality: boolField(false),
weaponType: stringField("melee"), weaponType: stringField("melee"),
material: stringField(""), material: stringField(""),
damageAspect: stringField("metal"), damageAspect: stringField("metal"),
@@ -28,7 +30,7 @@ export default class WeaponDataModel extends foundry.abstract.TypeDataModel {
range: stringField("contact"), // contact | courte | mediane | longue | extreme range: stringField("contact"), // contact | courte | mediane | longue | extreme
obtainLevel: intField(0, { min: 0, max: 5 }), obtainLevel: intField(0, { min: 0, max: 5 }),
obtainDifficulty: intField(0, { min: 0, max: 3 }), obtainDifficulty: intField(0, { min: 0, max: 3 }),
quantity: intField(1), quantity: intField(1, { min: 0 }),
notes: htmlField(""), notes: htmlField(""),
} }
} }
+3 -1
View File
@@ -31,7 +31,9 @@ export class CDECombat extends Combat {
* for each selected combatant, then sync the result to the Combatant document. * for each selected combatant, then sync the result to the Combatant document.
*/ */
async rollInitiative(ids, options = {}) { async rollInitiative(ids, options = {}) {
const combatantIds = typeof ids === "string" ? [ids] : ids const combatantIds = ids
? (typeof ids === "string" ? [ids] : ids)
: this.combatants.map(c => c.id)
for (const id of combatantIds) { for (const id of combatantIds) {
const combatant = this.combatants.get(id) const combatant = this.combatants.get(id)
if (!combatant) continue if (!combatant) continue
+92
View File
@@ -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),
+14 -1
View File
@@ -28,6 +28,7 @@ import { CDEItemSheet, CDEKungfuSheet, CDESpellSheet, CDESupernaturalSheet, CDEW
import { CDELoksyuApp } from "./ui/apps/loksyu-app.js" import { CDELoksyuApp } from "./ui/apps/loksyu-app.js"
import { CDETinjiApp } from "./ui/apps/tinji-app.js" import { CDETinjiApp } from "./ui/apps/tinji-app.js"
import { CDEWheelApp } from "./ui/apps/wheel-app.js" import { CDEWheelApp } from "./ui/apps/wheel-app.js"
import { registerSingletonSocket } from "./ui/apps/singletons.js"
import { injectRollActions, refreshAllRollActions } from "./ui/roll-actions.js" import { injectRollActions, refreshAllRollActions } from "./ui/roll-actions.js"
import { CDECombat } from "./documents/combat.js" import { CDECombat } from "./documents/combat.js"
import { showWelcomeMessage, injectWelcomeActions } from "./ui/apps/welcome.js" import { showWelcomeMessage, injectWelcomeActions } from "./ui/apps/welcome.js"
@@ -35,7 +36,18 @@ import { showWelcomeMessage, injectWelcomeActions } from "./ui/apps/welcome.js"
Hooks.once("i18nInit", preLocalizeConfig) Hooks.once("i18nInit", preLocalizeConfig)
Hooks.once("init", async () => { Hooks.once("init", async () => {
console.info(`CHRONIQUESDELETRANGE | Initializing ${SYSTEM_ID}`) console.log(
"%c╔══════════════════════════════════════════════════════════╗\n" +
"%c║ Chroniques de l'Étrange — FoundryVTT ║\n" +
"%c║ Système de jeu par Antre-Monde Éditions ║\n" +
"%c║ Made by Uberwald - https://www.ubwerwald.me ║\n" +
"%c╚══════════════════════════════════════════════════════════╝",
"color: #d4af37; font-weight: bold;",
"color: #e2e8f4;",
"color: #7d94b8;",
"color: #5a7a9a;",
"color: #d4af37; font-weight: bold;",
)
registerSettings() registerSettings()
@@ -133,6 +145,7 @@ Hooks.once("ready", async () => {
await migrateIfNeeded() await migrateIfNeeded()
await loadWelcomeSceneIfNeeded() await loadWelcomeSceneIfNeeded()
CDEWheelApp.registerHooks() CDEWheelApp.registerHooks()
registerSingletonSocket()
if (game.user.isGM) showWelcomeMessage() if (game.user.isGM) showWelcomeMessage()
}) })
+15 -1
View File
@@ -43,6 +43,9 @@ export class CDELoksyuApp extends foundry.applications.api.HandlebarsApplication
/** @type {Function|null} bound hook handler */ /** @type {Function|null} bound hook handler */
_updateHook = null _updateHook = null
/** @type {Function|null} updateSetting hook handler (for socket-propagated writes) */
_settingHook = null
/** Singleton accessor — open or bring to front */ /** Singleton accessor — open or bring to front */
static open() { static open() {
const existing = Array.from(foundry.applications.instances.values()).find( const existing = Array.from(foundry.applications.instances.values()).find(
@@ -78,7 +81,14 @@ export class CDELoksyuApp extends foundry.applications.api.HandlebarsApplication
super._onRender(context, options) super._onRender(context, options)
this.#bindInputs() this.#bindInputs()
this._updateHook = Hooks.on("cde:loksyuUpdated", () => this.render()) if (!this._updateHook) {
this._updateHook = Hooks.on("cde:loksyuUpdated", () => this.render())
}
if (!this._settingHook) {
this._settingHook = Hooks.on("updateSetting", (setting) => {
if (setting.key === `${SYSTEM_ID}.loksyuData`) this.render()
})
}
} }
_onClose(options) { _onClose(options) {
@@ -86,6 +96,10 @@ export class CDELoksyuApp extends foundry.applications.api.HandlebarsApplication
Hooks.off("cde:loksyuUpdated", this._updateHook) Hooks.off("cde:loksyuUpdated", this._updateHook)
this._updateHook = null this._updateHook = null
} }
if (this._settingHook !== null) {
Hooks.off("updateSetting", this._settingHook)
this._settingHook = null
}
super._onClose(options) super._onClose(options)
} }
+59 -22
View File
@@ -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 20242026 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()
}
} }
+110 -13
View File
@@ -15,10 +15,14 @@
* Loksyu / TinJi settings-based helpers. * Loksyu / TinJi settings-based helpers.
* *
* Data is stored as world settings instead of singleton Actor documents. * Data is stored as world settings instead of singleton Actor documents.
* Socket-based replication allows non-GM players to update Loksyu and
* TinJi — the GM processes the actual writes.
*/ */
import { SYSTEM_ID, WU_XING_CYCLE, ASPECT_FACES } from "../../config/constants.js" import { SYSTEM_ID, WU_XING_CYCLE, ASPECT_FACES } from "../../config/constants.js"
const SOCKET_CHANNEL = `system.${SYSTEM_ID}`
/** Read the current loksyu data object from world settings */ /** Read the current loksyu data object from world settings */
export function getLoksyuData() { export function getLoksyuData() {
return game.settings.get(SYSTEM_ID, "loksyuData") ?? { return game.settings.get(SYSTEM_ID, "loksyuData") ?? {
@@ -26,27 +30,112 @@ export function getLoksyuData() {
} }
} }
/** Write the loksyu data object to world settings */ /** Write the loksyu data object to world settings (GM only). */
export async function setLoksyuData(data) { async function writeLoksyuData(data) {
await game.settings.set(SYSTEM_ID, "loksyuData", data) await game.settings.set(SYSTEM_ID, "loksyuData", data)
Hooks.callAll("cde:loksyuUpdated", data) Hooks.callAll("cde:loksyuUpdated", data)
} }
/** Write TinJi value to world settings (GM only). */
async function writeTinjiValue(value) {
value = Math.max(0, value)
await game.settings.set(SYSTEM_ID, "tinjiData", value)
Hooks.callAll("cde:tinjiUpdated", value)
}
/** Write the loksyu data object — non-GM emits via socket. */
export async function setLoksyuData(data) {
if (game.user.isGM) return writeLoksyuData(data)
game.socket.emit(SOCKET_CHANNEL, { action: "setLoksyuData", data })
}
/** Read current TinJi value from world settings */ /** Read current TinJi value from world settings */
export function getTinjiValue() { export function getTinjiValue() {
return game.settings.get(SYSTEM_ID, "tinjiData") ?? 0 return game.settings.get(SYSTEM_ID, "tinjiData") ?? 0
} }
/** Write TinJi value to world settings */ /** Write TinJi value — non-GM emits via socket. */
export async function setTinjiValue(value) { export async function setTinjiValue(value) {
await game.settings.set(SYSTEM_ID, "tinjiData", Math.max(0, value)) if (game.user.isGM) return writeTinjiValue(value)
Hooks.callAll("cde:tinjiUpdated", Math.max(0, value)) game.socket.emit(SOCKET_CHANNEL, { action: "setTinjiValue", value })
}
/**
* Non-GM: request an atomic Loksyu die draw via socket. The GM reads
* current state, decrements, and writes — avoiding stale-read races.
*/
export function requestLoksyuDraw(aspect, order) {
game.socket.emit(SOCKET_CHANNEL, { action: "loksyuDraw", aspect, order })
}
/**
* Non-GM: request an atomic TinJi spend via socket. The GM reads current
* value, decrements if > 0, and writes.
*/
export function requestTinjiSpend() {
game.socket.emit(SOCKET_CHANNEL, { action: "tinjiSpend" })
}
/**
* Register the socket listener that processes Loksyu/TinJi write requests
* from non-GM clients. Only the GM actually performs the writes; other
* clients ignore the message.
*
* Must be called after the `ready` hook (when game.socket is available).
*/
export function registerSingletonSocket() {
game.socket.on(SOCKET_CHANNEL, async (payload) => {
if (!game.user.isGM) return
switch (payload.action) {
case "setLoksyuData":
await writeLoksyuData(payload.data)
break
case "setTinjiValue":
await writeTinjiValue(payload.value)
break
case "updateLoksyuFromRoll":
await updateLoksyuFromRoll(payload.activeAspect, payload.faces)
break
case "updateTinjiFromRoll":
await updateTinjiFromRoll(payload.delta)
break
case "loksyuDraw": {
const data = getLoksyuData()
const entry = data[payload.aspect] ?? { yin: 0, yang: 0 }
const order = payload.order ?? "yang-first"
if (order === "yin-first") {
if (entry.yin > 0) entry.yin--
else entry.yang--
} else if (order === "balanced") {
if (entry.yin > entry.yang) entry.yin--
else if (entry.yang > entry.yin) entry.yang--
else if (entry.yang > 0) entry.yang--
else entry.yin--
} else {
if (entry.yang > 0) entry.yang--
else entry.yin--
}
data[payload.aspect] = entry
await writeLoksyuData(data)
break
}
case "tinjiSpend": {
const cur = getTinjiValue()
if (cur > 0) await writeTinjiValue(cur - 1)
break
}
}
})
} }
/** /**
* After a WuXing roll, add the loksyu faces (yin + yang) of the relevant * After a WuXing roll, add the loksyu faces (yin + yang) of the relevant
* aspect to the loksyu settings data. * aspect to the loksyu settings data.
* *
* Non-GM: emits raw activeAspect+faces via socket so the GM recomputes
* from current state — avoids stale-read races when two players roll
* simultaneously.
*
* @param {string} activeAspect - e.g. "fire" * @param {string} activeAspect - e.g. "fire"
* @param {Object} faces - Die face counts { 0: n, 1: n, …, 9: n } * @param {Object} faces - Die face counts { 0: n, 1: n, …, 9: n }
*/ */
@@ -62,22 +151,30 @@ export async function updateLoksyuFromRoll(activeAspect, faces) {
const yangCount = faces[yangFace] ?? 0 const yangCount = faces[yangFace] ?? 0
if (yinCount === 0 && yangCount === 0) return if (yinCount === 0 && yangCount === 0) return
const data = getLoksyuData() if (game.user.isGM) {
const current = data[lokAspect] ?? { yin: 0, yang: 0 } const data = getLoksyuData()
data[lokAspect] = { const current = data[lokAspect] ?? { yin: 0, yang: 0 }
yin: (current.yin ?? 0) + yinCount, data[lokAspect] = { yin: (current.yin ?? 0) + yinCount, yang: (current.yang ?? 0) + yangCount }
yang: (current.yang ?? 0) + yangCount, await writeLoksyuData(data)
} else {
game.socket.emit(SOCKET_CHANNEL, { action: "updateLoksyuFromRoll", activeAspect, faces })
} }
await setLoksyuData(data)
} }
/** /**
* After a WuXing roll, add tinji faces to the TinJi settings. * After a WuXing roll, add tinji faces to the TinJi settings.
* *
* Non-GM: emits delta via socket so the GM adds to the current value
* atomically.
*
* @param {number} count - Number of tinji faces rolled * @param {number} count - Number of tinji faces rolled
*/ */
export async function updateTinjiFromRoll(count) { export async function updateTinjiFromRoll(count) {
if (!count || count <= 0) return if (!count || count <= 0) return
const current = getTinjiValue() if (game.user.isGM) {
await setTinjiValue(current + count) const current = getTinjiValue()
await writeTinjiValue(current + count)
} else {
game.socket.emit(SOCKET_CHANNEL, { action: "updateTinjiFromRoll", delta: count })
}
} }
+16 -2
View File
@@ -44,6 +44,9 @@ export class CDETinjiApp extends foundry.applications.api.HandlebarsApplicationM
/** @type {Function|null} */ /** @type {Function|null} */
_updateHook = null _updateHook = null
/** @type {Function|null} */
_settingHook = null
static open() { static open() {
const existing = Array.from(foundry.applications.instances.values()).find( const existing = Array.from(foundry.applications.instances.values()).find(
(app) => app instanceof CDETinjiApp (app) => app instanceof CDETinjiApp
@@ -64,7 +67,14 @@ export class CDETinjiApp extends foundry.applications.api.HandlebarsApplicationM
_onRender(context, options) { _onRender(context, options) {
super._onRender(context, options) super._onRender(context, options)
this.#bindDirectInput() this.#bindDirectInput()
this._updateHook = Hooks.on("cde:tinjiUpdated", () => this.render()) if (!this._updateHook) {
this._updateHook = Hooks.on("cde:tinjiUpdated", () => this.render())
}
if (!this._settingHook) {
this._settingHook = Hooks.on("updateSetting", (setting) => {
if (setting.key === `${SYSTEM_ID}.tinjiData`) this.render()
})
}
} }
_onClose(options) { _onClose(options) {
@@ -72,6 +82,10 @@ export class CDETinjiApp extends foundry.applications.api.HandlebarsApplicationM
Hooks.off("cde:tinjiUpdated", this._updateHook) Hooks.off("cde:tinjiUpdated", this._updateHook)
this._updateHook = null this._updateHook = null
} }
if (this._settingHook !== null) {
Hooks.off("updateSetting", this._settingHook)
this._settingHook = null
}
super._onClose(options) super._onClose(options)
} }
@@ -105,7 +119,7 @@ export class CDETinjiApp extends foundry.applications.api.HandlebarsApplicationM
return return
} }
await setTinjiValue(current - 1) await setTinjiValue(current - 1)
ChatMessage.create({ await ChatMessage.create({
user: game.user.id, user: game.user.id,
content: `<div class="cde-tinji-spend-msg"> content: `<div class="cde-tinji-spend-msg">
<i class="fas fa-star"></i> <i class="fas fa-star"></i>
+15 -9
View File
@@ -20,7 +20,7 @@
* with the new counts, without creating noise. * with the new counts, without creating noise.
*/ */
import { getLoksyuData, setLoksyuData, getTinjiValue, setTinjiValue } from "./apps/singletons.js" import { getLoksyuData, setLoksyuData, getTinjiValue, setTinjiValue, requestLoksyuDraw, requestTinjiSpend } from "./apps/singletons.js"
import { SYSTEM_ID, WU_XING_CYCLE, ASPECT_LABELS, ASPECT_ICONS } from "../config/constants.js" import { SYSTEM_ID, WU_XING_CYCLE, ASPECT_LABELS, ASPECT_ICONS } from "../config/constants.js"
const RESULT_TEMPLATE = "systems/fvtt-chroniques-de-l-etrange/templates/form/cde-dice-result.html" const RESULT_TEMPLATE = "systems/fvtt-chroniques-de-l-etrange/templates/form/cde-dice-result.html"
@@ -53,9 +53,8 @@ function refreshRollActions(rollCard, aspect, message) {
const successAvail = (loksyu[aspect]?.yin ?? 0) + (loksyu[aspect]?.yang ?? 0) const successAvail = (loksyu[aspect]?.yin ?? 0) + (loksyu[aspect]?.yang ?? 0)
const fasteAvail = (loksyu[fasteAspect]?.yin ?? 0) + (loksyu[fasteAspect]?.yang ?? 0) const fasteAvail = (loksyu[fasteAspect]?.yin ?? 0) + (loksyu[fasteAspect]?.yang ?? 0)
const isGM = game.user.isGM
const hasSomething = successAvail > 0 || fasteAvail > 0 || (isGM && tinji > 0) const hasSomething = successAvail > 0 || fasteAvail > 0 || tinji > 0
if (!hasSomething) return if (!hasSomething) return
const aspLabel = game.i18n.localize(ASPECT_LABELS[aspect]) const aspLabel = game.i18n.localize(ASPECT_LABELS[aspect])
@@ -79,7 +78,7 @@ function refreshRollActions(rollCard, aspect, message) {
</button>` </button>`
} }
if (isGM && tinji > 0) { if (tinji > 0) {
btns += `<button class="cde-roll-action-btn cde-roll-action--tinji" data-action="tinji"> btns += `<button class="cde-roll-action-btn cde-roll-action--tinji" data-action="tinji">
<span class="cde-roll-action-tinji-char">天</span> <span class="cde-roll-action-tinji-char">天</span>
<span class="cde-roll-action-label">${game.i18n.localize("CDE.TinJi2")}</span> <span class="cde-roll-action-label">${game.i18n.localize("CDE.TinJi2")}</span>
@@ -149,7 +148,11 @@ async function _drawFromLoksyu(message, aspect, type, aspectLabel) {
else entry.yin-- else entry.yin--
} }
data[aspect] = entry data[aspect] = entry
await setLoksyuData(data) if (game.user.isGM) {
await setLoksyuData(data)
} else {
requestLoksyuDraw(aspect, order)
}
// Update the roll-result message in-place if it has stored flags // Update the roll-result message in-place if it has stored flags
const flags = message?.flags?.[SYSTEM_ID] const flags = message?.flags?.[SYSTEM_ID]
@@ -177,7 +180,7 @@ async function _drawFromLoksyu(message, aspect, type, aspectLabel) {
? game.i18n.localize("CDE.Successes") ? game.i18n.localize("CDE.Successes")
: game.i18n.localize("CDE.AuspiciousDie") : game.i18n.localize("CDE.AuspiciousDie")
ChatMessage.create({ await ChatMessage.create({
user: game.user.id, user: game.user.id,
content: `<div class="cde-loksyu-draw-msg"> content: `<div class="cde-loksyu-draw-msg">
<div class="cde-loksyu-draw-header"> <div class="cde-loksyu-draw-header">
@@ -200,14 +203,17 @@ async function _drawFromLoksyu(message, aspect, type, aspectLabel) {
* Spend 1 Tin Ji point (GM only) and post a notification. * Spend 1 Tin Ji point (GM only) and post a notification.
*/ */
async function _spendTinjiPostRoll() { async function _spendTinjiPostRoll() {
if (!game.user.isGM) return
const current = getTinjiValue() const current = getTinjiValue()
if (current <= 0) { if (current <= 0) {
ui.notifications.warn(game.i18n.localize("CDE.TinjiEmpty")) ui.notifications.warn(game.i18n.localize("CDE.TinjiEmpty"))
return return
} }
await setTinjiValue(current - 1) if (game.user.isGM) {
ChatMessage.create({ await setTinjiValue(current - 1)
} else {
requestTinjiSpend()
}
await ChatMessage.create({
user: game.user.id, user: game.user.id,
content: `<div class="cde-tinji-spend-msg"> content: `<div class="cde-tinji-spend-msg">
<span class="cde-tinji-icon">天</span> <span class="cde-tinji-icon">天</span>
+53 -51
View File
@@ -164,19 +164,16 @@ async function showMagicPrompt(params) {
title: params.title, title: params.title,
template: MAGIC_PROMPT_TEMPLATE, template: MAGIC_PROMPT_TEMPLATE,
data: { data: {
numberofdice: params.numberofdice ?? 0, numberofdice: params.numberofdice ?? 0,
aspectskill: Number(params.aspectskill ?? 0), aspectskill: Number(params.aspectskill ?? 0),
bonusmalusskill: params.bonusmalusskill ?? 0, bonusmalusskill: params.bonusmalusskill ?? 0,
bonusauspiciousdice: params.bonusauspiciousdice ?? 0, bonusauspiciousdice: params.bonusauspiciousdice ?? 0,
aspectspeciality: Number(params.aspectspeciality ?? 0), rolldifficulty: params.rolldifficulty ?? 1,
rolldifficulty: params.rolldifficulty ?? 1, freepowerlevels: params.freepowerlevels ?? 0,
bonusmalusspeciality: params.bonusmalusspeciality ?? 0, typeofthrow: Number(params.typeofthrow ?? 0),
heispend: params.heispend ?? 0,
typeofthrow: Number(params.typeofthrow ?? 0),
}, },
fields: ["aspectskill", "bonusmalusskill", "bonusauspiciousdice", fields: ["aspectskill", "bonusmalusskill", "bonusauspiciousdice",
"aspectspeciality", "rolldifficulty", "bonusmalusspeciality", "rolldifficulty", "freepowerlevels", "typeofthrow"],
"heispend", "typeofthrow"],
}) })
} }
@@ -318,7 +315,9 @@ export async function rollForActor(actor, rollKey) {
const kfSkill = kfItem.system.skill ?? "kungfu" const kfSkill = kfItem.system.skill ?? "kungfu"
numberofdice = sys.skills?.[kfSkill]?.value ?? 0 numberofdice = sys.skills?.[kfSkill]?.value ?? 0
title = `${kfItem.name} [${game.i18n.localize(sys.skills?.[kfSkill]?.label ?? "CDE.KungFu")}]` title = `${kfItem.name} [${game.i18n.localize(sys.skills?.[kfSkill]?.label ?? "CDE.KungFu")}]`
kfDefaultAspect = ASPECT_NAMES.indexOf(kfItem.system.aspect ?? "metal") const kfAspect = kfItem.system.aspect?.toLowerCase() ?? "metal"
const ASPECT_NORMALIZE = { eau: "water", terre: "earth", feu: "fire", bois: "wood" }
kfDefaultAspect = ASPECT_NAMES.indexOf(ASPECT_NORMALIZE[kfAspect] ?? kfAspect)
if (kfDefaultAspect < 0) kfDefaultAspect = 0 if (kfDefaultAspect < 0) kfDefaultAspect = 0
break break
} }
@@ -374,7 +373,9 @@ export async function rollForActor(actor, rollKey) {
const wpThrowMode = Number(wParams.typeofthrow ?? 0) const wpThrowMode = Number(wParams.typeofthrow ?? 0)
const wpDamageBase = wpItem.system.damageBase ?? 0 const wpDamageBase = wpItem.system.damageBase ?? 0
const wpTotalDice = wpSkillDice + wpAspectDice + wpRangeMalus + wpBonusMalus - wpWoundMalus const wpSpecialtyBonus = wpItem.system.hasSpeciality ? 1 : 0
const wpTotalDice = wpSkillDice + wpAspectDice + wpRangeMalus + wpBonusMalus - wpWoundMalus + wpSpecialtyBonus
if (wpTotalDice <= 0) { if (wpTotalDice <= 0) {
ui.notifications.warn(game.i18n.localize("CDE.Error0")) ui.notifications.warn(game.i18n.localize("CDE.Error0"))
return return
@@ -394,9 +395,12 @@ export async function rollForActor(actor, rollKey) {
if (wpWoundMalus !== 0) wpModParts.push(`-${wpWoundMalus} ${game.i18n.localize("CDE.WoundMalus")}`) if (wpWoundMalus !== 0) wpModParts.push(`-${wpWoundMalus} ${game.i18n.localize("CDE.WoundMalus")}`)
if (wpBonusAusp !== 0) wpModParts.push(`+${wpBonusAusp} ${game.i18n.localize("CDE.BonusAuspiciousDice")}`) if (wpBonusAusp !== 0) wpModParts.push(`+${wpBonusAusp} ${game.i18n.localize("CDE.BonusAuspiciousDice")}`)
// Damage = character's aspect value + weapon base damage (additive, not multiplied by successes) // Damage = character's aspect value (from weapon's damageAspect) + weapon base damage
const wpDamageAspectValue = sys.aspect?.[ASPECT_NAMES[wpAspFinal]]?.value ?? 0 const wpDamageAspectRaw = wpItem.system.damageAspect ?? "metal"
const wpDamageAspectLabel = game.i18n.localize(ASPECT_LABELS[ASPECT_NAMES[wpAspFinal]] ?? "") const wpDamageAspectIdx = WEAPON_ASPECT_INDEX[wpDamageAspectRaw] ?? 0
const wpDamageAspectName = ASPECT_NAMES[wpDamageAspectIdx]
const wpDamageAspectValue = sys.aspect?.[wpDamageAspectName]?.value ?? 0
const wpDamageAspectLabel = game.i18n.localize(ASPECT_LABELS[wpDamageAspectName] ?? "")
const wpMsg = await sendResultMessage(actor, { const wpMsg = await sendResultMessage(actor, {
rollLabel: `${wpItem.name}`, rollLabel: `${wpItem.name}`,
@@ -422,7 +426,7 @@ export async function rollForActor(actor, rollKey) {
}, wpRoll, ROLL_MODES[wpThrowMode] ?? "roll") }, wpRoll, ROLL_MODES[wpThrowMode] ?? "roll")
if (game.modules.get("dice-so-nice")?.active && wpMsg?.id) { if (game.modules.get("dice-so-nice")?.active && wpMsg?.id) {
await game.dice3d.waitFor3DAnimationByMessageID(wpMsg.id) try { await game.dice3d.waitFor3DAnimationByMessageID(wpMsg.id) } catch (_e) { /* DSN not available */ }
} }
// Auto-update Loksyu/TinJi singletons from weapon roll faces // Auto-update Loksyu/TinJi singletons from weapon roll faces
if ((wpResults.loksyudice ?? 0) > 0) await updateLoksyuFromRoll(wpAspectName, wpFaces) if ((wpResults.loksyudice ?? 0) > 0) await updateLoksyuFromRoll(wpAspectName, wpFaces)
@@ -434,8 +438,7 @@ export async function rollForActor(actor, rollKey) {
return return
} }
// For magic rolls the prompt allows adding HEI dice, so don't block early. // For magic rolls / itemkungfu, allow 0 base dice (user can add bonus dice in the prompt).
// For itemkungfu, allow 0 base dice (user can add bonus dice in the prompt).
if (numberofdice <= 0 && typeLibel !== "aspect" && typeLibel !== "itemkungfu" && !isMagic) { if (numberofdice <= 0 && typeLibel !== "aspect" && typeLibel !== "itemkungfu" && !isMagic) {
ui.notifications.warn(game.i18n.localize("CDE.Error0")) ui.notifications.warn(game.i18n.localize("CDE.Error0"))
return return
@@ -460,16 +463,6 @@ export async function rollForActor(actor, rollKey) {
defaultAspect = kfDefaultAspect defaultAspect = kfDefaultAspect
} }
let defaultSpecialAspect = 0
if (isMagicSpecial && specialLibel) {
// Look up the speciality's element from the MAGICS config constant
const specialCfg = MAGICS?.[skillLibel]?.speciality?.[specialLibel]
const aspectName = LABELELEMENT_TO_ASPECT[specialCfg?.labelelement]
if (aspectName) {
defaultSpecialAspect = ASPECT_NAMES.indexOf(aspectName)
}
}
// ---- Show roll prompt ---- // ---- Show roll prompt ----
let params let params
@@ -480,10 +473,8 @@ export async function rollForActor(actor, rollKey) {
aspectskill: defaultAspect, aspectskill: defaultAspect,
bonusmalusskill: 0, bonusmalusskill: 0,
bonusauspiciousdice: 0, bonusauspiciousdice: 0,
aspectspeciality: defaultSpecialAspect,
rolldifficulty: 1, rolldifficulty: 1,
bonusmalusspeciality: 0, freepowerlevels: 0,
heispend: 0,
typeofthrow: typeOfThrow, typeofthrow: typeOfThrow,
}) })
} else { } else {
@@ -503,22 +494,18 @@ export async function rollForActor(actor, rollKey) {
// ---- Compute total dice and roll ---- // ---- Compute total dice and roll ----
let aspectIndex, bonusMalus, bonusAuspicious, throwMode let aspectIndex, bonusMalus, bonusAuspicious, throwMode
let spellAspectIndex = null // magic only: aspect of the speciality for Wu Xing let rollDifficulty = 1 // magic only: multiplier applied to successes
let rollDifficulty = 1 // magic only: multiplier applied to successes
if (isMagic) { if (isMagic) {
const skillAspectIndex = Number(params.aspectskill ?? 0) const skillAspectIndex = Number(params.aspectskill ?? 0)
spellAspectIndex = Number(params.aspectspeciality ?? skillAspectIndex) aspectIndex = skillAspectIndex // used for both dice pool and Wu Xing cycle
aspectIndex = skillAspectIndex // used only for skill dice pool
bonusMalus = Number(params.bonusmalusskill ?? 0) bonusMalus = Number(params.bonusmalusskill ?? 0)
bonusAuspicious = Number(params.bonusauspiciousdice ?? 0) bonusAuspicious = Number(params.bonusauspiciousdice ?? 0)
rollDifficulty = Math.max(1, Number(params.rolldifficulty ?? 1)) rollDifficulty = Math.max(1, Number(params.rolldifficulty ?? 1))
throwMode = Number(params.typeofthrow ?? 0) throwMode = Number(params.typeofthrow ?? 0)
// magic: magic skill + aspect + bonuses + 1 (speciality base) + HEI spent // magic: magic skill + aspect + bonuses + 1 (speciality base) + HEI spent
const aspectDice = sys.aspect?.[ASPECT_NAMES[aspectIndex]]?.value ?? 0 const aspectDice = sys.aspect?.[ASPECT_NAMES[aspectIndex]]?.value ?? 0
const bonusSpec = Number(params.bonusmalusspeciality ?? 0) numberofdice = numberofdice + aspectDice + bonusMalus + 1
const heiDice = Number(params.heispend ?? 0)
numberofdice = numberofdice + aspectDice + bonusMalus + 1 + bonusSpec + heiDice
} else { } else {
aspectIndex = Number(params.aspect ?? 0) aspectIndex = Number(params.aspect ?? 0)
bonusMalus = Number(params.bonusmalus ?? 0) bonusMalus = Number(params.bonusmalus ?? 0)
@@ -545,31 +532,43 @@ export async function rollForActor(actor, rollKey) {
const rollModeKey = ROLL_MODES[throwMode] ?? "roll" const rollModeKey = ROLL_MODES[throwMode] ?? "roll"
// ---- Compute spell power (magic only) ----
// Power = rollDifficulty × character aspect value for the speciality's
// associated element (or the school's aspect for base magic rolls).
let spellPower = null
let spellPowerAspectName = null
let spellPowerAspectValue = null
if (isMagic) {
if (isMagicSpecial && specialLibel) {
const specialCfg = MAGICS?.[skillLibel]?.speciality?.[specialLibel]
const elemName = LABELELEMENT_TO_ASPECT[specialCfg?.labelelementkey]
if (elemName) spellPowerAspectName = elemName
}
if (!spellPowerAspectName) spellPowerAspectName = ASPECT_NAMES[aspectIndex]
spellPowerAspectValue = sys.aspect?.[spellPowerAspectName]?.value ?? 0
const freePowerLevels = Number(params.freepowerlevels ?? 0)
spellPower = rollDifficulty * (spellPowerAspectValue + freePowerLevels)
}
// ---- Compute Wu Xing results ---- // ---- Compute Wu Xing results ----
// For magic rolls, the spell's aspect (aspectspeciality) governs the Wu Xing // The Wu Xing cycle always uses the roll's aspect (skill aspect for magic,
// cycle (which faces count as successes/auspicious/etc.), not the skill aspect. // skill/resource aspect otherwise) to determine which faces count as
const wuXingAspectName = spellAspectIndex !== null // successes/auspicious/etc.
? ASPECT_NAMES[spellAspectIndex] const wuXingAspectName = ASPECT_NAMES[aspectIndex]
: ASPECT_NAMES[aspectIndex]
const allResults = roll.dice[0]?.results ?? [] const allResults = roll.dice[0]?.results ?? []
const faces = countFaces(allResults) const faces = countFaces(allResults)
const results = computeWuXingResults(faces, wuXingAspectName, bonusAuspicious) const results = computeWuXingResults(faces, wuXingAspectName, bonusAuspicious)
if (!results) return if (!results) return
// For magic, successesdice × rollDifficulty = spell power
const spellPower = isMagic ? results.successesdice * rollDifficulty : null
// ---- Build modifier summary text ---- // ---- Build modifier summary text ----
const modParts = [] const modParts = []
if (isMagic) { if (isMagic) {
const bm = Number(params.bonusmalusskill ?? 0) const bm = Number(params.bonusmalusskill ?? 0)
const bs = Number(params.bonusmalusspeciality ?? 0)
const hs = Number(params.heispend ?? 0)
const ba = Number(params.bonusauspiciousdice ?? 0) const ba = Number(params.bonusauspiciousdice ?? 0)
const fp = Number(params.freepowerlevels ?? 0)
if (bm !== 0) modParts.push(`${bm > 0 ? "+" : ""}${bm} ${game.i18n.localize("CDE.BonusMalus")}`) if (bm !== 0) modParts.push(`${bm > 0 ? "+" : ""}${bm} ${game.i18n.localize("CDE.BonusMalus")}`)
if (bs !== 0) modParts.push(`${bs > 0 ? "+" : ""}${bs} ${game.i18n.localize("CDE.SpellBonus")}`)
if (ba !== 0) modParts.push(`+${ba} ${game.i18n.localize("CDE.BonusAuspiciousDice")}`) if (ba !== 0) modParts.push(`+${ba} ${game.i18n.localize("CDE.BonusAuspiciousDice")}`)
if (hs !== 0) modParts.push(`${hs} ${game.i18n.localize("CDE.HeiSpend")}`) if (fp !== 0) modParts.push(`+${fp} ${game.i18n.localize("CDE.FreePowerLevels")}`)
if (rollDifficulty !== 1) modParts.push(`×${rollDifficulty} ${game.i18n.localize("CDE.RollDifficulty")}`) if (rollDifficulty !== 1) modParts.push(`×${rollDifficulty} ${game.i18n.localize("CDE.RollDifficulty")}`)
} else { } else {
const bm = Number(params.bonusmalus ?? 0) const bm = Number(params.bonusmalus ?? 0)
@@ -591,6 +590,9 @@ export async function rollForActor(actor, rollKey) {
modifiersText: modParts.length ? modParts.join(" · ") : "", modifiersText: modParts.length ? modParts.join(" · ") : "",
// Spell power (magic only) // Spell power (magic only)
spellPower, spellPower,
spellPowerAspectLabel: spellPowerAspectName ? game.i18n.localize(ASPECT_LABELS[spellPowerAspectName] ?? "") : "",
spellPowerAspectValue,
spellPowerFreeLevels: isMagic ? Number(params.freepowerlevels ?? 0) : 0,
rollDifficulty: isMagic ? rollDifficulty : null, rollDifficulty: isMagic ? rollDifficulty : null,
// Actor info // Actor info
actorName: actor.name ?? "", actorName: actor.name ?? "",
@@ -605,7 +607,7 @@ export async function rollForActor(actor, rollKey) {
// ---- Wait for Dice So Nice animation ---- // ---- Wait for Dice So Nice animation ----
if (game.modules.get("dice-so-nice")?.active && msg?.id) { if (game.modules.get("dice-so-nice")?.active && msg?.id) {
await game.dice3d.waitFor3DAnimationByMessageID(msg.id) try { await game.dice3d.waitFor3DAnimationByMessageID(msg.id) } catch (_e) { /* DSN not available */ }
} }
// ---- Auto-update Loksyu / TinJi singletons ---- // ---- Auto-update Loksyu / TinJi singletons ----
+55 -19
View File
@@ -19,6 +19,10 @@ import { CDEBaseActorSheet } from "./base.js"
export class CDECharacterSheet extends CDEBaseActorSheet { export class CDECharacterSheet extends CDEBaseActorSheet {
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
classes: ["character"], classes: ["character"],
actions: {
moveMagicUp: CDECharacterSheet.#onMoveMagicUp,
moveMagicDown: CDECharacterSheet.#onMoveMagicDown,
},
} }
static PARTS = { static PARTS = {
@@ -48,25 +52,35 @@ export class CDECharacterSheet extends CDEBaseActorSheet {
// Build magicsDisplay: only include the 5 relevant specialities per magic type + grimoire // Build magicsDisplay: only include the 5 relevant specialities per magic type + grimoire
const systemMagics = context.systemData.magics ?? {} const systemMagics = context.systemData.magics ?? {}
context.magicsDisplay = Object.fromEntries( const magicEntries = Object.entries(MAGICS).map(([magicKey, magicDef]) => {
Object.entries(MAGICS).map(([magicKey, magicDef]) => { const magicData = systemMagics[magicKey] ?? {}
const magicData = systemMagics[magicKey] ?? {} return [
return [ magicKey,
magicKey, {
{ value: magicData.value ?? 0,
value: magicData.value ?? 0, visible: magicData.visible ?? false,
visible: magicData.visible ?? false, speciality: Object.fromEntries(
speciality: Object.fromEntries( Object.keys(magicDef.speciality).map((specKey) => [
Object.keys(magicDef.speciality).map((specKey) => [ specKey,
specKey, { check: magicData.speciality?.[specKey]?.check ?? false },
{ check: magicData.speciality?.[specKey]?.check ?? false }, ])
]) ),
), grimoire: spellsByDiscipline[magicKey] ?? [],
grimoire: spellsByDiscipline[magicKey] ?? [], },
}, ]
] })
const order = context.systemData.magicOrder ?? []
if (order.length > 0) {
magicEntries.sort((a, b) => {
const ia = order.indexOf(a[0])
const ib = order.indexOf(b[0])
if (ia === -1 && ib === -1) return 0
if (ia === -1) return 1
if (ib === -1) return -1
return ia - ib
}) })
) }
context.magicsDisplay = Object.fromEntries(magicEntries)
return context return context
} }
@@ -155,7 +169,7 @@ export class CDECharacterSheet extends CDEBaseActorSheet {
cell.addEventListener("click", (event) => { cell.addEventListener("click", (event) => {
event.preventDefault() event.preventDefault()
const rollKey = cell.dataset.libelId const rollKey = cell.dataset.libelId
if (rollKey) rollForActor(this.document, rollKey) if (rollKey) rollForActor(this.document, rollKey)?.catch(err => console.error("Roll failed:", err))
}) })
}) })
} }
@@ -171,6 +185,28 @@ export class CDECharacterSheet extends CDEBaseActorSheet {
}) })
} }
static async #onMoveMagicUp(event, target) {
const key = target.dataset.magicKey
let order = this.document.system.magicOrder ?? []
if (!order.length) order = [...Object.keys(MAGICS)]
else order = [...order]
const idx = order.indexOf(key)
if (idx <= 0) return
[order[idx - 1], order[idx]] = [order[idx], order[idx - 1]]
await this.document.update({ "system.magicOrder": order })
}
static async #onMoveMagicDown(event, target) {
const key = target.dataset.magicKey
let order = this.document.system.magicOrder ?? []
if (!order.length) order = [...Object.keys(MAGICS)]
else order = [...order]
const idx = order.indexOf(key)
if (idx === -1 || idx >= order.length - 1) return
[order[idx], order[idx + 1]] = [order[idx + 1], order[idx]]
await this.document.update({ "system.magicOrder": order })
}
#bindComponentRandomize() { #bindComponentRandomize() {
const btn = this.element?.querySelector("[data-action='randomize-component']") const btn = this.element?.querySelector("[data-action='randomize-component']")
if (!btn) return if (!btn) return
+1 -1
View File
@@ -50,7 +50,7 @@ export class CDENpcSheet extends CDEBaseActorSheet {
cell.addEventListener("click", (event) => { cell.addEventListener("click", (event) => {
event.preventDefault() event.preventDefault()
const rollKey = cell.dataset.libelId const rollKey = cell.dataset.libelId
if (rollKey) rollForActor(this.document, rollKey) if (rollKey) rollForActor(this.document, rollKey)?.catch(err => console.error("Roll failed:", err))
}) })
}) })
} }
+5 -5
View File
@@ -26,11 +26,11 @@ export class CDESanheiSheet extends CDEBaseItemSheet {
async _prepareContext() { async _prepareContext() {
const context = await super._prepareContext() const context = await super._prepareContext()
const enrich = (content) => foundry.applications.ux.TextEditor.implementation.enrichHTML(content ?? "", { async: true }) const enrich = (content) => foundry.applications.ux.TextEditor.implementation.enrichHTML(content ?? "", { async: true })
const props = this.document.system.properties const props = this.document.system.properties ?? {}
context.prop1DescriptionHTML = await enrich(props.prop1.description) context.prop1DescriptionHTML = await enrich(props.prop1?.description)
context.prop2DescriptionHTML = await enrich(props.prop2.description) context.prop2DescriptionHTML = await enrich(props.prop2?.description)
context.prop3DescriptionHTML = await enrich(props.prop3.description) context.prop3DescriptionHTML = await enrich(props.prop3?.description)
context.propFields = this.document.system.schema.fields.properties.fields context.propFields = this.document.system.schema.fields.properties?.fields
return context return context
} }
} }
+2 -1
View File
@@ -1,6 +1,7 @@
{ {
"id": "fvtt-chroniques-de-l-etrange", "id": "fvtt-chroniques-de-l-etrange",
"version": "13.0.0", "version": "13.0.0",
"socket": true,
"title": "Les Chroniques de l'Étrange", "title": "Les Chroniques de l'Étrange",
"url": "https://www.uberwald.me/gitea/uberwald/fvtt-chroniques-de-l-etrange", "url": "https://www.uberwald.me/gitea/uberwald/fvtt-chroniques-de-l-etrange",
"description": "Game system for Les Chroniques de l'Étrange, from Antre-Monde éditions", "description": "Game system for Les Chroniques de l'Étrange, from Antre-Monde éditions",
@@ -245,7 +246,7 @@
} }
}, },
"compatibility": { "compatibility": {
"minimum": "13", "minimum": "14",
"verified": "14" "verified": "14"
}, },
"relationships": {}, "relationships": {},
@@ -81,6 +81,14 @@
title="{{ localize 'CDE.Roll' }} {{getMagicLabel key}}"> title="{{ localize 'CDE.Roll' }} {{getMagicLabel key}}">
<i class="fas fa-dice-d10"></i> <i class="fas fa-dice-d10"></i>
</a> </a>
<a class="cde-magic-order-btn" data-action="moveMagicUp" data-magic-key="{{key}}"
title="{{ localize 'CDE.MoveUp' }}">
<i class="fas fa-chevron-up"></i>
</a>
<a class="cde-magic-order-btn" data-action="moveMagicDown" data-magic-key="{{key}}"
title="{{ localize 'CDE.MoveDown' }}">
<i class="fas fa-chevron-down"></i>
</a>
<label class="cde-magic-toggle" title="{{ localize 'CDE.PracticeSpecialty' }}"> <label class="cde-magic-toggle" title="{{ localize 'CDE.PracticeSpecialty' }}">
<input type="checkbox" name="system.magics.{{key}}.visible" {{checked magic.visible}} /> <input type="checkbox" name="system.magics.{{key}}.visible" {{checked magic.visible}} />
<i class="fas {{#if magic.visible}}fa-chevron-up{{else}}fa-chevron-down{{/if}}"></i> <i class="fas {{#if magic.visible}}fa-chevron-up{{else}}fa-chevron-down{{/if}}"></i>
+43 -5
View File
@@ -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>
+1
View File
@@ -30,6 +30,7 @@
{{#if spellPower}} {{#if spellPower}}
<div class="cde-rr-spell-power"> <div class="cde-rr-spell-power">
<span class="cde-rr-spell-power-count">{{spellPower}}</span> <span class="cde-rr-spell-power-count">{{spellPower}}</span>
<span class="cde-rr-spell-power-formula">{{spellPowerAspectLabel}} ({{spellPowerAspectValue}}{{#if spellPowerFreeLevels}} + {{spellPowerFreeLevels}}{{/if}}) × {{rollDifficulty}}</span>
<span class="cde-rr-spell-power-label">{{ localize "CDE.SpellPower" }}</span> <span class="cde-rr-spell-power-label">{{ localize "CDE.SpellPower" }}</span>
</div> </div>
{{/if}} {{/if}}
+9 -34
View File
@@ -54,16 +54,6 @@
<div class="cde-roll-section cde-roll-section--separator"> <div class="cde-roll-section cde-roll-section--separator">
<p class="cde-roll-section-title">② {{ localize "CDE.TwoPowerOfSpell" }}</p> <p class="cde-roll-section-title">② {{ localize "CDE.TwoPowerOfSpell" }}</p>
<div class="cde-roll-fields"> <div class="cde-roll-fields">
<div class="cde-roll-field">
<label>{{ localize "CDE.AspectSpeciality" }}</label>
<select name="aspectspeciality">
<option value="0" {{#if (eq aspectspeciality 0)}}selected{{/if}}>{{ localize "CDE.Metal" }}</option>
<option value="1" {{#if (eq aspectspeciality 1)}}selected{{/if}}>{{ localize "CDE.Water" }}</option>
<option value="2" {{#if (eq aspectspeciality 2)}}selected{{/if}}>{{ localize "CDE.Earth" }}</option>
<option value="3" {{#if (eq aspectspeciality 3)}}selected{{/if}}>{{ localize "CDE.Fire" }}</option>
<option value="4" {{#if (eq aspectspeciality 4)}}selected{{/if}}>{{ localize "CDE.Wood" }}</option>
</select>
</div>
<div class="cde-roll-field"> <div class="cde-roll-field">
<label>{{ localize "CDE.RollDifficulty" }} (×)</label> <label>{{ localize "CDE.RollDifficulty" }} (×)</label>
<select name="rolldifficulty"> <select name="rolldifficulty">
@@ -75,32 +65,17 @@
</select> </select>
</div> </div>
<div class="cde-roll-field"> <div class="cde-roll-field">
<label>{{ localize "CDE.BonusMalus" }}</label> <label>{{ localize "CDE.FreePowerLevels" }}</label>
<select name="bonusmalusspeciality"> <select name="freepowerlevels">
<option value="-5" {{#if (eq bonusmalusspeciality -5)}}selected{{/if}}>5 dés</option> <option value="0" {{#if (eq freepowerlevels 0)}}selected{{/if}}>0</option>
<option value="-4" {{#if (eq bonusmalusspeciality -4)}}selected{{/if}}>4 dés</option> <option value="1" {{#if (eq freepowerlevels 1)}}selected{{/if}}>+1</option>
<option value="-3" {{#if (eq bonusmalusspeciality -3)}}selected{{/if}}>3 dés</option> <option value="2" {{#if (eq freepowerlevels 2)}}selected{{/if}}>+2</option>
<option value="-2" {{#if (eq bonusmalusspeciality -2)}}selected{{/if}}>2 dés</option> <option value="3" {{#if (eq freepowerlevels 3)}}selected{{/if}}>+3</option>
<option value="-1" {{#if (eq bonusmalusspeciality -1)}}selected{{/if}}>1 dé</option> <option value="4" {{#if (eq freepowerlevels 4)}}selected{{/if}}>+4</option>
<option value="0" {{#if (eq bonusmalusspeciality 0)}}selected{{/if}}>0 (aucun)</option> <option value="5" {{#if (eq freepowerlevels 5)}}selected{{/if}}>+5</option>
<option value="1" {{#if (eq bonusmalusspeciality 1)}}selected{{/if}}>+1 dé</option>
<option value="2" {{#if (eq bonusmalusspeciality 2)}}selected{{/if}}>+2 dés</option>
<option value="3" {{#if (eq bonusmalusspeciality 3)}}selected{{/if}}>+3 dés</option>
<option value="4" {{#if (eq bonusmalusspeciality 4)}}selected{{/if}}>+4 dés</option>
<option value="5" {{#if (eq bonusmalusspeciality 5)}}selected{{/if}}>+5 dés</option>
</select>
</div>
<div class="cde-roll-field">
<label>{{ localize "CDE.HeiSpend" }}</label>
<select name="heispend">
<option value="0" {{#if (eq heispend 0)}}selected{{/if}}>0 Hei</option>
<option value="1" {{#if (eq heispend 1)}}selected{{/if}}>1 Hei</option>
<option value="2" {{#if (eq heispend 2)}}selected{{/if}}>2 Hei</option>
<option value="3" {{#if (eq heispend 3)}}selected{{/if}}>3 Hei</option>
<option value="4" {{#if (eq heispend 4)}}selected{{/if}}>4 Hei</option>
<option value="5" {{#if (eq heispend 5)}}selected{{/if}}>5 Hei</option>
</select> </select>
</div> </div>
</div> </div>
<p class="cde-roll-hint"><i>{{ localize "CDE.DoNotModify" }}</i></p> <p class="cde-roll-hint"><i>{{ localize "CDE.DoNotModify" }}</i></p>
</div> </div>
+4
View File
@@ -36,6 +36,10 @@
<span class="cde-stat-label">{{ localize "CDE.DamageBase" }}</span> <span class="cde-stat-label">{{ localize "CDE.DamageBase" }}</span>
<input type="number" name="system.damageBase" value="{{systemData.damageBase}}" /> <input type="number" name="system.damageBase" value="{{systemData.damageBase}}" />
</div> </div>
<div class="cde-stat-cell">
<span class="cde-stat-label">{{ localize "CDE.Speciality" }}</span>
<input type="checkbox" name="system.hasSpeciality" {{checked systemData.hasSpeciality}} />
</div>
<div class="cde-stat-cell"> <div class="cde-stat-cell">
<span class="cde-stat-label">{{ localize "CDE.DamageAspect" }}</span> <span class="cde-stat-label">{{ localize "CDE.DamageAspect" }}</span>
<select name="system.damageAspect"> <select name="system.damageAspect">