| 12-06-2008, 10:42 PM | #1 |
This spell was made for XeNiM666, who requested it in Pyrogasm's Spell Request Thread. It follows the JESP Manifest. The Spell Uses: TimerUtils It was coded using vJASS and the JNGP. Cyclone The caster becomes a destructive cyclone that rushes to the target unit and bounces from enemy to enemy, destroying trees in its path and damaging nearby units. Deals 20 damage per level to units in a small range and 30 damage per level to each main target. Each level increases the target limit, starting with 3 targets. This is my first JESP spell and I would appreciate any corrections to the script or just comments and suggestions. Enjoy! Script:/ Cyclone by Zerzax: Coded for XeNiM666 as per his request in the Spell Request Thread at Wc3c. // Conforms to the JESP Standard. // Uses vJass // Credits to Vexorian for TimerUtils and collision sizes from xebasic and xecollider. // Credits to Hero12341234's for his incredibly long list of possible tree destructables // And thanks to Anitarf for helping make this script more efficient. scope Cyclone globals // Calibration Constants private constant integer ABILITY_ID = 'A001' // Spell ability. private constant integer WIND_UNIT = 'e000' // Dummy cyclone unit used for special effects as well as finding targets. // After a fraction of a second has passed between regular damage, a damaged unit may be hit again. private constant string WHIRLWIND_EFFECTS = "Abilities\\Spells\\NightElf\\Cyclone\\CycloneTarget.mdl" private constant string WHIRLWIND_ATTACH_POINT = "origin" private constant string WIND_STAND_ANIMATION = "stand" private constant attacktype ATTK_TYPE = ATTACK_TYPE_CHAOS // Configurable damage constants, though I believe changing them won't do anything. private constant damagetype DMG_TYPE = DAMAGE_TYPE_NORMAL private constant weapontype WEP_TYPE = WEAPON_TYPE_WHOKNOWS private constant boolean KILL_TREES = true private constant real WAIT_TIME = 1.00 // The interval that must pass before a unit can be again damaged by the same cyclone. private constant real MAX_COLLISION_DIST = 197.00 // A unit group will find units that are within the range of MAX distance + BASE distance. A filter then private constant real BASE_COLLISION_DIST = 50.00 // confirms that the enumerated units' collision circles are within the base distance. private constant real ACQUISITION_RANGE = 600.00 // Range to acquire a new target private constant real MOVE_INTERVAL = 0.04 // 25 intervals per second in which the cyclone is moved. private constant real MOVE_OFFSET = 25.00 // 25 units per interval of the timer private constant real CUTOFF_DISTANCE = 1000.00 // The spell will end if the current target is further than this distance from the cyclone. private constant real TREE_RANGE = 100.00 // About the width of a tornado or cyclone model. endglobals // Configuration functions private function IsCharging takes unit cast, unit targ returns boolean return GetWidgetLife(cast) > 0.405 and GetWidgetLife(targ) > 0.405 and IsUnitVisible(targ, GetOwningPlayer(cast)) // Spell will end if any of these conditions aren't true. endfunction private function IsEnemy takes unit cast, unit targ returns boolean return IsUnitEnemy(targ, GetOwningPlayer(cast)) and IsUnitVisible(targ, GetOwningPlayer(cast)) and not IsUnitType(targ, UNIT_TYPE_STRUCTURE) and GetWidgetLife(targ) > 0.405 endfunction // Checks to see if the found unit is an eligible target. private function DamageReg takes integer level returns real return level * 20.00 endfunction private function DamageTarg takes integer level returns real return level * 30.00 endfunction private function ChainLimit takes integer level returns integer return 2 + level // 3, 4, 5, 6, 7 chains endfunction private function TreeTypes takes integer id returns boolean return id == 'ATtr' or id == 'BTtw' or id == 'KTtw' or id == 'YTft' or id == 'JTct' or id == 'YTst' or id == 'YTct' or id == 'YTwt' or id == 'JTwt' or id == 'JTwt' or id == 'FTtw' or id == 'CTtr' or id == 'ITtw' or id == 'NTtw' or id == 'OTtw' or id == 'ZTtw' or id == 'WTst' or id == 'LTlt' or id == 'GTsh' or id == 'Xtlt' or id == 'WTtw' or id == 'Attc' or id == 'BTtc' or id == 'CTtc' or id == 'ITtc' or id == 'NTtc' or id == 'ZTtc' endfunction // End of calibration // Struct that contains a unit hit by a cyclone, its parent group that prevents it from being damaged, and the timer that eventually removes it from the parent group. private struct damaged unit u = null group instGroup = null timer waitTim = null static method restore takes nothing returns nothing call damaged(GetTimerData(GetExpiredTimer())).destroy() // Remove the unit from the group in onDestroy endmethod static method create takes unit u, group g returns damaged local damaged d = damaged.allocate() set d.u = u set d.instGroup = g set d.waitTim = NewTimer() call SetTimerData(d.waitTim, integer(d)) call TimerStart(d.waitTim, WAIT_TIME, false, function damaged.restore) return d endmethod method onDestroy takes nothing returns nothing call GroupRemoveUnit(this.instGroup, this.u) call ReleaseTimer(this.waitTim) endmethod endstruct // The struct is the center of this spell; actual initialization is taken care of in the onInit method. private struct cStorage static boolexpr enumFilter // Filters anyone in range of the cyclone static boolexpr targFilter // Finds a new chain target static boolexpr treeFilter // If a destructable is found, and it is a tree, it will die static group enumGroup // "Global" group to find enemies in range static rect treeKiller // Region that shifts around to find trees static unit tempTarget = null static cStorage tempInstance = 0 static boolean tempBool=false static boolean newTarg=false unit caster = null unit currentTarget = null unit lastHit = null // Previously hit chain target unit wind // The cyclone real cX = 0 // cyclone X / Y real cY = 0 real targX = 0 // The target's position. real targY = 0 real xVel = 0 // Values that reflect direction in which the cyclone is moving real yVel = 0 integer castlvl = 0 integer chainCount = 0 group hitUnits timer moveTimer = null static method rangeEnum takes nothing returns boolean local unit enum=GetFilterUnit() local cStorage this = .tempInstance // Damages units found. If the enumerated unit is the current target, that target is damaged and a new target is found. if IsEnemy(this.caster, enum) and IsUnitInRange(this.wind, enum, BASE_COLLISION_DIST) then if ( enum == this.currentTarget ) then set .newTarg = true call UnitDamageTarget(this.caster, enum, DamageTarg(this.castlvl), true, false, ATTK_TYPE, DMG_TYPE, WEP_TYPE) call GroupAddUnit(this.hitUnits, enum) call damaged.create(enum, this.hitUnits) elseif not ( IsUnitInGroup(enum, this.hitUnits) ) then call UnitDamageTarget(this.caster, enum, DamageReg(this.castlvl), true, false, ATTK_TYPE, DMG_TYPE, WEP_TYPE) call GroupAddUnit(this.hitUnits, enum) call damaged.create(enum, this.hitUnits) endif endif set enum=null return false endmethod static method findTarget takes nothing returns boolean // Finds a new target and assigns a static member (essentially global) to indicate the found unit. local unit enum=GetFilterUnit() if IsEnemy(.tempInstance.caster, enum) and enum != .tempInstance.lastHit and .tempBool then set .tempTarget=enum set .tempBool=false endif set enum=null return false endmethod method changeTarget takes nothing returns boolean // Checks to see if there are targets in range and if so find that target, sort of like a condition and action rolled into one. set .tempTarget=null set .tempBool=true set this.lastHit=this.currentTarget call GroupEnumUnitsInRange(.enumGroup, this.cX, this.cY, ACQUISITION_RANGE, .targFilter) set this.currentTarget = this.lastHit if .tempTarget!=null then set this.currentTarget = .tempTarget endif return .tempTarget != null endmethod static method filterTrees takes nothing returns boolean return TreeTypes(GetDestructableTypeId(GetFilterDestructable())) endmethod static method killTrees takes nothing returns nothing call KillDestructable(GetEnumDestructable()) endmethod static method update takes nothing returns nothing local cStorage c=GetTimerData(GetExpiredTimer()) local real current_ang local real targX=GetUnitX(c.currentTarget) local real targY=GetUnitY(c.currentTarget) if IsCharging(c.caster, c.currentTarget) and IsUnitInRange(c.wind, c.currentTarget, CUTOFF_DISTANCE) then // Proceed if everything is running smoothy if targX != c.targX or targY != c.targY then // A change in coordinates requires updating stored angles and coordinates set current_ang=Atan2(targY-c.cY,targX-c.cX) set c.xVel=Cos(current_ang) * MOVE_OFFSET set c.yVel=Sin(current_ang) * MOVE_OFFSET set c.targX=targX set c.targY=targY endif set c.cX=c.cX + c.xVel // Move the wind effect set c.cY=c.cY + c.yVel call SetUnitX(c.wind, c.cX) call SetUnitY(c.wind, c.cY) if KILL_TREES then // Self-explanatory call MoveRectTo(.treeKiller, c.cX, c.cY) call EnumDestructablesInRect(.treeKiller, .treeFilter, function cStorage.killTrees) endif set .tempInstance=c set .tempTarget=null call GroupEnumUnitsInRange(.enumGroup, c.cX, c.cY, MAX_COLLISION_DIST + BASE_COLLISION_DIST, .enumFilter) // Finds any units in range to damage or replace the current target if .newTarg then set .newTarg=false if (c.chainCount > ChainLimit(c.castlvl) or not c.changeTarget()) then // If the chain count has reached the maximum or no new targets or found, the spell ends. call c.destroy() else set c.chainCount=c.chainCount+1 endif endif else call c.destroy() endif endmethod static method create takes unit cast, unit targ returns cStorage local cStorage c=cStorage.allocate() local real cX = GetUnitX(cast) local real cY = GetUnitY(cast) local real tX = GetUnitX(targ) local real tY = GetUnitY(targ) local real ang = Atan2(tY - cY, tX - cX) set c.caster=cast // Data allocation set c.currentTarget=targ set c.castlvl=GetUnitAbilityLevel(cast, ABILITY_ID) set c.cX=cX set c.cY=cY set c.targX=tX set c.targY=tY set c.xVel=Cos(ang) * MOVE_OFFSET set c.yVel=Sin(ang) * MOVE_OFFSET if c.wind == null then set c.wind=CreateUnit(GetOwningPlayer(cast), WIND_UNIT, c.cX, c.cY, 270) // Creates a wind effect call SetUnitAnimation(c.wind, WIND_STAND_ANIMATION) set c.hitUnits = CreateGroup() else call SetUnitX(c.wind, cX) call SetUnitY(c.wind, cY) call ShowUnit(c.wind, true) call UnitRemoveAbility(c.wind, 'Aloc') call UnitAddAbility(c.wind, 'Aloc') endif call ShowUnit(cast, false) // Prep the caster's invisible form call SetUnitInvulnerable(cast, true) call PauseUnit(cast, true) call SetUnitPathing(cast, false) if GetLocalPlayer()==GetOwningPlayer(cast) then call SelectUnit(cast, false) endif call UnitAddAbility(cast, 'Aloc') set c.moveTimer=NewTimer() call SetTimerData(c.moveTimer, integer(c)) call TimerStart(c.moveTimer, MOVE_INTERVAL, true, function cStorage.update) return c endmethod static method spellExecute takes nothing returns boolean if GetSpellAbilityId()==ABILITY_ID then call cStorage.create(GetTriggerUnit(), GetSpellTargetUnit()) endif return false endmethod method onDestroy takes nothing returns nothing call GroupClear(this.hitUnits) // Clean / recycle used objects. call ReleaseTimer(this.moveTimer) call ShowUnit(this.wind, false) // Hides for now call ShowUnit(this.caster, true) // Return the caster to normal settings call SetUnitPathing(this.caster, true) call SetUnitPosition(this.caster, this.cX, this.cY) call SetUnitInvulnerable(this.caster,false) call PauseUnit(this.caster, false) call UnitRemoveAbility(this.caster, 'Aloc') if GetLocalPlayer()==GetOwningPlayer(this.caster) then call SelectUnit(this.caster, true) endif endmethod static method onInit takes nothing returns nothing local trigger t=CreateTrigger() local unit preload=CreateUnit(Player(15), WIND_UNIT, 0,0,0) call TriggerAddCondition(t, Condition(function cStorage.spellExecute)) // Trigger Setup call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_EFFECT) call UnitAddAbility(preload, ABILITY_ID) // Preloading call UnitRemoveAbility(preload, ABILITY_ID) call DestroyEffect(AddSpecialEffectTarget(WHIRLWIND_EFFECTS, preload, "origin")) //set .cTim = CreateTimer() set .targFilter=Filter(function cStorage.findTarget) // Creating handles for the structure. set .enumFilter=Filter(function cStorage.rangeEnum) if KILL_TREES then set .treeKiller=Rect(-TREE_RANGE, -TREE_RANGE, TREE_RANGE, TREE_RANGE) set .treeFilter=Filter(function cStorage.filterTrees) endif set .enumGroup=CreateGroup() call RemoveUnit(preload) set preload=null endmethod endstruct endscope Version A has now been updated for more efficiency. I've also added a smaller screenshot that perhaps will do the spell justice. The cyclone is hard to see. Oh well. |
| 12-06-2008, 11:36 PM | #2 |
I didn't really read the full code, nor did I test the spell, but I noticed this right off the bat:
|
| 12-07-2008, 12:07 AM | #3 |
I put in the InitTrig because of JESP, it stated that "the initTrig function must be named "initTrig_<Spellname>". I took that to mean it required the function, guess not. I will update those constants, and merge the two unit constants. Good idea about the tables. I always wondered what the best way to recycle them was. EDIT: Posted updates. |
| 12-07-2008, 05:06 AM | #4 |
The JESP standard is really kind-of irrelevant now because it hasn't been updated to account for the new syntax now. Essentially, if you make a scope with your spell's name and all of the functions and globals are either public or private, then it follows the JESP standard :P |
| 12-07-2008, 02:09 PM | #5 |
Haha, well I recommend trying out the spell ASAP and seeing what it's like. Apparently Version B (maybe A too?) has a bug. I'll try to fix it by the end of the week. EDIT: I think the bug does not exist in this testmap, so worry not about it. EDIT2: Found some extra junk lying around that won't impact the spell, such as an extra "show unit". Also, I think locust removes the ability to target the affected unit permanently so I will fix that too as soon as someone looks at the script. |
| 01-05-2009, 11:53 PM | #6 |
I'm bumping this because it's been atrophying for almost a month. Is the spell approveable, besides the fact it needs an extraneous function call removed? I'm ready to fix anything that needs it. |
| 02-01-2009, 06:53 PM | #7 | |
This looks good enough for me. Nothing jumped out at me, really. Quote:
|
| 02-01-2009, 10:08 PM | #8 |
I looked at the code too and didn't see anything else, so let's approve it! |
| 02-02-2009, 12:18 AM | #9 |
Hey Hey Hey! Thanks guys :D. I wanted to get a least one submission in, for the experience of it. I'll start using global timers + array stack from now on for my structured spells. |
| 02-02-2009, 03:13 PM | #10 |
You could inline your group recycling like this since the struct itself works like a stack anyway. You should flush a gamecache value, not just set it to 0, furthermore the units should be removed from the hit group when their time runs out. Well, I don't really like the heavy gamecache calls you're doing anyway. I'd likely be more efficient to just have a timer per hit unit per spell instance instead. Your description of the COLLISION_DIST calibration constant is incorrect (you're using InRange natives which take collision size into account) and useless (it doesn't really describe what the constant is used for). Also, the enum call should enum units in COLLISION_DIST+ENUM_SIZE range. The ENUM_SIZE constant also has a fairly poor description of what it does. The line that checks if the destructable is a tree should be a calibration function, in case users have custom trees in their map. |
| 02-03-2009, 07:30 PM | #11 |
Thanks Ani, I'll get right on it. These are things I've addressed in other scripts but never went back to cyclone to fix. I guess the timer per unit idea is soundest as it would indeed wipe out the gamecache usage. EDIT: Without using the 2-dimensional functionality of gamecache, how would I be able to (a have a unique counter for each unit per spell instance and (b associate the counter to the unit so that it can be referenced by the main group enum? Also, I'll take a better screeny for each spell type. |
| 02-03-2009, 08:47 PM | #12 | |
Quote:
You don't really need b). |
| 02-03-2009, 09:42 PM | #13 |
Okay. I can understand how an individual timer per unit would run and eventually satisfy the time duration. OHH I just figured it out. I could check whether or not the unit was in the struct's main group. The individual struct instances would have references to the "parent" spell instance's group, and remove itself once duration is up. I'm guessing that's a good way to do it, so I'll go ahead with it. Thanks. EDIT: New Version of A up. I'm too lazy at the moment to update B also, I prefer A anyway. |
| 02-08-2009, 11:28 PM | #14 | |
Quote:
I think you should use something like max collision size constant from xe for determining the GroupEnum range instead of having it as a separate poorly explained ENUM_RANGE constant. Enuming units in a larger range is done because the IsUnitInRange* natives take the collision size into account to get proper War3 area-of-effect, yet you describe COLLISION_DIST as a distance between unit centers, which would leave the user in confusion as to why they need to specify a separate ENUM_RANGE to begin with. |
| 02-09-2009, 02:00 AM | #15 |
B has been removed. I'll fix up the script and alter the constants in the desired manner. This should be complete by tomorrow night. EDIT: Changed the description of the constants as well as their names. I hope it's clear. |
