Files

2451 lines
99 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 = `
<form class="flexcol">
<div class="form-group">
<label>${game.i18n.localize("CDE.ThrowType")}</label>
<select name="choice">
<option value="0"${current.choice === "0" ? " selected" : ""}>0</option>
<option value="1"${current.choice === "1" ? " selected" : ""}>1</option>
<option value="2"${current.choice === "2" ? " selected" : ""}>2</option>
<option value="3"${current.choice === "3" ? " selected" : ""}>3</option>
</select>
</div>
<div class="form-group">
<label>${game.i18n.localize("CDE.EnablePrompt")}</label>
<input type="checkbox" name="check" ${current.check ? "checked" : ""}/>
</div>
</form>`;
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 ? `<strong>${componentValue}</strong>` : `<em>${game.i18n.localize("CDE.Component")}${face}</em>`;
const content = `
<div class="cde-chat-random-component">
<span class="cde-chat-component-label">${game.i18n.localize("CDE.ChanceThrowResult")}</span>
<span class="cde-chat-component-value">${label}</span>
</div>`;
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: `<div class="cde-tinji-spend-msg">
<i class="fas fa-star"></i>
<strong>${game.i18n.localize("CDE.TinJi2")}</strong>
${game.i18n.format("CDE.TinjiSpent", { name: game.user.name })}
<span class="cde-tinji-remain">(${current - 1} ${game.i18n.localize("CDE.TinjiRemaining")})</span>
</div>`
});
}
};
// 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 += `<button class="cde-roll-action-btn cde-roll-action--success" data-action="loksyu-success">
<img src="${ASPECT_ICONS[aspect]}" class="cde-roll-action-icon" alt="${aspLabel}"/>
<span class="cde-roll-action-label">+1 ${game.i18n.localize("CDE.Successes")}</span>
<span class="cde-roll-action-count">${successAvail}</span>
</button>`;
}
if (fasteAvail > 0) {
btns += `<button class="cde-roll-action-btn cde-roll-action--faste" data-action="loksyu-faste">
<img src="${ASPECT_ICONS[fasteAspect]}" class="cde-roll-action-icon" alt="${fasteLabel}"/>
<span class="cde-roll-action-label">+1 ${game.i18n.localize("CDE.AuspiciousDie")}</span>
<span class="cde-roll-action-count">${fasteAvail}</span>
</button>`;
}
if (isGM && tinji > 0) {
btns += `<button class="cde-roll-action-btn cde-roll-action--tinji" data-action="tinji">
<span class="cde-roll-action-tinji-char">\u5929</span>
<span class="cde-roll-action-label">${game.i18n.localize("CDE.TinJi2")}</span>
<span class="cde-roll-action-count">${tinji}</span>
</button>`;
}
const wrapper = document.createElement("div");
wrapper.className = "cde-roll-actions";
wrapper.innerHTML = `
<div class="cde-roll-actions-title">
<i class="fas fa-yin-yang"></i>
${game.i18n.localize("CDE.PostRollActions")}
</div>
<div class="cde-roll-actions-btns">${btns}</div>
`;
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: `<div class="cde-loksyu-draw-msg">
<div class="cde-loksyu-draw-header">
<img src="${ASPECT_ICONS[aspect]}" class="cde-loksyu-draw-aspect-icon" alt="${aspectLabel}"/>
<span class="cde-loksyu-draw-user">${game.user.name}</span>
<span class="cde-loksyu-draw-action">${game.i18n.localize("CDE.LoksyuDrawsA")}</span>
<span class="cde-loksyu-draw-type">${typeLabel}</span>
<span class="cde-loksyu-draw-from">${game.i18n.localize("CDE.LoksyuFromAspect")} <em>${aspectLabel}</em></span>
</div>
<div class="cde-loksyu-draw-footer">
<i class="fas fa-yin-yang"></i>
<span>${game.i18n.localize("CDE.Loksyu")} ${aspectLabel} : </span>
<strong class="cde-loksyu-remain">${remain} ${game.i18n.localize("CDE.LoksyuRemaining")}</strong>
</div>
</div>`
});
}
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: `<div class="cde-tinji-spend-msg">
<span class="cde-tinji-icon">\u5929</span>
<span class="cde-tinji-text">
<strong>${game.user.name}</strong> ${game.i18n.localize("CDE.TinjiSpent").replace("{name}", game.user.name)}
</span>
<span class="cde-tinji-remain">(${current - 1} ${game.i18n.localize("CDE.TinjiRemaining")})</span>
</div>`
});
}
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 = `
<button type="button" class="cde-chat-btn cde-chat-btn--loksyu">
<i class="fas fa-yin-yang"></i> ${game.i18n.localize("CDE.Loksyu")}
</button>
<button type="button" class="cde-chat-btn cde-chat-btn--tinji">
<i class="fas fa-star"></i> ${game.i18n.localize("CDE.TinJi2")}
</button>
`;
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 20242026 LeRatierBretonnien
* @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
*/
//# sourceMappingURL=system.js.map