The Meat Grinder

Dirac

22710180
Reaction score
147
The Meat Grinder
A tower for your TD



Well this is a very old project i submitted to a tower contest made a long time back, but i made it in GUI at that time, and it was buggy, inefficient and laggy. So here's the remake

Summon Meat Grinder - [X]

Gathers the corpses of nearby fallen enemy units to use them as ammunition against any other.

And not a single drop of blood was wasted
It's a very unique tower coded with the best intentions of not causing lag

Evaluation as a tower
Pros:
-It's very efficient against creeps, since it's meant to deal a lot of AoE damage.
-You can't just put it anywhere, so it adds some strategy to your TD
-Cleans up the map of corpses
Cons:
-Wouldn't be as effective against bosses
-May conflict with towers that use corpses (not itself obviously)

Evaluation as something to implement to your map
Pros:
-Very unique
-Has a nice eye-candy
-It was hard coding this so it saves you the trouble of making a tower this complicated
Cons:
-Lags if they're more than 500 corpses TO PICK in the map at the same time (corpses outside the range of the tower won't cause lag)
-Requires libraries (is this something bad?)

The Code(Requires AIDS, TimerUtils, T32 and AutoFly)
JASS:
library MeatGrinder initializer start uses AIDS,T32,TimerUtils,AutoFly
//====================================================//
//                   MEAT GRINDER
//====================================================//
//                   made by Dirac

//best used with default settings and tower model Slaughterhouse with "work" animation
//if you want to prevent any unit in your map from being summoed as a corpse make sure to add it to MeatGrinder_AvoidGroup

    globals
//main setup
        private constant integer DUMMY='e000' //raw code of the dummy
        private constant integer TOWER='u000' //raw code of the tower
//damage and stats
        private constant real ANGLEGAP=35 //aperture of the angle it takes the corpse to adquire a target based on it's facing
        private constant real SPEED=950*T32_PERIOD //speed of the corpse when attacking
        private constant real NORMALSPEED=450*T32_PERIOD //speed of the corpse when spinning around tower
        private constant real SPIN=147.2*T32_PERIOD //angle rotation speed around tower
        private constant real DELAY=0.5 //the delay between summoning corpses
        private constant real COOLDOWN=0 //attack cooldown
        private constant real DMG=0
        private constant real AOE=270
        private constant real HPDMG=0.30 //corpses can deal a fraction of the unit's max life if wanted
        private constant real AOEMULT=1 //this multiplies the damage done to the target and deals that number to AoE targets.
        private constant real ADQUISITION=1200
        private constant real RANGE=800
        private constant real MAXCORPSES=6
//stetics
        private constant real TOWERANGLE=90 //facing angle of the corpse towards the tower
        private constant real RADIUS=100 //radius the corpses rotate around
        private constant real HEIGHT=180 //initial height of the corpses
        private constant real CORPSESIZE=1.2 //size of each corpse (1.00=100%)
        private constant string CORPSEFX="Abilities\\Weapons\\MeatwagonMissile\\MeatwagonMissile.mdl" //corpse effect
        private constant string SUMMONFX="Abilities\\Spells\\Undead\\RaiseSkeletonWarrior\\RaiseSkeleton.mdl" //effect created when summons a corpse
    endglobals
//====================================================//
//                END CONFIGURATION
//====================================================//

    globals
        private unit TempUnit
        private group Group=CreateGroup()
        private group TowerGroup=CreateGroup()
        public group AvoidGroup=CreateGroup() //units inside this group won't be summoned as corpses
    endglobals
    
    //===========TOWER STRUCT==========//
    private struct MeatGrinder extends array
        readonly real x
        readonly real y
        readonly real z
        real tick
        real cooldown
        integer corpsecount
        group corpsegroup
        
        private static method AIDS_filter takes unit u returns boolean
            return GetUnitTypeId(u)==TOWER
        endmethod
        
        private method periodic takes nothing returns nothing
            set .tick=RMaxBJ(.tick-T32_PERIOD,0.) //i use T32 here because i wan't this to be very exact, using TimerUtils would give me a I2S(DELAY) margin of error
            set .cooldown=.cooldown-T32_PERIOD
        endmethod
        
        implement T32x
        
        private method AIDS_onCreate takes nothing returns nothing
            set .corpsecount=0
            set .cooldown=0.
            set .tick=0.
            set .corpsegroup=CreateGroup()
            set .x=GetUnitX(.unit)
            set .y=GetUnitY(.unit)
            set .z=GetLocationZ(Location(.x,.y))
        endmethod
        
        private method AIDS_onDestroy takes nothing returns nothing
            set .corpsecount=0
            set .tick=0.
            call DestroyGroup(.corpsegroup)
        endmethod

        //! runtextmacro AIDS()
    endstruct
    
    //===========INDEX CORPSE==========//
    private struct CorpseIndex extends array
        integer data //this attaches every corpse their struct.
        //I made this to remove the corpses when the tower dies
        
        static method AIDS_filter takes unit u returns boolean
            return GetUnitTypeId(u)==DUMMY
        endmethod
        //! runtextmacro AIDS()
    endstruct
    
    //===========CORPSE STRUCT==========//
    private struct Corpse
        MeatGrinder tower
        readonly real towerX
        readonly real towerY
        readonly real distance
        private real selfdelay
        private real damage
        private real ux
        private real uy
        private real ua
        private player owner
        private unit thecorpse
        private unit target
        private unit dead
        private effect fx
        private static thistype currentdata

        method operator x takes nothing returns real
            return .ux
        endmethod
        method operator x= takes real a returns nothing
            call SetUnitX(.thecorpse,a)
            set .ux=a
        endmethod
        method operator y takes nothing returns real
            return .uy
        endmethod
        method operator y= takes real a returns nothing
            call SetUnitY(.thecorpse,a)
            set .uy=a
        endmethod
        method operator xyangle takes nothing returns real
            return .ua
        endmethod
        method operator xyangle= takes real a returns nothing
            call SetUnitFacing(.thecorpse,a*bj_RADTODEG)
            set .ua=a
        endmethod

        method destroy takes nothing returns nothing
            call DestroyEffect(.fx)
            call RemoveUnit(.thecorpse)
            set .target=null
            set .thecorpse=null
            call .stopPeriodic()
            call .deallocate()
        endmethod

        static method AddCorpse takes nothing returns boolean
            local unit filter=GetFilterUnit()
            local MeatGrinder this=MeatGrinder[TempUnit]
            if IsUnitAlly(TempUnit,GetOwningPlayer(filter))==false and IsUnitInGroup(filter,AvoidGroup)==false and this.corpsecount<MAXCORPSES and GetWidgetLife(filter)<=0 then
                call thistype.create(GetUnitX(filter),GetUnitY(filter),DELAY,filter,this)
            endif
            set filter=null
            return false
        endmethod

        private static method SetTarget takes nothing returns boolean
            local unit filter=GetFilterUnit()
            local real x
            local real y
            local real a
            local real dif

            if GetWidgetLife(filter)>0 and IsUnitAlly(filter,currentdata.owner)!=true and currentdata.tower.cooldown<=0 and currentdata.selfdelay>=0.8 then
                //first check if the unit is a valid target to then do the math
                set x=GetUnitX(filter)
                set y=GetUnitY(filter)
                set a=Atan2(y-currentdata.y,x-currentdata.x)
                set dif=(currentdata.xyangle-a)*bj_RADTODEG
                if ((dif<=(ANGLEGAP/2) and dif>=-(ANGLEGAP/2)) or (dif<=360+(ANGLEGAP/2) and dif>=360-(ANGLEGAP/2)) or (dif<=-360+(ANGLEGAP/2) and dif>=-360-(ANGLEGAP/2))) then
                    set currentdata.target=filter
                    set currentdata.tower.cooldown=COOLDOWN
                endif
            endif
            set filter=null
            return false
        endmethod
        
        private static method aoedmg takes nothing returns boolean
            local unit filter=GetFilterUnit()
            local real x=GetUnitX(filter)
            local real y=GetUnitY(filter)
            if GetUnitState(filter,UNIT_STATE_LIFE)>0 and IsUnitAlly(filter,currentdata.owner)!=true and currentdata.target!=filter then
                call UnitDamageTarget(currentdata.tower.unit,filter,currentdata.damage*AOEMULT,true,false,ATTACK_TYPE_NORMAL,DAMAGE_TYPE_NORMAL,WEAPON_TYPE_WHOKNOWS)
            endif
            set filter=null
            return false
        endmethod

        private method periodic takes nothing returns nothing
            local real a
            local real x
            local real y
            local real z
            local real dist
            local location l
            
            if GetUnitTypeId(.target)==0 then
                set dist=SquareRoot((.towerX-.x)*(.towerX-.x)+(.towerY-.y)*(.towerY-.y))
                set .selfdelay=RADIUS/dist
                set a=Atan2(.towerY-.y,.towerX-.x)-90*((RADIUS)/(dist))*bj_DEGTORAD
                set .currentdata=this
                call GroupEnumUnitsInRange(Group,.x,.y,RANGE,function thistype.SetTarget)
                set z=HEIGHT+.tower.z+(((HEIGHT+.tower.z)/(.distance-RADIUS))*(.distance-dist)-HEIGHT-.tower.z)
                set x=.x+NORMALSPEED*Cos(a)
                set y=.y+NORMALSPEED*Sin(a)
            else
                set x=GetUnitX(.target)
                set y=GetUnitY(.target)
                set a=Atan2(y-.y,x-.x)
                set .distance=SquareRoot((.towerX-x)*(.towerX-x)+(.towerY-y)*(.towerY-y))
                set dist=SquareRoot((.x-towerX)*(.x-towerX)+(.y-towerY)*(.y-towerY))
                if SquareRoot((.x-x)*(.x-x)+(.y-y)*(.y-y))<=SPEED+1 then
                    call UnitDamageTarget(.tower.unit,.target,.damage,true,false,ATTACK_TYPE_NORMAL,DAMAGE_TYPE_NORMAL,WEAPON_TYPE_WHOKNOWS)
                    set .tower.corpsecount=.tower.corpsecount-1
                    if .tower.corpsecount==0 then //this add unused corpses nearby to the tower
                        set TempUnit=.tower.unit
                        call GroupEnumUnitsInRange(Group,.towerX,.towerY,ADQUISITION,Filter(function thistype.AddCorpse))
                    endif
                    set currentdata=this
                    call GroupEnumUnitsInRange(Group,x,y,AOE,function thistype.aoedmg)
                    call DestroyEffect(AddSpecialEffect(CORPSEFX,x,y))
                    call .destroy()
                    return
                endif
                set x=.x+SPEED*Cos(a)
                set y=.y+SPEED*Sin(a)
                set z=HEIGHT+.tower.z+(((HEIGHT+.tower.z)/(.distance-RADIUS))*(.distance-dist)-HEIGHT-.tower.z)
            endif
            set .xyangle=a
            set .x=x
            set .y=y
            set l=Location(.x,.y)
            call SetUnitFlyHeight(.thecorpse,z-GetLocationZ(l),0.)
            call RemoveLocation(l)
            set l=null
        endmethod
        
        implement T32x
        
        private static method summon takes nothing returns nothing
            local timer t=GetExpiredTimer()
            local thistype this=GetTimerData(t)
            
            if IsUnitInGroup(.tower.unit,TowerGroup) then
                set .fx=AddSpecialEffectTarget(CORPSEFX,.thecorpse,"origin")
                call DestroyEffect(AddSpecialEffect(SUMMONFX,.x,.y))
                call .startPeriodic()
            else
                call RemoveUnit(.thecorpse)
            endif
            call RemoveUnit(.dead)
            set .dead=null
            call ReleaseTimer(t)
        endmethod
        
        static method create takes real x,real y,real delay,unit whichUnit,MeatGrinder whichStruct returns thistype
            local thistype this=thistype.allocate()
            local real centerX=GetUnitX(whichStruct.unit)
            local real centerY=GetUnitY(whichStruct.unit)
            local real a=Atan2(centerY-y,centerX-x)
            local timer t=NewTimer()
            
            call SetTimerData(t,this)
            set whichStruct.tick=whichStruct.tick+delay
            set whichStruct.corpsecount=whichStruct.corpsecount+1
            call GroupAddUnit(AvoidGroup,whichUnit)
            call TimerStart(t,whichStruct.tick,false,function thistype.summon)
            set .distance=SquareRoot((centerX-x)*(centerX-x)+(centerY-y)*(centerY-y))
            set .towerX=centerX
            set .towerY=centerY
            set .target=null
            set .tower=whichStruct
            set .selfdelay=1.2
            set .dead=whichUnit
            set .damage=HPDMG*GetUnitState(whichUnit,UNIT_STATE_MAX_LIFE)+DMG
            set .owner=GetOwningPlayer(whichStruct.unit)
            set .thecorpse=CreateUnit(.owner,DUMMY,x,y,a*bj_RADTODEG)
            set CorpseIndex[.thecorpse].data=this
            set .ux=GetUnitX(.thecorpse)
            set .uy=GetUnitY(.thecorpse)
            call SetUnitScale(.thecorpse,CORPSESIZE,CORPSESIZE,CORPSESIZE)
            call GroupAddUnit(whichStruct.corpsegroup,.thecorpse)
            set t=null
            return this
        endmethod
    endstruct

//===========STARTUP AND UNIT DEATH==========//
    private function range takes nothing returns nothing
        local unit filter=GetEnumUnit()
        local MeatGrinder this=MeatGrinder[filter]
        local real x1=GetUnitX(filter)
        local real y1=GetUnitY(filter)
        local real x2=GetUnitX(TempUnit)
        local real y2=GetUnitY(TempUnit)
        local real xy=SquareRoot((x1-x2)*(x1-x2)+(y1-y2)*(y1-y2))
        
        if xy<=ADQUISITION and IsUnitAlly(TempUnit,GetOwningPlayer(filter))==false and IsUnitInGroup(TempUnit,AvoidGroup)==false and this.corpsecount<MAXCORPSES then
            call Corpse.create(x2,y2,DELAY,TempUnit,this)
            return
        endif
        set filter=null
    endfunction
    
    private function DeleteCorpse takes nothing returns nothing
        local Corpse this=CorpseIndex[GetEnumUnit()].data
        call this.destroy()
    endfunction

    private function UnitDeath takes nothing returns boolean
        set TempUnit=GetTriggerUnit()
        call ForGroup(TowerGroup,function range)
        if GetUnitTypeId(TempUnit)==TOWER then
            call ForGroup(MeatGrinder[TempUnit].corpsegroup,function DeleteCorpse)
            call GroupRemoveUnit(TowerGroup,TempUnit)
        endif
        return false
    endfunction

    private function EnableTower takes nothing returns boolean
        local unit u=GetTriggerUnit()
        
        if GetUnitTypeId(u)==TOWER then
            call MeatGrinder<u>.startPeriodic()
            call GroupAddUnit(TowerGroup,u)
            set TempUnit=u
            call GroupEnumUnitsInRange(Group,GetUnitX(u),GetUnitY(u),ADQUISITION,function Corpse.AddCorpse)
        endif
        set u=null
        return false
    endfunction
    
    private function InitTower takes nothing returns boolean
        local unit u=GetFilterUnit()
        
        if GetUnitTypeId(u)==TOWER then
            call MeatGrinder<u>.startPeriodic()
            call GroupAddUnit(TowerGroup,u)
        endif
        set u=null
        return false
    endfunction
    
    private function start takes nothing returns nothing
        local trigger trig=CreateTrigger()
        local trigger construction=CreateTrigger()
        local integer i=0
        loop
            call TriggerRegisterPlayerUnitEvent(trig,Player(i),EVENT_PLAYER_UNIT_DEATH, null)
            call TriggerRegisterPlayerUnitEvent(construction,Player(i),EVENT_PLAYER_UNIT_CONSTRUCT_FINISH, null)
            set i=i+1
            exitwhen i==bj_MAX_PLAYER_SLOTS
        endloop
        call TriggerAddCondition(trig,function UnitDeath)
        call TriggerAddCondition(construction,function EnableTower)
        call GroupEnumUnitsInRect(Group,bj_mapInitialPlayableArea,function InitTower)
        set trig=null
    endfunction
endlibrary</u></u>


Thanks to:
Jesus4Lyf for his awesome libraries
Vexorian for NG

In this thread we discuss bugs, issues and efficiency

If you wan't a custom-made version of this tower PM me and i'll find time to do it

CHANGELOG:
v0.01
-Initial Release
v0.02
-Minor Tweaking to the map
-Increased efficiency by removing a boolean check performed every period
-Removed xefx as a library (was causing issues indexing corpses)
v0.03
-Minor Tweaking to the map
-Changed the way corpses move to increase efficiency
-Some changes to the code that also improve efficiency
v0.04
-The code is more legible
-Some changes to the code that also improve efficiency
v0.05
-Removed some not-needed configuration values
-Now the corpses fly accordingly to the floor's Z
v0.06
-Changed struct names to Camel Case
-Triggers now use conditions
Warning: in the demo map the T32 period is set very high (0.09) to enhance efficiency, set it to 0.03125 if you want to see the tower the way it's meant to be


 

Dirac

22710180
Reaction score
147
Update
v0.02
-Minor Tweaking to the map
-Increased efficiency by removing a boolean check performed every period
-Removed xefx as a library (was causing issues indexing corpses)
I wan't to hear some feedback, maybe some suggestions? :D
 

Dirac

22710180
Reaction score
147
Another Update
v0.03
-Minor Tweaking to the map
-Changed the way corpses move to increase efficiency
-Some changes to the code that also improve efficiency
 

tooltiperror

Super Moderator
Reaction score
231
I like it. The code is fine as far as I can see.

Approved.

EDIT Moved back to T&R section as it could use some work.
 

Bribe

vJass errors are legion
Reaction score
67
This section needs the most work:
JASS:
    private function getdmg takes unit tower returns real
        return (GetUnitAbilityLevel(tower,ABIL)*0.)+500.
    endfunction
    //this function determines the splash AoE
    private function getaoe takes unit tower returns real
        return (GetUnitAbilityLevel(tower,ABIL)*0.)+200.
    endfunction
    //this function determines how far it can summon corpses
    private function getadquisition takes unit tower returns real
        return (GetUnitAbilityLevel(tower,ABIL)*0.)+1000.
    endfunction
    //this function determines how far the tower can attack
    private function getrange takes unit tower returns real
        return (GetUnitAbilityLevel(tower,ABIL)*0.)+800.
    endfunction
    //this function determines how much corpses it can hold
    private function getcount takes unit tower returns integer
        return (GetUnitAbilityLevel(tower,ABIL)*0)+6
    endfunction


You could pass "level" as a parameter rather than calling
it every time. You also don't need to do "*0." the full level
part should just be deleted and added only by the user if
he wants it, you also don't need the parenthesis because
programming math parses mul/div before add/sub.

You also could multiply by 0 instead of by 0. because it will
return a real number any way.
 

tooltiperror

Super Moderator
Reaction score
231
And you have a lot of handles you don't null.

Things named like addcorpse should be AddCorpse.

Rather than do this:
JASS:
            call UnitAddAbility(.thecorpse,CROW)
            call SetUnitFlyHeight(.thecorpse,z,0.)
            call UnitRemoveAbility(.thecorpse,CROW)
Just add AutoFly as a requirement.

JASS:
            if IsUnitInGroup(.tower.unit,TOWERGROUP)==true then

You don't need "== true".

JASS:
    globals
        private integer TEMPDATA
        private unit TEMPUNIT
        private group GROUP=CreateGroup()
        private group TOWERGROUP=CreateGroup()
        public group AVOIDGROUP=CreateGroup() //units inside this group won&#039;t be summoned as corpses
    endglobals

These should be named in CamelCase, and "TEMPDATA" is a bad name anyways. What does it do?

All struct names should be named in CamelCase also.

in corpse (corpse -> Corpse) you use onDestroy. Just override "destroy" and add in .deallocate into it at the end.

In start you use TriggerAddAction, just use Conditions, they're faster.
 

Dirac

22710180
Reaction score
147
>These should be named in CamelCase, and "TEMPDATA" is a bad name anyways. What does it do?
It's a var i was using before, but replaced it and didn't realize it was still there

>in corpse (corpse -> Corpse) you use onDestroy. Just override "destroy" and add in .deallocate into it at the end.
I was thinking of maybe adding Alloc as an optional, so... should i change this?

>In start you use TriggerAddAction, just use Conditions, they're faster.
Conditions regarding event unit death are bugged, MUST use actions for this one, already tested this before

>And you have a lot of handles you don't null.
Is it bad when i don't null an unit and set it to another value?

EDIT:
v0.04
-The code is more legible
-Some changes to the code that also improve efficiency
 

tooltiperror

Super Moderator
Reaction score
231
>It's a var i was using before, but replaced it and didn't realize it was still there
Alright.

>I was thinking of maybe adding Alloc as an optional, so... should i change this?
Doesn't matter, just don't use onDestroy and use .destroy...

>Conditions regarding event unit death are bugged, MUST use actions for this one, already tested this before
Interesting.

>Is it bad when i don't null an unit and set it to another value?
You have to null local variable handles or else their reference count will climb and make them slower to read/write (I think that's why) but you definitely have to null them.
 

tooltiperror

Super Moderator
Reaction score
231
Make struct names correct. "corpseindex" => "CorpseIndex".

Make it use TriggerAddCondition, it's more efficient and such, and then the trigger could hypothetically be used dynamically and such.

That should be it.
 

Dirac

22710180
Reaction score
147
Make struct names correct. "corpseindex" => "CorpseIndex".

Make it use TriggerAddCondition, it's more efficient and such, and then the trigger could hypothetically be used dynamically and such.

That should be it.
Done
 

tooltiperror

Super Moderator
Reaction score
231
>Done
>private struct corpseindex extends array

oh yeah?
 

Sgqvur

FullOfUltimateTruthsAndEt ernalPrinciples, i.e shi
Reaction score
62
tooltiperror:
1. Excellent. Approved.

1. The spell leaks 32 locations per second:

Corpse.periodic(last line):
[ljass]call SetUnitFlyHeight(.thecorpse,z-GetLocationZ(Location(.x,.y)),0.)[/ljass]
 

Dirac

22710180
Reaction score
147
tooltiperror:
1. Excellent. Approved.

1. The spell leaks 32 locations per second:

Corpse.periodic(last line):
[ljass]call SetUnitFlyHeight(.thecorpse,z-GetLocationZ(Location(.x,.y)),0.)[/ljass]
This is correct, fixed it.

IMO this tower needs some work because it's impossible to micro it. Update on that coming soon.

EDIT: Forgot to mention:
The development of this tower uses systems that are not available on this website (such as UnitIndexer) so i don't really know if i should update it at all.
 
General chit-chat
Help Users
  • No one is chatting at the moment.

      The Helper Discord

      Staff online

      Members online

      Affiliates

      Hive Workshop NUON Dome World Editor Tutorials

      Network Sponsors

      Apex Steel Pipe - Buys and sells Steel Pipe.
      Top