TheMillsFabula/scripts/token_combat_border.mjs
trotFunky 95bb1c3445 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.
2025-06-01 23:51:26 +01:00

306 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"
import { FUCombat } from '/systems/projectfu/module/ui/combat.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);
for (let border of borders) {
if (border.borderType === BorderTypes.Active) {
border.visible = !played;
} else {
border.visible = played;
}
}
}
/**
* 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.
* 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
*/
function token_has_played(token, round = -1) {
/**
* 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;
}
}
let round_turns_taken = combat?.flags.projectfu.CombatantsTurnTaken[round < 0 ? combat?.round : round]
// No token has taken a turn, or all turns were reverted.
if (!round_turns_taken || round_turns_taken.length === 0) {
return false;
}
// Token might have played, let's search now.
return round_turns_taken.includes(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*.
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();
}
}
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.
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);
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));
})
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;
}
Hooks.on("combatTurn", combat_hook_update_token_borders);
Hooks.on("combatRound", combat_hook_update_token_borders);
// 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()