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()