library MeatPuppet initializer Init uses LastOrder, GroupUtils, DestructableLib, SpellEvent, Table,/*
*/UnitIndexingUtils, AbortSpell, Knockback, TimerUtils
private keyword Data // DO NOT CHANGE!
globals
private constant integer LEARN_AID = 'A006'
private constant integer CAST_AID = 'A000'
private constant integer BID = 'B000' // the buff placed by CAST_AID on the target unit.
private constant real TICK = 1./32
private constant integer RELEASE_AID = 'A005' // ability is based off of Berserk
private constant integer RELEASE_BID = 'B002' // placed on caster by RELEASE_AID
private constant boolean RELEASE_ALLOW = true // if true, RELEASE_AID gets added after casting the spell
private constant boolean RELEASE_REPLACE_CAST_AID = true // if true and MAX_INSTANCES_PER_CASTER equals 1, CAST_AID gets removed before placing RELEASE_AID
private constant string LIGHTNING_ID = "INIT" // Ordinary Lightning
private constant real LIGHTNING_RED = 1.0
private constant real LIGHTNING_GREEN = 0.2
private constant real LIGHTNING_BLUE = 0.7
private constant real LIGHTNING_ALPHA = 0.8 // put "0." in here if you dont want the lightning
private constant string COLLISION_FX = "" // empty, but someone might want to use it
private constant real COLLISION_AOE = 95.
private real array COLLISION_DAMAGE // When the puppet hits an enemy unit, how much damage does the unit hit take
private real array COLLISION_PUPPET_DAMAGE // When the puppet hits another unit, how much damage does the puppet take
private constant boolean COLLISION_DAMAGE_FRIENDLY = false // should a friendly puppet take damage from colliding?
private constant attacktype COLLISION_ATTACK_TYPE = ATTACK_TYPE_NORMAL
private constant damagetype COLLISION_DAMAGE_TYPE = DAMAGE_TYPE_NORMAL
private constant weapontype COLLISION_SOUND = WEAPON_TYPE_METAL_MEDIUM_BASH
private constant weapontype COLLISION_TREE_SOUND = WEAPON_TYPE_WOOD_MEDIUM_BASH
private real array COLLISION_KB_DISTANCE // how far is the unit hit pushed back
private real array COLLISION_KB_DURATION // for how long is the unit hit pushed back // values less than or equal to 0 disable knocking back
private constant boolean KB_UNITS_KILL_TREES = true
private constant boolean KB_UNITS_KNOCK_OTHERS = false
private constant boolean KB_UNITS_CHAIN = false // should units knocked back by units knocked back by this spell be able to knock other units back // refer to the manual of Knockback for information
private constant real MIN_RANGE = 200.
private constant real MIN_RANGE_TIME = 0.25 // 0. or less is instant
private constant real ANGLE_CHANGE_SPEED = 0.2 // from 0.0 to 1.0, please // closer to 0: longer time to sync with casters facing, closer to 1: less time to sync with casters facing (1 being instant syncing)
private constant real DISTANCE_CHANGE_SPEED = 0.1 // from 0.0 to 1.0, please
private constant integer MAX_INSTANCES_PER_CASTER = 1
private constant string ERROR_TARGET_ALREADY_IN_INSTANCE = "Target already targeted by another instance of this spell!"
private constant string ERROR_TOO_MANY_INSTANCES = "Too many instances of this spell for the caster!"
private constant string ABILITY_HOTKEY = "T" // hotkey triggering AID
endglobals
// Damage
private function SetUpCOLLISION_DAMAGE takes nothing returns nothing
set COLLISION_DAMAGE[1]=125.
set COLLISION_DAMAGE[2]=200.
set COLLISION_DAMAGE[3]=300.
endfunction
private function Collision_Damage takes integer level returns real
return COLLISION_DAMAGE[level]
endfunction
// Puppet Damage
private function SetUpCOLLISION_PUPPET_DAMAGE takes nothing returns nothing
set COLLISION_PUPPET_DAMAGE[1]=0.
set COLLISION_PUPPET_DAMAGE[2]=0.
set COLLISION_PUPPET_DAMAGE[3]=0.
endfunction
private function Collision_Puppet_Damage takes integer level returns real
return COLLISION_PUPPET_DAMAGE[level]
endfunction
// Distance
private function SetUpCOLLISION_KB_DISTANCE takes nothing returns nothing
set COLLISION_KB_DISTANCE[1]=125.
set COLLISION_KB_DISTANCE[2]=175.
set COLLISION_KB_DISTANCE[3]=225.
endfunction
private function Collision_Kb_Distance takes integer level returns real
return COLLISION_KB_DISTANCE[level]
endfunction
// Duration
private function SetUpCOLLISION_KB_DURATION takes nothing returns nothing
set COLLISION_KB_DURATION[1]=1.0
set COLLISION_KB_DURATION[2]=1.2
set COLLISION_KB_DURATION[3]=1.4
endfunction
private function Collision_Kb_Duration takes integer level returns real
return COLLISION_KB_DURATION[level]
endfunction
// Validate Target
private function ValidTarget takes unit u, Data s returns boolean
return IsUnitType(u, UNIT_TYPE_DEAD)==false/*
*/ and IsUnitType(u, UNIT_TYPE_STRUCTURE)==false/*
*/ and IsUnitType(u, UNIT_TYPE_FLYING)==IsUnitType(s.t, UNIT_TYPE_FLYING)/*
*/ and IsUnitEnemy(u, GetOwningPlayer(s.caster))/*
*/ and (not IsKnockedBack(u))/*
*/ and u!=s.t/*
*/ and GetUnitMoveSpeed(u)>0/*
*/
endfunction
//
private struct Data
unit caster
unit t // Target
lightning l // Link
integer level
integer instanceofcaster
real a // angleOffset
real d // distanceOffset
real dd // deltadistance // MIN_RANGE
real tf // targets facing, relative
real c // counter
static Table InstanceOfCaster
static integer array Instances
static thistype array InstanceOfUnit // Linking Back, so instances dont conflict with each other
static boolexpr Collision
static boolexpr TreeFilter
static thistype tmps
static rect R
static location LocZ=Location(0,0)
private integer i
private static thistype array Structs
private static timer T=CreateTimer()
private static integer Count=0
static method GetLocZ takes real x, real y returns real
call MoveLocation(.LocZ, x,y)
return GetLocationZ(.LocZ)
endmethod
method onDestroy takes nothing returns nothing
local integer id=GetUnitId(.caster)
// Recycle instance of this caster properly
set .Instances[id]=.Instances[id]-1
set .InstanceOfCaster[id*MAX_INSTANCES_PER_CASTER+.instanceofcaster]=.InstanceOfCaster[id*MAX_INSTANCES_PER_CASTER+.Instances[id]]
set thistype(.InstanceOfCaster[id*MAX_INSTANCES_PER_CASTER+.instanceofcaster]).instanceofcaster=.instanceofcaster
call .InstanceOfCaster.flush(id*MAX_INSTANCES_PER_CASTER+.Instances[id])
if RELEASE_ALLOW and .Instances[id]==0 then
call UnitRemoveAbility(.caster, RELEASE_AID)
if MAX_INSTANCES_PER_CASTER==1 and RELEASE_REPLACE_CAST_AID and GetUnitAbilityLevel(.caster, LEARN_AID)>0 then
call UnitAddAbility(.caster, CAST_AID)
call SetUnitAbilityLevel(.caster, CAST_AID, GetUnitAbilityLevel(.caster, LEARN_AID))
call UnitMakeAbilityPermanent(.caster, true, CAST_AID)
endif
endif
// Normal cleanup
set .InstanceOfUnit[GetUnitId(.t)]=0
call SetUnitPosition(.t, GetUnitX(.t), GetUnitY(.t)) // avoid getting stuck
call IssueLastOrder(.t) // avoid not carrying out the last order
set .t=null
set .caster=null
call DestroyLightning(.l)
set .l=null
// clean your struct here
set .Count=.Count-1
set .Structs[.i]=.Structs[.Count]
set .Structs[.i].i=.i
if .Count==0 then
call PauseTimer(.T)
endif
endmethod
private static method CollisionFunc takes nothing returns boolean
local unit u=GetFilterUnit()
local real a
local real v
if ValidTarget(u, .tmps) then
if UnitDamageTarget(.tmps.caster, u, Collision_Damage(.tmps.level), true, false, COLLISION_ATTACK_TYPE, COLLISION_DAMAGE_TYPE, COLLISION_SOUND) then // should the unit for some reason be immune to damage, dont continue
if COLLISION_DAMAGE_FRIENDLY or IsUnitEnemy(.tmps.t, GetOwningPlayer(.tmps.caster)) then
call UnitDamageTarget(.tmps.caster, .tmps.t, Collision_Puppet_Damage(.tmps.level), true, false, COLLISION_ATTACK_TYPE, COLLISION_DAMAGE_TYPE, null) // sound has already been played
endif
call DestroyEffect(AddSpecialEffect(COLLISION_FX, GetUnitX(.tmps.t), GetUnitY(.tmps.t)))
if Collision_Kb_Duration(.tmps.level)>0 then // if the user doesnt want the units hit to be knocked back, dont do it
set a=(2*Collision_Kb_Distance(.tmps.level))/(Collision_Kb_Duration(.tmps.level)*Collision_Kb_Duration(.tmps.level)) // s=a/2*t^2 --> a=2*s/(t^2)
set v=a*Collision_Kb_Duration(.tmps.level) // v=a*t
call KnockbackTarget(.tmps.caster, u, Atan2(GetUnitY(u)-GetUnitY(.tmps.t), GetUnitX(u)-GetUnitX(.tmps.t))*bj_RADTODEG, v, a, KB_UNITS_KILL_TREES, KB_UNITS_KNOCK_OTHERS, KB_UNITS_CHAIN)
endif
endif
endif
set u=null
return false
endmethod
private static method TreeFilterFunc takes nothing returns boolean
local destructable d=GetFilterDestructable()
local real dx=GetWidgetX(d)-GetUnitX(.tmps.t)
local real dy=GetWidgetY(d)-GetUnitY(.tmps.t)
if (not IsDestructableDead(d)) and IsDestructableTree(d) and (dx*dx+dy*dy<=COLLISION_AOE*COLLISION_AOE) then
if COLLISION_DAMAGE_FRIENDLY or IsUnitEnemy(.tmps.t, GetOwningPlayer(.tmps.caster)) then
call UnitDamageTarget(.tmps.caster, .tmps.t, Collision_Puppet_Damage(.tmps.level), true, false, COLLISION_ATTACK_TYPE, COLLISION_DAMAGE_TYPE, COLLISION_TREE_SOUND)
endif
call KillDestructable(d)
endif
set d=null
return false
endmethod
private static method Callback takes nothing returns nothing
local integer i=.Count-1
local thistype s
local real a
local real x
local real y
local real dx
local real dy
local real r
loop
exitwhen i<0
set s=.Structs[i]
if GetUnitAbilityLevel(s.t, BID)==0 or IsUnitType(s.caster, UNIT_TYPE_DEAD)==true then
call s.destroy()
endif
if s.d<MIN_RANGE then // move the unit to MIN_RANGE
set s.d=s.d+s.dd/MIN_RANGE_TIME*TICK
endif
set x=GetUnitX(s.caster)
set y=GetUnitY(s.caster)
set dx=GetUnitX(s.t)-x
set dy=GetUnitY(s.t)-y
set r=Atan2(dy, dx) // current angle from caster to target
if r<0 then // map the angle from -Pi..Pi to 0..2*Pi
set r=2*bj_PI+r
endif
set a=ModuloReal(((GetUnitFacing(s.caster)*bj_DEGTORAD+s.a)-r), 2*bj_PI) // calculate the difference between current angle between caster and target and the current facing of the caster
if a>bj_PI then // map the delta angle from 0..2*Pi to -Pi..Pi
set a=-2*bj_PI+a
endif
set a=r+a*ANGLE_CHANGE_SPEED // calculate the new angle in which the target if offset from the caster
set r=SquareRoot(dx*dx+dy*dy) // calculate the distance between caster and target
set r=r+(s.d-r)*DISTANCE_CHANGE_SPEED // calculate the new distance the target is offset from the caster
set x=Cos(a)*r+x
set y=Sin(a)*r+y
call SetUnitX(s.t, x)
call SetUnitY(s.t, y)
call SetUnitFacing(s.t, GetUnitFacing(s.caster)+s.tf)
call MoveLightningEx(s.l, true, GetUnitX(s.caster), GetUnitY(s.caster), .GetLocZ(GetUnitX(s.caster), GetUnitY(s.caster))+GetUnitFlyHeight(s.caster), x, y, .GetLocZ(x, y)+GetUnitFlyHeight(s.t))
set .tmps=s
call GroupEnumUnitsInRange(ENUM_GROUP, x,y, COLLISION_AOE, .Collision)
call MoveRectTo(.R, x,y)
call EnumDestructablesInRect(.R, .TreeFilter, null)
// do your things here, dont forget to call s.destroy() somewhen
//
set i=i-1
endloop
endmethod
private static method PlaceRelease takes nothing returns nothing
local thistype s=thistype(GetTimerData(GetExpiredTimer()))
if MAX_INSTANCES_PER_CASTER==1 and RELEASE_REPLACE_CAST_AID then
call UnitRemoveAbility(s.caster, CAST_AID)
endif
call UnitAddAbility(s.caster, RELEASE_AID)
call UnitMakeAbilityPermanent(s.caster, true, RELEASE_AID)
call ReleaseTimer(GetExpiredTimer())
endmethod
static method create takes nothing returns thistype
local thistype s=.allocate()
local real dx
local real dy
local real r
local integer id
local timer t
// Populate the struct
set s.caster=SpellEvent.CastingUnit
set s.t=SpellEvent.TargetUnit
set .InstanceOfUnit[GetUnitId(s.t)]=s
set s.level=GetUnitAbilityLevel(s.caster, CAST_AID)
// SetUp the relative position
set dx=GetUnitX(s.t)-GetUnitX(s.caster)
set dy=GetUnitY(s.t)-GetUnitY(s.caster)
set r=Atan2(dy, dx)
if r<0 then
set r=2*bj_PI+r
endif
set s.a=r-GetUnitFacing(s.caster)*bj_DEGTORAD
set s.d=SquareRoot(dx*dx+dy*dy)
if s.d<MIN_RANGE then
set s.dd=MIN_RANGE-s.d
if MIN_RANGE_TIME<=0 then
set s.d=MIN_RANGE
endif
endif
set s.tf=GetUnitFacing(s.t)-GetUnitFacing(s.caster)
// SetUp the lightning
set s.l=AddLightningEx(LIGHTNING_ID, true, GetUnitX(s.caster), GetUnitY(s.caster), .GetLocZ(GetUnitX(s.caster), GetUnitY(s.caster))+GetUnitFlyHeight(s.caster), GetUnitX(s.t), GetUnitY(s.t), .GetLocZ(GetUnitX(s.t), GetUnitY(s.t))+GetUnitFlyHeight(s.t))
call SetLightningColor(s.l, LIGHTNING_RED, LIGHTNING_GREEN, LIGHTNING_BLUE, LIGHTNING_ALPHA)
set id=GetUnitId(s.caster)
set .InstanceOfCaster[id*MAX_INSTANCES_PER_CASTER+.Instances[id]]=s
set s.instanceofcaster=.Instances[id]
if RELEASE_ALLOW and .Instances[id]==0 then
set t=NewTimer()
call SetTimerData(t, s)
call TimerStart(t, 0, false, function thistype.PlaceRelease)
endif
set .Instances[id]=.Instances[id]+1
// initialize the struct here
set .Structs[.Count]=s
set s.i=.Count
if .Count==0 then
call TimerStart(.T, TICK, true, function thistype.Callback)
endif
set .Count=.Count+1
return s
endmethod
private static method onInit takes nothing returns nothing
set .InstanceOfCaster=Table.create()
set .Collision=Condition(function thistype.CollisionFunc)
set .TreeFilter=Condition(function thistype.TreeFilterFunc)
set .R=Rect(-COLLISION_AOE, -COLLISION_AOE, COLLISION_AOE, COLLISION_AOE)
if MAX_INSTANCES_PER_CASTER<=0 then
call BJDebugMsg(SCOPE_PREFIX+": MAX_INSTANCES_PER_CASTER is too low (<=0)!")
endif
if ANGLE_CHANGE_SPEED<=0 then
call BJDebugMsg(SCOPE_PREFIX+": ANGLE_CHANGE_SPEED is too low (<=0)!")
elseif ANGLE_CHANGE_SPEED>1 then
call BJDebugMsg(SCOPE_PREFIX+": ANGLE_CHANGE_SPEED is too high (>1)!")
endif
if DISTANCE_CHANGE_SPEED<=0 then
call BJDebugMsg(SCOPE_PREFIX+": DISTANCE_CHANGE_SPEED is too low (<=0)!")
elseif DISTANCE_CHANGE_SPEED>1 then
call BJDebugMsg(SCOPE_PREFIX+": DISTANCE_CHANGE_SPEED is too high (>1)!")
endif
//
call SetUpCOLLISION_DAMAGE()
call SetUpCOLLISION_PUPPET_DAMAGE()
call SetUpCOLLISION_KB_DISTANCE()
call SetUpCOLLISION_KB_DURATION()
endmethod
endstruct
private function CreateProxy takes nothing returns nothing
call Data.create()
endfunction
private function CheckValidTarget takes nothing returns nothing
if Data.InstanceOfUnit[GetUnitId(SpellEvent.TargetUnit)]!=0 then
call AbortSpell(SpellEvent.CastingUnit, "\n"+ERROR_TARGET_ALREADY_IN_INSTANCE, ABILITY_HOTKEY)
endif
if Data.Instances[GetUnitId(SpellEvent.CastingUnit)]>=MAX_INSTANCES_PER_CASTER then
call AbortSpell(SpellEvent.CastingUnit, "\n"+ERROR_TOO_MANY_INSTANCES, ABILITY_HOTKEY)
endif
endfunction
private function Release takes nothing returns nothing
local integer id=GetUnitId(SpellEvent.CastingUnit)
call UnitRemoveAbility(SpellEvent.CastingUnit, RELEASE_BID)
call UnitRemoveAbility(Data(Data.InstanceOfCaster[id*MAX_INSTANCES_PER_CASTER+Data.Instances[id]-1]).t, BID)
endfunction
globals
private integer array LearnedAbilityLevel
endglobals
private function IsLearnAbility takes nothing returns boolean
return GetLearnedSkill()==LEARN_AID
endfunction
private function Learning takes nothing returns nothing
local unit u=GetLearningUnit()
if GetLearnedSkillLevel()==1 then
call UnitAddAbility(u, CAST_AID)
call UnitMakeAbilityPermanent(u, true, CAST_AID)
endif
if not(RELEASE_REPLACE_CAST_AID and MAX_INSTANCES_PER_CASTER==1 and Data.Instances[GetUnitId(u)]==1) then
call SetUnitAbilityLevel(u, CAST_AID, GetLearnedSkillLevel())
endif
set LearnedAbilityLevel[GetUnitId(u)]=GetLearnedSkillLevel()
set u=null
endfunction
private function UnlearnedAbility takes nothing returns boolean
return GetUnitAbilityLevel(GetTriggerUnit(), LEARN_AID)!=LearnedAbilityLevel[GetUnitId(GetTriggerUnit())]
endfunction
private function ResetAbility takes nothing returns nothing
call UnitRemoveAbility(GetTriggerUnit(), CAST_AID)
endfunction
private function Init takes nothing returns nothing
local trigger t=CreateTrigger()
call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_HERO_SKILL)
call TriggerAddCondition(t, Condition(function IsLearnAbility))
call TriggerAddAction(t, function Learning)
set t=CreateTrigger() // just in case someone uses that damn tome of relearning // untested code
call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_PICKUP_ITEM)
call TriggerAddCondition(t, Condition(function UnlearnedAbility))
call TriggerAddAction(t, function ResetAbility)
call RegisterSpellEffectResponse(CAST_AID, CreateProxy)
call RegisterSpellCastResponse(CAST_AID, CheckValidTarget)
call RegisterSpellEffectResponse(RELEASE_AID, Release)
endfunction
endlibrary[/hidden]