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.
306 lines
12 KiB
JavaScript
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()
|