feat: fiche de Communauté (socle)
- acteur community (identité, géographie, histoire, objectifs, taille/effectif) - 4 Domaines x 2 jauges avec états (Crise/Bas/Stable/Opulent), moral, réserves - onglets Identité / Domaines / Ressources, édition ProseMirror (formInput) - header avec fond, totem, niveau, effectif ; icône par défaut - libellé du type via typeLabels (sans écraser TYPES.* du core)
This commit is contained in:
@@ -4,6 +4,7 @@ export { default as VermineCharacterSheetV2 } from "./character-sheet.mjs"
|
||||
export { default as VermineNpcSheetV2 } from "./npc-sheet.mjs"
|
||||
export { default as VermineGroupSheetV2 } from "./group-sheet.mjs"
|
||||
export { default as VermineCreatureSheetV2 } from "./creature-sheet.mjs"
|
||||
export { default as VermineCommunitySheetV2 } from "./community-sheet.mjs"
|
||||
export {
|
||||
VermineItemSheetV2,
|
||||
VermineWeaponSheetV2,
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import VermineBaseActorSheet from "./base-actor-sheet.mjs"
|
||||
|
||||
export default class VermineCommunitySheetV2 extends VermineBaseActorSheet {
|
||||
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["community"],
|
||||
position: { width: 760, height: 640 },
|
||||
window: { contentClasses: ["community-content"] },
|
||||
actions: {
|
||||
addObjective: VermineCommunitySheetV2.#onAddObjective,
|
||||
deleteObjective: VermineCommunitySheetV2.#onDeleteObjective
|
||||
}
|
||||
}
|
||||
|
||||
static PARTS = {
|
||||
main: { template: "systems/vermine2047/templates/actor/appv2/community-main.hbs", scrollable: [""] },
|
||||
tabs: { template: "templates/generic/tab-navigation.hbs" },
|
||||
identity: { template: "systems/vermine2047/templates/actor/appv2/community-identity.hbs", scrollable: [""] },
|
||||
domains: { template: "systems/vermine2047/templates/actor/appv2/community-domains.hbs", scrollable: [""] },
|
||||
resources: { template: "systems/vermine2047/templates/actor/appv2/community-resources.hbs", scrollable: [""] }
|
||||
}
|
||||
|
||||
tabGroups = { sheet: "identity" }
|
||||
|
||||
#getTabs() {
|
||||
const tabs = {
|
||||
identity: { id: "identity", group: "sheet", icon: "fas fa-info-circle", label: "COMMUNITY.tabs.identity" },
|
||||
domains: { id: "domains", group: "sheet", icon: "fas fa-chart-simple", label: "COMMUNITY.tabs.domains" },
|
||||
resources: { id: "resources", group: "sheet", icon: "fas fa-boxes-stacked", label: "COMMUNITY.tabs.resources" }
|
||||
}
|
||||
for (const v of Object.values(tabs)) {
|
||||
v.active = this.tabGroups[v.group] === v.id
|
||||
v.cssClass = v.active ? "active" : ""
|
||||
}
|
||||
return tabs
|
||||
}
|
||||
|
||||
async _prepareContext() {
|
||||
const context = await super._prepareContext()
|
||||
context.tabs = this.#getTabs()
|
||||
// Groupe de personnages rattaché
|
||||
context.resolvedGroup = null
|
||||
if (this.document.system.groupId) {
|
||||
const g = game.actors.get(this.document.system.groupId)
|
||||
if (g) context.resolvedGroup = { id: g.id, name: g.name }
|
||||
}
|
||||
// Enrichissement HTML des champs longs (même logique que la fiche PNJ)
|
||||
const enrich = async (path) => {
|
||||
const val = foundry.utils.getProperty(this.document.system, path)
|
||||
return val ? await foundry.applications.ux.TextEditor.implementation.enrichHTML(val, { async: true }) : ""
|
||||
}
|
||||
context.enrichedNotes = await enrich("identity.notes")
|
||||
context.enrichedLandmarks = await enrich("location.landmarks")
|
||||
context.enrichedTerritory = await enrich("location.territory")
|
||||
context.enrichedEvents = await enrich("history.events")
|
||||
context.enrichedRelations = await enrich("history.relations")
|
||||
return context
|
||||
}
|
||||
|
||||
async _preparePartContext(partId, context) {
|
||||
const doc = this.document
|
||||
switch (partId) {
|
||||
case "main": break
|
||||
case "identity":
|
||||
context.tab = context.tabs.identity
|
||||
context.ageOptions = Object.entries(CONFIG.VERMINE.communityAges).map(([key, label]) => ({ key, label: game.i18n.localize(label) }))
|
||||
break
|
||||
case "domains":
|
||||
context.tab = context.tabs.domains
|
||||
context.sizeOptions = CONFIG.VERMINE.communitySizeLevels
|
||||
context.sizeLabel = doc.system.size?.value ? game.i18n.localize(CONFIG.VERMINE.communitySizeLevels[doc.system.size.value]?.label ?? "") : ""
|
||||
context.sizeInfo = doc.system.size?.value ? CONFIG.VERMINE.communitySizeLevels[doc.system.size.value] : null
|
||||
// Préparer les 4 Domaines avec leurs 2 sections (jauges) et leur état
|
||||
context.domains = []
|
||||
for (const [dkey, dcfg] of Object.entries(CONFIG.VERMINE.communityDomains)) {
|
||||
const domain = doc.system.domains?.[dkey]
|
||||
context.domains.push({
|
||||
key: dkey,
|
||||
label: game.i18n.localize(dcfg.label),
|
||||
sections: Object.entries(dcfg.sections).map(([skey, label]) => {
|
||||
const gauge = domain?.[skey] ?? { value: 0 }
|
||||
const state = gauge.state || "crisis"
|
||||
return {
|
||||
domainKey: dkey,
|
||||
key: skey,
|
||||
label: game.i18n.localize(label),
|
||||
gauge,
|
||||
state,
|
||||
stateLabel: game.i18n.localize(CONFIG.VERMINE.communityGaugeStates[state]?.label ?? "COMMUNITY.states.crisis")
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
break
|
||||
case "resources":
|
||||
context.tab = context.tabs.resources
|
||||
break
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
changeTab(tab, group, options = {}) {
|
||||
super.changeTab(tab, group, options)
|
||||
if (group === "sheet") {
|
||||
const main = this.element?.querySelector('[data-group="sheet"][data-tab="main"]')
|
||||
if (main) main.classList.add("active")
|
||||
}
|
||||
}
|
||||
|
||||
static #onDeleteObjective(event, target) {
|
||||
const index = parseInt(target.dataset.index)
|
||||
if (isNaN(index)) return
|
||||
const objectives = foundry.utils.duplicate(this.document.system.objectives || { major: "", minor: [] })
|
||||
objectives.minor.splice(index, 1)
|
||||
this.document.update({ "system.objectives": objectives })
|
||||
}
|
||||
|
||||
static #onAddObjective(event, target) {
|
||||
const objectives = foundry.utils.duplicate(this.document.system.objectives || { major: "", minor: [] })
|
||||
objectives.minor.push("")
|
||||
this.document.update({ "system.objectives": objectives })
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ export { default as VermineCharacterData } from "./character.mjs"
|
||||
export { default as VermineNpcData } from "./npc.mjs"
|
||||
export { default as VermineGroupData } from "./group.mjs"
|
||||
export { default as VermineCreatureData } from "./creature.mjs"
|
||||
export { default as VermineCommunityData } from "./community.mjs"
|
||||
export { default as VermineItemData } from "./item.mjs"
|
||||
export { default as VermineWeaponData } from "./weapon.mjs"
|
||||
export { default as VermineDefenseData } from "./defense.mjs"
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* DataModel pour les acteurs de type "community" (Communauté).
|
||||
* La Communauté est l'extension sédentarisée d'un Groupe : elle englobe le
|
||||
* Groupe de personnages, des Figures et des Habitants répartis en Compagnies.
|
||||
* Le socle couvre l'identité, la géographie, l'histoire, les objectifs, la
|
||||
* taille/l'effectif, les quatre Domaines (8 jauges), le Moral et les réserves.
|
||||
*/
|
||||
import {
|
||||
attributeSchema,
|
||||
levelSchema,
|
||||
combatStatusSchema
|
||||
} from "./_shared.mjs"
|
||||
|
||||
export default class VermineCommunityData extends foundry.abstract.TypeDataModel {
|
||||
|
||||
/** @override */
|
||||
static LOCALIZATION_PREFIXES = ["VERMINE.community"]
|
||||
|
||||
/** @override */
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields
|
||||
const reqInt = { required: true, nullable: false, integer: true }
|
||||
const gauge = () => attributeSchema(0, 0, 10)
|
||||
|
||||
return {
|
||||
// Statut de combat (base, peu utilisé pour une Communauté)
|
||||
combatStatus: combatStatusSchema("9"),
|
||||
|
||||
// Identité
|
||||
identity: new fields.SchemaField({
|
||||
totem: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
profile: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
origin: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
theme: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
instincts: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
prohibits: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
// Âge de la Communauté : recent | established | old
|
||||
age: new fields.StringField({ required: true, nullable: false, initial: "recent" }),
|
||||
notes: new fields.HTMLField({ required: true, initial: "", textSearch: true })
|
||||
}),
|
||||
|
||||
// Géographie / localisation
|
||||
location: new fields.SchemaField({
|
||||
sector: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
placeType: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
landmarks: new fields.HTMLField({ required: true, initial: "", textSearch: true }),
|
||||
territory: new fields.HTMLField({ required: true, initial: "", textSearch: true })
|
||||
}),
|
||||
|
||||
// Histoire
|
||||
history: new fields.SchemaField({
|
||||
founding: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
events: new fields.HTMLField({ required: true, initial: "", textSearch: true }),
|
||||
relations: new fields.HTMLField({ required: true, initial: "", textSearch: true })
|
||||
}),
|
||||
|
||||
// Objectifs : un majeur + des mineurs
|
||||
objectives: new fields.SchemaField({
|
||||
major: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
minor: new fields.ArrayField(new fields.StringField({ required: true, nullable: false, initial: "" }))
|
||||
}),
|
||||
|
||||
// Taille (1-3) et effectif
|
||||
size: attributeSchema(1, 1, 3),
|
||||
population: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
|
||||
|
||||
// Domaines : 4 Domaines × 2 sections (jauges en Dés)
|
||||
domains: new fields.SchemaField({
|
||||
resources: new fields.SchemaField({
|
||||
vivre: gauge(),
|
||||
supplies: gauge()
|
||||
}),
|
||||
security: new fields.SchemaField({
|
||||
defense: gauge(),
|
||||
care: gauge()
|
||||
}),
|
||||
dynamism: new fields.SchemaField({
|
||||
cohesion: gauge(),
|
||||
productivity: gauge()
|
||||
}),
|
||||
relations: new fields.SchemaField({
|
||||
human: gauge(),
|
||||
ecology: gauge()
|
||||
})
|
||||
}),
|
||||
|
||||
// Moral (comme le Groupe) : value 0-7, level dérivé
|
||||
morale: new fields.SchemaField({
|
||||
level: new fields.StringField({ required: true, nullable: false, initial: "normal" }),
|
||||
value: new fields.NumberField({ ...reqInt, initial: 7, min: 0, max: 7 }),
|
||||
min: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
|
||||
max: new fields.NumberField({ ...reqInt, initial: 7, min: 0 })
|
||||
}),
|
||||
|
||||
// Réserve de Groupe
|
||||
reserve: attributeSchema(0, 0, 10),
|
||||
|
||||
// Dés collectifs de la Communauté
|
||||
material: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
|
||||
experience: new fields.SchemaField({
|
||||
dice: new fields.NumberField({ ...reqInt, initial: 0, min: 0 })
|
||||
}),
|
||||
|
||||
// Niveau de la Communauté (pour l'expérience des habitants)
|
||||
level: levelSchema(1, 1, 10),
|
||||
|
||||
// Groupe de personnages rattaché (id d'acteur)
|
||||
groupId: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
|
||||
// Habitants (phase 2) : Figures et Compagnies
|
||||
figures: new fields.ArrayField(new fields.SchemaField({
|
||||
name: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
role: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
totem: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
specialty: new fields.SchemaField({
|
||||
name: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
score: new fields.NumberField({ ...reqInt, initial: 3, min: 0, max: 10 })
|
||||
})
|
||||
})),
|
||||
companies: new fields.ArrayField(new fields.SchemaField({
|
||||
name: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
specialty: new fields.SchemaField({
|
||||
name: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
score: new fields.NumberField({ ...reqInt, initial: 3, min: 0, max: 10 })
|
||||
}),
|
||||
level: levelSchema(1, 1, 4),
|
||||
members: new fields.NumberField({ ...reqInt, initial: 1, min: 0 }),
|
||||
figureName: new fields.StringField({ required: true, nullable: false, initial: "" })
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/** @override */
|
||||
prepareDerivedData() {
|
||||
super.prepareDerivedData()
|
||||
|
||||
// 1. État (Crise/Bas/Stable/Opulent) de chaque jauge de Domaine
|
||||
this._computeGaugeStates()
|
||||
|
||||
// 2. Les quatre Domaines sont-ils Bas ? (Moral inactif)
|
||||
this._computeAllDomainsLow()
|
||||
|
||||
// 3. Mettre à jour le niveau de Moral selon la valeur
|
||||
this._updateMorale()
|
||||
}
|
||||
|
||||
/**
|
||||
* État d'une jauge selon sa valeur en Dés.
|
||||
* Crise 0D / Bas 1-2D / Stable 3-5D / Opulent 6D+.
|
||||
* @param {number} value
|
||||
* @returns {string}
|
||||
*/
|
||||
static gaugeState(value) {
|
||||
const v = parseInt(value, 10) || 0
|
||||
if (v >= 6) return "opulent"
|
||||
if (v >= 3) return "stable"
|
||||
if (v >= 1) return "low"
|
||||
return "crisis"
|
||||
}
|
||||
|
||||
_computeGaugeStates() {
|
||||
for (const domain of Object.values(this.domains)) {
|
||||
for (const gauge of Object.values(domain)) {
|
||||
gauge.state = VermineCommunityData.gaugeState(gauge.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Un Domaine est "Bas" lorsque ses deux sections sont à 2D ou moins.
|
||||
* Si les quatre Domaines sont Bas, le Bonus de Moral est inactif.
|
||||
*/
|
||||
_computeAllDomainsLow() {
|
||||
const domainLow = (domain) => Object.values(domain).every(g => (parseInt(g.value, 10) || 0) <= 2)
|
||||
this.allDomainsLow = Object.values(this.domains).every(domainLow)
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour le niveau de Moral en fonction de la valeur de dés.
|
||||
* Règles : 7D = Haut, 6-3D = Normal, 2-1D = Bas, 0D = Crise.
|
||||
*/
|
||||
_updateMorale() {
|
||||
const moraleValue = parseInt(this.morale?.value, 10) || 0
|
||||
if (this.morale.level && this.morale.level !== "normal") return
|
||||
if (moraleValue >= 7) {
|
||||
this.morale.level = "high"
|
||||
} else if (moraleValue >= 3) {
|
||||
this.morale.level = "normal"
|
||||
} else if (moraleValue >= 1) {
|
||||
this.morale.level = "low"
|
||||
} else {
|
||||
this.morale.level = "crisis"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -469,3 +469,54 @@ VERMINE.totemEffects = {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Communauté — Domaines et sections (jauges).
|
||||
*/
|
||||
VERMINE.communityDomains = {
|
||||
resources: {
|
||||
label: "COMMUNITY.domains.resources",
|
||||
sections: { vivre: "COMMUNITY.sections.vivre", supplies: "COMMUNITY.sections.supplies" }
|
||||
},
|
||||
security: {
|
||||
label: "COMMUNITY.domains.security",
|
||||
sections: { defense: "COMMUNITY.sections.defense", care: "COMMUNITY.sections.care" }
|
||||
},
|
||||
dynamism: {
|
||||
label: "COMMUNITY.domains.dynamism",
|
||||
sections: { cohesion: "COMMUNITY.sections.cohesion", productivity: "COMMUNITY.sections.productivity" }
|
||||
},
|
||||
relations: {
|
||||
label: "COMMUNITY.domains.relations",
|
||||
sections: { human: "COMMUNITY.sections.human", ecology: "COMMUNITY.sections.ecology" }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Communauté — Tailles (effectif, Figures, Compagnies recommandés).
|
||||
*/
|
||||
VERMINE.communitySizeLevels = {
|
||||
1: { label: "COMMUNITY.sizeLevels.1", population: "7-50", figures: 3, companies: 5 },
|
||||
2: { label: "COMMUNITY.sizeLevels.2", population: "51-75", figures: 5, companies: 7 },
|
||||
3: { label: "COMMUNITY.sizeLevels.3", population: "100+", figures: 7, companies: 9 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Communauté — États d'une jauge de Domaine.
|
||||
* Crise 0D / Bas 1-2D / Stable 3-5D / Opulent 6D+.
|
||||
*/
|
||||
VERMINE.communityGaugeStates = {
|
||||
crisis: { label: "COMMUNITY.states.crisis" },
|
||||
low: { label: "COMMUNITY.states.low" },
|
||||
stable: { label: "COMMUNITY.states.stable" },
|
||||
opulent: { label: "COMMUNITY.states.opulent" }
|
||||
}
|
||||
|
||||
/**
|
||||
* Communauté — Âge (notion de temps).
|
||||
*/
|
||||
VERMINE.communityAges = {
|
||||
recent: "COMMUNITY.age.recent",
|
||||
established: "COMMUNITY.age.established",
|
||||
old: "COMMUNITY.age.old"
|
||||
}
|
||||
|
||||
+16
-1
@@ -37,6 +37,14 @@ Hooks.once('init', async function () {
|
||||
CONFIG.Actor.dataModels.npc = models.VermineNpcData;
|
||||
CONFIG.Actor.dataModels.group = models.VermineGroupData;
|
||||
CONFIG.Actor.dataModels.creature = models.VermineCreatureData;
|
||||
CONFIG.Actor.dataModels.community = models.VermineCommunityData;
|
||||
|
||||
// Libellé du type "community" (TYPES.Actor.* du core ne le connaît pas)
|
||||
CONFIG.Actor.typeLabels ??= {};
|
||||
CONFIG.Actor.typeLabels.community = "VERMINE.type_community";
|
||||
Hooks.once('i18nInit', () => {
|
||||
CONFIG.Actor.typeLabels.community = "VERMINE.type_community";
|
||||
});
|
||||
|
||||
CONFIG.Item.dataModels.item = models.VermineItemData;
|
||||
CONFIG.Item.dataModels.weapon = models.VermineWeaponData;
|
||||
@@ -89,6 +97,9 @@ Hooks.once('init', async function () {
|
||||
foundry.documents.collections.Actors.registerSheet("vermine2047", sheets.VermineGroupSheetV2, {
|
||||
types: ["group"], makeDefault: true, label: "VERMINE.Sheet.group"
|
||||
})
|
||||
foundry.documents.collections.Actors.registerSheet("vermine2047", sheets.VermineCommunitySheetV2, {
|
||||
types: ["community"], makeDefault: true, label: "VERMINE.Sheet.community"
|
||||
})
|
||||
|
||||
// Item sheets — un par type
|
||||
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineItemSheetV2, {
|
||||
@@ -193,7 +204,11 @@ Hooks.once('init', async function () {
|
||||
"systems/vermine2047/templates/actor/appv2/creature-info.hbs",
|
||||
"systems/vermine2047/templates/actor/appv2/creature-stats.hbs",
|
||||
"systems/vermine2047/templates/actor/appv2/creature-combat.hbs",
|
||||
"systems/vermine2047/templates/actor/appv2/creature-effects.hbs"
|
||||
"systems/vermine2047/templates/actor/appv2/creature-effects.hbs",
|
||||
"systems/vermine2047/templates/actor/appv2/community-main.hbs",
|
||||
"systems/vermine2047/templates/actor/appv2/community-identity.hbs",
|
||||
"systems/vermine2047/templates/actor/appv2/community-domains.hbs",
|
||||
"systems/vermine2047/templates/actor/appv2/community-resources.hbs"
|
||||
]);
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user