TheMillsFabula/scripts/token_combat_border.mjs
trotFunky 6c3ce53101 Initial commit - v0.1.0
This initial version adds a custom compendium with a
macro for the tinkerer, and a dynamic token border
to track player turn status.

It has a couple of known major bugs but is needed for
an upcoming game, with fixes hopefully later.

It is only compatible with Foundry v12 at this time.
2025-05-12 22:55:13 +01:00

234 lines
9.2 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({
UpdateBorder: "update-border",
})
const BorderSettings = Object.freeze({
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 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
* @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;
// 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 ↑↑↑");
}
/**
* 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)
}
/**
* 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 ↓↓↓");
// 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();
}
// console.debug("↑↑↑ combat_hook_update_token_borders ↑↑↑");
}
function combat_border_main() {
Hooks.once("init", () => {
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: true,
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: true,
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.UpdateBorder, token_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) => {
// 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", () => {
// 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.
// 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();
}
}
})
})
}
combat_border_main()