diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..6f36498 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,87 @@ +# Copilot Instructions — fvtt-celestopol + +## Project Overview + +This is a **Foundry VTT system** for **Célestopol 1922**, a French tabletop RPG set in an art-deco 1922 universe. The project targets FoundryVTT v13+ and is developed in French. + +The reference rulebooks are in `__regles/` (gitignored): +- *Célestopol 1922 Livre de base* — core rulebook +- *Célestopol 1922 Fiches de prêts à jouer* — pre-generated character sheets + +## Architecture + +This system uses **FoundryVTT v13 DataModels + ApplicationV2** — NOT the legacy template.json / AppV1 approach. + +``` +fvtt-celestopol.mjs # Main entry point (Hooks.once("init")) +module/ + config/system.mjs # All game constants (SYSTEM export) + models/ # TypeDataModel subclasses (character, npc, items) + documents/ # Actor, Item, ChatMessage, Roll wrappers + applications/sheets/ # AppV2 sheets (HandlebarsApplicationMixin) +lang/fr.json # French i18n (key prefix: CELESTOPOL.*) +styles/ # LESS source files +css/ # Compiled CSS (via gulp) +templates/ # Handlebars (.hbs) templates +assets/fonts/ # CopaseticNF art-deco font +assets/ui/ # Background images +assets/icons/ # Item icons +packs-system/ # Source files for compendium packs +``` + +## DataModels (no template.json) + +- Extend `foundry.abstract.TypeDataModel` +- Schema in `static defineSchema()` using `foundry.data.fields.*` +- `prepareDerivedData()` for computed values +- Files: `module/models/character.mjs`, `npc.mjs`, `items.mjs` + +## ApplicationV2 / Sheets + +- Actor sheets: `HandlebarsApplicationMixin(foundry.applications.sheets.ActorSheetV2)` +- Item sheets: `HandlebarsApplicationMixin(foundry.applications.sheets.ItemSheetV2)` +- `static DEFAULT_OPTIONS` for config; `static PARTS` for templates +- `_prepareContext()` for base context; `_preparePartContext(partId, context)` for per-tab +- Edit/Play mode toggle via `_sheetMode` + `isPlayMode`/`isEditMode` getters +- Actions: `static #onXxx(event, target)` private static methods in `DEFAULT_OPTIONS.actions` +- `form: { submitOnChange: true }` enables live saving + +## Roll Mechanics + +- Pool of d6 dice: `nbDice = max(1, skillValue + woundMalus)` +- Formula: `{n}d6 [+ moonBonus + modifier]` +- Moon phase bonus: Nouvelle Lune=0, Croissants=+1, Gibbeuse=+2, Pleine Lune=+3 +- Compare total vs difficulty threshold (normal=7) +- Wound malus: levels 1-2=0, 3-4=-1, 5-6=-2, 7=-3, 8=-999 (out) +- DialogV2 for roll configuration: `foundry.applications.api.DialogV2.wait(...)` + +## Game Data (4 stats × 4 skills) + +- **Âme**: Artifice, Attraction, Coercition, Faveur +- **Corps**: Échauffourée, Effacement, Mobilité, Prouesse +- **Cœur**: Appréciation, Arts, Inspiration, Traque +- **Esprit**: Instruction, Merveilleux technologique, Raisonnement, Traitement + +**Tracks**: Blessures (8 niveaux), Destin (8), Spleen (8) +**Anomalies**: 9 types (none + 8) +**Factions**: 8 standard + 2 custom + +## Build + +```bash +npm install # Install dev deps +npx gulp css # Compile LESS → css/fvtt-celestopol.css (once) +npx gulp # Compile + watch +``` + +## Visual Style + +- Font: **CopaseticNF** (Regular + Bold, in `assets/fonts/`) — art-deco style +- Header bg color: `rgb(12, 76, 12)` (dark green) with orange text (`#e07b00`) +- Sheet header texture: `assets/ui/fond_cadrille.jpg` +- CSS variables: `--cel-green`, `--cel-orange`, `--cel-font-title`, etc. + +## Language + +All in-game text, labels, and code comments should be in **French**. Code identifiers may be English. All i18n keys use the `CELESTOPOL.*` prefix (see `lang/fr.json`). + diff --git a/.gitignore b/.gitignore index 26b0ea6..1d83d81 100644 --- a/.gitignore +++ b/.gitignore @@ -7,8 +7,9 @@ node_modules/ package-lock.json -chroniquesdeletrange.lock -*.pdf -.github/ -__regles/ +# CSS compilé (généré par gulp depuis styles/) +css/*.css +# Règles (PDFs privés) +__regles/ +*.pdf diff --git a/assets/fonts/CopaseticNF-Bold.otf b/assets/fonts/CopaseticNF-Bold.otf new file mode 100644 index 0000000..d1e31de Binary files /dev/null and b/assets/fonts/CopaseticNF-Bold.otf differ diff --git a/assets/fonts/CopaseticNF.otf b/assets/fonts/CopaseticNF.otf new file mode 100644 index 0000000..5b418db Binary files /dev/null and b/assets/fonts/CopaseticNF.otf differ diff --git a/assets/icons/anomaly.png b/assets/icons/anomaly.png new file mode 100644 index 0000000..169098a Binary files /dev/null and b/assets/icons/anomaly.png differ diff --git a/assets/icons/aspect.png b/assets/icons/aspect.png new file mode 100644 index 0000000..836f28f Binary files /dev/null and b/assets/icons/aspect.png differ diff --git a/assets/icons/attribute.png b/assets/icons/attribute.png new file mode 100644 index 0000000..ce07bd2 Binary files /dev/null and b/assets/icons/attribute.png differ diff --git a/assets/icons/item.png b/assets/icons/item.png new file mode 100644 index 0000000..ff31401 Binary files /dev/null and b/assets/icons/item.png differ diff --git a/assets/ui/celestopol_background.webp b/assets/ui/celestopol_background.webp new file mode 100644 index 0000000..5d53fa5 Binary files /dev/null and b/assets/ui/celestopol_background.webp differ diff --git a/assets/ui/fond_cadrille.jpg b/assets/ui/fond_cadrille.jpg new file mode 100644 index 0000000..558f357 Binary files /dev/null and b/assets/ui/fond_cadrille.jpg differ diff --git a/fvtt-celestopol.mjs b/fvtt-celestopol.mjs new file mode 100644 index 0000000..0c0b8c7 --- /dev/null +++ b/fvtt-celestopol.mjs @@ -0,0 +1,212 @@ +/** + * fvtt-celestopol.mjs — Point d'entrée principal du système Célestopol 1922 + * FoundryVTT v13+ / DataModels / ApplicationV2 + */ + +import { SYSTEM, SYSTEM_ID, ASCII } from "./module/config/system.mjs" +import { + CelestopolCharacter, + CelestopolNPC, + CelestopolAnomaly, + CelestopolAspect, + CelestopolAttribute, + CelestopolEquipment, +} from "./module/models/_module.mjs" +import { + CelestopolActor, + CelestopolItem, + CelestopolChatMessage, + CelestopolRoll, +} from "./module/documents/_module.mjs" +import { + CelestopolCharacterSheet, + CelestopolNPCSheet, + CelestopolAnomalySheet, + CelestopolAspectSheet, + CelestopolAttributeSheet, + CelestopolEquipmentSheet, +} from "./module/applications/_module.mjs" + +/* ─── Init hook ──────────────────────────────────────────────────────────── */ + +Hooks.once("init", () => { + console.log(ASCII) + console.log(`${SYSTEM_ID} | Initializing Célestopol 1922 system`) + + // Expose SYSTEM constants in game.system namespace + game.celestopol = { SYSTEM } + + // ── DataModels ────────────────────────────────────────────────────────── + CONFIG.Actor.dataModels.character = CelestopolCharacter + CONFIG.Actor.dataModels.npc = CelestopolNPC + + CONFIG.Item.dataModels.anomaly = CelestopolAnomaly + CONFIG.Item.dataModels.aspect = CelestopolAspect + CONFIG.Item.dataModels.attribute = CelestopolAttribute + CONFIG.Item.dataModels.equipment = CelestopolEquipment + + // ── Document classes ──────────────────────────────────────────────────── + CONFIG.Actor.documentClass = CelestopolActor + CONFIG.Item.documentClass = CelestopolItem + CONFIG.ChatMessage.documentClass = CelestopolChatMessage + CONFIG.Dice.rolls.push(CelestopolRoll) + + // ── Token display defaults ─────────────────────────────────────────────── + CONFIG.Actor.trackableAttributes = { + character: { + bar: ["blessures.lvl"], + value: ["initiative", "anomaly.value"], + }, + npc: { + bar: ["blessures.lvl"], + value: ["initiative"], + }, + } + + // ── Sheets: unregister core, register system sheets ───────────────────── + foundry.applications.sheets.ActorSheetV2.unregisterSheet?.("core", "Actor", { types: ["character", "npc"] }) + Actors.unregisterSheet("core", ActorSheet) + Actors.registerSheet(SYSTEM_ID, CelestopolCharacterSheet, { + types: ["character"], + makeDefault: true, + label: "CELESTOPOL.Sheet.character", + }) + Actors.registerSheet(SYSTEM_ID, CelestopolNPCSheet, { + types: ["npc"], + makeDefault: true, + label: "CELESTOPOL.Sheet.npc", + }) + + Items.unregisterSheet("core", ItemSheet) + Items.registerSheet(SYSTEM_ID, CelestopolAnomalySheet, { + types: ["anomaly"], + makeDefault: true, + label: "CELESTOPOL.Sheet.anomaly", + }) + Items.registerSheet(SYSTEM_ID, CelestopolAspectSheet, { + types: ["aspect"], + makeDefault: true, + label: "CELESTOPOL.Sheet.aspect", + }) + Items.registerSheet(SYSTEM_ID, CelestopolAttributeSheet, { + types: ["attribute"], + makeDefault: true, + label: "CELESTOPOL.Sheet.attribute", + }) + Items.registerSheet(SYSTEM_ID, CelestopolEquipmentSheet, { + types: ["equipment"], + makeDefault: true, + label: "CELESTOPOL.Sheet.equipment", + }) + + // ── Handlebars helpers ─────────────────────────────────────────────────── + _registerHandlebarsHelpers() + + // ── Game settings ──────────────────────────────────────────────────────── + _registerSettings() + + // ── Pre-load templates ─────────────────────────────────────────────────── + _preloadTemplates() +}) + +/* ─── Ready hook ─────────────────────────────────────────────────────────── */ + +Hooks.once("ready", () => { + console.log(`${SYSTEM_ID} | System ready`) + + // Socket handler for GM-only operations (e.g. wound application) + if (game.socket) { + game.socket.on(`system.${SYSTEM_ID}`, _onSocketMessage) + } +}) + +/* ─── Handlebars helpers ─────────────────────────────────────────────────── */ + +function _registerHandlebarsHelpers() { + // Helper : concat strings + Handlebars.registerHelper("concat", (...args) => args.slice(0, -1).join("")) + + // Helper : strict equality + Handlebars.registerHelper("eq", (a, b) => a === b) + + // Helper : greater than + Handlebars.registerHelper("gt", (a, b) => a > b) + + // Helper : logical OR + Handlebars.registerHelper("or", (...args) => args.slice(0, -1).some(Boolean)) + + // Helper : build array from args (Handlebars doesn't have arrays natively) + Handlebars.registerHelper("array", (...args) => args.slice(0, -1)) + + // Helper : nested object lookup with dot path or multiple keys + Handlebars.registerHelper("lookup", (obj, ...args) => { + const options = args.pop() // last arg is Handlebars options hash + return args.reduce((acc, key) => (acc && acc[key] !== undefined ? acc[key] : undefined), obj) + }) + + // Helper : let (scope variable assignment inside template) + Handlebars.registerHelper("let", function(value, options) { + return options.fn({ value }) + }) +} + +/* ─── Settings ───────────────────────────────────────────────────────────── */ + +function _registerSettings() { + game.settings.register(SYSTEM_ID, "defaultMoonPhase", { + name: "CELESTOPOL.Setting.defaultMoonPhase.name", + hint: "CELESTOPOL.Setting.defaultMoonPhase.hint", + scope: "world", + config: true, + type: String, + default: "nouvelleLune", + choices: Object.fromEntries( + Object.entries(SYSTEM.MOON_DICE_PHASES).map(([k, v]) => [k, v.label]) + ), + }) + + game.settings.register(SYSTEM_ID, "autoWounds", { + name: "CELESTOPOL.Setting.autoWounds.name", + hint: "CELESTOPOL.Setting.autoWounds.hint", + scope: "world", + config: true, + type: Boolean, + default: false, + }) +} + +/* ─── Template preload ───────────────────────────────────────────────────── */ + +function _preloadTemplates() { + const base = `systems/${SYSTEM_ID}/templates` + loadTemplates([ + `${base}/character-main.hbs`, + `${base}/character-competences.hbs`, + `${base}/character-blessures.hbs`, + `${base}/character-factions.hbs`, + `${base}/character-biography.hbs`, + `${base}/npc-main.hbs`, + `${base}/npc-competences.hbs`, + `${base}/npc-blessures.hbs`, + `${base}/anomaly.hbs`, + `${base}/aspect.hbs`, + `${base}/attribute.hbs`, + `${base}/equipment.hbs`, + `${base}/roll-dialog.hbs`, + `${base}/chat-message.hbs`, + `${base}/partials/item-scores.hbs`, + ]) +} + +/* ─── Socket handler ─────────────────────────────────────────────────────── */ + +function _onSocketMessage(data) { + if (!game.user.isGM) return + switch (data.type) { + case "applyWound": { + const actor = game.actors.get(data.actorId) + if (actor) actor.update({ "system.blessures.lvl": data.level }) + break + } + } +} diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..ff97060 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,31 @@ +const gulp = require('gulp'); +const less = require('gulp-less'); + +/* ----------------------------------------- */ +/* Compile LESS +/* ----------------------------------------- */ +function compileLESS() { + return gulp.src("styles/fvtt-celestopol.less") + .pipe(less()).on('error', console.log.bind(console)) + .pipe(gulp.dest("./css")) +} +const css = gulp.series(compileLESS); + +/* ----------------------------------------- */ +/* Watch Updates +/* ----------------------------------------- */ +const SIMPLE_LESS = ["styles/*.less"]; + +function watchUpdates() { + gulp.watch(SIMPLE_LESS, css); +} + +/* ----------------------------------------- */ +/* Export Tasks +/* ----------------------------------------- */ +exports.default = gulp.series( + gulp.parallel(css), + watchUpdates +); +exports.css = css; +exports.watchUpdates = watchUpdates; diff --git a/lang/fr.json b/lang/fr.json new file mode 100644 index 0000000..45ad040 --- /dev/null +++ b/lang/fr.json @@ -0,0 +1,163 @@ +{ + "CELESTOPOL": { + "Actor": { + "name": "Nom", + "concept": "Concept / Profession", + "initiative": "Initiative", + "anomaly": "Anomalie", + "description": "Biographie", + "notes": "Notes" + }, + "Stat": { + "res": "Résistance", + "ame": "Âme", + "corps": "Corps", + "coeur": "Cœur", + "esprit": "Esprit" + }, + "Skill": { + "artifice": "Artifice", + "attraction": "Attraction", + "coercition": "Coercition", + "faveur": "Faveur", + "echauffouree": "Échauffourée", + "effacement": "Effacement", + "mobilite": "Mobilité", + "prouesse": "Prouesse", + "appreciation": "Appréciation", + "arts": "Arts", + "inspiration": "Inspiration", + "traque": "Traque", + "instruction": "Instruction", + "mtechnologique": "Merveilleux technologique", + "raisonnement": "Raisonnement", + "traitement": "Traitement" + }, + "Anomaly": { + "type": "Type d'anomalie", + "none": "Aucune", + "charnel": "Charnel", + "mecanique": "Mécanique", + "spectral": "Spectral", + "onirique": "Onirique", + "telepath": "Télépathique", + "alchimique": "Alchimique", + "cosmique": "Cosmique", + "temporel": "Temporel" + }, + "Attribut": { + "entregent": "Entregent", + "fortune": "Fortune", + "reve": "Rêve", + "vision": "Vision" + }, + "Faction": { + "label": "Faction", + "score": "Score", + "custom": "Faction personnalisée…", + "pinkerton": "Pinkerton", + "police": "Police", + "okhrana": "Okhrana", + "lunanovatek": "LunaNovaTek", + "oto": "OTO", + "syndicats": "Syndicats", + "vorovskoymir": "Vorovskoymir", + "cour": "Cour" + }, + "Track": { + "blessures": "Blessures", + "destin": "Destin", + "spleen": "Spleen", + "level": "Niveau", + "currentMalus": "Malus actuel" + }, + "Tab": { + "main": "Principal", + "competences": "Compétences", + "blessures": "Blessures", + "factions": "Factions", + "biography": "Biographie", + "description": "Description", + "technique": "Technique" + }, + "Roll": { + "clickToRoll": "Cliquer pour lancer", + "moonPhase": "Phase de lune", + "difficulty": "Difficulté", + "modifier": "Modificateur", + "nbDice": "Nombre de dés", + "total": "Total", + "success": "SUCCÈS", + "failure": "ÉCHEC", + "criticalSuccess": "Succès critique !", + "criticalFailure": "Échec critique !", + "moonBonus": "Bonus de lune", + "rollTitle": "Lancer les dés" + }, + "Moon": { + "nouvelleLune": "Nouvelle Lune", + "croissantDebutant": "Croissant débutant", + "croissantMontant": "Croissant montant", + "gibbeuseMontante": "Gibbeuse montante", + "pleineLune": "Pleine Lune", + "gibbeuseDecroissante": "Gibbeuse décroissante", + "croissantDecroissant": "Croissant décroissant", + "croissantFinissant": "Croissant finissant" + }, + "Difficulty": { + "facile": "Facile", + "normal": "Normal", + "difficile": "Difficile", + "extreme": "Extrême", + "impossible": "Impossible" + }, + "Item": { + "anomalies": "Anomalies", + "aspects": "Aspects", + "attributes": "Attributs", + "equipments": "Équipements", + "newAnomaly": "Nouvelle anomalie", + "newAspect": "Nouvel aspect", + "newAttribute": "Nouvel attribut", + "newEquipment": "Nouvel équipement", + "value": "Valeur", + "scores": "Scores bonus / malus", + "reference": "Référence (page)", + "technique": "Description technique", + "narratif": "Description narrative", + "quantity": "Quantité", + "damage": "Dégâts", + "range": "Portée", + "protection": "Protection", + "speed": "Vitesse", + "crew": "Équipage", + "weight": "Poids" + }, + "Equipment": { + "type": { + "general": "Général", + "arme": "Arme", + "armure": "Armure", + "vehicule": "Véhicule", + "gadget": "Gadget" + } + }, + "Sheet": { + "editMode": "Mode édition", + "playMode": "Mode jeu" + }, + "Setting": { + "autoWounds": { + "name": "Blessures automatiques", + "hint": "Appliquer automatiquement les malus de blessures lors des jets" + }, + "defaultMoonPhase": { + "name": "Phase de lune par défaut", + "hint": "Phase de lune utilisée par défaut dans les jets de dés" + } + }, + "ChatCard": { + "rollFor": "Jet de {skill} ({stat})" + } + } +} diff --git a/module/applications/_module.mjs b/module/applications/_module.mjs new file mode 100644 index 0000000..ebb0911 --- /dev/null +++ b/module/applications/_module.mjs @@ -0,0 +1,3 @@ +export { default as CelestopolCharacterSheet } from "./sheets/character-sheet.mjs" +export { default as CelestopolNPCSheet } from "./sheets/npc-sheet.mjs" +export { CelestopolAnomalySheet, CelestopolAspectSheet, CelestopolAttributeSheet, CelestopolEquipmentSheet } from "./sheets/item-sheets.mjs" diff --git a/module/applications/sheets/base-actor-sheet.mjs b/module/applications/sheets/base-actor-sheet.mjs new file mode 100644 index 0000000..cf31d37 --- /dev/null +++ b/module/applications/sheets/base-actor-sheet.mjs @@ -0,0 +1,126 @@ +const { HandlebarsApplicationMixin } = foundry.applications.api + +export default class CelestopolActorSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ActorSheetV2) { + static SHEET_MODES = { EDIT: 0, PLAY: 1 } + + constructor(options = {}) { + super(options) + this.#dragDrop = this.#createDragDropHandlers() + } + + #dragDrop + + /** @override */ + static DEFAULT_OPTIONS = { + classes: ["fvtt-celestopol", "actor"], + position: { width: 900, height: "auto" }, + form: { submitOnChange: true }, + window: { resizable: true }, + dragDrop: [{ dragSelector: '[data-drag="true"], .rollable', dropSelector: null }], + actions: { + editImage: CelestopolActorSheet.#onEditImage, + toggleSheet: CelestopolActorSheet.#onToggleSheet, + edit: CelestopolActorSheet.#onItemEdit, + delete: CelestopolActorSheet.#onItemDelete, + }, + } + + _sheetMode = this.constructor.SHEET_MODES.PLAY + + get isPlayMode() { return this._sheetMode === this.constructor.SHEET_MODES.PLAY } + get isEditMode() { return this._sheetMode === this.constructor.SHEET_MODES.EDIT } + + /** @override */ + async _prepareContext() { + return { + fields: this.document.schema.fields, + systemFields: this.document.system.schema.fields, + actor: this.document, + system: this.document.system, + source: this.document.toObject(), + isEditMode: this.isEditMode, + isPlayMode: this.isPlayMode, + isEditable: this.isEditable, + } + } + + /** @override */ + _onRender(context, options) { + this.#dragDrop.forEach(d => d.bind(this.element)) + this.element.querySelectorAll(".rollable").forEach(el => { + el.addEventListener("click", this._onRoll.bind(this)) + }) + } + + async _onRoll(event) { + if (!this.isPlayMode) return + const el = event.currentTarget + const statId = el.dataset.statId + const skillId = el.dataset.skillId + if (!statId || !skillId) return + await this.document.system.roll(statId, skillId) + } + + #createDragDropHandlers() { + return this.options.dragDrop.map(d => { + d.permissions = { + dragstart: this._canDragStart.bind(this), + drop: this._canDragDrop.bind(this), + } + d.callbacks = { + dragover: this._onDragOver.bind(this), + drop: this._onDrop.bind(this), + } + return new foundry.applications.ux.DragDrop.implementation(d) + }) + } + + _canDragStart() { return this.isEditable } + _canDragDrop() { return true } + _onDragOver() {} + + async _onDrop(event) { + if (!this.isEditable) return + const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event) + if (data.type === "Item") { + const item = await fromUuid(data.uuid) + if (item) return this._onDropItem(item) + } + } + + async _onDropItem(item) { + await this.document.createEmbeddedDocuments("Item", [item.toObject()], { renderSheet: false }) + } + + static async #onEditImage(event, target) { + const attr = target.dataset.edit + const current = foundry.utils.getProperty(this.document, attr) + const fp = new FilePicker({ + current, + type: "image", + callback: (path) => this.document.update({ [attr]: path }), + top: this.position.top + 40, + left: this.position.left + 10, + }) + return fp.browse() + } + + static #onToggleSheet() { + const modes = this.constructor.SHEET_MODES + this._sheetMode = this.isEditMode ? modes.PLAY : modes.EDIT + this.render() + } + + static async #onItemEdit(event, target) { + const uuid = target.getAttribute("data-item-uuid") + const id = target.getAttribute("data-item-id") + const item = uuid ? await fromUuid(uuid) : this.document.items.get(id) + item?.sheet.render(true) + } + + static async #onItemDelete(event, target) { + const uuid = target.getAttribute("data-item-uuid") + const item = await fromUuid(uuid) + await item?.deleteDialog() + } +} diff --git a/module/applications/sheets/base-item-sheet.mjs b/module/applications/sheets/base-item-sheet.mjs new file mode 100644 index 0000000..f4c0cd5 --- /dev/null +++ b/module/applications/sheets/base-item-sheet.mjs @@ -0,0 +1,39 @@ +const { HandlebarsApplicationMixin } = foundry.applications.api + +export default class CelestopolItemSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ItemSheetV2) { + /** @override */ + static DEFAULT_OPTIONS = { + classes: ["fvtt-celestopol", "item"], + position: { width: 580, height: "auto" }, + form: { submitOnChange: true }, + window: { resizable: true }, + actions: { + editImage: CelestopolItemSheet.#onEditImage, + }, + } + + /** @override */ + async _prepareContext() { + return { + fields: this.document.schema.fields, + systemFields: this.document.system.schema.fields, + item: this.document, + system: this.document.system, + source: this.document.toObject(), + isEditable: this.isEditable, + } + } + + static async #onEditImage(event, target) { + const attr = target.dataset.edit + const current = foundry.utils.getProperty(this.document, attr) + const fp = new FilePicker({ + current, + type: "image", + callback: (path) => this.document.update({ [attr]: path }), + top: this.position.top + 40, + left: this.position.left + 10, + }) + return fp.browse() + } +} diff --git a/module/applications/sheets/character-sheet.mjs b/module/applications/sheets/character-sheet.mjs new file mode 100644 index 0000000..a605a9f --- /dev/null +++ b/module/applications/sheets/character-sheet.mjs @@ -0,0 +1,116 @@ +import CelestopolActorSheet from "./base-actor-sheet.mjs" +import { SYSTEM } from "../../config/system.mjs" + +export default class CelestopolCharacterSheet extends CelestopolActorSheet { + /** @override */ + static DEFAULT_OPTIONS = { + classes: ["character"], + position: { width: 920, height: 660 }, + window: { contentClasses: ["character-content"] }, + actions: { + createAnomaly: CelestopolCharacterSheet.#onCreateAnomaly, + createAspect: CelestopolCharacterSheet.#onCreateAspect, + createAttribute: CelestopolCharacterSheet.#onCreateAttribute, + createEquipment: CelestopolCharacterSheet.#onCreateEquipment, + }, + } + + /** @override */ + static PARTS = { + main: { template: "systems/fvtt-celestopol/templates/character-main.hbs" }, + tabs: { template: "templates/generic/tab-navigation.hbs" }, + competences:{ template: "systems/fvtt-celestopol/templates/character-competences.hbs" }, + blessures: { template: "systems/fvtt-celestopol/templates/character-blessures.hbs" }, + factions: { template: "systems/fvtt-celestopol/templates/character-factions.hbs" }, + biography: { template: "systems/fvtt-celestopol/templates/character-biography.hbs" }, + } + + tabGroups = { sheet: "competences" } + + #getTabs() { + const tabs = { + competences:{ id: "competences", group: "sheet", icon: "fa-solid fa-dice-d6", label: "CELESTOPOL.Tab.competences" }, + blessures: { id: "blessures", group: "sheet", icon: "fa-solid fa-heart-crack", label: "CELESTOPOL.Tab.blessures" }, + factions: { id: "factions", group: "sheet", icon: "fa-solid fa-flag", label: "CELESTOPOL.Tab.factions" }, + biography: { id: "biography", group: "sheet", icon: "fa-solid fa-book", label: "CELESTOPOL.Tab.biography" }, + } + for (const v of Object.values(tabs)) { + v.active = this.tabGroups[v.group] === v.id + v.cssClass = v.active ? "active" : "" + } + return tabs + } + + /** @override */ + async _prepareContext() { + const context = await super._prepareContext() + context.tabs = this.#getTabs() + context.stats = SYSTEM.STATS + context.skills = SYSTEM.SKILLS + context.anomalyTypes = SYSTEM.ANOMALY_TYPES + context.factions = SYSTEM.FACTIONS + context.woundLevels = SYSTEM.WOUND_LEVELS + return context + } + + /** @override */ + async _preparePartContext(partId, context) { + context.systemFields = this.document.system.schema.fields + const doc = this.document + + switch (partId) { + case "main": + break + + case "competences": + context.tab = context.tabs.competences + context.anomalies = doc.itemTypes.anomaly + context.aspects = doc.itemTypes.aspect + context.attributes = doc.itemTypes.attribute + break + + case "blessures": + context.tab = context.tabs.blessures + break + + case "factions": + context.tab = context.tabs.factions + break + + case "biography": + context.tab = context.tabs.biography + context.equipments = doc.itemTypes.equipment + context.equipments.sort((a, b) => a.name.localeCompare(b.name)) + context.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML( + doc.system.description, { async: true }) + context.enrichedNotes = await foundry.applications.ux.TextEditor.implementation.enrichHTML( + doc.system.notes, { async: true }) + break + } + return context + } + + static #onCreateAnomaly() { + this.document.createEmbeddedDocuments("Item", [{ + name: game.i18n.localize("CELESTOPOL.Item.newAnomaly"), type: "anomaly", + }]) + } + + static #onCreateAspect() { + this.document.createEmbeddedDocuments("Item", [{ + name: game.i18n.localize("CELESTOPOL.Item.newAspect"), type: "aspect", + }]) + } + + static #onCreateAttribute() { + this.document.createEmbeddedDocuments("Item", [{ + name: game.i18n.localize("CELESTOPOL.Item.newAttribute"), type: "attribute", + }]) + } + + static #onCreateEquipment() { + this.document.createEmbeddedDocuments("Item", [{ + name: game.i18n.localize("CELESTOPOL.Item.newEquipment"), type: "equipment", + }]) + } +} diff --git a/module/applications/sheets/item-sheets.mjs b/module/applications/sheets/item-sheets.mjs new file mode 100644 index 0000000..cb86ef1 --- /dev/null +++ b/module/applications/sheets/item-sheets.mjs @@ -0,0 +1,83 @@ +import CelestopolItemSheet from "./base-item-sheet.mjs" +import { SYSTEM } from "../../config/system.mjs" + +export class CelestopolAnomalySheet extends CelestopolItemSheet { + static DEFAULT_OPTIONS = { + classes: ["anomaly"], + position: { width: 620, height: 560 }, + } + static PARTS = { + main: { template: "systems/fvtt-celestopol/templates/anomaly.hbs" }, + } + async _prepareContext() { + const ctx = await super._prepareContext() + ctx.anomalyTypes = SYSTEM.ANOMALY_TYPES + ctx.skills = SYSTEM.SKILLS + ctx.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML( + this.document.system.description, { async: true }) + ctx.enrichedTechnique = await foundry.applications.ux.TextEditor.implementation.enrichHTML( + this.document.system.technique, { async: true }) + ctx.enrichedNarratif = await foundry.applications.ux.TextEditor.implementation.enrichHTML( + this.document.system.narratif, { async: true }) + return ctx + } +} + +export class CelestopolAspectSheet extends CelestopolItemSheet { + static DEFAULT_OPTIONS = { + classes: ["aspect"], + position: { width: 620, height: 520 }, + } + static PARTS = { + main: { template: "systems/fvtt-celestopol/templates/aspect.hbs" }, + } + async _prepareContext() { + const ctx = await super._prepareContext() + ctx.skills = SYSTEM.SKILLS + ctx.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML( + this.document.system.description, { async: true }) + ctx.enrichedTechnique = await foundry.applications.ux.TextEditor.implementation.enrichHTML( + this.document.system.technique, { async: true }) + ctx.enrichedNarratif = await foundry.applications.ux.TextEditor.implementation.enrichHTML( + this.document.system.narratif, { async: true }) + return ctx + } +} + +export class CelestopolAttributeSheet extends CelestopolItemSheet { + static DEFAULT_OPTIONS = { + classes: ["attribute"], + position: { width: 620, height: 520 }, + } + static PARTS = { + main: { template: "systems/fvtt-celestopol/templates/attribute.hbs" }, + } + async _prepareContext() { + const ctx = await super._prepareContext() + ctx.skills = SYSTEM.SKILLS + ctx.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML( + this.document.system.description, { async: true }) + ctx.enrichedTechnique = await foundry.applications.ux.TextEditor.implementation.enrichHTML( + this.document.system.technique, { async: true }) + ctx.enrichedNarratif = await foundry.applications.ux.TextEditor.implementation.enrichHTML( + this.document.system.narratif, { async: true }) + return ctx + } +} + +export class CelestopolEquipmentSheet extends CelestopolItemSheet { + static DEFAULT_OPTIONS = { + classes: ["equipment"], + position: { width: 540, height: 420 }, + } + static PARTS = { + main: { template: "systems/fvtt-celestopol/templates/equipment.hbs" }, + } + async _prepareContext() { + const ctx = await super._prepareContext() + ctx.equipmentTypes = SYSTEM.EQUIPMENT_TYPES + ctx.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML( + this.document.system.description, { async: true }) + return ctx + } +} diff --git a/module/applications/sheets/npc-sheet.mjs b/module/applications/sheets/npc-sheet.mjs new file mode 100644 index 0000000..b15e435 --- /dev/null +++ b/module/applications/sheets/npc-sheet.mjs @@ -0,0 +1,57 @@ +import CelestopolActorSheet from "./base-actor-sheet.mjs" +import { SYSTEM } from "../../config/system.mjs" + +export default class CelestopolNPCSheet extends CelestopolActorSheet { + /** @override */ + static DEFAULT_OPTIONS = { + classes: ["npc"], + position: { width: 760, height: 600 }, + window: { contentClasses: ["npc-content"] }, + } + + /** @override */ + static PARTS = { + main: { template: "systems/fvtt-celestopol/templates/npc-main.hbs" }, + tabs: { template: "templates/generic/tab-navigation.hbs" }, + competences:{ template: "systems/fvtt-celestopol/templates/npc-competences.hbs" }, + blessures: { template: "systems/fvtt-celestopol/templates/npc-blessures.hbs" }, + } + + tabGroups = { sheet: "competences" } + + #getTabs() { + const tabs = { + competences:{ id: "competences", group: "sheet", icon: "fa-solid fa-dice-d6", label: "CELESTOPOL.Tab.competences" }, + blessures: { id: "blessures", group: "sheet", icon: "fa-solid fa-heart-crack", label: "CELESTOPOL.Tab.blessures" }, + } + for (const v of Object.values(tabs)) { + v.active = this.tabGroups[v.group] === v.id + v.cssClass = v.active ? "active" : "" + } + return tabs + } + + /** @override */ + async _prepareContext() { + const context = await super._prepareContext() + context.tabs = this.#getTabs() + context.stats = SYSTEM.STATS + context.skills = SYSTEM.SKILLS + context.woundLevels = SYSTEM.WOUND_LEVELS + return context + } + + /** @override */ + async _preparePartContext(partId, context) { + context.systemFields = this.document.system.schema.fields + switch (partId) { + case "competences": + context.tab = context.tabs.competences + break + case "blessures": + context.tab = context.tabs.blessures + break + } + return context + } +} diff --git a/module/config/system.mjs b/module/config/system.mjs new file mode 100644 index 0000000..1c24cbe --- /dev/null +++ b/module/config/system.mjs @@ -0,0 +1,130 @@ +export const SYSTEM_ID = "fvtt-celestopol" + +export const ASCII = ` + ░█▀▀░█▀▀░█░░░█▀▀░█▀▀░▀█▀░█▀█░█▀█░█▀█░█░░ + ░█░░░█▀▀░█░░░█▀▀░▀▀█░░█░░█░█░█▀▀░█░█░█░░ + ░▀▀▀░▀▀▀░▀▀▀░▀▀▀░▀▀▀░░▀░░▀▀▀░▀░░░▀▀▀░▀▀▀ + ░░░░░░░░░░░░░░░░░░1922░░░░░░░░░░░░░░░░░░░ +` + +/** Les 4 attributs principaux (stats). Chacun a une résistance (res) et 4 compétences. */ +export const STATS = { + ame: { id: "ame", label: "CELESTOPOL.Stat.ame" }, + corps: { id: "corps", label: "CELESTOPOL.Stat.corps" }, + coeur: { id: "coeur", label: "CELESTOPOL.Stat.coeur" }, + esprit: { id: "esprit", label: "CELESTOPOL.Stat.esprit" }, +} + +/** Compétences groupées par attribut. */ +export const SKILLS = { + ame: { + artifice: { id: "artifice", label: "CELESTOPOL.Skill.artifice", stat: "ame" }, + attraction: { id: "attraction", label: "CELESTOPOL.Skill.attraction", stat: "ame" }, + coercition: { id: "coercition", label: "CELESTOPOL.Skill.coercition", stat: "ame" }, + faveur: { id: "faveur", label: "CELESTOPOL.Skill.faveur", stat: "ame" }, + }, + corps: { + echauffouree: { id: "echauffouree", label: "CELESTOPOL.Skill.echauffouree", stat: "corps" }, + effacement: { id: "effacement", label: "CELESTOPOL.Skill.effacement", stat: "corps" }, + mobilite: { id: "mobilite", label: "CELESTOPOL.Skill.mobilite", stat: "corps" }, + prouesse: { id: "prouesse", label: "CELESTOPOL.Skill.prouesse", stat: "corps" }, + }, + coeur: { + appreciation: { id: "appreciation", label: "CELESTOPOL.Skill.appreciation", stat: "coeur" }, + arts: { id: "arts", label: "CELESTOPOL.Skill.arts", stat: "coeur" }, + inspiration: { id: "inspiration", label: "CELESTOPOL.Skill.inspiration", stat: "coeur" }, + traque: { id: "traque", label: "CELESTOPOL.Skill.traque", stat: "coeur" }, + }, + esprit: { + instruction: { id: "instruction", label: "CELESTOPOL.Skill.instruction", stat: "esprit" }, + mtechnologique: { id: "mtechnologique", label: "CELESTOPOL.Skill.mtechnologique", stat: "esprit" }, + raisonnement: { id: "raisonnement", label: "CELESTOPOL.Skill.raisonnement", stat: "esprit" }, + traitement: { id: "traitement", label: "CELESTOPOL.Skill.traitement", stat: "esprit" }, + }, +} + +/** Liste plate de toutes les compétences (utile pour les DataModels d'items). */ +export const ALL_SKILLS = Object.values(SKILLS).flatMap(group => Object.values(group)) + +/** Types d'anomalies (pouvoirs paranormaux). */ +export const ANOMALY_TYPES = { + none: { id: "none", label: "CELESTOPOL.Anomaly.none" }, + entropie: { id: "entropie", label: "CELESTOPOL.Anomaly.entropie" }, + communicationaveclesmorts:{ id: "communicationaveclesmorts",label: "CELESTOPOL.Anomaly.communicationaveclesmorts" }, + illusion: { id: "illusion", label: "CELESTOPOL.Anomaly.illusion" }, + suggestion: { id: "suggestion", label: "CELESTOPOL.Anomaly.suggestion" }, + tarotdivinatoire: { id: "tarotdivinatoire", label: "CELESTOPOL.Anomaly.tarotdivinatoire" }, + telekinesie: { id: "telekinesie", label: "CELESTOPOL.Anomaly.telekinesie" }, + telepathie: { id: "telepathie", label: "CELESTOPOL.Anomaly.telepathie" }, + voyageastral: { id: "voyageastral", label: "CELESTOPOL.Anomaly.voyageastral" }, +} + +/** Factions du monde de Célestopol. */ +export const FACTIONS = { + pinkerton: { id: "pinkerton", label: "CELESTOPOL.Faction.pinkerton" }, + police: { id: "police", label: "CELESTOPOL.Faction.police" }, + okhrana: { id: "okhrana", label: "CELESTOPOL.Faction.okhrana" }, + lunanovatek: { id: "lunanovatek", label: "CELESTOPOL.Faction.lunanovatek" }, + oto: { id: "oto", label: "CELESTOPOL.Faction.oto" }, + syndicats: { id: "syndicats", label: "CELESTOPOL.Faction.syndicats" }, + vorovskoymir:{ id: "vorovskoymir",label: "CELESTOPOL.Faction.vorovskoymir" }, + cour: { id: "cour", label: "CELESTOPOL.Faction.cour" }, +} + +/** Niveaux de blessures avec leur malus associé. */ +export const WOUND_LEVELS = [ + { id: 0, label: "CELESTOPOL.Wound.none", malus: 0 }, + { id: 1, label: "CELESTOPOL.Wound.anodin", malus: 0 }, + { id: 2, label: "CELESTOPOL.Wound.derisoire", malus: 0 }, + { id: 3, label: "CELESTOPOL.Wound.negligeable", malus: -1 }, + { id: 4, label: "CELESTOPOL.Wound.superficiel", malus: -1 }, + { id: 5, label: "CELESTOPOL.Wound.leger", malus: -2 }, + { id: 6, label: "CELESTOPOL.Wound.modere", malus: -2 }, + { id: 7, label: "CELESTOPOL.Wound.grave", malus: -3 }, + { id: 8, label: "CELESTOPOL.Wound.dramatique", malus: -999 }, +] + +/** Seuils de difficulté pour les jets de dés. */ +export const DIFFICULTY_CHOICES = { + unknown: { id: "unknown", label: "CELESTOPOL.Difficulty.unknown", value: 0 }, + facile: { id: "facile", label: "CELESTOPOL.Difficulty.facile", value: 5 }, + normal: { id: "normal", label: "CELESTOPOL.Difficulty.normal", value: 7 }, + difficile:{ id: "difficile", label: "CELESTOPOL.Difficulty.difficile", value: 9 }, + ardu: { id: "ardu", label: "CELESTOPOL.Difficulty.ardu", value: 11 }, + extreme: { id: "extreme", label: "CELESTOPOL.Difficulty.extreme", value: 13 }, +} + +/** Phases de la lune (dé de lune). */ +export const MOON_DICE_PHASES = { + none: { id: "none", label: "CELESTOPOL.Moon.none", bonus: 0 }, + nouvellelune: { id: "nouvellelune", label: "CELESTOPOL.Moon.nouvellelune", bonus: 0 }, + premiercroissant: { id: "premiercroissant", label: "CELESTOPOL.Moon.premiercroissant", bonus: 1 }, + premierquartier: { id: "premierquartier", label: "CELESTOPOL.Moon.premierquartier", bonus: 1 }, + lunegibbeuse: { id: "lunegibbeuse", label: "CELESTOPOL.Moon.lunegibbeuse", bonus: 2 }, + lunevoutee: { id: "lunevoutee", label: "CELESTOPOL.Moon.lunevoutee", bonus: 2 }, + derniercroissant: { id: "derniercroissant", label: "CELESTOPOL.Moon.derniercroissant", bonus: 1 }, + dernierquartier: { id: "dernierquartier", label: "CELESTOPOL.Moon.dernierquartier", bonus: 1 }, + pleinelune: { id: "pleinelune", label: "CELESTOPOL.Moon.pleinelune", bonus: 3 }, +} + +/** Types d'équipements. */ +export const EQUIPMENT_TYPES = { + autre: { id: "autre", label: "CELESTOPOL.Equipment.autre" }, + arme: { id: "arme", label: "CELESTOPOL.Equipment.arme" }, + protection:{ id: "protection",label: "CELESTOPOL.Equipment.protection" }, + vehicule: { id: "vehicule", label: "CELESTOPOL.Equipment.vehicule" }, +} + +export const SYSTEM = { + id: SYSTEM_ID, + ASCII, + STATS, + SKILLS, + ALL_SKILLS, + ANOMALY_TYPES, + FACTIONS, + WOUND_LEVELS, + DIFFICULTY_CHOICES, + MOON_DICE_PHASES, + EQUIPMENT_TYPES, +} diff --git a/module/documents/_module.mjs b/module/documents/_module.mjs new file mode 100644 index 0000000..fad50dc --- /dev/null +++ b/module/documents/_module.mjs @@ -0,0 +1,4 @@ +export { default as CelestopolActor } from "./actor.mjs" +export { default as CelestopolItem } from "./item.mjs" +export { default as CelestopolChatMessage } from "./chat-message.mjs" +export { CelestopolRoll } from "./roll.mjs" diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs new file mode 100644 index 0000000..5cadf79 --- /dev/null +++ b/module/documents/actor.mjs @@ -0,0 +1,12 @@ +export default class CelestopolActor extends Actor { + /** @override */ + prepareDerivedData() { + super.prepareDerivedData() + this.system.prepareDerivedData?.() + } + + /** @override */ + getRollData() { + return this.toObject(false).system + } +} diff --git a/module/documents/chat-message.mjs b/module/documents/chat-message.mjs new file mode 100644 index 0000000..5bae106 --- /dev/null +++ b/module/documents/chat-message.mjs @@ -0,0 +1,7 @@ +export default class CelestopolChatMessage extends ChatMessage { + /** @override */ + async getHTML() { + const html = await super.getHTML() + return html + } +} diff --git a/module/documents/item.mjs b/module/documents/item.mjs new file mode 100644 index 0000000..b96cb45 --- /dev/null +++ b/module/documents/item.mjs @@ -0,0 +1,11 @@ +export default class CelestopolItem extends Item { + /** @override */ + prepareDerivedData() { + super.prepareDerivedData() + } + + /** @override */ + getRollData() { + return this.toObject(false).system + } +} diff --git a/module/documents/roll.mjs b/module/documents/roll.mjs new file mode 100644 index 0000000..cab0c32 --- /dev/null +++ b/module/documents/roll.mjs @@ -0,0 +1,170 @@ +import { SYSTEM } from "../config/system.mjs" + +/** + * Système de dés de Célestopol 1922. + * + * Le jet de base est : (valeur compétence)d6 comparé à un seuil de difficulté. + * Le dé de lune ajoute un bonus selon la phase actuelle. + * Destin et Spleen modifient le nombre de dés. + */ +export class CelestopolRoll extends Roll { + static CHAT_TEMPLATE = "systems/fvtt-celestopol/templates/chat-message.hbs" + + get resultType() { return this.options.resultType } + get isSuccess() { return this.resultType === "success" } + get isFailure() { return this.resultType === "failure" } + get actorId() { return this.options.actorId } + get actorName() { return this.options.actorName } + get actorImage() { return this.options.actorImage } + get skillLabel() { return this.options.skillLabel } + get difficulty() { return this.options.difficulty } + get moonBonus() { return this.options.moonBonus ?? 0 } + + /** + * Ouvre le dialogue de configuration du jet via DialogV2 et exécute le jet. + * @param {object} options + * @returns {Promise} + */ + static async prompt(options = {}) { + const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes) + const fieldRollMode = new foundry.data.fields.StringField({ + choices: rollModes, + blank: false, + default: "publicroll", + }) + + const dialogContext = { + actorName: options.actorName, + skillLabel: options.skillLabel, + skillValue: options.skillValue, + woundMalus: options.woundMalus ?? 0, + difficultyChoices:SYSTEM.DIFFICULTY_CHOICES, + moonPhaseChoices: SYSTEM.MOON_DICE_PHASES, + defaultDifficulty:options.difficulty ?? "normal", + defaultMoonPhase: options.moonPhase ?? "none", + rollModes, + fieldRollMode, + } + + const content = await foundry.applications.handlebars.renderTemplate( + "systems/fvtt-celestopol/templates/roll-dialog.hbs", + dialogContext + ) + + const title = `${game.i18n.localize("CELESTOPOL.Roll.title")} — ${game.i18n.localize(options.skillLabel ?? "")}` + + const rollContext = await foundry.applications.api.DialogV2.wait({ + window: { title }, + classes: ["fvtt-celestopol", "roll-dialog"], + content, + buttons: [ + { + label: game.i18n.localize("CELESTOPOL.Roll.roll"), + callback: (event, button) => { + return Array.from(button.form.elements).reduce((obj, input) => { + if (input.name) obj[input.name] = input.value + return obj + }, {}) + }, + }, + ], + rejectClose: false, + }) + + if (!rollContext) return null + + const difficulty = rollContext.difficulty ?? "normal" + const diffConfig = SYSTEM.DIFFICULTY_CHOICES[difficulty] ?? SYSTEM.DIFFICULTY_CHOICES.normal + const moonPhase = rollContext.moonPhase ?? "none" + const moonConfig = SYSTEM.MOON_DICE_PHASES[moonPhase] ?? SYSTEM.MOON_DICE_PHASES.none + const modifier = parseInt(rollContext.modifier ?? 0) || 0 + const woundMalus = options.woundMalus ?? 0 + const skillValue = Math.max(0, (options.skillValue ?? 0) + woundMalus) + const nbDice = Math.max(1, skillValue) + const moonBonus = moonConfig.bonus ?? 0 + const totalModifier = moonBonus + modifier + + const formula = totalModifier !== 0 + ? `${nbDice}d6 + ${totalModifier}` + : `${nbDice}d6` + + const rollData = { + ...options, + difficulty, + difficultyValue: diffConfig.value, + moonPhase, + moonBonus, + modifier, + formula, + rollMode: rollContext.visibility ?? "publicroll", + } + + const roll = new this(formula, {}, rollData) + await roll.evaluate() + roll.computeResult() + await roll.toMessage({}, { rollMode: rollData.rollMode }) + + // Mémoriser les préférences sur l'acteur + const actor = game.actors.get(options.actorId) + if (actor) { + await actor.update({ + "system.prefs.moonPhase": moonPhase, + "system.prefs.difficulty": difficulty, + }) + } + + return roll + } + + /** Détermine succès/échec selon le total vs le seuil. */ + computeResult() { + const threshold = SYSTEM.DIFFICULTY_CHOICES[this.options.difficulty]?.value ?? 0 + if (threshold === 0) { + this.options.resultType = "unknown" + } else if (this.total >= threshold) { + this.options.resultType = "success" + } else { + this.options.resultType = "failure" + } + } + + /** @override */ + async render(chatOptions = {}) { + const data = await this._getChatCardData(chatOptions.isPrivate) + return foundry.applications.handlebars.renderTemplate(this.constructor.CHAT_TEMPLATE, data) + } + + async _getChatCardData(isPrivate) { + return { + css: [SYSTEM.id, "dice-roll"], + cssClass: [SYSTEM.id, "dice-roll"].join(" "), + actorId: this.actorId, + actingCharName: this.actorName, + actingCharImg: this.actorImage, + skillLabel: this.skillLabel, + formula: this.formula, + total: this.total, + resultType: this.resultType, + isSuccess: this.isSuccess, + isFailure: this.isFailure, + difficulty: this.options.difficulty, + difficultyValue:this.options.difficultyValue, + moonPhase: this.options.moonPhase, + moonBonus: this.moonBonus, + isPrivate, + tooltip: isPrivate ? "" : await this.getTooltip(), + results: this.dice[0]?.results ?? [], + } + } + + /** @override */ + async toMessage(messageData = {}, { rollMode, create = true } = {}) { + return super.toMessage( + { + flavor: `${game.i18n.localize(this.skillLabel ?? "")}`, + ...messageData, + }, + { rollMode } + ) + } +} diff --git a/module/models/_module.mjs b/module/models/_module.mjs new file mode 100644 index 0000000..9afd95e --- /dev/null +++ b/module/models/_module.mjs @@ -0,0 +1,3 @@ +export { default as CelestopolCharacter } from "./character.mjs" +export { default as CelestopolNPC } from "./npc.mjs" +export { CelestopolAnomaly, CelestopolAspect, CelestopolAttribute, CelestopolEquipment } from "./items.mjs" diff --git a/module/models/character.mjs b/module/models/character.mjs new file mode 100644 index 0000000..4abe1e5 --- /dev/null +++ b/module/models/character.mjs @@ -0,0 +1,175 @@ +import { SYSTEM } from "../config/system.mjs" + +export default class CelestopolCharacter extends foundry.abstract.TypeDataModel { + static defineSchema() { + const fields = foundry.data.fields + const reqInt = { required: true, nullable: false, integer: true } + const schema = {} + + // Concept du personnage + schema.concept = new fields.StringField({ required: true, nullable: false, initial: "" }) + + // Initiative (calculée mais stockée pour affichage) + schema.initiative = new fields.NumberField({ ...reqInt, initial: 0, min: 0 }) + + // Anomalie du personnage + schema.anomaly = new fields.SchemaField({ + type: new fields.StringField({ required: true, nullable: false, initial: "none", + choices: Object.keys(SYSTEM.ANOMALY_TYPES) }), + value: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }), + }) + + // Les 4 stats avec leurs compétences + const skillField = (label) => new fields.SchemaField({ + label: new fields.StringField({ required: true, initial: label }), + value: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }), + }) + + const statField = (statId) => { + const skills = SYSTEM.SKILLS[statId] + const skillSchema = {} + for (const [key, skill] of Object.entries(skills)) { + skillSchema[key] = skillField(skill.label) + } + return new fields.SchemaField({ + label: new fields.StringField({ required: true, initial: SYSTEM.STATS[statId].label }), + res: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }), + ...skillSchema, + }) + } + + schema.stats = new fields.SchemaField({ + ame: statField("ame"), + corps: statField("corps"), + coeur: statField("coeur"), + esprit: statField("esprit"), + }) + + // Blessures (8 cases) + const woundField = (idx) => new fields.SchemaField({ + checked: new fields.BooleanField({ required: true, initial: false }), + malus: new fields.NumberField({ ...reqInt, initial: SYSTEM.WOUND_LEVELS[idx]?.malus ?? 0 }), + }) + schema.blessures = new fields.SchemaField({ + lvl: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }), + b1: woundField(1), b2: woundField(2), b3: woundField(3), b4: woundField(4), + b5: woundField(5), b6: woundField(6), b7: woundField(7), b8: woundField(8), + }) + + // Destin (8 cases) + const destField = () => new fields.SchemaField({ + checked: new fields.BooleanField({ required: true, initial: false }), + }) + schema.destin = new fields.SchemaField({ + lvl: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }), + d1: destField(), d2: destField(), d3: destField(), d4: destField(), + d5: destField(), d6: destField(), d7: destField(), d8: destField(), + }) + + // Spleen (8 cases) + schema.spleen = new fields.SchemaField({ + lvl: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }), + s1: destField(), s2: destField(), s3: destField(), s4: destField(), + s5: destField(), s6: destField(), s7: destField(), s8: destField(), + }) + + // Attributs de personnage (Entregent, Fortune, Rêve, Vision) + const persoAttrField = () => new fields.SchemaField({ + value: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }), + max: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }), + }) + schema.attributs = new fields.SchemaField({ + entregent: persoAttrField(), + fortune: persoAttrField(), + reve: persoAttrField(), + vision: persoAttrField(), + }) + + // Factions + const factionField = () => new fields.SchemaField({ + value: new fields.NumberField({ ...reqInt, initial: 0 }), + }) + schema.factions = new fields.SchemaField({ + pinkerton: factionField(), + police: factionField(), + okhrana: factionField(), + lunanovatek: factionField(), + oto: factionField(), + syndicats: factionField(), + vorovskoymir: factionField(), + cour: factionField(), + perso1: new fields.SchemaField({ + label: new fields.StringField({ required: true, nullable: false, initial: "" }), + value: new fields.NumberField({ ...reqInt, initial: 0 }), + }), + perso2: new fields.SchemaField({ + label: new fields.StringField({ required: true, nullable: false, initial: "" }), + value: new fields.NumberField({ ...reqInt, initial: 0 }), + }), + }) + + // Préférences de jet (mémorisé entre sessions) + schema.prefs = new fields.SchemaField({ + moonPhase: new fields.StringField({ required: true, nullable: false, initial: "none" }), + difficulty: new fields.StringField({ required: true, nullable: false, initial: "normal" }), + }) + + // Description & notes + schema.description = new fields.HTMLField({ required: true, textSearch: true }) + schema.notes = new fields.HTMLField({ required: true, textSearch: true }) + + // Données biographiques + schema.biodata = new fields.SchemaField({ + age: new fields.StringField({ required: true, nullable: false, initial: "" }), + genre: new fields.StringField({ required: true, nullable: false, initial: "" }), + taille: new fields.StringField({ required: true, nullable: false, initial: "" }), + yeux: new fields.StringField({ required: true, nullable: false, initial: "" }), + naissance: new fields.StringField({ required: true, nullable: false, initial: "" }), + cheveux: new fields.StringField({ required: true, nullable: false, initial: "" }), + origine: new fields.StringField({ required: true, nullable: false, initial: "" }), + }) + + return schema + } + + static LOCALIZATION_PREFIXES = ["CELESTOPOL.Character"] + + prepareDerivedData() { + super.prepareDerivedData() + // L'initiative est basée sur la résistance Corps + this.initiative = this.stats.corps.res + } + + /** + * Calcule le malus de blessures actif. + * @returns {number} + */ + getWoundMalus() { + const lvl = Math.max(0, Math.min(8, this.blessures.lvl)) + return SYSTEM.WOUND_LEVELS[lvl]?.malus ?? 0 + } + + /** + * Lance les dés pour une compétence donnée. + * @param {string} statId - Id de la stat (ame, corps, coeur, esprit) + * @param {string} skillId - Id de la compétence + */ + async roll(statId, skillId) { + const { CelestopolRoll } = await import("../documents/roll.mjs") + const skill = this.stats[statId][skillId] + if (!skill) return null + + return CelestopolRoll.prompt({ + actorId: this.parent.id, + actorName: this.parent.name, + actorImage: this.parent.img, + statId, + skillId, + skillLabel: skill.label, + skillValue: skill.value, + woundMalus: this.getWoundMalus(), + moonPhase: this.prefs.moonPhase, + difficulty: this.prefs.difficulty, + }) + } +} diff --git a/module/models/items.mjs b/module/models/items.mjs new file mode 100644 index 0000000..6fea258 --- /dev/null +++ b/module/models/items.mjs @@ -0,0 +1,98 @@ +import { SYSTEM } from "../config/system.mjs" + +/** Schéma partagé pour les bonus/malus par compétence (utilisé dans anomaly/aspect/attribute). */ +function skillScoresSchema() { + const fields = foundry.data.fields + const reqInt = { required: true, nullable: false, integer: true } + const scoreField = () => new fields.SchemaField({ + bonus: new fields.NumberField({ ...reqInt, initial: 0 }), + malus: new fields.NumberField({ ...reqInt, initial: 0 }), + }) + + const statGroup = (statId) => { + const skills = SYSTEM.SKILLS[statId] + const schema = {} + for (const key of Object.keys(skills)) { + schema[key] = scoreField() + } + return new fields.SchemaField(schema) + } + + return new fields.SchemaField({ + ame: statGroup("ame"), + corps: statGroup("corps"), + coeur: statGroup("coeur"), + esprit: statGroup("esprit"), + }) +} + +export class CelestopolAnomaly extends foundry.abstract.TypeDataModel { + static defineSchema() { + const fields = foundry.data.fields + const reqInt = { required: true, nullable: false, integer: true } + return { + subtype: new fields.StringField({ required: true, nullable: false, initial: "none", + choices: Object.keys(SYSTEM.ANOMALY_TYPES) }), + value: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }), + reference: new fields.StringField({ required: true, nullable: false, initial: "" }), + scores: skillScoresSchema(), + description: new fields.HTMLField({ required: true, textSearch: true }), + technique: new fields.HTMLField({ required: true, textSearch: true }), + narratif: new fields.HTMLField({ required: true, textSearch: true }), + notes: new fields.HTMLField({ required: true, textSearch: true }), + } + } +} + +export class CelestopolAspect extends foundry.abstract.TypeDataModel { + static defineSchema() { + const fields = foundry.data.fields + const reqInt = { required: true, nullable: false, integer: true } + return { + value: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }), + reference: new fields.StringField({ required: true, nullable: false, initial: "" }), + scores: skillScoresSchema(), + description: new fields.HTMLField({ required: true, textSearch: true }), + technique: new fields.HTMLField({ required: true, textSearch: true }), + narratif: new fields.HTMLField({ required: true, textSearch: true }), + notes: new fields.HTMLField({ required: true, textSearch: true }), + } + } +} + +export class CelestopolAttribute extends foundry.abstract.TypeDataModel { + static defineSchema() { + const fields = foundry.data.fields + const reqInt = { required: true, nullable: false, integer: true } + return { + value: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }), + reference: new fields.StringField({ required: true, nullable: false, initial: "" }), + scores: skillScoresSchema(), + description: new fields.HTMLField({ required: true, textSearch: true }), + technique: new fields.HTMLField({ required: true, textSearch: true }), + narratif: new fields.HTMLField({ required: true, textSearch: true }), + notes: new fields.HTMLField({ required: true, textSearch: true }), + } + } +} + +export class CelestopolEquipment extends foundry.abstract.TypeDataModel { + static defineSchema() { + const fields = foundry.data.fields + const reqInt = { required: true, nullable: false, integer: true } + return { + subtype: new fields.StringField({ required: true, nullable: false, initial: "autre", + choices: Object.keys(SYSTEM.EQUIPMENT_TYPES) }), + reference: new fields.StringField({ required: true, nullable: false, initial: "" }), + quantity: new fields.NumberField({ ...reqInt, initial: 1, min: 0 }), + weight: new fields.NumberField({ required: true, nullable: false, initial: 0, min: 0 }), + damage: new fields.StringField({ required: true, nullable: false, initial: "" }), + range: new fields.StringField({ required: true, nullable: false, initial: "" }), + speed: new fields.StringField({ required: true, nullable: false, initial: "" }), + protection: new fields.StringField({ required: true, nullable: false, initial: "" }), + crew: new fields.StringField({ required: true, nullable: false, initial: "" }), + description:new fields.HTMLField({ required: true, textSearch: true }), + notes: new fields.HTMLField({ required: true, textSearch: true }), + } + } +} diff --git a/module/models/npc.mjs b/module/models/npc.mjs new file mode 100644 index 0000000..cab25d9 --- /dev/null +++ b/module/models/npc.mjs @@ -0,0 +1,99 @@ +import { SYSTEM } from "../config/system.mjs" + +export default class CelestopolNPC extends foundry.abstract.TypeDataModel { + static defineSchema() { + const fields = foundry.data.fields + const reqInt = { required: true, nullable: false, integer: true } + const schema = {} + + schema.concept = new fields.StringField({ required: true, nullable: false, initial: "" }) + schema.initiative = new fields.NumberField({ ...reqInt, initial: 0, min: 0 }) + + schema.anomaly = new fields.SchemaField({ + type: new fields.StringField({ required: true, nullable: false, initial: "none", + choices: Object.keys(SYSTEM.ANOMALY_TYPES) }), + value: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }), + }) + + const skillField = (label) => new fields.SchemaField({ + label: new fields.StringField({ required: true, initial: label }), + value: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }), + }) + + const statField = (statId) => { + const skills = SYSTEM.SKILLS[statId] + const skillSchema = {} + for (const [key, skill] of Object.entries(skills)) { + skillSchema[key] = skillField(skill.label) + } + return new fields.SchemaField({ + label: new fields.StringField({ required: true, initial: SYSTEM.STATS[statId].label }), + res: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }), + actuel: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }), // res + wound malus + ...skillSchema, + }) + } + + schema.stats = new fields.SchemaField({ + ame: statField("ame"), + corps: statField("corps"), + coeur: statField("coeur"), + esprit: statField("esprit"), + }) + + const woundField = (idx) => new fields.SchemaField({ + checked: new fields.BooleanField({ required: true, initial: false }), + malus: new fields.NumberField({ ...reqInt, initial: SYSTEM.WOUND_LEVELS[idx]?.malus ?? 0 }), + }) + schema.blessures = new fields.SchemaField({ + lvl: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }), + b1: woundField(1), b2: woundField(2), b3: woundField(3), b4: woundField(4), + b5: woundField(5), b6: woundField(6), b7: woundField(7), b8: woundField(8), + }) + + schema.prefs = new fields.SchemaField({ + moonPhase: new fields.StringField({ required: true, nullable: false, initial: "none" }), + difficulty: new fields.StringField({ required: true, nullable: false, initial: "normal" }), + }) + + schema.description = new fields.HTMLField({ required: true, textSearch: true }) + schema.notes = new fields.HTMLField({ required: true, textSearch: true }) + + return schema + } + + static LOCALIZATION_PREFIXES = ["CELESTOPOL.NPC"] + + prepareDerivedData() { + super.prepareDerivedData() + const malus = this.getWoundMalus() + this.initiative = Math.max(0, this.stats.corps.res + malus) + for (const stat of Object.values(this.stats)) { + stat.actuel = Math.max(0, stat.res + malus) + } + } + + getWoundMalus() { + const lvl = Math.max(0, Math.min(8, this.blessures.lvl)) + return SYSTEM.WOUND_LEVELS[lvl]?.malus ?? 0 + } + + async roll(statId, skillId) { + const { CelestopolRoll } = await import("../documents/roll.mjs") + const skill = this.stats[statId][skillId] + if (!skill) return null + + return CelestopolRoll.prompt({ + actorId: this.parent.id, + actorName: this.parent.name, + actorImage: this.parent.img, + statId, + skillId, + skillLabel: skill.label, + skillValue: skill.value, + woundMalus: this.getWoundMalus(), + moonPhase: this.prefs.moonPhase, + difficulty: this.prefs.difficulty, + }) + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..24e803a --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "fvtt-celestopol", + "private": true, + "version": "1.0.0", + "description": "Système FoundryVTT pour Célestopol 1922", + "author": "LeRatierBretonnien / Uberwald", + "license": "UNLICENSED", + "main": "gulpfile.js", + "devDependencies": { + "@eslint/js": "^9.8.0", + "@foundryvtt/foundryvtt-cli": "^1.0.2", + "commander": "^11.1.0", + "eslint": "^9.9.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-jsdoc": "^48.11.0", + "eslint-plugin-prettier": "^5.2.1", + "globals": "^15.9.0", + "less": "^4.1.3", + "prettier": "^3.3.3" + }, + "dependencies": { + "gulp": "^5.0.0", + "gulp-less": "^5.0.0" + }, + "scripts": { + "build": "gulp css", + "watch": "gulp" + }, + "repository": { + "type": "git", + "url": "https://www.uberwald.me/gitea/uberwald/fvtt-celestopol.git" + } +} diff --git a/styles/character.less b/styles/character.less new file mode 100644 index 0000000..f27daa1 --- /dev/null +++ b/styles/character.less @@ -0,0 +1,241 @@ +@import "mixins"; + +// ─── Character sheet specifics ─────────────────────────────────────────────── + +.celestopol.character-sheet { + + // Attributs perso (Entregent, Fortune, Rêve, Vision) + .perso-attributs { + display: flex; + gap: 8px; + flex-wrap: wrap; + margin-top: 4px; + + .perso-attr { + display: flex; + flex-direction: column; + align-items: center; + background: rgba(0,0,0,0.25); + border: 1px solid var(--cel-orange); + border-radius: 4px; + padding: 4px 8px; + min-width: 60px; + + label { + font-size: 0.6em; + text-transform: uppercase; + color: var(--cel-orange-light); + } + + .attr-display, .attr-val, .attr-max { + color: var(--cel-orange); + font-family: var(--cel-font-title); + font-size: 1em; + font-weight: bold; + } + + input.attr-val, input.attr-max { + width: 28px; + text-align: center; + background: transparent; + border: none; + border-bottom: 1px solid var(--cel-orange-light); + color: var(--cel-orange); + } + } + } + + // Stats × Compétences grid + .stats-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; + padding: 8px 0; + + .stat-block { + border: 1px solid var(--cel-border); + border-radius: 4px; + overflow: hidden; + + .stat-header { + background: var(--cel-green); + color: var(--cel-orange); + display: flex; + justify-content: space-between; + align-items: center; + padding: 4px 8px; + + .stat-name { + font-family: var(--cel-font-title); + font-weight: bold; + font-size: 1em; + text-transform: uppercase; + letter-spacing: 0.05em; + } + + .stat-res { + display: flex; + align-items: center; + gap: 4px; + font-size: 0.8em; + + label { color: var(--cel-orange-light); } + input[type="number"] { width: 30px; .cel-input-std(); } + .stat-res-value { + font-size: 1.3em; + font-weight: bold; + min-width: 24px; + text-align: center; + } + } + } + + .skills-list { + background: var(--cel-cream); + + .skill-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 3px 8px; + border-bottom: 1px solid rgba(139,115,85,0.2); + font-size: 0.85em; + + &.rollable { .cel-rollable(); } + + .skill-name { flex: 1; } + .skill-value { + font-weight: bold; + min-width: 24px; + text-align: center; + color: var(--cel-green); + } + .skill-value-input { + width: 36px; + .cel-input-std(); + text-align: center; + } + } + } + } + } + + // Items (anomalies, aspects, attributs) + .items-section { + display: flex; + flex-direction: column; + gap: 8px; + padding: 8px 0; + + .items-group { + .items-header { + .cel-section-header(); + display: flex; + justify-content: space-between; + align-items: center; + a { color: var(--cel-orange); cursor: pointer; } + } + + .item-row { .cel-item-row(); } + } + } + + // Tracks (Blessures, Destin, Spleen) + .track-section { + border: 1px solid var(--cel-border); + border-radius: 4px; + margin-bottom: 12px; + overflow: hidden; + + .track-header { + background: var(--cel-green); + color: var(--cel-orange); + display: flex; + justify-content: space-between; + align-items: center; + padding: 4px 8px; + + .track-title { + font-family: var(--cel-font-title); + font-weight: bold; + text-transform: uppercase; + font-size: 0.9em; + } + } + + .track-boxes { + display: flex; + padding: 8px; + gap: 8px; + flex-wrap: wrap; + background: var(--cel-cream); + + .track-box { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; + + input[type="checkbox"] { .cel-box(); } + .box-label { + font-size: 0.65em; + color: var(--cel-border); + } + + &.checked input[type="checkbox"] { + accent-color: var(--cel-orange); + } + } + } + + .track-level { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 8px; + background: rgba(139,115,85,0.1); + font-size: 0.85em; + label { color: var(--cel-border); } + input[type="number"] { width: 40px; .cel-input-std(); } + } + } + + // Factions table + .factions-table { + width: 100%; + border-collapse: collapse; + font-size: 0.9em; + + thead tr { + background: var(--cel-green); + color: var(--cel-orange); + th { padding: 4px 8px; font-family: var(--cel-font-title); } + } + + .faction-row { + &:nth-child(odd) td { background: rgba(139,115,85,0.08); } + td { padding: 4px 8px; border-bottom: 1px solid rgba(139,115,85,0.2); } + &.custom td { font-style: italic; color: #666; } + + .faction-value input[type="number"] { + width: 50px; + .cel-input-std(); + text-align: center; + } + } + } + + // Biography / Equipment + .equipments-section { + margin-bottom: 12px; + .section-header { .cel-section-header(); display: flex; justify-content: space-between; } + .item-row { .cel-item-row(); } + .item-qty { font-size: 0.8em; color: var(--cel-border); } + } + + .biography-section, .notes-section { + margin-bottom: 12px; + .section-header { .cel-section-header(); } + .enriched-html { font-size: 0.9em; line-height: 1.6; } + } +} diff --git a/styles/fonts.less b/styles/fonts.less new file mode 100644 index 0000000..1d78adc --- /dev/null +++ b/styles/fonts.less @@ -0,0 +1,13 @@ +@font-face { + font-family: "CopaseticNF"; + src: url("../assets/fonts/CopaseticNF.otf") format("opentype"); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: "CopaseticNF"; + src: url("../assets/fonts/CopaseticNF-Bold.otf") format("opentype"); + font-weight: bold; + font-style: normal; +} diff --git a/styles/fvtt-celestopol.less b/styles/fvtt-celestopol.less new file mode 100644 index 0000000..8130131 --- /dev/null +++ b/styles/fvtt-celestopol.less @@ -0,0 +1,10 @@ +// ─── Master LESS file for fvtt-celestopol ──────────────────────────────────── +// Compilation : gulp css → css/fvtt-celestopol.css + +@import "fonts"; +@import "mixins"; +@import "global"; +@import "character"; +@import "npc"; +@import "items"; +@import "roll"; diff --git a/styles/global.less b/styles/global.less new file mode 100644 index 0000000..4dbcc87 --- /dev/null +++ b/styles/global.less @@ -0,0 +1,158 @@ +// ─── Variables CSS (couleurs + typo) ──────────────────────────────────────── + +:root { + --cel-green: rgb(12, 76, 12); + --cel-green-light: rgb(20, 110, 20); + --cel-green-dark: rgb(6, 40, 6); + --cel-orange: #e07b00; + --cel-orange-light: #f0a040; + --cel-cream: #f5f0e8; + --cel-border: #8b7355; + --cel-shadow: rgba(0,0,0,0.4); + + --cel-font-title: "CopaseticNF", "Palatino Linotype", serif; + --cel-font-body: "Palatino Linotype", "Book Antiqua", Palatino, serif; + --cel-font-ui: "Signika", "Palatino Linotype", sans-serif; +} + +// ─── Sheet base layout ─────────────────────────────────────────────────────── + +.celestopol { + &.sheet { + background: var(--cel-cream); + font-family: var(--cel-font-body); + color: #1a1209; + + .window-content { + padding: 0; + overflow: hidden; + display: flex; + flex-direction: column; + background: var(--cel-cream); + } + } + + // ─── Header ────────────────────────────────────────────────────────────── + + .sheet-header { + display: flex; + align-items: stretch; + background: var(--cel-green); + background-image: url("../assets/ui/fond_cadrille.jpg"); + background-blend-mode: overlay; + padding: 8px; + gap: 8px; + border-bottom: 3px solid var(--cel-orange); + + .actor-portrait { + width: 80px; + height: 80px; + object-fit: cover; + border: 2px solid var(--cel-orange); + cursor: pointer; + } + + .header-fields { + flex: 1; + display: flex; + flex-direction: column; + gap: 4px; + color: var(--cel-cream); + + input[type="text"] { + background: transparent; + border: none; + border-bottom: 1px solid var(--cel-orange-light); + color: var(--cel-orange); + font-family: var(--cel-font-title); + font-size: 1.4em; + font-weight: bold; + padding: 2px 4px; + &::placeholder { color: var(--cel-orange-light); } + } + + .actor-name { + font-family: var(--cel-font-title); + font-size: 1.4em; + color: var(--cel-orange); + margin: 0; + } + + .concept-display, + input[name="system.concept"] { + font-style: italic; + font-size: 0.9em; + color: var(--cel-cream); + background: transparent; + border-bottom: 1px solid rgba(255,255,255,0.3); + } + } + + .header-stats-row { + display: flex; + gap: 12px; + align-items: center; + + .header-stat { + display: flex; + flex-direction: column; + align-items: center; + background: rgba(0,0,0,0.3); + border: 1px solid var(--cel-orange); + border-radius: 4px; + padding: 4px 8px; + + label { + font-size: 0.65em; + text-transform: uppercase; + color: var(--cel-orange-light); + } + + .stat-value { + font-size: 1.4em; + font-family: var(--cel-font-title); + color: var(--cel-orange); + font-weight: bold; + } + } + } + } + + // ─── Tabs ──────────────────────────────────────────────────────────────── + + .sheet-tabs { + display: flex; + background: var(--cel-green-dark); + padding: 0; + border-bottom: 2px solid var(--cel-orange); + + .item { + padding: 6px 12px; + color: var(--cel-cream); + font-family: var(--cel-font-title); + font-size: 0.85em; + text-transform: uppercase; + letter-spacing: 0.05em; + cursor: pointer; + border-right: 1px solid var(--cel-green-light); + transition: background 0.2s; + + &:hover { background: var(--cel-green-light); } + &.active { + background: var(--cel-orange); + color: #1a0a00; + font-weight: bold; + } + } + } + + // ─── Sheet body / tabs content ─────────────────────────────────────────── + + .sheet-body { + flex: 1; + overflow-y: auto; + padding: 8px; + } + + .tab { display: none; &.active { display: block; } } +} diff --git a/styles/items.less b/styles/items.less new file mode 100644 index 0000000..940e601 --- /dev/null +++ b/styles/items.less @@ -0,0 +1,158 @@ +@import "mixins"; + +// ─── Item sheets shared ─────────────────────────────────────────────────────── + +.celestopol.item-sheet { + + .item-header { + display: flex; + align-items: center; + gap: 8px; + padding: 8px; + background: var(--cel-green); + background-image: url("../assets/ui/fond_cadrille.jpg"); + background-blend-mode: overlay; + border-bottom: 2px solid var(--cel-orange); + + .item-portrait img { + width: 56px; + height: 56px; + object-fit: cover; + border: 2px solid var(--cel-orange); + cursor: pointer; + } + + .item-header-fields { + flex: 1; + display: flex; + flex-direction: column; + gap: 4px; + + input[type="text"] { + background: transparent; + border: none; + border-bottom: 1px solid var(--cel-orange-light); + color: var(--cel-orange); + font-family: var(--cel-font-title); + font-size: 1.2em; + font-weight: bold; + padding: 2px 4px; + } + + .item-meta { + display: flex; + gap: 8px; + align-items: center; + + select { + background: transparent; + border: 1px solid var(--cel-orange-light); + color: var(--cel-orange); + font-size: 0.85em; + option { background: var(--cel-green-dark); color: var(--cel-cream); } + } + } + + .item-value-field { + display: flex; + align-items: center; + gap: 4px; + label { color: var(--cel-orange-light); font-size: 0.75em; text-transform: uppercase; } + input[type="number"] { + width: 40px; + background: transparent; + border: 1px solid var(--cel-orange-light); + color: var(--cel-orange); + text-align: center; + font-size: 1.1em; + font-weight: bold; + } + } + } + } + + .item-tabs { + display: flex; + background: var(--cel-green-dark); + border-bottom: 2px solid var(--cel-orange); + + .item { + padding: 5px 10px; + color: var(--cel-cream); + font-family: var(--cel-font-title); + font-size: 0.8em; + text-transform: uppercase; + cursor: pointer; + &:hover { background: var(--cel-green-light); } + &.active { background: var(--cel-orange); color: #1a0a00; font-weight: bold; } + } + } + + section.tab { + padding: 8px; + display: none; + &.active { display: block; } + } + + .form-group { + margin-bottom: 8px; + label { + display: block; + font-size: 0.75em; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--cel-green); + margin-bottom: 2px; + } + input[type="text"], input[type="number"] { .cel-input-std(); width: 100%; box-sizing: border-box; } + } + + // Scores grid + .scores-section { + .scores-header { .cel-section-header(); } + + .scores-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 8px; + + .scores-stat-col { + .scores-stat-name { + font-family: var(--cel-font-title); + font-size: 0.75em; + color: var(--cel-green); + text-transform: uppercase; + border-bottom: 1px solid var(--cel-border); + margin-bottom: 4px; + } + + .score-row { + display: flex; + align-items: center; + gap: 4px; + padding: 2px 0; + font-size: 0.8em; + + .score-skill-name { flex: 1; } + .score-bonus, .score-malus { + width: 36px; + .cel-input-std(); + text-align: center; + } + .score-bonus { border-color: #4a8a4a; } + .score-malus { border-color: #8a4a4a; } + } + } + } + } + + // Equipment-specific + &.equipment-sheet { + .equipment-stats { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 6px; + margin-bottom: 8px; + } + } +} diff --git a/styles/mixins.less b/styles/mixins.less new file mode 100644 index 0000000..30c1b7c --- /dev/null +++ b/styles/mixins.less @@ -0,0 +1,72 @@ +// ─── Mixins ────────────────────────────────────────────────────────────────── + +.cel-section-header() { + font-family: var(--cel-font-title); + font-size: 0.8em; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--cel-green); + border-bottom: 1px solid var(--cel-border); + padding-bottom: 2px; + margin-bottom: 6px; +} + +.cel-input-std() { + background: rgba(255,255,255,0.7); + border: 1px solid var(--cel-border); + border-radius: 2px; + padding: 2px 4px; + color: #1a1209; + font-family: var(--cel-font-body); +} + +.cel-box() { + display: inline-block; + width: 22px; + height: 22px; + border: 2px solid var(--cel-border); + border-radius: 3px; + background: rgba(255,255,255,0.5); + cursor: pointer; + &.checked { background: var(--cel-orange); border-color: var(--cel-orange); } +} + +.cel-rollable() { + cursor: pointer; + transition: background 0.15s, color 0.15s; + &:hover { + background: var(--cel-orange-light); + color: #1a0a00; + border-radius: 3px; + } +} + +.cel-item-row() { + display: flex; + align-items: center; + gap: 6px; + padding: 3px 6px; + border-bottom: 1px solid rgba(139,115,85,0.3); + transition: background 0.1s; + &:hover { background: rgba(224,123,0,0.08); } + + .item-icon { + width: 24px; + height: 24px; + object-fit: cover; + border: 1px solid var(--cel-border); + border-radius: 2px; + } + + .item-name { flex: 1; font-style: italic; } + + .item-controls { + display: flex; + gap: 4px; + opacity: 0; + transition: opacity 0.15s; + a { color: var(--cel-orange); cursor: pointer; } + } + + &:hover .item-controls { opacity: 1; } +} diff --git a/styles/npc.less b/styles/npc.less new file mode 100644 index 0000000..7b06383 --- /dev/null +++ b/styles/npc.less @@ -0,0 +1,117 @@ +@import "mixins"; + +// ─── NPC sheet specifics ───────────────────────────────────────────────────── + +.celestopol.npc-sheet { + + .stats-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; + padding: 8px 0; + + .stat-block { + border: 1px solid var(--cel-border); + border-radius: 4px; + overflow: hidden; + + .stat-header { + background: var(--cel-green); + color: var(--cel-orange); + display: flex; + justify-content: space-between; + align-items: center; + padding: 4px 8px; + + .stat-name { + font-family: var(--cel-font-title); + font-weight: bold; + text-transform: uppercase; + font-size: 0.9em; + } + + .stat-res { + display: flex; + align-items: center; + gap: 4px; + font-size: 0.8em; + + label { color: var(--cel-orange-light); } + .stat-res-value { font-weight: bold; color: var(--cel-orange); } + .stat-actuel { + font-size: 0.9em; + color: rgba(255,200,0,0.7); + font-style: italic; + } + input[type="number"] { width: 30px; .cel-input-std(); } + } + } + + .skills-list { + background: var(--cel-cream); + + .skill-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 3px 8px; + border-bottom: 1px solid rgba(139,115,85,0.2); + font-size: 0.85em; + + &.rollable { .cel-rollable(); } + + .skill-name { flex: 1; } + .skill-value { font-weight: bold; color: var(--cel-green); min-width: 24px; text-align: center; } + .skill-value-input { width: 36px; .cel-input-std(); text-align: center; } + } + } + } + } + + .track-section { + border: 1px solid var(--cel-border); + border-radius: 4px; + margin-bottom: 8px; + overflow: hidden; + + .track-header { + background: var(--cel-green); + color: var(--cel-orange); + padding: 4px 8px; + font-family: var(--cel-font-title); + font-weight: bold; + text-transform: uppercase; + font-size: 0.9em; + } + + .track-boxes { + display: flex; + padding: 8px; + gap: 6px; + background: var(--cel-cream); + + .track-box { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; + .box-label { font-size: 0.65em; color: var(--cel-border); } + } + } + + .track-level { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 8px; + background: rgba(139,115,85,0.1); + font-size: 0.85em; + input[type="number"] { width: 40px; .cel-input-std(); } + } + } + + .description-section { + margin-top: 8px; + .enriched-html { font-size: 0.9em; line-height: 1.6; } + } +} diff --git a/styles/roll.less b/styles/roll.less new file mode 100644 index 0000000..0d08f42 --- /dev/null +++ b/styles/roll.less @@ -0,0 +1,177 @@ +// ─── Roll dialog ───────────────────────────────────────────────────────────── + +.roll-dialog.celestopol { + padding: 8px 12px; + font-family: var(--cel-font-body, "Palatino Linotype", serif); + + .roll-title { + text-align: center; + font-family: var(--cel-font-title, "CopaseticNF", serif); + font-size: 1.1em; + color: var(--cel-green, rgb(12,76,12)); + margin-bottom: 10px; + + .separator { + margin: 0 6px; + color: var(--cel-orange, #e07b00); + } + } + + .form-group { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; + + label { + flex: 0 0 140px; + font-size: 0.85em; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--cel-green, rgb(12,76,12)); + } + + select, input[type="number"] { + flex: 1; + border: 1px solid var(--cel-border, #8b7355); + border-radius: 2px; + padding: 2px 6px; + background: rgba(255,255,255,0.7); + } + } + + .dice-preview { + text-align: center; + font-size: 1em; + margin-top: 10px; + padding: 6px; + background: rgba(12,76,12,0.08); + border-radius: 4px; + border: 1px solid var(--cel-green, rgb(12,76,12)); + + .nb-dice { + font-family: var(--cel-font-title, "CopaseticNF", serif); + font-size: 1.5em; + color: var(--cel-orange, #e07b00); + font-weight: bold; + } + } +} + +// ─── Chat message ───────────────────────────────────────────────────────────── + +.celestopol.chat-roll { + border: 1px solid var(--cel-border, #8b7355); + border-radius: 4px; + overflow: hidden; + font-family: var(--cel-font-body, "Palatino Linotype", serif); + + .roll-header { + display: flex; + align-items: center; + gap: 8px; + background: var(--cel-green, rgb(12,76,12)); + padding: 6px 8px; + + .actor-img { + width: 36px; + height: 36px; + object-fit: cover; + border: 1px solid var(--cel-orange, #e07b00); + border-radius: 2px; + } + + .roll-info { + display: flex; + flex-direction: column; + .actor-name { + font-family: var(--cel-font-title, "CopaseticNF", serif); + color: var(--cel-orange, #e07b00); + font-weight: bold; + } + .skill-info { + color: var(--cel-cream, #f5f0e8); + font-size: 0.8em; + font-style: italic; + } + } + } + + .roll-details { + padding: 8px; + background: var(--cel-cream, #f5f0e8); + + .dice-results { + display: flex; + gap: 4px; + flex-wrap: wrap; + margin-bottom: 6px; + + .die.d6 { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border: 2px solid var(--cel-border, #8b7355); + border-radius: 4px; + background: white; + font-weight: bold; + font-size: 1em; + } + } + + .bonus-line { + display: flex; + justify-content: space-between; + font-size: 0.85em; + color: #666; + padding: 1px 0; + } + + .roll-total-line { + display: flex; + align-items: center; + gap: 8px; + margin-top: 6px; + padding-top: 4px; + border-top: 1px solid var(--cel-border, #8b7355); + + .total-label { + text-transform: uppercase; + font-size: 0.75em; + color: var(--cel-green, rgb(12,76,12)); + } + + .total-value { + font-family: var(--cel-font-title, "CopaseticNF", serif); + font-size: 1.6em; + font-weight: bold; + color: var(--cel-orange, #e07b00); + } + + .vs-difficulty { + font-size: 0.8em; + color: #999; + } + } + } + + .roll-result-banner { + text-align: center; + padding: 4px 8px; + font-family: var(--cel-font-title, "CopaseticNF", serif); + font-size: 1.1em; + text-transform: uppercase; + letter-spacing: 0.08em; + + &.success { background: var(--cel-green, rgb(12,76,12)); color: var(--cel-orange, #e07b00); } + &.failure { background: #4a1a1a; color: #e0a0a0; } + + .critical { + display: block; + font-size: 0.75em; + letter-spacing: 0.12em; + } + } +} diff --git a/system.json b/system.json new file mode 100644 index 0000000..1ac868c --- /dev/null +++ b/system.json @@ -0,0 +1,74 @@ +{ + "id": "fvtt-celestopol", + "title": "Célestopol 1922", + "description": "Système FoundryVTT pour Célestopol 1922, le jeu de rôle d'Antre Monde Éditions.", + "manifest": "https://www.uberwald.me/gitea/uberwald/fvtt-celestopol/releases/download/latest/system.json", + "download": "#{DOWNLOAD}#", + "url": "https://www.uberwald.me/gitea/public/fvtt-celestopol", + "license": "LICENSE", + "version": "13.0.0", + "authors": [ + { + "name": "Uberwald", + "discord": "LeRatierBretonnien" + } + ], + "flags": { + "hotReload": { + "extensions": ["css", "html", "hbs", "json"], + "paths": ["css/", "templates/", "lang/fr.json"] + } + }, + "compatibility": { + "minimum": "13", + "verified": "13" + }, + "esmodules": ["fvtt-celestopol.mjs"], + "styles": ["css/fvtt-celestopol.css"], + "languages": [ + { + "lang": "fr", + "name": "Français", + "path": "lang/fr.json" + } + ], + "documentTypes": { + "Actor": { + "character": { + "htmlFields": ["description", "notes"] + }, + "npc": { + "htmlFields": ["description", "notes"] + } + }, + "Item": { + "anomaly": { "htmlFields": ["description", "technique", "narratif", "notes"] }, + "aspect": { "htmlFields": ["description", "technique", "narratif", "notes"] }, + "attribute": { "htmlFields": ["description", "technique", "narratif", "notes"] }, + "equipment": { "htmlFields": ["description", "notes"] } + } + }, + "packs": [ + { + "name": "aspects", + "label": "Célestopol 1922 — Aspects", + "system": "fvtt-celestopol", + "path": "packs-system/aspects", + "type": "Item" + }, + { + "name": "anomalies", + "label": "Célestopol 1922 — Anomalies", + "system": "fvtt-celestopol", + "path": "packs-system/anomalies", + "type": "Item" + } + ], + "grid": { + "distance": 5, + "units": "m" + }, + "primaryTokenAttribute": "resource", + "socket": true, + "background": "systems/fvtt-celestopol/assets/ui/celestopol_background.webp" +} diff --git a/templates/anomaly.hbs b/templates/anomaly.hbs new file mode 100644 index 0000000..056d937 --- /dev/null +++ b/templates/anomaly.hbs @@ -0,0 +1,52 @@ +
+
+
+ {{item.name}} +
+
+ +
+ +
+ + +
+
+
+
+ + + +
+
+ {{editor system.description target="system.description" button=true editable=isEditable}} +
+
+ + +
+
+ +
+
+ + {{editor system.technique target="system.technique" button=true editable=isEditable}} +
+
+ + {{editor system.narratif target="system.narratif" button=true editable=isEditable}} +
+
+ +
+ {{> "systems/fvtt-celestopol/templates/partials/item-scores.hbs" skills=skills stats=stats}} +
+
diff --git a/templates/aspect.hbs b/templates/aspect.hbs new file mode 100644 index 0000000..7b84b46 --- /dev/null +++ b/templates/aspect.hbs @@ -0,0 +1,32 @@ +
+
+
+ {{item.name}} +
+
+ +
+ + +
+
+
+ +
+ {{editor system.description target="system.description" button=true editable=isEditable}} + + +
+
+ {{editor system.technique target="system.technique" button=true editable=isEditable}} + {{editor system.narratif target="system.narratif" button=true editable=isEditable}} +
+
+ {{> "systems/fvtt-celestopol/templates/partials/item-scores.hbs" skills=skills}} +
+
diff --git a/templates/attribute.hbs b/templates/attribute.hbs new file mode 100644 index 0000000..8daade3 --- /dev/null +++ b/templates/attribute.hbs @@ -0,0 +1,32 @@ +
+
+
+ {{item.name}} +
+
+ +
+ + +
+
+
+ +
+ {{editor system.description target="system.description" button=true editable=isEditable}} + + +
+
+ {{editor system.technique target="system.technique" button=true editable=isEditable}} + {{editor system.narratif target="system.narratif" button=true editable=isEditable}} +
+
+ {{> "systems/fvtt-celestopol/templates/partials/item-scores.hbs" skills=skills}} +
+
diff --git a/templates/character-biography.hbs b/templates/character-biography.hbs new file mode 100644 index 0000000..943422c --- /dev/null +++ b/templates/character-biography.hbs @@ -0,0 +1,42 @@ +
+ {{!-- Équipements --}} +
+
+ {{localize "CELESTOPOL.Item.equipments"}} + {{#if isEditMode}} + + {{/if}} +
+ {{#each equipments as |item|}} +
+ + {{item.name}} + ×{{item.system.quantity}} +
+ + {{#if ../isEditMode}}{{/if}} +
+
+ {{/each}} +
+ + {{!-- Description / Biographie --}} +
+
{{localize "CELESTOPOL.Actor.description"}}
+ {{#if isEditMode}} + {{editor system.description target="system.description" button=true editable=isEditable}} + {{else}} +
{{{enrichedDescription}}}
+ {{/if}} +
+ + {{!-- Notes --}} +
+
{{localize "CELESTOPOL.Actor.notes"}}
+ {{#if isEditMode}} + {{editor system.notes target="system.notes" button=true editable=isEditable}} + {{else}} +
{{{enrichedNotes}}}
+ {{/if}} +
+
diff --git a/templates/character-blessures.hbs b/templates/character-blessures.hbs new file mode 100644 index 0000000..9aa2fe8 --- /dev/null +++ b/templates/character-blessures.hbs @@ -0,0 +1,77 @@ +
+ {{!-- Blessures --}} +
+
+ {{localize "CELESTOPOL.Track.blessures"}} + {{localize "CELESTOPOL.Track.currentMalus"}} : + {{system.blessures.lvl}} + +
+
+ {{#each (array "b1" "b2" "b3" "b4" "b5" "b6" "b7" "b8") as |key idx|}} +
+ + +
+ {{/each}} +
+
+ + {{#if isEditMode}} + + {{else}} + {{system.blessures.lvl}} + {{/if}} +
+
+ + {{!-- Destin --}} +
+
+ {{localize "CELESTOPOL.Track.destin"}} +
+
+ {{#each (array "d1" "d2" "d3" "d4" "d5" "d6" "d7" "d8") as |key|}} +
+ +
+ {{/each}} +
+
+ + {{#if isEditMode}} + + {{else}} + {{system.destin.lvl}} + {{/if}} +
+
+ + {{!-- Spleen --}} +
+
+ {{localize "CELESTOPOL.Track.spleen"}} +
+
+ {{#each (array "s1" "s2" "s3" "s4" "s5" "s6" "s7" "s8") as |key|}} +
+ +
+ {{/each}} +
+
+ + {{#if isEditMode}} + + {{else}} + {{system.spleen.lvl}} + {{/if}} +
+
+
diff --git a/templates/character-competences.hbs b/templates/character-competences.hbs new file mode 100644 index 0000000..d18ad8c --- /dev/null +++ b/templates/character-competences.hbs @@ -0,0 +1,102 @@ +
+ {{!-- Grille des 4 stats × 4 compétences --}} +
+ {{#each stats as |stat statId|}} +
+
+ {{localize stat.label}} +
+ + {{#if ../isEditMode}} + + {{else}} + {{lookup ../system.stats statId 'res'}} + {{/if}} +
+
+
+ {{#each (lookup ../skills statId) as |skill skillId|}} +
+ {{localize skill.label}} + {{#if ../isEditMode}} + + {{else}} + {{lookup (lookup ../system.stats statId) skillId 'value'}} + {{/if}} +
+ {{/each}} +
+
+ {{/each}} +
+ + {{!-- Items : Anomalies, Aspects, Attributs --}} +
+ {{!-- Anomalies --}} +
+
+ {{localize "CELESTOPOL.Item.anomalies"}} + {{#if isEditMode}} + + {{/if}} +
+ {{#each anomalies as |item|}} +
+ {{item.name}} + {{item.name}} + {{item.system.value}} +
+ + {{#if ../isEditMode}}{{/if}} +
+
+ {{/each}} +
+ + {{!-- Aspects --}} +
+
+ {{localize "CELESTOPOL.Item.aspects"}} + {{#if isEditMode}} + + {{/if}} +
+ {{#each aspects as |item|}} +
+ {{item.name}} + {{item.name}} + {{item.system.value}} +
+ + {{#if ../isEditMode}}{{/if}} +
+
+ {{/each}} +
+ + {{!-- Attributs --}} +
+
+ {{localize "CELESTOPOL.Item.attributes"}} + {{#if isEditMode}} + + {{/if}} +
+ {{#each attributes as |item|}} +
+ {{item.name}} + {{item.name}} + {{item.system.value}} +
+ + {{#if ../isEditMode}}{{/if}} +
+
+ {{/each}} +
+
+
diff --git a/templates/character-factions.hbs b/templates/character-factions.hbs new file mode 100644 index 0000000..3868e8d --- /dev/null +++ b/templates/character-factions.hbs @@ -0,0 +1,63 @@ +
+ + + + + + + + + {{#each factions as |faction factionId|}} + + + + + {{/each}} + + {{!-- Factions personnalisées --}} + + + + + + + + + +
{{localize "CELESTOPOL.Faction.label"}}{{localize "CELESTOPOL.Faction.score"}}
{{localize faction.label}} + {{#if ../isEditMode}} + + {{else}} + {{lookup ../system.factions factionId 'value'}} + {{/if}} +
+ {{#if isEditMode}} + + {{else}} + {{#if system.factions.perso1.label}}{{system.factions.perso1.label}}{{else}}—{{/if}} + {{/if}} + + {{#if isEditMode}} + + {{else}} + {{system.factions.perso1.value}} + {{/if}} +
+ {{#if isEditMode}} + + {{else}} + {{#if system.factions.perso2.label}}{{system.factions.perso2.label}}{{else}}—{{/if}} + {{/if}} + + {{#if isEditMode}} + + {{else}} + {{system.factions.perso2.value}} + {{/if}} +
+
diff --git a/templates/character-main.hbs b/templates/character-main.hbs new file mode 100644 index 0000000..3e347f7 --- /dev/null +++ b/templates/character-main.hbs @@ -0,0 +1,62 @@ +
+
+
+ {{actor.name}} +
+
+
+ {{#if isEditMode}} + + {{else}} +

{{actor.name}}

+ {{/if}} +
+
+ {{#if isEditMode}} + + {{else}} + {{system.concept}} + {{/if}} +
+
+
+ + {{system.initiative}} +
+
+ + {{#if isEditMode}} + + + {{else}} + {{localize system.anomaly.type}} {{#if system.anomaly.value}}({{system.anomaly.value}}){{/if}} + {{/if}} +
+
+ + {{!-- Attributs personnage (Entregent, Fortune, Rêve, Vision) --}} +
+ {{#each system.attributs as |attr key|}} +
+ + {{#if ../isEditMode}} + + / + + {{else}} + {{attr.value}} / {{attr.max}} + {{/if}} +
+ {{/each}} +
+
+ +
+ + + +
+
+
diff --git a/templates/chat-message.hbs b/templates/chat-message.hbs new file mode 100644 index 0000000..0d1986f --- /dev/null +++ b/templates/chat-message.hbs @@ -0,0 +1,49 @@ +
+
+ {{#if actorImg}} + {{actorName}} + {{/if}} +
+ {{actorName}} + {{statLabel}} › {{skillLabel}} +
+
+ +
+
+ {{#each diceResults as |die|}} + {{die}} + {{/each}} +
+ + {{#if moonBonus}} +
+ {{localize "CELESTOPOL.Roll.moonBonus"}} ({{moonPhaseLabel}}) : + +{{moonBonus}} +
+ {{/if}} + + {{#if modifier}} +
+ {{localize "CELESTOPOL.Roll.modifier"}} : + {{#if (gt modifier 0)}}+{{/if}}{{modifier}} +
+ {{/if}} + +
+ {{localize "CELESTOPOL.Roll.total"}} + {{total}} + vs {{difficulty}} +
+
+ +
+ {{#if success}} + {{localize "CELESTOPOL.Roll.success"}} + {{#if criticalSuccess}}{{localize "CELESTOPOL.Roll.criticalSuccess"}}{{/if}} + {{else}} + {{localize "CELESTOPOL.Roll.failure"}} + {{#if criticalFailure}}{{localize "CELESTOPOL.Roll.criticalFailure"}}{{/if}} + {{/if}} +
+
diff --git a/templates/equipment.hbs b/templates/equipment.hbs new file mode 100644 index 0000000..35bee20 --- /dev/null +++ b/templates/equipment.hbs @@ -0,0 +1,56 @@ +
+
+
+ {{item.name}} +
+
+ +
+ +
+ + +
+
+
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ {{editor system.description target="system.description" button=true editable=isEditable}} +
+
+ + +
+
diff --git a/templates/npc-blessures.hbs b/templates/npc-blessures.hbs new file mode 100644 index 0000000..e63c1e3 --- /dev/null +++ b/templates/npc-blessures.hbs @@ -0,0 +1,34 @@ +
+
+
+ {{localize "CELESTOPOL.Track.blessures"}} +
+
+ {{#each (array "b1" "b2" "b3" "b4" "b5" "b6" "b7" "b8") as |key|}} +
+ + +
+ {{/each}} +
+
+ + {{#if isEditMode}} + + {{else}} + {{system.blessures.lvl}} + {{/if}} +
+
+ + {{!-- Description --}} +
+ {{#if isEditMode}} + {{editor system.description target="system.description" button=true editable=isEditable}} + {{else}} +
{{{enrichedDescription}}}
+ {{/if}} +
+
diff --git a/templates/npc-competences.hbs b/templates/npc-competences.hbs new file mode 100644 index 0000000..f37a991 --- /dev/null +++ b/templates/npc-competences.hbs @@ -0,0 +1,36 @@ +
+
+ {{#each stats as |stat statId|}} +
+
+ {{localize stat.label}} +
+ + {{#if ../isEditMode}} + + {{else}} + {{lookup ../system.stats statId 'res'}} + ({{lookup ../system.stats statId 'actuel'}}) + {{/if}} +
+
+
+ {{#each (lookup ../skills statId) as |skill skillId|}} +
+ {{localize skill.label}} + {{#if ../isEditMode}} + + {{else}} + {{lookup (lookup ../system.stats statId) skillId 'value'}} + {{/if}} +
+ {{/each}} +
+
+ {{/each}} +
+
diff --git a/templates/npc-main.hbs b/templates/npc-main.hbs new file mode 100644 index 0000000..01be299 --- /dev/null +++ b/templates/npc-main.hbs @@ -0,0 +1,46 @@ +
+
+
+ {{actor.name}} +
+
+
+ {{#if isEditMode}} + + {{else}} +

{{actor.name}}

+ {{/if}} +
+
+ {{#if isEditMode}} + + {{else}} + {{system.concept}} + {{/if}} +
+
+
+ + {{system.initiative}} +
+
+ + {{#if isEditMode}} + + + {{else}} + {{localize system.anomaly.type}} {{#if system.anomaly.value}}({{system.anomaly.value}}){{/if}} + {{/if}} +
+
+
+
+ + + +
+
+
diff --git a/templates/partials/item-scores.hbs b/templates/partials/item-scores.hbs new file mode 100644 index 0000000..c56a044 --- /dev/null +++ b/templates/partials/item-scores.hbs @@ -0,0 +1,26 @@ +{{!-- Template partagé pour les scores bonus/malus d'un item par compétence --}} +
+
+ {{localize "CELESTOPOL.Item.scores"}} +
+
+ {{#each skills as |statSkills statId|}} +
+
{{localize (concat "CELESTOPOL.Stat." statId)}}
+ {{#each statSkills as |skill skillId|}} +
+ {{localize skill.label}} + {{#if ../isEditable}} + + + {{/if}} +
+ {{/each}} +
+ {{/each}} +
+
diff --git a/templates/roll-dialog.hbs b/templates/roll-dialog.hbs new file mode 100644 index 0000000..1c00d64 --- /dev/null +++ b/templates/roll-dialog.hbs @@ -0,0 +1,40 @@ +
+
+ {{statLabel}} + + {{skillLabel}} +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ {{localize "CELESTOPOL.Roll.nbDice"}} + {{nbDice}} + d6 +
+