Building a Framework (Step by Step)

Discuss macro implementations, ask for macro help (to share your creations, see User Creations, probably either Campaign Frameworks or Drop-in Resources).

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

User avatar
Doc_Waldo
Giant
Posts: 108
Joined: Wed Sep 08, 2010 11:41 pm
Location: Boise, ID

Re: Building a Framework (Step by Step)

Post by Doc_Waldo »

What system is the framework for?
--DOC

User avatar
JonathanTheBlack
Dragon
Posts: 544
Joined: Mon Jun 28, 2010 12:12 pm
Location: I'm the worm...

Re: Building a Framework (Step by Step)

Post by JonathanTheBlack »

So... all that work I did trying to get the line spacing to work properly was because of a "hidden feature" of how frames work. Tables in frames default to a cellpadding of greater than zero. Probably two or so. This means line spacing in tables will always be off. Back to all that work I did... made worthless by: <table cellpadding='0'>. Thank you wolph42 for helping fix that problem. Trying to fix that problem felt like fixing CSS for IE6.0.

User avatar
JonathanTheBlack
Dragon
Posts: 544
Joined: Mon Jun 28, 2010 12:12 pm
Location: I'm the worm...

Re: Building a Framework (Step by Step)

Post by JonathanTheBlack »

Doc_Waldo wrote:What system is the framework for?

My own. It's heavily derived from Dungeons & Dragons 4th Edition however. There's a lot of things about D&D 4e that I don't like scripting for, so I streamlined them in my framework.

User avatar
JonathanTheBlack
Dragon
Posts: 544
Joined: Mon Jun 28, 2010 12:12 pm
Location: I'm the worm...

Re: Building a Framework (Step by Step)

Post by JonathanTheBlack »

Here's the latest version (v0a22) of the framework. Versions 20, 20-Old, and 21 have been scrapped, deleted, and banished from my flash drive and hard drive at home. They have too many conflicting scripts at different stages that don't work.

LatestBuild.jpg
LatestBuild.jpg (71.01 KiB) Viewed 3271 times


Updates
- Combat Log works properly! Doesn't throw stack overflow errors. Was missing a } somewhere very important.
- Combat Log doesn't have line spacing issues any more!
- Out of Range display works
- Fudge Factor implement and works wonderfully!

Up Next
- Automatically damage targets of attacks
-- This will account for temporary health and damage reduction from barrier powers
- Apply conditions (states) to target of attacks

Next Big Project
Items!! After the application of damage and conditions to the targets of attacks is done, I'm going to tackle the management of items on the Compendium and player tokens. Items right now consisit only of weapons and implements. Their properties will be used to determine the range, damage, and other aspects of attacks. The inventory management macros will allow players to create weapons and implements, store them on their character, edit them, and delete them. Before I get started on this, I will need to sit down and decide just how much of an effect items should have.

For example, something I considered is a Diablo-style naming method that would allow players to create weapons and implements like: Legendary Adamantium Burning Sceptre of Domination. Each word in the name would imbue the item with specific traits and powers. Adamantium could break through barrier powers more easily while Burning would add fire damage to the power. The Domination keyword could be a rare trait that gives the player a new power they can use once per encounter. Something like this will require a lot of work and may be worth adding later. Getting basic weapon and implement range and damage added is more important, but I need to think about where I'm heading with this so I don't have to re-write code later.
Attachments
Monsters and Mayhem v0a22.cmpgn
(3.76 MiB) Downloaded 105 times

User avatar
Doc_Waldo
Giant
Posts: 108
Joined: Wed Sep 08, 2010 11:41 pm
Location: Boise, ID

Re: Building a Framework (Step by Step)

Post by Doc_Waldo »

Great. Keep posting your steps and process. It is very helpful for me as I am working on teaching myself MT's macro language, along with json. Again, thanks for posting everything.
--DOC

User avatar
JonathanTheBlack
Dragon
Posts: 544
Joined: Mon Jun 28, 2010 12:12 pm
Location: I'm the worm...

Re: Building a Framework (Step by Step)

Post by JonathanTheBlack »

A short diversion away from the Lib:Combat macro's to some handy utility macro's...

Narrator Tools
I use the following macro's as the Narrator to manage npc tokens mostly, though they're also helpful to check what's going on with pc tokens. I've also included a Narration macro that abuses /emit to create shaded box narrations for players.

Narration - Very simple macro that could be expanded to do more. I'm considering adding checks to alter background colors.
[spoiler]

Code: Select all

/emit
  [h: status = input("Message | Enter message here || TEXT | SPAN=TRUE WIDTH=40")][h: abort(status)]
  <table width=500 border=1 style="border-style: inset; color: #000000; font-family: Georgia; background-color: #B4CFEC; font-size: 105%; font-style: italic; text-align:center;">
    <tr><td style="padding: 5px;">{Message}</td></tr>
  </table>
[/spoiler]

Mass Heal -- Another simple macro but very useful. I use it mostly to reset monster tokens back to full health and no conditions for use in another encounter. It can also be used to reset a pc token if needed and restore their spent encounter and recharge powers.
[spoiler]

Code: Select all

<!-- Remove all states, heal the token to full health, restore all vitality to PC's, and reset the health bar to 100% -->
  [h: setAllStates(0)]
  [h: CurrentHealth = MaxHealth]
  [h, if(isNPC() == 0): CurrentVitality = MaxVitality]
  [h: setBar("Health", 100)]

<!-- Recovers Spent Encounter/Recharge Powers -->
  [h: list.Index = getMacroGroup("Encounter")]
  [h: list.Index = listAppend(list.Index, getMacroGroup("Recharge"))]
  [h, foreach(Index, list.Index), CODE:
    {
      [macroProps=getMacroProps(Index)]
      [Group = getStrProp(macroProps, "group")]
      [if(Group  == "Encounter"): setMacroProps(Index, "color=red;fontColor=black")]
      [if(Group  == "Recharge"): setMacroProps(Index, "color=blue;fontColor=white")]
    }]

<!-- Suppress text output to chat -->
  [h: abort(0)]
[/spoiler]

Toggle States -- A basic macro that is still a work in progress. Eventually I plan to "borrow" code from Rumble that puts all the states as images and allow clicking on images of the states to toggle them on and off.
[spoiler]

Code: Select all

[h: statelist = listSort(getTokenStates(), "A+")]
[h: status = input("statename | " + statelist + " |Which state should I toggle| LIST | VALUE=STRING")]
[h: abort(status)]
[h: setState(statename, 1 - eval("state."+statename))]
[/spoiler]

Get All Properties (Indent) -- This macro and the next one are useful for checking the properties stored on a token, both campaign and hidden. Hidden properties are ones that are put onto a token using setProperty and can be retrieved using getProperty. They're good for tracking things without the players knowing. The first function indents json objects so they're easily readable, but sometimes a token has a variable that won't work inside the json.indent function. The next macro does the same thing, but doesn't use json.indent.
[spoiler]

Code: Select all

[h: PropList = getPropertyNames()]
[h: PropCount = listCount(PropList)]
[r, dialog("Properties"), CODE:
  {
    [r, COUNT(PropCount, "<br>"), CODE:
      {
        [h: PropName = listGet(PropList, roll.count)]
        [h: Property = getProperty(PropName)]
        <b>Property Name</b>: [r: PropName]<br>
        <b>Value</b>: <pre>[r: json.indent(Property)]</pre>
      }]
  }]
  [h: abort(0)]
[/spoiler]

Get All Properties (Plain)
[spoiler]

Code: Select all

[h: PropList = getPropertyNames()]
[h: PropCount = listCount(PropList)]
[r, dialog("Properties"), CODE:
  {
    [r, COUNT(PropCount, "<br>"), CODE:
      {
        [h: PropName = listGet(PropList, roll.count)]
        [h: Property = getProperty(PropName)]
        <b>Property Name</b>: [r: PropName]<br>
        <b>Value</b>: [r: Property]
      }]
  }]
  [h: abort(0)]
[/spoiler]

Reset Token -- This one works in tandem with Get All properties. You can reset the entire token and remove all properties from it. I may expand it later and create a way to selectively remove properties. Until then, you can use resetProperty("PropertyName")
[spoiler]

Code: Select all

<!-- Verify that the token should be reset since this cannot be undone. -->
  [h: status = input("ResetToken | 0 | Are you absolutely sure you want to reset the token | CHECK")]
  [h: abort(status)]
  [h: assert(ResetToken, "<b><font color='#660000'>Token reset aborted.</font></b>", 0)]

<!-- Get the list of Properties and Reset them All. getPropertyNames() creates a list of all properties on the token, including variables set using setProperty() -->
  [h: PropertyList = getPropertyNames()]
  [h, count(listCount(PropertyList), "<br>"), CODE:
    {
      [r: Prop = listGet(PropertyList, roll.count)]
      [h: resetProperty(Prop)]
    }]

<!-- Send a message to the person who clicked the button that the token has been reset using assert(). -->
  [h: assert(0, "<b><font color='#660000'>" + token.name + " has had all their properties reset and removed.</font></b>", 0)]
[/spoiler]

User avatar
JonathanTheBlack
Dragon
Posts: 544
Joined: Mon Jun 28, 2010 12:12 pm
Location: I'm the worm...

Re: Building a Framework (Step by Step)

Post by JonathanTheBlack »

Back to Combat!

The script below is the beginning of what I've completed for applying damage to the target of an attack. It currently works and updates the targets current health and health bar properly, but it doesn't account for temp health or barrier/ward powers. It also doesn't account for resistances either.

The difficult part of accounting for the temp health and other damage reductions, is getting an accurant tally of how much damage has been reduced, blocked, and/or resisted. It takes a lot of math and confusing variable names. My mind is too frazzled to deal with it at the moment however.

I also need to add state.Dead and state.Bloodied to the campaign and apply those states as needed. That's pretty easy though. - edit - And done! Updated the code in the post. I still need to include a Dying state for players when they reach negative health. I don't have my images however. So it's hampering my work a bit.

[spoiler]

Code: Select all

[h: original.dmg.Total = dmg.Total]
[h: dmg.Resisted = 0] [h: dmg.Blocked = 0] [h: dmg.Reduced = 0]
[h: list.dmg.Reduction = ""]

<!-- Build tooltip for Damage -->
  [h, if(atk.Missed == 1 && power.HalfDamageOnMiss == "No"), code:
    {
      [dmg.Total = "Missed!"]
      [dmg.Tooltip = "-- No Damage --"]
    };{
      <!-- Create dmg.Tooltip for CompletedAttackResults -->
        [dmg.Tooltip = "<html><table width=300><tr><td><b><font size=5>Damage</font></b></td></tr>"]
        [if(power.StaticDamage == "No"): dmg.Tooltip = dmg.Tooltip + "<tr><td>" + dmg.Roll + " (" + item.Damage + ")</td></tr>"]
        [if(power.StaticDamage == "Yes"): dmg.Tooltip = dmg.Tooltip + "<tr><td>" + eval(power.StaticDamageAmount) + " (" + power.StaticDamageAmount + ")</td></tr>"]
        [dmg.Tooltip = dmg.Tooltip + "<tr><td>" + eval(atk.Attribute) + " (" + atk.Attribute + ")</td></tr>"]
        [if(dmg.mod.Miscellaneous != 0): dmg.Tooltip = dmg.Tooltip + "<tr><td>" + dmg.mod.Miscellaneous + " (Miscellaneous)</td><tr>"]
        [dmg.Tooltip = dmg.Tooltip + "</table></html>"]

      <!-- Deal damage to target and update Health bar -->
        [token(Target), code:
          {
            [CurrentHealth = CurrentHealth - dmg.Total]
            [setBar("Health", CurrentHealth / MaxHealth)]

            <!-- If target is at half health or less, apply bloodied -->
              [if(CurrentHealth <=  MaxHealth/2 && state.Bloodied != 1): state.Bloodied = 1]

            <!-- If target is an npc and at zero health or less, apply death -->
              [if(CurrentHealth <= 0 && isNPC() == 1): setAllStates(0)]
              [if(CurrentHealth <= 0 && isNPC() == 1): state.Dead = 1]
          }]
    }]

<!-- Build display for damage roll -->
  [h: dmg.Display = dmg.Total + " " + lower(power.DamageTypes) + " damage"]
  [h, if(dmg.Resisted != 0 || dmg.Blocked != 0 || dmg.Reduced != 0): dmg.Display = dmg.Display + " (" + list.dmg.Reduction + ")"]
  [h, if(atk.Missed == 1 && power.HalfDamageOnMiss == "No"): dmg.Display = "Missed!"]

<!-- Hide npc damage calculations in the tooltip -->
  [h, if(HIDE_MONSTER_DAMAGE_TOOLTIP == "TRUE" && isNPC() == 1): dmg.Tooltip = "-- Monster Damage Hidden --"]
[/spoiler]

User avatar
JonathanTheBlack
Dragon
Posts: 544
Joined: Mon Jun 28, 2010 12:12 pm
Location: I'm the worm...

Re: Building a Framework (Step by Step)

Post by JonathanTheBlack »

Ran into some issues with the mass heal macro. It didn't work for tokens with the same name. Only tokens of different names. So, I updated it to the macro below. This one will work regardless if they're uniquely named or not.

[spoiler]

Code: Select all

<!-- Create list of targets from selected tokens -->
  [h: list.Targets = getSelected()]

  [h, foreach(Target, list.Targets), code:
    {
      [switchToken(Target)]
      [setAllStates(0)]
      [CurrentHealth = MaxHealth]
      [setBar("Health", 100)]
      [if(isNPC() == 0): CurrentVitality = MaxVitality]

      <!-- Recovers Spent Encounter/Recharge Powers -->
        [list.Index = getMacroGroup("Encounter")]
        [list.Index = listAppend(list.Index, getMacroGroup("Recharge"))]
        [foreach(Index, list.Index), CODE:
          {
            [macroProps=getMacroProps(Index)]
            [Group = getStrProp(macroProps, "group")]
            [if(Group  == "Encounter"): setMacroProps(Index, "color=red;fontColor=black")]
            [if(Group  == "Recharge"): setMacroProps(Index, "color=blue;fontColor=white")]
          }]
    }]

<!-- Suppress output to chat -->
  [h: abort(0)]
[/spoiler]

User avatar
JonathanTheBlack
Dragon
Posts: 544
Joined: Mon Jun 28, 2010 12:12 pm
Location: I'm the worm...

Re: Building a Framework (Step by Step)

Post by JonathanTheBlack »

Haven't been able to get a lot of new work done. Just fiddling around with the combat log some more and changing font sizes here and there. I really need to ignore the combat log for the most part and get the nuts and bolts done. Especially when it comes to applying temp health and resists to incoming damage. My blocked damage test worked and displays properly in the combat log and reduces damage properly. So now I just need to apply those commands to temp health. That part is easy.

jtb.ApplyDamage() -- Some powers in MM create temporary wards or barriers that surround a character and prevent incoming damage. Wards are the weaker of the two, preventing only 10 damage. Barrier powers prevent up to 20 damage per attack. Unlike temp health, these are not depleted after an attack. This makes them well suited for encounter, recharge, and limit break powers. Never at-will powers. I'm still fuzzy on the math, but it works... through trial and error. Going to be fun trying to figure it out again to get it to work for temp health and to accurately track how much damage is reduced at each step.

Ok, breaking it down. First, it checks to see if a dmg.WardBarrier is active (which is done earlier in the function by checking for state.Ward and state.Barrier. If they are active, it takes the current dmg.Total and subtracts the amount blocked from the dmg. Using the Wiki: max() function, we make sure that the damage cannot drop below zero. This is very important because negative damage will actually heal the token. So, if an attack deals 14 dmg and dmg.WardBarrier is set to 10, dmg.Total = 14 - 10, or 4 dmg leftover. Using the original.dmg.Total, we can determine just how much damage was blocked. This is useful if the attack deals less than the damage blocked. For example, an attack that deals 4 damage is blocked by a Ward. dmg.Total = max(0, 4 - 10) or max(0, -6). Since -6 is lower than zero, the max function sets dmg.Total to zero.

Because we saved the original.dmg.Total at the start of the function, we can subtract the current dmg.Total from original.dmg.Total to determine how much damage was blocked. 4 (original) - 0 (dmg.Total) = 4, so the ward blocked 4 and is saved as dmg.Blocked for use in building the damage display later in the macro. In the first example, the dmg blocked would be 10, with 4 dmg soaking through to the next step. Finally, I save the dmg into a list to make it easier to display in the combat log later.

Code: Select all

<!-- Block damage with Ward or Barrier -->
  [h, if(dmg.WardBarrier != 0), code:
    {
      [dmg.Total = max(0, dmg.Total - dmg.WardBarrier)]
      [if(dmg.WardBarrier == 10): dmg.Blocked = min(10, original.dmg.Total - dmg.Total)]
      [if(dmg.WardBarrier == 20): dmg.Blocked = min(20, original.dmg.Total - dmg.Total)]
      [list.dmg.Reduction = listAppend(list.dmg.Reduction, dmg.Blocked + " blocked")]
    }]


Adding resists... that's gonna be hell and require some creative string concatenation and looping to get it done smoothly.

User avatar
JonathanTheBlack
Dragon
Posts: 544
Joined: Mon Jun 28, 2010 12:12 pm
Location: I'm the worm...

Re: Building a Framework (Step by Step)

Post by JonathanTheBlack »

Ugh... I absolutely loathe writing the code to build tooltips for attack and damage rolls. At least I only have to do it once.

Code: Select all

      <!-- Create dmg.Tooltip for CompletedAttackResults -->
        [dmg.Tooltip = "<html><table width=300><tr><td colspan=3><b><font size=5>Damage to " + TargetName + "</b></td></tr>"]
        [if(power.StaticDamage == "Yes"): dmg.Tooltip = dmg.Tooltip + "<tr><td width=10 align=right><font size=4></font></td><td width=15><font size=4>" + eval(power.StaticDamageAmount) + "</font></td><td width=175><font size=4> (" + power.StaticDamageAmount + ")</font></td></tr>"]
        [if(power.StaticDamage == "No"): dmg.Tooltip = dmg.Tooltip + "<tr><td width=10 align=right><font size=4></font></td><td width=15><font size=4>" + dmg.Roll + "</font></td><td width=175><font size=4>" (" + item.Damage + ")</font></td></tr>"]
        [if(power.StaticDamage == "No"): dmg.Tooltip = dmg.Tooltip + "<tr><td width=10 align=right><font size=4>+</font></td><td width=15><font size=4>" + eval(atk.Attribute) + "</font></td><td width=175><font size=4> (" + atk.Attribute + ")</font></td></tr>"]
        [if(power.StaticDamage == "No" && dmg.mod.Miscellaneous != 0): dmg.Tooltip = dmg.Tooltip + "<tr><td width=10 align=right><font size=4>+</font></td><td width=15><font size=4>" + dmg.mod.Miscellaneous + "</font></td><td width=175><font size=4> (Miscellaneous)</font></td></tr>"]
        [if(power.HalfDamageOnMiss == "Yes"): dmg.Tooltip = dmg.Tooltip + "<tr><td width=10 align=right><font size=4>=</font></td><td width=15><font size=4>" + floor(original.DamageForTooltip/2) + "</font></td><td width=175><font size=4> (Half damage on miss! " + original.DamageForTooltip + " / 2)</font></td></tr>"]
        [if(dmg.Blocked != 0): dmg.Tooltip = dmg.Tooltip + "<tr><td width=10 align=right><font size=4>-</font></td><td width=15><font size=4>" + dmg.Blocked + "</font></td><td width=175><font size=4> (Blocked)</font></td></tr>"]
        [if(dmg.Reduced != 0): dmg.Tooltip = dmg.Tooltip + "<tr><td width=10 align=right><font size=4>-</font></td><td width=15><font size=4>" + dmg.Reduced + "</font></td><td width=175><font size=4> (Reduced)</font></td></tr>"]
        [if(dmg.Resisted != 0): dmg.Tooltip = dmg.Tooltip + "<tr><td width=10 align=right><font size=4>-</font></td><td width=15><font size=4>" + dmg.Resisted + "</font></td><td width=175><font size=4> (Resisted)</font></td></tr>"]
        [dmg.Tooltip = dmg.Tooltip + "<tr><td colspan=3><font size=5><b>" + dmg.Total + " " + lower(power.DamageTypes) + " damage</b></font></td></tr></table></html>"]

User avatar
JonathanTheBlack
Dragon
Posts: 544
Joined: Mon Jun 28, 2010 12:12 pm
Location: I'm the worm...

Re: Building a Framework (Step by Step)

Post by JonathanTheBlack »

Updating jtb.ApplyDamage... again...

After spending two hours wrangling the temp health code to work properly and to get the tooltip fixed, it's finally 90% done. The remaining 10% is figuring out the loop and if checks required to compare damage types to damage resistances. I'll also need to include vulnerabilities and their special conditions later. For example, if a target is vulnerable to lightning damage, they take double damage if the attack deals lightning damage and they are Pinned (-2 defenses and immobilized) until the end of their next turn. Each type of damage has a condition it applies to the target of an attack that's vulnerable to that damage type.

The most difficult part of this macro has been getting the math right for tracking damage dealt as it's blocked, reduced, and resisted. The tooltip was just tedious.

[spoiler]

Code: Select all

[h: original.dmg.Total = dmg.Total]
[h: dmg.Blocked = 0]
[h: dmg.Reduced = 0]
[h: dmg.Resisted = 0]
[h: list.dmg.Reduction = ""]
[h: dmg.WardBarrier = 0]
[h, if(getState("Ward", Target) == 1): dmg.WardBarrier = 10]
[h, if(getState("Barrier", Target) == 1): dmg.WardBarrier = 20]
[h: target.TempHealth = getProperty("TempHealth", Target)]

<!-- Block damage with Ward or Barrier -->
  [h, if(dmg.WardBarrier != 0), code:
    {
      [dmg.Total = max(0, dmg.Total - dmg.WardBarrier)]
      [if(dmg.WardBarrier == 10): dmg.Blocked = min(10, original.dmg.Total - dmg.Total)]
      [if(dmg.WardBarrier == 20): dmg.Blocked = min(20, original.dmg.Total - dmg.Total)]
      [if(dmg.Blocked != 0): list.dmg.Reduction = listAppend(list.dmg.Reduction, dmg.Blocked + " blocked")]
    }]

<!-- Reduce damage with TempHealth -->
  [h, if(target.TempHealth != 0), code:
    {
      [dmg.Total = max(0, dmg.Total - target.TempHealth)]
      [dmg.Reduced = min(target.TempHealth, original.dmg.Total - dmg.Blocked - dmg.Total)]
      [if(dmg.Reduced != 0): list.dmg.Reduction = listAppend(list.dmg.Reduction, dmg.Reduced + " reduced")]
      [setProperty("TempHealth", max(0, target.TempHealth - dmg.Reduced), Target)]
    }]

<!-- Resist damage with Resists -->
  [h, if(ResistAll != 0), code:
    {
      [dmg.Total = max(0, dmg.Total - ResistAll)]
      [dmg.Resisted = min(ResistAll, original.dmg.Total - dmg.Blocked - dmg.Reduced - dmg.Total)]
      [if(dmg.Resisted != 0): list.dmg.Reduction = listAppend(list.dmg.Reduction, dmg.Resisted + " resisted")]
    }]

<!-- Build tooltip for Damage -->
  [h, if(atk.Missed == 1 && power.HalfDamageOnMiss == "No"), code:
    {
      [dmg.Total = "Missed!"]
      [dmg.Tooltip = "-- No Damage --"]
      [targetHealthDisplay = ""]
    };{
      <!-- Create dmg.Tooltip for CompletedAttackResults -->
        [dmg.Tooltip = "<html><table width=300><tr><td colspan=3><b><font size=5>Damage to " + TargetName + "</b></td></tr>"]
        [if(power.StaticDamage == "Yes"): dmg.Tooltip = dmg.Tooltip + "<tr><td width=10 align=right><font size=4></font></td><td width=15><font size=4>" + eval(power.StaticDamageAmount) + "</font></td><td width=175><font size=4> (" + power.StaticDamageAmount + ")</font></td></tr>"]
        [if(power.StaticDamage == "No"): dmg.Tooltip = dmg.Tooltip + "<tr><td width=10 align=right><font size=4></font></td><td width=15><font size=4>" + dmg.Roll + "</font></td><td width=175><font size=4> (" + item.Damage + ")</font></td></tr>"]
        [if(power.StaticDamage == "No"): dmg.Tooltip = dmg.Tooltip + "<tr><td width=10 align=right><font size=4>+</font></td><td width=15><font size=4>" + eval(atk.Attribute) + "</font></td><td width=175><font size=4> (" + atk.Attribute + ")</font></td></tr>"]
        [if(power.StaticDamage == "No" && dmg.mod.Miscellaneous != 0): dmg.Tooltip = dmg.Tooltip + "<tr><td width=10 align=right><font size=4>+</font></td><td width=15><font size=4>" + dmg.mod.Miscellaneous + "</font></td><td width=175><font size=4> (Miscellaneous)</font></td></tr>"]
        [if(power.HalfDamageOnMiss == "Yes" && atk.Missed == 1): dmg.Tooltip = dmg.Tooltip + "<tr><td width=10 align=right><font size=4>=</font></td><td width=15><font size=4>" + floor(original.DamageForTooltip/2) + "</font></td><td width=175><font size=4> (Half damage on miss! " + original.DamageForTooltip + " / 2)</font></td></tr>"]
        [if(dmg.Blocked != 0): dmg.Tooltip = dmg.Tooltip + "<tr><td width=10 align=right><font size=4>-</font></td><td width=15><font size=4>" + dmg.Blocked + "</font></td><td width=175><font size=4> (Blocked)</font></td></tr>"]
        [if(dmg.Reduced != 0): dmg.Tooltip = dmg.Tooltip + "<tr><td width=10 align=right><font size=4>-</font></td><td width=15><font size=4>" + dmg.Reduced + "</font></td><td width=175><font size=4> (Reduced)</font></td></tr>"]
        [if(dmg.Resisted != 0): dmg.Tooltip = dmg.Tooltip + "<tr><td width=10 align=right><font size=4>-</font></td><td width=15><font size=4>" + dmg.Resisted + "</font></td><td width=175><font size=4> (Resisted)</font></td></tr>"]
        [dmg.Tooltip = dmg.Tooltip + "<tr><td colspan=3><font size=5><b>" + dmg.Total + " " + lower(power.DamageTypes) + " damage</b></font></td></tr></table></html>"]

      <!-- Deal damage to target and update Health bar -->
        [token(Target), code:
          {
            [CurrentHealth = CurrentHealth - dmg.Total]
            [setBar("Health", CurrentHealth / MaxHealth)]
            [targetHealthDisplay = "&#91;Health " + CurrentHealth + " / " + MaxHealth + "&#93;"]
            [if(HIDE_MONSTER_HEALTH == "TRUE" && isNPC() == 1): target.HealthDisplay = ""]

            <!-- If target is at half health or less, apply bloodied -->
              [if(CurrentHealth <=  MaxHealth/2 && state.Bloodied != 1): state.Bloodied = 1]

            <!-- If target is an npc and at zero health or less, apply death -->
              [if(CurrentHealth <= 0 && isNPC() == 1): setAllStates(0)]
              [if(CurrentHealth <= 0 && isNPC() == 1): state.Dead = 1]

            <!-- If target is a pc and at more than half their health in the negatives, apply death -->
              [if(CurrentHealth <=  MaxHealth - MaxHealth * 1.5 && isPC() == 1): setAllStates(0)]
              [if(CurrentHealth <=  MaxHealth - MaxHealth * 1.5 && isPC() == 1): state.Dead = 1]
          }]
    }]

<!-- Build display for damage roll -->
  [h: dmg.Display = dmg.Total + " " + lower(power.DamageTypes) + " damage "]
  [h, if(dmg.Resisted != 0 || dmg.Blocked != 0 || dmg.Reduced != 0): dmg.Display = dmg.Display + "(" + list.dmg.Reduction + ") "]
  [h: dmg.Display = dmg.Display + targetHealthDisplay]
  [h, if(atk.Missed == 1 && power.HalfDamageOnMiss == "No"): dmg.Display = "Missed!"]

<!-- Hide npc damage calculations in the tooltip -->
  [h, if(HIDE_MONSTER_DAMAGE_TOOLTIP == "TRUE" && isNPC() == 1): dmg.Tooltip = "-- Monster Damage Hidden --"]
[/spoiler]

User avatar
JonathanTheBlack
Dragon
Posts: 544
Joined: Mon Jun 28, 2010 12:12 pm
Location: I'm the worm...

Re: Building a Framework (Step by Step)

Post by JonathanTheBlack »

Adding Targeting Filters to jtb.AttackHandler

Now that I've got the bulk of the combat engine done, I need to take a step back and review how the target list is built and utilize options chosen when creating a power to filter target lists. This includes aborting an attack if they've selected too many targets or if all targets are out of range. The current TargetType options are:

No Target
Self
One Target
One Ally
One Enemy
One or Two Targets
One or Two Allies
One or Two Enemies
Up to Three Targets
Up to Three Allies
Up to Three Enemies
Each Target in Burst
Each Ally in Burst
Each Enemy in Burst

Code: Select all

<!-- Creates a list of targets based on the tokens selected by the attacker -->
  [h: list.Targets = getSelected()]

<!-- Removes the Attacker from the target list, unless the Attacker is the only target and the power does not have the target type of Self -->
  [h, if(power.TargetType != "Self" && listCount(list.Targets) > 1): list.Targets = listDelete(list.Targets, listFind(list.Targets, Attacker))]
  [h, if(power.TargetType == "Self"): list.Targets = Attacker]


Right now, the only filtering going on is removing the attacker, as long as they're not the only target in the list and the power has a TargetType of something other than Self. It also changes the target list to just the attacker if the TargetType is Self. First, I'm going to filter out allies if the TargetType specifically targets enemies.

User avatar
JonathanTheBlack
Dragon
Posts: 544
Joined: Mon Jun 28, 2010 12:12 pm
Location: I'm the worm...

Re: Building a Framework (Step by Step)

Post by JonathanTheBlack »

Spent some time today at home working on this and managed to get the target selection aspect of AttackHandler working better. I should probably move all the targeting to it's own UDF though. I could also trim out some of the fat if I created a power.MaxTargets property on the compendium. Right now I just want to get things working and then I can go back and utilize this alpha version of the framework to organize the beta version from scratch and streamline it. As of right now, it still runs with a stack size of 2 and any delay after clicking the button is nearly imperceptible. Of course this is only for attacks on up to nine targets, which is the default max of targets since Ranged Bursts are only 1 square in each direction from the origin. Melee bursts can have more targets, but only if the attacker is Large or bigger.

I've added in two Wiki: assert() calls to cancel the attack if there aren't enough targets or if too many tokens are targeted. Checking for out of range targets on ranged burst attacks is something I'd like to do, but it would require defining an origin square somehow and if the square was empty, the player would have to have some way of targeting that square with a dummy target to mark the center of the burst. Leaving burst targeting up to the players just speeds the game along and makes my job easier.

jtb.AttackHandler
[spoiler]

Code: Select all

<!-- Creates a list of targets based on the tokens selected by the attacker -->
  [h: list.Targets = getSelected()]
  [h: target.Count = listCount(list.Targets)]
  [h, if(power.TargetType == "No Target"): list.Targets = ""]
  [h, if(power.TargetType != "No Target" && target.Count == 0): assert(0, "<b style='color:#660000;'>You have not selected any targets.</b>", 0)]
  [h, if(power.TargetType == "Self"): list.Targets = Attacker]
  [h, if(power.TargetType != "Self" && listCount(list.Targets) > 1): list.Targets = listDelete(list.Targets, listFind(list.Targets, Attacker))]
  [h: target.Count = listCount(list.Targets)]


<!-- Sets the max number of targets -->
  [h, if(power.TargetType == "No Target"): MaxTargets = 0]
  [h, if(power.TargetType == "One Target" || power.TargetType == "One Ally" || power.TargetType == "One Enemy" || power.TargetType == "Self"): MaxTargets = 1]
  [h, if(matches(power.TargetType, ".*One or Two.*")): MaxTargets = 2]
  [h, if(matches(power.TargetType, ".*Up to Three.*")): MaxTargets = 3]
  [h, if(matches(power.TargetType, ".*Each.*")), code:
    {
      [MaxTargets = 9]
      [if(getSize(Attacker) == "Large" && power.RangeType == "Melee Burst"): MaxTargets = 12]
      [if(getSize(Attacker) == "Huge" && power.RangeType == "Melee Burst"): MaxTargets = 16]
      [if(getSize(Attacker) == "Gargantuan" && power.RangeType == "Melee Burst"): MaxTargets = 20]
      [if(getSize(Attacker) == "Colossal" && power.RangeType == "Melee Burst"): MaxTargets = 28]
    }]


  <!-- Abort if attacker selected too many targets -->
  [h, if(target.Count > MaxTargets): assert(0, "<b style='color:#660000;'>You have selected too many targets.</b>", 0)]

<!-- Filter enemies and/or allies from attacks that target one or the other -->
  [h, if(indexOf(power.TargetType, "Enemy") > 0 || indexOf(power.TargetType, "Enemies") > 0), code:
    {
      [foreach(TargetCheck, list.Targets, ""), code:
        {
          [if(isPC(Attacker) == isPC(TargetCheck)): list.Targets = listDelete(list.Targets, listFind(list.Targets, TargetCheck))]
          [if(isNPC(Attacker) == isNPC(TargetCheck)): list.Targets = listDelete(list.Targets, listFind(list.Targets, TargetCheck))]
        }]
    }]
  [h, if(indexOf(power.TargetType, "Ally") > 0 || indexOf(power.TargetType, "Allies") > 0), code:
    {
      [foreach(TargetCheck, list.Targets, ""), code:
        {
          [if(isPC(Attacker) != isPC(TargetCheck)): list.Targets = listDelete(list.Targets, listFind(list.Targets, TargetCheck))]
          [if(isNPC(Attacker) != isNPC(TargetCheck)): list.Targets = listDelete(list.Targets, listFind(list.Targets, TargetCheck))]
        }]
    }]
[/spoiler]

User avatar
JonathanTheBlack
Dragon
Posts: 544
Joined: Mon Jun 28, 2010 12:12 pm
Location: I'm the worm...

Re: Building a Framework (Step by Step)

Post by JonathanTheBlack »

Spent a little more time working on the targeting aspect of AttackHandler. The following snippet will stop the attack if the selected targets are not valid for the attack. This is used to prevent players from targeting enemies with powers meant for allies and vice versa.

[if(list.Targets == ""),assert(0, "<b style='color:#660000;'>You must select a valid target.</b>", 0)]

I also changed the order of functions in the encounter/recharge macros on player tokens. This prevents the button from changing color after the attack is used if the attack fails because of invalid or out of range targets. All I had to do was move [r: jtb.Attack('Arcane Shield])] above the color changing calls and this prevents the button from changing colors if an Wiki: assert() function is used at any point in the attack. Shifting that line saved me at minimum ten lines of code changing the buttons if the action was invalid.

Code: Select all

<!-- Abort if Used Already -->
  [h: Index = getMacroButtonIndex()]
  [h: ButtonProps = getMacroProps(Index)]
  [h: Color = getStrProp(ButtonProps, 'color')]
  [h, if(Color == 'gray' && isGM() == 0): assert(0, '<b>You have already used this power.</b>', 0)]
  [r: jtb.Attack('Arcane Shield')]
  [h, if(Color == 'blue' || Color == 'red'): setMacroProps(Index, 'color=gray')]
  [h, if(Color == 'blue' || Color == 'red'): setMacroProps(Index, 'fontColor=silver')]
  [h: abort(0)]

User avatar
JonathanTheBlack
Dragon
Posts: 544
Joined: Mon Jun 28, 2010 12:12 pm
Location: I'm the worm...

Re: Building a Framework (Step by Step)

Post by JonathanTheBlack »

Pretty cool... I just tested the speed of the framework as is and one target is 399 ms while eight targets requires 1279 ms. Just a little over a second. Not bad at all. 8)

Post Reply

Return to “Macros”