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

Modular Mouse Spells v1.2

Modular Mouse Spells v1.2


I thought it would be fun to make a couple spells using the recently added mouse-move events, and then stumbled upon a snippet by MyPad to easily track the cursor position. Something that's always bothered me about spellmaking is how often behavior is hardcoded into the ability itself and unable to be easily modified, so I also gave these spells a spellBehavior module. In theory, users with sufficient JASS knowledge (or without but with sufficient gumption and google-fu) can easily modify these spells to function exactly as they envision them.

naphtha.gif
MoonerangLaunch a boulder at target area. Upon reaching the area the boulder ignites and will track the mouse cursor for a short duration. Units hit by the initial shot are stunned, and those hit by the tracking shot have their movement and attack speeds slowed. Each impact deals damage, and units can only be affected by each phase once.
clusterrockets.gif
Point DefenseSpawn defense drones that encircle the caster. Drones launch themselves to impact at the mouse cursor location periodically. Each drone can travel a maximum distance, and those closest to the target are launched first. Enemies in the impact radius take damage per drone and are briefly blinded, causing them to miss most of their attacks.
These spells have 3 dependency libraries:
Can be found here: [vJASS] - [Snippet] Mouse Utility
JASS:
library MouseUtils
/*
    -------------------
        MouseUtils
         - MyPad
     
         1.0.2.2
    -------------------
 
    ----------------------------------------------------------------------------
        A simple snippet that allows one to
        conveniently use the mouse natives
        as they were meant to be...
    
     -------------------
    |    API            |
     -------------------
 
        struct UserMouse extends array
            static method operator [] (player p) -> thistype
                - Returns the player's id + 1
            
            static method getCurEventType() -> integer
                - Returns the custom event that got executed.
            
            method operator player -> player
                - Returns Player(this - 1)
            
            readonly real mouseX
            readonly real mouseY
                - Returns the current mouse coordinates.
            
            readonly method operator isMouseClicked -> boolean
                - Determines whether any mouse key has been clicked,
                  and will return true on the first mouse key.
              
            method isMouseButtonClicked(mousebuttontype mouseButton)
                - Returns true if the mouse button hasn't been
                  released yet.
            method setMousePos(real x, y) (introduced in 1.0.2.2)
                - Sets the mouse position for a given player.
              
            static method registerCode(code c, integer ev) -> triggercondition
                - Lets code run upon the execution of a certain event.
                - Returns a triggercondition that can be removed later.
            
            static method unregisterCallback(triggercondition trgHndl, integer ev)
                - Removes a generated triggercondition from the trigger.
            
        functions:
            GetPlayerMouseX(player p) -> real
            GetPlayerMouseY(player p) -> real
                - Returns the coordinates of the mouse of the player.
            
            OnMouseEvent(code func, integer eventId) -> triggercondition
                - See UserMouse.registerCode
            
            GetMouseEventType() -> integer
                - See UserMouse.getCurEventType
            
            UnregisterMouseCallback(triggercondition t, integer eventId)
                - See UserMouse.unregisterCallback
            SetUserMousePos(player p, real x, real y)
                - See UserMouse.setMousePos
 
  Unique Global Constants:
   IMPL_LOCK (Introduced in v.1.0.2.2)
    - Enables or disables the lock option
     -------------------
    |    Credits        |
     -------------------
 
        -   Pyrogasm for pointing out a comparison logic flaw
            in operator isMouseClicked.
        
        -   Illidan(Evil)X for the useful enum handles that
            grant more functionality to this snippet.
    
        -   TriggerHappy for the suggestion to include
            associated events and callbacks to this snippet.
        
        -   Quilnez for pointing out a bug related to the
            method isMouseButtonClicked not working as intended
            in certain situations.
        
    ----------------------------------------------------------------------------
*/
//  Arbitrary constants
globals
    constant integer EVENT_MOUSE_UP     = 1024
    constant integer EVENT_MOUSE_DOWN   = 2048
 constant integer EVENT_MOUSE_MOVE   = 3072
 
 private constant boolean IMPL_LOCK = false
endglobals
private module Init
    private static method onInit takes nothing returns nothing
        call thistype.init()
    endmethod
endmodule
struct UserMouse extends array
 static if IMPL_LOCK then
  //  Determines the minimum interval that a mouse move event detector
  //  will be deactivated. (Globally-based)
  //  You can configure it to any amount you like.
  private static constant real INTERVAL           = 0.031250000
 
  //  Determines how many times a mouse move event detector can fire
  //  before being deactivated. (locally-based)
  //  You can configure this to any integer value. (Preferably positive)
  private static constant integer MOUSE_COUNT_MAX  = 16
 
  // Determines the amount to be deducted from mouseEventCount
  // per INTERVAL. Runs independently of resetTimer
  private static constant integer MOUSE_COUNT_LOSS = 8
  private static constant boolean IS_INSTANT   = INTERVAL <= 0.
 endif
    private static integer currentEventType         = 0
    private static integer updateCount               = 0
    private static trigger stateDetector             = null
 static if IMPL_LOCK and not IS_INSTANT then
  private static timer resetTimer                 = null
 endif
    private static trigger array evTrigger
 
    private static integer array mouseButtonStack
 
 static if IMPL_LOCK and not IS_INSTANT then
  private integer  mouseEventCount
  private timer mouseEventReductor
 endif
    private thistype next
    private thistype prev
 
    private thistype resetNext
    private thistype resetPrev
    private trigger posDetector
 private integer mouseClickCount
 
    readonly real mouseX
    readonly real mouseY
 
    //  Converts the enum type mousebuttontype into an integer
    private static method toIndex takes mousebuttontype mouseButton returns integer
        return GetHandleId(mouseButton)
    endmethod
 
    static method getCurEventType takes nothing returns integer
        return currentEventType
    endmethod
 
    static method operator [] takes player p returns thistype
        if thistype(GetPlayerId(p) + 1).posDetector != null then
            return GetPlayerId(p) + 1
        endif
        return 0
    endmethod
    
    method operator player takes nothing returns player
        return Player(this - 1)
    endmethod
    method operator isMouseClicked takes nothing returns boolean
        return .mouseClickCount > 0
    endmethod
    method isMouseButtonClicked takes mousebuttontype mouseButton returns boolean
        return UserMouse.mouseButtonStack[(this - 1)*3 + UserMouse.toIndex(mouseButton)] > 0
 endmethod
    method setMousePos takes integer x, integer y returns nothing
        if GetLocalPlayer() == this.player then
            call BlzSetMousePos(x, y)
        endif
    endmethod
 static if IMPL_LOCK then
 private static method getMouseEventReductor takes timer t returns thistype
  local thistype this = thistype(0).next
  loop
   exitwhen this.mouseEventReductor == t or this == 0
   set this = this.next
  endloop
  return this
 endmethod
    private static method onMouseUpdateListener takes nothing returns nothing
        local thistype this = thistype(0).resetNext
        set updateCount     = 0
    
        loop
            exitwhen this == 0
            set updateCount = updateCount + 1
                    
            set this.mouseEventCount = 0
            call EnableTrigger(this.posDetector)
        
            set this.resetNext.resetPrev = this.resetPrev
            set this.resetPrev.resetNext = this.resetNext
        
            set this                     = this.resetNext
  endloop
 
  if updateCount > 0 then
   static if not IS_INSTANT then
    call TimerStart(resetTimer, INTERVAL, false, function thistype.onMouseUpdateListener)
   else
    call onMouseUpdateListener()
   endif
  else
   static if not IS_INSTANT then
    call TimerStart(resetTimer, 0.00, false, null)
    call PauseTimer(resetTimer)
   endif
        endif
 endmethod
 
 private static method onMouseReductListener takes nothing returns nothing
  local thistype this  = getMouseEventReductor(GetExpiredTimer())
  if this.mouseEventCount <= 0 then
   call PauseTimer(this.mouseEventReductor)
  else
   set this.mouseEventCount = IMaxBJ(this.mouseEventCount - MOUSE_COUNT_LOSS, 0)
   call TimerStart(this.mouseEventReductor, INTERVAL, false, function thistype.onMouseReductListener)
  endif
 endmethod
 endif
    private static method onMouseUpOrDown takes nothing returns nothing
        local thistype this = thistype[GetTriggerPlayer()]
        local integer index = (this - 1)*3 + UserMouse.toIndex(BlzGetTriggerPlayerMouseButton())
        local boolean releaseFlag   = false
    
        if GetTriggerEventId() == EVENT_PLAYER_MOUSE_DOWN then
            set this.mouseClickCount    = IMinBJ(this.mouseClickCount + 1, 3)
            set releaseFlag          = UserMouse.mouseButtonStack[index] <= 0
            set UserMouse.mouseButtonStack[index]  = IMinBJ(UserMouse.mouseButtonStack[index] + 1, 1)
       
            if releaseFlag then
                set currentEventType = EVENT_MOUSE_DOWN
                call TriggerEvaluate(evTrigger[EVENT_MOUSE_DOWN])
            endif
        else
            set this.mouseClickCount = IMaxBJ(this.mouseClickCount - 1, 0)
            set releaseFlag          = UserMouse.mouseButtonStack[index] > 0
            set UserMouse.mouseButtonStack[index]  = IMaxBJ(UserMouse.mouseButtonStack[index] - 1, 0)
        
            if releaseFlag then
                set currentEventType = EVENT_MOUSE_UP
                call TriggerEvaluate(evTrigger[EVENT_MOUSE_UP])
            endif
        endif
    endmethod
 
    private static method onMouseMove takes nothing returns nothing
  local thistype this   = thistype[GetTriggerPlayer()]
  local boolean started  = false
            
        set this.mouseX      = BlzGetTriggerPlayerMouseX()
        set this.mouseY      = BlzGetTriggerPlayerMouseY()
 
  static if IMPL_LOCK then
   set this.mouseEventCount  = this.mouseEventCount + 1
   if this.mouseEventCount <= 1 then
    call TimerStart(this.mouseEventReductor, INTERVAL, false, function thistype.onMouseReductListener)
   endif
  endif
  set currentEventType   = EVENT_MOUSE_MOVE
  call TriggerEvaluate(evTrigger[EVENT_MOUSE_MOVE])
  static if IMPL_LOCK then
   if this.mouseEventCount >= thistype.MOUSE_COUNT_MAX then
    call DisableTrigger(this.posDetector)              
 
    if thistype(0).resetNext == 0 then
     static if not IS_INSTANT then
      call TimerStart(resetTimer, INTERVAL, false, function thistype.onMouseUpdateListener)
      // Mouse event reductor should be paused
     else
      set started  = true
     endif
     call PauseTimer(this.mouseEventReductor)
    endif
 
    set this.resetNext              = 0
    set this.resetPrev              = this.resetNext.resetPrev
    set this.resetPrev.resetNext    = this
    set this.resetNext.resetPrev    = this
  
    if started then
     call onMouseUpdateListener()
    endif
   endif
  endif
    endmethod
    
    private static method init takes nothing returns nothing
        local thistype this = 1
        local player p      = this.player
 
  static if IMPL_LOCK and not IS_INSTANT then
   set resetTimer  = CreateTimer()
  endif
        set stateDetector   = CreateTrigger()
    
        set evTrigger[EVENT_MOUSE_UP]   = CreateTrigger()
        set evTrigger[EVENT_MOUSE_DOWN] = CreateTrigger()
        set evTrigger[EVENT_MOUSE_MOVE] = CreateTrigger()
    
        call TriggerAddCondition( stateDetector, Condition(function thistype.onMouseUpOrDown))
        loop
            exitwhen integer(this) > bj_MAX_PLAYER_SLOTS
        
            if GetPlayerController(p) == MAP_CONTROL_USER and GetPlayerSlotState(p) == PLAYER_SLOT_STATE_PLAYING then
                set this.next             = 0
                set this.prev             = thistype(0).prev
                set thistype(0).prev.next = this
                set thistype(0).prev      = this
            
                set this.posDetector         = CreateTrigger()
                static if IMPL_LOCK and not IS_INSTANT then
                    set this.mouseEventReductor  = CreateTimer()
                endif
                call TriggerRegisterPlayerEvent( this.posDetector, p, EVENT_PLAYER_MOUSE_MOVE )
                call TriggerAddCondition( this.posDetector, Condition(function thistype.onMouseMove))            
            
                call TriggerRegisterPlayerEvent( stateDetector, p, EVENT_PLAYER_MOUSE_UP )
                call TriggerRegisterPlayerEvent( stateDetector, p, EVENT_PLAYER_MOUSE_DOWN )
            endif
        
            set this = this + 1
            set p    = this.player
        endloop
    endmethod
 
    static method registerCode takes code handlerFunc, integer eventId returns triggercondition
        return TriggerAddCondition(evTrigger[eventId], Condition(handlerFunc))
    endmethod
 
    static method unregisterCallback takes triggercondition whichHandler, integer eventId returns nothing
        call TriggerRemoveCondition(evTrigger[eventId], whichHandler)
    endmethod
 
    implement Init
endstruct
function GetPlayerMouseX takes player p returns real
    return UserMouse.mouseX
endfunction
function GetPlayerMouseY takes player p returns real
    return UserMouse.mouseY
endfunction
function OnMouseEvent takes code func, integer eventId returns triggercondition
    return UserMouse.registerCode(func, eventId)
endfunction
function GetMouseEventType takes nothing returns integer
    return UserMouse.getCurEventType()
endfunction
function UnregisterMouseCallback takes triggercondition whichHandler, integer eventId returns nothing
    call UserMouse.unregisterCallback(whichHandler, eventId)
endfunction
function SetUserMousePos takes player p, integer x, integer y returns nothing
    call UserMouse.setMousePos(x, y)
endfunction
endlibrary
A simple library I wrote to remove effects without playing death animation and sounds.
JASS:
library RemoveEffect
  //Destroying an effect always plays its death animation and any associated sounds.
  //This library fixes it in a simple way: move the effect to a location where nobody can see or hear it.

  //By Pyrogasm, additional help by Bribe
  //v1.1

  globals
    private real SAFE_X = -3900.
    private real SAFE_Y =  3900.
    private real SAFE_Z = -1000.

    private real TIMESCALE = 10. //doesn't really matter what this is but we set it to > 0 so the animations actually finish
  endglobals

  function RemoveEffect takes effect e returns nothing
    call BlzSetSpecialEffectAlpha(e, 255)
    call BlzSetSpecialEffectZ(e, SAFE_Z)
    call BlzSetSpecialEffectPosition(e, SAFE_X, SAFE_Y, SAFE_Z)
    call BlzSetSpecialEffectTimeScale(e, TIMESCALE)
    call DestroyEffect(e)
  endfunction
endlibrary
Another simple library necessary to get the height of terrain at an XY location. This can be substituted for any other comparable library, just change the GetLocalZ(...) calls.
JASS:
library LocalZ
  //Can be substituted with any other library that does the same thing
  //or a Blz native if Blizzard adds something like that

  globals
    private location l = Location(0.,0.)
  endglobals

  function GetLocalZ takes real x, real y returns real
    call MoveLocation(l, x, y)
    return GetLocationZ(l)
  endfunction
endlibrary

JASS:
library Moonerang requires MouseUtils, RemoveEffect, LocalZ
  //=============================================================================================================================
  //  Moonerang v1.2
  //     by Pyrogasm
  //
  // Launch a boulder at target area. Upon reaching the area the boulder ignites and will track the mouse cursor for a short duration.
  //
  // Spell behavior is highly configurable, but by default:
  // Units hit by the initial shot are stunned, and those hit by the tracking shot have their movement and attack speeds slowed.
  // Each impact deals damage, and units can only be affected by each phase once.
  //
  // Dependencies:
  //   - MouseUtils   by MyPad:    https://www.hiveworkshop.com/threads/snippet-mouse-utility.310523/
  //   - RemoveEffect by Pyrogasm: https://www.hiveworkshop.com/threads/small-code-snippets.40758/page-45#post-3361522
  //   - LocalZ - any library that contains a function to get the Z height of X/Y coordinates
  //=============================================================================================================================
 

  globals
    //Configuration globals, muck with these as much as you want
    public  constant integer SPELL_ID   = 'Moon'
    public  constant integer DUMMY_ID   = 'dmmy'
    private constant real    TIMER_TICK = 0.03
    private constant real MAX_COLLISION = 128.00 //this value is added to GroupEnum calls before checking with IsUnitInRangeXY() so collision is properly accounted for
                                                 //set this to the max collision size a unit has in your map
    //For all effect variables:
    //1A = primary pre-flip
    //1B = primary post-flip
    //2A = secondary pre-flip
    //2B = secondary post-flip
 
    private constant real    EFFECT_H   = 125.
    private constant string  EFFECT_1A  = "Abilities\\Weapons\\AncientProtectorMissile\\AncientProtectorMissile.mdl"
    private constant string  EFFECT_1B  = "Abilities\\Spells\\Other\\Volcano\\VolcanoMissile.mdl"
    private constant string  EFFECT_2A  = "Abilities\\Spells\\Human\\ManaFlare\\ManaFlareTarget.mdl"
    private constant string  EFFECT_2B  = "Abilities\\Spells\\Human\\ManaFlare\\ManaFlareTarget.mdl"

    private constant real    E1A_SCALE  = 1.5
    private constant real    E1B_SCALE  = 1.5
    private constant real    E2A_SCALE  = 1.5
    private constant real    E2B_SCALE  = 1.5
    private constant real    E1A_TSCALE = 0.  //timescale, aka animation speed
    private constant real    E1B_TSCALE = 0.
    private constant real    E2A_TSCALE = 0.
    private constant real    E2B_TSCALE = 0.

    private constant integer E1A_COL_R  = 255
    private constant integer E1A_COL_G  = 255
    private constant integer E1A_COL_B  = 255

    private constant integer E1B_COL_R  = 255
    private constant integer E1B_COL_G  = 255
    private constant integer E1B_COL_B  = 255

    private constant integer E2A_COL_R  = 255
    private constant integer E2A_COL_G  = 255
    private constant integer E2A_COL_B  = 255

    private constant integer E2B_COL_R  = 255
    private constant integer E2B_COL_G  = 255
    private constant integer E2B_COL_B  = 255

    private constant real ORBIT_PERIOD    = 1.5 //this is only used in the line below
    private constant real ORBIT_ANGLE_INC = -2*bj_PI/ORBIT_PERIOD*TIMER_TICK
    private constant real ORBIT_OFFSET    = 70.

    private constant real LAUNCH_DECEL  = 900.
    private constant real LAUNCH_ENDVEL = 250.

    private constant real FOLLOW_DURATION = 1.5
    private constant real FOLLOW_ACCEL    = 2400.
    private constant real FOLLOW_MAX_VEL  = 800.
  endglobals
 
  globals
    //Spell globals, probably don't change these but you can reference the public ones
    public  keyword SpellData
    private SpellData array GSD
    private integer COUNT = 0
    private integer CURR = 0
    public  timer   SpellTimer  = CreateTimer()
    public  trigger CastTrigger = CreateTrigger()
  endglobals
 
  //Need this for checks in the spellBehavior module
  native UnitAlive takes unit u returns boolean

  public module spellBehavior
    // the following member variables are defined in the struct below for you to use
    // they do not need to be destroyed or removed, that is handled already:
    //
    // unit DUMMY                 <-- can cast instantly and is moved to (.cx, .cy) every timer step
    //                                add abilities to it with .prepDummy(abilCode) (level is set automatically)
    // group HIT_GROUP            <-- units already hit by this instance of the spell, you are free to clear this as necessary, but you must add units manually
    // static group SEARCH_GROUP  <-- a group you can use to search for targets via GroupEnumUnitsIn...
    // integer l                  <-- level of the spell when it was cast
    // unit c                     <-- caster
    // player p                   <-- owner of caster
    // real cx, cy, cz            <-- aboslute coordinates of the center of the boulder
    //
    // look at the SpellData struct below for additional members if you want more information
    // you may end an insance early by calling .endInstance()
    // you may add your own instance or static members here:
 
    static integer CRIPPLE_ABIL  = 'Mcpl'
    static string  CRIPPLE_ORDER = "cripple"
    static integer STUN_ABIL     = 'Mstn'
    static string  STUN_ORDER    = "firebolt"
    static real RADIUS = 70.
    static real DMG = 60.
 

    static method onInitEx takes nothing returns nothing
      //called on map init, so you can set up anything you need to here (most likely arrays or some global dummies)
    endmethod

    method onStart takes nothing returns nothing
      //called when the spell starts
      call this.prepDummy(STUN_ABIL)
      call this.prepDummy(CRIPPLE_ABIL)
    endmethod

    method onPeriodic takes boolean afterFlip returns nothing
      local unit u

      call GroupEnumUnitsInRange(SEARCH_GROUP, this.cx, this.cy, RADIUS+MAX_COLLISION, null)
      loop
        set u = FirstOfGroup(SEARCH_GROUP)
        exitwhen u == null
        call GroupRemoveUnit(SEARCH_GROUP, u)

        if IsUnitInRangeXY(u, this.cx, this.cy, RADIUS) and /*
        */ IsUnitEnemy(u, this.p) and UnitAlive(u)      and /*
        */ not IsUnitType(u, UNIT_TYPE_MAGIC_IMMUNE)    and /*
        */ not BlzIsUnitInvulnerable(u)                 and /*
        */ not IsUnitInGroup(u, this.HIT_GROUP)         then
    
          if afterFlip then
            call UnitDamageTarget(this.c, u, DMG, false, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_FIRE, WEAPON_TYPE_WHOKNOWS)
            call IssueTargetOrder(this.DUMMY, CRIPPLE_ORDER, u)
          else
            call UnitDamageTarget(this.c, u, DMG, false, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_FORCE, WEAPON_TYPE_WHOKNOWS)
            call IssueTargetOrder(this.DUMMY, STUN_ORDER, u)
          endif
          call GroupAddUnit(this.HIT_GROUP, u)
        endif
      endloop

      set u = null
    endmethod

    method onFlip takes nothing returns nothing
      //called when the boulder switches from linear movement to mouse cursor tracking movement
      call GroupClear(this.HIT_GROUP)
    endmethod

    method onEnd takes nothing returns nothing
      //called when the spell ends
    endmethod
  endmodule



  public struct SpellData
    effect e1 = null
    effect e2 = null
    unit c    = null
    player p  = null
    integer l = 0
    real vx   = 0.
    real vy   = 0.
    real ax   = 0.
    real ay   = 0.
    real tx   = 0.
    real ty   = 0.
    real cx   = 0.
    real cy   = 0.
    real cz   = 0.
    real a    = 0.
    real e2a  = 0.
    integer d = 0
    boolean b = false
    unit DUMMY = null
    group HIT_GROUP = CreateGroup()
    static group SEARCH_GROUP = CreateGroup()

    method prepDummy takes integer abil returns nothing
      call UnitAddAbility(this.DUMMY, abil)
      call SetUnitAbilityLevel(this.DUMMY, abil, this.l)
    endmethod

    implement spellBehavior

    method endInstance takes nothing returns nothing
      call this.onEnd()

      call RemoveUnit(this.DUMMY)
      call DestroyGroup(this.HIT_GROUP)
      call BlzSetSpecialEffectTimeScale(this.e1, 1.)
      call BlzSetSpecialEffectTimeScale(this.e2, 1.)
      call DestroyEffect(this.e1)
      call DestroyEffect(this.e2)

      set GSD[CURR] = GSD[COUNT-1]
      set COUNT = COUNT-1
      set CURR = CURR-1

      if COUNT == 0 then
        call PauseTimer(SpellTimer)
      endif

      call this.destroy()
    endmethod

    static method periodic takes nothing returns nothing
      local SpellData sd

      set CURR = 0
      loop
        exitwhen CURR >= COUNT
        set sd = GSD[CURR]

        set sd.e2a = sd.e2a + ORBIT_ANGLE_INC
        call BlzSetSpecialEffectYaw(sd.e1, sd.e2a)
        call BlzSetSpecialEffectYaw(sd.e2, sd.e2a+bj_PI)
        if not sd.b then
          set sd.cx = sd.cx + sd.vx*TIMER_TICK
          set sd.cy = sd.cy + sd.vy*TIMER_TICK
          set sd.cz = GetLocalZ(sd.cx, sd.cy)+EFFECT_H
          set sd.vx = sd.vx + sd.ax*TIMER_TICK
          set sd.vy = sd.vy + sd.ay*TIMER_TICK
          call SetUnitX(sd.DUMMY, sd.cx)
          call SetUnitY(sd.DUMMY, sd.cy)
          call sd.onPeriodic(false)

          set sd.d = sd.d-1
          if sd.d <= 0 then
            set sd.b = true
            set sd.d = R2I(FOLLOW_DURATION/TIMER_TICK)

            call RemoveEffect(sd.e1)
            call RemoveEffect(sd.e2)

            set sd.e1 = AddSpecialEffect(EFFECT_1B, sd.cx, sd.cy)
            call BlzSetSpecialEffectZ(sd.e1, sd.cz)
            call BlzSetSpecialEffectScale(sd.e1, E1B_SCALE)
            call BlzSetSpecialEffectTimeScale(sd.e1, E1B_TSCALE)
            call BlzSetSpecialEffectYaw(sd.e1, sd.e2a)
            call BlzSetSpecialEffectColor(sd.e1, E1B_COL_R, E1B_COL_G, E1B_COL_B)

            set sd.e2 = AddSpecialEffect(EFFECT_2B, sd.cx, sd.cy)
            call BlzSetSpecialEffectZ(sd.e2, sd.cz)
            call BlzSetSpecialEffectScale(sd.e2, E2B_SCALE)
            call BlzSetSpecialEffectTimeScale(sd.e2, E2B_TSCALE)
            call BlzSetSpecialEffectYaw(sd.e2, bj_PI)
            call BlzSetSpecialEffectColor(sd.e2, E2B_COL_R, E2B_COL_G, E2B_COL_B)

            call BlzSetSpecialEffectTime(sd.e2, 1.)
            call sd.onFlip()
          endif


          call BlzSetSpecialEffectPosition(sd.e1, sd.cx, sd.cy, sd.cz)
          call BlzSetSpecialEffectPosition(sd.e2, sd.cx + ORBIT_OFFSET*Cos(sd.e2a), sd.cy + ORBIT_OFFSET*Sin(sd.e2a), sd.cz)
        else
          set sd.a = GetUnitFacing(sd.c)*bj_DEGTORAD
          set sd.tx = UserMouse[sd.p].mouseX
          set sd.ty = UserMouse[sd.p].mouseY

          set sd.a = Atan2(sd.ty-sd.cy, sd.tx-sd.cx)
          set sd.ax = FOLLOW_ACCEL*Cos(sd.a)
          set sd.ay = FOLLOW_ACCEL*Sin(sd.a)
          set sd.vx = sd.vx + sd.ax*TIMER_TICK
          set sd.vy = sd.vy + sd.ay*TIMER_TICK

          if sd.vx*sd.vx + sd.vy*sd.vy > FOLLOW_MAX_VEL*FOLLOW_MAX_VEL then
            set sd.a = Atan2(sd.vy, sd.vx)
            set sd.vx = FOLLOW_MAX_VEL*Cos(sd.a)
            set sd.vy = FOLLOW_MAX_VEL*Sin(sd.a)
          endif

          set sd.cx = sd.cx + sd.vx*TIMER_TICK
          set sd.cy = sd.cy + sd.vy*TIMER_TICK
          set sd.cz = GetLocalZ(sd.cx, sd.cy)+EFFECT_H

          call BlzSetSpecialEffectPosition(sd.e1, sd.cx, sd.cy, sd.cz)
          call BlzSetSpecialEffectPosition(sd.e2, sd.cx + ORBIT_OFFSET*Cos(sd.e2a), sd.cy + ORBIT_OFFSET*Sin(sd.e2a), sd.cz)
          call SetUnitX(sd.DUMMY, sd.cx)
          call SetUnitY(sd.DUMMY, sd.cy)
          call sd.onPeriodic(true)

          set sd.d = sd.d-1
          if sd.d <= 0 then
            call sd.endInstance()
          endif
        endif

        set CURR = CURR+1
      endloop
    endmethod

    static method create takes unit c, real x, real y returns thistype
      local thistype sd = thistype.allocate()
      local real v
      local real dx
      local real dy

      set sd.c = c
      set sd.p = GetOwningPlayer(sd.c)
      set sd.l = GetUnitAbilityLevel(sd.c, SPELL_ID)
      set sd.cx = GetUnitX(sd.c)
      set sd.cy = GetUnitY(sd.c)
      set sd.cz = GetLocalZ(sd.cx, sd.cy) + EFFECT_H
      set sd.tx = x
      set sd.ty = y
      set dx = sd.tx-sd.cx
      set dy = sd.ty-sd.cy
      set sd.a = Atan2(dy,dx)

      set v = SquareRoot(LAUNCH_ENDVEL*LAUNCH_ENDVEL + 2*LAUNCH_DECEL*SquareRoot(dx*dx + dy*dy))
      set sd.vx = v*Cos(sd.a)
      set sd.vy = v*Sin(sd.a)
      set sd.ax = -1*LAUNCH_DECEL*Cos(sd.a)
      set sd.ay = -1*LAUNCH_DECEL*Sin(sd.a)
      set sd.d = R2I((v-LAUNCH_ENDVEL)/LAUNCH_DECEL/TIMER_TICK)

      set sd.e1 = AddSpecialEffect(EFFECT_1A, sd.cx, sd.cy)
      call BlzSetSpecialEffectZ(sd.e1, sd.cz)
      call BlzSetSpecialEffectScale(sd.e1, E1A_SCALE)
      call BlzSetSpecialEffectTimeScale(sd.e1, E1A_TSCALE)
      call BlzSetSpecialEffectYaw(sd.e1, 0.)
      call BlzSetSpecialEffectColor(sd.e1, E1A_COL_R, E1A_COL_G, E1A_COL_B)

      set sd.e2 = AddSpecialEffect(EFFECT_2A, sd.cx+ORBIT_OFFSET, sd.cy)
      call BlzSetSpecialEffectZ(sd.e2, sd.cz)
      call BlzSetSpecialEffectScale(sd.e2, E2A_SCALE)
      call BlzSetSpecialEffectTimeScale(sd.e2, E2A_TSCALE)
      call BlzSetSpecialEffectYaw(sd.e2, bj_PI)
      call BlzSetSpecialEffectColor(sd.e2, E2A_COL_R, E2A_COL_G, E2A_COL_B)
  
      call BlzSetSpecialEffectTime(sd.e2, 1.)
      set sd.DUMMY = CreateUnit(sd.p, DUMMY_ID, sd.cx, sd.cy, 0.)

      if COUNT == 0 then
        call TimerStart(SpellTimer, TIMER_TICK, true, function thistype.periodic)
      endif
      set GSD[COUNT] = sd
      set COUNT = COUNT+1

      return sd
    endmethod

    static method onCast takes nothing returns boolean
      if GetSpellAbilityId() == SPELL_ID then
        call SpellData.create(GetTriggerUnit(), GetSpellTargetX(), GetSpellTargetY())
        call GSD[COUNT-1].onStart()
      endif
      return false
    endmethod

    static method onInit takes nothing returns nothing
      call TriggerRegisterAnyUnitEventBJ(CastTrigger, EVENT_PLAYER_UNIT_SPELL_EFFECT)
      call TriggerAddCondition(CastTrigger, Condition(function thistype.onCast))
      call thistype.onInitEx()
    endmethod
  endstruct

endlibrary
JASS:
library PointDefense requires MouseUtils, RemoveEffect, LocalZ
  //=============================================================================================================================
  //  PointDefense v1.2
  //     by Pyrogasm
  //
  // Spawn defense drones that encircle the caster. Drones launch themselves to impact at the mouse cursor location periodically.
  // Each drone can travel a maximum distance, and those closest to the target are launched first.
  //
  // Spell behavior is highly configurable, but by default:
  // Enemies in the impact radius take damage per drone and are briefly blinded, causing them to miss most of their attacks.
  //
  // Dependencies:
  //   - MouseUtils   by MyPad:    https://www.hiveworkshop.com/threads/snippet-mouse-utility.310523/
  //   - RemoveEffect by Pyrogasm: https://www.hiveworkshop.com/threads/small-code-snippets.40758/page-45#post-3361522
  //   - LocalZ - any library that contains a function to get the Z height of X/Y coordinates
  //=============================================================================================================================

  globals
    public  constant integer SPELL_ID   = 'Pdfs'
    public  constant integer DUMMY_ID   = 'dmmy'
    private constant real    TIMER_TICK = 0.03
    private constant real MAX_COLLISION = 128.00 //this value is added to GroupEnum calls before checking with IsUnitInRangeXY() so collision is properly accounted for
                                                 //set this to the max collision size a unit has in your map
                                                   
    private constant integer MAX_DEFENDERS = 24 //used in array member sizing
    private constant real    ORBIT_RADIUS = 170.
    private constant real    ORBIT_PERIOD = 10. //this is only used in the line below
    private constant real    ORBIT_ANGLEINC = 2*bj_PI/ORBIT_PERIOD*TIMER_TICK
    private constant real    ORBIT_DELAY    = 0.51
 
    private constant real    FIRE_PERIOD = 0.24
    private constant real    FIRE_IMPACT_H = 0.
    private constant real    FIRE_MAX_DIST = 800.
    private constant real    FIRE_MAX_VEL = 1500.
    private constant real    FIRE_START_VEL = 0.
    private constant real    FIRE_ACCEL = 3000.
 
    //For all effect variables:
    //1A = drone pre-launch
    //1B = drone post-launch
    //2A = impact fx
 
    private constant real    EFFECT_H   = 150.
    private constant string  EFFECT_1A  = "Abilities\\Weapons\\WitchDoctorMissile\\WitchDoctorMissile.mdl"
    private constant string  EFFECT_1B  = "Abilities\\Weapons\\WitchDoctorMissile\\WitchDoctorMissile.mdl"
    private constant string  EFFECT_2A  = "Abilities\\Spells\\NightElf\\Blink\\BlinkCaster.mdl"   // Human\\ThunderClap\\ThunderClapCaster.mdl"

    private constant real    E1A_SCALE  = 0.5
    private constant real    E1A_TSCALE = 1. //timescale, aka animation speed

    private constant real    E1B_SCALE  = 1.
    private constant real    E1B_TSCALE = 1.

    private constant real    E2A_SCALE  = 0.6
    private constant real    E2A_TSCALE = 2.

    private constant integer E1A_COL_R  = 255
    private constant integer E1A_COL_G  = 255
    private constant integer E1A_COL_B  = 255

    private constant integer E1B_COL_R  = 255
    private constant integer E1B_COL_G  = 70
    private constant integer E1B_COL_B  = 70

    private constant integer E2A_COL_R  = 255
    private constant integer E2A_COL_G  = 0
    private constant integer E2A_COL_B  = 100
  endglobals
 
  globals
    public  keyword SpellData
    private keyword defender
    private SpellData array GSD
    private integer COUNT = 0
    private integer CURR  = 0
    public  timer   SpellTimer  = CreateTimer()
    public  trigger CastTrigger = CreateTrigger()
  endglobals
 
  //Need this for checks in the spellBehavior module
  native UnitAlive takes unit u returns boolean
 

  public module spellBehavior
    // the following variables are defined in the struct below for you to use
    // they do not need to be destroyed or removed, that is handled already:
    //
    // unit DUMMY                 <-- can cast instantly and is moved to (.cx, .cy) every timer step
    //                                add abilities to it with .prepDummy(abilCode) (level is set automatically)
    // group HIT_GROUP            <-- units already hit by this instance of the spell, you are free to clear this as necessary, but you must add units manually
    // static group SEARCH_GROUP  <-- a group you can use to search for targets via GroupEnumUnitsIn...
    // integer l                  <-- level of the spell when it was cast
    // unit c                     <-- caster
    // player p                   <-- owner of caster
    //
    // look at the SpellData struct below for additional members if you want more information
    // you may end an insance early by calling .endInstance()
    // you may add your own instance or static members here:
 
    static integer SECONDARY_ID   = 'Pdcs'
    static string SECONDARY_ORDER = "curse"
    static real RADIUS = 100.
    static real DMG    = 20.
 
    method numDefenders takes nothing returns integer
      //called on spell cast, determines the number of defenders to create
      return GetRandomInt(5,15)
    endmethod

    static method onInitEx takes nothing returns nothing
      //called on map init, so you can set up anything you need to here (most likely arrays or some global dummies)
    endmethod

    method onStart takes nothing returns nothing
      //called when the spell is cast
      call this.prepDummy(SECONDARY_ID)
    endmethod

    method onPeriodic takes nothing returns nothing
      //called periodically while spell is running
    endmethod
 
    method onLaunch takes defender def, real tx, real ty, real th returns nothing
      //called when defender def is launched
      //can use members def.x, def.y, def.z for current location (def.h for height) and the tx, ty, th parameters for targeted location
    endmethod
 
    method onImpact takes defender def returns nothing
      //can use members def.x, def.y, def.z for current location
      local unit u
      local effect e

      set e = AddSpecialEffect(EFFECT_2A, def.x, def.y)
      call BlzSetSpecialEffectColor(e, E2A_COL_R, E2A_COL_G, E2A_COL_B)
      call BlzSetSpecialEffectScale(e, E2A_SCALE)
      call BlzSetSpecialEffectTimeScale(e, E2A_TSCALE)
      call DestroyEffect(e)
   
      call GroupEnumUnitsInRange(SEARCH_GROUP, def.x, def.y, RADIUS+MAX_COLLISION, null)
      loop
        set u = FirstOfGroup(SEARCH_GROUP)
        exitwhen u == null
        call GroupRemoveUnit(SEARCH_GROUP, u)

        if IsUnitInRangeXY(u, def.x, def.y, RADIUS)    and /*
        */     IsUnitEnemy(u, this.p) and UnitAlive(u) and /*
        */ not IsUnitType(u, UNIT_TYPE_MAGIC_IMMUNE)   and /*
        */ not BlzIsUnitInvulnerable(u)                then

          call UnitDamageTarget(this.c, u, DMG, false, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_FORCE, WEAPON_TYPE_WHOKNOWS)
          call IssueTargetOrder(this.DUMMY, SECONDARY_ORDER, u)
        endif
      endloop

      set u = null
    endmethod

    method onEnd takes nothing returns nothing
      //called when the spell ends
    endmethod
endmodule

  private struct defender
    SpellData sd = 0
    boolean f = false
    boolean i = false
    effect e = null
    integer n = 0
    integer d = 0
    real x = 0.
    real y = 0.
    real z = 0.
    real h = 0.
    real v = 0
    real cosA = 0.
    real sinA = 0.
    real dz = 0.

    method move takes nothing returns nothing
      if not this.f then
        set this.x = this.sd.cx + this.sd.orbitR*Cos(this.sd.defAng*this.n+this.sd.defTheta)
        set this.y = this.sd.cy + this.sd.orbitR*Sin(this.sd.defAng*this.n+this.sd.defTheta)
        set this.z = BlzGetUnitZ(this.sd.c)+this.h
      else
        set this.x = this.x + this.v*this.cosA*TIMER_TICK
        set this.y = this.y + this.v*this.sinA*TIMER_TICK
        set this.h = this.h - this.dz
        set this.z = GetLocalZ(this.x, this.y) + this.h
        set this.v = this.v + FIRE_ACCEL*TIMER_TICK
        if this.v > FIRE_MAX_VEL then
          set this.v = FIRE_MAX_VEL
        endif
     
        set this.d = this.d - 1
      endif
   
      call BlzSetSpecialEffectPosition(this.e, this.x, this.y, this.z)
      if this.d <= 0 then
        call this.sd.onImpact(this)
        call DestroyEffect(this.e)
        set this.sd.d = this.sd.d - 1
        set this.i = true
      endif
    endmethod

    method fireAt takes real x, real y, real h returns nothing
      local real a = Atan2(y-this.y, x-this.x)
      local real t1
      local real t2
      local real qa
      local real qb
      local real qc
      local real d


      call RemoveEffect(this.e)
      set this.e = AddSpecialEffect(EFFECT_1B, this.x, this.y)
      call BlzSetSpecialEffectZ(this.e, this.z)
      call BlzSetSpecialEffectScale(this.e, E1B_SCALE)
      call BlzSetSpecialEffectTimeScale(this.e, E1B_TSCALE)
      call BlzSetSpecialEffectColor(this.e, E1B_COL_R, E1B_COL_G, E1B_COL_B)
      call this.sd.onLaunch(this, x, y, h)
   
      set this.f = true
      set this.cosA = Cos(a)
      set this.sinA = Sin(a)
      set this.v = FIRE_START_VEL
   
      set d = SquareRoot((x-this.x)*(x-this.x) + (y-this.y)*(y-this.y))
      set qa = FIRE_ACCEL/2.
      set qb = FIRE_START_VEL
      set qc = -1.*d
   
      set t1 = (-1.*qb + SquareRoot(qb*qb - 4.*qa*qc))/(2.*qa)
      set t2 = 0.
   
      if this.v+FIRE_ACCEL*t1 > FIRE_MAX_VEL then
        set t1 = (FIRE_MAX_VEL-this.v)/FIRE_ACCEL
        set t2 = (d - (FIRE_MAX_VEL-this.v)/2.*t1)/FIRE_MAX_VEL
      endif
   
      set this.d = R2I((t1+t2)/TIMER_TICK)   
      set this.dz = ((this.h - h)/this.d)
    endmethod
 
    static method create takes SpellData sd, integer n returns thistype
      local thistype def = thistype.allocate()
   
      set def.sd = sd
      set def.n = n
      set def.f = false
      set def.i = false
      set def.x = sd.cx + sd.orbitR*Cos(sd.defAng*n+sd.defTheta)
      set def.y = sd.cy + sd.orbitR*Sin(sd.defAng*n+sd.defTheta)
      set def.h = EFFECT_H
      set def.z = BlzGetUnitZ(sd.c)+def.h
   
      set def.d = 1 //Simplest solution I guess...
   
      set def.e = AddSpecialEffect(EFFECT_1A, def.x, def.y)
      call BlzSetSpecialEffectZ(def.e, def.z)
      call BlzSetSpecialEffectScale(def.e, E1A_SCALE)
      call BlzSetSpecialEffectTimeScale(def.e, E1A_TSCALE)
      call BlzSetSpecialEffectColor(def.e, E1A_COL_R, E1A_COL_G, E1A_COL_B)
   
      return def
    endmethod
  endstruct


  public struct SpellData
    static group SEARCH_GROUP = CreateGroup()
    group HIT_GROUP = CreateGroup()
    unit DUMMY = null
 
    unit c = null
    player p = null
    integer l = 0
    integer d = 0
    integer fire = 0
    real cx = 0.
    real cy = 0.

    defender array defs[MAX_DEFENDERS]
    integer defCount = 0
    real defAng = 0.
    real defTheta = 0.
    integer phase = 0
    real orbitA = 0.
    real orbitV = 0.
    real orbitR = 0.


    method prepDummy takes integer abil returns nothing
      call UnitAddAbility(this.DUMMY, abil)
      call SetUnitAbilityLevel(this.DUMMY, abil, this.l)
    endmethod

    method endInstance takes nothing returns nothing
      local integer i = 0

      call this.onEnd()
      call RemoveUnit(this.DUMMY)
      call DestroyGroup(this.HIT_GROUP)
   
      loop
        call this.defs[i].destroy()
     
        set i = i+1
        exitwhen i >= this.defCount
      endloop
   

      set GSD[CURR] = GSD[COUNT-1]
      set COUNT = COUNT-1
      set CURR = CURR-1

      if COUNT == 0 then
        call PauseTimer(SpellTimer)
      endif
      call this.destroy()
    endmethod

    implement spellBehavior
 
    method fireDefender takes real x, real y, real z returns nothing
      local integer i = 0
      local real dx
      local real dy
      local real d2
      local integer closest = -1
      local real d2Closest = (FIRE_MAX_DIST+ORBIT_RADIUS)*(FIRE_MAX_DIST+ORBIT_RADIUS)
   
      loop
        if not this.defs[i].f then
          set dx = this.defs[i].x - x
          set dy = this.defs[i].y - y
          set d2 = dx*dx + dy*dy
          if d2 <= d2Closest then
            set closest = i
            set d2Closest = d2
          endif
        endif
     
        set i = i+1
        exitwhen i >= this.defCount
      endloop
   
      if closest >= 0 then
        call this.defs[closest].fireAt(x, y, z)
      endif
    endmethod
 
    method updateDefenders takes nothing returns nothing
      local integer i = 0
      loop
        if not this.defs[i].i then
          call this.defs[i].move()
        endif
        set i = i+1
        exitwhen i >= this.defCount
      endloop
    endmethod

    static method periodic takes nothing returns nothing
      local SpellData sd
      local integer i
      local real mx
      local real my
      local real a

      set CURR = 0
      loop
        exitwhen CURR >= COUNT
        set sd = GSD[CURR]
     
        set sd.cx = GetUnitX(sd.c)
        set sd.cy = GetUnitY(sd.c)
        call SetUnitX(sd.DUMMY, sd.cx)
        call SetUnitY(sd.DUMMY, sd.cy)
        set sd.defTheta = sd.defTheta + ORBIT_ANGLEINC
     
        if sd.phase == 0 then
          set sd.orbitR = sd.orbitR + sd.orbitV*TIMER_TICK
          set sd.orbitV = sd.orbitV - sd.orbitA*TIMER_TICK
          call sd.updateDefenders()
       
          set sd.d = sd.d-1
          if sd.d <= 0 then
            set sd.phase = sd.phase+1
            set sd.d = sd.defCount
          endif
        elseif sd.phase == 1 then
          call sd.updateDefenders()
          call sd.onPeriodic()
       
          set sd.fire = sd.fire-1
          if sd.fire <= 0 then
            set mx = UserMouse[sd.p].mouseX
            set my = UserMouse[sd.p].mouseY
            if (mx-sd.cx)*(mx-sd.cx) + (my-sd.cy)*(my-sd.cy) > FIRE_MAX_DIST*FIRE_MAX_DIST then
              set a = Atan2(my-sd.cy, mx-sd.cx)
              set mx = sd.cx + FIRE_MAX_DIST*Cos(a)
              set my = sd.cy + FIRE_MAX_DIST*Sin(a)
            endif
         
            call sd.fireDefender(mx, my, FIRE_IMPACT_H)
            set sd.fire = R2I(FIRE_PERIOD/TIMER_TICK)
          endif

          if sd.d <= 0 then
            call sd.endInstance()
          endif
        endif
     
        set CURR = CURR+1
      endloop
    endmethod

    static method create takes unit c returns thistype
      local thistype sd = thistype.allocate()
      local integer i = 0

      set sd.c = c
      set sd.cx = GetUnitX(sd.c)
      set sd.cy = GetUnitY(sd.c)
      set sd.p = GetOwningPlayer(sd.c)
      set sd.l = GetUnitAbilityLevel(sd.c, SPELL_ID)
      set sd.d = R2I(ORBIT_DELAY/TIMER_TICK)
      set sd.defCount = sd.numDefenders()
      set sd.defAng = 2*bj_PI/sd.defCount
      set sd.DUMMY = CreateUnit(sd.p, DUMMY_ID, sd.cx, sd.cy, 0.)
   
      set sd.orbitR = 0.
      set sd.orbitV = 2.*ORBIT_RADIUS/ORBIT_DELAY
      set sd.orbitA = 2./ORBIT_DELAY*(sd.orbitV - ORBIT_RADIUS/ORBIT_DELAY)

      loop
        set sd.defs[i] = defender.create(sd, i)
     
        set i = i+1
        exitwhen i >= sd.defCount
      endloop

      if COUNT == 0 then
        call TimerStart(SpellTimer, TIMER_TICK, true, function thistype.periodic)
      endif
      set GSD[COUNT] = sd
      set COUNT = COUNT+1

      return sd
    endmethod

    static method onCast takes nothing returns boolean
      if GetSpellAbilityId() == SPELL_ID then
        call SpellData.create(GetTriggerUnit())
        call GSD[COUNT-1].onStart()
      endif
      return false
    endmethod

    static method onInit takes nothing returns nothing
      call TriggerRegisterAnyUnitEventBJ(CastTrigger, EVENT_PLAYER_UNIT_SPELL_EFFECT)
      call TriggerAddCondition(CastTrigger, Condition(function thistype.onCast))
      call thistype.onInitEx()
    endmethod
  endstruct
endlibrary

1.2 - 7/31/19 - Yep I actually fucked up the UnitInRangeXY call in 1.1
1.1 - 7/21/19 - Updated MouseUtils version, added UnitInRangeXY calls for better collision detection
1.0 - 7/20/19 - Initial release
Contents

ModularMouseSpells v1.2 (Map)

Reviews
MyPad
With this, I can definitely agree while testing. The aesthetic of having a missile that follows one's specified coordinates makes one feel like a general who had just authorized a test missile launch on a clear field. The druids won't be having any of...
Level 38
Joined
Feb 27, 2007
Messages
4,951
To be completely honest, code is not very good - not extendable. But oh boiiiii it was fun to play with point defense! Super sweet ability and a great kickstart for a really cool toolkit for a character!
There’s literally an entire module with different functions that are called periodically throughout the spell. You can put whatever you want in it. You can muck with the spell struct, fire defenders early, end the instance, change the center point, alter the variables that control visuals all without breaking the spell. What’s not extendable about that? Instead of just saying it isn’t I would appreciate actual suggestions.
 
Wow I had fun with that point defense one!

With this, I can definitely agree while testing. The aesthetic of having a missile that follows one's specified coordinates makes one feel like a general who had just authorized a test missile launch on a clear field. The druids won't be having any of those illegal murlocs crossing their forests, I'm sure. With their perfected moonerangs and point defenses, these druids are ready to make Kalimdor great again.


Code:


So far, short variable names are convenient for developing these sets of spells quickly, but they also hinder (a bit) the process of reviewing. Despite that, the underlying code is clean and quite understandable enough that it can give some insights as to what it does even before testing.

Another concern I would like to address is the name of the resource causing a bit of confusion, as I thought it would describe a generic Mouse-based missile class that supports different ability ids, and not regular spells with the configurable bits located in the modules.

Nevertheless, the issues presented above (if they are issues at all) are of such an order of triviality that it would be unfair to hold this back to Awaiting Update.

Approved
 
Level 38
Joined
Feb 27, 2007
Messages
4,951
Another concern I would like to address is the name of the resource causing a bit of confusion, as I thought it would describe a generic Mouse-based missile class that supports different ability ids, and not regular spells with the configurable bits located in the modules.
I’m fine changing the name of the resource, but not sure how to capture what they really are. Extendable Mouse Spells Pack, Modular Mouse Spellpack, Editable Mouse Spells? Open to suggestions to avoid confusion.

Also I’m not really sure I grok what you thought this might do, but it sounds like a cool project. Mouse-tracking missiles are very easy but I’m not convinced they would be very useful in an rts setting.
 
Top