diff --git a/LICENSE.txt b/LICENSE.txt
index 1648bd0..a037f98 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -12,7 +12,7 @@ Vous pouvez retrouver le nom de leurs auteurs respectifs sur ce site.
version 1.2 ou toute version ultérieure publiée par la Free Software Foundation,
https://commons.wikimedia.org/wiki/File:Hong_Kong_18_Districts_Blank_Map.svg
- 'loksyu_roue_d_initiative.jpg' est une création originale de 'Darkwin'.
-- L'organisation des images provient du système produit par MysteryMan
+- L'organisation du reste des images provient du système produit par MysteryMan (merci à lui)
Code Author :
- Developed by LeRatierBretonnien
diff --git a/dist/system.js b/dist/system.js
new file mode 100644
index 0000000..e9c739d
--- /dev/null
+++ b/dist/system.js
@@ -0,0 +1,2450 @@
+// src/config/constants.js
+var SYSTEM_ID = "fvtt-chroniques-de-l-etrange";
+var ACTOR_TYPES = {
+ character: "character",
+ npc: "npc"
+};
+var ITEM_TYPES = {
+ item: "item",
+ kungfu: "kungfu",
+ spell: "spell",
+ supernatural: "supernatural",
+ weapon: "weapon",
+ armor: "armor",
+ sanhei: "sanhei",
+ ingredient: "ingredient"
+};
+var SUBTYPES = {
+ weapon: { id: "weapon", label: "CDE.Weapon" },
+ armor: { id: "armor", label: "CDE.Armor" },
+ sanhei: { id: "sanhei", label: "CDE.Sanhei" },
+ other: { id: "other", label: "CDE.Other" }
+};
+var MAGICS = {
+ internalcinnabar: {
+ id: "internalcinnabar",
+ background: "linear-grey",
+ label: "CDE.InternalCinnabar",
+ aspectlabel: "CDE.Metal",
+ speciality: {
+ 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" },
+ 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" },
+ 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" },
+ 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" },
+ 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" }
+ }
+ },
+ alchemy: {
+ id: "alchemy",
+ background: "linear-blue",
+ label: "CDE.Alchemy",
+ aspectlabel: "CDE.Water",
+ speciality: {
+ 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" },
+ 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" },
+ 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" },
+ 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" },
+ 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" }
+ }
+ },
+ masteryoftheway: {
+ id: "masteryoftheway",
+ background: "linear-brown",
+ label: "CDE.MasteryOfTheWay",
+ aspectlabel: "CDE.Earth",
+ speciality: {
+ 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" },
+ 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" },
+ 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" },
+ 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" },
+ 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" }
+ }
+ },
+ exorcism: {
+ id: "exorcism",
+ background: "linear-red",
+ label: "CDE.Exorcism",
+ aspectlabel: "CDE.Fire",
+ speciality: {
+ 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" },
+ 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" },
+ 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" },
+ 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" },
+ 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" }
+ }
+ },
+ geomancy: {
+ id: "geomancy",
+ background: "linear-green",
+ label: "CDE.Geomancy",
+ aspectlabel: "CDE.Wood",
+ speciality: {
+ 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" },
+ 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" },
+ 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" },
+ 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" },
+ 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" }
+ }
+ }
+};
+var ASPECT_LABELS = {
+ metal: "CDE.Metal",
+ water: "CDE.Water",
+ earth: "CDE.Earth",
+ fire: "CDE.Fire",
+ wood: "CDE.Wood"
+};
+var ASPECT_ICONS = {
+ metal: "systems/fvtt-chroniques-de-l-etrange/images/cde_metal.webp",
+ water: "systems/fvtt-chroniques-de-l-etrange/images/cde_eau.webp",
+ earth: "systems/fvtt-chroniques-de-l-etrange/images/cde_terre.webp",
+ fire: "systems/fvtt-chroniques-de-l-etrange/images/cde_feu.webp",
+ wood: "systems/fvtt-chroniques-de-l-etrange/images/cde_bois.webp"
+};
+var ASPECT_FACES = {
+ metal: [3, 8],
+ water: [1, 6],
+ earth: [0, 5],
+ // 0 = face "10"
+ fire: [2, 7],
+ wood: [4, 9]
+};
+var ASPECT_NAMES = ["metal", "water", "earth", "fire", "wood"];
+var WU_XING_CYCLE = {
+ wood: ["wood", "fire", "water", "earth", "metal"],
+ fire: ["fire", "earth", "wood", "metal", "water"],
+ earth: ["earth", "metal", "fire", "water", "wood"],
+ metal: ["metal", "water", "earth", "wood", "fire"],
+ water: ["water", "wood", "metal", "fire", "earth"]
+};
+var TEMPLATE_PARTIALS = [
+ "systems/fvtt-chroniques-de-l-etrange/templates/actor/parts/cde-character-skills.html",
+ "systems/fvtt-chroniques-de-l-etrange/templates/actor/parts/cde-character-magics.html",
+ "systems/fvtt-chroniques-de-l-etrange/templates/actor/parts/cde-character-nghang.html",
+ "systems/fvtt-chroniques-de-l-etrange/templates/actor/parts/cde-character-treasures.html",
+ "systems/fvtt-chroniques-de-l-etrange/templates/actor/parts/cde-character-items.html",
+ "systems/fvtt-chroniques-de-l-etrange/templates/actor/parts/cde-character-kungfus.html",
+ "systems/fvtt-chroniques-de-l-etrange/templates/actor/parts/cde-character-spells.html",
+ "systems/fvtt-chroniques-de-l-etrange/templates/actor/parts/cde-npc-supernaturals.html",
+ "systems/fvtt-chroniques-de-l-etrange/templates/actor/parts/cde-npc-spells.html",
+ "systems/fvtt-chroniques-de-l-etrange/templates/actor/parts/cde-npc-kungfus.html",
+ "systems/fvtt-chroniques-de-l-etrange/templates/actor/parts/cde-npc-items.html",
+ "systems/fvtt-chroniques-de-l-etrange/templates/apps/cde-loksyu-app.html",
+ "systems/fvtt-chroniques-de-l-etrange/templates/apps/cde-tinji-app.html"
+];
+
+// src/config/settings.js
+function registerSettings() {
+ game.settings.register(SYSTEM_ID, "loksyuData", {
+ scope: "world",
+ config: false,
+ type: Object,
+ default: {
+ 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 }
+ }
+ });
+ game.settings.register(SYSTEM_ID, "tinjiData", {
+ scope: "world",
+ config: false,
+ type: Number,
+ default: 0
+ });
+}
+async function migrateIfNeeded() {
+}
+
+// src/config/localize.js
+function preLocalizeConfig() {
+ const localizeConfigObject = (obj, keys) => {
+ for (const o of Object.values(obj)) {
+ for (const key of keys) {
+ o[key] = game.i18n.localize(o[key]);
+ }
+ }
+ };
+ localizeConfigObject(SUBTYPES, ["label"]);
+ Object.values(MAGICS).forEach((magic) => {
+ magic.label = game.i18n.localize(magic.label);
+ magic.aspectlabel = game.i18n.localize(magic.aspectlabel);
+ Object.values(magic.speciality).forEach((spec) => {
+ spec.label = game.i18n.localize(spec.label);
+ spec.labelelement = game.i18n.localize(spec.labelelement);
+ });
+ });
+}
+
+// src/config/runtime.js
+function configureRuntime() {
+ CONFIG.Actor.compendiumBanner = "/systems/fvtt-chroniques-de-l-etrange/images/banners/actor-banner.webp";
+ CONFIG.Adventure.compendiumBanner = "/systems/fvtt-chroniques-de-l-etrange/images/banners/adventure-banner.webp";
+ CONFIG.Cards.compendiumBanner = "ui/banners/cards-banner.webp";
+ CONFIG.Item.compendiumBanner = "/systems/fvtt-chroniques-de-l-etrange/images/banners/item-banner.webp";
+ CONFIG.JournalEntry.compendiumBanner = "/systems/fvtt-chroniques-de-l-etrange/images/banners/journalentry-banner.webp";
+ CONFIG.Macro.compendiumBanner = "ui/banners/macro-banner.webp";
+ CONFIG.Playlist.compendiumBanner = "ui/banners/playlist-banner.webp";
+ CONFIG.RollTable.compendiumBanner = "ui/banners/rolltable-banner.webp";
+ CONFIG.Scene.compendiumBanner = "/systems/fvtt-chroniques-de-l-etrange/images/banners/scene-banner.webp";
+}
+
+// src/data/actors/character.js
+var CharacterDataModel = class extends foundry.abstract.TypeDataModel {
+ static defineSchema() {
+ const { fields } = foundry.data;
+ const numberField = (initial = 0, extra = {}) => new fields.NumberField({ required: true, nullable: false, integer: true, initial, ...extra });
+ const stringField = (initial = "") => new fields.StringField({ required: true, nullable: false, initial });
+ const boolField = (initial = false) => new fields.BooleanField({ required: true, initial });
+ const htmlField = (initial = "") => new fields.HTMLField({ required: true, nullable: false, initial, textSearch: true });
+ const aspectField = (label, chinese) => new fields.SchemaField({
+ chinese: stringField(chinese),
+ label: stringField(label),
+ value: numberField(15, { min: 0 })
+ });
+ const skillField = (label) => new fields.SchemaField({
+ label: stringField(label),
+ specialities: stringField(""),
+ value: numberField(0, { min: 0 })
+ });
+ const resourceField = (label) => new fields.SchemaField({
+ label: stringField(label),
+ specialities: stringField(""),
+ value: numberField(0, { min: 0 }),
+ debt: boolField(false)
+ });
+ const componentField = () => new fields.SchemaField({
+ value: stringField("")
+ });
+ const magicSpecialityField = () => new fields.SchemaField({
+ check: boolField(false)
+ });
+ const magicField = () => new fields.SchemaField({
+ visible: boolField(true),
+ value: numberField(0, { min: 0 }),
+ speciality: new fields.SchemaField({
+ essence: magicSpecialityField(),
+ mind: magicSpecialityField(),
+ purification: magicSpecialityField(),
+ manipulation: magicSpecialityField(),
+ aura: magicSpecialityField(),
+ acupuncture: magicSpecialityField(),
+ elixirs: magicSpecialityField(),
+ poisons: magicSpecialityField(),
+ arsenal: magicSpecialityField(),
+ potions: magicSpecialityField(),
+ curse: magicSpecialityField(),
+ transfiguration: magicSpecialityField(),
+ necromancy: magicSpecialityField(),
+ climatecontrol: magicSpecialityField(),
+ goldenmagic: magicSpecialityField(),
+ invocation: magicSpecialityField(),
+ tracking: magicSpecialityField(),
+ protection: magicSpecialityField(),
+ punishment: magicSpecialityField(),
+ domination: magicSpecialityField(),
+ neutralization: magicSpecialityField(),
+ divination: magicSpecialityField(),
+ earthlyprayer: magicSpecialityField(),
+ heavenlyprayer: magicSpecialityField(),
+ fungseoi: magicSpecialityField()
+ })
+ });
+ const treasureBranch = () => new fields.SchemaField({
+ value: numberField(0, { min: 0 }),
+ max: numberField(0, { min: 0 }),
+ min: numberField(0, { min: 0 })
+ });
+ const treasureLevel = () => new fields.SchemaField({
+ san: treasureBranch(),
+ zing: treasureBranch()
+ });
+ const schema = {
+ concept: stringField(""),
+ guardian: numberField(0, { min: 0, max: 5 }),
+ initiative: numberField(1, { min: 0 }),
+ anti_initiative: numberField(24, { min: 0 }),
+ description: htmlField(""),
+ prefs: new fields.SchemaField({
+ typeofthrow: new fields.SchemaField({
+ check: boolField(true),
+ choice: stringField("0")
+ })
+ }),
+ prompt: new fields.SchemaField({
+ typeofthrow: new fields.SchemaField({
+ check: boolField(true),
+ choice: stringField("0")
+ }),
+ configure: new fields.SchemaField({
+ numberofdice: numberField(0),
+ aspect: numberField(0),
+ bonus: numberField(0),
+ bonusauspiciousdice: numberField(0),
+ typeofthrow: numberField(0),
+ aspectskill: numberField(0),
+ bonusmalusskill: numberField(0),
+ aspectspeciality: numberField(0),
+ rolldifficulty: numberField(0),
+ bonusmalusspeciality: numberField(0)
+ })
+ }),
+ aspect: new fields.SchemaField({
+ fire: aspectField("CDE.Fire", "\u328B"),
+ earth: aspectField("CDE.Earth", "\u328F"),
+ metal: aspectField("CDE.Metal", "\u328E"),
+ water: aspectField("CDE.Water", "\u328C"),
+ wood: aspectField("CDE.Wood", "\u328D")
+ }),
+ skills: new fields.SchemaField({
+ art: skillField("CDE.Art"),
+ investigation: skillField("CDE.Investigation"),
+ erudition: skillField("CDE.Erudition"),
+ knavery: skillField("CDE.Knavery"),
+ wordliness: skillField("CDE.Wordliness"),
+ prowess: skillField("CDE.Prowess"),
+ sciences: skillField("CDE.Sciences"),
+ technologies: skillField("CDE.Technologies"),
+ kungfu: skillField("CDE.KungFu"),
+ rangedcombat: skillField("CDE.RangedCombat")
+ }),
+ resources: new fields.SchemaField({
+ supply: resourceField("CDE.Supply"),
+ inquiry: resourceField("CDE.Inquiry"),
+ influence: resourceField("CDE.Influence")
+ }),
+ component: new fields.SchemaField({
+ one: componentField(),
+ two: componentField(),
+ three: componentField(),
+ four: componentField(),
+ five: componentField(),
+ six: componentField(),
+ seven: componentField(),
+ eight: componentField(),
+ nine: componentField(),
+ zero: componentField()
+ }),
+ magics: new fields.SchemaField({
+ internalcinnabar: magicField(),
+ alchemy: magicField(),
+ masteryoftheway: magicField(),
+ exorcism: magicField(),
+ geomancy: magicField()
+ }),
+ threetreasures: new fields.SchemaField({
+ heiyang: new fields.SchemaField({ value: numberField(0, { min: 0 }), max: numberField(0, { min: 0 }) }),
+ heiyin: new fields.SchemaField({ value: numberField(0, { min: 0 }), max: numberField(0, { min: 0 }) }),
+ dicelevel: new fields.SchemaField({
+ level0d: treasureLevel(),
+ level1d: treasureLevel(),
+ level2d: treasureLevel()
+ })
+ }),
+ experience: new fields.SchemaField({
+ value: numberField(0, { min: 0 }),
+ max: numberField(0, { min: 0 }),
+ min: numberField(0, { min: 0 })
+ })
+ };
+ return schema;
+ }
+};
+
+// src/data/actors/npc.js
+var NpcDataModel = class extends foundry.abstract.TypeDataModel {
+ static defineSchema() {
+ const { fields } = foundry.data;
+ const numberField = (initial = 0, extra = {}) => new fields.NumberField({ required: true, nullable: false, integer: true, initial, ...extra });
+ const stringField = (initial = "") => new fields.StringField({ required: true, nullable: false, initial });
+ const boolField = (initial = false) => new fields.BooleanField({ required: true, initial });
+ const htmlField = (initial = "") => new fields.HTMLField({ required: true, nullable: false, initial, textSearch: true });
+ const aptitudeField = () => new fields.SchemaField({
+ value: numberField(0, { min: 0 }),
+ speciality: stringField("")
+ });
+ const trackedField = () => new fields.SchemaField({
+ value: numberField(0, { min: 0 }),
+ calcul: numberField(0, { min: 0 }),
+ note: stringField("")
+ });
+ return {
+ type: stringField(""),
+ threat: numberField(0, { min: 0, max: 4 }),
+ // profane(0) | apprentice(1) | initiate(2) | accomplished(3) | renowned(4)
+ nuisance: numberField(0, { min: 0, max: 5 }),
+ // figurant(0) | minion(1) | adversary(2) | ally(3) | boss(4) | divinity(5)
+ initiative: numberField(1, { min: 0 }),
+ anti_initiative: numberField(24, { min: 0 }),
+ aptitudes: new fields.SchemaField({
+ physical: aptitudeField(),
+ martial: aptitudeField(),
+ mental: aptitudeField(),
+ social: aptitudeField(),
+ spiritual: aptitudeField()
+ }),
+ vitality: trackedField(),
+ hei: trackedField(),
+ description: htmlField(""),
+ prefs: new fields.SchemaField({
+ typeofthrow: new fields.SchemaField({
+ check: boolField(false),
+ choice: stringField("0")
+ })
+ })
+ };
+ }
+};
+
+// src/data/items/item.js
+var EquipmentDataModel = class extends foundry.abstract.TypeDataModel {
+ static defineSchema() {
+ const { fields } = foundry.data;
+ const numberField = (initial = 0, extra = {}) => new fields.NumberField({ required: true, nullable: false, integer: true, initial, ...extra });
+ const stringField = (initial = "") => new fields.StringField({ required: true, nullable: false, initial });
+ const htmlField = (initial = "") => new fields.HTMLField({ required: true, nullable: false, initial, textSearch: true });
+ return {
+ reference: stringField(""),
+ description: htmlField(""),
+ quantity: numberField(1, { min: 0 }),
+ weight: numberField(0, { min: 0 }),
+ notes: htmlField("")
+ };
+ }
+};
+
+// src/data/items/kungfu.js
+var KungfuDataModel = class extends foundry.abstract.TypeDataModel {
+ static defineSchema() {
+ const { fields } = foundry.data;
+ const stringField = (initial = "") => new fields.StringField({ required: true, nullable: false, initial });
+ const htmlField = (initial = "") => new fields.HTMLField({ required: true, nullable: false, initial, textSearch: true });
+ const boolField = (initial = false) => new fields.BooleanField({ required: true, initial });
+ const techniqueField = () => new fields.SchemaField({
+ check: boolField(false),
+ name: stringField(""),
+ activation: stringField("action-attack"),
+ // action-attack | action-defense | action-aid | action-attack-defense | reaction | dice | damage-inflicted | damage-received
+ technique: htmlField("")
+ });
+ return {
+ reference: stringField(""),
+ description: htmlField(""),
+ orientation: stringField("yin"),
+ // yin | yang | yinyang
+ aspect: stringField("metal"),
+ // metal | eau | terre | feu | bois
+ skill: stringField("kungfu"),
+ // kungfu | rangedcombat
+ speciality: stringField(""),
+ style: stringField(""),
+ techniques: new fields.SchemaField({
+ technique1: techniqueField(),
+ technique2: techniqueField(),
+ technique3: techniqueField()
+ }),
+ notes: htmlField("")
+ };
+ }
+};
+
+// src/data/items/spell.js
+var SpellDataModel = class extends foundry.abstract.TypeDataModel {
+ static defineSchema() {
+ const { fields } = foundry.data;
+ const stringField = (initial = "") => new fields.StringField({ required: true, nullable: false, initial });
+ const htmlField = (initial = "") => new fields.HTMLField({ required: true, nullable: false, initial, textSearch: true });
+ return {
+ reference: stringField(""),
+ description: htmlField(""),
+ specialityname: stringField(""),
+ associatedelement: stringField("metal"),
+ // metal | eau | terre | feu | bois
+ hei: stringField(""),
+ realizationtimeritual: stringField(""),
+ realizationtimeaccelerated: stringField(""),
+ flashback: stringField(""),
+ components: htmlField(""),
+ effects: htmlField(""),
+ examples: htmlField(""),
+ notes: htmlField(""),
+ discipline: stringField("internalcinnabar"),
+ heiType: stringField("yin"),
+ heiCost: new fields.NumberField({ required: true, nullable: false, integer: true, min: 0, initial: 1 }),
+ difficulty: new fields.NumberField({ required: true, nullable: false, integer: true, min: 0, initial: 1 })
+ };
+ }
+};
+
+// src/data/items/supernatural.js
+var SupernaturalDataModel = class extends foundry.abstract.TypeDataModel {
+ static defineSchema() {
+ const { fields } = foundry.data;
+ const stringField = (initial = "") => new fields.StringField({ required: true, nullable: false, initial });
+ const htmlField = (initial = "") => new fields.HTMLField({ required: true, nullable: false, initial, textSearch: true });
+ return {
+ reference: stringField(""),
+ description: htmlField(""),
+ notes: htmlField(""),
+ heiType: stringField("yin"),
+ heiCost: new fields.NumberField({ required: true, nullable: false, integer: true, min: 0, initial: 0 }),
+ trigger: stringField(""),
+ effects: htmlField("")
+ };
+ }
+};
+
+// src/data/items/weapon.js
+var WeaponDataModel = class extends foundry.abstract.TypeDataModel {
+ static defineSchema() {
+ const { fields } = foundry.data;
+ const stringField = (initial = "") => new fields.StringField({ required: true, nullable: false, initial });
+ const htmlField = (initial = "") => new fields.HTMLField({ required: true, nullable: false, initial, textSearch: true });
+ const intField = (initial = 0, opts = {}) => new fields.NumberField({ required: true, nullable: false, integer: true, initial, ...opts });
+ return {
+ reference: stringField(""),
+ description: htmlField(""),
+ weaponType: stringField("melee"),
+ material: stringField(""),
+ damageAspect: stringField("metal"),
+ damageBase: intField(1),
+ range: stringField("contact"),
+ // contact | courte | mediane | longue | extreme
+ obtainLevel: intField(0, { min: 0, max: 5 }),
+ obtainDifficulty: intField(0, { min: 0, max: 3 }),
+ quantity: intField(1),
+ notes: htmlField("")
+ };
+ }
+};
+
+// src/data/items/armor.js
+var ArmorDataModel = class extends foundry.abstract.TypeDataModel {
+ static defineSchema() {
+ const { fields } = foundry.data;
+ const stringField = (initial = "") => new fields.StringField({ required: true, nullable: false, initial });
+ const htmlField = (initial = "") => new fields.HTMLField({ required: true, nullable: false, initial, textSearch: true });
+ const intField = (initial = 0, opts = {}) => new fields.NumberField({ required: true, nullable: false, integer: true, initial, ...opts });
+ return {
+ reference: stringField(""),
+ description: htmlField(""),
+ protectionValue: intField(0),
+ domain: stringField(""),
+ obtainLevel: intField(0, { min: 0, max: 5 }),
+ obtainDifficulty: intField(0, { min: 0, max: 3 }),
+ quantity: intField(1),
+ notes: htmlField("")
+ };
+ }
+};
+
+// src/data/items/sanhei.js
+var SanheiDataModel = class extends foundry.abstract.TypeDataModel {
+ static defineSchema() {
+ const { fields } = foundry.data;
+ const stringField = (initial = "") => new fields.StringField({ required: true, nullable: false, initial });
+ const htmlField = (initial = "") => new fields.HTMLField({ required: true, nullable: false, initial, textSearch: true });
+ const intField = (initial = 0, opts = {}) => new fields.NumberField({ required: true, nullable: false, integer: true, initial, ...opts });
+ const propertySchema = () => new fields.SchemaField({
+ name: stringField(""),
+ heiCost: intField(0),
+ heiType: stringField("yin"),
+ description: htmlField("")
+ });
+ return {
+ reference: stringField(""),
+ description: htmlField(""),
+ heiType: stringField("yin"),
+ properties: new fields.SchemaField({
+ prop1: propertySchema(),
+ prop2: propertySchema(),
+ prop3: propertySchema()
+ }),
+ notes: htmlField("")
+ };
+ }
+};
+
+// src/data/items/ingredient.js
+var IngredientDataModel = class extends foundry.abstract.TypeDataModel {
+ static defineSchema() {
+ const { fields } = foundry.data;
+ const stringField = (initial = "") => new fields.StringField({ required: true, nullable: false, initial });
+ const htmlField = (initial = "") => new fields.HTMLField({ required: true, nullable: false, initial, textSearch: true });
+ const intField = (initial = 0, opts = {}) => new fields.NumberField({ required: true, nullable: false, integer: true, initial, ...opts });
+ return {
+ reference: stringField(""),
+ description: htmlField(""),
+ school: stringField("all"),
+ obtainLevel: intField(0, { min: 0, max: 5 }),
+ obtainDifficulty: intField(0, { min: 0, max: 3 }),
+ quantity: intField(1),
+ notes: htmlField("")
+ };
+ }
+};
+
+// src/documents/chat-message.js
+var CDEMessage = class extends ChatMessage {
+ async renderHTML({ canDelete, canClose = false, ...rest } = {}) {
+ const html = await super.renderHTML({ canDelete, canClose, ...rest });
+ this.#enrichChatCard(html);
+ return html;
+ }
+ getAssociatedActor() {
+ if (this.speaker.scene && this.speaker.token) {
+ const scene = game.scenes.get(this.speaker.scene);
+ const token = scene?.tokens.get(this.speaker.token);
+ if (token) return token.actor;
+ }
+ return game.actors.get(this.speaker.actor);
+ }
+ #enrichChatCard(html) {
+ const actor = this.getAssociatedActor();
+ let img;
+ let nameText;
+ if (this.isContentVisible) {
+ img = actor?.img ?? this.author.avatar;
+ nameText = this.alias;
+ } else {
+ img = this.author.avatar;
+ nameText = this.author.name;
+ }
+ const avatar = document.createElement("a");
+ avatar.classList.add("avatar");
+ if (actor) avatar.dataset.uuid = actor.uuid;
+ const avatarImg = document.createElement("img");
+ Object.assign(avatarImg, { src: img, alt: nameText });
+ avatar.append(avatarImg);
+ const name = document.createElement("span");
+ name.classList.add("name-stacked");
+ const title = document.createElement("span");
+ title.classList.add("title");
+ title.append(nameText);
+ name.append(title);
+ const sender = html.querySelector(".message-sender");
+ sender?.replaceChildren(avatar, name);
+ }
+};
+
+// src/documents/actor.js
+var CDEActor = class extends Actor {
+ getRollData() {
+ const data = this.toObject(false).system;
+ return data;
+ }
+ prepareBaseData() {
+ super.prepareBaseData();
+ if (this.type === ACTOR_TYPES.character) {
+ this.system.anti_initiative = 25 - (this.system.initiative ?? 0);
+ }
+ if (this.type === ACTOR_TYPES.npc) {
+ this.system.vitality.calcul = (this.system.aptitudes.physical.value ?? 0) * 4;
+ this.system.hei.calcul = (this.system.aptitudes.spiritual.value ?? 0) * 4;
+ this.system.anti_initiative = 25 - (this.system.initiative ?? 0);
+ }
+ }
+};
+
+// src/documents/item.js
+var CDEItem = class extends Item {
+};
+
+// src/ui/dice.js
+var DIGIT_LABELS = [
+ "systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/digit/d10-1.webp",
+ "systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/digit/d10-2.webp",
+ "systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/digit/d10-3.webp",
+ "systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/digit/d10-4.webp",
+ "systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/digit/d10-5.webp",
+ "systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/digit/d10-6.webp",
+ "systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/digit/d10-7.webp",
+ "systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/digit/d10-8.webp",
+ "systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/digit/d10-9.webp",
+ "systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/digit/d10-10.webp"
+];
+var CLASSIC_LABELS = [
+ "systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/d10-1.webp",
+ "systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/d10-2.webp",
+ "systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/d10-3.webp",
+ "systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/d10-4.webp",
+ "systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/d10-5.webp",
+ "systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/d10-6.webp",
+ "systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/d10-7.webp",
+ "systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/d10-8.webp",
+ "systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/d10-9.webp",
+ "systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/d10-10.webp"
+];
+function registerDice() {
+ Hooks.once("diceSoNiceReady", (dice3d) => {
+ dice3d.addColorset(
+ {
+ name: "cde",
+ description: "CdE",
+ foreground: "#000000",
+ background: "#ffffff",
+ edge: "#ffffff",
+ font: "DeliusUnicase",
+ texture: "ice",
+ material: "plastic"
+ },
+ "preferred"
+ );
+ dice3d.addSystem({ id: "fvtt-chroniques-de-l-etrangedigit", name: "Chroniques de l'\xE9trange digits" }, "preferred");
+ dice3d.addDicePreset({ type: "d10", labels: DIGIT_LABELS, system: "fvtt-chroniques-de-l-etrangedigit" });
+ dice3d.addSystem({ id: "fvtt-chroniques-de-l-etrange", name: "Chroniques de l'\xE9trange" }, "preferred");
+ dice3d.addDicePreset({ type: "d10", labels: CLASSIC_LABELS, system: "fvtt-chroniques-de-l-etrange" });
+ });
+}
+
+// src/ui/helpers.js
+function registerHandlebarsHelpers() {
+ const { Handlebars } = globalThis;
+ if (!Handlebars) return;
+ Handlebars.registerHelper("select", function(selected, options) {
+ const escapedValue = RegExp.escape(Handlebars.escapeExpression(selected));
+ const rgx = new RegExp(` value=["']${escapedValue}["']`);
+ const html = options.fn(this);
+ return html.replace(rgx, "$& selected");
+ });
+ Handlebars.registerHelper("getMagicBackground", function(magic) {
+ return game.i18n.localize(MAGICS[magic]?.background ?? "");
+ });
+ Handlebars.registerHelper("getMagicLabel", function(magic) {
+ return game.i18n.localize(MAGICS[magic]?.label ?? "");
+ });
+ Handlebars.registerHelper("getMagicAspectLabel", function(magic) {
+ return game.i18n.localize(MAGICS[magic]?.aspectlabel ?? "");
+ });
+ Handlebars.registerHelper("getMagicSpecialityLabel", function(magic, speciality) {
+ return game.i18n.localize(MAGICS[magic]?.speciality?.[speciality]?.label ?? "");
+ });
+ Handlebars.registerHelper("getMagicSpecialityClassIcon", function(magic, speciality) {
+ return MAGICS[magic]?.speciality?.[speciality]?.classicon ?? "";
+ });
+ Handlebars.registerHelper("getMagicSpecialityIcon", function(magic, speciality) {
+ return MAGICS[magic]?.speciality?.[speciality]?.icon ?? "";
+ });
+ Handlebars.registerHelper("getMagicSpecialityElementIcon", function(magic, speciality) {
+ return MAGICS[magic]?.speciality?.[speciality]?.elementicon ?? "";
+ });
+ Handlebars.registerHelper("getMagicSpecialityLabelIcon", function(magic, speciality) {
+ return MAGICS[magic]?.speciality?.[speciality]?.labelicon ?? "";
+ });
+ Handlebars.registerHelper("getMagicSpecialityLabelElement", function(magic, speciality) {
+ return game.i18n.localize(MAGICS[magic]?.speciality?.[speciality]?.labelelement ?? "");
+ });
+ Handlebars.registerHelper("getMagicAspectIcon", function(magic) {
+ const icons = {
+ internalcinnabar: "/systems/fvtt-chroniques-de-l-etrange/images/cde_metal.webp",
+ alchemy: "/systems/fvtt-chroniques-de-l-etrange/images/cde_eau.webp",
+ masteryoftheway: "/systems/fvtt-chroniques-de-l-etrange/images/cde_terre.webp",
+ exorcism: "/systems/fvtt-chroniques-de-l-etrange/images/cde_feu.webp",
+ geomancy: "/systems/fvtt-chroniques-de-l-etrange/images/cde_bois.webp"
+ };
+ return icons[magic] ?? "";
+ });
+ Handlebars.registerHelper("getElementIcon", function(aspect) {
+ const icons = {
+ metal: "/systems/fvtt-chroniques-de-l-etrange/images/cde_metal.webp",
+ water: "/systems/fvtt-chroniques-de-l-etrange/images/cde_eau.webp",
+ earth: "/systems/fvtt-chroniques-de-l-etrange/images/cde_terre.webp",
+ fire: "/systems/fvtt-chroniques-de-l-etrange/images/cde_feu.webp",
+ wood: "/systems/fvtt-chroniques-de-l-etrange/images/cde_bois.webp",
+ // legacy French keys
+ eau: "/systems/fvtt-chroniques-de-l-etrange/images/cde_eau.webp",
+ terre: "/systems/fvtt-chroniques-de-l-etrange/images/cde_terre.webp",
+ feu: "/systems/fvtt-chroniques-de-l-etrange/images/cde_feu.webp",
+ bois: "/systems/fvtt-chroniques-de-l-etrange/images/cde_bois.webp"
+ };
+ return icons[aspect] ?? "";
+ });
+ Handlebars.registerHelper("getOrientationIcon", function(orientation) {
+ const icons = {
+ yin: "/systems/fvtt-chroniques-de-l-etrange/images/cde_yin.webp",
+ yang: "/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.webp",
+ yinyang: "/systems/fvtt-chroniques-de-l-etrange/images/yin_yang.webp"
+ };
+ return icons[orientation] ?? "";
+ });
+ Handlebars.registerHelper("getOrientationLabel", function(orientation) {
+ const keys = {
+ yin: "CDE.OrientationYin",
+ yang: "CDE.OrientationYang",
+ yinyang: "CDE.OrientationYinYang"
+ };
+ return game.i18n.localize(keys[orientation] ?? "CDE.Orientation");
+ });
+ Handlebars.registerHelper("getActivationLabel", function(activation) {
+ const keys = {
+ "action-attack": "CDE.ActivationAttack",
+ "action-defense": "CDE.ActivationDefense",
+ "action-aid": "CDE.ActivationAid",
+ "action-attack-defense": "CDE.ActivationAttackOrDefense",
+ reaction: "CDE.ActivationReaction",
+ dice: "CDE.ActivationDice",
+ "damage-inflicted": "CDE.ActivationDamageInflicted",
+ "damage-received": "CDE.ActivationDamageReceived"
+ };
+ return game.i18n.localize(keys[activation] ?? "CDE.Activation");
+ });
+}
+
+// src/ui/templates.js
+async function preloadPartials() {
+ return foundry.applications.handlebars.loadTemplates(TEMPLATE_PARTIALS);
+}
+
+// src/ui/initiative.js
+var PC_PROMPT_TEMPLATE = "systems/fvtt-chroniques-de-l-etrange/templates/form/cde-initiative-prompt.html";
+var NPC_PROMPT_TEMPLATE = "systems/fvtt-chroniques-de-l-etrange/templates/form/cde-initiative-prompt-npc.html";
+var RESULT_TEMPLATE = "systems/fvtt-chroniques-de-l-etrange/templates/form/cde-initiative-result.html";
+function buildPCOptions(sys) {
+ const sk = sys.skills ?? {};
+ const rs = sys.resources ?? {};
+ const mg = sys.magics ?? {};
+ return [
+ { key: "art", label: game.i18n.localize("CDE.Art"), value: sk.art?.value ?? 0 },
+ { key: "investigation", label: game.i18n.localize("CDE.Investigation"), value: sk.investigation?.value ?? 0 },
+ { key: "erudition", label: game.i18n.localize("CDE.Erudition"), value: sk.erudition?.value ?? 0 },
+ { key: "knavery", label: game.i18n.localize("CDE.Knavery"), value: sk.knavery?.value ?? 0 },
+ { key: "wordliness", label: game.i18n.localize("CDE.Wordliness"), value: sk.wordliness?.value ?? 0 },
+ { key: "prowess", label: game.i18n.localize("CDE.Prowess"), value: sk.prowess?.value ?? 0 },
+ { key: "sciences", label: game.i18n.localize("CDE.Sciences"), value: sk.sciences?.value ?? 0 },
+ { key: "technologies", label: game.i18n.localize("CDE.Technologies"), value: sk.technologies?.value ?? 0 },
+ { key: "kungfu", label: game.i18n.localize("CDE.KungFu"), value: sk.kungfu?.value ?? 0 },
+ { key: "rangedcombat", label: game.i18n.localize("CDE.RangedCombat"), value: sk.rangedcombat?.value ?? 0 },
+ { key: "supply", label: game.i18n.localize("CDE.Supply"), value: rs.supply?.value ?? 0 },
+ { key: "inquiry", label: game.i18n.localize("CDE.Inquiry"), value: rs.inquiry?.value ?? 0 },
+ { key: "influence", label: game.i18n.localize("CDE.Influence"), value: rs.influence?.value ?? 0 },
+ { key: "internalcinnabar", label: game.i18n.localize("CDE.InternalCinnabar"), value: mg.internalcinnabar?.value ?? 0 },
+ { key: "alchemy", label: game.i18n.localize("CDE.Alchemy"), value: mg.alchemy?.value ?? 0 },
+ { key: "masteryoftheway", label: game.i18n.localize("CDE.MasteryOfTheWay"), value: mg.masteryoftheway?.value ?? 0 },
+ { key: "exorcism", label: game.i18n.localize("CDE.Exorcism"), value: mg.exorcism?.value ?? 0 },
+ { key: "geomancy", label: game.i18n.localize("CDE.Geomancy"), value: mg.geomancy?.value ?? 0 }
+ ];
+}
+function buildNPCOptions(sys) {
+ const ap = sys.aptitudes ?? {};
+ return [
+ { key: "physical", label: game.i18n.localize("CDE.Physical"), value: ap.physical?.value ?? 0 },
+ { key: "martial", label: game.i18n.localize("CDE.Martial"), value: ap.martial?.value ?? 0 },
+ { key: "mental", label: game.i18n.localize("CDE.Mental"), value: ap.mental?.value ?? 0 },
+ { key: "social", label: game.i18n.localize("CDE.Social"), value: ap.social?.value ?? 0 },
+ { key: "spiritual", label: game.i18n.localize("CDE.Spiritual"), value: ap.spiritual?.value ?? 0 }
+ ];
+}
+function readInitFields(dialog) {
+ const root = dialog.element ?? dialog;
+ const selectedKey = root.querySelector("select[name='firstaction']")?.value ?? "";
+ const modifier = parseInt(root.querySelector("input[name='modifier']")?.value ?? "0", 10) || 0;
+ return { selectedKey, modifier };
+}
+async function sendInitChatMessage({ actor, baseName, baseValue, actionName, actionValue, modifier, initiative, antiInitiative }) {
+ const html = await foundry.applications.handlebars.renderTemplate(RESULT_TEMPLATE, {
+ actorName: actor.name,
+ actorImg: actor.img,
+ baseName,
+ baseValue,
+ actionName,
+ actionValue,
+ modifier,
+ hasModifier: modifier !== 0,
+ initiative,
+ antiInitiative
+ });
+ await ChatMessage.create({
+ user: game.user.id,
+ speaker: ChatMessage.getSpeaker({ actor }),
+ content: html
+ });
+}
+async function rollInitiativePC(actor) {
+ const sys = actor.system;
+ const prowess = sys.skills?.prowess?.value ?? 0;
+ const options = buildPCOptions(sys);
+ const baseName = game.i18n.localize("CDE.Prowess");
+ const content = await foundry.applications.handlebars.renderTemplate(PC_PROMPT_TEMPLATE, {
+ prowessValue: prowess,
+ options,
+ modifier: 0
+ });
+ const result = await foundry.applications.api.DialogV2.prompt({
+ window: { title: game.i18n.localize("CDE.InitiativeRoll") },
+ content,
+ rejectClose: false,
+ ok: {
+ label: game.i18n.localize("CDE.Validate"),
+ callback: (_ev, _btn, dialog) => readInitFields(dialog)
+ }
+ });
+ if (!result) return;
+ const { selectedKey, modifier } = result;
+ const selected = options.find((o) => o.key === selectedKey) ?? options[0];
+ const rawValue = prowess + selected.value + modifier;
+ const initiative = Math.max(1, Math.min(24, rawValue));
+ const antiInit = 25 - initiative;
+ await actor.update({ "system.initiative": initiative });
+ await sendInitChatMessage({
+ actor,
+ baseName,
+ baseValue: prowess,
+ actionName: selected.label,
+ actionValue: selected.value,
+ modifier,
+ initiative,
+ antiInitiative: antiInit
+ });
+}
+async function rollInitiativeNPC(actor) {
+ const sys = actor.system;
+ const physical = sys.aptitudes?.physical?.value ?? 0;
+ const options = buildNPCOptions(sys);
+ const baseName = game.i18n.localize("CDE.Physical");
+ const content = await foundry.applications.handlebars.renderTemplate(NPC_PROMPT_TEMPLATE, {
+ physicalValue: physical,
+ options,
+ modifier: 0
+ });
+ const result = await foundry.applications.api.DialogV2.prompt({
+ window: { title: game.i18n.localize("CDE.InitiativeRoll") },
+ content,
+ rejectClose: false,
+ ok: {
+ label: game.i18n.localize("CDE.Validate"),
+ callback: (_ev, _btn, dialog) => readInitFields(dialog)
+ }
+ });
+ if (!result) return;
+ const { selectedKey, modifier } = result;
+ const selected = options.find((o) => o.key === selectedKey) ?? options[0];
+ const rawValue = physical + selected.value + modifier;
+ const initiative = Math.max(1, Math.min(24, rawValue));
+ const antiInit = 25 - initiative;
+ await actor.update({ "system.initiative": initiative });
+ await sendInitChatMessage({
+ actor,
+ baseName,
+ baseValue: physical,
+ actionName: selected.label,
+ actionValue: selected.value,
+ modifier,
+ initiative,
+ antiInitiative: antiInit
+ });
+}
+
+// src/ui/apps/singletons.js
+function getLoksyuData() {
+ return game.settings.get(SYSTEM_ID, "loksyuData") ?? {
+ 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 }
+ };
+}
+async function setLoksyuData(data) {
+ await game.settings.set(SYSTEM_ID, "loksyuData", data);
+ Hooks.callAll("cde:loksyuUpdated", data);
+}
+function getTinjiValue() {
+ return game.settings.get(SYSTEM_ID, "tinjiData") ?? 0;
+}
+async function setTinjiValue(value) {
+ await game.settings.set(SYSTEM_ID, "tinjiData", Math.max(0, value));
+ Hooks.callAll("cde:tinjiUpdated", Math.max(0, value));
+}
+async function updateLoksyuFromRoll(activeAspect, faces) {
+ const cycle = WU_XING_CYCLE[activeAspect];
+ if (!cycle) return;
+ const lokAspect = cycle[3];
+ const [yinFace, yangFace] = ASPECT_FACES[lokAspect] ?? [];
+ if (yinFace === void 0) return;
+ const yinCount = faces[yinFace] ?? 0;
+ const yangCount = faces[yangFace] ?? 0;
+ if (yinCount === 0 && yangCount === 0) return;
+ const data = getLoksyuData();
+ const current = data[lokAspect] ?? { yin: 0, yang: 0 };
+ data[lokAspect] = {
+ yin: (current.yin ?? 0) + yinCount,
+ yang: (current.yang ?? 0) + yangCount
+ };
+ await setLoksyuData(data);
+}
+async function updateTinjiFromRoll(count) {
+ if (!count || count <= 0) return;
+ const current = getTinjiValue();
+ await setTinjiValue(current + count);
+}
+
+// src/ui/rolling.js
+var RESULT_TEMPLATE2 = "systems/fvtt-chroniques-de-l-etrange/templates/form/cde-dice-result.html";
+var SKILL_PROMPT_TEMPLATE = "systems/fvtt-chroniques-de-l-etrange/templates/form/cde-skill-dice-prompt.html";
+var SKILL_SPECIAL_PROMPT_TEMPLATE = "systems/fvtt-chroniques-de-l-etrange/templates/form/cde-skill-special-dice-prompt.html";
+var MAGIC_PROMPT_TEMPLATE = "systems/fvtt-chroniques-de-l-etrange/templates/form/cde-magic-dice-prompt.html";
+var WEAPON_PROMPT_TEMPLATE = "systems/fvtt-chroniques-de-l-etrange/templates/form/cde-weapon-dice-prompt.html";
+var LABELELEMENT_TO_ASPECT = {
+ "CDE.Metal": "metal",
+ "CDE.Water": "water",
+ "CDE.Earth": "earth",
+ "CDE.Fire": "fire",
+ "CDE.Wood": "wood"
+};
+var RANGE_MALUS = {
+ contact: 0,
+ courte: 0,
+ mediane: -1,
+ longue: -2,
+ extreme: -3
+};
+var WEAPON_TYPE_SKILL = {
+ melee: "kungfu",
+ thrown: "rangedcombat",
+ ranged: "rangedcombat",
+ firearm: "rangedcombat"
+};
+var WEAPON_ASPECT_INDEX = { metal: 0, eau: 1, water: 1, terre: 2, earth: 2, feu: 3, fire: 3, bois: 4, wood: 4 };
+function countFaces(rollResults) {
+ const counts = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0, 0: 0 };
+ for (const die of rollResults) {
+ const face = die.result === 10 ? 0 : die.result;
+ counts[face]++;
+ }
+ return counts;
+}
+function computeWuXingResults(faces, aspectName, bonusAuspicious = 0) {
+ const cycle = WU_XING_CYCLE[aspectName];
+ if (!cycle) return null;
+ const [succAspect, ausAspect, noxAspect, lokAspect, tinAspect] = cycle;
+ const [succYin, succYang] = ASPECT_FACES[succAspect];
+ const [ausYin, ausYang] = ASPECT_FACES[ausAspect];
+ const [noxYin, noxYang] = ASPECT_FACES[noxAspect];
+ const [lokYin, lokYang] = ASPECT_FACES[lokAspect];
+ const [tinYin, tinYang] = ASPECT_FACES[tinAspect];
+ const yin = game.i18n.localize("CDE.Yin");
+ const yang = game.i18n.localize("CDE.Yang");
+ return {
+ successesdice: faces[succYin] + faces[succYang],
+ auspiciousdice: faces[ausYin] + faces[ausYang] + bonusAuspicious,
+ noxiousdice: faces[noxYin] + faces[noxYang],
+ loksyudice: faces[lokYin] + faces[lokYang],
+ loksyurepartition: `[${yin}(${faces[lokYin]}) ${yang}(${faces[lokYang]})]`,
+ tinjidice: faces[tinYin] + faces[tinYang]
+ };
+}
+function readField(dlg, name) {
+ const el = dlg.querySelector(`[name="${name}"]`);
+ if (!el) return null;
+ return el.type === "checkbox" ? el.checked : el.value;
+}
+async function showRollPrompt({ title, template, data, fields }) {
+ const content = await foundry.applications.handlebars.renderTemplate(template, data);
+ return foundry.applications.api.DialogV2.prompt({
+ window: { title },
+ content,
+ rejectClose: false,
+ ok: {
+ label: game.i18n.localize("CDE.Validate"),
+ callback: (event, button, dialog) => {
+ const root = dialog.element ?? dialog;
+ const result = {};
+ for (const field of fields) {
+ result[field] = readField(root, field);
+ }
+ return result;
+ }
+ }
+ });
+}
+async function showSkillPrompt(params) {
+ return showRollPrompt({
+ title: params.title,
+ template: params.isSpecial ? SKILL_SPECIAL_PROMPT_TEMPLATE : SKILL_PROMPT_TEMPLATE,
+ data: {
+ numberofdice: params.numberofdice,
+ aspect: Number(params.aspect ?? 0),
+ bonusmalus: params.bonusmalus ?? 0,
+ woundmalus: params.woundmalus ?? 0,
+ bonusauspiciousdice: params.bonusauspiciousdice ?? 0,
+ typeofthrow: Number(params.typeofthrow ?? 0)
+ },
+ fields: ["aspect", "bonusmalus", "woundmalus", "bonusauspiciousdice", "typeofthrow"]
+ });
+}
+async function showMagicPrompt(params) {
+ return showRollPrompt({
+ title: params.title,
+ template: MAGIC_PROMPT_TEMPLATE,
+ data: {
+ numberofdice: params.numberofdice ?? 0,
+ aspectskill: Number(params.aspectskill ?? 0),
+ bonusmalusskill: params.bonusmalusskill ?? 0,
+ bonusauspiciousdice: params.bonusauspiciousdice ?? 0,
+ aspectspeciality: Number(params.aspectspeciality ?? 0),
+ rolldifficulty: params.rolldifficulty ?? 1,
+ bonusmalusspeciality: params.bonusmalusspeciality ?? 0,
+ heispend: params.heispend ?? 0,
+ typeofthrow: Number(params.typeofthrow ?? 0)
+ },
+ fields: [
+ "aspectskill",
+ "bonusmalusskill",
+ "bonusauspiciousdice",
+ "aspectspeciality",
+ "rolldifficulty",
+ "bonusmalusspeciality",
+ "heispend",
+ "typeofthrow"
+ ]
+ });
+}
+async function showWeaponPrompt(params) {
+ return showRollPrompt({
+ title: params.title,
+ template: WEAPON_PROMPT_TEMPLATE,
+ data: {
+ numberofdice: params.numberofdice ?? 0,
+ weaponName: params.weaponName ?? "",
+ weaponTypeLabel: params.weaponTypeLabel ?? "CDE.Weapon",
+ weaponAspectIcon: params.weaponAspectIcon ?? "",
+ weaponAspectLabel: params.weaponAspectLabel ?? "",
+ damageBase: params.damageBase ?? 1,
+ weaponskill: params.weaponskill ?? "kungfu",
+ aspect: Number(params.aspect ?? 0),
+ effectiverange: params.effectiverange ?? "contact",
+ bonusmalus: params.bonusmalus ?? 0,
+ woundmalus: params.woundmalus ?? 0,
+ bonusauspiciousdice: params.bonusauspiciousdice ?? 0,
+ typeofthrow: Number(params.typeofthrow ?? 0)
+ },
+ fields: [
+ "weaponskill",
+ "aspect",
+ "effectiverange",
+ "bonusmalus",
+ "woundmalus",
+ "bonusauspiciousdice",
+ "typeofthrow"
+ ]
+ });
+}
+async function sendResultMessage(actor, resultData, roll, rollMode) {
+ const html = await foundry.applications.handlebars.renderTemplate(RESULT_TEMPLATE2, resultData);
+ return ChatMessage.create({
+ user: game.user.id,
+ speaker: ChatMessage.getSpeaker({ actor }),
+ content: html,
+ rolls: [roll],
+ rollMode,
+ flags: {
+ "fvtt-chroniques-de-l-etrange": { rollResult: { ...resultData } }
+ }
+ });
+}
+var ROLL_MODES = ["roll", "gmroll", "blindroll", "selfroll"];
+async function rollForActor(actor, rollKey) {
+ const parts = rollKey.split("-");
+ const skillLibel = parts[0];
+ const typeLibel = parts[1];
+ const specialLibel = parts[2] ?? null;
+ const sys = actor.system;
+ const typeOfThrow = Number(sys.prefs?.typeofthrow?.choice ?? 0);
+ let numberofdice = 0;
+ let title = "";
+ let isSpecial = false;
+ let isMagic = false;
+ let isMagicSpecial = false;
+ let kfDefaultAspect = -1;
+ const MAGIC_I18N_KEYS = {
+ internalcinnabar: "CDE.InternalCinnabar",
+ alchemy: "CDE.Alchemy",
+ masteryoftheway: "CDE.MasteryOfTheWay",
+ exorcism: "CDE.Exorcism",
+ geomancy: "CDE.Geomancy"
+ };
+ switch (typeLibel) {
+ case "aspect":
+ numberofdice = sys.aspect[skillLibel]?.value ?? 0;
+ title = game.i18n.localize(sys.aspect[skillLibel]?.label ?? "CDE.Roll");
+ break;
+ case "aptitude":
+ numberofdice = sys.aptitudes?.[skillLibel]?.value ?? 0;
+ title = game.i18n.localize(`CDE.${skillLibel.charAt(0).toUpperCase() + skillLibel.slice(1)}`);
+ break;
+ case "skill":
+ numberofdice = sys.skills?.[skillLibel]?.value ?? 0;
+ title = game.i18n.localize(sys.skills?.[skillLibel]?.label ?? "CDE.Roll");
+ break;
+ case "special":
+ numberofdice = sys.skills?.[skillLibel]?.value ?? 0;
+ title = game.i18n.localize(sys.skills?.[skillLibel]?.label ?? "CDE.Roll");
+ title += ` [${game.i18n.localize("CDE.Speciality")}]`;
+ isSpecial = true;
+ if (!sys.skills?.[skillLibel]?.specialities) {
+ ui.notifications.warn(game.i18n.localize("CDE.Error2"));
+ return;
+ }
+ break;
+ case "resource":
+ numberofdice = sys.resources?.[skillLibel]?.value ?? 0;
+ title = game.i18n.localize(sys.resources?.[skillLibel]?.label ?? "CDE.Roll");
+ break;
+ case "field":
+ numberofdice = sys.resources?.[skillLibel]?.value ?? 0;
+ title = game.i18n.localize(sys.resources?.[skillLibel]?.label ?? "CDE.Roll");
+ title += ` [${game.i18n.localize("CDE.Field")}]`;
+ isSpecial = true;
+ if (!sys.resources?.[skillLibel]?.specialities) {
+ ui.notifications.warn(game.i18n.localize("CDE.Error4"));
+ return;
+ }
+ break;
+ case "magic":
+ numberofdice = sys.magics?.[skillLibel]?.value ?? 0;
+ isMagic = true;
+ title = game.i18n.localize(MAGIC_I18N_KEYS[skillLibel] ?? "CDE.Magics");
+ break;
+ case "magicspecial":
+ numberofdice = sys.magics?.[skillLibel]?.value ?? 0;
+ isMagicSpecial = true;
+ isMagic = true;
+ if (!sys.magics?.[skillLibel]?.speciality?.[specialLibel]?.check) {
+ ui.notifications.warn(game.i18n.localize("CDE.Error6"));
+ return;
+ }
+ title = `${game.i18n.localize(MAGIC_I18N_KEYS[skillLibel] ?? "CDE.Magics")} [${game.i18n.localize(MAGICS?.[skillLibel]?.speciality?.[specialLibel]?.label ?? "")}]`;
+ break;
+ case "itemkungfu": {
+ const kfItem = actor.items.get(skillLibel);
+ if (!kfItem) {
+ ui.notifications.warn(game.i18n.localize("CDE.Error0"));
+ return;
+ }
+ const kfSkill = kfItem.system.skill ?? "kungfu";
+ numberofdice = sys.skills?.[kfSkill]?.value ?? 0;
+ title = `${kfItem.name} [${game.i18n.localize(sys.skills?.[kfSkill]?.label ?? "CDE.KungFu")}]`;
+ kfDefaultAspect = ASPECT_NAMES.indexOf(kfItem.system.aspect ?? "metal");
+ if (kfDefaultAspect < 0) kfDefaultAspect = 0;
+ break;
+ }
+ case "itemweapon": {
+ const wpItem = actor.items.get(skillLibel);
+ if (!wpItem) {
+ ui.notifications.warn(game.i18n.localize("CDE.Error0"));
+ return;
+ }
+ const wpType = wpItem.system.weaponType ?? "melee";
+ const wpSkill = WEAPON_TYPE_SKILL[wpType] ?? "kungfu";
+ numberofdice = sys.skills?.[wpSkill]?.value ?? 0;
+ const wpAspectRaw = wpItem.system.damageAspect ?? "metal";
+ const wpAspectIdx = WEAPON_ASPECT_INDEX[wpAspectRaw] ?? 0;
+ const wpRange = wpItem.system.range ?? "contact";
+ const WEAPON_TYPE_LABELS = {
+ melee: "CDE.WeaponMelee",
+ thrown: "CDE.WeaponThrown",
+ ranged: "CDE.WeaponRanged",
+ firearm: "CDE.WeaponFirearm"
+ };
+ const wParams = await showWeaponPrompt({
+ title: `${wpItem.name} [${game.i18n.localize(sys.skills?.[wpSkill]?.label ?? "CDE.WeaponRoll")}]`,
+ numberofdice,
+ weaponName: wpItem.name,
+ weaponTypeLabel: WEAPON_TYPE_LABELS[wpType] ?? "CDE.Weapon",
+ weaponAspectIcon: ASPECT_ICONS[ASPECT_NAMES[wpAspectIdx]] ?? "",
+ weaponAspectLabel: game.i18n.localize(ASPECT_LABELS[ASPECT_NAMES[wpAspectIdx]] ?? ""),
+ damageBase: wpItem.system.damageBase ?? 1,
+ weaponskill: wpSkill,
+ aspect: wpAspectIdx,
+ effectiverange: wpRange,
+ bonusmalus: 0,
+ woundmalus: 0,
+ bonusauspiciousdice: 0,
+ typeofthrow: typeOfThrow
+ });
+ if (!wParams) return;
+ const wpChosenSkill = wParams.weaponskill ?? wpSkill;
+ const wpSkillDice = sys.skills?.[wpChosenSkill]?.value ?? 0;
+ const wpAspFinal = Number(wParams.aspect ?? wpAspectIdx);
+ const wpAspectDice = sys.aspect[ASPECT_NAMES[wpAspFinal]]?.value ?? 0;
+ const wpRangeMalus = RANGE_MALUS[wParams.effectiverange ?? "contact"] ?? 0;
+ const wpBonusMalus = Number(wParams.bonusmalus ?? 0);
+ const wpWoundMalus = Number(wParams.woundmalus ?? 0);
+ const wpBonusAusp = Number(wParams.bonusauspiciousdice ?? 0);
+ const wpThrowMode = Number(wParams.typeofthrow ?? 0);
+ const wpDamageBase = wpItem.system.damageBase ?? 1;
+ const wpTotalDice = wpSkillDice + wpAspectDice + wpRangeMalus + wpBonusMalus - wpWoundMalus;
+ if (wpTotalDice <= 0) {
+ ui.notifications.warn(game.i18n.localize("CDE.Error0"));
+ return;
+ }
+ const wpRoll = new Roll(`${wpTotalDice}d10`);
+ await wpRoll.evaluate();
+ const wpAspectName = ASPECT_NAMES[wpAspFinal] ?? "metal";
+ const wpFaces = countFaces(wpRoll.dice[0]?.results ?? []);
+ const wpResults = computeWuXingResults(wpFaces, wpAspectName, wpBonusAusp);
+ if (!wpResults) return;
+ const wpModParts = [];
+ if (wpRangeMalus !== 0) wpModParts.push(`${wpRangeMalus} ${game.i18n.localize("CDE.RangePenalty")}`);
+ if (wpBonusMalus !== 0) wpModParts.push(`${wpBonusMalus > 0 ? "+" : ""}${wpBonusMalus} ${game.i18n.localize("CDE.BonusMalus")}`);
+ if (wpWoundMalus !== 0) wpModParts.push(`-${wpWoundMalus} ${game.i18n.localize("CDE.WoundMalus")}`);
+ if (wpBonusAusp !== 0) wpModParts.push(`+${wpBonusAusp} ${game.i18n.localize("CDE.BonusAuspiciousDice")}`);
+ const wpMsg = await sendResultMessage(actor, {
+ rollLabel: `${wpItem.name}`,
+ aspectName: wpAspectName,
+ aspectLabel: game.i18n.localize(ASPECT_LABELS[wpAspectName] ?? ""),
+ aspectIcon: ASPECT_ICONS[wpAspectName] ?? "",
+ totalDice: wpTotalDice,
+ modifiersText: wpModParts.length ? wpModParts.join(" \xB7 ") : "",
+ spellPower: null,
+ rollDifficulty: null,
+ actorName: actor.name ?? "",
+ actorImg: actor.img ?? "",
+ // weapon-specific
+ weaponName: wpItem.name,
+ damageBase: wpDamageBase,
+ totalDamage: wpResults.successesdice * wpDamageBase,
+ ...wpResults,
+ aspect: wpAspectName,
+ d1: wpFaces[1],
+ d2: wpFaces[2],
+ d3: wpFaces[3],
+ d4: wpFaces[4],
+ d5: wpFaces[5],
+ d6: wpFaces[6],
+ d7: wpFaces[7],
+ d8: wpFaces[8],
+ d9: wpFaces[9],
+ d0: wpFaces[0]
+ }, wpRoll, ROLL_MODES[wpThrowMode] ?? "roll");
+ if (game.modules.get("dice-so-nice")?.active && wpMsg?.id) {
+ await game.dice3d.waitFor3DAnimationByMessageID(wpMsg.id);
+ }
+ if ((wpResults.loksyudice ?? 0) > 0) await updateLoksyuFromRoll(wpAspectName, wpFaces);
+ if ((wpResults.tinjidice ?? 0) > 0) await updateTinjiFromRoll(wpResults.tinjidice);
+ return;
+ }
+ default:
+ ui.notifications.warn(`Unknown roll type: ${typeLibel}`);
+ return;
+ }
+ if (numberofdice <= 0 && typeLibel !== "aspect" && typeLibel !== "itemkungfu" && !isMagic) {
+ ui.notifications.warn(game.i18n.localize("CDE.Error0"));
+ return;
+ }
+ const MAGIC_ASPECTS = {
+ internalcinnabar: 0,
+ // metal
+ alchemy: 1,
+ // water
+ masteryoftheway: 2,
+ // earth
+ exorcism: 3,
+ // fire
+ geomancy: 4
+ // wood
+ };
+ let defaultAspect = typeLibel === "aspect" ? ["metal", "water", "earth", "fire", "wood"].indexOf(skillLibel) : 0;
+ if (isMagic && MAGIC_ASPECTS[skillLibel] !== void 0) {
+ defaultAspect = MAGIC_ASPECTS[skillLibel];
+ }
+ if (kfDefaultAspect >= 0) {
+ defaultAspect = kfDefaultAspect;
+ }
+ let defaultSpecialAspect = 0;
+ if (isMagicSpecial && specialLibel) {
+ const specialCfg = MAGICS?.[skillLibel]?.speciality?.[specialLibel];
+ const aspectName = LABELELEMENT_TO_ASPECT[specialCfg?.labelelement];
+ if (aspectName) {
+ defaultSpecialAspect = ASPECT_NAMES.indexOf(aspectName);
+ }
+ }
+ let params;
+ if (isMagic) {
+ params = await showMagicPrompt({
+ title,
+ numberofdice,
+ aspectskill: defaultAspect,
+ bonusmalusskill: 0,
+ bonusauspiciousdice: 0,
+ aspectspeciality: defaultSpecialAspect,
+ rolldifficulty: 1,
+ bonusmalusspeciality: 0,
+ heispend: 0,
+ typeofthrow: typeOfThrow
+ });
+ } else {
+ params = await showSkillPrompt({
+ title,
+ numberofdice,
+ aspect: defaultAspect,
+ bonusmalus: 0,
+ woundmalus: 0,
+ bonusauspiciousdice: 0,
+ typeofthrow: typeOfThrow,
+ isSpecial
+ });
+ }
+ if (!params) return;
+ let aspectIndex, bonusMalus, bonusAuspicious, throwMode;
+ let spellAspectIndex = null;
+ let rollDifficulty = 1;
+ if (isMagic) {
+ const skillAspectIndex = Number(params.aspectskill ?? 0);
+ spellAspectIndex = Number(params.aspectspeciality ?? skillAspectIndex);
+ aspectIndex = skillAspectIndex;
+ bonusMalus = Number(params.bonusmalusskill ?? 0);
+ bonusAuspicious = Number(params.bonusauspiciousdice ?? 0);
+ rollDifficulty = Math.max(1, Number(params.rolldifficulty ?? 1));
+ throwMode = Number(params.typeofthrow ?? 0);
+ const aspectDice = sys.aspect?.[ASPECT_NAMES[aspectIndex]]?.value ?? 0;
+ const bonusSpec = Number(params.bonusmalusspeciality ?? 0);
+ const heiDice = Number(params.heispend ?? 0);
+ numberofdice = numberofdice + aspectDice + bonusMalus + 1 + bonusSpec + heiDice;
+ } else {
+ aspectIndex = Number(params.aspect ?? 0);
+ bonusMalus = Number(params.bonusmalus ?? 0);
+ const woundMalus = Number(params.woundmalus ?? 0);
+ bonusAuspicious = Number(params.bonusauspiciousdice ?? 0);
+ throwMode = Number(params.typeofthrow ?? 0);
+ const aspectDice = typeLibel !== "aspect" ? sys.aspect?.[ASPECT_NAMES[aspectIndex]]?.value ?? 0 : 0;
+ numberofdice = numberofdice + aspectDice + bonusMalus - woundMalus;
+ if (isSpecial) numberofdice += 1;
+ }
+ if (numberofdice <= 0) {
+ ui.notifications.warn(game.i18n.localize("CDE.Error0"));
+ return;
+ }
+ const roll = new Roll(`${numberofdice}d10`);
+ await roll.evaluate();
+ const rollModeKey = ROLL_MODES[throwMode] ?? "roll";
+ const wuXingAspectName = spellAspectIndex !== null ? ASPECT_NAMES[spellAspectIndex] : ASPECT_NAMES[aspectIndex];
+ const allResults = roll.dice[0]?.results ?? [];
+ const faces = countFaces(allResults);
+ const results = computeWuXingResults(faces, wuXingAspectName, bonusAuspicious);
+ if (!results) return;
+ const spellPower = isMagic ? results.successesdice * rollDifficulty : null;
+ const modParts = [];
+ if (isMagic) {
+ const bm = Number(params.bonusmalusskill ?? 0);
+ const bs = Number(params.bonusmalusspeciality ?? 0);
+ const hs = Number(params.heispend ?? 0);
+ const ba = Number(params.bonusauspiciousdice ?? 0);
+ if (bm !== 0) modParts.push(`${bm > 0 ? "+" : ""}${bm} ${game.i18n.localize("CDE.BonusMalus")}`);
+ if (bs !== 0) modParts.push(`${bs > 0 ? "+" : ""}${bs} ${game.i18n.localize("CDE.SpellBonus")}`);
+ if (ba !== 0) modParts.push(`+${ba} ${game.i18n.localize("CDE.BonusAuspiciousDice")}`);
+ if (hs !== 0) modParts.push(`${hs} ${game.i18n.localize("CDE.HeiSpend")}`);
+ if (rollDifficulty !== 1) modParts.push(`\xD7${rollDifficulty} ${game.i18n.localize("CDE.RollDifficulty")}`);
+ } else {
+ const bm = Number(params.bonusmalus ?? 0);
+ const wm = Number(params.woundmalus ?? 0);
+ const ba = Number(params.bonusauspiciousdice ?? 0);
+ if (bm !== 0) modParts.push(`${bm > 0 ? "+" : ""}${bm} ${game.i18n.localize("CDE.BonusMalus")}`);
+ if (wm !== 0) modParts.push(`-${wm} ${game.i18n.localize("CDE.WoundMalus")}`);
+ if (ba !== 0) modParts.push(`+${ba} ${game.i18n.localize("CDE.BonusAuspiciousDice")}`);
+ }
+ const msg = await sendResultMessage(actor, {
+ // Roll identity
+ rollLabel: title,
+ aspectName: wuXingAspectName,
+ aspectLabel: game.i18n.localize(ASPECT_LABELS[wuXingAspectName] ?? ""),
+ aspectIcon: ASPECT_ICONS[wuXingAspectName] ?? "",
+ totalDice: numberofdice,
+ modifiersText: modParts.length ? modParts.join(" \xB7 ") : "",
+ // Spell power (magic only)
+ spellPower,
+ rollDifficulty: isMagic ? rollDifficulty : null,
+ // Actor info
+ actorName: actor.name ?? "",
+ actorImg: actor.img ?? "",
+ // Wu Xing results
+ aspect: wuXingAspectName,
+ ...results,
+ // Die face counts
+ d1: faces[1],
+ d2: faces[2],
+ d3: faces[3],
+ d4: faces[4],
+ d5: faces[5],
+ d6: faces[6],
+ d7: faces[7],
+ d8: faces[8],
+ d9: faces[9],
+ d0: faces[0]
+ }, roll, rollModeKey);
+ if (game.modules.get("dice-so-nice")?.active && msg?.id) {
+ await game.dice3d.waitFor3DAnimationByMessageID(msg.id);
+ }
+ if ((results.loksyudice ?? 0) > 0) await updateLoksyuFromRoll(wuXingAspectName, faces);
+ if ((results.tinjidice ?? 0) > 0) await updateTinjiFromRoll(results.tinjidice);
+}
+
+// src/ui/sheets/actors/base.js
+var { HandlebarsApplicationMixin } = foundry.applications.api;
+var CDEBaseActorSheet = class _CDEBaseActorSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ActorSheetV2) {
+ static DEFAULT_OPTIONS = {
+ classes: ["fvtt-chroniques-de-l-etrange", "actor"],
+ position: { width: 920, height: 800 },
+ window: { resizable: true },
+ form: { submitOnChange: true },
+ dragDrop: [{ dragSelector: ".item, [data-drag='true']", dropSelector: null }],
+ actions: {
+ create: _CDEBaseActorSheet.#onItemCreate,
+ edit: _CDEBaseActorSheet.#onItemEdit,
+ delete: _CDEBaseActorSheet.#onItemDelete,
+ editImage: _CDEBaseActorSheet.#onEditImage
+ }
+ };
+ tabGroups = { primary: "description" };
+ get title() {
+ return this.document.name;
+ }
+ async _prepareContext() {
+ const descriptionHTML = await foundry.applications.ux.TextEditor.implementation.enrichHTML(this.document.system.description ?? "", { async: true });
+ const cssClass = this.options.classes?.join(" ") ?? "";
+ return {
+ actor: this.document,
+ system: this.document.system,
+ systemData: this.document.system,
+ systemFields: this.document.system.schema.fields,
+ items: this.document.items.contents,
+ descriptionHTML,
+ editable: this.isEditable,
+ cssClass
+ };
+ }
+ // Restore the active tab after every render (including re-renders from submitOnChange).
+ // AppV2 does NOT preserve tab state natively — we must re-apply it from this.tabGroups,
+ // which is dynamically updated by changeTab() when the user clicks a tab.
+ _onRender(context, options) {
+ super._onRender?.(context, options);
+ for (const [group, tab] of Object.entries(this.tabGroups)) {
+ this.changeTab(tab, group, { force: true });
+ }
+ }
+ static async #onItemCreate(event, target) {
+ const type = target.dataset.type ?? "item";
+ const cls = getDocumentClass("Item");
+ const labels = {
+ item: "CDE.ItemNew",
+ weapon: "CDE.WeaponNew",
+ armor: "CDE.ArmorNew",
+ sanhei: "CDE.SanheiNew",
+ ingredient: "CDE.IngredientNew",
+ kungfu: "CDE.KFNew",
+ spell: "CDE.SpellNew",
+ supernatural: "CDE.SupernaturalNew"
+ };
+ const name = game.i18n.localize(labels[type] ?? "CDE.ItemNew");
+ const systemData = {};
+ if (type === "spell" && target.dataset.discipline) {
+ systemData.discipline = target.dataset.discipline;
+ }
+ return cls.create({ name, type, system: systemData }, { parent: this.document });
+ }
+ static #onItemEdit(event, target) {
+ const itemId = target.dataset.itemId ?? target.closest("[data-item-id]")?.dataset.itemId;
+ const item = this.document.items.get(itemId);
+ if (item) item.sheet.render(true);
+ }
+ static #onItemDelete(event, target) {
+ const itemId = target.dataset.itemId ?? target.closest("[data-item-id]")?.dataset.itemId;
+ const item = this.document.items.get(itemId);
+ if (item) item.delete();
+ }
+ static async #onEditImage(event, target) {
+ const attr = target.dataset.edit;
+ const current = foundry.utils.getProperty(this.document, attr);
+ const { img } = this.document.constructor.getDefaultArtwork?.(this.document.toObject()) ?? {};
+ const fp = new FilePicker({
+ current,
+ type: "image",
+ redirectToRoot: img ? [img] : [],
+ callback: (path) => this.document.update({ [attr]: path }),
+ top: this.position.top + 40,
+ left: this.position.left + 10
+ });
+ return fp.browse();
+ }
+};
+
+// src/ui/sheets/actors/character.js
+var CDECharacterSheet = class extends CDEBaseActorSheet {
+ static DEFAULT_OPTIONS = {
+ classes: ["character"]
+ };
+ static PARTS = {
+ main: { template: "systems/fvtt-chroniques-de-l-etrange/templates/actor/cde-character-sheet.html" }
+ };
+ tabGroups = { primary: "description" };
+ async _prepareContext() {
+ const context = await super._prepareContext();
+ context.equipments = context.items.filter((item) => item.type === "item");
+ context.weapons = context.items.filter((item) => item.type === "weapon");
+ context.armors = context.items.filter((item) => item.type === "armor");
+ context.sanheis = context.items.filter((item) => item.type === "sanhei");
+ context.ingredients = context.items.filter((item) => item.type === "ingredient");
+ context.spells = context.items.filter((item) => item.type === "spell");
+ context.kungfus = context.items.filter((item) => item.type === "kungfu");
+ context.CDE = { MAGICS, SUBTYPES };
+ const spellsByDiscipline = {};
+ for (const spell of context.spells) {
+ const disc = spell.system?.discipline ?? "internalcinnabar";
+ if (!spellsByDiscipline[disc]) spellsByDiscipline[disc] = [];
+ spellsByDiscipline[disc].push(spell);
+ }
+ const systemMagics = context.systemData.magics ?? {};
+ context.magicsDisplay = Object.fromEntries(
+ Object.entries(MAGICS).map(([magicKey, magicDef]) => {
+ const magicData = systemMagics[magicKey] ?? {};
+ return [
+ magicKey,
+ {
+ value: magicData.value ?? 0,
+ visible: magicData.visible ?? false,
+ speciality: Object.fromEntries(
+ Object.keys(magicDef.speciality).map((specKey) => [
+ specKey,
+ { check: magicData.speciality?.[specKey]?.check ?? false }
+ ])
+ ),
+ grimoire: spellsByDiscipline[magicKey] ?? []
+ }
+ ];
+ })
+ );
+ return context;
+ }
+ _onRender(context, options) {
+ super._onRender(context, options);
+ this.#bindInitiativeControls();
+ this.#bindPrefs();
+ this.#bindRollButtons();
+ this.#bindComponentRandomize();
+ }
+ #bindInitiativeControls() {
+ const buttons = this.element?.querySelectorAll(".click-initiative");
+ if (!buttons?.length) return;
+ buttons.forEach((button) => {
+ button.addEventListener("click", async () => {
+ const action = button.dataset.libelId;
+ let initiative = this.document.system.initiative ?? 1;
+ if (action === "plus") {
+ initiative = initiative >= 24 ? 1 : initiative + 1;
+ await this.document.update({ "system.initiative": initiative });
+ return;
+ }
+ if (action === "minus") {
+ initiative = initiative <= 1 ? 24 : initiative - 1;
+ await this.document.update({ "system.initiative": initiative });
+ return;
+ }
+ if (action === "create") {
+ await rollInitiativePC(this.document);
+ }
+ });
+ });
+ }
+ #bindPrefs() {
+ const button = this.element?.querySelector(".click-prefs");
+ if (!button) return;
+ button.addEventListener("click", async () => {
+ const current = this.document.system.prefs?.typeofthrow ?? { choice: "0", check: true };
+ const html = `
+
`;
+ const prefs = await foundry.applications.api.DialogV2.prompt({
+ window: { title: game.i18n.localize("CDE.Preferences") },
+ content: html,
+ rejectClose: false,
+ ok: {
+ label: game.i18n.localize("CDE.Validate"),
+ callback: (_ev, _btn, dialog) => {
+ const root = dialog.element ?? dialog;
+ const choice = root.querySelector("select[name='choice']")?.value ?? "0";
+ const check = root.querySelector("input[name='check']")?.checked ?? false;
+ return { choice, check };
+ }
+ }
+ });
+ if (prefs) {
+ await this.document.update({
+ "system.prefs.typeofthrow.choice": String(prefs.choice),
+ "system.prefs.typeofthrow.check": !!prefs.check
+ });
+ }
+ });
+ }
+ #bindRollButtons() {
+ const cells = this.element?.querySelectorAll("td.click[data-libel-id], td.click2[data-libel-id], .cde-roll-trigger[data-libel-id]");
+ if (!cells?.length) return;
+ cells.forEach((cell) => {
+ cell.addEventListener("click", (event) => {
+ event.preventDefault();
+ const rollKey = cell.dataset.libelId;
+ if (rollKey) rollForActor(this.document, rollKey);
+ });
+ });
+ }
+ #bindComponentRandomize() {
+ const btn = this.element?.querySelector("[data-action='randomize-component']");
+ if (!btn) return;
+ btn.addEventListener("click", async () => {
+ const roll = new Roll("1d10");
+ await roll.evaluate();
+ const face = roll.total === 10 ? 0 : roll.total;
+ const COMPONENT_KEYS = {
+ 1: "one",
+ 2: "two",
+ 3: "three",
+ 4: "four",
+ 5: "five",
+ 6: "six",
+ 7: "seven",
+ 8: "eight",
+ 9: "nine",
+ 0: "zero"
+ };
+ const componentKey = COMPONENT_KEYS[face];
+ const componentValue = this.document.system.component?.[componentKey]?.value ?? "";
+ const label = componentValue ? `${componentValue}` : `${game.i18n.localize("CDE.Component")}${face}`;
+ const content = `
+
+ ${game.i18n.localize("CDE.ChanceThrowResult")}
+ ${label}
+
`;
+ await ChatMessage.create({
+ user: game.user.id,
+ speaker: ChatMessage.getSpeaker({ actor: this.document }),
+ content,
+ rolls: [roll],
+ rollMode: game.settings.get("core", "rollMode") ?? "roll"
+ });
+ });
+ }
+};
+
+// src/ui/sheets/actors/npc.js
+var CDENpcSheet = class extends CDEBaseActorSheet {
+ static DEFAULT_OPTIONS = {
+ classes: ["npc"]
+ };
+ static PARTS = {
+ main: { template: "systems/fvtt-chroniques-de-l-etrange/templates/actor/cde-npc-sheet.html" }
+ };
+ tabGroups = { primary: "description" };
+ async _prepareContext() {
+ const context = await super._prepareContext();
+ context.supernaturals = context.items.filter((item) => item.type === "supernatural");
+ context.spells = context.items.filter((item) => item.type === "spell");
+ context.kungfus = context.items.filter((item) => item.type === "kungfu");
+ context.weapons = context.items.filter((item) => item.type === "weapon");
+ context.armors = context.items.filter((item) => item.type === "armor");
+ context.equipments = context.items.filter((item) => item.type === "item");
+ return context;
+ }
+ _onRender(context, options) {
+ super._onRender(context, options);
+ this.#bindInitiativeControls();
+ this.#bindRollButtons();
+ }
+ #bindRollButtons() {
+ const cells = this.element?.querySelectorAll(".cde-roll-trigger[data-libel-id]");
+ if (!cells?.length) return;
+ cells.forEach((cell) => {
+ cell.addEventListener("click", (event) => {
+ event.preventDefault();
+ const rollKey = cell.dataset.libelId;
+ if (rollKey) rollForActor(this.document, rollKey);
+ });
+ });
+ }
+ #bindInitiativeControls() {
+ const buttons = this.element?.querySelectorAll(".click-initiative-npc");
+ if (!buttons?.length) return;
+ buttons.forEach((button) => {
+ button.addEventListener("click", async () => {
+ const action = button.dataset.libelId;
+ let initiative = this.document.system.initiative ?? 1;
+ if (action === "plus") {
+ initiative = initiative >= 24 ? 1 : initiative + 1;
+ await this.document.update({ "system.initiative": initiative });
+ return;
+ }
+ if (action === "minus") {
+ initiative = initiative <= 1 ? 24 : initiative - 1;
+ await this.document.update({ "system.initiative": initiative });
+ return;
+ }
+ if (action === "create") {
+ await rollInitiativeNPC(this.document);
+ }
+ });
+ });
+ }
+};
+
+// src/ui/sheets/items/base.js
+var { HandlebarsApplicationMixin: HandlebarsApplicationMixin2 } = foundry.applications.api;
+var CDEBaseItemSheet = class _CDEBaseItemSheet extends HandlebarsApplicationMixin2(foundry.applications.sheets.ItemSheetV2) {
+ static DEFAULT_OPTIONS = {
+ classes: ["fvtt-chroniques-de-l-etrange", "item"],
+ position: { width: 520, height: "auto" },
+ window: { resizable: true },
+ form: { submitOnChange: true },
+ actions: {
+ editImage: _CDEBaseItemSheet.#onEditImage
+ }
+ };
+ tabGroups = { primary: "details" };
+ get title() {
+ return this.document.name;
+ }
+ async _prepareContext() {
+ const cssClass = this.options.classes?.join(" ") ?? "";
+ const enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML(this.document.system.description ?? "", { async: true });
+ const enrichedNotes = await foundry.applications.ux.TextEditor.implementation.enrichHTML(this.document.system.notes ?? "", { async: true });
+ return {
+ item: this.document,
+ system: this.document.system,
+ systemData: this.document.system,
+ systemFields: this.document.system.schema.fields,
+ editable: this.isEditable,
+ cssClass,
+ enrichedDescription,
+ enrichedNotes,
+ descriptionHTML: enrichedDescription,
+ notesHTML: enrichedNotes
+ };
+ }
+ // Restore the active tab after every render (including re-renders from submitOnChange).
+ _onRender(context, options) {
+ super._onRender?.(context, options);
+ for (const [group, tab] of Object.entries(this.tabGroups)) {
+ this.changeTab(tab, group, { force: true });
+ }
+ }
+ static async #onEditImage(event, target) {
+ const attr = target.dataset.edit;
+ const current = foundry.utils.getProperty(this.document, attr);
+ const { img } = this.document.constructor.getDefaultArtwork?.(this.document.toObject()) ?? {};
+ const fp = new FilePicker({
+ current,
+ type: "image",
+ redirectToRoot: img ? [img] : [],
+ callback: (path) => this.document.update({ [attr]: path }),
+ top: this.position.top + 40,
+ left: this.position.left + 10
+ });
+ return fp.browse();
+ }
+};
+
+// src/ui/sheets/items/item.js
+var CDEItemSheet = class extends CDEBaseItemSheet {
+ static DEFAULT_OPTIONS = {
+ classes: ["equipment"],
+ position: { width: 560, height: 460 }
+ };
+ static PARTS = {
+ main: { template: "systems/fvtt-chroniques-de-l-etrange/templates/item/cde-item-sheet.html" }
+ };
+};
+
+// src/ui/sheets/items/kungfu.js
+var CDEKungfuSheet = class extends CDEBaseItemSheet {
+ static DEFAULT_OPTIONS = {
+ classes: ["kungfu"],
+ position: { width: 720, height: 680 }
+ };
+ static PARTS = {
+ main: { template: "systems/fvtt-chroniques-de-l-etrange/templates/item/cde-kungfu-sheet.html" }
+ };
+ async _prepareContext() {
+ const context = await super._prepareContext();
+ const techniques = this.document.system.techniques ?? {};
+ const enrich = (value) => foundry.applications.ux.TextEditor.implementation.enrichHTML(value ?? "", { async: true });
+ context.descriptionTechnique1HTML = await enrich(techniques.technique1?.technique);
+ context.descriptionTechnique2HTML = await enrich(techniques.technique2?.technique);
+ context.descriptionTechnique3HTML = await enrich(techniques.technique3?.technique);
+ return context;
+ }
+};
+
+// src/ui/sheets/items/spell.js
+var CDESpellSheet = class extends CDEBaseItemSheet {
+ static DEFAULT_OPTIONS = {
+ classes: ["spell"],
+ position: { width: 660, height: 680 }
+ };
+ static PARTS = {
+ main: { template: "systems/fvtt-chroniques-de-l-etrange/templates/item/cde-spell-sheet.html" }
+ };
+ async _prepareContext() {
+ const context = await super._prepareContext();
+ const enrich = (content) => foundry.applications.ux.TextEditor.implementation.enrichHTML(content ?? "", { async: true });
+ context.spellDescriptionHTML = await enrich(this.document.system.description);
+ context.componentsDescriptionHTML = await enrich(this.document.system.components);
+ context.effectsDescriptionHTML = await enrich(this.document.system.effects);
+ context.examplesDescriptionHTML = await enrich(this.document.system.examples);
+ return context;
+ }
+};
+
+// src/ui/sheets/items/supernatural.js
+var CDESupernaturalSheet = class extends CDEBaseItemSheet {
+ static DEFAULT_OPTIONS = {
+ classes: ["supernatural"],
+ position: { width: 560, height: 520 }
+ };
+ static PARTS = {
+ main: { template: "systems/fvtt-chroniques-de-l-etrange/templates/item/cde-supernatural-sheet.html" }
+ };
+ async _prepareContext() {
+ const context = await super._prepareContext();
+ const enrich = (content) => foundry.applications.ux.TextEditor.implementation.enrichHTML(content ?? "", { async: true });
+ context.effectsHTML = await enrich(this.document.system.effects);
+ return context;
+ }
+};
+
+// src/ui/sheets/items/weapon.js
+var CDEWeaponSheet = class extends CDEBaseItemSheet {
+ static DEFAULT_OPTIONS = {
+ classes: ["weapon"],
+ position: { width: 580, height: 520 }
+ };
+ static PARTS = {
+ main: { template: "systems/fvtt-chroniques-de-l-etrange/templates/item/cde-weapon-sheet.html" }
+ };
+};
+
+// src/ui/sheets/items/armor.js
+var CDEArmorSheet = class extends CDEBaseItemSheet {
+ static DEFAULT_OPTIONS = {
+ classes: ["armor"],
+ position: { width: 520, height: 460 }
+ };
+ static PARTS = {
+ main: { template: "systems/fvtt-chroniques-de-l-etrange/templates/item/cde-armor-sheet.html" }
+ };
+};
+
+// src/ui/sheets/items/sanhei.js
+var CDESanheiSheet = class extends CDEBaseItemSheet {
+ static DEFAULT_OPTIONS = {
+ classes: ["sanhei"],
+ position: { width: 580, height: 620 }
+ };
+ static PARTS = {
+ main: { template: "systems/fvtt-chroniques-de-l-etrange/templates/item/cde-sanhei-sheet.html" }
+ };
+ async _prepareContext() {
+ const context = await super._prepareContext();
+ const enrich = (content) => foundry.applications.ux.TextEditor.implementation.enrichHTML(content ?? "", { async: true });
+ const props = this.document.system.properties;
+ context.prop1DescriptionHTML = await enrich(props.prop1.description);
+ context.prop2DescriptionHTML = await enrich(props.prop2.description);
+ context.prop3DescriptionHTML = await enrich(props.prop3.description);
+ context.propFields = this.document.system.schema.fields.properties.fields;
+ return context;
+ }
+};
+
+// src/ui/sheets/items/ingredient.js
+var CDEIngredientSheet = class extends CDEBaseItemSheet {
+ static DEFAULT_OPTIONS = {
+ classes: ["ingredient"],
+ position: { width: 520, height: 460 }
+ };
+ static PARTS = {
+ main: { template: "systems/fvtt-chroniques-de-l-etrange/templates/item/cde-ingredient-sheet.html" }
+ };
+};
+
+// src/ui/apps/loksyu-app.js
+var CDELoksyuApp = class _CDELoksyuApp extends foundry.applications.api.HandlebarsApplicationMixin(
+ foundry.applications.api.ApplicationV2
+) {
+ static DEFAULT_OPTIONS = {
+ id: "cde-loksyu-app",
+ tag: "div",
+ window: {
+ title: "CDE.Loksyu",
+ icon: "fas fa-yin-yang",
+ resizable: false
+ },
+ classes: ["cde-app", "cde-loksyu-standalone"],
+ position: { width: 520, height: "auto" },
+ actions: {
+ resetElement: _CDELoksyuApp.#onResetElement,
+ resetAll: _CDELoksyuApp.#onResetAll
+ }
+ };
+ static PARTS = {
+ main: {
+ template: `systems/${SYSTEM_ID}/templates/apps/cde-loksyu-app.html`
+ }
+ };
+ /** @type {Function|null} bound hook handler */
+ _updateHook = null;
+ /** Singleton accessor — open or bring to front */
+ static open() {
+ const existing = Array.from(foundry.applications.instances.values()).find(
+ (app2) => app2 instanceof _CDELoksyuApp
+ );
+ if (existing) {
+ existing.bringToFront();
+ return existing;
+ }
+ const app = new _CDELoksyuApp();
+ app.render(true);
+ return app;
+ }
+ async _prepareContext() {
+ const sys = getLoksyuData();
+ const ELEMENTS = [
+ { key: "wood", nameKey: "CDE.Wood", qualKey: "CDE.WoodQualities", img: `systems/${SYSTEM_ID}/images/cde_bois.webp` },
+ { key: "fire", nameKey: "CDE.Fire", qualKey: "CDE.FireQualities", img: `systems/${SYSTEM_ID}/images/cde_feu.webp` },
+ { key: "earth", nameKey: "CDE.Earth", qualKey: "CDE.EarthQualities", img: `systems/${SYSTEM_ID}/images/cde_terre.webp` },
+ { key: "metal", nameKey: "CDE.Metal", qualKey: "CDE.MetalQualities", img: `systems/${SYSTEM_ID}/images/cde_metal.webp` },
+ { key: "water", nameKey: "CDE.Water", qualKey: "CDE.WaterQualities", img: `systems/${SYSTEM_ID}/images/cde_eau.webp` }
+ ];
+ return {
+ canEdit: game.user.isGM,
+ elements: ELEMENTS.map((el) => ({
+ ...el,
+ yang: sys[el.key]?.yang ?? 0,
+ yin: sys[el.key]?.yin ?? 0
+ }))
+ };
+ }
+ _onRender(context, options) {
+ super._onRender(context, options);
+ this.#bindInputs();
+ this._updateHook = Hooks.on("cde:loksyuUpdated", () => this.render());
+ }
+ _onClose(options) {
+ if (this._updateHook !== null) {
+ Hooks.off("cde:loksyuUpdated", this._updateHook);
+ this._updateHook = null;
+ }
+ super._onClose(options);
+ }
+ #bindInputs() {
+ const inputs = this.element?.querySelectorAll("input[data-field]");
+ if (!inputs?.length) return;
+ inputs.forEach((input) => {
+ input.addEventListener("change", async (ev) => {
+ const field = ev.currentTarget.dataset.field;
+ const val = parseInt(ev.currentTarget.value, 10);
+ if (!field || isNaN(val)) return;
+ const [aspect, dim] = field.split(".");
+ if (!aspect || !dim) return;
+ const data = getLoksyuData();
+ if (!data[aspect]) data[aspect] = { yin: 0, yang: 0 };
+ data[aspect][dim] = Math.max(0, val);
+ await setLoksyuData(data);
+ });
+ });
+ }
+ static async #onResetElement(event, target) {
+ const key = target.dataset.element;
+ if (!key) return;
+ const data = getLoksyuData();
+ data[key] = { yin: 0, yang: 0 };
+ await setLoksyuData(data);
+ }
+ static async #onResetAll(_event, _target) {
+ const KEYS = ["wood", "fire", "earth", "metal", "water"];
+ const data = getLoksyuData();
+ for (const k of KEYS) data[k] = { yin: 0, yang: 0 };
+ await setLoksyuData(data);
+ }
+};
+
+// src/ui/apps/tinji-app.js
+var CDETinjiApp = class _CDETinjiApp extends foundry.applications.api.HandlebarsApplicationMixin(
+ foundry.applications.api.ApplicationV2
+) {
+ static DEFAULT_OPTIONS = {
+ id: "cde-tinji-app",
+ tag: "div",
+ window: {
+ title: "CDE.TinJi2",
+ icon: "fas fa-star",
+ resizable: false
+ },
+ classes: ["cde-app", "cde-tinji-standalone"],
+ position: { width: 320, height: "auto" },
+ actions: {
+ increment: _CDETinjiApp.#onIncrement,
+ decrement: _CDETinjiApp.#onDecrement,
+ reset: _CDETinjiApp.#onReset,
+ spend: _CDETinjiApp.#onSpend
+ }
+ };
+ static PARTS = {
+ main: {
+ template: `systems/${SYSTEM_ID}/templates/apps/cde-tinji-app.html`
+ }
+ };
+ /** @type {Function|null} */
+ _updateHook = null;
+ static open() {
+ const existing = Array.from(foundry.applications.instances.values()).find(
+ (app2) => app2 instanceof _CDETinjiApp
+ );
+ if (existing) {
+ existing.bringToFront();
+ return existing;
+ }
+ const app = new _CDETinjiApp();
+ app.render(true);
+ return app;
+ }
+ async _prepareContext() {
+ return {
+ canEdit: game.user.isGM,
+ value: getTinjiValue()
+ };
+ }
+ _onRender(context, options) {
+ super._onRender(context, options);
+ this.#bindDirectInput();
+ this._updateHook = Hooks.on("cde:tinjiUpdated", () => this.render());
+ }
+ _onClose(options) {
+ if (this._updateHook !== null) {
+ Hooks.off("cde:tinjiUpdated", this._updateHook);
+ this._updateHook = null;
+ }
+ super._onClose(options);
+ }
+ #bindDirectInput() {
+ const input = this.element?.querySelector("input.cde-tinji-direct");
+ if (!input) return;
+ input.addEventListener("change", async (ev) => {
+ const val = parseInt(ev.currentTarget.value, 10);
+ if (!isNaN(val)) await setTinjiValue(val);
+ });
+ }
+ static async #onIncrement() {
+ await setTinjiValue(getTinjiValue() + 1);
+ }
+ static async #onDecrement() {
+ const current = getTinjiValue();
+ if (current <= 0) return;
+ await setTinjiValue(current - 1);
+ }
+ static async #onReset() {
+ await setTinjiValue(0);
+ }
+ static async #onSpend() {
+ const current = getTinjiValue();
+ if (current <= 0) {
+ ui.notifications.warn(game.i18n.localize("CDE.TinjiEmpty"));
+ return;
+ }
+ await setTinjiValue(current - 1);
+ ChatMessage.create({
+ user: game.user.id,
+ content: `
+
+ ${game.i18n.localize("CDE.TinJi2")}
+ ${game.i18n.format("CDE.TinjiSpent", { name: game.user.name })}
+ (${current - 1} ${game.i18n.localize("CDE.TinjiRemaining")})
+
`
+ });
+ }
+};
+
+// src/ui/roll-actions.js
+var RESULT_TEMPLATE3 = "systems/fvtt-chroniques-de-l-etrange/templates/form/cde-dice-result.html";
+function injectRollActions(message, html) {
+ const rollCard = html.querySelector(".cde-roll-result");
+ if (!rollCard) return;
+ const aspect = rollCard.dataset.aspect;
+ if (!aspect || !WU_XING_CYCLE[aspect]) return;
+ refreshRollActions(rollCard, aspect, message);
+}
+function refreshRollActions(rollCard, aspect, message) {
+ rollCard.querySelector(".cde-roll-actions")?.remove();
+ const cycle = WU_XING_CYCLE[aspect];
+ const fasteAspect = cycle[1];
+ const loksyu = getLoksyuData();
+ const tinji = getTinjiValue();
+ const successAvail = (loksyu[aspect]?.yin ?? 0) + (loksyu[aspect]?.yang ?? 0);
+ const fasteAvail = (loksyu[fasteAspect]?.yin ?? 0) + (loksyu[fasteAspect]?.yang ?? 0);
+ const isGM = game.user.isGM;
+ const hasSomething = successAvail > 0 || fasteAvail > 0 || isGM && tinji > 0;
+ if (!hasSomething) return;
+ const aspLabel = game.i18n.localize(ASPECT_LABELS[aspect]);
+ const fasteLabel = game.i18n.localize(ASPECT_LABELS[fasteAspect]);
+ let btns = "";
+ if (successAvail > 0) {
+ btns += ``;
+ }
+ if (fasteAvail > 0) {
+ btns += ``;
+ }
+ if (isGM && tinji > 0) {
+ btns += ``;
+ }
+ const wrapper = document.createElement("div");
+ wrapper.className = "cde-roll-actions";
+ wrapper.innerHTML = `
+
+
+ ${game.i18n.localize("CDE.PostRollActions")}
+
+ ${btns}
+ `;
+ rollCard.appendChild(wrapper);
+ wrapper.addEventListener("click", async (ev) => {
+ const btn = ev.target.closest("[data-action]");
+ if (!btn || btn.disabled) return;
+ const action = btn.dataset.action;
+ if (action === "loksyu-success") {
+ await _drawFromLoksyu(message, aspect, "success", aspLabel);
+ } else if (action === "loksyu-faste") {
+ await _drawFromLoksyu(message, fasteAspect, "faste", fasteLabel);
+ } else if (action === "tinji") {
+ await _spendTinjiPostRoll();
+ }
+ if (action === "tinji") refreshRollActions(rollCard, aspect, message);
+ });
+}
+async function _drawFromLoksyu(message, aspect, type, aspectLabel) {
+ const data = getLoksyuData();
+ const entry = data[aspect] ?? { yin: 0, yang: 0 };
+ const total = entry.yin + entry.yang;
+ if (total <= 0) {
+ ui.notifications.warn(game.i18n.localize("CDE.LoksyuEmpty"));
+ return;
+ }
+ if (entry.yang > 0) entry.yang--;
+ else entry.yin--;
+ data[aspect] = entry;
+ await setLoksyuData(data);
+ const flags = message?.flags?.[SYSTEM_ID];
+ if (flags?.rollResult && message.isOwner) {
+ const updated = foundry.utils.deepClone(flags.rollResult);
+ if (type === "success") {
+ updated.successesdice = (updated.successesdice ?? 0) + 1;
+ updated.loksyuBonusSuc = (updated.loksyuBonusSuc ?? 0) + 1;
+ if (updated.damageBase) updated.totalDamage = updated.successesdice * updated.damageBase;
+ } else {
+ updated.auspiciousdice = (updated.auspiciousdice ?? 0) + 1;
+ updated.loksyuBonusFaste = (updated.loksyuBonusFaste ?? 0) + 1;
+ }
+ const newHtml = await foundry.applications.handlebars.renderTemplate(RESULT_TEMPLATE3, updated);
+ await message.update({
+ content: newHtml,
+ [`flags.${SYSTEM_ID}.rollResult`]: updated
+ });
+ }
+ const remain = entry.yin + entry.yang;
+ const typeLabel = type === "success" ? game.i18n.localize("CDE.Successes") : game.i18n.localize("CDE.AuspiciousDie");
+ ChatMessage.create({
+ user: game.user.id,
+ content: `
+
+
+
`
+ });
+}
+async function _spendTinjiPostRoll() {
+ if (!game.user.isGM) return;
+ const current = getTinjiValue();
+ if (current <= 0) {
+ ui.notifications.warn(game.i18n.localize("CDE.TinjiEmpty"));
+ return;
+ }
+ await setTinjiValue(current - 1);
+ ChatMessage.create({
+ user: game.user.id,
+ content: `
+ \u5929
+
+ ${game.user.name} ${game.i18n.localize("CDE.TinjiSpent").replace("{name}", game.user.name)}
+
+ (${current - 1} ${game.i18n.localize("CDE.TinjiRemaining")})
+
`
+ });
+}
+function refreshAllRollActions() {
+ document.querySelectorAll(".chat-message .cde-roll-result[data-aspect]").forEach((card) => {
+ const aspect = card.dataset.aspect;
+ if (!aspect || !WU_XING_CYCLE[aspect]) return;
+ const msgEl = card.closest("[data-message-id]");
+ const msgId = msgEl?.dataset?.messageId;
+ const message = msgId ? game.messages.get(msgId) : null;
+ refreshRollActions(card, aspect, message);
+ });
+}
+
+// src/system.js
+Hooks.once("i18nInit", preLocalizeConfig);
+Hooks.once("init", async () => {
+ console.info(`CHRONIQUESDELETRANGE | Initializing ${SYSTEM_ID}`);
+ registerSettings();
+ game.system.CONST = { MAGICS, SUBTYPES };
+ game.cde = { CDELoksyuApp, CDETinjiApp };
+ CONFIG.Actor.dataModels = {
+ [ACTOR_TYPES.character]: CharacterDataModel,
+ [ACTOR_TYPES.npc]: NpcDataModel
+ };
+ CONFIG.Item.dataModels = {
+ [ITEM_TYPES.item]: EquipmentDataModel,
+ [ITEM_TYPES.kungfu]: KungfuDataModel,
+ [ITEM_TYPES.spell]: SpellDataModel,
+ [ITEM_TYPES.supernatural]: SupernaturalDataModel,
+ [ITEM_TYPES.weapon]: WeaponDataModel,
+ [ITEM_TYPES.armor]: ArmorDataModel,
+ [ITEM_TYPES.sanhei]: SanheiDataModel,
+ [ITEM_TYPES.ingredient]: IngredientDataModel
+ };
+ CONFIG.Actor.documentClass = CDEActor;
+ CONFIG.Item.documentClass = CDEItem;
+ CONFIG.ChatMessage.documentClass = CDEMessage;
+ configureRuntime();
+ foundry.applications.apps.DocumentSheetConfig.unregisterSheet(Actor, "core", foundry.appv1.sheets.ActorSheet);
+ foundry.applications.apps.DocumentSheetConfig.unregisterSheet(Item, "core", foundry.appv1.sheets.ItemSheet);
+ foundry.applications.apps.DocumentSheetConfig.registerSheet(Actor, SYSTEM_ID, CDECharacterSheet, {
+ types: [ACTOR_TYPES.character],
+ makeDefault: true,
+ label: "CDE Character Sheet (V2)"
+ });
+ foundry.applications.apps.DocumentSheetConfig.registerSheet(Actor, SYSTEM_ID, CDENpcSheet, {
+ types: [ACTOR_TYPES.npc],
+ makeDefault: true,
+ label: "CDE NPC Sheet (V2)"
+ });
+ foundry.applications.apps.DocumentSheetConfig.registerSheet(Item, SYSTEM_ID, CDEItemSheet, {
+ types: [ITEM_TYPES.item],
+ makeDefault: true,
+ label: "CDE Item Sheet (V2)"
+ });
+ foundry.applications.apps.DocumentSheetConfig.registerSheet(Item, SYSTEM_ID, CDEKungfuSheet, {
+ types: [ITEM_TYPES.kungfu],
+ makeDefault: true,
+ label: "CDE KungFu Sheet (V2)"
+ });
+ foundry.applications.apps.DocumentSheetConfig.registerSheet(Item, SYSTEM_ID, CDESpellSheet, {
+ types: [ITEM_TYPES.spell],
+ makeDefault: true,
+ label: "CDE Spell Sheet (V2)"
+ });
+ foundry.applications.apps.DocumentSheetConfig.registerSheet(Item, SYSTEM_ID, CDESupernaturalSheet, {
+ types: [ITEM_TYPES.supernatural],
+ makeDefault: true,
+ label: "CDE Supernatural Sheet (V2)"
+ });
+ foundry.applications.apps.DocumentSheetConfig.registerSheet(Item, SYSTEM_ID, CDEWeaponSheet, {
+ types: [ITEM_TYPES.weapon],
+ makeDefault: true,
+ label: "CDE Weapon Sheet (V2)"
+ });
+ foundry.applications.apps.DocumentSheetConfig.registerSheet(Item, SYSTEM_ID, CDEArmorSheet, {
+ types: [ITEM_TYPES.armor],
+ makeDefault: true,
+ label: "CDE Armor Sheet (V2)"
+ });
+ foundry.applications.apps.DocumentSheetConfig.registerSheet(Item, SYSTEM_ID, CDESanheiSheet, {
+ types: [ITEM_TYPES.sanhei],
+ makeDefault: true,
+ label: "CDE Sanhei Sheet (V2)"
+ });
+ foundry.applications.apps.DocumentSheetConfig.registerSheet(Item, SYSTEM_ID, CDEIngredientSheet, {
+ types: [ITEM_TYPES.ingredient],
+ makeDefault: true,
+ label: "CDE Ingredient Sheet (V2)"
+ });
+ await preloadPartials();
+ registerHandlebarsHelpers();
+ registerDice();
+ console.info(`CHRONIQUESDELETRANGE | Initialized`);
+});
+Hooks.once("ready", async () => {
+ await migrateIfNeeded();
+});
+Hooks.on("renderChatLog", (_app, html) => {
+ const el = html instanceof HTMLElement ? html : html[0] ?? html;
+ if (!el?.querySelector) return;
+ if (el.querySelector(".cde-chat-app-buttons")) return;
+ const wrapper = document.createElement("div");
+ wrapper.classList.add("cde-chat-app-buttons");
+ wrapper.innerHTML = `
+
+
+ `;
+ wrapper.addEventListener("click", (ev) => {
+ if (ev.target.closest(".cde-chat-btn--loksyu")) CDELoksyuApp.open();
+ if (ev.target.closest(".cde-chat-btn--tinji")) CDETinjiApp.open();
+ });
+ const anchor = el.querySelector(".chat-form") ?? el.querySelector(".chat-message-form") ?? el.querySelector("form");
+ if (anchor) anchor.parentElement.insertBefore(wrapper, anchor);
+ else el.appendChild(wrapper);
+});
+Hooks.on("renderChatMessageHTML", (message, html) => {
+ injectRollActions(message, html);
+});
+Hooks.on("updateSetting", (setting) => {
+ if (!setting.key) return;
+ if (setting.key.includes("loksyuData") || setting.key.includes("tinjiData")) {
+ refreshAllRollActions();
+ }
+});
+/**
+ * Chroniques de l'Étrange — Système FoundryVTT
+ *
+ * Chroniques de l'Étrange est un jeu de rôle édité par Antre-Monde Éditions.
+ * Ce système FoundryVTT est une implémentation indépendante et n'est pas
+ * affilié à Antre-Monde Éditions.
+ *
+ * @author LeRatierBretonnien
+ * @copyright 2024–2026 LeRatierBretonnien
+ * @license CC BY-NC-SA 4.0 – https://creativecommons.org/licenses/by-nc-sa/4.0/
+ */
+//# sourceMappingURL=system.js.map
diff --git a/dist/system.js.map b/dist/system.js.map
new file mode 100644
index 0000000..57336b5
--- /dev/null
+++ b/dist/system.js.map
@@ -0,0 +1,7 @@
+{
+ "version": 3,
+ "sources": ["../src/config/constants.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/ui/roll-actions.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 *\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-character-spells.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]\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 *\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\"\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.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, \"tinjiData\", {\n scope: \"world\",\n config: false,\n type: Number,\n default: 0,\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 * 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 *\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.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 *\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 *\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 aspectspeciality: numberField(0),\n rolldifficulty: numberField(0),\n bonusmalusspeciality: 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 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", "/**\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 *\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", "/**\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 *\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 *\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 | eau | terre | feu | bois\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 *\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 | eau | terre | feu | bois\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 *\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 *\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\n return {\n reference: stringField(\"\"),\n description: htmlField(\"\"),\n weaponType: stringField(\"melee\"),\n material: stringField(\"\"),\n damageAspect: stringField(\"metal\"),\n damageBase: intField(1),\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),\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 *\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),\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 *\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 *\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),\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 *\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 getAssociatedActor() {\n if (this.speaker.scene && this.speaker.token) {\n const scene = game.scenes.get(this.speaker.scene)\n const token = scene?.tokens.get(this.speaker.token)\n if (token) return token.actor\n }\n return game.actors.get(this.speaker.actor)\n }\n\n #enrichChatCard(html) {\n const actor = this.getAssociatedActor()\n\n let img\n let nameText\n if (this.isContentVisible) {\n img = actor?.img ?? this.author.avatar\n nameText = this.alias\n } else {\n img = this.author.avatar\n nameText = this.author.name\n }\n\n const avatar = document.createElement(\"a\")\n avatar.classList.add(\"avatar\")\n if (actor) avatar.dataset.uuid = actor.uuid\n const avatarImg = document.createElement(\"img\")\n Object.assign(avatarImg, { src: img, alt: nameText })\n avatar.append(avatarImg)\n\n const name = document.createElement(\"span\")\n name.classList.add(\"name-stacked\")\n const title = document.createElement(\"span\")\n title.classList.add(\"title\")\n title.append(nameText)\n name.append(title)\n\n const sender = html.querySelector(\".message-sender\")\n 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 *\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 { ACTOR_TYPES } from \"../config/constants.js\"\n\nexport class CDEActor extends Actor {\n getRollData() {\n const data = this.toObject(false).system\n return data\n }\n\n prepareBaseData() {\n super.prepareBaseData()\n\n if (this.type === ACTOR_TYPES.character) {\n this.system.anti_initiative = 25 - (this.system.initiative ?? 0)\n }\n\n if (this.type === ACTOR_TYPES.npc) {\n this.system.vitality.calcul = (this.system.aptitudes.physical.value ?? 0) * 4\n this.system.hei.calcul = (this.system.aptitudes.spiritual.value ?? 0) * 4\n this.system.anti_initiative = 25 - (this.system.initiative ?? 0)\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 *\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 CDEItem extends Item {\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 *\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 *\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(\"select\", function (selected, options) {\n const escapedValue = RegExp.escape(Handlebars.escapeExpression(selected))\n const rgx = new RegExp(` value=[\"']${escapedValue}[\"']`)\n const html = options.fn(this)\n return html.replace(rgx, \"$& selected\")\n })\n\n Handlebars.registerHelper(\"getMagicBackground\", function (magic) {\n return game.i18n.localize(MAGICS[magic]?.background ?? \"\")\n })\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(\"getMagicSpecialityIcon\", function (magic, speciality) {\n return MAGICS[magic]?.speciality?.[speciality]?.icon ?? \"\"\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 * 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 *\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 *\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 *\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 *\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