scope GatlingBot /*
Gatling Bot v3.7
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
Created by: Quilnez
A. Description
¯¯¯¯¯¯¯¯¯¯¯¯¯¯
Summons an automatic drone at the target location. The drone will
attack enemy units around with hail of gun firings.
B. How to import
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
1. Import these stuffs into your map:
- (Object Editor - Unit) Missile Dummy
- (Object Editor - Unit) Gatling Bot
- (Object Editor - Abilities) Summon Gatling Bot
- (Import Manager) dummy.mdx
- Other files are rather optional, you can use yours instead
2. Import Gatling Bot trigger into your map
3. Import and configure all required external dependencies properly
4. Configure the spell properly
5. Done!
C. External Dependencies
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
- CTL @ github.com/nestharus/JASS/tree/master/jass/Systems/ConstantTimerLoop32
- Missile @ hiveworkshop.com/forums/jass-resources-412/system-missile-207854/
- RapidSound @ hiveworkshop.com/threads/snippet-rapidsound.258991/
- LockBone @ hiveworkshop.com/forums/submissions-414/snippet-lockbone-259005/
D. Credits
¯¯¯¯¯¯¯¯¯¯
Author | Contribution(s)
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
Nestharus | CTL
BPower | Missile
Muoteck | BTNSpiderbot
Vexorian | dummy.mdx
Gottfrei | LT-1.mdx
Grey Knight | Bullet.mdx
*/
private keyword SFX
/** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** **
______________________________
General Settings
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯ */
globals
// Main spell's raw code
private constant integer SPELL_ID = 'A000'
// Drone unit's raw code
private constant integer DRONE_ID = 'h000'
// Dummy unit's raw code (only useful if you don't use MissileRecycler)
private constant integer DUMMY_ID = 'dumi'
// Range to indicate that the master is moving
private constant real MOVE_RANGE_INDICATOR = 64.0
// Drone model's bone that will be rotated around
private constant string LOCKED_BONE = BONE_PART_CHEST
// This value will make sure the drone will
// face straight forward
private constant real LOCKED_BONE_ZOFFSET = 100.0
// If true, the accuracy's inconsistency will be calculated only for once
// Will be noticeable when the drone fires more than one missile at once
private constant boolean ACCURACY_MODE = false
// "move" order id
private constant integer MOVE_ORDER = 851986
// Drone's animation that going to be played
// when it attacks
private constant string ATTACK_ANIMATION = "attack"
// If true all damages by drone will be considered to come
// from it's master instead
private constant boolean CASTER_DAMAGE_SOURCE = true
// If false, the missile will only travel from drone's
// position to it's attack target, ignoring it's optimum
// attack range
private constant boolean STATIC_ATTACK_RANGE = true
// Attack & damage type of dealt damage
private constant attacktype ATTACK_TYPE = ATTACK_TYPE_PIERCE
private constant damagetype DAMAGE_TYPE = DAMAGE_TYPE_NORMAL
// Time for special effects to complete their animations
private constant real SFX_DECAY_TIME = 3.0
// Played when the drone attacks
private constant string FIRE_SOUND_PATH = "war3mapImported\\fire.wav"
private constant integer FIRE_SOUND_VOLUME = 200
// Missile's detail
private constant string MISSILE_MODEL_PATH = "war3mapImported\\Bullet.mdx"
private constant real MISSILE_LAUNCH_Z = 40.0
private constant real MISSILE_IMPACT_Z = 40.0
private constant real MISSILE_COLLISION_SIZE = 45.0
endglobals
private module DroneInitialization
/* ______________________________
Drone's attribute data
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
Notes:
- This block will execute every time a drone
is created
- Use "level" to refer to drone's level
- Use ".unit" to refer to the drone unit */
// 1. Damage amount
// Drone's damage is increased by 6 pts every 5 levels
set .dmgTop = 14.0 + 6.0*I2R(level/5)
set .dmgLow = 11.0 + 6.0*I2R(level/5)
// 2. Piercing attack
// If true, drone's missiles will become piercing
// If ability's level is equal or more than 15, drone's attacks
// will become piercing
set .setPierce = level >= 15
// 3. Impact damage
// Area damage dealt when drone's missile is destroyed
if level < 20 then
set .impactDmg = 0.0
else
set .impactDmg = GetRandomReal(.dmgLow, .dmgTop)
endif
// 4. Impact damage AoE
// AoE range of impact damage
// Impact events will only fire if this AoE > 0
if level < 20 then
set .impactAoE = 0.0
else
set .impactAoE = 200.0
endif
// 5. Missile count
// Number of missile launched at once
// If ability's level is less than 10, drone fires 1 missile
// at once. Else it fires two missiles at once.
if level < 10 then
set .mslCount = 1
else
set .mslCount = 2
endif
// 6. Missile spacing
// Distance between missile. Works if only mslCount is more than 1
set .mslSpace = 30
// 7.Accuracy
// Drone's firing accuracy. Higher value means less accurate
// it would be
set .accuracy = 45.0*bj_DEGTORAD
// 8. Drone's head turn rate
// Drone's head can turn 4 degrees per tick (0.03125 s)
set .turnRate = 4.0*bj_DEGTORAD
// 9. Acquisition range
// Drone can acquire targets within 800 range (level 1-4)
// and 1000 range on higher levels
if level < 5 then
set .acqRange = 800.0
else
set .acqRange = 1000.0
endif
// 10. Attack range
// If ability's level is less than 5, drone's attack range
// is 600. Else it's 800.
if level < 5 then
set .atkRange = 600.0
else
set .atkRange = 800.0
endif
// 11. Attack rate
// Drone will attack every 0.075s
set .atkSpeed = 0.075
// 12. Follow interval
// Drone will follow its master every 0.5s
set .fllwRate = 0.5
// 13. Wander area
// How far the drone is allowed to wander around
// it's master
set .wanderArea = 300.0
// 14. Wander interval
// Drone will wander around every 4s
set .wndrRate = 4.0
// 15. Cautious (self secure)
// If true, the drone will keep some distances from it's target
set .cautious = true
/* ______________________________
Missile's data
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯ */
// 1. Missile move rate
// Missiles can move 50 range away per tick
set .mslSpeed = 50.0
// 2. Missile acceleration
// May causes missiles to move faster over times
set .mslAccel = 0.0
// 3. Missile homing state
// If true, the missile will become homing
set .setHoming = false
// 4. Missile turn rate (in radians)
// How agile can the missile turn around (if setHoming=true)
set .mslTrRate = 0.0
// 5. Missile curve in radians (the absolute value must be below HP/2)
set .mslCurve = 0.0
// 6. Missile arc in radians (the absolute value must be below HP/2)
set .mslArc = 0.0
/* ______________________________
Additional Config.
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯ */
// Drone has 25 additional health points every level
call SetUnitState(.unit, UNIT_STATE_LIFE, 125 + 25*level)
// Drone lasts for 59+level seconds
call UnitApplyTimedLife(.unit, 'BTLF', 59+level)
endmodule
// p = owner of the drone, t = damage target, level = drone level
// By default drone will only attack visible alive units
private function droneTargets takes player p, unit t, integer level returns boolean
return IsUnitEnemy(t, p) and not IsUnitType(t, UNIT_TYPE_MECHANICAL)
endfunction
/* ______________________________
Event Catcher
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯ */
private module EventCatcher
// You can use these parameters to manipulate things:
// - "dex" refers to the triggering missile instance ..
// .. read Missile lib's API for more detail
// - "source" refers to the source of the damage
// - "target" refers to the target of the damage
// - "level" refers to the drone's level
// Fires when a unit receive damage from missiles
static method onMissileHit takes Missile dex, unit source, unit target, integer level returns nothing
// Bonus API to create effects with detailed data
// call SFX.onPoint(modelPath, facing, scale, x, y, z, duration)
call SFX.onPoint("war3mapImported\\bullet hit blood.mdx", dex.angle*bj_RADTODEG+180, 1, dex.x, dex.y, dex.z, 3)
endmethod
// Fires when a missile impacts
static method onImpact takes Missile dex, integer level returns nothing
call SFX.onPoint("Abilities\\Weapons\\Rifle\\RifleImpact.mdl", dex.angle*bj_RADTODEG, 1, dex.x, dex.y, dex.z, 1)
endmethod
// Fires when a unit takes damage from impact
static method onImpactHit takes Missile dex, unit source, unit target, integer level returns nothing
endmethod
endmodule
/** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** **/
globals
private constant real INTERVAL = 0.031250000
endglobals
native UnitAlive takes unit id returns boolean
private keyword GatlingBot
private struct ResetDummy
unit dummy
real dur
static real MapEdgeX
static real MapEdgeY
implement CTLExpire
set .dur = .dur - INTERVAL
if .dur <= 0 then
call SetUnitX(.dummy, MapEdgeX)
call SetUnitY(.dummy, MapEdgeY)
call SetUnitScale(.dummy, 1, 1, 1)
call destroy()
set .dummy = null
endif
implement CTLEnd
static method reset takes unit whichUnit returns nothing
local thistype this = create()
set .dummy = whichUnit
set .dur = SFX_DECAY_TIME
endmethod
static method onInit takes nothing returns nothing
set MapEdgeX = GetRectMaxX(bj_mapInitialPlayableArea)
set MapEdgeY = GetRectMaxY(bj_mapInitialPlayableArea)
endmethod
endstruct
private struct SFX
static constant player PASSIVE = Player(PLAYER_NEUTRAL_PASSIVE)
static method onPoint takes string model, real angle, real scale, real x, real y, real z, real dur returns nothing
local unit u
static if LIBRARY_MissileRecycler then
set u = GetRecycledMissile(x, y, z, angle)
else
set u = CreateUnit(PASSIVE, DUMMY_ID, x, y, angle)
static if not LIBRARY_AutoFly then
if UnitAddAbility(u, 'Amrf') and UnitRemoveAbility(u, 'Amrf') then
endif
endif
call SetUnitFlyHeight(u, z, 0)
endif
static if LIBRARY_MissileRecycler then
call RecycleMissile(u)
call ResetDummy.reset(u)
else
call UnitApplyTimedLife(u, 'BTLF', dur)
endif
call SetUnitScale(u, scale, 1, 1)
call DestroyEffect(AddSpecialEffectTarget(model, u, "origin"))
set u = null
endmethod
endstruct
private struct Bullet
real distance
Missile missile
GatlingBot index
static group Group = CreateGroup()
implement EventCatcher
static method onRemove takes Missile dex returns boolean
local thistype this = dex.data
local GatlingBot dex2 = .index
local unit u
if dex2.impactAoE > 0 then
call thistype.onImpact(dex, dex2.level)
// Deal impact damage
call GroupEnumUnitsInRange(Group, dex.x, dex.y, dex2.impactAoE, null)
loop
set u = FirstOfGroup(Group)
exitwhen u == null
call GroupRemoveUnit(Group, u)
if u == dex2.target or (droneTargets(dex2.owner, u, dex2.level) and UnitAlive(u) and IsUnitVisible(u, dex2.owner)) then
// If damage source should be drone's master and the master is available
if CASTER_DAMAGE_SOURCE and UnitAlive(dex2.master) then
call thistype.onImpactHit(dex, dex2.master, u, dex2.level)
call UnitDamageTarget(dex2.master, u, dex2.impactDmg, false, false, ATTACK_TYPE, DAMAGE_TYPE, null)
else
call thistype.onImpactHit(dex, dex2.unit, u, dex2.level)
call UnitDamageTarget(dex2.unit, u, dex2.impactDmg, false, false, ATTACK_TYPE, DAMAGE_TYPE, null)
endif
endif
endloop
endif
call ResetDummy.reset(dex.dummy)
call .destroy()
return true
endmethod
static method onPeriod takes Missile dex returns boolean
local thistype this = dex.data
local GatlingBot dex2 = .index
// If the missile doesn't collide with ground
if GetUnitFlyHeight(dex.dummy) > .01 then
set .distance = .distance+dex.speed
return (dex2.setHoming or not STATIC_ATTACK_RANGE) and .distance > dex2.atkRange
else
return true
endif
endmethod
static method onCollide takes Missile dex, unit justHit returns boolean
local thistype this = dex.data
local GatlingBot dex2 = .index
local real d
if justHit == dex2.target or (droneTargets(dex2.owner, justHit, dex2.level) and UnitAlive(justHit) and IsUnitVisible(justHit, dex2.owner)) then
set d = GetRandomReal(dex2.dmgLow, dex2.dmgTop)
// If damage source should be drone's master and the master is available
if CASTER_DAMAGE_SOURCE and UnitAlive(dex2.master) then
call thistype.onMissileHit(dex, dex2.master, justHit, dex2.level)
call UnitDamageTarget(dex2.master, justHit, d, false, false, ATTACK_TYPE, DAMAGE_TYPE, null)
else
call thistype.onMissileHit(dex, dex2.unit, justHit, dex2.level)
call UnitDamageTarget(dex2.unit, justHit, d, false, false, ATTACK_TYPE, DAMAGE_TYPE, null)
endif
call dex.hitWidget(justHit)
// Instantly destroy missile if not piercing
return not dex2.setPierce
else
return false
endif
endmethod
implement MissileStruct
static method fire takes GatlingBot dex returns nothing
local thistype this
local integer i = 0
local real a = dex.angle-bj_PI/2
local real cos = Cos(a)
local real sin = Sin(a)
local real d = (dex.mslSpace*dex.mslCount)/2
local real x = GetUnitX(dex.unit)-d*cos
local real y = GetUnitY(dex.unit)-d*sin
local real droneZ = GetUnitFlyHeight(dex.unit)
local real targetX
local real targetY
local real targetZ
local real acc
local real ar
// If true, the inconsistency will only be calculated once
static if ACCURACY_MODE then
set acc = GetRandomReal(-dex.accuracy,dex.accuracy)/2
endif
set targetZ = GetUnitFlyHeight(dex.target)
if STATIC_ATTACK_RANGE then
set ar = dex.atkRange
else
set targetX = GetUnitX(dex.target)
set targetY = GetUnitY(dex.target)
set ar = SquareRoot((targetX-x)*(targetX-x)+(targetY-y)*(targetY-y))
endif
// Launch missiles
loop
exitwhen i >= dex.mslCount
set this = allocate()
set .distance = 0
static if not ACCURACY_MODE then
set acc = GetRandomReal(-dex.accuracy,dex.accuracy)/2
endif
set .index = dex
set .missile = Missile.create(x, y, droneZ+MISSILE_LAUNCH_Z, dex.angle+acc, ar, MISSILE_IMPACT_Z+targetZ)
set .missile.curve = dex.mslCurve
set .missile.source = dex.unit
set .missile.speed = dex.mslSpeed
set .missile.model = MISSILE_MODEL_PATH
set .missile.arc = dex.mslArc
set .missile.data = this
set .missile.acceleration = dex.mslAccel
set .missile.collision = MISSILE_COLLISION_SIZE
if dex.setHoming then
set .missile.target = dex.target
set .missile.turn = dex.mslTrRate
endif
call launch(.missile)
// Calculate new offset
set x = x+dex.mslSpace*cos
set y = y+dex.mslSpace*sin
set i = i+1
endloop
endmethod
endstruct
private struct GatlingBot extends array
real dmgLow
real dmgTop
real accuracy
real acqRange
real atkRange
real atkSpeed
real fllwRate
real wndrRate
real turnRate
real impactAoE
real impactDmg
real mslAccel
real mslSpace
real mslSpeed
real mslTrRate
real mslCurve
real mslArc
integer mslCount
boolean cautious
boolean setPierce
boolean setHoming
integer level
player owner
unit unit
unit target
unit master
real angle
real masterLocX
real masterLocY
real moveDelay
real attackDelay
real wanderArea
RSound sound
static group TempGroup = CreateGroup()
static unit TempUnit
method getClosestTarget takes real x, real y returns unit
local unit fog
local real closestDist
local real x2
local real y2
local real d
set TempUnit = null
call GroupEnumUnitsInRange(TempGroup, x, y, .acqRange, null)
loop
set fog = FirstOfGroup(TempGroup)
exitwhen fog == null
call GroupRemoveUnit(TempGroup, fog)
if droneTargets(.owner, fog, .level) and UnitAlive(fog) and IsUnitVisible(fog, .owner) then
set x2 = GetUnitX(fog)
set y2 = GetUnitY(fog)
set d = (x2-x)*(x2-x)+(y2-y)*(y2-y)
if TempUnit == null or d<closestDist then
set closestDist = d
set TempUnit = fog
endif
endif
endloop
return TempUnit
endmethod
implement CTL
local real a
local real d
local real r
local real droneX
local real droneY
local real targetX
local real targetY
local real masterX
local real masterY
local unit nt
local boolean b
implement CTLExpire
if UnitAlive(.unit) then
set droneX = GetUnitX(.unit)
set droneY = GetUnitY(.unit)
// If doesn't have target, attempt to obtain one
if .target == null then
set .target = getClosestTarget(droneX, droneY)
set a = GetUnitFacing(.unit)*bj_DEGTORAD
else
// If the target is still valid
if UnitAlive(.target) and IsUnitVisible(.target, .owner) then
set targetX = GetUnitX(.target)
set targetY = GetUnitY(.target)
set r = (droneX-targetX)*(droneX-targetX)+(droneY-targetY)*(droneY-targetY)
// If target is too far
if r > .acqRange*.acqRange then
set .target = null
// If target is beyond the max attack range
elseif r > .atkRange*.atkRange then
// Search for new closer target
set nt = getClosestTarget(droneX, droneY)
if nt != null then
// Switch to the new target
set .target = nt
set targetX = GetUnitX(.target)
set targetY = GetUnitY(.target)
set nt = null
endif
// If it's time to move, then allow drone to come closer to target
if .moveDelay > .fllwRate then
set .moveDelay = 0
if .master != null then
set a = Atan2(GetUnitY(.master)-targetY, GetUnitX(.master)-targetX)+GetRandomReal(-bj_PI, bj_PI)/4
else
set a = Atan2(droneY-targetY, droneX-targetX)+GetRandomReal(-bj_PI, bj_PI)/4
endif
call IssuePointOrderById(.unit, MOVE_ORDER, targetX+.atkRange*Cos(a), targetY+.atkRange*Sin(a))
endif
else
if .cautious and .moveDelay > .fllwRate then
set .moveDelay = 0
if .master != null then
set a = Atan2(GetUnitY(.master)-targetY, GetUnitX(.master)-targetX)+GetRandomReal(-bj_PI, bj_PI)/4
else
set a = Atan2(droneY-targetY, droneX-targetX)+GetRandomReal(-bj_PI, bj_PI)/4
endif
call IssuePointOrderById(.unit, MOVE_ORDER, targetX+.atkRange*Cos(a), targetY+.atkRange*Sin(a))
endif
// If ready to fire (attack)
if .attackDelay > .atkSpeed then
set .attackDelay = 0
call Bullet.fire(this)
call SetUnitAnimation(.unit, ATTACK_ANIMATION)
if .sound != 0 then
call .sound.play(droneX, droneY, MISSILE_LAUNCH_Z, FIRE_SOUND_VOLUME)
endif
else
set .attackDelay = .attackDelay+INTERVAL
endif
endif
set a = Atan2(targetY-droneY, targetX-droneX)
else
if .target != null then
set .moveDelay = .wndrRate-.fllwRate
set .target = null
endif
set a = GetUnitFacing(.unit)*bj_DEGTORAD
endif
endif
set b = false
// Update the bone rotation angle
if .turnRate > 0 and Cos(.angle-a) < Cos(.turnRate) then
if Sin(a-.angle) >= 0 then
set .angle = .angle+.turnRate
else
set .angle = .angle-.turnRate
endif
set b = true
elseif .angle != a then
set .angle = a
set b = true
endif
if b then
// If drone doesn't have target yet
if .target == null then
set d = LOCKED_BONE_ZOFFSET+GetUnitFlyHeight(.unit)
call LockBone.lockAtAngle(.unit, LOCKED_BONE, .angle, d, d)
else
set d = SquareRoot((targetX-droneX)*(targetX-droneX)+(targetY-droneY)*(targetY-droneY))
call LockBone.lockAtAngle(.unit, LOCKED_BONE, .angle, d, LOCKED_BONE_ZOFFSET+GetUnitFlyHeight(.target))
endif
endif
if UnitAlive(.master) then
set masterX = GetUnitX(.master)
set masterY = GetUnitY(.master)
// If master is moving
if (.masterLocX-masterX)*(.masterLocX-masterX)+(.masterLocY-masterY)*(.masterLocY-masterY) > MOVE_RANGE_INDICATOR*MOVE_RANGE_INDICATOR then
if .moveDelay > .fllwRate then
set .moveDelay = 0
set a = (GetUnitFacing(.master)*bj_DEGTORAD+bj_PI)+GetRandomReal(-bj_PI, bj_PI)/2
set d = GetRandomReal(0, .wanderArea)
call IssuePointOrderById(.unit, MOVE_ORDER, .masterLocX+d*Cos(a), .masterLocY+d*Sin(a))
endif
set .masterLocX = masterX
set .masterLocY = masterY
elseif .moveDelay > .wndrRate then
set .moveDelay = 0
set a = GetRandomReal(-bj_PI, bj_PI)
set d = GetRandomReal(0, .wanderArea)
call IssuePointOrderById(.unit, MOVE_ORDER, .masterLocX+d*Cos(a), .masterLocY+d*Sin(a))
endif
set .moveDelay = .moveDelay+INTERVAL
endif
else
if .sound != 0 then
call .sound.kill()
set .sound = 0
endif
call destroy()
set .unit = null
set .target = null
set .master = null
endif
implement CTLEnd
static method onCast takes nothing returns boolean
local thistype this
if GetSpellAbilityId() == SPELL_ID then
set this = create()
set .owner = GetTriggerPlayer()
set .angle = GetRandomReal(-bj_PI, bj_PI)
set .sound = RSound.create(FIRE_SOUND_PATH, true, false, 12700, 12700)
set .unit = CreateUnit(.owner, DRONE_ID, GetSpellTargetX(), GetSpellTargetY(), .angle*bj_RADTODEG)
set .level = GetUnitAbilityLevel(GetTriggerUnit(), SPELL_ID)
set .target = null
set .attackDelay = 999999
set .master = GetTriggerUnit()
set .masterLocX = GetUnitX(.master)
set .masterLocY = GetUnitY(.master)
set .moveDelay = 0.0
implement DroneInitialization
endif
return false
endmethod
static method onInit takes nothing returns nothing
local trigger t = CreateTrigger()
call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_EFFECT)
call TriggerAddCondition(t, Condition(function thistype.onCast))
endmethod
endstruct
endscope