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

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)
}