feat: portée armes, récupération 3T, socket loksyu, import batch

Portée des armes
- Suppression des malus de portée (mediane/longue/extreme)
- Sélecteur filtré : seules les portées <= portée max de l'arme

Récupération (3 Trésors)
- Nouveau bouton dans l'onglet Trésors
- Jet basé sur l'aspect du Gardien Céleste
- Dé pair : +1 Hei Yang, +1 San
- Dé impair : +1 Hei Yin, +1 Zing
- Fenêtre de bonus dice pour les pouvoirs (Acupuncture...)
- Ignore les malus de palier San/Zing

Socket Loksyu/TinJi
- Les joueurs peuvent mettre à jour Loksyu/TinJi via socket
- Le MJ proxy les écritures (game.settings world = GM only)
- Opérations atomiques pour éviter les races conditions (roll simultanés)

Import batch
- 4 nouveaux migrators item (weapon/armor/sanhei/ingredient)
- magicOrder dans la migration character
- Confirmation en 2 clics avant l'import
- Détection des doublons avec le monde
- Progression en direct pendant l'import
- Dédoublonnage non-silencieux + flag _duplicate calculé en direct

UI
- Noms d'items en Signika (plus lisible qu'Averia)
- ASCII art au lancement dans la console
- Clés i18n manquantes ajoutées (BonusMalus, EnablePrompt, Material, Roll, ThrowType)

Fix divers
- Calcul puissance sort : utilise l'élément de la spécialité, pas celui de l'école
- preLocalizeConfig préserve labelelementkey pour la résolution d'aspect
- combat.js: guard undefined ids dans rollInitiative
This commit is contained in:
2026-06-28 19:18:03 +02:00
parent 9c4f5948fd
commit 6b2ead8fbb
10 changed files with 497 additions and 38 deletions
+114 -19
View File
@@ -804,15 +804,6 @@ function preLocalizeConfig() {
// src/config/runtime.js
function configureRuntime() {
CONFIG.Actor.compendiumBanner = "/systems/fvtt-chroniques-de-l-etrange/images/banners/actor-banner.webp";
CONFIG.Adventure.compendiumBanner = "/systems/fvtt-chroniques-de-l-etrange/images/banners/adventure-banner.webp";
CONFIG.Cards.compendiumBanner = "ui/banners/cards-banner.webp";
CONFIG.Item.compendiumBanner = "/systems/fvtt-chroniques-de-l-etrange/images/banners/item-banner.webp";
CONFIG.JournalEntry.compendiumBanner = "/systems/fvtt-chroniques-de-l-etrange/images/banners/journalentry-banner.webp";
CONFIG.Macro.compendiumBanner = "ui/banners/macro-banner.webp";
CONFIG.Playlist.compendiumBanner = "ui/banners/playlist-banner.webp";
CONFIG.RollTable.compendiumBanner = "ui/banners/rolltable-banner.webp";
CONFIG.Scene.compendiumBanner = "/systems/fvtt-chroniques-de-l-etrange/images/banners/scene-banner.webp";
}
// src/data/actors/character.js
@@ -1682,12 +1673,13 @@ var LABELELEMENT_TO_ASPECT = {
"CDE.Fire": "fire",
"CDE.Wood": "wood"
};
var RANGE_MALUS = {
contact: 0,
courte: 0,
mediane: -1,
longue: -2,
extreme: -3
var RANGE_ORDER = ["contact", "courte", "mediane", "longue", "extreme"];
var RANGE_LABELS = {
contact: "CDE.RangeContact",
courte: "CDE.RangeCourte",
mediane: "CDE.RangeMediane",
longue: "CDE.RangeLongue",
extreme: "CDE.RangeExtreme"
};
var WEAPON_TYPE_SKILL = {
melee: "kungfu",
@@ -1787,6 +1779,13 @@ async function showMagicPrompt(params) {
});
}
async function showWeaponPrompt(params) {
const maxRange = params.effectiverange ?? "contact";
const maxIdx = RANGE_ORDER.indexOf(maxRange);
const validRanges = RANGE_ORDER.slice(0, maxIdx + 1).map((r) => ({
value: r,
label: RANGE_LABELS[r],
selected: r === maxRange
}));
return showRollPrompt({
title: params.title,
template: WEAPON_PROMPT_TEMPLATE,
@@ -1800,6 +1799,7 @@ async function showWeaponPrompt(params) {
weaponskill: params.weaponskill ?? "kungfu",
aspect: Number(params.aspect ?? 0),
effectiverange: params.effectiverange ?? "contact",
validRanges,
bonusmalus: params.bonusmalus ?? 0,
woundmalus: params.woundmalus ?? 0,
bonusauspiciousdice: params.bonusauspiciousdice ?? 0,
@@ -1956,14 +1956,13 @@ async function rollForActor(actor, rollKey) {
const wpSkillDice = sys.skills?.[wpChosenSkill]?.value ?? 0;
const wpAspFinal = Number(wParams.aspect ?? wpAspectIdx);
const wpAspectDice = sys.aspect[ASPECT_NAMES[wpAspFinal]]?.value ?? 0;
const wpRangeMalus = RANGE_MALUS[wParams.effectiverange ?? "contact"] ?? 0;
const wpBonusMalus = Number(wParams.bonusmalus ?? 0);
const wpWoundMalus = Number(wParams.woundmalus ?? 0);
const wpBonusAusp = Number(wParams.bonusauspiciousdice ?? 0);
const wpThrowMode = Number(wParams.typeofthrow ?? 0);
const wpDamageBase = wpItem.system.damageBase ?? 0;
const wpSpecialtyBonus = wpItem.system.hasSpeciality ? 1 : 0;
const wpTotalDice = wpSkillDice + wpAspectDice + wpRangeMalus + wpBonusMalus - wpWoundMalus + wpSpecialtyBonus;
const wpTotalDice = wpSkillDice + wpAspectDice + wpBonusMalus - wpWoundMalus + wpSpecialtyBonus;
if (wpTotalDice <= 0) {
ui.notifications.warn(game.i18n.localize("CDE.Error0"));
return;
@@ -1975,7 +1974,6 @@ async function rollForActor(actor, rollKey) {
const wpResults = computeWuXingResults(wpFaces, wpAspectName, wpBonusAusp);
if (!wpResults) return;
const wpModParts = [];
if (wpRangeMalus !== 0) wpModParts.push(`${wpRangeMalus} ${game.i18n.localize("CDE.RangePenalty")}`);
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 (wpBonusAusp !== 0) wpModParts.push(`+${wpBonusAusp} ${game.i18n.localize("CDE.BonusAuspiciousDice")}`);
@@ -2277,7 +2275,8 @@ var CDECharacterSheet = class _CDECharacterSheet extends CDEBaseActorSheet {
classes: ["character"],
actions: {
moveMagicUp: _CDECharacterSheet.#onMoveMagicUp,
moveMagicDown: _CDECharacterSheet.#onMoveMagicDown
moveMagicDown: _CDECharacterSheet.#onMoveMagicDown,
recovery: _CDECharacterSheet.#onRecovery
}
};
static PARTS = {
@@ -2447,6 +2446,102 @@ var CDECharacterSheet = class _CDECharacterSheet extends CDEBaseActorSheet {
[order[idx], order[idx + 1]] = [order[idx + 1], order[idx]];
await this.document.update({ "system.magicOrder": order });
}
static async #onRecovery(event, target) {
const actor = this.document;
const sys = actor.system;
const guardian = Number(sys.guardian ?? 0);
if (guardian < 1 || guardian > 5) {
ui.notifications.warn(game.i18n.localize("CDE.RecoveryNoGuardian"));
return;
}
const aspectName = ASPECT_NAMES[guardian - 1];
const aspectLabel = game.i18n.localize(ASPECT_LABELS[aspectName]);
const aspectValue = sys.aspect?.[aspectName]?.value ?? 0;
if (aspectValue <= 0) {
ui.notifications.warn(game.i18n.localize("CDE.RecoveryNoAspect"));
return;
}
const RECOVERY_PROMPT = `systems/${SYSTEM_ID}/templates/form/cde-recovery-prompt.html`;
const content = await foundry.applications.handlebars.renderTemplate(RECOVERY_PROMPT, {
basedice: aspectValue
});
const params = await foundry.applications.api.DialogV2.prompt({
window: { title: game.i18n.localize("CDE.Recovery") },
content,
rejectClose: false,
ok: {
label: game.i18n.localize("CDE.Validate"),
callback: (event2, button, dialog) => {
const root = dialog.element ?? dialog;
return { bonusdice: Number(root.querySelector("[name=bonusdice]")?.value ?? 0) };
}
}
});
if (params === void 0) return;
const totalDice = aspectValue + Math.max(0, Number(params.bonusdice ?? 0));
const roll = new Roll(`${totalDice}d10`);
await roll.evaluate();
const results = roll.dice[0].results.map((r) => r.result);
const evenCount = results.filter((r) => r % 2 === 0).length;
const oddCount = results.filter((r) => r % 2 !== 0).length;
const cap = (max) => max > 0 ? max : Infinity;
const heiYang = Math.min(
(sys.threetreasures?.heiyang?.value ?? 0) + evenCount,
cap(sys.threetreasures?.heiyang?.max)
);
const heiYin = Math.min(
(sys.threetreasures?.heiyin?.value ?? 0) + oddCount,
cap(sys.threetreasures?.heiyin?.max)
);
const san = Math.min(
(sys.threetreasures?.dicelevel?.level0d?.san?.value ?? 0) + evenCount,
cap(sys.threetreasures?.dicelevel?.level0d?.san?.max)
);
const zing = Math.min(
(sys.threetreasures?.dicelevel?.level0d?.zing?.value ?? 0) + oddCount,
cap(sys.threetreasures?.dicelevel?.level0d?.zing?.max)
);
await actor.update({
"system.threetreasures.heiyang.value": heiYang,
"system.threetreasures.heiyin.value": heiYin,
"system.threetreasures.dicelevel.level0d.san.value": san,
"system.threetreasures.dicelevel.level0d.zing.value": zing
});
const rollLabel = `${game.i18n.localize("CDE.Recovery")} \u2014 ${aspectLabel}`;
const diceStr = results.join(" \xB7 ");
const typeOfThrow = Number(sys.prefs?.typeofthrow?.choice ?? 0);
const ROLL_MODES2 = ["roll", "gmroll", "blindroll", "selfroll"];
const rollMode = ROLL_MODES2[typeOfThrow] ?? "roll";
await ChatMessage.create({
user: game.user.id,
speaker: ChatMessage.getSpeaker({ actor }),
content: `
<div class="cde-chat-recovery">
<div class="cde-chat-recovery-header">
<img src="${ASPECT_ICONS[aspectName]}" class="cde-chat-recovery-icon" alt="${aspectLabel}"/>
<span class="cde-chat-recovery-title">${rollLabel}</span>
</div>
<div class="cde-chat-recovery-dice">
<span class="cde-chat-recovery-dice-label">${totalDice}d10</span>
<span class="cde-chat-recovery-dice-results">${diceStr}</span>
</div>
<div class="cde-chat-recovery-results">
<div class="cde-chat-recovery-even">
<span class="cde-chat-recovery-count">${evenCount}</span>
<span><strong>${game.i18n.localize("CDE.RecoveryEvenLabel")}</strong></span>
<span>\u2192 +${evenCount} Hei Yang, +${evenCount} San</span>
</div>
<div class="cde-chat-recovery-odd">
<span class="cde-chat-recovery-count">${oddCount}</span>
<span><strong>${game.i18n.localize("CDE.RecoveryOddLabel")}</strong></span>
<span>\u2192 +${oddCount} Hei Yin, +${oddCount} Zing</span>
</div>
</div>
</div>`,
rolls: [roll],
rollMode
});
}
#bindComponentRandomize() {
const btn = this.element?.querySelector("[data-action='randomize-component']");
if (!btn) return;
+3 -3
View File
File diff suppressed because one or more lines are too long