diff --git a/assets/icons/actors/community.webp b/assets/icons/actors/community.webp new file mode 100644 index 0000000..242947b Binary files /dev/null and b/assets/icons/actors/community.webp differ diff --git a/lang/en.json b/lang/en.json index 69d6b9d..e7b2eb4 100644 --- a/lang/en.json +++ b/lang/en.json @@ -174,6 +174,15 @@ "range": "Range", "ranges": "Ranges", "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_sm": "Rar.", "reliability": "Reliability", @@ -204,6 +213,7 @@ "type_character": "Character", "type_npc": "NPC", "type_creature": "Creature", + "type_community": "Community", "type": "Type", "group_members": "Members", "encounters": "Encounters", @@ -379,6 +389,13 @@ "totem": "Totem", "experience": "Experience" }, + "Sheet": { + "character": "Character", + "npc": "NPC", + "group": "Group", + "creature": "Creature", + "community": "Community" + }, "base_values": "Base Values", "computed_values": "Computed Values", "cost": "Cost", @@ -441,6 +458,91 @@ "effects.passive": "Passive", "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": { "item": "Item", "items": "Items", diff --git a/lang/fr.json b/lang/fr.json index 10b1185..99c17b9 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -255,6 +255,15 @@ "range": "Portée", "ranges": "Portées", "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_sm": "Rar.", "reliability": "Fiabilité", @@ -395,6 +404,7 @@ "type_character": "Personnage", "type_npc": "PNJ", "type_creature": "Créature", + "type_community": "Communauté", "type": "Type", "group_members": "Membres", "encounters": "Rencontres", @@ -440,7 +450,8 @@ "character": "Personnage", "npc": "PNJ", "group": "Groupe", - "creature": "Créature" + "creature": "Créature", + "community": "Communauté" } }, "UI": { @@ -458,6 +469,91 @@ "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": { "item": "Objet", "items": "Objets", diff --git a/less/actor/community.less b/less/actor/community.less new file mode 100644 index 0000000..f176676 --- /dev/null +++ b/less/actor/community.less @@ -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; } + } +} diff --git a/less/base.less b/less/base.less index fba1522..f2fab58 100644 --- a/less/base.less +++ b/less/base.less @@ -126,7 +126,8 @@ ul.unstyled { // 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 // 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; } @@ -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 { .group-content, .npc-content, - .creature-content { + .creature-content, + .community-content { padding: 15px; } } @@ -1119,7 +1121,7 @@ ol.chat-log { label { font-family: "DistressBlack", sans-serif; text-transform: uppercase; - font-size: @font-size-10; + font-size: @font-size-11; color: @color-amber; } } diff --git a/less/vermine2047.less b/less/vermine2047.less index 217f224..af9cea7 100644 --- a/less/vermine2047.less +++ b/less/vermine2047.less @@ -22,6 +22,7 @@ @import "actor/npc"; @import "actor/group"; @import "actor/creature"; +@import "actor/community"; // 6. Styles des items @import "items"; diff --git a/module/applications/sheets/_module.mjs b/module/applications/sheets/_module.mjs index bd1eb28..cfe806c 100644 --- a/module/applications/sheets/_module.mjs +++ b/module/applications/sheets/_module.mjs @@ -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, diff --git a/module/applications/sheets/community-sheet.mjs b/module/applications/sheets/community-sheet.mjs new file mode 100644 index 0000000..c6ccc88 --- /dev/null +++ b/module/applications/sheets/community-sheet.mjs @@ -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 }) + } +} diff --git a/module/models/_module.mjs b/module/models/_module.mjs index 5d4023a..9de8d92 100644 --- a/module/models/_module.mjs +++ b/module/models/_module.mjs @@ -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" diff --git a/module/models/community.mjs b/module/models/community.mjs new file mode 100644 index 0000000..8d1890d --- /dev/null +++ b/module/models/community.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" + } + } +} diff --git a/module/system/config.mjs b/module/system/config.mjs index ccf16f8..757d81d 100644 --- a/module/system/config.mjs +++ b/module/system/config.mjs @@ -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" +} diff --git a/module/vermine2047.mjs b/module/vermine2047.mjs index e7f33f3..09d24e9 100644 --- a/module/vermine2047.mjs +++ b/module/vermine2047.mjs @@ -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" ]); }); diff --git a/system.json b/system.json index 71c75ae..9d2548c 100644 --- a/system.json +++ b/system.json @@ -34,7 +34,8 @@ "character": { "htmlFields": ["equipment.description", "identity.biography", "identity.relations"] }, "npc": { "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": { "htmlFields": ["description"] }, diff --git a/templates/actor/appv2/community-domains.hbs b/templates/actor/appv2/community-domains.hbs new file mode 100644 index 0000000..0b6b32b --- /dev/null +++ b/templates/actor/appv2/community-domains.hbs @@ -0,0 +1,54 @@ +
+
+

{{ localize 'COMMUNITY.size_title' }}

+
+
+ + {{#if isEditMode}} + + {{else}} + {{ sizeLabel }} + {{/if}} +
+
+ + {{#if isEditMode}} + + {{else}} + {{system.population}} + {{/if}} +
+
+ {{#if sizeInfo}} +

+ {{ localize 'COMMUNITY.population_range' }}: {{ sizeInfo.population }} · + {{ localize 'COMMUNITY.figures' }}: {{ sizeInfo.figures }}+ · + {{ localize 'COMMUNITY.companies' }}: {{ sizeInfo.companies }}+ +

+ {{/if}} +
+ + {{#each domains}} +
+

{{ this.label }}

+
+ {{#each this.sections}} +
+ + {{#if @root.isEditMode}} + + {{else}} + {{ this.gauge.value }}D + {{/if}} + {{ this.stateLabel }} +
+ {{/each}} +
+
+ {{/each}} +
diff --git a/templates/actor/appv2/community-identity.hbs b/templates/actor/appv2/community-identity.hbs new file mode 100644 index 0000000..9034775 --- /dev/null +++ b/templates/actor/appv2/community-identity.hbs @@ -0,0 +1,137 @@ +
+
+

{{ localize 'COMMUNITY.identity' }}

+
+
+ + {{#if isEditMode}} + + {{else}} + {{system.identity.origin}} + {{/if}} +
+
+ + {{#if isEditMode}} + + {{else}} + {{system.identity.theme}} + {{/if}} +
+
+ + {{#if isEditMode}} + + {{else}} + {{#each ageOptions}}{{#ife @root.system.identity.age this.key}}{{ this.label }}{{/ife}}{{/each}} + {{/if}} +
+
+ + {{#if isEditMode}} + + {{else}} + {{system.identity.instincts}} + {{/if}} +
+
+ + {{#if isEditMode}} + + {{else}} + {{system.identity.prohibits}} + {{/if}} +
+
+
+ + {{formInput systemFields.identity.fields.notes enriched=enrichedNotes value=system.identity.notes name="system.identity.notes" toggled=true}} +
+
+ +
+

{{ localize 'COMMUNITY.location' }}

+
+
+ + {{#if isEditMode}} + + {{else}} + {{system.location.sector}} + {{/if}} +
+
+ + {{#if isEditMode}} + + {{else}} + {{system.location.placeType}} + {{/if}} +
+
+
+ + {{formInput systemFields.location.fields.landmarks enriched=enrichedLandmarks value=system.location.landmarks name="system.location.landmarks" toggled=true}} +
+
+ + {{formInput systemFields.location.fields.territory enriched=enrichedTerritory value=system.location.territory name="system.location.territory" toggled=true}} +
+
+ +
+

{{ localize 'COMMUNITY.history' }}

+
+ + {{#if isEditMode}} + + {{else}} + {{system.history.founding}} + {{/if}} +
+
+ + {{formInput systemFields.history.fields.events enriched=enrichedEvents value=system.history.events name="system.history.events" toggled=true}} +
+
+ + {{formInput systemFields.history.fields.relations enriched=enrichedRelations value=system.history.relations name="system.history.relations" toggled=true}} +
+
+ +
+

{{ localize 'COMMUNITY.objectives' }}

+
+ + {{#if isEditMode}} + + {{else}} + {{system.objectives.major}} + {{/if}} +
+
+ +
    + {{#each system.objectives.minor as |obj index|}} +
  1. + {{#if @root.isEditMode}} + + + {{else}} + {{obj}} + {{/if}} +
  2. + {{/each}} + {{#if isEditMode}} +
  3. + {{ localize 'COMMUNITY.add_objective' }} +
  4. + {{/if}} +
+
+
+
diff --git a/templates/actor/appv2/community-main.hbs b/templates/actor/appv2/community-main.hbs new file mode 100644 index 0000000..34986ef --- /dev/null +++ b/templates/actor/appv2/community-main.hbs @@ -0,0 +1,79 @@ +
+ {{!-- Toggle Edit/Play --}} +
+ {{#if isEditMode}} + + {{else}} + + {{/if}} +
+ + {{!-- HEADER --}} +
+
+ +
+ +
+

+ + {{#if isEditMode}} + + {{else}} + {{actor.name}} + {{/if}} +

+
+ + {{#if isEditMode}} + + {{else}} + {{#if system.identity.profile}}{{system.identity.profile}}{{else}}{{ localize 'COMMUNITY.profile_hint' }}{{/if}} + {{/if}} +
+
+ + {{#if isEditMode}} + + {{else}} + {{system.identity.origin}} + {{/if}} +
+
+ +
+

+ {{#if (eq system.identity.totem "")}} + {{ localize 'COMMUNITY.choose_totem' }}… + {{ else }} + {{ smarttl "TOTEMS" system.identity.totem }} + {{/if}} +

+
+ + {{#if isEditMode}} + + {{else}} + {{system.level.value}} + {{/if}} + / {{system.level.max}} +
+
+ + {{#if isEditMode}} + + {{else}} + {{system.population}} + {{/if}} +
+
+
+ + {{#if resolvedGroup}} +
+ {{ localize 'COMMUNITY.linked_group' }}: + {{ resolvedGroup.name }} +
+ {{/if}} +
diff --git a/templates/actor/appv2/community-resources.hbs b/templates/actor/appv2/community-resources.hbs new file mode 100644 index 0000000..4a0cdf8 --- /dev/null +++ b/templates/actor/appv2/community-resources.hbs @@ -0,0 +1,63 @@ +
+
+

{{ localize 'COMMUNITY.reserves' }}

+
+
+ + {{#if isEditMode}} + + {{else}} + {{system.reserve.value}} / {{system.reserve.max}} + {{/if}} +
+
+ + {{#if isEditMode}} + + {{else}} + {{system.material}}D + {{/if}} +
+
+ + {{#if isEditMode}} + + {{else}} + {{system.experience.dice}}D + {{/if}} +
+
+
+ +
+

{{ localize 'COMMUNITY.morale' }}

+
+
+ + {{#if isEditMode}} + + {{else}} + {{system.morale.value}} + {{/if}} +
+
+ + {{ localize (concat 'COMMUNITY.morale_levels.' system.morale.level) }} +
+
+ {{#if system.allDomainsLow}} +

{{ localize 'COMMUNITY.morale_blocked' }}

+ {{/if}} +
+ +
+

{{ localize 'COMMUNITY.habitants_title' }}

+

{{ localize 'COMMUNITY.habitants_coming' }}

+ {{#if resolvedGroup}} +

+ {{ localize 'COMMUNITY.linked_group' }}: + {{ resolvedGroup.name }} +

+ {{/if}} +
+