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.
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.