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:
2026-08-04 17:20:29 +02:00
parent 8d8428cad3
commit 6247f65590
17 changed files with 1080 additions and 7 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

+102
View File
@@ -174,6 +174,15 @@
"range": "Range", "range": "Range",
"ranges": "Ranges", "ranges": "Ranges",
"mobility": "Mobility", "mobility": "Mobility",
"vehicle_specs": "Vehicle specifications",
"kind": "Kind",
"kind_vehicle": "Vehicle",
"kind_mount": "Mount",
"distance": "Distance",
"speed": "Speed",
"speed_duration_ph": "top speed duration (e.g. 10 min)",
"effort_reserve": "Effort reserve",
"maintenance": "Maintenance",
"rarity": "Rarity", "rarity": "Rarity",
"rarity_sm": "Rar.", "rarity_sm": "Rar.",
"reliability": "Reliability", "reliability": "Reliability",
@@ -204,6 +213,7 @@
"type_character": "Character", "type_character": "Character",
"type_npc": "NPC", "type_npc": "NPC",
"type_creature": "Creature", "type_creature": "Creature",
"type_community": "Community",
"type": "Type", "type": "Type",
"group_members": "Members", "group_members": "Members",
"encounters": "Encounters", "encounters": "Encounters",
@@ -379,6 +389,13 @@
"totem": "Totem", "totem": "Totem",
"experience": "Experience" "experience": "Experience"
}, },
"Sheet": {
"character": "Character",
"npc": "NPC",
"group": "Group",
"creature": "Creature",
"community": "Community"
},
"base_values": "Base Values", "base_values": "Base Values",
"computed_values": "Computed Values", "computed_values": "Computed Values",
"cost": "Cost", "cost": "Cost",
@@ -441,6 +458,91 @@
"effects.passive": "Passive", "effects.passive": "Passive",
"effects.temporary": "Temporary" "effects.temporary": "Temporary"
}, },
"COMMUNITY": {
"tabs": {
"identity": "Identity",
"domains": "Domains",
"resources": "Resources"
},
"identity": "Identity",
"name_placeholder": "Community name",
"choose_totem": "Choose totem",
"origin": "Origin",
"theme": "Theme",
"age_label": "Community age",
"age": {
"recent": "A few days",
"established": "A few months",
"old": "A few years"
},
"instincts": "Instincts",
"prohibits": "Prohibitions",
"notes": "Notes",
"location": "Location",
"sector": "Sector",
"placeType": "Place type",
"landmarks": "Landmarks",
"territory": "Territory",
"history": "History",
"founding": "Founding",
"events": "Key events",
"history_relations": "Relations",
"objectives": "Objectives",
"objective_major": "Major objective",
"objectives_minor": "Minor objectives",
"add_objective": "Add objective",
"size_title": "Size & Population",
"size_label": "Size",
"sizeLevels": {
"1": "Size 1",
"2": "Size 2",
"3": "Size 3"
},
"population": "Population",
"population_range": "Recommended population",
"figures": "Figures",
"companies": "Companies",
"domains": {
"resources": "Resources",
"security": "Security",
"dynamism": "Dynamism",
"relations": "Relations"
},
"sections": {
"vivre": "Food & Water",
"supplies": "Supplies",
"defense": "Defense",
"care": "Care",
"cohesion": "Cohesion",
"productivity": "Productivity",
"human": "Human",
"ecology": "Ecology"
},
"states": {
"crisis": "Crisis",
"low": "Low",
"stable": "Stable",
"opulent": "Opulent"
},
"reserves": "Reserves",
"group_reserve": "Group reserve",
"material_dice": "Material dice",
"experience_dice": "Experience dice",
"morale": "Morale",
"morale_value": "Value",
"morale_state": "State",
"morale_levels": {
"high": "High",
"normal": "Normal",
"low": "Low",
"crisis": "Crisis"
},
"morale_blocked": "All four Domains are Low: the Morale bonus is inactive.",
"habitants_title": "Inhabitants",
"habitants_coming": "Figures and Companies management comes in a future version.",
"linked_group": "Linked group",
"profile_hint": "Community profile"
},
"ITEMS": { "ITEMS": {
"item": "Item", "item": "Item",
"items": "Items", "items": "Items",
+97 -1
View File
@@ -255,6 +255,15 @@
"range": "Portée", "range": "Portée",
"ranges": "Portées", "ranges": "Portées",
"mobility": "Mobilité", "mobility": "Mobilité",
"vehicle_specs": "Caractéristiques du véhicule",
"kind": "Type",
"kind_vehicle": "Véhicule",
"kind_mount": "Monture",
"distance": "Distance",
"speed": "Vitesse",
"speed_duration_ph": "durée de pointe (ex: 10 min)",
"effort_reserve": "Réserve d'Effort",
"maintenance": "Entretien",
"rarity": "Rareté", "rarity": "Rareté",
"rarity_sm": "Rar.", "rarity_sm": "Rar.",
"reliability": "Fiabilité", "reliability": "Fiabilité",
@@ -395,6 +404,7 @@
"type_character": "Personnage", "type_character": "Personnage",
"type_npc": "PNJ", "type_npc": "PNJ",
"type_creature": "Créature", "type_creature": "Créature",
"type_community": "Communauté",
"type": "Type", "type": "Type",
"group_members": "Membres", "group_members": "Membres",
"encounters": "Rencontres", "encounters": "Rencontres",
@@ -440,7 +450,8 @@
"character": "Personnage", "character": "Personnage",
"npc": "PNJ", "npc": "PNJ",
"group": "Groupe", "group": "Groupe",
"creature": "Créature" "creature": "Créature",
"community": "Communauté"
} }
}, },
"UI": { "UI": {
@@ -458,6 +469,91 @@
"inactive": "Effets inactifs" "inactive": "Effets inactifs"
} }
}, },
"COMMUNITY": {
"tabs": {
"identity": "Identité",
"domains": "Domaines",
"resources": "Ressources"
},
"identity": "Identité",
"name_placeholder": "Nom de la Communauté",
"choose_totem": "Choisir le totem",
"origin": "Origine",
"theme": "Thème",
"age_label": "Âge de la Communauté",
"age": {
"recent": "Quelques jours",
"established": "Quelques mois",
"old": "Quelques années"
},
"instincts": "Instincts",
"prohibits": "Interdits",
"notes": "Notes",
"location": "Localisation",
"sector": "Secteur",
"placeType": "Type de lieu",
"landmarks": "Éléments marquants",
"territory": "Territoire",
"history": "Histoire",
"founding": "Fondation",
"events": "Événements marquants",
"history_relations": "Relations",
"objectives": "Objectifs",
"objective_major": "Objectif majeur",
"objectives_minor": "Objectifs mineurs",
"add_objective": "Ajouter un objectif",
"size_title": "Taille & Effectif",
"size_label": "Taille",
"sizeLevels": {
"1": "Taille 1",
"2": "Taille 2",
"3": "Taille 3"
},
"population": "Effectif",
"population_range": "Population recommandée",
"figures": "Figures",
"companies": "Compagnies",
"domains": {
"resources": "Ressources",
"security": "Sécurité",
"dynamism": "Dynamisme",
"relations": "Rapports"
},
"sections": {
"vivre": "Vivres",
"supplies": "Fournitures",
"defense": "Défense",
"care": "Soins",
"cohesion": "Cohésion",
"productivity": "Productivité",
"human": "Humain",
"ecology": "Écologie"
},
"states": {
"crisis": "Crise",
"low": "Bas",
"stable": "Stable",
"opulent": "Opulent"
},
"reserves": "Réserves",
"group_reserve": "Réserve de Groupe",
"material_dice": "Dés de Matériel",
"experience_dice": "Dés d'Expérience",
"morale": "Moral",
"morale_value": "Valeur",
"morale_state": "État",
"morale_levels": {
"high": "Haut",
"normal": "Normal",
"low": "Bas",
"crisis": "Crise"
},
"morale_blocked": "Les quatre Domaines sont Bas : le Bonus de Moral est inactif.",
"habitants_title": "Habitants",
"habitants_coming": "La gestion des Figures et des Compagnies arrive dans une prochaine version.",
"linked_group": "Groupe rattaché",
"profile_hint": "Profil de la Communauté"
},
"ITEMS": { "ITEMS": {
"item": "Objet", "item": "Objet",
"items": "Objets", "items": "Objets",
+152
View File
@@ -0,0 +1,152 @@
@import "../variables";
@import "../utilities";
.system-vermine2047 .vermine2047.actor.community {
.char-header {
.background-image(url("@{ui-path}/barre_haut.webp"), no-repeat, 100% 100%, right top);
max-height: 130px;
.community-portrait {
display: flex;
align-items: center;
justify-content: center;
}
.char-name,
.char-vermine2047 {
border-bottom: none;
line-height: 2rem;
}
.char-vermine2047 {
font-size: 1.1rem;
}
.profile,
.origin,
.level,
.population {
width: 100%;
}
input[type="text"] {
flex: 1;
min-width: 0;
}
.level input,
.population input {
width: 60px;
text-align: center;
}
}
.community-section {
padding: 8px;
border: 1px solid @color-border-dark-4;
background: fade(@color-chitin-dark, 20%);
h3 {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-13;
color: @theme-color-dark;
margin: 0 0 8px;
text-align: left;
border-bottom: none;
}
}
.community-linked-group {
display: flex;
gap: 6px;
margin-bottom: 8px;
padding: 6px 8px;
border: 1px dashed @color-hemolymph;
background: fade(@color-hemolymph, 8%);
.label {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-11;
color: @color-amber;
}
}
.community-size-info {
margin: 6px 0 0;
font-size: @font-size-11;
color: @color-text-light-0;
opacity: 0.85;
}
.community-warning {
margin: 6px 0 0;
padding: 4px 8px;
font-size: @font-size-11;
font-weight: 700;
color: @color-hemolymph;
background: fade(@color-hemolymph, 12%);
border: 1px solid @color-hemolymph;
}
.community-hint {
margin: 0;
font-size: @font-size-11;
font-style: italic;
color: @color-text-light-0;
opacity: 0.7;
}
// Jauges de Domaines
.gauge {
display: flex;
flex-direction: column;
gap: 2px;
padding: 6px;
border: 1px solid @color-border-dark-4;
border-left: 4px solid @color-border-dark-4;
label {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-11;
color: @color-amber;
}
input[type="number"] {
width: 48px;
text-align: center;
padding: 2px;
}
.gauge-value {
font-family: "DistressBlack", sans-serif;
font-size: @font-size-16;
color: @color-text-light-0;
}
.gauge-state {
font-family: "Roboto", sans-serif;
font-size: @font-size-11;
font-weight: 700;
text-transform: uppercase;
}
&.crisis { border-left-color: @color-hemolymph; .gauge-state { color: @color-hemolymph; } }
&.low { border-left-color: @color-amber; .gauge-state { color: @color-amber; } }
&.stable { border-left-color: @color-acid-green; .gauge-state { color: @color-acid-green; } }
&.opulent { border-left-color: @theme-color-accent; .gauge-state { color: @theme-color-accent; } }
}
.morale-level {
font-family: "Roboto", sans-serif;
font-size: @font-size-12;
font-weight: 700;
&.high { color: @color-acid-green; }
&.normal { color: @color-text-light-0; }
&.low { color: @color-amber; }
&.crisis { color: @color-hemolymph; }
}
}
+6 -4
View File
@@ -126,7 +126,8 @@ ul.unstyled {
// La fiche de groupe contient beaucoup de zones (membres, objectifs, totem…) : // La fiche de groupe contient beaucoup de zones (membres, objectifs, totem…) :
// dès que le contenu dépasse la hauteur de la fenêtre, on autorise le défilement // dès que le contenu dépasse la hauteur de la fenêtre, on autorise le défilement
// pour ne pas perdre l'accès aux zones du bas. // pour ne pas perdre l'accès aux zones du bas.
.application.sheet.vermine2047.group .window-content { .application.sheet.vermine2047.group .window-content,
.application.sheet.vermine2047.community .window-content {
overflow-y: auto; overflow-y: auto;
} }
@@ -1027,11 +1028,12 @@ ol.chat-log {
} }
} }
// Padding supplémentaire pour les fiches groupe, npc et créature // Padding supplémentaire pour les fiches groupe, npc, créature et communauté
.system-vermine2047 { .system-vermine2047 {
.group-content, .group-content,
.npc-content, .npc-content,
.creature-content { .creature-content,
.community-content {
padding: 15px; padding: 15px;
} }
} }
@@ -1119,7 +1121,7 @@ ol.chat-log {
label { label {
font-family: "DistressBlack", sans-serif; font-family: "DistressBlack", sans-serif;
text-transform: uppercase; text-transform: uppercase;
font-size: @font-size-10; font-size: @font-size-11;
color: @color-amber; color: @color-amber;
} }
} }
+1
View File
@@ -22,6 +22,7 @@
@import "actor/npc"; @import "actor/npc";
@import "actor/group"; @import "actor/group";
@import "actor/creature"; @import "actor/creature";
@import "actor/community";
// 6. Styles des items // 6. Styles des items
@import "items"; @import "items";
+1
View File
@@ -4,6 +4,7 @@ export { default as VermineCharacterSheetV2 } from "./character-sheet.mjs"
export { default as VermineNpcSheetV2 } from "./npc-sheet.mjs" export { default as VermineNpcSheetV2 } from "./npc-sheet.mjs"
export { default as VermineGroupSheetV2 } from "./group-sheet.mjs" export { default as VermineGroupSheetV2 } from "./group-sheet.mjs"
export { default as VermineCreatureSheetV2 } from "./creature-sheet.mjs" export { default as VermineCreatureSheetV2 } from "./creature-sheet.mjs"
export { default as VermineCommunitySheetV2 } from "./community-sheet.mjs"
export { export {
VermineItemSheetV2, VermineItemSheetV2,
VermineWeaponSheetV2, 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 })
}
}
+1
View File
@@ -2,6 +2,7 @@ export { default as VermineCharacterData } from "./character.mjs"
export { default as VermineNpcData } from "./npc.mjs" export { default as VermineNpcData } from "./npc.mjs"
export { default as VermineGroupData } from "./group.mjs" export { default as VermineGroupData } from "./group.mjs"
export { default as VermineCreatureData } from "./creature.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 VermineItemData } from "./item.mjs"
export { default as VermineWeaponData } from "./weapon.mjs" export { default as VermineWeaponData } from "./weapon.mjs"
export { default as VermineDefenseData } from "./defense.mjs" export { default as VermineDefenseData } from "./defense.mjs"
+195
View File
@@ -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"
}
}
}
+51
View File
@@ -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
View File
@@ -37,6 +37,14 @@ Hooks.once('init', async function () {
CONFIG.Actor.dataModels.npc = models.VermineNpcData; CONFIG.Actor.dataModels.npc = models.VermineNpcData;
CONFIG.Actor.dataModels.group = models.VermineGroupData; CONFIG.Actor.dataModels.group = models.VermineGroupData;
CONFIG.Actor.dataModels.creature = models.VermineCreatureData; 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.item = models.VermineItemData;
CONFIG.Item.dataModels.weapon = models.VermineWeaponData; CONFIG.Item.dataModels.weapon = models.VermineWeaponData;
@@ -89,6 +97,9 @@ Hooks.once('init', async function () {
foundry.documents.collections.Actors.registerSheet("vermine2047", sheets.VermineGroupSheetV2, { foundry.documents.collections.Actors.registerSheet("vermine2047", sheets.VermineGroupSheetV2, {
types: ["group"], makeDefault: true, label: "VERMINE.Sheet.group" 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 // Item sheets — un par type
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineItemSheetV2, { 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-info.hbs",
"systems/vermine2047/templates/actor/appv2/creature-stats.hbs", "systems/vermine2047/templates/actor/appv2/creature-stats.hbs",
"systems/vermine2047/templates/actor/appv2/creature-combat.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"
]); ]);
}); });
+2 -1
View File
@@ -34,7 +34,8 @@
"character": { "htmlFields": ["equipment.description", "identity.biography", "identity.relations"] }, "character": { "htmlFields": ["equipment.description", "identity.biography", "identity.relations"] },
"npc": { "htmlFields": ["equipment.description", "identity.notes"] }, "npc": { "htmlFields": ["equipment.description", "identity.notes"] },
"group": { "htmlFields": ["equipment.description", "identity.notes"] }, "group": { "htmlFields": ["equipment.description", "identity.notes"] },
"creature": { "htmlFields": ["equipment.description", "identity.notes", "identity.biography"] } "creature": { "htmlFields": ["equipment.description", "identity.notes", "identity.biography"] },
"community": { "htmlFields": ["identity.notes", "location.landmarks", "location.territory", "history.events", "history.relations"] }
}, },
"Item": { "Item": {
"item": { "htmlFields": ["description"] }, "item": { "htmlFields": ["description"] },
@@ -0,0 +1,54 @@
<div class="tab domains sheet-part {{#if @root.tabs.domains.active}}active{{/if}}" data-group="sheet" data-tab="domains">
<section class="community-section">
<h3>{{ localize 'COMMUNITY.size_title' }}</h3>
<div class="flexrow community-size">
<div class="form-group" style="flex: 1;">
<label for="system.size.value">{{ localize 'COMMUNITY.size_label' }}</label>
{{#if isEditMode}}
<select name="system.size.value" data-dtype="Number">
{{#each sizeOptions as |cfg level|}}
<option value="{{ level }}" {{#ife @root.system.size.value level}}selected{{/ife}}>{{ localize cfg.label }}</option>
{{/each}}
</select>
{{else}}
<span>{{ sizeLabel }}</span>
{{/if}}
</div>
<div class="form-group" style="flex: 1;">
<label for="system.population">{{ localize 'COMMUNITY.population' }}</label>
{{#if isEditMode}}
<input type="number" name="system.population" value="{{system.population}}" data-dtype="Number" min="0" />
{{else}}
<span>{{system.population}}</span>
{{/if}}
</div>
</div>
{{#if sizeInfo}}
<p class="community-size-info">
{{ localize 'COMMUNITY.population_range' }}: <strong>{{ sizeInfo.population }}</strong> ·
{{ localize 'COMMUNITY.figures' }}: <strong>{{ sizeInfo.figures }}+</strong> ·
{{ localize 'COMMUNITY.companies' }}: <strong>{{ sizeInfo.companies }}+</strong>
</p>
{{/if}}
</section>
{{#each domains}}
<section class="community-section">
<h3>{{ this.label }}</h3>
<div class="grid grid-2col">
{{#each this.sections}}
<div class="gauge {{ this.state }}">
<label for="system.domains.{{ this.domainKey }}.{{ this.key }}.value">{{ this.label }}</label>
{{#if @root.isEditMode}}
<input type="number" name="system.domains.{{ this.domainKey }}.{{ this.key }}.value"
value="{{ this.gauge.value }}" data-dtype="Number" min="0" />
{{else}}
<span class="gauge-value">{{ this.gauge.value }}D</span>
{{/if}}
<span class="gauge-state {{ this.state }}">{{ this.stateLabel }}</span>
</div>
{{/each}}
</div>
</section>
{{/each}}
</div>
@@ -0,0 +1,137 @@
<div class="tab identity sheet-part {{#if @root.tabs.identity.active}}active{{/if}}" data-group="sheet" data-tab="identity">
<section class="community-section">
<h3>{{ localize 'COMMUNITY.identity' }}</h3>
<div class="grid grid-2col">
<div class="form-group">
<label for="system.identity.origin">{{ localize 'COMMUNITY.origin' }}</label>
{{#if isEditMode}}
<input type="text" name="system.identity.origin" value="{{system.identity.origin}}" data-dtype="String" />
{{else}}
<span>{{system.identity.origin}}</span>
{{/if}}
</div>
<div class="form-group">
<label for="system.identity.theme">{{ localize 'COMMUNITY.theme' }}</label>
{{#if isEditMode}}
<input type="text" name="system.identity.theme" value="{{system.identity.theme}}" data-dtype="String" />
{{else}}
<span>{{system.identity.theme}}</span>
{{/if}}
</div>
<div class="form-group">
<label for="system.identity.age">{{ localize 'COMMUNITY.age_label' }}</label>
{{#if isEditMode}}
<select name="system.identity.age" data-dtype="String">
{{#each ageOptions}}
<option value="{{ this.key }}" {{#ife @root.system.identity.age this.key}}selected{{/ife}}>{{ this.label }}</option>
{{/each}}
</select>
{{else}}
<span>{{#each ageOptions}}{{#ife @root.system.identity.age this.key}}{{ this.label }}{{/ife}}{{/each}}</span>
{{/if}}
</div>
<div class="form-group">
<label>{{ localize 'COMMUNITY.instincts' }}</label>
{{#if isEditMode}}
<input type="text" name="system.identity.instincts" value="{{system.identity.instincts}}" data-dtype="String" />
{{else}}
<span>{{system.identity.instincts}}</span>
{{/if}}
</div>
<div class="form-group">
<label>{{ localize 'COMMUNITY.prohibits' }}</label>
{{#if isEditMode}}
<input type="text" name="system.identity.prohibits" value="{{system.identity.prohibits}}" data-dtype="String" />
{{else}}
<span>{{system.identity.prohibits}}</span>
{{/if}}
</div>
</div>
<div class="form-group">
<label for="system.identity.notes">{{ localize 'COMMUNITY.notes' }}</label>
{{formInput systemFields.identity.fields.notes enriched=enrichedNotes value=system.identity.notes name="system.identity.notes" toggled=true}}
</div>
</section>
<section class="community-section">
<h3>{{ localize 'COMMUNITY.location' }}</h3>
<div class="grid grid-2col">
<div class="form-group">
<label for="system.location.sector">{{ localize 'COMMUNITY.sector' }}</label>
{{#if isEditMode}}
<input type="text" name="system.location.sector" value="{{system.location.sector}}" data-dtype="String" />
{{else}}
<span>{{system.location.sector}}</span>
{{/if}}
</div>
<div class="form-group">
<label for="system.location.placeType">{{ localize 'COMMUNITY.placeType' }}</label>
{{#if isEditMode}}
<input type="text" name="system.location.placeType" value="{{system.location.placeType}}" data-dtype="String" />
{{else}}
<span>{{system.location.placeType}}</span>
{{/if}}
</div>
</div>
<div class="form-group">
<label for="system.location.landmarks">{{ localize 'COMMUNITY.landmarks' }}</label>
{{formInput systemFields.location.fields.landmarks enriched=enrichedLandmarks value=system.location.landmarks name="system.location.landmarks" toggled=true}}
</div>
<div class="form-group">
<label for="system.location.territory">{{ localize 'COMMUNITY.territory' }}</label>
{{formInput systemFields.location.fields.territory enriched=enrichedTerritory value=system.location.territory name="system.location.territory" toggled=true}}
</div>
</section>
<section class="community-section">
<h3>{{ localize 'COMMUNITY.history' }}</h3>
<div class="form-group">
<label for="system.history.founding">{{ localize 'COMMUNITY.founding' }}</label>
{{#if isEditMode}}
<input type="text" name="system.history.founding" value="{{system.history.founding}}" data-dtype="String" />
{{else}}
<span>{{system.history.founding}}</span>
{{/if}}
</div>
<div class="form-group">
<label for="system.history.events">{{ localize 'COMMUNITY.events' }}</label>
{{formInput systemFields.history.fields.events enriched=enrichedEvents value=system.history.events name="system.history.events" toggled=true}}
</div>
<div class="form-group">
<label for="system.history.relations">{{ localize 'COMMUNITY.history_relations' }}</label>
{{formInput systemFields.history.fields.relations enriched=enrichedRelations value=system.history.relations name="system.history.relations" toggled=true}}
</div>
</section>
<section class="community-section">
<h3>{{ localize 'COMMUNITY.objectives' }}</h3>
<div class="form-group">
<label for="system.objectives.major">{{ localize 'COMMUNITY.objective_major' }}</label>
{{#if isEditMode}}
<input type="text" name="system.objectives.major" value="{{system.objectives.major}}" data-dtype="String" />
{{else}}
<span>{{system.objectives.major}}</span>
{{/if}}
</div>
<div class="form-group">
<label>{{ localize 'COMMUNITY.objectives_minor' }}</label>
<ol class="items-list">
{{#each system.objectives.minor as |obj index|}}
<li class="item flexrow" data-index="{{index}}">
{{#if @root.isEditMode}}
<input type="text" name="system.objectives.minor.{{index}}" value="{{obj}}" data-dtype="String" style="flex: 1;" />
<a data-action="deleteObjective" data-index="{{index}}" title="{{ localize 'VERMINE.delete' }}"><i class="fas fa-trash"></i></a>
{{else}}
<span style="flex: 1;">{{obj}}</span>
{{/if}}
</li>
{{/each}}
{{#if isEditMode}}
<li class="item flexrow">
<a data-action="addObjective" title="{{ localize 'VERMINE.add' }}"><i class="fas fa-plus"></i> {{ localize 'COMMUNITY.add_objective' }}</a>
</li>
{{/if}}
</ol>
</div>
</section>
</div>
+79
View File
@@ -0,0 +1,79 @@
<div class="tab main sheet-part" data-group="sheet" data-tab="main">
{{!-- Toggle Edit/Play --}}
<div class="sheet-header-toggle">
{{#if isEditMode}}
<button type="button" data-action="toggleSheet">{{localize "VERMINE.playMode"}}</button>
{{else}}
<button type="button" data-action="toggleSheet">{{localize "VERMINE.editMode"}}</button>
{{/if}}
</div>
{{!-- HEADER --}}
<header class="char-header grid grid-12col w-full">
<div class="grid-span-2 community-portrait">
<img class="profile-img" src="{{actor.img}}" data-action="editImage" data-edit="img" width="110"
height="110" title="{{actor.name}}" />
</div>
<section class="char-details grid-span-6">
<h1 class="char-name flexrow flex-group-left w-full">
<label>{{ localize 'IDENTITY.name' }}</label>
{{#if isEditMode}}
<input name="name" type="text" value="{{actor.name}}" placeholder="{{ localize 'COMMUNITY.name_placeholder' }}" />
{{else}}
<span>{{actor.name}}</span>
{{/if}}
</h1>
<div class="profile flexrow flex-group-center">
<label>{{ localize 'IDENTITY.profile' }}</label>
{{#if isEditMode}}
<input name="system.identity.profile" type="text" value="{{system.identity.profile}}" data-dtype="String" />
{{else}}
<span>{{#if system.identity.profile}}{{system.identity.profile}}{{else}}{{ localize 'COMMUNITY.profile_hint' }}{{/if}}</span>
{{/if}}
</div>
<div class="origin flexrow flex-group-center">
<label>{{ localize 'COMMUNITY.origin' }}</label>
{{#if isEditMode}}
<input name="system.identity.origin" type="text" value="{{system.identity.origin}}" data-dtype="String" />
{{else}}
<span>{{system.identity.origin}}</span>
{{/if}}
</div>
</section>
<section class="char-level grid-span-4">
<h1 class="char-vermine2047 flexrow flex-group-left w-full">
{{#if (eq system.identity.totem "")}}
<a data-action="chooseTotem" class="chooseTotem">{{ localize 'COMMUNITY.choose_totem' }}&hellip;</a>
{{ else }}
<a data-action="chooseTotem" class="chooseTotem">{{ smarttl "TOTEMS" system.identity.totem }}</a>
{{/if}}
</h1>
<div class="level flexrow flex-group-center">
<label>{{ localize 'VERMINE.level' }}</label>
{{#if isEditMode}}
<input name="system.level.value" type="number" value="{{system.level.value}}" data-dtype="Number" min="{{system.level.min}}" max="{{system.level.max}}" />
{{else}}
<span>{{system.level.value}}</span>
{{/if}}
<span>/ {{system.level.max}}</span>
</div>
<div class="population flexrow flex-group-center">
<label>{{ localize 'COMMUNITY.population' }}</label>
{{#if isEditMode}}
<input name="system.population" type="number" value="{{system.population}}" data-dtype="Number" min="0" />
{{else}}
<span>{{system.population}}</span>
{{/if}}
</div>
</section>
</header>
{{#if resolvedGroup}}
<div class="community-linked-group">
<span class="label">{{ localize 'COMMUNITY.linked_group' }}:</span>
<span class="value">{{ resolvedGroup.name }}</span>
</div>
{{/if}}
</div>
@@ -0,0 +1,63 @@
<div class="tab resources sheet-part {{#if @root.tabs.resources.active}}active{{/if}}" data-group="sheet" data-tab="resources">
<section class="community-section">
<h3>{{ localize 'COMMUNITY.reserves' }}</h3>
<div class="grid grid-3col">
<div class="form-group">
<label for="system.reserve.value">{{ localize 'COMMUNITY.group_reserve' }}</label>
{{#if isEditMode}}
<input type="number" name="system.reserve.value" value="{{system.reserve.value}}" data-dtype="Number" min="0" max="{{system.reserve.max}}" />
{{else}}
<span>{{system.reserve.value}} / {{system.reserve.max}}</span>
{{/if}}
</div>
<div class="form-group">
<label for="system.material">{{ localize 'COMMUNITY.material_dice' }}</label>
{{#if isEditMode}}
<input type="number" name="system.material" value="{{system.material}}" data-dtype="Number" min="0" />
{{else}}
<span>{{system.material}}D</span>
{{/if}}
</div>
<div class="form-group">
<label for="system.experience.dice">{{ localize 'COMMUNITY.experience_dice' }}</label>
{{#if isEditMode}}
<input type="number" name="system.experience.dice" value="{{system.experience.dice}}" data-dtype="Number" min="0" />
{{else}}
<span>{{system.experience.dice}}D</span>
{{/if}}
</div>
</div>
</section>
<section class="community-section">
<h3>{{ localize 'COMMUNITY.morale' }}</h3>
<div class="flexrow">
<div class="form-group" style="flex: 1;">
<label for="system.morale.value">{{ localize 'COMMUNITY.morale_value' }}</label>
{{#if isEditMode}}
<input type="number" name="system.morale.value" value="{{system.morale.value}}" data-dtype="Number" min="0" max="7" />
{{else}}
<span>{{system.morale.value}}</span>
{{/if}}
</div>
<div class="form-group" style="flex: 1;">
<label>{{ localize 'COMMUNITY.morale_state' }}</label>
<span class="morale-level {{ system.morale.level }}">{{ localize (concat 'COMMUNITY.morale_levels.' system.morale.level) }}</span>
</div>
</div>
{{#if system.allDomainsLow}}
<p class="community-warning">{{ localize 'COMMUNITY.morale_blocked' }}</p>
{{/if}}
</section>
<section class="community-section">
<h3>{{ localize 'COMMUNITY.habitants_title' }}</h3>
<p class="community-hint">{{ localize 'COMMUNITY.habitants_coming' }}</p>
{{#if resolvedGroup}}
<p class="community-linked-group">
<span class="label">{{ localize 'COMMUNITY.linked_group' }}:</span>
<span class="value">{{ resolvedGroup.name }}</span>
</p>
{{/if}}
</section>
</div>