Migrate to FoundryVTT v13 AppV2 +

│ DataModels
│
│ - Reorganize DataModels into src/module/models/ (one .mjs per type)
│ - Create AppV2 actor/item sheets (HandlebarsApplicationMixin)…
This commit is contained in:
2026-04-19 10:54:43 +02:00
parent e3002dd602
commit 86b2cd5777
30 changed files with 445 additions and 1679 deletions

View File

@@ -695,12 +695,11 @@ class ActorCharacter {
}
if (recalculEncumbrance || recalculWeight) {
const cloneActor = foundry.utils.deepClone($this);
const updateData = {};
await this.recalculateArmor($this, cloneActor);
this.recalculateArmor($this, updateData);
if (recalculEncumbrance) {
//console.log("recalculEncumbrance");
const str = $this.system.characteristics.strength.value;
const end = $this.system.characteristics.endurance.value;
let sumSkill = 0;
@@ -708,19 +707,21 @@ class ActorCharacter {
let normal = str + end + sumSkill;
let heavy = normal * 2;
cloneActor.system.states.encumbrance = $this.system.inventory.weight > normal;
cloneActor.system.encumbrance.normal = normal;
cloneActor.system.encumbrance.heavy = heavy;
updateData["system.states.encumbrance"] = $this.system.inventory.weight > normal;
updateData["system.inventory.encumbrance.normal"] = normal;
updateData["system.inventory.encumbrance.heavy"] = heavy;
}
if (recalculWeight)
await this.recalculateWeight($this, cloneActor);
await this.recalculateWeight($this, updateData);
else if (Object.keys(updateData).length > 0)
await $this.update(updateData);
}
}
static async recalculateArmor($this, cloneActor) {
if (cloneActor === null || cloneActor === undefined)
cloneActor = foundry.utils.deepClone($this);
static recalculateArmor($this, updateData) {
if (updateData === null || updateData === undefined)
updateData = {};
let armor = 0;
for (let item of $this.items) {
@@ -731,18 +732,18 @@ class ActorCharacter {
}
}
cloneActor.system.inventory.armor = armor;
updateData["system.inventory.armor"] = armor;
return updateData;
}
static async recalculateWeight($this, cloneActor) {
static async recalculateWeight($this, updateData) {
if (cloneActor === null || cloneActor === undefined)
cloneActor = foundry.utils.deepClone($this);
if (updateData === null || updateData === undefined)
updateData = {};
let updatedContainers = [];
let containerChanges = {};
//console.log("recalculWeight");
let containers = [];
// List all containers
@@ -791,19 +792,16 @@ class ActorCharacter {
}
}
//cloneActor.system.inventory.weight = onHandWeight.toFixed(1);
// Check containers new weight
for (let container of containers) {
let newWeight = containerChanges[container._id].weight;
let newCount = containerChanges[container._id].count;
if (container.system.weight !== newWeight || container.system.count !== newCount) {
//const cloneContainer = foundry.utils.deepClone();
const cloneContainer = foundry.utils.deepClone($this.getEmbeddedDocument("Item", container._id));
//foundry.utils.setProperty(cloneContainer, "system.weight", newWeight);
cloneContainer.system.weight = newWeight;
cloneContainer.system.count = newCount;
updatedContainers.push(cloneContainer);
updatedContainers.push({
_id: container._id,
"system.weight": newWeight,
"system.count": newCount,
});
if (container.system.onHand === true &&
(container.system.weight > 0 || container.system.weightless !== true)) {
@@ -812,11 +810,10 @@ class ActorCharacter {
}
}
cloneActor.system.inventory.weight = onHandWeight;
cloneActor.system.states.encumbrance = onHandWeight > $this.system.inventory.encumbrance.normal;
updateData["system.inventory.weight"] = onHandWeight;
updateData["system.states.encumbrance"] = onHandWeight > $this.system.inventory.encumbrance.normal;
await $this.update(cloneActor);
await $this.update(updateData);
if (updatedContainers.length > 0) {
await $this.updateEmbeddedDocuments('Item', updatedContainers);
@@ -1198,7 +1195,7 @@ class MGT2ActorSheet extends HandlebarsApplicationMixin$1(foundry.applications.s
return this._sheetMode === this.constructor.SHEET_MODES.EDIT;
}
tabGroups = { primary: "stats" }
tabGroups = { sidebar: "health" }
/** @override */
async _prepareContext() {
@@ -1226,6 +1223,9 @@ class MGT2ActorSheet extends HandlebarsApplicationMixin$1(foundry.applications.s
/** @override */
_onRender(context, options) {
super._onRender(context, options);
// Inject theme class dynamically (can't use game.settings in static DEFAULT_OPTIONS)
const theme = game.settings.get("mgt2", "theme");
if (theme) this.element.classList.add(theme);
this._activateTabGroups();
}
@@ -1493,17 +1493,15 @@ class MGT2Helper {
}
}
class RollPromptDialog extends Dialog {
constructor(dialogData = {}, options = {}) {
super(dialogData, options);
this.options.classes = ["mgt2", game.settings.get("mgt2", "theme"), "sheet", "dialog"];
}
const { DialogV2: DialogV2$1 } = foundry.applications.api;
const { renderTemplate: renderTemplate$1 } = foundry.applications.handlebars;
const { FormDataExtended: FormDataExtended$1 } = foundry.applications.ux;
static async create(options) {
class RollPromptHelper {
const htmlContent = await renderTemplate('systems/mgt2/templates/roll-prompt.html', {
static async roll(options) {
const htmlContent = await renderTemplate$1('systems/mgt2/templates/roll-prompt.html', {
config: CONFIG.MGT2,
//formula: formula,
characteristics: options.characteristics,
characteristic: options.characteristic,
skills: options.skills,
@@ -1513,242 +1511,117 @@ class RollPromptDialog extends Dialog {
difficulty: options.difficulty
});
const results = new Promise(resolve => {
new this({
title: options.title,
content: htmlContent,
buttons: {
boon: {
label: game.i18n.localize("MGT2.RollPrompt.Boon"),
callback: (html) => {
const formData = new FormDataExtended(html[0].querySelector('form')).object;
formData.diceModifier = "dl";
resolve(formData);
}
},
submit: {
label: game.i18n.localize("MGT2.RollPrompt.Roll"),
icon: '<i class="fa-solid fa-dice"></i>',
callback: (html) => {
const formData = new FormDataExtended(html[0].querySelector('form')).object;
resolve(formData);
},
},
bane: {
label: game.i18n.localize("MGT2.RollPrompt.Bane"),
//icon: '<i class="fa-solid fa-dice"></i>',
callback: (html) => {
const formData = new FormDataExtended(html[0].querySelector('form')).object;
formData.diceModifier = "dh";
resolve(formData);
}
}
}
//close: () => { resolve(false) }
}).render(true);
});
game.settings.get("mgt2", "theme");
//console.log(Promise.resolve(results));
return results;
}
}
class RollPromptHelper {
static async roll(options) {
return await RollPromptDialog.create(options);
}
static async promptForFruitTraits() {
const htmlContent = await renderTemplate('systems/mgt2/templateschat/chat/roll-prompt.html');
return new Promise((resolve, reject) => {
const dialog = new Dialog({
title: "Fruit Traits",
content: htmlContent,
buttons: {
submit: {
label: "Roll",
icon: '<i class="fa-solid fa-dice"></i>',
callback: (html) => {
const formData = new FormDataExtended(html[0].querySelector('form'))
.toObject();
//verifyFruitInputs(formData);
resolve(formData);
},
},
skip: {
label: "Cancel",
callback: () => resolve(null),
return await DialogV2$1.wait({
window: { title: options.title ?? options.rollTypeName ?? game.i18n.localize("MGT2.RollPrompt.Roll") },
content: htmlContent,
rejectClose: false,
buttons: [
{
action: "boon",
label: game.i18n.localize("MGT2.RollPrompt.Boon"),
callback: (event, button, dialog) => {
const formData = new FormDataExtended$1(dialog.element.querySelector('form')).object;
formData.diceModifier = "dl";
return formData;
}
},
render: (html) => {
//html.on('click', 'button[data-preset]', handleFruitPreset);
{
action: "submit",
label: game.i18n.localize("MGT2.RollPrompt.Roll"),
icon: '<i class="fa-solid fa-dice"></i>',
default: true,
callback: (event, button, dialog) => {
return new FormDataExtended$1(dialog.element.querySelector('form')).object;
}
},
close: () => {
reject('User closed dialog without making a selection.');
},
});
dialog.render(true);
});
}
}
class EditorFullViewDialog extends Dialog {
constructor(dialogData = {}, options = {}) {
super(dialogData, options);
this.options.classes = ["mgt2", game.settings.get("mgt2", "theme"), "sheet"];
this.options.resizable = true;
}
static async create(title, html) {
const htmlContent = await renderTemplate("systems/mgt2/templates/editor-fullview.html", {
config: CONFIG.MGT2,
html: html
});
const results = new Promise(resolve => {
new this({
title: title,
content: htmlContent,
buttons: {
//close: { label: game.i18n.localize("MGT2.Close") }
}
}).render(true);
});
return results;
}
}
class ActorConfigDialog extends Dialog {
constructor(dialogData = {}, options = {}) {
super(dialogData, options);
this.options.classes = ["mgt2", game.settings.get("mgt2", "theme"), "sheet"];
}
static async create(system) {
const htmlContent = await renderTemplate("systems/mgt2/templates/actors/actor-config-sheet.html", {
config: CONFIG.MGT2,
system: system
});
const results = new Promise(resolve => {
new this({
title: "Configuration",
content: htmlContent,
buttons: {
submit: {
label: game.i18n.localize("MGT2.Save"),
icon: '<i class="fa-solid fa-floppy-disk"></i>',
callback: (html) => {
const formData = new FormDataExtended(html[0].querySelector('form')).object;
resolve(formData);
},
{
action: "bane",
label: game.i18n.localize("MGT2.RollPrompt.Bane"),
callback: (event, button, dialog) => {
const formData = new FormDataExtended$1(dialog.element.querySelector('form')).object;
formData.diceModifier = "dh";
return formData;
}
}
}).render(true);
]
});
return results;
}
}
class ActorCharacteristicDialog extends Dialog {
// https://foundryvtt.wiki/en/development/api/dialog
constructor(dialogData = {}, options = {}) {
super(dialogData, options);
this.options.classes = ["mgt2", game.settings.get("mgt2", "theme"), "sheet"];
}
const { DialogV2 } = foundry.applications.api;
const { renderTemplate } = foundry.applications.handlebars;
const { FormDataExtended } = foundry.applications.ux;
static async create(name, show, showMax, showAll = false) {
const htmlContent = await renderTemplate("systems/mgt2/templates/actors/actor-config-characteristic-sheet.html", {
name: name,
show: show,
showMax: showMax,
showAll: showAll
});
const results = new Promise(resolve => {
new this({
title: "Configuration: " + name,
content: htmlContent,
buttons: {
submit: {
label: game.i18n.localize("MGT2.Save"),
icon: '<i class="fa-solid fa-floppy-disk"></i>',
callback: (html) => {
const formData = new FormDataExtended(html[0].querySelector('form')).object;
resolve(formData);
},
}
async function _dialogWithForm(title, templatePath, templateData) {
const htmlContent = await renderTemplate(templatePath, templateData);
game.settings.get("mgt2", "theme");
return await DialogV2.wait({
window: { title },
content: htmlContent,
rejectClose: false,
buttons: [
{
action: "submit",
label: game.i18n.localize("MGT2.Save"),
icon: '<i class="fa-solid fa-floppy-disk"></i>',
default: true,
callback: (event, button, dialog) => {
return new FormDataExtended(dialog.element.querySelector('form')).object;
}
}).render(true);
});
return results;
}
}
class TraitEditDialog extends Dialog {
constructor(dialogData = {}, options = {}) {
super(dialogData, options);
this.options.classes = ["mgt2", game.settings.get("mgt2", "theme"), "sheet"];
}
static async create(data) {
const htmlContent = await renderTemplate("systems/mgt2/templates/actors/trait-sheet.html", {
config: CONFIG.MGT2,
data: data
});
const title = data.hasOwnProperty("name") && data.name !== undefined ? data.name : game.i18n.localize("MGT2.Actor.EditTrait");
const results = new Promise(resolve => {
new this({
title: title,
content: htmlContent,
buttons: {
submit: {
label: game.i18n.localize("MGT2.Save"),
icon: '<i class="fa-solid fa-floppy-disk"></i>',
callback: (html) => {
const formData = new FormDataExtended(html[0].querySelector('form')).object;
resolve(formData);
},
}
//cancel: { label: "Cancel" }
}
// close: (html) => {
// console.log("This always is logged no matter which option is chosen");
// const formData = new FormDataExtended(html[0].querySelector('form')).object;
// resolve(formData);
// }
}).render(true);
});
return results;
}
}
]
});
}
class CharacterPrompts {
static async openConfig(system) {
return await ActorConfigDialog.create(system);
return _dialogWithForm(
"Configuration",
"systems/mgt2/templates/actors/actor-config-sheet.html",
{ config: CONFIG.MGT2, system }
);
}
static async openCharacteristic(name, hide, showMax, showAll = false) {
return await ActorCharacteristicDialog.create(name, hide, showMax, showAll);
static async openCharacteristic(name, show, showMax, showAll = false) {
return _dialogWithForm(
"Configuration: " + name,
"systems/mgt2/templates/actors/actor-config-characteristic-sheet.html",
{ name, show, showMax, showAll }
);
}
static async openTraitEdit(data) {
return await TraitEditDialog.create(data);
const title = data.name ?? game.i18n.localize("MGT2.Actor.EditTrait");
return _dialogWithForm(
title,
"systems/mgt2/templates/actors/trait-sheet.html",
{ config: CONFIG.MGT2, data }
);
}
static async openEditorFullView(title, html) {
return await EditorFullViewDialog.create(title, html);
}
const htmlContent = await renderTemplate("systems/mgt2/templates/editor-fullview.html", {
config: CONFIG.MGT2,
html
});
game.settings.get("mgt2", "theme");
await DialogV2.wait({
window: { title },
content: htmlContent,
rejectClose: false,
buttons: [
{
action: "close",
label: game.i18n.localize("MGT2.Close") || "Fermer",
default: true,
callback: () => null
}
]
});
}
}
class TravellerCharacterSheet extends MGT2ActorSheet {
@@ -1792,7 +1665,7 @@ class TravellerCharacterSheet extends MGT2ActorSheet {
/** @override */
tabGroups = {
primary: "inventory",
sidebar: "inventory",
characteristics: "core",
inventory: "onhand",
}
@@ -2468,7 +2341,7 @@ class TravellerCharacterSheet extends MGT2ActorSheet {
return;
}
let roll = await new Roll(rollFormula, this.actor.getRollData()).roll({ async: true, rollMode: userRollData.rollMode });
let roll = await new Roll(rollFormula, this.actor.getRollData()).roll({ rollMode: userRollData.rollMode });
if (isInitiative && this.token?.combatant) {
await this.token.combatant.update({ initiative: roll.total });
@@ -2480,7 +2353,6 @@ class TravellerCharacterSheet extends MGT2ActorSheet {
formula: roll._formula,
tooltip: await roll.getTooltip(),
total: Math.round(roll.total * 100) / 100,
type: CONST.CHAT_MESSAGE_TYPES.ROLL,
showButtons: true,
showLifeButtons: false,
showRollRequest: false,
@@ -2500,7 +2372,7 @@ class TravellerCharacterSheet extends MGT2ActorSheet {
chatData.rollFailure = true;
}
const html = await renderTemplate("systems/mgt2/templates/chat/roll.html", chatData);
const html = await foundry.applications.handlebars.renderTemplate("systems/mgt2/templates/chat/roll.html", chatData);
chatData.content = html;
let flags = null;
@@ -2640,12 +2512,14 @@ class TravellerItemSheet extends HandlebarsApplicationMixin(foundry.applications
},
}
/** @override */
static PARTS = {
sheet: {
// template is dynamic — resolved in _prepareContext / _renderHTML
template: "",
},
/** Dynamic PARTS: template resolved per item type */
get PARTS() {
const type = this.document?.type ?? "item";
return {
sheet: {
template: `systems/mgt2/templates/items/${type}-sheet.html`,
},
};
}
/** Resolve template dynamically based on item type */
@@ -2699,7 +2573,7 @@ class TravellerItemSheet extends HandlebarsApplicationMixin(foundry.applications
systemFields: item.system.schema.fields,
isEditable: this.isEditable,
isGM: game.user.isGM,
config: CONFIG,
config: CONFIG.MGT2,
settings: settings,
containers: containers,
computers: computers,
@@ -2713,13 +2587,16 @@ class TravellerItemSheet extends HandlebarsApplicationMixin(foundry.applications
/** @override — resolve the per-type template before rendering */
async _renderHTML(context, options) {
const templatePath = `systems/mgt2/templates/items/${this.document.type}-sheet.html`;
const html = await renderTemplate(templatePath, context);
const html = await foundry.applications.handlebars.renderTemplate(templatePath, context);
return { sheet: html };
}
/** @override — put rendered HTML into the window content */
_replaceHTML(result, content, options) {
content.innerHTML = result.sheet;
// Inject theme class dynamically (can't use game.settings in static DEFAULT_OPTIONS)
const theme = game.settings.get("mgt2", "theme");
if (theme) this.element.classList.add(theme);
this._activateTabGroups();
this._bindItemEvents();
}
@@ -2878,6 +2755,17 @@ class TravellerItemSheet extends HandlebarsApplicationMixin(foundry.applications
const preloadHandlebarsTemplates = async function() {
const templatePaths = [
"systems/mgt2/templates/items/armor-sheet.html",
"systems/mgt2/templates/items/career-sheet.html",
"systems/mgt2/templates/items/computer-sheet.html",
"systems/mgt2/templates/items/contact-sheet.html",
"systems/mgt2/templates/items/container-sheet.html",
"systems/mgt2/templates/items/disease-sheet.html",
"systems/mgt2/templates/items/equipment-sheet.html",
"systems/mgt2/templates/items/item-sheet.html",
"systems/mgt2/templates/items/species-sheet.html",
"systems/mgt2/templates/items/talent-sheet.html",
"systems/mgt2/templates/items/weapon-sheet.html",
"systems/mgt2/templates/items/parts/sheet-configuration.html",
"systems/mgt2/templates/items/parts/sheet-physical-item.html",
"systems/mgt2/templates/roll-prompt.html",
@@ -2887,7 +2775,6 @@ const preloadHandlebarsTemplates = async function() {
"systems/mgt2/templates/actors/actor-config-characteristic-sheet.html",
"systems/mgt2/templates/actors/trait-sheet.html",
"systems/mgt2/templates/editor-fullview.html"
//"systems/mgt2/templates/actors/parts/actor-characteristic.html"
];
return loadTemplates(templatePaths);
@@ -2895,42 +2782,32 @@ const preloadHandlebarsTemplates = async function() {
class ChatHelper {
// _injectContent(message, type, html) {
// _setupCardListeners(message, html);
// }
static setupCardListeners(message, html, messageData) {
if (!message || !html) {
static setupCardListeners(message, element, messageData) {
if (!message || !element) {
return;
}
// if (SettingsUtility.getSettingValue(SETTING_NAMES.MANUAL_DAMAGE_MODE) > 0) {
// html.find('.card-buttons').find(`[data-action='rsr-${ROLL_TYPE.DAMAGE}']`).click(async event => {
// await _processDamageButtonEvent(message, event);
// });
// }
html.find('button[data-action="rollDamage"]').click(async event => {
//ui.notifications.warn("rollDamage");
await this._processRollDamageButtonEvent(message, event);
element.querySelectorAll('button[data-action="rollDamage"]').forEach(el => {
el.addEventListener('click', async event => {
await this._processRollDamageButtonEvent(message, event);
});
});
html.find('button[data-action="damage"]').click(async event => {
//ui.notifications.warn("damage");
await this._applyChatCardDamage(message, event);
//await _processApplyButtonEvent(message, event);
element.querySelectorAll('button[data-action="damage"]').forEach(el => {
el.addEventListener('click', async event => {
await this._applyChatCardDamage(message, event);
});
});
html.find('button[data-action="healing"]').click(async event => {
ui.notifications.warn("healing");
//await _processApplyTotalButtonEvent(message, event);
element.querySelectorAll('button[data-action="healing"]').forEach(el => {
el.addEventListener('click', async event => {
ui.notifications.warn("healing");
});
});
html.find('button[data-index]').click(async event => {
await this._processRollButtonEvent(message, event);
element.querySelectorAll('button[data-index]').forEach(el => {
el.addEventListener('click', async event => {
await this._processRollButtonEvent(message, event);
});
});
}
@@ -2940,8 +2817,7 @@ class ChatHelper {
let buttons = message.flags.mgt2.buttons;
const index = event.target.dataset.index;
const button = buttons[index];
let roll = await new Roll(button.formula, {}).roll({ async: true });
//console.log(message);
let roll = await new Roll(button.formula, {}).roll();
const chatData = {
user: game.user.id,
@@ -2949,15 +2825,11 @@ class ChatHelper {
formula: roll._formula,
tooltip: await roll.getTooltip(),
total: Math.round(roll.total * 100) / 100,
//formula: isPrivate ? "???" : roll._formula,
//tooltip: isPrivate ? "" : await roll.getTooltip(),
//total: isPrivate ? "?" : Math.round(roll.total * 100) / 100,
type: CONST.CHAT_MESSAGE_TYPES.ROLL,
rollObjectName: button.message.objectName,
rollMessage: MGT2Helper.format(button.message.flavor, Math.round(roll.total * 100) / 100),
};
const html = await renderTemplate("systems/mgt2/templates/chat/roll.html", chatData);
const html = await foundry.applications.handlebars.renderTemplate("systems/mgt2/templates/chat/roll.html", chatData);
chatData.content = html;
return roll.toMessage(chatData);
}
@@ -2967,7 +2839,7 @@ class ChatHelper {
event.stopPropagation();
let rollFormula = message.flags.mgt2.damage.formula;
let roll = await new Roll(rollFormula, {}).roll({ async: true });
let roll = await new Roll(rollFormula, {}).roll();
let speaker;
let selectTokens = canvas.tokens.controlled;
@@ -2985,30 +2857,18 @@ class ChatHelper {
formula: roll._formula,
tooltip: await roll.getTooltip(),
total: Math.round(roll.total * 100) / 100,
type: CONST.CHAT_MESSAGE_TYPES.ROLL,
showButtons: true,
hasDamage: true,
rollTypeName: rollTypeName,
rollObjectName: message.flags.mgt2.damage.rollObjectName
};
const html = await renderTemplate("systems/mgt2/templates/chat/roll.html", chatData);
const html = await foundry.applications.handlebars.renderTemplate("systems/mgt2/templates/chat/roll.html", chatData);
chatData.content = html;
return roll.toMessage(chatData);
}
async _processDamageButtonEvent(message, event) {
event.preventDefault();
event.stopPropagation();
//message.flags[MODULE_SHORT].manualDamage = false
//message.flags[MODULE_SHORT].renderDamage = true;
// current user/actor
await ItemUtility.runItemAction(null, message, ROLL_TYPE.DAMAGE);
}
static _applyChatCardDamage(message, event) {
const roll = message.rolls[0];
return Promise.all(canvas.tokens.controlled.map(t => {
@@ -3193,8 +3053,8 @@ Hooks.once("init", async function () {
});
Hooks.on("renderChatMessage", (message, html, messageData) => {
ChatHelper.setupCardListeners(message, html, messageData);
Hooks.on("renderChatMessageHTML", (message, element, messageData) => {
ChatHelper.setupCardListeners(message, element, messageData);
});
// Preload template partials