scope FieryFountain /* v1.02
by Flux
http://www.hiveworkshop.com/forums/members/flux/
DESCRIPTION:
Fiery objects will start flying from the targeted
location for a certain duration after a delay.
When feiry objects hit the ground, it deals damages
to nearby enemy units.
SPELL MECHANICS NOTES:
- Start creating fiery objects at a random facing angle
- The fountain facing angle periodically moves to a certain
direction (based on Configuration)
- The fountain facing anglular movement accelerates
(acceleration is based on configuration)
- The elevation angle of each feiry object is random so the
the AOE random distribution is only approximately linear.
IMPORTING NOTES:
- If DESTROY_TREE is true, it requires IsDestructableTree
- optionally requires:
* Missile Recycler
* SpellEffectEvent
CREDITS:
- BPower: IsDestructableTree
- Bribe: MissileRecycler, SpellEffectEvent
*/
native UnitAlive takes unit u returns boolean
//=================================================================
//======================== CONFIGURATION ========================
//=================================================================
globals
//------------------ RAWCODES --------------------
private constant integer SPELL_ID = 'A000'
private constant integer DUMMY_ID = 'dumi'
private constant player DUMMY_OWNER = Player(15)
//-------------- SPELL MECHANICS ----------------
private constant boolean CLOCKWISE = true
//decrease in downward speed every TIMEOUT
private constant real GRAVITY = 5
//How fast the Fountain rotate (in degrees per second)
private constant real ANGULAR_VELOCITY = 200
//How fast the Fountain rotation accelerates (in degrees per second)
//It means ANGULULAR_VELOCITY increases
//by ANGULAR_ACCELERATION per second
private constant real ANGULAR_ACCELERATION = 20
//Maximum ANGULAR_VELOCITY
private constant real ANGULAR_VELOCITY_LIMIT = 350
//Fiery objects maximum height
private constant real FOUNTAIN_HEIGHT = 700
private constant boolean DESTROY_TREES = true
private constant real DESTROY_TREE_RADIUS = 120
private constant real TREE_DAMAGE = 10
//Attack and Damage types
private constant attacktype ATTACK_TYPE = ATTACK_TYPE_NORMAL
private constant damagetype DAMAGE_TYPE = DAMAGE_TYPE_MAGIC
private constant weapontype WEAPON_TYPE = WEAPON_TYPE_WHOKNOWS
//--------------- VISUAL EFFECT ----------------
//Number of possible missile appearances
private constant integer NUM_OF_MISSILES = 5
//Look at function SetMissileEffects to set the path
private string array missile
private constant string FOUNTAIN_PATH = "Doodads\\LordaeronSummer\\Props\\TorchHumanOmni\\TorchHumanOmni.mdl"
//Display a Floating Text of the time remaning before the fountain
//will start
private constant boolean SHOW_TIME_REMAINING = true
//Enemies can see the floating text of the time remaining
private constant boolean ENEMIES_CAN_SEE_TIME = true
//Floating Text Properties
private constant real TEXT_SIZE_INIT = 0.024
private constant real TEXT_SIZE_FINAL = 0.038
private constant real TEXT_HEIGHT = 50.0
private constant real TEXT_HEIGHT_FINAL = 100.0
//----------------- TIMING -------------------
//Loop timeout
private constant real TIMEOUT = 0.03125000
endglobals
private function AreaOfEffect takes integer lvl returns real
return lvl*50.0 + 250.0 //300, 350, 400
endfunction
//Spawn Rate
private function RocketsPerSecond takes integer lvl returns integer
return lvl*5 + 15 //15, 20, 25
endfunction
private function ExplodeRadius takes integer lvl returns real
return lvl*20.0 + 180.0 //200, 220, 240
endfunction
//Damage per Fiery Object
private function Damage takes integer lvl returns real
return lvl*2.5 + 2.5 //5, 7.5, 10
endfunction
private function SpellDuration takes integer lvl returns real
return lvl*2.0 + 6.0 //8, 10, 12
endfunction
private function SpellActivationDelay takes integer lvl returns real
return 5.0//-lvl*0.25 + 3.25 //3, 2.75, 2.5
endfunction
private function TargetFilter takes player owner, unit target returns boolean
return UnitAlive(target) and IsUnitEnemy(target, owner) and not IsUnitType(target, UNIT_TYPE_STRUCTURE)
endfunction
private function SetMissileEffects takes nothing returns nothing
set missile[1] = "Abilities\\Weapons\\ZigguratFrostMissile\\ZigguratFrostMissile.mdl"
set missile[2] = "Abilities\\Weapons\\LavaSpawnMissile\\LavaSpawnMissile.mdl"
set missile[3] = "Abilities\\Weapons\\FaerieDragonMissile\\FaerieDragonMissile.mdl"
set missile[4] = "Abilities\\Weapons\\RedDragonBreath\\RedDragonMissile.mdl"
set missile[5] = "Abilities\\Weapons\\GreenDragonMissile\\GreenDragonMissile.mdl"
endfunction
//=================================================================
//===================== END CONFIGURATION =======================
//=================================================================
private keyword Spell
globals
private real TEXT_SIZE_DIFF = TEXT_SIZE_FINAL - TEXT_SIZE_INIT
endglobals
private struct FieryObject
private real x
private real y
private real z
private real dx
private real dy
private real dz
private real speedXY
private Spell s
private unit u
private effect model
readonly thistype next
readonly thistype prev
private static location l = Location(0, 0)
private static group g = CreateGroup()
static if DESTROY_TREES then
private static rect r
private static method destroyTrees takes nothing returns nothing
local destructable d = GetEnumDestructable()
if IsTreeAlive(d) then
call SetWidgetLife(d, GetWidgetLife(d) - TREE_DAMAGE)
endif
set d = null
endmethod
endif
private method destroy takes nothing returns nothing
local unit u
//Deals damage to an Area
call GroupEnumUnitsInRange(g, .x, .y, ExplodeRadius(s.lvl), null)
loop
set u = FirstOfGroup(g)
exitwhen u == null
call GroupRemoveUnit(g, u)
if TargetFilter(s.owner, u) then
call UnitDamageTarget(.u, u, Damage(s.lvl), true, false, ATTACK_TYPE, DAMAGE_TYPE, WEAPON_TYPE)
endif
endloop
//Destroy Trees
static if DESTROY_TREES then
call MoveRectTo(r, .x, .y)
call EnumDestructablesInRect(r, null, function thistype.destroyTrees)
endif
//Clean Handles
call DestroyEffect(.model)
static if LIBRARY_MissileRecycler then
call RecycleMissile(.u)
else
call KillUnit(.u)
endif
//Remove from the list
set .next.prev = .prev
set .prev.next = .next
set .model = null
set .u = null
call .deallocate()
endmethod
method move takes nothing returns nothing
local real height
//Horizontal Movement
set .x = .x + .dx
set .y = .y + .dy
call SetUnitX(.u, .x)
call SetUnitY(.u, .y)
//Vertical Movement
set .z = .z + .dz
set .dz = .dz - GRAVITY
call MoveLocation(l, .x, .y)
set height = .z - GetLocationZ(l)
//Destroy Rocket when it hits the ground
if height > 0 then
call SetUnitFlyHeight(.u, height, 0)
//New Pitch angle
call SetUnitAnimationByIndex(.u, R2I(Atan(.dz/.speedXY)*bj_RADTODEG) + 90)
else
call .destroy()
endif
endmethod
static method head takes nothing returns thistype
local thistype this = .allocate()
set .next = 0
set .prev = 0
return this
endmethod
static method create takes Spell instance returns thistype
local thistype this = .allocate()
local real angle = GetRandomReal(instance.angleMin, 0.5*bj_PI)
set .s = instance
set .x = s.x
set .y = s.y
call MoveLocation(l, .x, .y)
set .z = GetLocationZ(l)
set .dx = s.speed*Cos(angle)*Cos(s.facing)
set .dy = s.speed*Cos(angle)*Sin(s.facing)
set .dz = s.speed*Sin(angle)
set .speedXY = s.speed*Cos(angle)
static if LIBRARY_MissileRecycler then
set .u = GetRecycledMissile(.x, .y, 0, s.facing*bj_RADTODEG)
set .model = AddSpecialEffectTarget(missile[s.missileIndex], .u, "origin")
else
set .u = CreateUnit(s.owner, DUMMY_ID, .x, .y, s.facing*bj_RADTODEG)
set .model = AddSpecialEffectTarget(missile[s.missileIndex], .u, "origin")
call PauseUnit(.u, true)
endif
call SetUnitAnimationByIndex(.u, R2I(Atan(.dz/.speedXY)*bj_RADTODEG) + 90)
//Insert in the list
set .next = s.head.next
set .prev = s.head
set .prev.next = this
set .next.prev = this
return this
endmethod
static if DESTROY_TREES then
private static method onInit takes nothing returns nothing
set r = Rect(-DESTROY_TREE_RADIUS, -DESTROY_TREE_RADIUS, DESTROY_TREE_RADIUS, DESTROY_TREE_RADIUS)
endmethod
endif
endstruct
private struct Spell
private unit caster
private unit u
readonly player owner
private effect model
readonly real x
readonly real y
readonly real facing
readonly real speed
readonly real angleMin
readonly integer missileIndex
private real angleVel
private real angleAcc
private real spawnDuration
private real spawnCtr
private real spawnTime
private boolean spawning
private real delayLeft
private real delay
static if SHOW_TIME_REMAINING then
private texttag text
endif
readonly integer lvl
readonly FieryObject head
private static timer t = CreateTimer()
private static integer array next
private static integer array prev
private static real angleVelLimit = ANGULAR_VELOCITY_LIMIT*bj_DEGTORAD*TIMEOUT
private method destroy takes nothing returns nothing
//Remove from the List
set next[prev[this]] = next[this]
set prev[next[this]] = prev[this]
if next[0] == 0 then
call PauseTimer(t)
endif
static if LIBRARY_MissileRecycler then
call RecycleMissile(.u)
call SetUnitOwner(.u, DUMMY_OWNER, false)
else
call KillUnit(.u)
endif
call DestroyEffect(.model)
set .model = null
set .u = null
set .caster = null
set .owner = null
call .deallocate()
endmethod
private method periodic takes nothing returns nothing
local FieryObject f = .head.next
static if SHOW_TIME_REMAINING then
local real percent
endif
//Move all the rockets
loop
exitwhen f == 0
call f.move()
set f = f.next
endloop
if .spawning then
set .spawnDuration = .spawnDuration - TIMEOUT
if .spawnDuration < 0 then
if head.next == 0 then
call .destroy()
endif
else
if .angleVel < angleVelLimit then
set .angleVel = .angleVel + .angleAcc
endif
set .spawnCtr = .spawnCtr + TIMEOUT
if .spawnCtr > spawnTime then
set .spawnCtr = .spawnCtr - .spawnTime
//Only update facing when about to spawn a rocket
static if CLOCKWISE then
set .facing = .facing - angleVel
else
set .facing = .facing + angleVel
endif
//Spawn Rockets here
call FieryObject.create(this)
set .missileIndex = .missileIndex + 1
if .missileIndex > NUM_OF_MISSILES then
set .missileIndex = 1
endif
endif
endif
else
set .delayLeft = .delayLeft - TIMEOUT
static if SHOW_TIME_REMAINING then
set percent = .delayLeft/.delay
//TextTag Effects
call SetTextTagText(.text, R2SW(.delayLeft, 0, 1), (TEXT_SIZE_DIFF)*(1 - percent) + TEXT_SIZE_INIT)
//Increase Size
//Color Change from Green to Red
call SetTextTagColor(.text, R2I(255*(1 - percent)), R2I(255*percent), 0, 255)
static if ENEMIES_CAN_SEE_TIME then
call SetTextTagVisibility(.text, IsUnitVisible(.u, GetLocalPlayer()))
else
call SetTextTagVisibility(.text, IsUnitVisible(.u, GetLocalPlayer()) and IsUnitAlly(.u, GetLocalPlayer()))
endif
if .delayLeft < 0 then
set .spawning = true
call DestroyTextTag(.text)
set .text = null
endif
else
if .delayLeft < 0 then
set .spawning = true
endif
endif
endif
endmethod
private static method pickAll takes nothing returns nothing
local thistype this = next[0]
loop
exitwhen this == 0
call .periodic()
set this = next[this]
endloop
endmethod
private static method onCast takes nothing returns boolean
local thistype this = .allocate()
set .caster = GetTriggerUnit()
set .owner = GetTriggerPlayer()
set .lvl = GetUnitAbilityLevel(.caster, SPELL_ID)
set .x = GetSpellTargetX()
set .y = GetSpellTargetY()
set .facing = GetRandomReal(0, 2*bj_PI)
set .spawnCtr = 0
set .spawnTime = 1.0/RocketsPerSecond(.lvl)
set .spawnDuration = SpellDuration(.lvl)
set .spawning = false
set .delay = SpellActivationDelay(.lvl)
set .delayLeft = .delay //gets decremented
set .head = FieryObject.head()
set .missileIndex = 1
//The speed of the fiery object to fit the FOUNTAIN_HEIGHT
set .speed = SquareRoot(2*GRAVITY*FOUNTAIN_HEIGHT)
//Compute the minimum elevation angle to fit the AOE configured
set .angleMin = bj_PI/2 - 0.5*Asin(AreaOfEffect(.lvl)*GRAVITY/(.speed*.speed))
set .angleVel = ANGULAR_VELOCITY*bj_DEGTORAD*TIMEOUT
set .angleAcc = ANGULAR_ACCELERATION*bj_DEGTORAD*TIMEOUT*TIMEOUT
static if LIBRARY_MissileRecycler then
set .u = GetRecycledMissile(.x, .y, 0, 0)
set .model = AddSpecialEffectTarget(FOUNTAIN_PATH, .u, "origin")
call SetUnitOwner(.u, .owner, false)
else
set .u = CreateUnit(.owner, DUMMY_ID, .x, .y, 0)
set .model = AddSpecialEffectTarget(FOUNTAIN_PATH, .u, "origin")
call PauseUnit(.u, true)
endif
static if SHOW_TIME_REMAINING then
set .text = CreateTextTag()
call SetTextTagText(.text, R2S(.delayLeft), TEXT_SIZE_INIT)
call SetTextTagPos(.text, .x - 10, .y, TEXT_HEIGHT)
call SetTextTagVisibility(.text, false)
call SetTextTagVelocity(.text, 0, 0.001*(TEXT_HEIGHT_FINAL - TEXT_HEIGHT)/.delay)
endif
//Insert in the list
set next[this] = 0
set prev[this] = prev[0]
set next[prev[this]] = this
set prev[0] = this
if prev[this] == 0 then
call TimerStart(t, TIMEOUT, true, function thistype.pickAll)
endif
return false
endmethod
static if not LIBRARY_SpellEffectEvent then
private static method condition takes nothing returns boolean
return (GetSpellAbilityId() == SPELL_ID and thistype.onCast() )
endmethod
endif
private static method onInit takes nothing returns nothing
static if LIBRARY_SpellEffectEvent then
call RegisterSpellEffectEvent(SPELL_ID, function thistype.onCast)
else
local trigger t = CreateTrigger()
call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_EFFECT)
call TriggerAddCondition(t, Condition(function thistype.condition))
set t = null
endif
call SetMissileEffects()
endmethod
endstruct
endscope