Spell Black Arrow (remade)

Flare

Stops copies me!
Reaction score
662
Posted in a new thread because after asking Andrewgosu about it, he suggested I resubmit it completely

After about 3-4 days of lazy coding, I finally finished reconstructing my Black Arrow's JASS version (since, by my standards now, it was quite fail :p) - so, without further ado, I present the new and improved

Black Arrow
The Ranger fires a dark arrow towards the target point. If an enemy is hit by the arrow, it will take damage and the arrow will store some damage. If an ally is hit, it will be healed and will store some healing. When the arrow reaches the target point, it explodes, healing allies for the accumulated healing and damaging enemies for the accumulated damage

Why this version rules compared to the other one:
  • System-independant (for minimal importing requirements)
  • No more dynamic triggers
  • Much more efficient
  • More configurable properties
  • Abuse of colon syntax to make amusing code smilies :D

Importing guidelines
  • Either copy the Black Arrow (new) trigger in the demo map into your map, or create a new trigger (name doesn't matter), convert to custom text, delete what is in that trigger, and copy the spell code into it
  • Copy the ability Black Arrow
  • Copy the unit Black Arrow
  • Modify the constants DUMMYID and SPELLID so that they match the rawcodes of the ability and dummy (press Ctrl-D in Object Editor to view rawcodes)
  • Modify any other constants/config functions as necessary
  • Add the ability to a unit, and enjoy :D

Code:
JASS:
//------------------------------------------------------------------------------------\\
//                                  Black Arrow                                       \\
//                                   by Flare                                         \\
//                              Constructed using vJASS                               \\
//------------------------------------------------------------------------------------\\

scope BlackArrowNew initializer InitFunc

globals
//Raw code and SFX related globals
//------------------------------------------------------------------------------------\\
//Raw code of the base ability
    private constant integer SPELLID = 'A000'
    
//Raw code of the dummy unit
    private constant integer DUMMYID = 'h000'
    
//The SFX model used when an enemy is hit
    private constant string DAMAGEFX = "Abilities\\Spells\\Undead\\DeathCoil\\DeathCoilSpecialArt.mdl"
    
//The SFX model used when an ally is hit
    private constant string HEALFX = "Abilities\\Spells\\Undead\\AnimateDead\\AnimateDeadTarget.mdl"
    
//The SFX model used on the end effect
    private constant string ENDFX = "Abilities\\Spells\\Other\\HowlOfTerror\\HowlCaster.mdl"
    
    
    
//Floating text related globals
//------------------------------------------------------------------------------------\\

//Heal colour string
    private constant string HEALSTRING = "|c0025D600"
    
//Damage colour string
    private constant string DAMAGESTRING = "|c00710072"
    
    
//Globals that -SHOULD- be constant at all times (such as max distance, end damage/heal bonus per hit
//------------------------------------------------------------------------------------\\
//Should the ability heal the caster?
    private constant boolean PROJHEALCASTER = false
    private constant boolean ENDHEALCASTER = true
    
//Bonus to end heal/damage amount per unit hit
    private constant real ENDBONUS = 10
    
//Distance travelled per second
    private constant real SPEED = 800
    
//Timer interval
    private constant real TIMERINTERVAL = 0.03
    
//Range in which unit is elligible to be hit by the arrow
    private constant real DETECTRANGE = 100
    
    
//Damage related globals
//------------------------------------------------------------------------------------\\

//Boolean to determine whether impact damage/heal is constant or not
    private constant boolean CONSTANTDAMAGE = false
    
//Constant values for impact damage/heal if above boolean is true
    private constant real IMPACTDAMAGE = 25
    private constant real IMPACTHEAL = 25
    
//Attack, damage, and weapon types for damage dealt
    private constant attacktype AT = ATTACK_TYPE_MAGIC
    private constant damagetype DT = DAMAGE_TYPE_NORMAL
    private constant weapontype WT = null
endglobals


//This function is used to determine your own customised unit-type filter
//NOTE: SHOULD ONLY BE USED IF YOU KNOW WHAT YOU'RE DOING

//If, say, you want to exclude mechanical, structure, ethereal, etc. units
//Can be used for pretty much any filtering you want
//Ally/enemy filter are already covered, along with living filter, within the heart of the spell's code
private function GenericTypeFilter takes unit u returns boolean
    return IsUnitType (GetFilterUnit (), UNIT_TYPE_STRUCTURE) == false and IsUnitType (GetFilterUnit (), UNIT_TYPE_MECHANICAL) == false
endfunction


//Varying damage
private function VariableDamage takes unit u returns real
    local real base = 10
    local real m1 = GetHeroInt (u, true)
    local real m2 = 0.25
    return base + m1 * m2
endfunction

//Varying heal
private function VariableHeal takes unit u returns real
    local real base = 10
    local real m1 = GetHeroInt (u, true)
    local real m2 = 0.25
    return base + m2 * m2
endfunction

//Varying end radius
private function VariableEndRadius takes unit u returns real
    local real base = 150
    local real m1 = GetUnitAbilityLevel (u, SPELLID)
    local real m2 = 50
    return base + m1 * m2
endfunction


//END OF CONFIGURATION\\

private keyword Data


globals
    private timer T = CreateTimer ()
    private integer N = 0
    private Data array D
    private Data g
    private group Enum = CreateGroup ()
    private boolexpr DamageFilter
    private boolexpr TrueFilter
    private boolexpr EndFilter
    private real MaxX
    private real MaxY
    private real MinX
    private real MinY
    private constant real UNITS_PER_SEC = SPEED * TIMERINTERVAL
endglobals


private function SetUnitXY takes unit u, real x, real y returns nothing
    if x > MaxX then
        set x = MaxX
    elseif x < MinX then
        set x = MinX
    endif
    
    if y > MaxY then
        set y = MaxY
    elseif y < MinY then
        set y = MinY
    endif
    
    call SetUnitX (u, x)
    call SetUnitY (u, y)
endfunction


private struct Data
    unit caster
    unit dummy
    real offsetcos
    real offsetsin
    real dmg
    real heal
    group prevhit
    real dist
    real healcount
    real damagecount
    method onDestroy takes nothing returns nothing
        call GroupClear (.prevhit)
    endmethod
endstruct


private function QuickTextTag takes string text, real x, real y, boolean dmgtext returns nothing
    local texttag tt = CreateTextTag ()
    call SetTextTagPos (tt, x, y, 0)
    call SetTextTagPermanent (tt, false)
    if dmgtext then
        call SetTextTagText (tt, DAMAGESTRING + text + "|r", 0.023)
        call SetTextTagVelocity (tt, Cos (bj_PI * 0.5) * 0.0355, Sin (bj_PI * 0.5) * 0.0355)
    else
        call SetTextTagText (tt, HEALSTRING + text + "|r", 0.023)
        call SetTextTagVelocity (tt, Cos (bj_PI * 1.5) * 0.0355, Sin (bj_PI * 1.5) * 0.0355)
    endif
    call SetTextTagLifespan (tt, 3.)
    call SetTextTagFadepoint (tt, 2.)
    set tt = null
endfunction


private function DamageFunc takes nothing returns boolean
    local unit u = GetFilterUnit ()
    if IsUnitType (u, UNIT_TYPE_DEAD) == false and IsUnitInGroup (u, g.prevhit) == false and GenericTypeFilter (u) then
        if IsUnitEnemy (u, GetOwningPlayer (g.caster)) then
            set g.damagecount = g.damagecount + ENDBONUS
            call UnitDamageTarget (g.caster, u, g.dmg, false, true, AT, DT, WT)
            call GroupAddUnit (g.prevhit, u)
            call DestroyEffect (AddSpecialEffect (DAMAGEFX, GetUnitX (u), GetUnitY (u)))
        else
            if u == g.caster and PROJHEALCASTER then
                set g.healcount = g.healcount + ENDBONUS
                call SetWidgetLife (u, GetWidgetLife (u) + g.heal)
                call GroupAddUnit (g.prevhit, u)
                call DestroyEffect (AddSpecialEffect (HEALFX, GetUnitX (u), GetUnitY (u)))
            elseif u != g.caster then
                set g.healcount = g.healcount + ENDBONUS
                call SetWidgetLife (u, GetWidgetLife (u) + g.heal)
                call GroupAddUnit (g.prevhit, u)
                call DestroyEffect (AddSpecialEffect (HEALFX, GetUnitX (u), GetUnitY (u)))
            endif
        endif
    endif
    set u = null
    return false
endfunction


private function EndFunc takes nothing returns boolean
    local unit u = GetFilterUnit ()
    if IsUnitType (u, UNIT_TYPE_DEAD) == false and GenericTypeFilter (u) then
        if IsUnitEnemy (u, GetOwningPlayer (g.caster)) then
            call UnitDamageTarget (g.caster, u, g.damagecount, false, true, AT, DT, WT)
        else
            if u == g.caster and ENDHEALCASTER then
                set g.healcount = g.healcount + ENDBONUS
                call SetWidgetLife (u, GetWidgetLife (u) + g.healcount)
            elseif u != g.caster then
                set g.healcount = g.healcount + ENDBONUS
                call SetWidgetLife (u, GetWidgetLife (u) + g.healcount)
            endif
        endif
    endif
    set u = null
    return false
endfunction


private function TimerCallback takes nothing returns nothing
    local integer i = N
    local Data a
    local real x
    local real y
    loop
    exitwhen i == 0
        set a = i<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" class="smilie smilie--sprite smilie--sprite8" alt=":D" title="Big Grin    :D" loading="lazy" data-shortname=":D" />
        set x = GetUnitX (a.dummy)
        set y = GetUnitY (a.dummy)
        set x = x + a.offsetcos
        set y = y + a.offsetsin
        call SetUnitXY (a.dummy, x, y)
        set g = a
        call GroupEnumUnitsInRange (Enum, x, y, DETECTRANGE, DamageFilter)
        set a.dist = a.dist - UNITS_PER_SEC
        if a.dist &lt;= 0 then
            call KillUnit (a.dummy)
            call GroupEnumUnitsInRange (Enum, x, y, VariableEndRadius (a.caster), EndFilter)
            call DestroyEffect (AddSpecialEffect (ENDFX, x, y))
            call QuickTextTag (I2S (R2I (a.healcount)) + &quot;!&quot;, x, y, false)
            call QuickTextTag (I2S (R2I (a.damagecount)) + &quot;!&quot;, x, y, true)
            call a.destroy ()
            set i<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" class="smilie smilie--sprite smilie--sprite8" alt=":D" title="Big Grin    :D" loading="lazy" data-shortname=":D" /> = N<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" class="smilie smilie--sprite smilie--sprite8" alt=":D" title="Big Grin    :D" loading="lazy" data-shortname=":D" />
            set N = N - 1
            if N == 0 then
                call PauseTimer (T)
            endif
        endif
        set i = i - 1
    endloop
endfunction


private function CondFunc takes nothing returns boolean
    return GetSpellAbilityId () == SPELLID
endfunction


private function ActionFunc takes nothing returns nothing
    local Data a = Data.create ()
    local location l = GetSpellTargetLoc ()
    local real tx = GetLocationX (l)
    local real ty = GetLocationY (l)
    local real cx
    local real cy
    local real dx
    local real dy
    local real angle
    call RemoveLocation (l)
    set l = null
    set a.caster = GetTriggerUnit ()
    set a.healcount = 0
    set a.damagecount = 0
    set cx = GetUnitX (a.caster)
    set cy = GetUnitY (a.caster)
    set dx = tx - cx
    set dy = ty - cy
    set a.dist = SquareRoot (dx*dx + dy*dy)
    set a.dmg = VariableDamage (a.caster)
    set a.heal = VariableHeal (a.caster)
    set angle = Atan2 (dy, dx)
    set a.dummy = CreateUnit (GetOwningPlayer (a.caster), DUMMYID, cx, cy, angle * bj_RADTODEG)
    set a.offsetcos = Cos (angle) * (TIMERINTERVAL * SPEED)
    set a.offsetsin = Sin (angle) * (TIMERINTERVAL * SPEED)
    if a.prevhit == null then
        set a.prevhit = CreateGroup ()
    endif
    if N == 0 then
        call TimerStart (T, TIMERINTERVAL, true, function TimerCallback)
    endif
    set N = N + 1
    set N<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" class="smilie smilie--sprite smilie--sprite8" alt=":D" title="Big Grin    :D" loading="lazy" data-shortname=":D" /> = a
endfunction


private function ReturnTrue takes nothing returns boolean
    return true
endfunction


private function InitFunc takes nothing returns nothing
    local trigger t = CreateTrigger ()
    local integer i = 0
    set MaxX = GetRectMaxX (bj_mapInitialPlayableArea)
    set MaxY = GetRectMaxY (bj_mapInitialPlayableArea)
    set MinX = GetRectMinX (bj_mapInitialPlayableArea)
    set MinY = GetRectMinY (bj_mapInitialPlayableArea)
    set DamageFilter = Condition (function DamageFunc)
    set TrueFilter = Condition (function ReturnTrue)
    set EndFilter = Condition (function EndFunc)
    loop
    exitwhen i == 16
        call TriggerRegisterPlayerUnitEvent (t, Player (i), EVENT_PLAYER_UNIT_SPELL_EFFECT, TrueFilter)
        set i = i + 1
    endloop
    call TriggerAddCondition (t, Condition (function CondFunc))
    call TriggerAddAction (t, function ActionFunc)
endfunction

endscope


Screenshots (taken from GUI version, only difference in this version is the lack of custom modelfile)
blackarrowpic3vj0.png
blackarrowpic4yo1.png

Updates:
Code:
v1 - Initial release
v2 - Paused the timer when no instances running
v3 - Got rid of most of the floating text configuration (other than colour strings)

If you are interested in seeing the GUI version (don't be :p), it can be found here
 

Attachments

  • Black Arrow (remake) v3.w3x
    31.4 KB · Views: 442

Vestras

Retired
Reaction score
248
SetUnitXY, why not just use SetUnitPosition? It's a dummy anyways.
Other than that, looks good.
 

Flare

Stops copies me!
Reaction score
662
SetUnitX/Y ignores pathing, and it'd look really dumb if the arrow started drilling into a unit, but going nowhere (thus making the spell kinda sh*t since it can't do much damage/healing if it's blocked :p) - it's magic, it can do anything it wants :D

Anyway, what's done is done, and I don't feel like changing it over something so insignificant (and unnecessary) - and what does the dummy being a dummy have to do with anything?
 

Crusher

You can change this now in User CP.
Reaction score
121
Code:
v1 - Initial release
v2 - Paused the timer when no instances running

Imba Version fixes (Kouds :p)

Great spell!+rep
 

Romek

Super Moderator
Reaction score
963

Flare

Stops copies me!
Reaction score
662
It's faster.
1) Care to show me any benchmark results that would make it worth my while changing it? If not, it won't be changing since it's not really significant
Preferably not Bob666's benchmark, assuming those results over at Clan Mapz were due to the tester itself
2) The IsUnitType check is safer since if you add life to a dead unit, GetWidgetLife won't return 0, but the unit will still be dead, thus causing failure.

There was a thread about it (well, about speed/consistency of Get/SetUnitState vs Get/SetWidgetLife, and the UNIT_TYPE_DEAD thing was brought up), and I'd personally prefer know that I'm not doing anything to dead units, because it'd be really useless to do so, it could very well fuck up other spells that may have a GetWidgetLife check) - for more info, read me
 

Romek

Super Moderator
Reaction score
963
1) Care to show me any benchmark results that would make it worth my while changing it? If not, it won't be changing since it's not really significant
Preferably not Bob666's benchmark, assuming those results over at Clan Mapz were due to the tester itself
That was because Vestras made a really crappy job of using the benchmark :p

2) The IsUnitType check is safer since if you add life to a dead unit, GetWidgetLife won't return 0, but the unit will still be dead, thus causing failure.
I never thought of that.
I doubt someone would set the life of a dead unit though.
 

emjlr3

Change can be a good thing
Reaction score
395
looks fun, how did you avoid the, place an arrow unit, his facing is always the same way and must be moved, bug?
 

emjlr3

Change can be a good thing
Reaction score
395
I have had issues in the past where when creating a unit in game with the dark arrow model, it was always placed with its facing at 270., and had to be moved to face the correct angle (which we know isn't instant) - so it always looked a little weird
 

Romek

Super Moderator
Reaction score
963
I have had issues in the past where when creating a unit in game with the dark arrow model, it was always placed with its facing at 270., and had to be moved to face the correct angle (which we know isn't instant) - so it always looked a little weird
Did you give the arrow a turn rate greater than ~0.6?
Did it have more than 0 movement speed?

I'm pretty sure it's not a problem with the model.
 

Flare

Stops copies me!
Reaction score
662
~bump~

And late response =D
I doubt someone would set the life of a dead unit though.
All it takes is one incomplete enumeration condition, which isn't that unlikely to be honest

Once that happens, everything can possibly go downhill (you could be enumerating units with >0.405HP and healing them, then you could enumerate units with >100HP and steal their life, and so on, so forth)
 

emjlr3

Change can be a good thing
Reaction score
395
healcount and damagecount can come preset to zero

set N:D = a

works like

N[D] = a

??? who knew...

I would havet named my gobals a little better, but thats just me

I think your configurables are overkill, but whatever

rest is acceptable
 

Flare

Stops copies me!
Reaction score
662
healcount and damagecount can come preset to zero
Doesn't really make a difference where it's defaulted to be honest - either way, it's going to be set to 0 :S

set N: D = a

works like

N[D] = a
Indeed :p It's always fun to play around with new syntax you find :)

I would havet named my gobals a little better, but thats just me
What's wrong with them? The are logical enough for me (D = data, N = number, g = global, T = timer, and rest is self-explanatory I would hope)

I think your configurables are overkill, but whatever
Which ones? And that's just me, I like to make anything anad everything configurable, if possible
 

emjlr3

Change can be a good thing
Reaction score
395
im not one to want to spend 15 minutes configing a spell (as if I did at all)

the global names were not logical to me - on second thought I guess they were, its the private data g that screwed me up
 

emjlr3

Change can be a good thing
Reaction score
395
anyway I approve the code - I have not tested in game but I trust your work
 
General chit-chat
Help Users
  • No one is chatting at the moment.
  • Varine Varine:
    How can you tell the difference between real traffic and indexing or AI generation bots?
  • The Helper The Helper:
    The bots will show up as users online in the forum software but they do not show up in my stats tracking. I am sure there are bots in the stats but the way alot of the bots treat the site do not show up on the stats
  • Varine Varine:
    I want to build a filtration system for my 3d printer, and that shit is so much more complicated than I thought it would be
  • Varine Varine:
    Apparently ABS emits styrene particulates which can be like .2 micrometers, which idk if the VOC detectors I have can even catch that
  • Varine Varine:
    Anyway I need to get some of those sensors and two air pressure sensors installed before an after the filters, which I need to figure out how to calculate the necessary pressure for and I have yet to find anything that tells me how to actually do that, just the cfm ratings
  • Varine Varine:
    And then I have to set up an arduino board to read those sensors, which I also don't know very much about but I have a whole bunch of crash course things for that
  • Varine Varine:
    These sensors are also a lot more than I thought they would be. Like 5 to 10 each, idk why but I assumed they would be like 2 dollars
  • Varine Varine:
    Another issue I'm learning is that a lot of the air quality sensors don't work at very high ambient temperatures. I'm planning on heating this enclosure to like 60C or so, and that's the upper limit of their functionality
  • Varine Varine:
    Although I don't know if I need to actually actively heat it or just let the plate and hotend bring the ambient temp to whatever it will, but even then I need to figure out an exfiltration for hot air. I think I kind of know what to do but it's still fucking confusing
  • The Helper The Helper:
    Maybe you could find some of that information from AC tech - like how they detect freon and such
  • Varine Varine:
    That's mostly what I've been looking at
  • Varine Varine:
    I don't think I'm dealing with quite the same pressures though, at the very least its a significantly smaller system. For the time being I'm just going to put together a quick scrubby box though and hope it works good enough to not make my house toxic
  • Varine Varine:
    I mean I don't use this enough to pose any significant danger I don't think, but I would still rather not be throwing styrene all over the air
  • The Helper The Helper:
    New dessert added to recipes Southern Pecan Praline Cake https://www.thehelper.net/threads/recipe-southern-pecan-praline-cake.193555/
  • The Helper The Helper:
    Another bot invasion 493 members online most of them bots that do not show up on stats
  • Varine Varine:
    I'm looking at a solid 378 guests, but 3 members. Of which two are me and VSNES. The third is unlisted, which makes me think its a ghost.
    +1
  • The Helper The Helper:
    Some members choose invisibility mode
    +1
  • The Helper The Helper:
    I bitch about Xenforo sometimes but it really is full featured you just have to really know what you are doing to get the most out of it.
  • The Helper The Helper:
    It is just not easy to fix styles and customize but it definitely can be done
  • The Helper The Helper:
    I do know this - xenforo dropped the ball by not keeping the vbulletin reputation comments as a feature. The loss of the Reputation comments data when we switched to Xenforo really was the death knell for the site when it came to all the users that left. I know I missed it so much and I got way less interested in the site when that feature was gone and I run the site.
  • Blackveiled Blackveiled:
    People love rep, lol
    +1
  • The Helper The Helper:
    The recipe today is Sloppy Joe Casserole - one of my faves LOL https://www.thehelper.net/threads/sloppy-joe-casserole-with-manwich.193585/
  • The Helper The Helper:
    Decided to put up a healthier type recipe to mix it up - Honey Garlic Shrimp Stir-Fry https://www.thehelper.net/threads/recipe-honey-garlic-shrimp-stir-fry.193595/

      The Helper Discord

      Members online

      Affiliates

      Hive Workshop NUON Dome World Editor Tutorials

      Network Sponsors

      Apex Steel Pipe - Buys and sells Steel Pipe.
      Top