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.
298 lines
12 KiB
JavaScript
298 lines
12 KiB
JavaScript
import * as MillsFabula from "./mills_fabula.mjs";
|
|
import { FUHooks } from "/systems/projectfu/module/hooks.mjs"
|
|
import { FU } from "/systems/projectfu/module/helpers/config.mjs"
|
|
|
|
// NOTES
|
|
// Tokens are working PIXI.js objects, can .addChild() a sprite
|
|
// Get Token : game.scenes.active.collections.tokens.contents[0]._object
|
|
// Paths are relative to either the core data, or the base data. No need to do anything.
|
|
// https://pixijs.download/v4.8.0/docs/PIXI.Sprite.html
|
|
|
|
/**
|
|
* Socket to use to execute things on clients, may be shared by other scripts of the module.
|
|
* @type {SocketlibSocket}
|
|
*/
|
|
let socket;
|
|
const SocketMessages = Object.freeze({
|
|
CombatUpdateBorder: "combat-update-border",
|
|
SetBorder: "set-border",
|
|
})
|
|
|
|
const BorderSettings = Object.freeze({
|
|
BorderEnabled: "borderEnabled",
|
|
BaseBorderPath: "baseBorderPath",
|
|
PlayedBorderPath: "playedBorderPath",
|
|
})
|
|
|
|
/**
|
|
* The texture that will be used to create the base player token borders
|
|
* Will be created after fetching a path from the settings.
|
|
* @type {PIXI.BaseTexture}
|
|
*/
|
|
let base_border;
|
|
/**
|
|
* The texture that will be used to create the borders for player tokens that have taken their turns
|
|
* Will be created after fetching a path from the settings.
|
|
* @type {PIXI.BaseTexture}
|
|
*/
|
|
let played_border;
|
|
|
|
// Enum for the border types.
|
|
const BorderTypes = Object.freeze({
|
|
Active: "active",
|
|
Played: "played",
|
|
});
|
|
|
|
/**
|
|
* @param {BorderTypes} type The kind of border to create, deciding which texture to use
|
|
* @param {number} width Width of the final sprite on the canvas, in pixels
|
|
* @param {number} height Height of the final sprite on the canvas, in pixels
|
|
* @param {boolean} visible Should the new sprite be visible now ?
|
|
* @returns {PIXI.Sprite} A new sprite object corresponding to the border requested
|
|
*/
|
|
function create_new_border(type, width, height, visible) {
|
|
let border_texture;
|
|
switch (type) {
|
|
case BorderTypes.Active:
|
|
border_texture = base_border;
|
|
break;
|
|
case BorderTypes.Played:
|
|
border_texture = played_border;
|
|
break;
|
|
default:
|
|
throw new TypeError("Invalid BorderTypes Enum value: " + type);
|
|
}
|
|
/** @type {PIXI.Sprite} */
|
|
let border = PIXI.Sprite.from(border_texture);
|
|
border.interactive = false;
|
|
border.width = width;
|
|
border.height = height;
|
|
border.visible = visible;
|
|
|
|
/**
|
|
* Custom member, to be able to toggle border visibility dynamically
|
|
* @type {BorderTypes}
|
|
*/
|
|
border.borderType = type;
|
|
|
|
return border;
|
|
}
|
|
|
|
/**
|
|
* 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, 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;
|
|
} else {
|
|
border.visible = 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
|
|
* @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
|
|
*/
|
|
function token_has_played(token, round = -1) {
|
|
// console.debug("↓↓↓ token_has_played ↓↓↓");
|
|
// console.debug(token.inCombat)
|
|
if (!token.inCombat) {
|
|
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)
|
|
|
|
// 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(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
|
|
* @param {{round: number, turn: 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*.
|
|
// 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,
|
|
// which is always the new one.
|
|
socket.executeForEveryone(SocketMessages.CombatUpdateBorder,
|
|
combatant.sceneId, combatant.tokenId, data.round).then();
|
|
}
|
|
// console.debug("↑↑↑ combat_hook_update_token_borders ↑↑↑");
|
|
}
|
|
|
|
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);
|
|
})
|
|
|
|
// 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)) {
|
|
return;
|
|
}
|
|
// Only apply the borders to player tokens
|
|
if (drawn_token.actor?.type !== "character")
|
|
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("============")
|
|
})
|
|
|
|
|
|
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) {
|
|
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) => {
|
|
if (combat_event.type === FU.combatEvent.endOfCombat) {
|
|
for (let combatant of combat_event.combatants) {
|
|
// End of combat, clear all tokens.
|
|
socket.executeForEveryone(SocketMessages.SetBorder,
|
|
combatant.sceneId, combatant.tokenId, false).then();
|
|
}
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
combat_border_main()
|