Files
fvtt-chroniques-de-l-etrange/dist/system.js

2146 lines
88 KiB
JavaScript

// src/config/constants.js
var SYSTEM_ID = "fvtt-chroniques-de-l-etrange";
var ACTOR_TYPES = {
character: "character",
npc: "npc",
tinji: "tinji",
loksyu: "loksyu"
};
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.png", elementicon: "/systems/fvtt-chroniques-de-l-etrange/images/cde_metal.png", labelicon: "Yin", labelelement: "CDE.Metal" },
mind: { label: "CDE.Mind", classicon: "icon-yin", icon: "/systems/fvtt-chroniques-de-l-etrange/images/cde_yin.png", elementicon: "/systems/fvtt-chroniques-de-l-etrange/images/cde_eau.png", labelicon: "Yin", labelelement: "CDE.Water" },
purification: { label: "CDE.Purification", classicon: "icon-yinyang", icon: "/systems/fvtt-chroniques-de-l-etrange/images/yin_yang.png", elementicon: "/systems/fvtt-chroniques-de-l-etrange/images/cde_terre.png", labelicon: "Yin/Yang", labelelement: "CDE.Earth" },
manipulation: { label: "CDE.Manipulation", classicon: "icon-yang", icon: "/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.png", elementicon: "/systems/fvtt-chroniques-de-l-etrange/images/cde_feu.png", labelicon: "Yang", labelelement: "CDE.Fire" },
aura: { label: "CDE.Aura", classicon: "icon-yang", icon: "/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.png", elementicon: "/systems/fvtt-chroniques-de-l-etrange/images/cde_bois.png", 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.png", elementicon: "/systems/fvtt-chroniques-de-l-etrange/images/cde_metal.png", labelicon: "Yin", labelelement: "CDE.Metal" },
elixirs: { label: "CDE.Elixirs", classicon: "icon-yin", icon: "/systems/fvtt-chroniques-de-l-etrange/images/cde_yin.png", elementicon: "/systems/fvtt-chroniques-de-l-etrange/images/cde_eau.png", labelicon: "Yin", labelelement: "CDE.Water" },
poisons: { label: "CDE.Poisons", classicon: "icon-yinyang", icon: "/systems/fvtt-chroniques-de-l-etrange/images/yin_yang.png", elementicon: "/systems/fvtt-chroniques-de-l-etrange/images/cde_terre.png", labelicon: "Yin/Yang", labelelement: "CDE.Earth" },
arsenal: { label: "CDE.Arsenal", classicon: "icon-yang", icon: "/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.png", elementicon: "/systems/fvtt-chroniques-de-l-etrange/images/cde_feu.png", labelicon: "Yang", labelelement: "CDE.Fire" },
potions: { label: "CDE.Potions", classicon: "icon-yang", icon: "/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.png", elementicon: "/systems/fvtt-chroniques-de-l-etrange/images/cde_bois.png", 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.png", elementicon: "/systems/fvtt-chroniques-de-l-etrange/images/cde_metal.png", labelicon: "Yin", labelelement: "CDE.Metal" },
transfiguration: { label: "CDE.Transfiguration", classicon: "icon-yin", icon: "/systems/fvtt-chroniques-de-l-etrange/images/cde_yin.png", elementicon: "/systems/fvtt-chroniques-de-l-etrange/images/cde_eau.png", labelicon: "Yin", labelelement: "CDE.Water" },
necromancy: { label: "CDE.Necromancy", classicon: "icon-yinyang", icon: "/systems/fvtt-chroniques-de-l-etrange/images/yin_yang.png", elementicon: "/systems/fvtt-chroniques-de-l-etrange/images/cde_terre.png", labelicon: "Yin/Yang", labelelement: "CDE.Earth" },
climatecontrol: { label: "CDE.ClimateControl", classicon: "icon-yang", icon: "/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.png", elementicon: "/systems/fvtt-chroniques-de-l-etrange/images/cde_feu.png", labelicon: "Yang", labelelement: "CDE.Fire" },
goldenmagic: { label: "CDE.GoldenMagic", classicon: "icon-yang", icon: "/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.png", elementicon: "/systems/fvtt-chroniques-de-l-etrange/images/cde_bois.png", 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.png", elementicon: "/systems/fvtt-chroniques-de-l-etrange/images/cde_metal.png", labelicon: "Yin", labelelement: "CDE.Metal" },
tracking: { label: "CDE.Tracking", classicon: "icon-yin", icon: "/systems/fvtt-chroniques-de-l-etrange/images/cde_yin.png", elementicon: "/systems/fvtt-chroniques-de-l-etrange/images/cde_eau.png", labelicon: "Yin", labelelement: "CDE.Water" },
protection: { label: "CDE.Protection", classicon: "icon-yinyang", icon: "/systems/fvtt-chroniques-de-l-etrange/images/yin_yang.png", elementicon: "/systems/fvtt-chroniques-de-l-etrange/images/cde_terre.png", labelicon: "Yin/Yang", labelelement: "CDE.Earth" },
punishment: { label: "CDE.Punishment", classicon: "icon-yang", icon: "/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.png", elementicon: "/systems/fvtt-chroniques-de-l-etrange/images/cde_feu.png", labelicon: "Yang", labelelement: "CDE.Fire" },
domination: { label: "CDE.Domination", classicon: "icon-yang", icon: "/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.png", elementicon: "/systems/fvtt-chroniques-de-l-etrange/images/cde_bois.png", 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.png", elementicon: "/systems/fvtt-chroniques-de-l-etrange/images/cde_metal.png", labelicon: "Yin", labelelement: "CDE.Metal" },
divination: { label: "CDE.Divination", classicon: "icon-yin", icon: "/systems/fvtt-chroniques-de-l-etrange/images/cde_yin.png", elementicon: "/systems/fvtt-chroniques-de-l-etrange/images/cde_eau.png", labelicon: "Yin", labelelement: "CDE.Water" },
earthlyprayer: { label: "CDE.EarthlyPrayer", classicon: "icon-yinyang", icon: "/systems/fvtt-chroniques-de-l-etrange/images/yin_yang.png", elementicon: "/systems/fvtt-chroniques-de-l-etrange/images/cde_terre.png", labelicon: "Yin/Yang", labelelement: "CDE.Earth" },
heavenlyprayer: { label: "CDE.HeavenlyPrayer", classicon: "icon-yang", icon: "/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.png", elementicon: "/systems/fvtt-chroniques-de-l-etrange/images/cde_feu.png", labelicon: "Yang", labelelement: "CDE.Fire" },
fungseoi: { label: "CDE.Fungseoi", classicon: "icon-yang", icon: "/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.png", elementicon: "/systems/fvtt-chroniques-de-l-etrange/images/cde_bois.png", labelicon: "Yang", labelelement: "CDE.Wood" }
}
}
};
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"
];
// 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/actors/tinji.js
var TinjiDataModel = 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 htmlField = (initial = "") => new fields.HTMLField({ required: true, nullable: false, initial, textSearch: true });
return {
value: numberField(0, { min: 0 }),
description: htmlField("")
};
}
};
// src/data/actors/loksyu.js
var LoksyuDataModel = 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 htmlField = (initial = "") => new fields.HTMLField({ required: true, nullable: false, initial, textSearch: true });
const polarity = () => new fields.SchemaField({
yin: new fields.SchemaField({ value: numberField(0, { min: 0 }) }),
yang: new fields.SchemaField({ value: numberField(0, { min: 0 }) })
});
return {
fire: polarity(),
earth: polarity(),
metal: polarity(),
water: polarity(),
wood: polarity(),
description: htmlField("")
};
}
};
// 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 {
get isWeapon() {
return this.system.subtype === "weapon";
}
get isArmor() {
return this.system.subtype === "armor";
}
get isSanhei() {
return this.system.subtype === "sanhei";
}
get isOther() {
return this.system.subtype === "other";
}
};
// 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.png",
alchemy: "/systems/fvtt-chroniques-de-l-etrange/images/cde_eau.png",
masteryoftheway: "/systems/fvtt-chroniques-de-l-etrange/images/cde_terre.png",
exorcism: "/systems/fvtt-chroniques-de-l-etrange/images/cde_feu.png",
geomancy: "/systems/fvtt-chroniques-de-l-etrange/images/cde_bois.png"
};
return icons[magic] ?? "";
});
Handlebars.registerHelper("getElementIcon", function(aspect) {
const icons = {
metal: "/systems/fvtt-chroniques-de-l-etrange/images/cde_metal.png",
eau: "/systems/fvtt-chroniques-de-l-etrange/images/cde_eau.png",
terre: "/systems/fvtt-chroniques-de-l-etrange/images/cde_terre.png",
feu: "/systems/fvtt-chroniques-de-l-etrange/images/cde_feu.png",
bois: "/systems/fvtt-chroniques-de-l-etrange/images/cde_bois.png"
};
return icons[aspect] ?? "";
});
Handlebars.registerHelper("getOrientationIcon", function(orientation) {
const icons = {
yin: "/systems/fvtt-chroniques-de-l-etrange/images/cde_yin.png",
yang: "/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.png",
yinyang: "/systems/fvtt-chroniques-de-l-etrange/images/yin_yang.png"
};
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 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) || 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/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 ASPECT_NAMES = ["metal", "water", "earth", "fire", "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.png",
water: "systems/fvtt-chroniques-de-l-etrange/images/cde_eau.png",
earth: "systems/fvtt-chroniques-de-l-etrange/images/cde_terre.png",
fire: "systems/fvtt-chroniques-de-l-etrange/images/cde_feu.png",
wood: "systems/fvtt-chroniques-de-l-etrange/images/cde_bois.png"
};
var ASPECT_FACES = {
metal: [3, 8],
water: [1, 6],
earth: [0, 5],
// 0 = face "10"
fire: [2, 7],
wood: [4, 9]
};
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 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
});
}
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 "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(game.system.CONST?.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);
}
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 = game.system.CONST?.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);
}
}
// 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
}
};
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();
}
};
// 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" value="${current.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 Dialog.prompt({
title: game.i18n.localize("CDE.Preferences"),
content: html,
label: game.i18n.localize("CDE.Validate"),
callback: (dlg) => {
const choice = dlg.querySelector("select[name='choice']")?.value ?? "0";
const check = dlg.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: "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.equipments = context.items.filter((item) => item.type === "item");
return context;
}
_onRender(context, options) {
super._onRender?.(context, options);
this.#bindInitiativeControls();
}
#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/actors/tinji.js
var CDETinjiSheet = class extends CDEBaseActorSheet {
static DEFAULT_OPTIONS = {
classes: ["tinji"]
};
static PARTS = {
main: { template: "systems/fvtt-chroniques-de-l-etrange/templates/actor/cde-tinji-sheet.html" }
};
tabGroups = { primary: "tinji" };
};
// src/ui/sheets/actors/loksyu.js
var CDELoksyuSheet = class extends CDEBaseActorSheet {
static DEFAULT_OPTIONS = {
classes: ["loksyu"]
};
static PARTS = {
main: { template: "systems/fvtt-chroniques-de-l-etrange/templates/actor/cde-loksyu-sheet.html" }
};
tabGroups = { primary: "loksyu" };
};
// src/ui/sheets/items/base.js
var { HandlebarsApplicationMixin: HandlebarsApplicationMixin2 } = foundry.applications.api;
var CDEBaseItemSheet = class 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: {}
};
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 });
}
}
};
// 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/migration.js
var MIGRATION_VERSION = "3.0.0";
function registerSettings() {
game.settings.register(SYSTEM_ID, "migrationVersion", {
name: "Migration version",
scope: "world",
config: false,
type: String,
default: "0.0.0"
});
}
async function migrateIfNeeded() {
const current = game.system.version ?? MIGRATION_VERSION;
const stored = game.settings.get(SYSTEM_ID, "migrationVersion") ?? "0.0.0";
if (!foundry.utils.isNewerVersion(current, stored)) return;
ui.notifications.info(`CHRONIQUESDELETRANGE | Migration vers ${current} en cours...`, { permanent: true });
await migrateActors();
await migrateItems();
await migrateCompendiumActors();
await migrateCompendiumItems();
await game.settings.set(SYSTEM_ID, "migrationVersion", current);
ui.notifications.info(`CHRONIQUESDELETRANGE | Migration vers ${current} termin\xE9e.`);
}
async function migrateActors() {
const updates = [];
for (const actor of game.actors.contents) {
const updateData = migrateActorData(actor);
if (Object.keys(updateData).length > 0) {
updates.push(actor.update(updateData, { enforceTypes: false }));
}
}
await Promise.all(updates);
}
async function migrateCompendiumActors() {
const packs = game.packs.filter((p) => p.documentName === "Actor" && p.metadata.system === SYSTEM_ID);
for (const pack of packs) {
const content = await pack.getDocuments();
for (const actor of content) {
const updateData = migrateActorData(actor);
if (Object.keys(updateData).length > 0) {
await actor.update(updateData, { pack: pack.collection, enforceTypes: false });
}
}
}
}
async function migrateItems() {
const updates = [];
for (const item of game.items.contents) {
const updateData = migrateItemData(item);
if (Object.keys(updateData).length > 0) {
updates.push(item.update(updateData, { enforceTypes: false }));
}
}
await Promise.all(updates);
}
async function migrateCompendiumItems() {
const packs = game.packs.filter((p) => p.documentName === "Item" && p.metadata.system === SYSTEM_ID);
for (const pack of packs) {
const content = await pack.getDocuments();
for (const item of content) {
const updateData = migrateItemData(item);
if (Object.keys(updateData).length > 0) {
await item.update(updateData, { pack: pack.collection, enforceTypes: false });
}
}
}
}
function migrateActorData(actor) {
const updateData = {};
const system = actor.system ?? {};
const actorType = actor.type;
const legacyMagic = system.magics?.masteryofthway;
if (legacyMagic && !system.magics?.masteryoftheway) {
updateData["system.magics.masteryoftheway"] = legacyMagic;
updateData["system.magics.-=masteryofthway"] = null;
}
if ((actorType === "character" || actorType === "npc") && !system.prefs?.typeofthrow) {
const defaultCheck = actorType === "character";
updateData["system.prefs.typeofthrow"] = { check: defaultCheck, choice: "0" };
}
if (actorType === "npc") {
if (system.levelofthreat !== void 0 && system.threat === void 0) {
updateData["system.threat"] = system.levelofthreat;
updateData["system.-=levelofthreat"] = null;
}
if (system.powerofnuisance !== void 0 && system.nuisance === void 0) {
updateData["system.nuisance"] = system.powerofnuisance;
updateData["system.-=powerofnuisance"] = null;
}
}
if (actorType === "character" && typeof system.guardian === "string") {
const guardianNum = parseInt(system.guardian, 10);
if (!isNaN(guardianNum)) {
updateData["system.guardian"] = guardianNum;
}
}
return updateData;
}
function migrateItemData(item) {
const updateData = {};
const system = item.system ?? {};
return updateData;
}
// src/system.js
Hooks.once("i18nInit", preLocalizeConfig);
Hooks.once("init", async () => {
console.info(`CHRONIQUESDELETRANGE | Initializing ${SYSTEM_ID}`);
registerSettings();
game.system.CONST = { MAGICS, SUBTYPES };
CONFIG.Actor.dataModels = {
[ACTOR_TYPES.character]: CharacterDataModel,
[ACTOR_TYPES.npc]: NpcDataModel,
[ACTOR_TYPES.tinji]: TinjiDataModel,
[ACTOR_TYPES.loksyu]: LoksyuDataModel
};
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", ActorSheet);
foundry.applications.apps.DocumentSheetConfig.unregisterSheet(Item, "core", 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(Actor, SYSTEM_ID, CDETinjiSheet, {
types: [ACTOR_TYPES.tinji],
makeDefault: true,
label: "CDE Tinji Sheet (V2)"
});
foundry.applications.apps.DocumentSheetConfig.registerSheet(Actor, SYSTEM_ID, CDELoksyuSheet, {
types: [ACTOR_TYPES.loksyu],
makeDefault: true,
label: "CDE Loksyu 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();
Hooks.on("renderSettings", (_app, html) => injectCompendiumLink(html));
console.info(`CHRONIQUESDELETRANGE | Initialized`);
});
Hooks.once("ready", async () => {
if (!game.modules.get("lib-wrapper")?.active && game.user.isGM) {
ui.notifications.error("System fvtt-chroniques-de-l-etrange requires the 'libWrapper' module. Please install and activate it.");
}
await migrateIfNeeded();
});
function injectCompendiumLink(html) {
const header = html[0]?.querySelector?.("h4.divider");
if (!header) return;
const section = document.createElement("section");
section.classList.add("settings", "flexcol");
section.innerHTML = `
<section class="links flexcol">
<img class="logo-info" src="systems/fvtt-chroniques-de-l-etrange/images/logo_jeu.png" />
<h4 class="divider">&nbsp;Lien utile&nbsp;<i class="fa-light fa-up-right-from-square"></i>&nbsp;</h4>
</section>
<section class="settings flexcol">
<button type="button" data-action="open-cde-link">
<i class="fa fa-download"></i>&nbsp;Compendium pour Les CdE&nbsp;<i class="fa-light fa-up-right-from-square"></i>
</button>
<details>
<summary><small>Guide d'installation</small></summary>
<small style="text-align: center;">
<p>Rendez-vous sur le site de l'\xE9diteur, t\xE9l\xE9chargez les PDF contenant les liens vers les compendia, puis ajoutez leurs manifestes dans Foundry.</p>
</small>
</details>
</section>
`;
section.querySelector("button[data-action='open-cde-link']")?.addEventListener("click", () => {
window.open("https://antre-monde.com/les-chroniques-de-letrengae/", "_blank");
});
header.parentNode.insertBefore(section, header);
}
//# sourceMappingURL=system.js.map