• 🏆 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!

Problems with custom missile system

Status
Not open for further replies.
Hello guys! I posted a thread earlier about how to approach making your own custom missile system, and got some nice responses. Now i am almost done making it, but have run into some awkward bugs.

First of all, i am trying to create missiles for ships which launch from an offset point. To calculate this offset point, i use relative x, y, and z coordinates, and rotate them using a simple 2d rotation matrix to align to the units rotation. The problem is, that GetLocationZ() returns the water height when used on water. This means my values become totally faulty when launching ship missiles. Is there any way to surpass this, or do i just add an if-statement which replaces the "GetTerrainZ()" for the constant value of the water height, if the point is water-pathable?

Second, my way of detecting when a unit fires was to set the attack damage multipiler to 0 for the attack type (in map constants), make the attack type instant, and detect when a unit takes damage. I was going to use the condition "if GetEventDamage() == 0" to detect a custom attack, but when i print the damage taken, the game says it is 1.00 (even though the target takes no damage). I figured this might come from the roll calculation, but if i set the roll to 0, the unit loses the attack altogether. How do i detect if the attack is custom?

Apart from this, i somehow feel like my attack system is inconsistent. Some times, the missiles don't show up until they have reached a certain height. Also, when some units are in range of two enemies at once, they fire one missile at each of them, at the same time! I checked to see if "number of targets" was set to something else than 1 in the object editor - it was not. I am posting the libraries below, if you want to have a look at them. Any feedback is very welcome.

JASS:
library mMissile initializer Init requires mEnvironment, mHashtable

    globals
        private constant real INTERVAL = 0.034
        private constant real AIR_FRICTION = 0.98
        private constant real GRAVITY = 4*INTERVAL
        
        private constant integer SYSTEM_ID = StringHash("mMissile")
        
        missile_class Missile_Nuke
        missile_class Missile_Aegis
        missile_class Missile_Trident
        missile_class Missile_MLRS
        missile_class Missile_ICBM
    endglobals

    struct missile_class
    
        string path
    
        integer unitid = 0
    
        real muzzlevel = 10
        real accel = 0.6
        real tspeed = 0.2
        real blastrad = 300
        real damage = 50
        real trackdelay = 0
        real lifespan = 10
        
        boolean homing = false
        
        static method create takes string path, integer unitid, real muzzlevel, real accel, real tspeed, real blastrad, real damage, real trackdelay, real lifespan, boolean homing returns thistype
            local thistype this = thistype.allocate()
            
            set .path       = path
            set .unitid     = unitid
            set .muzzlevel  = muzzlevel
            set .accel      = accel
            set .tspeed     = tspeed
            set .blastrad   = blastrad
            set .damage     = damage
            set .trackdelay = trackdelay
            set .lifespan   = lifespan
            set .homing     = homing
            
            return this
        endmethod
    endstruct

    struct missile
    
        implement List
        
        missile_class c
    
        unit u
        unit target
        
        effect e
        
        quaternion dir
        
        real age = 0
        
        real x = 0
        real y = 0
        real z = 0
        
        real vx = 0
        real vy = 0
        real vz = 0
        
        real tx = 0
        real ty = 0
        
        static timer t
        
        method operator owner takes nothing returns player
            return GetOwningPlayer(.u)
        endmethod
        
        method kill takes nothing returns nothing
            if .count == 1 then
                call PauseTimer(.t)
            endif
            call DestroyEffect(.e)
            call KillUnit(.u)
            call .listRemove()
            call .dir.destroy()
            call .destroy()
            
            call BJDebugMsg("Missile destroyed!")
        endmethod
        
        method explode takes nothing returns nothing
            
            if .c == Missile_Nuke then
                call explosion.create("NuclearExplosion.mdl", .x, .y, .z+90, 100, .c.blastrad*Pow(1.1, GetPlayerTechCount(.owner, 'Rabm', true)), .c.damage*Pow(1.25, GetPlayerTechCount(.owner, 'Rabm', true)), 0.31, 100, 0.65, true)
            elseif .c == Missile_ICBM then
                call explosion.create("CruiseMissile.mdl", .x, .y, .z+90, 100, .c.blastrad, .c.damage, 0.25, 100, 0.7, true)
            else
                call explosion.createSmall("NewGroundEX.mdl", .x, .y, .z+45, .c.blastrad, .c.damage, 0.65, GetOwningPlayer(.u))
            endif
            
            call .kill()
        endmethod
        
        method intercept takes nothing returns nothing
            local vector heading    = vector.create(1,0,0).rotateByQuaternion(.dir)
            local vector course     = vector.create(.tx-.x, .ty-.y, getTerrainZ(.tx, .ty)-.z).normalize()
            local vector axis       = vector.cross(heading, course)
            local real angle        = vector.angleBetween(heading, course)
            local quaternion rot
            
            if angle > .c.tspeed then
                set angle = .c.tspeed
            endif
            
            set rot = quaternion.createAxisAngle(axis, angle).multiply(.dir)
            call .dir.setTo(rot).normalize()
            
            call rot.destroy()
            call heading.destroy()
            call course.destroy()
            call axis.destroy()
        endmethod
        
        static method update takes nothing returns nothing
            local thistype this = .first
            local thistype temp
            local real tz
            local vector v
            
            loop
                exitwhen this == 0
                set temp = .next
                
                set .age = .age+INTERVAL
                
                if .age > .c.lifespan then
                    call BJDebugMsg("Timed out")
                    call .kill()
                else
                    if .c.homing and .age > .c.trackdelay then
                        call .intercept()
                    else
                        //call BJDebugMsg("No track..")
                    endif
                    
                    if .target != null and .c.homing then
                        set .tx = GetUnitX(.target)
                        set .ty = GetUnitY(.target)
                    endif
                
                    if .c.accel > 0 then
                        set v = vector.create(.c.accel, 0, 0).rotateByQuaternion(.dir)
                        set .vx = (.vx + v.xv)*AIR_FRICTION
                        set .vy = (.vy + v.yv)*AIR_FRICTION
                        set .vz = (.vz + v.zv)*AIR_FRICTION
                    else
                        set v = vector.create(1, 0, 0).rotateByQuaternion(.dir)
                    endif
                    
                    set .x = .x + .vx
                    set .y = .y + .vy
                    set .z = .z + .vz
                    
                    //call BJDebugMsg("Velocity: "+R2S(SquareRoot(.vx*.vx+.vy*.vy+.vz*.vz)))
                    //call BJDebugMsg("Angle: "+R2S(vector.angleBetween(v, v2)*bj_RADTODEG))
                    
                    set tz = getTerrainZ(.x, .y)
                    
                    if .z < tz then
                        call .explode()
                    endif
                    
                    call SetUnitLookAt(.u, "Bone_Head", .u, v.xv*100000, v.yv*100000, v.zv*100000)
                    call SetUnitPosition(.u, .x, .y)
                    call SetUnitFlyHeight(.u, .z-tz, 0)
                    
                    call v.destroy()
                endif
                
                set this = temp
            endloop
        
        endmethod
        
        static method create takes real x, real y, real z, real tx, real ty, unit target, real pitch, player owner, missile_class c returns thistype
            local thistype this = thistype.allocate()
            local real yaw = Atan2(ty-y, tx-x)
            local vector v
            
            call .listAdd()
            
            set .c = c
            
            set .x = x
            set .y = y
            set .z = z
            
            set .tx = tx
            set .ty = ty
            
            set .target = target
            
            set .dir = quaternion.createEuler(0, pitch*bj_DEGTORAD, yaw).normalize()
            if .c.muzzlevel > 0 then
                set v = vector.create(.c.muzzlevel, 0, 0).rotateByQuaternion(.dir)
                
                set .vx = v.xv
                set .vy = v.yv
                set .vz = v.zv
                
                call v.destroy()
            endif
            
            set .u = CreateUnit(owner, .c.unitid, .x, .y, 0)
            call SetUnitAnimationByIndex(.u, 0)
            call UnitAddAbility(.u, 'Amrf')
            call UnitRemoveAbility(.u, 'Amrf')
            call SetUnitFlyHeight(.u, .z-getTerrainZ(.x, .y), 0)
            set .e = AddSpecialEffectTarget(.c.path, .u, "origin")
            
            if .count == 1 then
                call TimerStart(.t, INTERVAL, true, function thistype.update)
            endif
            
            
            return this
        endmethod
        
        static method onInit takes nothing returns nothing
            set .t = CreateTimer()
        endmethod
    endstruct
    
    struct weapon
        missile_class ammo
        
        real rel_x
        real rel_y
        real rel_z
        
        real theta
        real phi
        
        static method create takes integer unitid, missile_class c, real rx, real ry, real rz, real theta, real phi returns thistype
            local thistype this = thistype.allocate()
            
            set .ammo = c
            
            set .rel_x = rx
            set .rel_y = ry
            set .rel_z = rz
            
            set .theta = theta
            set .phi = phi
            
            
            call SaveInteger(Hash, SYSTEM_ID, unitid, this)
            
            return this
        endmethod
        
        static method get takes unit u returns thistype
            if HaveSavedInteger(Hash, SYSTEM_ID, GetUnitTypeId(u)) then
                return LoadInteger(Hash, SYSTEM_ID, GetUnitTypeId(u))
            endif
            return 0
        endmethod
        
        method fire takes unit u, real tx, real ty, unit target returns nothing
            local real f = GetUnitFacing(u)
            local real cos = Cos(f*bj_DEGTORAD)
            local real sin = Sin(f*bj_DEGTORAD)
            local real x = (.rel_x*cos - .rel_y*sin) + GetUnitX(u)
            local real y = (.rel_x*sin + .rel_y*cos) + GetUnitY(u)
            local real z = getTerrainZ(x, y)+.rel_z
            
            call BJDebugMsg("X: "+R2S(x)+"Y: "+R2S(y)+"Z: "+R2S(z))
            
            call BJDebugMsg("Firing!")
            
            call missile.create(x, y, z, tx, ty, target, .phi, GetOwningPlayer(u), .ammo)
        endmethod
    
    endstruct
    
    private function Init takes nothing returns nothing
    
        //           takes integer unitid, real muzzlevel, real accel, real tspeed, real blastrad, real damage, real trackdelay, real lifespan, boolean homing
        set Missile_Nuke = missile_class.create("Abilities\\Weapons\\Mortar\\MortarMissile.mdl", 'dumm', 0, 8*INTERVAL, 0.06, 650, 2000, 3., 9999, true)
        set Missile_Aegis = missile_class.create("Abilities\\Weapons\\RocketMissile\\RocketMissile.mdl", 'dumm', 0, 8*INTERVAL, 0.06, 290, 55, 3., 12, true)
        set Missile_Trident = missile_class.create("Abilities\\Weapons\\RocketMissile\\RocketMissile.mdl", 'dumm', 0, 8*INTERVAL, 0.06, 330, 65, 3., 12, true)
        set Missile_ICBM = missile_class.create("Abilities\\Weapons\\Mortar\\MortarMissile.mdl", 'dumm', 0, 8*INTERVAL, 0.06, 400, 250, 3., 9999, true)
        
        call weapon.create('acru', Missile_Aegis, 30, 0, 50, 0, 270)
        call weapon.create('tsub', Missile_Trident, -25, 0, 50, 0, 270)
    endfunction

endlibrary

JASS:
library mDamage initializer Init requires UnitDex

    globals
        private constant integer ABIL_PREVENT_DEATH = 'hlth'
        private constant integer SYSTEM_ID = StringHash("DamageEngine")
        
        private constant real TRIGGER_REFRESH_PERIOD = 300.0
    
        private group damageGroup = CreateGroup()
        private group damageUnits = CreateGroup()
        private timer damageTimer = CreateTimer()
        
        private trigger damageTrigger = CreateTrigger()
        private trigger damageTriggerOld
        
        private real array unitNewLife
    endglobals
    
    struct damageDetection extends array
    
    
        static method filter takes unit u returns boolean
            return GetUnitTypeId(u) != 'dumm' and GetUnitAbilityLevel(u, 'Avul') == 0
        endmethod
        
        static method add takes unit u returns nothing
            call GroupAddUnit(damageUnits, u)
            call TriggerRegisterUnitEvent(damageTrigger, u, EVENT_UNIT_DAMAGED)
        endmethod
        
        static method addOnIndex takes nothing returns boolean
            local unit u = GetIndexedUnit()
            
            if thistype.filter(u) then
                //Register damage to all units which pass the filter.
                call TriggerRegisterUnitEvent(damageTrigger, u, EVENT_UNIT_DAMAGED)
                call GroupAddUnit(damageUnits, u)
            endif
            
            set u = null
            
            return false
        endmethod
        
        static method addEnum takes nothing returns boolean
            local unit u = GetFilterUnit()
            
            if thistype.filter(u) then
                //Register damage to all units which pass the filter.
                call TriggerRegisterUnitEvent(damageTrigger, u, EVENT_UNIT_DAMAGED)
                call GroupAddUnit(damageUnits, u)
            endif
            
            set u = null
            
            return false
        endmethod
        
        static method stopDamageCallback takes nothing returns nothing
            local unit u
            loop
                set u = FirstOfGroup(damageGroup)
                exitwhen u == null
                
                if GetUnitAbilityLevel(u, ABIL_PREVENT_DEATH) > 0 then
                    call UnitRemoveAbility(u, ABIL_PREVENT_DEATH)
                endif
                
                call SetUnitState(u, UNIT_STATE_LIFE, unitNewLife[GetUnitId(u)])
                call GroupRemoveUnit(damageGroup, u)
            endloop
            set u = null
        endmethod
        
        static method stopDamage takes unit u, real newlife, boolean overkill returns nothing
            local thistype this = thistype[GetUnitId(u)]
            
            if u == null then
                return
            endif
            
            if overkill then
                call UnitAddAbility(u, ABIL_PREVENT_DEATH)
            endif
            
            call SetUnitState(u, UNIT_STATE_LIFE, 10000)
            call GroupAddUnit(damageGroup, u)

            set unitNewLife[GetUnitId(u)] = newlife
            call TimerStart(damageTimer, 0, false, function thistype.stopDamageCallback)
        endmethod
    
        static method onDamage takes nothing returns boolean
            local unit target = GetTriggerUnit()
            local unit source = GetEventDamageSource()
            local real damage = GetEventDamage()
            local integer id = GetUnitTypeId(source)
            local weapon wpn
            local producer p
            
            if id == 'acru' or id == 'tsub' then
                set wpn = weapon.get(source)
                if wpn != 0 then
                    call wpn.fire(source, GetUnitX(target), GetUnitY(target), target)
                endif
            endif
            
            set p = producer.get(target)
            
            if p != 0 then
                if p.city and GetWidgetLife(target) - damage < GetUnitState(target, UNIT_STATE_MAX_LIFE)*0.2 then
                    call stopDamage(target, GetUnitState(target, UNIT_STATE_MAX_LIFE), damage > GetWidgetLife(target))
                    call p.changeOwner(GetOwningPlayer(source))
                endif
            endif
            set target = null
            set source = null
            return false
        endmethod
        
        static method enumTriggerUnits takes nothing returns nothing
            call TriggerRegisterUnitEvent(damageTrigger, GetEnumUnit(), EVENT_UNIT_DAMAGED)
        endmethod
        
        static method refresh takes nothing returns nothing
            if damageTriggerOld != null then
                call DestroyTrigger(damageTriggerOld)
            endif
            
            call DisableTrigger(damageTrigger)
            
            set damageTriggerOld = damageTrigger
            set damageTrigger = CreateTrigger()
            
            call TriggerAddAction(damageTrigger, function thistype.onDamage)
            call ForGroup(damageUnits, function thistype.enumTriggerUnits)
        endmethod
    
    endstruct
    
    private function Init takes nothing returns nothing
        call RegisterUnitIndexEvent(Filter(function damageDetection.addOnIndex), EVENT_UNIT_INDEX)
        call TriggerAddAction(damageTrigger, function damageDetection.onDamage)
        
        call TimerStart(CreateTimer(), TRIGGER_REFRESH_PERIOD, true, function damageDetection.refresh)

    endfunction

endlibrary
 
Status
Not open for further replies.
Top