library CustomMissle requires CustomEffect, Loc
globals
// Just looked good
constant real CM_PERIOD = 0.03175
// 22.5 * bj_PI / 180
private constant real CM_ANGLE_TOLERANCE = 0.392699082
endglobals
//! you forgot to give credit to moyack...
function ParabolaZ takes real h, real d, real x returns real
return (4 * h / d) * (d - x) * (x / d)
endfunction
function ParabolaZ2 takes real y0, real y1, real h, real d, real x returns real
local real A = (2*(y0+y1)-4*h)/(d*d)
local real B = (y1-y0-A*d*d)/d
return A*x*x + B*x + y0
endfunction
// == -- - - -- ==
// Default settings
// == -- - - -- ==
globals
// Declares the default height of a missle.
// Please note, this is irrelevant
private constant real CM_DEFAULT_HEIGHT = 0.
// Declares the default xyArc of the missle.
// xyArc is a curve, but horizontal.
private constant real CM_DEFAULT_XYARC = 0.
// Declares the default zArc of the missle.
private constant real CM_DEFAULT_ZARC = 0.
// Declares whether missles have decay at default or not
private constant boolean CM_DEFAULT_HASDECAY = true
// Declares the time of the default decay.
// Doesn not do anything when hasdecay is on false
private constant real CM_DEFAULT_DECAY = 5.
// Declares the default movespeed of the missle
private constant real CM_DEFAULT_MOVESPEED = 512.
// Declares the default turn of the missle
// Turnspeed: How fast the missel can change its degrees
private constant real CM_DEFAULT_TURNSPEED = 0.33
// Declares the default range of the missle.
// Warning: An unit with 0 range will !never! reach the target.
private constant real CM_DEFAULT_HITRANGE = 64.
// Some booleans whether actions
// should be done on those hits or not
private constant boolean CM_DEFAULT_HITSWALLS = false
private constant boolean CM_DEFAULT_HITSUNITS = false
private constant boolean CM_DEFAULT_HITSDESTS = false
private constant boolean CM_DEFAULT_HITSMISSLES = true
endglobals
private interface eventHandler
//: If we have a homing missle, we can just use this to react on the
//: target reached event.
method onTargetReach takes nothing returns nothing defaults nothing
//: Whenever the missle touches an unit it will trigger this event.
//: Note: You will need to enable unit hitting first!
//: (set .hitunits = true)
method onUnitTouch takes unit theUnit returns nothing defaults nothing
//: Whenever the missle touches a destructable it will trigger this event.
//: Note: You will need to enable destructable hitting first!
//: (set .hitdests = true)
method onDestTouch takes destructable theDestructable returns nothing defaults nothing
//: Whenever the missle touches a terrain wall it will trigger this event.
//: Note: You will need to enable terrain hitting first!
//: (set .hitwalls = true)
method onWallHit takes nothing returns nothing defaults nothing
//: Whenever the missle touches a missle wall it will trigger this event.
//: Note: You will need to enable missle hitting first!
//: (set .hitmissles = true)
method onCollide takes CustomMissle theCollider returns nothing defaults nothing
//: This will be runned the first time the unit moves (and only once).
//: Note: Paused units do not move.
method onStart takes nothing returns nothing defaults nothing
//: This will be runned everytime the missle is in proceeding,
//: whether its active or not.
method onLoop takes nothing returns nothing defaults nothing
//: This will be runned at the end, in the destroy method.
method onEnd takes nothing returns nothing defaults nothing
//: This will be runned right after the creation.
//: Use this to apply your missle settings here.
method onCreate takes nothing returns nothing defaults nothing
//: This will return the missle speed.
//: Use this method to slow / freeze missles.
method getMissleSpeed takes nothing returns real defaults 0.
endinterface
struct CustomMissle extends eventHandler
//: == ---------------------------- ----------------------------
//: # Primary Values
//: == ---------------------------- ----------------------------
private delegate CustomEffect eff = 0
private boolean isAlive = false
private boolean isActive = false
private boolean isStarted = false
private boolean isCreated = false
private Loc start = 0
private Loc end = 0
private real h = CM_DEFAULT_HEIGHT
//: == ---------------------------- ----------------------------
//: # Arc movement (Column flight)
//: == ---------------------------- ----------------------------
private real xyArc = CM_DEFAULT_XYARC
private real zArc = CM_DEFAULT_ZARC
//: == ---------------------------- ----------------------------
//: # Decay (Death timer)
//: == ---------------------------- ----------------------------
private boolean hasDecay = CM_DEFAULT_HASDECAY
private real decy = CM_DEFAULT_DECAY
//: == ---------------------------- ----------------------------
//: # Haze (Circles)
//: == ---------------------------- ----------------------------
private boolean hasHaze = false
private boolean hazeNeg = false
private real hazeInc = 0.
private real hazeMin = 0.
private real hazeMax = 0.
private real hazeCur = 0.
//: == ---------------------------- ----------------------------
//: # Turn / Movespeed & Management
//: == ---------------------------- ----------------------------
private real moveSpd = CM_DEFAULT_MOVESPEED
private real turnSpd = CM_DEFAULT_TURNSPEED
private boolean isTurning = false
private boolean turnForward = false
//: == ---------------------------- ----------------------------
//: # Unit/Wall/Dest/Missle Colliding
//: == ---------------------------- ----------------------------
private real hitRange = CM_DEFAULT_HITRANGE
private boolean hitsUnits = CM_DEFAULT_HITSUNITS
private boolean hitsWalls = CM_DEFAULT_HITSWALLS
private boolean hitsDests = CM_DEFAULT_HITSDESTS
private boolean hitsMissles = CM_DEFAULT_HITSMISSLES
//: == ---------------------------- ----------------------------
//: # Targets & Management
//: == ---------------------------- ----------------------------
private boolean hasTarget = false
private boolean targetZ = false
private unit target = null
//: == ---------------------------- ----------------------------
//: # Enumeration & Filters
//: == ---------------------------- ----------------------------
private group enumGroup = null
private static boolexpr enumFilter = null
private rect locRect = null
//: == ---------------------------- ----------------------------
//: # Indexing & Structstuff
//: == ---------------------------- ----------------------------
private integer index = 0
private static thistype curInstance = 0
private static timer t = CreateTimer()
private static integer a = 0
private static thistype array i
//: == ---------------------------- ----------------------------
//: # Methods - Instance Creation
//: == ---------------------------- ----------------------------
public static method create takes real x, real y, real z, real f returns thistype
local thistype this = thistype.allocate()
/* Users are so innocent */
//! what if users want to specify an absolute height?
set z = GetLocZ(x, y) + z
/* Initialize our members */
set .eff = CustomEffect.create(x, y, z, f)
set .start = Loc.create(x, y, z, 0.)
set .end = Loc.create(x, y, z, 0.)
set .enumGroup = CreateGroup()
set .locRect = Rect(0., 0., 0., 0.)
set .isAlive = true
set .isActive = true
set .index = thistype.a
set thistype.i[thistype.a] = this
set thistype.a = thistype.a +1
if thistype.a == 1 then
call TimerStart(thistype.t, CM_PERIOD, true, function thistype.run)
endif
return this
endmethod
public static method createForTarget takes real x, real y, real z, unit target, boolean useZ returns thistype
local real f = bj_RADTODEG * Atan2(GetUnitY(target) - y, GetUnitX(target) - x)
local thistype this = thistype.create(x, y, z, f)
call .setTargetUnit(target, useZ)
return this
endmethod
public static method createFromSourceToTarget takes unit source, unit target returns thistype
local real x = GetUnitX(source)
local real y = GetUnitY(source)
local real z = GetLocZ(x, y) + GetUnitFlyHeight(source)
return thistype.createForTarget(x, y, z, target, true)
endmethod
//: == ---------------------------- ----------------------------
//: # Methods - Creating general behaviours
//: == ---------------------------- ----------------------------
stub method onWallHit takes nothing returns nothing
set .alive = false
set .active = false
endmethod
stub method onCollide takes CustomMissle theCollider returns nothing
set .alive = false
set .active = false
endmethod
stub method getMissleSpeed takes nothing returns real
return .moveSpd
endmethod
//: == ---------------------------- ----------------------------
//: # Methods - Setting / Getting of members
//: == ---------------------------- ----------------------------
method operator xyarc takes nothing returns real
return .xyArc
endmethod
method operator xyarc= takes real val returns nothing
set .xyArc = val
endmethod
method operator zarc takes nothing returns real
return .zArc
endmethod
method operator zarc= takes real val returns nothing
set .zArc = val
endmethod
method operator height takes nothing returns real
return .h
endmethod
method operator height= takes real val returns nothing
set .h = val
endmethod
method operator decay takes nothing returns real
return .decy
endmethod
method operator decay= takes real val returns nothing
set .decy = val
if .decy > 0. then
set .hasDecay = true
else
set .hasDecay = false
endif
endmethod
method operator haze takes nothing returns real
return .hazeInc
endmethod
method operator haze= takes real val returns nothing
set .hazeInc = val
if .hazeInc > 0. then
set .hasHaze = true
else
set .hasHaze = false
endif
endmethod
public method hazeBounds takes real min, real max returns nothing
set .hazeMin = min
set .hazeMax = max
endmethod
method operator hitrange takes nothing returns real
return .hitRange
endmethod
method operator hitrange= takes real val returns nothing
set .hitRange = val
endmethod
method operator movespeed takes nothing returns real
return .moveSpd
endmethod
method operator movespeed= takes real val returns nothing
set .moveSpd = val
endmethod
method operator turnspeed takes nothing returns real
//! the user inputs radians and you handle degrees? Won't radians confuse the user?
return .turnSpd / TWO_PI
endmethod
method operator turnspeed= takes real val returns nothing
set .turnSpd = TWO_PI * val
endmethod
method operator active takes nothing returns boolean
return .isActive
endmethod
method operator active= takes boolean bol returns nothing
set .isActive = bol
endmethod
method operator alive takes nothing returns boolean
return .isAlive
endmethod
method operator alive= takes boolean bol returns nothing
set .isAlive = bol
endmethod
method operator hitwalls takes nothing returns boolean
return .hitsWalls
endmethod
method operator hitwalls= takes boolean bol returns nothing
set .hitsWalls = bol
endmethod
method operator hitdests takes nothing returns boolean
return .hitsDests
endmethod
method operator hitdests= takes boolean bol returns nothing
set .hitsDests = bol
endmethod
method operator hitunits takes nothing returns boolean
return .hitsUnits
endmethod
method operator hitunits= takes boolean bol returns nothing
set .hitsUnits = bol
endmethod
method operator hitmissles takes nothing returns boolean
return .hitsMissles
endmethod
method operator hitmissles= takes boolean bol returns nothing
set .hitsMissles = bol
endmethod
//: == ---------------------------- ----------------------------
//: # Methods - Targeting & Detargeting
//: == ---------------------------- ----------------------------
//! what if the target is null? then surely hasTarget should be false?
public method setTargetUnit takes unit u, boolean z returns nothing
set .end.x = GetUnitX(u)
set .end.y = GetUnitY(u)
if z then
set .end.z = GetLocZ(.end.x, .end.y) + GetUnitFlyHeight(u)
else
set .end.z = 0.
endif
set .hasTarget = true
set .target = u
set .targetZ = z
endmethod
public method setTargetUnitPos takes unit u, boolean z returns nothing
call .setTargetUnit(u, z)
set .target = null
endmethod
public method setTargetPos takes real x, real y returns nothing
set .end.x = x
set .end.y = y
set .end.z = GetLocZ(x, y)
set .target = null
set .targetZ = false
endmethod
public method setTargetPosZ takes real x, real y, real z returns nothing
set .end.x = x
set .end.y = y
set .end.z = z
set .target = null
set .targetZ = true
endmethod
//! "lose" is spelled incorrectly in the method name...
public method looseTarget takes nothing returns nothing
set .end.x = .x
set .end.y = .y
set .end.z = .z
set .target = null
set .targetZ = false
endmethod
//: == ---------------------------- ----------------------------
//: # Methods - Missle Movement
//: == ---------------------------- ----------------------------
//! All these methods are called every frame? That's bad. Really bad. If you MUST split them up, maybe use textmacros?
private method updateTarget takes nothing returns nothing
if .hasTarget then
//! checking if the target is null in the set target method eliminates the need for this check
//! to check for death / removal, you could use the AutoIndex OnDeindexed events...
//! or just make a death event and a RemoveUnit hook yourself.
if .target != null then
set .end.x = GetUnitX(.target)
set .end.y = GetUnitY(.target)
//! avoid using this GetLocZ function every frame -- just handle the location yourself...
set .end.z = GetLocZ(.end.x, .end.y) + GetUnitFlyHeight(.target)
endif
else
call .end.applyLoc(.eff.loc)
endif
endmethod
private method updateF takes nothing returns nothing
local real aimF = .eff.loc.angleTo(.end)
local real newF = 0.
if .eff.f != aimF and not .isTurning then
set .isTurning = true
if .eff.f < aimF then
set .turnForward = false
else
set .turnForward = true
endif
else
set .isTurning = false
return
endif
if .turnForward then
//! you have a TWO_PI constant - use it!
set newF = .eff.f + .turnSpd / (2. * bj_PI * CM_PERIOD)
if newF >= aimF then
set newF = aimF
set .isTurning = false
endif
else
set newF = .eff.f - .turnSpd / (2. * bj_PI * CM_PERIOD)
if newF <= aimF then
set newF = aimF
set .isTurning = false
endif
endif
set .f = newF
endmethod
private method updateXY takes nothing returns nothing
//! again, lots of these loc function you use every frame, inlining them would increase efficiency
call .eff.loc.move(.getMissleSpeed() * CM_PERIOD)
set .eff.loc = .eff.loc
if .eff.loc.distanceTo(.end) < .hitRange then
if not .targetZ or (.targetZ and .eff.loc.z >= .end.z - .h /2 and .eff.loc.z <= .end.z + .h /2) then
set .alive = false
set .active = false
call .onTargetReach()
endif
endif
endmethod
private method updateZ takes nothing returns nothing
//! loc functions...
if .targetZ then
set .eff.loc.z = .start.z + (.start.z - .end.z) * .eff.loc.distanceTo(.start) / .start.distanceTo(.end)
endif
endmethod
private method updateXYarc takes nothing returns nothing
//! loc functions...
//! all these variables could be initialised within the first if, increasing efficiency for missiles without xyarcs
local Loc new = Loc.create(.x, .y, .z, .start.angleTo(.end))
local real dist = .start.distanceTo(.end)
local real toStart = .eff.loc.distanceTo(.start)
local real face = 0.
if .xyArc != 0. then
//! To optimise, bj_DEGTORAD * 90 is PI/2. New constant? HALF_PI?
call new.moveFaced(ParabolaZ(dist * .xyArc, dist, toStart), new.f + bj_DEGTORAD * 90)
set face = new.angleTo(.end)
set new.f = face
call .eff.applyLocOnce(new)
endif
//! why are these in the xyarc function??? Make a new collision one?
if .hitsUnits then
call GroupEnumUnitsInRange(.enumGroup, new.x, new.y, .hitRange, thistype.enumFilter)
endif
if .hitsDests then
call SetRect(.locRect, new.x - .hitRange, new.y - .hitRange, new.x + .hitRange, new.y + .hitRange)
call EnumDestructablesInRect(.locRect, Condition(function thistype.enumDestsInRange), null)
endif
call new.destroy()
endmethod
private method updateZarc takes nothing returns nothing
//! loc functions...
//! same as above for variables
local Loc new = Loc.create(.x, .y, .z, .start.angleTo(.end))
local real dist = .start.distanceTo(.end)
local real toStart = .eff.loc.distanceTo(.start)
if .zArc != 0. then
set new.z = ParabolaZ2(.start.z, .end.z, .z * .zArc, dist, toStart)
call .eff.applyLocOnce(new)
endif
//! again, why is this in an arc function?
if .hitsWalls then
if new.z < GetLocZ(new.x, new.y) then
call .onWallHit()
endif
endif
call new.destroy()
endmethod
private method haze takes nothing returns nothing
local Loc new = 0
if .hasHaze then
if hazeNeg then
set .hazeCur = .hazeCur - .hazeInc * CM_PERIOD
if .hazeCur <= .hazeMin then
set .hazeCur = .hazeMin
set .hazeNeg = false
endif
else
set .hazeCur = .hazeCur + .hazeInc * CM_PERIOD
if .hazeCur >= .hazeMax then
set .hazeCur = .hazeMax
set .hazeNeg = true
endif
endif
//! loc functions...
set new = Loc.create(.x, .y, .z, .f)
//! HALF_PI
call new.moveFaced(.hazeCur, .f + 90 * bj_DEGTORAD)
call .eff.applyLocOnce(new)
call new.destroy()
endif
endmethod
//: == ---------------------------- ----------------------------
//: # Methods - Instancemanagement & Instanceexecution
//: == ---------------------------- ----------------------------
private method move takes nothing returns nothing
if not .isCreated then
set .isCreated = true
call .onCreate()
else
call .onLoop()
if .hasDecay then
set .decy = .decy - CM_PERIOD
endif
if .decy < 0. then
set .alive = false
set .active = false
endif
endif
if not .isActive or not .isAlive then
return
endif
if not .isStarted then
set .isStarted = true
call .onStart()
endif
call .updateTarget()
call .updateXY()
call .updateF()
call .updateZ()
call .updateXYarc()
call .updateZarc()
call .haze()
if .hitsMissles then
call .enumMissles()
endif
endmethod
public static method run takes nothing returns nothing
local integer i = 0
if thistype.a == 0 then
call PauseTimer(thistype.t)
return
endif
loop
exitwhen i >= thistype.a
if not thistype.i[i].alive then
call thistype.i[i].destroy()
set thistype.a = thistype.a -1
set thistype.i[i] = thistype.i[thistype.a]
set thistype.i[i].index = i
set i = i -1
else
set thistype.curInstance = thistype.i[i]
call thistype.i[i].move()
endif
set i = i +1
endloop
endmethod
//: == ---------------------------- ----------------------------
//: # Methods - Enumeration of Missles, Destructables, Units
//: == ---------------------------- ----------------------------
private method enumMissles takes nothing returns nothing
local unit rallyThis = null
local unit rallyThat = null
local real dist = 0.
local thistype that = 0
local integer i = this.index +1
local boolean collide = false
loop
exitwhen i >= thistype.a or collide
set that = thistype.i[i]
set rallyThis = this.eff.getEffUnit()
set rallyThat = that.eff.getEffUnit()
if not this.targetZ then
set dist = DistBetweenUnits(rallyThis, rallyThat)
else
set dist = DistBetweenUnitsZ(rallyThis, rallyThat)
endif
if dist <= .hitRange and dist != -1 then
set collide = true
endif
set i = i +1
endloop
set rallyThis = null
set rallyThat = null
if collide then
call this.onCollide(that)
call that.onCollide(this)
endif
endmethod
private static method enumUnitsInRange takes nothing returns boolean
local thistype this = thistype.curInstance
local unit u = GetFilterUnit()
local real z = GetLocZ(GetUnitX(u), GetUnitY(u)) + GetUnitFlyHeight(u)
if IsUnitInGroup(u, .enumGroup) then
return false
endif
if not .targetZ or (.targetZ and z >= .z - .h /2 and z <= .z + .h /2 ) then
call GroupAddUnit(.enumGroup, u)
call .onUnitTouch(u)
endif
return false
endmethod
private static method enumDestsInRange takes nothing returns boolean
local thistype this = thistype.curInstance
call .onDestTouch(GetFilterDestructable())
return false
endmethod
//: == ---------------------------- ----------------------------
//: # Methods - Destroying & Filter init
//: == ---------------------------- ----------------------------
private method onDestroy takes nothing returns nothing
set .alive = false
set .active = false
call .onEnd()
call .start.destroy()
call .end.destroy()
call DestroyGroup(.enumGroup)
call RemoveRect(.locRect)
call DestroyGroup(.enumGroup)
call .eff.destroy()
endmethod
private static method onInit takes nothing returns nothing
set thistype.enumFilter = Condition(function thistype.enumUnitsInRange)
endmethod
endstruct
endlibrary