Pudge Wars Meat Hook
Revisited
Version 1.1.2
General Information:
After reading over Trollvottel's Pudge Wars Meat Hook, I decided to attempt my own version of the spell to fix some of the bugs and stuff I found with the spell. This is the final product of my work, a totally rewritten script that uses similar methods to that of the Meat Hook in Pudge Wars (linked list to keep track of units). I also scabbed Trollvottel's bouncing formula.
Requires:
- GTrigger by Jesus4Lyf.
- AIDS by Jesus4Lyf.
- T32 by Jesus4Lyf.
- AutoFly by Azlier (optional).
- JassHelper. At least version 0.A.2.A, however the latest is preferable.
Details:
- Spell is written in vJASS.
- Spell is MUI/MPI.
- Spell is leakless.
- Spell generates very little lag (as much if not less than the latest Meat Hook from Pudge Wars). By similar lag to Pudge Wars, I mean a similar FPS drop (Actually about 2-3 more FPS than pudge wars).
- Spell is highly configurable.
Description:
Launches a hook which will grab the first unit which comes in range of the hookhead. Will bounce off buildings and map boundaries.
Level 1 - Deals 100 damage, has 1200 maximum range.
Level 2 - Deals 200 damage, has 1600 maximum range.
Level 3 - Deals 300 damage, has 2000 maximum range.
Level 1 - Deals 100 damage, has 1200 maximum range.
Level 2 - Deals 200 damage, has 1600 maximum range.
Level 3 - Deals 300 damage, has 2000 maximum range.
Screenshots:
Implementation:
JASS:
//------------------------------------------------------------------------------------\\
// \\
// To Implement: \\
// \\
// - This version requires GTrigger by Jesus4Lyf. Either copy the trigger labelled \\
// GT or create a new trigger in your map, convert it to custom script and paste \\
// the script for GTrigger into it. \\
// \\
// - This version also requires T32 and AIDS by Jesus4Lyf. Follow the instructions \\
// on how to implement them in the documentation for each system. \\
// \\
// - This version optionally requires AutoFly by Azlier. Just copy and paste the \\
// AutoFly library into your map and you will be able to adjust the hooks height \\
// from the MeatHook library. ` \\
// \\
// - Copy the MeatHookRevisited trigger below this, or make a new trigger in your \\
// map and name it MeatHookRevisited, then copy the script from this map to you \\
// own. You can actually name the trigger whatever you want. Eg: MeatHook. \\
// \\
// - Import the headshot.wav sound into your map from this map and don't change \\
// the path. The path should be: war3mapImported\headshot.wav \\
// \\
// - Copy the 2 units and 1 spell needed for the spell into your map. \\
// \\
// - Once you have copied the spell and units, change the raw codes in the \\
// MeatHookRevisited trigger to the ones in your map, then change any of the \\
// configurables you want. \\
// \\
// - Add the ability to a unit and enjoy <img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" class="smilie smilie--sprite smilie--sprite1" alt=":)" title="Smile :)" loading="lazy" data-shortname=":)" />. \\
// \\
//------------------------------------------------------------------------------------\\
//------------------------------------------------------------------------------------\\
// \\
// Credits: \\
// \\
// - Credits to Tossrock for the original concept behind the spell. \\
// \\
// - Credits to Vexorian for JassHelper, and the NewGen team for NewGen WE. \\
// \\
// - Credits to Trollvottel for some ideas for the spell and the test map. \\
// \\
// - Credits to Jesus4Lyf for GTrigger. It is awesome. \\
// \\
// - Credits to Jesus4Lyf for T32. \\
// \\
// - Credits to Jesus4Lyf for AIDS. \\
// \\
// - Credits to whoever made the headshot.wav sound (Probably from Unreal Tourny). \\
// \\
// - And thanks to anyone who helped with this spell. \\
// \\
//------------------------------------------------------------------------------------\\
Configurable Descriptions:
JASS:
//------------------------------------------------------------------------------------\\
// \\
// Config Descriptions: \\
// ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯ \\
// Below is a description of all the constant globals and functions used in the \\
// Pudge Wars Meat Hook spell. \\
// \\
// GLOBALS: \\
// ¯¯¯¯¯¯¯¯¯ \\
// Integers: \\
// ¯¯¯¯¯¯¯¯¯¯ \\
// - ABIL_ID - Raw code of the hero ability 'Meat Hook'. \\
// \\
// - HEAD_ID - Raw code of the dummy unit used as the 'head' of the hook. \\
// \\
// - LINK_ID - Raw code of the dummy unit used as the links in the chain. \\
// \\
// - LOCUST_ID - Raw code of the locust ability. \\
// \\
// - MAX_LINKS - Max amount of links that can be created at any one time. \\
// \\
// - ANIM_INDEX - Animation of the unit by index. \\
// \\
// - UNITS_PER_INT - How many links can be created per interval. Limit is put \\
// in place to reduce lag if the caster moves a long distance \\
// over a short period of time. \\
// \\
// - HEADSHOT_VOL - The volume of the headshot sound when used (xxx/127). \\
// \\
// Reals: \\
// ¯¯¯¯¯¯¯ \\
// - MAX_HEIGHT - Height units have to be under to be considered targets. \\
// \\
// - UNIT_RADIUS - Radius to pick up units near the hook head. \\
// \\
// - BUILD_RADIUS - Radius to detect nearby buildings to bounce off. \\
// \\
// - DEST_RADIUS - Radius to detect nearby destructables to bounce off. \\
// \\
// - TIME_SCALE - Time scale (speed of animations) for the hook head. \\
// \\
// - HEAD_SCALE - Scale size of the head of the chain. \\
// \\
// - LINK_SCALE - Scale size of the links of the chain. \\
// \\
// - FLY_HEIGHT - Height of the chain off the ground. Will only be set if you \\
// have AutoFly in your map. If you don't you need to set the \\
// height via object editor. \\
// \\
// - DEST_DMG_PERC - Percentage of original damage (dealt to units) that will be \\
// dealt to destructables if the user wishes for them to be \\
// damaged. Set this to 0.00 to deal no damage. \\
// \\
// Strings: \\
// ¯¯¯¯¯¯¯¯¯ \\
// - BLOOD_SFX - Special effect played when an enemy is hit by a hook. \\
// \\
// - BLOOD_POINT - Attachment point of the above special effect. \\
// \\
// - HEADSHOT_TEXT - Text that is shown when a headshot occurs. \\
// \\
// - TEXT_COLOUR - The colour of the headshot text, in that hexidecimal format \\
// or whatever it is. \\
// \\
// - HEADSHOT_SOUND - The sound that is played when a headshot occurs. Be sure to \\
// import the custom sound into your map (and not change the \\
// path) so that the sound will work. \\
// \\
// Booleans: \\
// ¯¯¯¯¯¯¯¯¯¯ \\
// - ALLOW_PRELOAD - Whether or not to allow preloading of special effects. \\
// \\
// - ALLOW_BOUNCE - Whether or not to allow the hook to bounce off of buildings. \\
// If this is true, the hook cannot be used to grapple. \\
// \\
// - ALLOW_GRAPPLE - Whether or not to allow the hook to grapple to building. \\
// Cannot grapple if the hook is allowed to bounce. \\
// \\
// - ALLOW_HEADSHOT - Whether or not to allow headshots to occur. \\
// \\
// - ABOVE_CASTER - If true, the HEADSHOT! text will be above the caster, other- \\
// wise it will be above the dying unit. \\
// \\
// - DISCARD_TARGET - If true, when the target unit of the hook dies, it will be \\
// let go, and the hook can grab another unit. If false, when \\
// the target dies, it will be dragged back to the caster. \\
// \\
// - DEST_BOUNCE - Whether or not to allow the hook to bounce off of destruct- \\
// ables. \\
// \\
// - KILL_DESTS - Whether or not to kill destructables that are hit by the \\
// hook. If set to false, the destructables will be damaged \\
// depending on DAMAGE() * DEST_DMG_PERC. \\
// \\
// FUNCTIONS: \\
// ¯¯¯¯¯¯¯¯¯¯¯ \\
// - MAXDIST - Maximum distance of the meat hook. \\
// \\
// - DAMAGE - Damage dealt to enemy units by the hook. \\
// \\
// - SPEED - Speed the hook travels per second (in wc3 units). \\
// \\
// - FilterUnits - Custom filter function to filter out any units you don't \\
// want to be hooked. \\
// \\
//------------------------------------------------------------------------------------\\
Script:
JASS:
//------------------------------------------------------------------------------------\\
// Pudge Wars Meat Hook [v1.1.2] \\
// By kenny! \\
// Constructed using vJASS \\
// Requires NewGen WE, GT, AIDS, T32 & AutoFly (optional). \\
//------------------------------------------------------------------------------------\\
library MeatHook requires GT, AIDS, T32, optional AutoFly
globals
private constant integer ABIL_ID = 039;A000039;
private constant integer HEAD_ID = 039;h000039;
private constant integer LINK_ID = 039;h001039;
private constant integer LOCUST_ID = 039;Aloc039;
private constant integer MAX_LINKS = 8191
private constant integer ANIM_INDEX = 2
private constant integer UNITS_PER_INT = 10
private constant integer HEADSHOT_VOL = 120
private constant real MAX_HEIGHT = 40.00
private constant real UNIT_RADIUS = 100.00
private constant real BUILD_RADIUS = 128.00
private constant real DEST_RADIUS = 128.00
private constant real TIME_SCALE = 1.00
private constant real HEAD_SCALE = 1.00
private constant real LINK_SCALE = 1.25
private constant real FLY_HEIGHT = 75.00
private constant real DEST_DMG_PERC = 0.00
private constant string BLOOD_SFX = "Objects\\Spawnmodels\\Human\\HumanBlood\\BloodElfSpellThiefBlood.mdl"
private constant string BLOOD_POINT = "origin"
private constant string HEADSHOT_TEXT = "HEADSHOT!"
private constant string TEXT_COLOUR = "|cFFFF0000"
private constant string HEADSHOT_SOUND = "war3mapImported\\headshot.wav"
private constant attacktype A_TYPE = ATTACK_TYPE_CHAOS
private constant damagetype D_TYPE = DAMAGE_TYPE_UNIVERSAL
private constant weapontype W_TYPE = WEAPON_TYPE_WHOKNOWS
private constant boolean ALLOW_PRELOAD = true
private constant boolean ALLOW_BOUNCE = true
private constant boolean ALLOW_HEADSHOT = true
private constant boolean DEST_BOUNCE = true
private constant boolean ALLOW_GRAPPLE = false
private constant boolean ABOVE_CASTER = false
private constant boolean DISCARD_TARGET = false
private constant boolean KILL_DESTS = false
endglobals
//---------------------------------------------------------------------\\
private constant function MAXDIST takes integer lvl returns real
return 800.00 + (400.00 * lvl)
endfunction
private constant function DAMAGE takes integer lvl returns real
return 0.00 + (100.00 * lvl)
endfunction
private constant function SPEED takes integer lvl returns real
return 1000.00 + (0.00 * lvl)
endfunction
private function FilterUnits takes unit caster, unit filter returns boolean
return GetUnitFlyHeight(filter) < MAX_HEIGHT and GetUnitAbilityLevel(filter,LOCUST_ID) == 0
endfunction
//---------------------------------------------------------------------\\
// \\
// DO NOT TOUCH PAST THIS POINT UNLESS YOU KNOW WHAT YOUR ARE DOING! \\
// \\
//---------------------------------------------------------------------\\
// To make peoples happy. To bad there is no WidgetAlive native.
native UnitAlive takes unit id returns boolean
private struct Link[MAX_LINKS] // Individual links in the linked list for units.
thistype next = 0 // Next link in the list.
thistype prev = 0 // Previous link in the list.
unit link = null // The actual chain link.
// Reset all the members for this struct instance.
private method onDestroy takes nothing returns nothing
// Remove the chain link.
call RemoveUnit(this.link)
set this.link = null
endmethod
endstruct
//---------------------------------------------------------------------\\
private struct List // Linked list to hold all the units that make the chain.
Link head = 0 // First link in the list.
Link last = 0 // Last link in the list.
integer size = 0 // Size of the list.
// Attach a link to the list.
method attach takes nothing returns Link
local Link curr = Link.create() // Create a link.
// Restructuring the start of the list.
if this.head == 0 then
// If there is nothing in the first, set the new link to the head.
set this.head = curr
else
// Otherwise, attach the new link to the end of the list.
set curr.prev = this.last
set this.last.next = curr
endif
// Setting the rest of this link and incrementing list size.
set this.last = curr
set this.size = this.size + 1
return curr
endmethod
// Detach a link from the list.
method detach takes Link curr returns nothing
// Removing a link from the list.
if curr.next == 0 then
// If we are removing the last node, reset last.
set this.last = curr.prev
else
set curr.next.prev = curr.prev
endif
set curr.prev.next = curr.next
// Destroy the link.
call curr.destroy()
// Decrement size of list.
set this.size = this.size - 1
endmethod
// Destroy the list.
private method onDestroy takes nothing returns nothing
// Just to make sure no links are left.
loop
exitwhen this.size == 0
call this.detach(this.last)
endloop
endmethod
endstruct
//---------------------------------------------------------------------\\
private struct IsTarget extends array // To check if a unit is already a target.
//! runtextmacro AIDS()
integer level // This gets incremented by 1 every time a unit becomes a target, and decremented when discarded.
private static method AIDS_filter takes unit u returns boolean
return GetUnitAbilityLevel(u,LOCUST_ID) == 0
endmethod
private method AIDS_onCreate takes nothing returns nothing
set this.level = 0 // Initialise the variable.
endmethod
private method AIDS_onDestroy takes nothing returns nothing
set this.level = 0 // Set it to 0 upon death.
endmethod
endstruct
//---------------------------------------------------------------------\\
private struct IsGrappling extends array // To check if a unit is already grappling.
//! runtextmacro AIDS()
integer level // This gets incremented by 1 every time a unit starts grappling, and decremented when it finishes.
private static method AIDS_filter takes unit u returns boolean
return GetUnitAbilityLevel(u,LOCUST_ID) == 0
endmethod
private method AIDS_onCreate takes nothing returns nothing
set this.level = 0 // Initialise the variable.
endmethod
private method AIDS_onDestroy takes nothing returns nothing
set this.level = 0 // Set it to 0 upon death.
endmethod
endstruct
//---------------------------------------------------------------------\\
private struct Hook // Struct that takes care of the rest.
// Instance members.
unit cast = null // The caster.
unit targ = null // The possible target.
unit head = null // The head of the chain.
player own = null // The owner of the caster.
real ang = 0.00 // The angle the chain is travelling in.
real dist = 0.00 // The current distance travelled.
real max = 0.00 // Maximum distance of the hook.
real cos = 0.00 // Cos of the current angle.
real sin = 0.00 // Sin of the current angle.
real speed = 0.00 // Speed of the hook, saves function calls.
integer level = 0 // Level of the Meat Hook spell for the caster.
List list = 0 // Linked list needed to track chain links.
boolean ret = false // If the hook is extending.
boolean hit = false // If a unit has been hit.
boolean grap = false // If the caster is grappling.
// Static members.
static thistype tempData = 0 // Temp struct needed for data transfer.
static rect enumRect = null // Rect for bouncing off destructables.
static group enumGroup = null // Group for enumerating units and buildings.
static code enumDests = null // Filter function for finding destructables.
static boolexpr enumUnits = null // Filter function for finding units.
static boolexpr enumBuild = null // Filter function for finding buildings.
static real worldMaxX = 0.00 // Map boundaries.
static real worldMaxY = 0.00 // Map boundaries.
static real worldMinX = 0.00 // Map boundaries.
static real worldMinY = 0.00 // Map boundaries.
// Clean up the struct instance and finish the spell.
method destroy takes nothing returns nothing
call this.list.destroy() // Destroy the unit list.
// If there is a target, set it to a position infront of the caster. If you don't like this, make DISTANCE 0.00.
if this.targ != null then
set IsTarget[this.targ].level = IsTarget[this.targ].level - 1
endif
if this.grap then
set IsGrappling[this.cast].level = IsGrappling[this.cast].level - 1
call SetUnitPathing(this.cast,true)
endif
// Clear struct members.
set this.cast = null
set this.targ = null
set this.head = null
set this.ret = false
set this.hit = false
set this.grap = false
call this.deallocate()
endmethod
// Quick text tag for easy use.
method setTextTag takes nothing returns nothing
local texttag text = CreateTextTag()
static if ABOVE_CASTER then
call SetTextTagPos(text,GetUnitX(this.cast),GetUnitY(this.cast),0.00)
else
call SetTextTagPos(text,GetUnitX(this.head),GetUnitY(this.head),0.00)
endif
call SetTextTagPermanent(text,false)
call SetTextTagText(text,TEXT_COLOUR + HEADSHOT_TEXT + "|r",0.023)
call SetTextTagVelocity(text,Cos(1.57079) * 0.0355,Sin(1.57079) * 0.0355)
call SetTextTagLifespan(text,3.00)
call SetTextTagFadepoint(text,1.50)
set text = null
endmethod
// Set the new angle for bouncing.
method setAngle takes real tempx, real tempy returns boolean
local real ang1 = this.ang * bj_RADTODEG
local real ang2 = ModuloReal(Atan2((GetUnitY(this.head) - tempy),(GetUnitX(this.head) - tempx)) * bj_RADTODEG,360.00)
local real diff = RAbsBJ(ang2 - ang1)
// Lots of math stuff <img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" class="smilie smilie--sprite smilie--sprite1" alt=":)" title="Smile :)" loading="lazy" data-shortname=":)" />.
if diff >= 90.00 and diff <= 270.00 then
if ( ang2 > 45.00 and ang2 < 135.00 ) or ( ang2 > 225.00 and ang2 < 315.00 ) then
set this.ang = -ang1 * bj_DEGTORAD
else
set this.ang = (180.00 - ang1) * bj_DEGTORAD
endif
// Set the new travelling angle.
set this.sin = Sin(this.ang)
set this.cos = Cos(this.ang)
return true
endif
return false
endmethod
// Sets the target unit.
method setTarget takes unit target returns boolean
set this.targ = target
set this.hit = true
set IsTarget[this.targ].level = IsTarget[this.targ].level + 1
// Damage the target if it is an enemy.
if IsUnitEnemy(this.targ,this.own) then
call UnitDamageTarget(this.cast,this.targ,DAMAGE(this.level),true,false,A_TYPE,D_TYPE,W_TYPE)
call DestroyEffect(AddSpecialEffectTarget(BLOOD_SFX,this.targ,BLOOD_POINT))
endif
// Stop the hook from moving.
call SetUnitTimeScale(this.head,0.00)
return true
endmethod
// Find any destructables in range of the head and bounce off them.
static method getDest takes nothing returns nothing
local thistype this = thistype.tempData
local destructable dest = GetEnumDestructable()
// Only bounce off alive destructables.
if GetWidgetLife(dest) > 0.406 then
call this.setAngle(GetWidgetX(dest),GetWidgetY(dest))
// Kill them if you want.
static if KILL_DESTS then
call KillDestructable(dest)
else
call UnitDamageTarget(this.cast,dest,(DAMAGE(this.level) * DEST_DMG_PERC),true,false,A_TYPE,D_TYPE,W_TYPE)
endif
endif
set dest = null
endmethod
// Searches for buildings to bounce off.
static method getBuild takes nothing returns boolean
local thistype this = thistype.tempData
local unit filt = GetFilterUnit()
if UnitAlive(filt) and IsUnitType(filt,UNIT_TYPE_STRUCTURE) == true and GetUnitTypeId(filt) != LINK_ID and GetUnitTypeId(filt) != HEAD_ID then
if this.cast != filt and FilterUnits(this.cast,filt) then
static if ALLOW_BOUNCE then // If bouncing is allowed, then set the new angle.
call this.setAngle(GetUnitX(filt),GetUnitY(filt))
else
// Else find out if grappling is allowed.
static if ALLOW_GRAPPLE then
if not this.grap then
if IsGrappling[this.cast].level == 0 then
// If grappling is allowed and the caster is not grappling already, make it grapple.
set IsGrappling[this.cast].level = IsGrappling[this.cast].level + 1
set this.hit = true
set this.grap = true
call SetUnitPathing(this.cast,false)
call SetUnitTimeScale(this.head,0.00)
else
// If the caster is already grappling, but has two hooks that want to grapple, make one retract.
set this.hit = true
endif
endif
endif
endif
endif
endif
set filt = null
return false
endmethod
// Searches for units to become the hook's target.
static method getUnit takes nothing returns boolean
local thistype this = thistype.tempData
local unit filt = GetFilterUnit()
local sound shot = null
if UnitAlive(filt) and IsUnitType(filt,UNIT_TYPE_STRUCTURE) == false and GetUnitTypeId(filt) != LINK_ID and GetUnitTypeId(filt) != HEAD_ID then
if this.cast != filt and FilterUnits(this.cast,filt) then
static if ALLOW_HEADSHOT then // If headshots are allowed continue on.
if not this.hit then // If a unit hasn't been it continue on.
if IsTarget[filt].level != 0 then // If the possible unit is already a target continue.
// Play the sound.
set shot = CreateSound(HEADSHOT_SOUND,false,false,false,10,10,"")
call SetSoundVolume(shot,HEADSHOT_VOL)
call StartSound(shot)
call KillSoundWhenDone(shot)
// Create a text tag.
call this.setTextTag()
// Kill the unit.
call UnitDamageTarget(this.cast,filt,1000000.00,true,false,A_TYPE,D_TYPE,W_TYPE)
call DestroyEffect(AddSpecialEffectTarget(BLOOD_SFX,filt,BLOOD_POINT))
set this.hit = true // Set hit to true
else
// If the unit isn't a target, make it one.
call this.setTarget(filt)
endif
endif
else
// If we aren't allowing headshots, then just find a suitable unit and make it the target.
if not this.hit and IsTarget[filt].level == 0 then
call this.setTarget(filt)
endif
endif
endif
endif
set filt = null
return false
endmethod
// Retract the hook.
method retract takes nothing returns nothing
local Link linkx = 0
local integer count = 0
local unit temp = this.list.last.link
local unit prev = null
local unit dumm = null
local real casx = GetUnitX(this.cast)
local real casy = GetUnitY(this.cast)
local real temx = GetUnitX(temp)
local real temy = GetUnitY(temp)
local real newx = 0.00
local real newy = 0.00
local real ang2 = 0.00
local real dist = SquareRoot((temx - casx) * (temx - casx) + (temy - casy) * (temy - casy))
// If the distance between the caster and the last link is too much, create more links.
if dist > (this.speed * 2.00) and not this.grap then
// Using a loop so the hooks work when great distances are covered in a short time.
loop
exitwhen dist <= (this.speed * 2.00) or count == UNITS_PER_INT// Create as many links as needed to fill the gap.
set ang2 = Atan2((casy - temy),(casx - temx))
set temx = temx + (this.speed * 2.00) * Cos(ang2)
set temy = temy + (this.speed * 2.00) * Sin(ang2)
// Create a new chain link.
set linkx = this.list.attach()
set linkx.link = CreateUnit(this.own,LINK_ID,temx,temy,(ang2 * bj_RADTODEG) + 180.00)
// Set its height and size.
call SetUnitScale(linkx.link,LINK_SCALE,LINK_SCALE,LINK_SCALE)
static if LIBRARY_AutoFly then
call SetUnitFlyHeight(linkx.link,FLY_HEIGHT,0.00)
endif
// Set the new last link in the chain to the last created link.
set temp = linkx.link
set temx = GetUnitX(temp)
set temy = GetUnitY(temp)
set dist = dist - this.speed
set count = count + 1
endloop
endif
// If the caster is grappling, change its position.
if this.grap then
set temx = GetUnitX(this.list.last.link)
set temy = GetUnitY(this.list.last.link)
set ang2 = Atan2((temy - casy),(temx - casx))
// Set the caster's new position using SetUnitPosition() to stop extra casting.
call SetUnitPosition(this.cast,casx + this.speed * Cos(ang2),casy + this.speed * Sin(ang2))
else
// If the caster is no grappling, check for a target unit.
if this.targ != null then
// If there is a target unit, set it's new position.
call SetUnitX(this.targ,GetUnitX(this.head))
call SetUnitY(this.targ,GetUnitY(this.head))
static if DISCARD_TARGET then
// Check if it is bead so it can be discarded.
if not UnitAlive(this.targ) then
set IsTarget[this.targ].level = IsTarget[this.targ].level - 1
set this.targ = null
call SetUnitTimeScale(this.head,TIME_SCALE)
endif
endif
else
// Otherwise, find a new unit.
call GroupEnumUnitsInRange(thistype.enumGroup,GetUnitX(this.head),GetUnitY(this.head),UNIT_RADIUS,thistype.enumUnits)
endif
endif
set linkx = this.list.last
// This is used to remove the unneeded links and to set their new position if they are needed.
loop
exitwhen linkx == 0
set temp = linkx.link
set temx = GetUnitX(temp)
set temy = GetUnitY(temp)
if linkx.prev.prev != 0 then
set dumm = linkx.prev.prev.link
if IsUnitInRange(temp,dumm,(this.speed * 1.50)) then
call this.list.detach(linkx.prev)
call SetUnitFacing(temp,(Atan2((GetUnitY(dumm) - temy),(GetUnitX(dumm) - temx)) * bj_RADTODEG) + 180.00)
endif
endif
// Find the angle that the chain link should be facing.
if temp == this.list.last.link then
set ang2 = Atan2((casy - temy),(casx - temx))
else
set ang2 = Atan2((GetUnitY(prev) - temy),(GetUnitX(prev) - temx))
endif
if linkx == this.list.last and IsUnitInRange(temp,this.cast,this.speed) then
call this.list.detach(linkx)
// Detach the link if needed.
endif
// Set the chain links new position.
if not this.grap then
call SetUnitX(temp,temx + this.speed * Cos(ang2))
call SetUnitY(temp,temy + this.speed * Sin(ang2))
endif
call SetUnitFacing(temp,(ang2 * bj_RADTODEG) + 180.00)
set prev = temp
set linkx = linkx.prev
endloop
set temp = null
set prev = null
set dumm = null
endmethod
// Extend the hook.
method extend takes nothing returns nothing
local Link linkx = 0
local integer count = 0
local unit temp = this.list.last.link
local unit next = null
local unit dumm = null
local real casx = GetUnitX(this.cast)
local real casy = GetUnitY(this.cast)
local real temx = GetUnitX(temp)
local real temy = GetUnitY(temp)
local real newx = 0.00
local real newy = 0.00
local real ang2 = 0.00
local real dist = SquareRoot((temx - casx) * (temx - casx) + (temy - casy) * (temy - casy))
// If the distance between the caster and the last link is too much, create more links.
if dist > (this.speed * 2.00) then
// Using a loop so the hooks work when great distances are covered in a short time.
loop
exitwhen dist <= (this.speed * 2.00) or count == UNITS_PER_INT // Create as many links as needed to fill the gap.
set ang2 = Atan2((casy - temy),(casx - temx))
set temx = temx + (this.speed * 2.00) * Cos(ang2)
set temy = temy + (this.speed * 2.00) * Sin(ang2)
// Create a new chain link.
set linkx = this.list.attach()
set linkx.link = CreateUnit(this.own,LINK_ID,temx,temy,(ang2 * bj_RADTODEG) + 180.00)
// Set its height and size.
call SetUnitScale(linkx.link,LINK_SCALE,LINK_SCALE,LINK_SCALE)
static if LIBRARY_AutoFly then
call SetUnitFlyHeight(linkx.link,FLY_HEIGHT,0.00)
endif
// Set the new last link of the chain the the last created link.
set temp = linkx.link
set temx = GetUnitX(temp)
set temy = GetUnitY(temp)
set dist = dist - this.speed
set count = count + 1
endloop
endif
set linkx = this.list.head
// This loop moves all the current links in the chain.
loop
exitwhen linkx == 0
set temp = linkx.link
set temx = GetUnitX(temp)
set temy = GetUnitY(temp)
if linkx.next.next != 0 then
set dumm = linkx.next.next.link
if IsUnitInRange(temp,dumm,(this.speed * 1.50)) then
call this.list.detach(linkx.next)
call SetUnitFacing(temp,(Atan2((GetUnitY(dumm) - temy),(GetUnitX(dumm) - temx)) * bj_RADTODEG) + 180.00)
endif
endif
// Set the new angle for each chain link.
if temp == this.head then
set ang2 = this.ang // If it is the head link, get the angle to the travelling angle.
else
set ang2 = Atan2((GetUnitY(next) - temy),(GetUnitX(next) - temx))
endif
set newx = temx + this.speed * Cos(ang2)
set newy = temy + this.speed * Sin(ang2)
// If the new point is outside the map, find a new point and make the hook bounce.
if temp == this.head then
if newx > thistype.worldMaxX or newx < thistype.worldMinX then
if this.ang < 0.00 then
set this.ang = -1.57079 + (-1.57079 - this.ang)
else
set this.ang = 1.57079 - (this.ang - 1.57079)
endif
set this.cos = Cos(this.ang)
set this.sin = Sin(this.ang)
elseif newy > thistype.worldMaxY or newy < thistype.worldMinY then
if this.ang < 1.57079 then
set this.ang = 0.00 - this.ang
else
set this.ang = -this.ang
endif
set this.cos = Cos(this.ang)
set this.sin = Sin(this.ang)
endif
// Search for units, buildings and destructables.
set thistype.tempData = this
call GroupEnumUnitsInRange(thistype.enumGroup,newx,newy,UNIT_RADIUS,thistype.enumUnits)
call GroupEnumUnitsInRange(thistype.enumGroup,newx,newy,BUILD_RADIUS,thistype.enumBuild)
static if DEST_BOUNCE then
call SetRect(thistype.enumRect,newx - DEST_RADIUS,newy - DEST_RADIUS,newx + DEST_RADIUS,newy + DEST_RADIUS)
call EnumDestructablesInRect(thistype.enumRect,null,thistype.enumDests)
endif
endif
// Set the new position of each hook.
call SetUnitX(temp,newx)
call SetUnitY(temp,newy)
call SetUnitFacing(temp,ang2 * bj_RADTODEG)
set next = temp
set linkx = linkx.next
endloop
// If the max distance has been reached or a unit has been hit, stop extending.
if this.dist >= this.max or this.hit then
set this.ret = true
else
set this.dist = this.dist + this.speed
endif
set temp = null
set next = null
set dumm = null
endmethod
// The handler method, makes things much easier.
method periodic takes nothing returns nothing
if this.list.size == 0 then // If the hook is done.
call this.stopPeriodic()
call this.destroy() // Clean up the struct.
else
if this.ret then // If the hook is retracting.
call this.retract() // Keep retracting.
else // Otherwise.
call this.extend() // Extend it.
endif
endif
endmethod
implement T32x
// Start of the hook, assembles struct members and stuff.
static method actions takes nothing returns boolean
local thistype this = thistype.allocate()
local real casx = 0.00
local real casy = 0.00
// Sets struct members.
set this.cast = GetTriggerUnit()
set this.own = GetOwningPlayer(this.cast)
set casx = GetUnitX(this.cast)
set casy = GetUnitY(this.cast)
set this.ang = Atan2((GetSpellTargetY() - casy),(GetSpellTargetX() - casx))
set this.cos = Cos(this.ang)
set this.sin = Sin(this.ang)
set this.list = List.create()
set this.level = GetUnitAbilityLevel(this.cast,ABIL_ID)
set this.speed = SPEED(this.level) * T32_PERIOD
set this.max = MAXDIST(this.level)
// Create the head of the chain.
set this.head = CreateUnit(this.own,HEAD_ID,casx + this.speed * this.cos,casy + this.speed * this.sin,this.ang * bj_RADTODEG)
set this.list.attach().link = this.head
// Set up the head unit.
call SetUnitTimeScale(this.head,TIME_SCALE)
call SetUnitScale(this.head,HEAD_SCALE,HEAD_SCALE,HEAD_SCALE)
static if LIBRARY_AutoFly then
call SetUnitFlyHeight(this.head,FLY_HEIGHT,0.00)
endif
// Play a casting animation.
call SetUnitAnimationByIndex(this.cast,ANIM_INDEX)
// Add this instance to T32.
call this.startPeriodic()
// Return a boolean value to work with GTrigger.
return false
endmethod
// Needed this so that the sound worked first go.
static method loadSound takes nothing returns nothing
local timer time = GetExpiredTimer()
local sound shot = CreateSound(HEADSHOT_SOUND,false,false,false,10,10,"")
// Play the sound, but quietly.
call SetSoundVolume(shot,0)
call StartSound(shot)
call KillSoundWhenDone(shot)
// Using the static timer for this, seeing as no one will cast hook at 0.00 seconds game time.
call PauseTimer(time)
call DestroyTimer(time)
set time = null
set shot = null
endmethod
// Initialisation method.
static method onInit takes nothing returns nothing
local unit dummy = null
// Set all the static struct members.
set thistype.enumRect = Rect(0.00,0.00,1.00,1.00)
set thistype.enumGroup = CreateGroup()
set thistype.enumDests = function thistype.getDest
set thistype.enumUnits = Filter(function thistype.getUnit)
set thistype.enumBuild = Filter(function thistype.getBuild)
set thistype.worldMaxX = GetRectMaxX(bj_mapInitialPlayableArea) - 64.00
set thistype.worldMaxY = GetRectMaxY(bj_mapInitialPlayableArea) - 64.00
set thistype.worldMinX = GetRectMinX(bj_mapInitialPlayableArea) + 64.00
set thistype.worldMinY = GetRectMinY(bj_mapInitialPlayableArea) + 64.00
// Add a "starts effect" event to the spell.
call GT_AddStartsEffectAction(function thistype.actions,ABIL_ID)
// Needed to load the sound properly.
call TimerStart(CreateTimer(),0.00,false,function thistype.loadSound)
// If preloading is allowed, then preload.
static if ALLOW_PRELOAD then
// Load the "chain" unit and add the crow form ability to load that too.
set dummy = CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE),LINK_ID,0.00,0.00,0.00)
call RemoveUnit(dummy)
// Load the "head" unit.
set dummy = CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE),HEAD_ID,0.00,0.00,0.00)
call RemoveUnit(dummy)
set dummy = null
endif
endmethod
endstruct
endlibrary
Notes:
I think this spell is finally 100% complete, unless more bugs are found, or I want to change something.
Changelog:
- Version 1.0.0 - Initial release. [5th August, 2009]
- Version 1.0.1 - Minor update. Added configurable descriptions, updated implementation, updated the headshot sound to not use gg_snd_headshot. [5th August 2009]
- Version 1.0.2 - Minor update. The spell -should- now work with the 1.24 patch. Also fixed a bug found by CrackUps involving grappling and pathing. [10th August 2009]
- Version 1.0.3 - Minor update. Rewrote the retract method for the spell (removed about 20 lines). Added another configurable option. [10th August 2009]
- Version 1.0.4 - Minor update. Added sound preloading so that the headshot sound works first go. Thanks for the motivation BlackRose. [12th August 2009]
- Version 1.0.5 - Minor update. Fixed some small bugs found with the spell and added the option for the hook to bounce off of destructables. Also updated configurables descriptions, and some very minor script changes. [18th August 2009]
- Version 1.0.6 - Minor update. Spell no longer requires the Linked List Module. Now uses an internalised T32 system (Thanks Jesus4Lyf - Credits to go him for the system). Got rid of some maths and replaced it with native calls. Updated the test map. [3rd September 2009]
- Version 1.0.7 - Pretty major update. Reduced the amount of work that is needed by the spell by getting rid of one of the O(n) complexity searches every interval. Making it far more efficient and friendly for larger games. [18th September 2009]
- Version 1.1.0 - Major update. Made all the changes that were in the last pages of this thread, including: actually making the linked list implementation work, efficiency updates, cleaning up the code, requiring more libraries and all that stuff.
- Version 1.1.1 - Minor update. Fixed a small bug found by INCINERATE. [6th January 2010]
- Version 1.1.2 - Minor update. Added an extra configurable option for sound. Added a struct member for max distance to reduce function calls. [20th January 2010]