HomeUser Control Panel (unavailable in archive)ForumsTutorialsArt GalleryResourcesMaps

JCast (advanced casting-system)

06-24-2010, 11:34 AM#1
ProFeT
Hi folks,

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 before releasing it as a resource 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:
Collapse 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:
Collapse 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
Collapse 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
Collapse 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, [url]http://www.wc3c.net/showthread.php?t=101246[/url] )
//*   TimerUtils (by Vexorian, [url]http://www.wc3c.net/showthread.php?t=101322[/url] )
//*
//*  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's configuration variables and functions.
//  Feel free to modify them to suit your needs !
//===========================================================================
globals
    
    //Period of the spell's update timer, keeping it as low as possible increases precision but lowers system'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'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 "attack" or "smart" 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 "stop" nor "holdposition"
    // the target is flagged as "previous target" 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
        //-> Point order or "Stop"
            if (.m_target != null) then
                set .m_target = null
                //set .m_current = false
            endif
        endmethod
        
        method Set takes unit target returns nothing
        //-> "Attack" or "Smart" order
            if (target != null) then
                set .m_target = target
                set .m_current = true
            endif
        endmethod
        
        method Unset takes nothing returns nothing
        //-> 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) > .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 "Attack" order (or "Smart" 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 "INIT"
        elseif (state == STATE_FAIL) then
            return "FAIL"
        elseif (state == STATE_CAST) then
            return "CAST"
        elseif (state == STATE_EFFECT) then
            return "EFFECT"
        elseif (state == STATE_CHANNEL) then
            return "CHANNEL"
        elseif (state == STATE_INTERRUPT) then
            return "INTERRUPT"
        elseif (state == STATE_END) then
            return "END"
        endif
        return ""
    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's manacost
    private real                m_manaCost              = 0.
    
    //Properties loaded from custom struct'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'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 :)
    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 > 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("JCast("+I2S(this)+")", "State changed from "+I2S(.m_state)+":"+.getStateName(.m_state)+" to "+I2S(newState)+":"+.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'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'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'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 > 0.) and (m_tickProgress >= .m_channelTickTime) then
                set .m_tickProgress = .m_tickProgress - .m_channelTickTime
                //Execute custom event
                    call OnChannelTick()
                //Check again spell'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 >= .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 > 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'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 >= .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'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 > 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'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'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'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'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'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 > 0.) or (this.m_channelTime > 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'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'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'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( "JCast", "Trying to terminate a cast via interrupt() is not allowed, use CanCast() method instead" )
                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 <= 0) or (GetObjectName(thistype.AbilityId) == "Default string") 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's event
            //      EVENT_PLAYER_UNIT_SPELL_EFFECT  : fired just before mana is removed and cooldown started (if the cast was not canceled during war3's casting time)
            //      EVENT_PLAYER_UNIT_SPELL_FINISH  : fired only when War3's cast is complete, interrupted spells won'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
Attached Files
File type: w3xJCast_demo_v02.w3x (108.4 KB)
06-24-2010, 04:18 PM#2
0zyx0
I took a quick look at the system, and I noticed it contains quite a lot of features, some of them which aren't always needed. For improving the execution time, the user should be able to set some configurable flags which disables certain features, through the use of static ifs.
06-24-2010, 05:10 PM#3
ProFeT
Quote:
Originally Posted by 0zyx0
I took a quick look at the system, and I noticed it contains quite a lot of features, some of them which aren't always needed. For improving the execution time, the user should be able to set some configurable flags which disables certain features, through the use of static ifs.
Examples? :)
06-25-2010, 04:56 AM#4
Ignitedstar
Ooooooh, this is awesome. I've gotta try this.

Are you going to submit this as a system or sorts?
06-25-2010, 05:37 AM#5
Bribe
That tent-entering is EPIC, EPIC, EPIC win!
06-25-2010, 07:19 AM#6
ProFeT
Quote:
Are you going to submit this as a system or sorts?
Yes, I posted it in the Triggers & Script section because I think it needs some changes, but once remarks and modifications done I'll submit this in the Systems forum :)
I also have to make a clean demo map.

Other J-systems will follow very soon.
06-25-2010, 08:31 AM#7
Archmage Owenalacaster
Additional features that are not essential to the core functionality, like pushback, ought to be separate; you could place them in supplemental libraries and include optional require requirements and static ifs in the core. This promotes modularity.
06-25-2010, 10:17 AM#8
ProFeT
Quote:
Originally Posted by Archmage Owenalacaster
Additional features that are not essential to the core functionality, like pushback, ought to be separate; you could place them in supplemental libraries and include optional require requirements and static ifs in the core. This promotes modularity.

The question of separating systems' functionalities is complex and I carefully considered it about JCast, for example I first meant the castbar to be an external lib.
Before everything, I believe that splitting a system must be justified by interchangeability and in the case of the castbar, it turns out that a simple setting function would be enough to allow a total freedom of behaviour.

Making pushback a separated lib would be justified if it could be used by another system aswell. However, I could expose it as an overloadable struct to allow more customization/flexibility (but separated objects often mean speed counterparts).
Besides, some "optional" things need to be integrated into JCast code, that would be lame with separated libs.

Concerning "optimization" (using configuration variables and static ifs), I'm torn about the right decision, but...
The main rule that leads me when designing gameplay features is making it SIMPLE (to keep user focused on gameplay).
For the cost of few if-tests each time a spell is cast, I free the user from guessing if he needs or not particular features, the choice lays only in using or not functions (like pushback, that is completely transparent if you don't use the pushback function).

That's why I choosed to curb the speadfreak attitude :)
06-25-2010, 12:03 PM#9
Anitarf
Some of your features are far too specific to make sense as a part of the core functions set. For example, pushback: the core features should only include a function/method for changing the time remaining on a cast. How that time is changed is already a map-specific issue and your current setup doesn't cover all the possibilities and since it is mixed in with the rest of the system, it is difficult to change.

The design of casting bars is also a map-specific issue that you can't cover with just a few calibration constants. Again, it should be a separate library so users can tweak/replace it.

Furthermore, the system is large enough that breaking it up into subsystems makes sense from an organisational standpoint regardless.
06-25-2010, 01:01 PM#10
ProFeT
Quote:
Originally Posted by Anitarf
The design of casting bars is also a map-specific issue that you can't cover with just a few calibration constants. Again, it should be a separate library so users can tweak/replace it.
The castbar is not really a problem since JCast only gives six event fonctions, wich are supplied with caster and progress value, thus you are totaly free to use any custom-bar library you like :)


Quote:
Originally Posted by Anitarf
Some of your features are far too specific to make sense as a part of the core functions set. For example, pushback: the core features should only include a function/method for changing the time remaining on a cast. How that time is changed is already a map-specific issue and your current setup doesn't cover all the possibilities and since it is mixed in with the rest of the system, it is difficult to change.
That's true, and I'm not hostile to bring modifications to pushback.
However I'm not sure of the need to externalize this mechanism rather than modifying the functioning of GetPushbackCastPenalty() method because the overloading-based design of JCast already provides the freedom anyone needs (that is the core point of the system).

EDIT: for example I could modify the GetPushbackCastPenalty to pass it the current spell progress as argument and make it return the modified time. That would allow us more freedom in using value or factor modifiers.
06-25-2010, 03:02 PM#11
Ignitedstar
Quote:
Originally Posted by ProFeT
...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.
Grrr, you...! If only you had splattered this all over the floor earlier.

Thanks to Anitarf for being more specific. I would have blown up.

But still... I don't see how something like knockback is an issue at all. Considering that the maximum map size for a hosted map has increased (for DotA) and how it seemed many people seem to boast that a few lines of code doesn't add too much space, having a built-in knockback into a system with dynamic usage over a spell's insides doesn't seem to matter. Everyone knows the standard for a knockback function- hell, it's probably one of the most asked for functions since the beginning of modding for Warcraft 3. I don't see how this any better or any worse than any knockback function made. I hope this isn't a matter of people's pride.

I'm actually glad that this has a built-in knockback function. Consistency is the idea- if I'm going to have one spell use this system, I'd rather have ALL spells use this system. It IS a system. I suppose I don't see how this is a map specific issue because you should be considering to use this when you have a fresh map that doesn't have anything this system has. I think that, because I sooooo want to use this, but my map is too far in development that I would have to re-haul everything to implement this. Plus, I had to find a lot of work-arounds to kind of do what this system does. This puts many things that I would to have already in there (not a complaint; a relief). Needless to say, when I get an opportunity to utilize this, I dare say that I wouldn't try and have a blast with it. I'm tempted to toy with it, too. <3

If anything, what we should be concerned about is compatibility with other systems, since many maps usually have more than one.
06-25-2010, 03:28 PM#12
ProFeT
Ignitedstar, I'm really sorry for been so long to release it =)

Also, you may have misunderstood the meaning of "pushback", it's not refering to knockback (wich relies on my JPhysic system) but to wow's spells pushback effect:

Quote:
Originally Posted by WowWiki
Whenever a damaging spell or ability hits the caster, the cast time will be set back by a given amount, slowing the casting. In general any melee attack (be it a normal or a special attack) or direct damage spell will cause interruption. Damage over Time spells and most Channelled spells will not cause interruption.

:)
06-25-2010, 05:35 PM#13
Viikuna-
Quote:
Originally Posted by Anitarf

The design of casting bars is also a map-specific issue that you can't cover with just a few calibration constants. Again, it should be a separate library so users can tweak/replace it.


Someone might wanna use Deaods image bars for casting bars, for example. ( Because those are pretty cool. )

This just crossed my mind.
06-25-2010, 06:32 PM#14
ProFeT
I didn't use calibration constants, thus it is possible to use Deaods image bars as well :)
06-25-2010, 08:33 PM#15
Ignitedstar
Quote:
Originally Posted by ProFeT
Ignitedstar, I'm really sorry for been so long to release it =)

Also, you may have misunderstood the meaning of "pushback", it's not refering to knockback (wich relies on my JPhysic system) but to wow's spells pushback effect:

:)

Ooooo, nifty. In that case, I really don't see how this causes any map issues. All of it can be easily turned off, it seems.