Spell Damage Accumulator

AceHart

Your Friendly Neighborhood Admin
Reaction score
1,495
What is it?

Damage Accumulator is an area targeted spell that deals the total damage the Hero has been hit with since the last cast to all enemy units in that area.
Up to some maximum total.

I.e. the longer you wait, the stronger it will be.
If you wait too long, the additional damage is lost.
And the more units you hit at once, the less effective it will be.


Looks:

You see the damage in the usual floating text that fades up and away way.
Additionally, some cloud thingy appears over the target units and casts lightning on them.
It looks better than it sounds.


JASS:
library FT
globals
    private constant real vx = 0.071 * 0.5 * Cos(45.0 * bj_DEGTORAD)
    private constant real vy = 0.071 * 0.5 * Sin(45.0 * bj_DEGTORAD)
endglobals
// Floating text is a complete pain in JASS
// Where, for example, GUI uses 10 as default text size, JASS comes with... 0.023?
// Makes all the sense really
// Hence this function
function FloatingTextXY takes string s,real x, real y returns nothing
    set bj_lastCreatedTextTag = CreateTextTag() // New floating text
    call SetTextTagText(bj_lastCreatedTextTag, s, 0.023) // Set the message and the size
    call SetTextTagPos(bj_lastCreatedTextTag, x + GetRandomReal(0, 50.0), y + GetRandomReal(0, 50.0), 0) // Slightly offset from the point
    call SetTextTagColor(bj_lastCreatedTextTag, 255, 0, 0, 255) // Red, no transparency
    call SetTextTagVelocity(bj_lastCreatedTextTag, vx, vy) // The speed it moves with. A direction actually.
    call SetTextTagFadepoint(bj_lastCreatedTextTag, 2.0) // Start fading
    call SetTextTagLifespan(bj_lastCreatedTextTag, 3.0) // Show text this long
    call SetTextTagPermanent(bj_lastCreatedTextTag, false) // Have it remove itself
endfunction
endlibrary


scope DA initializer Init
globals
    // Configuration options
    private constant integer SPELL_ID = 'A000' // Damage Accumulator
    private constant real SPELL_RANGE = 450.0 // Radius of area of effect
    private constant real MAX_DAMAGE = 500.0 // Maximum total damage you can accumulate
    private constant integer DUMMY = 'n000'
    private constant integer DUMMY_SPELL =  'A001' // Single-target, 1 damage Chain Lightning
    private constant string DUMMY_ORDER = "chainlightning" // Order to use to cast it
    private constant string DUMMY_EFFECT = "Abilities\\Spells\\Other\\Doom\\DoomTarget.mdl" // Alternatively, some effect to show
    // Leave as is
    private trigger dt = CreateTrigger() // Trigger for "A unit takes damage"
    private group dg = CreateGroup() // Used to make sure every unit is only ever added once to the above trigger
    private real array damage // To keep track of the damage so far
    private group g = CreateGroup() // Used whenever we need some units
    private real c = 0 // Used to count the number of units we're going to hit
endglobals

private function H2I takes handle h returns integer // Converts a handle to a usable number
    return h
    return 0
endfunction

private function AddCaster takes unit u returns boolean
    // If it has the ability and we never seen it before
    if GetUnitAbilityLevel(u, SPELL_ID) > 0 and IsUnitInGroup(u, dg) == false then
        call GroupAddUnit(dg, u) // Remember unit to prevent duplicate events
        set damage[H2I(u) - 0x100000] = 0.0 // Start its damage count at 0
        call TriggerRegisterUnitEvent(dt, u, EVENT_UNIT_DAMAGED) // add the "This unit takes damage" event
    endif
    return false
endfunction
private function IsStartingCaster takes nothing returns boolean // Called from "elapsed time is 0.0", to register pre-placed units, if any
    return AddCaster(GetFilterUnit())
endfunction
private function IsNewCaster takes nothing returns boolean // Called from "a unit enters map", in case the unit is created or bought or ... later
    return AddCaster(GetTriggerUnit())
endfunction

// Get all units in the map and register events if they have the spell
private function StartingUnits takes nothing returns nothing
    call GroupClear(g)
    call GroupEnumUnitsInRect(g, GetWorldBounds(), Condition(function IsStartingCaster))
endfunction

// Track the total damage
private function Accumulate takes nothing returns nothing
    local integer i = H2I(GetTriggerUnit()) - 0x100000 // Poor man's attachment system, but try to beat this for speed
    set damage<i> = damage<i> + GetEventDamage()
    if damage<i> &gt; MAX_DAMAGE then // only up to the max allowed
        set damage<i> = MAX_DAMAGE
    endif
endfunction

private function GetTargetCount takes nothing returns boolean
    // Enemy, still moving, no structure
    if IsUnitEnemy(GetFilterUnit(), GetOwningPlayer(GetTriggerUnit())) and GetUnitState(GetFilterUnit(), UNIT_STATE_LIFE) &gt; 0.5 and IsUnitType(GetFilterUnit(), UNIT_TYPE_STRUCTURE) == false then
        set c = c + 1.0 // Count them as we find them
        return true
    endif
    return false // Otherwise ignore it
endfunction

private function HasTarget takes nothing returns nothing
    local location l = GetSpellTargetLoc()

    call GroupClear(g)
    set c = 0.0
    call GroupEnumUnitsInRangeOfLoc(g, l, SPELL_RANGE, Condition(function GetTargetCount))
    call RemoveLocation(l)
    set l = null
    // We cancel the cast if there aren&#039;t any enemies nearby or if we do not have any damage to deal back
    if c &lt; 1.0 then
        call DisplayTextToPlayer(GetOwningPlayer(GetTriggerUnit()), 0, 0, &quot;|cffffcc00No units in range...|r&quot;)
        call IssueImmediateOrder(GetTriggerUnit(), &quot;stop&quot;)
    elseif damage[H2I(GetTriggerUnit()) - 0x100000] &lt; c then
        call DisplayTextToPlayer(GetOwningPlayer(GetTriggerUnit()), 0, 0, &quot;|cffffcc00Not enough damage accumulated yet...|r&quot;)
        call IssueImmediateOrder(GetTriggerUnit(), &quot;stop&quot;)
    endif
endfunction

// Every spell needs this
private function Conditions takes nothing returns boolean
    return GetSpellAbilityId() == SPELL_ID
endfunction

// The interesting part
private function Actions takes nothing returns nothing
    local unit u = GetTriggerUnit() // Who casted
    local location l = GetSpellTargetLoc() // Where
    local player p = GetOwningPlayer(u) // What player
    local integer i = H2I(u) - 0x100000 // Index to damage array for casting unit
    local unit dummy
    local unit t // Temporary target unit
    local real d // Damage to deal per unit

    // Get and count all units we&#039;re going to hit
    call GroupClear(g)
    set c = 0.0
    call GroupEnumUnitsInRangeOfLoc(g, l, SPELL_RANGE, Condition(function GetTargetCount))
    call RemoveLocation(l)
    set l = null
    if c &lt; 1.0 then // Can&#039;t happen, but we&#039;re paranoid
        set p = null
        set u = null
        return
    endif
    // Amount of damage to deal to each unit
    set d = 0.5 + damage<i> / c
    // Reset damage count
    set damage<i> = 0.0
    loop // For each target unit
        set t = FirstOfGroup(g)
        exitwhen t == null
        call GroupRemoveUnit(g, t)
        if GetUnitState(t, UNIT_STATE_LIFE) &gt; d then // If the hit won&#039;t kill it
            set dummy = CreateUnit(p, DUMMY, GetUnitX(t), GetUnitY(t), bj_UNIT_FACING) // New dummy caster
            call UnitAddAbility(dummy, DUMMY_SPELL) // Chain Lightning
            call IssueTargetOrder(dummy, DUMMY_ORDER, t) // Cast it
            call UnitApplyTimedLife(dummy, &#039;BTLF&#039;, 1.0) // Begone, foul shade
        else // Otherwise the unit&#039;s nearly dead already: show simple effect
            call DestroyEffect(AddSpecialEffect(DUMMY_EFFECT, GetUnitX(t), GetUnitY(t)))
        endif
        // Deal the damage
        call UnitDamageTarget(u, t, d, true, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_NORMAL, WEAPON_TYPE_WHOKNOWS)
        // Show damage dealt to this target unit
        call FloatingTextXY(I2S(R2I(d)) + &quot;!&quot;, GetUnitX(t), GetUnitY(t))
    endloop

    set dummy = null // Clean up
    set p = null
    set u = null
endfunction

private function Init takes nothing returns nothing
    local region r = CreateRegion()

    // Elapsed game time is 0.0 seconds
    local trigger t = CreateTrigger()
    call TriggerRegisterTimerEvent(t, 0.0, false)
    call TriggerAddAction(t, function StartingUnits)

    // A unit enters entire map
    set t = CreateTrigger()
    call RegionAddRect(r, GetWorldBounds())
    call TriggerRegisterEnterRegion(t, r, null)
    call TriggerAddCondition(t, Condition(function IsNewCaster))

    // A unit begins casting an ability
    set t = CreateTrigger()
    call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_CAST)
    call TriggerAddCondition(t, Condition(function Conditions))
    call TriggerAddAction(t, function HasTarget)
    // A unit starts the effect of an ability
    set t = CreateTrigger()
    call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_EFFECT)
    call TriggerAddCondition(t, Condition(function Conditions))
    call TriggerAddAction(t, function Actions)

    // Events are added dynamically.
    // Once there are some, this function will be called as &quot;specific unit takes damage&quot;
    call TriggerAddAction(dt, function Accumulate)
endfunction
endscope
</i></i></i></i></i></i>



Getting it into your own map:

Create a trigger,
menu: Edit / Convert to custom script,
replace whatever is showing there with the above script.
Edit the configuration options as needed.

You need an area ability.
I used Silence for no particular reason.
With a better effect and, of course, it doesn't silence anything.

A flying dummy.
The usual, no attack, no sound, no corpse, flying, plenty mana, "cloudy" model, ...
A Chain Lighting that only hits one target for only 1 damage.
Or whatever else you want your dummy to cast.

Note:
Requires NewGen.


Enjoy.

Yours,
AceHart



Q: Impressive!
A: Admit it, you started reading here and not even seen it yet

Q: What map could possibly want to use this?
A: The one I'm working on for example

Q: What map is that?
A: Uhm... the one I'm working on?

Q: MUI?
A: This is multi-whatever-the-heck-you-care-for-and-then-some-including-but-not-limited-to-the-kitchen-sink-and-the-cat

Q: It uses a global though to count
A: It's still <see above>

Q: I can has cookie?
A: Noez, phyrex1an ated them all

Q: Can I edit this so it gets a higher max total per level?
A: Yes.

Q: Any chance you might be slightly more specific?
A: Replace the max total constant with an array, set the maxs per level in the init function, compare with array[level]

Q: Same for range then, right?
A: Right.

Q: Why not simply run the actions in the conditions? Much more efficient!
A1: Sure.
A2: Doesn't work, it needs the number of units to hit before it can hit them

Q: Wouldn't it be better to copy AddCaster twice?
A: Maybe... though that's counting micro-seconds. How many new units with that spell do you create per second?

Q: Why count the units again? You already did on "begins casting".
A: Only the paranoid survive

Q: Then why not cast it entirely on "begins"?
A: See previous answer

Q: When's the next BlizzCon?
A: August

Q: 5 triggers for one single spell...
A: I'm open to improvements

Q: Your... uhm... "attachment system" can only handle a couple thousand units!
A: Damn! I knew it!

Q: What's that "Easter Egg" deal on Bnet?
A1: Old news...
A2: http://www.battle.net/ - scroll down to the bottom, see that warp pad in the center? Hover over the middle blue part until some text shows. Click. Don't move.

Q: Set t = null? Remove the region? Set r = null?
A: Removing the region is not that great an idea. And the rest isn't a leak either as the triggers are going to stay around forever.

Q: ...
A: Sorry, in order to keep this below 300 lines, that's it

Q: Damn!
A: There's always another time



The map:
 

Attachments

  • DamageAccumulator_Spell_AceHart.w3x
    15.6 KB · Views: 196

Romek

Super Moderator
Reaction score
964
Cool Spell, and funny FAQ :p


GetWidgetLife() is faster, and IsUnitType(unit, UNIT_TYPE_DEAD) is more 'stable'.
And players don't need nulling.

I also get a lag-spike when I cast it for the first time. Quite possibly a problem on my side though.

Good job anyway. :)
I'm surprised a spell like this hasn't already been made. :p
Well, unique ideas are very rare at the moment.

I bet everybody is eager to find out more about your project now too!
 
Reaction score
91
> I also get a lag-spike when I cast it for the first time. Quite possibly a problem on my side though.
Seriously, almost every custom spell has a lag spike on first cast, even if it was preloaded.

Interesting and nice spell, however couldn't you make the error message like an ordinary game error such as "Not enough mana" (SimError comes to mind, though it could be independant)?
 

Romek

Super Moderator
Reaction score
964
> Seriously, almost every custom spell has a lag spike on first cast, even if it was preloaded.
No they don't. Definitely not like this anyway.
 

emjlr3

Change can be a good thing
Reaction score
395
JASS:
Q: Your... uhm... &quot;attachment system&quot; can only handle a couple thousand units!
A: Damn! I knew it!


this could be remedied...
 

Bloodcount

Starcraft II Moderator
Reaction score
297
Nice spell, although when I read the Q&A I liked to hear that you are preparing a new project.
 

Viikuna

No Marlo no game.
Reaction score
265
GetWidgetLife() is faster, and IsUnitType(unit, UNIT_TYPE_DEAD) is more 'stable'.

GetWidgetLife will fail if you modify dead units hp. IsUnitType(unit, UNIT_TYPE_DEAD) will fail if unit has already decayed. ( since it doesnt exist anymore )

edit. The best method to check if unit is death, would be order some dummy to cast spell on it or something.
But since both of these functions work most of the time, no one really wants to bother.
 

BlackRose

Forum User
Reaction score
239
I played it and............ I don't get it, all I see is a thudnerbolt from a poof :)... which is really smart idea....

Is it the more damage you take, you cast, boom!?
 

emjlr3

Change can be a good thing
Reaction score
395
It could. But is it worth it?


As for the first time cast lag, that's normal.
It uses lightning effects with no pre-loading...

maybe, regardless its a 2s fix, so why not
 
General chit-chat
Help Users
  • No one is chatting at the moment.
  • Monovertex Monovertex:
    How are you all? :D
    +1
  • Ghan Ghan:
    Howdy
  • Ghan Ghan:
    Still lurking
    +3
  • The Helper The Helper:
    I am great and it is fantastic to see you my friend!
    +1
  • The Helper The Helper:
    If you are new to the site please check out the Recipe and Food Forum https://www.thehelper.net/forums/recipes-and-food.220/
  • Monovertex Monovertex:
    How come you're so into recipes lately? Never saw this much interest in this topic in the old days of TH.net
  • Monovertex Monovertex:
    Hmm, how do I change my signature?
  • tom_mai78101 tom_mai78101:
    Signatures can be edit in your account profile. As for the old stuffs, I'm thinking it's because Blizzard is now under Microsoft, and because of Microsoft Xbox going the way it is, it's dreadful.
  • The Helper The Helper:
    I am not big on the recipes I am just promoting them - I use the site as a practice place promoting stuff
    +2
  • Monovertex Monovertex:
    @tom_mai78101 I must be blind. If I go on my profile I don't see any area to edit the signature; If I go to account details (settings) I don't see any signature area either.
  • The Helper The Helper:
    You can get there if you click the bell icon (alerts) and choose preferences from the bottom, signature will be in the menu on the left there https://www.thehelper.net/account/preferences
  • The Helper The Helper:
    I think I need to split the Sci/Tech news forum into 2 one for Science and one for Tech but I am hating all the moving of posts I would have to do
  • The Helper The Helper:
    What is up Old Mountain Shadow?
  • The Helper The Helper:
    Happy Thursday!
    +1
  • Varine Varine:
    Crazy how much 3d printing has come in the last few years. Sad that it's not as easily modifiable though
  • Varine Varine:
    I bought an Ender 3 during the pandemic and tinkered with it all the time. Just bought a Sovol, not as easy. I'm trying to make it use a different nozzle because I have a fuck ton of Volcanos, and they use what is basically a modified volcano that is just a smidge longer, and almost every part on this thing needs to be redone to make it work
  • Varine Varine:
    Luckily I have a 3d printer for that, I guess. But it's ridiculous. The regular volcanos are 21mm, these Sovol versions are about 23.5mm
  • Varine Varine:
    So, 2.5mm longer. But the thing that measures the bed is about 1.5mm above the nozzle, so if I swap it with a volcano then I'm 1mm behind it. So cool, new bracket to swap that, but THEN the fan shroud to direct air at the part is ALSO going to be .5mm to low, and so I need to redo that, but by doing that it is a little bit off where it should be blowing and it's throwing it at the heating block instead of the part, and fuck man
  • Varine Varine:
    I didn't realize they designed this entire thing to NOT be modded. I would have just got a fucking Bambu if I knew that, the whole point was I could fuck with this. And no one else makes shit for Sovol so I have to go through them, and they have... interesting pricing models. So I have a new extruder altogether that I'm taking apart and going to just design a whole new one to use my nozzles. Dumb design.
  • Varine Varine:
    Can't just buy a new heatblock, you need to get a whole hotend - so block, heater cartridge, thermistor, heatbreak, and nozzle. And they put this fucking paste in there so I can't take the thermistor or cartridge out with any ease, that's 30 dollars. Or you can get the whole extrudor with the direct driver AND that heatblock for like 50, but you still can't get any of it to come apart
  • Varine Varine:
    Partsbuilt has individual parts I found but they're expensive. I think I can get bits swapped around and make this work with generic shit though

      The Helper Discord

      Members online

      Affiliates

      Hive Workshop NUON Dome World Editor Tutorials

      Network Sponsors

      Apex Steel Pipe - Buys and sells Steel Pipe.
      Top