From 3f0f81ee545557738a12a437f3ebd22be90b957b Mon Sep 17 00:00:00 2001 From: trotFunky Date: Fri, 30 May 2025 21:35:48 +0100 Subject: [PATCH 01/17] Start work on v0.3.0 --- README.md | 2 +- module.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4c0ae2c..2172534 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# The Mill's Fabula - v0.2.0 +# The Mill's Fabula - v0.3.0-dev This little [FoundryVTT](https://foundryvtt.com/) module is a collection of compendiums and functionalities to power our Fabula Ultima campaigns. diff --git a/module.json b/module.json index 4145ab5..8ff3426 100644 --- a/module.json +++ b/module.json @@ -2,7 +2,7 @@ "id": "the-mills-fabula", "title": "Fabula Moletrina", "description": "A module implementing our idiosyncratic ideas for the Mill's campaigns.", - "version": "0.2.0", + "version": "0.3.0", "compatibility": { "minimum": "12", "verified": "12", @@ -72,5 +72,5 @@ "readme": "README.md", "url": "https://git.tfk-astrodome.net/trotFunky/TheMillsFabula/src/branch/release", "manifest": "https://git.tfk-astrodome.net/trotFunky/TheMillsFabula/raw/branch/release/module.json", - "download": "https://git.tfk-astrodome.net/trotFunky/TheMillsFabula/archive/v0.2.0.zip" + "download": "https://git.tfk-astrodome.net/trotFunky/TheMillsFabula/archive/v0.3.0.zip" } From fe9b56acaa13f2743e83083a2daa13f4e5fcb963 Mon Sep 17 00:00:00 2001 From: trotFunky Date: Fri, 30 May 2025 21:38:02 +0100 Subject: [PATCH 02/17] Introduce token UI adjustment script Most of the token UI like tracking bars go over the token in its own square. However, the dynamic borders do the same and conflict with them. This script will handle moving the already existing UI elements to free up the view of the token. --- module.json | 3 ++- scripts/token_ui_adjust.mjs | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 scripts/token_ui_adjust.mjs diff --git a/module.json b/module.json index 8ff3426..05b71e1 100644 --- a/module.json +++ b/module.json @@ -50,7 +50,8 @@ } ], "esmodules": [ - "scripts/token_combat_border.mjs" + "scripts/token_combat_border.mjs", + "scripts/token_ui_adjust.mjs" ], "socket": true, "packs": [ diff --git a/scripts/token_ui_adjust.mjs b/scripts/token_ui_adjust.mjs new file mode 100644 index 0000000..d9fe92f --- /dev/null +++ b/scripts/token_ui_adjust.mjs @@ -0,0 +1,2 @@ +import * as MillsFabula from "./mills_fabula.mjs"; + From ab02dd139a95fac877bd5f4972e91f901761630b Mon Sep 17 00:00:00 2001 From: trotFunky Date: Sun, 1 Jun 2025 18:21:57 +0100 Subject: [PATCH 03/17] README: Fix feature sub-section levels Each main feature should have its own sub-sections for settings, limitations, etc. This was not the case for the combat borders : lower the title level for its subsections --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2172534..ff63d83 100644 --- a/README.md +++ b/README.md @@ -31,12 +31,12 @@ highlighting the tokens that have played for this combat turn. The module supports going back and forth in the combat rounds, as well as going back in the turn order. (Though because of limitations of the Fabula Ultima system, does not allow going *forward* in the turn order.) -## Settings +### Settings - An image to use for the default/idle border - An image to use for the took turn/played/inactive border -## Limitations +### Limitations There are currently two main issues that need to be fixed : 1. The tokens will not be updated when the GM is not on the scene. From 22dc500801d8cf0ccfbab9394cb1aa2068154593 Mon Sep 17 00:00:00 2001 From: trotFunky Date: Sun, 1 Jun 2025 18:37:18 +0100 Subject: [PATCH 04/17] Implement Token UI adjustments Implement the sub-module moving the attribute bars and nameplate to work well with our dynamic borders. The attribute bars are moved below the token, one on top of the other, and the nameplate is moved above. This is done by monkey patching the `_drawBar()` and `_refreshNameplate()` functions from the Token class. This is vulnerable to compatibility issues with other modules, but is the only way I found for the changes to be shown without further input. Indeed, those functions are called after the `drawToken` hook, which makes it impossible to move those UI elements there. We can do it in the `refreshToken` hook, which works *but* requires the canvas view to be moved at least once for the position change to be reflected, which looks a bit broken. So we monkey patch the functions, but bind them beforehand to be able to call them within our custom function as we don't want to duplicate what they do. Update README and translations for the added setting. --- README.md | 13 ++++++++++ lang/en.json | 8 ++++++ lang/fr.json | 8 ++++++ scripts/token_ui_adjust.mjs | 51 +++++++++++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+) diff --git a/README.md b/README.md index ff63d83..e2897c0 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,19 @@ There are currently two main issues that need to be fixed : and thus the border type, cannot be properly determined on scene switch. - This can be fixed by receiving a combat update, either from the players or the GM, on the scene. +## Token UI adjustments + +Given that we add a border *on* the tokens, it conflicts with the base attribute bars which are drawn over the token's +square. +The token UI adjustments move the two attribute bars below the token, outside its space, first HP then mana. +As this is where the nameplate of the token should be, move it above the token instead. + +### Limitations + +- The token's detailed UI when right-clicking will overlap the bars in their new positions (it already overlapped the name) +- The current implementation relies on monkey patching, which make it vulnerable to compatibility issues with + other modules manipulating the same methods. + # Compendia - The only thing in the compendium pack is a macro automating the tinkerer's alchemy potions, diff --git a/lang/en.json b/lang/en.json index 12be337..9a0f3ce 100644 --- a/lang/en.json +++ b/lang/en.json @@ -17,6 +17,14 @@ "Hint": "The image to use as a border for player tokens that have taken their turn." } } + }, + "TokenUiAdjust": { + "Settings": { + "Enabled": { + "Name": "Token UI adjustments", + "Hint": "Moves both attribute bars below the token and the nameplate above." + } + } } } } diff --git a/lang/fr.json b/lang/fr.json index b0dc8f5..de723f1 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -17,6 +17,14 @@ "Hint": "L'image à utiliser comme bordure pour les jetons joueurs qui ont joué leur tour." } } + }, + "TokenUiAdjust": { + "Settings": { + "Enabled": { + "Name": "Ajustements d'interface des tokens", + "Hint": "Déplace les barres d'attributs sous le token et le nom au dessus." + } + } } } } diff --git a/scripts/token_ui_adjust.mjs b/scripts/token_ui_adjust.mjs index d9fe92f..44af60e 100644 --- a/scripts/token_ui_adjust.mjs +++ b/scripts/token_ui_adjust.mjs @@ -1,2 +1,53 @@ import * as MillsFabula from "./mills_fabula.mjs"; +const TokenUiSettings = Object.freeze({ + Enabled: "uiAdjustEnabled", +}) + +Hooks.once("setup", () => { + game.settings.register(MillsFabula.id, TokenUiSettings.Enabled, { + name: "MF.TokenUiAdjust.Settings.Enabled.Name", + hint: "MF.TokenUiAdjust.Settings.Enabled.Hint", + type: Boolean, + config: true, + scope: "world", + requiresReload: true, + default: true, + }) + + if (!game.settings.get(MillsFabula.id, TokenUiSettings.Enabled)) { + return; + } + + /** Padding above and below the token, percentage of a grid square. */ + const top_bot_padding = 5; + + Hooks.on("drawToken", (drawn_token) => { + /* + * Drawing attribute bars and nameplates are handled by private functions called + * after the `drawToken` hook. That means that we cannot change their position in this hook. + * + * We could update them in the `refreshToken` hook, which works, but is called a lot more than + * necessary and needs a view update (move the view) from the canvas to show the updates. + * + * Instead : we can replace the draw functions handling the bars and nameplate. + * They are still useful, so make of copy bound to the drawn token so that we can call them + * in our replacement function, then make the changes that we need. + */ + let base_drawBar = drawn_token._drawBar.bind(drawn_token); + drawn_token._drawBar = (number, bar, data) => { + const token_height = drawn_token.getSize().height + const padding = drawn_token.scene.grid.size * top_bot_padding/100; + base_drawBar(number, bar, data) + bar.position.set(0, token_height + padding + number * bar.height) + } + + let base_refreshNameplate = drawn_token._refreshNameplate.bind(drawn_token); + drawn_token._refreshNameplate = () => { + const token_width = drawn_token.getSize().width; + const padding = drawn_token.scene.grid.size * top_bot_padding/100; + base_refreshNameplate() + drawn_token.nameplate.position.set(token_width/2, -(padding + drawn_token.nameplate.height)) + } + }) +}) From 7cd85468c7e4e0a4c1a92b89f3ecf92f83cbff87 Mon Sep 17 00:00:00 2001 From: trotFunky Date: Sun, 1 Jun 2025 20:26:08 +0100 Subject: [PATCH 05/17] module: Add compendium folder We currently only have a macro compendium, but we might have more in the future (items, powers...). All the Fabula Ultima compendium are also in folders. Add a compendium folder and move the macro compendium within. --- module.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/module.json b/module.json index 05b71e1..98b4406 100644 --- a/module.json +++ b/module.json @@ -54,6 +54,7 @@ "scripts/token_ui_adjust.mjs" ], "socket": true, + "packs": [ { "name": "Macros", @@ -68,6 +69,14 @@ "flags": {} } ], + "packFolders": [ + { + "name": "Fabula Moletrina", + "sorting": "a", + "color": "#d3b719", + "packs": ["Macros"] + } + ], "license": "LICENSE", "readme": "README.md", From 73d2a08b887bc1861e2fa5e96c7575d1acd796f3 Mon Sep 17 00:00:00 2001 From: trotFunky Date: Sun, 1 Jun 2025 22:17:36 +0100 Subject: [PATCH 06/17] Borders: Make clients check combat updates In the Fabula Ultima system, the clients cannot use the combat hooks, only the GM. So the GM orchestrates the token visiblity changes for all clients. Currently, the GM checks the status of tokens when they run the combat hook. However, this doesn't work when the GM is on another scene, as the tokens *do not exist* for their client in this case, as only those in the current scene do. To fix this, we need to do the token status check on all clients, and verify that we are on the proper scene : otherwise there won't be a token to update. Change the method called on the clients by the GM to be a wrapper around token_set_border_visibility() which checks the scene and the token locally. Introduce another method to manually set the border status on clients via SocketLib, used for the end of combat. Remove the mentions of this issue in the README and comments as this commit fixes it. --- README.md | 10 +---- scripts/token_combat_border.mjs | 73 ++++++++++++++++++++++++++------- 2 files changed, 60 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index e2897c0..064b28c 100644 --- a/README.md +++ b/README.md @@ -38,14 +38,8 @@ The module supports going back and forth in the combat rounds, as well as going ### Limitations -There are currently two main issues that need to be fixed : -1. The tokens will not be updated when the GM is not on the scene. - - Indeed, the Fabula Ultima system seems to prevent players from receiving combat events, - so the GM is the only one that can receive them and update the tokens. That means they need to be in the active - combat scene for the changes to take effect. - - However, given that the module only uses the current state, if the GM comes back to the scene and a combat event is - triggered, the token borders will become correct. -2. The token borders will be incorrect when switching to a new scene +There are currently one main issue that need to be fixed : +1. The token borders will be incorrect when switching to a new scene - It is unclear why, but apparently switching to another scene is very different from loading a new scene, and the combat encounter of the scene is not available when tokens are created. This means that the combat status, and thus the border type, cannot be properly determined on scene switch. diff --git a/scripts/token_combat_border.mjs b/scripts/token_combat_border.mjs index 7dd4753..e96431b 100644 --- a/scripts/token_combat_border.mjs +++ b/scripts/token_combat_border.mjs @@ -14,7 +14,8 @@ import { FU } from "/systems/projectfu/module/helpers/config.mjs" */ let socket; const SocketMessages = Object.freeze({ - UpdateBorder: "update-border", + CombatUpdateBorder: "combat-update-border", + SetBorder: "set-border", }) const BorderSettings = Object.freeze({ @@ -78,12 +79,11 @@ function create_new_border(type, width, height, visible) { } /** - * Toggle the borders of a token by ID, as we cannot pass the Token object via SocketLib (most objects being recursive). - * @param {string} token_id The ID of the token to update + * Toggle the borders of a token. + * @param {Token} token The token to update * @param {boolean} played Status of the token to update */ -function token_set_border_visibility(token_id, played) { - let token = canvas.scene?.tokens.get(token_id).object; +function token_set_border_visibility(token, played) { // Get the borders we manually added in the `drawToken` hook, if it has any. let borders = token.children.filter((child) => child.borderType); // console.debug("↓↓↓ set_border_visibility ↓↓↓"); @@ -98,6 +98,26 @@ function token_set_border_visibility(token_id, played) { // console.debug("↑↑↑ set_border_visibility ↑↑↑"); } +/** + * A wrapper around `token_set_border_visibility()` to be invoked via SocketLib. + * It checks that the client is on the proper scene, otherwise the Token wouldn't be found. + * + * @param {string} scene_id The ID of the scene of the token to update + * @param {string} token_id The ID of the token to update + * @param {boolean} played Should the visible borders be the base or the played one ? + */ +function token_remote_set_border_visibility(scene_id, token_id, played) { + /* + * Check that we are on the same scene as the token being updated, + * otherwise it won't exist for us. + */ + if (scene_id !== canvas.scene?.id) { + return; + } + + token_set_border_visibility(canvas.scene?.tokens.get(token_id).object, played) +} + /** * Check if a token has played in a specific round, or the current combat round if it exists for the token. * @param {Token} token The actual token object to check combat status @@ -130,6 +150,30 @@ function token_has_played(token, round = -1) { return round_turns_taken.includes(token.combatant.id) } +/** + * A wrapper function around `token_set_border_visibility()` that is called via SocketLib for combat updates. + * It is executed by every client, which allows us to check if we are on the proper scene, ensuring that + * we can find the token on each relevant client, rather than only on the GM's side which doesn't + * work if they are not in the combat scene. + * + * Scene and Token are passed by IDs as the Foundry objects are recursive and cannot be passed via SocketLib. + * @param {string} scene_id The ID of the scene of the token to update + * @param {string} token_id The ID of the token to update + * @param {number} round The combat round relating to the update + */ +function token_combat_visibility_remote_update(scene_id, token_id, round) { + /* + * Check that we are on the same scene as the token being updated, + * otherwise it won't exist for us. + */ + if (scene_id !== canvas.scene?.id) { + return; + } + + let token = canvas.scene?.tokens.get(token_id).object; + token_set_border_visibility(token, token_has_played(token, round)) +} + /** * Called by turn and round hooks, so the borders can be updated when going backwards as well as forwards. * @param {Combat} combat The combat in which the token is active @@ -138,14 +182,12 @@ function token_has_played(token, round = -1) { function combat_hook_update_token_borders(combat, data) { // Turns is the array of *tokens having turns in this encounter*. // console.debug("↓↓↓ combat_hook_update_token_borders ↓↓↓"); - // console.debug(combat) - // console.debug(data) for (let combatant of combat.turns) { - // The combat passed by the hook still is on the previous round or turn, which would make the check - // use the previous round rather than the new one. Use the round contained in data instead, which is always - // the new one. - socket.executeForEveryone(SocketMessages.UpdateBorder, combatant.token.id, - token_has_played(combatant?.token.object, data.round)).then(); + // The combat passed by the hook is still on the previous round or turn, which would make the check + // use the previous round rather than the new one. Use the round contained in data instead, + // which is always the new one. + socket.executeForEveryone(SocketMessages.CombatUpdateBorder, + combatant.sceneId, combatant.tokenId, data.round).then(); } // console.debug("↑↑↑ combat_hook_update_token_borders ↑↑↑"); } @@ -193,7 +235,8 @@ function combat_border_main() { Hooks.once("socketlib.ready", () => { socket = socketlib.registerModule(MillsFabula.id); - socket.register(SocketMessages.UpdateBorder, token_set_border_visibility); + socket.register(SocketMessages.CombatUpdateBorder, token_combat_visibility_remote_update); + socket.register(SocketMessages.SetBorder, token_remote_set_border_visibility); }) // Create the borders from defined textures and add them to the tokens when they are first created on canvas. @@ -240,12 +283,12 @@ function combat_border_main() { // console.debug("↑↑↑ Registering ↑↑↑") // No Foundry hook on end of combat, so use Fabula Ultima's. - // FIXME: Does not work when the GM is in another scene, as they don't receive the event. Hooks.on(FUHooks.COMBAT_EVENT, (combat_event) => { if (combat_event.type === FU.combatEvent.endOfCombat) { for (let combatant of combat_event.combatants) { // End of combat, clear all tokens. - socket.executeForEveryone(SocketMessages.UpdateBorder, combatant.token?.id, false).then(); + socket.executeForEveryone(SocketMessages.SetBorder, + combatant.sceneId, combatant.tokenId, false).then(); } } }) From 77ae01a7b3df1c34816622e0fa8228c5b258689f Mon Sep 17 00:00:00 2001 From: trotFunky Date: Sun, 1 Jun 2025 23:42:22 +0100 Subject: [PATCH 07/17] Borders: Don't rely on `.inCombat` to check combat status When changing scenes, *a lot* of data is either empty or points to the previous scene, preventing us from knowing whether a token is in combat or not directly from the token. To work around that, go through the list of combats directly and try to match them with the scene and token we are drawing instead of using `.inCombat`. We cannot rely on `game.combats.active` either as it points to the active combat in the previous scene. This is far more work, but it is the only working solution I found. It depends quite a bit on Fabula Ultima's system combat type which lists combatants. This leaves a more minor issue that the borders are not updated when switching between encounters on the same scene, but this is a rarer scenario. Remove the issue from the README and mention the encounter switch issue. --- README.md | 11 ++++---- scripts/token_combat_border.mjs | 46 ++++++++++++++++++++++++++------- 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 064b28c..67181fd 100644 --- a/README.md +++ b/README.md @@ -38,12 +38,11 @@ The module supports going back and forth in the combat rounds, as well as going ### Limitations -There are currently one main issue that need to be fixed : -1. The token borders will be incorrect when switching to a new scene - - It is unclear why, but apparently switching to another scene is very different from loading a new scene, - and the combat encounter of the scene is not available when tokens are created. This means that the combat status, - and thus the border type, cannot be properly determined on scene switch. - - This can be fixed by receiving a combat update, either from the players or the GM, on the scene. +There are currently one minor issue that might be fixed : +1. The token borders will be incorrect when switching between different encounters in the same scene + - Producing a combat event or switching away and back to the scene will fix it. + - It doesn't appear that there is an event on combat switch that could be hooked into, which makes fixing the + issue uncertain. ## Token UI adjustments diff --git a/scripts/token_combat_border.mjs b/scripts/token_combat_border.mjs index e96431b..6af32bf 100644 --- a/scripts/token_combat_border.mjs +++ b/scripts/token_combat_border.mjs @@ -1,6 +1,7 @@ import * as MillsFabula from "./mills_fabula.mjs"; import { FUHooks } from "/systems/projectfu/module/hooks.mjs" import { FU } from "/systems/projectfu/module/helpers/config.mjs" +import { FUCombat } from '/systems/projectfu/module/ui/combat.mjs'; // NOTES // Tokens are working PIXI.js objects, can .addChild() a sprite @@ -120,6 +121,7 @@ function token_remote_set_border_visibility(scene_id, token_id, played) { /** * Check if a token has played in a specific round, or the current combat round if it exists for the token. + * The function does not rely on the token's internal data, as it is not properly set when changing scenes. * @param {Token} token The actual token object to check combat status * @param {number} [round] The round to check the token's status in, will check the token's combat current turn otherwise * @returns {boolean} If the token has played in the current or requested round @@ -127,15 +129,42 @@ function token_remote_set_border_visibility(scene_id, token_id, played) { function token_has_played(token, round = -1) { // console.debug("↓↓↓ token_has_played ↓↓↓"); // console.debug(token.inCombat) - if (!token.inCombat) { - return false; + /** + * When we change scene, the token might not be updated with the combat + * data, so we might need to go check ourselves if they are not set. + * @type {FUCombat} + */ + let combat = token.combatant?.combat; + /** @type {Combatant} */ + let combatant = token.combatant; + + // We might be changing scene, so let's check if that's true. + if (!combatant) { + /* + * There should only be one active combat per scene. + * If there isn't one, we're sure that the token is not in combat. + */ + let active_scene_combat = game.combats.contents.filter( + (combat) => combat.active && combat.scene.id === token.scene.id) + if (active_scene_combat.length === 0) { + return false; + } + + combat = active_scene_combat[0]; + + // Now search among the combatants + for (let active_combatant of combat.combatants) { + if (active_combatant.tokenId === token.id) { + combatant = active_combatant; + break; + } + } + // We have not found our token in the active combat, assume it isn't in combat. + if (!combatant) { + return false; + } } - /** - * As we check beforehand, combat should always exist. - * @type {Combat} - */ - let combat = token.combatant.combat; let round_turns_taken = combat?.flags.projectfu.CombatantsTurnTaken[round < 0 ? combat?.round : round] // console.debug(`Testing played for round : ${combat?.round}`) console.debug(round_turns_taken) @@ -147,7 +176,7 @@ function token_has_played(token, round = -1) { // console.debug("↑↑↑ token_has_played ↑↑↑"); // Token might have played, let's search now. - return round_turns_taken.includes(token.combatant.id) + return round_turns_taken.includes(combatant.id) } /** @@ -240,7 +269,6 @@ function combat_border_main() { }) // Create the borders from defined textures and add them to the tokens when they are first created on canvas. - // FIXME: Does not work on scene change ! Hooks.on("drawToken", (drawn_token) => { // FIXME: Handle deactivation properly if (!game.settings.get(MillsFabula.id, BorderSettings.BorderEnabled)) { From 95bb1c3445ff964a87a38022c8cc0b8a8576eb5e Mon Sep 17 00:00:00 2001 From: trotFunky Date: Sun, 1 Jun 2025 23:51:26 +0100 Subject: [PATCH 08/17] Borders: Clean up debug prints A lot of console logs where left for debugging the scene switch issues. Those issues being fixed now, they are not useful anymore : remove them. --- scripts/token_combat_border.mjs | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/scripts/token_combat_border.mjs b/scripts/token_combat_border.mjs index 6af32bf..9884a41 100644 --- a/scripts/token_combat_border.mjs +++ b/scripts/token_combat_border.mjs @@ -87,8 +87,7 @@ function create_new_border(type, width, height, visible) { function token_set_border_visibility(token, played) { // Get the borders we manually added in the `drawToken` hook, if it has any. let borders = token.children.filter((child) => child.borderType); - // console.debug("↓↓↓ set_border_visibility ↓↓↓"); - // console.debug(token) + for (let border of borders) { if (border.borderType === BorderTypes.Active) { border.visible = !played; @@ -96,7 +95,6 @@ function token_set_border_visibility(token, played) { border.visible = played; } } - // console.debug("↑↑↑ set_border_visibility ↑↑↑"); } /** @@ -127,8 +125,6 @@ function token_remote_set_border_visibility(scene_id, token_id, played) { * @returns {boolean} If the token has played in the current or requested round */ function token_has_played(token, round = -1) { - // console.debug("↓↓↓ token_has_played ↓↓↓"); - // console.debug(token.inCombat) /** * When we change scene, the token might not be updated with the combat * data, so we might need to go check ourselves if they are not set. @@ -166,15 +162,12 @@ function token_has_played(token, round = -1) { } let round_turns_taken = combat?.flags.projectfu.CombatantsTurnTaken[round < 0 ? combat?.round : round] - // console.debug(`Testing played for round : ${combat?.round}`) - console.debug(round_turns_taken) // No token has taken a turn, or all turns were reverted. if (!round_turns_taken || round_turns_taken.length === 0) { return false; } - // console.debug("↑↑↑ token_has_played ↑↑↑"); // Token might have played, let's search now. return round_turns_taken.includes(combatant.id) } @@ -210,7 +203,6 @@ function token_combat_visibility_remote_update(scene_id, token_id, round) { */ function combat_hook_update_token_borders(combat, data) { // Turns is the array of *tokens having turns in this encounter*. - // console.debug("↓↓↓ combat_hook_update_token_borders ↓↓↓"); for (let combatant of combat.turns) { // The combat passed by the hook is still on the previous round or turn, which would make the check // use the previous round rather than the new one. Use the round contained in data instead, @@ -218,7 +210,6 @@ function combat_hook_update_token_borders(combat, data) { socket.executeForEveryone(SocketMessages.CombatUpdateBorder, combatant.sceneId, combatant.tokenId, data.round).then(); } - // console.debug("↑↑↑ combat_hook_update_token_borders ↑↑↑"); } function combat_border_main() { @@ -279,18 +270,9 @@ function combat_border_main() { return; let has_played_turn = token_has_played(drawn_token); - // console.log(drawn_token) - // console.log(`Is in combat ? ${drawn_token.inCombat}`) - // console.log(`Combatant ? ${drawn_token.combatant}`) - // console.log(`Combat ?`) - // console.log(drawn_token.combatant.combat); - // console.log(game.combats.combats) - const token_size = drawn_token.getSize(); drawn_token.addChild(create_new_border(BorderTypes.Active, token_size.width, token_size.height, !has_played_turn)); drawn_token.addChild(create_new_border(BorderTypes.Played, token_size.width, token_size.height, has_played_turn)); - - // console.log("============") }) @@ -305,10 +287,8 @@ function combat_border_main() { return; } - // console.debug("↓↓↓ Registering ↓↓↓") Hooks.on("combatTurn", combat_hook_update_token_borders); Hooks.on("combatRound", combat_hook_update_token_borders); - // console.debug("↑↑↑ Registering ↑↑↑") // No Foundry hook on end of combat, so use Fabula Ultima's. Hooks.on(FUHooks.COMBAT_EVENT, (combat_event) => { From 9bc73100f333f2c18a625efd8923605f90427bb5 Mon Sep 17 00:00:00 2001 From: trotFunky Date: Mon, 2 Jun 2025 00:00:42 +0100 Subject: [PATCH 09/17] Borders: Move main call in init hook The main function is called at script invocation, which is nice and early but everything we do is done in hooks. This makes it not really useful, and harder to properly disable the module with the settings. So, register our `init` hook at script invocation and call the main function from within. This makes handling the disabling via the setting trivial, and ensures that *everything* is properly disabled. However, the `socketlib.ready` hook fires earlier than the `init` one, so register the socket at invocation as well, but only register the remote methods in the main function. --- scripts/token_combat_border.mjs | 106 ++++++++++++++++---------------- 1 file changed, 52 insertions(+), 54 deletions(-) diff --git a/scripts/token_combat_border.mjs b/scripts/token_combat_border.mjs index 9884a41..9ee4a07 100644 --- a/scripts/token_combat_border.mjs +++ b/scripts/token_combat_border.mjs @@ -213,58 +213,11 @@ function combat_hook_update_token_borders(combat, data) { } function combat_border_main() { - Hooks.once("init", () => { - game.settings.register(MillsFabula.id, BorderSettings.BorderEnabled, { - name: game.i18n.localize('MF.Border.Settings.BorderEnabled.Name'), - hint: game.i18n.localize('MF.Border.Settings.BorderEnabled.Hint'), - type: Boolean, - config: true, - scope: 'world', - requiresReload: true, - default: true - }); - - // Only show the settings if the borders are enabled. - let borders_enabled = game.settings.get(MillsFabula.id, BorderSettings.BorderEnabled); - game.settings.register(MillsFabula.id, BorderSettings.BaseBorderPath, { - name: game.i18n.localize('MF.Border.Settings.BaseBorder.Name'), - hint: game.i18n.localize('MF.Border.Settings.BaseBorder.Hint'), - type: String, - config: borders_enabled, - scope: 'world', - requiresReload: true, - filePicker: 'image', - default: 'modules/the-mills-fabula/assets/default-borders/base.webp' - }); - - game.settings.register(MillsFabula.id, BorderSettings.PlayedBorderPath, { - name: game.i18n.localize('MF.Border.Settings.PlayedBorder.Name'), - hint: game.i18n.localize('MF.Border.Settings.PlayedBorder.Hint'), - type: String, - config: borders_enabled, - scope: 'world', - requiresReload: true, - filePicker: 'image', - default: 'modules/the-mills-fabula/assets/default-borders/played.webp' - }) - - // Create the border textures based on the image chose in the settings. - base_border = PIXI.BaseTexture.from(game.settings.get(MillsFabula.id, BorderSettings.BaseBorderPath)); - played_border = PIXI.BaseTexture.from(game.settings.get(MillsFabula.id, BorderSettings.PlayedBorderPath)); - }); - - Hooks.once("socketlib.ready", () => { - socket = socketlib.registerModule(MillsFabula.id); - socket.register(SocketMessages.CombatUpdateBorder, token_combat_visibility_remote_update); - socket.register(SocketMessages.SetBorder, token_remote_set_border_visibility); - }) + socket.register(SocketMessages.CombatUpdateBorder, token_combat_visibility_remote_update); + socket.register(SocketMessages.SetBorder, token_remote_set_border_visibility); // Create the borders from defined textures and add them to the tokens when they are first created on canvas. Hooks.on("drawToken", (drawn_token) => { - // FIXME: Handle deactivation properly - if (!game.settings.get(MillsFabula.id, BorderSettings.BorderEnabled)) { - return; - } // Only apply the borders to player tokens if (drawn_token.actor?.type !== "character") return; @@ -277,10 +230,6 @@ function combat_border_main() { Hooks.on("ready", () => { - // FIXME: Handle deactivation properly - if (!game.settings.get(MillsFabula.id, BorderSettings.BorderEnabled)) { - return; - } // Players cannot run the combat hooks used here, which trigger for the GM no matter what. // So register them for the GM only, who will execute the updates for players via SocketLib. if (!game.user?.isGM) { @@ -303,4 +252,53 @@ function combat_border_main() { }) } -combat_border_main() +// We need to setup socketlib early, doing it in the `init` hook is too late. +Hooks.once("socketlib.ready", () => { + socket = socketlib.registerModule(MillsFabula.id); +}) + +Hooks.once("init", () => { + game.settings.register(MillsFabula.id, BorderSettings.BorderEnabled, { + name: game.i18n.localize('MF.Border.Settings.BorderEnabled.Name'), + hint: game.i18n.localize('MF.Border.Settings.BorderEnabled.Hint'), + type: Boolean, + config: true, + scope: 'world', + requiresReload: true, + default: true + }); + + // Only show the settings if the borders are enabled. + let borders_enabled = game.settings.get(MillsFabula.id, BorderSettings.BorderEnabled); + game.settings.register(MillsFabula.id, BorderSettings.BaseBorderPath, { + name: game.i18n.localize('MF.Border.Settings.BaseBorder.Name'), + hint: game.i18n.localize('MF.Border.Settings.BaseBorder.Hint'), + type: String, + config: borders_enabled, + scope: 'world', + requiresReload: true, + filePicker: 'image', + default: 'modules/the-mills-fabula/assets/default-borders/base.webp' + }); + + game.settings.register(MillsFabula.id, BorderSettings.PlayedBorderPath, { + name: game.i18n.localize('MF.Border.Settings.PlayedBorder.Name'), + hint: game.i18n.localize('MF.Border.Settings.PlayedBorder.Hint'), + type: String, + config: borders_enabled, + scope: 'world', + requiresReload: true, + filePicker: 'image', + default: 'modules/the-mills-fabula/assets/default-borders/played.webp' + }) + + if (!game.settings.get(MillsFabula.id, BorderSettings.BorderEnabled)) { + return; + } + + // Create the border textures based on the image chose in the settings. + base_border = PIXI.BaseTexture.from(game.settings.get(MillsFabula.id, BorderSettings.BaseBorderPath)); + played_border = PIXI.BaseTexture.from(game.settings.get(MillsFabula.id, BorderSettings.PlayedBorderPath)); + + combat_border_main() +}); From 383f66e2979333118dad6651c35239532f3be6d2 Mon Sep 17 00:00:00 2001 From: trotFunky Date: Mon, 2 Jun 2025 00:05:32 +0100 Subject: [PATCH 10/17] v0.3.0 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 67181fd..803b775 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# The Mill's Fabula - v0.3.0-dev +# The Mill's Fabula - v0.3.0 This little [FoundryVTT](https://foundryvtt.com/) module is a collection of compendiums and functionalities to power our Fabula Ultima campaigns. From cc7db632d1304c0d0c40195966a4c7be78fd298c Mon Sep 17 00:00:00 2001 From: trotFunky Date: Mon, 2 Jun 2025 22:11:14 +0100 Subject: [PATCH 11/17] Borders: Fix borders on active combat change A scene can have multiple combat encounters, but only one can be active at a time. Changing from one to the other does not trigger any of the regular combat hooks, but it can be detected using a combat /document/ hook. Register on the combat document update hook and update the borders when the active state of the encounter is changed. Both the previous and new active combats produce an update, and the previous combat becoming inactive always comes first. This means that we can safely set all token borders from the previous document to the default border and they will be updated accordingly by the new combat becoming active. Update the comment of the data member for `combat_hook_update_token_borders()`, as we re-use it for the combat switch and we didn't use the `turn` member anymore. --- README.md | 8 -------- scripts/token_combat_border.mjs | 27 ++++++++++++++++++++++++++- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 803b775..9444577 100644 --- a/README.md +++ b/README.md @@ -36,14 +36,6 @@ The module supports going back and forth in the combat rounds, as well as going - An image to use for the default/idle border - An image to use for the took turn/played/inactive border -### Limitations - -There are currently one minor issue that might be fixed : -1. The token borders will be incorrect when switching between different encounters in the same scene - - Producing a combat event or switching away and back to the scene will fix it. - - It doesn't appear that there is an event on combat switch that could be hooked into, which makes fixing the - issue uncertain. - ## Token UI adjustments Given that we add a border *on* the tokens, it conflicts with the base attribute bars which are drawn over the token's diff --git a/scripts/token_combat_border.mjs b/scripts/token_combat_border.mjs index 9ee4a07..22235a3 100644 --- a/scripts/token_combat_border.mjs +++ b/scripts/token_combat_border.mjs @@ -199,7 +199,7 @@ function token_combat_visibility_remote_update(scene_id, token_id, round) { /** * Called by turn and round hooks, so the borders can be updated when going backwards as well as forwards. * @param {Combat} combat The combat in which the token is active - * @param {{round: number, turn: number}} data The turn data for this update + * @param {{round: number}} data The turn data for this update */ function combat_hook_update_token_borders(combat, data) { // Turns is the array of *tokens having turns in this encounter*. @@ -239,6 +239,31 @@ function combat_border_main() { Hooks.on("combatTurn", combat_hook_update_token_borders); Hooks.on("combatRound", combat_hook_update_token_borders); + // Hook on the combat document update directly to handle active combat switches. + Hooks.on("updateCombat", (combat, changed) => { + // We only care about switching active combats + if (!("active" in changed)) { + return; + } + + /* + * The *previous* active combat is switched to inactive *first*, so we can + * just revert all borders to the default state. + * When the new combat becomes active, we can just check as if it were a + * regular combat update. + * This makes sure to handle tokens which were in the previous combat, but not + * in the new one. + */ + if (!changed.active) { + for (let combatant of combat.combatants) { + socket.executeForEveryone(SocketMessages.SetBorder, + combatant.sceneId, combatant.tokenId, false).then(); + } + } else { + combat_hook_update_token_borders(combat, {round: combat.round}) + } + }) + // No Foundry hook on end of combat, so use Fabula Ultima's. Hooks.on(FUHooks.COMBAT_EVENT, (combat_event) => { if (combat_event.type === FU.combatEvent.endOfCombat) { From f6437d7884f643baa5b340cd0ef128a118ee0651 Mon Sep 17 00:00:00 2001 From: trotFunky Date: Wed, 4 Jun 2025 11:26:50 +0100 Subject: [PATCH 12/17] Fix version numbers that were forgotten I forgot to update the version numbers in the manifest and README for the previous point update. Make a new "release" to fix it so the module can be properly updated in Foundry. --- README.md | 2 +- module.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9444577..07ecad1 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# The Mill's Fabula - v0.3.0 +# The Mill's Fabula - v0.3.2 This little [FoundryVTT](https://foundryvtt.com/) module is a collection of compendiums and functionalities to power our Fabula Ultima campaigns. diff --git a/module.json b/module.json index 98b4406..6ee5515 100644 --- a/module.json +++ b/module.json @@ -2,7 +2,7 @@ "id": "the-mills-fabula", "title": "Fabula Moletrina", "description": "A module implementing our idiosyncratic ideas for the Mill's campaigns.", - "version": "0.3.0", + "version": "0.3.2", "compatibility": { "minimum": "12", "verified": "12", @@ -82,5 +82,5 @@ "readme": "README.md", "url": "https://git.tfk-astrodome.net/trotFunky/TheMillsFabula/src/branch/release", "manifest": "https://git.tfk-astrodome.net/trotFunky/TheMillsFabula/raw/branch/release/module.json", - "download": "https://git.tfk-astrodome.net/trotFunky/TheMillsFabula/archive/v0.3.0.zip" + "download": "https://git.tfk-astrodome.net/trotFunky/TheMillsFabula/archive/v0.3.2.zip" } From 3914c979b2c7fd28f3f7db8b926714b38bbc9be2 Mon Sep 17 00:00:00 2001 From: trotFunky Date: Tue, 10 Jun 2025 22:49:42 +0100 Subject: [PATCH 13/17] Borders: don't check turns when combat has not started When trying to figure out if a token has played or not, we might get an active combat which hasn't started. In this case the Fabula Ultima flags do not exist yet, and trying to access them will error. Check that the combat we found has indeed been started, otherwise there's no way a token can have played and we don't need to check. --- scripts/token_combat_border.mjs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/token_combat_border.mjs b/scripts/token_combat_border.mjs index 22235a3..3291bb5 100644 --- a/scripts/token_combat_border.mjs +++ b/scripts/token_combat_border.mjs @@ -161,6 +161,10 @@ function token_has_played(token, round = -1) { } } + // The token can't have played if the combat hasn't started yet. + if (!combat.started) + return false; + let round_turns_taken = combat?.flags.projectfu.CombatantsTurnTaken[round < 0 ? combat?.round : round] // No token has taken a turn, or all turns were reverted. From b1bde69a7d22424e3a48d2d8feae1584f8dc4a3b Mon Sep 17 00:00:00 2001 From: trotFunky Date: Tue, 10 Jun 2025 23:00:08 +0100 Subject: [PATCH 14/17] UI: Move the status effect indicators to the right As the token square is already busy due to the border, the status effect indicators get hidden. Move them entirely to the right of the token, so they don't conflict anymore. Do it only for PCs, as NPC tokens do not have the border and the GM will probably have their token UI open more often, which would hide the status effect indicators. --- README.md | 8 +++++--- lang/en.json | 2 +- lang/fr.json | 2 +- scripts/token_ui_adjust.mjs | 6 ++++++ 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 07ecad1..f63a1d1 100644 --- a/README.md +++ b/README.md @@ -38,10 +38,12 @@ The module supports going back and forth in the combat rounds, as well as going ## Token UI adjustments -Given that we add a border *on* the tokens, it conflicts with the base attribute bars which are drawn over the token's -square. +Given that we add a border *on* the tokens, it conflicts with the base attribute bars and status effect indicators which +are drawn over the token's square. The token UI adjustments move the two attribute bars below the token, outside its space, first HP then mana. -As this is where the nameplate of the token should be, move it above the token instead. +As this is where the nameplate of the token should be, move it above the token instead. +The status effect indicators are moved entirely outside the token space as well, to the right, but as it is not useful +for NPCs this is only done for PC tokens. ### Limitations diff --git a/lang/en.json b/lang/en.json index 9a0f3ce..a6a782c 100644 --- a/lang/en.json +++ b/lang/en.json @@ -22,7 +22,7 @@ "Settings": { "Enabled": { "Name": "Token UI adjustments", - "Hint": "Moves both attribute bars below the token and the nameplate above." + "Hint": "Moves both attribute bars below the token, the nameplate above and, for PCs, the status icons to the right." } } } diff --git a/lang/fr.json b/lang/fr.json index de723f1..3630ef7 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -22,7 +22,7 @@ "Settings": { "Enabled": { "Name": "Ajustements d'interface des tokens", - "Hint": "Déplace les barres d'attributs sous le token et le nom au dessus." + "Hint": "Déplace les barres d'attributs sous le token, le nom au dessus et, pour les PJs, les indicateurs de statut à droite." } } } diff --git a/scripts/token_ui_adjust.mjs b/scripts/token_ui_adjust.mjs index 44af60e..bff8393 100644 --- a/scripts/token_ui_adjust.mjs +++ b/scripts/token_ui_adjust.mjs @@ -21,6 +21,8 @@ Hooks.once("setup", () => { /** Padding above and below the token, percentage of a grid square. */ const top_bot_padding = 5; + /** Padding left and right the token, percentage of a grid square. */ + const left_right_padding = 5; Hooks.on("drawToken", (drawn_token) => { /* @@ -49,5 +51,9 @@ Hooks.once("setup", () => { base_refreshNameplate() drawn_token.nameplate.position.set(token_width/2, -(padding + drawn_token.nameplate.height)) } + + if (drawn_token.document.hasPlayerOwner) + drawn_token.effects.position.x += drawn_token.getSize().width + + drawn_token.scene.grid.size * (left_right_padding/100); }) }) From eba433cc0594a75575f24e1767e3456af15e61ff Mon Sep 17 00:00:00 2001 From: trotFunky Date: Tue, 10 Jun 2025 23:04:34 +0100 Subject: [PATCH 15/17] module: Update verified compatibility The module has been tested with newer releases of the system and socketlib, so add them to the verified verisons. --- README.md | 2 +- module.json | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f63a1d1..b9dbd08 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# The Mill's Fabula - v0.3.2 +# The Mill's Fabula - v0.3.3 This little [FoundryVTT](https://foundryvtt.com/) module is a collection of compendiums and functionalities to power our Fabula Ultima campaigns. diff --git a/module.json b/module.json index 6ee5515..9531077 100644 --- a/module.json +++ b/module.json @@ -2,7 +2,7 @@ "id": "the-mills-fabula", "title": "Fabula Moletrina", "description": "A module implementing our idiosyncratic ideas for the Mill's campaigns.", - "version": "0.3.2", + "version": "0.3.3", "compatibility": { "minimum": "12", "verified": "12", @@ -28,7 +28,8 @@ "compatibility": { "verified": [ "2.4.10", - "3.0.2" + "3.0.2", + "3.1.7" ] } } @@ -38,7 +39,10 @@ "id": "socketlib", "type": "module", "compatibility": { - "verified": "1.1.2" + "verified": [ + "1.1.2", + "1.1.3" + ] } } ] @@ -82,5 +86,5 @@ "readme": "README.md", "url": "https://git.tfk-astrodome.net/trotFunky/TheMillsFabula/src/branch/release", "manifest": "https://git.tfk-astrodome.net/trotFunky/TheMillsFabula/raw/branch/release/module.json", - "download": "https://git.tfk-astrodome.net/trotFunky/TheMillsFabula/archive/v0.3.2.zip" + "download": "https://git.tfk-astrodome.net/trotFunky/TheMillsFabula/archive/v0.3.3.zip" } From 67d4b0387ad8987ec43c3e0ce676329095e71e42 Mon Sep 17 00:00:00 2001 From: trotFunky Date: Sun, 6 Jul 2025 21:32:25 +0100 Subject: [PATCH 16/17] Fortune: Introduce fortune display Add a new sub-module that displays Fortune in character and party sheets. In the character sheet, this is an input field and is automatically tracked thanks to flags. Add an icon for the party sheet and the according CSS class. --- README.md | 10 ++- assets/fortune.png | Bin 0 -> 565 bytes lang/en.json | 9 +++ lang/fr.json | 9 +++ module.json | 10 ++- scripts/fortune_integration.mjs | 118 ++++++++++++++++++++++++++++++++ styles/mills_fabula.css | 3 + 7 files changed, 155 insertions(+), 4 deletions(-) create mode 100644 assets/fortune.png create mode 100644 scripts/fortune_integration.mjs create mode 100644 styles/mills_fabula.css diff --git a/README.md b/README.md index b9dbd08..f269d36 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# The Mill's Fabula - v0.3.3 +# The Mill's Fabula - v0.4.0 This little [FoundryVTT](https://foundryvtt.com/) module is a collection of compendiums and functionalities to power our Fabula Ultima campaigns. @@ -51,6 +51,14 @@ for NPCs this is only done for PC tokens. - The current implementation relies on monkey patching, which make it vulnerable to compatibility issues with other modules manipulating the same methods. +## Fortune display + +In our game, "Fortune" is a key currency in the world. +This adds an interactible display in the character sheets, allowing it to be tracked, and in the party sheet, +for a nice overview. + +The value is stored in flags, and will not be lost when disabling the module or option. + # Compendia - The only thing in the compendium pack is a macro automating the tinkerer's alchemy potions, diff --git a/assets/fortune.png b/assets/fortune.png new file mode 100644 index 0000000000000000000000000000000000000000..4d360b39a134559b23e83e3a02310c7848558c68 GIT binary patch literal 565 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%C&rs6b?Si}mUKs7M+SzC z{oH>NSwSk3J%W507^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10f-TA0 z-G$*l2rk&Wd@@jkv%n*=n1O*?7=#%aX3dcRTG!+0;usQfI5|Ng;ef}wV@|n?|DX8t zZ}oGDm(N~-IPvT5IUIb!wFa&1DwlJ=)RWDdIMpDtun^Wyz+`mk#?`=6%|4`mB(?8}JT%A*`E z_>|$9{(X*96QzZGu5jel@GY6e-0@MVPO<81y zhvxQ&eLJeURSFkzD=j)zy~1BoLPFwS)@s2J=P)S+EiH~8Vs{01ykeTuDX?03!w;Tx z4FO4^iekov6B=3%9cOkwQO0sGhohW@vDr?}qUo~Yjwbp1W)|i2DbpOKH15V~O#7tZ z%IIGvba?v8&p?wUB!0-|ba=Js99b5&gWv1)zv&gf1w;6`d*`j4{UI%P((1>cfU4e> zcSzv*Nxwq^$|;sd&wdihw-@P7Jm|mXX-MXTgw1nA4mjnWPQ2)xFjf#fsw`F>FVdQ&MBb@0EdFx#{d8T literal 0 HcmV?d00001 diff --git a/lang/en.json b/lang/en.json index a6a782c..377da2d 100644 --- a/lang/en.json +++ b/lang/en.json @@ -25,6 +25,15 @@ "Hint": "Moves both attribute bars below the token, the nameplate above and, for PCs, the status icons to the right." } } + }, + "Fortune": { + "Name": "Fortune", + "Settings": { + "DisplayEnabled": { + "Name": "Fortune display", + "Hint": "Display and track \"Fortune\" currency in the character and party sheets." + } + } } } } diff --git a/lang/fr.json b/lang/fr.json index 3630ef7..b8280a5 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -25,6 +25,15 @@ "Hint": "Déplace les barres d'attributs sous le token, le nom au dessus et, pour les PJs, les indicateurs de statut à droite." } } + }, + "Fortune": { + "Name": "Fortune", + "Settings": { + "DisplayEnabled": { + "Name": "Affichage de la Fortune", + "Hint": "Affiche et gère une monnaie, la \"Fortune\", dans les fiches de personnage et de groupe." + } + } } } } diff --git a/module.json b/module.json index 9531077..fbe6c83 100644 --- a/module.json +++ b/module.json @@ -2,7 +2,7 @@ "id": "the-mills-fabula", "title": "Fabula Moletrina", "description": "A module implementing our idiosyncratic ideas for the Mill's campaigns.", - "version": "0.3.3", + "version": "0.4.0", "compatibility": { "minimum": "12", "verified": "12", @@ -55,7 +55,11 @@ ], "esmodules": [ "scripts/token_combat_border.mjs", - "scripts/token_ui_adjust.mjs" + "scripts/token_ui_adjust.mjs", + "scripts/fortune_integration.mjs" + ], + "styles": [ + "styles/mills_fabula.css" ], "socket": true, @@ -86,5 +90,5 @@ "readme": "README.md", "url": "https://git.tfk-astrodome.net/trotFunky/TheMillsFabula/src/branch/release", "manifest": "https://git.tfk-astrodome.net/trotFunky/TheMillsFabula/raw/branch/release/module.json", - "download": "https://git.tfk-astrodome.net/trotFunky/TheMillsFabula/archive/v0.3.3.zip" + "download": "https://git.tfk-astrodome.net/trotFunky/TheMillsFabula/archive/v0.4.0.zip" } diff --git a/scripts/fortune_integration.mjs b/scripts/fortune_integration.mjs new file mode 100644 index 0000000..dc92409 --- /dev/null +++ b/scripts/fortune_integration.mjs @@ -0,0 +1,118 @@ +import * as MillsFabula from "./mills_fabula.mjs"; + +const fortune_flag = "char_fortune" + +const FortuneSettings = Object.freeze({ + EnableFortuneDisplay: "enableFortuneDisplay", +}) + +/** + * Find all the characters in the group sheet and insert their Fortune amount + * within the resources, copying the system's style. + * @param {DocumentSheet|foundry.applications.api.DocumentSheetV2} app The source Application + * @param {HTMLElement} html The rendered HTML + * @returns {Promise} Does not return + */ +async function party_sheet_fortune(app, html) { + if (!(app instanceof foundry.applications.api.DocumentSheetV2)) + html = html[0] /* Retrieve the internal DOM element */ + else + console.warn("AppV2 is not properly tested yet") + + let character_divs = html.querySelectorAll(".section-container.plate.character") + for (let char_div of character_divs) { + /** @type{FUActor} */ + let actor = await fromUuid(char_div.getAttribute("data-uuid")) + let resources = char_div.getElementsByClassName("resources")[0] + let icon = document.createElement("i"); + icon.classList.add("fuk", "icon-aff", "mf-fortune") + icon.setAttribute("data-tooltip", game.i18n.localize('MF.Fortune.Name')) + + /* Insert the Fortune elements before Fabula points, so find it */ + let fp_icon = resources.getElementsByClassName("fu-fp")[0] + resources.insertBefore(icon, fp_icon) + /* The system also has whitespace for spacing */ + resources.insertBefore( + document.createTextNode(` ${actor.getFlag(MillsFabula.id, fortune_flag) ?? 0} `), + fp_icon) + } +} + +/** + * Insert a Fortune input at the top of the character sheet, + * following the same styling as the system for Fabula points. + * @param {DocumentSheet|foundry.applications.api.DocumentSheetV2} app The source Application + * @param {HTMLElement} html The rendered HTML + */ +function character_sheet_fortune(app, html) { + /* Only player characters have Fortune. */ + if (app.actor.type !== "character") + return + + if (!(app instanceof foundry.applications.api.DocumentSheetV2)) + html = html[0] /* Retrieve the internal DOM element */ + else + console.warn("AppV2 is not properly tested yet") + + let resources_div = html.getElementsByClassName("header-center")[0].lastElementChild + resources_div.classList.replace("grid-3col", "grid-4col") + + /* + * We want the Fortune to have the same importance/weight as Fabula points, + * so we'll copy the style and bundle them in a 2 column div. + */ + /* TODO: This all should probably be some form of template ? */ + let fortune_fabula_div = document.createElement("div") + fortune_fabula_div.classList.add("grid", "grid-2col") + + let fortune_div = document.createElement("div") + fortune_div.classList.add("flex-group-center", "resource-content") + let fortune_label = document.createElement("span") + fortune_label.classList.add("resource-label-l") + let fortune_icon = document.createElement("i") + fortune_icon.classList.add("fas", "fa-leaf", "icon") + fortune_label.appendChild(fortune_icon) + fortune_label.appendChild(document.createTextNode(game.i18n.localize('MF.Fortune.Name'))) + fortune_div.appendChild(fortune_label) + let fortune_data_div = document.createElement("div") + fortune_data_div.classList.add("buttons-inc") + let fortune_input = foundry.applications.fields.createNumberInput({ + /* + * By setting the name of the field using this format, the backing + * flag will automatically be picked up by Foundry's code and updated + * when the field is changed on the sheet. + * This works even if it wasn't set previously ! + */ + name: `flags.${MillsFabula.id}.${fortune_flag}`, + value: app.document.getFlag(MillsFabula.id, fortune_flag) ?? 0, + step: 1, + min: 0, + type: "number" + }) + fortune_input.classList.add("fp-resource-inputs") + fortune_data_div.appendChild(fortune_input) + fortune_div.appendChild(fortune_data_div) + + fortune_fabula_div.appendChild(fortune_div) + /* The last element should always be the Fabula Points div. */ + fortune_fabula_div.appendChild(resources_div.lastElementChild) + + resources_div.appendChild(fortune_fabula_div) +} + +Hooks.once("init", () => { + game.settings.register(MillsFabula.id, FortuneSettings.EnableFortuneDisplay, { + name: game.i18n.localize('MF.Fortune.Settings.DisplayEnabled.Name'), + hint: game.i18n.localize('MF.Fortune.Settings.DisplayEnabled.Hint'), + type: Boolean, + config: true, + scope: 'world', + requiresReload: true, + default: true + }); + + if (game.settings.get(MillsFabula.id, FortuneSettings.EnableFortuneDisplay)) { + Hooks.on("renderFUPartySheet", party_sheet_fortune) + Hooks.on("renderFUStandardActorSheet", character_sheet_fortune) + } +}) diff --git a/styles/mills_fabula.css b/styles/mills_fabula.css new file mode 100644 index 0000000..6ea8d11 --- /dev/null +++ b/styles/mills_fabula.css @@ -0,0 +1,3 @@ +.mf-fortune { + background-image: url("/modules/the-mills-fabula/assets/fortune.png"); +} From 6d4c5b74dcf694d125967424af6b6e1312a4a5c6 Mon Sep 17 00:00:00 2001 From: trotFunky Date: Sun, 6 Jul 2025 21:48:18 +0100 Subject: [PATCH 17/17] Fortune: Fix indenting in character sheet code When moving this part of the code to a dedicated function, the IDE "helpfully" "fixed" the indentation, making the organization of this part of the code less clear. Re-introduce the indentation to make the structure clearer. --- scripts/fortune_integration.mjs | 46 ++++++++++++++++----------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/scripts/fortune_integration.mjs b/scripts/fortune_integration.mjs index dc92409..5a34984 100644 --- a/scripts/fortune_integration.mjs +++ b/scripts/fortune_integration.mjs @@ -67,30 +67,30 @@ function character_sheet_fortune(app, html) { let fortune_div = document.createElement("div") fortune_div.classList.add("flex-group-center", "resource-content") - let fortune_label = document.createElement("span") - fortune_label.classList.add("resource-label-l") - let fortune_icon = document.createElement("i") - fortune_icon.classList.add("fas", "fa-leaf", "icon") - fortune_label.appendChild(fortune_icon) - fortune_label.appendChild(document.createTextNode(game.i18n.localize('MF.Fortune.Name'))) + let fortune_label = document.createElement("span") + fortune_label.classList.add("resource-label-l") + let fortune_icon = document.createElement("i") + fortune_icon.classList.add("fas", "fa-leaf", "icon") + fortune_label.appendChild(fortune_icon) + fortune_label.appendChild(document.createTextNode(game.i18n.localize('MF.Fortune.Name'))) fortune_div.appendChild(fortune_label) - let fortune_data_div = document.createElement("div") - fortune_data_div.classList.add("buttons-inc") - let fortune_input = foundry.applications.fields.createNumberInput({ - /* - * By setting the name of the field using this format, the backing - * flag will automatically be picked up by Foundry's code and updated - * when the field is changed on the sheet. - * This works even if it wasn't set previously ! - */ - name: `flags.${MillsFabula.id}.${fortune_flag}`, - value: app.document.getFlag(MillsFabula.id, fortune_flag) ?? 0, - step: 1, - min: 0, - type: "number" - }) - fortune_input.classList.add("fp-resource-inputs") - fortune_data_div.appendChild(fortune_input) + let fortune_data_div = document.createElement("div") + fortune_data_div.classList.add("buttons-inc") + let fortune_input = foundry.applications.fields.createNumberInput({ + /* + * By setting the name of the field using this format, the backing + * flag will automatically be picked up by Foundry's code and updated + * when the field is changed on the sheet. + * This works even if it wasn't set previously ! + */ + name: `flags.${MillsFabula.id}.${fortune_flag}`, + value: app.document.getFlag(MillsFabula.id, fortune_flag) ?? 0, + step: 1, + min: 0, + type: "number" + }) + fortune_input.classList.add("fp-resource-inputs") + fortune_data_div.appendChild(fortune_input) fortune_div.appendChild(fortune_data_div) fortune_fabula_div.appendChild(fortune_div)