System JCast (advanced casting-system)

profet

New Member
Reaction score
7
Hi everybody,

I've been deeply involved in wc3 mapping community (war3/wow models importer pluggin for 3dsMAX) and a very active system maker for years.

Long time ago, I started an RPG project Nights of Kalimdor for which I made several advanced systems (spell, buffs, physic, equipment, item sets...)
I've made some videos that are available here. They are quite rough but give a good preview of their use.
Because of the SC2 beta release and the wc3's programmed death, I decided to share these systems among the developers community before they fall into oblivion :)

I'm gonna start with my cast system, so, all I wish is feedback to improve the system, and maybe projects that use it ! ;)


Overview (goal: break wc3 spells' limits and get a very reactive spells system)

JCast is a surrogate to the whole warcraft3's casting system : it replaces the functioning of the cast itself and allows a lot of new behaviours like variable casttime (a buff or an item could increase or decrease the cast/channeling time).
It also encapsulates the whole spell inside a struct, that enables us to easily attach data to each spell instance (effects, numerical values...), solving by the way a lot of programing problems.


Main features

- New timed-cast system (wc3 already implements this functionality but the hero is stuck on place wich is very unfriendly and supress all gameplay-reactivity, moreover the cast/channeling duration may be changed dynamically ingame)
- Many spell events : OnFail, OnBegin, OnCast, OnCastEnd, OnCastEffect, OnChannel, OnChannelTick, OnChannelEnd, OnEnd... (GetCaster() always returns the same value whatever the event)
- Highly configurable (ShowCastBar, ShowChannelBar, ReverseChannelBar, ResumeAttack, CanBeInterrupted...)
- Enable to create complex buffs or over-time spell's effects (ie. irregular ticks sequence).
- Custom casting check (through overloading of the CanCast method)
- Pushback system (the cast duration may increase if the units takes damage when casting, similar to WoW's pushback effect)
- Interruption system (a single function call can cancel any cast/channeling)
- Optional castbar (a customizable castbar can be updated on cast/channeling)
- Target watch (the caster can keep trace of it's previous target, thus it is now possible to create melee-targetless spells that don't interrupt attack)
- Consise and maintanable code (like the buffs system, a spell is defined by a single struct with overriden methods, no trigger have to be created)


Detailed features
The spell definition structure allows the following functions overriding:
JASS:
method CanCast takes nothing returns boolean                    defaults true
method CanBeInterrupted takes nothing returns boolean           defaults true

method GetCastTime takes nothing returns real                   defaults 0.
method GetChannelTime takes nothing returns real                defaults 0.
method GetChannelTickTime takes nothing returns real            defaults 0.

method ResumeAttack takes nothing returns boolean               defaults false
method RefundManaOnCancel takes nothing returns boolean         defaults true

method OnBegin takes nothing returns nothing                    defaults nothing
method OnFail takes nothing returns nothing                     defaults nothing
method OnPreCast takes nothing returns nothing                  defaults nothing

method OnCastStart takes nothing returns nothing                defaults nothing
method OnCastEnd takes nothing returns nothing                  defaults nothing
method OnEffect takes nothing returns nothing                   defaults nothing
method OnChannelStart takes nothing returns nothing             defaults nothing
method OnChannelTick takes nothing returns nothing              defaults nothing
method OnChannelEnd takes nothing returns nothing               defaults nothing
method OnEnd takes nothing returns nothing                      defaults nothing


It also provides built-in functions to retrieves spell's informations:
JASS:
method GetLevel takes nothing returns integer
method GetCaster takes nothing returns unit
method GetManaCost takes nothing returns real

method GetAttackTarget takes nothing returns unit

method GetTargetX takes nothing returns real
method GetTargetY takes nothing returns real
method GetTargetUnit takes nothing returns unit
method GetTargetItem takes nothing returns item
method GetTargetDestructable takes nothing returns destructable

method IsCast takes nothing returns boolean
static constant method GetUpdateRate takes nothing returns real



Use example
JASS:

library AbilityLifeDrain uses AbilityCore
// This spell is a remake of the Warcraft III's life drain.


private struct spell extends IJCast
    
//Ability declaration
    private static constant integer AbilityId = 'AoLD'
    
    
//The channeling time lasts 10 seconds.
    method GetChannelTime takes nothing returns real
        return 10.
    endmethod
    
//Channeling tick event occurs each second.
    method GetChannelTickTime takes nothing returns real
        return 1.
    endmethod
    
//The spell can be cast only if the caster sees the target, if it's in given range and if it is still alive!
//Note: During channeling, CanCast method is also called each time a tick occurs (after the OnChannelTick call), then
//      the channeling could be interrupted if the target gets out of range, or die...
    method CanCast takes nothing returns boolean
        return IsUnitInSight(GetTargetUnit(), GetCaster()) and IsUnitInRange(GetTargetUnit(), GetCaster(), 600.) and (GetUnitState(GetTargetUnit(),UNIT_STATE_LIFE) > 0.)
    endmethod
    
//We need some specific code for this spell
    private effect effectCaster
    private effect effectTarget
    private lightning ray
    private sound snd
    private real casterX = 0.
    private real casterY = 0.
    private real casterZ = 0.
    private real targetX = 0.
    private real targetY = 0.
    private real targetZ = 0.
    private real progressT = 0.
    
    private method rayUpdate takes nothing returns nothing
        //Link the ray from caster's hands...
            set casterX = GetUnitX(GetCaster()) + 75.*Cos( GetUnitFacing(GetCaster())*bj_DEGTORAD )
            set casterY = GetUnitY(GetCaster()) + 75.*Sin( GetUnitFacing(GetCaster())*bj_DEGTORAD )
        //...to target's feet
            set targetX = GetUnitX(GetTargetUnit())
            set targetY = GetUnitY(GetTargetUnit())
            call MoveLightningEx( ray, true, casterX, casterY, casterZ, targetX, targetY, targetZ )
        //and make the caster face its target
            call SetUnitFacing( GetCaster(), Atan2(targetY-casterY,targetX-casterX)*bj_RADTODEG )
    endmethod
    
    
//Channeling progress is tracked to update the channelbar and the ray
    method OnChannelUpdate takes real progress returns nothing
        //Update the channel bar
            call Demo_ChannelbarUpdate(GetCaster(), progress)
        //Update the ray (only every 0.05s)
            set progressT = progressT + GetUpdateRate()
            if (progressT >= 0.05)then
                call rayUpdate()
                set progressT = progressT - 0.05
            endif
    endmethod
    
    
//Channeling starts, just create some special effects and initialize the ray
    method OnChannelStart takes nothing returns nothing
        //Add special effects
            set effectCaster = AddSpecialEffectTarget("Abilities\\Spells\\Other\\Drain\\DrainCaster.mdl", GetCaster(), "origin")
            set effectTarget = AddSpecialEffectTarget("Abilities\\Spells\\Other\\Drain\\DrainTarget.mdl", GetTargetUnit(), "chest")
        //Add lighning effect
            set casterZ = 50.
            set targetZ = 50.
            set ray = AddLightningEx("DRAL", true, casterX, casterY, casterZ, targetX, targetY, targetZ)
            call .rayUpdate()
    endmethod
    
//When a tick occurs, this code is executed !
    method OnChannelTick takes nothing returns nothing
    local real amount = 20. //+ I2R(GetHeroInt(GetCaster(),true)) //use hero's int as dmg bonus
        call SetUnitState(GetCaster(), UNIT_STATE_LIFE, GetUnitState(GetCaster(), UNIT_STATE_LIFE) + amount)
        call UnitDamageTarget(GetCaster(), GetTargetUnit(), amount, false/*attack*/, true/*ranged*/, ATTACK_TYPE_MAGIC, DAMAGE_TYPE_UNIVERSAL/*ignore armor*/, WEAPON_TYPE_WHOKNOWS)
    endmethod
    
    
//The spell is over, that's time to reset the channelbar and destroy created effects.
    method OnChannelEnd takes nothing returns nothing
        call Demo_ChannelbarReset(GetCaster())
        call DestroyLightning(ray)
        call DestroyEffect(effectCaster)
        call DestroyEffect(effectTarget)
    endmethod
    
    
//When hit by an attack, channeling duration is reduced by 25% (max. 2 times per cast)
    method ApplyChannelPushback takes real progress, real channelTime, real pushCount returns real
        if (pushCount <= 2) then
            return progress + channelTime * 0.25
        else
            return progress
        endif
    endmethod
    
//Implement JCastModule at the end of the spell code
    implement JCastModule
    
endstruct

//===========================================================================
endlibrary



System code
JASS:
//***************************************************************************************************
//*  JCast
//* -------
//* Author: profet (profetiser_AT_hotmail.com)
//*
//* Description:
//*     
//*     This system provides an easy way to create complex custom spells.
//*
//*     JCast is a surrogate to the whole warcraft3's casting system : it replaces the functioning of
//*     the cast itself and allows a lot of new behaviours like variable casttime (a buff or an item
//*     could increases or decreases the cast/channeling time).
//*     It also encapsulates the whole spell inside a struct, that enables us to easily attach data to
//*     each spell instance (effects, numerical values...), solving by the way a lot of programing problems.
//*
//*
//* Usage:
//*     
//*     The creation of a new spell is a very simple task:
//*         1. Create a new structure that extends the IJCast interface.
//*         2. Define the static constant AbilityId, needed for spell's registration.
//*         3. Implement JCastModule (at the end of the struct, see FocusedHealing example for details).
//*         4. Override the methods required to fit your needs.
//*     
//*     Overridable methods are numerous:
/*         
            method CanCast takes nothing returns boolean defaults true
            //Returns FALSE to deny the cast.
            
            method CanBeInterrupted takes nothing returns boolean defaults true
            //Returns FALSE to allow the caster to move during the cast (see RemoteBomb example for details).
            
            method ResumeAttack takes nothing returns boolean defaults false
            //Returns TRUE to restore caster's attack order when the spell is cast.
            
            method RefundManaOnCancel takes nothing returns boolean defaults true
            //Returns FALSE to make the mana consumed if the spell is canceled.
            
            method GetCastTime takes nothing returns real defaults 0.
            //Returns the duration of the cast (time before the spell's effects)
            
            method GetChannelTime takes nothing returns real defaults 0.
            //Returns the duration of the channeling (duration of spell's effects, used in combination with GetChannelTickTime)
            
            method GetChannelTickTime takes nothing returns real defaults 0.
            //Returns the period of channeling tick's (see LifeDrain example for details).
            
            method ApplyCastPushback takes real progress, real castTime, real pushCount returns real
            //Returns the new casting progress value, modified by the pushback effect.
            //Note: You can return a changeless 'progress' value to ignore this effect.
            
            method ApplyChannelPushback takes real progress, real channelTime, real pushCount returns real
            //Returns the new channeling progress value, modified by the pushback effect.
            //Note: You can return a changeless 'progress' value to ignore this effect.
            
            
            static method OnInit takes nothing returns nothing
            //Called just after struct's initialization.
            
            method OnBegin takes nothing returns nothing
            //Called at spell's initialization start.
            
            method OnFail takes nothing returns nothing
            //Called when the spell fails (CanCast returned FALSE).
            
            method OnPreCast takes nothing returns nothing
            //Called when the spell starts (after manacost calculation).
            
            method OnCastStart takes nothing returns nothing
            //(only if cast duration > 0) Called when the cast starts.
            
            method OnCastUpdate takes real progress returns nothing
            //(only if cast duration > 0) Called at each cast update (ref. TIMER_THRESHOLD constant),
            //use this only if you need to update things all along the cast.
            
            method OnCastEnd takes nothing returns nothing
            //(only if cast duration > 0) Called when the cast ends (after the duration returned by GetCastTime).
            
            method OnEffect takes nothing returns nothing
            //Called just after OnCastEnd if spell conditions are still valid (CanCast is reevaluated when the cast ends)
            
            method OnChannelStart takes nothing returns nothing
            //(only if channel duration > 0) Called when the channeling starts.
            
            method OnChannelUpdate takes real progress returns nothing
            //(only if channel duration > 0) Called at each channeling update (ref. TIMER_THRESHOLD
            //constant), use this only if you need to update things all along the channeling.
            
            method OnChannelTick takes nothing returns nothing
            //(only if channel duration > 0) Called when a channeling tick is reached (used to
            //execute periodical code during channeling)
            
            method OnChannelEnd takes nothing returns nothing
            //(only if channel duration > 0) Called when the channeling ends.
            
            method OnEnd takes nothing returns nothing
            //Called whenever the spell ends, even on failure.
            
            
*/       
//*
//*
//* Requirements:
//*   Table (by Vexorian, <a href="http://www.wc3c.net/showthread.php?t=101246" target="_blank" class="link link--external" rel="nofollow ugc noopener">http://www.wc3c.net/showthread.php?t=101246</a> )
//*   TimerUtils (by Vexorian, <a href="http://www.wc3c.net/showthread.php?t=101322" target="_blank" class="link link--external" rel="nofollow ugc noopener">http://www.wc3c.net/showthread.php?t=101322</a> )
//*
//*  To implement, just get a vJass compiler and
//* copy this library/trigger to your map.
//*
//***************************************************************************************************
library JCast initializer init uses Table, TimerUtils, optional JDebug
//===========================================================================
// SETTINGS
// ¯¯¯¯¯¯¯¯
//  This section exposes all the system&#039;s configuration variables and functions.
//  Feel free to modify them to suit your needs !
//===========================================================================
globals
    
    //Period of the spell&#039;s update timer, keeping it as low as possible increases precision but lowers system&#039;s performances.
    //Note that update period also affects casting/channeling bar and OnCastUpdate/OnChannelUpdate update rate.
    private constant real           TIMER_THRESHOLD                     = 0.01
    
endglobals
    
    
//===========================================================================
//* LIBRARY&#039;S CORE
//* ¯¯¯¯¯¯¯¯¯¯¯¯¯¯
//*  Do NOT modify without a really good reason !!
//===========================================================================

//***********************************************************
//* LAST TARGET SYSTEM
//*  Allow to retrieve the last attacked unit if interrupted by a cast
//*

    // The target is stored whenever the unit issue an &quot;attack&quot; or &quot;smart&quot; order.
    // When the unit issue a point order, stored target is instantly cleared.
    // When the unit issue an order with no target, if issue order is not &quot;stop&quot; nor &quot;holdposition&quot;
    // the target is flagged as &quot;previous target&quot; and will be cleared if an other no-target order is issued.
    
    globals
        private HandleTable     TARGET_TABLE
        private integer         tempOrder
    endglobals
    
    private struct sTargetManager
        private unit        m_unit
        private unit        m_target    = null
        private boolean     m_current   = false
        
        method Reset takes nothing returns nothing
        //-&gt; Point order or &quot;Stop&quot;
            if (.m_target != null) then
                set .m_target = null
                //set .m_current = false
            endif
        endmethod
        
        method Set takes unit target returns nothing
        //-&gt; &quot;Attack&quot; or &quot;Smart&quot; order
            if (target != null) then
                set .m_target = target
                set .m_current = true
            endif
        endmethod
        
        method Unset takes nothing returns nothing
        //-&gt; order with no target
            if (.m_target != null) then
                if (.m_current) then
                    set .m_current = false
                else
                    set .m_target = null
                    set .m_current = false
                endif
            endif
        endmethod
        
        method getTarget takes nothing returns unit
            if (.m_target != null) and (GetWidgetLife(.m_target) &gt; .405) then
                return .m_target
            endif
            return null
        endmethod
        
        static method GetTarget takes unit un returns unit
            if TARGET_TABLE.exists(un) then
                return thistype(TARGET_TABLE[un]).getTarget()
            endif
            return null
        endmethod
        
        static method create takes unit un returns thistype
        local thistype this = thistype.allocate()
            set this.m_unit = un
            return this
        endmethod
        
    endstruct
    
    
    //smart:        851971
    //stop:         851972
    //attack:       851983
    //move:         851986
    //patrol:       851990
    //holdposition: 851993
    private function TargetOrder_Actions takes nothing returns nothing
        if TARGET_TABLE.exists(GetOrderedUnit()) then
            //If the unit issued an &quot;Attack&quot; order (or &quot;Smart&quot; order on hostile target)
            if (GetIssuedOrderId() == 851983 /*attack*/) or ((GetIssuedOrderId() == 851971 /*smart*/) and IsUnitEnemy(GetOrderTargetUnit(), GetOwningPlayer(GetOrderedUnit()))) then
                call sTargetManager(TARGET_TABLE[GetOrderedUnit()]).Set( GetOrderTargetUnit() )
            endif
        endif
    endfunction
    
    private function VoidOrder_Actions takes nothing returns nothing
        if TARGET_TABLE.exists(GetOrderedUnit()) then
            if (GetIssuedOrderId() == 851972 /*stop*/) or (GetIssuedOrderId() == 851993 /*holdposition*/) then
                call sTargetManager(TARGET_TABLE[GetOrderedUnit()]).Reset()
            else
                call sTargetManager(TARGET_TABLE[GetOrderedUnit()]).Unset()
            endif
        endif
    endfunction
    
    private function PointOrder_Actions takes nothing returns nothing
        if TARGET_TABLE.exists(GetOrderedUnit()) then
            call sTargetManager(TARGET_TABLE[GetOrderedUnit()]).Reset()
        endif
    endfunction
    
    
    private function initLastTarget takes nothing returns nothing
    local integer i = 0
    local trigger t
        
        set TARGET_TABLE = HandleTable.create()
        
        //Issue a target order
        set t = CreateTrigger()
        call TriggerAddAction(t, function TargetOrder_Actions)
        loop
            call TriggerRegisterPlayerUnitEvent(t, Player(i), EVENT_PLAYER_UNIT_ISSUED_TARGET_ORDER, null)
            set i = i +1
            exitwhen (i == bj_MAX_PLAYER_SLOTS)
        endloop
        set i = 0
        
        //Issue a point order
        set t = CreateTrigger()
        call TriggerAddAction(t, function PointOrder_Actions)
        loop
            call TriggerRegisterPlayerUnitEvent(t, Player(i), EVENT_PLAYER_UNIT_ISSUED_POINT_ORDER, null)
            set i = i +1
            exitwhen (i == bj_MAX_PLAYER_SLOTS)
        endloop
        set i = 0
        
        //Issue an order with no target
        set t = CreateTrigger()
        call TriggerAddAction(t, function VoidOrder_Actions)
        loop
            call TriggerRegisterPlayerUnitEvent(t, Player(i), EVENT_PLAYER_UNIT_ISSUED_ORDER, null)
            set i = i +1
            exitwhen (i == bj_MAX_PLAYER_SLOTS)
        endloop
        
    endfunction
    
    
    
//***********************************************************
//* CASTING SYSTEM

    globals
        private HandleTable     UNIT_TABLE
        private trigger         STOP_TRIGGER
    endglobals
    
    
    interface IJCast
        //Optional methods
        method CanCast takes nothing returns boolean                    defaults true
        method CanBeInterrupted takes nothing returns boolean           defaults true
        
        method GetCastTime takes nothing returns real                   defaults 0.
        method GetChannelTime takes nothing returns real                defaults 0.
        method GetChannelTickTime takes nothing returns real            defaults 0.
        
        method ResumeAttack takes nothing returns boolean               defaults false
        method RefundManaOnCancel takes nothing returns boolean         defaults true
        
        method OnBegin takes nothing returns nothing                  defaults nothing
        method OnFail takes nothing returns nothing                     defaults nothing
        method OnPreCast takes nothing returns nothing                  defaults nothing
        
        method OnCastStart takes nothing returns nothing                defaults nothing
        method OnCastEnd takes nothing returns nothing                  defaults nothing
        method OnEffect takes nothing returns nothing                   defaults nothing
        method OnChannelStart takes nothing returns nothing             defaults nothing
        method OnChannelTick takes nothing returns nothing              defaults nothing
        method OnChannelEnd takes nothing returns nothing               defaults nothing
        method OnEnd takes nothing returns nothing                      defaults nothing
        
        //Automatically implemented by JCastModule
        method destroy takes nothing returns nothing
        method interrupt takes trigger t returns nothing
        method pushback takes nothing returns nothing
    endinterface



module JCastModule
//----------------------------------------------------------------------
// Private Members
    
    //States
    private static constant integer     STATE_INIT      = 0
    private static constant integer     STATE_FAIL      = 1 //only if the cast fails
    private static constant integer     STATE_CAST      = 2 //skipped if no cast time
    private static constant integer     STATE_EFFECT    = 3
    private static constant integer     STATE_CHANNEL   = 4 //skipped if no channel time
    private static constant integer     STATE_INTERRUPT = 5
    private static constant integer     STATE_END       = 6
    
    private static method getStateName takes integer state returns string
        if (state == STATE_INIT) then
            return &quot;INIT&quot;
        elseif (state == STATE_FAIL) then
            return &quot;FAIL&quot;
        elseif (state == STATE_CAST) then
            return &quot;CAST&quot;
        elseif (state == STATE_EFFECT) then
            return &quot;EFFECT&quot;
        elseif (state == STATE_CHANNEL) then
            return &quot;CHANNEL&quot;
        elseif (state == STATE_INTERRUPT) then
            return &quot;INTERRUPT&quot;
        elseif (state == STATE_END) then
            return &quot;END&quot;
        endif
        return &quot;&quot;
    endmethod
    
    //Core properties
    private timer               m_timer                 = null
    private integer             m_state                 = STATE_INIT
    private boolean             m_isRegistered          = false
    private boolean             m_isCast                = false
    private real                m_progress              = 0.
    private real                m_tickProgress          = 0.
    private integer             m_pushCount             = 0
    private real                m_manaCostTemp          //used to calculate spell&#039;s manacost
    private real                m_manaCost              = 0.
    
    //Properties loaded from custom struct&#039;s definition
    private boolean             m_manaRefundOnCancel
    private unit                m_attackTarget
    private boolean             m_resumeAttack
    private real                m_castTime
    private real                m_channelTime
    private real                m_channelTickTime
    
    //Spell&#039;s event response properties
    private unit                m_caster
    private integer             m_level
    private real                m_targetX
    private real                m_targetY  
    private unit                m_targetUnit
    private destructable        m_targetDest
    private item                m_targetItem
    
    
//----------------------------------------------------------------------
// Constructor and Destructor
    
    //Constructor is defined as private to prevent wild allocation of this struct
    private static method create takes nothing returns thistype
        return 0
    endmethod
    
    //Simple destructor <img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" class="smilie smilie--sprite smilie--sprite1" alt=":)" title="Smile    :)" loading="lazy" data-shortname=":)" />
    private method destroy takes nothing returns nothing
        //If the cast is not complete, give the mana back to the caster
            if (not .m_isCast) and (.m_manaCost &gt; 0) and .m_manaRefundOnCancel then
                call SetUnitState(.m_caster, UNIT_STATE_MANA, GetUnitState(.m_caster,UNIT_STATE_MANA) + .m_manaCost)
            endif
        //Clear instance
            if (.m_timer != null) then
                call ReleaseTimer( .m_timer )
            endif
        call this.deallocate()
    endmethod
    
//----------------------------------------------------------------------
// Private Helpers

    private method setState takes integer newState returns nothing
        static if LIBRARY_JDebug then
        call JDebug_LibMsg(&quot;JCast(&quot;+I2S(this)+&quot;)&quot;, &quot;State changed from &quot;+I2S(.m_state)+&quot;:&quot;+.getStateName(.m_state)+&quot; to &quot;+I2S(newState)+&quot;:&quot;+.getStateName(newState))
        endif
        set .m_state = newState
    endmethod
    
    private method getState takes nothing returns integer
        return .m_state
    endmethod
    
    
    method pushback takes nothing returns nothing
        static if thistype.ApplyCastPushback.exists then
        if (getState() == STATE_CAST) then
            set .m_pushCount = .m_pushCount + 1
            set .m_progress = ApplyCastPushback(.m_progress, .m_castTime, .m_pushCount)
            return
        endif
        else
        //No casting pushback!
        endif
        static if thistype.ApplyChannelPushback.exists then
        if (getState() == STATE_CHANNEL) then
            set .m_pushCount = .m_pushCount + 1
            set .m_progress = ApplyChannelPushback(.m_progress, .m_channelTime, .m_pushCount)
        endif
        else
        //No channeling pushback!
        endif
    endmethod
    
    
//----------------------------------------------------------------------
// Public Helpers
    
    method GetLevel takes nothing returns integer
        return .m_level
    endmethod
    
    method GetCaster takes nothing returns unit
        return .m_caster
    endmethod
    
    method GetAttackTarget takes nothing returns unit
        return .m_attackTarget
    endmethod
    
    method GetTargetX takes nothing returns real
        return .m_targetX
    endmethod
    
    method GetTargetY takes nothing returns real
        return .m_targetY
    endmethod
    
    method GetTargetUnit takes nothing returns unit
        return .m_targetUnit
    endmethod
    
    method GetTargetItem takes nothing returns item
        return .m_targetItem
    endmethod
    
    method GetTargetDestructable takes nothing returns destructable
        return .m_targetDest
    endmethod
    
    method GetManaCost takes nothing returns real
        return .m_manaCost
    endmethod
    
    method IsCast takes nothing returns boolean
        return .m_isCast
    endmethod
    
    static constant method GetUpdateRate takes nothing returns real
        return TIMER_THRESHOLD
    endmethod
    
    
//----------------------------------------------------------------------
// STATE : End
    
    private method stopAttackContinuation takes nothing returns nothing
        call IssueTargetOrderById(m_caster, 851983 /*attack*/, .m_attackTarget)
        call destroy()
    endmethod
    private static method stopAttackContinuation_Callback takes nothing returns nothing
        call thistype(GetTimerData(GetExpiredTimer())).stopAttackContinuation()
    endmethod
    
    private method doEnd takes nothing returns nothing
        //Execute custom event
            if (getState() != STATE_FAIL) then
                call OnEnd()
            endif
        //Change state
            call setState(STATE_END)
        //If the spell is registered, we have to stop the spell
        //(prevent the unit from keeping channeling if the spell&#039;s based on Channel that has a follow through time)
        //If the spell uses attack continuation, order the caster to attack with a tiny delay (else 851973 order won&#039;t stop the cast)
        if .m_resumeAttack then
            call TimerStart(m_timer, 0.00, false, function thistype.stopAttackContinuation_Callback)
            return
        endif
        //else destroy the spell instance
        call destroy()
    endmethod
    
    private method doFail takes nothing returns nothing
        //Stop the WC3 spell
            call IssueImmediateOrderById(m_caster, 851973 /*stunned*/)
        //Change state from STATE_INIT
            call setState(STATE_FAIL)
        //Execute custom event
            call OnFail()
        //Stop the spell before mana and cooldown are consumed
            if (not .m_isRegistered) then
                call doEnd()
            endif
    endmethod
    
//----------------------------------------------------------------------
// STATE : Channel
    
    private method cleanChannel takes nothing returns nothing
        call PauseTimer(m_timer)
        //if .m_showChannelBar then
        //    call channelBarStop(m_caster)
        //endif
        call OnChannelEnd()
    endmethod
    
    private method channelTerminate takes nothing returns nothing
        //Clear channeling data
            call cleanChannel()
        //Jump to STOP state
            if .m_isRegistered then
                call IssueImmediateOrderById(m_caster, 851973 /*stunned*/)
            else
                call doEnd()
            endif
    endmethod
    
    private method channelUpdate takes nothing returns nothing
        //If custom spell&#039;s conditions are not met, end the channeling
            if (not CanCast()) then
                call channelTerminate()
                return
            endif
        //Update channeling and tick progress
            set .m_progress     = .m_progress     + TIMER_THRESHOLD
            set .m_tickProgress = .m_tickProgress + TIMER_THRESHOLD
        //Execute custom update method
            static if thistype.OnChannelUpdate.exists then
            call OnChannelUpdate(.m_progress / .m_channelTime)
            else
            //No update method call (OnChannelUpdate() method is not found)
            endif
        //If we reached a tick, execute tick event
            if (m_channelTickTime &gt; 0.) and (m_tickProgress &gt;= .m_channelTickTime) then
                set .m_tickProgress = .m_tickProgress - .m_channelTickTime
                //Execute custom event
                    call OnChannelTick()
                //Check again spell&#039;s conditions
                    if (not CanCast()) then
                        call channelTerminate()
                        return
                    endif
                //Update channel tick period
                    set .m_channelTickTime = .GetChannelTickTime()
            endif
        //If channeling is complete, terminate.
            if (m_progress &gt;= .m_channelTime) then
                call channelTerminate()
            endif
    endmethod
    
    private static method channelUpdate_Callback takes nothing returns nothing
        call thistype(GetTimerData(GetExpiredTimer())).channelUpdate()
    endmethod
    
    
    private method doChannel takes nothing returns nothing
        //Change state from STATE_EFFECT
            call setState(STATE_CHANNEL)
        //Execute custom event and start channeling
            call OnChannelStart()
            call TimerStart(m_timer, TIMER_THRESHOLD, true, function thistype.channelUpdate_Callback)
    endmethod
            
            
//----------------------------------------------------------------------
// STATE : Effect
    
    private method doEffect takes nothing returns nothing
        //Change state from STATE_START or STATE_CAST
            call setState(STATE_EFFECT)
        //Mana is now irreparably lost
            set .m_isCast = true
        //If the spell do have a channel time, start channeling
            if (m_channelTime &gt; 0.) then
                call OnEffect()
                call doChannel()
        //The cast is complete, then execute custom event and stop the spell
            else
                if .m_isRegistered then
                    call IssueImmediateOrderById(m_caster, 851973 /*stunned*/)  //stop WC3 spell casting
                    call OnEffect()
                else
                    call OnEffect()
                    call doEnd()
                endif
            endif
    endmethod
    
    
//----------------------------------------------------------------------
// STATE : Cast
    
    private method cleanCast takes nothing returns nothing
        call PauseTimer(m_timer)
        set .m_progress = 0.
        set .m_pushCount = 0
        call OnCastEnd()
    endmethod
    
    private method castTerminate takes nothing returns nothing
        //Clear casting data
            call cleanCast()
        //If custom spell&#039;s conditions are met, go to the EFFECT state or stop the spell
            if CanCast() then
                call doEffect()
            else
                call doFail()
            endif
    endmethod
    
    private method castUpdate takes nothing returns nothing
        //Update precast progress
            set .m_progress = .m_progress + TIMER_THRESHOLD
        //Execute custom update method
            static if thistype.OnCastUpdate.exists then
            call OnCastUpdate(.m_progress / .m_castTime)
            else
            //No update method call (OnCastUpdate() method is not found)
            endif
        //If progress is complete, terminate.
            if (m_progress &gt;= .m_castTime) then
                call castTerminate()
            endif
    endmethod
    private static method castUpdate_Callback takes nothing returns nothing
        call thistype(GetTimerData(GetExpiredTimer())).castUpdate()
    endmethod
    
    private method doCast takes nothing returns nothing
        //Change state from STATE_INIT
            call setState(STATE_CAST)
        //Execute custom event and start casting
            call OnCastStart()
            call TimerStart(m_timer, TIMER_THRESHOLD, true, function thistype.castUpdate_Callback)
    endmethod
    
    
//----------------------------------------------------------------------
// Initialization
//
//  This system automatically handles Warcraft3&#039;s casting events.
//  To do that, a trigger is created at struct initialization.
//  
//  Each time the spell is cast by a unit, an instance of this structure
//  is created and proceeded.
//
//----------------------------------------------------------------------
    
    private method initTerminate takes nothing returns nothing
        //Execute custom event
            call OnPreCast()
        //If the spell do have a cast time, start casting
            if (m_castTime &gt; 0.) then
                call doCast()
        //Else skip the CAST state and jump to the EFFECT state
            else
                call doEffect()
            endif
    endmethod
    
    private method initAttackContinuation takes nothing returns nothing
        set .m_resumeAttack = false
        call IssueTargetOrderById(m_caster, 851983 /*attack*/, .m_attackTarget)
        call initTerminate()
    endmethod
    private static method initAttackContinuation_Callback takes nothing returns nothing
        call thistype(GetTimerData(GetExpiredTimer())).initAttackContinuation()
    endmethod
    
    private method initFull takes nothing returns nothing
        //Calculate spell&#039;s manacost
            set .m_manaCost = .m_manaCostTemp - GetUnitState(m_caster, UNIT_STATE_MANA)
        //If the spell is not registered, the caster should not be involved in the cast then stop his current order
        //(prevent the unit from keeping channeling if the spell&#039;s based on Channel that has a follow through time)
            if (not .m_isRegistered) then
                call IssueImmediateOrderById(m_caster, 851973 /*stunned*/)
                //If the spell uses attack continuation, order the caster to attack with a tiny delay (else 851973 order won&#039;t stop the cast)
                if .m_resumeAttack then
                    call TimerStart(m_timer, 0.00, false, function thistype.initAttackContinuation_Callback)
                    return
                endif
            endif
        //Go to the casting phase
            call initTerminate()
    endmethod
    private static method initFull_Callback takes nothing returns nothing
        call thistype(GetTimerData(GetExpiredTimer())).initFull()
    endmethod
    
    
    private static method init_Conditions takes nothing returns boolean
        return (GetSpellAbilityId() == thistype.AbilityId)
    endmethod
    
    private static method init_Actions takes nothing returns nothing
    local thistype this = thistype.allocate()
        //Initialization of core data (availaible in struct&#039;s methods through the use of getters)
            set this.m_manaCost = 0.
            set this.m_caster   = GetSpellAbilityUnit()
            set this.m_level    = GetUnitAbilityLevel(this.m_caster, GetSpellAbilityId())
            set this.m_targetX  = GetSpellTargetX()
            set this.m_targetY  = GetSpellTargetY()
            set this.m_targetUnit = GetSpellTargetUnit()
            set this.m_targetDest = GetSpellTargetDestructable()
            set this.m_targetItem = GetSpellTargetItem()
            set this.m_attackTarget = sTargetManager.GetTarget(this.m_caster)
            set this.m_timer = NewTimer()
            call SetTimerData(this.m_timer, this)
            call this.OnBegin()
            set this.m_resumeAttack = this.ResumeAttack() and (this.m_attackTarget != null)
        //If custom spell&#039;s conditions are met, start the cast
            if this.CanCast() then
                //Gather more required informations about the spell
                    set this.m_manaCostTemp           = GetUnitState(this.m_caster, UNIT_STATE_MANA)
                    set this.m_manaRefundOnCancel     = this.RefundManaOnCancel()
                    set this.m_castTime               = this.GetCastTime()
                    set this.m_channelTime            = this.GetChannelTime()
                    set this.m_channelTickTime        = this.GetChannelTickTime()
                //If the spell is interruptable and has a casting or channeling time, register this instance
                    if this.CanBeInterrupted() and ((this.m_castTime &gt; 0.) or (this.m_channelTime &gt; 0.)) then
                        set this.m_isRegistered = true
                        set UNIT_TABLE[this.m_caster] = integer(this)
                    endif
                //We are at EVENT_PLAYER_UNIT_SPELL_ENDCAST stage and neither cooldown nor manacost was applied yet,
                //then wait for a very short period to calculate the spell&#039;s manacost
                    call TimerStart(this.m_timer, 0.00, false, function thistype.initFull_Callback)
        //Else abort the cast
            else
                call this.doFail()
            endif
            
    endmethod
    
    
//----------------------------------------------------------------------
//
//
//
//----------------------------------------------------------------------
    
    //Note: I needed a public (since there is no protected keyword in vJass) method to be call by system&#039;s STOP_TRIGGER.
    //However, allowing users to call this method from inside the struct (although it may seem interesting) would be
    //contrary to the way I designed the system ; spell&#039;s control should be done through CanCast() method only.
    method interrupt takes trigger t returns nothing
        call PauseTimer(m_timer)
        //Check if the call is legitimate
            if (t != STOP_TRIGGER) then
                static if LIBRARY_JDebug then
                call JDebug_LibError( &quot;JCast&quot;, &quot;Trying to terminate a cast via interrupt() is not allowed, use CanCast() method instead&quot; )
                endif
                return
            endif
        //Unregister the caster
            call UNIT_TABLE.flush(.m_caster)
        //Interrupt the spell
            if (getState() == STATE_CAST) then
                call cleanCast()
            elseif (getState() == STATE_CHANNEL) then
                call cleanChannel()
            else
                return // do nothing if we are in STATE_STOP
            endif
        //Change state and stop the spell
            call setState(STATE_INTERRUPT)
            call doEnd()
    endmethod
    
    
//----------------------------------------------------------------------
//
//
//
//----------------------------------------------------------------------
    
    private static method onInit takes nothing returns nothing
    local trigger t
        //Check if the specified ability exists or already registered
            if (thistype.AbilityId &lt;= 0) or (GetObjectName(thistype.AbilityId) == &quot;Default string&quot;) then
                return
            endif
        //Create a trigger to detect the cast of this spell
            set t = CreateTrigger()
            call TriggerAddAction(t, function thistype.init_Actions)
            call TriggerAddCondition(t, Condition(function thistype.init_Conditions) )
            call TriggerRegisterPlayerUnitEvent(t, Player(0),  EVENT_PLAYER_UNIT_SPELL_EFFECT, null)
            call TriggerRegisterPlayerUnitEvent(t, Player(1),  EVENT_PLAYER_UNIT_SPELL_EFFECT, null)
            call TriggerRegisterPlayerUnitEvent(t, Player(2),  EVENT_PLAYER_UNIT_SPELL_EFFECT, null)
            call TriggerRegisterPlayerUnitEvent(t, Player(3),  EVENT_PLAYER_UNIT_SPELL_EFFECT, null)
            call TriggerRegisterPlayerUnitEvent(t, Player(4),  EVENT_PLAYER_UNIT_SPELL_EFFECT, null)
            call TriggerRegisterPlayerUnitEvent(t, Player(5),  EVENT_PLAYER_UNIT_SPELL_EFFECT, null)
            call TriggerRegisterPlayerUnitEvent(t, Player(6),  EVENT_PLAYER_UNIT_SPELL_EFFECT, null)
            call TriggerRegisterPlayerUnitEvent(t, Player(7),  EVENT_PLAYER_UNIT_SPELL_EFFECT, null)
            call TriggerRegisterPlayerUnitEvent(t, Player(8),  EVENT_PLAYER_UNIT_SPELL_EFFECT, null)
            call TriggerRegisterPlayerUnitEvent(t, Player(9),  EVENT_PLAYER_UNIT_SPELL_EFFECT, null)
            call TriggerRegisterPlayerUnitEvent(t, Player(10), EVENT_PLAYER_UNIT_SPELL_EFFECT, null)
            call TriggerRegisterPlayerUnitEvent(t, Player(11), EVENT_PLAYER_UNIT_SPELL_EFFECT, null)
            //Note: EVENT_PLAYER_UNIT_SPELL_CHANNEL : fired whenever a spell is cast, this is the first spell&#039;s event
            //      EVENT_PLAYER_UNIT_SPELL_EFFECT  : fired just before mana is removed and cooldown started (if the cast was not canceled during war3&#039;s casting time)
            //      EVENT_PLAYER_UNIT_SPELL_FINISH  : fired only when War3&#039;s cast is complete, interrupted spells won&#039;t fire this event
            //      EVENT_PLAYER_UNIT_SPELL_ENDCAST : fired whenever a spell is stopped, even if it was canceled
        //Execute custom initialization event
            static if thistype.OnInit.exists then
            call thistype.OnInit()
            else
            //No init method call (OnInit() method is not found)
            endif
    endmethod
    
endmodule


//***********************************************************
//* LIBRARY INITIALIZER
//* A global trigger is used to catch SPELL_ENDCAST event, and stop current
//* cast spell instance.
//*
    
    private function endCast_Actions takes nothing returns nothing
        if UNIT_TABLE.exists(GetSpellAbilityUnit()) then
            call IJCast(UNIT_TABLE[GetSpellAbilityUnit()]).interrupt(GetTriggeringTrigger()) //Destroy current cast instance
        endif
    endfunction
    
    private function init takes nothing returns nothing
        set STOP_TRIGGER = CreateTrigger()
        call TriggerAddAction(STOP_TRIGGER, function endCast_Actions )
        call TriggerRegisterPlayerUnitEvent(STOP_TRIGGER, Player(0), EVENT_PLAYER_UNIT_SPELL_ENDCAST, null)
        call TriggerRegisterPlayerUnitEvent(STOP_TRIGGER, Player(1), EVENT_PLAYER_UNIT_SPELL_ENDCAST, null)
        call TriggerRegisterPlayerUnitEvent(STOP_TRIGGER, Player(2), EVENT_PLAYER_UNIT_SPELL_ENDCAST, null)
        call TriggerRegisterPlayerUnitEvent(STOP_TRIGGER, Player(3), EVENT_PLAYER_UNIT_SPELL_ENDCAST, null)
        call TriggerRegisterPlayerUnitEvent(STOP_TRIGGER, Player(4), EVENT_PLAYER_UNIT_SPELL_ENDCAST, null)
        call TriggerRegisterPlayerUnitEvent(STOP_TRIGGER, Player(5), EVENT_PLAYER_UNIT_SPELL_ENDCAST, null)
        call TriggerRegisterPlayerUnitEvent(STOP_TRIGGER, Player(6), EVENT_PLAYER_UNIT_SPELL_ENDCAST, null)
        call TriggerRegisterPlayerUnitEvent(STOP_TRIGGER, Player(7), EVENT_PLAYER_UNIT_SPELL_ENDCAST, null)
        call TriggerRegisterPlayerUnitEvent(STOP_TRIGGER, Player(8), EVENT_PLAYER_UNIT_SPELL_ENDCAST, null)
        call TriggerRegisterPlayerUnitEvent(STOP_TRIGGER, Player(9), EVENT_PLAYER_UNIT_SPELL_ENDCAST, null)
        call TriggerRegisterPlayerUnitEvent(STOP_TRIGGER, Player(10), EVENT_PLAYER_UNIT_SPELL_ENDCAST, null)
        call TriggerRegisterPlayerUnitEvent(STOP_TRIGGER, Player(11), EVENT_PLAYER_UNIT_SPELL_ENDCAST, null)
        set UNIT_TABLE = HandleTable.create()
        call initLastTarget()
    endfunction



//===========================================================================
//* PUBLIC FUNCTIONS
//===========================================================================

    function JCast_IsCasting takes unit un returns boolean
        return ((un != null) and UNIT_TABLE.exists(un))
    endfunction
    
    function JCast_Pushback takes unit target returns nothing
        if (target != null) and UNIT_TABLE.exists(target) then
            call IJCast(UNIT_TABLE[target]).pushback()
        endif
    endfunction
    
    function JCast_Interrupt takes unit target returns nothing
        if (target != null) and UNIT_TABLE.exists(target) then
            call IssueImmediateOrderById(target, 851972 /*stop*/)
        endif
    endfunction
    
    function JCast_RegisterTargetWatch takes unit watchedUnit returns nothing
        if (watchedUnit == null) or TARGET_TABLE.exists(watchedUnit) then
            return
        endif
        set TARGET_TABLE[watchedUnit] = integer(sTargetManager.create(watchedUnit))
    endfunction
    
    
endlibrary
 

Attachments

  • JCast_demo_v02.w3x
    108.4 KB · Views: 344

Romek

Super Moderator
Reaction score
963
Post a demo map please.
 

Nestharus

o-o
Reaction score
84
Looks darn nice ; o, although it might have been nicer to split it into multiple resources and provide the main system as a manager to manage it all.
 

PurgeandFire

zxcvmkgdfg
Reaction score
509
That is awesome. Very creative. :thup: I like how you implemented push back as well, most casting systems neglect that. I love the usage of the xp bar. ;D
 

profet

New Member
Reaction score
7
Post a demo map please.
It will be available soon because the only test map I have yet, is the dev map of my own project, and it requires a lot of custom assets :)

Looks darn nice ; o, although it might have been nicer to split it into multiple resources and provide the main system as a manager to manage it all.
About what kind of sub-systems separation do you mean?
 

Nestharus

o-o
Reaction score
84
About what kind of sub-systems separation do you mean?

With a more normal design, each resource within a system is split into its own smaller resource or snippet that can be used for any purpose. The manager sort of brings this all together to form a very specific purpose = ).

For example, the cast bar thing might be its own system = ).

I'm just talking pretty standard design here, which is virtually never used in wc3 systems, so you can take what I'm saying with a grain of salt = ). After all, if people were to do that, one system might be spread across 30 threads, lol.
 

profet

New Member
Reaction score
7
Yeah I know what is a manager and layered-design but i didn't get how you would have split the system.

If you look carefully at the code, you can see that the castbar is fully customizable. I didn't want to separate it in a different system because it is a really tiny feature :)
 

SanKakU

Member
Reaction score
21
i never frown at another example of coding. it looks like a bunch of stuff that you wrote for your own use, and i like that. i may not use your system, but i bet i'll learn a lot from reading through it. whether i use it or not, i'll benefit greatly from your contribution.
 

profet

New Member
Reaction score
7
As promised, a little demo map (attached to the first post) :)

Changes:
- Castbar can now be specified inside the struct itself, instead of in the six setting functions (it gives the possibility to have different castbars for each spells)
- Pushback effect is no longer defined through setting constants, two new overridable methods (ApplyCastPushback and ApplyChannelPushback) are now available to modify the progress value.
- Added test map and system documentation
 

PurgeandFire

zxcvmkgdfg
Reaction score
509
Nice demo. :) Although, there is a double-free when finishing the life drain, of both the timer and the instance.
 

profet

New Member
Reaction score
7
I finally found the time to update the system. This one should be the final version, then let me know if you want some changes before I submit it as resource :)

Changelogs:
- Fixed a bug making uninterruptable spells to be interrupted under certain conditions.
- Renamed OnBegin method (to OnPreCast)
- Added a new event method OnBegin (see documentation for more details)
- Fixed the double-free with channeled spells

(first post updated)
 

Tom_Kazansky

--- wraith it ! ---
Reaction score
157
man, very nice. :thup:

I'm gonna use the idea of xp bar usage. :D

so, I will credit to you as profet?
 

SanKakU

Member
Reaction score
21
I'm curious as to why this breaks methodNamingConvetion of vJass..
We inherit that convention from the JassHelper Manual, and it makes code more intuitive to follow it.

you mean you want him to use camel case or whatever?

seems like a silly thing to pick on, if you ask me...

i mean, it's just for human's eyes, right? i don't know, that's why i'm asking, lol.

obviously there are lots of humans on this forum but people tell me i'm a monster so maybe i don't qualify to pick on it.
 

Jesus4Lyf

Good Idea™
Reaction score
397
it makes code more intuitive
I did state why - when code follows a predictable (standard) pattern, it makes the API easier to use because it is more intuitive, and also keeps code neat. If you use a different naming convention for every method or function, it suddenly gets hard to use anything. :(
 
General chit-chat
Help Users
  • No one is chatting at the moment.

      The Helper Discord

      Members online

      No members online now.

      Affiliates

      Hive Workshop NUON Dome World Editor Tutorials

      Network Sponsors

      Apex Steel Pipe - Buys and sells Steel Pipe.
      Top