| 06-24-2010, 11:34 AM | #1 |
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: 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, [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 |
| 06-24-2010, 04:18 PM | #2 |
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 | |
Quote:
|
| 06-25-2010, 04:56 AM | #4 |
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 |
That tent-entering is EPIC, EPIC, EPIC win! |
| 06-25-2010, 07:19 AM | #6 | |
Quote:
I also have to make a clean demo map. Other J-systems will follow very soon. |
| 06-25-2010, 08:31 AM | #7 |
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 | |
Quote:
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 |
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 | ||
Quote:
Quote:
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 | |
Quote:
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 | |
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:
:) |
| 06-25-2010, 05:35 PM | #13 | |
Quote:
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 |
I didn't use calibration constants, thus it is possible to use Deaods image bars as well :) |
| 06-25-2010, 08:33 PM | #15 | |
Quote:
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. |
