Nouveaux items Arme et Armure (DataModel + feuille + CSS)

Items:
- CelestopolWeapon : degats (0/1/2/X), portee (contact/courte/longue), description
- CelestopolArmure : protection (1-2), malus (0-2), description

Config:
- WEAPON_DAMAGE_TYPES et WEAPON_RANGE_TYPES ajoutés dans system.mjs
- Enregistrement des DataModels, sheets et templates dans fvtt-celestopol.mjs
- system.json : types weapon et armure avec htmlFields

UI:
- weapon.hbs : badge de dégâts avec hint, sélecteurs portée/dégâts
- armure.hbs : blocs protection + malus art-déco
- items.less : styles .weapon et .armure

i18n: clés Weapon.*, Armure.*, Sheet.weapon, Sheet.armure

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-03-29 17:03:23 +02:00
parent be323e6f68
commit 5a8b151451
11 changed files with 321 additions and 14 deletions

View File

@@ -10,6 +10,8 @@ import {
CelestopolAnomaly, CelestopolAnomaly,
CelestopolAspect, CelestopolAspect,
CelestopolEquipment, CelestopolEquipment,
CelestopolWeapon,
CelestopolArmure,
} from "./module/models/_module.mjs" } from "./module/models/_module.mjs"
import { import {
CelestopolActor, CelestopolActor,
@@ -23,6 +25,8 @@ import {
CelestopolAnomalySheet, CelestopolAnomalySheet,
CelestopolAspectSheet, CelestopolAspectSheet,
CelestopolEquipmentSheet, CelestopolEquipmentSheet,
CelestopolWeaponSheet,
CelestopolArmureSheet,
} from "./module/applications/_module.mjs" } from "./module/applications/_module.mjs"
/* ─── Init hook ──────────────────────────────────────────────────────────── */ /* ─── Init hook ──────────────────────────────────────────────────────────── */
@@ -41,6 +45,8 @@ Hooks.once("init", () => {
CONFIG.Item.dataModels.anomaly = CelestopolAnomaly CONFIG.Item.dataModels.anomaly = CelestopolAnomaly
CONFIG.Item.dataModels.aspect = CelestopolAspect CONFIG.Item.dataModels.aspect = CelestopolAspect
CONFIG.Item.dataModels.equipment = CelestopolEquipment CONFIG.Item.dataModels.equipment = CelestopolEquipment
CONFIG.Item.dataModels.weapon = CelestopolWeapon
CONFIG.Item.dataModels.armure = CelestopolArmure
// ── Document classes ──────────────────────────────────────────────────── // ── Document classes ────────────────────────────────────────────────────
CONFIG.Actor.documentClass = CelestopolActor CONFIG.Actor.documentClass = CelestopolActor
@@ -90,6 +96,16 @@ Hooks.once("init", () => {
makeDefault: true, makeDefault: true,
label: "CELESTOPOL.Sheet.equipment", label: "CELESTOPOL.Sheet.equipment",
}) })
foundry.documents.collections.Items.registerSheet(SYSTEM_ID, CelestopolWeaponSheet, {
types: ["weapon"],
makeDefault: true,
label: "CELESTOPOL.Sheet.weapon",
})
foundry.documents.collections.Items.registerSheet(SYSTEM_ID, CelestopolArmureSheet, {
types: ["armure"],
makeDefault: true,
label: "CELESTOPOL.Sheet.armure",
})
// ── Handlebars helpers ─────────────────────────────────────────────────── // ── Handlebars helpers ───────────────────────────────────────────────────
_registerHandlebarsHelpers() _registerHandlebarsHelpers()
@@ -197,6 +213,8 @@ function _preloadTemplates() {
`${base}/anomaly.hbs`, `${base}/anomaly.hbs`,
`${base}/aspect.hbs`, `${base}/aspect.hbs`,
`${base}/equipment.hbs`, `${base}/equipment.hbs`,
`${base}/weapon.hbs`,
`${base}/armure.hbs`,
`${base}/roll-dialog.hbs`, `${base}/roll-dialog.hbs`,
`${base}/chat-message.hbs`, `${base}/chat-message.hbs`,
`${base}/partials/item-scores.hbs`, `${base}/partials/item-scores.hbs`,

View File

@@ -184,7 +184,11 @@
"protection": "Protection", "protection": "Protection",
"speed": "Vitesse", "speed": "Vitesse",
"crew": "Équipage", "crew": "Équipage",
"weight": "Poids" "weight": "Poids",
"weapons": "Armes",
"armures": "Armures",
"newWeapon": "Nouvelle arme",
"newArmure": "Nouvelle armure"
}, },
"Equipment": { "Equipment": {
"autre": "Autre", "autre": "Autre",
@@ -195,7 +199,9 @@
}, },
"Sheet": { "Sheet": {
"editMode": "Mode édition", "editMode": "Mode édition",
"playMode": "Mode jeu" "playMode": "Mode jeu",
"weapon": "Fiche Arme",
"armure": "Fiche Armure"
}, },
"Setting": { "Setting": {
"autoWounds": { "autoWounds": {
@@ -213,6 +219,27 @@
}, },
"ChatCard": { "ChatCard": {
"rollFor": "Jet de {skill} ({stat})" "rollFor": "Jet de {skill} ({stat})"
},
"Weapon": {
"degats": "Dégâts",
"degats0": "Dégâts 0",
"degats0Hint": "Mains nues, arme improvisée, matraque, rasoir, arc, couteau",
"degats1": "Dégâts 1",
"degats1Hint": "Arbalète, épée, hachette, masse, rapière, fléau, hache, hallebarde",
"degats2": "Dégâts 2",
"degats2Hint": "Armes à feu",
"degatsX": "Dégâts X",
"degatsXHint": "Explosifs, sélénium, etc. (à l'appréciation du narrateur)",
"portee": "Portée",
"rangeContact": "Contact",
"rangeCourte": "Courte portée",
"rangeLongue": "Longue portée"
},
"Armure": {
"protection": "Protection",
"protectionHint": "Réduit les blessures subies de ce montant",
"malus": "Malus",
"malusHint": "Malus aux tests de Mobilité et Discrétion (ou Domaine Corps pour PNJ)"
} }
} }
} }

View File

@@ -1,3 +1,3 @@
export { default as CelestopolCharacterSheet } from "./sheets/character-sheet.mjs" export { default as CelestopolCharacterSheet } from "./sheets/character-sheet.mjs"
export { default as CelestopolNPCSheet } from "./sheets/npc-sheet.mjs" export { default as CelestopolNPCSheet } from "./sheets/npc-sheet.mjs"
export { CelestopolAnomalySheet, CelestopolAspectSheet, CelestopolEquipmentSheet } from "./sheets/item-sheets.mjs" export { CelestopolAnomalySheet, CelestopolAspectSheet, CelestopolEquipmentSheet, CelestopolWeaponSheet, CelestopolArmureSheet } from "./sheets/item-sheets.mjs"

View File

@@ -69,3 +69,37 @@ export class CelestopolEquipmentSheet extends CelestopolItemSheet {
return ctx return ctx
} }
} }
export class CelestopolWeaponSheet extends CelestopolItemSheet {
static DEFAULT_OPTIONS = {
classes: ["weapon"],
position: { width: 480, height: 460 },
}
static PARTS = {
main: { template: "systems/fvtt-celestopol/templates/weapon.hbs" },
}
async _prepareContext() {
const ctx = await super._prepareContext()
ctx.damageTypes = SYSTEM.WEAPON_DAMAGE_TYPES
ctx.rangeTypes = SYSTEM.WEAPON_RANGE_TYPES
ctx.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
this.document.system.description, { async: true })
return ctx
}
}
export class CelestopolArmureSheet extends CelestopolItemSheet {
static DEFAULT_OPTIONS = {
classes: ["armure"],
position: { width: 440, height: 380 },
}
static PARTS = {
main: { template: "systems/fvtt-celestopol/templates/armure.hbs" },
}
async _prepareContext() {
const ctx = await super._prepareContext()
ctx.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
this.document.system.description, { async: true })
return ctx
}
}

View File

@@ -140,6 +140,21 @@ export const EQUIPMENT_TYPES = {
vehicule: { id: "vehicule", label: "CELESTOPOL.Equipment.vehicule" }, vehicule: { id: "vehicule", label: "CELESTOPOL.Equipment.vehicule" },
} }
/** Niveaux de dégâts des armes (règles p. ?). */
export const WEAPON_DAMAGE_TYPES = {
"0": { id: "0", label: "CELESTOPOL.Weapon.degats0", hint: "CELESTOPOL.Weapon.degats0Hint" },
"1": { id: "1", label: "CELESTOPOL.Weapon.degats1", hint: "CELESTOPOL.Weapon.degats1Hint" },
"2": { id: "2", label: "CELESTOPOL.Weapon.degats2", hint: "CELESTOPOL.Weapon.degats2Hint" },
"X": { id: "X", label: "CELESTOPOL.Weapon.degatsX", hint: "CELESTOPOL.Weapon.degatsXHint" },
}
/** Portées des armes. */
export const WEAPON_RANGE_TYPES = {
contact: { id: "contact", label: "CELESTOPOL.Weapon.rangeContact" },
courte: { id: "courte", label: "CELESTOPOL.Weapon.rangeCourte" },
longue: { id: "longue", label: "CELESTOPOL.Weapon.rangeLongue" },
}
export const SYSTEM = { export const SYSTEM = {
id: SYSTEM_ID, id: SYSTEM_ID,
ASCII, ASCII,
@@ -154,4 +169,6 @@ export const SYSTEM = {
MOON_DIE_FACES, MOON_DIE_FACES,
MOON_RESULT_TYPES, MOON_RESULT_TYPES,
EQUIPMENT_TYPES, EQUIPMENT_TYPES,
WEAPON_DAMAGE_TYPES,
WEAPON_RANGE_TYPES,
} }

View File

@@ -1,3 +1,3 @@
export { default as CelestopolCharacter } from "./character.mjs" export { default as CelestopolCharacter } from "./character.mjs"
export { default as CelestopolNPC } from "./npc.mjs" export { default as CelestopolNPC } from "./npc.mjs"
export { CelestopolAnomaly, CelestopolAspect, CelestopolEquipment } from "./items.mjs" export { CelestopolAnomaly, CelestopolAspect, CelestopolEquipment, CelestopolWeapon, CelestopolArmure } from "./items.mjs"

View File

@@ -76,3 +76,29 @@ export class CelestopolEquipment extends foundry.abstract.TypeDataModel {
} }
} }
} }
export class CelestopolWeapon extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields
const reqInt = { required: true, nullable: false, integer: true }
return {
degats: new fields.StringField({ required: true, nullable: false, initial: "0",
choices: Object.keys(SYSTEM.WEAPON_DAMAGE_TYPES) }),
portee: new fields.StringField({ required: true, nullable: false, initial: "contact",
choices: Object.keys(SYSTEM.WEAPON_RANGE_TYPES) }),
description: new fields.HTMLField({ required: true, textSearch: true }),
}
}
}
export class CelestopolArmure extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields
const reqInt = { required: true, nullable: false, integer: true }
return {
protection: new fields.NumberField({ ...reqInt, initial: 1, min: 1, max: 2 }),
malus: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 2 }),
description: new fields.HTMLField({ required: true, textSearch: true }),
}
}
}

View File

@@ -333,4 +333,61 @@
margin-bottom: 8px; margin-bottom: 8px;
} }
} }
// Weapon-specific
&.weapon {
.weapon-meta {
display: flex;
gap: 10px;
flex-wrap: wrap;
.form-group {
display: flex;
align-items: center;
gap: 4px;
label { font-size: 0.75em; color: var(--cel-orange-light); white-space: nowrap; }
select { background: rgba(0,0,0,0.3); border: 1px solid var(--cel-orange); color: var(--cel-orange); font-family: var(--cel-font-title); border-radius: 3px; padding: 2px 4px; font-size: 0.85em; }
}
}
.weapon-damage-badge {
display: flex;
align-items: center;
gap: 10px;
background: var(--cel-green-dark);
border: 1px solid var(--cel-orange);
border-radius: 6px;
padding: 6px 12px;
margin: 8px 0;
.damage-label { font-size: 0.72em; text-transform: uppercase; color: var(--cel-orange-light); letter-spacing: 0.05em; }
.damage-value { font-family: var(--cel-font-title); font-size: 1.6em; font-weight: bold; color: var(--cel-orange); min-width: 28px; text-align: center; }
.damage-hint { font-size: 0.78em; color: var(--cel-cream); font-style: italic; }
}
}
// Armure-specific
&.armure {
.armure-stats {
display: flex;
gap: 14px;
justify-content: center;
margin: 12px 0;
}
.armure-stat-box {
display: flex;
flex-direction: column;
align-items: center;
background: var(--cel-green-dark);
border: 1px solid var(--cel-orange);
border-radius: 6px;
padding: 8px 20px;
min-width: 110px;
label { font-size: 0.72em; text-transform: uppercase; color: var(--cel-orange-light); letter-spacing: 0.05em; }
.armure-stat-value {
input, span {
font-family: var(--cel-font-title); font-size: 1.8em; font-weight: bold; color: var(--cel-orange);
text-align: center; background: transparent; border: none; width: 40px;
}
}
.armure-stat-hint { font-size: 0.7em; color: var(--cel-cream); font-style: italic; text-align: center; margin-top: 4px; }
}
}
} }

View File

@@ -15,16 +15,29 @@
], ],
"flags": { "flags": {
"hotReload": { "hotReload": {
"extensions": ["css", "html", "hbs", "json"], "extensions": [
"paths": ["css/", "templates/", "lang/fr.json"] "css",
"html",
"hbs",
"json"
],
"paths": [
"css/",
"templates/",
"lang/fr.json"
]
} }
}, },
"compatibility": { "compatibility": {
"minimum": "13", "minimum": "13",
"verified": "13" "verified": "13"
}, },
"esmodules": ["fvtt-celestopol.mjs"], "esmodules": [
"styles": ["css/fvtt-celestopol.css?v=1774698726"], "fvtt-celestopol.mjs"
],
"styles": [
"css/fvtt-celestopol.css?v=1774698726"
],
"languages": [ "languages": [
{ {
"lang": "fr", "lang": "fr",
@@ -35,16 +48,50 @@
"documentTypes": { "documentTypes": {
"Actor": { "Actor": {
"character": { "character": {
"htmlFields": ["description", "notes"] "htmlFields": [
"description",
"notes"
]
}, },
"npc": { "npc": {
"htmlFields": ["description", "notes"] "htmlFields": [
"description",
"notes"
]
} }
}, },
"Item": { "Item": {
"anomaly": { "htmlFields": ["technique", "narratif", "exemples"] }, "anomaly": {
"aspect": { "htmlFields": ["description", "technique", "narratif", "notes"] }, "htmlFields": [
"equipment": { "htmlFields": ["description", "notes"] } "technique",
"narratif",
"exemples"
]
},
"aspect": {
"htmlFields": [
"description",
"technique",
"narratif",
"notes"
]
},
"equipment": {
"htmlFields": [
"description",
"notes"
]
},
"weapon": {
"htmlFields": [
"description"
]
},
"armure": {
"htmlFields": [
"description"
]
}
} }
}, },
"packs": [ "packs": [

39
templates/armure.hbs Normal file
View File

@@ -0,0 +1,39 @@
<div class="item-sheet armure">
<header class="item-header">
<div class="item-portrait" data-action="editImage">
<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>
</header>
<div class="armure-stats">
<div class="armure-stat-box">
<label>{{localize "CELESTOPOL.Armure.protection"}}</label>
<div class="armure-stat-value">
{{#if isEditable}}
<input type="number" name="system.protection" value="{{system.protection}}" min="1" max="2">
{{else}}
<span>{{system.protection}}</span>
{{/if}}
</div>
<div class="armure-stat-hint">{{localize "CELESTOPOL.Armure.protectionHint"}}</div>
</div>
<div class="armure-stat-box">
<label>{{localize "CELESTOPOL.Armure.malus"}}</label>
<div class="armure-stat-value">
{{#if isEditable}}
<input type="number" name="system.malus" value="{{system.malus}}" min="0" max="2">
{{else}}
<span>{{system.malus}}</span>
{{/if}}
</div>
<div class="armure-stat-hint">{{localize "CELESTOPOL.Armure.malusHint"}}</div>
</div>
</div>
<div class="form-group description-group">
{{formInput systemFields.description enriched=enrichedDescription value=system.description name="system.description" toggled=true}}
</div>
</div>

42
templates/weapon.hbs Normal file
View File

@@ -0,0 +1,42 @@
<div class="item-sheet weapon">
<header class="item-header">
<div class="item-portrait" data-action="editImage">
<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 weapon-meta">
<div class="form-group">
<label>{{localize "CELESTOPOL.Weapon.degats"}}</label>
<select name="system.degats" {{#unless isEditable}}disabled{{/unless}}>
{{#each damageTypes as |dtype key|}}
<option value="{{key}}" {{#if (eq key ../system.degats)}}selected{{/if}}>
{{localize dtype.label}}
</option>
{{/each}}
</select>
</div>
<div class="form-group">
<label>{{localize "CELESTOPOL.Weapon.portee"}}</label>
<select name="system.portee" {{#unless isEditable}}disabled{{/unless}}>
{{#each rangeTypes as |rtype key|}}
<option value="{{key}}" {{#if (eq key ../system.portee)}}selected{{/if}}>
{{localize rtype.label}}
</option>
{{/each}}
</select>
</div>
</div>
</div>
</header>
<div class="weapon-damage-badge">
<span class="damage-label">{{localize "CELESTOPOL.Weapon.degats"}}</span>
<span class="damage-value">{{system.degats}}</span>
<span class="damage-hint">{{localize (lookup (lookup damageTypes system.degats) "hint")}}</span>
</div>
<div class="form-group description-group">
{{formInput systemFields.description enriched=enrichedDescription value=system.description name="system.description" toggled=true}}
</div>
</div>