scope SleightOfFistSpell/* v1.22
by Flux
http://www.hiveworkshop.com/forums/members/flux/
DESCRIPTION:
Ember Spirit dashes around with blazing speed, attacking all
enemies in the targeted area of effect, then returning to his
start location. Deals bonus damage to heroes, and less damage
to creeps.
NOTES:
Required:
- BonusMod
http://www.hiveworkshop.com/forums/graveyard-418/system-bonus-mod-setunitmaxstate-65622/
optionally requires:
- Table
http://www.hiveworkshop.com/forums/jass-resources-412/snippet-new-table-188084/
- MissileRecycler
http://www.hiveworkshop.com/forums/jass-resources-412/system-missilerecycler-206086/
- SpellEffectEvent
http://www.hiveworkshop.com/forums/jass-resources-412/snippet-spelleffectevent-187193/
CREDITS:
Earth-Fury: BonusMod
Bribe: Missile Recycler, SpellEffectEvent, Table
ILH: Imported Slash Effect
Vexorian: Attachable Dummy model
*/
native UnitAlive takes unit u returns boolean
//==================================================================
//====================== CONFIGURATION ===========================
//==================================================================
globals
//------------------------------------------------------
// ----------------- RAWCODES ----------------------
//------------------------------------------------------
//The Rawcode of the Spell
private constant integer SPELL_ID = 'Asof'
//The Rawcode of a Spell that will immediately ends
//Sleight of Fist when cast, e.g. Fire Remnant
private constant integer SPELL_END_ID = 'A000'
//While in Sleight of Fist, caster have maximum attack speed
//provided by this ability
private constant integer SPELL_BONUS_ATTACKSPEED = 'SfSB'
//Deals 50% damage to non-hero units
//Spell Book Rawcodes
private constant integer DAMAGE_DEC = 'SfHL'
//Command Aura based Ability
private constant integer DECREASE_DAMAGE = 'SfLD'
//Buff
private constant integer DAMAGE_BUFF = 'Bsof'
//Ability that transform the caster to a new unit with no
//Attack Damage Point allowing him to attack instantly
private constant integer SLEIGHT_OF_FIST_TRANSFORM = 'SfTr'
//Ability that transform caster back to normal
private constant integer SLEIGHT_OF_FIST_BACK = 'SfBa'
//To prevent caster from attacking twice, his attack is temporarily
//disable when waiting for the next jump
private constant integer DISABLE_ATTACK = 'SfNA'
//This spell creates a remnant at your location since the caster
//will eventually return to that position after slashing all targets
//Dummy unit for remnant
private constant integer DUMMY_ID = 'dumi'
//------------------------------------------------------
// -------------- REMNANT PROPERTIES ------------------
//------------------------------------------------------
//Remnant color
private constant integer REMNANT_RED = 255
private constant integer REMNANT_GREEN = 120
private constant integer REMNANT_BLUE = 20
private constant integer REMNANT_ALPHA = 220
//Value between 0 and 255, where 0 means fully transparent
//Opacity of the caster casting Sleight of Fist
private constant integer OPACITY = 130
//------------------------------------------------------
// ----------------- VISUAL EFFECTS -------------------
//------------------------------------------------------
//Units that will be attacked will be marked
private constant string TARGET_MARKING = "Abilities\\Spells\\Orc\\TrollBerserk\\HeadhunterWEAPONSLeft.mdl"
//Where the TARGET_MARKING will appear
private constant string TARGET_MARKING_ATTACHMENT = "overhead"
//Area of Effect Border
private constant string AOE_BORDER = "Environment\\LargeBuildingFire\\LargeBuildingFire2.mdl"
//How far away AOE_BORDER are to each other
private constant integer AOE_BORDER_SPACING = 64
//While casting Sleight of Fist, caster will have an attached sfx
private constant string CASTER_SFX = "Abilities\\Weapons\\PhoenixMissile\\Phoenix_Missile.mdl"
//When Sleight of Fist is cast, it leaves a remnant in the casting position
private constant string REMNANT_MODEL = "Units\\Creeps\\FirePandarenBrewmaster\\FirePandarenBrewmaster.mdl"
private constant string SLICED_SFX = "war3mapImported\\Slash.mdx"
//The caster's animation when no unit is hit
private constant string ANIMATION = "spell"
//------------------------------------------------------
// ----------- SPELL TIMING PROPERTIES ---------------
//------------------------------------------------------
//How long the caster will wait before it attacks the next target.
//Do not use values lower than 0.15 (based on Tested Values at Reaction Delay = 0)
private constant real JUMP_INTERVAL = 0.2
//The delay before attacking the first unit and before returning to the original location
//Can be any value, the purpose is to create a tail in the Sfx
private constant real FIRST_JUMP_DELAY = 0.05
//How much faster the attack animation when in Sleight of Fist
private constant real TIME_SCALE = 5.0
//------------------------------------------------------
// -------------- PRELOADING OPTION ------------------
//------------------------------------------------------
//Preload abilities so that it won't lag the first time the spell is used
private constant boolean PRELOAD = true
endglobals
//------------------------------------------------------------------
// -------------------- SPELL PROPERTIES -------------------------
//------------------------------------------------------------------
//Must match Object Editor Data
private function AreaOfEffect takes integer lvl returns real
return lvl*100.0 + 150.0
endfunction
//Bonus Damage to Units belonging in BonusFilter
private function BonusDamage takes integer lvl returns integer
return lvl*20 //20, 40, 60, 80
endfunction
//Determines what gets targeted
private function TargetFilter takes player owner, unit target returns boolean
return (UnitAlive(target) and IsUnitEnemy(target, owner) and not IsUnitType(target, UNIT_TYPE_STRUCTURE) and not IsUnitType(target, UNIT_TYPE_FLYING))
endfunction
//Determines what gets extra damage
private function BonusFilter takes unit target returns boolean
return IsUnitType(target, UNIT_TYPE_HERO)
endfunction
//Other properties can be configured in Object Editor
//==================================================================
//==================== END CONFIGURATION =========================
//==================================================================
private struct spell
//Handles
private unit caster
private unit remnant
private player owner
private group g
private timer t
private effect casterSfx1
private effect casterSfx2
private effect remnantModel
private integer lvl
private real x
private real y
private integer bonus
private integer bonusDmg
private boolean firstTarget
private static thistype instance
static if LIBRARY_Table then
private static Table tb
else
private static hashtable hash = InitHashtable()
endif
private static real targetX
private static real targetY
private static real aoe
private static boolean groupHasUnits
private method destroy takes nothing returns nothing
local unit u
local integer id
//Revert caster properties back to normal
call UnitAddAbility(.caster, SLEIGHT_OF_FIST_BACK)
call UnitRemoveAbility(.caster, SLEIGHT_OF_FIST_BACK)
call UnitClearBonus(.caster, BONUS_DAMAGE)
call SetUnitTimeScale(.caster, 1)
//If all units were not attacked for whatever
//reason, remove the markings on unattacked units
loop
set u = FirstOfGroup(.g)
exitwhen u == null
call GroupRemoveUnit(.g, u)
//Remove Target Marking
static if LIBRARY_Table then
set id = GetHandleId(u)
call DestroyEffect(tb.effect[id])
call tb.remove(id)
else
set id = GetHandleId(u)
call DestroyEffect(LoadEffectHandle(hash, id, 0))
call FlushChildHashtable(hash, id)
endif
endloop
//Clean Table/Hash
static if LIBRARY_Table then
call tb.remove(GetHandleId(.caster))
call tb.remove(GetHandleId(.t))
else
call FlushChildHashtable(hash, GetHandleId(.caster))
call FlushChildHashtable(hash, GetHandleId(.t))
endif
//Remove the Damage Buff
call UnitRemoveAbility(.caster, DAMAGE_BUFF)
//destroy handles
call DestroyEffect(.casterSfx1)
call DestroyEffect(.casterSfx2)
call DestroyGroup(.g)
call DestroyTimer(.t)
call DestroyEffect(.remnantModel)
static if LIBRARY_MissileRecycler then
call RecycleMissile(.remnant)
call SetUnitScale(.remnant, 0, 0, 0)
else
call KillUnit(.remnant)
endif
//not necessary, but I'll do it anyway
set .casterSfx1 = null
set .casterSfx2 = null
set .remnantModel = null
set .caster = null
set .remnant = null
set .owner = null
set .g = null
set .t = null
//necessary
call .deallocate()
endmethod
private static method delayedDestroy takes nothing returns nothing
local integer id = GetHandleId(GetExpiredTimer())
static if LIBRARY_Table then
if tb.has(id) then
call thistype(tb[id]).destroy()
endif
else
if HaveSavedInteger(hash, id, 0) then
call thistype(LoadInteger(hash, id, 0)).destroy()
endif
endif
endmethod
private static method removeAtk takes nothing returns nothing
local timer t = GetExpiredTimer()
local integer id = GetHandleId(t)
local thistype this
//Only remove the attack if there is still an existing
//instance of the spell
static if LIBRARY_Table then
if tb.has(id) then
set this = tb[id]
call UnitAddAbility(.caster, DISABLE_ATTACK)
endif
else
if HaveSavedInteger(hash, id, 0) then
set this = LoadInteger(hash, id, 0)
call UnitAddAbility(.caster, DISABLE_ATTACK)
endif
endif
call DestroyTimer(t)
set t = null
endmethod
private static method periodic takes nothing returns nothing
static if LIBRARY_Table then
local thistype this = tb[GetHandleId(GetExpiredTimer())]
else
local thistype this = LoadInteger(hash, GetHandleId(GetExpiredTimer()), 0)
endif
local integer id
local unit u
local real x
local real y
local real angle
local boolean exit = false
local integer hid
local timer delay
local unit slicedSfx
local fogmodifier fog
loop
set u = FirstOfGroup(.g)
call GroupRemoveUnit(.g, u)
if u == null then
//Return caster back to his casting location
call SetUnitX(.caster, .x)
call SetUnitY(.caster, .y)
call IssueImmediateOrderById(.caster, 851972)
call TimerStart(.t, FIRST_JUMP_DELAY, false, function thistype.delayedDestroy)
exitwhen true
else
set x = GetUnitX(u)
set y = GetUnitY(u)
//Reveal Target
set fog = CreateFogModifierRadius(.owner, FOG_OF_WAR_VISIBLE, x, y, 150, false, false)
call FogModifierStart(fog)
//If the target happens to die, check agan using TargetFilter
if IsUnitVisible(u, .owner) and TargetFilter(.owner, u) then
//Enable the caster to attack
call UnitRemoveAbility(.caster, DISABLE_ATTACK)
//Altering Damage based on Target
if BonusFilter(u) then
if .bonus != 1 then
call UnitRemoveAbility(.caster, DAMAGE_DEC)
call UnitRemoveAbility(.caster, DAMAGE_BUFF)
call UnitSetBonus(.caster, BONUS_DAMAGE, .bonusDmg)
set .bonus = 1
endif
else
if .bonus != -1 then
call UnitClearBonus(.caster, BONUS_DAMAGE)
call UnitAddAbility(.caster, DAMAGE_DEC)
set .bonus = -1
endif
endif
set angle = GetRandomReal(0, 360) //random angle for the sfx
static if LIBRARY_MissileRecycler then
set slicedSfx = GetRecycledMissile(x, y, 0, angle)
call DestroyEffect(AddSpecialEffectTarget(SLICED_SFX, slicedSfx, "origin"))
call RecycleMissile(slicedSfx)
else
set slicedSfx = CreateUnit(.owner, DUMMY_ID, x, y, angle)
call DestroyEffect(AddSpecialEffectTarget(SLICED_SFX, slicedSfx, "origin"))
call UnitApplyTimedLife(slicedSfx, 'BTLF', 1.0)
endif
set slicedSfx = null
//Position the caster where he doesn't need to turn.
set angle = -GetUnitFacing(.caster)*bj_DEGTORAD
call SetUnitX(.caster, x + 50*Cos(angle))
call SetUnitY(.caster, y + 50*Sin(angle))
//Order to attack
call IssueTargetOrderById(.caster, 851983, u)
//Remove Attack timer
set delay = CreateTimer()
static if LIBRARY_Table then
set tb[GetHandleId(delay)] = this
else
call SaveInteger(hash, GetHandleId(delay), 0, this)
endif
//0.103 is enough time to make the caster attack the target once
//See here http://www.hiveworkshop.com/forums/pastebin_data/scr8y9/Damage%20Delay.jpg
// ^Those results were consistent/precise.
call TimerStart(delay, 0.103, false, function thistype.removeAtk)
set delay = null
if .firstTarget then
set .firstTarget = false
call TimerStart(.t, JUMP_INTERVAL, true, function thistype.periodic)
endif
//Only exit the loop when a visible target is attacked
set exit = true
endif
call DestroyFogModifier(fog)
set fog = null
//Remove Target Marking
static if LIBRARY_Table then
set id = GetHandleId(u)
call DestroyEffect(tb.effect[id])
call tb.remove(id)
else
set id = GetHandleId(u)
call DestroyEffect(LoadEffectHandle(hash, id, 0))
call FlushChildHashtable(hash, id)
endif
endif
set u = null
exitwhen exit
endloop
endmethod
private static method enumGroup takes nothing returns nothing
local unit u = GetEnumUnit()
local thistype this = instance
local fogmodifier fog
if TargetFilter(.owner, u) and IsUnitInRangeXY(u, targetX, targetY, aoe) then
set fog = CreateFogModifierRadius(.owner, FOG_OF_WAR_VISIBLE, GetUnitX(u), GetUnitY(u), 150, false, false)
call FogModifierStart(fog)
if not IsUnitInvisible(u, .owner) then
//Mark unit with SFX
static if LIBRARY_Table then
set tb.effect[GetHandleId(u)] = AddSpecialEffectTarget(TARGET_MARKING, u, TARGET_MARKING_ATTACHMENT)
else
call SaveEffectHandle(hash, GetHandleId(u), 0, AddSpecialEffectTarget(TARGET_MARKING, u, TARGET_MARKING_ATTACHMENT))
endif
if not groupHasUnits then
set groupHasUnits = true
endif
else
call GroupRemoveUnit(.g, u)
endif
call DestroyFogModifier(fog)
set fog = null
else
call GroupRemoveUnit(.g, u)
endif
set u = null
endmethod
private static method noHitAnimation takes nothing returns nothing
local timer delay = GetExpiredTimer()
local integer id = GetHandleId(delay)
static if LIBRARY_Table then
call SetUnitAnimation(tb.unit[id], ANIMATION)
call tb.remove(id)
else
call SetUnitAnimation(LoadUnitHandle(hash, id, 0), ANIMATION)
call FlushChildHashtable(hash, id)
endif
call DestroyTimer(delay)
set delay = null
endmethod
private static method onCast takes nothing returns boolean
local thistype this = allocate()
local integer id
local real end = 2*bj_PI
local real i = 0
local timer delay
set .caster = GetTriggerUnit()
set id = GetHandleId(.caster)
//If the caster is currently in Sleight of Fist, destroy it
static if LIBRARY_Table then
if tb.has(id) then
call thistype(tb[id]).destroy()
endif
else
if HaveSavedInteger(hash, id, 0) then
call thistype(LoadInteger(hash, id, 0)).destroy()
endif
endif
set .owner = GetTriggerPlayer()
set .lvl = GetUnitAbilityLevel(caster, SPELL_ID)
set targetX = GetSpellTargetX()
set targetY = GetSpellTargetY()
set .x = GetUnitX(.caster)
set .y = GetUnitY(.caster)
set aoe = AreaOfEffect(.lvl)
set .firstTarget = true
set .g = CreateGroup()
//Group globals
set instance = this
set groupHasUnits = false
call GroupEnumUnitsInRange(.g, targetX, targetY, aoe + 128.0, null)
call ForGroup(.g, function thistype.enumGroup)
if groupHasUnits then
//Bonus Damage
set .bonus = 0
set .bonusDmg = BonusDamage(.lvl)
//Create ATTACH_SFX
set .casterSfx1 = AddSpecialEffectTarget(CASTER_SFX, .caster, "right,weapon")
set .casterSfx2 = AddSpecialEffectTarget(CASTER_SFX, .caster, "left,weapon")
//Transform
call UnitAddAbility(.caster, SLEIGHT_OF_FIST_TRANSFORM)
call UnitRemoveAbility(.caster, SLEIGHT_OF_FIST_TRANSFORM)
//Add Bonus Attack Speed
call UnitAddAbility(.caster, SPELL_BONUS_ATTACKSPEED)
//Disable Attack
call UnitAddAbility(.caster, DISABLE_ATTACK)
//Disable Movement
call UnitRemoveAbility(.caster, 'Amov')
//Make the caster invulnerable
call UnitAddAbility(.caster, 'Avul')
//Other Properties
call SetUnitPathing(.caster, false)
call SetUnitTimeScale(.caster, TIME_SCALE)
call SetUnitVertexColor(.caster, 255, 255, 255, OPACITY)
//Create REMNANT_SFX in your caster's location
static if LIBRARY_MissileRecycler then
set .remnant = GetRecycledMissile(x, y, 0, GetUnitFacing(.caster))
set .remnantModel = AddSpecialEffectTarget(REMNANT_MODEL, .remnant, "origin")
call SetUnitVertexColor(.remnant, REMNANT_RED, REMNANT_GREEN, REMNANT_BLUE, REMNANT_ALPHA)
else
set .remnant = CreateUnit(.owner, DUMMY_ID, x, y, GetUnitFacing(.caster))
set .remnantModel = AddSpecialEffectTarget(REMNANT_MODEL, .remnant, "origin")
call SetUnitVertexColor(.remnant, REMNANT_RED, REMNANT_GREEN, REMNANT_BLUE, REMNANT_ALPHA)
call PauseUnit(.remnant, true)
endif
set .t = CreateTimer()
//Store in Table/Hash
static if LIBRARY_Table then
set tb[id] = this
set tb[GetHandleId(.t)] = this
else
call SaveInteger(hash, id, 0, this)
call SaveInteger(hash, GetHandleId(.t), 0, this)
endif
//Start Timer
call TimerStart(.t, FIRST_JUMP_DELAY, false, function thistype.periodic)
else
//When no unit is hit, perform some animation
set delay = CreateTimer()
static if LIBRARY_Table then
set tb.unit[GetHandleId(delay)] = .caster
else
call SaveUnitHandle(hash, GetHandleId(delay), 0, .caster)
endif
call TimerStart(delay, 0.01, false, function thistype.noHitAnimation)
set delay = null
//Clean stuffs
call DestroyGroup(.g)
set .g = null
set .owner = null
set .caster = null
call .deallocate()
endif
//Create AOE Border, i = 0
loop
exitwhen i > end
call DestroyEffect(AddSpecialEffect(AOE_BORDER, targetX + aoe*Cos(i), targetY + aoe*Sin(i)))
set i = i + AOE_BORDER_SPACING/aoe
endloop
return false
endmethod
private static method onEnd takes nothing returns boolean
local thistype this
local integer id = GetHandleId(GetTriggerUnit())
static if LIBRARY_Table then
if tb.has(id) then
call thistype(tb[id]).destroy()
endif
else
if HaveSavedInteger(hash, id, 0) then
call thistype(LoadInteger(hash, id, 0)).destroy()
endif
endif
return false
endmethod
static if not LIBRARY_SpellEffectEvent then
private static method condition takes nothing returns boolean
return (GetSpellAbilityId() == SPELL_ID and thistype.onCast() )
endmethod
endif
static if not LIBRARY_SpellEffectEvent then
private static method endCondition takes nothing returns boolean
return (GetSpellAbilityId() == SPELL_END_ID and thistype.onEnd() )
endmethod
endif
private static method onInit takes nothing returns nothing
local integer i = 0
static if PRELOAD then
local unit u = CreateUnit(Player(0), DUMMY_ID, 0, 0, 0)
endif
static if LIBRARY_SpellEffectEvent then
call RegisterSpellEffectEvent(SPELL_ID, function thistype.onCast)
call RegisterSpellEffectEvent(SPELL_END_ID, function thistype.onEnd)
else
local trigger t1 = CreateTrigger()
local trigger t2 = CreateTrigger()
local player p
loop
set p = Player(i)
call TriggerRegisterPlayerUnitEvent(t1, p, EVENT_PLAYER_UNIT_SPELL_EFFECT, null)
call TriggerRegisterPlayerUnitEvent(t2, p, EVENT_PLAYER_UNIT_SPELL_EFFECT, null)
set i = i + 1
exitwhen i == bj_MAX_PLAYER_SLOTS
endloop
call TriggerAddCondition(t1, Condition(function thistype.condition))
call TriggerAddCondition(t2, Condition(function thistype.endCondition))
set t1 = null
set t2 = null
set i = 0
endif
static if LIBRARY_Table then
set tb = tb.create()
endif
loop
call SetPlayerAbilityAvailable(Player(i), DAMAGE_DEC, false)
set i = i + 1
exitwhen i == bj_MAX_PLAYER_SLOTS
endloop
//Preload
static if PRELOAD then
call UnitAddAbility(u, DAMAGE_DEC)
call UnitAddAbility(u, DISABLE_ATTACK)
call UnitAddAbility(u, SPELL_BONUS_ATTACKSPEED)
call UnitAddAbility(u, SLEIGHT_OF_FIST_TRANSFORM)
call UnitAddAbility(u, SLEIGHT_OF_FIST_BACK)
call RemoveUnit(u)
set u = null
endif
endmethod
endstruct
endscope
//The studied warrior must whip and weave through
//its enemies, burning each without pause.