feat: implémentation complète du système Célestopol 1922 pour FoundryVTT v13
- DataModels (character, npc, anomaly, aspect, attribute, equipment) - ApplicationV2 sheets (character 5 tabs, npc 3 tabs, 4 item sheets) - DialogV2 pour les jets de dés avec phase de lune - Templates Handlebars complets (fiches PJ/PNJ, items, jet, chat) - Styles LESS → CSS compilé (thème vert foncé / orange CopaseticNF) - i18n fr.json complet (clés CELESTOPOL.*) - Point d'entrée fvtt-celestopol.mjs avec hooks init/ready - Assets : polices CopaseticNF, images UI, icônes items - Mise à jour copilot-instructions.md avec l'architecture réelle Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
87
.github/copilot-instructions.md
vendored
Normal file
87
.github/copilot-instructions.md
vendored
Normal file
@@ -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`).
|
||||||
|
|
||||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -7,8 +7,9 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
|
||||||
chroniquesdeletrange.lock
|
# CSS compilé (généré par gulp depuis styles/)
|
||||||
*.pdf
|
css/*.css
|
||||||
.github/
|
|
||||||
__regles/
|
|
||||||
|
|
||||||
|
# Règles (PDFs privés)
|
||||||
|
__regles/
|
||||||
|
*.pdf
|
||||||
|
|||||||
BIN
assets/fonts/CopaseticNF-Bold.otf
Normal file
BIN
assets/fonts/CopaseticNF-Bold.otf
Normal file
Binary file not shown.
BIN
assets/fonts/CopaseticNF.otf
Normal file
BIN
assets/fonts/CopaseticNF.otf
Normal file
Binary file not shown.
BIN
assets/icons/anomaly.png
Normal file
BIN
assets/icons/anomaly.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
BIN
assets/icons/aspect.png
Normal file
BIN
assets/icons/aspect.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
BIN
assets/icons/attribute.png
Normal file
BIN
assets/icons/attribute.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
BIN
assets/icons/item.png
Normal file
BIN
assets/icons/item.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
BIN
assets/ui/celestopol_background.webp
Normal file
BIN
assets/ui/celestopol_background.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 183 KiB |
BIN
assets/ui/fond_cadrille.jpg
Normal file
BIN
assets/ui/fond_cadrille.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 MiB |
212
fvtt-celestopol.mjs
Normal file
212
fvtt-celestopol.mjs
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
gulpfile.js
Normal file
31
gulpfile.js
Normal file
@@ -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;
|
||||||
163
lang/fr.json
Normal file
163
lang/fr.json
Normal file
@@ -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})"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
module/applications/_module.mjs
Normal file
3
module/applications/_module.mjs
Normal file
@@ -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"
|
||||||
126
module/applications/sheets/base-actor-sheet.mjs
Normal file
126
module/applications/sheets/base-actor-sheet.mjs
Normal file
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
39
module/applications/sheets/base-item-sheet.mjs
Normal file
39
module/applications/sheets/base-item-sheet.mjs
Normal file
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
116
module/applications/sheets/character-sheet.mjs
Normal file
116
module/applications/sheets/character-sheet.mjs
Normal file
@@ -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",
|
||||||
|
}])
|
||||||
|
}
|
||||||
|
}
|
||||||
83
module/applications/sheets/item-sheets.mjs
Normal file
83
module/applications/sheets/item-sheets.mjs
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
57
module/applications/sheets/npc-sheet.mjs
Normal file
57
module/applications/sheets/npc-sheet.mjs
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
130
module/config/system.mjs
Normal file
130
module/config/system.mjs
Normal file
@@ -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,
|
||||||
|
}
|
||||||
4
module/documents/_module.mjs
Normal file
4
module/documents/_module.mjs
Normal file
@@ -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"
|
||||||
12
module/documents/actor.mjs
Normal file
12
module/documents/actor.mjs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export default class CelestopolActor extends Actor {
|
||||||
|
/** @override */
|
||||||
|
prepareDerivedData() {
|
||||||
|
super.prepareDerivedData()
|
||||||
|
this.system.prepareDerivedData?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @override */
|
||||||
|
getRollData() {
|
||||||
|
return this.toObject(false).system
|
||||||
|
}
|
||||||
|
}
|
||||||
7
module/documents/chat-message.mjs
Normal file
7
module/documents/chat-message.mjs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export default class CelestopolChatMessage extends ChatMessage {
|
||||||
|
/** @override */
|
||||||
|
async getHTML() {
|
||||||
|
const html = await super.getHTML()
|
||||||
|
return html
|
||||||
|
}
|
||||||
|
}
|
||||||
11
module/documents/item.mjs
Normal file
11
module/documents/item.mjs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export default class CelestopolItem extends Item {
|
||||||
|
/** @override */
|
||||||
|
prepareDerivedData() {
|
||||||
|
super.prepareDerivedData()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @override */
|
||||||
|
getRollData() {
|
||||||
|
return this.toObject(false).system
|
||||||
|
}
|
||||||
|
}
|
||||||
170
module/documents/roll.mjs
Normal file
170
module/documents/roll.mjs
Normal file
@@ -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<CelestopolRoll|null>}
|
||||||
|
*/
|
||||||
|
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: `<strong>${game.i18n.localize(this.skillLabel ?? "")}</strong>`,
|
||||||
|
...messageData,
|
||||||
|
},
|
||||||
|
{ rollMode }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
3
module/models/_module.mjs
Normal file
3
module/models/_module.mjs
Normal file
@@ -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"
|
||||||
175
module/models/character.mjs
Normal file
175
module/models/character.mjs
Normal file
@@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
98
module/models/items.mjs
Normal file
98
module/models/items.mjs
Normal file
@@ -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 }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
99
module/models/npc.mjs
Normal file
99
module/models/npc.mjs
Normal file
@@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
33
package.json
Normal file
33
package.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
241
styles/character.less
Normal file
241
styles/character.less
Normal file
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
13
styles/fonts.less
Normal file
13
styles/fonts.less
Normal file
@@ -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;
|
||||||
|
}
|
||||||
10
styles/fvtt-celestopol.less
Normal file
10
styles/fvtt-celestopol.less
Normal file
@@ -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";
|
||||||
158
styles/global.less
Normal file
158
styles/global.less
Normal file
@@ -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; } }
|
||||||
|
}
|
||||||
158
styles/items.less
Normal file
158
styles/items.less
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
72
styles/mixins.less
Normal file
72
styles/mixins.less
Normal file
@@ -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; }
|
||||||
|
}
|
||||||
117
styles/npc.less
Normal file
117
styles/npc.less
Normal file
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
177
styles/roll.less
Normal file
177
styles/roll.less
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
74
system.json
Normal file
74
system.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
52
templates/anomaly.hbs
Normal file
52
templates/anomaly.hbs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<div class="item-sheet anomaly">
|
||||||
|
<header class="item-header">
|
||||||
|
<div class="item-portrait" data-action="editImage" data-edit="img">
|
||||||
|
<img src="{{item.img}}" alt="{{item.name}}">
|
||||||
|
</div>
|
||||||
|
<div class="item-header-fields">
|
||||||
|
<input type="text" name="name" value="{{item.name}}" {{#unless isEditable}}disabled{{/unless}}>
|
||||||
|
<div class="item-meta">
|
||||||
|
<select name="system.subtype" {{#unless isEditable}}disabled{{/unless}}>
|
||||||
|
{{selectOptions systemFields.subtype.choices selected=system.subtype localize=true}}
|
||||||
|
</select>
|
||||||
|
<div class="item-value-field">
|
||||||
|
<label>{{localize "CELESTOPOL.Item.value"}}</label>
|
||||||
|
<input type="number" name="system.value" value="{{system.value}}" min="0" max="8"
|
||||||
|
{{#unless isEditable}}disabled{{/unless}}>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<nav class="item-tabs sheet-tabs tabs" data-group="item-tabs">
|
||||||
|
<a class="item active" data-tab="description">{{localize "CELESTOPOL.Tab.description"}}</a>
|
||||||
|
<a class="item" data-tab="technique">{{localize "CELESTOPOL.Tab.technique"}}</a>
|
||||||
|
<a class="item" data-tab="scores">{{localize "CELESTOPOL.Item.scores"}}</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<section class="tab active" data-tab="description">
|
||||||
|
<div class="form-group">
|
||||||
|
{{editor system.description target="system.description" button=true editable=isEditable}}
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>{{localize "CELESTOPOL.Item.reference"}}</label>
|
||||||
|
<input type="text" name="system.reference" value="{{system.reference}}"
|
||||||
|
{{#unless isEditable}}disabled{{/unless}}>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="tab" data-tab="technique">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>{{localize "CELESTOPOL.Item.technique"}}</label>
|
||||||
|
{{editor system.technique target="system.technique" button=true editable=isEditable}}
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>{{localize "CELESTOPOL.Item.narratif"}}</label>
|
||||||
|
{{editor system.narratif target="system.narratif" button=true editable=isEditable}}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="tab" data-tab="scores">
|
||||||
|
{{> "systems/fvtt-celestopol/templates/partials/item-scores.hbs" skills=skills stats=stats}}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
32
templates/aspect.hbs
Normal file
32
templates/aspect.hbs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<div class="item-sheet aspect">
|
||||||
|
<header class="item-header">
|
||||||
|
<div class="item-portrait" data-action="editImage" data-edit="img">
|
||||||
|
<img src="{{item.img}}" alt="{{item.name}}">
|
||||||
|
</div>
|
||||||
|
<div class="item-header-fields">
|
||||||
|
<input type="text" name="name" value="{{item.name}}" {{#unless isEditable}}disabled{{/unless}}>
|
||||||
|
<div class="item-value-field">
|
||||||
|
<label>{{localize "CELESTOPOL.Item.value"}}</label>
|
||||||
|
<input type="number" name="system.value" value="{{system.value}}" min="0" max="8"
|
||||||
|
{{#unless isEditable}}disabled{{/unless}}>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<nav class="item-tabs sheet-tabs tabs" data-group="item-tabs">
|
||||||
|
<a class="item active" data-tab="description">{{localize "CELESTOPOL.Tab.description"}}</a>
|
||||||
|
<a class="item" data-tab="technique">{{localize "CELESTOPOL.Tab.technique"}}</a>
|
||||||
|
<a class="item" data-tab="scores">{{localize "CELESTOPOL.Item.scores"}}</a>
|
||||||
|
</nav>
|
||||||
|
<section class="tab active" data-tab="description">
|
||||||
|
{{editor system.description target="system.description" button=true editable=isEditable}}
|
||||||
|
<label>{{localize "CELESTOPOL.Item.reference"}}</label>
|
||||||
|
<input type="text" name="system.reference" value="{{system.reference}}" {{#unless isEditable}}disabled{{/unless}}>
|
||||||
|
</section>
|
||||||
|
<section class="tab" data-tab="technique">
|
||||||
|
{{editor system.technique target="system.technique" button=true editable=isEditable}}
|
||||||
|
{{editor system.narratif target="system.narratif" button=true editable=isEditable}}
|
||||||
|
</section>
|
||||||
|
<section class="tab" data-tab="scores">
|
||||||
|
{{> "systems/fvtt-celestopol/templates/partials/item-scores.hbs" skills=skills}}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
32
templates/attribute.hbs
Normal file
32
templates/attribute.hbs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<div class="item-sheet attribute">
|
||||||
|
<header class="item-header">
|
||||||
|
<div class="item-portrait" data-action="editImage" data-edit="img">
|
||||||
|
<img src="{{item.img}}" alt="{{item.name}}">
|
||||||
|
</div>
|
||||||
|
<div class="item-header-fields">
|
||||||
|
<input type="text" name="name" value="{{item.name}}" {{#unless isEditable}}disabled{{/unless}}>
|
||||||
|
<div class="item-value-field">
|
||||||
|
<label>{{localize "CELESTOPOL.Item.value"}}</label>
|
||||||
|
<input type="number" name="system.value" value="{{system.value}}" min="0" max="8"
|
||||||
|
{{#unless isEditable}}disabled{{/unless}}>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<nav class="item-tabs sheet-tabs tabs" data-group="item-tabs">
|
||||||
|
<a class="item active" data-tab="description">{{localize "CELESTOPOL.Tab.description"}}</a>
|
||||||
|
<a class="item" data-tab="technique">{{localize "CELESTOPOL.Tab.technique"}}</a>
|
||||||
|
<a class="item" data-tab="scores">{{localize "CELESTOPOL.Item.scores"}}</a>
|
||||||
|
</nav>
|
||||||
|
<section class="tab active" data-tab="description">
|
||||||
|
{{editor system.description target="system.description" button=true editable=isEditable}}
|
||||||
|
<label>{{localize "CELESTOPOL.Item.reference"}}</label>
|
||||||
|
<input type="text" name="system.reference" value="{{system.reference}}" {{#unless isEditable}}disabled{{/unless}}>
|
||||||
|
</section>
|
||||||
|
<section class="tab" data-tab="technique">
|
||||||
|
{{editor system.technique target="system.technique" button=true editable=isEditable}}
|
||||||
|
{{editor system.narratif target="system.narratif" button=true editable=isEditable}}
|
||||||
|
</section>
|
||||||
|
<section class="tab" data-tab="scores">
|
||||||
|
{{> "systems/fvtt-celestopol/templates/partials/item-scores.hbs" skills=skills}}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
42
templates/character-biography.hbs
Normal file
42
templates/character-biography.hbs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<div class="tab biography" data-group="sheet" data-tab="biography">
|
||||||
|
{{!-- Équipements --}}
|
||||||
|
<div class="equipments-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<span>{{localize "CELESTOPOL.Item.equipments"}}</span>
|
||||||
|
{{#if isEditMode}}
|
||||||
|
<a data-action="createEquipment" title="{{localize 'CELESTOPOL.Item.newEquipment'}}"><i class="fas fa-plus"></i></a>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{#each equipments as |item|}}
|
||||||
|
<div class="item-row equipment" data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}" data-drag="true">
|
||||||
|
<img src="{{item.img}}" class="item-icon">
|
||||||
|
<span class="item-name">{{item.name}}</span>
|
||||||
|
<span class="item-qty">×{{item.system.quantity}}</span>
|
||||||
|
<div class="item-controls">
|
||||||
|
<a data-action="edit" data-item-uuid="{{item.uuid}}"><i class="fas fa-edit"></i></a>
|
||||||
|
{{#if ../isEditMode}}<a data-action="delete" data-item-uuid="{{item.uuid}}"><i class="fas fa-trash"></i></a>{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{!-- Description / Biographie --}}
|
||||||
|
<div class="biography-section">
|
||||||
|
<div class="section-header">{{localize "CELESTOPOL.Actor.description"}}</div>
|
||||||
|
{{#if isEditMode}}
|
||||||
|
{{editor system.description target="system.description" button=true editable=isEditable}}
|
||||||
|
{{else}}
|
||||||
|
<div class="enriched-html">{{{enrichedDescription}}}</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{!-- Notes --}}
|
||||||
|
<div class="notes-section">
|
||||||
|
<div class="section-header">{{localize "CELESTOPOL.Actor.notes"}}</div>
|
||||||
|
{{#if isEditMode}}
|
||||||
|
{{editor system.notes target="system.notes" button=true editable=isEditable}}
|
||||||
|
{{else}}
|
||||||
|
<div class="enriched-html">{{{enrichedNotes}}}</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
77
templates/character-blessures.hbs
Normal file
77
templates/character-blessures.hbs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<div class="tab blessures" data-group="sheet" data-tab="blessures">
|
||||||
|
{{!-- Blessures --}}
|
||||||
|
<section class="track-section">
|
||||||
|
<div class="track-header">
|
||||||
|
<span class="track-title">{{localize "CELESTOPOL.Track.blessures"}}</span>
|
||||||
|
<span class="wound-malus">{{localize "CELESTOPOL.Track.currentMalus"}} :
|
||||||
|
<strong>{{system.blessures.lvl}}</strong>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="track-boxes">
|
||||||
|
{{#each (array "b1" "b2" "b3" "b4" "b5" "b6" "b7" "b8") as |key idx|}}
|
||||||
|
<div class="track-box {{#if (lookup ../system.blessures key 'checked')}}checked{{/if}}">
|
||||||
|
<input type="checkbox" name="system.blessures.{{key}}.checked"
|
||||||
|
{{#if (lookup ../system.blessures key 'checked')}}checked{{/if}}
|
||||||
|
{{#unless ../isEditable}}disabled{{/unless}}>
|
||||||
|
<label class="box-label">{{lookup ../system.blessures key 'malus'}}</label>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
<div class="track-level">
|
||||||
|
<label>{{localize "CELESTOPOL.Track.level"}}</label>
|
||||||
|
{{#if isEditMode}}
|
||||||
|
<input type="number" name="system.blessures.lvl" value="{{system.blessures.lvl}}" min="0" max="8">
|
||||||
|
{{else}}
|
||||||
|
<span>{{system.blessures.lvl}}</span>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{{!-- Destin --}}
|
||||||
|
<section class="track-section">
|
||||||
|
<div class="track-header">
|
||||||
|
<span class="track-title">{{localize "CELESTOPOL.Track.destin"}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="track-boxes destin-boxes">
|
||||||
|
{{#each (array "d1" "d2" "d3" "d4" "d5" "d6" "d7" "d8") as |key|}}
|
||||||
|
<div class="track-box destiny {{#if (lookup ../system.destin key 'checked')}}checked{{/if}}">
|
||||||
|
<input type="checkbox" name="system.destin.{{key}}.checked"
|
||||||
|
{{#if (lookup ../system.destin key 'checked')}}checked{{/if}}
|
||||||
|
{{#unless ../isEditable}}disabled{{/unless}}>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
<div class="track-level">
|
||||||
|
<label>{{localize "CELESTOPOL.Track.level"}}</label>
|
||||||
|
{{#if isEditMode}}
|
||||||
|
<input type="number" name="system.destin.lvl" value="{{system.destin.lvl}}" min="0" max="8">
|
||||||
|
{{else}}
|
||||||
|
<span>{{system.destin.lvl}}</span>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{{!-- Spleen --}}
|
||||||
|
<section class="track-section">
|
||||||
|
<div class="track-header">
|
||||||
|
<span class="track-title">{{localize "CELESTOPOL.Track.spleen"}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="track-boxes spleen-boxes">
|
||||||
|
{{#each (array "s1" "s2" "s3" "s4" "s5" "s6" "s7" "s8") as |key|}}
|
||||||
|
<div class="track-box spleen {{#if (lookup ../system.spleen key 'checked')}}checked{{/if}}">
|
||||||
|
<input type="checkbox" name="system.spleen.{{key}}.checked"
|
||||||
|
{{#if (lookup ../system.spleen key 'checked')}}checked{{/if}}
|
||||||
|
{{#unless ../isEditable}}disabled{{/unless}}>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
<div class="track-level">
|
||||||
|
<label>{{localize "CELESTOPOL.Track.level"}}</label>
|
||||||
|
{{#if isEditMode}}
|
||||||
|
<input type="number" name="system.spleen.lvl" value="{{system.spleen.lvl}}" min="0" max="8">
|
||||||
|
{{else}}
|
||||||
|
<span>{{system.spleen.lvl}}</span>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
102
templates/character-competences.hbs
Normal file
102
templates/character-competences.hbs
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
<div class="tab competences" data-group="sheet" data-tab="competences">
|
||||||
|
{{!-- Grille des 4 stats × 4 compétences --}}
|
||||||
|
<div class="stats-grid">
|
||||||
|
{{#each stats as |stat statId|}}
|
||||||
|
<div class="stat-block">
|
||||||
|
<div class="stat-header">
|
||||||
|
<span class="stat-name">{{localize stat.label}}</span>
|
||||||
|
<div class="stat-res">
|
||||||
|
<label>{{localize "CELESTOPOL.Stat.res"}}</label>
|
||||||
|
{{#if ../isEditMode}}
|
||||||
|
<input type="number" name="system.stats.{{statId}}.res" value="{{lookup ../system.stats statId 'res'}}" min="0" max="8" class="stat-res-input">
|
||||||
|
{{else}}
|
||||||
|
<span class="stat-res-value">{{lookup ../system.stats statId 'res'}}</span>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="skills-list">
|
||||||
|
{{#each (lookup ../skills statId) as |skill skillId|}}
|
||||||
|
<div class="skill-row {{#unless ../isEditMode}}rollable{{/unless}}"
|
||||||
|
data-stat-id="{{statId}}" data-skill-id="{{skillId}}"
|
||||||
|
title="{{#unless ../isEditMode}}{{localize 'CELESTOPOL.Roll.clickToRoll'}}{{/unless}}">
|
||||||
|
<span class="skill-name">{{localize skill.label}}</span>
|
||||||
|
{{#if ../isEditMode}}
|
||||||
|
<input type="number" name="system.stats.{{statId}}.{{skillId}}.value"
|
||||||
|
value="{{lookup (lookup ../system.stats statId) skillId 'value'}}"
|
||||||
|
min="0" max="8" class="skill-value-input">
|
||||||
|
{{else}}
|
||||||
|
<span class="skill-value">{{lookup (lookup ../system.stats statId) skillId 'value'}}</span>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{!-- Items : Anomalies, Aspects, Attributs --}}
|
||||||
|
<div class="items-section">
|
||||||
|
{{!-- Anomalies --}}
|
||||||
|
<div class="items-group">
|
||||||
|
<div class="items-header">
|
||||||
|
<span>{{localize "CELESTOPOL.Item.anomalies"}}</span>
|
||||||
|
{{#if isEditMode}}
|
||||||
|
<a data-action="createAnomaly" title="{{localize 'CELESTOPOL.Item.newAnomaly'}}"><i class="fas fa-plus"></i></a>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{#each anomalies as |item|}}
|
||||||
|
<div class="item-row" data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}" data-drag="true">
|
||||||
|
<img src="{{item.img}}" class="item-icon" alt="{{item.name}}">
|
||||||
|
<span class="item-name">{{item.name}}</span>
|
||||||
|
<span class="item-value">{{item.system.value}}</span>
|
||||||
|
<div class="item-controls">
|
||||||
|
<a data-action="edit" data-item-uuid="{{item.uuid}}"><i class="fas fa-edit"></i></a>
|
||||||
|
{{#if ../isEditMode}}<a data-action="delete" data-item-uuid="{{item.uuid}}"><i class="fas fa-trash"></i></a>{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{!-- Aspects --}}
|
||||||
|
<div class="items-group">
|
||||||
|
<div class="items-header">
|
||||||
|
<span>{{localize "CELESTOPOL.Item.aspects"}}</span>
|
||||||
|
{{#if isEditMode}}
|
||||||
|
<a data-action="createAspect" title="{{localize 'CELESTOPOL.Item.newAspect'}}"><i class="fas fa-plus"></i></a>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{#each aspects as |item|}}
|
||||||
|
<div class="item-row" data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}" data-drag="true">
|
||||||
|
<img src="{{item.img}}" class="item-icon" alt="{{item.name}}">
|
||||||
|
<span class="item-name">{{item.name}}</span>
|
||||||
|
<span class="item-value">{{item.system.value}}</span>
|
||||||
|
<div class="item-controls">
|
||||||
|
<a data-action="edit" data-item-uuid="{{item.uuid}}"><i class="fas fa-edit"></i></a>
|
||||||
|
{{#if ../isEditMode}}<a data-action="delete" data-item-uuid="{{item.uuid}}"><i class="fas fa-trash"></i></a>{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{!-- Attributs --}}
|
||||||
|
<div class="items-group">
|
||||||
|
<div class="items-header">
|
||||||
|
<span>{{localize "CELESTOPOL.Item.attributes"}}</span>
|
||||||
|
{{#if isEditMode}}
|
||||||
|
<a data-action="createAttribute" title="{{localize 'CELESTOPOL.Item.newAttribute'}}"><i class="fas fa-plus"></i></a>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{#each attributes as |item|}}
|
||||||
|
<div class="item-row" data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}" data-drag="true">
|
||||||
|
<img src="{{item.img}}" class="item-icon" alt="{{item.name}}">
|
||||||
|
<span class="item-name">{{item.name}}</span>
|
||||||
|
<span class="item-value">{{item.system.value}}</span>
|
||||||
|
<div class="item-controls">
|
||||||
|
<a data-action="edit" data-item-uuid="{{item.uuid}}"><i class="fas fa-edit"></i></a>
|
||||||
|
{{#if ../isEditMode}}<a data-action="delete" data-item-uuid="{{item.uuid}}"><i class="fas fa-trash"></i></a>{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
63
templates/character-factions.hbs
Normal file
63
templates/character-factions.hbs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<div class="tab factions" data-group="sheet" data-tab="factions">
|
||||||
|
<table class="factions-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{localize "CELESTOPOL.Faction.label"}}</th>
|
||||||
|
<th>{{localize "CELESTOPOL.Faction.score"}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{#each factions as |faction factionId|}}
|
||||||
|
<tr class="faction-row">
|
||||||
|
<td class="faction-name">{{localize faction.label}}</td>
|
||||||
|
<td class="faction-value">
|
||||||
|
{{#if ../isEditMode}}
|
||||||
|
<input type="number" name="system.factions.{{factionId}}.value"
|
||||||
|
value="{{lookup ../system.factions factionId 'value'}}" min="0">
|
||||||
|
{{else}}
|
||||||
|
<span>{{lookup ../system.factions factionId 'value'}}</span>
|
||||||
|
{{/if}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
|
{{!-- Factions personnalisées --}}
|
||||||
|
<tr class="faction-row custom">
|
||||||
|
<td>
|
||||||
|
{{#if isEditMode}}
|
||||||
|
<input type="text" name="system.factions.perso1.label"
|
||||||
|
value="{{system.factions.perso1.label}}"
|
||||||
|
placeholder="{{localize 'CELESTOPOL.Faction.custom'}}">
|
||||||
|
{{else}}
|
||||||
|
<span>{{#if system.factions.perso1.label}}{{system.factions.perso1.label}}{{else}}—{{/if}}</span>
|
||||||
|
{{/if}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{#if isEditMode}}
|
||||||
|
<input type="number" name="system.factions.perso1.value" value="{{system.factions.perso1.value}}" min="0">
|
||||||
|
{{else}}
|
||||||
|
<span>{{system.factions.perso1.value}}</span>
|
||||||
|
{{/if}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="faction-row custom">
|
||||||
|
<td>
|
||||||
|
{{#if isEditMode}}
|
||||||
|
<input type="text" name="system.factions.perso2.label"
|
||||||
|
value="{{system.factions.perso2.label}}"
|
||||||
|
placeholder="{{localize 'CELESTOPOL.Faction.custom'}}">
|
||||||
|
{{else}}
|
||||||
|
<span>{{#if system.factions.perso2.label}}{{system.factions.perso2.label}}{{else}}—{{/if}}</span>
|
||||||
|
{{/if}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{#if isEditMode}}
|
||||||
|
<input type="number" name="system.factions.perso2.value" value="{{system.factions.perso2.value}}" min="0">
|
||||||
|
{{else}}
|
||||||
|
<span>{{system.factions.perso2.value}}</span>
|
||||||
|
{{/if}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
62
templates/character-main.hbs
Normal file
62
templates/character-main.hbs
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<div class="character-main sheet-part">
|
||||||
|
<header class="sheet-header">
|
||||||
|
<div class="portrait" data-action="editImage" data-edit="img">
|
||||||
|
<img src="{{actor.img}}" alt="{{actor.name}}" class="actor-portrait {{#unless isEditMode}}click-to-edit{{/unless}}">
|
||||||
|
</div>
|
||||||
|
<div class="header-fields">
|
||||||
|
<div class="charname">
|
||||||
|
{{#if isEditMode}}
|
||||||
|
<input type="text" name="name" value="{{actor.name}}" placeholder="{{localize 'CELESTOPOL.Actor.name'}}">
|
||||||
|
{{else}}
|
||||||
|
<h1 class="actor-name">{{actor.name}}</h1>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
<div class="concept-row">
|
||||||
|
{{#if isEditMode}}
|
||||||
|
<input type="text" name="system.concept" value="{{system.concept}}" placeholder="{{localize 'CELESTOPOL.Actor.concept'}}">
|
||||||
|
{{else}}
|
||||||
|
<span class="concept-display">{{system.concept}}</span>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
<div class="header-stats-row">
|
||||||
|
<div class="header-stat initiative">
|
||||||
|
<label>{{localize "CELESTOPOL.Actor.initiative"}}</label>
|
||||||
|
<span class="stat-value">{{system.initiative}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="header-stat anomaly">
|
||||||
|
<label>{{localize "CELESTOPOL.Actor.anomaly"}}</label>
|
||||||
|
{{#if isEditMode}}
|
||||||
|
<select name="system.anomaly.type">
|
||||||
|
{{selectOptions systemFields.anomaly.fields.type.choices selected=system.anomaly.type localize=true}}
|
||||||
|
</select>
|
||||||
|
<input type="number" name="system.anomaly.value" value="{{system.anomaly.value}}" min="0" max="8" class="anomaly-value">
|
||||||
|
{{else}}
|
||||||
|
<span>{{localize system.anomaly.type}} {{#if system.anomaly.value}}({{system.anomaly.value}}){{/if}}</span>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{!-- Attributs personnage (Entregent, Fortune, Rêve, Vision) --}}
|
||||||
|
<div class="perso-attributs">
|
||||||
|
{{#each system.attributs as |attr key|}}
|
||||||
|
<div class="perso-attr">
|
||||||
|
<label>{{localize (concat "CELESTOPOL.Attribut." key)}}</label>
|
||||||
|
{{#if ../isEditMode}}
|
||||||
|
<input type="number" name="system.attributs.{{key}}.value" value="{{attr.value}}" min="0" class="attr-val">
|
||||||
|
<span>/</span>
|
||||||
|
<input type="number" name="system.attributs.{{key}}.max" value="{{attr.max}}" min="0" class="attr-max">
|
||||||
|
{{else}}
|
||||||
|
<span class="attr-display">{{attr.value}} / {{attr.max}}</span>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="header-buttons">
|
||||||
|
<a class="toggle-sheet" data-action="toggleSheet" title="{{#if isEditMode}}{{localize 'CELESTOPOL.Sheet.playMode'}}{{else}}{{localize 'CELESTOPOL.Sheet.editMode'}}{{/if}}">
|
||||||
|
<i class="fas {{#if isEditMode}}fa-eye{{else}}fa-edit{{/if}}"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</div>
|
||||||
49
templates/chat-message.hbs
Normal file
49
templates/chat-message.hbs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<div class="celestopol chat-roll {{resultClass}}">
|
||||||
|
<div class="roll-header">
|
||||||
|
{{#if actorImg}}
|
||||||
|
<img src="{{actorImg}}" class="actor-img" alt="{{actorName}}">
|
||||||
|
{{/if}}
|
||||||
|
<div class="roll-info">
|
||||||
|
<span class="actor-name">{{actorName}}</span>
|
||||||
|
<span class="skill-info">{{statLabel}} › {{skillLabel}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="roll-details">
|
||||||
|
<div class="dice-results">
|
||||||
|
{{#each diceResults as |die|}}
|
||||||
|
<span class="die d6">{{die}}</span>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if moonBonus}}
|
||||||
|
<div class="bonus-line">
|
||||||
|
<span class="bonus-label">{{localize "CELESTOPOL.Roll.moonBonus"}} ({{moonPhaseLabel}}) :</span>
|
||||||
|
<span class="bonus-value">+{{moonBonus}}</span>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if modifier}}
|
||||||
|
<div class="bonus-line">
|
||||||
|
<span class="bonus-label">{{localize "CELESTOPOL.Roll.modifier"}} :</span>
|
||||||
|
<span class="bonus-value">{{#if (gt modifier 0)}}+{{/if}}{{modifier}}</span>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<div class="roll-total-line">
|
||||||
|
<span class="total-label">{{localize "CELESTOPOL.Roll.total"}}</span>
|
||||||
|
<span class="total-value">{{total}}</span>
|
||||||
|
<span class="vs-difficulty">vs {{difficulty}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="roll-result-banner {{resultClass}}">
|
||||||
|
{{#if success}}
|
||||||
|
<span class="result-label success">{{localize "CELESTOPOL.Roll.success"}}</span>
|
||||||
|
{{#if criticalSuccess}}<span class="critical">{{localize "CELESTOPOL.Roll.criticalSuccess"}}</span>{{/if}}
|
||||||
|
{{else}}
|
||||||
|
<span class="result-label failure">{{localize "CELESTOPOL.Roll.failure"}}</span>
|
||||||
|
{{#if criticalFailure}}<span class="critical">{{localize "CELESTOPOL.Roll.criticalFailure"}}</span>{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
56
templates/equipment.hbs
Normal file
56
templates/equipment.hbs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<div class="item-sheet equipment">
|
||||||
|
<header class="item-header">
|
||||||
|
<div class="item-portrait" data-action="editImage" data-edit="img">
|
||||||
|
<img src="{{item.img}}" alt="{{item.name}}">
|
||||||
|
</div>
|
||||||
|
<div class="item-header-fields">
|
||||||
|
<input type="text" name="name" value="{{item.name}}" {{#unless isEditable}}disabled{{/unless}}>
|
||||||
|
<div class="item-meta">
|
||||||
|
<select name="system.subtype" {{#unless isEditable}}disabled{{/unless}}>
|
||||||
|
{{selectOptions systemFields.subtype.choices selected=system.subtype localize=true}}
|
||||||
|
</select>
|
||||||
|
<div class="item-qty">
|
||||||
|
<label>{{localize "CELESTOPOL.Item.quantity"}}</label>
|
||||||
|
<input type="number" name="system.quantity" value="{{system.quantity}}" min="0"
|
||||||
|
{{#unless isEditable}}disabled{{/unless}}>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="form-grid equipment-stats">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>{{localize "CELESTOPOL.Item.damage"}}</label>
|
||||||
|
<input type="text" name="system.damage" value="{{system.damage}}" {{#unless isEditable}}disabled{{/unless}}>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>{{localize "CELESTOPOL.Item.range"}}</label>
|
||||||
|
<input type="text" name="system.range" value="{{system.range}}" {{#unless isEditable}}disabled{{/unless}}>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>{{localize "CELESTOPOL.Item.protection"}}</label>
|
||||||
|
<input type="text" name="system.protection" value="{{system.protection}}" {{#unless isEditable}}disabled{{/unless}}>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>{{localize "CELESTOPOL.Item.speed"}}</label>
|
||||||
|
<input type="text" name="system.speed" value="{{system.speed}}" {{#unless isEditable}}disabled{{/unless}}>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>{{localize "CELESTOPOL.Item.crew"}}</label>
|
||||||
|
<input type="text" name="system.crew" value="{{system.crew}}" {{#unless isEditable}}disabled{{/unless}}>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>{{localize "CELESTOPOL.Item.weight"}}</label>
|
||||||
|
<input type="number" name="system.weight" value="{{system.weight}}" min="0"
|
||||||
|
{{#unless isEditable}}disabled{{/unless}}>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group description-group">
|
||||||
|
{{editor system.description target="system.description" button=true editable=isEditable}}
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>{{localize "CELESTOPOL.Item.reference"}}</label>
|
||||||
|
<input type="text" name="system.reference" value="{{system.reference}}" {{#unless isEditable}}disabled{{/unless}}>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
34
templates/npc-blessures.hbs
Normal file
34
templates/npc-blessures.hbs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<div class="tab blessures" data-group="sheet" data-tab="blessures">
|
||||||
|
<section class="track-section">
|
||||||
|
<div class="track-header">
|
||||||
|
<span class="track-title">{{localize "CELESTOPOL.Track.blessures"}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="track-boxes">
|
||||||
|
{{#each (array "b1" "b2" "b3" "b4" "b5" "b6" "b7" "b8") as |key|}}
|
||||||
|
<div class="track-box {{#if (lookup ../system.blessures key 'checked')}}checked{{/if}}">
|
||||||
|
<input type="checkbox" name="system.blessures.{{key}}.checked"
|
||||||
|
{{#if (lookup ../system.blessures key 'checked')}}checked{{/if}}
|
||||||
|
{{#unless ../isEditable}}disabled{{/unless}}>
|
||||||
|
<label class="box-label">{{lookup ../system.blessures key 'malus'}}</label>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
<div class="track-level">
|
||||||
|
<label>{{localize "CELESTOPOL.Track.level"}}</label>
|
||||||
|
{{#if isEditMode}}
|
||||||
|
<input type="number" name="system.blessures.lvl" value="{{system.blessures.lvl}}" min="0" max="8">
|
||||||
|
{{else}}
|
||||||
|
<span>{{system.blessures.lvl}}</span>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{{!-- Description --}}
|
||||||
|
<div class="description-section">
|
||||||
|
{{#if isEditMode}}
|
||||||
|
{{editor system.description target="system.description" button=true editable=isEditable}}
|
||||||
|
{{else}}
|
||||||
|
<div class="enriched-html">{{{enrichedDescription}}}</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
36
templates/npc-competences.hbs
Normal file
36
templates/npc-competences.hbs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<div class="tab competences" data-group="sheet" data-tab="competences">
|
||||||
|
<div class="stats-grid">
|
||||||
|
{{#each stats as |stat statId|}}
|
||||||
|
<div class="stat-block">
|
||||||
|
<div class="stat-header">
|
||||||
|
<span class="stat-name">{{localize stat.label}}</span>
|
||||||
|
<div class="stat-res">
|
||||||
|
<label>{{localize "CELESTOPOL.Stat.res"}}</label>
|
||||||
|
{{#if ../isEditMode}}
|
||||||
|
<input type="number" name="system.stats.{{statId}}.res"
|
||||||
|
value="{{lookup ../system.stats statId 'res'}}" min="0" max="8">
|
||||||
|
{{else}}
|
||||||
|
<span class="stat-res-value">{{lookup ../system.stats statId 'res'}}</span>
|
||||||
|
<span class="stat-actuel">({{lookup ../system.stats statId 'actuel'}})</span>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="skills-list">
|
||||||
|
{{#each (lookup ../skills statId) as |skill skillId|}}
|
||||||
|
<div class="skill-row {{#unless ../isEditMode}}rollable{{/unless}}"
|
||||||
|
data-stat-id="{{statId}}" data-skill-id="{{skillId}}">
|
||||||
|
<span class="skill-name">{{localize skill.label}}</span>
|
||||||
|
{{#if ../isEditMode}}
|
||||||
|
<input type="number" name="system.stats.{{statId}}.{{skillId}}.value"
|
||||||
|
value="{{lookup (lookup ../system.stats statId) skillId 'value'}}"
|
||||||
|
min="0" max="8" class="skill-value-input">
|
||||||
|
{{else}}
|
||||||
|
<span class="skill-value">{{lookup (lookup ../system.stats statId) skillId 'value'}}</span>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
46
templates/npc-main.hbs
Normal file
46
templates/npc-main.hbs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<div class="npc-main sheet-part">
|
||||||
|
<header class="sheet-header">
|
||||||
|
<div class="portrait" data-action="editImage" data-edit="img">
|
||||||
|
<img src="{{actor.img}}" alt="{{actor.name}}" class="actor-portrait">
|
||||||
|
</div>
|
||||||
|
<div class="header-fields">
|
||||||
|
<div class="charname">
|
||||||
|
{{#if isEditMode}}
|
||||||
|
<input type="text" name="name" value="{{actor.name}}">
|
||||||
|
{{else}}
|
||||||
|
<h1 class="actor-name">{{actor.name}}</h1>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
<div class="concept-row">
|
||||||
|
{{#if isEditMode}}
|
||||||
|
<input type="text" name="system.concept" value="{{system.concept}}"
|
||||||
|
placeholder="{{localize 'CELESTOPOL.Actor.concept'}}">
|
||||||
|
{{else}}
|
||||||
|
<span class="concept-display">{{system.concept}}</span>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
<div class="header-stats-row">
|
||||||
|
<div class="header-stat">
|
||||||
|
<label>{{localize "CELESTOPOL.Actor.initiative"}}</label>
|
||||||
|
<span>{{system.initiative}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="header-stat">
|
||||||
|
<label>{{localize "CELESTOPOL.Actor.anomaly"}}</label>
|
||||||
|
{{#if isEditMode}}
|
||||||
|
<select name="system.anomaly.type">
|
||||||
|
{{selectOptions systemFields.anomaly.fields.type.choices selected=system.anomaly.type localize=true}}
|
||||||
|
</select>
|
||||||
|
<input type="number" name="system.anomaly.value" value="{{system.anomaly.value}}" min="0" max="8" class="small-input">
|
||||||
|
{{else}}
|
||||||
|
<span>{{localize system.anomaly.type}} {{#if system.anomaly.value}}({{system.anomaly.value}}){{/if}}</span>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="header-buttons">
|
||||||
|
<a data-action="toggleSheet">
|
||||||
|
<i class="fas {{#if isEditMode}}fa-eye{{else}}fa-edit{{/if}}"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</div>
|
||||||
26
templates/partials/item-scores.hbs
Normal file
26
templates/partials/item-scores.hbs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{{!-- Template partagé pour les scores bonus/malus d'un item par compétence --}}
|
||||||
|
<div class="scores-section">
|
||||||
|
<div class="scores-header">
|
||||||
|
<span>{{localize "CELESTOPOL.Item.scores"}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="scores-grid">
|
||||||
|
{{#each skills as |statSkills statId|}}
|
||||||
|
<div class="scores-stat-col">
|
||||||
|
<div class="scores-stat-name">{{localize (concat "CELESTOPOL.Stat." statId)}}</div>
|
||||||
|
{{#each statSkills as |skill skillId|}}
|
||||||
|
<div class="score-row">
|
||||||
|
<span class="score-skill-name">{{localize skill.label}}</span>
|
||||||
|
{{#if ../isEditable}}
|
||||||
|
<input type="number" name="system.scores.{{statId}}.{{skillId}}.bonus"
|
||||||
|
value="{{lookup (lookup (lookup ../system.scores statId) skillId) 'bonus'}}"
|
||||||
|
min="0" class="score-bonus" title="+bonus">
|
||||||
|
<input type="number" name="system.scores.{{statId}}.{{skillId}}.malus"
|
||||||
|
value="{{lookup (lookup (lookup ../system.scores statId) skillId) 'malus'}}"
|
||||||
|
min="0" class="score-malus" title="-malus">
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
40
templates/roll-dialog.hbs
Normal file
40
templates/roll-dialog.hbs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<form class="roll-dialog celestopol">
|
||||||
|
<div class="roll-title">
|
||||||
|
<span class="stat-label">{{statLabel}}</span>
|
||||||
|
<span class="separator">›</span>
|
||||||
|
<span class="skill-label">{{skillLabel}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="moonPhase">{{localize "CELESTOPOL.Roll.moonPhase"}}</label>
|
||||||
|
<select id="moonPhase" name="moonPhase">
|
||||||
|
{{#each moonPhases as |phase key|}}
|
||||||
|
<option value="{{key}}" {{#if (eq key ../currentMoonPhase)}}selected{{/if}}>
|
||||||
|
{{localize phase.label}} {{#if phase.bonus}}(+{{phase.bonus}}){{else}}(+0){{/if}}
|
||||||
|
</option>
|
||||||
|
{{/each}}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="difficulty">{{localize "CELESTOPOL.Roll.difficulty"}}</label>
|
||||||
|
<select id="difficulty" name="difficulty">
|
||||||
|
{{#each difficulties as |diff key|}}
|
||||||
|
<option value="{{diff.threshold}}" {{#if (eq diff.threshold ../currentDifficulty)}}selected{{/if}}>
|
||||||
|
{{localize diff.label}} ({{diff.threshold}})
|
||||||
|
</option>
|
||||||
|
{{/each}}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="modifier">{{localize "CELESTOPOL.Roll.modifier"}}</label>
|
||||||
|
<input type="number" id="modifier" name="modifier" value="0">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dice-preview">
|
||||||
|
<span>{{localize "CELESTOPOL.Roll.nbDice"}}</span>
|
||||||
|
<span class="nb-dice">{{nbDice}}</span>
|
||||||
|
<span>d6</span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
Reference in New Issue
Block a user