@@ -3354,6 +3451,7 @@ Hooks.once("ready", async () => {
await migrateIfNeeded();
await loadWelcomeSceneIfNeeded();
CDEWheelApp.registerHooks();
+ registerSingletonSocket();
if (game.user.isGM) showWelcomeMessage();
});
Hooks.on("renderChatLog", (_app, html) => {
diff --git a/dist/system.js.map b/dist/system.js.map
index 5057387..f85322b 100644
--- a/dist/system.js.map
+++ b/dist/system.js.map
@@ -1,7 +1,7 @@
{
"version": 3,
"sources": ["../src/config/constants.js", "../src/migration/migrator.js", "../src/ui/apps/migration-app.js", "../src/config/settings.js", "../src/config/localize.js", "../src/config/runtime.js", "../src/data/actors/character.js", "../src/data/actors/npc.js", "../src/data/items/item.js", "../src/data/items/kungfu.js", "../src/data/items/spell.js", "../src/data/items/supernatural.js", "../src/data/items/weapon.js", "../src/data/items/armor.js", "../src/data/items/sanhei.js", "../src/data/items/ingredient.js", "../src/documents/chat-message.js", "../src/documents/actor.js", "../src/documents/item.js", "../src/ui/dice.js", "../src/ui/helpers.js", "../src/ui/templates.js", "../src/ui/initiative.js", "../src/ui/apps/singletons.js", "../src/ui/rolling.js", "../src/ui/sheets/actors/base.js", "../src/ui/sheets/actors/character.js", "../src/ui/sheets/actors/npc.js", "../src/ui/sheets/items/base.js", "../src/ui/sheets/items/item.js", "../src/ui/sheets/items/kungfu.js", "../src/ui/sheets/items/spell.js", "../src/ui/sheets/items/supernatural.js", "../src/ui/sheets/items/weapon.js", "../src/ui/sheets/items/armor.js", "../src/ui/sheets/items/sanhei.js", "../src/ui/sheets/items/ingredient.js", "../src/ui/apps/loksyu-app.js", "../src/ui/apps/tinji-app.js", "../src/documents/combat.js", "../src/ui/apps/wheel-app.js", "../src/ui/roll-actions.js", "../src/ui/apps/welcome.js", "../src/system.js"],
- "sourcesContent": ["/**\n * Chroniques de l'\u00C9trange \u2014 Syst\u00E8me FoundryVTT\n *\n * Chroniques de l'\u00C9trange est un jeu de r\u00F4le \u00E9dit\u00E9 par Antre-Monde \u00C9ditions.\n * Ce syst\u00E8me FoundryVTT est une impl\u00E9mentation ind\u00E9pendante et n'est pas\n * affili\u00E9 \u00E0 Antre-Monde \u00C9ditions,\n * mais a \u00E9t\u00E9 r\u00E9alis\u00E9 avec l'autorisation d'Antre-Monde \u00C9ditions.\n *\n * @author LeRatierBretonnien\n * @copyright 2024\u20132026 LeRatierBretonnien\n * @license CC BY-NC-SA 4.0 \u2013 https://creativecommons.org/licenses/by-nc-sa/4.0/\n */\n\nexport const SYSTEM_ID = \"fvtt-chroniques-de-l-etrange\"\n\nexport const ACTOR_TYPES = {\n character: \"character\",\n npc: \"npc\",\n}\n\nexport const ITEM_TYPES = {\n item: \"item\",\n kungfu: \"kungfu\",\n spell: \"spell\",\n supernatural: \"supernatural\",\n weapon: \"weapon\",\n armor: \"armor\",\n sanhei: \"sanhei\",\n ingredient: \"ingredient\",\n}\n\nexport const SUBTYPES = {\n weapon: { id: \"weapon\", label: \"CDE.Weapon\" },\n armor: { id: \"armor\", label: \"CDE.Armor\" },\n sanhei: { id: \"sanhei\", label: \"CDE.Sanhei\" },\n other: { id: \"other\", label: \"CDE.Other\" },\n}\n\nexport const MAGICS = {\n internalcinnabar: {\n id: \"internalcinnabar\",\n background: \"linear-grey\",\n label: \"CDE.InternalCinnabar\",\n aspectlabel: \"CDE.Metal\",\n speciality: {\n essence: { label: \"CDE.Essence\", classicon: \"icon-yin\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yin.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_metal.webp\", labelicon: \"Yin\", labelelement: \"CDE.Metal\" },\n mind: { label: \"CDE.Mind\", classicon: \"icon-yin\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yin.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_eau.webp\", labelicon: \"Yin\", labelelement: \"CDE.Water\" },\n purification: { label: \"CDE.Purification\", classicon: \"icon-yinyang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/yin_yang.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_terre.webp\", labelicon: \"Yin/Yang\", labelelement: \"CDE.Earth\" },\n manipulation: { label: \"CDE.Manipulation\", classicon: \"icon-yang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_feu.webp\", labelicon: \"Yang\", labelelement: \"CDE.Fire\" },\n aura: { label: \"CDE.Aura\", classicon: \"icon-yang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_bois.webp\", labelicon: \"Yang\", labelelement: \"CDE.Wood\" },\n },\n },\n alchemy: {\n id: \"alchemy\",\n background: \"linear-blue\",\n label: \"CDE.Alchemy\",\n aspectlabel: \"CDE.Water\",\n speciality: {\n acupuncture: { label: \"CDE.Acupuncture\", classicon: \"icon-yin\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yin.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_metal.webp\", labelicon: \"Yin\", labelelement: \"CDE.Metal\" },\n elixirs: { label: \"CDE.Elixirs\", classicon: \"icon-yin\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yin.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_eau.webp\", labelicon: \"Yin\", labelelement: \"CDE.Water\" },\n poisons: { label: \"CDE.Poisons\", classicon: \"icon-yinyang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/yin_yang.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_terre.webp\", labelicon: \"Yin/Yang\", labelelement: \"CDE.Earth\" },\n arsenal: { label: \"CDE.Arsenal\", classicon: \"icon-yang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_feu.webp\", labelicon: \"Yang\", labelelement: \"CDE.Fire\" },\n potions: { label: \"CDE.Potions\", classicon: \"icon-yang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_bois.webp\", labelicon: \"Yang\", labelelement: \"CDE.Wood\" },\n },\n },\n masteryoftheway: {\n id: \"masteryoftheway\",\n background: \"linear-brown\",\n label: \"CDE.MasteryOfTheWay\",\n aspectlabel: \"CDE.Earth\",\n speciality: {\n curse: { label: \"CDE.Curse\", classicon: \"icon-yin\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yin.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_metal.webp\", labelicon: \"Yin\", labelelement: \"CDE.Metal\" },\n transfiguration: { label: \"CDE.Transfiguration\", classicon: \"icon-yin\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yin.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_eau.webp\", labelicon: \"Yin\", labelelement: \"CDE.Water\" },\n necromancy: { label: \"CDE.Necromancy\", classicon: \"icon-yinyang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/yin_yang.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_terre.webp\", labelicon: \"Yin/Yang\", labelelement: \"CDE.Earth\" },\n climatecontrol: { label: \"CDE.ClimateControl\", classicon: \"icon-yang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_feu.webp\", labelicon: \"Yang\", labelelement: \"CDE.Fire\" },\n goldenmagic: { label: \"CDE.GoldenMagic\", classicon: \"icon-yang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_bois.webp\", labelicon: \"Yang\", labelelement: \"CDE.Wood\" },\n },\n },\n exorcism: {\n id: \"exorcism\",\n background: \"linear-red\",\n label: \"CDE.Exorcism\",\n aspectlabel: \"CDE.Fire\",\n speciality: {\n invocation: { label: \"CDE.Invocation\", classicon: \"icon-yin\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yin.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_metal.webp\", labelicon: \"Yin\", labelelement: \"CDE.Metal\" },\n tracking: { label: \"CDE.Tracking\", classicon: \"icon-yin\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yin.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_eau.webp\", labelicon: \"Yin\", labelelement: \"CDE.Water\" },\n protection: { label: \"CDE.Protection\", classicon: \"icon-yinyang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/yin_yang.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_terre.webp\", labelicon: \"Yin/Yang\", labelelement: \"CDE.Earth\" },\n punishment: { label: \"CDE.Punishment\", classicon: \"icon-yang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_feu.webp\", labelicon: \"Yang\", labelelement: \"CDE.Fire\" },\n domination: { label: \"CDE.Domination\", classicon: \"icon-yang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_bois.webp\", labelicon: \"Yang\", labelelement: \"CDE.Wood\" },\n },\n },\n geomancy: {\n id: \"geomancy\",\n background: \"linear-green\",\n label: \"CDE.Geomancy\",\n aspectlabel: \"CDE.Wood\",\n speciality: {\n neutralization: { label: \"CDE.Neutralization\", classicon: \"icon-yin\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yin.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_metal.webp\", labelicon: \"Yin\", labelelement: \"CDE.Metal\" },\n divination: { label: \"CDE.Divination\", classicon: \"icon-yin\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yin.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_eau.webp\", labelicon: \"Yin\", labelelement: \"CDE.Water\" },\n earthlyprayer: { label: \"CDE.EarthlyPrayer\", classicon: \"icon-yinyang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/yin_yang.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_terre.webp\", labelicon: \"Yin/Yang\", labelelement: \"CDE.Earth\" },\n heavenlyprayer: { label: \"CDE.HeavenlyPrayer\", classicon: \"icon-yang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_feu.webp\", labelicon: \"Yang\", labelelement: \"CDE.Fire\" },\n fungseoi: { label: \"CDE.Fungseoi\", classicon: \"icon-yang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_bois.webp\", labelicon: \"Yang\", labelelement: \"CDE.Wood\" },\n },\n },\n}\n\n/** Map aspect name \u2192 i18n label key */\nexport const ASPECT_LABELS = {\n metal: \"CDE.Metal\",\n water: \"CDE.Water\",\n earth: \"CDE.Earth\",\n fire: \"CDE.Fire\",\n wood: \"CDE.Wood\",\n}\n\n/** Map aspect name \u2192 image path */\nexport const ASPECT_ICONS = {\n metal: \"systems/fvtt-chroniques-de-l-etrange/images/cde_metal.webp\",\n water: \"systems/fvtt-chroniques-de-l-etrange/images/cde_eau.webp\",\n earth: \"systems/fvtt-chroniques-de-l-etrange/images/cde_terre.webp\",\n fire: \"systems/fvtt-chroniques-de-l-etrange/images/cde_feu.webp\",\n wood: \"systems/fvtt-chroniques-de-l-etrange/images/cde_bois.webp\",\n}\n\n/** Map aspect name \u2192 die face pair [yin, yang] (face=10 stored as 0) */\nexport const ASPECT_FACES = {\n metal: [3, 8],\n water: [1, 6],\n earth: [0, 5], // 0 = face \"10\"\n fire: [2, 7],\n wood: [4, 9],\n}\n\n/** Ordered aspect names by index (metal=0, water=1, earth=2, fire=3, wood=4) */\nexport const ASPECT_NAMES = [\"metal\", \"water\", \"earth\", \"fire\", \"wood\"]\n\n/**\n * Wu Xing generating/overcoming cycle.\n * For each active aspect, the five result categories in order:\n * [successes, auspicious, noxious, loksyu, tinji]\n */\nexport const WU_XING_CYCLE = {\n wood: [\"wood\", \"fire\", \"water\", \"earth\", \"metal\"],\n fire: [\"fire\", \"earth\", \"wood\", \"metal\", \"water\"],\n earth: [\"earth\", \"metal\", \"fire\", \"water\", \"wood\"],\n metal: [\"metal\", \"water\", \"earth\", \"wood\", \"fire\"],\n water: [\"water\", \"wood\", \"metal\", \"fire\", \"earth\"],\n}\n\nexport const TEMPLATE_PARTIALS = [\n \"systems/fvtt-chroniques-de-l-etrange/templates/actor/parts/cde-character-skills.html\",\n \"systems/fvtt-chroniques-de-l-etrange/templates/actor/parts/cde-character-magics.html\",\n \"systems/fvtt-chroniques-de-l-etrange/templates/actor/parts/cde-character-nghang.html\",\n \"systems/fvtt-chroniques-de-l-etrange/templates/actor/parts/cde-character-treasures.html\",\n \"systems/fvtt-chroniques-de-l-etrange/templates/actor/parts/cde-character-items.html\",\n \"systems/fvtt-chroniques-de-l-etrange/templates/actor/parts/cde-character-kungfus.html\",\n \"systems/fvtt-chroniques-de-l-etrange/templates/actor/parts/cde-npc-supernaturals.html\",\n \"systems/fvtt-chroniques-de-l-etrange/templates/actor/parts/cde-npc-spells.html\",\n \"systems/fvtt-chroniques-de-l-etrange/templates/actor/parts/cde-npc-kungfus.html\",\n \"systems/fvtt-chroniques-de-l-etrange/templates/actor/parts/cde-npc-items.html\",\n \"systems/fvtt-chroniques-de-l-etrange/templates/apps/cde-loksyu-app.html\",\n \"systems/fvtt-chroniques-de-l-etrange/templates/apps/cde-tinji-app.html\",\n \"systems/fvtt-chroniques-de-l-etrange/templates/apps/cde-wheel-app.html\",\n]\n", "/**\n * Chroniques de l'\u00C9trange \u2014 Syst\u00E8me FoundryVTT\n *\n * Chroniques de l'\u00C9trange est un jeu de r\u00F4le \u00E9dit\u00E9 par Antre-Monde \u00C9ditions.\n * Ce syst\u00E8me FoundryVTT est une impl\u00E9mentation ind\u00E9pendante et n'est pas\n * affili\u00E9 \u00E0 Antre-Monde \u00C9ditions,\n * mais a \u00E9t\u00E9 r\u00E9alis\u00E9 avec l'autorisation d'Antre-Monde \u00C9ditions.\n *\n * @author LeRatierBretonnien\n * @copyright 2024\u20132026 LeRatierBretonnien\n * @license CC BY-NC-SA 4.0 \u2013 https://creativecommons.org/licenses/by-nc-sa/4.0/\n */\n\n/**\n * Migrates actor JSON from the legacy CDE system to the current system format.\n *\n * This module is pure logic \u2014 it does not interact with Foundry APIs directly.\n * All transformation is done in-memory; the caller is responsible for creating\n * Foundry documents from the returned data.\n */\n\n// \u2500\u2500 Element label \u2192 key \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst ELEMENT_LABEL_TO_KEY = {\n \"m\u00E9tal\": \"metal\",\n \"metal\": \"metal\",\n \"eau\": \"eau\",\n \"terre\": \"terre\",\n \"feu\": \"feu\",\n \"bois\": \"bois\",\n}\n\n/** Normalise a French element label to its system key (e.g. \"M\u00E9tal\" \u2192 \"metal\"). */\nfunction elementKey(label = \"\") {\n return ELEMENT_LABEL_TO_KEY[label.toLowerCase().trim()] ?? \"metal\"\n}\n\n// \u2500\u2500 Hei polarity label \u2192 key \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction heiKey(label = \"\") {\n const l = label.toLowerCase().trim()\n if (l === \"yin/yang\" || l === \"yinyang\") return \"yinyang\"\n if (l === \"yang\") return \"yang\"\n return \"yin\"\n}\n\n// \u2500\u2500 Spell discipline inference from speciality name \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** Maps French speciality labels (lowercase, accents stripped) \u2192 school key. */\nconst SPECIALITY_TO_DISCIPLINE = {\n // internalcinnabar\n \"essence\": \"internalcinnabar\",\n \"esprit\": \"internalcinnabar\",\n \"mind\": \"internalcinnabar\",\n \"purification\": \"internalcinnabar\",\n \"manipulation\": \"internalcinnabar\",\n \"aura\": \"internalcinnabar\",\n // alchemy\n \"acupuncture\": \"alchemy\",\n \"\u00E9lixirs\": \"alchemy\",\n \"elixirs\": \"alchemy\",\n \"poisons\": \"alchemy\",\n \"arsenal\": \"alchemy\",\n \"potions\": \"alchemy\",\n // masteryoftheway\n \"mal\u00E9diction\": \"masteryoftheway\",\n \"malediction\": \"masteryoftheway\",\n \"transfiguration\": \"masteryoftheway\",\n \"n\u00E9cromancie\": \"masteryoftheway\",\n \"necromancie\": \"masteryoftheway\",\n \"contr\u00F4le climatique\": \"masteryoftheway\",\n \"controle climatique\": \"masteryoftheway\",\n \"magie d'or\": \"masteryoftheway\",\n \"magie dor\": \"masteryoftheway\",\n // exorcism\n \"invocation\": \"exorcism\",\n \"pistage\": \"exorcism\",\n \"tra\u00E7age\": \"exorcism\",\n \"tracage\": \"exorcism\",\n \"protection\": \"exorcism\",\n \"ch\u00E2timent\": \"exorcism\",\n \"chatiment\": \"exorcism\",\n \"domination\": \"exorcism\",\n // geomancy\n \"neutralisation\": \"geomancy\",\n \"divination\": \"geomancy\",\n \"pri\u00E8re terrestre\": \"geomancy\",\n \"priere terrestre\": \"geomancy\",\n \"pri\u00E8re c\u00E9leste\": \"geomancy\",\n \"priere celeste\": \"geomancy\",\n \"g\u00E9omancie\": \"geomancy\",\n \"geomancie\": \"geomancy\",\n \"feng shui\": \"geomancy\",\n \"fungseoi\": \"geomancy\",\n}\n\n/**\n * Attempt to infer the magic school (discipline) from a spell's speciality name.\n * Falls back to scanning the item name for school keywords if needed.\n */\nfunction inferDiscipline(specialityName = \"\", itemName = \"\") {\n const key = specialityName.toLowerCase().trim()\n if (SPECIALITY_TO_DISCIPLINE[key]) return SPECIALITY_TO_DISCIPLINE[key]\n\n // Fuzzy fallback: check item name for school markers\n const name = itemName.toLowerCase()\n if (name.includes(\"exorcis\")) return \"exorcism\"\n if (name.includes(\"g\u00E9omanci\") || name.includes(\"geomanci\")) return \"geomancy\"\n if (name.includes(\"alchimi\")) return \"alchemy\"\n if (name.includes(\"cinnabre\") || name.includes(\"interne\")) return \"internalcinnabar\"\n if (name.includes(\"ma\u00EEtrise\") || name.includes(\"maitrise\") || name.includes(\"tao\")) return \"masteryoftheway\"\n\n return \"internalcinnabar\"\n}\n\n// \u2500\u2500 KungFu activation mapping \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction mapActivation(oldActivation = \"\") {\n const s = oldActivation.toLowerCase()\n if (s.includes(\"inflig\u00E9s\") || s.includes(\"infliges\")) return \"damage-inflicted\"\n if (s.includes(\"re\u00E7us\") || s.includes(\"recus\")) return \"damage-received\"\n if (s.includes(\"r\u00E9action\") || s.includes(\"reaction\")) return \"reaction\"\n if (s.includes(\"d\u00E9s-fastes\") || s.includes(\"des-fastes\") || s.includes(\"fastes\")) return \"dice\"\n if (s.includes(\"aide\")) return \"action-aid\"\n if (s.includes(\"attaque\") && s.includes(\"d\u00E9fense\")) return \"action-attack-defense\"\n if (s.includes(\"attaque\") && s.includes(\"defense\")) return \"action-attack-defense\"\n if (s.includes(\"attaque\")) return \"action-attack\"\n if (s.includes(\"d\u00E9fense\") || s.includes(\"defense\")) return \"action-defense\"\n return \"action-attack\"\n}\n\n// \u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst DEFAULT_ACTOR_IMG = \"icons/svg/mystery-man.svg\"\nconst DEFAULT_ITEM_IMG = \"icons/svg/item-bag.svg\"\n\n// \u2500\u2500 Item migration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction migrateEquipmentItem(oldItem) {\n const s = oldItem.system ?? {}\n return {\n name: oldItem.name,\n type: \"item\",\n img: oldItem.img || DEFAULT_ITEM_IMG,\n system: {\n reference: s.reference ?? \"\",\n description: s.description ?? \"\",\n quantity: Number(s.quantity ?? 1),\n weight: Number(s.weight ?? 0),\n notes: s.notes ?? \"\",\n },\n }\n}\n\nfunction migrateKungfuItem(oldItem) {\n const s = oldItem.system ?? {}\n const techs = s.techniques ?? {}\n\n const migratedTechs = {}\n for (const key of [\"technique1\", \"technique2\", \"technique3\"]) {\n const t = techs[key] ?? {}\n migratedTechs[key] = {\n check: Boolean(t.check),\n name: t.name ?? \"\",\n activation: mapActivation(t.activation ?? \"\"),\n technique: t.technique ?? \"\",\n }\n }\n\n return {\n name: oldItem.name,\n type: \"kungfu\",\n img: oldItem.img || DEFAULT_ITEM_IMG,\n system: {\n reference: s.reference ?? \"\",\n description: s.description ?? \"\",\n orientation: s.orientation || \"yin\",\n aspect: s.aspect || \"metal\",\n skill: s.skill || \"kungfu\",\n speciality: s.speciality ?? \"\",\n style: s.style ?? \"\",\n techniques: migratedTechs,\n notes: s.notes ?? \"\",\n },\n }\n}\n\nfunction migrateSpellItem(oldItem) {\n const s = oldItem.system ?? {}\n return {\n name: oldItem.name,\n type: \"spell\",\n img: oldItem.img || DEFAULT_ITEM_IMG,\n system: {\n reference: s.reference ?? \"\",\n description: s.description ?? \"\",\n specialityname: s.specialityname ?? \"\",\n associatedelement: elementKey(s.associatedelement ?? \"\"),\n heiType: heiKey(s.hei ?? \"\"),\n heiCost: Number(s.heiCost ?? 0),\n difficulty: Number(s.difficulty ?? 0),\n realizationtimeritual: s.realizationtimeritual ?? \"\",\n realizationtimeaccelerated: s.realizationtimeaccelerated ?? \"\",\n flashback: s.flashback ?? \"\",\n components: s.components ?? \"\",\n effects: s.effects ?? \"\",\n examples: s.examples ?? \"\",\n notes: s.notes ?? \"\",\n discipline: inferDiscipline(s.specialityname ?? \"\", oldItem.name ?? \"\"),\n },\n }\n}\n\nfunction migrateSupernaturalItem(oldItem) {\n const s = oldItem.system ?? {}\n // Old system stored a nested `supernatural: { reference }` \u2014 prefer that reference if top-level is empty\n const nestedRef = s.supernatural?.reference ?? \"\"\n return {\n name: oldItem.name,\n type: \"supernatural\",\n img: oldItem.img || DEFAULT_ITEM_IMG,\n system: {\n reference: s.reference || nestedRef,\n description: s.description ?? \"\",\n notes: s.notes ?? \"\",\n heiType: \"yin\",\n heiCost: 0,\n trigger: \"\",\n effects: \"\",\n },\n }\n}\n\nfunction migrateWeaponItem(oldItem) {\n const s = oldItem.system ?? {}\n return {\n name: oldItem.name,\n type: \"weapon\",\n img: oldItem.img || DEFAULT_ITEM_IMG,\n system: {\n reference: s.reference ?? \"\",\n description: s.description ?? \"\",\n hasSpeciality: Boolean(s.hasSpeciality),\n weaponType: s.weaponType || \"melee\",\n material: s.material ?? \"\",\n damageAspect: elementKey(s.damageAspect ?? \"\"),\n damageBase: Number(s.damageBase ?? 0),\n range: s.range || \"contact\",\n obtainLevel: Number(s.obtainLevel ?? 0),\n obtainDifficulty: Number(s.obtainDifficulty ?? 0),\n quantity: Number(s.quantity ?? 1),\n notes: s.notes ?? \"\",\n },\n }\n}\n\nfunction migrateArmorItem(oldItem) {\n const s = oldItem.system ?? {}\n return {\n name: oldItem.name,\n type: \"armor\",\n img: oldItem.img || DEFAULT_ITEM_IMG,\n system: {\n reference: s.reference ?? \"\",\n description: s.description ?? \"\",\n protectionValue: Number(s.protectionValue ?? 0),\n domain: s.domain ?? \"\",\n obtainLevel: Number(s.obtainLevel ?? 0),\n obtainDifficulty: Number(s.obtainDifficulty ?? 0),\n quantity: Number(s.quantity ?? 1),\n notes: s.notes ?? \"\",\n },\n }\n}\n\nfunction migrateSanheiItem(oldItem) {\n const s = oldItem.system ?? {}\n const props = s.properties ?? {}\n const propSchema = (p) => ({\n name: p?.name ?? \"\",\n heiCost: Number(p?.heiCost ?? 0),\n heiType: heiKey(p?.heiType ?? \"\"),\n description: p?.description ?? \"\",\n })\n return {\n name: oldItem.name,\n type: \"sanhei\",\n img: oldItem.img || DEFAULT_ITEM_IMG,\n system: {\n reference: s.reference ?? \"\",\n description: s.description ?? \"\",\n heiType: heiKey(s.heiType ?? \"\"),\n properties: {\n prop1: propSchema(props.prop1),\n prop2: propSchema(props.prop2),\n prop3: propSchema(props.prop3),\n },\n notes: s.notes ?? \"\",\n },\n }\n}\n\nfunction migrateIngredientItem(oldItem) {\n const s = oldItem.system ?? {}\n return {\n name: oldItem.name,\n type: \"ingredient\",\n img: oldItem.img || DEFAULT_ITEM_IMG,\n system: {\n reference: s.reference ?? \"\",\n description: s.description ?? \"\",\n school: s.school ?? \"all\",\n obtainLevel: Number(s.obtainLevel ?? 0),\n obtainDifficulty: Number(s.obtainDifficulty ?? 0),\n quantity: Number(s.quantity ?? 1),\n notes: s.notes ?? \"\",\n },\n }\n}\n\nfunction migrateItem(oldItem) {\n switch (oldItem.type) {\n case \"item\": return migrateEquipmentItem(oldItem)\n case \"kungfu\": return migrateKungfuItem(oldItem)\n case \"spell\": return migrateSpellItem(oldItem)\n case \"supernatural\": return migrateSupernaturalItem(oldItem)\n case \"weapon\": return migrateWeaponItem(oldItem)\n case \"armor\": return migrateArmorItem(oldItem)\n case \"sanhei\": return migrateSanheiItem(oldItem)\n case \"ingredient\": return migrateIngredientItem(oldItem)\n default:\n // Unknown item type: keep as generic equipment\n return migrateEquipmentItem({ ...oldItem, type: \"item\" })\n }\n}\n\n// \u2500\u2500 Actor migration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction migrateCharacter(old) {\n const s = old.system ?? {}\n\n // aspects: keep only { chinese, label, value }\n const aspect = {}\n for (const [k, v] of Object.entries(s.aspect ?? {})) {\n aspect[k] = { chinese: v.chinese ?? \"\", label: v.label ?? \"\", value: Number(v.value ?? 0) }\n }\n\n // skills: keep { label, specialities, value }\n const skills = {}\n for (const [k, v] of Object.entries(s.skills ?? {})) {\n skills[k] = { label: v.label ?? \"\", specialities: v.specialities ?? \"\", value: Number(v.value ?? 0) }\n }\n\n // resources: keep { label, specialities, value, debt }\n const resources = {}\n for (const [k, v] of Object.entries(s.resources ?? {})) {\n resources[k] = { label: v.label ?? \"\", specialities: v.specialities ?? \"\", value: Number(v.value ?? 0), debt: Boolean(v.debt) }\n }\n\n // components: keep { value }\n const component = {}\n for (const [k, v] of Object.entries(s.component ?? {})) {\n component[k] = { value: v.value ?? \"\" }\n }\n\n // magics: keep { visible, value, speciality: { [key]: { check } } }\n const MAGIC_SPECIALITIES = {\n internalcinnabar: [\"essence\", \"mind\", \"purification\", \"manipulation\", \"aura\"],\n alchemy: [\"acupuncture\", \"elixirs\", \"poisons\", \"arsenal\", \"potions\"],\n masteryoftheway: [\"curse\", \"transfiguration\", \"necromancy\", \"climatecontrol\", \"goldenmagic\"],\n exorcism: [\"invocation\", \"tracking\", \"protection\", \"punishment\", \"domination\"],\n geomancy: [\"neutralization\", \"divination\", \"earthlyprayer\", \"heavenlyprayer\", \"fungseoi\"],\n }\n const magics = {}\n for (const [school, specs] of Object.entries(MAGIC_SPECIALITIES)) {\n const om = s.magics?.[school] ?? {}\n const speciality = {}\n for (const spec of specs) {\n speciality[spec] = { check: Boolean(om.speciality?.[spec]?.check) }\n }\n magics[school] = { visible: Boolean(om.visible), value: Number(om.value ?? 0), speciality }\n }\n\n // threetreasures: strip `min` from heiyang/heiyin; keep dicelevel as-is\n const tt = s.threetreasures ?? {}\n const threetreasures = {\n heiyang: { value: Number(tt.heiyang?.value ?? 0), max: Number(tt.heiyang?.max ?? 0) },\n heiyin: { value: Number(tt.heiyin?.value ?? 0), max: Number(tt.heiyin?.max ?? 0) },\n dicelevel: {\n level0d: {\n san: { value: Number(tt.dicelevel?.level0d?.san?.value ?? 0), max: Number(tt.dicelevel?.level0d?.san?.max ?? 0) },\n zing: { value: Number(tt.dicelevel?.level0d?.zing?.value ?? 0), max: Number(tt.dicelevel?.level0d?.zing?.max ?? 0) },\n },\n level1d: {\n san: { value: Number(tt.dicelevel?.level1d?.san?.value ?? 0), max: Number(tt.dicelevel?.level1d?.san?.max ?? 0) },\n zing: { value: Number(tt.dicelevel?.level1d?.zing?.value ?? 0), max: Number(tt.dicelevel?.level1d?.zing?.max ?? 0) },\n },\n level2d: {\n san: { value: Number(tt.dicelevel?.level2d?.san?.value ?? 0), max: Number(tt.dicelevel?.level2d?.san?.max ?? 0) },\n zing: { value: Number(tt.dicelevel?.level2d?.zing?.value ?? 0), max: Number(tt.dicelevel?.level2d?.zing?.max ?? 0) },\n },\n },\n }\n\n // biography (old separate field) merged into description\n const description = s.description || s.biography || \"\"\n\n return {\n name: old.name,\n type: \"character\",\n img: old.img || DEFAULT_ACTOR_IMG,\n system: {\n concept: s.concept ?? \"\",\n guardian: parseInt(s.guardian ?? \"0\") || 0,\n initiative: Number(s.initiative ?? 1),\n anti_initiative: Number(s.anti_initiative ?? 24),\n description,\n aspect,\n skills,\n resources,\n component,\n magics,\n magicOrder: [],\n threetreasures,\n experience: {\n value: Number(s.experience?.value ?? 0),\n max: Number(s.experience?.max ?? 0),\n min: Number(s.experience?.min ?? 0),\n },\n },\n items: (old.items ?? []).map(migrateItem),\n }\n}\n\nfunction migrateNpc(old) {\n const s = old.system ?? {}\n\n const aptitudes = {}\n for (const [k, v] of Object.entries(s.aptitudes ?? {})) {\n aptitudes[k] = { value: Number(v.value ?? 0), speciality: v.speciality ?? \"\" }\n }\n\n return {\n name: old.name,\n type: \"npc\",\n img: old.img || DEFAULT_ACTOR_IMG,\n system: {\n type: s.type ?? \"\",\n // Old system had separate `levelofthreat`/`powerofnuisance` as numbers\n // and string copies `threat`/`nuisance` \u2014 use the numeric fields\n threat: Number(s.levelofthreat ?? s.threat ?? 0),\n nuisance: Number(s.powerofnuisance ?? s.nuisance ?? 0),\n initiative: Number(s.initiative ?? 1),\n anti_initiative: Number(s.anti_initiative ?? 24),\n aptitudes,\n vitality: {\n value: Number(s.vitality?.value ?? 0),\n calcul: Number(s.vitality?.calcul ?? 0),\n note: s.vitality?.note ?? \"\",\n },\n hei: {\n value: Number(s.hei?.value ?? 0),\n calcul: Number(s.hei?.calcul ?? 0),\n note: s.hei?.note ?? \"\",\n },\n description: s.description ?? \"\",\n },\n items: (old.items ?? []).map(migrateItem),\n }\n}\n\n// \u2500\u2500 Public API \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Migrate a single legacy actor JSON to the current system format.\n *\n * @param {object} oldJson Parsed JSON from the old system export\n * @returns {{ name: string, type: string, img: string, system: object, items: object[] }}\n * @throws {Error} if the actor type is unrecognised\n */\nexport function migrateActor(oldJson) {\n switch (oldJson.type) {\n case \"character\": return migrateCharacter(oldJson)\n case \"npc\": return migrateNpc(oldJson)\n default:\n throw new Error(`Unknown actor type \"${oldJson.type}\" in \"${oldJson.name}\"`)\n }\n}\n\n/**\n * Parse one or more legacy JSON strings and return migrated actor data.\n * Accepts a single actor object or an array of actor objects in one file.\n *\n * @param {string} jsonText Raw JSON text from a file\n * @returns {Array<{ name, type, img, system, items }>}\n */\nexport function parseLegacyJson(jsonText) {\n const parsed = JSON.parse(jsonText)\n if (typeof parsed !== \"object\" || parsed === null) {\n throw new Error(\"Le fichier JSON doit contenir un objet acteur ou un tableau d'acteurs\")\n }\n const actors = Array.isArray(parsed) ? parsed : [parsed]\n return actors.map(migrateActor)\n}\n", "import { parseLegacyJson } from \"../../migration/migrator.js\"\n\nconst MIGRATION_TEMPLATE = \"systems/fvtt-chroniques-de-l-etrange/templates/apps/cde-migration-app.html\"\n\n/**\n * Dialog for importing legacy CDE actor JSON files into the current system.\n *\n * Accessible via the System Settings menu (registerMenu).\n * Supports multi-file selection and shows a preview table before importing.\n */\nexport class CDEMigrationApp extends foundry.applications.api.HandlebarsApplicationMixin(\n foundry.applications.api.ApplicationV2\n) {\n static DEFAULT_OPTIONS = {\n id: \"cde-migration-app\",\n classes: [\"cde-migration-app\"],\n tag: \"div\",\n window: {\n title: \"CDE.MigrationTitle\",\n icon: \"fas fa-file-import\",\n resizable: false,\n },\n position: { width: 600, height: \"auto\" },\n actions: {\n clearFiles: CDEMigrationApp.#clearFiles,\n doImport: CDEMigrationApp.#doImport,\n confirmImport: CDEMigrationApp.#confirmImport,\n cancelImport: CDEMigrationApp.#cancelImport,\n },\n }\n\n static PARTS = {\n form: { template: MIGRATION_TEMPLATE },\n }\n\n /** @type {Array<{name: string, type: string, img: string, system: object, items: object[], _srcFile: string}>} */\n #pending = []\n\n /** @type {string[]} - error messages per file */\n #errors = []\n\n /** @type {\"idle\"|\"confirm\"|\"importing\"} */\n #importState = \"idle\"\n\n /** @type {number} - actors created so far (during importing) */\n #progress = 0\n\n async _prepareContext(options) {\n // Compute _duplicate live from the world each render, to avoid stale flags\n const enrichDuplicate = (a) => ({\n ...a,\n _duplicate: game.actors?.getName(a.name) !== null,\n })\n const pending = this.#pending.map(enrichDuplicate)\n const duplicateCount = pending.filter(a => a._duplicate).length\n return {\n pending,\n errors: this.#errors,\n hasPending: this.#pending.length > 0,\n hasErrors: this.#errors.length > 0,\n hasDuplicates: duplicateCount > 0,\n duplicateCount,\n count: this.#pending.length,\n importState: this.#importState,\n progress: this.#progress,\n total: this.#pending.length,\n }\n }\n\n /** After render, wire up the file input. */\n _onRender(context, options) {\n super._onRender(context, options)\n const input = this.element.querySelector(\".cde-migration-file-input\")\n input?.addEventListener(\"change\", this.#onFileChange.bind(this))\n\n const dropZone = this.element.querySelector(\".cde-migration-drop-zone\")\n if (dropZone) {\n dropZone.addEventListener(\"dragover\", (e) => { e.preventDefault(); dropZone.classList.add(\"is-dragover\") })\n dropZone.addEventListener(\"dragleave\", () => dropZone.classList.remove(\"is-dragover\"))\n dropZone.addEventListener(\"drop\", (e) => {\n e.preventDefault()\n dropZone.classList.remove(\"is-dragover\")\n this.#processFiles(Array.from(e.dataTransfer.files))\n })\n }\n }\n\n async #onFileChange(event) {\n const files = Array.from(event.target.files ?? [])\n event.target.value = \"\"\n await this.#processFiles(files)\n }\n\n async #processFiles(files) {\n for (const file of files) {\n if (!file.name.endsWith(\".json\")) {\n this.#errors.push(game.i18n.format(\"CDE.MigrationErrorNotJson\", { file: file.name }))\n continue\n }\n try {\n const text = await file.text()\n const actors = parseLegacyJson(text)\n for (const actor of actors) {\n actor._srcFile = file.name\n // Avoid duplicates-by-name in our pending list\n if (this.#pending.some(p => p.name === actor.name)) {\n this.#errors.push(`\u00AB ${actor.name} \u00BB ignor\u00E9 (nom d\u00E9j\u00E0 dans la liste d'attente, fichier \u00AB ${file.name} \u00BB)`)\n continue\n }\n this.#pending.push(actor)\n }\n } catch (err) {\n this.#errors.push(game.i18n.format(\"CDE.MigrationErrorParse\", { file: file.name, error: err.message }))\n }\n }\n this.#importState = \"idle\"\n this.render()\n }\n\n static async #clearFiles() {\n this.#pending = []\n this.#errors = []\n this.#importState = \"idle\"\n this.#progress = 0\n this.render()\n }\n\n /** First click: switch to confirmation state instead of importing immediately. */\n static async #doImport() {\n if (!this.#pending.length) return\n this.#importState = \"confirm\"\n this.render()\n }\n\n /** Second click: actually perform the import. */\n static async #confirmImport() {\n if (!this.#pending.length) return\n\n this.#importState = \"importing\"\n this.#progress = 0\n this.render()\n\n const total = this.#pending.length\n const created = []\n const failed = []\n\n for (let i = 0; i < total; i++) {\n const data = this.#pending[i]\n try {\n const { _srcFile, ...actorData } = data\n const actor = await Actor.create(actorData)\n created.push(actor.name)\n } catch (err) {\n failed.push(`${data.name}: ${err.message}`)\n console.error(`CHRONIQUESDELETRANGE | Import failed for \"${data.name}\":`, err)\n }\n this.#progress = i + 1\n // Live-update the progress element in the DOM without full re-render\n const progEl = this.element?.querySelector(\".cde-migration-progress-count\")\n if (progEl) progEl.textContent = `${this.#progress}/${total}`\n }\n\n this.#pending = []\n this.#errors = failed\n this.#importState = \"idle\"\n this.#progress = 0\n this.render()\n\n if (created.length) {\n ui.notifications.info(\n game.i18n.format(\"CDE.MigrationSuccess\", { count: created.length, names: created.join(\", \") })\n )\n }\n if (failed.length) {\n ui.notifications.warn(\n game.i18n.format(\"CDE.MigrationPartialError\", { count: failed.length })\n )\n }\n }\n\n static async #cancelImport() {\n this.#importState = \"idle\"\n this.render()\n }\n}\n", "/**\n * Chroniques de l'\u00C9trange \u2014 Syst\u00E8me FoundryVTT\n *\n * Chroniques de l'\u00C9trange est un jeu de r\u00F4le \u00E9dit\u00E9 par Antre-Monde \u00C9ditions.\n * Ce syst\u00E8me FoundryVTT est une impl\u00E9mentation ind\u00E9pendante et n'est pas\n * affili\u00E9 \u00E0 Antre-Monde \u00C9ditions,\n * mais a \u00E9t\u00E9 r\u00E9alis\u00E9 avec l'autorisation d'Antre-Monde \u00C9ditions.\n *\n * @author LeRatierBretonnien\n * @copyright 2024\u20132026 LeRatierBretonnien\n * @license CC BY-NC-SA 4.0 \u2013 https://creativecommons.org/licenses/by-nc-sa/4.0/\n */\n\nimport { SYSTEM_ID } from \"./constants.js\"\nimport { CDEMigrationApp } from \"../ui/apps/migration-app.js\"\n\n/**\n * Register all world/client settings for the system.\n * Called during the \"init\" hook before sheets and data-models are set up.\n */\nexport function registerSettings() {\n game.settings.registerMenu(SYSTEM_ID, \"migrationTool\", {\n name: \"CDE.MigrationTitle\",\n label: \"CDE.MigrationMenuLabel\",\n hint: \"CDE.MigrationMenuHint\",\n icon: \"fas fa-file-import\",\n type: CDEMigrationApp,\n restricted: true,\n })\n\n game.settings.register(SYSTEM_ID, \"loksyuData\", {\n scope: \"world\",\n config: false,\n type: Object,\n default: {\n wood: { yin: 0, yang: 0 },\n fire: { yin: 0, yang: 0 },\n earth: { yin: 0, yang: 0 },\n metal: { yin: 0, yang: 0 },\n water: { yin: 0, yang: 0 },\n },\n })\n\n game.settings.register(SYSTEM_ID, \"loksyuConsumptionOrder\", {\n name: \"CDE.Settings.LoksyuConsumptionOrder\",\n hint: \"CDE.Settings.LoksyuConsumptionOrderHint\",\n scope: \"world\",\n config: true,\n type: String,\n choices: {\n \"yang-first\": \"CDE.Settings.LoksyuOrderYangFirst\",\n \"yin-first\": \"CDE.Settings.LoksyuOrderYinFirst\",\n \"balanced\": \"CDE.Settings.LoksyuOrderBalanced\",\n },\n default: \"yang-first\",\n onChange: () => Hooks.callAll(\"cde:loksyuUpdated\"),\n })\n\n game.settings.register(SYSTEM_ID, \"tinjiData\", {\n scope: \"world\",\n config: false,\n type: Number,\n default: 0,\n })\n\n game.settings.register(SYSTEM_ID, \"welcomeSceneLoaded\", {\n scope: \"world\",\n config: false,\n type: Boolean,\n default: false,\n })\n}\n\n/**\n * Run any pending data migrations on the \"ready\" hook.\n * Reserved for future schema migrations.\n */\nexport async function migrateIfNeeded() {\n // No migrations required yet.\n}\n\n/**\n * On first startup, import the \"Accueil\" scene from the cde-scenes compendium\n * into the world and activate it. Only runs once (tracked via the\n * `welcomeSceneLoaded` setting). GM-only.\n */\nexport async function loadWelcomeSceneIfNeeded() {\n if (!game.user.isGM) return\n if (game.settings.get(SYSTEM_ID, \"welcomeSceneLoaded\")) return\n\n try {\n const pack = game.packs.get(`${SYSTEM_ID}.cde-scenes`)\n if (!pack) return\n\n const index = await pack.getIndex()\n const entry = index.find(e => e.name === \"Accueil\")\n if (!entry) return\n\n // Check if the scene already exists in the world (e.g. manually imported)\n const existing = game.scenes.find(s => s.name === \"Accueil\")\n let scene = existing\n\n if (!scene) {\n const doc = await pack.getDocument(entry._id)\n ;[scene] = await Scene.createDocuments([doc.toObject()])\n }\n\n await game.settings.set(SYSTEM_ID, \"welcomeSceneLoaded\", true)\n await scene.activate()\n } catch (err) {\n console.error(\"CHRONIQUESDELETRANGE | loadWelcomeSceneIfNeeded failed:\", err)\n }\n}\n", "/**\n * Chroniques de l'\u00C9trange \u2014 Syst\u00E8me FoundryVTT\n *\n * Chroniques de l'\u00C9trange est un jeu de r\u00F4le \u00E9dit\u00E9 par Antre-Monde \u00C9ditions.\n * Ce syst\u00E8me FoundryVTT est une impl\u00E9mentation ind\u00E9pendante et n'est pas\n * affili\u00E9 \u00E0 Antre-Monde \u00C9ditions,\n * mais a \u00E9t\u00E9 r\u00E9alis\u00E9 avec l'autorisation d'Antre-Monde \u00C9ditions.\n *\n * @author LeRatierBretonnien\n * @copyright 2024\u20132026 LeRatierBretonnien\n * @license CC BY-NC-SA 4.0 \u2013 https://creativecommons.org/licenses/by-nc-sa/4.0/\n */\n\nimport { MAGICS, SUBTYPES } from \"./constants.js\"\n\nexport function preLocalizeConfig() {\n const localizeConfigObject = (obj, keys) => {\n for (const o of Object.values(obj)) {\n for (const key of keys) {\n o[key] = game.i18n.localize(o[key])\n }\n }\n }\n\n localizeConfigObject(SUBTYPES, [\"label\"])\n Object.values(MAGICS).forEach((magic) => {\n magic.label = game.i18n.localize(magic.label)\n magic.aspectlabel = game.i18n.localize(magic.aspectlabel)\n Object.values(magic.speciality).forEach((spec) => {\n spec.label = game.i18n.localize(spec.label)\n spec.labelelementkey = spec.labelelement\n spec.labelelement = game.i18n.localize(spec.labelelement)\n })\n })\n}\n", "/**\n * Chroniques de l'\u00C9trange \u2014 Syst\u00E8me FoundryVTT\n *\n * Chroniques de l'\u00C9trange est un jeu de r\u00F4le \u00E9dit\u00E9 par Antre-Monde \u00C9ditions.\n * Ce syst\u00E8me FoundryVTT est une impl\u00E9mentation ind\u00E9pendante et n'est pas\n * affili\u00E9 \u00E0 Antre-Monde \u00C9ditions,\n * mais a \u00E9t\u00E9 r\u00E9alis\u00E9 avec l'autorisation d'Antre-Monde \u00C9ditions.\n *\n * @author LeRatierBretonnien\n * @copyright 2024\u20132026 LeRatierBretonnien\n * @license CC BY-NC-SA 4.0 \u2013 https://creativecommons.org/licenses/by-nc-sa/4.0/\n */\n\nexport function configureRuntime() {\n CONFIG.Actor.compendiumBanner = \"/systems/fvtt-chroniques-de-l-etrange/images/banners/actor-banner.webp\"\n CONFIG.Adventure.compendiumBanner = \"/systems/fvtt-chroniques-de-l-etrange/images/banners/adventure-banner.webp\"\n CONFIG.Cards.compendiumBanner = \"ui/banners/cards-banner.webp\"\n CONFIG.Item.compendiumBanner = \"/systems/fvtt-chroniques-de-l-etrange/images/banners/item-banner.webp\"\n CONFIG.JournalEntry.compendiumBanner = \"/systems/fvtt-chroniques-de-l-etrange/images/banners/journalentry-banner.webp\"\n CONFIG.Macro.compendiumBanner = \"ui/banners/macro-banner.webp\"\n CONFIG.Playlist.compendiumBanner = \"ui/banners/playlist-banner.webp\"\n CONFIG.RollTable.compendiumBanner = \"ui/banners/rolltable-banner.webp\"\n CONFIG.Scene.compendiumBanner = \"/systems/fvtt-chroniques-de-l-etrange/images/banners/scene-banner.webp\"\n}\n", "/**\n * Chroniques de l'\u00C9trange \u2014 Syst\u00E8me FoundryVTT\n *\n * Chroniques de l'\u00C9trange est un jeu de r\u00F4le \u00E9dit\u00E9 par Antre-Monde \u00C9ditions.\n * Ce syst\u00E8me FoundryVTT est une impl\u00E9mentation ind\u00E9pendante et n'est pas\n * affili\u00E9 \u00E0 Antre-Monde \u00C9ditions,\n * mais a \u00E9t\u00E9 r\u00E9alis\u00E9 avec l'autorisation d'Antre-Monde \u00C9ditions.\n *\n * @author LeRatierBretonnien\n * @copyright 2024\u20132026 LeRatierBretonnien\n * @license CC BY-NC-SA 4.0 \u2013 https://creativecommons.org/licenses/by-nc-sa/4.0/\n */\n\nexport default class CharacterDataModel extends foundry.abstract.TypeDataModel {\n static defineSchema() {\n const { fields } = foundry.data\n const numberField = (initial = 0, extra = {}) => new fields.NumberField({ required: true, nullable: false, integer: true, initial, ...extra })\n const stringField = (initial = \"\") => new fields.StringField({ required: true, nullable: false, initial })\n const boolField = (initial = false) => new fields.BooleanField({ required: true, initial })\n const htmlField = (initial = \"\") => new fields.HTMLField({ required: true, nullable: false, initial, textSearch: true })\n\n const aspectField = (label, chinese) =>\n new fields.SchemaField({\n chinese: stringField(chinese),\n label: stringField(label),\n value: numberField(15, { min: 0 }),\n })\n\n const skillField = (label) =>\n new fields.SchemaField({\n label: stringField(label),\n specialities: stringField(\"\"),\n value: numberField(0, { min: 0 }),\n })\n\n const resourceField = (label) =>\n new fields.SchemaField({\n label: stringField(label),\n specialities: stringField(\"\"),\n value: numberField(0, { min: 0 }),\n debt: boolField(false),\n })\n\n const componentField = () =>\n new fields.SchemaField({\n value: stringField(\"\"),\n })\n\n const magicSpecialityField = () =>\n new fields.SchemaField({\n check: boolField(false),\n })\n\n const magicField = () =>\n new fields.SchemaField({\n visible: boolField(true),\n value: numberField(0, { min: 0 }),\n speciality: new fields.SchemaField({\n essence: magicSpecialityField(),\n mind: magicSpecialityField(),\n purification: magicSpecialityField(),\n manipulation: magicSpecialityField(),\n aura: magicSpecialityField(),\n acupuncture: magicSpecialityField(),\n elixirs: magicSpecialityField(),\n poisons: magicSpecialityField(),\n arsenal: magicSpecialityField(),\n potions: magicSpecialityField(),\n curse: magicSpecialityField(),\n transfiguration: magicSpecialityField(),\n necromancy: magicSpecialityField(),\n climatecontrol: magicSpecialityField(),\n goldenmagic: magicSpecialityField(),\n invocation: magicSpecialityField(),\n tracking: magicSpecialityField(),\n protection: magicSpecialityField(),\n punishment: magicSpecialityField(),\n domination: magicSpecialityField(),\n neutralization: magicSpecialityField(),\n divination: magicSpecialityField(),\n earthlyprayer: magicSpecialityField(),\n heavenlyprayer: magicSpecialityField(),\n fungseoi: magicSpecialityField(),\n }),\n })\n\n const treasureBranch = () =>\n new fields.SchemaField({\n value: numberField(0, { min: 0 }),\n max: numberField(0, { min: 0 }),\n min: numberField(0, { min: 0 }),\n })\n\n const treasureLevel = () =>\n new fields.SchemaField({\n san: treasureBranch(),\n zing: treasureBranch(),\n })\n\n const schema = {\n concept: stringField(\"\"),\n guardian: numberField(0, { min: 0, max: 5 }),\n initiative: numberField(1, { min: 0 }),\n anti_initiative: numberField(24, { min: 0 }),\n description: htmlField(\"\"),\n prefs: new fields.SchemaField({\n typeofthrow: new fields.SchemaField({\n check: boolField(true),\n choice: stringField(\"0\"),\n }),\n }),\n prompt: new fields.SchemaField({\n typeofthrow: new fields.SchemaField({\n check: boolField(true),\n choice: stringField(\"0\"),\n }),\n configure: new fields.SchemaField({\n numberofdice: numberField(0),\n aspect: numberField(0),\n bonus: numberField(0),\n bonusauspiciousdice: numberField(0),\n typeofthrow: numberField(0),\n aspectskill: numberField(0),\n bonusmalusskill: numberField(0),\n rolldifficulty: numberField(0),\n freepowerlevels: numberField(0),\n }),\n }),\n aspect: new fields.SchemaField({\n fire: aspectField(\"CDE.Fire\", \"\u328B\"),\n earth: aspectField(\"CDE.Earth\", \"\u328F\"),\n metal: aspectField(\"CDE.Metal\", \"\u328E\"),\n water: aspectField(\"CDE.Water\", \"\u328C\"),\n wood: aspectField(\"CDE.Wood\", \"\u328D\"),\n }),\n skills: new fields.SchemaField({\n art: skillField(\"CDE.Art\"),\n investigation: skillField(\"CDE.Investigation\"),\n erudition: skillField(\"CDE.Erudition\"),\n knavery: skillField(\"CDE.Knavery\"),\n wordliness: skillField(\"CDE.Wordliness\"),\n prowess: skillField(\"CDE.Prowess\"),\n sciences: skillField(\"CDE.Sciences\"),\n technologies: skillField(\"CDE.Technologies\"),\n kungfu: skillField(\"CDE.KungFu\"),\n rangedcombat: skillField(\"CDE.RangedCombat\"),\n }),\n resources: new fields.SchemaField({\n supply: resourceField(\"CDE.Supply\"),\n inquiry: resourceField(\"CDE.Inquiry\"),\n influence: resourceField(\"CDE.Influence\"),\n }),\n component: new fields.SchemaField({\n one: componentField(),\n two: componentField(),\n three: componentField(),\n four: componentField(),\n five: componentField(),\n six: componentField(),\n seven: componentField(),\n eight: componentField(),\n nine: componentField(),\n zero: componentField(),\n }),\n magicOrder: new fields.ArrayField(\n new fields.StringField({ required: true, nullable: false, initial: \"\" }),\n { required: true, initial: [] }\n ),\n magics: new fields.SchemaField({\n internalcinnabar: magicField(),\n alchemy: magicField(),\n masteryoftheway: magicField(),\n exorcism: magicField(),\n geomancy: magicField(),\n }),\n threetreasures: new fields.SchemaField({\n heiyang: new fields.SchemaField({ value: numberField(0, { min: 0 }), max: numberField(0, { min: 0 }) }),\n heiyin: new fields.SchemaField({ value: numberField(0, { min: 0 }), max: numberField(0, { min: 0 }) }),\n dicelevel: new fields.SchemaField({\n level0d: treasureLevel(),\n level1d: treasureLevel(),\n level2d: treasureLevel(),\n }),\n }),\n experience: new fields.SchemaField({\n value: numberField(0, { min: 0 }),\n max: numberField(0, { min: 0 }),\n min: numberField(0, { min: 0 }),\n }),\n }\n\n return schema\n }\n\n prepareDerivedData() {\n this.anti_initiative = 25 - (this.initiative ?? 0)\n }\n}\n", "/**\n * Chroniques de l'\u00C9trange \u2014 Syst\u00E8me FoundryVTT\n *\n * Chroniques de l'\u00C9trange est un jeu de r\u00F4le \u00E9dit\u00E9 par Antre-Monde \u00C9ditions.\n * Ce syst\u00E8me FoundryVTT est une impl\u00E9mentation ind\u00E9pendante et n'est pas\n * affili\u00E9 \u00E0 Antre-Monde \u00C9ditions,\n * mais a \u00E9t\u00E9 r\u00E9alis\u00E9 avec l'autorisation d'Antre-Monde \u00C9ditions.\n *\n * @author LeRatierBretonnien\n * @copyright 2024\u20132026 LeRatierBretonnien\n * @license CC BY-NC-SA 4.0 \u2013 https://creativecommons.org/licenses/by-nc-sa/4.0/\n */\n\nexport default class NpcDataModel extends foundry.abstract.TypeDataModel {\n static defineSchema() {\n const { fields } = foundry.data\n const numberField = (initial = 0, extra = {}) => new fields.NumberField({ required: true, nullable: false, integer: true, initial, ...extra })\n const stringField = (initial = \"\") => new fields.StringField({ required: true, nullable: false, initial })\n const boolField = (initial = false) => new fields.BooleanField({ required: true, initial })\n const htmlField = (initial = \"\") => new fields.HTMLField({ required: true, nullable: false, initial, textSearch: true })\n\n const aptitudeField = () =>\n new fields.SchemaField({\n value: numberField(0, { min: 0 }),\n speciality: stringField(\"\"),\n })\n\n const trackedField = () =>\n new fields.SchemaField({\n value: numberField(0, { min: 0 }),\n calcul: numberField(0, { min: 0 }),\n note: stringField(\"\"),\n })\n\n return {\n type: stringField(\"\"),\n threat: numberField(0, { min: 0, max: 4 }), // profane(0) | apprentice(1) | initiate(2) | accomplished(3) | renowned(4)\n nuisance: numberField(0, { min: 0, max: 5 }), // figurant(0) | minion(1) | adversary(2) | ally(3) | boss(4) | divinity(5)\n initiative: numberField(1, { min: 0 }),\n anti_initiative: numberField(24, { min: 0 }),\n aptitudes: new fields.SchemaField({\n physical: aptitudeField(),\n martial: aptitudeField(),\n mental: aptitudeField(),\n social: aptitudeField(),\n spiritual: aptitudeField(),\n }),\n vitality: trackedField(),\n hei: trackedField(),\n description: htmlField(\"\"),\n prefs: new fields.SchemaField({\n typeofthrow: new fields.SchemaField({\n check: boolField(false),\n choice: stringField(\"0\"),\n }),\n }),\n }\n }\n\n prepareDerivedData() {\n this.anti_initiative = 25 - (this.initiative ?? 0)\n this.vitality.calcul = (this.aptitudes.physical.value ?? 0) * 4\n this.hei.calcul = (this.aptitudes.spiritual.value ?? 0) * 4\n }\n}\n", "/**\n * Chroniques de l'\u00C9trange \u2014 Syst\u00E8me FoundryVTT\n *\n * Chroniques de l'\u00C9trange est un jeu de r\u00F4le \u00E9dit\u00E9 par Antre-Monde \u00C9ditions.\n * Ce syst\u00E8me FoundryVTT est une impl\u00E9mentation ind\u00E9pendante et n'est pas\n * affili\u00E9 \u00E0 Antre-Monde \u00C9ditions,\n * mais a \u00E9t\u00E9 r\u00E9alis\u00E9 avec l'autorisation d'Antre-Monde \u00C9ditions.\n *\n * @author LeRatierBretonnien\n * @copyright 2024\u20132026 LeRatierBretonnien\n * @license CC BY-NC-SA 4.0 \u2013 https://creativecommons.org/licenses/by-nc-sa/4.0/\n */\n\nexport default class EquipmentDataModel extends foundry.abstract.TypeDataModel {\n static defineSchema() {\n const { fields } = foundry.data\n const numberField = (initial = 0, extra = {}) => new fields.NumberField({ required: true, nullable: false, integer: true, initial, ...extra })\n const stringField = (initial = \"\") => new fields.StringField({ required: true, nullable: false, initial })\n const htmlField = (initial = \"\") => new fields.HTMLField({ required: true, nullable: false, initial, textSearch: true })\n\n return {\n reference: stringField(\"\"),\n description: htmlField(\"\"),\n quantity: numberField(1, { min: 0 }),\n weight: numberField(0, { min: 0 }),\n notes: htmlField(\"\"),\n }\n }\n}\n", "/**\n * Chroniques de l'\u00C9trange \u2014 Syst\u00E8me FoundryVTT\n *\n * Chroniques de l'\u00C9trange est un jeu de r\u00F4le \u00E9dit\u00E9 par Antre-Monde \u00C9ditions.\n * Ce syst\u00E8me FoundryVTT est une impl\u00E9mentation ind\u00E9pendante et n'est pas\n * affili\u00E9 \u00E0 Antre-Monde \u00C9ditions,\n * mais a \u00E9t\u00E9 r\u00E9alis\u00E9 avec l'autorisation d'Antre-Monde \u00C9ditions.\n *\n * @author LeRatierBretonnien\n * @copyright 2024\u20132026 LeRatierBretonnien\n * @license CC BY-NC-SA 4.0 \u2013 https://creativecommons.org/licenses/by-nc-sa/4.0/\n */\n\nexport default class KungfuDataModel extends foundry.abstract.TypeDataModel {\n static defineSchema() {\n const { fields } = foundry.data\n const stringField = (initial = \"\") => new fields.StringField({ required: true, nullable: false, initial })\n const htmlField = (initial = \"\") => new fields.HTMLField({ required: true, nullable: false, initial, textSearch: true })\n const boolField = (initial = false) => new fields.BooleanField({ required: true, initial })\n\n const techniqueField = () =>\n new fields.SchemaField({\n check: boolField(false),\n name: stringField(\"\"),\n activation: stringField(\"action-attack\"), // action-attack | action-defense | action-aid | action-attack-defense | reaction | dice | damage-inflicted | damage-received\n technique: htmlField(\"\"),\n })\n\n return {\n reference: stringField(\"\"),\n description: htmlField(\"\"),\n orientation: stringField(\"yin\"), // yin | yang | yinyang\n aspect: stringField(\"metal\"), // metal | water | earth | fire | wood\n skill: stringField(\"kungfu\"), // kungfu | rangedcombat\n speciality: stringField(\"\"),\n style: stringField(\"\"),\n techniques: new fields.SchemaField({\n technique1: techniqueField(),\n technique2: techniqueField(),\n technique3: techniqueField(),\n }),\n notes: htmlField(\"\"),\n }\n }\n}\n", "/**\n * Chroniques de l'\u00C9trange \u2014 Syst\u00E8me FoundryVTT\n *\n * Chroniques de l'\u00C9trange est un jeu de r\u00F4le \u00E9dit\u00E9 par Antre-Monde \u00C9ditions.\n * Ce syst\u00E8me FoundryVTT est une impl\u00E9mentation ind\u00E9pendante et n'est pas\n * affili\u00E9 \u00E0 Antre-Monde \u00C9ditions,\n * mais a \u00E9t\u00E9 r\u00E9alis\u00E9 avec l'autorisation d'Antre-Monde \u00C9ditions.\n *\n * @author LeRatierBretonnien\n * @copyright 2024\u20132026 LeRatierBretonnien\n * @license CC BY-NC-SA 4.0 \u2013 https://creativecommons.org/licenses/by-nc-sa/4.0/\n */\n\nexport default class SpellDataModel extends foundry.abstract.TypeDataModel {\n static defineSchema() {\n const { fields } = foundry.data\n const stringField = (initial = \"\") => new fields.StringField({ required: true, nullable: false, initial })\n const htmlField = (initial = \"\") => new fields.HTMLField({ required: true, nullable: false, initial, textSearch: true })\n\n return {\n reference: stringField(\"\"),\n description: htmlField(\"\"),\n specialityname: stringField(\"\"),\n associatedelement: stringField(\"metal\"), // metal | water | earth | fire | wood\n hei: stringField(\"\"),\n realizationtimeritual: stringField(\"\"),\n realizationtimeaccelerated: stringField(\"\"),\n flashback: stringField(\"\"),\n components: htmlField(\"\"),\n effects: htmlField(\"\"),\n examples: htmlField(\"\"),\n notes: htmlField(\"\"),\n discipline: stringField(\"internalcinnabar\"),\n heiType: stringField(\"yin\"),\n heiCost: new fields.NumberField({ required: true, nullable: false, integer: true, min: 0, initial: 1 }),\n difficulty: new fields.NumberField({ required: true, nullable: false, integer: true, min: 0, initial: 1 }),\n }\n }\n}\n", "/**\n * Chroniques de l'\u00C9trange \u2014 Syst\u00E8me FoundryVTT\n *\n * Chroniques de l'\u00C9trange est un jeu de r\u00F4le \u00E9dit\u00E9 par Antre-Monde \u00C9ditions.\n * Ce syst\u00E8me FoundryVTT est une impl\u00E9mentation ind\u00E9pendante et n'est pas\n * affili\u00E9 \u00E0 Antre-Monde \u00C9ditions,\n * mais a \u00E9t\u00E9 r\u00E9alis\u00E9 avec l'autorisation d'Antre-Monde \u00C9ditions.\n *\n * @author LeRatierBretonnien\n * @copyright 2024\u20132026 LeRatierBretonnien\n * @license CC BY-NC-SA 4.0 \u2013 https://creativecommons.org/licenses/by-nc-sa/4.0/\n */\n\nexport default class SupernaturalDataModel extends foundry.abstract.TypeDataModel {\n static defineSchema() {\n const { fields } = foundry.data\n const stringField = (initial = \"\") => new fields.StringField({ required: true, nullable: false, initial })\n const htmlField = (initial = \"\") => new fields.HTMLField({ required: true, nullable: false, initial, textSearch: true })\n\n return {\n reference: stringField(\"\"),\n description: htmlField(\"\"),\n notes: htmlField(\"\"),\n heiType: stringField(\"yin\"),\n heiCost: new fields.NumberField({ required: true, nullable: false, integer: true, min: 0, initial: 0 }),\n trigger: stringField(\"\"),\n effects: htmlField(\"\"),\n }\n }\n}\n", "/**\n * Chroniques de l'\u00C9trange \u2014 Syst\u00E8me FoundryVTT\n *\n * Chroniques de l'\u00C9trange est un jeu de r\u00F4le \u00E9dit\u00E9 par Antre-Monde \u00C9ditions.\n * Ce syst\u00E8me FoundryVTT est une impl\u00E9mentation ind\u00E9pendante et n'est pas\n * affili\u00E9 \u00E0 Antre-Monde \u00C9ditions,\n * mais a \u00E9t\u00E9 r\u00E9alis\u00E9 avec l'autorisation d'Antre-Monde \u00C9ditions.\n *\n * @author LeRatierBretonnien\n * @copyright 2024\u20132026 LeRatierBretonnien\n * @license CC BY-NC-SA 4.0 \u2013 https://creativecommons.org/licenses/by-nc-sa/4.0/\n */\n\nexport default class WeaponDataModel extends foundry.abstract.TypeDataModel {\n static defineSchema() {\n const { fields } = foundry.data\n const stringField = (initial = \"\") => new fields.StringField({ required: true, nullable: false, initial })\n const htmlField = (initial = \"\") => new fields.HTMLField({ required: true, nullable: false, initial, textSearch: true })\n const intField = (initial = 0, opts = {}) => new fields.NumberField({ required: true, nullable: false, integer: true, initial, ...opts })\n const boolField = (initial = false) => new fields.BooleanField({ required: true, initial })\n\n return {\n reference: stringField(\"\"),\n description: htmlField(\"\"),\n hasSpeciality: boolField(false),\n weaponType: stringField(\"melee\"),\n material: stringField(\"\"),\n damageAspect: stringField(\"metal\"),\n damageBase: intField(0),\n range: stringField(\"contact\"), // contact | courte | mediane | longue | extreme\n obtainLevel: intField(0, { min: 0, max: 5 }),\n obtainDifficulty: intField(0, { min: 0, max: 3 }),\n quantity: intField(1, { min: 0 }),\n notes: htmlField(\"\"),\n }\n }\n}\n", "/**\n * Chroniques de l'\u00C9trange \u2014 Syst\u00E8me FoundryVTT\n *\n * Chroniques de l'\u00C9trange est un jeu de r\u00F4le \u00E9dit\u00E9 par Antre-Monde \u00C9ditions.\n * Ce syst\u00E8me FoundryVTT est une impl\u00E9mentation ind\u00E9pendante et n'est pas\n * affili\u00E9 \u00E0 Antre-Monde \u00C9ditions,\n * mais a \u00E9t\u00E9 r\u00E9alis\u00E9 avec l'autorisation d'Antre-Monde \u00C9ditions.\n *\n * @author LeRatierBretonnien\n * @copyright 2024\u20132026 LeRatierBretonnien\n * @license CC BY-NC-SA 4.0 \u2013 https://creativecommons.org/licenses/by-nc-sa/4.0/\n */\n\nexport default class ArmorDataModel extends foundry.abstract.TypeDataModel {\n static defineSchema() {\n const { fields } = foundry.data\n const stringField = (initial = \"\") => new fields.StringField({ required: true, nullable: false, initial })\n const htmlField = (initial = \"\") => new fields.HTMLField({ required: true, nullable: false, initial, textSearch: true })\n const intField = (initial = 0, opts = {}) => new fields.NumberField({ required: true, nullable: false, integer: true, initial, ...opts })\n\n return {\n reference: stringField(\"\"),\n description: htmlField(\"\"),\n protectionValue: intField(0),\n domain: stringField(\"\"),\n obtainLevel: intField(0, { min: 0, max: 5 }),\n obtainDifficulty: intField(0, { min: 0, max: 3 }),\n quantity: intField(1, { min: 0 }),\n notes: htmlField(\"\"),\n }\n }\n}\n", "/**\n * Chroniques de l'\u00C9trange \u2014 Syst\u00E8me FoundryVTT\n *\n * Chroniques de l'\u00C9trange est un jeu de r\u00F4le \u00E9dit\u00E9 par Antre-Monde \u00C9ditions.\n * Ce syst\u00E8me FoundryVTT est une impl\u00E9mentation ind\u00E9pendante et n'est pas\n * affili\u00E9 \u00E0 Antre-Monde \u00C9ditions,\n * mais a \u00E9t\u00E9 r\u00E9alis\u00E9 avec l'autorisation d'Antre-Monde \u00C9ditions.\n *\n * @author LeRatierBretonnien\n * @copyright 2024\u20132026 LeRatierBretonnien\n * @license CC BY-NC-SA 4.0 \u2013 https://creativecommons.org/licenses/by-nc-sa/4.0/\n */\n\nexport default class SanheiDataModel extends foundry.abstract.TypeDataModel {\n static defineSchema() {\n const { fields } = foundry.data\n const stringField = (initial = \"\") => new fields.StringField({ required: true, nullable: false, initial })\n const htmlField = (initial = \"\") => new fields.HTMLField({ required: true, nullable: false, initial, textSearch: true })\n const intField = (initial = 0, opts = {}) => new fields.NumberField({ required: true, nullable: false, integer: true, initial, ...opts })\n\n const propertySchema = () => new fields.SchemaField({\n name: stringField(\"\"),\n heiCost: intField(0),\n heiType: stringField(\"yin\"),\n description: htmlField(\"\"),\n })\n\n return {\n reference: stringField(\"\"),\n description: htmlField(\"\"),\n heiType: stringField(\"yin\"),\n properties: new fields.SchemaField({\n prop1: propertySchema(),\n prop2: propertySchema(),\n prop3: propertySchema(),\n }),\n notes: htmlField(\"\"),\n }\n }\n}\n", "/**\n * Chroniques de l'\u00C9trange \u2014 Syst\u00E8me FoundryVTT\n *\n * Chroniques de l'\u00C9trange est un jeu de r\u00F4le \u00E9dit\u00E9 par Antre-Monde \u00C9ditions.\n * Ce syst\u00E8me FoundryVTT est une impl\u00E9mentation ind\u00E9pendante et n'est pas\n * affili\u00E9 \u00E0 Antre-Monde \u00C9ditions,\n * mais a \u00E9t\u00E9 r\u00E9alis\u00E9 avec l'autorisation d'Antre-Monde \u00C9ditions.\n *\n * @author LeRatierBretonnien\n * @copyright 2024\u20132026 LeRatierBretonnien\n * @license CC BY-NC-SA 4.0 \u2013 https://creativecommons.org/licenses/by-nc-sa/4.0/\n */\n\nexport default class IngredientDataModel extends foundry.abstract.TypeDataModel {\n static defineSchema() {\n const { fields } = foundry.data\n const stringField = (initial = \"\") => new fields.StringField({ required: true, nullable: false, initial })\n const htmlField = (initial = \"\") => new fields.HTMLField({ required: true, nullable: false, initial, textSearch: true })\n const intField = (initial = 0, opts = {}) => new fields.NumberField({ required: true, nullable: false, integer: true, initial, ...opts })\n\n return {\n reference: stringField(\"\"),\n description: htmlField(\"\"),\n school: stringField(\"all\"),\n obtainLevel: intField(0, { min: 0, max: 5 }),\n obtainDifficulty: intField(0, { min: 0, max: 3 }),\n quantity: intField(1, { min: 0 }),\n notes: htmlField(\"\"),\n }\n }\n}\n", "/**\n * Chroniques de l'\u00C9trange \u2014 Syst\u00E8me FoundryVTT\n *\n * Chroniques de l'\u00C9trange est un jeu de r\u00F4le \u00E9dit\u00E9 par Antre-Monde \u00C9ditions.\n * Ce syst\u00E8me FoundryVTT est une impl\u00E9mentation ind\u00E9pendante et n'est pas\n * affili\u00E9 \u00E0 Antre-Monde \u00C9ditions,\n * mais a \u00E9t\u00E9 r\u00E9alis\u00E9 avec l'autorisation d'Antre-Monde \u00C9ditions.\n *\n * @author LeRatierBretonnien\n * @copyright 2024\u20132026 LeRatierBretonnien\n * @license CC BY-NC-SA 4.0 \u2013 https://creativecommons.org/licenses/by-nc-sa/4.0/\n */\n\nexport class CDEMessage extends ChatMessage {\n async renderHTML({ canDelete, canClose = false, ...rest } = {}) {\n const html = await super.renderHTML({ canDelete, canClose, ...rest })\n this.#enrichChatCard(html)\n return html\n }\n\n #enrichChatCard(html) {\n const tokenDoc = (this.speaker.scene && this.speaker.token)\n ? game.scenes.get(this.speaker.scene)?.tokens.get(this.speaker.token)\n : null\n const actor = tokenDoc?.actor ?? game.actors.get(this.speaker.actor) ?? null\n\n const [img, nameText] = this.isContentVisible\n ? [actor?.img ?? this.author.avatar, this.alias]\n : [this.author.avatar, this.author.name]\n\n const avatarImg = Object.assign(document.createElement(\"img\"), { src: img, alt: nameText })\n const avatar = Object.assign(document.createElement(\"a\"), { className: \"avatar\" })\n if (actor) avatar.dataset.uuid = actor.uuid\n avatar.append(avatarImg)\n\n const title = Object.assign(document.createElement(\"span\"), { className: \"title\", textContent: nameText })\n const name = Object.assign(document.createElement(\"span\"), { className: \"name-stacked\" })\n name.append(title)\n\n html.querySelector(\".message-sender\")?.replaceChildren(avatar, name)\n }\n}\n", "/**\n * Chroniques de l'\u00C9trange \u2014 Syst\u00E8me FoundryVTT\n *\n * Chroniques de l'\u00C9trange est un jeu de r\u00F4le \u00E9dit\u00E9 par Antre-Monde \u00C9ditions.\n * Ce syst\u00E8me FoundryVTT est une impl\u00E9mentation ind\u00E9pendante et n'est pas\n * affili\u00E9 \u00E0 Antre-Monde \u00C9ditions,\n * mais a \u00E9t\u00E9 r\u00E9alis\u00E9 avec l'autorisation d'Antre-Monde \u00C9ditions.\n *\n * @author LeRatierBretonnien\n * @copyright 2024\u20132026 LeRatierBretonnien\n * @license CC BY-NC-SA 4.0 \u2013 https://creativecommons.org/licenses/by-nc-sa/4.0/\n */\n\nexport class CDEActor extends Actor {\n getRollData() {\n return this.toObject(false).system\n }\n}\n", "/**\n * Chroniques de l'\u00C9trange \u2014 Syst\u00E8me FoundryVTT\n *\n * Chroniques de l'\u00C9trange est un jeu de r\u00F4le \u00E9dit\u00E9 par Antre-Monde \u00C9ditions.\n * Ce syst\u00E8me FoundryVTT est une impl\u00E9mentation ind\u00E9pendante et n'est pas\n * affili\u00E9 \u00E0 Antre-Monde \u00C9ditions,\n * mais a \u00E9t\u00E9 r\u00E9alis\u00E9 avec l'autorisation d'Antre-Monde \u00C9ditions.\n *\n * @author LeRatierBretonnien\n * @copyright 2024\u20132026 LeRatierBretonnien\n * @license CC BY-NC-SA 4.0 \u2013 https://creativecommons.org/licenses/by-nc-sa/4.0/\n */\n\n/** Default icons per item type. */\nconst DEFAULT_ICONS = {\n kungfu: \"systems/fvtt-chroniques-de-l-etrange/images/icons/icon-kungfu.svg\",\n spell: \"systems/fvtt-chroniques-de-l-etrange/images/icons/icon-spell.svg\",\n supernatural: \"systems/fvtt-chroniques-de-l-etrange/images/icons/icon-supernatural.svg\",\n weapon: \"systems/fvtt-chroniques-de-l-etrange/images/icons/icon-weapon.svg\",\n armor: \"systems/fvtt-chroniques-de-l-etrange/images/icons/icon-armor.svg\",\n sanhei: \"systems/fvtt-chroniques-de-l-etrange/images/icons/icon-sanhei.svg\",\n ingredient: \"systems/fvtt-chroniques-de-l-etrange/images/icons/icon-ingredient.svg\",\n item: \"systems/fvtt-chroniques-de-l-etrange/images/icons/icon-item.svg\",\n};\n\nexport class CDEItem extends Item {\n\n /** @override */\n async _preCreate(data, options, userId) {\n await super._preCreate(data, options, userId);\n const defaultIcon = DEFAULT_ICONS[this.type];\n if (defaultIcon && (!data.img || data.img === Item.DEFAULT_ICON)) {\n this.updateSource({ img: defaultIcon });\n }\n }\n}\n", "/**\n * Chroniques de l'\u00C9trange \u2014 Syst\u00E8me FoundryVTT\n *\n * Chroniques de l'\u00C9trange est un jeu de r\u00F4le \u00E9dit\u00E9 par Antre-Monde \u00C9ditions.\n * Ce syst\u00E8me FoundryVTT est une impl\u00E9mentation ind\u00E9pendante et n'est pas\n * affili\u00E9 \u00E0 Antre-Monde \u00C9ditions,\n * mais a \u00E9t\u00E9 r\u00E9alis\u00E9 avec l'autorisation d'Antre-Monde \u00C9ditions.\n *\n * @author LeRatierBretonnien\n * @copyright 2024\u20132026 LeRatierBretonnien\n * @license CC BY-NC-SA 4.0 \u2013 https://creativecommons.org/licenses/by-nc-sa/4.0/\n */\n\nconst DIGIT_LABELS = [\n \"systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/digit/d10-1.webp\",\n \"systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/digit/d10-2.webp\",\n \"systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/digit/d10-3.webp\",\n \"systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/digit/d10-4.webp\",\n \"systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/digit/d10-5.webp\",\n \"systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/digit/d10-6.webp\",\n \"systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/digit/d10-7.webp\",\n \"systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/digit/d10-8.webp\",\n \"systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/digit/d10-9.webp\",\n \"systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/digit/d10-10.webp\",\n]\n\nconst CLASSIC_LABELS = [\n \"systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/d10-1.webp\",\n \"systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/d10-2.webp\",\n \"systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/d10-3.webp\",\n \"systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/d10-4.webp\",\n \"systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/d10-5.webp\",\n \"systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/d10-6.webp\",\n \"systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/d10-7.webp\",\n \"systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/d10-8.webp\",\n \"systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/d10-9.webp\",\n \"systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/d10-10.webp\",\n]\n\nexport function registerDice() {\n Hooks.once(\"diceSoNiceReady\", (dice3d) => {\n dice3d.addColorset(\n {\n name: \"cde\",\n description: \"CdE\",\n foreground: \"#000000\",\n background: \"#ffffff\",\n edge: \"#ffffff\",\n font: \"DeliusUnicase\",\n texture: \"ice\",\n material: \"plastic\",\n },\n \"preferred\",\n )\n\n dice3d.addSystem({ id: \"fvtt-chroniques-de-l-etrangedigit\", name: \"Chroniques de l'\u00E9trange digits\" }, \"preferred\")\n dice3d.addDicePreset({ type: \"d10\", labels: DIGIT_LABELS, system: \"fvtt-chroniques-de-l-etrangedigit\" })\n\n dice3d.addSystem({ id: \"fvtt-chroniques-de-l-etrange\", name: \"Chroniques de l'\u00E9trange\" }, \"preferred\")\n dice3d.addDicePreset({ type: \"d10\", labels: CLASSIC_LABELS, system: \"fvtt-chroniques-de-l-etrange\" })\n })\n}\n", "/**\n * Chroniques de l'\u00C9trange \u2014 Syst\u00E8me FoundryVTT\n *\n * Chroniques de l'\u00C9trange est un jeu de r\u00F4le \u00E9dit\u00E9 par Antre-Monde \u00C9ditions.\n * Ce syst\u00E8me FoundryVTT est une impl\u00E9mentation ind\u00E9pendante et n'est pas\n * affili\u00E9 \u00E0 Antre-Monde \u00C9ditions,\n * mais a \u00E9t\u00E9 r\u00E9alis\u00E9 avec l'autorisation d'Antre-Monde \u00C9ditions.\n *\n * @author LeRatierBretonnien\n * @copyright 2024\u20132026 LeRatierBretonnien\n * @license CC BY-NC-SA 4.0 \u2013 https://creativecommons.org/licenses/by-nc-sa/4.0/\n */\n\nimport { MAGICS } from \"../config/constants.js\"\n\nexport function registerHandlebarsHelpers() {\n const { Handlebars } = globalThis\n if (!Handlebars) return\n\n Handlebars.registerHelper(\"getMagicLabel\", function (magic) {\n return game.i18n.localize(MAGICS[magic]?.label ?? \"\")\n })\n\n Handlebars.registerHelper(\"getMagicAspectLabel\", function (magic) {\n return game.i18n.localize(MAGICS[magic]?.aspectlabel ?? \"\")\n })\n\n Handlebars.registerHelper(\"getMagicSpecialityLabel\", function (magic, speciality) {\n return game.i18n.localize(MAGICS[magic]?.speciality?.[speciality]?.label ?? \"\")\n })\n\n Handlebars.registerHelper(\"getMagicSpecialityClassIcon\", function (magic, speciality) {\n return MAGICS[magic]?.speciality?.[speciality]?.classicon ?? \"\"\n })\n\n Handlebars.registerHelper(\"getMagicSpecialityElementIcon\", function (magic, speciality) {\n return MAGICS[magic]?.speciality?.[speciality]?.elementicon ?? \"\"\n })\n\n Handlebars.registerHelper(\"getMagicSpecialityLabelIcon\", function (magic, speciality) {\n return MAGICS[magic]?.speciality?.[speciality]?.labelicon ?? \"\"\n })\n\n Handlebars.registerHelper(\"getMagicSpecialityLabelElement\", function (magic, speciality) {\n return game.i18n.localize(MAGICS[magic]?.speciality?.[speciality]?.labelelement ?? \"\")\n })\n\n Handlebars.registerHelper(\"getMagicAspectIcon\", function (magic) {\n const icons = {\n internalcinnabar: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_metal.webp\",\n alchemy: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_eau.webp\",\n masteryoftheway: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_terre.webp\",\n exorcism: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_feu.webp\",\n geomancy: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_bois.webp\",\n }\n return icons[magic] ?? \"\"\n })\n\n Handlebars.registerHelper(\"getElementIcon\", function (aspect) {\n const icons = {\n metal: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_metal.webp\",\n water: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_eau.webp\",\n earth: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_terre.webp\",\n fire: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_feu.webp\",\n wood: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_bois.webp\",\n // legacy French keys\n eau: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_eau.webp\",\n terre: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_terre.webp\",\n feu: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_feu.webp\",\n bois: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_bois.webp\",\n }\n return icons[aspect] ?? \"\"\n })\n\n Handlebars.registerHelper(\"getOrientationIcon\", function (orientation) {\n const icons = {\n yin: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yin.webp\",\n yang: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.webp\",\n yinyang: \"/systems/fvtt-chroniques-de-l-etrange/images/yin_yang.webp\",\n }\n return icons[orientation] ?? \"\"\n })\n\n Handlebars.registerHelper(\"getOrientationLabel\", function (orientation) {\n const keys = {\n yin: \"CDE.OrientationYin\",\n yang: \"CDE.OrientationYang\",\n yinyang: \"CDE.OrientationYinYang\",\n }\n return game.i18n.localize(keys[orientation] ?? \"CDE.Orientation\")\n })\n\n Handlebars.registerHelper(\"getActivationLabel\", function (activation) {\n const keys = {\n \"action-attack\": \"CDE.ActivationAttack\",\n \"action-defense\": \"CDE.ActivationDefense\",\n \"action-aid\": \"CDE.ActivationAid\",\n \"action-attack-defense\": \"CDE.ActivationAttackOrDefense\",\n reaction: \"CDE.ActivationReaction\",\n dice: \"CDE.ActivationDice\",\n \"damage-inflicted\": \"CDE.ActivationDamageInflicted\",\n \"damage-received\": \"CDE.ActivationDamageReceived\",\n }\n return game.i18n.localize(keys[activation] ?? \"CDE.Activation\")\n })\n\n /**\n * Compute the SVG x,y coordinates for a cran on the initiative wheel.\n * Cran 1\u201324 are arranged counter-clockwise from the bottom (reference at 6 o'clock).\n * angle = 90\u00B0 + cran * 15\u00B0 (counter-clockwise = positive in standard math, negative in SVG).\n * In SVG coords: x = cx + r*cos(a), y = cy - r*sin(a) [y-axis is flipped in SVG].\n */\n Handlebars.registerHelper(\"cranPosition\", function (cran, cx, cy, r) {\n const angleDeg = 90 + cran * 15 // counter-clockwise from bottom\n const angleRad = (angleDeg * Math.PI) / 180\n const x = Math.round(cx + r * Math.cos(angleRad))\n const y = Math.round(cy - r * Math.sin(angleRad))\n return { x, y }\n })\n\n /** X offset for overlapping fighters on the same cran. Centres a 30px image on the cran cx. */\n Handlebars.registerHelper(\"fighterX\", function (cx, index, total) {\n const offset = total > 1 ? (index - (total - 1) / 2) * 34 : 0\n return Math.round(cx - 15 + offset)\n })\n\n /** Y offset for fighters \u2014 positions image just above the cran circle. */\n Handlebars.registerHelper(\"fighterY\", function (cy, index, total) {\n return Math.round(cy - 50)\n })\n}\n", "/**\n * Chroniques de l'\u00C9trange \u2014 Syst\u00E8me FoundryVTT\n *\n * Chroniques de l'\u00C9trange est un jeu de r\u00F4le \u00E9dit\u00E9 par Antre-Monde \u00C9ditions.\n * Ce syst\u00E8me FoundryVTT est une impl\u00E9mentation ind\u00E9pendante et n'est pas\n * affili\u00E9 \u00E0 Antre-Monde \u00C9ditions,\n * mais a \u00E9t\u00E9 r\u00E9alis\u00E9 avec l'autorisation d'Antre-Monde \u00C9ditions.\n *\n * @author LeRatierBretonnien\n * @copyright 2024\u20132026 LeRatierBretonnien\n * @license CC BY-NC-SA 4.0 \u2013 https://creativecommons.org/licenses/by-nc-sa/4.0/\n */\n\nimport { TEMPLATE_PARTIALS } from \"../config/constants.js\"\n\nexport async function preloadPartials() {\n return foundry.applications.handlebars.loadTemplates(TEMPLATE_PARTIALS)\n}\n", "/**\n * Chroniques de l'\u00C9trange \u2014 Syst\u00E8me FoundryVTT\n *\n * Chroniques de l'\u00C9trange est un jeu de r\u00F4le \u00E9dit\u00E9 par Antre-Monde \u00C9ditions.\n * Ce syst\u00E8me FoundryVTT est une impl\u00E9mentation ind\u00E9pendante et n'est pas\n * affili\u00E9 \u00E0 Antre-Monde \u00C9ditions,\n * mais a \u00E9t\u00E9 r\u00E9alis\u00E9 avec l'autorisation d'Antre-Monde \u00C9ditions.\n *\n * @author LeRatierBretonnien\n * @copyright 2024\u20132026 LeRatierBretonnien\n * @license CC BY-NC-SA 4.0 \u2013 https://creativecommons.org/licenses/by-nc-sa/4.0/\n */\n\n/**\n * Initiative determination system for Chroniques de l'\u00C9trange.\n *\n * PJ formula: Initiative = Prouesse + Premi\u00E8re action (comp\u00E9tence/ressource/magie)\n * PNJ formula: Initiative = Aptitude physique + Premi\u00E8re action (aptitude)\n *\n * Range 1-24 ; anti-initiative = 25 \u2212 initiative.\n * Combat order is ascending (low initiative acts first).\n */\n\nconst PC_PROMPT_TEMPLATE = \"systems/fvtt-chroniques-de-l-etrange/templates/form/cde-initiative-prompt.html\"\nconst NPC_PROMPT_TEMPLATE = \"systems/fvtt-chroniques-de-l-etrange/templates/form/cde-initiative-prompt-npc.html\"\nconst RESULT_TEMPLATE = \"systems/fvtt-chroniques-de-l-etrange/templates/form/cde-initiative-result.html\"\n\n/** Skills, resources and magics available as \"premi\u00E8re action\" for a PC. */\nfunction buildPCOptions(sys) {\n const sk = sys.skills ?? {}\n const rs = sys.resources ?? {}\n const mg = sys.magics ?? {}\n return [\n { key: \"art\", label: game.i18n.localize(\"CDE.Art\"), value: sk.art?.value ?? 0 },\n { key: \"investigation\", label: game.i18n.localize(\"CDE.Investigation\"), value: sk.investigation?.value ?? 0 },\n { key: \"erudition\", label: game.i18n.localize(\"CDE.Erudition\"), value: sk.erudition?.value ?? 0 },\n { key: \"knavery\", label: game.i18n.localize(\"CDE.Knavery\"), value: sk.knavery?.value ?? 0 },\n { key: \"wordliness\", label: game.i18n.localize(\"CDE.Wordliness\"), value: sk.wordliness?.value ?? 0 },\n { key: \"prowess\", label: game.i18n.localize(\"CDE.Prowess\"), value: sk.prowess?.value ?? 0 },\n { key: \"sciences\", label: game.i18n.localize(\"CDE.Sciences\"), value: sk.sciences?.value ?? 0 },\n { key: \"technologies\", label: game.i18n.localize(\"CDE.Technologies\"), value: sk.technologies?.value ?? 0 },\n { key: \"kungfu\", label: game.i18n.localize(\"CDE.KungFu\"), value: sk.kungfu?.value ?? 0 },\n { key: \"rangedcombat\", label: game.i18n.localize(\"CDE.RangedCombat\"), value: sk.rangedcombat?.value ?? 0 },\n { key: \"supply\", label: game.i18n.localize(\"CDE.Supply\"), value: rs.supply?.value ?? 0 },\n { key: \"inquiry\", label: game.i18n.localize(\"CDE.Inquiry\"), value: rs.inquiry?.value ?? 0 },\n { key: \"influence\", label: game.i18n.localize(\"CDE.Influence\"), value: rs.influence?.value ?? 0 },\n { key: \"internalcinnabar\", label: game.i18n.localize(\"CDE.InternalCinnabar\"), value: mg.internalcinnabar?.value ?? 0 },\n { key: \"alchemy\", label: game.i18n.localize(\"CDE.Alchemy\"), value: mg.alchemy?.value ?? 0 },\n { key: \"masteryoftheway\", label: game.i18n.localize(\"CDE.MasteryOfTheWay\"), value: mg.masteryoftheway?.value ?? 0 },\n { key: \"exorcism\", label: game.i18n.localize(\"CDE.Exorcism\"), value: mg.exorcism?.value ?? 0 },\n { key: \"geomancy\", label: game.i18n.localize(\"CDE.Geomancy\"), value: mg.geomancy?.value ?? 0 },\n ]\n}\n\n/** Aptitudes available as \"premi\u00E8re action\" for an NPC. */\nfunction buildNPCOptions(sys) {\n const ap = sys.aptitudes ?? {}\n return [\n { key: \"physical\", label: game.i18n.localize(\"CDE.Physical\"), value: ap.physical?.value ?? 0 },\n { key: \"martial\", label: game.i18n.localize(\"CDE.Martial\"), value: ap.martial?.value ?? 0 },\n { key: \"mental\", label: game.i18n.localize(\"CDE.Mental\"), value: ap.mental?.value ?? 0 },\n { key: \"social\", label: game.i18n.localize(\"CDE.Social\"), value: ap.social?.value ?? 0 },\n { key: \"spiritual\", label: game.i18n.localize(\"CDE.Spiritual\"), value: ap.spiritual?.value ?? 0 },\n ]\n}\n\n/** Parse the dialog element and extract firstaction + modifier. */\nfunction readInitFields(dialog) {\n const root = dialog.element ?? dialog\n const selectedKey = root.querySelector(\"select[name='firstaction']\")?.value ?? \"\"\n const modifier = parseInt(root.querySelector(\"input[name='modifier']\")?.value ?? \"0\", 10) || 0\n return { selectedKey, modifier }\n}\n\n/** Post a styled initiative chat message. */\nasync function sendInitChatMessage({ actor, baseName, baseValue, actionName, actionValue, modifier, initiative, antiInitiative }) {\n const html = await foundry.applications.handlebars.renderTemplate(RESULT_TEMPLATE, {\n actorName: actor.name,\n actorImg: actor.img,\n baseName,\n baseValue,\n actionName,\n actionValue,\n modifier,\n hasModifier: modifier !== 0,\n initiative,\n antiInitiative,\n })\n await ChatMessage.create({\n user: game.user.id,\n speaker: ChatMessage.getSpeaker({ actor }),\n content: html,\n })\n}\n\n/**\n * Open the PC initiative dialog, compute initiative (Prouesse + Premi\u00E8re action + modificateur)\n * and update the actor, then post a chat card.\n */\nexport async function rollInitiativePC(actor) {\n const sys = actor.system\n const prowess = sys.skills?.prowess?.value ?? 0\n const options = buildPCOptions(sys)\n const baseName = game.i18n.localize(\"CDE.Prowess\")\n\n const content = await foundry.applications.handlebars.renderTemplate(PC_PROMPT_TEMPLATE, {\n prowessValue: prowess,\n options,\n modifier: 0,\n })\n\n const result = await foundry.applications.api.DialogV2.prompt({\n window: { title: game.i18n.localize(\"CDE.InitiativeRoll\") },\n content,\n rejectClose: false,\n ok: {\n label: game.i18n.localize(\"CDE.Validate\"),\n callback: (_ev, _btn, dialog) => readInitFields(dialog),\n },\n })\n if (!result) return\n\n const { selectedKey, modifier } = result\n const selected = options.find((o) => o.key === selectedKey) ?? options[0]\n const rawValue = prowess + selected.value + modifier\n const initiative = Math.max(1, Math.min(24, rawValue))\n const antiInit = 25 - initiative\n\n await actor.update({ \"system.initiative\": initiative })\n await sendInitChatMessage({\n actor,\n baseName,\n baseValue: prowess,\n actionName: selected.label,\n actionValue: selected.value,\n modifier,\n initiative,\n antiInitiative: antiInit,\n })\n}\n\n/**\n * Open the NPC initiative dialog, compute initiative (Aptitude physique + Premi\u00E8re action + modificateur)\n * and update the actor, then post a chat card.\n */\nexport async function rollInitiativeNPC(actor) {\n const sys = actor.system\n const physical = sys.aptitudes?.physical?.value ?? 0\n const options = buildNPCOptions(sys)\n const baseName = game.i18n.localize(\"CDE.Physical\")\n\n const content = await foundry.applications.handlebars.renderTemplate(NPC_PROMPT_TEMPLATE, {\n physicalValue: physical,\n options,\n modifier: 0,\n })\n\n const result = await foundry.applications.api.DialogV2.prompt({\n window: { title: game.i18n.localize(\"CDE.InitiativeRoll\") },\n content,\n rejectClose: false,\n ok: {\n label: game.i18n.localize(\"CDE.Validate\"),\n callback: (_ev, _btn, dialog) => readInitFields(dialog),\n },\n })\n if (!result) return\n\n const { selectedKey, modifier } = result\n const selected = options.find((o) => o.key === selectedKey) ?? options[0]\n const rawValue = physical + selected.value + modifier\n const initiative = Math.max(1, Math.min(24, rawValue))\n const antiInit = 25 - initiative\n\n await actor.update({ \"system.initiative\": initiative })\n await sendInitChatMessage({\n actor,\n baseName,\n baseValue: physical,\n actionName: selected.label,\n actionValue: selected.value,\n modifier,\n initiative,\n antiInitiative: antiInit,\n })\n}\n", "/**\n * Chroniques de l'\u00C9trange \u2014 Syst\u00E8me FoundryVTT\n *\n * Chroniques de l'\u00C9trange est un jeu de r\u00F4le \u00E9dit\u00E9 par Antre-Monde \u00C9ditions.\n * Ce syst\u00E8me FoundryVTT est une impl\u00E9mentation ind\u00E9pendante et n'est pas\n * affili\u00E9 \u00E0 Antre-Monde \u00C9ditions,\n * mais a \u00E9t\u00E9 r\u00E9alis\u00E9 avec l'autorisation d'Antre-Monde \u00C9ditions.\n *\n * @author LeRatierBretonnien\n * @copyright 2024\u20132026 LeRatierBretonnien\n * @license CC BY-NC-SA 4.0 \u2013 https://creativecommons.org/licenses/by-nc-sa/4.0/\n */\n\n/**\n * Loksyu / TinJi settings-based helpers.\n *\n * Data is stored as world settings instead of singleton Actor documents.\n */\n\nimport { SYSTEM_ID, WU_XING_CYCLE, ASPECT_FACES } from \"../../config/constants.js\"\n\n/** Read the current loksyu data object from world settings */\nexport function getLoksyuData() {\n return game.settings.get(SYSTEM_ID, \"loksyuData\") ?? {\n wood: {yin:0,yang:0}, fire: {yin:0,yang:0}, earth: {yin:0,yang:0}, metal: {yin:0,yang:0}, water: {yin:0,yang:0},\n }\n}\n\n/** Write the loksyu data object to world settings */\nexport async function setLoksyuData(data) {\n await game.settings.set(SYSTEM_ID, \"loksyuData\", data)\n Hooks.callAll(\"cde:loksyuUpdated\", data)\n}\n\n/** Read current TinJi value from world settings */\nexport function getTinjiValue() {\n return game.settings.get(SYSTEM_ID, \"tinjiData\") ?? 0\n}\n\n/** Write TinJi value to world settings */\nexport async function setTinjiValue(value) {\n await game.settings.set(SYSTEM_ID, \"tinjiData\", Math.max(0, value))\n Hooks.callAll(\"cde:tinjiUpdated\", Math.max(0, value))\n}\n\n/**\n * After a WuXing roll, add the loksyu faces (yin + yang) of the relevant\n * aspect to the loksyu settings data.\n *\n * @param {string} activeAspect - e.g. \"fire\"\n * @param {Object} faces - Die face counts { 0: n, 1: n, \u2026, 9: n }\n */\nexport async function updateLoksyuFromRoll(activeAspect, faces) {\n const cycle = WU_XING_CYCLE[activeAspect]\n if (!cycle) return\n\n const lokAspect = cycle[3]\n const [yinFace, yangFace] = ASPECT_FACES[lokAspect] ?? []\n if (yinFace === undefined) return\n\n const yinCount = faces[yinFace] ?? 0\n const yangCount = faces[yangFace] ?? 0\n if (yinCount === 0 && yangCount === 0) return\n\n const data = getLoksyuData()\n const current = data[lokAspect] ?? { yin: 0, yang: 0 }\n data[lokAspect] = {\n yin: (current.yin ?? 0) + yinCount,\n yang: (current.yang ?? 0) + yangCount,\n }\n await setLoksyuData(data)\n}\n\n/**\n * After a WuXing roll, add tinji faces to the TinJi settings.\n *\n * @param {number} count - Number of tinji faces rolled\n */\nexport async function updateTinjiFromRoll(count) {\n if (!count || count <= 0) return\n const current = getTinjiValue()\n await setTinjiValue(current + count)\n}\n", "/**\n * Chroniques de l'\u00C9trange \u2014 Syst\u00E8me FoundryVTT\n *\n * Chroniques de l'\u00C9trange est un jeu de r\u00F4le \u00E9dit\u00E9 par Antre-Monde \u00C9ditions.\n * Ce syst\u00E8me FoundryVTT est une impl\u00E9mentation ind\u00E9pendante et n'est pas\n * affili\u00E9 \u00E0 Antre-Monde \u00C9ditions,\n * mais a \u00E9t\u00E9 r\u00E9alis\u00E9 avec l'autorisation d'Antre-Monde \u00C9ditions.\n *\n * @author LeRatierBretonnien\n * @copyright 2024\u20132026 LeRatierBretonnien\n * @license CC BY-NC-SA 4.0 \u2013 https://creativecommons.org/licenses/by-nc-sa/4.0/\n */\n\n/**\n * Wu Xing rolling system for Chroniques de l'\u00C9trange.\n *\n * The Wu Xing cycle maps each aspect (by index 0-4) to die face groups:\n * - metal=0 : faces 3,8\n * - water=1 : faces 1,6\n * - earth=2 : faces 0/10,5\n * - fire=3 : faces 2,7\n * - wood=4 : faces 4,9\n *\n * For a given active aspect the five result categories are:\n * successes / auspicious / noxious / loksyu (yin face, yang face) / tinji\n * Each category is associated with one of the five aspects in Wu Xing cycle order.\n */\n\nimport { MAGICS, ASPECT_LABELS, ASPECT_ICONS, ASPECT_FACES, ASPECT_NAMES, WU_XING_CYCLE } from \"../config/constants.js\"\nimport { updateLoksyuFromRoll, updateTinjiFromRoll } from \"./apps/singletons.js\"\n\nconst RESULT_TEMPLATE = \"systems/fvtt-chroniques-de-l-etrange/templates/form/cde-dice-result.html\"\nconst SKILL_PROMPT_TEMPLATE = \"systems/fvtt-chroniques-de-l-etrange/templates/form/cde-skill-dice-prompt.html\"\nconst SKILL_SPECIAL_PROMPT_TEMPLATE = \"systems/fvtt-chroniques-de-l-etrange/templates/form/cde-skill-special-dice-prompt.html\"\nconst MAGIC_PROMPT_TEMPLATE = \"systems/fvtt-chroniques-de-l-etrange/templates/form/cde-magic-dice-prompt.html\"\nconst WEAPON_PROMPT_TEMPLATE = \"systems/fvtt-chroniques-de-l-etrange/templates/form/cde-weapon-dice-prompt.html\"\n\n/** Maps i18n element label \u2192 aspect name (for speciality default aspect lookup) */\nconst LABELELEMENT_TO_ASPECT = {\n \"CDE.Metal\": \"metal\",\n \"CDE.Water\": \"water\",\n \"CDE.Earth\": \"earth\",\n \"CDE.Fire\": \"fire\",\n \"CDE.Wood\": \"wood\",\n}\n\n/** Maps weapon range string \u2192 dice malus applied to the attack pool */\nconst RANGE_MALUS = {\n contact: 0,\n courte: 0,\n mediane: -1,\n longue: -2,\n extreme: -3,\n}\n\n/** Maps weapon type string \u2192 default skill key */\nconst WEAPON_TYPE_SKILL = {\n melee: \"kungfu\",\n thrown: \"rangedcombat\",\n ranged: \"rangedcombat\",\n firearm: \"rangedcombat\",\n}\n\n/** Maps weapon damageAspect name \u2192 ASPECT_NAMES index */\nconst WEAPON_ASPECT_INDEX = { metal: 0, eau: 1, water: 1, terre: 2, earth: 2, feu: 3, fire: 3, bois: 4, wood: 4 }\n\n/** Count how many times each die face appeared in the roll results */\nfunction countFaces(rollResults) {\n const counts = { 1:0, 2:0, 3:0, 4:0, 5:0, 6:0, 7:0, 8:0, 9:0, 0:0 }\n for (const die of rollResults) {\n const face = die.result === 10 ? 0 : die.result\n counts[face]++\n }\n return counts\n}\n\n/**\n * Compute Wu Xing result categories from face counts and active aspect.\n * Returns { successesdice, auspiciousdice, noxiousdice, loksyudice, tinjidice, loksyurepartition }\n */\nfunction computeWuXingResults(faces, aspectName, bonusAuspicious = 0) {\n const cycle = WU_XING_CYCLE[aspectName]\n if (!cycle) return null\n\n const [succAspect, ausAspect, noxAspect, lokAspect, tinAspect] = cycle\n const [succYin, succYang] = ASPECT_FACES[succAspect]\n const [ausYin, ausYang] = ASPECT_FACES[ausAspect]\n const [noxYin, noxYang] = ASPECT_FACES[noxAspect]\n const [lokYin, lokYang] = ASPECT_FACES[lokAspect]\n const [tinYin, tinYang] = ASPECT_FACES[tinAspect]\n\n const yin = game.i18n.localize(\"CDE.Yin\")\n const yang = game.i18n.localize(\"CDE.Yang\")\n\n return {\n successesdice: faces[succYin] + faces[succYang],\n auspiciousdice: faces[ausYin] + faces[ausYang] + bonusAuspicious,\n noxiousdice: faces[noxYin] + faces[noxYang],\n loksyudice: faces[lokYin] + faces[lokYang],\n loksyurepartition: `[${yin}(${faces[lokYin]}) ${yang}(${faces[lokYang]})]`,\n tinjidice: faces[tinYin] + faces[tinYang],\n }\n}\n\n/** Read a named field from a dialog DOM element */\nfunction readField(dlg, name) {\n const el = dlg.querySelector(`[name=\"${name}\"]`)\n if (!el) return null\n return el.type === \"checkbox\" ? el.checked : el.value\n}\n\n/**\n * Open a DialogV2.prompt with the given template + data and return the resolved form values.\n * The callback receives the DialogV2 application instance; fields are read from its .element.\n * @returns {Promise
|null>}\n */\nasync function showRollPrompt({ title, template, data, fields }) {\n const content = await foundry.applications.handlebars.renderTemplate(template, data)\n return foundry.applications.api.DialogV2.prompt({\n window: { title },\n content,\n rejectClose: false,\n ok: {\n label: game.i18n.localize(\"CDE.Validate\"),\n callback: (event, button, dialog) => {\n // In AppV2, dialog is the application instance; .element is the root HTMLElement\n const root = dialog.element ?? dialog\n const result = {}\n for (const field of fields) {\n result[field] = readField(root, field)\n }\n return result\n },\n },\n })\n}\n\n/**\n * Open the skill roll prompt and return the user-confirmed parameters.\n * @param {object} params - Initial values\n * @returns {Promise