I decided I like the convention of having different monsters of the same type roll shared initiative and all act together. Before I'd just roll initiative for one of each kind of monster, but that breaks when I decide to just delete tokens upon death and accidentally delete the one that actually holds the initiative number. Plus, MadJoker's nice initiative state tracking system only works when the actual token comes up in initiative, so wouldn't work with that particular usage.
So I decided to take a fun adventure into the land of Maptools macros to accomplish that goal
This is a mod to the framework that will - for the GM only[1] - allow you to group monsters in initiative when you roll them in. The logic is as follows:
1. You can group monsters by (none), Name, GM Name, or Label
2. Two monsters are considered by the same group if they have identical names (excluding any trailing numbers -- so "Kobold", "Kobold 2", and "Kobold 25" are all in the same group, "Kobold Dragonshield" is in its own group.). If you opt to group by GM Name or Label, the same logic applies just using the relevant field. (If you name all of your kobolds "Kobold" so it's not readily obvious to your players what kind of kobolds they are, you can set their GM names to "Kobold Dragonshield", "Kobold Wyrmpriest", etc. and then group by initiative. Monsters with no name, label, or GM name (depending on grouping method) are never treated as grouped.
3. As each monster is added to initiative, it scans the initiative list for a monster in the same group. If a match is found, the new monster copies the old monster's initiative. ("Kobold 2: I share Kobold 1's initiative of 13.01"); otherwise initiative is done normally.
Note the exact mechanism for this process is fairly inefficient -- the entire initiative list has to be looped for each monster if grouping is enabled, so the cost is exponential as the list grows. There are a few ways this could be improved[2], but it's more fiddly than I'd like to get right now.
Notes:
* Re-adding tokens to initiative with grouping enabled is ineffective, as each token will find another token in its group and readd using its number.
* Behavior if tokens exist at differing initiative counts in the same group is undefined. (Actually, it'll follow the order of getInitiativeList() )
* Grouping tokens with differing initiative bonuses probably isn't a good idea.
With all the caveats out of the way, here's the actual code changes:
Code: Select all
<!--InitiativeCheckEntry-->
[H: DNT_macroTarget = if(json.get(macro.args, "MacroTarget") == "Multiple", "Selected", currentToken())]
<head>
<meta name="input" content = "true">
<title>Initiative Check</title>
</head>
<body><form name="Initiative Check" method="json" action="[r: macroLinkText('CheckOutput@Lib:Execute', 'none', '', DNT_macroTarget)]">
<table width="100%" nowrap="nowrap" bgcolor="DBD9C0">
<tr>
<td width="1">Re-add to List:</td>
<td><input type="checkbox" name="Readd" value="1"></input></td>
<td width="1">Set Value:</td>
<td align="left"><input type="text" name="Value" size="5"></input></td>
</tr>
</table>
<table width="100%" nowrap="nowrap" bgcolor="DBD9C0">
<tr>
[R, if(isGM() == 1 && getLibProperty("GMScreen", "Lib:Information") == 1), CODE:
{
<td width="1"><b>Modifier:</b></td>
<td align="left"><input type="text" name="Mod" size="5"></input></td>
<td width="1" align="right">Die Roll:</td>
<td align="left"><input type="text" name="GMDieRoll" size="5"></input></td>
};{
<td width="1"><b>Modifier:</b></td>
<td align="left"><input type="text" name="Mod" size="5"></input></td>
}
]
</tr>
</table>
[R, if(isGM() == 1), CODE:
{
<table width="100%" nowrap="nowrap" bgcolor="DBD9C0">
<tr>
<td width="1"><b>Group By:</b></td>
<td><Select name="GroupBy">
<Option selected="selected" value="None">(none)</option>
<Option value="Name">Name</option>
<Option value="Label">Label</option>
<Option value="GM Name">GM Name</option>
</select>
</td>
</tr>
</table>
};{}]
<table width="100%" nowrap="nowrap">
<tr bgcolor="B3AF94">
<td width="1">Private:</td>
<td align="left"><input type="checkbox" name="Output" value="Private" [R, if(DNT_MacroTarget == "Selected"): if(getProperty("DNA_PrivateRolls" + getPlayerName()) == "1", "checked", "")]></input></td>
<td align="right">Hide Roll Info:</td>
<td align="left"><input type="checkbox" name="HideRoll" value="1" [R, if(DNT_MacroTarget == "Selected"): if(getProperty("DNA_HiddenRolls" + getPlayerName()) == "1", "checked", "")]></input></td>
</tr>
<tr bgcolor="DBD9C0">
<td colspan="4" align="center">
<input type="submit" name="SaveEntry" value="Fight!"></input>
</td>
</tr>
</table>
<input type="hidden" name="ExeMacro" value="InitiativeCheck"></input>
<input type="hidden" name="MacroTarget" value="[R: DNT_macroTarget]"></input>
</form></body>
(The only thing new here is the "Group By" section)
Find "InitiativeCheck" in the list, and change to:
I also fixed a few minor bugs I was experiencing with character notes and equipment fields (always having to scroll a tiny bit to be able to click the save button) by finding and updating the following:
Code: Select all
"EquipmentNotes", "385, 570",
"Notes", "385, 570",
Replace with
Code: Select all
<!--InitiativeCheck-->
[H, forEach(DNT_variable, "Mod, Readd, Value,Output, HideRoll"): set("DNT_Raw" + DNT_variable, json.get(macro.args, DNT_variable))]
[H: DNT_RawMod = DNT_RawMod + 0]
[H: DNT_Mod = eval(string(DNT_RawMod))]
[H: Die = if(json.get(macro.args, "GMDieRoll") == "", 1d20, json.get(macro.args, "GMDieRoll"))]
[H: Rating = json.get(json.get(DNA_AbilityScores, 2), 2) + DNA_LevelBonus + DNA_InitBonus]
[H: DNT_InitTotal = if(DNT_RawValue == "", Die + Rating + DNT_Mod + ((Rating+DNT_Mod)/100), DNT_RawValue)]
<!--Sets if the token was on the initiative list to start with, this the output formating is correct-->
[H: DNT_OnInitList = if(getInitiative() == "The token is not in the initiative list.", 0, 1)]
<!--See if we are set to group with other like-named tokens-->
[H: DNT_Groupby = if(isGm(), json.get(macro.args, "GroupBy"), "None")]
[H, switch(DNT_Groupby), CODE:
case "Name" : {[H: DNT_Groupfunction = "getName()" ]};
case "Label": {[H: DNT_Groupfunction = "getLabel()" ]};
case "GM Name": {[H: DNT_Groupfunction = "getGMName()" ]};
default: {[H: DNT_Groupfunction = ""]};
]
<!-- Save our current token ID so we know if we compare against ourself later -->
[H: DNT_SelfId = currentToken()]
<!-- ID of the token that we decided to match against, if we find one -->
[H: DNT_OtherId = ""]
[H, if(DNT_Groupfunction == ""): DNT_SelfBaseName = ""; DNT_SelfBaseName = replace(eval(DNT_Groupfunction), "^(.*?) [0-9]+","\$1")]
[H, if(DNT_SelfBaseName != ""), CODE:
{
[H: DNT_InitiativeList = json.get(getInitiativeList(), "tokens")]
[H, foreach(DNT_InitiativeEntry, DNT_InitiativeList), CODE:
{
In initiative scanning loop, [R: DNT_CheckId = json.get(DNT_InitiativeEntry, "tokenId")]
[H, token(DNT_CheckId): DNT_OtherBaseName = replace(eval(DNT_Groupfunction), "(.*?) [0-9]+","\$1")]
[H, if(DNT_CheckId != DNT_SelfId && DNT_OtherBaseName == DNT_SelfBaseName && DNT_OtherId == ""): DNT_OtherId = DNT_CheckId]
}
]
}
]
[H, if(DNT_OtherId != ""), CODE:
{
[H, token(DNT_OtherId): DNT_InitTotal = getInitiative()]
}
]
<!--Output Formating-->
[H: DNT_ModFormat = "%s(%s)"]
[H, if(isNumber(DNT_RawMod) == 1): Mod = DNT_RawMod; Mod = strformat(DNT_ModFormat, string(DNT_RawMod), DNT_Mod)]
[H: DNT_GMCheatMsg = "<b>[Rolls behind the GM Screen]</b>"]
<!--Adds to the initiative, only does it if the token is not on the list, if readd has been checked, or if there is a set Value-->
[H, if(DNT_OnInitList == 0 || DNT_RawReadd == 1 || DNT_RawValue != ""), CODE:
{
[H: addToInitiative()]
[H: setInitiative(DNT_InitTotal)]
}
]
[R, if(DNT_RawValue == "" && DNT_OtherId == ""), CODE:
{
[R: getLibProperty("GameMessage", "Lib:Information")] [R: if(isGM() == 1 && getLibProperty("GMScreen", "Lib:Information") == 1, DNT_GMCheatMsg, "")] [R: if(DNT_RawOutput == "", "", token.name + ":")] I make an Inititiative check and get a
[R, if(DNT_RawHideRoll == 1), CODE:
{
[gt, st, t(Die + Rating + DNT_Mod): Die + Rating + Mod]
};{
[t(Die + Rating + DNT_Mod): Die + Rating + Mod]
}
]. [R: if(DNT_OnInitList == 0 || DNT_RawReadd == 1, "", "But I'm already on the Initiative list.")]
};{}
]
[R, if(DNT_RawValue != "" && DNT_OtherId == ""), CODE:
{
[R: getLibProperty("GameMessage", "Lib:Information")] [R: if(isGM() == 1 && getLibProperty("GMScreen", "Lib:Information") == 1, DNT_GMCheatMsg, "")] [R: if(DNT_RawOutput == "", "", token.name + ":")] I set my Initiative to [R: DNT_InitTotal].
};{}
]
[R, if(DNT_OtherId != ""), CODE:
{
[R: getLibProperty("GameMessage", "Lib:Information")] [R: if(isGM() == 1 && getLibProperty("GMScreen", "Lib:Information") == 1, DNT_GMCheatMsg, "")] [R: if(DNT_RawOutput == "", "", token.name + ":")] I share [R: getName(DNT_OtherId)][R:"'s"] initiative of [R: DNT_InitTotal]. [R: if(DNT_OnInitList == 0 || DNT_RawReadd == 1, "", "But I'm already on the Initiative list.")]
};{}
]
[1] Trying to get an 'untrusted' view of the initiative list from a trusted macro seems to be non-trivial, which means there's a 50/50 chance that it'd take all of one line of code and someone just needs to tell me what it is.
[2] By making a JSON map of group -> initiative_or_owner_token_ID at the start of adding tokens to initiative and updating it at the same time we add tokens to initiative. But there's no clear entry point between the "Okay, we're starting initiative now" and "For each token, run this macro" steps without more editing than I'd like to do.