• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

Polarisation - v1.3

Polarisation - v1.3

By Mr_Bean


Introduction

I saw Sage Chow's post in a spell idea workshop and I liked it a lot, so I decided to code it. Full credits to Sage Chow for the awesome idea!

Spell Description


Polarisation

btncustpolarisation.jpg

Active: Runs an electric charge through all units in the targeted area, applying either a positive or negative charge to them.
If the target has no charge or a negative charge, a positive charge will be applied. If it has a positive charge, a negavite charge will be applied.
All negatively charged units will zap a nearby positively charged enemy unit periodically.
Charges last for 90 seconds.

Level 1: 50 zap damage
Level 2: 75 zap damage
Level 3: 100 zap damage

Cooldown: 10 seconds.

Requirements

For this spell you will simply need JASS NewGen Pack [LINK] and the latest version of JASSHelper [LINK].

Code

JASS:
library Polarisation initializer onInit requires TimerUtils

    //**************************************************\\
    //*** START OF CONFIGURABLES                     ***\\
    //**************************************************\\
    
    globals
    
        // Raw ID of the "Polarisation" hero ability:
        private constant integer SPELL_ID           = 'A000'
        
        // Enable preloading to prevent in-game lag:
        private constant boolean ENABLE_PRELOAD     = true
        // How long the charge lasts on each unit:
        private constant real    DURATION           = 90.0
        // Area of effect of the spell:
        private constant real    SPELL_AOE          = 300.0
        // How often negative units zap positive units:
        private constant real    ZAP_FREQUENCY      = 1.0
        // How close a positive unit needs to be to a negative one to get zapped:
        private constant real    ZAP_AOE            = 500.0
        // How often the system updates each unit's remaining time (shouldn't need to be changed):
        private constant real    UPDATE_FREQUENCY   = 0.1
        // How long the lightning bolt lasts:
        private constant real    BOLT_DURATION      = 0.5
        // Code of the desired lightning:
        private constant string  BOLT_CODE          = "CLPB"
        // Effect created on charged untis:
        private constant string  EFFECT             = "Abilities\\Spells\\Orc\\Purge\\PurgeBuffTarget.mdl"
        // Attachment point of EFFECT:
        private constant string  EFFECT_ATTACHMENT  = "origin"
        // Effect created on units when they get zapped:
        private constant string  ZAP_EFFECT         = "Abilities\\Weapons\\Bolt\\BoltImpact.mdl"
        // Attachment point for ZAP_EFFECT:
        private constant string  ZAP_EFFECT_ATTACH  = "origin"
        // Effect created at the target point:
        private constant string  AREA_EFFECT        = "war3mapImported\\RollingStormSFX.mdx"
        // Colour of the floating text showing a +:
        private constant string  POSITIVE_COLOUR    = "|cff00ff00"
        // Colour of the floating text showing a -:
        private constant string  NEGATIVE_COLOUR    = "|cffff0000"

    endglobals
    
    // Configure how the damage per level is calculated:
    private function GetDamage takes integer level returns real
        return (25.0 * level) + 25.0
    endfunction
    
    //**************************************************\\
    //*** ADVANCED CONFIGURABLES                     ***\\
    //**************************************************\\
    
    globals
    
        private constant real    LIGHTNING_UPDATE_FREQUENCY     = 0.03      // How often lightnings are updated.
        private constant real    TEXTTAG_UPDATE_FREQUENCY       = 0.03      // How often texttags are updated.
        private constant real    TEXTTAG_HEIGHT_OFFSET          = 25.0      // Height offset of texttags.
        private constant real    TEXTTAG_HEIGHT                 = 0.035     // Height of texttags when created.
        
        private constant string  POSITIVE = "positive"  // Name for positive charge.
        private constant string  NEGATIVE = "negative"  // Name for negative charge.
        private constant key     CHARGE_KEY             // The unit's current charge (positive/negative).
        private constant key     TIME_KEY               // How long the charge has left on the unit.
        private constant key     LEVEL_KEY              // The level of the spell cast on the unit.
        private constant key     TEXT_TAG_KEY           // The unit's texttag showing it's charge.
        private constant key     EFFECT_KEY             // The unit's effect.
        private constant key     CASTER_KEY             // Who cast the spell on the unit.
        
    endglobals
    
    //**************************************************\\
    //*** END OF CONFIGURABLES                       ***\\
    //**************************************************\\
    
    globals
    
        private hashtable ht = InitHashtable()  // Stores all the data.
        private group positive = CreateGroup()  // Stores all positive units.
        private group negative = CreateGroup()  // Stores all negative units.
        private group enumG = CreateGroup()     // For group enumeration.
        private unit dummy                      // Temporary unit for dummy casting.
        private unit enumU                      // For group enumeration.
        private timer textTagTimer              // Timer that moves all units' texttags.
        private timer updateTimer               // Timer that updates all units' time remaining.
        private timer zapTimer                  // Timer that makes negatives zap positives.
        private boolean isTicking = false       // Saves whether the timers are ticking or not.
        
    endglobals
    
    //**************************************************
    
    /*
        True if unit is:
            - alive
            - not magic immune
            - not a structure
    */
    
    private function IsUnitTargetable takes unit u returns boolean
        return not IsUnitType(u, UNIT_TYPE_DEAD) and not IsUnitType(u, UNIT_TYPE_MAGIC_IMMUNE) and not IsUnitType(u, UNIT_TYPE_STRUCTURE)
    endfunction
    
    
    
    // Creates a texttag which is only visible to the specified player:
    private function NewTextTagUnitVis takes string colour, string text, unit u, player p returns texttag
        local texttag tt = CreateTextTag()
        
        call SetTextTagText(tt, colour + text + "|r", TEXTTAG_HEIGHT)
        call SetTextTagPosUnit(tt, u, TEXTTAG_HEIGHT_OFFSET)
        call SetTextTagPermanent(tt, true)
        
        if IsPlayerAlly(GetLocalPlayer(), p) then
            call SetTextTagVisibility(tt, true)
        endif
        
        return tt
    endfunction

    //**************************************************
    
    private struct Lightning
        unit u1
        unit u2
        unit caster // Used for dealing damage.
        lightning bolt
        real damage
        real left
        
        private method destroy takes nothing returns nothing
            call DestroyLightning(.bolt)
            set .bolt = null
            set .u1 = null
            set .u2 = null
            set .caster = null
            call .deallocate()
        endmethod
        
        private static method move takes nothing returns nothing
            local thistype this = GetTimerData(GetExpiredTimer())
            
            // Move the lightning and update time left:
            call MoveLightning(.bolt, false, GetUnitX(.u1), GetUnitY(.u1), GetUnitX(.u2), GetUnitY(.u2))
            set .left = .left - LIGHTNING_UPDATE_FREQUENCY
            
            // If the time has expired, damage the target and create an effect:
            if .left <= 0 then
                call ReleaseTimer(GetExpiredTimer())
                call UnitDamageTarget(.caster, .u2, .damage, false, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_NORMAL, null)
                call DestroyEffect(AddSpecialEffectTarget(ZAP_EFFECT, .u2, ZAP_EFFECT_ATTACH))
                call .destroy()
            endif
            
        endmethod
        
        public static method create takes unit u1, unit u2, unit c, real dmg returns thistype
            local thistype this = thistype.allocate()
            
            // Store stuff:
            set .u1 = u1
            set .u2 = u2
            set .caster = c
            set .damage = dmg
            set .left = BOLT_DURATION
            
            // Create lightning and start timer:
            set .bolt = AddLightning(BOLT_CODE, true, GetUnitX(.u1), GetUnitY(.u1), GetUnitX(.u2), GetUnitY(.u2))
            call TimerStart(NewTimerEx(this), LIGHTNING_UPDATE_FREQUENCY, true, function thistype.move)
            
            return this
        endmethod
    
    endstruct
    
    private function MoveTextTag takes nothing returns nothing
        // Move the texttag to the unit:
        call SetTextTagPosUnit(LoadTextTagHandle(ht, GetHandleId(GetEnumUnit()), TEXT_TAG_KEY), GetEnumUnit(), TEXTTAG_HEIGHT_OFFSET)
    endfunction
    
    private function DisplayCharge takes nothing returns nothing
        // If there are no units in either group, pause all timers:
        if CountUnitsInGroup(positive) == 0 and CountUnitsInGroup(negative) == 0 then
        
            set isTicking = false
            call PauseTimer(textTagTimer)
            call PauseTimer(updateTimer)
            call PauseTimer(zapTimer)
        
        // Otherwise, move all the units' texttags:
        else
            call ForGroup(positive, function MoveTextTag)
            call ForGroup(negative, function MoveTextTag)
        endif
        
    endfunction
    
    private function ZapPositivesEnum takes nothing returns nothing
        local unit u = GetEnumUnit()
        local unit temp
        local boolean stop = false
        
        // Group all unit near the target:
        call GroupEnumUnitsInRange(enumG, GetUnitX(u), GetUnitY(u), ZAP_AOE, null)
        
        loop
            set temp = GroupPickRandomUnit(enumG) // Pick a random unit.
            exitwhen temp == null or stop // Stop searching if the unit is alive and positive.
            
            if not IsUnitType(temp, UNIT_TYPE_DEAD) and LoadStr(ht, GetHandleId(temp), CHARGE_KEY) == POSITIVE then
                set stop = true
                call Lightning.create(u, temp, LoadUnitHandle(ht, GetHandleId(temp), CASTER_KEY), GetDamage(LoadInteger(ht, GetHandleId(temp), LEVEL_KEY))) // Create a lightning (also damages).
            endif
            
            call GroupRemoveUnit(enumG, temp)
            
        endloop
        
        call GroupClear(enumG)
        set u = null
        set temp = null
    endfunction
    
    private function ZapPositives takes nothing returns nothing
        // For all positive units, zap nearby negatives:
        call ForGroup(negative, function ZapPositivesEnum)
    endfunction
    
    private function CheckDuration takes nothing returns nothing
        local unit u = GetEnumUnit()
        local integer handleId = GetHandleId(u)
        local real time = LoadReal(ht, handleId, TIME_KEY)
        local string status = LoadStr(ht, handleId, CHARGE_KEY)
        
        set time = time - UPDATE_FREQUENCY
        
        // If the unit's charge expires or it is dead:
        if time <= 0 or IsUnitType(u, UNIT_TYPE_DEAD) then
            
            // Remove from groups:
            if status == POSITIVE then
                call GroupRemoveUnit(positive, u)
            else
                call GroupRemoveUnit(negative, u)
            endif
            
            call DestroyTextTag(LoadTextTagHandle(ht, handleId, TEXT_TAG_KEY)) // Destroy texttag.
            call DestroyEffect(LoadEffectHandle(ht, handleId, EFFECT_KEY)) // Destroy effect.
            call FlushChildHashtable(ht, handleId) // Flush hashtable.
        
        // Otherwise, update it's time left:
        else
            call SaveReal(ht, handleId, TIME_KEY, time)
        endif
        
        set u = null
    endfunction
    
    private function CheckGroups takes nothing returns nothing
        // For positives and negatives, check their remaining durations and whether each unit is alive:
        call ForGroup(positive, function CheckDuration)
        call ForGroup(negative, function CheckDuration)
    endfunction
    
    private function RegisterUnit takes unit u, unit c, integer level, player p returns nothing
        local integer handleId = GetHandleId(u)
        local string status = LoadStr(ht, handleId, CHARGE_KEY)
        local texttag tt = LoadTextTagHandle(ht, handleId, TEXT_TAG_KEY)
        local effect eff = LoadEffectHandle(ht, handleId, EFFECT_KEY)
        
        // If the unit already has a texttag, destroy it:
        if tt != null then
            call DestroyTextTag(tt)
            set tt = null
        endif
        
        // If the unit already has an effect, destroy it:
        if eff != null then
            call DestroyEffect(eff)
            set eff = null
        endif
        
        call SaveReal(ht, handleId, TIME_KEY, DURATION) // Save duration.
        call SaveInteger(ht, handleId, LEVEL_KEY, level) // Save ability level.
        call SaveEffectHandle(ht, handleId, EFFECT_KEY, AddSpecialEffectTarget(EFFECT, u, EFFECT_ATTACHMENT)) // Create and save effect.
        call SaveUnitHandle(ht, handleId, CASTER_KEY, c) // Save caster (for dealing damage).
        
        // If the unit has a positive charge:
        if status == POSITIVE then
        
            call SaveStr(ht, handleId, CHARGE_KEY, NEGATIVE) // Change it to negative.
            call GroupRemoveUnit(positive, u) // Remove from positives.
            call GroupAddUnit(negative, u) // Add to negatives.
            call SaveTextTagHandle(ht, handleId, TEXT_TAG_KEY, NewTextTagUnitVis(NEGATIVE_COLOUR, "-", u, p)) // Create a texttag and save it.
            
        // Otherwise:
        else
        
            call SaveStr(ht, handleId, CHARGE_KEY, POSITIVE) // Change it to positive.
            
            if IsUnitInGroup(u, negative) then // Remove from negatives if it is in the group.
                call GroupRemoveUnit(negative, u)
            endif
            
            call GroupAddUnit(positive, u) // Add to positives.
            call SaveTextTagHandle(ht, handleId, TEXT_TAG_KEY, NewTextTagUnitVis(POSITIVE_COLOUR, "+", u, p)) // Create a texttag and save it.
            
        endif
        
    endfunction
    
    private function SpellActions takes nothing returns nothing
        local unit caster = GetTriggerUnit()
        local player owner = GetTriggerPlayer()
        local integer level = GetUnitAbilityLevel(caster, SPELL_ID)
        
        call DestroyEffect(AddSpecialEffect(AREA_EFFECT, GetSpellTargetX(), GetSpellTargetY()))
        // Group all units within range of the point:
        call GroupEnumUnitsInRange(enumG, GetSpellTargetX(), GetSpellTargetY(), SPELL_AOE, null)
        
        for enumU in enumG
        
            if not IsUnitType(enumU, UNIT_TYPE_DEAD) and IsUnitTargetable(enumU) then
            
                // Register the target in the 'system':
                call RegisterUnit(enumU, caster, level, owner)
                
                // Start the timers if they aren't already going:
                if not isTicking then
                
                    set isTicking = true
                    call TimerStart(textTagTimer, TEXTTAG_UPDATE_FREQUENCY, true, function DisplayCharge)
                    call TimerStart(updateTimer, UPDATE_FREQUENCY, true, function CheckGroups)
                    call TimerStart(zapTimer, ZAP_FREQUENCY, true, function ZapPositives)
                    
                endif
                
            endif
        
        endfor
        
        set caster = null
        set owner = null
    endfunction
    
    private function CheckSpell takes nothing returns boolean
        if GetSpellAbilityId() == SPELL_ID then
            call SpellActions()
        endif
        
        return false
    endfunction
    
    private function onInit takes nothing returns nothing
        local trigger t = CreateTrigger()
        
        // Preloading:
        static if ENABLE_PRELOAD then
        
            local unit u = CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), 'Hpal', 0, 0, 0)
            call UnitAddAbility(u, SPELL_ID)
            call RemoveUnit(u)
            call Preload(ZAP_EFFECT)
            call Preload(EFFECT)
            set u = null
            
        endif
        
        // Timers:
        set textTagTimer = NewTimer()
        set zapTimer = NewTimer()
        set updateTimer = NewTimer()
        
        // Register the spell:
        call TriggerAddCondition(t, Condition(function CheckSpell))
        call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_EFFECT)
        set t = null
    endfunction

endlibrary

Changelogs

- All texttags are now updated by one timer instead of a periodic trigger.
- The zapping of positive units by negative units is now handled by one timer.
- Each affected unit's time remaining is now updated by a single timer.
- Added an effect at the target point.
- Made the texttag offset, texttag height and update frequency configurable (advanced users).
- Other minor code improvements.

- Fixed it so you can customise the spell AoE.
- Coded my own lightning so you can change what the lightning looks like, how long it lasts and the effect it creates when a unit is zapped. Thanks to KnnO.
- Because of the above change, the dummy unit and ability are no longer needed. Instead TimerUtils is needed.
- The creeps in the demo map are now owned by Neutral Hostile and not by Player 1 :p
- If you enable preloading, the effects are now preloaded too.

- Added a new preview image.
- Reduced mana cost of the spell in the demo map.
- Added a cooldown for the spell in the demo map.
- You can now configure the colour of the floating text that shows a - or + above affected units.

- Initial release

Credits

- Sage Chow for the spell idea.

Contact

If you find any bugs in the system or if something is unclear in the documentation, please let me know!
- Email
- Private Message

Keywords:
electricity, polarize, polarise, mr_bean, sage chow
Contents

Polarisation (Map)

Reviews
00:22, 17th Jul 2012 Magtheridon96: Very original, solid, well-written and readable. Approved. Instead of using those BJs to get the number of units in each group, it would be better to just keep track of the counts yourself. (Using integer...

Moderator

M

Moderator

00:22, 17th Jul 2012
Magtheridon96: Very original, solid, well-written and readable. Approved.

  • Instead of using those BJs to get the number of units in each
    group, it would be better to just keep track of the counts yourself.
    (Using integer variables)
  • JASS:
    if IsPlayerAlly(GetLocalPlayer(), p) then
        call SetTextTagVisibility(tt, true)
    endif
    This can be shortened to one line. I'll let you figure out how on your own ;)
  • It's completely fine that you're using those ForGroup calls, but
    there are faster, more efficient ways to handle these kinds of things.
    A linked list would make it much faster. I'm still totally fine with this.
    I will approve it with the current method. ;)
  • It would be better to cache things into local variables instead of
    recalling them. For example, in your ZapPositivesEnum function, it would
    be much better if you were to cache GetHandleId(temp) into a local
    variable.

I won't rate it now, but if changes are done, it will get a solid 4.3/5.
(Because I really don't want to give this baby a "Useful - 3" rating)

Excellent work.
 
Level 17
Joined
Feb 11, 2011
Messages
1,860
Update:
- Added a new preview image.
- Reduced mana cost of the spell in the demo map.
- Added a cooldown for the spell in the demo map.
- You can now configure the colour of the floating text that shows a - or + above affected units.
 
Level 17
Joined
Feb 11, 2011
Messages
1,860
Thanks for the feedback! I designed it so that you charge any unit, provided they are alive and not magic immune or a structure. I think I will code the lightning, just didn't feel like it at the time :p
I will also make the area of effect configurable. It is currently hard-coded at 300.

Also, I was wondering about GroupPickRandomUnit. Is this safe to use? I looked at it and it didn't look bad...
 
Level 17
Joined
Feb 11, 2011
Messages
1,860
Update: (v1.2)
- Fixed it so you can customise the spell AoE.
- Coded my own lightning so you can change what the lightning looks like, how long it lasts and the effect it creates when a unit is zapped. Thanks to KnnO.
- Because of the above change, the dummy unit and ability are no longer needed. Instead TimerUtils is needed.
- The creeps in the demo map are now owned by Neutral Hostile and not by Player 1 :p
- If you enable preloading, the effects are now preloaded too.
 
Level 14
Joined
Nov 18, 2007
Messages
816
Heres whats wrong in my opinion:
  • StringHash() instead of keys
  • Periodic triggers instead of periodic timers
  • Making texttags visible asynchronously instead of creating them asynchronously
  • Hashtable instead of simply using a unit indexer
  • Using a periodic timer per Lightning instance instead of a single timer for all, or any other similar solution
  • Using your own temporary group instead of using the one provided by GroupUtils
  • Myriads of hardcoded values that are either nondescriptive or prone to change.
 
Level 17
Joined
Feb 11, 2011
Messages
1,860
What's right in your opinion? No nice words about the spell?

Anyway;
- I will replace StringHash with key.
- The "myriads" of hard-coded values are the timer durations (0.03) and texttag height and offset. Is it really necessary to make these configurable?
- I don't use GroupUtils...is it really needed to use it?
- I will look into making your other improvements...
 
Level 14
Joined
Nov 18, 2007
Messages
816
- I will replace StringHash with key.
Which should be plan B after trying to use a unit indexer.

- The "myriads" of hard-coded values are the timer durations (0.03) and texttag height and offset. Is it really necessary to make these configurable?
Yes, if for nothing else than readability. Ive actually had to adjust the frequency of execution for some spells to decrease workload.

- I don't use GroupUtils...is it really needed to use it?
Well, you dont create groups dynamically, so not every part of GroupUtils is needed. But GroupUnitsInArea could be used, as well as the ENUM_GROUP GroupUtils provides (which is what i was referring to).

2 - HT is OK even if UI is faster
Of course its "OK". If the standard for acceptance is "Works", then many things are "OK". But IMO, "Works" is not enough. An elegant, efficient solution (no, dont go over the top with optimizations) is needed.

3 - GroupUtils is slow, uses 2 hashtables plus he's looping by EnumUnit in the group, not in area
Yes, but GroupUtils is an external library that can be used by multiple spells/libraries so the cost of it is shared. Also, GroupUnitsInArea is exactly what he would want to use for this spell.
And how is GroupUtils slow if all youre using is the ENUM_GROUP variable? Besides this utterly ridiculous claim, whats wrong with being a bit slower? Theres a tradeoff between API elegance and implementation efficiency that you have to make. In almost all cases it should be decided in favor of API elegance. Theres a huge trend to optimize EVERYTHING to maximum extent possible. With the result that noone can read the libraries because they look like shit due to all the hacks that have to be employed to get the intended result.
Really, write code that is easy to maintain. If its a tad bit slower than a more efficient solution that is much harder to understand, so what?
 
Level 17
Joined
Feb 11, 2011
Messages
1,860
Update:
Finally uploaded version 1.3, which includes improvements mentioned by Deaod:
- All texttags are now updated by one timer instead of a periodic trigger.
- The zapping of positive units by negative units is now handled by one timer.
- Each affected unit's time remaining is now updated by a single timer.
- Added an effect at the target point.
- Made the texttag offset, texttag height and update frequency configurable (advanced users).
- Other minor code improvements.
 
Top