A simple set of macros for a second-by-second initiative tracker

Threads specific to teaching (or step-by-step resources) for writing macros and scripts for use in MapTool.

Moderators: dorpond, trevor, Azhrei, giliath, Gamerdude, jay, Mr.Ice

Post Reply
LarryWoestman
Cave Troll
Posts: 28
Joined: Tue Jul 21, 2020 7:50 pm

A simple set of macros for a second-by-second initiative tracker

Post by LarryWoestman »

I wanted a "who goes next" tracking process for a Fudge-based game that doesn't have an "initiative roll" or "rounds", and tracks time second-by-second. I also wanted some "timers" that kept track of elapsed seconds to use for seeing when a spell is "done" or any other purposes.

The following posts will contain the code for what I developed. I was able to use the existing "initiative" frame for most of it, with custom macros for initializing initiative, adding selected tokens to initiative, "next token in initiative order", setting the timers, ... Those macros are in the GM frame.

There is one "Lib:custom_initiative" library token with various "helper" macros to modify the initiative value for a token, pack and parse the initiative value (since I'm keeping the timers as well as the other initiative info in it) and some macros that define the values used in a couple of properties that affect initiative order.

One "trick" that I am using is that the initiative value is negative so sortInitiative() sorts "smallest first" instead of "largest first", so the lowest second count goes first.

LarryWoestman
Cave Troll
Posts: 28
Joined: Tue Jul 21, 2020 7:50 pm

Re: A simple set of macros for a second-by-second initiative tracker

Post by LarryWoestman »

This is the code for the "modify_initiative" macro that lives on the "Lib:custom_initiative" library token.

It takes a "number of seconds before the next action" and applies it to the token that currently has initiative.

modify_initiative:

Code: Select all

[h: '<!--
Changes the initiative value of the token that currently has the initiative.

The passed-in change_amount is the maximum amount that might be added to the integer_part (seconds)
    and subtracted from any timers that are currently running.  If it turns out that one or more
    timers are within change_amount seconds from going off, then the change_amount is reduced
    to the shortest number of seconds so that the current token will get the initiative again
    at the time the shortest timer goes off.
-->']
[h: '<-- Do not do anything if the initiative list is not initialized -->']
[h, if(getCurrentInitiative() == -1): abort(0)]
[h: change_amount = macro.args]
[h: '<-- Get the inititative value from the current token and split it up into the various pieces -->']
[h: token_id = getInitiativeToken()]
[MACRO("parse_initiative@Lib:custom_initiative"): getInitiative(token_id)]
[h: varsFromStrProp(macro.return)]
[h: token_name = getName(token_id)]
[h: '<!-- figure out how many seconds to change the integer_part and timers -->']
[h: '<!-- also output messages reminding the GM that timers will be going off soon -->']
[h: timer_1_change_amount = change_amount]
[h: timer_2_change_amount = change_amount]
[h: timer_3_change_amount = change_amount]
[if(timer_1_part != 0 && timer_1_part <= change_amount), code:
{
	[h: timer_1_change_amount = timer_1_part]
	[g: "<br>timer 1 will go off in " + timer_1_change_amount + " seconds for " + token_name + "<br>"]
};
{
}]
[if(timer_2_part != 0 && timer_2_part <= change_amount), code:
{
	[h: timer_2_change_amount = timer_2_part]
	[g: "<br>timer 2 will go off in " + timer_2_change_amount + " seconds for " + token_name + "<br>"]
};
{
}]
[if(timer_3_part != 0 && timer_3_part <= change_amount), code:
{
	[h: timer_3_change_amount = timer_3_part]
	[g: "<br>timer 3 will go off in " + timer_3_change_amount + " seconds for " + token_name + "<br>"]
};
{
}]
[h: change_amount = min(change_amount, timer_1_change_amount, timer_2_change_amount, timer_3_change_amount)]
[h: '<-- increase the seconds counter and decrease any active timers by the appropriate amount -->']
[h: integer_part = integer_part + change_amount]
[h, if(timer_1_part != 0): timer_1_part = timer_1_part - change_amount]
[h, if(timer_2_part != 0): timer_2_part = timer_2_part - change_amount]
[h, if(timer_3_part != 0): timer_3_part = timer_3_part - change_amount]
[h: '<!-- put all the pieces of the initiative value back together again -->']
[h: arguments = "integer_part=" + integer_part + "; order_part=" + order_part + "; timer_1_part=" + timer_1_part + "; timer_2_part=" + timer_2_part + "; timer_3_part=" + timer_3_part]
[MACRO("pack_initiative@Lib:custom_initiative"): arguments]
[h: setInitiative(macro.return, getInitiativeToken())]
[h: '<!-- Set the current initiative to -1 to keep from seeing unneeded initiative messages while sorting -->']
[h: setCurrentInitiative(-1)]
[h: sortInitiative()]
[h: '<!-- Set the current initiative to the token with the youngest time -->']
[h: setCurrentInitiative(0)]
Last edited by LarryWoestman on Sun Aug 23, 2020 6:40 pm, edited 1 time in total.

LarryWoestman
Cave Troll
Posts: 28
Joined: Tue Jul 21, 2020 7:50 pm

Re: A simple set of macros for a second-by-second initiative tracker

Post by LarryWoestman »

This is the macro code that packs and parses the initiative value. These macros live on the "Lib:custom_initiative" library token.

There are two properties that are used by this code:
  • Reaction_Time is a rating of how quickly a character can make sense of a situation and figure out what to do about it. It isn't "fast reflexes" so much as "fast thinking", and ranges from 2 seconds to 6 seconds for most characters/monsters. This is used to figure out how long before the next action of the character and as part of the tie-breaker calculations when two characters both have initiative in the same second.
  • Agility is a rating of how easily and quickly a character can move their body. This is used as part of the tie-breaker calculations when two characters both have initiative in the same second.

parse_initiative:

Code: Select all

[h: '<!--
Separates the passed-in initiative value into its pieces.

The sortInititive routine sorts from largest to smallest.  We are tracking seconds, so
    we want to sort "youngest first".  The easiest way to do this is to make the
    initiative value a negative number, and ignore the minus sign as much as possible.

The integer part is seconds since initiative tracking started.

The first two digits of the fractional part is related to the Reaction_Time attribute of the token.
The next two digits of the fractional part is related to the Agility attribute of the token.
The next two digits of the fractional part is a number that starts at 1 and increases for
     each token to provide a tie-breaker value so the same token does not get initiative
     twice in a row when next to another token that happens to have the same agility.
Those 6 digits are combined into the "order_part" in the code below.

The next 9 digits of the fractional part are three "timers" that count down seconds
    with a maximum time of 999 seconds per timer.
-->']
[h: current_init = macro.args]
[h, if(current_init == "null"): current_init = 0]
[h: current_init = current_init * -1]
[h: integer_part = floor(current_init)]
[h: fractional_part = current_init - integer_part]
[h: fractional_part = fractional_part * 1000000]
[h: order_part = floor(fractional_part)]
[h: timer_parts = fractional_part - order_part]
[h: timer_parts = timer_parts * 1000]
[h: timer_1_part = floor(timer_parts)]
[h: timer_parts = timer_parts - timer_1_part]
[h: timer_parts = timer_parts * 1000]
[h: timer_2_part = floor(timer_parts)]
[h: timer_parts = timer_parts - timer_2_part]
[h: timer_parts = timer_parts * 1000]
[h: timer_3_part = floor(timer_parts)]
[h: macro.return = "integer_part=" + integer_part + "; order_part=" + order_part + "; timer_1_part=" + timer_1_part + "; timer_2_part=" + timer_2_part + "; timer_3_part=" + timer_3_part]
pack_initiative:

Code: Select all

[h: '<!-- 
Puts the pieces of the initiative value back together again
    from the passed-in string properties.
-->']
[h: varsFromStrProp(macro.args)]
[h: order_part = order_part * 0.000001]
[h: timer_1_part = timer_1_part * 0.000000001]
[h: timer_2_part = timer_2_part * 0.000000000001]
[h: timer_3_part = timer_3_part * 0.000000000000001]
[h: initiative_value = integer_part + order_part + timer_1_part + timer_2_part + timer_3_part]
[h: initiative_value = initiative_value * -1]
[h: macro.return = initiative_value]

LarryWoestman
Cave Troll
Posts: 28
Joined: Tue Jul 21, 2020 7:50 pm

Re: A simple set of macros for a second-by-second initiative tracker

Post by LarryWoestman »

These are the remaining macros that live on the "Lib:custom_initiative" library token. They define the possible values that might show up in the properties this code refers to, ways to convert the string values to numeric values for computation, and ways to convert the numeric values back to a human-friendly output form. These are going to be generically useful for other code that might be written in the future, as well.


get_trait_levels:

Code: Select all

[h: macro.return = "Abysmal=9; Terrible=10; Poor=11; Mediocre=12; Fair=13; Good=14; Great=15; Superb=16; Legendary=17;"]
get_difficulty_levels:

Code: Select all

[h: macro.return = "Effortless=9; Extremely_Easy=10; Very_Easy=11; Easy=12; Moderate=13; Hard=14; Very_Hard=15; Extremely_Hard=16; Near_Impossible=17;"]
get_reaction_levels:

Code: Select all

[h: macro.return = "Abysmal=8; Terrible=7; Poor=6; Mediocre=5; Fair=4; Good=3; Great=2; Superb=1; Legendary=1;"]
get_trait_level_name_array:

Code: Select all

[h: macro.return = json.append("[]",
	"Abysmal-9",
	"Abysmal-8",
	"Abysmal-7",
	"Abysmal-6",
	"Abysmal-5",
	"Abysmal-4",
	"Abysmal-3",
	"Abysmal-2",
	"Abysmal-1",
	"Abysmal",
	"Terrible",
	"Poor",
	"Mediocre",
	"Fair",
	"Good",
	"Great",
	"Superb",
	"Legendary",
	"Legendary+1",
	"Legendary+2",
	"Legendary+3",
	"Legendary+4",
	"Legendary+5",
	"Legendary+6",
	"Legendary+7",
	"Legendary+8",
	"Legendary+9"
)]

LarryWoestman
Cave Troll
Posts: 28
Joined: Tue Jul 21, 2020 7:50 pm

Re: A simple set of macros for a second-by-second initiative tracker

Post by LarryWoestman »

This is the macro to initialize the initiative values for any tokens that are in the initiative list. This is useful either when you have just added a bunch of tokens to the list for the first time, or you want to start tracking initiative over again. This macro is in the GM frame.

_Initialize_Initiative:

Code: Select all

[h: '<!--
Assign starting initiative values to any tokens that have been added to the initiative list.

Start by getting the definitions of possible names used for the a.Agility property.
-->']
[MACRO("get_trait_levels@Lib:custom_initiative"): ""]
[h: '<!-- assign the values for the names to corresponding variables -->']
[h: varsFromStrProp(macro.return)]
[h: '<!-- keep the initiative frame from outputing "extra" initiative messages -->']
[h: setCurrentInitiative(-1)]
[h: '<!-- get the array of token information from the initiative list -->']
[h: init_list = getInitiativeList()]
[h: token_array = json.get(init_list, "tokens")]
[h: '<!--
If you happen to have two tokens next to each other in the initiative list
    with the same Reaction_time and Agility properties, then you end up with
    a token getting the initiative twice in a row at times, rather than having
    the other token with the same Reaction_Time and Agility get to go first.
    By assigning an increasing number after the Agility digits, you can be sure
    that no tokens have exactly the same initiative values, which fixes the problem.
-->']
[h: tie_breaker = 0.000001]
[h, foreach(item, token_array), code:
{
	[h: token_id = json.get(item, "tokenId")]
	[h: reaction_property = getProperty("a.Reaction_Time", token_id)]
	[h: reaction_value = eval(string(reaction_property))]
	[h: '<!--
	          The first two fractional digits are related to the Reaction_Time
	          property.  I am trying to have a number between 1 and 49
	          so the range from 50 to 99 is available for possible
	          future use.
	    -->']
	[h: reaction_value = (49 - reaction_value) / 100.0]
	[h: agility_property = getProperty("a.Agility", token_id)]
	[h: agility_value = eval(string(agility_property))]
	[h: '<!--
	          The next two fractional digits are related to the Agility
	          property.  I am trying to have a number between 1 and 49
	          so the range from 50 to 99 is available for possible
	          future use.
	    -->']
	[h: agility_value = (49 - agility_value) / 10000.0]
	[h: initiative_value = -1 * (reaction_value + agility_value + tie_breaker)]
	[h: setInitiative(initiative_value, token_id)]
	[h: tie_breaker = tie_breaker + 0.000001]
}]
[h: sortInitiative()]
[h: setCurrentInitiative(0)]

LarryWoestman
Cave Troll
Posts: 28
Joined: Tue Jul 21, 2020 7:50 pm

Re: A simple set of macros for a second-by-second initiative tracker

Post by LarryWoestman »

This is the macro for adding selected tokens to the initiative list. It can be used to add tokens to an empty initiative list or to add new tokens to an already existing initiative list. It lives in the GM frame.

Add_Selected_To_Initiative:

Code: Select all

[h: '<!--
Add any selected tokens to the initiative list, including initializing the initiative value.
-->']
[h: init_list = getInitiativeList()]
[h: token_array = json.get(init_list, "tokens")]
[h: '<!--
Run through the tokens that have initiative values assigned
    and figure out the maximum tie_breaker value that is already used.
Also figure out the "youngest" time that is assigned to any of the tokens.
-->']          
[h: max_tie_breaker = 0]
[h: min_time_value = 1000000]
[h, foreach(item, token_array, ""), code:
{
	[h: initiative_value = json.get(item, "initiative")]
	[h, if(initiative_value == "null"): initiative_value = 0]
	[MACRO("parse_initiative@Lib:custom_initiative"): initiative_value]
	[h: varsFromStrProp(macro.return)]
	[h, if(integer_part < min_time_value): min_time_value = integer_part]
	[h: order_part = order_part / 10000]
	[h: ignore = floor(order_part)]
	[h: order_part = order_part - ignore]
	[h: order_part = order_part * 100]
	[h, if(order_part > max_tie_breaker): max_tie_breaker = order_part]
}]
[h, if(min_time_value == 1000000): min_time_value = 0]
[h: '<!-- set max_tie_breaker to one more than the highest value already used -->']
[h: max_tie_breaker = floor(max_tie_breaker + 1)]
[h: '<!--
Get the possible string values that can be used for the a.Reaction_time and
    a.Agility properties and create corresponding variables.
-->']
[MACRO("get_trait_levels@Lib:custom_initiative"): ""]
[h: varsFromStrProp(macro.return)]
[h: '<!--
Run through any tokens that have been selected, add them to the initiative list,
    and assign them initial initiative values.
-->']
[h: selected_tokens = getSelected()]
[foreach(selected_token, selected_tokens, ""), code:
{
	[h: reaction_property = getProperty("a.Reaction_Time", selected_token)]
	[h: reaction_value = eval(string(reaction_property))]
	[h: '<!--
	          The first two fractional digits are related to the
	          Reaction_Time property.  I am trying to have a number
	          between 1 and 49 so the range from 50 to 99 is available
	          for possible future use.
	    -->']
	[h: reaction_value = (49 - reaction_value) / 100.0]
	[h: agility_property = getProperty("a.Agility", selected_token)]
	[h: agility_value = eval(string(agility_property))]
	[h: '<!--
	          The next two fractional digits are related to the Agility
	          property.  I am trying to have a number between 1 and 49
	          so the range from 50 to 99 is available for possible
	          future use.
	    -->']
	[h: agility_value = (49 - agility_value) / 10000.0]
	[h: order_part = reaction_value + agility_value + (max_tie_breaker / 1000000)]
	[h: addToInitiative(0, (-1 * (min_time_value + order_part)), selected_token)]
	[h: max_tie_breaker = max_tie_breaker + 1]
}]
[h: '<!-- keep the initiative frame from outputing "extra" initiative messages -->']
[h: setCurrentInitiative(-1)]
[h: sortInitiative()]
[h: setCurrentInitiative(0)]

LarryWoestman
Cave Troll
Posts: 28
Joined: Tue Jul 21, 2020 7:50 pm

Re: A simple set of macros for a second-by-second initiative tracker

Post by LarryWoestman »

This macro figures out the "usual" amount of time for this token and changes the initiative value so the token will show up on the initiative list in the proper order. It uses the "Reaction_Time" property to figure out how many seconds should be used. This token is on the GM frame.

Next_Initiative:

Code: Select all

[h: '<!--
Figure out the usual amount of time before the next initiative check
for the token that currently has initiative.  That amount of time is
determined by the a.Reaction_Time property value.

The modify_initiative macro will adjust the amount of time to be shorter
if it turns out that one or more timers will go off before the usual amount
of time has elapsed.
-->']
[h, if(getCurrentInitiative() == -1): abort(0)]
[h: '<!--
get the string values that are used for the a.Reaction_Time property
    and set the corresponding variable names.
-->']
[MACRO("get_reaction_levels@Lib:custom_initiative"): ""]
[h: varsFromStrProp(macro.return)]
[h: reaction_level = getProperty("a.Reaction_Time", getInitiativeToken())]
[h: reaction_time = eval(string(reaction_level))]
[MACRO("modify_initiative@Lib:custom_initiative"): reaction_time]

LarryWoestman
Cave Troll
Posts: 28
Joined: Tue Jul 21, 2020 7:50 pm

Re: A simple set of macros for a second-by-second initiative tracker

Post by LarryWoestman »

These macros are used to add either 1 second or a GM-entered amount of seconds to the initiative value of the token that currently has initiative. These macros are used for "unusual" amounts of initiative. These macros are in the GM frame.

Init+1:

Code: Select all

[h, if(getCurrentInitiative() == -1): abort(0)]
[MACRO("modify_initiative@Lib:custom_initiative"): 1]
Init+?:

Code: Select all

[h, if(getCurrentInitiative() == -1): abort(0)]
[MACRO("modify_initiative@Lib:custom_initiative"): Initiative_Amount]

LarryWoestman
Cave Troll
Posts: 28
Joined: Tue Jul 21, 2020 7:50 pm

Re: A simple set of macros for a second-by-second initiative tracker

Post by LarryWoestman »

These macros are used to set the times for three "timers" that count down in seconds as the initiative values increase over time. They can be used for whatever the GM wants. The most common use (so far) is tracking when a spell has been cast. In this game, spells take longer to cast as they get more complex and/or more powerful. Most spells average around 6 seconds or so to cast, so it helps to be able to track when they "go off". These macros live in the GM frame.

T1+?:

Code: Select all

[h: '<!--
Start a timer that counts down (in units that I am interpreting as seconds)
    while the integer part of the initiative value counts up in the same number
    of units.

There are three digits assigned to each timer, so the possible range is 0 to 999 units.
-->']
[h, if(getCurrentInitiative() == -1): abort(0)]
[h: '<!-- ask the GM for the amount of time to measure -->']
[h: change_amount = timer_1_amount]
[h: change_amount = floor(change_amount)]
[h, if(change_amount <= 0): change_amount = 1]
[h, if(change_amount > 999): change_amount = 999]
[h: '<!-- get the current initiative value, set the new timer value, set the new initiative value -->']
[MACRO("parse_initiative@Lib:custom_initiative"): getInitiative(getInitiativeToken())]
[h: varsFromStrProp(macro.return)]
[g, if(timer_1_part == 0): "<br>setting new timer 1 value<br>"; "<br>Warning: replacing non-zero timer 1<br>"]
[h: arguments = "integer_part=" + integer_part + "; order_part=" + order_part + "; timer_1_part=" + change_amount + "; timer_2_part=" + timer_2_part + "; timer_3_part=" + timer_3_part]
[MACRO("pack_initiative@Lib:custom_initiative"): arguments]
[h: setInitiative(macro.return, getInitiativeToken())]
T2+?:

Code: Select all

[h: '<!--
Start a timer that counts down (in units that I am interpreting as seconds)
    while the integer part of the initiative value counts up in the same number
    of units.

There are three digits assigned to each timer, so the possible range is 0 to 999 units.
-->']
[h, if(getCurrentInitiative() == -1): abort(0)]
[h: '<!-- ask the GM for the amount of time to measure -->']
[h: change_amount = timer_2_amount]
[h: change_amount = floor(change_amount)]
[h, if(change_amount <= 0): change_amount = 1]
[h, if(change_amount > 999): change_amount = 999]
[h: '<!-- get the current initiative value, set the new timer value, set the new initiative value -->']
[MACRO("parse_initiative@Lib:custom_initiative"): getInitiative(getInitiativeToken())]
[h: varsFromStrProp(macro.return)]
[g, if(timer_2_part == 0): "<br>setting new timer 2 value<br>"; "<br>Warning: replacing non-zero timer 2<br>"]
[h: arguments = "integer_part=" + integer_part + "; order_part=" + order_part + "; timer_1_part=" + timer_1_part + "; timer_2_part=" + change_amount + "; timer_3_part=" + timer_3_part]
[MACRO("pack_initiative@Lib:custom_initiative"): arguments]
[h: setInitiative(macro.return, getInitiativeToken())]
T3+?:

Code: Select all

[h: '<!--
Start a timer that counts down (in units that I am interpreting as seconds)
    while the integer part of the initiative value counts up in the same number
    of units.

There are three digits assigned to each timer, so the possible range is 0 to 999 units.
-->']
[h, if(getCurrentInitiative() == -1): abort(0)]
[h: '<!-- ask the GM for the amount of time to measure -->']
[h: change_amount = timer_3_amount]
[h: change_amount = floor(change_amount)]
[h, if(change_amount <= 0): change_amount = 1]
[h, if(change_amount > 999): change_amount = 999]
[h: '<!-- get the current initiative value, set the new timer value, set the new initiative value -->']
[MACRO("parse_initiative@Lib:custom_initiative"): getInitiative(getInitiativeToken())]
[h: varsFromStrProp(macro.return)]
[g, if(timer_3_part == 0): "<br>setting new timer 3 value<br>"; "<br>Warning: replacing non-zero timer 3<br>"]
[h: arguments = "integer_part=" + integer_part + "; order_part=" + order_part + "; timer_1_part=" + timer_1_part + "; timer_2_part=" + timer_2_part + "; timer_3_part=" + change_amount]
[MACRO("pack_initiative@Lib:custom_initiative"): arguments]
[h: setInitiative(macro.return, getInitiativeToken())]

LarryWoestman
Cave Troll
Posts: 28
Joined: Tue Jul 21, 2020 7:50 pm

Re: A simple set of macros for a second-by-second initiative tracker

Post by LarryWoestman »

I would appreciate any comments and/or suggestions on any of this.

I'm still a relative beginner at MapTool macro programming, so be gentle :)

Post Reply

Return to “Drop-In Macros and Scripts”