PDA

View Full Version : Additional "Health" Bar



Thinkcrown
May 23rd, 2025, 19:19
I'd like to put together an extension that:


Creates an additional health bar widget to the right of the current, for the purposes of tracking stress a la Darkest Dungeon.
The core gameplay loop would feature this stress bar increasing on stress accumulation (rather than decreasing on hit point loss). I imagine this would be done by utilizing effects that "heal" (to increase the bar from zero, and up), and utilizing code in the extension to search for a specific key word, taking and changing the integer that follows it.
Here is a mockup image: 64458
The stress bar would have the same health bar option of being constant, or hover over (because I use hover over for health bars).
This bar retains the same colour/saturation/brightness (so no code needed for changing colour at different tiers).
At "max hp" for the stress bar, the token's associated character gains a new effect on the CT tracker (such as Heart Attack (auto system shock roll, stunned for [[6 turns] - [1 turn per CON mod]] (minimum 1))).
The "max HP" for this "health" (stress) bar is determined by the token's associated PC/NPC sheet's HD# + half CON mod—in other words, the highest face on the highest hit die (e.g., "8" for a 17d8 HD Vampire, "10" for a 2d10 HD 2nd-Level Paladin) plus half the creature's Constitution ability modifier.
Of course, just because that's what determines the max integer for the bar doesn't mean I want the bar lowering half the con mod / HD—because I don't, that sounds like a coding nightmare (and also, not the intention either). I don't care/don't need a "Current Stress / Maximum Stress" sub-window section in sheets (that would be cool, but more complicated to code than I can handle atm, surely).
So, instead, I imagine the stress bar should take (and update) its max integer from the number immediately following some specified key-word added to (anywhere that's easiest to do this) on the PC/NPC sheet.
I'm unsure whether the health bar widget-and-scripts/etc. are part of the CoreRPG code, or individual to each subsequent ruleset. If the former, I imagine I'd make this for CoreRPG. If the latter, it would be relegated to the 5E Ruleset.


My current understanding of Lua, XML and FG storage is limited to:


I know the extension is an .ext file (software, or manually renaming to .zip, then unzipping, to open—and rezipping, then renaming back to .ext, to close).
I know the insides feature: graphics folder, scripts folder (scripts being the heart of the extension, and in .lua format), strings folder (strings in .xml format, and... (these ones) used for adding menu options?), and finally the "extension.xml" file, which I assume acts as the helper/brain; plugging the rest into each other, and into the ruleset. (There's also the LICENSE.txt and README.md of course.)
I have access to all of the various guides.
I have a moderate understanding of Python (different from Lua and XML, but coding is coding in many ways).


Bashed-together code collected to begin theorizing how the actual end-result code might work:

Scripts
stress_graphics.lua
-- Options Hover
local option_Stress_Hover_Over = false;

function onInit()
OptionsManager.registerOption2('STRESS_HOVER_OVER' , false, 'option_Stress_Hover_Over', 'option_entry_cycler',
{
labels = 'One|Two',
values = '1|2',
baselabel = 'option_val_off',
baseval = 'off',
default = 'Off'
});
end

--[[
Script to create a stress bar.
]]--

-- Stress bar = full token height when stress 100%
function drawStressBar(tokenCT, widgetStressBar, bVisible)
local nPercentStressed, sStatus, sColor = TokenManager2.getStressInfo(CombatManager.getCTFro mToken(tokenCT));
widgetStressBar = tokenCT.addBitmapWidget("stressbar_horizontal");
widgetStressBar.sendToBack();
widgetStressBar.setName("stressbar");
widgetStressBar.setColor(sColor);
widgetStressBar.setTooltipText(sStatus);
widgetStressBar.setVisible(bVisible);
updateStressBarScale(tokenCT, nPercentStressed);
end

-- Scale stress bar
function updateStressBarScale(tokenCT, nPercentStressed)
local widgetStressBar = tokenCT.findWidget("stressbar");

if widgetStressBar then

--[[
local hToken, wToken = tokenCT.getSize();
widgetStressBar.setSize(hToken, wToken);
local hBar, wBar = widgetStressBar.getSize();

token_stress_minbar = 0;

--Resize bar to match stress percentage
if hToken >= token_stress_minbar then
hBar = (math.max(1.0 - nPercentStressed, 0) * (math.min(hToken, hBar) - token_stress_minbar)) + token_stress_minbar;
else
hBar = token_stress_minbar;
end
]]--

local hScaled = getStressBarHeightScale(tokenCT, nPercentStressed);

-- making stress bars wider and taller, appearing on top, resize and place ratio wise due to different map grids and resolution sizes

-- Stress bar

widgetStressBar.setSize(hScaled, 10, "right");
widgetStressBar.setPosition("bottom right", getRightPositioning(tokenCT, nPercentStressed), -10);
end
end
end


-- returns a % scaled version width of the stress bar, considers accumulated stress
function getStressBarHeightScale(tokenCT, nPercentStressed)
local sSize = Helper.getActorSize(tokenCT);
if (nPercentStressed > 1) then nPercentStressed = 1; end -- set to 1 if above 100% stress (prevents negative stress)
local nScaledHeight = 100 - (nPercentStressed * 100); -- scale = % of token height

if (sSize == 'Large') then
nScaledHeight = nScaledHeight * 2;
elseif (sSize == 'Huge') then
nScaledHeight = nScaledHeight * 3;
elseif (sSize == 'Gargantuan') then
nScaledHeight = nScaledHeight * 4;
end

return nScaledHeight;
end

-- returns a % scaled version of stress bar, for token (grid 80%)
function getLeftPositioning(tokenCT, nPercentStressed)
local sSize = Helper.getActorSize(tokenCT);
local nPositioning = 0;


-- if auto scale 80% / 100%
local sAutoScaleSetting = OptionsManager.getOption("TASG"); -- off | 80 | 100

if ( (sAutoScaleSetting == '80') or (sAutoScaleSetting == 'off') ) then
nPositioning = 40;
elseif (sAutoScaleSetting == '100') then
nPositioning = 48;
end

-- shift location based on size of actor
if (sSize == 'Large') then
nPositioning = math.floor( nPositioning * 2 );
elseif (sSize == 'Huge') then
nPositioning = math.floor( nPositioning * 3 );
elseif (sSize == 'Gargantuan') then
nPositioning = math.floor( nPositioning * 3.8 );
end

-- Debug
nPositioning = math.floor( nPositioning - (nPositioning * nPercentStressed / 2 ) );

return nPositioning;
end

helper_functions.lua

--[[
Helper functions for the extension
]] --
-- returns the text describing the size of the token, possible sizes: Tiny, Small, Medium, Large, Huge, Gargantuan
function getActorSize(tokenCT)
local ctEntry = CombatManager.getCTFromToken(tokenCT);
local actor = ActorManager.getActorFromCT(ctEntry);

local dbPath = DB.getPath(actor.sCreatureNode, 'size');
local sSize = DB.getText(dbPath);

return sSize;
end

--
-- code required to link nPercentStressed to a integer next to unique keywords in individual character or NPC sheets goes here
--

-- resizes condition art to span token size
-- scaling is an optional parameter, if nil set to 1
function resizeForTokenSize(tokenCT, widget, scaling)
if (scaling == nil) then scaling = 1; end
local baseSize = 80;
local sSize = getActorSize(tokenCT);

-- change size depending on token size description
if (sSize == 'Tiny') or (sSize == 'Small') then
widget.setSize(baseSize * 0.5 * scaling, baseSize * 0.5 * scaling);
elseif (sSize == 'Large') then
widget.setSize(baseSize * 2 * scaling, baseSize * 2 * scaling);
elseif (sSize == 'Huge') then
widget.setSize(baseSize * 3 * scaling, baseSize * 3 * scaling);
elseif (sSize == 'Gargantuan') then
widget.setSize(baseSize * 4 * scaling, baseSize * 4 * scaling);
else
widget.setSize(baseSize * scaling, baseSize * scaling);
end
end


Strings
strings_stressed.xml

<?xml version="1.0" encoding="iso-8859-1"?>
<root>
<string name="option_Stress">Stress</string>
<string name="option_Stress_Hover_Over">Hover over token for Stress bar</string>
</root>

Operator
extension.xml

<?xml version="1.0" encoding="iso-8859-1"?>
<root release="3.0" version="1">
<properties>
<loadorder>50</loadorder>
<announcement text="Stress Barred v0.0.1" font="emotefont" icon="stress" />

<name>Feature: Stress Barred</name>
<version>v0.0.1</version>

<author>(Code Thief)R. Whyte, (Actual authors of majority of code) Styrmir T. and R. Hagelstrom</author>
<description>Adds stress as an "inverted" (rising) additional "health bar", and adds the "Stress" damage type/healing effect</description>

<ruleset>
<name>5E</name>
</ruleset>
</properties>
<base>
<icon name="stress" file="graphics/icons/stress.png">
</icon>
<includefile source="strings/strings_stress.xml" />
<script name="EffectsManagerStressBarred" file="scripts/manager_effect_stress_barred.lua" />

<!-- Stress Bar -->
<script name="StressGraphicUpdater" file="scripts/stress_graphics.lua" />
<includefile source="graphics/graphics_icons.xml" />
</base>
</root>

Most code ripped from Styrmir and Rhagelstrom.

MrDDT
May 23rd, 2025, 19:46
Super NICE, I love it.

Thinkcrown
July 23rd, 2025, 00:55
Hey hey. I was wondering if anyone knows what the "window" that precedes a number of dot operators in the code references. For example, in ct_host_entry.xml:

function update() window.onHealthChanged(); end

I've been able to locate almost everything else on my own, as most end up being the shortened script names for lua files, but "window" eludes me, quite frustratingly.

I can't solve script execution errors to attempt to a field like onStressChanged unless I can access whatever file (or etc) this "window" refers to.

Moon Wizard
July 23rd, 2025, 00:59
It is documented in the Developer Guide.
https://fantasygroundsunity.atlassian.net/wiki/spaces/FGCP/pages/996644496/Ruleset+-+Scripting#Script-Block-Scope

It's a variable that is automatically registered for any window control object to point to the window instance that contains the control (i.e. stringcontrol/stringfield, numbercontrol/numberfield, etc.). It is not valid in other contexts (such as window instance, token instance, etc.)

Regards,
JPG

Thinkcrown
July 23rd, 2025, 18:50
More than I could have hoped for. Thank you very much.

Thinkcrown
November 21st, 2025, 21:00
Hey! I've gotten the additional health-like bar to function (for the most part) for the host-side, but I've been having trouble getting the additional bar to automatically appear when logging in on the client-side (or when drag-dropping a token from the CT to a map on both the client side and the host side).

I've wracked my brain attempting to find the code that decides to generate even the normal health bar on log-in (as I assume it might help me get a jumping off point to track down where the bug is located) but I haven't been able to find it. Desperate, I've returned to ask for any insights!

Some additional details that might help locate the problem:

Altering the "current stress" value (think "wound" for HP) on the CT or player's sheet triggers (I assume) the onStressUpdate functions, which ARE able to force the new bar to appear (at least until the token is deleted or the session ends).
The top-right corner display (the small pop-up window thingy in the top-right corner that shows whatever token your cursor is hovering over) ALWAYS shows the new additional bar (which is great! that's what I want there, but also on the actual tokens themselves!).

65839 Host GM PoV

65840 Client Player PoV

Thinkcrown
November 23rd, 2025, 03:06
Update. The "or when drag-dropping a token from the CT to a map on both the client side and the host side" issue, I solved (fixed subtle syntax in the combat manager).

Inspired. I went through all the files again, but I still couldn't figure what's stopping the additional "health" (stress) bar from loading (being visible) on the client (player) PoV on logging into the server. Sigh.

GKEnialb
November 26th, 2025, 21:16
For one of my old extensions, I had to send a message from the host to the client to update a token on the client side. Here's the relevant snippet:



-- notifies clients and other extensions that height changed
function notifyHeightChange(token)
local msgOOB = {}
msgOOB.type = OOB_MSGTYPE_TOKENHEIGHTCHANGE
local ctNode = getCTFromToken(token)
msgOOB.sNode = nil
if ctNode then
msgOOB.sNode = ctNode.getNodeName()
end
Comm.deliverOOBMessage(msgOOB)
end

-- notifies clients to update token height
function updateTokenHeightIndicators(msgOOB)
if msgOOB.sNode then
local ctNode = DB.findNode(msgOOB.sNode)
if ctNode then
local token = CombatManager.getTokenFromCT(ctNode)
displayHeight(token)
end
end
end

Thinkcrown
December 1st, 2025, 19:58
Thank you for sharing your insights and codeblock examples with me, GKEnialb. I really appreciate it!

GKEnialb
December 5th, 2025, 19:28
Thank you for sharing your insights and codeblock examples with me, GKEnialb. I really appreciate it!

No worries. Let me know if you need anything else (or the whole set of code from the old extension).