feat: gestion de l'expérience (XP)
- Schéma xp dans CelestopolCharacter : actuel (éditable), log[] ({montant, raison, date}), depense (calculé dans prepareDerivedData)
- Bouton 'Dépenser XP' → DialogV2 (montant + raison) : décrémente actuel, logge l'entrée
- Suppression d'entrée de log avec remboursement des points (mode édition)
- Section XP en haut de l'onglet Biographie : compteurs, tableau du log, référentiel des coûts
- i18n : section CELESTOPOL.XP.* complète
- CSS : .xp-section avec compteurs, tableau de log et accordéon de référence
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 70 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 70 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 18 KiB |
@@ -68,7 +68,7 @@ Hooks.once("init", () => {
|
||||
|
||||
// ── Sheets: unregister core, register system sheets ─────────────────────
|
||||
foundry.applications.sheets.ActorSheetV2.unregisterSheet?.("core", "Actor", { types: ["character", "npc"] })
|
||||
foundry.documents.collections.Actors.unregisterSheet("core", foundry.appv1.sheets.ActorSheet)
|
||||
foundry.appv1?.sheets?.ActorSheet && foundry.documents.collections.Actors.unregisterSheet("core", foundry.appv1.sheets.ActorSheet)
|
||||
foundry.documents.collections.Actors.registerSheet(SYSTEM_ID, CelestopolCharacterSheet, {
|
||||
types: ["character"],
|
||||
makeDefault: true,
|
||||
@@ -80,7 +80,7 @@ Hooks.once("init", () => {
|
||||
label: "CELESTOPOL.Sheet.npc",
|
||||
})
|
||||
|
||||
foundry.documents.collections.Items.unregisterSheet("core", foundry.appv1.sheets.ItemSheet)
|
||||
foundry.appv1?.sheets?.ItemSheet && foundry.documents.collections.Items.unregisterSheet("core", foundry.appv1.sheets.ItemSheet)
|
||||
foundry.documents.collections.Items.registerSheet(SYSTEM_ID, CelestopolAnomalySheet, {
|
||||
types: ["anomaly"],
|
||||
makeDefault: true,
|
||||
@@ -130,6 +130,7 @@ Hooks.once("ready", () => {
|
||||
// Migration : supprime les items de types obsolètes (ex: "attribute")
|
||||
if (game.user.isGM) {
|
||||
_migrateObsoleteItems()
|
||||
_migrateIntegerTracks()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -156,6 +157,68 @@ async function _migrateObsoleteItems() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migration : convertit les anciennes données booléennes (level1..level8, b1..b8, etc.)
|
||||
* vers le nouveau stockage entier direct.
|
||||
* Ne s'applique qu'aux acteurs ayant encore l'ancien format dans leur source.
|
||||
*/
|
||||
async function _migrateIntegerTracks() {
|
||||
const validActors = game.actors.contents.filter(a => ["character", "npc"].includes(a.type))
|
||||
|
||||
for (const actor of validActors) {
|
||||
const src = actor._source?.system
|
||||
if (!src) continue
|
||||
|
||||
const updateData = {}
|
||||
|
||||
// Blessures : si b1 existe dans la source, recalculer lvl depuis les booléens
|
||||
const blessures = src.blessures ?? {}
|
||||
if ("b1" in blessures) {
|
||||
const lvl = [1,2,3,4,5,6,7,8].filter(i => blessures[`b${i}`]?.checked === true).length
|
||||
updateData["system.blessures.lvl"] = lvl
|
||||
}
|
||||
|
||||
if (actor.type === "character") {
|
||||
// Destin
|
||||
const destin = src.destin ?? {}
|
||||
if ("d1" in destin) {
|
||||
const lvl = [1,2,3,4,5,6,7,8].filter(i => destin[`d${i}`]?.checked === true).length
|
||||
updateData["system.destin.lvl"] = lvl
|
||||
}
|
||||
|
||||
// Spleen
|
||||
const spleen = src.spleen ?? {}
|
||||
if ("s1" in spleen) {
|
||||
const lvl = [1,2,3,4,5,6,7,8].filter(i => spleen[`s${i}`]?.checked === true).length
|
||||
updateData["system.spleen.lvl"] = lvl
|
||||
}
|
||||
|
||||
// Domaines : si level1 existe dans un domaine, recalculer value depuis les booléens
|
||||
const stats = src.stats ?? {}
|
||||
for (const [statId, statData] of Object.entries(stats)) {
|
||||
for (const [skillId, skill] of Object.entries(statData ?? {})) {
|
||||
if (typeof skill !== "object" || !("level1" in skill)) continue
|
||||
const value = [1,2,3,4,5,6,7,8].filter(i => skill[`level${i}`] === true).length
|
||||
updateData[`system.stats.${statId}.${skillId}.value`] = value
|
||||
}
|
||||
}
|
||||
|
||||
// Factions : si level1 existe dans une faction, recalculer value depuis les booléens
|
||||
const factions = src.factions ?? {}
|
||||
for (const [factionId, faction] of Object.entries(factions)) {
|
||||
if (typeof faction !== "object" || !("level1" in faction)) continue
|
||||
const value = [1,2,3,4,5,6,7,8,9].filter(i => faction[`level${i}`] === true).length
|
||||
updateData[`system.factions.${factionId}.value`] = value
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(updateData).length > 0) {
|
||||
console.log(`${SYSTEM_ID} | Migration tracks → entiers : ${actor.name}`, updateData)
|
||||
await actor.update(updateData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Handlebars helpers ─────────────────────────────────────────────────── */
|
||||
|
||||
function _registerHandlebarsHelpers() {
|
||||
@@ -183,6 +246,9 @@ function _registerHandlebarsHelpers() {
|
||||
// Helper : build array from args (Handlebars doesn't have arrays natively)
|
||||
Handlebars.registerHelper("array", (...args) => args.slice(0, -1))
|
||||
|
||||
// Helper : range(n) → [1, 2, ..., n] — pour les boucles de cases à cocher
|
||||
Handlebars.registerHelper("range", (n) => Array.from({ length: n }, (_, i) => i + 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
|
||||
|
||||
41
lang/fr.json
41
lang/fr.json
@@ -95,7 +95,8 @@
|
||||
"destin": "Destin",
|
||||
"spleen": "Spleen",
|
||||
"level": "Niveau",
|
||||
"currentMalus": "Malus actuel"
|
||||
"currentMalus": "Malus actuel",
|
||||
"destinTooltip": "Usages du Destin :\n• Réaliser un test avec 3d8\n• Gagner l'initiative lors d'un combat\n• Trouver l'ensemble des indices\n• Éviter une blessure\n• Sortir de l'inconscience\n• Obtenir un Triomphe"
|
||||
},
|
||||
"Wound": {
|
||||
"none": "Aucune blessure",
|
||||
@@ -106,7 +107,12 @@
|
||||
"leger": "Léger",
|
||||
"modere": "Modéré",
|
||||
"grave": "Grave",
|
||||
"dramatique": "Dramatique (hors combat)"
|
||||
"dramatique": "Dramatique (hors combat)",
|
||||
"duration1min": "1 min",
|
||||
"duration10min": "10 min",
|
||||
"duration30min": "30 min",
|
||||
"duration1jour": "1 journée",
|
||||
"status": "État : "
|
||||
},
|
||||
"Combat": {
|
||||
"attack": "Attaquer",
|
||||
@@ -117,7 +123,12 @@
|
||||
"failureHit": "Joueur touché — 1 blessure (mêlée)",
|
||||
"distanceNoWound": "Raté — pas de riposte",
|
||||
"weaponDamage": "dégâts supplémentaires",
|
||||
"playerWounded": "Blessure infligée au joueur (mêlée)"
|
||||
"playerWounded": "Blessure infligée au joueur (mêlée)",
|
||||
"rangedDefenseTitle": "Esquiver (Mobilité)",
|
||||
"rangedDefenseTag": "Défense à distance",
|
||||
"rangedDefenseSuccess": "Attaque esquivée — pas de blessure",
|
||||
"rangedDefenseFailure": "Touché par le PNJ — 1 blessure",
|
||||
"rangedDefensePlayerWounded":"Blessure infligée par attaque à distance"
|
||||
},
|
||||
"Tab": {
|
||||
"main": "Principal",
|
||||
@@ -294,6 +305,30 @@
|
||||
},
|
||||
"Aspect": {
|
||||
"valeur": "Valeur"
|
||||
},
|
||||
"XP": {
|
||||
"title": "Expérience",
|
||||
"actuel": "XP disponible",
|
||||
"depense": "XP dépensée",
|
||||
"depenser": "Dépenser XP",
|
||||
"confirmer": "Confirmer",
|
||||
"montant": "Montant",
|
||||
"raison": "Raison",
|
||||
"raisonPlaceholder": "Ex : Augmentation Mobilité à 4",
|
||||
"date": "Date",
|
||||
"supprimer": "Annuler cette dépense",
|
||||
"disponible": "{n} XP disponibles",
|
||||
"insuffisant": "XP insuffisante — seulement {n} disponibles",
|
||||
"montantInvalide": "Le montant doit être supérieur à 0",
|
||||
"refTitle": "Tableau des coûts",
|
||||
"refAmelioration": "Amélioration",
|
||||
"refCout": "Coût (XP)",
|
||||
"refAugmenterSpec": "Augmenter une Spécialisation",
|
||||
"refCoutNiveau": "= niveau à atteindre",
|
||||
"refAcquerirAspect": "Acquérir un nouvel Aspect",
|
||||
"refAugmenterAspect":"Augmenter / Diminuer un Aspect",
|
||||
"refAcquerirAttribut":"Acquérir ou augmenter un Attribut",
|
||||
"refCoutAttributTotal":"= total des points × 10"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { SYSTEM } from "../../config/system.mjs"
|
||||
|
||||
const { HandlebarsApplicationMixin } = foundry.applications.api
|
||||
|
||||
export default class CelestopolActorSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ActorSheetV2) {
|
||||
@@ -23,6 +25,10 @@ export default class CelestopolActorSheet extends HandlebarsApplicationMixin(fou
|
||||
edit: CelestopolActorSheet.#onItemEdit,
|
||||
delete: CelestopolActorSheet.#onItemDelete,
|
||||
attack: CelestopolActorSheet.#onAttack,
|
||||
rangedDefense: CelestopolActorSheet.#onRangedDefense,
|
||||
trackBox: CelestopolActorSheet.#onTrackBox,
|
||||
skillLevel: CelestopolActorSheet.#onSkillLevel,
|
||||
factionLevel: CelestopolActorSheet.#onFactionLevel,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -42,6 +48,7 @@ export default class CelestopolActorSheet extends HandlebarsApplicationMixin(fou
|
||||
isEditMode: this.isEditMode,
|
||||
isPlayMode: this.isPlayMode,
|
||||
isEditable: this.isEditable,
|
||||
woundLevels: SYSTEM.WOUND_LEVELS,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,28 +58,9 @@ export default class CelestopolActorSheet extends HandlebarsApplicationMixin(fou
|
||||
this.element.querySelectorAll(".rollable").forEach(el => {
|
||||
el.addEventListener("click", this._onRoll.bind(this))
|
||||
})
|
||||
|
||||
// Setup sequential checkbox logic for wound tracks
|
||||
this._setupSequentialCheckboxes()
|
||||
|
||||
// Setup sequential checkbox logic for factions
|
||||
this._setupFactionCheckboxes()
|
||||
}
|
||||
|
||||
/** @override */
|
||||
_onClick(event) {
|
||||
// Skip checkbox clicks in edit mode
|
||||
if (this.isEditMode && event.target.classList.contains('skill-level-checkbox')) {
|
||||
return
|
||||
}
|
||||
super._onClick(event)
|
||||
}
|
||||
|
||||
async _onRoll(event) {
|
||||
// Don't roll if clicking on a checkbox
|
||||
if (event.target.classList.contains('skill-level-checkbox')) {
|
||||
return
|
||||
}
|
||||
if (!this.isPlayMode) return
|
||||
const el = event.currentTarget
|
||||
const statId = el.dataset.statId
|
||||
@@ -158,132 +146,39 @@ export default class CelestopolActorSheet extends HandlebarsApplicationMixin(fou
|
||||
await this.document.system.rollAttack(itemId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup sequential checkbox logic for wound/destin/spleen tracks
|
||||
* Only allows checking the next checkbox in sequence
|
||||
*/
|
||||
_setupSequentialCheckboxes() {
|
||||
this.element.querySelectorAll('.wound-checkbox').forEach(checkbox => {
|
||||
checkbox.addEventListener('change', (event) => {
|
||||
this._handleSequentialCheckboxChange(event)
|
||||
})
|
||||
})
|
||||
static async #onRangedDefense(_event, target) {
|
||||
const itemId = target.getAttribute("data-item-id")
|
||||
if (!itemId) return
|
||||
await this.document.system.rollRangedDefense(itemId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle sequential checkbox change logic
|
||||
* @param {Event} event - The change event
|
||||
*/
|
||||
_handleSequentialCheckboxChange(event) {
|
||||
const checkbox = event.target
|
||||
if (!checkbox.classList.contains('wound-checkbox') || checkbox.disabled) return
|
||||
|
||||
const track = checkbox.dataset.track
|
||||
const currentIndex = parseInt(checkbox.dataset.index)
|
||||
const isChecked = checkbox.checked
|
||||
|
||||
// Get all checkboxes in this track
|
||||
const trackCheckboxes = Array.from(this.element.querySelectorAll(`.wound-checkbox[data-track="${track}"]`))
|
||||
|
||||
if (isChecked) {
|
||||
// Checking a box: uncheck all boxes after this one
|
||||
for (let i = currentIndex + 1; i < trackCheckboxes.length; i++) {
|
||||
trackCheckboxes[i].checked = false
|
||||
}
|
||||
// Check all boxes before this one
|
||||
for (let i = 0; i < currentIndex; i++) {
|
||||
trackCheckboxes[i].checked = true
|
||||
}
|
||||
} else {
|
||||
// Unchecking a box: uncheck all boxes after this one
|
||||
for (let i = currentIndex; i < trackCheckboxes.length; i++) {
|
||||
trackCheckboxes[i].checked = false
|
||||
}
|
||||
/** Met à jour une jauge de piste (blessures/destin/spleen) par clic sur une case. */
|
||||
static #onTrackBox(_event, target) {
|
||||
if (!this.isEditable) return
|
||||
const path = target.dataset.path
|
||||
const index = parseInt(target.dataset.index)
|
||||
const current = foundry.utils.getProperty(this.document, path) ?? 0
|
||||
const newValue = (index <= current) ? index - 1 : index
|
||||
this.document.update({ [path]: Math.max(0, newValue) })
|
||||
}
|
||||
|
||||
// Update the visual state
|
||||
this._updateTrackVisualState()
|
||||
/** Met à jour la valeur d'un domaine par clic sur un point de niveau. */
|
||||
static #onSkillLevel(_event, target) {
|
||||
if (!this.isEditable) return
|
||||
const { statId, skillId } = target.dataset
|
||||
const index = parseInt(target.dataset.index)
|
||||
const current = this.document.system.stats[statId]?.[skillId]?.value ?? 0
|
||||
const newValue = (index <= current) ? index - 1 : index
|
||||
this.document.update({ [`system.stats.${statId}.${skillId}.value`]: Math.max(0, newValue) })
|
||||
}
|
||||
|
||||
/**
|
||||
* Update visual state of track boxes based on checkbox states
|
||||
*/
|
||||
_updateTrackVisualState() {
|
||||
this.element.querySelectorAll('.track-box').forEach(box => {
|
||||
const checkbox = box.querySelector('.wound-checkbox')
|
||||
if (checkbox) {
|
||||
if (checkbox.checked) {
|
||||
box.classList.add('checked')
|
||||
} else {
|
||||
box.classList.remove('checked')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup sequential checkbox logic for faction tracks
|
||||
*/
|
||||
_setupFactionCheckboxes() {
|
||||
this.element.querySelectorAll('.faction-checkbox').forEach(checkbox => {
|
||||
checkbox.addEventListener('change', (event) => {
|
||||
this._handleFactionCheckboxChange(event)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle faction checkbox change logic
|
||||
* @param {Event} event - The change event
|
||||
*/
|
||||
_handleFactionCheckboxChange(event) {
|
||||
const checkbox = event.target
|
||||
if (!checkbox.classList.contains('faction-checkbox') || checkbox.disabled) return
|
||||
|
||||
const factionId = checkbox.dataset.faction
|
||||
const currentLevel = parseInt(checkbox.dataset.level)
|
||||
const isChecked = checkbox.checked
|
||||
|
||||
// Get all checkboxes for this faction
|
||||
const factionCheckboxes = Array.from(this.element.querySelectorAll(`.faction-checkbox[data-faction="${factionId}"]`))
|
||||
|
||||
if (isChecked) {
|
||||
// Checking a box: check all boxes before this one, uncheck all boxes after this one
|
||||
for (let i = 0; i < currentLevel; i++) {
|
||||
factionCheckboxes[i].checked = true
|
||||
}
|
||||
for (let i = currentLevel; i < factionCheckboxes.length; i++) {
|
||||
factionCheckboxes[i].checked = false
|
||||
}
|
||||
} else {
|
||||
// Unchecking a box: uncheck all boxes after this one
|
||||
for (let i = currentLevel - 1; i < factionCheckboxes.length; i++) {
|
||||
factionCheckboxes[i].checked = false
|
||||
}
|
||||
}
|
||||
|
||||
// Update the count display
|
||||
this._updateFactionCount(factionId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the faction count display based on checked checkboxes
|
||||
* @param {string} factionId - The faction ID
|
||||
*/
|
||||
_updateFactionCount(factionId) {
|
||||
const checkboxes = Array.from(this.element.querySelectorAll(`.faction-checkbox[data-faction="${factionId}"]:checked`))
|
||||
const count = checkboxes.length
|
||||
|
||||
// Update the hidden input field
|
||||
const input = this.element.querySelector(`input[name="system.factions.${factionId}.value"]`)
|
||||
if (input) {
|
||||
input.value = count
|
||||
}
|
||||
|
||||
// Update the visual count display
|
||||
const countDisplay = this.element.querySelector(`.faction-row[data-faction="${factionId}"] .faction-count`)
|
||||
if (countDisplay) {
|
||||
countDisplay.textContent = count
|
||||
}
|
||||
/** Met à jour le score d'une faction par clic sur un point. */
|
||||
static #onFactionLevel(_event, target) {
|
||||
if (!this.isEditable) return
|
||||
const factionId = target.dataset.faction
|
||||
const index = parseInt(target.dataset.index)
|
||||
const current = this.document.system.factions[factionId]?.value ?? 0
|
||||
const newValue = (index <= current) ? index - 1 : index
|
||||
this.document.update({ [`system.factions.${factionId}.value`]: Math.max(0, newValue) })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ export default class CelestopolCharacterSheet extends CelestopolActorSheet {
|
||||
createArmure: CelestopolCharacterSheet.#onCreateArmure,
|
||||
useAnomaly: CelestopolCharacterSheet.#onUseAnomaly,
|
||||
resetAnomalyUses: CelestopolCharacterSheet.#onResetAnomalyUses,
|
||||
depenseXp: CelestopolCharacterSheet.#onDepenseXp,
|
||||
supprimerXpLog: CelestopolCharacterSheet.#onSupprimerXpLog,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -95,6 +97,7 @@ export default class CelestopolCharacterSheet extends CelestopolActorSheet {
|
||||
|
||||
case "biography":
|
||||
context.tab = context.tabs.biography
|
||||
context.xpLogEmpty = (doc.system.xp?.log?.length ?? 0) === 0
|
||||
context.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
|
||||
doc.system.description, { async: true })
|
||||
context.enrichedNotes = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
|
||||
@@ -111,36 +114,36 @@ export default class CelestopolCharacterSheet extends CelestopolActorSheet {
|
||||
return context
|
||||
}
|
||||
|
||||
static #onCreateAnomaly() {
|
||||
static async #onCreateAnomaly() {
|
||||
if (this.document.itemTypes.anomaly.length > 0) {
|
||||
ui.notifications.warn(game.i18n.localize("CELESTOPOL.Anomaly.maxAnomaly"))
|
||||
return
|
||||
}
|
||||
this.document.createEmbeddedDocuments("Item", [{
|
||||
await this.document.createEmbeddedDocuments("Item", [{
|
||||
name: game.i18n.localize("CELESTOPOL.Item.newAnomaly"), type: "anomaly",
|
||||
}])
|
||||
}
|
||||
|
||||
static #onCreateAspect() {
|
||||
this.document.createEmbeddedDocuments("Item", [{
|
||||
static async #onCreateAspect() {
|
||||
await this.document.createEmbeddedDocuments("Item", [{
|
||||
name: game.i18n.localize("CELESTOPOL.Item.newAspect"), type: "aspect",
|
||||
}])
|
||||
}
|
||||
|
||||
static #onCreateEquipment() {
|
||||
this.document.createEmbeddedDocuments("Item", [{
|
||||
static async #onCreateEquipment() {
|
||||
await this.document.createEmbeddedDocuments("Item", [{
|
||||
name: game.i18n.localize("TYPES.Item.equipment"), type: "equipment",
|
||||
}])
|
||||
}
|
||||
|
||||
static #onCreateWeapon() {
|
||||
this.document.createEmbeddedDocuments("Item", [{
|
||||
static async #onCreateWeapon() {
|
||||
await this.document.createEmbeddedDocuments("Item", [{
|
||||
name: game.i18n.localize("TYPES.Item.weapon"), type: "weapon",
|
||||
}])
|
||||
}
|
||||
|
||||
static #onCreateArmure() {
|
||||
this.document.createEmbeddedDocuments("Item", [{
|
||||
static async #onCreateArmure() {
|
||||
await this.document.createEmbeddedDocuments("Item", [{
|
||||
name: game.i18n.localize("TYPES.Item.armure"), type: "armure",
|
||||
}])
|
||||
}
|
||||
@@ -163,4 +166,73 @@ export default class CelestopolCharacterSheet extends CelestopolActorSheet {
|
||||
if (!anomaly) return
|
||||
await anomaly.update({ "system.usesRemaining": anomaly.system.level })
|
||||
}
|
||||
|
||||
/** Ouvre un dialogue pour dépenser de l'XP. */
|
||||
static async #onDepenseXp() {
|
||||
const actor = this.document
|
||||
const currentXp = actor.system.xp?.actuel ?? 0
|
||||
const i18n = game.i18n
|
||||
|
||||
const content = `
|
||||
<form class="cel-dialog-form">
|
||||
<div class="form-group">
|
||||
<label>${i18n.localize("CELESTOPOL.XP.montant")}</label>
|
||||
<input type="number" name="montant" value="1" min="1" max="${currentXp}" autofocus />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>${i18n.localize("CELESTOPOL.XP.raison")}</label>
|
||||
<input type="text" name="raison" placeholder="${i18n.localize("CELESTOPOL.XP.raisonPlaceholder")}" />
|
||||
</div>
|
||||
<p class="xp-dialog-hint">${i18n.format("CELESTOPOL.XP.disponible", { n: currentXp })}</p>
|
||||
</form>`
|
||||
|
||||
const result = await foundry.applications.api.DialogV2.prompt({
|
||||
window: { title: i18n.localize("CELESTOPOL.XP.depenser") },
|
||||
content,
|
||||
ok: {
|
||||
label: i18n.localize("CELESTOPOL.XP.confirmer"),
|
||||
callback: (event, button) => {
|
||||
const form = button.form
|
||||
return {
|
||||
montant: parseInt(form.querySelector("[name=montant]").value) || 0,
|
||||
raison: form.querySelector("[name=raison]").value.trim(),
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (!result) return
|
||||
const { montant, raison } = result
|
||||
|
||||
if (montant <= 0) {
|
||||
ui.notifications.warn(i18n.localize("CELESTOPOL.XP.montantInvalide"))
|
||||
return
|
||||
}
|
||||
if (montant > currentXp) {
|
||||
ui.notifications.warn(i18n.format("CELESTOPOL.XP.insuffisant", { n: currentXp }))
|
||||
return
|
||||
}
|
||||
|
||||
const date = new Date().toLocaleDateString("fr-FR")
|
||||
const log = [...(actor.system.xp.log ?? []), { montant, raison, date }]
|
||||
await actor.update({
|
||||
"system.xp.actuel": currentXp - montant,
|
||||
"system.xp.log": log,
|
||||
})
|
||||
}
|
||||
|
||||
/** Supprime une entrée du log XP et rembourse les points (mode édition). */
|
||||
static async #onSupprimerXpLog(event, target) {
|
||||
const idx = parseInt(target.dataset.idx)
|
||||
const actor = this.document
|
||||
const log = [...(actor.system.xp.log ?? [])]
|
||||
if (isNaN(idx) || idx < 0 || idx >= log.length) return
|
||||
|
||||
const entry = log[idx]
|
||||
log.splice(idx, 1)
|
||||
await actor.update({
|
||||
"system.xp.actuel": (actor.system.xp?.actuel ?? 0) + entry.montant,
|
||||
"system.xp.log": log,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,15 +90,15 @@ export const FACTIONS = {
|
||||
|
||||
/** 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 },
|
||||
{ id: 0, label: "CELESTOPOL.Wound.none", malus: 0, duration: "" },
|
||||
{ id: 1, label: "CELESTOPOL.Wound.anodin", malus: 0, duration: "CELESTOPOL.Wound.duration1min" },
|
||||
{ id: 2, label: "CELESTOPOL.Wound.negligeable", malus: 0, duration: "CELESTOPOL.Wound.duration1min" },
|
||||
{ id: 3, label: "CELESTOPOL.Wound.derisoire", malus: -1, duration: "CELESTOPOL.Wound.duration10min" },
|
||||
{ id: 4, label: "CELESTOPOL.Wound.superficiel", malus: -1, duration: "CELESTOPOL.Wound.duration10min" },
|
||||
{ id: 5, label: "CELESTOPOL.Wound.leger", malus: -2, duration: "CELESTOPOL.Wound.duration30min" },
|
||||
{ id: 6, label: "CELESTOPOL.Wound.modere", malus: -2, duration: "CELESTOPOL.Wound.duration30min" },
|
||||
{ id: 7, label: "CELESTOPOL.Wound.grave", malus: -3, duration: "CELESTOPOL.Wound.duration1jour" },
|
||||
{ id: 8, label: "CELESTOPOL.Wound.dramatique", malus: -999, duration: "" },
|
||||
]
|
||||
|
||||
/** Seuils de difficulté pour les jets de dés. */
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
export default class CelestopolActor extends Actor {
|
||||
/** @override */
|
||||
prepareDerivedData() {
|
||||
super.prepareDerivedData()
|
||||
this.system.prepareDerivedData?.()
|
||||
}
|
||||
|
||||
/** @override */
|
||||
getRollData() {
|
||||
return this.toObject(false).system
|
||||
|
||||
@@ -1,6 +1 @@
|
||||
export default class CelestopolChatMessage extends ChatMessage {
|
||||
/** @override */
|
||||
async renderHTML(options = {}) {
|
||||
return super.renderHTML(options)
|
||||
}
|
||||
}
|
||||
export default class CelestopolChatMessage extends ChatMessage {}
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
export default class CelestopolItem extends Item {
|
||||
/** @override */
|
||||
prepareDerivedData() {
|
||||
super.prepareDerivedData()
|
||||
}
|
||||
|
||||
/** @override */
|
||||
getRollData() {
|
||||
return this.toObject(false).system
|
||||
|
||||
@@ -42,6 +42,7 @@ export class CelestopolRoll extends Roll {
|
||||
const fortuneValue = options.fortuneValue ?? 0
|
||||
const isResistance = options.isResistance ?? false
|
||||
const isCombat = options.isCombat ?? false
|
||||
const isRangedDefense = options.isRangedDefense ?? false
|
||||
const weaponType = options.weaponType ?? "melee"
|
||||
const weaponName = options.weaponName ?? null
|
||||
const weaponDegats = options.weaponDegats ?? "0"
|
||||
@@ -72,6 +73,7 @@ export class CelestopolRoll extends Roll {
|
||||
defaultRollMoonDie: options.rollMoonDie ?? false,
|
||||
isResistance,
|
||||
isCombat,
|
||||
isRangedDefense,
|
||||
weaponType,
|
||||
weaponName,
|
||||
weaponDegats,
|
||||
@@ -221,6 +223,7 @@ export class CelestopolRoll extends Roll {
|
||||
autoSuccess,
|
||||
isResistance,
|
||||
isCombat,
|
||||
isRangedDefense,
|
||||
weaponType,
|
||||
weaponName,
|
||||
weaponDegats,
|
||||
@@ -243,66 +246,49 @@ export class CelestopolRoll extends Roll {
|
||||
// Test de résistance échoué → cocher automatiquement la prochaine case de blessure
|
||||
const actor = game.actors.get(options.actorId)
|
||||
if (isResistance && actor && roll.options.resultType === "failure") {
|
||||
const wounds = actor.system.blessures
|
||||
const nextIdx = [1,2,3,4,5,6,7,8].find(i => !wounds[`b${i}`]?.checked)
|
||||
if (nextIdx) {
|
||||
await actor.update({ [`system.blessures.b${nextIdx}.checked`]: true })
|
||||
roll.options.woundTaken = nextIdx
|
||||
const nextLvl = (actor.system.blessures.lvl ?? 0) + 1
|
||||
if (nextLvl <= 8) {
|
||||
await actor.update({ "system.blessures.lvl": nextLvl })
|
||||
roll.options.woundTaken = nextLvl
|
||||
}
|
||||
}
|
||||
|
||||
// Combat mêlée échoué → joueur prend une blessure
|
||||
if (isCombat && weaponType === "melee" && actor && roll.options.resultType === "failure") {
|
||||
const wounds = actor.system.blessures
|
||||
const nextIdx = [1,2,3,4,5,6,7,8].find(i => !wounds[`b${i}`]?.checked)
|
||||
if (nextIdx) {
|
||||
await actor.update({ [`system.blessures.b${nextIdx}.checked`]: true })
|
||||
roll.options.woundTaken = nextIdx
|
||||
// Mêlée échouée OU défense à distance échouée → joueur prend une blessure
|
||||
if (isCombat && (weaponType === "melee" || isRangedDefense) && actor && roll.options.resultType === "failure") {
|
||||
const nextLvl = (actor.system.blessures.lvl ?? 0) + 1
|
||||
if (nextLvl <= 8) {
|
||||
await actor.update({ "system.blessures.lvl": nextLvl })
|
||||
roll.options.woundTaken = nextLvl
|
||||
}
|
||||
}
|
||||
|
||||
await roll.toMessage({}, { rollMode: rollData.rollMode })
|
||||
|
||||
// Destin utilisé → vider la jauge (reset à 0)
|
||||
if (rollData.useDestin && actor) {
|
||||
await actor.update({
|
||||
"system.destin.lvl": 0,
|
||||
"system.destin.d1.checked": false,
|
||||
"system.destin.d2.checked": false,
|
||||
"system.destin.d3.checked": false,
|
||||
"system.destin.d4.checked": false,
|
||||
"system.destin.d5.checked": false,
|
||||
"system.destin.d6.checked": false,
|
||||
"system.destin.d7.checked": false,
|
||||
"system.destin.d8.checked": false,
|
||||
})
|
||||
// Batching de toutes les mises à jour de l'acteur en un seul appel réseau
|
||||
if (actor) {
|
||||
const updateData = {}
|
||||
|
||||
if (rollData.useDestin) {
|
||||
updateData["system.destin.lvl"] = 0
|
||||
}
|
||||
|
||||
// Fortune utilisée → décrémenter de 1 (min 0)
|
||||
if (rollData.useFortune && actor) {
|
||||
if (rollData.useFortune) {
|
||||
const currentFortune = actor.system.attributs.fortune.value ?? 0
|
||||
await actor.update({ "system.attributs.fortune.value": Math.max(0, currentFortune - 1) })
|
||||
updateData["system.attributs.fortune.value"] = Math.max(0, currentFortune - 1)
|
||||
}
|
||||
|
||||
// Puiser dans ses ressources → coche une case de spleen
|
||||
if (rollData.puiserRessources && actor) {
|
||||
if (rollData.puiserRessources) {
|
||||
const currentSpleen = actor.system.spleen.lvl ?? 0
|
||||
if (currentSpleen < 8) {
|
||||
const newLvl = currentSpleen + 1
|
||||
const key = `s${newLvl}`
|
||||
await actor.update({
|
||||
"system.spleen.lvl": newLvl,
|
||||
[`system.spleen.${key}.checked`]: true,
|
||||
})
|
||||
updateData["system.spleen.lvl"] = currentSpleen + 1
|
||||
}
|
||||
}
|
||||
|
||||
// Mémoriser les préférences sur l'acteur
|
||||
if (actor) {
|
||||
await actor.update({
|
||||
"system.prefs.rollMoonDie": rollData.rollMoonDie,
|
||||
"system.prefs.difficulty": difficulty,
|
||||
})
|
||||
// Mémoriser les préférences
|
||||
updateData["system.prefs.rollMoonDie"] = rollData.rollMoonDie
|
||||
updateData["system.prefs.difficulty"] = difficulty
|
||||
|
||||
await actor.update(updateData)
|
||||
}
|
||||
|
||||
return roll
|
||||
@@ -421,6 +407,7 @@ export class CelestopolRoll extends Roll {
|
||||
weaponName: this.options.weaponName ?? null,
|
||||
weaponDegats: this.options.weaponDegats ?? null,
|
||||
weaponType: this.options.weaponType ?? null,
|
||||
isRangedDefense: this.options.isRangedDefense ?? false,
|
||||
woundTaken: this.options.woundTaken ?? null,
|
||||
// Dé de lune
|
||||
hasMoonDie: moonDieResult !== null,
|
||||
|
||||
@@ -21,18 +21,10 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
|
||||
value: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }),
|
||||
})
|
||||
|
||||
// Les 4 stats avec leurs domaines
|
||||
// Les 4 stats avec leurs domaines — niveau stocké directement comme entier
|
||||
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 }),
|
||||
level1: new fields.BooleanField({ required: true, initial: false }),
|
||||
level2: new fields.BooleanField({ required: true, initial: false }),
|
||||
level3: new fields.BooleanField({ required: true, initial: false }),
|
||||
level4: new fields.BooleanField({ required: true, initial: false }),
|
||||
level5: new fields.BooleanField({ required: true, initial: false }),
|
||||
level6: new fields.BooleanField({ required: true, initial: false }),
|
||||
level7: new fields.BooleanField({ required: true, initial: false }),
|
||||
level8: new fields.BooleanField({ required: true, initial: false }),
|
||||
})
|
||||
|
||||
const statField = (statId) => {
|
||||
@@ -55,32 +47,19 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
|
||||
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 }),
|
||||
})
|
||||
// Blessures — niveau entier direct (0 = aucune, 8 = fatale)
|
||||
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 }),
|
||||
})
|
||||
// Destin — jauge entière directe
|
||||
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)
|
||||
// Spleen — jauge entière directe
|
||||
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)
|
||||
@@ -95,18 +74,9 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
|
||||
vision: persoAttrField(),
|
||||
})
|
||||
|
||||
// Factions - 9 checkboxes per faction (like wound tracks)
|
||||
// Factions — score entier direct (0-9)
|
||||
const factionField = () => new fields.SchemaField({
|
||||
value: new fields.NumberField({ ...reqInt, initial: 0 }),
|
||||
level1: new fields.BooleanField({ required: true, initial: false }),
|
||||
level2: new fields.BooleanField({ required: true, initial: false }),
|
||||
level3: new fields.BooleanField({ required: true, initial: false }),
|
||||
level4: new fields.BooleanField({ required: true, initial: false }),
|
||||
level5: new fields.BooleanField({ required: true, initial: false }),
|
||||
level6: new fields.BooleanField({ required: true, initial: false }),
|
||||
level7: new fields.BooleanField({ required: true, initial: false }),
|
||||
level8: new fields.BooleanField({ required: true, initial: false }),
|
||||
level9: new fields.BooleanField({ required: true, initial: false }),
|
||||
value: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 9 }),
|
||||
})
|
||||
schema.factions = new fields.SchemaField({
|
||||
pinkerton: factionField(),
|
||||
@@ -133,6 +103,16 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
|
||||
difficulty: new fields.StringField({ required: true, nullable: false, initial: "normal" }),
|
||||
})
|
||||
|
||||
// Expérience
|
||||
schema.xp = new fields.SchemaField({
|
||||
actuel: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
|
||||
log: new fields.ArrayField(new fields.SchemaField({
|
||||
montant: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
|
||||
raison: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
date: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
})),
|
||||
})
|
||||
|
||||
// Description & notes
|
||||
schema.description = new fields.HTMLField({ required: true, textSearch: true })
|
||||
schema.notes = new fields.HTMLField({ required: true, textSearch: true })
|
||||
@@ -156,15 +136,7 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
|
||||
prepareDerivedData() {
|
||||
super.prepareDerivedData()
|
||||
|
||||
// Calcul automatique de la valeur de chaque domaine = nombre de cases cochées
|
||||
for (const stat of Object.values(this.stats)) {
|
||||
for (const skill of Object.values(stat)) {
|
||||
if (typeof skill !== "object" || !("level1" in skill)) continue
|
||||
skill.value = [1,2,3,4,5,6,7,8].filter(i => skill[`level${i}`]).length
|
||||
}
|
||||
}
|
||||
|
||||
// Calcul automatique de la Résistance par stat = +2 par domaine atteignant son seuil
|
||||
// Résistance par stat = +2 par domaine atteignant son seuil de spécialisation
|
||||
for (const [statId, statData] of Object.entries(this.stats)) {
|
||||
let res = 0
|
||||
for (const [skillId, skill] of Object.entries(statData)) {
|
||||
@@ -175,19 +147,11 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
|
||||
statData.res = res
|
||||
}
|
||||
|
||||
// Calcul automatique de la valeur de chaque faction = nombre de cases cochées
|
||||
for (const faction of Object.values(this.factions)) {
|
||||
if (typeof faction !== "object" || !("level1" in faction)) continue
|
||||
faction.value = [1,2,3,4,5,6,7,8,9].filter(i => faction[`level${i}`]).length
|
||||
}
|
||||
|
||||
// Calcul automatique du niveau des jauges depuis les cases cochées
|
||||
this.blessures.lvl = [1,2,3,4,5,6,7,8].filter(i => this.blessures[`b${i}`]?.checked).length
|
||||
this.destin.lvl = [1,2,3,4,5,6,7,8].filter(i => this.destin[`d${i}`]?.checked).length
|
||||
this.spleen.lvl = [1,2,3,4,5,6,7,8].filter(i => this.spleen[`s${i}`]?.checked).length
|
||||
|
||||
// Initiative PJ : 4 + Mobilité (Corps) + Inspiration (Cœur) [après calcul des domaines]
|
||||
// Initiative PJ : 4 + Mobilité (Corps) + Inspiration (Cœur)
|
||||
this.initiative = 4 + (this.stats.corps.mobilite?.value ?? 0) + (this.stats.coeur.inspiration?.value ?? 0)
|
||||
|
||||
// XP dépensée = somme des montants du log
|
||||
this.xp.depense = this.xp.log.reduce((sum, entry) => sum + entry.montant, 0)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -239,10 +203,29 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
|
||||
const statData = this.stats[statId]
|
||||
if (!statData) return null
|
||||
|
||||
return CelestopolRoll.prompt({
|
||||
actorId: this.parent.id,
|
||||
actorName: this.parent.name,
|
||||
actorImage: this.parent.img,
|
||||
statId,
|
||||
skillId: null,
|
||||
statLabel: SYSTEM.STATS[statId]?.label,
|
||||
skillLabel: "CELESTOPOL.Roll.resistanceTest",
|
||||
skillValue: statData.res,
|
||||
woundMalus: this.getWoundMalus(),
|
||||
woundLevel: this.blessures.lvl,
|
||||
isResistance: true,
|
||||
rollMoonDie: false,
|
||||
destGaugeFull: false,
|
||||
fortuneValue: 0,
|
||||
difficulty: "normal",
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Lance une attaque avec une arme (test Échauffourée vs Corps PNJ).
|
||||
* Mêlée : échec → blessure joueur auto-cochée.
|
||||
* Distance : échec → simple raté, pas de blessure joueur.
|
||||
* Lance une attaque avec une arme.
|
||||
* Mêlée : test Échauffourée vs Corps PNJ ; échec → blessure joueur.
|
||||
* Distance : test Échauffourée vs Corps PNJ ; échec → pas de blessure joueur.
|
||||
* Égalité (marge=0) → personne n'est blessé.
|
||||
* @param {string} itemId - Id de l'item arme
|
||||
*/
|
||||
@@ -269,9 +252,46 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
|
||||
destGaugeFull: this.destin.lvl > 0,
|
||||
fortuneValue: this.attributs.fortune.value,
|
||||
isCombat: true,
|
||||
isRangedDefense: false,
|
||||
weaponType: item.system.type,
|
||||
weaponName: item.name,
|
||||
weaponDegats: item.system.degats,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Lance un jet de défense contre une attaque à distance (test Mobilité vs Corps PNJ).
|
||||
* Succès → esquive réussie.
|
||||
* Échec → blessure automatique (le PNJ touche).
|
||||
* @param {string} itemId - Id de l'item arme (distance uniquement)
|
||||
*/
|
||||
async rollRangedDefense(itemId) {
|
||||
const { CelestopolRoll } = await import("../documents/roll.mjs")
|
||||
const item = this.parent.items.get(itemId)
|
||||
if (!item || item.type !== "weapon" || item.system.type !== "distance") return null
|
||||
|
||||
const mobilite = this.stats.corps.mobilite
|
||||
if (!mobilite) return null
|
||||
|
||||
return CelestopolRoll.prompt({
|
||||
actorId: this.parent.id,
|
||||
actorName: this.parent.name,
|
||||
actorImage: this.parent.img,
|
||||
statId: "corps",
|
||||
skillId: "mobilite",
|
||||
statLabel: SYSTEM.STATS.corps.label,
|
||||
skillLabel: SYSTEM.SKILLS.corps.mobilite.label,
|
||||
skillValue: mobilite.value,
|
||||
woundMalus: this.getWoundMalus(),
|
||||
woundLevel: this.blessures.lvl,
|
||||
rollMoonDie: this.prefs.rollMoonDie ?? false,
|
||||
destGaugeFull: this.destin.lvl > 0,
|
||||
fortuneValue: this.attributs.fortune.value,
|
||||
isCombat: true,
|
||||
isRangedDefense: true,
|
||||
weaponType: "distance",
|
||||
weaponName: item.name,
|
||||
weaponDegats: "0",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,14 +41,8 @@ export default class CelestopolNPC extends foundry.abstract.TypeDataModel {
|
||||
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({
|
||||
|
||||
@@ -1 +1 @@
|
||||
MANIFEST-000006
|
||||
MANIFEST-000018
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
2026/03/29-17:12:00.740305 7f4bda7ed6c0 Recovering log #4
|
||||
2026/03/29-17:12:00.787211 7f4bda7ed6c0 Delete type=3 #2
|
||||
2026/03/29-17:12:00.787276 7f4bda7ed6c0 Delete type=0 #4
|
||||
2026/03/30-23:54:32.064751 7ff9c7fff6c0 Recovering log #16
|
||||
2026/03/30-23:54:32.074311 7ff9c7fff6c0 Delete type=3 #14
|
||||
2026/03/30-23:54:32.074383 7ff9c7fff6c0 Delete type=0 #16
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
2026/03/28-09:47:34.669467 7f0018fff6c0 Delete type=3 #1
|
||||
2026/03/29-17:08:09.756858 7effca7fc6c0 Level-0 table #5: started
|
||||
2026/03/29-17:08:09.756892 7effca7fc6c0 Level-0 table #5: 0 bytes OK
|
||||
2026/03/29-17:08:09.762851 7effca7fc6c0 Delete type=0 #3
|
||||
2026/03/29-17:08:09.769416 7effca7fc6c0 Manual compaction at level-0 from 'undefined' @ 72057594037927935 : 1 .. 'undefined' @ 0 : 0; will stop at (end)
|
||||
2026/03/30-09:43:32.818417 7f4bda7ed6c0 Recovering log #12
|
||||
2026/03/30-09:43:32.832361 7f4bda7ed6c0 Delete type=3 #10
|
||||
2026/03/30-09:43:32.832436 7f4bda7ed6c0 Delete type=0 #12
|
||||
2026/03/30-14:14:04.399110 7f4bd8fea6c0 Level-0 table #17: started
|
||||
2026/03/30-14:14:04.399143 7f4bd8fea6c0 Level-0 table #17: 0 bytes OK
|
||||
2026/03/30-14:14:04.436937 7f4bd8fea6c0 Delete type=0 #15
|
||||
2026/03/30-14:14:04.520163 7f4bd8fea6c0 Manual compaction at level-0 from 'undefined' @ 72057594037927935 : 1 .. 'undefined' @ 0 : 0; will stop at (end)
|
||||
|
||||
Binary file not shown.
BIN
packs-system/anomalies/MANIFEST-000018
Normal file
BIN
packs-system/anomalies/MANIFEST-000018
Normal file
Binary file not shown.
@@ -1 +1 @@
|
||||
MANIFEST-000006
|
||||
MANIFEST-000018
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
2026/03/29-17:12:00.691509 7f4bd9fec6c0 Recovering log #4
|
||||
2026/03/29-17:12:00.738164 7f4bd9fec6c0 Delete type=3 #2
|
||||
2026/03/29-17:12:00.738214 7f4bd9fec6c0 Delete type=0 #4
|
||||
2026/03/30-23:54:32.051664 7ff9fd1fe6c0 Recovering log #16
|
||||
2026/03/30-23:54:32.062889 7ff9fd1fe6c0 Delete type=3 #14
|
||||
2026/03/30-23:54:32.062954 7ff9fd1fe6c0 Delete type=0 #16
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
2026/03/28-09:47:34.653497 7effcaffd6c0 Delete type=3 #1
|
||||
2026/03/29-17:08:09.762957 7effca7fc6c0 Level-0 table #5: started
|
||||
2026/03/29-17:08:09.762977 7effca7fc6c0 Level-0 table #5: 0 bytes OK
|
||||
2026/03/29-17:08:09.769218 7effca7fc6c0 Delete type=0 #3
|
||||
2026/03/29-17:08:09.769426 7effca7fc6c0 Manual compaction at level-0 from 'undefined' @ 72057594037927935 : 1 .. 'undefined' @ 0 : 0; will stop at (end)
|
||||
2026/03/30-09:43:32.805788 7f4bd9fec6c0 Recovering log #12
|
||||
2026/03/30-09:43:32.816248 7f4bd9fec6c0 Delete type=3 #10
|
||||
2026/03/30-09:43:32.816303 7f4bd9fec6c0 Delete type=0 #12
|
||||
2026/03/30-14:14:04.367410 7f4bd8fea6c0 Level-0 table #17: started
|
||||
2026/03/30-14:14:04.367477 7f4bd8fea6c0 Level-0 table #17: 0 bytes OK
|
||||
2026/03/30-14:14:04.398962 7f4bd8fea6c0 Delete type=0 #15
|
||||
2026/03/30-14:14:04.520149 7f4bd8fea6c0 Manual compaction at level-0 from 'undefined' @ 72057594037927935 : 1 .. 'undefined' @ 0 : 0; will stop at (end)
|
||||
|
||||
Binary file not shown.
BIN
packs-system/aspects/MANIFEST-000018
Normal file
BIN
packs-system/aspects/MANIFEST-000018
Normal file
Binary file not shown.
@@ -131,39 +131,27 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
// Track de niveau (cases à cocher Art Déco)
|
||||
// Points de niveau Art Déco (remplacent les cases à cocher)
|
||||
.skill-checkboxes-container {
|
||||
.skill-checkboxes {
|
||||
display: flex;
|
||||
gap: 3px;
|
||||
align-items: center;
|
||||
}
|
||||
.skill-checkbox-wrapper {
|
||||
line-height: 0;
|
||||
cursor: pointer;
|
||||
.skill-level-checkbox {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
.skill-level-dot {
|
||||
display: inline-block;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
border: 1px solid var(--cel-border);
|
||||
border-radius: 1px;
|
||||
background: rgba(255,255,255,0.3);
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
transition: background 0.1s, border-color 0.1s;
|
||||
&:checked {
|
||||
&.filled {
|
||||
background: var(--cel-orange);
|
||||
border-color: var(--cel-border);
|
||||
}
|
||||
&:disabled { cursor: default; }
|
||||
&:disabled:checked {
|
||||
background: var(--cel-orange);
|
||||
border-color: var(--cel-border);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
&[data-action] { cursor: pointer; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -215,6 +203,11 @@
|
||||
text-transform: uppercase;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.track-title-destin {
|
||||
cursor: help;
|
||||
border-bottom: 1px dashed currentColor;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.track-boxes {
|
||||
@@ -228,17 +221,29 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
width: 22px;
|
||||
min-height: 22px;
|
||||
border: 2px solid var(--cel-border);
|
||||
border-radius: 2px;
|
||||
background: rgba(255,255,255,0.45);
|
||||
transition: background 0.1s, border-color 0.1s;
|
||||
|
||||
&.filled {
|
||||
background: var(--cel-orange);
|
||||
border-color: var(--cel-orange);
|
||||
}
|
||||
|
||||
&[data-action] { cursor: pointer; }
|
||||
|
||||
input[type="checkbox"] { .cel-box(); accent-color: var(--cel-orange); }
|
||||
.box-label {
|
||||
font-size: 0.65em;
|
||||
font-size: 0.6em;
|
||||
color: var(--cel-border);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&.checked input[type="checkbox"] {
|
||||
accent-color: var(--cel-orange);
|
||||
}
|
||||
&.filled .box-label { color: rgba(30,10,0,0.65); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,6 +279,38 @@
|
||||
td { padding: 4px 8px; border-bottom: 1px solid rgba(122,92,32,0.2); }
|
||||
&.custom td { font-style: italic; color: #666; }
|
||||
|
||||
.faction-checkboxes-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.faction-checkboxes {
|
||||
display: flex;
|
||||
gap: 3px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.faction-dot {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 1px solid var(--cel-border);
|
||||
border-radius: 1px;
|
||||
background: rgba(255,255,255,0.3);
|
||||
transition: background 0.1s;
|
||||
&.filled { background: var(--cel-orange); border-color: var(--cel-orange); }
|
||||
&[data-action] { cursor: pointer; }
|
||||
}
|
||||
|
||||
.faction-count {
|
||||
font-size: 0.85em;
|
||||
font-weight: bold;
|
||||
color: var(--cel-orange);
|
||||
min-width: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.faction-value input[type="number"] {
|
||||
width: 50px;
|
||||
.cel-input-std();
|
||||
@@ -335,6 +372,156 @@
|
||||
.enriched-html { font-size: 0.9em; line-height: 1.6; }
|
||||
}
|
||||
|
||||
// ── Section Expérience (onglet Biographie) ──────────────────────────────
|
||||
.xp-section {
|
||||
margin-bottom: 14px;
|
||||
|
||||
.section-header { .cel-section-header(); }
|
||||
|
||||
.xp-counters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 6px 0 8px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.xp-counter {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background: rgba(0,0,0,0.18);
|
||||
border: 1px solid var(--cel-orange);
|
||||
border-radius: 4px;
|
||||
padding: 4px 12px;
|
||||
min-width: 80px;
|
||||
|
||||
label {
|
||||
font-size: 0.6em;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--cel-orange-light);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
width: 52px;
|
||||
text-align: center;
|
||||
.cel-input-std();
|
||||
font-family: var(--cel-font-title);
|
||||
font-size: 1.1em;
|
||||
font-weight: bold;
|
||||
color: var(--cel-orange);
|
||||
}
|
||||
}
|
||||
|
||||
.xp-depense-counter {
|
||||
border-color: rgba(196,154,26,0.4);
|
||||
.xp-depense-value {
|
||||
font-family: var(--cel-font-title);
|
||||
font-size: 1.1em;
|
||||
font-weight: bold;
|
||||
color: rgba(196,154,26,0.7);
|
||||
}
|
||||
}
|
||||
|
||||
.xp-btn-depenser {
|
||||
background: var(--cel-green);
|
||||
border: 1px solid var(--cel-orange);
|
||||
color: var(--cel-orange);
|
||||
font-size: 0.78em;
|
||||
padding: 5px 12px;
|
||||
cursor: pointer;
|
||||
font-family: var(--cel-font-title);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
border-radius: 2px;
|
||||
transition: background 0.15s;
|
||||
margin-left: auto;
|
||||
&:hover { background: var(--cel-green-light); }
|
||||
i { margin-right: 4px; }
|
||||
}
|
||||
}
|
||||
|
||||
.xp-log-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.82em;
|
||||
margin-bottom: 8px;
|
||||
|
||||
thead tr {
|
||||
background: rgba(12,76,12,0.35);
|
||||
th {
|
||||
color: var(--cel-orange-light);
|
||||
font-size: 0.75em;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
padding: 3px 6px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid rgba(196,154,26,0.3);
|
||||
}
|
||||
}
|
||||
|
||||
tbody tr {
|
||||
border-bottom: 1px solid rgba(196,154,26,0.12);
|
||||
&:nth-child(even) { background: rgba(0,0,0,0.08); }
|
||||
|
||||
td { padding: 3px 6px; color: var(--cel-text-dark, #3a2a0a); }
|
||||
.xp-date { white-space: nowrap; color: var(--cel-border); font-size: 0.9em; }
|
||||
.xp-montant { font-weight: bold; color: #c04040; white-space: nowrap; }
|
||||
|
||||
.xp-suppr-cell {
|
||||
text-align: center;
|
||||
.xp-btn-suppr {
|
||||
background: none;
|
||||
border: none;
|
||||
color: rgba(180,60,60,0.6);
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
padding: 1px 4px;
|
||||
&:hover { color: #c04040; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Référentiel de coûts (accordéon)
|
||||
.xp-ref {
|
||||
margin-top: 6px;
|
||||
summary {
|
||||
font-size: 0.78em;
|
||||
color: var(--cel-orange-light);
|
||||
cursor: pointer;
|
||||
letter-spacing: 0.03em;
|
||||
text-transform: uppercase;
|
||||
user-select: none;
|
||||
&:hover { color: var(--cel-orange); }
|
||||
}
|
||||
|
||||
.xp-ref-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.78em;
|
||||
margin-top: 5px;
|
||||
opacity: 0.75;
|
||||
|
||||
th {
|
||||
color: var(--cel-orange-light);
|
||||
text-transform: uppercase;
|
||||
font-size: 0.85em;
|
||||
letter-spacing: 0.03em;
|
||||
padding: 2px 6px;
|
||||
border-bottom: 1px solid rgba(196,154,26,0.25);
|
||||
text-align: left;
|
||||
}
|
||||
td {
|
||||
padding: 2px 6px;
|
||||
border-bottom: 1px solid rgba(196,154,26,0.1);
|
||||
color: var(--cel-text-dark, #3a2a0a);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Bloc Anomalie sur l'onglet Domaines ──────────────────────────────────
|
||||
.anomaly-block {
|
||||
border: 1px solid rgba(196,154,26,0.5);
|
||||
|
||||
@@ -277,18 +277,63 @@
|
||||
&:disabled { cursor: default; opacity: 0.7; }
|
||||
}
|
||||
|
||||
.faction-count {
|
||||
margin-left: 8px;
|
||||
// ── Badge d'état de blessure intégré dans header-stats-row ─────────────────
|
||||
.wound-status-badge {
|
||||
// Supprime le fond/bord générique du .header-stat
|
||||
background: transparent;
|
||||
border-color: currentColor;
|
||||
|
||||
label {
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wound-value {
|
||||
font-family: var(--cel-font-title);
|
||||
font-size: 0.95em;
|
||||
font-weight: bold;
|
||||
color: var(--cel-orange);
|
||||
min-width: 20px;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.wound-duration { font-weight: normal; opacity: 0.85; }
|
||||
.wound-malus { opacity: 0.9; }
|
||||
|
||||
// Niveaux 1-2 : aucun malus → vert doux
|
||||
&.wound-level-1, &.wound-level-2 {
|
||||
color: #6abf5e;
|
||||
background: rgba(106,191,94,0.10);
|
||||
label { color: #6abf5e; }
|
||||
}
|
||||
// Niveaux 3-4 : malus -1 → ambre
|
||||
&.wound-level-3, &.wound-level-4 {
|
||||
color: #e8a020;
|
||||
background: rgba(232,160,32,0.13);
|
||||
label { color: #e8a020; }
|
||||
}
|
||||
// Niveaux 5-6 : malus -2 → orange vif
|
||||
&.wound-level-5, &.wound-level-6 {
|
||||
color: #e06020;
|
||||
background: rgba(224,96,32,0.13);
|
||||
label { color: #e06020; }
|
||||
}
|
||||
// Niveau 7 : malus -3 → rouge
|
||||
&.wound-level-7 {
|
||||
color: #d43030;
|
||||
background: rgba(212,48,48,0.13);
|
||||
label { color: #d43030; }
|
||||
}
|
||||
// Niveau 8 : hors combat → rouge sombre + pulsation
|
||||
&.wound-level-8 {
|
||||
color: #c00;
|
||||
background: rgba(192,0,0,0.18);
|
||||
label { color: #c00; }
|
||||
animation: wound-pulse 1.4s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.faction-value-input { width: 40px; margin-left: 8px; }
|
||||
@keyframes wound-pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.55; }
|
||||
}
|
||||
|
||||
.faction-row {
|
||||
pointer-events: auto !important;
|
||||
td { pointer-events: auto !important; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
"distance": 5,
|
||||
"units": "m"
|
||||
},
|
||||
"primaryTokenAttribute": "resource",
|
||||
"primaryTokenAttribute": "blessures.lvl",
|
||||
"socket": true,
|
||||
"background": "systems/fvtt-celestopol/assets/ui/celestopol_background.webp"
|
||||
}
|
||||
@@ -1,5 +1,76 @@
|
||||
<div class="tab biography {{tab.cssClass}}" data-group="sheet" data-tab="biography">
|
||||
|
||||
{{!-- Section XP --}}
|
||||
<div class="xp-section">
|
||||
<div class="section-header">{{localize "CELESTOPOL.XP.title"}}</div>
|
||||
|
||||
<div class="xp-counters">
|
||||
<div class="xp-counter">
|
||||
<label>{{localize "CELESTOPOL.XP.actuel"}}</label>
|
||||
{{formInput systemFields.xp.fields.actuel value=system.xp.actuel name="system.xp.actuel"}}
|
||||
</div>
|
||||
<div class="xp-counter xp-depense-counter">
|
||||
<label>{{localize "CELESTOPOL.XP.depense"}}</label>
|
||||
<span class="xp-depense-value">{{system.xp.depense}}</span>
|
||||
</div>
|
||||
{{#if isPlayMode}}
|
||||
<button type="button" class="xp-btn-depenser" data-action="depenseXp">
|
||||
<i class="fa-solid fa-coins"></i> {{localize "CELESTOPOL.XP.depenser"}}
|
||||
</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{!-- Log des dépenses --}}
|
||||
{{#unless xpLogEmpty}}
|
||||
<table class="xp-log-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{localize "CELESTOPOL.XP.date"}}</th>
|
||||
<th>{{localize "CELESTOPOL.XP.raison"}}</th>
|
||||
<th>{{localize "CELESTOPOL.XP.montant"}}</th>
|
||||
{{#if isEditMode}}<th></th>{{/if}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each system.xp.log}}
|
||||
<tr>
|
||||
<td class="xp-date">{{this.date}}</td>
|
||||
<td class="xp-raison">{{this.raison}}</td>
|
||||
<td class="xp-montant">−{{this.montant}}</td>
|
||||
{{#if ../isEditMode}}
|
||||
<td class="xp-suppr-cell">
|
||||
<button type="button" class="xp-btn-suppr" data-action="supprimerXpLog"
|
||||
data-idx="{{@index}}" title="{{localize 'CELESTOPOL.XP.supprimer'}}">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
{{/if}}
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{/unless}}
|
||||
|
||||
{{!-- Tableau de référence des coûts --}}
|
||||
<details class="xp-ref">
|
||||
<summary>{{localize "CELESTOPOL.XP.refTitle"}}</summary>
|
||||
<table class="xp-ref-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{localize "CELESTOPOL.XP.refAmelioration"}}</th>
|
||||
<th>{{localize "CELESTOPOL.XP.refCout"}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>{{localize "CELESTOPOL.XP.refAugmenterSpec"}}</td><td>{{localize "CELESTOPOL.XP.refCoutNiveau"}}</td></tr>
|
||||
<tr><td>{{localize "CELESTOPOL.XP.refAcquerirAspect"}}</td><td>5</td></tr>
|
||||
<tr><td>{{localize "CELESTOPOL.XP.refAugmenterAspect"}}</td><td>5</td></tr>
|
||||
<tr><td>{{localize "CELESTOPOL.XP.refAcquerirAttribut"}}</td><td>{{localize "CELESTOPOL.XP.refCoutAttributTotal"}}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
{{!-- Description / Biographie --}}
|
||||
<div class="biography-section">
|
||||
<div class="section-header">{{localize "CELESTOPOL.Actor.description"}}</div>
|
||||
|
||||
@@ -4,19 +4,14 @@
|
||||
<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>
|
||||
<strong>{{lookup @root.woundLevels system.blessures.lvl 'malus'}}</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}}
|
||||
class="wound-checkbox"
|
||||
data-track="blessures"
|
||||
data-index="{{idx}}">
|
||||
<label class="box-label">{{lookup ../system.blessures key 'malus'}}</label>
|
||||
{{#each (range 8) as |lvl|}}
|
||||
<div class="track-box {{#if (lte lvl ../system.blessures.lvl)}}filled{{/if}}"
|
||||
{{#if ../isEditable}}data-action="trackBox" data-path="system.blessures.lvl" data-index="{{lvl}}"{{/if}}>
|
||||
<span class="box-label">{{lookup @root.woundLevels lvl 'malus'}}</span>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
@@ -29,18 +24,13 @@
|
||||
{{!-- Destin --}}
|
||||
<section class="track-section">
|
||||
<div class="track-header">
|
||||
<span class="track-title">{{localize "CELESTOPOL.Track.destin"}}</span>
|
||||
<span class="track-title track-title-destin"
|
||||
title="{{localize 'CELESTOPOL.Track.destinTooltip'}}">{{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}}
|
||||
class="wound-checkbox"
|
||||
data-track="destin"
|
||||
data-index="{{@index}}">
|
||||
</div>
|
||||
{{#each (range 8) as |lvl|}}
|
||||
<div class="track-box destiny {{#if (lte lvl ../system.destin.lvl)}}filled{{/if}}"
|
||||
{{#if ../isEditable}}data-action="trackBox" data-path="system.destin.lvl" data-index="{{lvl}}"{{/if}}></div>
|
||||
{{/each}}
|
||||
</div>
|
||||
<div class="track-level">
|
||||
@@ -55,15 +45,9 @@
|
||||
<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}}
|
||||
class="wound-checkbox"
|
||||
data-track="spleen"
|
||||
data-index="{{@index}}">
|
||||
</div>
|
||||
{{#each (range 8) as |lvl|}}
|
||||
<div class="track-box spleen {{#if (lte lvl ../system.spleen.lvl)}}filled{{/if}}"
|
||||
{{#if ../isEditable}}data-action="trackBox" data-path="system.spleen.lvl" data-index="{{lvl}}"{{/if}}></div>
|
||||
{{/each}}
|
||||
</div>
|
||||
<div class="track-level">
|
||||
|
||||
@@ -18,18 +18,13 @@
|
||||
<span class="skill-name">{{localize skill.label}}</span>
|
||||
<div class="skill-checkboxes-container">
|
||||
<div class="skill-checkboxes">
|
||||
{{#each (array 1 2 3 4 5 6 7 8) as |level|}}
|
||||
<label class="skill-checkbox-wrapper">
|
||||
<input type="checkbox" name="system.stats.{{statId}}.{{skillId}}.level{{level}}"
|
||||
{{#if (lookup (lookup (lookup @root.system.stats statId) skillId) (concat 'level' level))}}checked{{/if}}
|
||||
class="skill-level-checkbox">
|
||||
</label>
|
||||
{{#each (range 8) as |lvl|}}
|
||||
<span class="skill-level-dot {{#if (lte lvl (lookup @root.system.stats statId skillId 'value'))}}filled{{/if}}"
|
||||
data-action="skillLevel" data-stat-id="{{statId}}" data-skill-id="{{skillId}}" data-index="{{lvl}}"></span>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
<input type="number" name="system.stats.{{statId}}.{{skillId}}.value"
|
||||
value="{{lookup (lookup @root.system.stats statId) skillId 'value'}}"
|
||||
min="0" max="8" class="skill-value-input">
|
||||
<span class="skill-value">{{lookup @root.system.stats statId skillId 'value'}}</span>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="skill-row rollable" data-stat-id="{{statId}}" data-skill-id="{{skillId}}"
|
||||
@@ -37,16 +32,12 @@
|
||||
<span class="skill-name">{{localize skill.label}}</span>
|
||||
<div class="skill-checkboxes-container">
|
||||
<div class="skill-checkboxes">
|
||||
{{#each (array 1 2 3 4 5 6 7 8) as |level|}}
|
||||
<label class="skill-checkbox-wrapper">
|
||||
<input type="checkbox"
|
||||
{{#if (lookup (lookup (lookup @root.system.stats statId) skillId) (concat 'level' level))}}checked{{/if}}
|
||||
disabled class="skill-level-checkbox">
|
||||
</label>
|
||||
{{#each (range 8) as |lvl|}}
|
||||
<span class="skill-level-dot {{#if (lte lvl (lookup @root.system.stats statId skillId 'value'))}}filled{{/if}}"></span>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
<span class="skill-value">{{lookup (lookup @root.system.stats statId) skillId 'value'}}</span>
|
||||
<span class="skill-value">{{lookup @root.system.stats statId skillId 'value'}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
<div class="item-controls">
|
||||
{{#unless ../isEditMode}}
|
||||
<a data-action="attack" data-item-id="{{item.id}}" title="{{localize 'CELESTOPOL.Combat.attack'}}"><i class="fas fa-khanda"></i></a>
|
||||
{{#if (eq item.system.type "distance")}}
|
||||
<a data-action="rangedDefense" data-item-id="{{item.id}}" title="{{localize 'CELESTOPOL.Combat.rangedDefenseTitle'}}"><i class="fas fa-shield-halved"></i></a>
|
||||
{{/if}}
|
||||
{{/unless}}
|
||||
<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}}
|
||||
|
||||
@@ -13,32 +13,12 @@
|
||||
<td class="faction-value">
|
||||
<div class="faction-checkboxes-container">
|
||||
<div class="faction-checkboxes">
|
||||
{{#each (array 1 2 3 4 5 6 7 8 9) as |level|}}
|
||||
{{#if @root.isEditMode}}
|
||||
<label class="faction-checkbox-wrapper">
|
||||
<input type="checkbox" name="system.factions.{{factionId}}.level{{level}}"
|
||||
{{#if (lookup (lookup @root.system.factions factionId) (concat 'level' level))}}checked{{/if}}
|
||||
class="faction-checkbox"
|
||||
data-faction="{{factionId}}"
|
||||
data-level="{{level}}">
|
||||
</label>
|
||||
{{else}}
|
||||
<label class="faction-checkbox-wrapper">
|
||||
<input type="checkbox"
|
||||
{{#if (lookup (lookup @root.system.factions factionId) (concat 'level' level))}}checked{{/if}}
|
||||
disabled class="faction-checkbox">
|
||||
</label>
|
||||
{{/if}}
|
||||
{{#each (range 9) as |level|}}
|
||||
<span class="faction-dot {{#if (lte level (lookup @root.system.factions factionId 'value'))}}filled{{/if}}"
|
||||
{{#if @root.isEditable}}data-action="factionLevel" data-faction="{{factionId}}" data-index="{{level}}"{{/if}}></span>
|
||||
{{/each}}
|
||||
</div>
|
||||
<span class="faction-count">
|
||||
{{#if ../isEditMode}}
|
||||
<input type="number" name="system.factions.{{factionId}}.value"
|
||||
value="{{lookup (lookup ../system.factions factionId) 'value'}}" min="0" max="9" class="faction-value-input">
|
||||
{{else}}
|
||||
{{lookup (lookup ../system.factions factionId) 'value'}}
|
||||
{{/if}}
|
||||
</span>
|
||||
<span class="faction-count">{{lookup @root.system.factions factionId 'value'}}</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -58,32 +38,12 @@
|
||||
<td>
|
||||
<div class="faction-checkboxes-container">
|
||||
<div class="faction-checkboxes">
|
||||
{{#each (array 1 2 3 4 5 6 7 8 9) as |level|}}
|
||||
{{#if ../isEditMode}}
|
||||
<label class="faction-checkbox-wrapper">
|
||||
<input type="checkbox" name="system.factions.perso1.level{{level}}"
|
||||
{{#if (lookup ../system.factions.perso1 (concat 'level' level))}}checked{{/if}}
|
||||
class="faction-checkbox"
|
||||
data-faction="perso1"
|
||||
data-level="{{level}}">
|
||||
</label>
|
||||
{{else}}
|
||||
<label class="faction-checkbox-wrapper">
|
||||
<input type="checkbox"
|
||||
{{#if (lookup ../system.factions.perso1 (concat 'level' level))}}checked{{/if}}
|
||||
disabled class="faction-checkbox">
|
||||
</label>
|
||||
{{/if}}
|
||||
{{#each (range 9) as |level|}}
|
||||
<span class="faction-dot {{#if (lte level ../system.factions.perso1.value)}}filled{{/if}}"
|
||||
{{#if ../isEditable}}data-action="factionLevel" data-faction="perso1" data-index="{{level}}"{{/if}}></span>
|
||||
{{/each}}
|
||||
</div>
|
||||
<span class="faction-count">
|
||||
{{#if ../isEditMode}}
|
||||
<input type="number" name="system.factions.perso1.value"
|
||||
value="{{system.factions.perso1.value}}" min="0" max="9" class="faction-value-input">
|
||||
{{else}}
|
||||
{{system.factions.perso1.value}}
|
||||
{{/if}}
|
||||
</span>
|
||||
<span class="faction-count">{{system.factions.perso1.value}}</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -100,32 +60,12 @@
|
||||
<td>
|
||||
<div class="faction-checkboxes-container">
|
||||
<div class="faction-checkboxes">
|
||||
{{#each (array 1 2 3 4 5 6 7 8 9) as |level|}}
|
||||
{{#if ../isEditMode}}
|
||||
<label class="faction-checkbox-wrapper">
|
||||
<input type="checkbox" name="system.factions.perso2.level{{level}}"
|
||||
{{#if (lookup ../system.factions.perso2 (concat 'level' level))}}checked{{/if}}
|
||||
class="faction-checkbox"
|
||||
data-faction="perso2"
|
||||
data-level="{{level}}">
|
||||
</label>
|
||||
{{else}}
|
||||
<label class="faction-checkbox-wrapper">
|
||||
<input type="checkbox"
|
||||
{{#if (lookup ../system.factions.perso2 (concat 'level' level))}}checked{{/if}}
|
||||
disabled class="faction-checkbox">
|
||||
</label>
|
||||
{{/if}}
|
||||
{{#each (range 9) as |level|}}
|
||||
<span class="faction-dot {{#if (lte level ../system.factions.perso2.value)}}filled{{/if}}"
|
||||
{{#if ../isEditable}}data-action="factionLevel" data-faction="perso2" data-index="{{level}}"{{/if}}></span>
|
||||
{{/each}}
|
||||
</div>
|
||||
<span class="faction-count">
|
||||
{{#if ../isEditMode}}
|
||||
<input type="number" name="system.factions.perso2.value"
|
||||
value="{{system.factions.perso2.value}}" min="0" max="9" class="faction-value-input">
|
||||
{{else}}
|
||||
{{system.factions.perso2.value}}
|
||||
{{/if}}
|
||||
</span>
|
||||
<span class="faction-count">{{system.factions.perso2.value}}</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -70,6 +70,18 @@
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/each}}
|
||||
{{#with (lookup woundLevels system.blessures.lvl) as |wound|}}
|
||||
{{#if wound.id}}
|
||||
<div class="header-stat wound-status-badge wound-level-{{wound.id}}">
|
||||
<label>{{localize "CELESTOPOL.Wound.status"}}</label>
|
||||
<span class="wound-value">
|
||||
<span class="wound-label">{{localize wound.label}}</span>
|
||||
{{#if wound.duration}}<span class="wound-duration"> — {{localize wound.duration}}</span>{{/if}}
|
||||
{{#if wound.malus}}<span class="wound-malus"> ({{wound.malus}})</span>{{/if}}
|
||||
</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/with}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -123,7 +123,11 @@
|
||||
<span class="result-icon">✦✦</span>
|
||||
<span class="result-label">{{localize "CELESTOPOL.Roll.criticalSuccess"}}</span>
|
||||
{{#if isCombat}}
|
||||
{{#if isRangedDefense}}
|
||||
<span class="result-desc">{{localize "CELESTOPOL.Combat.rangedDefenseSuccess"}}</span>
|
||||
{{else}}
|
||||
<span class="result-desc">{{localize "CELESTOPOL.Combat.successHit"}}{{#if (gt weaponDegats "0")}} +{{weaponDegats}} {{localize "CELESTOPOL.Combat.weaponDamage"}}{{/if}}</span>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<span class="result-desc">{{localize "CELESTOPOL.Roll.criticalSuccessDesc"}}</span>
|
||||
{{/if}}
|
||||
@@ -131,13 +135,23 @@
|
||||
<span class="result-icon">✦</span>
|
||||
<span class="result-label">{{localize "CELESTOPOL.Roll.success"}}</span>
|
||||
{{#if isCombat}}
|
||||
{{#if isRangedDefense}}
|
||||
<span class="result-desc">{{localize "CELESTOPOL.Combat.rangedDefenseSuccess"}}</span>
|
||||
{{else}}
|
||||
<span class="result-desc">{{localize "CELESTOPOL.Combat.successHit"}}{{#if (gt weaponDegats "0")}} +{{weaponDegats}} {{localize "CELESTOPOL.Combat.weaponDamage"}}{{/if}}</span>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{else if isCriticalFailure}}
|
||||
<span class="result-icon">✖✖</span>
|
||||
<span class="result-label">{{localize "CELESTOPOL.Roll.criticalFailure"}}</span>
|
||||
{{#if isCombat}}
|
||||
<span class="result-desc">{{#if (eq weaponType "melee")}}{{localize "CELESTOPOL.Combat.failureHit"}}{{else}}{{localize "CELESTOPOL.Combat.distanceNoWound"}}{{/if}}</span>
|
||||
{{#if (eq weaponType "melee")}}
|
||||
<span class="result-desc">{{localize "CELESTOPOL.Combat.failureHit"}}</span>
|
||||
{{else if isRangedDefense}}
|
||||
<span class="result-desc">{{localize "CELESTOPOL.Combat.rangedDefenseFailure"}}</span>
|
||||
{{else}}
|
||||
<span class="result-desc">{{localize "CELESTOPOL.Combat.distanceNoWound"}}</span>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<span class="result-desc">{{localize "CELESTOPOL.Roll.criticalFailureDesc"}}</span>
|
||||
{{/if}}
|
||||
@@ -145,7 +159,13 @@
|
||||
<span class="result-icon">✖</span>
|
||||
<span class="result-label">{{localize "CELESTOPOL.Roll.failure"}}</span>
|
||||
{{#if isCombat}}
|
||||
<span class="result-desc">{{#if (eq weaponType "melee")}}{{localize "CELESTOPOL.Combat.failureHit"}}{{else}}{{localize "CELESTOPOL.Combat.distanceNoWound"}}{{/if}}</span>
|
||||
{{#if (eq weaponType "melee")}}
|
||||
<span class="result-desc">{{localize "CELESTOPOL.Combat.failureHit"}}</span>
|
||||
{{else if isRangedDefense}}
|
||||
<span class="result-desc">{{localize "CELESTOPOL.Combat.rangedDefenseFailure"}}</span>
|
||||
{{else}}
|
||||
<span class="result-desc">{{localize "CELESTOPOL.Combat.distanceNoWound"}}</span>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
@@ -154,7 +174,7 @@
|
||||
{{#if woundTaken}}
|
||||
<div class="resistance-wound-notice">
|
||||
<span class="wound-icon">🩹</span>
|
||||
<span>{{#if isCombat}}{{localize "CELESTOPOL.Combat.playerWounded"}}{{else}}{{localize "CELESTOPOL.Roll.woundTaken"}}{{/if}}</span>
|
||||
<span>{{#if isCombat}}{{#if isRangedDefense}}{{localize "CELESTOPOL.Combat.rangedDefensePlayerWounded"}}{{else}}{{localize "CELESTOPOL.Combat.playerWounded"}}{{/if}}{{else}}{{localize "CELESTOPOL.Roll.woundTaken"}}{{/if}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
|
||||
@@ -4,22 +4,16 @@
|
||||
<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>
|
||||
{{#each (range 8) as |lvl|}}
|
||||
<div class="track-box {{#if (lte lvl ../system.blessures.lvl)}}filled{{/if}}"
|
||||
{{#if ../isEditable}}data-action="trackBox" data-path="system.blessures.lvl" data-index="{{lvl}}"{{/if}}>
|
||||
<span class="box-label">{{lookup @root.woundLevels lvl 'malus'}}</span>
|
||||
</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>
|
||||
|
||||
|
||||
@@ -37,6 +37,18 @@
|
||||
<span class="anomaly-type-display">{{localize (lookup (lookup anomalyTypes system.anomaly.type) 'label')}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{#with (lookup woundLevels system.blessures.lvl) as |wound|}}
|
||||
{{#if wound.id}}
|
||||
<div class="header-stat wound-status-badge wound-level-{{wound.id}}">
|
||||
<label>{{localize "CELESTOPOL.Wound.status"}}</label>
|
||||
<span class="wound-value">
|
||||
<span class="wound-label">{{localize wound.label}}</span>
|
||||
{{#if wound.duration}}<span class="wound-duration"> — {{localize wound.duration}}</span>{{/if}}
|
||||
{{#if wound.malus}}<span class="wound-malus"> ({{wound.malus}})</span>{{/if}}
|
||||
</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/with}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-buttons">
|
||||
|
||||
@@ -6,9 +6,13 @@
|
||||
{{!-- Arme (mode combat) --}}
|
||||
{{#if isCombat}}
|
||||
<div class="roll-weapon-line">
|
||||
<span class="weapon-icon">⚔</span>
|
||||
<span class="weapon-icon">{{#if isRangedDefense}}🛡{{else}}⚔{{/if}}</span>
|
||||
<span class="weapon-name">{{weaponName}}</span>
|
||||
{{#if isRangedDefense}}
|
||||
<span class="weapon-tag ranged-defense">{{localize "CELESTOPOL.Combat.rangedDefenseTag"}}</span>
|
||||
{{else}}
|
||||
<span class="weapon-degats">+{{weaponDegats}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="roll-skill-line">
|
||||
|
||||
Reference in New Issue
Block a user