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.
This commit is contained in:
trotFunky 2025-06-01 22:17:36 +01:00
parent 7cd85468c7
commit 73d2a08b88
2 changed files with 60 additions and 23 deletions

View file

@ -38,14 +38,8 @@ The module supports going back and forth in the combat rounds, as well as going
### Limitations ### Limitations
There are currently two main issues that need to be fixed : There are currently one main issue that need to be fixed :
1. The tokens will not be updated when the GM is not on the scene. 1. The token borders will be incorrect when switching to a new 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
- It is unclear why, but apparently switching to another scene is very different from loading 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 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. and thus the border type, cannot be properly determined on scene switch.

View file

@ -14,7 +14,8 @@ import { FU } from "/systems/projectfu/module/helpers/config.mjs"
*/ */
let socket; let socket;
const SocketMessages = Object.freeze({ const SocketMessages = Object.freeze({
UpdateBorder: "update-border", CombatUpdateBorder: "combat-update-border",
SetBorder: "set-border",
}) })
const BorderSettings = Object.freeze({ 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). * Toggle the borders of a token.
* @param {string} token_id The ID of the token to update * @param {Token} token The token to update
* @param {boolean} played Status of the token to update * @param {boolean} played Status of the token to update
*/ */
function token_set_border_visibility(token_id, played) { function token_set_border_visibility(token, played) {
let token = canvas.scene?.tokens.get(token_id).object;
// Get the borders we manually added in the `drawToken` hook, if it has any. // Get the borders we manually added in the `drawToken` hook, if it has any.
let borders = token.children.filter((child) => child.borderType); let borders = token.children.filter((child) => child.borderType);
// console.debug("↓↓↓ set_border_visibility ↓↓↓"); // console.debug("↓↓↓ set_border_visibility ↓↓↓");
@ -98,6 +98,26 @@ function token_set_border_visibility(token_id, played) {
// console.debug("↑↑↑ set_border_visibility ↑↑↑"); // 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. * 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 * @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) 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. * 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 {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) { function combat_hook_update_token_borders(combat, data) {
// Turns is the array of *tokens having turns in this encounter*. // Turns is the array of *tokens having turns in this encounter*.
// console.debug("↓↓↓ combat_hook_update_token_borders ↓↓↓"); // console.debug("↓↓↓ combat_hook_update_token_borders ↓↓↓");
// console.debug(combat)
// console.debug(data)
for (let combatant of combat.turns) { 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 // 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 // use the previous round rather than the new one. Use the round contained in data instead,
// the new one. // which is always the new one.
socket.executeForEveryone(SocketMessages.UpdateBorder, combatant.token.id, socket.executeForEveryone(SocketMessages.CombatUpdateBorder,
token_has_played(combatant?.token.object, data.round)).then(); combatant.sceneId, combatant.tokenId, data.round).then();
} }
// console.debug("↑↑↑ combat_hook_update_token_borders ↑↑↑"); // console.debug("↑↑↑ combat_hook_update_token_borders ↑↑↑");
} }
@ -193,7 +235,8 @@ function combat_border_main() {
Hooks.once("socketlib.ready", () => { Hooks.once("socketlib.ready", () => {
socket = socketlib.registerModule(MillsFabula.id); 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. // 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 ↑↑↑") // console.debug("↑↑↑ Registering ↑↑↑")
// No Foundry hook on end of combat, so use Fabula Ultima's. // 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) => { Hooks.on(FUHooks.COMBAT_EVENT, (combat_event) => {
if (combat_event.type === FU.combatEvent.endOfCombat) { if (combat_event.type === FU.combatEvent.endOfCombat) {
for (let combatant of combat_event.combatants) { for (let combatant of combat_event.combatants) {
// End of combat, clear all tokens. // 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();
} }
} }
}) })