Compare commits

...

7 commits
v0.3.0 ... main

Author SHA1 Message Date
6d4c5b74dc Fortune: Fix indenting in character sheet code
When moving this part of the code to a dedicated function, the IDE
"helpfully"  "fixed" the indentation, making the organization
of this part of the code less clear.

Re-introduce the indentation to make the structure clearer.
2025-07-06 21:48:18 +01:00
67d4b0387a Fortune: Introduce fortune display
Add a new sub-module that displays Fortune in character and party sheets.
In the character sheet, this is an input field and is automatically tracked
thanks to flags.

Add an icon for the party sheet and the according CSS class.
2025-07-06 21:32:25 +01:00
eba433cc05 module: Update verified compatibility
The module has been tested with newer releases of the
system and socketlib, so add them to the verified verisons.
2025-06-10 23:04:34 +01:00
b1bde69a7d UI: Move the status effect indicators to the right
As the token square is already busy due to the border, the
status effect indicators get hidden.
Move them entirely to the right of the token, so they don't
conflict anymore.

Do it only for PCs, as NPC tokens do not have the border and
the GM will probably have their token UI open more often, which
would hide the status effect indicators.
2025-06-10 23:03:19 +01:00
3914c979b2 Borders: don't check turns when combat has not started
When trying to figure out if a token has played or not, we might get
an active combat which hasn't started.
In this case the Fabula Ultima flags do not exist yet, and trying to
access them will error.

Check that the combat we found has indeed been started, otherwise
there's no way a token can have played and we don't need to check.
2025-06-10 22:54:18 +01:00
f6437d7884 Fix version numbers that were forgotten
I forgot to update the version numbers in the manifest and README for
the previous point update.
Make a new "release" to fix it so the module can be properly updated
in Foundry.
2025-06-04 11:26:50 +01:00
cc7db632d1 Borders: Fix borders on active combat change
A scene can have multiple combat encounters, but only one
can be active at a time.
Changing from one to the other does not trigger any of the regular
combat hooks, but it can be detected using a combat /document/ hook.

Register on the combat document update hook and update the borders
when the active state of the encounter is changed.

Both the previous and new active combats produce an update, and
the previous combat becoming inactive always comes first.
This means that we can safely set all token borders from the
previous document to the default border and they will be
updated accordingly by the new combat becoming active.

Update the comment of the data member for
`combat_hook_update_token_borders()`, as we re-use it for
the combat switch and we didn't use the `turn` member anymore.
2025-06-02 22:26:21 +01:00
9 changed files with 204 additions and 20 deletions

View file

@ -1,4 +1,4 @@
# The Mill's Fabula - v0.3.0 # The Mill's Fabula - v0.4.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.
@ -36,20 +36,14 @@ The module supports going back and forth in the combat rounds, as well as going
- 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
There are currently one minor issue that might be fixed :
1. The token borders will be incorrect when switching between different encounters in the same scene
- Producing a combat event or switching away and back to the scene will fix it.
- It doesn't appear that there is an event on combat switch that could be hooked into, which makes fixing the
issue uncertain.
## Token UI adjustments ## Token UI adjustments
Given that we add a border *on* the tokens, it conflicts with the base attribute bars which are drawn over the token's Given that we add a border *on* the tokens, it conflicts with the base attribute bars and status effect indicators which
square. are drawn over the token's square.
The token UI adjustments move the two attribute bars below the token, outside its space, first HP then mana. The token UI adjustments move the two attribute bars below the token, outside its space, first HP then mana.
As this is where the nameplate of the token should be, move it above the token instead. As this is where the nameplate of the token should be, move it above the token instead.
The status effect indicators are moved entirely outside the token space as well, to the right, but as it is not useful
for NPCs this is only done for PC tokens.
### Limitations ### Limitations
@ -57,6 +51,14 @@ As this is where the nameplate of the token should be, move it above the token i
- The current implementation relies on monkey patching, which make it vulnerable to compatibility issues with - The current implementation relies on monkey patching, which make it vulnerable to compatibility issues with
other modules manipulating the same methods. other modules manipulating the same methods.
## Fortune display
In our game, "Fortune" is a key currency in the world.
This adds an interactible display in the character sheets, allowing it to be tracked, and in the party sheet,
for a nice overview.
The value is stored in flags, and will not be lost when disabling the module or option.
# Compendia # Compendia
- The only thing in the compendium pack is a macro automating the tinkerer's alchemy potions, - The only thing in the compendium pack is a macro automating the tinkerer's alchemy potions,

BIN
assets/fortune.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 565 B

View file

@ -22,7 +22,16 @@
"Settings": { "Settings": {
"Enabled": { "Enabled": {
"Name": "Token UI adjustments", "Name": "Token UI adjustments",
"Hint": "Moves both attribute bars below the token and the nameplate above." "Hint": "Moves both attribute bars below the token, the nameplate above and, for PCs, the status icons to the right."
}
}
},
"Fortune": {
"Name": "Fortune",
"Settings": {
"DisplayEnabled": {
"Name": "Fortune display",
"Hint": "Display and track \"Fortune\" currency in the character and party sheets."
} }
} }
} }

View file

@ -22,7 +22,16 @@
"Settings": { "Settings": {
"Enabled": { "Enabled": {
"Name": "Ajustements d'interface des tokens", "Name": "Ajustements d'interface des tokens",
"Hint": "Déplace les barres d'attributs sous le token et le nom au dessus." "Hint": "Déplace les barres d'attributs sous le token, le nom au dessus et, pour les PJs, les indicateurs de statut à droite."
}
}
},
"Fortune": {
"Name": "Fortune",
"Settings": {
"DisplayEnabled": {
"Name": "Affichage de la Fortune",
"Hint": "Affiche et gère une monnaie, la \"Fortune\", dans les fiches de personnage et de groupe."
} }
} }
} }

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.4.0",
"compatibility": { "compatibility": {
"minimum": "12", "minimum": "12",
"verified": "12", "verified": "12",
@ -28,7 +28,8 @@
"compatibility": { "compatibility": {
"verified": [ "verified": [
"2.4.10", "2.4.10",
"3.0.2" "3.0.2",
"3.1.7"
] ]
} }
} }
@ -38,7 +39,10 @@
"id": "socketlib", "id": "socketlib",
"type": "module", "type": "module",
"compatibility": { "compatibility": {
"verified": "1.1.2" "verified": [
"1.1.2",
"1.1.3"
]
} }
} }
] ]
@ -51,7 +55,11 @@
], ],
"esmodules": [ "esmodules": [
"scripts/token_combat_border.mjs", "scripts/token_combat_border.mjs",
"scripts/token_ui_adjust.mjs" "scripts/token_ui_adjust.mjs",
"scripts/fortune_integration.mjs"
],
"styles": [
"styles/mills_fabula.css"
], ],
"socket": true, "socket": true,
@ -82,5 +90,5 @@
"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.4.0.zip"
} }

View file

@ -0,0 +1,118 @@
import * as MillsFabula from "./mills_fabula.mjs";
const fortune_flag = "char_fortune"
const FortuneSettings = Object.freeze({
EnableFortuneDisplay: "enableFortuneDisplay",
})
/**
* Find all the characters in the group sheet and insert their Fortune amount
* within the resources, copying the system's style.
* @param {DocumentSheet|foundry.applications.api.DocumentSheetV2} app The source Application
* @param {HTMLElement} html The rendered HTML
* @returns {Promise<void>} Does not return
*/
async function party_sheet_fortune(app, html) {
if (!(app instanceof foundry.applications.api.DocumentSheetV2))
html = html[0] /* Retrieve the internal DOM element */
else
console.warn("AppV2 is not properly tested yet")
let character_divs = html.querySelectorAll(".section-container.plate.character")
for (let char_div of character_divs) {
/** @type{FUActor} */
let actor = await fromUuid(char_div.getAttribute("data-uuid"))
let resources = char_div.getElementsByClassName("resources")[0]
let icon = document.createElement("i");
icon.classList.add("fuk", "icon-aff", "mf-fortune")
icon.setAttribute("data-tooltip", game.i18n.localize('MF.Fortune.Name'))
/* Insert the Fortune elements before Fabula points, so find it */
let fp_icon = resources.getElementsByClassName("fu-fp")[0]
resources.insertBefore(icon, fp_icon)
/* The system also has whitespace for spacing */
resources.insertBefore(
document.createTextNode(` ${actor.getFlag(MillsFabula.id, fortune_flag) ?? 0} `),
fp_icon)
}
}
/**
* Insert a Fortune input at the top of the character sheet,
* following the same styling as the system for Fabula points.
* @param {DocumentSheet|foundry.applications.api.DocumentSheetV2} app The source Application
* @param {HTMLElement} html The rendered HTML
*/
function character_sheet_fortune(app, html) {
/* Only player characters have Fortune. */
if (app.actor.type !== "character")
return
if (!(app instanceof foundry.applications.api.DocumentSheetV2))
html = html[0] /* Retrieve the internal DOM element */
else
console.warn("AppV2 is not properly tested yet")
let resources_div = html.getElementsByClassName("header-center")[0].lastElementChild
resources_div.classList.replace("grid-3col", "grid-4col")
/*
* We want the Fortune to have the same importance/weight as Fabula points,
* so we'll copy the style and bundle them in a 2 column div.
*/
/* TODO: This all should probably be some form of template ? */
let fortune_fabula_div = document.createElement("div")
fortune_fabula_div.classList.add("grid", "grid-2col")
let fortune_div = document.createElement("div")
fortune_div.classList.add("flex-group-center", "resource-content")
let fortune_label = document.createElement("span")
fortune_label.classList.add("resource-label-l")
let fortune_icon = document.createElement("i")
fortune_icon.classList.add("fas", "fa-leaf", "icon")
fortune_label.appendChild(fortune_icon)
fortune_label.appendChild(document.createTextNode(game.i18n.localize('MF.Fortune.Name')))
fortune_div.appendChild(fortune_label)
let fortune_data_div = document.createElement("div")
fortune_data_div.classList.add("buttons-inc")
let fortune_input = foundry.applications.fields.createNumberInput({
/*
* By setting the name of the field using this format, the backing
* flag will automatically be picked up by Foundry's code and updated
* when the field is changed on the sheet.
* This works even if it wasn't set previously !
*/
name: `flags.${MillsFabula.id}.${fortune_flag}`,
value: app.document.getFlag(MillsFabula.id, fortune_flag) ?? 0,
step: 1,
min: 0,
type: "number"
})
fortune_input.classList.add("fp-resource-inputs")
fortune_data_div.appendChild(fortune_input)
fortune_div.appendChild(fortune_data_div)
fortune_fabula_div.appendChild(fortune_div)
/* The last element should always be the Fabula Points div. */
fortune_fabula_div.appendChild(resources_div.lastElementChild)
resources_div.appendChild(fortune_fabula_div)
}
Hooks.once("init", () => {
game.settings.register(MillsFabula.id, FortuneSettings.EnableFortuneDisplay, {
name: game.i18n.localize('MF.Fortune.Settings.DisplayEnabled.Name'),
hint: game.i18n.localize('MF.Fortune.Settings.DisplayEnabled.Hint'),
type: Boolean,
config: true,
scope: 'world',
requiresReload: true,
default: true
});
if (game.settings.get(MillsFabula.id, FortuneSettings.EnableFortuneDisplay)) {
Hooks.on("renderFUPartySheet", party_sheet_fortune)
Hooks.on("renderFUStandardActorSheet", character_sheet_fortune)
}
})

View file

@ -161,6 +161,10 @@ function token_has_played(token, round = -1) {
} }
} }
// The token can't have played if the combat hasn't started yet.
if (!combat.started)
return false;
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]
// No token has taken a turn, or all turns were reverted. // No token has taken a turn, or all turns were reverted.
@ -199,7 +203,7 @@ function token_combat_visibility_remote_update(scene_id, token_id, round) {
/** /**
* Called by turn and round hooks, so the borders can be updated when going backwards as well as forwards. * 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 {Combat} combat The combat in which the token is active
* @param {{round: number, turn: number}} data The turn data for this update * @param {{round: number}} data The turn data for this update
*/ */
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*.
@ -239,6 +243,31 @@ function combat_border_main() {
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);
// Hook on the combat document update directly to handle active combat switches.
Hooks.on("updateCombat", (combat, changed) => {
// We only care about switching active combats
if (!("active" in changed)) {
return;
}
/*
* The *previous* active combat is switched to inactive *first*, so we can
* just revert all borders to the default state.
* When the new combat becomes active, we can just check as if it were a
* regular combat update.
* This makes sure to handle tokens which were in the previous combat, but not
* in the new one.
*/
if (!changed.active) {
for (let combatant of combat.combatants) {
socket.executeForEveryone(SocketMessages.SetBorder,
combatant.sceneId, combatant.tokenId, false).then();
}
} else {
combat_hook_update_token_borders(combat, {round: combat.round})
}
})
// No Foundry hook on end of combat, so use Fabula Ultima's. // No Foundry hook on end of combat, so use Fabula Ultima's.
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) {

View file

@ -21,6 +21,8 @@ Hooks.once("setup", () => {
/** Padding above and below the token, percentage of a grid square. */ /** Padding above and below the token, percentage of a grid square. */
const top_bot_padding = 5; const top_bot_padding = 5;
/** Padding left and right the token, percentage of a grid square. */
const left_right_padding = 5;
Hooks.on("drawToken", (drawn_token) => { Hooks.on("drawToken", (drawn_token) => {
/* /*
@ -49,5 +51,9 @@ Hooks.once("setup", () => {
base_refreshNameplate() base_refreshNameplate()
drawn_token.nameplate.position.set(token_width/2, -(padding + drawn_token.nameplate.height)) drawn_token.nameplate.position.set(token_width/2, -(padding + drawn_token.nameplate.height))
} }
if (drawn_token.document.hasPlayerOwner)
drawn_token.effects.position.x += drawn_token.getSize().width +
drawn_token.scene.grid.size * (left_right_padding/100);
}) })
}) })

3
styles/mills_fabula.css Normal file
View file

@ -0,0 +1,3 @@
.mf-fortune {
background-image: url("/modules/the-mills-fabula/assets/fortune.png");
}