• 🏆 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!

[Spell] Spell that creates death marks [vJass]

Status
Not open for further replies.
Level 7
Joined
Feb 9, 2021
Messages
301
I have been trying to create a spell, but I encountered a problem with transferring data and with the damage system, so I thought someone might help me.
For the spell I use the following libraries:

Description: on cast, the spell selects an enemy unit in front of the caster, prioritising the closest hero unit. If the target's hp is below a certain %, after a delay, the caster will leave a stamp from the attacking angle on the target. Otherwise, the spell will cause 0 damage and will display "miss". Overall, there are 6 possible angles to leave a stamp. The effect of the stamp moves with the target, attached to a certain angle. On the second stamp at the same angle, the caster instakills the target (if the hp is still below a certain %, otherwise, the attack misses again).

I encountered two problems:
(1) I can't transfer the stamp's integer along with the id of the class to the timer
(2) OnDamageAfter does not work for me for some reason. I want to use it to show "miss", hide the target and show a death effect

Any other suggestions are welcome.

Here is my code:

JASS:
library Suzumebachi /*


     [I]/ uses /[/I]

     [I]/ SpellFramework /[/I]
     */ BaseFunctions


     /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
     * SPELL CONFIGURATION *
     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
    private module SpellConfiguration

        static constant integer SPELL_ABILITY_ID    = 'A00G'
        static constant integer SPELL_EVENT_TYPE    = EVENT_SPELL_EFFECT
        static constant real SFX_DEATH_TIME         = 1.50
        static constant integer STACKS_GAIN_ON_CD   = 1
        static constant real SPELL_RANGE            = 64
        static constant real SPELL_AOE              = 300
        static constant attacktype ATTACK_TYPE      = ATTACK_TYPE_MAGIC
        static constant damagetype DAMAGE_TYPE      = DAMAGE_TYPE_MAGIC
        static constant weapontype WEAPON_TYPE      = WEAPON_TYPE_WHOKNOWS
      
        //Suz
        static constant string SUZ_EFFECT_ID        = "Abilities\\Weapons\\LordofFlameMissile\\LordofFlameMissile.mdl"
        static constant string SUZ_DEATH_EFFECT_ID  = "Abilities\\Spells\\Undead\\AnimateDead\\AnimateDeadTarget.mdl"
        static constant real SUZ_DELAY              = 0.5
        static constant real SUZ_EFFECT_DIST        = 100
        static constant real SUZ_EFFECT_DEF_SCALE   = 1.0
        static constant real SUZ_EFFECT_KILL_SCALE  = 2.0
        static constant real SUZ_Z                  = 100
      
        //Rangle of angles per side
        static constant real ANGLE_FRONT            = 210.0
        static constant real ANGLE_SIDE_1           = 150.0
        static constant real ANGLE_SIDE_2           = 90.0
        static constant real ANGLE_BACK             = 30.0
        static constant real ANGLE_SIDE_3           = 330.0
        static constant real ANGLE_SIDE_4           = 270.0
      
        static constant integer INT_ANGLE_FRONT     = 1
        static constant integer INT_ANGLE_SIDE_1    = 2
        static constant integer INT_ANGLE_SIDE_2    = 3
        static constant integer INT_ANGLE_BACK      = 4
        static constant integer INT_ANGLE_SIDE_3    = 5
        static constant integer INT_ANGLE_SIDE_4    = 6
      
      
      
        static method Dmg takes integer level, unit caster returns real
            return 50. + 0. * level + 0.1 * GetHeroStr(caster, true)
        endmethod
      
        static method szbDuration takes integer level returns real
            return 5.0 + 0.0 * level
        endmethod
      
        static method FrontMaxHp takes unit caster returns real
            return 50 + 0.1 * GetHeroStr(caster, true)
        endmethod
      
        static method SideMaxHp takes unit caster returns real
            return 0.10 + 0.1 * GetHeroStr(caster, true)
        endmethod
      
        static method BackMaxHp takes unit caster returns real
            return 0.25 + 0.1 * GetHeroStr(caster, true)
        endmethod
      
        static method StackCdTime takes integer level returns integer
            return 3
        endmethod
      
        static method MaxStacks takes integer level returns integer
            return 3 + level
        endmethod
      
      
      
        static method TargetsFilter takes unit target, unit caster returns boolean
            return GetWidgetLife(target) > 0.405 and /*
             [I]/ IsUnitEnemy(target, GetOwningPlayer(caster)) and /[/I]
             [I]/ (IsUnitVisible(target, GetOwningPlayer(caster))) and /[/I]
             [I]/ not IsUnitType(target, UNIT_TYPE_STRUCTURE) and /[/I]
             */ GetUnitAbilityLevel(GetFilterUnit(), 'Aloc') == 0
        endmethod

    endmodule

  

     /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
     * END OF SPELL CONFIGURATION *
     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

     /* == == == == == == == == == == == == = SPELL CODE == == == == == == == == == == == == = */
  
  
    //Buff
     struct SuzBuff extends Buff
        private static constant integer RAWCODE         = 'A016'
        private static constant integer DISPEL_TYPE     = BUFF_NONE
        private static constant integer STACK_TYPE      = BUFF_STACK_NONE
      
        implement SpellConfiguration
        private timer timer
        private boolean array scaleChange[6]
        private integer targetId
        integer suzNum
        static HashTable suzStacks
        static HashTable suzEffect
        //static HashTable suzDelayTimer
      
      
        method effectMove takes integer suzNum returns nothing
            local real tX = GetUnitX(this.target)
            local real tY = GetUnitY(this.target)
            local real effectAngle360 = GetUnitFacing(this.target) - 60 * (suzNum - 1)
            local real moveX = GetXWithOffset(tX, SUZ_EFFECT_DIST, effectAngle360)
            local real moveY = GetYWithOffset(tY, SUZ_EFFECT_DIST, effectAngle360)
            local real tPercentHp = GetUnitStatePercentEx(this.target, UNIT_STATE_LIFE, UNIT_STATE_MAX_LIFE)
          
            call SetEffectX(thistype.suzEffect[targetId].effect[suzNum], moveX)
            call SetEffectY(thistype.suzEffect[targetId].effect[suzNum], moveY)
            call SetEffectFacing(thistype.suzEffect[targetId].effect[suzNum], effectAngle360)
          
          
            if (tPercentHp > thistype.suzStacks[targetId].real[suzNum] and this.scaleChange[suzNum]) then
                call SetEffectScale(thistype.suzEffect[targetId].effect[suzNum], SUZ_EFFECT_DEF_SCALE)
                set this.scaleChange[suzNum] = false
                return
            endif
          
            if (tPercentHp <= thistype.suzStacks[targetId].real[suzNum] and not this.scaleChange[suzNum]) then
                call SetEffectScale(thistype.suzEffect[targetId].effect[suzNum], SUZ_EFFECT_KILL_SCALE)
                set this.scaleChange[suzNum] = true
                return
            endif
          
        endmethod
      
        static method onPeriod takes nothing returns nothing
            local thistype this = GetTimerData(GetExpiredTimer())
            local integer tempInt = 1
           
            loop
            exitwhen tempInt > 6

            if this.suzStacks[this.targetId].boolean[tempInt] then
                call effectMove(tempInt)
            endif
            set tempInt = tempInt + 1
            endloop
      
        endmethod
      
        static method onStampEnd takes nothing returns nothing
            local thistype this = GetTimerData(GetExpiredTimer())
            local integer targetId = UnitIndex[this.target]
          
            call DestroyEffect(thistype.suzEffect[targetId].effect[this.suzNum])
            set thistype.suzStacks[targetId].boolean[this.suzNum] = false

            call ReleaseTimer(thistype.suzStacks[targetId].timer[this.suzNum])
            set thistype.suzStacks[targetId].timer[this.suzNum] = null
            set thistype.suzEffect[targetId].effect[this.suzNum] = null
        endmethod
      
       
        static method onExpire takes nothing returns nothing
            local thistype this = GetTimerData(GetExpiredTimer())
            local real tX
            local real tY
            local real effectAngle360
            local real moveX
            local real moveY
            local integer targetId
          
            if GetWidgetLife(this.target) < 0.405 then
                call ReleaseTimer(GetExpiredTimer())
                return
            endif
          
            set tX = GetUnitX(this.target)
            set tY = GetUnitY(this.target)
            set effectAngle360 = GetUnitFacing(this.target) - 60 * (this.suzNum - 1)
            set moveX = GetXWithOffset(tX, SUZ_EFFECT_DIST, effectAngle360)
            set moveY = GetYWithOffset(tY, SUZ_EFFECT_DIST, effectAngle360)
            set targetId = UnitIndex[this.target]
          
          
            set thistype.suzStacks[targetId].boolean[this.suzNum] = true
          
            set thistype.suzEffect[targetId].effect[this.suzNum] = AddSpecialEffect(SUZ_EFFECT_ID, moveX, moveY)
            call SetEffectFacing(thistype.suzEffect[targetId].effect[this.suzNum], effectAngle360)
            call SetEffectScale(thistype.suzEffect[targetId].effect[this.suzNum], SUZ_EFFECT_KILL_SCALE)
            call SetEffectZ(thistype.suzEffect[targetId].effect[this.suzNum], SUZ_Z)
            set this.scaleChange[this.suzNum] = true
          
            set thistype.suzStacks[targetId].timer[this.suzNum] = NewTimerEx(this)

            call TimerStart(thistype.suzStacks[targetId].timer[this.suzNum], (this.duration - SUZ_DELAY), false, function thistype.onStampEnd)
         
           // debug call BJDebugMsg("[On Expire] this: " + I2S(this))
          
          
            if this.timer == null then
                set this.timer = NewTimerEx(this)
                call TimerStart(this.timer, FPS, true, function thistype.onPeriod)
            endif
          
            call ReleaseTimer(GetExpiredTimer())
          
        endmethod
      
        method onRemove takes nothing returns nothing
            //local integer sourceId = GetUnitId(source)
            //ocal integer targetId = GetUnitId(target)
            //call DestroyEffect(this.suzEffect)
            //set SuzBuff.suzStacks[INT_ANGLE_FRONT].boolean[targetId] = true
            local integer tempInt = 1
          
            loop
            exitwhen tempInt > 6

            if this.suzStacks[this.targetId].boolean[tempInt] then
                if GetWidgetLife(this.target) < 0.405 and thistype.suzStacks[this.targetId].timer[this.suzNum] != null then
                    call DestroyEffect(thistype.suzEffect[this.targetId].effect[this.suzNum])
                    call ReleaseTimer(thistype.suzStacks[this.targetId].timer[this.suzNum])
                  
                    set thistype.suzStacks[this.targetId].timer[this.suzNum] = null
                    set thistype.suzEffect[this.targetId].effect[this.suzNum] = null
               endif
            endif
            set tempInt = tempInt + 1
            endloop
          
            call ReleaseTimer(this.timer)
            call thistype.suzStacks.remove(targetId)
            call thistype.suzEffect.remove(targetId)
          
          
            set this.timer = null
        endmethod
    
        method onApply takes nothing returns nothing
            set this.targetId = UnitIndex[this.target]
            call TimerStart(NewTimerEx(this), SUZ_DELAY, false, function thistype.onExpire)
        endmethod
      
        static method onInit takes nothing returns nothing
            set thistype.suzStacks = HashTable.create()
            set thistype.suzEffect = HashTable.create()
        endmethod
    
        implement BuffApply
    endstruct

  
    //Spell
  
    globals
        private group eG = CreateGroup()
        private group hG = CreateGroup()
        private group nonHG = CreateGroup()
    endglobals

    private struct Suzumebachi extends array

        implement SpellConfiguration

        private integer stacks
        private integer maxStacks
        private boolean isStackTimerOn
        private timer t
        private boolean hit
        
        private static method onSpellStart takes nothing returns thistype
            local unit closestUnit
            local real cX
            local real cY
            local real targetX
            local real targetY
            local real angle
            local unit u
            local thistype this = GetUnitId(Spell.triggerUnit)
            local real dmg

            if (this.stacks == 0) then
                debug call BJDebugMsg("[Suzumebachi] Not Enough Stacks. Stacks: " + I2S(this.stacks) )
                return 0
            endif
          
            set this.stacks = this.stacks - 1
            call SetCustomUnitAbilityCharges(Spell.triggerUnit, SPELL_ABILITY_ID, this.stacks)
            debug call BJDebugMsg("[Suzumebachi] Number of Stacks Left " + I2S(this.stacks))

            if not this.isStackTimerOn then
                set this.t = NewTimerEx(this)
                call TimerStart(this.t, StackCdTime(Spell.level), true, function thistype.StacksCd)
                set this.isStackTimerOn = true
            endif
           
            set cX = GetUnitX(Spell.triggerUnit)
            set cY = GetUnitY(Spell.triggerUnit)
            set angle = GetAngle(cX, Spell.targetX, cY, Spell.targetY)
            set targetX = GetXWithOffset(cX, SPELL_RANGE, angle)
            set targetY = GetYWithOffset(cY, SPELL_RANGE, angle)
            set dmg = Dmg(Spell.level, Spell.triggerUnit)
           
          
          
            call GroupEnumUnitsInRange(eG, targetX, targetY, SPELL_AOE, null)
            loop
                set u = FirstOfGroup(eG)
                exitwhen u == null
                if (TargetsFilter(u, Spell.triggerUnit) and IsUnitType(u, UNIT_TYPE_HERO)) then
                    call GroupAddUnit(hG, u)
                elseif (TargetsFilter(u, Spell.triggerUnit) and not IsUnitType(u, UNIT_TYPE_HERO)) then
                    call GroupAddUnit(nonHG, u)
                endif
                call GroupRemoveUnit(eG, u)
            endloop
            call GroupClear(eG)
                  
            if GetGroupUnitCount(hG) > 0 then
                set closestUnit = GetClosestUnitInGroup(targetX, targetY, hG)
                call thistype[UnitIndex[closestUnit]].enableDamageEventLocal()
                call UnitDamageTarget(Spell.triggerUnit, closestUnit, dmg, false, false, ATTACK_TYPE, DAMAGE_TYPE, WEAPON_TYPE )
                debug call BJDebugMsg("[Suzumebachi] A hero got damaged")
                set closestUnit = null
                  
            elseif GetGroupUnitCount(nonHG) > 0 then
                set closestUnit = GetClosestUnitInGroup(targetX, targetY, nonHG)
                call thistype[UnitIndex[closestUnit]].enableDamageEventLocal()
                call UnitDamageTarget(Spell.triggerUnit, closestUnit, dmg, false, false, ATTACK_TYPE, DAMAGE_TYPE, WEAPON_TYPE )
                debug call BJDebugMsg("[Suzumebachi] Non hero unit got damaged")
                set closestUnit = null
            endif
                  
            call GroupClear(hG)
            call GroupClear(nonHG)
           
          
            return this
        endmethod

        private method onSpellEnd takes nothing returns nothing
          
            if this.stacks == this.maxStacks then
                set this.isStackTimerOn = false
                call ReleaseTimer(this.t)
                set this.t = null
            elseif this.stacks == 0 then
                call StartUnitAbilityCooldown(GetUnitById(this), SPELL_ABILITY_ID, TimerGetRemaining(this.t))
            endif
          
        endmethod

        implement SpellEventEx
      
         private static method onDamage takes nothing returns nothing
            local real cFacing = GetUnitFacing(source)
            local real tFacing = GetUnitFacing(target)
            local real angleDiff = AngleDiff(cFacing, tFacing)
            local integer spellLevel = GetUnitAbilityLevel(source, SPELL_ABILITY_ID)
            local real tPercentHp = GetUnitStatePercentEx(target, UNIT_STATE_LIFE, UNIT_STATE_MAX_LIFE)
            local real hpShotPercent
            local SuzBuff szb
           
            set thistype[sourceId].hit = false
            debug call BJDebugMsg("[Suzumebachi] AngleDiff: " + R2S(angleDiff))
          
            //Front Stamp
            if (angleDiff <= ANGLE_FRONT and angleDiff > ANGLE_SIDE_1) then
                set hpShotPercent = FrontMaxHp(source)
                debug call BJDebugMsg("[Suzumebachi] hpShotPerecent: " + R2S(hpShotPercent))
                debug call BJDebugMsg("[Suzumebachi] tPercentHp: " + R2S(tPercentHp))
                if tPercentHp > hpShotPercent then
                    set damage = 0
                    return
                endif

                if not SuzBuff.suzStacks[targetId].boolean[INT_ANGLE_FRONT] then
                     debug call BJDebugMsg("[Suzumebachi] Buff Aplied ")
                    set szb = SuzBuff.add(source, target)
                    set szb.duration = szbDuration(spellLevel) + SUZ_DELAY
                    set szb.suzNum = INT_ANGLE_FRONT
                    set szb.suzStacks[targetId].real[INT_ANGLE_FRONT] = hpShotPercent
                    set thistype[sourceId].hit = true
                  
                    return
                endif
              
                set thistype[sourceId].hit = true
                set damage = 999999
              
              
                return
            endif
          
            if (angleDiff <= ANGLE_SIDE_1 and angleDiff > ANGLE_SIDE_2) then
                set hpShotPercent = FrontMaxHp(source)
                debug call BJDebugMsg("[Suzumebachi] hpShotPerecent: " + R2S(hpShotPercent))
                debug call BJDebugMsg("[Suzumebachi] tPercentHp: " + R2S(tPercentHp))
                if tPercentHp > hpShotPercent then
                    set damage = 0
                    return
                endif

                if not SuzBuff.suzStacks[targetId].boolean[INT_ANGLE_FRONT] then
                     debug call BJDebugMsg("[Suzumebachi] Buff Aplied ")
                    set szb = SuzBuff.add(source, target)
                    set szb.duration = szbDuration(spellLevel) + SUZ_DELAY
                    set szb.suzNum = INT_ANGLE_SIDE_1
                    set szb.suzStacks[targetId].real[INT_ANGLE_SIDE_1] = hpShotPercent
                    set thistype[sourceId].hit = true
                  
                    return
                endif
              
                set thistype[sourceId].hit = true
                set damage = 999999
              
              
                return
            endif
          
            //call thistype[sourceId].disableDamageEventLocalOutgoing()
        endmethod
      
        private static method onDamageAfter takes nothing returns nothing
            local thistype this = sourceId
            local texttag textTag
            local string toDisplay
            local real tX
            local real tY
          
            debug call BJDebugMsg("On Damage After WORKS ALOHA")
            /*
            if (disableDamageEventLocal()) then
                return
            endif
          
            set tX = GetUnitX(target)
            set tY = GetUnitY(target)
          
          
            if this.hit then
                call ShowUnit(target, true)
                call DestroyEffect(AddSpecialEffect(SUZ_DEATH_EFFECT_ID, tX, tY))
                return
            endif
          
            set textTag = CreateTextTag()
            set toDisplay = "missed"
      
            call SetTextTagPos(textTag, tX - 32, tY, 16)
            call SetTextTagText(textTag, toDisplay, 10*.0023)
            call SetTextTagVisibility(textTag, true)
            call SetTextTagPermanent(textTag, false)
            call SetTextTagVelocity(textTag, 0, .03)
            call SetTextTagLifespan(textTag, 3)
          
            set textTag = null
            set toDisplay = null
          
            call thistype[sourceId].enableDamageEventLocalOutgoing()
            */
        endmethod

        implement DDS
      
      
        //Stacks Mechanics
        static method StacksCd takes nothing returns nothing
            local thistype this = GetTimerData(GetExpiredTimer())
            if this.stacks < this.maxStacks then
                set this.stacks = this.stacks + STACKS_GAIN_ON_CD
                call SetCustomUnitAbilityCharges(GetUnitById(this), SPELL_ABILITY_ID, this.stacks)
                debug call BJDebugMsg("[Suzumebachi] CD Finished: 1 Shunpo Stack Added")
            else
                debug call BJDebugMsg("[Suzumebachi] CD Finished: 1 Shunpo Stack Added")
                set this.isStackTimerOn = false
                call ReleaseTimer(this.t)
                set this.t = null
            endif
        endmethod
      
        private static method GiveShunpoStacks takes nothing returns nothing
            local unit learner
            local thistype this
              
            if GetLearnedSkill() == SPELL_ABILITY_ID then
                set learner = GetTriggerUnit()
                set this = GetUnitId(learner)
                set this.maxStacks = MaxStacks(GetUnitAbilityLevel(learner, SPELL_ABILITY_ID))
              
                if GetUnitAbilityLevel(learner, SPELL_ABILITY_ID) == 1 then
                    call SetUnitAbilityCastpoint(learner, SPELL_ABILITY_ID, 0.3)
                    call InitCustomAbilityAddressChargesHook( GetUnitAbility(learner, SPELL_ABILITY_ID))
                    call EnableCustomUnitAbilityCharges(learner, SPELL_ABILITY_ID)
                    debug call BJDebugMsg("[Suzumebachi] Setup Finished")
                    set this.stacks = this.maxStacks
                else
                    set this.stacks = this.stacks + 1
                endif
                  
                call SetCustomUnitAbilityCharges(learner, SPELL_ABILITY_ID, this.stacks)
                debug call BJDebugMsg("[Suzumebachi] Stacks " + I2S(this.stacks))
                debug call BJDebugMsg("[Suzumebachi] Max Stacks " + I2S(this.maxStacks))

            endif
              
        endmethod
     
        private static method onInit takes nothing returns nothing

            call RegisterAnyPlayerUnitEvent(EVENT_PLAYER_HERO_SKILL, function thistype.GiveShunpoStacks)

        endmethod
  
    endstruct


endlibrary
 
Level 9
Joined
Mar 26, 2017
Messages
376
If you want the stamp to be dispellable, I reckon the best way to go is to create a Cast trigger with the appropriate logic. In here, a dummy unit applies one of four buff variants to the target. (based on the HP and angle at the moment of casting).
In this logic you can also apply a check if the unit already has a specific buff, in which case it will be instakilled.

Best way to show the effect would be to go to Magos Model Editor and create different versions of the effect with the appropriate offset. That way you don't need a system that continuously updates a seperate effect coordinates. And you get the appropriate behaviour if the buff is dispelled or the unit dies.
 
Level 7
Joined
Feb 9, 2021
Messages
301
If you want the stamp to be dispellable, I reckon the best way to go is to create a Cast trigger with the appropriate logic. In here, a dummy unit applies one of four buff variants to the target. (based on the HP and angle at the moment of casting).
In this logic you can also apply a check if the unit already has a specific buff, in which case it will be instakilled.

Best way to show the effect would be to go to Magos Model Editor and create different versions of the effect with the appropriate offset. That way you don't need a system that continuously updates a seperate effect coordinates. And you get the appropriate behaviour if the buff is dispelled or the unit dies.
Is there a way to transfer the data the way I did it? I want to do it through one buff. Another option I see is to make the buff stack full (which applies each time independent buff). However, it will be a similar problem with transferring boolean from one struct to another, which indicates whether the stamp was applied from the attacking side or not.
 
Status
Not open for further replies.
Top