Fix apv2, WIP
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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" } }
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -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 })
|
||||
})
|
||||
}
|
||||
@@ -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: []
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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: "" })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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: "" })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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: "" })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
@@ -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"
|
||||
]);
|
||||
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user