Files
vermine2047/module/applications/sheets/group-sheet.mjs
T
uberwald c96e8d69d7 fix: retours review - validation de phase, drop de véhicule et dés de Réserve
- resetPhaseLimits ne touche plus au compteur de phase (seul le
  dialogue d'attribution du MJ l'avance) : évite le double-incrément
  quand joueur et MJ valident la même phase.
- _onDropActor : import d'une copie monde pour un véhicule déposé
  depuis un compendium, confirmation avant de transférer un véhicule
  déjà rattaché à un autre Groupe, try/catch avec retour utilisateur.
- #onClickRadioHexa : garde isEditable (pas d'erreur de permission
  pour un utilisateur non éditeur).
2026-08-05 21:24:33 +02:00

228 lines
9.0 KiB
JavaScript

import VermineBaseActorSheet from "./base-actor-sheet.mjs"
export default class VermineGroupSheetV2 extends VermineBaseActorSheet {
static DEFAULT_OPTIONS = {
classes: ["group"],
position: { width: 700, height: 600 },
window: { contentClasses: ["group-content"] },
actions: {
chooseTotem: VermineGroupSheetV2.#onChooseTotem,
chooseActor: VermineGroupSheetV2.#onChooseActor,
deleteMember: VermineGroupSheetV2.#onDeleteMember,
deleteEncounter: VermineGroupSheetV2.#onDeleteEncounter,
deleteObjective: VermineGroupSheetV2.#onDeleteObjective,
addObjective: VermineGroupSheetV2.#onAddObjective,
createTotemAbility: VermineGroupSheetV2.#onCreateTotemAbility,
openLearning: VermineGroupSheetV2.#onOpenLearning,
openExperiencePhase: VermineGroupSheetV2.#onOpenExperiencePhase,
validatePhase: VermineGroupSheetV2.#onValidatePhase
}
}
static PARTS = {
main: { template: "systems/vermine2047/templates/actor/appv2/group-main.hbs", scrollable: [""] },
tabs: { template: "templates/generic/tab-navigation.hbs" },
info: { template: "systems/vermine2047/templates/actor/appv2/group-info.hbs", scrollable: [""] },
gear: { template: "systems/vermine2047/templates/actor/appv2/group-gear.hbs", scrollable: [""] },
road: { template: "systems/vermine2047/templates/actor/appv2/group-road.hbs", scrollable: [""] },
reserve: { template: "systems/vermine2047/templates/actor/appv2/group-reserve.hbs", scrollable: [""] }
}
tabGroups = { sheet: "info" }
#getTabs() {
const tabs = {
info: { id: "info", group: "sheet", icon: "fas fa-star", label: "VERMINE.information" },
gear: { id: "gear", group: "sheet", icon: "fas fa-gear", label: "VERMINE.gear" },
road: { id: "road", group: "sheet", icon: "fas fa-route", label: "VERMINE.road" },
reserve: { id: "reserve", group: "sheet", icon: "fas fa-users", label: "VERMINE.reserve" }
}
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()
// Résoudre les IDs des membres/encounters en données acteur
context.resolvedMembers = {}
if (this.document.system.members?.length > 0) {
for (const memberId of this.document.system.members) {
const a = game.actors.get(memberId)
if (a) context.resolvedMembers[memberId] = { name: a.name, id: a.id }
}
}
context.resolvedEncounters = {}
if (this.document.system.encounters?.length > 0) {
for (const encId of this.document.system.encounters) {
const a = game.actors.get(encId)
if (a) {
context.resolvedEncounters[encId] = {
name: a.name,
id: a.id,
type: a.type,
profile: a.system?.identity?.profile || "",
totem: a.system?.identity?.totem || ""
}
}
}
}
// Notes de voyage (riche texte)
const roadNotes = this.document.system?.roadNotes
context.enrichedRoadNotes = roadNotes
? await foundry.applications.ux.TextEditor.implementation.enrichHTML(roadNotes, { async: true })
: ""
return context
}
async _preparePartContext(partId, context) {
const doc = this.document
switch (partId) {
case "main": break
case "info":
context.tab = context.tabs.info
context.abilities = doc.itemTypes.ability.filter(i => i.system.type !== "totem")
context.totem_abilities = doc.itemTypes.ability.filter(i => i.system.type === "totem")
break
case "gear":
context.tab = context.tabs.gear
context.gear = doc.itemTypes.item
context.weapons = doc.itemTypes.weapon
context.defenses = doc.itemTypes.defense
context.vehicles = this.#getOwnedVehicles()
break
case "road":
context.tab = context.tabs.road
break
case "reserve":
context.tab = context.tabs.reserve
break
}
return context
}
/** Véhicules (acteurs) appartenant à ce groupe via system.ownerId. */
#getOwnedVehicles() {
return game.actors.filter(a => a.type === "vehicle" && a.system.ownerId === this.document.id)
}
/**
* Dépôt d'un acteur Véhicule sur la fiche : lie le véhicule au Groupe
* (system.ownerId) au lieu de le dupliquer, puis affiche la liste.
* Gère les dépôts depuis un compendium (copie importée dans le monde)
* et demande confirmation si le véhicule appartient déjà à un autre Groupe.
* @override
*/
async _onDropActor(event, data) {
let actor = data instanceof foundry.abstract.Document ? data : await fromUuid(data.uuid)
if (actor?.type !== "vehicle") {
return super._onDropActor?.(event, data)
}
// Véhicule issu d'un compendium : importer une copie dans le monde.
if (actor.compendium) {
try {
const copy = await game.actors.importFromCompendium(actor.compendium, actor._id, { renderSheet: false })
if (!copy) return
actor = copy
} catch {
ui.notifications.warn(game.i18n.localize("VERMINE.vehicle_import_failed"))
return
}
}
// Véhicule déjà rattaché à un autre Groupe : confirmation avant transfert.
const currentOwner = actor.system?.ownerId
if (currentOwner && currentOwner !== this.document.id) {
const other = game.actors.get(currentOwner)
const content = game.i18n.localize("VERMINE.vehicle_assign_confirm").replace("{group}", other?.name ?? currentOwner)
const confirmed = await Dialog.confirm({
title: game.i18n.localize("VERMINE.vehicle_assign"),
content,
defaultYes: false
})
if (!confirmed) return
}
try {
await actor.update({ "system.ownerId": this.document.id })
} catch {
ui.notifications.warn(game.i18n.localize("VERMINE.vehicle_assign_failed"))
return
}
this.render()
}
// Actions : délégation aux applications AppV1 existantes pour TotemPicker/ActorPicker
static async #onChooseTotem(event, target) {
const { TotemPicker } = await import("../../system/applications.mjs")
// Le Totem du Groupe remplit automatiquement Instincts/Interdits.
new TotemPicker(target, this.document, { fillInstincts: true }).render(true)
}
static async #onChooseActor(event, target) {
const { ActorPicker } = await import("../../system/applications.mjs")
new ActorPicker(target, this.document).render(true)
}
static #onDeleteMember(event, target) {
const li = target.closest("li.actor")
if (!li) return
const actorId = li.dataset.actorId
const idx = this.document.system.members.indexOf(actorId)
if (idx !== -1) {
const members = [...this.document.system.members]
members.splice(idx, 1)
this.document.update({ "system.members": members })
}
}
static #onDeleteEncounter(event, target) {
const li = target.closest("li.actor")
if (!li) return
const actorId = li.dataset.actorId
const idx = this.document.system.encounters.indexOf(actorId)
if (idx !== -1) {
const encounters = [...this.document.system.encounters]
encounters.splice(idx, 1)
this.document.update({ "system.encounters": encounters })
}
}
static #onDeleteObjective(event, target) {
const type = target.dataset.type
const index = parseInt(target.dataset.index)
if (isNaN(index)) return
const objectives = foundry.utils.duplicate(this.document.system.objectives || { major: [], minor: [] })
objectives[type].splice(index, 1)
this.document.update({ "system.objectives": objectives })
}
static #onAddObjective(event, target) {
const type = target.dataset.type === "major_objective" ? "major" : "minor"
const objectives = foundry.utils.duplicate(this.document.system.objectives || { major: [], minor: [] })
objectives[type].push("")
this.document.update({ "system.objectives": objectives })
}
static async #onCreateTotemAbility(event, target) {
const name = game.i18n.localize("ITEMS.new_ability")
await this.document.createEmbeddedDocuments("Item", [{ name, type: "ability", system: { type: "totem" } }])
}
static async #onOpenExperiencePhase(event, target) {
const { default: ExperiencePhaseDialog } = await import("../../system/dialogs/experiencePhaseDialog.mjs")
const dialog = await ExperiencePhaseDialog.create()
if (dialog) dialog.render(true)
}
static async #onOpenLearning(event, target) {
const { default: GroupLearningDialog } = await import("../../system/dialogs/groupLearningDialog.mjs")
const dialog = await GroupLearningDialog.create({ actorId: this.document.id })
if (dialog) dialog.render(true)
}
/** Valide la phase d'Expérience courante du Groupe et réinitialise les limites. */
static async #onValidatePhase() {
const { VermineExperience } = await import("../../system/experience.mjs")
await VermineExperience.resetPhaseLimits(this.document)
ui.notifications.info(game.i18n.localize("VERMINE.validate_phase_done"))
}
}