Neo-Tokyo Neon Noir design pour fiches items

- Nouvelle palette : #080c14 fond, accents néon par type (#00d4d4 item, #ff3d5a kungfu, #4a9eff spell, #cc44ff supernatural)
- Nouveaux composants LESS : .cde-neon-header (clip-path angulaire + accent line), .cde-avatar (clip-path), .cde-stat-grid/.cde-stat-cell (style terminal), .cde-badge (parallélogramme), .cde-neon-tabs (underline néon animé), .cde-check-cell
- Fix layout : .cde-sheet width: 100% + height: 100% + overflow: hidden, .cde-tab-body flex: 1 + min-height: 0, .cde-notes-editor flex stretch
- Fix positions : DEFAULT_OPTIONS height explicite pour tous les types (item 620x580, spell 660x680, kungfu 720x680, supernatural 560x520)
- 4 templates items reécrits avec nouvelles classes et structure épurée

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-03-26 00:18:04 +01:00
commit 068fca00e5
739 changed files with 7923 additions and 0 deletions

104
src/config/constants.js Normal file
View File

@@ -0,0 +1,104 @@
export const SYSTEM_ID = "fvtt-chroniques-de-l-etrange"
export const ACTOR_TYPES = {
character: "character",
npc: "npc",
tinji: "tinji",
loksyu: "loksyu",
}
export const ITEM_TYPES = {
item: "item",
kungfu: "kungfu",
spell: "spell",
supernatural: "supernatural",
}
export const 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" },
}
export const 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" },
},
},
}
export const 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",
]

21
src/config/localize.js Normal file
View File

@@ -0,0 +1,21 @@
import { MAGICS, SUBTYPES } from "./constants.js"
export 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)
})
})
}

11
src/config/runtime.js Normal file
View File

@@ -0,0 +1,11 @@
export 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"
}

View File

@@ -0,0 +1,178 @@
export default class CharacterDataModel 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: stringField("0"),
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", "㊋"),
earth: aspectField("CDE.Earth", "㊏"),
metal: aspectField("CDE.Metal", "㊎"),
water: aspectField("CDE.Water", "㊌"),
wood: aspectField("CDE.Wood", "㊍"),
}),
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 }) }),
heiyin: new fields.SchemaField({ value: 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
}
}

4
src/data/actors/index.js Normal file
View File

@@ -0,0 +1,4 @@
export { default as CharacterDataModel } from "./character.js"
export { default as NpcDataModel } from "./npc.js"
export { default as TinjiDataModel } from "./tinji.js"
export { default as LoksyuDataModel } from "./loksyu.js"

22
src/data/actors/loksyu.js Normal file
View File

@@ -0,0 +1,22 @@
export default class LoksyuDataModel 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(""),
}
}
}

46
src/data/actors/npc.js Normal file
View File

@@ -0,0 +1,46 @@
export default class NpcDataModel 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(""),
levelofthreat: numberField(0, { min: 0 }),
powerofnuisance: numberField(0, { min: 0 }),
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"),
}),
}),
}
}
}

12
src/data/actors/tinji.js Normal file
View File

@@ -0,0 +1,12 @@
export default class TinjiDataModel 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(""),
}
}
}

4
src/data/items/index.js Normal file
View File

@@ -0,0 +1,4 @@
export { default as EquipmentDataModel } from "./item.js"
export { default as KungfuDataModel } from "./kungfu.js"
export { default as SpellDataModel } from "./spell.js"
export { default as SupernaturalDataModel } from "./supernatural.js"

20
src/data/items/item.js Normal file
View File

@@ -0,0 +1,20 @@
export default class EquipmentDataModel 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 {
subtype: stringField(""),
reference: stringField(""),
description: htmlField(""),
quantity: numberField(1, { min: 0 }),
weight: numberField(0, { min: 0 }),
protection: stringField(""),
damage: stringField(""),
range: stringField(""),
notes: htmlField(""),
}
}
}

32
src/data/items/kungfu.js Normal file
View File

@@ -0,0 +1,32 @@
export default class KungfuDataModel 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(""),
technique: htmlField(""),
})
return {
reference: stringField(""),
description: htmlField(""),
orientation: stringField(""),
aspect: stringField(""),
skill: stringField(""),
speciality: stringField(""),
style: stringField(""),
techniques: new fields.SchemaField({
technique1: techniqueField(),
technique2: techniqueField(),
technique3: techniqueField(),
}),
notes: htmlField(""),
}
}
}

22
src/data/items/spell.js Normal file
View File

@@ -0,0 +1,22 @@
export default class SpellDataModel 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(""),
hei: stringField(""),
realizationtimeritual: stringField(""),
realizationtimeaccelerated: stringField(""),
flashback: stringField(""),
components: htmlField(""),
effects: htmlField(""),
examples: htmlField(""),
notes: htmlField(""),
}
}
}

View File

@@ -0,0 +1,13 @@
export default class SupernaturalDataModel 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(""),
}
}
}

22
src/documents/actor.js Normal file
View File

@@ -0,0 +1,22 @@
import { ACTOR_TYPES } from "../config/constants.js"
export class CDEActor 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)
}
}
}

View File

@@ -0,0 +1,47 @@
export class CDEMessage 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)
}
}

17
src/documents/item.js Normal file
View File

@@ -0,0 +1,17 @@
export class CDEItem 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"
}
}

105
src/migration.js Normal file
View File

@@ -0,0 +1,105 @@
import { SYSTEM_ID } from "./config/constants.js"
const MIGRATION_VERSION = "3.0.0"
export function registerSettings() {
game.settings.register(SYSTEM_ID, "migrationVersion", {
name: "Migration version",
scope: "world",
config: false,
type: String,
default: "0.0.0",
})
}
export async function migrateIfNeeded() {
const current = game.system.version ?? MIGRATION_VERSION
const stored = game.settings.get(SYSTEM_ID, "migrationVersion") ?? "0.0.0"
if (!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ée.`)
}
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
// Fix legacy typo: masteryofthway -> masteryoftheway
const legacyMagic = system.magics?.masteryofthway
if (legacyMagic && !system.magics?.masteryoftheway) {
updateData["system.magics.masteryoftheway"] = legacyMagic
updateData["system.magics.-=masteryofthway"] = null
}
// Ensure prefs.typeofthrow exists on relevant actor types
if ((actorType === "character" || actorType === "npc") && !system.prefs?.typeofthrow) {
const defaultCheck = actorType === "character"
updateData["system.prefs.typeofthrow"] = { check: defaultCheck, choice: "0" }
}
return updateData
}
function migrateItemData(item) {
const updateData = {}
const system = item.system ?? {}
// Add item-specific migrations here as needed
return updateData
}

132
src/system.js Normal file
View File

@@ -0,0 +1,132 @@
import { ACTOR_TYPES, ITEM_TYPES, MAGICS, SUBTYPES, SYSTEM_ID } from "./config/constants.js"
import { preLocalizeConfig } from "./config/localize.js"
import { configureRuntime } from "./config/runtime.js"
import { CharacterDataModel, LoksyuDataModel, NpcDataModel, TinjiDataModel } from "./data/actors/index.js"
import { EquipmentDataModel, KungfuDataModel, SpellDataModel, SupernaturalDataModel } from "./data/items/index.js"
import { CDEMessage } from "./documents/chat-message.js"
import { CDEActor } from "./documents/actor.js"
import { CDEItem } from "./documents/item.js"
import { registerDice } from "./ui/dice.js"
import { registerHandlebarsHelpers } from "./ui/helpers.js"
import { preloadPartials } from "./ui/templates.js"
import { CDELoksyuSheet, CDECharacterSheet, CDENpcSheet, CDETinjiSheet } from "./ui/sheets/actors/index.js"
import { CDEItemSheet, CDEKungfuSheet, CDESpellSheet, CDESupernaturalSheet } from "./ui/sheets/items/index.js"
import { migrateIfNeeded, registerSettings } from "./migration.js"
Hooks.once("i18nInit", preLocalizeConfig)
Hooks.once("init", async () => {
console.info(`CHRONIQUESDELETRANGE | Initializing ${SYSTEM_ID}`)
registerSettings()
game.system.CONST = { MAGICS, SUBTYPES }
CONFIG.Actor.systemDataModels = {
[ACTOR_TYPES.character]: CharacterDataModel,
[ACTOR_TYPES.npc]: NpcDataModel,
[ACTOR_TYPES.tinji]: TinjiDataModel,
[ACTOR_TYPES.loksyu]: LoksyuDataModel,
}
CONFIG.Item.systemDataModels = {
[ITEM_TYPES.item]: EquipmentDataModel,
[ITEM_TYPES.kungfu]: KungfuDataModel,
[ITEM_TYPES.spell]: SpellDataModel,
[ITEM_TYPES.supernatural]: SupernaturalDataModel,
}
CONFIG.Actor.documentClass = CDEActor
CONFIG.Item.documentClass = CDEItem
CONFIG.ChatMessage.documentClass = CDEMessage
configureRuntime()
DocumentSheetConfig.unregisterSheet(Actor, "core", ActorSheet)
DocumentSheetConfig.unregisterSheet(Item, "core", ItemSheet)
DocumentSheetConfig.registerSheet(Actor, SYSTEM_ID, CDECharacterSheet, {
types: [ACTOR_TYPES.character],
makeDefault: true,
label: "CDE Character Sheet (V2)",
})
DocumentSheetConfig.registerSheet(Actor, SYSTEM_ID, CDENpcSheet, {
types: [ACTOR_TYPES.npc],
makeDefault: true,
label: "CDE NPC Sheet (V2)",
})
DocumentSheetConfig.registerSheet(Actor, SYSTEM_ID, CDETinjiSheet, {
types: [ACTOR_TYPES.tinji],
makeDefault: true,
label: "CDE Tinji Sheet (V2)",
})
DocumentSheetConfig.registerSheet(Actor, SYSTEM_ID, CDELoksyuSheet, {
types: [ACTOR_TYPES.loksyu],
makeDefault: true,
label: "CDE Loksyu Sheet (V2)",
})
DocumentSheetConfig.registerSheet(Item, SYSTEM_ID, CDEItemSheet, {
types: [ITEM_TYPES.item],
makeDefault: true,
label: "CDE Item Sheet (V2)",
})
DocumentSheetConfig.registerSheet(Item, SYSTEM_ID, CDEKungfuSheet, {
types: [ITEM_TYPES.kungfu],
makeDefault: true,
label: "CDE KungFu Sheet (V2)",
})
DocumentSheetConfig.registerSheet(Item, SYSTEM_ID, CDESpellSheet, {
types: [ITEM_TYPES.spell],
makeDefault: true,
label: "CDE Spell Sheet (V2)",
})
DocumentSheetConfig.registerSheet(Item, SYSTEM_ID, CDESupernaturalSheet, {
types: [ITEM_TYPES.supernatural],
makeDefault: true,
label: "CDE Supernatural 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'éditeur, téléchargez 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)
}

49
src/ui/dice.js Normal file
View File

@@ -0,0 +1,49 @@
const 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",
]
const 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",
]
export 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'étrange 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'étrange" }, "preferred")
dice3d.addDicePreset({ type: "d10", labels: CLASSIC_LABELS, system: "fvtt-chroniques-de-l-etrange" })
})
}

49
src/ui/helpers.js Normal file
View File

@@ -0,0 +1,49 @@
import { MAGICS } from "../config/constants.js"
export 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 ?? "")
})
}

View File

@@ -0,0 +1,70 @@
const { HandlebarsApplicationMixin } = foundry.applications.api
export class CDEBaseActorSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ActorSheetV2) {
static DEFAULT_OPTIONS = {
classes: ["fvtt-chroniques-de-l-etrange", "actor"],
position: { width: 920, height: "auto" },
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" }
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,
items: this.document.items.contents,
descriptionHTML,
editable: this.isEditable,
cssClass,
}
}
async _onFirstRender(context, options) {
await super._onFirstRender(context, options)
for (const [group, tab] of Object.entries(this.tabGroups)) {
this.changeTab(tab, group, { force: true })
}
}
_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",
kungfu: "CDE.KFNew",
spell: "CDE.SpellNew",
supernatural: "CDE.SupernaturalNew",
}
const name = game.i18n.localize(labels[type] ?? "CDE.ItemNew")
return cls.create({ name, type }, { parent: this.document })
}
static #onItemEdit(event, target) {
const itemId = target.closest(".item")?.dataset.itemId
const item = this.document.items.get(itemId)
if (item) item.sheet.render(true)
}
static #onItemDelete(event, target) {
const itemId = target.closest(".item")?.dataset.itemId
const item = this.document.items.get(itemId)
if (item) item.delete()
}
}

View File

@@ -0,0 +1,112 @@
import { MAGICS, SUBTYPES } from "../../../config/constants.js"
import { CDEBaseActorSheet } from "./base.js"
export class CDECharacterSheet 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.spells = context.items.filter((item) => item.type === "spell")
context.kungfus = context.items.filter((item) => item.type === "kungfu")
context.CDE = { MAGICS, SUBTYPES }
return context
}
_onRender(context, options) {
super._onRender?.(context, options)
this.#bindInitiativeControls()
this.#bindPrefs()
}
#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") {
const html = `
<form class="flexcol">
<div class="form-group">
<label>${game.i18n.localize("CDE.TurnOrder")}</label>
<input type="number" name="initiative" value="${initiative}" min="1" max="24" />
</div>
</form>`
const value = await Dialog.prompt({
title: game.i18n.localize("CDE.TurnOrder"),
content: html,
label: game.i18n.localize("CDE.Validate"),
callback: (dlg) => {
const input = dlg.querySelector("input[name='initiative']")
return Number(input?.value ?? initiative)
},
})
if (Number.isFinite(value)) {
const sanitized = foundry.utils.clamp(Number(value), 1, 24)
await this.document.update({ "system.initiative": sanitized })
}
}
})
})
}
#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,
})
}
})
}
}

View File

@@ -0,0 +1,4 @@
export { CDECharacterSheet } from "./character.js"
export { CDENpcSheet } from "./npc.js"
export { CDETinjiSheet } from "./tinji.js"
export { CDELoksyuSheet } from "./loksyu.js"

View File

@@ -0,0 +1,13 @@
import { CDEBaseActorSheet } from "./base.js"
export class CDELoksyuSheet 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" }
}

View File

@@ -0,0 +1,67 @@
import { CDEBaseActorSheet } from "./base.js"
export class CDENpcSheet 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") {
const html = `
<form class="flexcol">
<div class="form-group">
<label>${game.i18n.localize("CDE.TurnOrder")}</label>
<input type="number" name="initiative" value="${initiative}" min="1" max="24" />
</div>
</form>`
const value = await Dialog.prompt({
title: game.i18n.localize("CDE.TurnOrder"),
content: html,
label: game.i18n.localize("CDE.Validate"),
callback: (dlg) => Number(dlg.querySelector("input[name='initiative']")?.value ?? initiative),
})
if (Number.isFinite(value)) {
const sanitized = foundry.utils.clamp(Number(value), 1, 24)
await this.document.update({ "system.initiative": sanitized })
}
}
})
})
}
}

View File

@@ -0,0 +1,13 @@
import { CDEBaseActorSheet } from "./base.js"
export class CDETinjiSheet 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" }
}

View File

@@ -0,0 +1,43 @@
const { HandlebarsApplicationMixin } = foundry.applications.api
export class CDEBaseItemSheet extends HandlebarsApplicationMixin(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: "description" }
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,
editable: this.isEditable,
cssClass,
enrichedDescription,
enrichedNotes,
descriptionHTML: enrichedDescription,
notesHTML: enrichedNotes,
}
}
async _onFirstRender(context, options) {
await super._onFirstRender(context, options)
for (const [group, tab] of Object.entries(this.tabGroups)) {
this.changeTab(tab, group, { force: true })
}
}
_onRender(context, options) {
for (const [group, tab] of Object.entries(this.tabGroups)) {
this.changeTab(tab, group, { force: true })
}
}
}

View File

@@ -0,0 +1,5 @@
export { CDEBaseItemSheet } from "./base.js"
export { CDEItemSheet } from "./item.js"
export { CDEKungfuSheet } from "./kungfu.js"
export { CDESpellSheet } from "./spell.js"
export { CDESupernaturalSheet } from "./supernatural.js"

View File

@@ -0,0 +1,23 @@
import { SUBTYPES } from "../../../config/constants.js"
import { CDEBaseItemSheet } from "./base.js"
export class CDEItemSheet extends CDEBaseItemSheet {
static DEFAULT_OPTIONS = {
classes: ["equipment"],
position: { width: 620, height: 580 },
}
static PARTS = {
main: { template: "systems/fvtt-chroniques-de-l-etrange/templates/item/cde-item-sheet.html" },
}
async _prepareContext() {
const context = await super._prepareContext()
context.subtypes = SUBTYPES
context.isWeapon = this.document.isWeapon
context.isArmor = this.document.isArmor
context.isSanhei = this.document.isSanhei
context.isOther = this.document.isOther
return context
}
}

View File

@@ -0,0 +1,22 @@
import { CDEBaseItemSheet } from "./base.js"
export class CDEKungfuSheet 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
}
}

View File

@@ -0,0 +1,22 @@
import { CDEBaseItemSheet } from "./base.js"
export class CDESpellSheet 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
}
}

View File

@@ -0,0 +1,12 @@
import { CDEBaseItemSheet } from "./base.js"
export class CDESupernaturalSheet 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" },
}
}

5
src/ui/templates.js Normal file
View File

@@ -0,0 +1,5 @@
import { TEMPLATE_PARTIALS } from "../config/constants.js"
export async function preloadPartials() {
return loadTemplates(TEMPLATE_PARTIALS)
}