| 11-29-2008, 02:24 AM | #1 | ||
Spell code:scope Tripwire initializer Init //***************************************************************** //* spell - Tripwire //* //* written by: Anitarf //* thanks to: Pyrogasm, for pioneering multi-target spells //* requires: -TimerUtils //* -LineSegment //* -SimError //* -IsUnitSpellResistant //* -xepreload //* -xedamage //* -ABuff //* -ABuffStun //* //* -a point target triggerer ability //* -a no-target cancelation detection ability //* //* description: Creates a tripwire between two target points. //* Enemy units that cross the wire will be stunned //* and damaged. //* //* technical: The spell requires a no-target ability without any //* mana cost, casting time or cooldown and a black //* icon that is used to detect if the player cancels //* the targeting procedure. The code does that by //* continualy pressing the ability's hotkey for the //* player, once the player cancels the spell targeting //* this causes the cancelation spell to be cast and //* detected. The hotkey used by this ability should //* not be used by any other ability on the map. //* //* The targeting system may malfunction when multiple //* players try to control the same unit at once. //* //***************************************************************** globals private constant integer SPELL_ABILITY = 'A000' //the main tripwire spell private constant string SPELL_ABILITY_HOTKEY = "t" //spell hotkey, must be lowercase private constant string SPELL_ABILITY_ORDER = "sleep" //the orderstring of the spell private constant integer CANCEL_DETECTION_ABILITY = 'A001' //the no-target ability for detecting cancelation private constant string CANCEL_DETECTION_HOTKEY = "l" //ability hotkey, must be lowercase private constant string CANCEL_DETECTION_ORDER = "slow" //the orderstring of the ability private constant real SPELL_PERIOD = 0.1 //how often does the tripwire check for enemies private constant real TRIPWIRE_WIDTH = 0.0 //how close must the enemy unit be to trigger it private constant real CANCEL_MARGIN = 0.1 //the delay for the activation of the cancel detection //if the game has a lower framerate than this then //the spell might get canceled on accident before the //targeting cursor comes up for the second point. private constant string LIGHTNING_CODE = "DRAB" //the visual effect for the tripwire private constant real FLASH_DURATION = 0.2 //for how long is the wire visible when it activates private constant real FADEOUT_DURATION = 1.0 //if the wire expires, how long does it take to fade out private constant string TARGET_EFFECT = "" //the effect displayed ont he unit that triggers the wire private constant string TARGET_EFFECT_ATTACHMENT = "origin" //the attachment point for the effect private constant string RANGE_ERROR_MESSAGE = " closer to the first target point." endglobals private constant function Damage takes integer level returns real return 50.0 //the damage dealt by the tripwire endfunction private constant function StunDurationUnit takes integer level returns real return 5.0 //the duration of the stun for units, if 0 then the unit doesn't get stunned endfunction private constant function StunDurationHero takes integer level returns real return 3.0 //the duration of the stun for heroes, if 0 then the hero doesn't get stunned endfunction private constant function Duration takes integer level returns real return 30.0+30*level //the duration of the tripwire, if 0 then it's permanent endfunction private function MaxDistance takes integer level returns real return 250.0+250.0*level //the maximum length of the tripwire endfunction private constant function ActivationDelay takes integer level returns real return 2.0 //the time it takes for the tripwire to affect units endfunction private constant function FadeDelay takes integer level returns real return 2.0 //the time it takes for the tripwire to become invisible, must be greater than 0.0 endfunction private function WireTargets takes unit inRange, player wireOwner returns boolean return GetWidgetLife(inRange)>0.405 and not(IsUnitType(inRange,UNIT_TYPE_STRUCTURE) or IsUnitType(inRange,UNIT_TYPE_FLYING)) and IsUnitEnemy(inRange, wireOwner) endfunction private function DamageOptions takes xedamage spellDamage returns nothing //useful read: [url]http://www.wc3campaigns.net/showpost.php?p=1030046&postcount=19[/url] set spellDamage.dtype=DAMAGE_TYPE_UNIVERSAL set spellDamage.atype=ATTACK_TYPE_NORMAL set spellDamage.tag=0 //the tag attached to the damage by xedamage //in this case xedamage is not used to determine which units can trigger the tripwire //because that tends to lag with multiple units in the area of multiple tripwires //instead, there's a special inline-friendly function for determining targets above endfunction // END OF CALIBRATION SECTION // ================================================================ //unit target validation private keyword si globals private group tempg = CreateGroup() private boolexpr tempbx private xedamage xed private si tempsi endglobals private function Targets takes nothing returns boolean return WireTargets(GetFilterUnit(), tempsi.owner) endfunction // ================================================================ //spell instance struct private struct si real x real y real dx real dy unit caster player owner integer level real duration lightning l boolean activated private integer index private static si array loopList private static integer loopCount=0 private static timer tim private static method periodic takes nothing returns nothing local integer i=0 local real r local unit u loop exitwhen i>=si.loopCount set tempsi = si.loopList[i] set tempsi.duration=tempsi.duration+SPELL_PERIOD if tempsi.activated then //the wire has activated, keep it around for a little longer if tempsi.duration>FLASH_DURATION then call tempsi.destroy() set i=i-1 endif else //still hasn't been activated set r=tempsi.duration/FadeDelay(tempsi.level) if r<=1.0 then //fading in after it has been created if IsPlayerAlly(tempsi.owner, GetLocalPlayer()) then call SetLightningColor(tempsi.l, 1.0, 1.0, 1.0, 1.0-(r/2)) else call SetLightningColor(tempsi.l, 1.0, 1.0, 1.0, 1.0-r) endif endif set r=Duration(tempsi.level) if r!=0.0 and tempsi.duration>r then //spell expired set r=(tempsi.duration-r)/FADEOUT_DURATION if r<=1.0 then //fading out after it has expired if IsPlayerAlly(tempsi.owner, GetLocalPlayer()) then call SetLightningColor(tempsi.l, 1.0, 1.0, 1.0, 0.5-(r/2)) endif else //finished fading out call tempsi.destroy() set i=i-1 endif elseif tempsi.duration>ActivationDelay(tempsi.level) then //check for targets call GroupEnumUnitsInRangeOfSeg(tempg, tempsi.x,tempsi.y, tempsi.x+tempsi.dx,tempsi.y+tempsi.dy, TRIPWIRE_WIDTH/2, tempbx) if FirstOfGroup(tempg)!=null then //at least one unit hit set u = FirstOfGroup(tempg) call DestroyEffect( AddSpecialEffectTarget(TARGET_EFFECT,u,TARGET_EFFECT_ATTACHMENT) ) call xed.damageTarget(tempsi.caster,u, Damage(tempsi.level)) if IsUnitSpellResistant(u) then set r=StunDurationHero(tempsi.level) else set r=StunDurationUnit(tempsi.level) endif if r>0 then call UnitStun(u, tempsi.caster, r) endif call GroupClear(tempg) set u = null if FLASH_DURATION>0.0 then //flash the wire if needed set tempsi.activated=true set tempsi.duration=0.0 call SetLightningColor(tempsi.l, 1.0, 1.0, 1.0, 1.0) else call tempsi.destroy() set i=i-1 endif endif endif endif set i=i+1 endloop endmethod static method create takes unit caster, integer level, real x1, real y1, real x2, real y2 returns si local si s=si.allocate() set s.caster=caster set s.owner=GetOwningPlayer(caster) set s.level=level set s.x=x1 set s.y=y1 set s.dx=x2-x1 set s.dy=y2-y1 set s.duration=0.0 set s.activated=false if si.loopCount==0 then //loop timer call TimerStart(si.tim, SPELL_PERIOD, true, function si.periodic) endif set s.l = AddLightning(LIGHTNING_CODE, true, s.x,s.y, s.x+s.dx,s.y+s.dy) set si.loopList[si.loopCount]=s set s.index=si.loopCount set si.loopCount=si.loopCount+1 return s endmethod method onDestroy takes nothing returns nothing set si.loopCount=si.loopCount-1 set si.loopList[this.index]=si.loopList[si.loopCount] set si.loopList[si.loopCount].index=this.index call DestroyLightning(this.l) if si.loopCount==0 then call PauseTimer(si.tim) endif endmethod static method onInit takes nothing returns nothing set si.tim=CreateTimer() endmethod endstruct // ================================================================ //targeting struct globals private constant integer TARGET_COUNT = 2 //for this spell this value must be 2 //the targeting code otherwise supports any nubmer of targets endglobals private struct trg unit caster real array x[TARGET_COUNT] real array y[TARGET_COUNT] integer targCount timer tm private integer index private static trg array cancelList private static integer cancelCount=0 private static timer tim static method cancelCheck takes nothing returns nothing local integer i=0 loop exitwhen i>=trg.cancelCount if GetLocalPlayer()==GetOwningPlayer(trg.cancelList[i].caster) then call ForceUIKey(CANCEL_DETECTION_HOTKEY) //use the ability if the player canceled the targeting endif set i=i+1 endloop endmethod method addToList takes nothing returns nothing if this.index==-1 then if trg.cancelCount==0 then call TimerStart(trg.tim, 0.01, true, function trg.cancelCheck) endif set trg.cancelList[trg.cancelCount]=this set this.index=trg.cancelCount set trg.cancelCount=trg.cancelCount+1 call UnitAddAbility(this.caster, CANCEL_DETECTION_ABILITY) endif endmethod method removeFromList takes nothing returns nothing if this.index!=-1 then set trg.cancelCount=trg.cancelCount-1 set trg.cancelList[this.index]=trg.cancelList[trg.cancelCount] set trg.cancelList[trg.cancelCount].index=this.index set this.index = -1 call UnitRemoveAbility(this.caster, CANCEL_DETECTION_ABILITY) if trg.cancelCount==0 then call PauseTimer(trg.tim) endif endif endmethod static method create takes unit u returns trg local trg t = trg.allocate() set t.caster = u set t.targCount = 0 set t.index = -1 set t.tm=NewTimer() call SetTimerData(t.tm, integer(t)) return t endmethod method onDestroy takes nothing returns nothing call this.removeFromList() call ReleaseTimer(this.tm) set this.tm=null endmethod static method onInit takes nothing returns nothing set trg.tim=CreateTimer() endmethod endstruct // ================================================================ //ABuff functions for the targeting buff private function Create takes aBuff eventBuff returns nothing set eventBuff.data=integer(trg.create(eventBuff.target.u)) endfunction private function Refresh takes aBuff eventBuff returns nothing set eventBuff.data=eventBuff.olddata endfunction private function Cleanup takes aBuff eventBuff returns nothing call trg(eventBuff.data).destroy() endfunction // ================================================================ //coordinate target validation private function ValidateTarget takes trg t returns boolean local real max = MaxDistance(GetUnitAbilityLevel(t.caster,SPELL_ABILITY)) local real dx local real dy if t.targCount==2 then //we only care where the second point is set dx=t.x[1]-t.x[0] set dy=t.y[1]-t.y[0] if (dx*dx)+(dy*dy)>max*max then //if the distance is too long set t.targCount=t.targCount-1 //player must repick the second point call SimError(GetOwningPlayer(t.caster), RANGE_ERROR_MESSAGE) endif endif return true //in any case, do not terminate the targeting cycle endfunction // ================================================================ //targeting events globals private aBuffType targ endglobals private function SafetyDelay takes nothing returns nothing local trg t=trg(GetTimerData(GetExpiredTimer())) call t.addToList() endfunction private function CastOrder takes nothing returns boolean local unit u local trg t local boolean b if GetIssuedOrderId() == OrderId(SPELL_ABILITY_ORDER) then set u = GetTriggerUnit() call ABuffApply(targ, u, u, 0.0,0,0 ) set t = trg(GetABuffFromUnitByType(u, targ).data) call t.removeFromList() if t.targCount == TARGET_COUNT then //new targeting cycle set t.targCount=0 endif set t.x[t.targCount]=GetOrderPointX() set t.y[t.targCount]=GetOrderPointY() set t.targCount=t.targCount+1 set b=ValidateTarget(t) //check if the target point is valid if b and t.targCount == TARGET_COUNT then //let the targeting cycle finish by allowing //the order to execute and cast the spell else //next target in the cycle call DisableTrigger(GetTriggeringTrigger()) call PauseUnit( u, true ) //at this point the order gets reissued for some reason //so we need to disable the trigger, otherwise it runs twice //and this somehow causes the order not to be canceled call IssueImmediateOrder( u, "stop" ) //cancel the order call PauseUnit( u, false ) call EnableTrigger(GetTriggeringTrigger()) if b then if (GetLocalPlayer() == GetOwningPlayer(u)) then call ForceUIKey(SPELL_ABILITY_HOTKEY) // bring back the targeting cursor for the next target endif call TimerStart(t.tm, CANCEL_MARGIN, false, function SafetyDelay) endif endif set u = null endif return false endfunction private function CancelOrder takes nothing returns boolean local unit u local trg t if GetIssuedOrderId() == OrderId(CANCEL_DETECTION_ORDER) then //abort targeting cycle set u = GetTriggerUnit() set t = trg(GetABuffFromUnitByType(u, targ).data) set t.targCount=0 //no need to destroy the buff since it will likely be reused //it will get cleaned up anyway when the unit dies call t.removeFromList() set u = null endif return false endfunction // ================================================================ private function SpellEffect takes nothing returns boolean local unit u local trg t local integer lvl if GetSpellAbilityId() == SPELL_ABILITY then set u = GetTriggerUnit() set t = trg(GetABuffFromUnitByType(u, targ).data) set lvl =GetUnitAbilityLevel(u, SPELL_ABILITY) call si.create(u, lvl, t.x[0], t.y[0], t.x[1], t.y[1]) set u = null endif return false endfunction private function Init takes nothing returns nothing local trigger t //init triggers set t = CreateTrigger() call TriggerRegisterAnyUnitEventBJ( t, EVENT_PLAYER_UNIT_ISSUED_POINT_ORDER ) call TriggerAddCondition( t, Condition( function CastOrder ) ) set t = CreateTrigger() call TriggerRegisterAnyUnitEventBJ( t, EVENT_PLAYER_UNIT_ISSUED_ORDER ) call TriggerAddCondition( t, Condition( function CancelOrder ) ) set t = CreateTrigger() call TriggerRegisterAnyUnitEventBJ( t, EVENT_PLAYER_UNIT_SPELL_EFFECT ) call TriggerAddCondition( t, Condition( function SpellEffect ) ) //init buff filter set tempbx=Condition(function Targets) //init xedamage profile set xed=xedamage.create() call DamageOptions(xed) //preload cancel-detection ablity call XE_PreloadAbility(CANCEL_DETECTION_ABILITY) //init targeting abuff set targ=aBuffType.create() set targ.eventCreate = Create set targ.eventRefresh = Refresh set targ.eventCleanup = Cleanup set targ.countsAsBuff=false set targ.ignoreAsBuff=true endfunction endscope |
| 11-29-2008, 03:13 AM | #2 |
Could you post the code for the lazy people who don't want to open the WE.... like me... thanks :P |
| 11-29-2008, 03:16 AM | #3 |
Moyack, you should already have had WarCiTy in your system. |
| 11-29-2008, 03:25 AM | #4 | |
Quote:
|
| 11-29-2008, 03:41 AM | #5 |
Very impressive. |
| 11-29-2008, 04:07 AM | #6 | |
Quote:
Ps: for the record, you have permission :P |
| 12-01-2008, 07:18 PM | #7 | |
Quote:
|
| 12-03-2008, 04:42 PM | #8 |
So why did you shamelessly copy Ammorth's line segments instead of just using it? |
| 12-03-2008, 08:20 PM | #9 |
I can't use it because his GroupEnumUnitsInRangeOfSeg function doesn't take unit's collision sizes into account and if I wanted to do that without inlining his code I'd have to deal with locations; so I inlined his code instead for the sake of efficiency. |
| 12-04-2008, 02:07 AM | #10 |
Not asking for you to not in-line it, but wouldn't adding the correction factor to the range accomplish the exact same thing? JASS:GetEnumUnitsInRangeOfSeg(arguments..., distance + collisionCorrection) In-lining is totally fine with me as performance with spells is important. |
| 12-04-2008, 05:09 AM | #11 |
CollisionCorrection as you have it would be unit-type dependent, something that can't be accessed in JASS without hardcoding it or using a processing-hog function. Performing IsUnitInRange..(...) checks guarantees that a unit's collision is properly factored. Anyways, you should probably update LineSegments to include collision checks and I think Anitarf should then update this to use LineSegments. It seems counter-productive to shoot for modularity as a site scripting goal if we're just inlining other peoples' codes because they lack a very specific feature that they should have anyways. |
| 12-15-2008, 09:39 PM | #12 |
Impressive in deed. However I fail to understand why it doesn't work sometimes .... When I tested the spell, there was a time I had a unit in the wire and yet nothing happened. Problem is that this "there was a time" gets repeated quite often ... here is the replay, I hope it helps: |
| 01-02-2009, 02:24 PM | #13 |
Updated, the spell now uses the LineSegment library which also fixes the bug that Flame_Phoenix reported. |
| 01-02-2009, 06:41 PM | #14 |
Good, then consider this approved. |
| 08-04-2009, 10:18 PM | #15 |
Updated the test map so it works in 1.24 (had to import the new Table and TimerUtils, everything else remains the same). |
