• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

Meat Hook v4.0

Meat Hook

Launches a bloody hook at a unit or location. The hook will snag the first target it encounters, dealing damage then dragging the victim back to the caster.

The demo map:
  • Meat Hook with all required resources
  • Detailed information how to import and use Meat Hook
  • A small demo map
Requires:

Credits:

  • CTL
  • WorldBounds
  • UnitIndexer
  • Event
  • Dummy
  • List
  • ErrorMessage
  • Table
  • SpellEffectEvent
  • RegisterPlayerUnitEvent


Author's Note:
This resource gave me alot of headaches, but finally its done and I kept all my promises. :)

Known Issues:
  • While retracting a hook can only once catch a target.
  • Sometimes a tail appears on the screen, because of the Dummy library and the fact that sfx maiev//.. which is used as chain model has a tail
    That's the price for recycling units, you have to use a different model to prevent this issue.

    In case you want a hook which supports headshots and grappling please use kenny's MeatHook on TH. There is nearly no performace gap between mine and his version.

JASS:
scope MeatHook /* v4.0
*************************************************************************************
*
*   Launches a bloody hook at an unit or location. The hook will snag the first target it encounters,
*   dealing damage, then dragging the victim back to the caster. 
*
*************************************************************************************
*
*   Credits
*
*       To Nestharus
*       -----------------------
*
*           For CTL , UnitIndexer, WorldBounds, Event, ErrorMessage, List, Dummy
*      
*       To Maghteridon96
*       -----------------------
*
*           For RegisterPlayerUnitEvent
*
*       To Bribe
*       -----------------------
*
*           For SpellEffectEvent, Table
*
*
**************************************************************************************
*       
*   Configuration
*   ¯¯¯¯¯¯¯¯¯¯¯¯¯
*/
    native UnitAlive takes unit id returns boolean
    
    globals     
        /*                General Meat Hook settings
        *                 ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
        */
        private constant integer HOOK_ABILITY               = 'A002'//Ability raw-code of Meat Hook
        private constant attacktype ATTACK_TYPE             = ATTACK_TYPE_NORMAL
        private constant damagetype DAMAGE_TYPE             = DAMAGE_TYPE_NORMAL
        private constant boolean SEARCH_TARGETS_ON_RETRACT  = true
        /*
        *   SEARCH_TARGETS_ON_RETRACT is a constant boolean, if true the hook looks up for targets while retracting
        *       --> A hook can only have one target at the same time.
        */
        private constant real HOOK_HEIGHT                   = 50.
        /*
        *   Offset of each chainlink towards the ground.
        */
        private constant real DETECTION_AOE                 = 100.
        /*
        *   Aoe in which the head is detecting valid targets.
        */
        private constant real MODEL_SCALING                 = 1.5
        /*
        *   Model scaling of each hook link.
        */
        private constant real GAP_BETWEEN_LINKS_FACTOR      = 2.2
        /*
        *   Is calculated GAP_BETWEEN_LINKS_FACTOR *TimerTimeout*Hookspeed. For instance 2.2*0.03125*600 = 41.25
        */
        private constant string HEAD_MODEL                  = "Abilities\\Weapons\\SentinelMissile\\SentinelMissile.mdl"
        private constant string CHAIN_MODEL                 = "Abilities\\Weapons\\WardenMissile\\WardenMissile.mdl"
        private constant string EFFECT_ONHIT                = "Objects\\Spawnmodels\\Human\\HumanBlood\\BloodElfSpellThiefBlood.mdl"
        private constant string ATTACH_POINT                = "origin"
        /*
        *                 FPS drop protection
        *                 ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
        *   MAX_LINKS_PER_LOOP prevents spamming hook links during one loop. A number of 10 is recommended.
        */
        private constant integer MAX_LINKS_CREATED_PER_LOOP = 10
    endglobals
        
    //Define which units are considered as targets
    private function TargetFilter takes unit target, unit caster returns boolean
        return UnitAlive(target) and not IsUnitType(target, UNIT_TYPE_STRUCTURE) and (target != caster) and (not IsUnitType(target, UNIT_TYPE_FLYING))
    endfunction
    
    //Define the hit damage. level is the ability "level" for the caster 
    private constant function GetDamage takes integer level returns real
        return 100. + 100.*level
    endfunction
    
    //Define the maximum range of the hook. "level" is the ability level for the caster 
    private constant function GetRange takes integer level returns real
        return 1000. + 200.*level
    endfunction
    
    //Define the speed of the hook measured in movement speed. "level" is the ability level for the caster 
    private constant function GetSpeed takes integer level returns real
        return 600.00 + (0.*level)
    endfunction

//Script code. Don't edit enything below.
    private keyword MEAT_HOOK_INITIALIZER
    private struct Link extends array
        implement List
        real x
        real y
        real angle
        effect sfx
        Dummy dum
        
        method attach takes real x, real y, real angle, string sfx returns nothing
            set this.x     = x
            set this.y     = y
            set this.angle = angle
        
            set this.dum   = Dummy.create(x, y, angle*bj_RADTODEG)
            set this.sfx   = AddSpecialEffectTarget(sfx, this.dum.unit, "origin")
            
            call SetUnitScale(this.dum.unit,MODEL_SCALING, 1, 1)
            call SetUnitFlyHeight(this.dum.unit, HOOK_HEIGHT, 0)
        endmethod
    endstruct
        
    private struct Main extends array
        static group enu = CreateGroup()
        static thistype array index//Use to release units from an hook on double hit.
        Link list
        player owner
        unit caster
        unit aim
        real damage
        real range
        real speed
        real angle
        real gap
        boolean end
        boolean hit
        boolean extend
        
        implement CTL
            local integer i//Required to prevent spamming within one loop
            local real x
            local real y
            local real x0
            local real y0
            local real a
            local real d
            local Link cur
            local thistype node//Required for the linked list
            local unit u//Group enumeration
        implement CTLExpire
            if (UnitAlive(caster)) then
                set i   = 0
                
                set cur = list.last
                
                set x   = GetUnitX(caster)
                set y   = GetUnitY(caster)
                
                set d   = SquareRoot((cur.x - x)*(cur.x - x) + (cur.y - y)*(cur.y - y))//Distance between the last link and the caster
                
                if (d > gap)then//Check if additional chain members have to be created
                    loop
                        exitwhen (d < gap) or MAX_LINKS_CREATED_PER_LOOP == i
                        set a  = Atan2(y - cur.y, x - cur.x)
                        set x0 = cur.x
                        set y0 = cur.y
                        /*
                        *   enqueue the list and attach a dummy to the returned instance.
                        */
                        set node = list.enqueue()
                        set cur  = node
                        call cur.attach(x0 + gap*Cos(a), y0 + gap*Sin(a), a, CHAIN_MODEL)
                        
                        set d = d - speed
                        set i = i + 1
                    endloop
                endif

                if (extend) then
                    set cur = list.first//Head of the hook
                    loop
                        exitwhen cur == cur.sentinel//sentinel equals 0
                        if (cur == list.first) then//Determine the correct angle
                            set cur.angle = angle
                        else
                            set cur.angle = Atan2(y - cur.y, x - cur.x)
                        endif
                        
                        set cur.x = cur.x + speed*Cos(cur.angle)//The correct new x/y coordinate according to the new angle
                        set cur.y = cur.y + speed*Sin(cur.angle)
                        set x     = cur.x
                        set y     = cur.y 
                        /*
                        *   WorldBounds is very important, otherwise the game may crash, if units are moved outside boundaries.
                        *   A good alternative would be BoundSentinel.
                        *   BoundSentinel creates less overhead.
                        */
                        if (x < WorldBounds.maxX) and (x > WorldBounds.minX) and (y < WorldBounds.maxY) and (y > WorldBounds.minY) then
                            call SetUnitX(cur.dum.unit, x)
                            call SetUnitY(cur.dum.unit, y)
                        else
                            set end = true
                        endif
      
                        if (cur == list.first) then
                            call GroupEnumUnitsInRange(enu, x, y, DETECTION_AOE, null)
                            loop
                                set u = FirstOfGroup(enu)
                                exitwhen u == null
                                call GroupRemoveUnit(enu, u)
            
                                if (TargetFilter(u, caster)) then
                                    set aim = u
                                    /*
                                    *   Detect if the unit has already been hooked
                                    *   ind[i] is a thistype array which stores
                                    *   this struct instance to the UnitId of aim.
                                    */
                                    set i = GetUnitId(u)
                                    if (0 == index[i]) then//Has already been hooked?
                                        set index[i]     = this
                                    else
                                        set index[i].aim = null//Release the unit from the other hook
                                        set index[i]     = this//And connect it to this struct instance
                                    endif
                                    
                                    if (IsUnitEnemy(aim, owner)) then//Damage only enemies.
                                        call UnitDamageTarget(caster, aim, damage, false, false, ATTACK_TYPE, DAMAGE_TYPE, null)
                                        call DestroyEffect(AddSpecialEffectTarget(EFFECT_ONHIT, aim, ATTACH_POINT))
                                    endif
                                    set extend = false
                                    set u      = null
                                endif
                                exitwhen u == null
                            endloop
                        endif
      
                        set cur = cur.next//Move on to the next chain member
                    endloop
                
                    set range = range - speed
                    if (0.00 > range) then
                        set extend = false
                    endif
                
                else
                    set cur = list.last//Nothing new here, just the other way around
                    loop
                        exitwhen cur == cur.sentinel
                        set node = cur.prev
                        if cur == list.last then
                            set angle = Atan2((GetUnitY(caster) - cur.y),(GetUnitX(caster) - cur.x))
                        else
                            set angle = Atan2((y - cur.y),(x - cur.x))
                        endif
                        
                        set cur.x = cur.x + speed*Cos(angle)
                        set cur.y = cur.y + speed*Sin(angle)
                        
                        set x     = cur.x
                        set y     = cur.y
                    
                        call SetUnitX(cur.dum.unit, x)
                        call SetUnitY(cur.dum.unit, y)
                        call SetUnitFacing(cur.dum.unit,(angle*bj_RADTODEG) + 180.00)
                        
                        if (cur == list.first) then
                            if (aim != null) and UnitAlive(aim) then
                                call SetUnitX(aim, x)
                                call SetUnitY(aim, y)
                            endif
                            
                            static if SEARCH_TARGETS_ON_RETRACT then
                                if (aim == null) and (not hit) then//You can only drag a unit once on retract to prevent abuses
                                    call GroupEnumUnitsInRange(enu, x, y, DETECTION_AOE, null)
                                    loop
                                        set u = FirstOfGroup(enu)
                                        exitwhen u == null
                                        call GroupRemoveUnit(enu, u)
            
                                        if (TargetFilter(u, caster)) then
                                            set aim = u
                                            set hit = not hit// == set .hit = true
                                            set i = GetUnitId(u)
                                            if (0 == index[i]) then
                                                set index[i]     = this
                                            else
                                                set index[i].aim = null
                                                set index[i]     = this
                                            endif
                                    
                                            if (IsUnitEnemy(aim, owner)) then
                                                call UnitDamageTarget(caster, aim, damage, false, false, ATTACK_TYPE, DAMAGE_TYPE, null)
                                                call DestroyEffect(AddSpecialEffectTarget(EFFECT_ONHIT, aim, ATTACH_POINT))
                                            endif
                                            set u = null
                                        endif
                                        exitwhen u == null
                                    endloop
                                endif
                            endif
                        endif
                        
                        if (cur == list.last) and (IsUnitInRange(cur.dum.unit, caster, speed)) then//Check if the member should be destroyed
                            call DestroyEffect(cur.sfx)
                            set cur.sfx = null
                            call SetUnitScale(cur.dum.unit, 1, 1, 1)
                            call SetUnitFlyHeight(cur.dum.unit, 0, 0)
                            call cur.dum.destroy()
                            call list.dequeue()
                            if (0 == node) then//Check if the list is empty
                                set end = true
                            endif
                        endif
                        
                        set cur = node
                    endloop
                endif
            else 
                set end = true 
            endif
                    
            if (end) then
                set cur = .list.last
                loop
                    exitwhen cur == cur.sentinel
                    call DestroyEffect(cur.sfx)
                    set cur.sfx = null
                    call SetUnitScale(cur.dum.unit, 1, 1, 1)
                    call SetUnitFlyHeight(cur.dum.unit, 0, 0)
                    call cur.dum.destroy()
                    set cur = cur.prev
                    call list.dequeue()
                endloop
                    
                call destroy()
                call list.destroy()
                set owner  = null
                set caster = null
                if (aim != null) then//Check if we had a target
                    set index[GetUnitId(aim)] = 0//Clear the thistype array
                    set aim = null
                endif
            endif
        implement CTLEnd
        
        private static method run takes nothing returns boolean
            local thistype this = create()
            local thistype node
            local Link cur
            
            local integer level 
            local real x 
            local real y
            
            set caster = GetTriggerUnit()
            set owner  = GetTriggerPlayer()
            
            set x      = GetUnitX(caster)
            set y      = GetUnitY(caster)            
            set angle  = Atan2(GetSpellTargetY() - y, GetSpellTargetX() - x)
            
            set level  = GetUnitAbilityLevel(caster, HOOK_ABILITY)
            set damage = GetDamage(level)
            set speed  = GetSpeed(level)*0.031250000
            set range  = GetRange(level)
            set gap    = speed*GAP_BETWEEN_LINKS_FACTOR
            
            set list   = Link.create()
            set node   = list.enqueue()
            set cur    = node
            call cur.attach(x + speed*Cos(angle), y + speed*Sin(angle), angle, HEAD_MODEL)
            
            set extend = true
            set end    = false
            set hit    = false
            
            return false
        endmethod
        
        static if not LIBRARY_SpellEffectEvent then
            private static method check takes nothing returns boolean
                if (GetSpellAbilityId() == HOOK_ABILITY) then
                    call thistype.run()
                endif
                return false
            endmethod
        endif
        
        implement MEAT_HOOK_INITIALIZER 
        
    endstruct
    
    private module MEAT_HOOK_INITIALIZER 
        private static method onInit takes nothing returns nothing
            static if LIBRARY_SpellEffectEvent then
                call RegisterSpellEffectEvent(HOOK_ABILITY, function thistype.run)
            else
                local trigger t = CreateTrigger()
                call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_EFFECT)
                call TriggerAddCondition(t, Condition(function thistype.check))
                set t = null
            endif
        endmethod
    endmodule
endscope

Keywords:
Dota, Pudge, Hook, Meat Hook, Pull, drag, extend, retract, PudgeWars
Contents

Meat Hook (Map)

Reviews
Meat Hook v1.0.2 | Reviewed by Maker | 20th October 2013 Concept[/COLOR]] A thrown hook that grabs an enemy to drag it toward the hero is a cool concept The idea has been seen and done may time over with varying results...

Moderator

M

Moderator


Meat Hook v1.0.2 | Reviewed by Maker | 20th October 2013

[COLOR="gray"

[COLOR="gray"

[COLOR="gray"

[COLOR="gray"

[COLOR="gray"

[COLOR="gray"

Concept[/COLOR]]
126248-albums6177-picture66521.png
  • A thrown hook that grabs an enemy to drag it toward the hero is a cool concept
126248-albums6177-picture66523.png
  • The idea has been seen and done may time over with varying results
Triggers[/COLOR]]
126248-albums6177-picture66521.png
  • Leakless, MUI, good instructions and configuration options
  • Good coding overall
126248-albums6177-picture66523.png
  • If the whole struct is private, you can leave private keywords out
    from struct members and methods. It does not make any kind of difference
    though
  • Sometimes the target is dragged too close to the caster and
    one of the units jump a bit as they occupy the same space
  • You could check flying height of targets and filter out allies by default
Objects[/COLOR]]
126248-albums6177-picture66521.png
  • Most of the things are set correctly and there are no unneeded objects
126248-albums6177-picture66523.png
  • You can remove Invulnerable ability from the dummy,
    Locust makes the unit invulneralbe
  • There is no learn hotkey
  • The tooltip could list the stats per level
Effects[/COLOR]]
126248-albums6177-picture66521.png
  • The sounds and models match the theme of the spell nicely
126248-albums6177-picture66523.png
  • The spinning axe might not be the perfect match for the head
    model, though it is good. You could try the other spinning missile
    models or the chain head model in the model database
Rating[/COLOR]]
CONCEPTTRIGGERSOBJECTSEFFECTSRATINGSTATUS
126248-albums6177-picture75359.jpg
126248-albums6177-picture75360.jpg
126248-albums6177-picture75359.jpg
126248-albums6177-picture75359.jpg
126248-albums6177-picture75359.jpg
APPROVED
Links[/COLOR]]

[COLOR="gray"

[/TD]
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
What would you say are the differences and/or benefits of yours over the GUI version?

To name only a few: Faster execution, less code and lag, Dummy, ... Whereas if you stick to GUI while map-making this has no real benefits.

dummies used for the chain first appear behind the caster (nearby leg)
If something more grave occurs, I'll fix it along with that. fixed
 
Last edited:
Level 37
Joined
Mar 6, 2006
Messages
9,240
-Null the effect in destroy for the link
-CTLNull is optional, you are not doing anything there so don't implement it
-Units are dragged too close, units are inside each other
-Put some effort in the tooltip, look at standard Blizzard abilities for reference
-I would like to be able to configure how far the hook goes
-Units with Locust do not need invulnerability ability

Overall it looks very well done.
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
-I would like to be able to configure how far the hook goes
function GetRange takes integer level returns real
-Units with Locust do not need invulnerability ability
Its the default setting of particle provided in the attached map of the Dummy snippet.
Fixed everything else mentioned. Thank you.
Its not bad, but I think it's a bit lack luster compared to Kenny's meathook
It's not posted on hiveworkshop. I didn't even know such a resource exists, also it uses outdated resources. I'll improve the eyecandy upon serious suggestions.
 
Level 15
Joined
Jul 6, 2009
Messages
889
Hmm. This makes me want to isolate the Meat Hook from the Pudge Wars map and submit it. But I have no time :'(

Firstly, some minor random housekeeping / cleaning issues:

- The learn ability hotkey is different from the use. I suggest just keeping them the same for consistency (as every other spell in wc3 does). Have both hotkeys as T. Not like the M hotkey actually works.
- The icon is Finger of Death? I suggest the Impale icon like it is for other Meat Hooks.
- Improved tooltips.
- Maybe make the hero the Abomination, rather than the Paladin. At least the model.

Now onto the more deeper stuff.

Its not bad, but I think it's a bit lack luster compared to Kenny's meathook

This I agree with.

1. Although Kenny's Pudge Wars Meat Hook uses "outdated" resources, it's so smooth.

I'm probably biased towards the Pudge Wars Meat Hook (probably since I worked on the map and always been a fan of it), but I feel this version is lacking. I suggest you take a look at the Meat Hook from the above link to see what makes this appear lackluster compared to it.

2. It seems you are aware that having each hook component return to the caster rather than being linked to the previous hook component is odd.

'The angle of all Links will now be determined between the Link instance and the caster, which is not perfect
Better solution: calculate the angle between the current Link and the one behind that one.'​

If you do this, it'll feel more like the hook.

3. The hook doesn't pick up units as it is retracting?

4. The speed of the hook should be customisable based on level rather than a constant. It should also be in Warcraft III units. So it would appear as 640 rather than as 20.

5. If you Blink or are displaced, there is a gap in the hook. I would suggest code to fill in any gaps created when necessary and then remove them if there's an excess of hook components.

6. I didn't test this, but what happens if two units are hooked at the same time?

7. Hardcoded values that should be configurables ("chest", 75, etc.)
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
These are valid arguments. I will put some more effort in this. Don't approve it until further update.


2, 5, 7. I will fix that.

3. This depends strongly on the philosophy, which opportunities hook should cover. Like straight or curved, bouncing or not, destroy destructables!? I'll consider adding statif if GRAP_RETRACTING.

6. I have no clue, yet :D
 
Level 15
Joined
Jul 6, 2009
Messages
889
These are valid arguments. I will put some more effort in this. Don't approve it until further update.


2, 5, 7. I will fix that.

3. This depends strongly on the philosophy, which opportunities hook should cover. Like straight or curved, bouncing or not, destroy destructables!? I'll consider adding statif if GRAP_RETRACTING.

6. I have no clue, yet :D

3. I generally try to make my spells as customisable as possible. But that's me :p

6. I tested it just now. The unit will be stuck to the first hook. The unit is then moved to the second hook if it's still active after the first ends. I wonder what the best approach is. Should the unit be detached from the first hook and move with the second? Should it be killed instantly like in Pudge Wars? Or should the second hook do nothing except damage (this means just discarding the target and retracting the hook -- not bringing it along).
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
When you use: call RegisterSpellEffectEvent , you don't need to return boolean on the function thistype.run :).

Quotation from PurgeandFire
It returns boolean because RegisterSpellEffectEvent actually passes the function into a trigger condition, rather than a trigger action. Thus, all of the checking and functionality is consolidated into one function. PJASS may throw an error if you provide a function that does not return boolean. If you have an older version of JNGP, though, you might not get that error.
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
I'm going to enhance the spell behaviour of MeatHook as soon as I solve an issue with backwards iteration, which currently double frees the dummy.
  • Hook will be curved depending on the casters position
  • Will fill existing gaps if required
  • A hooked target can be unlinked by hitting it with another hook
  • New model for the hook head

Edit: Updated
 
Last edited:
Level 19
Joined
Mar 18, 2012
Messages
1,716
Really .....

It was my first attempt with linked list and there was no real tutorial or template on THW how to use it.

Well luckily ErrorMessage isolates the problem to small piece inside the whole code.

I'll later look into it and try to find the mistake I made.

Thank you, for reporting that bug.

Ok I was also able to reproduce the error with ease and it only occurs if you double hook a unit, so maybe the bug is where I free a unit from a hook if hooked by another hook.

Finally found it here, its because I reused node here ...:
JASS:
                     loop
                        exitwhen cur == cur.sentinel
                        set node = cur.prev
                        //irrelevant code
                        set i = GetUnitId(u)
                        if table.has(i) then
                            set node = thistype(table[i])
                            set node.aim = null
                            set table[i] = this
                        else
                             set table[i] = this
                        endif
                         //irrelevant code
                         //node may be not part of this list ....
                         //cur may not even exist anymore ...
                         set cur = node
                     endloop
Works now but I found another bug. Allowing to target units while retracting the hook can cause hillarious damage, if both hooks are casted in one line because they detach the unit each loop and attach it again.
 
Last edited:
Level 19
Joined
Mar 18, 2012
Messages
1,716
I've made some stress tests and decided to add a maximum boundary of created links per cast in the next update to prevent an overflow of Dummy, allocation or List.
It will influence the spell performance slightly.

If you port a huge distance while throwing a hook it will crash Alloc and Dummy at a certain point.
 

Kazeon

Hosted Project: EC
Level 33
Joined
Oct 12, 2011
Messages
3,449
Very cool!

I'm going to enhance the spell behaviour of MeatHook as soon as I solve an issue with backwards iteration, which currently double frees the dummy.
  • Hook will be curved depending on the casters position
  • Will fill existing gaps if required
  • A hooked target can be unlinked by hitting it with another hook
  • New model for the hook head

Edit: Updated

That update makes this hook spell very different from another one. And literally a sweet addition as well!
 
Level 4
Joined
Jan 7, 2014
Messages
69
spell causing problems with wc3 vex map optimizer, violation access when i use optimize script. when i off spell, map optimizing without problems, spell works good. but qoz this problem i cant put into map. ;\
 
Top