Fix apv2, WIP

This commit is contained in:
2026-06-06 10:21:24 +02:00
parent 6cec1da910
commit 9b77a0c552
130 changed files with 12850 additions and 2830 deletions
+20
View File
@@ -0,0 +1,20 @@
export { VermineBaseActorSheet } from "./base-actor-sheet.mjs"
export { VermineBaseItemSheet } from "./base-item-sheet.mjs"
export { VermineCharacterSheetV2 } from "./character-sheet.mjs"
export { VermineNpcSheetV2 } from "./npc-sheet.mjs"
export { VermineGroupSheetV2 } from "./group-sheet.mjs"
export { VermineCreatureSheetV2 } from "./creature-sheet.mjs"
export {
VermineItemSheetV2,
VermineWeaponSheetV2,
VermineDefenseSheetV2,
VermineVehicleSheetV2,
VermineAbilitySheetV2,
VermineSpecialtySheetV2,
VermineBackgroundSheetV2,
VermineTraumaSheetV2,
VermineEvolutionSheetV2,
VermineRumorSheetV2,
VermineTargetSheetV2,
VermineRiteSheetV2
} from "./item-sheets.mjs"
@@ -0,0 +1,207 @@
import { onManageActiveEffect } from "../../system/effects.mjs"
const { HandlebarsApplicationMixin } = foundry.applications.api
/**
* Fiche de base pour tous les acteurs Vermine 2047 (ApplicationV2).
* Remplace VermineActorSheet (AppV1).
*/
export class VermineBaseActorSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ActorSheetV2) {
// ── Mode édition / jeu ──────────────────────────────────────────────
static SHEET_MODES = { EDIT: 0, PLAY: 1 }
_sheetMode = this.constructor.SHEET_MODES.PLAY
get isPlayMode() { return this._sheetMode === this.constructor.SHEET_MODES.PLAY }
get isEditMode() { return this._sheetMode === this.constructor.SHEET_MODES.EDIT }
// ── Options par défaut ──────────────────────────────────────────────
static DEFAULT_OPTIONS = {
classes: ["vermine2047", "actor"],
position: { width: 800, height: "auto" },
form: { submitOnChange: true },
window: { resizable: true },
dragDrop: [{ dragSelector: ".item", dropSelector: null }],
actions: {
editImage: VermineBaseActorSheet.#onEditImage,
toggleSheet: VermineBaseActorSheet.#onToggleSheet,
edit: VermineBaseActorSheet.#onItemEdit,
delete: VermineBaseActorSheet.#onItemDelete,
create: VermineBaseActorSheet.#onItemCreate,
roll: VermineBaseActorSheet.#onRollItem,
clickRadio: VermineBaseActorSheet.#onClickRadioHexa,
effectControl: VermineBaseActorSheet.#onEffectControl,
chooseTotem: VermineBaseActorSheet.#onChooseTotem
}
}
// ── Drag & Drop ─────────────────────────────────────────────────────
#dragDrop
constructor(options = {}) {
super(options)
this.#dragDrop = this.#createDragDropHandlers()
}
#createDragDropHandlers() {
return this.options.dragDrop.map(d => {
d.permissions = {
dragstart: this._canDragStart.bind(this),
drop: this._canDragDrop.bind(this)
}
d.callbacks = {
dragover: this._onDragOver.bind(this),
drop: this._onDrop.bind(this)
}
return new foundry.applications.ux.DragDrop.implementation(d)
})
}
_canDragStart() { return this.isEditable }
_canDragDrop() { return this.isEditable }
// ── Contexte commun ─────────────────────────────────────────────────
async _prepareContext() {
return {
fields: this.document.schema.fields,
systemFields: this.document.system.schema.fields,
actor: this.document,
system: this.document.system,
source: this.document.toObject(),
config: CONFIG.VERMINE,
rollData: this.document.getRollData(),
isGM: game.user.isGM,
isEditMode: this.isEditMode,
isPlayMode: this.isPlayMode,
isEditable: this.isEditable
}
}
// ── Rendu ───────────────────────────────────────────────────────────
async _onRoll(event) {
event.preventDefault()
const el = event.currentTarget
const type = el.dataset.type
const label = el.dataset.label
if (!type || !label) return
const { default: RollDialog } = await import("../../system/dialogs/rollDialog.mjs")
const dialog = await RollDialog.create({
actorId: this.document.id,
rolltype: type,
label
})
if (dialog) dialog.render(true)
}
// ── Actions ─────────────────────────────────────────────────────────
_onRender(context, options) {
super._onRender(context, options)
this.#dragDrop.forEach(d => d.bind(this.element))
this.element.querySelectorAll(".rollable").forEach(el => {
el.addEventListener("click", this._onRoll.bind(this))
})
// Auto-fill empty number inputs on change to prevent validation errors
this.element.addEventListener("change", e => {
const input = e.target
if (input?.type === "number" && !input.value && input.name && input !== document.activeElement) {
input.value = "0"
}
}, { capture: true })
}
/** @override */
async _onDropItem(item) {
const doc = item instanceof foundry.abstract.Document ? item : await fromUuid(item.uuid)
if (!doc) return
const itemData = doc.toObject()
await this.document.createEmbeddedDocuments("Item", [itemData], { renderSheet: false })
}
static #onToggleSheet() {
const modes = this.constructor.SHEET_MODES
this._sheetMode = this.isEditMode ? modes.PLAY : modes.EDIT
this.render()
}
static async #onEditImage(event, target) {
const attr = target.dataset.edit ?? "img"
const current = foundry.utils.getProperty(this.document, attr)
const fp = new FilePicker({
current,
type: "image",
callback: (path) => this.document.update({ [attr]: path }),
top: this.position.top + 40,
left: this.position.left + 10
})
return fp.browse()
}
static async #onItemEdit(event, target) {
const id = target.closest("[data-item-id]")?.dataset?.itemId
const uuid = target.closest("[data-item-uuid]")?.dataset?.itemUuid
let item
if (uuid) item = await fromUuid(uuid)
if (!item) item = this.document.items.get(id)
item?.sheet.render(true)
}
static async #onItemDelete(event, target) {
const itemUuid = target.closest("[data-item-uuid]")?.dataset?.itemUuid
if (itemUuid) {
const item = await fromUuid(itemUuid)
await item?.deleteDialog()
return
}
const id = target.closest("[data-item-id]")?.dataset?.itemId
const item = this.document.items.get(id)
await item?.deleteDialog()
}
static async #onItemCreate(event, target) {
const type = target.dataset.type
if (!type) return
const name = game.i18n.localize("ITEMS.new_" + type)
await this.document.createEmbeddedDocuments("Item", [{ name, type }])
}
static async #onRollItem(event, target) {
const id = target.closest("[data-item-id]")?.dataset?.itemId
if (!id) return
const item = this.document.items.get(id)
item?.roll()
}
static #onClickRadioHexa(event, target) {
event.preventDefault()
event.stopPropagation()
const input = target
const update = {}
let current = this.document
const propTree = input.name.split(".")
for (const prop of propTree) {
current = current[prop]
}
if (current != input.value) {
update[input.name] = parseInt(input.value)
} else {
update[input.name] = parseInt(input.value) - 1
}
this.document.update(update)
}
static #onEffectControl(event, target) {
onManageActiveEffect(event, this.document)
}
static async #onChooseTotem(event, target) {
const { TotemPicker } = await import("../../system/applications.mjs")
new TotemPicker(target, this.document).render(true)
}
}
@@ -0,0 +1,114 @@
const { HandlebarsApplicationMixin } = foundry.applications.api
/**
* Fiche de base pour tous les items Vermine 2047 (ApplicationV2).
*/
export class VermineBaseItemSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ItemSheetV2) {
// ── Mode édition ────────────────────────────────────────────────────
static SHEET_MODES = { EDIT: 0, PLAY: 1 }
_sheetMode = this.constructor.SHEET_MODES.PLAY
get isPlayMode() { return this._sheetMode === this.constructor.SHEET_MODES.PLAY }
get isEditMode() { return this._sheetMode === this.constructor.SHEET_MODES.EDIT }
// ── Options par défaut ──────────────────────────────────────────────
static DEFAULT_OPTIONS = {
classes: ["vermine2047", "item"],
position: { width: 560, height: "auto" },
form: { submitOnChange: true },
window: { resizable: true },
actions: {
editImage: VermineBaseItemSheet.#onEditImage,
toggleSheet: VermineBaseItemSheet.#onToggleSheet,
clickDamage: VermineBaseItemSheet.#onClickDamage,
openTraits: VermineBaseItemSheet.#onOpenTraits
}
}
// ── Drag & Drop ─────────────────────────────────────────────────────
#dragDrop
constructor(options = {}) {
super(options)
this.#dragDrop = this.#createDragDropHandlers()
}
#createDragDropHandlers() {
if (!this.options.dragDrop) return []
return this.options.dragDrop.map(d => {
d.permissions = {
dragstart: this._canDragStart.bind(this),
drop: this._canDragDrop.bind(this)
}
d.callbacks = {
dragover: this._onDragOver.bind(this),
drop: this._onDrop.bind(this)
}
return new foundry.applications.ux.DragDrop.implementation(d)
})
}
_canDragStart() { return this.isEditable }
_canDragDrop() { return this.isEditable }
// ── Contexte commun ─────────────────────────────────────────────────
async _prepareContext() {
return {
fields: this.document.schema.fields,
systemFields: this.document.system.schema.fields,
item: this.document,
system: this.document.system,
source: this.document.toObject(),
config: CONFIG.VERMINE,
isEditMode: this.isEditMode,
isPlayMode: this.isPlayMode,
isEditable: this.isEditable
}
}
// ── Rendu ───────────────────────────────────────────────────────────
_onRender(context, options) {
this.#dragDrop.forEach(d => d.bind(this.element))
}
// ── Actions ─────────────────────────────────────────────────────────
static #onToggleSheet() {
const modes = this.constructor.SHEET_MODES
this._sheetMode = this.isEditMode ? modes.PLAY : modes.EDIT
this.render()
}
static async #onEditImage(event, target) {
const attr = target.dataset.edit ?? "img"
const current = foundry.utils.getProperty(this.document, attr)
const fp = new FilePicker({
current,
type: "image",
callback: (path) => this.document.update({ [attr]: path }),
top: this.position.top + 40,
left: this.position.left + 10
})
return fp.browse()
}
static #onClickDamage(event, target) {
// Les radios de dégâts sont 1-based dans le template (value="{{@index}}" avec index 1..max)
// mais le stockage est 0-based. On soustrait 1 avant de sauvegarder.
const prop = target.name
const value = parseInt(target.value) - 1
this.document.update({ [prop]: value })
}
static async #onOpenTraits(event, target) {
const { TraitSelector } = await import("../../system/applications.mjs")
new TraitSelector(this.document).render(true)
}
}
@@ -0,0 +1,98 @@
import { VermineBaseActorSheet } from "./base-actor-sheet.mjs"
export class VermineCharacterSheetV2 extends VermineBaseActorSheet {
static DEFAULT_OPTIONS = {
classes: ["character"],
position: { width: 860, height: 720 },
window: { contentClasses: ["character-content"] },
actions: {
addSpecialty: VermineCharacterSheetV2.#onAddSpecialty
}
}
static PARTS = {
main: { template: "systems/vermine2047/templates/actor/appv2/character-main.hbs" },
tabs: { template: "templates/generic/tab-navigation.hbs" },
abilities: { template: "systems/vermine2047/templates/actor/appv2/character-abilities.hbs" },
totem: { template: "systems/vermine2047/templates/actor/appv2/character-totem.hbs" },
equipment: { template: "systems/vermine2047/templates/actor/appv2/character-equipment.hbs" },
stories: { template: "systems/vermine2047/templates/actor/appv2/character-stories.hbs" },
combat: { template: "systems/vermine2047/templates/actor/appv2/character-combat.hbs" }
}
tabGroups = { sheet: "abilities" }
#getTabs() {
const tabs = {
abilities: { id: "abilities", group: "sheet", icon: "fas fa-address-card", label: "VERMINE.tabs.abilities" },
totem: { id: "totem", group: "sheet", icon: "fas fa-star", label: "VERMINE.tabs.totem" },
equipment: { id: "equipment", group: "sheet", icon: "fas fa-hammer", label: "VERMINE.tabs.equipment" },
stories: { id: "stories", group: "sheet", icon: "fas fa-book-open-reader", label: "VERMINE.tabs.stories" },
combat: { id: "combat", group: "sheet", icon: "fas fa-medal", label: "VERMINE.tabs.combat" }
}
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()
return context
}
async _preparePartContext(partId, context) {
const doc = this.document
context.systemFields = doc.system.schema.fields
switch (partId) {
case "main": break
case "abilities":
context.tab = context.tabs.abilities
break
case "totem":
context.tab = context.tabs.totem
context.abilities = doc.itemTypes.ability.filter(i => i.system.type !== "totem")
context.totem_abilities = doc.itemTypes.ability.filter(i => i.system.type === "totem")
context.specialties = doc.itemTypes.specialty
context.backgrounds = doc.itemTypes.background
context.traumas = doc.itemTypes.trauma
context.evolutions = doc.itemTypes.evolution
break
case "equipment":
context.tab = context.tabs.equipment
context.gear = doc.itemTypes.item
context.weapons = doc.itemTypes.weapon
context.defenses = doc.itemTypes.defense
context.vehicles = doc.itemTypes.vehicle
break
case "stories":
context.tab = context.tabs.stories
break
case "combat":
context.tab = context.tabs.combat
const { prepareActiveEffectCategories } = await import("../../system/effects.mjs")
context.effects = prepareActiveEffectCategories(doc.effects)
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 async #onAddSpecialty(event, target) {
const skillKey = target.dataset.skill
const name = game.i18n.localize("ITEMS.new_specialty")
const itemData = { name, type: "specialty" }
if (skillKey) itemData.system = { skill: skillKey }
await this.document.createEmbeddedDocuments("Item", [itemData])
}
}
@@ -0,0 +1,67 @@
import { VermineBaseActorSheet } from "./base-actor-sheet.mjs"
export class VermineCreatureSheetV2 extends VermineBaseActorSheet {
static DEFAULT_OPTIONS = {
classes: ["creature"],
position: { width: 700, height: 650 },
window: { contentClasses: ["creature-content"] }
}
static PARTS = {
main: { template: "systems/vermine2047/templates/actor/appv2/creature-main.hbs" },
tabs: { template: "templates/generic/tab-navigation.hbs" },
info: { template: "systems/vermine2047/templates/actor/appv2/creature-info.hbs" },
stats: { template: "systems/vermine2047/templates/actor/appv2/creature-stats.hbs" },
combat: { template: "systems/vermine2047/templates/actor/appv2/creature-combat.hbs" },
effects: { template: "systems/vermine2047/templates/actor/appv2/creature-effects.hbs" }
}
tabGroups = { sheet: "info" }
#getTabs() {
const tabs = {
info: { id: "info", group: "sheet", icon: "fas fa-info-circle", label: "VERMINE.information" },
stats: { id: "stats", group: "sheet", icon: "fas fa-chart-bar", label: "VERMINE.stats" },
combat: { id: "combat", group: "sheet", icon: "fas fa-sword", label: "VERMINE.combat" },
effects: { id: "effects", group: "sheet", icon: "fas fa-magic", label: "UI.effects.name" }
}
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()
return context
}
async _preparePartContext(partId, context) {
const doc = this.document
switch (partId) {
case "main": break
case "info":
context.tab = context.tabs.info
break
case "stats":
context.tab = context.tabs.stats
context.patternLabel = doc.system.pattern?.value ? CONFIG.VERMINE.creaturePatternLevels[doc.system.pattern.value]?.label : ""
context.sizeLabel = doc.system.size?.value || ""
context.roleLabel = doc.system.role?.value ? CONFIG.VERMINE.creatureRoleLevels[doc.system.role.value]?.label : ""
context.packLabel = doc.system.pack?.value || game.i18n.localize("VERMINE.none")
break
case "combat":
context.tab = context.tabs.combat
break
case "effects":
context.tab = context.tabs.effects
const { prepareActiveEffectCategories } = await import("../../system/effects.mjs")
context.effects = prepareActiveEffectCategories(doc.effects)
break
}
return context
}
}
+139
View File
@@ -0,0 +1,139 @@
import { VermineBaseActorSheet } from "./base-actor-sheet.mjs"
export 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
}
}
static PARTS = {
main: { template: "systems/vermine2047/templates/actor/appv2/group-main.hbs" },
tabs: { template: "templates/generic/tab-navigation.hbs" },
info: { template: "systems/vermine2047/templates/actor/appv2/group-info.hbs" },
gear: { template: "systems/vermine2047/templates/actor/appv2/group-gear.hbs" },
road: { template: "systems/vermine2047/templates/actor/appv2/group-road.hbs" },
reserve: { template: "systems/vermine2047/templates/actor/appv2/group-reserve.hbs" }
}
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 }
}
}
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
context.specialties = doc.itemTypes.specialty
context.backgrounds = doc.itemTypes.background
context.traumas = doc.itemTypes.trauma
context.evolutions = doc.itemTypes.evolution
break
case "gear":
context.tab = context.tabs.gear
context.gear = doc.itemTypes.item
context.weapons = doc.itemTypes.weapon
context.defenses = doc.itemTypes.defense
break
case "road":
context.tab = context.tabs.road
context.vehicles = doc.itemTypes.vehicle
break
case "reserve":
context.tab = context.tabs.reserve
break
}
return context
}
// Actions : délégation aux applications AppV1 existantes pour TotemPicker/ActorPicker
static async #onChooseTotem(event, target) {
const { TotemPicker } = await import("../../system/applications.mjs")
new TotemPicker(target, this.document).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 })
}
}
@@ -0,0 +1,73 @@
import { VermineBaseItemSheet } from "./base-item-sheet.mjs"
// ── Item générique ────────────────────────────────────────────────────
export class VermineItemSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["item-gear"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-item-sheet.hbs" } }
}
// ── Arme ──────────────────────────────────────────────────────────────
export class VermineWeaponSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["weapon"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-weapon-sheet.hbs" } }
}
// ── Défense ───────────────────────────────────────────────────────────
export class VermineDefenseSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["defense"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-defense-sheet.hbs" } }
}
// ── Véhicule ──────────────────────────────────────────────────────────
export class VermineVehicleSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["vehicle"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-vehicle-sheet.hbs" } }
}
// ── Capacité ──────────────────────────────────────────────────────────
export class VermineAbilitySheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["ability"], position: { width: 560 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-ability-sheet.hbs" } }
}
// ── Spécialité ────────────────────────────────────────────────────────
export class VermineSpecialtySheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["specialty"], position: { width: 400 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-specialty-sheet.hbs" } }
}
// ── Historique ────────────────────────────────────────────────────────
export class VermineBackgroundSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["background"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-background-sheet.hbs" } }
}
// ── Traumatisme ───────────────────────────────────────────────────────
export class VermineTraumaSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["trauma"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-trauma-sheet.hbs" } }
}
// ── Évolution ─────────────────────────────────────────────────────────
export class VermineEvolutionSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["evolution"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-evolution-sheet.hbs" } }
}
// ── Rumeur ────────────────────────────────────────────────────────────
export class VermineRumorSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["rumor"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-rumor-sheet.hbs" } }
}
// ── Cible ─────────────────────────────────────────────────────────────
export class VermineTargetSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["target"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-target-sheet.hbs" } }
}
// ── Rite ──────────────────────────────────────────────────────────────
export class VermineRiteSheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["rite"], position: { width: 520 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-rite-sheet.hbs" } }
}
+76
View File
@@ -0,0 +1,76 @@
import { VermineBaseActorSheet } from "./base-actor-sheet.mjs"
export class VermineNpcSheetV2 extends VermineBaseActorSheet {
static DEFAULT_OPTIONS = {
classes: ["npc"],
position: { width: 750, height: 680 },
window: { contentClasses: ["npc-content"] }
}
static PARTS = {
main: { template: "systems/vermine2047/templates/actor/appv2/npc-main.hbs" },
tabs: { template: "templates/generic/tab-navigation.hbs" },
characteristics: { template: "systems/vermine2047/templates/actor/appv2/npc-characteristics.hbs" },
skills: { template: "systems/vermine2047/templates/actor/appv2/npc-skills.hbs" },
threat: { template: "systems/vermine2047/templates/actor/appv2/npc-threat.hbs" },
combat: { template: "systems/vermine2047/templates/actor/appv2/npc-combat.hbs" },
notes: { template: "systems/vermine2047/templates/actor/appv2/npc-notes.hbs" }
}
tabGroups = { sheet: "characteristics" }
#getTabs() {
const tabs = {
characteristics: { id: "characteristics", group: "sheet", icon: "fas fa-dice", label: "VERMINE.abilities" },
skills: { id: "skills", group: "sheet", icon: "fas fa-brain", label: "VERMINE.skills" },
threat: { id: "threat", group: "sheet", icon: "fas fa-exclamation-triangle", label: "ADVERSITY.threat" },
combat: { id: "combat", group: "sheet", icon: "fas fa-sword", label: "VERMINE.combat" },
notes: { id: "notes", group: "sheet", icon: "fas fa-sticky-note", label: "IDENTITY.notes" }
}
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()
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")
}
}
async _preparePartContext(partId, context) {
const doc = this.document
switch (partId) {
case "main": break
case "characteristics":
context.tab = context.tabs.characteristics
break
case "skills":
context.tab = context.tabs.skills
break
case "threat":
context.tab = context.tabs.threat
break
case "combat":
context.tab = context.tabs.combat
const { prepareActiveEffectCategories } = await import("../../system/effects.mjs")
context.effects = prepareActiveEffectCategories(doc.effects)
break
case "notes":
context.tab = context.tabs.notes
break
}
return context
}
}
+7
View File
@@ -0,0 +1,7 @@
/**
* Module de ré-export des classes de documents
* Compatible avec Foundry V2
*/
export { default as VermineActor } from "./actor.mjs";
export { default as VermineItem } from "./item.mjs";
+1 -38
View File
@@ -3,44 +3,7 @@
* Extend the base Actor document by defining a custom roll data structure which is ideal for the Simple system.
* @extends {Actor}
*/
export class VermineActor extends Actor {
/** @override */
prepareBaseData() {
// Data modifications in this step occur before processing embedded
// documents or derived data.
// Initialize wound data to prevent undefined errors with active effects
if (!this.system.minorWound) this.system.minorWound = { value: 0, min: 0, max: 5, threshold: 1 };
if (!this.system.majorWound) this.system.majorWound = { value: 0, min: 0, max: 4, threshold: 4 };
if (!this.system.deadlyWound) this.system.deadlyWound = { value: 0, min: 0, max: 2, threshold: 8 };
// Initialize combatStatus to prevent errors
if (!this.system.combatStatus) {
this.system.combatStatus = { difficulty: "9", label: "Passif" };
}
if (this.type == 'character') {
}
}
/** @override */
prepareEmbeddedDocuments() {
// Check if effects are already being applied in this preparation cycle
// In Foundry V11+, the parent prepareEmbeddedDocuments() calls applyActiveEffects()
// If this is called multiple times in the same cycle, we get the "phase already completed" error
const phase = this.effects?.applicationPhase;
// If we're already in the middle of applying effects (initial or final phase),
// don't call super as it would try to apply effects again
if (phase === "initial" || phase === "final") {
return;
}
// If effects haven't been applied yet, proceed normally
super.prepareEmbeddedDocuments();
}
export default class VermineActor extends Actor {
/**
* @override
+6 -13
View File
@@ -2,7 +2,7 @@
* Extend the basic Item with some very simple modifications.
* @extends {Item}
*/
export class VermineItem extends Item {
export default class VermineItem extends Item {
/**
* Augment the basic Item data model with additional dynamic data.
*/
@@ -15,8 +15,7 @@ export class VermineItem extends Item {
const actorType = (this.actor !== null) ? this.actor.type : 'character';
const itemType = this.type;
// Vérifie si une méthode spécifique au type existe// preparedData specifique au type
// Vérifie si une méthode spécifique au type existe
if (typeof this[`prepare${itemType.charAt(0).toUpperCase() + itemType.slice(1)}Data`] === 'function') {
this[`prepare${itemType.charAt(0).toUpperCase() + itemType.slice(1)}Data`]();
}
@@ -26,15 +25,14 @@ export class VermineItem extends Item {
this.damagedLabel = this.system.damages.state[parseInt(this.system.damages?.value) - 1];
switch (this.damagedLabel) {
case "endommagé":
this.damagedIcon = '<i class="fas fa-exclamation-circle" style:"color="yellow"></i>';
this.damagedIcon = '<i class="fas fa-exclamation-circle" style="color=yellow"></i>';
break;
case "défectueux":
this.damagedIcon = '<i class="fas fa-exclamation-triangle" style:"color="orange"></i>';
this.damagedIcon = '<i class="fas fa-exclamation-triangle" style="color=orange"></i>';
break;
case "hors d'usage":
this.damagedIcon = '<i class="fas fa-star-exclamation" style:"color="red"></i>';
this.damagedIcon = '<i class="fas fa-star-exclamation" style="color=red"></i>';
break;
}
}
}
@@ -44,11 +42,9 @@ export class VermineItem extends Item {
const actorType = (this.actor !== null) ? this.actor.type : 'character';
if (this.system.type == "") {
// console.log('je suis une capacité, avec pour sous-type', this.system.type, actorType);
this.system.type = actorType;
}
if (this.system.totem == "" && this.actor !== null && this.actor.system.identity.totem != "") {
// console.log('je suis une capacité, avec pour sous-type', this.system.type, actorType);
this.system.totem = this.actor.system.identity.totem;
}
}
@@ -79,16 +75,13 @@ export class VermineItem extends Item {
const rollMode = game.settings.get('core', 'rollMode');
const label = `[${item.type}] ${item.name}`;
// If there's no roll data, send a chat message.
let mess = {
speaker: speaker,
rollMode: rollMode,
flavor: label,
};
mess.content = await renderTemplate(`systems/vermine2047/templates/item/chatCards/${this.type}.hbs`, { item: this, message: mess }) ?? null;
mess.content = await foundry.applications.handlebars.renderTemplate(`systems/vermine2047/templates/item/chatCards/${this.type}.hbs`, { item: this, message: mess }) ?? null;
ChatMessage.create(mess)
}
}
+16
View File
@@ -0,0 +1,16 @@
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 VermineItemData } from "./item.mjs"
export { default as VermineWeaponData } from "./weapon.mjs"
export { default as VermineDefenseData } from "./defense.mjs"
export { default as VermineVehicleData } from "./vehicle.mjs"
export { default as VermineAbilityData } from "./ability.mjs"
export { default as VermineSpecialtyData } from "./specialty.mjs"
export { default as VermineBackgroundData } from "./background.mjs"
export { default as VermineTraumaData } from "./trauma.mjs"
export { default as VermineEvolutionData } from "./evolution.mjs"
export { default as VermineRumorData } from "./rumor.mjs"
export { default as VermineTargetData } from "./target.mjs"
export { default as VermineRiteData } from "./rite.mjs"
+285
View File
@@ -0,0 +1,285 @@
/**
* Schémas partagés pour les DataModels Vermine 2047.
* Fonctions factory retournant des objets SchemaField réutilisables.
*/
/**
* Retourne un schema pour une blessure (minor/major/deadly)
* @param {number} defaultThreshold
* @param {number} defaultMax
* @returns {Object}
*/
export function woundSchema(defaultThreshold = 1, defaultMax = 5) {
const fields = foundry.data.fields
const reqInt = { required: true, nullable: false, integer: true }
return {
threshold: new fields.NumberField({ ...reqInt, initial: defaultThreshold, min: 0 }),
value: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
min: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
max: new fields.NumberField({ ...reqInt, initial: defaultMax, min: 0 })
}
}
/**
* Schema des 3 types de blessures présents sur tous les acteurs.
*/
export function woundsSchema() {
const fields = foundry.data.fields
return new fields.SchemaField({
minorWound: new fields.SchemaField(woundSchema(1, 5)),
majorWound: new fields.SchemaField(woundSchema(4, 4)),
deadlyWound: new fields.SchemaField(woundSchema(8, 2))
})
}
/**
* Statut de combat (offensif/actif/passif).
*/
export function combatStatusSchema(defaultDifficulty = "7") {
const fields = foundry.data.fields
return new fields.SchemaField({
label: new fields.StringField({ required: true, nullable: false, initial: "" }),
difficulty: new fields.StringField({ required: true, nullable: false, initial: defaultDifficulty })
})
}
/**
* Description d'équipement.
*/
export function equipmentSchema() {
const fields = foundry.data.fields
return new fields.SchemaField({
description: new fields.HTMLField({ required: true, initial: "", textSearch: true })
})
}
/**
* Attribut avec value/min/max.
* @param {number} defaultVal
* @param {number} defaultMin
* @param {number} defaultMax
*/
export function attributeSchema(defaultVal = 0, defaultMin = 0, defaultMax = 10) {
const fields = foundry.data.fields
const reqInt = { required: true, nullable: false, integer: true }
return new fields.SchemaField({
value: new fields.NumberField({ ...reqInt, initial: defaultVal, min: defaultMin }),
min: new fields.NumberField({ ...reqInt, initial: defaultMin, min: 0 }),
max: new fields.NumberField({ ...reqInt, initial: defaultMax, min: 0 })
})
}
/**
* Caractéristique (capacité) avec catégorie.
* @param {string} category - physical, manual, mental, social
*/
export function abilityField(category) {
const fields = foundry.data.fields
const reqInt = { required: true, nullable: false, integer: true }
return new fields.SchemaField({
value: new fields.NumberField({ ...reqInt, initial: 1, min: 0, max: 5 }),
min: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
max: new fields.NumberField({ ...reqInt, initial: 5, min: 0 }),
category: new fields.StringField({ required: true, nullable: false, initial: category })
})
}
/**
* Les 8 caractéristiques communes à character et npc.
*/
export function abilitiesSchema() {
const fields = foundry.data.fields
return new fields.SchemaField({
vigor: abilityField("physical"),
health: abilityField("physical"),
precision: abilityField("manual"),
reflexes: abilityField("manual"),
knowledge: abilityField("mental"),
perception: abilityField("mental"),
will: abilityField("social"),
empathy: abilityField("social")
})
}
/**
* Une compétence individuelle.
* @param {string} category
* @param {number} rarity - 0, 1, ou 2
*/
export function skillField(category, rarity = 0) {
const fields = foundry.data.fields
const reqInt = { required: true, nullable: false, integer: true }
return new fields.SchemaField({
value: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 5 }),
min: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
max: new fields.NumberField({ ...reqInt, initial: 5, min: 0 }),
category: new fields.StringField({ required: true, nullable: false, initial: category }),
rarity: new fields.NumberField({ ...reqInt, initial: rarity, min: 0, max: 2 })
})
}
/**
* Les 30 compétences (character et npc).
*/
export function skillsSchema() {
const fields = foundry.data.fields
return new fields.SchemaField({
// man
arts: skillField("man", 1),
civilization: skillField("man", 2),
psychology: skillField("man", 1),
rumors: skillField("man", 0),
healing: skillField("man", 1),
// animal
animalism: skillField("animal", 1),
dissection: skillField("animal", 2),
wildlife: skillField("animal", 1),
repulsion: skillField("animal", 0),
tracks: skillField("animal", 0),
// tool
crafting: skillField("tool", 2),
diy: skillField("tool", 0),
mecanical: skillField("tool", 2),
piloting: skillField("tool", 1),
technology: skillField("tool", 2),
// weapon
firearms: skillField("weapon", 2),
archery: skillField("weapon", 0),
armory: skillField("weapon", 2),
throwing: skillField("weapon", 0),
melee: skillField("weapon", 0),
// survival
alertness: skillField("survival", 0),
atletics: skillField("survival", 0),
food: skillField("survival", 0),
stealth: skillField("survival", 0),
close: skillField("survival", 0),
// world
environment: skillField("world", 1),
flora: skillField("world", 1),
road: skillField("world", 0),
toxics: skillField("world", 2),
ruins: skillField("world", 1)
})
}
/**
* Catégories de compétences avec domaine de prédilection.
*/
export function skillCategoriesSchema() {
const fields = foundry.data.fields
return new fields.SchemaField({
preferred: new fields.StringField({ required: true, nullable: false, initial: "" }),
man: new fields.SchemaField({
label: new fields.StringField({ required: true, initial: "VERMINE.skill_category.man" }),
preferred: new fields.BooleanField({ required: true, initial: false })
}),
animal: new fields.SchemaField({
label: new fields.StringField({ required: true, initial: "VERMINE.skill_category.animal" }),
preferred: new fields.BooleanField({ required: true, initial: false })
}),
tool: new fields.SchemaField({
label: new fields.StringField({ required: true, initial: "VERMINE.skill_category.tool" }),
preferred: new fields.BooleanField({ required: true, initial: false })
}),
weapon: new fields.SchemaField({
label: new fields.StringField({ required: true, initial: "VERMINE.skill_category.weapon" }),
preferred: new fields.BooleanField({ required: true, initial: false })
}),
survival: new fields.SchemaField({
label: new fields.StringField({ required: true, initial: "VERMINE.skill_category.survival" }),
preferred: new fields.BooleanField({ required: true, initial: false })
}),
world: new fields.SchemaField({
label: new fields.StringField({ required: true, initial: "VERMINE.skill_category.world" }),
preferred: new fields.BooleanField({ required: true, initial: false })
})
})
}
// ── Item shared schemas ──────────────────────────────────────────────────
const reqInt = { required: true, nullable: false, integer: true }
/**
* Rareté avec handicap.
*/
export function raritySchema() {
const fields = foundry.data.fields
return new fields.SchemaField({
value: new fields.NumberField({ ...reqInt, initial: 3, min: 1, max: 5 }),
handicap: new fields.NumberField({ ...reqInt, initial: 0, min: 0 })
})
}
/**
* Dégâts des items (hors arme).
*/
export function itemDamagesSchema() {
const fields = foundry.data.fields
return new fields.SchemaField({
value: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 5 }),
min: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
max: new fields.NumberField({ ...reqInt, initial: 5, min: 0 }),
pannes: new fields.ArrayField(new fields.StringField({ required: true, initial: "" }), {
initial: ["mineure", "mineure", "grave", "grave", "critique"]
}),
state: new fields.ArrayField(new fields.StringField({ required: true, initial: "" }), {
initial: ["endommagé", "endommagé", "défectueux", "défectueux", "hors d'usage"]
}),
effect: new fields.ArrayField(new fields.StringField({ required: true, initial: "" }), {
initial: ["bonus annulé", "bonus annulé", "malus 1D", "malus 1D", "inutilisable"]
})
})
}
/**
* Base commune à tous les items (template "base" dans l'ancien template.json).
*/
export function baseItemSchema() {
const fields = foundry.data.fields
return {
description: new fields.HTMLField({ required: true, initial: "", textSearch: true }),
rarity: raritySchema(),
reliability: new fields.NumberField({ ...reqInt, initial: 3, min: 1, max: 5 }),
handicap: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
quantity: new fields.NumberField({ ...reqInt, initial: 1, min: 1 }),
weight: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
traits: new fields.ObjectField({ required: true, initial: {} }),
damages: itemDamagesSchema()
}
}
/**
* Template "list" pour les items abstraits (ability, background, trauma, evolution, rumor, target).
* Version légère avec seulement description.
*/
export function listItemSchema() {
const fields = foundry.data.fields
return {
description: new fields.HTMLField({ required: true, initial: "", textSearch: true })
}
}
/**
* Schéma d'apprentissage pour les abilities.
*/
export function learnSchema() {
const fields = foundry.data.fields
return new fields.SchemaField({
threshold: new fields.NumberField({ ...reqInt, initial: 5, min: 0 }),
hindrance: new fields.NumberField({ ...reqInt, initial: 0, min: 0 })
})
}
/**
* Niveau générique (value/min/max).
*/
export function levelSchema(defaultVal = 1, defaultMin = 1, defaultMax = 5) {
const fields = foundry.data.fields
return new fields.SchemaField({
value: new fields.NumberField({ ...reqInt, initial: defaultVal, min: defaultMin }),
min: new fields.NumberField({ ...reqInt, initial: defaultMin, min: 0 }),
max: new fields.NumberField({ ...reqInt, initial: defaultMax, min: 0 })
})
}
+25
View File
@@ -0,0 +1,25 @@
import { listItemSchema, learnSchema, levelSchema } from "./_shared.mjs"
/**
* DataModel pour les items de type "ability" (capacités, pouvoirs).
* @augments {foundry.abstract.TypeDataModel}
*/
export default class VermineAbilityData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.item.ability"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
return {
...listItemSchema(),
type: new fields.StringField({ required: true, initial: "" }),
totem: new fields.StringField({ required: true, initial: "" }),
learn: learnSchema(),
level: levelSchema(1, 1, 3),
effects: new fields.ArrayField(new fields.StringField({ required: true, initial: "" }), {
initial: []
})
}
}
}
+20
View File
@@ -0,0 +1,20 @@
import { listItemSchema } from "./_shared.mjs"
/**
* DataModel pour les items de type "background" (historiques, origines).
* @augments {foundry.abstract.TypeDataModel}
*/
export default class VermineBackgroundData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.item.background"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
const reqInt = { required: true, nullable: false, integer: true }
return {
...listItemSchema(),
cost: new fields.NumberField({ ...reqInt, initial: 1, min: 0 })
}
}
}
+215
View File
@@ -0,0 +1,215 @@
/**
* DataModel pour les acteurs de type "character" (personnage).
* Étend foundry.abstract.TypeDataModel.
*/
import {
woundSchema,
combatStatusSchema,
equipmentSchema,
attributeSchema,
abilitiesSchema,
skillCategoriesSchema,
skillsSchema
} from "./_shared.mjs"
export default class VermineCharacterData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.character"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
return {
// Blessures (base)
minorWound: new fields.SchemaField(woundSchema(1, 5)),
majorWound: new fields.SchemaField(woundSchema(4, 4)),
deadlyWound: new fields.SchemaField(woundSchema(8, 2)),
// Statut de combat (base)
combatStatus: combatStatusSchema("7"),
// Adaptation (totems humain/adapté)
adaptation: new fields.SchemaField({
value: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 1, min: 0, max: 5 }),
min: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
max: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 5, min: 0 }),
totems: new fields.SchemaField({
human: new fields.SchemaField({
value: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 1, min: 0, max: 3 }),
min: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
max: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 3, min: 0 })
}),
adapted: new fields.SchemaField({
value: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 1, min: 0, max: 3 }),
min: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
max: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 3, min: 0 })
})
})
}),
// Identité
identity: new fields.SchemaField({
height: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
weight: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
totem: new fields.StringField({ required: true, nullable: false, initial: "" }),
age: new fields.StringField({ required: true, nullable: false, initial: "15" }),
ageType: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 2 }),
profile: 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: "" }),
objectives: new fields.StringField({ required: true, nullable: false, initial: "" }),
relations: new fields.StringField({ required: true, nullable: false, initial: "" }),
biography: new fields.StringField({ required: true, nullable: false, initial: "" })
}),
// Équipement
equipment: equipmentSchema(),
// Attributs (XP, réputation, sang-froid, effort)
attributes: new fields.SchemaField({
xp: attributeSchema(0, 0, 10),
reputation: attributeSchema(0, 0, 10),
self_control: attributeSchema(0, 0, 5),
effort: attributeSchema(0, 0, 5)
}),
// Rencontres
encounters: new fields.ArrayField(new fields.StringField({ required: true, nullable: false, initial: "" })),
// Caractéristiques (8)
abilities: abilitiesSchema(),
// Catégories de compétences
skill_categories: skillCategoriesSchema(),
// Compétences (30)
skills: skillsSchema()
}
}
/** @override */
prepareDerivedData() {
super.prepareDerivedData()
// 1. Déterminer la tranche d'âge
this._setAgeType()
// 2. Calculer les modificateurs de caractéristiques
this._setAbilityModifiers()
// 3. Calculer les réserves (sang-froid et effort)
this._setSelfControlMax()
this._setEffortMax()
// 4. Calculer les seuils de blessures
this._setWoundThresholds()
// 5. Mettre à jour le statut de combat
this._updateCombatStatus()
}
/**
* Détermine la tranche d'âge (1=jeune, 2=adulte, 3=vieux)
* à partir de l'âge et de la config VERMINE.AgeTypes.
*/
_setAgeType() {
const age = this.identity.age
const ageTypes = CONFIG.VERMINE.AgeTypes
for (const [type, cfg] of Object.entries(ageTypes)) {
if (age >= parseInt(cfg.beginning, 10)) {
this.identity.ageType = parseInt(type, 10)
}
}
}
/**
* Calcule les modificateurs de caractéristiques (règle d20).
*/
_setAbilityModifiers() {
for (const ability of Object.values(this.abilities)) {
ability.mod = Math.floor((ability.value - 10) / 2)
}
}
/**
* Calcule le max de sang-froid :
* somme des caractéristiques mentales + sociales + modificateur d'âge.
*/
_setSelfControlMax() {
const abilities = Object.values(this.abilities)
const modFromAge = this._getModFromAgeSelfControl()
const sum = abilities
.filter(a => a.category === "mental" || a.category === "social")
.reduce((acc, a) => acc + a.value, 0)
this.attributes.self_control.max = sum + modFromAge
}
/**
* Calcule le max d'effort :
* somme des caractéristiques physiques + manuelles + modificateur d'âge.
*/
_setEffortMax() {
const abilities = Object.values(this.abilities)
const modFromAge = this._getModFromAgeEffort()
const sum = abilities
.filter(a => a.category === "physical" || a.category === "manual")
.reduce((acc, a) => acc + a.value, 0)
this.attributes.effort.max = sum + modFromAge
}
/**
* Calcule les seuils de blessures à partir de la Santé.
*/
_setWoundThresholds() {
const health = this.abilities.health.value
const ageMods = this._getModFromAgeWounds()
this.minorWound.threshold = health
this.majorWound.threshold = health + 3
this.deadlyWound.threshold = (health + 7 < 11) ? health + 7 : 10
this.minorWound.max = 4 + ageMods.l
this.majorWound.max = 3 + ageMods.h
this.deadlyWound.max = 2 + ageMods.d
}
/**
* Met à jour le label du statut de combat en fonction de la difficulté.
*/
_updateCombatStatus() {
const difficulty = parseInt(this.combatStatus.difficulty) || 9
let newLabel = "Passif"
switch (difficulty) {
case 5: newLabel = "Offensif"; break
case 7: newLabel = "Actif"; break
case 9: newLabel = "Passif"; break
}
if (this.combatStatus.label !== newLabel) {
this.combatStatus.label = newLabel
}
}
// ── Modificateurs liés à l'âge ──────────────────────────────────────
/** @returns {number} Modificateur de sang-froid selon l'âge. */
_getModFromAgeSelfControl() {
return this.identity.ageType === 1 ? -1 : 0
}
/** @returns {number} Modificateur d'effort selon l'âge. */
_getModFromAgeEffort() {
if (this.identity.ageType === 1) return -1
if (this.identity.ageType === 3) return -2
return 0
}
/** @returns {{l: number, h: number, d: number}} Modificateurs de blessures selon l'âge. */
_getModFromAgeWounds() {
if (this.identity.ageType === 1) return { l: 0, h: 0, d: -1 }
if (this.identity.ageType === 3) return { l: -1, h: -1, d: -1 }
return { l: 0, h: 0, d: 0 }
}
}
+182
View File
@@ -0,0 +1,182 @@
/**
* DataModel pour les acteurs de type "creature" (créature).
* Étend foundry.abstract.TypeDataModel.
*/
import {
woundSchema,
combatStatusSchema,
equipmentSchema,
attributeSchema
} from "./_shared.mjs"
export default class VermineCreatureData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.creature"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
return {
// Blessures (base)
minorWound: new fields.SchemaField(woundSchema(1, 5)),
majorWound: new fields.SchemaField(woundSchema(4, 4)),
deadlyWound: new fields.SchemaField(woundSchema(8, 2)),
// Statut de combat (base)
combatStatus: combatStatusSchema(),
// Identité
identity: new fields.SchemaField({
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: "" }),
notes: new fields.StringField({ required: true, nullable: false, initial: "" }),
biography: new fields.StringField({ required: true, nullable: false, initial: "" })
}),
// Compétences (description libre)
skills: new fields.StringField({ required: true, nullable: false, initial: "" }),
// Modes de jeu actifs
modes: new fields.SchemaField({
survival: new fields.BooleanField({ required: true, initial: true }),
nightmare: new fields.BooleanField({ required: true, initial: true }),
apocalypse: new fields.BooleanField({ required: true, initial: false })
}),
// Niveaux de créature (patron, taille, rôle, meute)
pattern: attributeSchema(1, 1, 4),
size: attributeSchema(1, 1, 3),
role: attributeSchema(1, 1, 4),
pack: attributeSchema(0, 0, 3),
// Valeurs calculées (dérivées de pattern/size/role/pack)
computed: new fields.SchemaField({
attack: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
damage: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
vigor: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
reaction: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
reactionBonus: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
pools: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
gear: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 9 }),
gearHindrance: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
protection: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 1 })
}),
// Équipement
equipment: equipmentSchema()
}
}
/** @override */
prepareDerivedData() {
super.prepareDerivedData()
// 1. Calculer les valeurs dérivées (attaque, dégâts, vigueur, etc.)
this._calculateCreatureComputedValues()
// 2. Calculer les seuils de blessures
this._calculateCreatureWoundThresholds()
// 3. Mettre à jour le statut de combat
this._updateCombatStatus()
}
/**
* Calcule les valeurs dérivées à partir des niveaux de patron, taille, rôle et meute.
* Utilise les configs CONFIG.VERMINE.creaturePatternLevels, .creatureSizeLevels,
* .creatureRoleLevels, .creaturePackLevels.
*
* Règles :
* - Attaque = pattern.attack + size.attack + pack.attack + role.reaction
* - Dégâts = pattern.damage + size.vigor + pack.damage
* - Vigueur = size.vigor + pack.damage
* - Réaction = role.reaction + role.reaction_bonus
*/
_calculateCreatureComputedValues() {
const patternLevel = this.pattern?.value || 1
const sizeLevel = this.size?.value || 1
const roleLevel = this.role?.value || 1
const packLevel = this.pack?.value || 0
const patternConfig = CONFIG.VERMINE.creaturePatternLevels[patternLevel] || {}
const sizeConfig = CONFIG.VERMINE.creatureSizeLevels[sizeLevel] || {}
const roleConfig = CONFIG.VERMINE.creatureRoleLevels[roleLevel] || {}
const packConfig = CONFIG.VERMINE.creaturePackLevels[packLevel] || {}
// Attaque : patron + taille + meute + réaction du rôle
this.computed.attack = (patternConfig.attack || 0)
+ (sizeConfig.attack || 0)
+ (packConfig.attack || 0)
+ (roleConfig.reaction || 0)
// Dégâts : patron + vigueur de taille + meute
this.computed.damage = (patternConfig.damage || 0)
+ (sizeConfig.vigor || 0)
+ (packConfig.damage || 0)
// Vigueur : taille + meute
this.computed.vigor = (sizeConfig.vigor || 0) + (packConfig.damage || 0)
// Réaction : rôle
this.computed.reaction = (roleConfig.reaction || 0) + (roleConfig.reaction_bonus || 0)
this.computed.reactionBonus = roleConfig.reaction_bonus || 0
// Réserves
this.computed.pools = roleConfig.pools || 0
// Équipement et handicap
this.computed.gear = roleConfig.gear || 9
this.computed.gearHindrance = roleConfig.gear_hindrance || 0
// Protection
this.computed.protection = roleConfig.protection || 1
}
/**
* Calcule les seuils de blessures à partir du patron, de la taille et de la meute.
* Les seuils sont la somme des valeurs correspondantes des trois sources.
*/
_calculateCreatureWoundThresholds() {
const patternLevel = this.pattern?.value || 1
const sizeLevel = this.size?.value || 1
const packLevel = this.pack?.value || 0
const patternConfig = CONFIG.VERMINE.creaturePatternLevels[patternLevel] || {}
const sizeConfig = CONFIG.VERMINE.creatureSizeLevels[sizeLevel] || {}
const packConfig = CONFIG.VERMINE.creaturePackLevels[packLevel] || {}
this.minorWound.threshold = (patternConfig.minorWound || 0)
+ (sizeConfig.minorWound || 0)
+ (packConfig.minorWound || 0)
this.majorWound.threshold = (patternConfig.majorWound || 0)
+ (sizeConfig.majorWound || 0)
+ (packConfig.majorWound || 0)
this.deadlyWound.threshold = (patternConfig.deadlyWound || 0)
+ (sizeConfig.deadlyWound || 0)
+ (packConfig.deadlyWound || 0)
// Max de blessures
this.minorWound.max = Math.min(5, this.minorWound.threshold + 2)
this.majorWound.max = Math.min(4, this.majorWound.threshold + 1)
this.deadlyWound.max = Math.min(2, this.deadlyWound.threshold)
}
/**
* Met à jour le label du statut de combat en fonction de la difficulté.
*/
_updateCombatStatus() {
const difficulty = parseInt(this.combatStatus.difficulty) || 9
let newLabel = "Passif"
switch (difficulty) {
case 5: newLabel = "Offensif"; break
case 7: newLabel = "Actif"; break
case 9: newLabel = "Passif"; break
}
if (this.combatStatus.label !== newLabel) {
this.combatStatus.label = newLabel
}
}
}
+26
View File
@@ -0,0 +1,26 @@
import { baseItemSchema } from "./_shared.mjs"
/**
* DataModel pour les items de type "defense" (protections, armures, boucliers).
* @augments {foundry.abstract.TypeDataModel}
*/
export default class VermineDefenseData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.item.defense"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
const reqInt = { required: true, nullable: false, integer: true }
return {
...baseItemSchema(),
level: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
specificLevel: new fields.SchemaField({
label: new fields.StringField({ required: true, initial: "" }),
level: new fields.NumberField({ ...reqInt, initial: 0, min: 0 })
}),
mobility: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
isShield: new fields.BooleanField({ required: true, initial: false })
}
}
}
+19
View File
@@ -0,0 +1,19 @@
import { listItemSchema, levelSchema } from './_shared.mjs'
/**
* DataModel pour les items de type "evolution" (évolutions du personnage).
* @augments {foundry.abstract.TypeDataModel}
*/
export default class VermineEvolutionData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.item.evolution"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
return {
...listItemSchema(),
level: levelSchema(1, 1, 4)
}
}
}
+164
View File
@@ -0,0 +1,164 @@
/**
* DataModel pour les acteurs de type "group" (groupe).
* Étend foundry.abstract.TypeDataModel.
*/
import {
woundSchema,
combatStatusSchema,
equipmentSchema,
attributeSchema,
levelSchema
} from "./_shared.mjs"
export default class VermineGroupData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.group"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
return {
// Blessures (base)
minorWound: new fields.SchemaField(woundSchema(1, 5)),
majorWound: new fields.SchemaField(woundSchema(4, 4)),
deadlyWound: new fields.SchemaField(woundSchema(8, 2)),
// Statut de combat (base)
combatStatus: combatStatusSchema(),
// Identité du groupe
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: "" }),
notes: new fields.StringField({ required: true, nullable: false, initial: "" })
}),
// Équipement
equipment: equipmentSchema(),
// Niveau du groupe (1-10)
level: levelSchema(1, 1, 10),
// Réputation
reputation: new fields.SchemaField({
value: new fields.NumberField({ required: true, nullable: true, integer: true, initial: 10, min: 2 }),
min: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 2, min: 0 }),
max: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 10, min: 0 })
}),
// Moral
morale: new fields.SchemaField({
level: new fields.StringField({ required: true, nullable: false, initial: "high" }),
value: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 7, min: 0, max: 7 }),
min: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
max: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 7, min: 0 })
}),
// Réserve
reserve: attributeSchema(0, 0, 10),
// Objectifs (majeurs et mineurs)
objectives: new fields.SchemaField({
major: new fields.ArrayField(new fields.StringField({ required: true, nullable: false, initial: "" })),
minor: new fields.ArrayField(new fields.StringField({ required: true, nullable: false, initial: "" }))
}),
// Capacités de groupe
groupAbilities: new fields.ArrayField(new fields.StringField({ required: true, nullable: false, initial: "" })),
// Membres (IDs d'acteurs)
members: new fields.ArrayField(new fields.StringField({ required: true, nullable: false, initial: "" })),
// Rencontres
encounters: new fields.ArrayField(new fields.StringField({ required: true, nullable: false, initial: "" }))
}
}
/** @override */
prepareDerivedData() {
super.prepareDerivedData()
// 1. Initialiser les données de groupe si absentes
this._initGroupData()
// 2. Calculer la réserve max selon le niveau
this._calculateGroupReserve()
// 3. Mettre à jour le moral selon la valeur de dés
this._updateGroupMorale()
// 4. Mettre à jour le statut de combat
this._updateCombatStatus()
}
/**
* Initialise les champs optionnels du groupe s'ils ne sont pas présents.
*/
_initGroupData() {
if (!this.objectives) {
this.objectives = { major: [], minor: [] }
}
if (!this.groupAbilities) {
this.groupAbilities = []
}
if (!this.reserve) {
this.reserve = { value: 0, min: 0, max: 10 }
}
}
/**
* Calcule la réserve max en fonction du niveau du groupe.
* Règle simplifiée : niveau × 2, plafonné à 10.
*/
_calculateGroupReserve() {
const level = this.level?.value || 1
this.reserve.max = Math.min(10, level * 2)
if (this.reserve.value > this.reserve.max) {
this.reserve.value = this.reserve.max
}
}
/**
* 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.
*/
_updateGroupMorale() {
const moraleValue = this.morale?.value || 0
// Ne pas écraser un niveau explicitement défini (sauf "high" qui est la valeur par défaut)
if (this.morale.level && this.morale.level !== "high") 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"
}
}
/**
* Met à jour le label du statut de combat en fonction de la difficulté.
*/
_updateCombatStatus() {
const difficulty = parseInt(this.combatStatus.difficulty) || 9
let newLabel = "Passif"
switch (difficulty) {
case 5: newLabel = "Offensif"; break
case 7: newLabel = "Actif"; break
case 9: newLabel = "Passif"; break
}
if (this.combatStatus.label !== newLabel) {
this.combatStatus.label = newLabel
}
}
}
+23
View File
@@ -0,0 +1,23 @@
/**
* DataModel pour les items de type "item" (équipement générique).
* @augments {foundry.abstract.TypeDataModel}
*/
export default class VermineItemData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.item.item"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
return {
...baseItemSchema(),
needSkill: new fields.SchemaField({
value: new fields.BooleanField({ required: true, initial: false }),
skill: new fields.StringField({ required: true, initial: "" })
})
}
}
}
// Import partagé — après la déclaration de classe car defineSchema est statique
import { baseItemSchema } from './_shared.mjs'
+162
View File
@@ -0,0 +1,162 @@
/**
* DataModel pour les acteurs de type "npc" (PNJ).
* Étend foundry.abstract.TypeDataModel.
*
* Note : le champ libre de compétences (texte descriptif) est nommé "freeSkills"
* pour éviter le conflit avec le SchemaField "skills" qui contient les 30 compétences.
*/
import {
woundSchema,
combatStatusSchema,
equipmentSchema,
attributeSchema,
abilitiesSchema,
skillCategoriesSchema,
skillsSchema
} from "./_shared.mjs"
export default class VermineNpcData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.npc"]
/**
* Migration des données avant traitement par le schéma.
* Avant DataModel, template.json définissait "skills" comme un champ texte libre
* pour les PNJ. Le DataModel réserve "skills" pour les 30 compétences individuelles
* (SchemaField) et utilise "freeSkills" pour le texte libre.
* @param {Object} source Données brutes avant validation du schéma
* @returns {Object} Données migrées
*/
static migrateData(source) {
if (typeof source.skills === "string") {
source.freeSkills = source.skills
}
return super.migrateData(source)
}
/** @override */
static defineSchema() {
const fields = foundry.data.fields
return {
// Blessures (base)
minorWound: new fields.SchemaField(woundSchema(1, 5)),
majorWound: new fields.SchemaField(woundSchema(4, 4)),
deadlyWound: new fields.SchemaField(woundSchema(8, 2)),
// Statut de combat (base, difficulté par défaut 9 pour PNJ)
combatStatus: combatStatusSchema("9"),
// Identité
identity: new fields.SchemaField({
name: 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: "" }),
totem: new fields.StringField({ required: true, nullable: false, initial: "" }),
theme: new fields.StringField({ required: true, nullable: false, initial: "" }),
notes: new fields.StringField({ required: true, nullable: false, initial: "" })
}),
// Attributs (XP, réputation, sang-froid, effort)
attributes: new fields.SchemaField({
xp: attributeSchema(0, 0, 10),
reputation: attributeSchema(0, 0, 10),
self_control: attributeSchema(0, 0, 5),
effort: attributeSchema(0, 0, 5)
}),
// Niveaux PNJ (menace, expérience, rôle)
threat: attributeSchema(1, 1, 4),
experience: attributeSchema(1, 1, 4),
role: attributeSchema(1, 1, 4),
// Compétences (les 30 compétences individuelles)
skills: skillsSchema(),
// Description libre des compétences (champ texte PNJ)
freeSkills: new fields.StringField({ required: true, nullable: false, initial: "" }),
// Catégories de compétences
skill_categories: skillCategoriesSchema(),
// Caractéristiques (8)
abilities: abilitiesSchema(),
// Équipement
equipment: equipmentSchema()
}
}
/** @override */
prepareDerivedData() {
super.prepareDerivedData()
// 1. Calculer les seuils de blessures selon le niveau de menace
this._setNpcWoundThresholds()
// 2. Calculer les réserves selon le niveau de rôle
this._setNpcAttributes()
// 3. Définir les libellés des caractéristiques
this._setAbilityLabels()
// 4. Mettre à jour le statut de combat
this._updateCombatStatus()
}
/**
* Calcule les seuils de blessures à partir du niveau de menace.
* Utilise CONFIG.VERMINE.npcThreatLevels.
*/
_setNpcWoundThresholds() {
const health = this.abilities?.health?.value || 1
const threatLevel = this.threat?.value || 1
const threatConfig = CONFIG.VERMINE.npcThreatLevels[threatLevel] || {}
this.minorWound.threshold = threatConfig.minorWound || health
this.majorWound.threshold = threatConfig.majorWound || (health + 3)
this.deadlyWound.threshold = threatConfig.deadlyWound || (health + 7 < 11 ? health + 7 : 10)
this.minorWound.max = threatConfig.minorWound || 4
this.majorWound.max = threatConfig.majorWound || 3
this.deadlyWound.max = threatConfig.deadlyWound || 2
}
/**
* Définit les attributs dérivés (effort, sang-froid) selon le niveau de rôle.
* Utilise CONFIG.VERMINE.npcRoleLevels.
*/
_setNpcAttributes() {
const roleLevel = this.role?.value || 1
const roleConfig = CONFIG.VERMINE.npcRoleLevels[roleLevel] || {}
this.attributes.effort.max = roleConfig.pools || 0
this.attributes.self_control.max = roleConfig.reaction_bonus || 0
}
/**
* Définit les libellés localisés des caractéristiques.
*/
_setAbilityLabels() {
for (const [k, v] of Object.entries(this.abilities)) {
v.label = game.i18n.localize(CONFIG.VERMINE.abilities[k]) ?? k
}
}
/**
* Met à jour le label du statut de combat en fonction de la difficulté.
*/
_updateCombatStatus() {
const difficulty = parseInt(this.combatStatus.difficulty) || 9
let newLabel = "Passif"
switch (difficulty) {
case 5: newLabel = "Offensif"; break
case 7: newLabel = "Actif"; break
case 9: newLabel = "Passif"; break
}
if (this.combatStatus.label !== newLabel) {
this.combatStatus.label = newLabel
}
}
}
+22
View File
@@ -0,0 +1,22 @@
import { baseItemSchema } from './_shared.mjs'
/**
* DataModel pour les items de type "rite" (rites, rituels).
* @augments {foundry.abstract.TypeDataModel}
*/
export default class VermineRiteData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.item.rite"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
return {
...baseItemSchema(),
rituel: new fields.StringField({ required: true, initial: "" }),
transe: new fields.StringField({ required: true, initial: "" }),
ability: new fields.StringField({ required: true, initial: "" }),
effect: new fields.StringField({ required: true, initial: "" })
}
}
}
+18
View File
@@ -0,0 +1,18 @@
import { listItemSchema } from "./_shared.mjs"
/**
* DataModel pour les items de type "rumor" (rumeurs).
* @augments {foundry.abstract.TypeDataModel}
*/
export default class VermineRumorData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.item.rumor"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
return {
...listItemSchema()
}
}
}
+18
View File
@@ -0,0 +1,18 @@
/**
* DataModel pour les items de type "specialty" (spécialités de compétence).
* Modèle minimal sans base partagée.
* @augments {foundry.abstract.TypeDataModel}
*/
export default class VermineSpecialtyData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.item.specialty"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
return {
name: new fields.StringField({ required: true, nullable: false, initial: "" }),
skill: new fields.StringField({ required: true, nullable: false, initial: "" })
}
}
}
+19
View File
@@ -0,0 +1,19 @@
import { listItemSchema } from './_shared.mjs'
/**
* DataModel pour les items de type "target" (cibles, objectifs).
* @augments {foundry.abstract.TypeDataModel}
*/
export default class VermineTargetData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.item.target"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
return {
...listItemSchema(),
level: new fields.StringField({ required: true, initial: "minor" })
}
}
}
+19
View File
@@ -0,0 +1,19 @@
import { listItemSchema } from './_shared.mjs'
/**
* DataModel pour les items de type "trauma" (traumatismes, séquelles).
* @augments {foundry.abstract.TypeDataModel}
*/
export default class VermineTraumaData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.item.trauma"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
return {
...listItemSchema(),
type: new fields.StringField({ required: true, initial: "" })
}
}
}
+20
View File
@@ -0,0 +1,20 @@
import { baseItemSchema } from "./_shared.mjs"
/**
* DataModel pour les items de type "vehicle" (véhicules).
* @augments {foundry.abstract.TypeDataModel}
*/
export default class VermineVehicleData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.item.vehicle"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
const reqInt = { required: true, nullable: false, integer: true }
return {
...baseItemSchema(),
mobility: new fields.NumberField({ ...reqInt, initial: 3, min: 0 })
}
}
}
+27
View File
@@ -0,0 +1,27 @@
import { baseItemSchema } from "./_shared.mjs"
/**
* DataModel pour les items de type "weapon" (armes).
* @augments {foundry.abstract.TypeDataModel}
*/
export default class VermineWeaponData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.item.weapon"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
const reqInt = { required: true, nullable: false, integer: true }
return {
...baseItemSchema(),
min_range: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
max_range: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
damage: new fields.SchemaField({
value: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
type: new fields.StringField({ required: true, initial: "" }),
addVigor: new fields.BooleanField({ required: true, initial: false })
}),
ammo: new fields.NumberField({ ...reqInt, initial: 0, min: 0 })
}
}
}
-157
View File
@@ -1,157 +0,0 @@
import { onManageActiveEffect, prepareActiveEffectCategories } from "../system/effects.mjs";
import { preloadHandlebarsTemplates } from "../system/handlebars-manager.mjs";
/**
* Extend the basic ActorSheet with some very simple modifications
* @extends {ActorSheet}
*/
export class VermineActorSheet extends ActorSheet {
/** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
/*classes: ["vermine2047", "sheet", "actor"],
template: "systems/vermine2047/templates/actor/actor-sheet.hbs",
height: 800,
width: 690,
resizable: false,
tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "features" }]*/
});
}
/** @override */
get template() {
return `systems/vermine2047/templates/actor/actor-${this.actor.type}-sheet.hbs`;
}
/* -------------------------------------------- */
/** @override */
getData() {
// Retrieve the data structure from the base sheet. You can inspect or log
// the context variable to see the structure, but some key properties for
// sheets are the actor object, the data object, whether or not it's
// editable, the items array, and the effects array.
const context = super.getData();
// Use a safe clone of the actor data for further operations.
const actorData = this.actor.toObject(false);
// Add the actor's data to context.data for easier access, as well as flags.
context.system = actorData.system;
context.flags = actorData.flags;
//add system config for convenience use
context.config = CONFIG.VERMINE;
// Add roll data for TinyMCE editors.
context.rollData = context.actor.getRollData();
// Prepare active effects
context.effects = prepareActiveEffectCategories(this.actor.effects);
return context;
}
/** @override */
activateListeners(html) {
super.activateListeners(html);
// Render the item sheet for viewing/editing prior to the editable check.
html.find('.item-edit').click(ev => {
const li = $(ev.currentTarget).parents(".item");
const item = this.actor.items.get(li.data("itemId"));
item.sheet.render(true);
});
// -------------------------------------------------------------
// Everything below here is only needed if the sheet is editable
if (!this.isEditable) return;
// Add Inventory Item
html.find('.item-create').click(this._onItemCreate.bind(this));
// Delete Inventory Item
html.find('.item-delete').click(ev => {
const li = $(ev.currentTarget).parents(".item");
const item = this.actor.items.get(li.data("itemId"));
item.delete();
li.slideUp(200, () => this.render(false));
});
html.find(".item-roll").click(ev => {
this._onRollItem(ev)
})
// Active Effect management
html.find(".effect-control").click(ev => onManageActiveEffect(ev, this.actor));
// Drag events for macros.
if (this.actor.isOwner) {
let handler = ev => this._onDragStart(ev);
html.find('li.item').each((i, li) => {
if (li.classList.contains("inventory-header")) return;
li.setAttribute("draggable", true);
li.addEventListener("dragstart", handler, false);
});
}
//click on wound radio
html.find('.hexa [type="radio"]').click(ev => {
ev.preventDefault();
ev.stopPropagation();
return this._onClickRadioHexa(ev)
})
}
async _onRollItem(ev) {
const li = $(ev.currentTarget).parents(".item");
const item = this.actor.items.get(li.data("itemId"));
item.roll();
}
_onClickRadioHexa(ev) {
let input = ev.currentTarget;
console.log(input.value, input.name);
let update = {};
update[input.name] = 0
let propTree = input.name.split('.')
let current = this.actor;
for (let prop of propTree) {
current = current[prop]
}
if (current != input.value) {
update[input.name] = parseInt(input.value)
} else {
update[input.name] = parseInt(input.value) - 1;
}
this.actor.update(update)
}
async _onItemCreate(event) {
event.preventDefault();
const header = event.currentTarget;
// Get the type of item to create.
const type = header.dataset.type;
// Grab any data associated with this control.
const data = foundry.utils.duplicate(header.dataset);
// Initialize a default name.
// const name = `New ${type.capitalize()}`;
const name = game.i18n.localize('ITEMS.new_' + type);
// Prepare the item object.
const itemData = {
name: name,
type: type,
system: data
};
// Remove the type from the dataset since it's in the itemData.type prop.
delete itemData.system["type"];
// Finally, create the item!
return await Item.create(itemData, { parent: this.actor });
}
}
-228
View File
@@ -1,228 +0,0 @@
import { onManageActiveEffect, prepareActiveEffectCategories } from "../system/effects.mjs";
import { VermineActorSheet } from "./actor-sheet.mjs";
import RollDialog from "../system/dialogs/rollDialog.mjs";
import { TotemPicker } from "../system/applications.mjs";
/**
* Extend the basic ActorSheet with some very simple modifications
* @extends {VermineActorSheet}
*/
export class VermineCharacterSheet extends VermineActorSheet {
/** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["vermine2047", "sheet", "character", "actor"],
template: "systems/vermine2047/templates/actor/actor-sheet.hbs",
width: "fit-content",
height: "fit-content",
tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "features" }]
});
}
/** @override */
get template() {
return `systems/vermine2047/templates/actor/actor-character-sheet.hbs`;
}
/* -------------------------------------------- */
/** @override */
getData() {
// Retrieve the data structure from the base sheet. You can inspect or log
// the context variable to see the structure, but some key properties for
// sheets are the actor object, the data object, whether or not it's
// editable, the items array, and the effects array.
const context = super.getData();
// Use a safe clone of the actor data for further operations.
const actorData = this.actor.toObject(false);
// Add the actor's data to context.data for easier access, as well as flags.
context.system = actorData.system;
context.flags = actorData.flags;
context.config = CONFIG.VERMINE;
// Prepare character data and items.
if (actorData.type == 'character') {
this._prepareItems(context);
this._prepareCharacterData(context);
}
// Prepare NPC data and items.
if (actorData.type == 'npc') {
this._prepareItems(context);
}
// Add roll data for TinyMCE editors.
context.rollData = context.actor.getRollData();
// Prepare active effects
context.effects = prepareActiveEffectCategories(this.actor.effects);
return context;
}
/**
* Organize and classify Items for Character sheets.
*
* @param {Object} actorData The actor to prepare.
*
* @return {undefined}
*/
_prepareCharacterData(context) {
// Handle ability scores.
for (let [k, v] of Object.entries(context.system.abilities)) {
v.label = game.i18n.localize(context.system.abilities[k].label) ?? k;
}
for (let [k, v] of Object.entries(context.system.skills)) {
if (v.value >= 2) {
let spe = this.actor.items.filter(it => it.type == "specialty").filter(spec => spec.system.skill == k);
v.specialties = spe
}
}
}
/**
* Organize and classify Items for Character sheets.
*
* @param {Object} actorData The actor to prepare.
*
* @return {undefined}
*/
_prepareItems(context) {
context.gear = this.actor.itemTypes['item'];
context.weapons = this.actor.itemTypes['weapon'];
context.defenses = this.actor.itemTypes['defense'];
context.traits = this.actor.itemTypes['trait'];
context.specialties = this.actor.itemTypes['specialty'];
context.abilities = this.actor.itemTypes['ability'];
context.evolutions = this.actor.itemTypes['evolution'];
context.traumas = this.actor.itemTypes['trauma'];
context.backgrounds = this.actor.itemTypes['background'];
context.rumors = this.actor.itemTypes['rumor'];
}
/* -------------------------------------------- */
/** @override */
activateListeners(html) {
super.activateListeners(html);
//desactiver les inputs si mode jeu
if (!this.actor.flags.world?.editMode) {
this.disableInputs(html)
}
// Choose Totem
html.find('.chooseTotem').click(this._onTotemButton.bind(this));
//activer lest jets
html.find('.ability .rollable').click(this._onRoll.bind(this));
//gérer les dés totems
html.find('[data-totem-name]').click(this._onClickTotemDice.bind(this));
//creation de specialités
html.find('i.add-specialty').click(this.addSpecialty.bind(this))
}
//mode jeu/edit en mode jeu on bloque les selects et input
disableInputs(html) {
for (let input of html.find('input')) {
//préserver le toggle mode jeu/ mode edit
if (input.name != "flags.world.editMode") {
input.setAttribute('disabled', true)
}
}
for (let select of html.find('select')) {
select.setAttribute('disabled', true)
}
}
async addSpecialty(ev) {
let skillName = ev.target.closest('.ability').querySelector('label').dataset.label;
let itemData = {
name: `spécialité, ${skillName}`,
type: 'specialty',
system: {
skill: skillName
}
}
let spec = await this.actor.createEmbeddedDocuments("Item", [itemData]);
spec[0].sheet.render(true)
}
async _onClickTotemDice(ev) {
let el = ev.currentTarget;
let totem = el.dataset.totemName;
let value = parseInt(el.dataset.totemValue) || 0;
let oldValue = this.actor.system.adaptation.totems[totem].value;
if (value === oldValue) { value-- };
let updates = {};
updates[`system.adaptation.totems.${totem}.value`] = value;
//verifier le max des dés totems
let sum = value;
switch (totem) {
case "human":
sum += this.actor.system.adaptation.totems.adapted.value;
break;
case "adapted":
sum += this.actor.system.adaptation.totems.human.value;
break;
}
if (sum > 5) { return ui.notifications.warn("pas plus de 5 dés totems") }
await this.actor.update(updates);
}
/**
* Handle clickable rolls.
* @param {Event} event The originating click event
* @private
*/
async _onRoll(event) {
event.preventDefault();
const element = event.currentTarget;
const dataset = element.dataset;
console.log("Ceci est un jet d'un personnage joueur", this.actor);
// Handle item rolls.
if (dataset.rollType) {
if (dataset.rollType == 'item') {
const itemId = element.closest('.item').dataset.itemId;
const item = this.actor.items.get(itemId);
if (item) return item.roll();
}
}
// Handle rolls that supply the formula directly.
if (dataset.label) {
dataset.rollType = dataset.type;
let data = {
actorId: this.object.id,
rollType: dataset.rollType,
labelKey: dataset.label,
label: game.i18n.localize(dataset.label)
};
let dial = await RollDialog.create(data);
console.log("from sheet", data, this)
return dial.render(true)
}
}
/**
* Handle totem pick
* @param {Event} event The originating click event
* @private
*/
_onTotemButton(event) {
event.preventDefault();
const el = event.currentTarget;
// const dataset = el.dataset;
const totemPicker = new TotemPicker(el, this.actor);
totemPicker.render(true);
}
}
-167
View File
@@ -1,167 +0,0 @@
import { onManageActiveEffect, prepareActiveEffectCategories } from "../system/effects.mjs";
import { VermineActorSheet } from "./actor-sheet.mjs";
/**
* Extend the basic ActorSheet with some very simple modifications
* @extends {ActorSheet}
*/
export class VermineCreatureSheet extends VermineActorSheet {
/** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["vermine2047", "sheet", "actor", "creature"],
template: "systems/vermine2047/templates/actor/actor-sheet.hbs",
width: 650,
height: 600,
tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "description" }]
});
}
/** @override */
get template() {
return `systems/vermine2047/templates/actor/actor-${this.actor.type}-sheet.hbs`;
}
/* -------------------------------------------- */
/** @override */
getData() {
// Retrieve the data structure from the base sheet. You can inspect or log
// the context variable to see the structure, but some key properties for
// sheets are the actor object, the data object, whether or not it's
// editable, the items array, and the effects array.
const context = super.getData();
// Use a safe clone of the actor data for further operations.
const actorData = this.actor.toObject(false);
// Add the actor's data to context.data for easier access, as well as flags.
context.system = actorData.system;
context.flags = actorData.flags;
context.config = CONFIG.VERMINE;
// Prepare character data and items.
if (actorData.type == 'character') {
this._prepareItems(context);
this._prepareCharacterData(context);
}
// Prepare NPC data and items.
if (actorData.type == 'npc') {
this._prepareItems(context);
}
// Prepare Creature data and items.
if (actorData.type == 'creature') {
this._prepareItems(context);
this._prepareCreatureData(context);
}
// Add roll data for TinyMCE editors.
context.rollData = context.actor.getRollData();
// Prepare active effects
context.effects = prepareActiveEffectCategories(this.actor.effects);
return context;
}
/**
* Organize and classify Items for Character sheets.
*
* @param {Object} actorData The actor to prepare.
*
* @return {undefined}
*/
_prepareItems(context) {
context.gear = this.actor.itemTypes['item'];
context.traits = this.actor.itemTypes['trait'];
}
/**
* Prepare Character type specific data.
*
* @param {Object} actorData The actor to prepare.
*
* @return {undefined}
*/
_prepareCharacterData(context) {
// Handle ability scores.
for (let [k, v] of Object.entries(context.system.abilities)) {
v.label = game.i18n.localize(context.system.abilities[k].label) ?? k;
}
}
/**
* Prepare Creature type specific data for the sheet.
*
* @param {Object} context The context data to prepare.
* @return {undefined}
*/
_prepareCreatureData(context) {
if (this.actor.type !== 'creature') return;
// Add computed values to context
context.computed = context.system.computed || {};
// Get labels for pattern, size, role
const patternLevel = context.system.pattern?.value || 1;
const sizeLevel = context.system.size?.value || 1;
const roleLevel = context.system.role?.value || 1;
const packLevel = context.system.pack?.value || 0;
// Add pattern label
const patternConfig = CONFIG.VERMINE.creaturePatternLevels[patternLevel];
if (patternConfig) {
context.patternLabel = game.i18n.localize(patternConfig.label);
}
// Add size label (using numeric for now)
context.sizeLabel = sizeLevel;
// Add role label
const roleConfig = CONFIG.VERMINE.creatureRoleLevels[roleLevel];
if (roleConfig) {
context.roleLabel = game.i18n.localize(roleConfig.label);
}
// Add pack label
context.packLabel = packLevel > 0 ? packLevel : game.i18n.localize('VERMINE.none');
}
/* -------------------------------------------- */
/** @override */
activateListeners(html) {
super.activateListeners(html);
html.find('.item-create').click(this._onItemCreate.bind(this));
}
async _onItemCreate(event) {
event.preventDefault();
const header = event.currentTarget;
// Get the type of item to create.
const type = header.dataset.type;
// Grab any data associated with this control.
const data = foundry.utils.duplicate(header.dataset);
// Initialize a default name.
// const name = `New ${type.capitalize()}`;
const name = game.i18n.localize('ITEMS.new_' + type);
console.log('onItemCreate child', data.type, this.actor.type);
// Prepare the item object.
const itemData = {
name: name,
type: type,
system: data
};
// Remove the type from the dataset since it's in the itemData.type prop.
delete itemData.system["type"];
// Finally, create the item!
return await Item.create(itemData, { parent: this.actor });
}
}
-167
View File
@@ -1,167 +0,0 @@
import { onManageActiveEffect, prepareActiveEffectCategories } from "../system/effects.mjs";
import { VermineActorSheet } from "./actor-sheet.mjs";
/**
* Extend the basic ActorSheet with some very simple modifications
* @extends {ActorSheet}
*/
export class VermineCreatureSheet extends VermineActorSheet {
/** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["vermine2047", "sheet", "actor", "creature"],
template: "systems/vermine2047/templates/actor/actor-sheet.hbs",
width: 650,
height: 600,
tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "description" }]
});
}
/** @override */
get template() {
return `systems/vermine2047/templates/actor/actor-${this.actor.type}-sheet.hbs`;
}
/* -------------------------------------------- */
/** @override */
getData() {
// Retrieve the data structure from the base sheet. You can inspect or log
// the context variable to see the structure, but some key properties for
// sheets are the actor object, the data object, whether or not it's
// editable, the items array, and the effects array.
const context = super.getData();
// Use a safe clone of the actor data for further operations.
const actorData = this.actor.toObject(false);
// Add the actor's data to context.data for easier access, as well as flags.
context.system = actorData.system;
context.flags = actorData.flags;
context.config = CONFIG.VERMINE;
// Prepare character data and items.
if (actorData.type == 'character') {
this._prepareItems(context);
this._prepareCharacterData(context);
}
// Prepare NPC data and items.
if (actorData.type == 'npc') {
this._prepareItems(context);
}
// Prepare Creature data and items.
if (actorData.type == 'creature') {
this._prepareItems(context);
this._prepareCreatureData(context);
}
// Add roll data for TinyMCE editors.
context.rollData = context.actor.getRollData();
// Prepare active effects
context.effects = prepareActiveEffectCategories(this.actor.effects);
return context;
/**
* Prepare Creature type specific data for the sheet.
*
* @param {Object} context The context data to prepare.
* @return {undefined}
*/
_prepareCreatureData(context) {
if (this.actor.type !== 'creature') return;
// Add computed values to context
context.computed = context.system.computed || {};
// Get labels for pattern, size, role
const patternLevel = context.system.pattern?.value || 1;
const sizeLevel = context.system.size?.value || 1;
const roleLevel = context.system.role?.value || 1;
const packLevel = context.system.pack?.value || 0;
// Add pattern label
const patternConfig = CONFIG.VERMINE.creaturePatternLevels[patternLevel];
if (patternConfig) {
context.patternLabel = game.i18n.localize(patternConfig.label);
}
// Add size label (using numeric for now)
context.sizeLabel = sizeLevel;
// Add role label
const roleConfig = CONFIG.VERMINE.creatureRoleLevels[roleLevel];
if (roleConfig) {
context.roleLabel = game.i18n.localize(roleConfig.label);
}
// Add pack label
context.packLabel = packLevel > 0 ? packLevel : game.i18n.localize('VERMINE.none');
}}
/**
* Organize and classify Items for Character sheets.
*
* @param {Object} actorData The actor to prepare.
*
* @return {undefined}
*/
_prepareCharacterData(context) {
// Handle ability scores.
for (let [k, v] of Object.entries(context.system.abilities)) {
v.label = game.i18n.localize(context.system.abilities[k].label) ?? k;
}
}
/**
* Organize and classify Items for Character sheets.
*
* @param {Object} actorData The actor to prepare.
*
* @return {undefined}
*/
_prepareItems(context) {
context.gear = this.actor.itemTypes['item'];
context.traits = this.actor.itemTypes['trait'];
}
/* -------------------------------------------- */
/** @override */
activateListeners(html) {
super.activateListeners(html);
html.find('.item-create').click(this._onItemCreate.bind(this));
}
async _onItemCreate(event) {
event.preventDefault();
const header = event.currentTarget;
// Get the type of item to create.
const type = header.dataset.type;
// Grab any data associated with this control.
const data = duplicate(header.dataset);
// Initialize a default name.
// const name = `New ${type.capitalize()}`;
const name = game.i18n.localize('ITEMS.new_' + type);
console.log('onItemCreate child', data.type, this.actor.type);
// Prepare the item object.
const itemData = {
name: name,
type: type,
system: data
};
// Remove the type from the dataset since it's in the itemData.type prop.
delete itemData.system["type"];
// Finally, create the item!
return await Item.create(itemData, { parent: this.actor });
}
}
-80
View File
@@ -1,80 +0,0 @@
import { TraitSelector } from "../system/applications.mjs";
/**
* Extend the basic ItemSheet with some very simple modifications
* @extends {ItemSheet}
*/
export class VermineItemSheet extends ItemSheet {
/** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["vermine2047", "sheet", "item"],
width: 450,
height: "max-content",
tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "description" }]
});
}
/** @override */
get template() {
const path = "systems/vermine2047/templates/item";
return `${path}/item-${this.item.type}-sheet.hbs`;
}
/* -------------------------------------------- */
/** @override */
getData() {
// Retrieve base data structure.
const context = super.getData();
// Use a safe clone of the item data for further operations.
const itemData = context.item;
// Retrieve the roll data for TinyMCE editors.
context.rollData = {};
let actor = this.object?.parent ?? null;
if (actor) {
context.rollData = actor.getRollData();
}
// Add the actor's data to context.data for easier access, as well as flags.
context.system = itemData.system;
context.flags = itemData.flags;
context.config = CONFIG.VERMINE;
return context;
}
/* -------------------------------------------- */
/** @override */
activateListeners(html) {
super.activateListeners(html);
// Everything below here is only needed if the sheet is editable
if (!this.isEditable) return;
//click on wound radio
html.find('.damages-row [type="radio"]').click(ev => {
this._onClickDamage(ev)
})
html.find('.traits-selector').click(ev => {
this.openTraitSelector(ev)
})
}
async _onClickDamage(ev) {
if (!ev.currentTarget.checked) { return }
let prop = ev.currentTarget.name;
let update = {};
update[prop] = ev.currentTarget.value - 1
this.item.update(update)
}
async openTraitSelector(ev) {
let selector = new TraitSelector(this.item);
selector.render(true)
}
}
-275
View File
@@ -1,275 +0,0 @@
import { onManageActiveEffect, prepareActiveEffectCategories } from "../system/effects.mjs";
import { VermineActorSheet } from "./actor-sheet.mjs";
import { TotemPicker, ActorPicker } from "../system/applications.mjs";
/**
* Extend the basic ActorSheet with some very simple modifications
* @extends {VermineActorSheet}
*/
export class VermineGroupSheet extends VermineActorSheet {
/** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["vermine2047", "sheet", "actor", "group"],
template: "systems/vermine2047/templates/actor/actor-sheet.hbs",
width: 700,
height: 600,
tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "description" }]
});
}
/** @override */
get template() {
return `systems/vermine2047/templates/actor/actor-${this.actor.type}-sheet.hbs`;
}
/* -------------------------------------------- */
/** @override */
getData() {
// Retrieve the data structure from the base sheet. You can inspect or log
// the context variable to see the structure, but some key properties for
// sheets are the actor object, the data object, whether or not it's
// editable, the items array, and the effects array.
const context = super.getData();
// Use a safe clone of the actor data for further operations.
const actorData = this.actor.toObject(false);
// Add the actor's data to context.data for easier access, as well as flags.
context.system = actorData.system;
context.flags = actorData.flags;
context.config = CONFIG.VERMINE;
// Prepare character data and items.
if (actorData.type == 'character') {
this._prepareItems(context);
this._prepareCharacterData(context);
}
// Prepare NPC data and items.
if (actorData.type == 'npc') {
this._prepareItems(context);
}
if (actorData.type == 'group') {
this._prepareItems(context);
this._prepareGroupData(context);
}
// Add roll data for TinyMCE editors.
context.rollData = this.actor.getRollData();
// Prepare active effects
context.effects = prepareActiveEffectCategories(this.actor.effects);
return context;
}
/**
* Organize and classify Items for Character sheets.
*
* @param {Object} actorData The actor to prepare.
*
* @return {undefined}
*/
_prepareCharacterData(context) {
// Handle ability scores.
for (let [k, v] of Object.entries(context.system.abilities)) {
v.label = game.i18n.localize(context.system.abilities[k].label) ?? k;
}
}
/**
* Prepare Group type specific data.
* Resolves member and encounter actor IDs to actual actor data.
*
* @param {Object} context The context data to prepare.
* @return {undefined}
*/
_prepareGroupData(context) {
if (this.actor.type !== 'group') return;
// Resolve member IDs to actor data
context.resolvedMembers = {};
if (context.system.members && context.system.members.length > 0) {
context.system.members.forEach(memberId => {
const actor = game.actors.get(memberId);
if (actor) {
context.resolvedMembers[memberId] = {
name: actor.name,
id: actor.id
};
}
});
}
// Resolve encounter IDs to actor data
context.resolvedEncounters = {};
if (context.system.encounters && context.system.encounters.length > 0) {
context.system.encounters.forEach(encounterId => {
const actor = game.actors.get(encounterId);
if (actor) {
context.resolvedEncounters[encounterId] = {
name: actor.name,
id: actor.id
};
}
});
}
// Set morale level based on dice value (rules: p. 68-69)
this._updateMoraleLevel(context);
}
/**
* Update morale level based on dice value.
* Rules: 7D+ = Haut, 6-3D = Normal, 2D- = Bas, 0D = Crise
*
* @param {Object} context The context data.
* @return {undefined}
*/
_updateMoraleLevel(context) {
const moraleValue = context.system.morale.value || 0;
// If level is already set, keep it
if (context.system.morale.level) return;
// Determine morale level based on dice value
if (moraleValue >= 7) {
context.system.morale.level = "high";
} else if (moraleValue >= 3) {
context.system.morale.level = "normal";
} else if (moraleValue >= 1) {
context.system.morale.level = "low";
} else {
context.system.morale.level = "crisis";
}
}
/**
* Organize and classify Items for Character sheets.
*
* @param {Object} actorData The actor to prepare.
*
* @return {undefined}
*/
_prepareItems(context) {
context.specialties = this.actor.itemTypes['specialty'];
context.backgrounds = this.actor.itemTypes['background'];
context.evolutions = this.actor.itemTypes['evolution'];
context.traumas = this.actor.itemTypes['trauma'];
context.gear = this.actor.itemTypes['item'];
context.weapons = this.actor.itemTypes['weapon'];
context.defenses = this.actor.itemTypes['defense'];
context.vehicles = this.actor.itemTypes['vehicle'];
context.totem_abilities = this.actor.itemTypes['ability'].filter(i => i.system.type === 'totem');
context.abilities = this.actor.itemTypes['ability'].filter(i => i.system.type !== 'totem');
context.members = this.actor.system.members;
context.encounters = this.actor.system.encounters;
}
/* -------------------------------------------- */
/** @override */
activateListeners(html) {
super.activateListeners(html);
// Choose Totem
html.find('.chooseTotem').click(this._onTotemButton.bind(this));
// Choose Members / Encounters
html.find('.chooseActor').click(this._onRoadButton.bind(this));
html.find('.member-delete').click(ev => {
const li = $(ev.currentTarget).parents("li.actor");
const actorId = li.data("actor-id");
const actorIdIndex = this.actor.system.members.indexOf(actorId);
if (actorIdIndex !== -1) {
this.actor.system.members.splice(actorIdIndex, 1);
}
this.actor.update({ "system.members": this.actor.system.members });
this.render(true);
});
html.find('.encounter-delete').click(ev => {
const li = $(ev.currentTarget).parents("li.actor");
const actorId = li.data("actor-id");
const actorIdIndex = this.actor.system.encounters.indexOf(actorId);
if (actorIdIndex !== -1) {
this.actor.system.encounters.splice(actorIdIndex, 1);
}
this.actor.update({ "system.encounters": this.actor.system.encounters });
this.render(true);
});
// Handle objective deletion
html.find('.objective-delete').click(ev => {
ev.preventDefault();
const btn = $(ev.currentTarget);
const type = btn.data("type"); // 'major' or 'minor'
const index = parseInt(btn.data("index"));
if (!isNaN(index)) {
const objectives = foundry.utils.duplicate(this.actor.system.objectives || { major: [], minor: [] });
objectives[type].splice(index, 1);
this.actor.update({ "system.objectives": objectives });
}
});
// Handle adding new objectives
html.find('.item-create[data-type="major_objective"], .item-create[data-type="minor_objective"]').click(ev => {
ev.preventDefault();
const btn = $(ev.currentTarget);
const type = btn.data("type") === "major_objective" ? "major" : "minor";
const objectives = foundry.utils.duplicate(this.actor.system.objectives || { major: [], minor: [] });
objectives[type].push("");
this.actor.update({ "system.objectives": objectives });
});
// Handle morale level change
html.find('select[name="system.morale.level"]').change(ev => {
const select = $(ev.currentTarget);
const level = select.val();
this.actor.update({ "system.morale.level": level });
});
}
/**
* Handle totem pick
* @param {Event} event The originating click event
* @private
*/
_onTotemButton(event) {
event.preventDefault();
const el = event.currentTarget;
// const dataset = el.dataset;
const totemPicker = new TotemPicker(el, this.actor);
totemPicker.render(true);
}
/**
* Handle actor pick
* @param {Event} event The originating click event
* @private
*/
_onRoadButton(event) {
event.preventDefault();
const el = event.currentTarget;
// const dataset = el.dataset;
const actorPicker = new ActorPicker(el, this.actor);
actorPicker.render(true);
}
}
-154
View File
@@ -1,154 +0,0 @@
import { onManageActiveEffect, prepareActiveEffectCategories } from "../system/effects.mjs";
import { VermineActorSheet } from "./actor-sheet.mjs";
import { TotemPicker } from "../system/applications.mjs";
/**
* Extend the basic ActorSheet for NPC type
* @extends {VermineActorSheet}
*/
export class VermineNpcSheet extends VermineActorSheet {
/** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["vermine2047", "sheet", "actor", "npc"],
template: "systems/vermine2047/templates/actor/actor-sheet.hbs",
width: 600,
height: 700,
tabs: [
{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "characteristics" }
]
});
}
/** @override */
get template() {
return `systems/vermine2047/templates/actor/actor-${this.actor.type}-sheet.hbs`;
}
/* -------------------------------------------- */
/** @override */
getData() {
// Retrieve the data structure from the base sheet.
const context = super.getData();
// Use a safe clone of the actor data for further operations.
const actorData = this.actor.toObject(false);
// Add the actor's data to context.data for easier access, as well as flags.
context.system = actorData.system;
context.flags = actorData.flags;
context.config = CONFIG.VERMINE;
// Prepare items for all actor types
this._prepareItems(context);
// Prepare NPC-specific data
if (actorData.type === 'npc') {
this._prepareNpcData(context);
}
// Add roll data for TinyMCE editors
context.rollData = this.actor.getRollData();
// Prepare active effects
context.effects = prepareActiveEffectCategories(this.actor.effects);
return context;
}
/**
* Prepare NPC specific data
*/
_prepareNpcData(context) {
// Calculate derived values from threat, experience, and role
const threat = CONFIG.VERMINE.npcThreatLevels[context.system.threat.value];
const experience = CONFIG.VERMINE.npcExperienceLevels[context.system.experience.value];
const role = CONFIG.VERMINE.npcRoleLevels[context.system.role.value];
// Add calculated values to context for easier access
context.threatData = threat;
context.experienceData = experience;
context.roleData = role;
// Set wound thresholds based on threat level
if (threat) {
context.system.minorWound.threshold = threat.minorWound || context.system.minorWound.threshold;
context.system.majorWound.threshold = threat.majorWound || context.system.majorWound.threshold;
context.system.deadlyWound.threshold = threat.deadlyWound || context.system.deadlyWound.threshold;
// Set max wounds
context.system.minorWound.max = threat.minorWound || context.system.minorWound.max;
context.system.majorWound.max = threat.majorWound || context.system.majorWound.max;
context.system.deadlyWound.max = threat.deadlyWound || context.system.deadlyWound.max;
}
// Set reserve max values based on role
if (role) {
context.system.attributes.effort.max = role.pools || context.system.attributes.effort.max;
context.system.attributes.self_control.max = role.reaction_bonus || context.system.attributes.self_control.max;
}
// Prepare abilities with labels
for (let [k, v] of Object.entries(context.system.abilities)) {
v.label = game.i18n.localize(CONFIG.VERMINE.abilities[k]) ?? k;
}
// Prepare skills with localized names
for (let [k, v] of Object.entries(context.system.skills)) {
const skillKey = `VERMINE.skill.${k}`;
v.name = game.i18n.localize(skillKey);
if (v.name === skillKey) {
// Fallback to key if no translation
v.name = k.charAt(0).toUpperCase() + k.slice(1);
}
}
// Prepare skill categories
for (let [k, v] of Object.entries(context.system.skill_categories)) {
if (k !== 'preferred') {
v.label = game.i18n.localize(v.label) ?? k;
}
}
}
/**
* Organize and classify Items for NPC sheets.
*
* @param {Object} context - The context to prepare.
*/
_prepareItems(context) {
context.gear = this.actor.itemTypes['item'];
context.weapons = this.actor.itemTypes['weapon'];
context.defenses = this.actor.itemTypes['defense'];
context.vehicles = this.actor.itemTypes['vehicle'];
context.abilities = this.actor.itemTypes['ability'];
context.specialties = this.actor.itemTypes['specialty'];
context.backgrounds = this.actor.itemTypes['background'];
context.traumas = this.actor.itemTypes['trauma'];
context.evolutions = this.actor.itemTypes['evolution'];
}
/* -------------------------------------------- */
/** @override */
activateListeners(html) {
super.activateListeners(html);
// Choose Totem
html.find('.chooseTotem').click(this._onTotemButton.bind(this));
}
/**
* Handle totem pick
* @param {Event} event - The originating click event
* @private
*/
_onTotemButton(event) {
event.preventDefault();
const el = event.currentTarget;
const totemPicker = new TotemPicker(el, this.actor);
totemPicker.render(true);
}
}
+1 -1
View File
@@ -251,7 +251,7 @@ VERMINE.skillCategories = {
}
}
VERMINE.sexes = { "male": "VERMINE.sexes.male", "female": "VERMINE.sexes.female" };
VERMINE.sexes = { "male": "SEXES.male", "female": "SEXES.female" };
VERMINE.totems = {
"human": "TOTEMS.human.name",
+327 -596
View File
@@ -1,596 +1,327 @@
import { VermineUtils } from "../roll.mjs";
/**
* Dialog for rolling dice in Vermine2047.
* Handles dice pool calculation, modifiers, and roll execution.
*/
export default class RollDialog extends Dialog {
/**
* Creates a new RollDialog instance.
* @param {Object} data - The data for the dialog
* @param {HTMLElement} html - The HTML content of the dialog
* @param {Object} options - The options for the dialog
* @param {Function} [close] - The callback function for closing the dialog
*/
constructor(data, html, options, close = undefined) {
const conf = {
title: "jet de dés",
content: html,
buttons: {
roll: {
icon: '<i class="fas fa-check"></i>',
label: "Lancer !",
callback: () => this._onRoll()
},
cancel: {
icon: '<i class="fas fa-times"></i>',
label: "Annuler",
callback: () => this.close()
}
},
close: close
};
super({ ...conf, ...data }, options);
// Store reference to close callback
this._closeCallback = close;
}
/**
* Creates a new RollDialog instance.
* @param {Object} [data] - The data for the dialog
* @param {string} [data.label] - Roll label
* @param {string} [data.rolltype] - Roll type
* @param {number} [data.NoD=1] - Number of dice
* @param {boolean} [data.Reroll=false] - Allow rerolls
* @param {string} [data.actorId] - Actor ID for the roll
* @returns {Promise<RollDialog|null>} The RollDialog instance or null if creation failed
*/
static async create(data = {
label: null,
rolltype: null,
NoD: 1,
Reroll: false,
actorId: game.user.character?.id ?? canvas.tokens.controlled[0]?.actor?.id
}) {
// Validate actorId
const actorId = data.actorId;
if (!actorId || typeof actorId !== 'string') {
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_actor_selected'));
return null;
}
// Retrieve the actor data based on the actorId
data.actor = await game.actors.get(actorId);
if (!data.actor) {
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_actor_selected'));
return null;
}
data.availableSpecialties = data.actor.items.filter(item => item.type === "specialty");
data.availableItems = data.actor.items.filter(item => item.type === "item");
data.config = CONFIG.VERMINE;
// Define options for the dialog
const options = {
classes: ["vermineDialog"],
width: "fit-content",
height: 'fit-content',
zIndex: 99999
};
// Render the HTML template for the dialog
const html = await renderTemplate('systems/vermine2047/templates/dialogs/roll-dialog.hbs', data);
// Return a new RollDialog instance with the provided data, HTML, and options
return new RollDialog(data, html, options);
}
/**
* Retrieves the default options for the RollDialog.
*/
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
focus: true,
classes: ["dialog vermine-roll"],
});
}
/**
* Retrieves the data for the dialog.
* @returns {Object} The context data for the dialog
*/
getData() {
// Get the context data from the superclass
const context = super.getData();
context.data = this.data;
context.config = CONFIG.VERMINE;
return context;
}
/**
* Prepares items for display.
* @returns {Array} Filtered list of items
*/
prepareItems() {
return this.data.actor.items.filter(it => it.type === "item");
}
/**
* Prepares specialties for display.
* @returns {Array} Filtered list of specialties
*/
prepareSpecialties() {
return this.data.actor.items.filter(it => it.type === "specialty");
}
/**
* Activates event listeners for the dialog.
* @param {HTMLElement} html - The HTML element of the dialog.
*/
async activateListeners(html) {
// Activate event listeners from the superclass
super.activateListeners(html);
// Initialize UI elements
this._html = html;
// Retrieve roll data and set up event listeners
await this.getRollData();
// Set up event listeners for all roll-related inputs
const rollInputs = html.find('[data-roll]');
for (const inp of rollInputs) {
inp.addEventListener('change', this._onRollInputChange.bind(this));
}
this.displaySpecialties();
const selectAbil = html.find('#ability')[0];
// Set the maximum value for self control based on ability value
html.find("#self_control")[0].max = selectAbil.value;
selectAbil.addEventListener('change', this._onChangeAbility.bind(this));
const selfControl = html.find('#self_control')[0];
// Add event listener for self control changes
selfControl.addEventListener('change', this._onChangeSelfControl.bind(this));
// Set up difficulty change listener
html.find('#difficulty')[0].addEventListener('change', this._onDifficultyChange.bind(this));
// Set up handicap change listener
html.find('#handicap')[0].addEventListener('change', this._onHandicapChange.bind(this));
// Set up totem checkbox listeners
html.find('#human-totem')[0]?.addEventListener('change', this._onTotemChange.bind(this));
html.find('#adapted-totem')[0]?.addEventListener('change', this._onTotemChange.bind(this));
// Initial update of all UI elements
this._updateUI();
};
/**
* Retrieves the roll data for the dialog.
* @param {Event} _ev - The event triggering the roll data retrieval (unused).
*/
getRollData(_ev) {
// Calculate and store the roll data
this.rollData = {
actor: this.data.actor,
NoD: this.getDicePool(),
Reroll: this.getReroll(),
difficulty: this.getDifficulty(),
handicap: this.getHandicap(),
rollType: this.getRollType(),
rollLabel: this.getLabel(),
totems: this.getTotems(),
self_control: this.getSelfControl(),
max_effort: this.getMaxEffort(),
keepTotem: this.getKeepTotem(),
skillCategory: this.getSkillCategory()
};
this.displaySpecialties();
this._updateUI();
};
/**
* Gets the selected skill category
* @returns {string|null} - The skill category
*/
getSkillCategory() {
const html = this.element[0];
const skillSelect = html.querySelector('#skill');
if (skillSelect && skillSelect.selectedIndex > 0) {
const selectedOption = skillSelect.options[skillSelect.selectedIndex];
return selectedOption.dataset.category || null;
}
return null;
}
/**
* Gets the selected skill level
* @returns {number|null} - The skill level
*/
getSkillLevel() {
const html = this.element[0];
const skillSelect = html.querySelector('#skill');
if (skillSelect && skillSelect.selectedIndex > 0) {
const selectedOption = skillSelect.options[skillSelect.selectedIndex];
return parseInt(selectedOption.value) || null;
}
return null;
}
/**
* Checks if a specialty is selected
* @returns {boolean} - True if a specialty is selected
*/
hasSpecialtySelected() {
const html = this.element[0];
const specialtyRadio = html.querySelector('input[name="usingSpecialization"]:checked');
return specialtyRadio && specialtyRadio.value !== 'aucune';
}
/**
* Handles changes to roll inputs and updates UI.
* @param {Event} ev - The change event.
*/
_onRollInputChange(ev) {
this.getRollData(ev);
}
/**
* Updates all UI elements based on current roll data
*/
_updateUI() {
if (!this._html) return;
const html = this._html[0];
// Update total dice pool display
const totalDice = this.getDicePool();
const totalEl = html.querySelector('#dice-pool-total');
if (totalEl) {
totalEl.textContent = `${totalDice}D`;
}
// Update bonus count
const bonusCount = this._calculateBonusCount();
const bonusEl = html.querySelector('#total-bonus');
if (bonusEl) {
bonusEl.textContent = bonusCount;
}
// Update difficulty display
const difficultyEl = html.querySelector('#current-difficulty');
const difficultySelect = html.querySelector('#difficulty');
if (difficultyEl && difficultySelect) {
const selectedIndex = difficultySelect.selectedIndex;
const diffValue = parseInt(difficultySelect.options[selectedIndex].value);
const diffLabel = difficultySelect.options[selectedIndex].text.split(' ')[0];
difficultyEl.textContent = `${diffLabel} (${diffValue})`;
}
// Update handicap display
const handicapEl = html.querySelector('#current-handicap');
const handicapSelect = html.querySelector('#handicap');
if (handicapEl && handicapSelect) {
const selectedIndex = handicapSelect.selectedIndex;
handicapEl.textContent = handicapSelect.options[selectedIndex].text;
}
// Update ability score display
const abilSelect = html.querySelector('#ability');
const abilScoreEl = html.querySelector('#abilityScoreValue');
if (abilSelect && abilScoreEl) {
const selectedIndex = abilSelect.selectedIndex;
if (selectedIndex > 0) {
abilScoreEl.textContent = abilSelect.options[selectedIndex].value;
} else {
abilScoreEl.textContent = '0';
}
}
// Update specialty display
const specialtyRadios = html.querySelectorAll('input[name="usingSpecialization"]:checked');
const currentSpecEl = html.querySelector('.current-specialty');
if (currentSpecEl && specialtyRadios.length > 0) {
const checkedRadio = specialtyRadios[0];
currentSpecEl.textContent = checkedRadio.value === 'aucune' ? game.i18n.localize('VERMINE.none') : checkedRadio.value;
}
}
/**
* Calculates the bonus count for display.
* @returns {number} Total bonus dice.
*/
_calculateBonusCount() {
let bonus = 0;
// Help bonus
if (this._html?.find('#helped')[0]?.checked) {
bonus += 1;
}
// Group bonus
const groupValue = parseInt(this._html?.find('#group')[0]?.value, 10) || 0;
bonus += groupValue;
// Self control bonus
const selfControlValue = parseInt(this._html?.find('#self_control')[0]?.value, 10) || 0;
bonus += selfControlValue;
// Tools bonus
const toolsChecked = this._html?.find('input[name="usingTools"]:checked')[0]?.value !== '0';
if (toolsChecked) {
bonus += 1;
}
// Totems bonus
if (this._html?.find('#human-totem')[0]?.checked) {
bonus += parseInt(this.data.actor?.system?.adaptation?.totems?.human?.value, 10) || 0;
}
if (this._html?.find('#adapted-totem')[0]?.checked) {
bonus += parseInt(this.data.actor?.system?.adaptation?.totems?.adapted?.value, 10) || 0;
}
// Specialty bonus
const specialtyChecked = this._html?.find('input[name="usingSpecialization"]:checked')[0]?.value !== 'aucune';
if (specialtyChecked) {
bonus += 1;
}
return bonus;
}
/**
* Handles difficulty change
* @param {Event} ev - The change event
*/
_onDifficultyChange(ev) {
this._updateUI();
}
/**
* Handles handicap change
* @param {Event} ev - The change event
*/
_onHandicapChange(ev) {
this._updateUI();
}
/**
* Handles totem checkbox change
* @param {Event} ev - The change event
*/
_onTotemChange(ev) {
this._updateUI();
}
/**
* Gets the selected totem to keep (for dual totem rolls)
* @returns {string|null} - The totem to keep ('human', 'adapted', or null)
*/
getKeepTotem() {
const keepTotemSelect = this._html?.find('#keep-totem-select')[0];
if (keepTotemSelect) {
return keepTotemSelect.value;
}
// Default to null (both totems used)
return null;
}
/**
* Handles the change in self control value.
* @param {Event} ev - The event triggering the change in self control value.
*/
_onChangeSelfControl(ev) {
const html = this.element[0];
const selfControlValueElement = html.querySelector('#self_control_value');
if (selfControlValueElement) {
selfControlValueElement.innerText = ev.currentTarget.value;
}
}
/**
* Retrieves the handicap value from the HTML element.
* @returns {number} The handicap value.
*/
getHandicap() {
const html = this.element[0];
const handicapValue = html.querySelector('#handicap')?.value ?? '1';
return parseInt(handicapValue, 10);
}
/**
* Gets the roll type (ability or skill).
* @returns {string} The roll type: 'skill' or 'ability'.
*/
getRollType() {
const html = this.element[0];
return html.querySelector('select#skill')?.value ? "skill" : "ability";
}
/**
* Gets the label for the roll.
* @returns {string} The roll label.
*/
getLabel() {
const html = this.element[0];
const rollType = this.getRollType();
if (rollType === "skill") {
const skillSelect = html.querySelector('select#skill');
const selectedIndex = skillSelect?.selectedIndex ?? 0;
return skillSelect?.options[selectedIndex]?.dataset?.label ?? "";
}
const abilitySelect = html.querySelector('select#ability');
const selectedIndex = abilitySelect?.selectedIndex ?? 0;
return abilitySelect?.options[selectedIndex]?.dataset?.label ?? "";
}
/**
* Displays specialties related to the selected skill.
*/
displaySpecialties() {
const specialties = this.element[0]?.querySelectorAll('[data-spec-skill]');
if (specialties) {
specialties.forEach(specEl => {
specEl.style.display = "inline";
});
}
}
/**
* Retrieves the self control value from the HTML element.
* @returns {number} The self control value.
*/
getSelfControl() {
const html = this.element[0];
const selfControlValue = html.querySelector('#self_control')?.value ?? '0';
return parseInt(selfControlValue, 10);
}
/**
* Retrieves the maximum effort value from the HTML element.
* @returns {number} The maximum effort value.
*/
getMaxEffort() {
const html = this.element[0];
const abilityValue = html.querySelector('#ability')?.value ?? '0';
return parseInt(abilityValue, 10);
}
/**
* Retrieves the selected totems from the HTML element.
* @returns {Object} An object containing the selected totems {human: boolean, adapted: boolean}.
*/
getTotems() {
const html = this.element[0];
return {
human: html.querySelector('#human-totem')?.checked ?? false,
adapted: html.querySelector('#adapted-totem')?.checked ?? false
};
}
/**
* Handles the change in ability value.
* @param {Event} ev - The event triggering the change in ability value.
*/
_onChangeAbility(ev) {
const html = this.element[0];
const abilitySelect = html.querySelector('#ability');
const selectedIndex = abilitySelect?.selectedIndex ?? 0;
const score = abilitySelect?.options[selectedIndex]?.value ?? '0';
const scoreElement = html.querySelector('#abilityScore');
if (scoreElement) {
scoreElement.value = score;
}
const selfControlElement = html.querySelector('#self_control');
if (selfControlElement) {
selfControlElement.max = score;
}
}
/**
* Retrieves the total dice pool based on various factors.
* @returns {number} The total dice pool value.
*/
getDicePool() {
// Retrieve the HTML element
const html = this.element[0];
// Safely get ability value
const abilitySelect = html.querySelector('#ability');
const abilValue = abilitySelect?.options[abilitySelect?.selectedIndex]?.value ?? 0;
// Safely get skill value and pool
const skillSelect = html.querySelector('#skill');
const skillOption = skillSelect?.options[skillSelect?.selectedIndex];
const skillValue = skillOption?.dataset?.pool ?? 0;
// Get the self control value
const selfControl = html.querySelector('#self_control')?.value ?? 0;
// Calculate bonuses based on certain conditions
const bonuses =
(html.querySelector('#usingSpecialization')?.checked ? 1 : 0) +
(html.querySelector('#helped')?.checked ? 1 : 0) +
(html.querySelector('#usingTools')?.checked ? 1 : 0);
// Calculate the total dice pool
const total = parseInt(abilValue, 10) + parseInt(selfControl, 10) + parseInt(skillValue, 10) + bonuses;
return total || 0;
}
/**
* Retrieves the reroll value based on selected skill.
* @returns {number} The reroll value.
*/
getReroll() {
const html = this.element[0];
const skillSelect = html.querySelector('#skill');
const selectedIndex = skillSelect?.selectedIndex ?? 0;
const rerollValue = skillSelect?.options[selectedIndex]?.dataset?.reroll ?? '0';
return parseInt(rerollValue, 10) || 0;
}
/**
* Retrieves the difficulty value based on selected option.
* @returns {number} The difficulty value.
*/
getDifficulty() {
const html = this.element[0];
const difficultySelect = html.querySelector('#difficulty');
const selectedIndex = difficultySelect?.selectedIndex ?? 0;
const diffValue = difficultySelect?.options[selectedIndex]?.value ?? '0';
return parseInt(diffValue, 10) || 0;
}
/**
* Performs a dice roll based on the roll data and handles self control checks.
* @returns {Promise<Roll|false>} A promise that resolves with the Roll result or false if cancelled.
*/
async _onRoll() {
// Check if self control is required for the roll
if (this.rollData.self_control > 0) {
// Check if the actor has enough self control
const currentSelfControl = this.rollData.actor?.system?.attributes?.self_control?.value ?? 0;
if (currentSelfControl < this.rollData.self_control) {
// Display a warning message if self control is insufficient
ui.notifications.warn(game.i18n.localize('VERMINE.error_not_enough_self_control'));
// Re-render the dialog
this.render(true);
return false; // Exit the function if self control is insufficient
}
}
const caracName = this.element[0]?.querySelector('[name="ability"]')?.value;
if (caracName === "0" || caracName === undefined) {
// Display a warning message if no ability selected
ui.notifications.warn(game.i18n.localize('VERMINE.error_select_ability'));
// Re-render the dialog
this.render(true);
return false; // Exit the function if no ability
}
// Deduct self control points if necessary
if (this.rollData.self_control > 0) {
const newSelfControl = this.rollData.actor.system.attributes.self_control.value - this.rollData.self_control;
// Update the actor's self control value
await this.rollData.actor.update({
"system.attributes.self_control.value": newSelfControl
});
}
// Perform the dice roll using VermineUtils
return VermineUtils.roll({
...this.rollData,
skillLevel: this.getSkillLevel(),
hasSpecialty: this.hasSpecialtySelected()
});
}
}
import { VermineUtils } from "../roll.mjs";
const { HandlebarsApplicationMixin } = foundry.applications.api;
export default class RollDialog extends HandlebarsApplicationMixin(foundry.applications.api.ApplicationV2) {
#actor;
get title() {
return game.i18n.localize("VERMINE.roll");
}
static DEFAULT_OPTIONS = {
classes: ["vermine-roll"],
tag: "form",
window: {
icon: "fas fa-dice-d10",
resizable: false
},
position: {
width: 520,
height: 600
},
actions: {
roll: RollDialog.#onRoll,
cancel: RollDialog.#onCancel
}
};
static PARTS = {
main: { template: "systems/vermine2047/templates/dialogs/roll-dialog.hbs" }
};
static async create(data = {}) {
const actorId = data.actorId ?? game.user.character?.id ?? canvas.tokens.controlled[0]?.actor?.id;
if (!actorId || typeof actorId !== "string") {
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_actor_selected"));
return null;
}
const actor = await game.actors.get(actorId);
if (!actor) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_actor_selected"));
return null;
}
return new RollDialog({ actor, label: data.label, rolltype: data.rolltype });
}
constructor(options = {}) {
super(options);
this.#actor = options.actor;
this.label = options.label ?? null;
this.rolltype = options.rolltype ?? null;
}
async _prepareContext() {
const actor = this.#actor;
return {
actor,
system: actor.system,
config: CONFIG.VERMINE,
label: this.label,
rollType: this.rolltype,
labelKey: this.label,
speakerId: actor.id,
ability: null,
help: false,
specialty: false,
availableSpecialties: actor.items.filter(i => i.type === "specialty"),
availableItems: actor.items.filter(i => i.type === "item")
};
}
async _onRender(context, options) {
this.element.dataset.actorId = this.#actor.id;
for (const inp of this.element.querySelectorAll("[data-roll]")) {
inp.addEventListener("change", this.#onInputChange.bind(this));
}
const ability = this.element.querySelector("#ability");
if (ability) {
ability.addEventListener("change", this.#onChangeAbility.bind(this));
const selfControl = this.element.querySelector("#self_control");
if (selfControl) selfControl.max = ability.value;
}
const selfControl = this.element.querySelector("#self_control");
if (selfControl) {
selfControl.addEventListener("change", this.#onChangeSelfControl.bind(this));
}
this.element.querySelector("#difficulty")?.addEventListener("change", () => this.#updateUI());
this.element.querySelector("#handicap")?.addEventListener("change", () => this.#updateUI());
this.element.querySelector("#human-totem")?.addEventListener("change", () => this.#updateUI());
this.element.querySelector("#adapted-totem")?.addEventListener("change", () => this.#updateUI());
this.#displaySpecialties();
this.#updateUI();
if (ability?.value !== "0") {
this.element.querySelector("#self_control")?.dispatchEvent(new Event("change"));
}
}
// ── Getters ──────────────────────────────────────────────────────────
get #el() { return this.element; }
#getAbility() { return this.#el.querySelector("#ability"); }
#getSkill() { return this.#el.querySelector("#skill"); }
#getDifficulty() { return this.#el.querySelector("#difficulty"); }
#getHandicap() { return this.#el.querySelector("#handicap"); }
#getSelfCtrl() { return this.#el.querySelector("#self_control"); }
getDicePool() {
const abil = this.#getAbility();
const abilVal = parseInt(abil?.options[abil?.selectedIndex]?.value, 10) || 0;
const skill = this.#getSkill();
const skillPool = parseInt(skill?.options[skill?.selectedIndex]?.dataset?.pool, 10) || 0;
const sc = parseInt(this.#getSelfCtrl()?.value, 10) || 0;
const specChecked = this.#el.querySelector("#usingSpecialization")?.checked;
const helped = this.#el.querySelector("#helped")?.checked;
const tools = this.#el.querySelector("input[name='usingTools']:checked")?.value !== "0";
const bonuses = (specChecked ? 1 : 0) + (helped ? 1 : 0) + (tools ? 1 : 0);
return (abilVal + sc + skillPool + bonuses) || 0;
}
getDifficultySelect() {
const sel = this.#getDifficulty();
const idx = sel?.selectedIndex ?? 0;
return parseInt(sel?.options[idx]?.value, 10) || 7;
}
getReroll() {
const sel = this.#getSkill();
const idx = sel?.selectedIndex ?? 0;
return parseInt(sel?.options[idx]?.dataset?.reroll, 10) || 0;
}
getHandicapSelect() {
const sel = this.#getHandicap();
return parseInt(sel?.value, 10) || 1;
}
getSkillCategory() {
const sel = this.#getSkill();
const idx = sel?.selectedIndex ?? 0;
return sel?.options[idx]?.dataset?.category ?? null;
}
getSkillLevel() {
const sel = this.#getSkill();
const idx = sel?.selectedIndex ?? 0;
const val = sel?.options[idx]?.value;
return val ? parseInt(val, 10) : null;
}
hasSpecialtySelected() {
const checked = this.#el.querySelector("input[name='usingSpecialization']:checked");
return checked && checked.value !== "aucune";
}
getRollType() {
const sel = this.#getSkill();
return sel?.value ? "skill" : "ability";
}
getLabel() {
const type = this.getRollType();
if (type === "skill") {
const sel = this.#getSkill();
const idx = sel?.selectedIndex ?? 0;
return sel?.options[idx]?.dataset?.label ?? "";
}
const sel = this.#getAbility();
const idx = sel?.selectedIndex ?? 0;
return sel?.options[idx]?.dataset?.label ?? "";
}
getSelfControl() {
return parseInt(this.#getSelfCtrl()?.value, 10) || 0;
}
getMaxEffort() {
const sel = this.#getAbility();
return parseInt(sel?.value, 10) || 0;
}
getTotems() {
return {
human: this.#el.querySelector("#human-totem")?.checked ?? false,
adapted: this.#el.querySelector("#adapted-totem")?.checked ?? false
};
}
getKeepTotem() {
return this.#el.querySelector("#keep-totem-select")?.value ?? null;
}
// ── UI ───────────────────────────────────────────────────────────────
#displaySpecialties() {
for (const el of this.#el.querySelectorAll("[data-spec-skill]")) {
el.style.display = "inline";
}
}
#calculateBonusCount() {
let b = 0;
if (this.#el.querySelector("#helped")?.checked) b += 1;
b += parseInt(this.#el.querySelector("#group")?.value, 10) || 0;
b += parseInt(this.#getSelfCtrl()?.value, 10) || 0;
const tools = this.#el.querySelector("input[name='usingTools']:checked");
if (tools && tools.value !== "0") b += 1;
const human = this.#el.querySelector("#human-totem");
if (human?.checked) b += parseInt(this.#actor?.system?.adaptation?.totems?.human?.value, 10) || 0;
const adapted = this.#el.querySelector("#adapted-totem");
if (adapted?.checked) b += parseInt(this.#actor?.system?.adaptation?.totems?.adapted?.value, 10) || 0;
if (this.hasSpecialtySelected()) b += 1;
return b;
}
#updateUI() {
const total = this.getDicePool();
const totalEl = this.#el.querySelector("#dice-pool-total");
if (totalEl) totalEl.textContent = `${total}D`;
const bonusEl = this.#el.querySelector("#total-bonus");
if (bonusEl) bonusEl.textContent = this.#calculateBonusCount();
const diffSel = this.#getDifficulty();
const diffEl = this.#el.querySelector("#current-difficulty");
if (diffEl && diffSel) {
const idx = diffSel.selectedIndex;
const val = diffSel.options[idx].value;
const lbl = diffSel.options[idx].text.split(" ")[0];
diffEl.textContent = `${lbl} (${val})`;
}
const handSel = this.#getHandicap();
const handEl = this.#el.querySelector("#current-handicap");
if (handEl && handSel) {
handEl.textContent = handSel.options[handSel.selectedIndex].text;
}
const abilSel = this.#getAbility();
const abilValEl = this.#el.querySelector("#abilityScoreValue");
if (abilSel && abilValEl) {
const idx = abilSel.selectedIndex;
abilValEl.textContent = idx > 0 ? abilSel.options[idx].value : "0";
}
const specChecked = this.#el.querySelector("input[name='usingSpecialization']:checked");
const specEl = this.#el.querySelector(".current-specialty");
if (specEl && specChecked) {
specEl.textContent = specChecked.value === "aucune"
? game.i18n.localize("VERMINE.none")
: specChecked.value;
}
}
// ── Event handlers ───────────────────────────────────────────────────
#onInputChange() {
this.#updateUI();
}
#onChangeAbility(ev) {
const sel = ev.currentTarget;
const score = sel.options[sel.selectedIndex]?.value ?? "0";
const scoreEl = this.#el.querySelector("#abilityScore");
if (scoreEl) scoreEl.value = score;
const sc = this.#getSelfCtrl();
if (sc) sc.max = score;
this.#updateUI();
}
#onChangeSelfControl(ev) {
const valEl = this.#el.querySelector("#self_control_value");
if (valEl) valEl.textContent = ev.currentTarget.value;
}
static async #onCancel(event, target) {
this.close();
}
static async #onRoll(event, target) {
const selfCtrl = this.getSelfControl();
if (selfCtrl > 0) {
const current = this.#actor?.system?.attributes?.self_control?.value ?? 0;
if (current < selfCtrl) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_not_enough_self_control"));
return;
}
}
const abilityVal = this.#el.querySelector('[name="ability"]')?.value;
if (!abilityVal || abilityVal === "0") {
ui.notifications.warn(game.i18n.localize("VERMINE.error_select_ability"));
return;
}
if (selfCtrl > 0) {
const newVal = this.#actor.system.attributes.self_control.value - selfCtrl;
await this.#actor.update({ "system.attributes.self_control.value": newVal });
}
await VermineUtils.roll({
actor: this.#actor,
NoD: this.getDicePool(),
Reroll: this.getReroll(),
difficulty: this.getDifficultySelect(),
handicap: this.getHandicapSelect(),
rollLabel: this.getLabel(),
totems: this.getTotems(),
self_control: selfCtrl,
max_effort: this.getMaxEffort(),
keepTotem: this.getKeepTotem(),
skillCategory: this.getSkillCategory(),
skillLevel: this.getSkillLevel(),
hasSpecialty: this.hasSpecialtySelected()
});
this.close();
}
}
+2 -2
View File
@@ -291,7 +291,7 @@ export class VermineFight {
console.log(data);
// render template
let html = await renderTemplate(data._template, data);
let html = await foundry.applications.handlebars.renderTemplate(data._template, data);
let ui = new Dialog({
title: game.i18n.localize("VERMINE.FightTool"),
@@ -415,7 +415,7 @@ export class VermineCombat extends Combat {
}
}
export class VermineCombatTracker extends CombatTracker {
export class VermineCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker {
get template() {
return "systems/vermine2047/templates/combat-tracker.hbs";
+2 -1
View File
@@ -4,7 +4,7 @@
* @return {Promise}
*/
export const preloadHandlebarsTemplates = async function () {
return loadTemplates([
return foundry.applications.handlebars.loadTemplates([
// Actor partials.
@@ -31,6 +31,7 @@ export const preloadHandlebarsTemplates = async function () {
// npc partials
"systems/vermine2047/templates/actor/npc/npc-combat.hbs",
"systems/vermine2047/templates/actor/parts/npc-skill-category.hbs",
// creature partials
"systems/vermine2047/templates/actor/creature/creature-combat.hbs",
+5 -5
View File
@@ -38,16 +38,16 @@ export const registerHooks = function () {
});
Hooks.on('renderChatMessage', async (message, html, data) => {
let rerollTitle = html[0].querySelector(".reroll-fromroll h4");
Hooks.on('renderChatMessageHTML', async (message, html, data) => {
let rerollTitle = html.querySelector(".reroll-fromroll h4");
if (rerollTitle) {
rerollTitle.addEventListener("click", () => { html[0].querySelector(".reroll").classList.toggle('visible') })
rerollTitle.addEventListener("click", () => { html.querySelector(".reroll").classList.toggle('visible') })
}
if (message.author?._id != game.user._id || !game.user.isGM) {
// désactiver les inputs pour les joueurs non-auteurs du message
html[0].querySelectorAll("input").forEach(inp => inp.disabled = true);
html.querySelectorAll("input").forEach(inp => inp.disabled = true);
//cacher le boutton reroll
html[0].querySelectorAll("div.reroll-from-effort").forEach(el => el.style.display = "none")
html.querySelectorAll("div.reroll-from-effort").forEach(el => el.style.display = "none")
return
}
await VermineUtils.chatListenners(html)
+1 -1
View File
@@ -468,7 +468,7 @@ export class VermineUtils {
* @returns {Promise<ChatMessage>} The created chat message
*/
static async diplayChatRoll(roll, param) {
const content = await renderTemplate("systems/vermine2047/templates/roll-message.hbs", { roll, param });
const content = await foundry.applications.handlebars.renderTemplate("systems/vermine2047/templates/roll-message.hbs", { roll, param });
const chatData = {
user: game.user?._id,
speaker: ChatMessage.getSpeaker(),
+1 -1
View File
@@ -48,7 +48,7 @@ class CreateActorDialog extends FormApplication {
});
}
}
class VermineTour extends Tour {
class VermineTour extends foundry.nue.Tour {
/** @override */
async _preStep() {
var _a2, _b, _c, _d, _e;
+124 -48
View File
@@ -3,19 +3,17 @@ import { registerSettings } from "./system/settings.mjs";
import { GroupLink } from "./system/group-link.mjs";
// Import document classes.
import { VermineActor } from "./documents/actor.mjs";
import { VermineCharacterSheet } from "./sheets/character-sheet.mjs";
import { VermineNpcSheet } from "./sheets/npc-sheet.mjs";
import { VermineGroupSheet } from "./sheets/npc-group.mjs";
import { VermineCreatureSheet } from "./sheets/creature-sheet.mjs";
import { VermineItem } from "./documents/item.mjs";
import { VermineItemSheet } from "./sheets/item-sheet.mjs";
import * as documents from "./documents/_module.mjs";
import { VermineUtils } from "./system/roll.mjs";
import { VermineCombat, VermineCombatant, VermineCombatTracker } from "./system/fight.mjs";
// Import DataModels
import * as models from "./models/_module.mjs"
// Import ApplicationV2 sheets
import * as sheets from "./applications/sheets/_module.mjs"
// Import helper/utility classes and constants.
import { preloadHandlebarsTemplates, registerHandlebarsHelpers } from "./system/handlebars-manager.mjs";
import { VERMINE } from "./system/config.mjs";
@@ -26,51 +24,109 @@ import { VERMINE } from "./system/config.mjs";
Hooks.once('init', async function () {
// System stylesheet is automatically loaded by Foundry from system.json
// No need to manually inject it - this was causing MIME type issues
// If you need to ensure fresh CSS, use cache-busting in the filename or system.json version
// Register GroupLink hooks for automatic synchronization
GroupLink.registerHooks();
// Register ALL DataModels FIRST - this is crucial for Foundry V2
// Use individual assignments like Celestopol for compatibility
CONFIG.Actor.dataModels.character = models.VermineCharacterData;
CONFIG.Actor.dataModels.npc = models.VermineNpcData;
CONFIG.Actor.dataModels.group = models.VermineGroupData;
CONFIG.Actor.dataModels.creature = models.VermineCreatureData;
CONFIG.Item.dataModels.item = models.VermineItemData;
CONFIG.Item.dataModels.weapon = models.VermineWeaponData;
CONFIG.Item.dataModels.defense = models.VermineDefenseData;
CONFIG.Item.dataModels.vehicle = models.VermineVehicleData;
CONFIG.Item.dataModels.ability = models.VermineAbilityData;
CONFIG.Item.dataModels.specialty = models.VermineSpecialtyData;
CONFIG.Item.dataModels.background = models.VermineBackgroundData;
CONFIG.Item.dataModels.trauma = models.VermineTraumaData;
CONFIG.Item.dataModels.evolution = models.VermineEvolutionData;
CONFIG.Item.dataModels.rumor = models.VermineRumorData;
CONFIG.Item.dataModels.target = models.VermineTargetData;
CONFIG.Item.dataModels.rite = models.VermineRiteData;
// Define custom Document classes AFTER ALL DataModels are registered
CONFIG.Actor.documentClass = documents.VermineActor;
CONFIG.Item.documentClass = documents.VermineItem;
// Add utility classes to the global game object so that they're more easily
// accessible in global contexts.
// Note: Do NOT expose Document classes here as it can cause issues with DataModel initialization
game.vermine2047 = {
VermineActor,
VermineItem,
VermineUtils,
VermineCombat,
GroupLink
};
// Register GroupLink hooks for automatic synchronization
GroupLink.registerHooks();
// Define custom Document classes
CONFIG.Actor.documentClass = VermineActor;
CONFIG.Item.documentClass = VermineItem;
CONFIG.ui.combat = VermineCombatTracker;
CONFIG.Combatant.documentClass = VermineCombatant;
CONFIG.Combat.documentClass = VermineCombat;
// Register sheet application classes
Actors.unregisterSheet("core", ActorSheet);
Actors.registerSheet('vermine2047', VermineCharacterSheet, {
types: ['character'],
makeDefault: true,
});
// Register sheet application classes (ApplicationV2)
// Unregister core sheets
foundry.applications.sheets.ActorSheetV2?.unregisterSheet?.("core", "Actor", {
types: ["character", "npc", "group", "creature"]
})
foundry.documents.collections.Items.unregisterSheet("core", foundry.appv1?.sheets?.ItemSheet)
Actors.registerSheet('vermine2047', VermineNpcSheet, {
types: ['npc'],
makeDefault: true,
});
// Actor sheets
foundry.documents.collections.Actors.registerSheet("vermine2047", sheets.VermineCharacterSheetV2, {
types: ["character"], makeDefault: true, label: "VERMINE.Sheet.character"
})
foundry.documents.collections.Actors.registerSheet("vermine2047", sheets.VermineNpcSheetV2, {
types: ["npc"], makeDefault: true, label: "VERMINE.Sheet.npc"
})
foundry.documents.collections.Actors.registerSheet("vermine2047", sheets.VermineCreatureSheetV2, {
types: ["creature"], makeDefault: true, label: "VERMINE.Sheet.creature"
})
foundry.documents.collections.Actors.registerSheet("vermine2047", sheets.VermineGroupSheetV2, {
types: ["group"], makeDefault: true, label: "VERMINE.Sheet.group"
})
Actors.registerSheet('vermine2047', VermineCreatureSheet, {
types: ['creature'],
makeDefault: true,
});
Actors.registerSheet('vermine2047', VermineGroupSheet, {
types: ['group'],
makeDefault: true,
});
Items.unregisterSheet("core", ItemSheet);
Items.registerSheet("vermine2047", VermineItemSheet, { makeDefault: true });
// Item sheets — un par type
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineItemSheetV2, {
types: ["item"], makeDefault: true
})
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineWeaponSheetV2, {
types: ["weapon"], makeDefault: true
})
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineDefenseSheetV2, {
types: ["defense"], makeDefault: true
})
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineVehicleSheetV2, {
types: ["vehicle"], makeDefault: true
})
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineAbilitySheetV2, {
types: ["ability"], makeDefault: true
})
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineSpecialtySheetV2, {
types: ["specialty"], makeDefault: true
})
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineBackgroundSheetV2, {
types: ["background"], makeDefault: true
})
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineTraumaSheetV2, {
types: ["trauma"], makeDefault: true
})
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineEvolutionSheetV2, {
types: ["evolution"], makeDefault: true
})
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineRumorSheetV2, {
types: ["rumor"], makeDefault: true
})
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineTargetSheetV2, {
types: ["target"], makeDefault: true
})
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineRiteSheetV2, {
types: ["rite"], makeDefault: true
})
registerHandlebarsHelpers(); // Register Handlebars helpers
registerHooks(); // register Hooks
@@ -79,13 +135,8 @@ Hooks.once('init', async function () {
// Add custom constants for configuration.
CONFIG.VERMINE = VERMINE;
// Set up model templates - must be done after system templates are loaded
if (game.system?.template?.Actor && game.system?.template?.Item) {
CONFIG.VERMINE.model = {
Actor: game.system.template.Actor,
Item: game.system.template.Item
};
}
// Les DataModels sont déjà enregistrés dans CONFIG.Actor.dataModels et
// CONFIG.Item.dataModels. On expose leurs définitions pour compatibilité.
/**
* Set an initiative formula for the system
@@ -116,8 +167,33 @@ Hooks.once('init', async function () {
document.querySelector('#ui-left').prepend(el);
// Preload Handlebars templates.
return preloadHandlebarsTemplates();
// Preload templates (ApplicationV2 + legacy)
await preloadHandlebarsTemplates();
await foundry.applications.handlebars.loadTemplates([
"systems/vermine2047/templates/actor/appv2/character-header.hbs",
"systems/vermine2047/templates/actor/appv2/character-main.hbs",
"systems/vermine2047/templates/actor/appv2/character-abilities.hbs",
"systems/vermine2047/templates/actor/appv2/character-totem.hbs",
"systems/vermine2047/templates/actor/appv2/character-equipment.hbs",
"systems/vermine2047/templates/actor/appv2/character-stories.hbs",
"systems/vermine2047/templates/actor/appv2/character-combat.hbs",
"systems/vermine2047/templates/actor/appv2/npc-main.hbs",
"systems/vermine2047/templates/actor/appv2/npc-characteristics.hbs",
"systems/vermine2047/templates/actor/appv2/npc-skills.hbs",
"systems/vermine2047/templates/actor/appv2/npc-threat.hbs",
"systems/vermine2047/templates/actor/appv2/npc-combat.hbs",
"systems/vermine2047/templates/actor/appv2/npc-notes.hbs",
"systems/vermine2047/templates/actor/appv2/group-main.hbs",
"systems/vermine2047/templates/actor/appv2/group-info.hbs",
"systems/vermine2047/templates/actor/appv2/group-gear.hbs",
"systems/vermine2047/templates/actor/appv2/group-road.hbs",
"systems/vermine2047/templates/actor/appv2/group-reserve.hbs",
"systems/vermine2047/templates/actor/appv2/creature-main.hbs",
"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"
]);
});