diff --git a/README.md b/README.md index 803b775..f269d36 100644 --- a/README.md +++ b/README.md @@ -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 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 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 -Given that we add a border *on* the tokens, it conflicts with the base attribute bars which are drawn over the token's -square. +Given that we add a border *on* the tokens, it conflicts with the base attribute bars and status effect indicators which +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. -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 @@ -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 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 - The only thing in the compendium pack is a macro automating the tinkerer's alchemy potions, diff --git a/assets/fortune.png b/assets/fortune.png new file mode 100644 index 0000000..4d360b3 Binary files /dev/null and b/assets/fortune.png differ diff --git a/lang/en.json b/lang/en.json index 9a0f3ce..377da2d 100644 --- a/lang/en.json +++ b/lang/en.json @@ -22,7 +22,16 @@ "Settings": { "Enabled": { "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." } } } diff --git a/lang/fr.json b/lang/fr.json index de723f1..b8280a5 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -22,7 +22,16 @@ "Settings": { "Enabled": { "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." } } } diff --git a/module.json b/module.json index 98b4406..fbe6c83 100644 --- a/module.json +++ b/module.json @@ -2,7 +2,7 @@ "id": "the-mills-fabula", "title": "Fabula Moletrina", "description": "A module implementing our idiosyncratic ideas for the Mill's campaigns.", - "version": "0.3.0", + "version": "0.4.0", "compatibility": { "minimum": "12", "verified": "12", @@ -28,7 +28,8 @@ "compatibility": { "verified": [ "2.4.10", - "3.0.2" + "3.0.2", + "3.1.7" ] } } @@ -38,7 +39,10 @@ "id": "socketlib", "type": "module", "compatibility": { - "verified": "1.1.2" + "verified": [ + "1.1.2", + "1.1.3" + ] } } ] @@ -51,7 +55,11 @@ ], "esmodules": [ "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, @@ -82,5 +90,5 @@ "readme": "README.md", "url": "https://git.tfk-astrodome.net/trotFunky/TheMillsFabula/src/branch/release", "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" } diff --git a/scripts/fortune_integration.mjs b/scripts/fortune_integration.mjs new file mode 100644 index 0000000..5a34984 --- /dev/null +++ b/scripts/fortune_integration.mjs @@ -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} 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) + } +}) diff --git a/scripts/token_combat_border.mjs b/scripts/token_combat_border.mjs index 9ee4a07..3291bb5 100644 --- a/scripts/token_combat_border.mjs +++ b/scripts/token_combat_border.mjs @@ -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] // 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. * @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) { // 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("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. Hooks.on(FUHooks.COMBAT_EVENT, (combat_event) => { if (combat_event.type === FU.combatEvent.endOfCombat) { diff --git a/scripts/token_ui_adjust.mjs b/scripts/token_ui_adjust.mjs index 44af60e..bff8393 100644 --- a/scripts/token_ui_adjust.mjs +++ b/scripts/token_ui_adjust.mjs @@ -21,6 +21,8 @@ Hooks.once("setup", () => { /** Padding above and below the token, percentage of a grid square. */ 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) => { /* @@ -49,5 +51,9 @@ Hooks.once("setup", () => { base_refreshNameplate() 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); }) }) diff --git a/styles/mills_fabula.css b/styles/mills_fabula.css new file mode 100644 index 0000000..6ea8d11 --- /dev/null +++ b/styles/mills_fabula.css @@ -0,0 +1,3 @@ +.mf-fortune { + background-image: url("/modules/the-mills-fabula/assets/fortune.png"); +}