Compare commits

..

No commits in common. "383f66e2979333118dad6651c35239532f3be6d2" and "f617b26fe17b769afeabbd97065bdabd1f41487e" have entirely different histories.

6 changed files with 115 additions and 249 deletions

View file

@ -1,4 +1,4 @@
# The Mill's Fabula - v0.3.0 # The Mill's Fabula - v0.2.0
This little [FoundryVTT](https://foundryvtt.com/) module is a collection of compendiums and functionalities This little [FoundryVTT](https://foundryvtt.com/) module is a collection of compendiums and functionalities
to power our Fabula Ultima campaigns. to power our Fabula Ultima campaigns.
@ -31,31 +31,25 @@ highlighting the tokens that have played for this combat turn.
The module supports going back and forth in the combat rounds, as well as going back in the turn order. The module supports going back and forth in the combat rounds, as well as going back in the turn order.
(Though because of limitations of the Fabula Ultima system, does not allow going *forward* in the turn order.) (Though because of limitations of the Fabula Ultima system, does not allow going *forward* in the turn order.)
### Settings ## Settings
- An image to use for the default/idle border - An image to use for the default/idle border
- An image to use for the took turn/played/inactive border - An image to use for the took turn/played/inactive border
### Limitations ## Limitations
There are currently one minor issue that might be fixed : There are currently two main issues that need to be fixed :
1. The token borders will be incorrect when switching between different encounters in the same scene 1. The tokens will not be updated when the GM is not on the scene.
- Producing a combat event or switching away and back to the scene will fix it. - Indeed, the Fabula Ultima system seems to prevent players from receiving combat events,
- It doesn't appear that there is an event on combat switch that could be hooked into, which makes fixing the so the GM is the only one that can receive them and update the tokens. That means they need to be in the active
issue uncertain. combat scene for the changes to take effect.
- However, given that the module only uses the current state, if the GM comes back to the scene and a combat event is
## Token UI adjustments triggered, the token borders will become correct.
2. The token borders will be incorrect when switching to a new scene
Given that we add a border *on* the tokens, it conflicts with the base attribute bars which are drawn over the token's - It is unclear why, but apparently switching to another scene is very different from loading a new scene,
square. and the combat encounter of the scene is not available when tokens are created. This means that the combat status,
The token UI adjustments move the two attribute bars below the token, outside its space, first HP then mana. and thus the border type, cannot be properly determined on scene switch.
As this is where the nameplate of the token should be, move it above the token instead. - This can be fixed by receiving a combat update, either from the players or the GM, on the scene.
### Limitations
- The token's detailed UI when right-clicking will overlap the bars in their new positions (it already overlapped the name)
- The current implementation relies on monkey patching, which make it vulnerable to compatibility issues with
other modules manipulating the same methods.
# Compendia # Compendia

View file

@ -17,14 +17,6 @@
"Hint": "The image to use as a border for player tokens that have taken their turn." "Hint": "The image to use as a border for player tokens that have taken their turn."
} }
} }
},
"TokenUiAdjust": {
"Settings": {
"Enabled": {
"Name": "Token UI adjustments",
"Hint": "Moves both attribute bars below the token and the nameplate above."
}
}
} }
} }
} }

View file

@ -17,14 +17,6 @@
"Hint": "L'image à utiliser comme bordure pour les jetons joueurs qui ont joué leur tour." "Hint": "L'image à utiliser comme bordure pour les jetons joueurs qui ont joué leur tour."
} }
} }
},
"TokenUiAdjust": {
"Settings": {
"Enabled": {
"Name": "Ajustements d'interface des tokens",
"Hint": "Déplace les barres d'attributs sous le token et le nom au dessus."
}
}
} }
} }
} }

View file

@ -2,7 +2,7 @@
"id": "the-mills-fabula", "id": "the-mills-fabula",
"title": "Fabula Moletrina", "title": "Fabula Moletrina",
"description": "A module implementing our idiosyncratic ideas for the Mill's campaigns.", "description": "A module implementing our idiosyncratic ideas for the Mill's campaigns.",
"version": "0.3.0", "version": "0.2.0",
"compatibility": { "compatibility": {
"minimum": "12", "minimum": "12",
"verified": "12", "verified": "12",
@ -50,11 +50,9 @@
} }
], ],
"esmodules": [ "esmodules": [
"scripts/token_combat_border.mjs", "scripts/token_combat_border.mjs"
"scripts/token_ui_adjust.mjs"
], ],
"socket": true, "socket": true,
"packs": [ "packs": [
{ {
"name": "Macros", "name": "Macros",
@ -69,18 +67,10 @@
"flags": {} "flags": {}
} }
], ],
"packFolders": [
{
"name": "Fabula Moletrina",
"sorting": "a",
"color": "#d3b719",
"packs": ["Macros"]
}
],
"license": "LICENSE", "license": "LICENSE",
"readme": "README.md", "readme": "README.md",
"url": "https://git.tfk-astrodome.net/trotFunky/TheMillsFabula/src/branch/release", "url": "https://git.tfk-astrodome.net/trotFunky/TheMillsFabula/src/branch/release",
"manifest": "https://git.tfk-astrodome.net/trotFunky/TheMillsFabula/raw/branch/release/module.json", "manifest": "https://git.tfk-astrodome.net/trotFunky/TheMillsFabula/raw/branch/release/module.json",
"download": "https://git.tfk-astrodome.net/trotFunky/TheMillsFabula/archive/v0.3.0.zip" "download": "https://git.tfk-astrodome.net/trotFunky/TheMillsFabula/archive/v0.2.0.zip"
} }

View file

@ -1,7 +1,6 @@
import * as MillsFabula from "./mills_fabula.mjs"; import * as MillsFabula from "./mills_fabula.mjs";
import { FUHooks } from "/systems/projectfu/module/hooks.mjs" import { FUHooks } from "/systems/projectfu/module/hooks.mjs"
import { FU } from "/systems/projectfu/module/helpers/config.mjs" import { FU } from "/systems/projectfu/module/helpers/config.mjs"
import { FUCombat } from '/systems/projectfu/module/ui/combat.mjs';
// NOTES // NOTES
// Tokens are working PIXI.js objects, can .addChild() a sprite // Tokens are working PIXI.js objects, can .addChild() a sprite
@ -15,8 +14,7 @@ import { FUCombat } from '/systems/projectfu/module/ui/combat.mjs';
*/ */
let socket; let socket;
const SocketMessages = Object.freeze({ const SocketMessages = Object.freeze({
CombatUpdateBorder: "combat-update-border", UpdateBorder: "update-border",
SetBorder: "set-border",
}) })
const BorderSettings = Object.freeze({ const BorderSettings = Object.freeze({
@ -80,14 +78,16 @@ function create_new_border(type, width, height, visible) {
} }
/** /**
* Toggle the borders of a token. * Toggle the borders of a token by ID, as we cannot pass the Token object via SocketLib (most objects being recursive).
* @param {Token} token The token to update * @param {string} token_id The ID of the token to update
* @param {boolean} played Status of the token to update * @param {boolean} played Status of the token to update
*/ */
function token_set_border_visibility(token, played) { 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. // Get the borders we manually added in the `drawToken` hook, if it has any.
let borders = token.children.filter((child) => child.borderType); let borders = token.children.filter((child) => child.borderType);
// console.debug("↓↓↓ set_border_visibility ↓↓↓");
// console.debug(token)
for (let border of borders) { for (let border of borders) {
if (border.borderType === BorderTypes.Active) { if (border.borderType === BorderTypes.Active) {
border.visible = !played; border.visible = !played;
@ -95,105 +95,39 @@ function token_set_border_visibility(token, played) {
border.visible = played; 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. * 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 {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 * @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 * @returns {boolean} If the token has played in the current or requested round
*/ */
function token_has_played(token, round = -1) { function token_has_played(token, round = -1) {
/** // console.debug("↓↓↓ token_has_played ↓↓↓");
* When we change scene, the token might not be updated with the combat // console.debug(token.inCombat)
* data, so we might need to go check ourselves if they are not set. if (!token.inCombat) {
* @type {FUCombat} return false;
*/
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;
}
} }
/**
* 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] 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. // No token has taken a turn, or all turns were reverted.
if (!round_turns_taken || round_turns_taken.length === 0) { if (!round_turns_taken || round_turns_taken.length === 0) {
return false; return false;
} }
// console.debug("↑↑↑ token_has_played ↑↑↑");
// Token might have played, let's search now. // Token might have played, let's search now.
return round_turns_taken.includes(combatant.id) 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))
} }
/** /**
@ -203,102 +137,119 @@ function token_combat_visibility_remote_update(scene_id, token_id, round) {
*/ */
function combat_hook_update_token_borders(combat, data) { function combat_hook_update_token_borders(combat, data) {
// Turns is the array of *tokens having turns in this encounter*. // 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) { 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 // 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, // use the previous round rather than the new one. Use the round contained in data instead, which is always
// which is always the new one. // the new one.
socket.executeForEveryone(SocketMessages.CombatUpdateBorder, socket.executeForEveryone(SocketMessages.UpdateBorder, combatant.token.id,
combatant.sceneId, combatant.tokenId, data.round).then(); token_has_played(combatant?.token.object, data.round)).then();
} }
// console.debug("↑↑↑ combat_hook_update_token_borders ↑↑↑");
} }
function combat_border_main() { function combat_border_main() {
socket.register(SocketMessages.CombatUpdateBorder, token_combat_visibility_remote_update); Hooks.once("init", () => {
socket.register(SocketMessages.SetBorder, token_remote_set_border_visibility); 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.UpdateBorder, token_set_border_visibility);
})
// Create the borders from defined textures and add them to the tokens when they are first created on canvas. // 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) => { 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 // Only apply the borders to player tokens
if (drawn_token.actor?.type !== "character") if (drawn_token.actor?.type !== "character")
return; return;
let has_played_turn = token_has_played(drawn_token); 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(); 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.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)); drawn_token.addChild(create_new_border(BorderTypes.Played, token_size.width, token_size.height, has_played_turn));
// console.log("============")
}) })
Hooks.on("ready", () => { 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. // 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. // So register them for the GM only, who will execute the updates for players via SocketLib.
if (!game.user?.isGM) { if (!game.user?.isGM) {
return; return;
} }
// console.debug("↓↓↓ Registering ↓↓↓")
Hooks.on("combatTurn", combat_hook_update_token_borders); Hooks.on("combatTurn", combat_hook_update_token_borders);
Hooks.on("combatRound", 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. // 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) => { Hooks.on(FUHooks.COMBAT_EVENT, (combat_event) => {
if (combat_event.type === FU.combatEvent.endOfCombat) { if (combat_event.type === FU.combatEvent.endOfCombat) {
for (let combatant of combat_event.combatants) { for (let combatant of combat_event.combatants) {
// End of combat, clear all tokens. // End of combat, clear all tokens.
socket.executeForEveryone(SocketMessages.SetBorder, socket.executeForEveryone(SocketMessages.UpdateBorder, combatant.token?.id, false).then();
combatant.sceneId, combatant.tokenId, false).then();
} }
} }
}) })
}) })
} }
// We need to setup socketlib early, doing it in the `init` hook is too late. combat_border_main()
Hooks.once("socketlib.ready", () => {
socket = socketlib.registerModule(MillsFabula.id);
})
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'
})
if (!game.settings.get(MillsFabula.id, BorderSettings.BorderEnabled)) {
return;
}
// 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));
combat_border_main()
});

View file

@ -1,53 +0,0 @@
import * as MillsFabula from "./mills_fabula.mjs";
const TokenUiSettings = Object.freeze({
Enabled: "uiAdjustEnabled",
})
Hooks.once("setup", () => {
game.settings.register(MillsFabula.id, TokenUiSettings.Enabled, {
name: "MF.TokenUiAdjust.Settings.Enabled.Name",
hint: "MF.TokenUiAdjust.Settings.Enabled.Hint",
type: Boolean,
config: true,
scope: "world",
requiresReload: true,
default: true,
})
if (!game.settings.get(MillsFabula.id, TokenUiSettings.Enabled)) {
return;
}
/** Padding above and below the token, percentage of a grid square. */
const top_bot_padding = 5;
Hooks.on("drawToken", (drawn_token) => {
/*
* Drawing attribute bars and nameplates are handled by private functions called
* after the `drawToken` hook. That means that we cannot change their position in this hook.
*
* We could update them in the `refreshToken` hook, which works, but is called a lot more than
* necessary and needs a view update (move the view) from the canvas to show the updates.
*
* Instead : we can replace the draw functions handling the bars and nameplate.
* They are still useful, so make of copy bound to the drawn token so that we can call them
* in our replacement function, then make the changes that we need.
*/
let base_drawBar = drawn_token._drawBar.bind(drawn_token);
drawn_token._drawBar = (number, bar, data) => {
const token_height = drawn_token.getSize().height
const padding = drawn_token.scene.grid.size * top_bot_padding/100;
base_drawBar(number, bar, data)
bar.position.set(0, token_height + padding + number * bar.height)
}
let base_refreshNameplate = drawn_token._refreshNameplate.bind(drawn_token);
drawn_token._refreshNameplate = () => {
const token_width = drawn_token.getSize().width;
const padding = drawn_token.scene.grid.size * top_bot_padding/100;
base_refreshNameplate()
drawn_token.nameplate.position.set(token_width/2, -(padding + drawn_token.nameplate.height))
}
})
})