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

Fiery Fountain v1.02

Preview GIFs (Outdated)

151345d1451408839-fiery-fountain-v1-00-fiery-fountain.gif



Code
JASS:
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


Changelog:
v1.00 - [30 December 2015]
- Initial Release

v1.01 - [8 January 2016]
- Rounded down the floating text time shown to have less entries in the String Table
- Added effects to the floating text
- Fixed some minor scripting mistakes.

v1.02 - [10 January 2016]
- Fixed some syntax error.
- Added Maximum Floating Text Height configuration.
- Added Floating Text Visibility configuration.
- Fiery Fountain now provides vision.

Keywords:
fireworks, fire works, new year, fire crackers, fiery, fountain
Contents

Fiery Fountain (Map)

Reviews
14:36, 13th Jan 2016 IcemanBo: Solid and nice looking spell. Recommended (+).
Level 22
Joined
Feb 6, 2014
Messages
2,466
From what I have experienced, floating text can be seen even in fog of war, don't know if that's the case here though.

Fortunately, that's not the case here ;)
See for yourself if you want, cast it on fogged location like higher cliff levels.

Nice looking ability with fitting effects during this time of year.
Thanks, planned to upload it on New Year's Eve and I started working on it earlier afternoon (in my time) and I estimated it will be finished on New Year's Eve, but it was finished ahead of schedule and I'm impatient to wait, so I uploaded it already. Look at the keywords ;)
 
Level 22
Joined
Feb 6, 2014
Messages
2,466
I mean with the floating texts. You may want to cut down the extra decimals as too many different strings mess up the string table.

How can I round off the number to the nearest tenths efficiently?
Using SubString won't do since it will populate the string table more. I was thinking of storing the ones and tenths digit to an integer and using I2S(ones) + "." + I2S(tenths) but are there better ways?
 
There could be a MAX_HEIGHT for texttags.
I've set the delay to a bigger number for fun, and it looked weird,
because the texttag just never stops in rising upward.

The texttag looks neat, but it would be a bit user-friendlier if it would not necesaarily create them.
Doing them optional would be optimal.

In this resource it would make perfec sense to me if the dummy has some sight radius vision. Maybe ~500 (?), idk exactly
Because otherwise you would not even see the firework if no other unit is next to it.
And then this would also mean that the owner of recyled missle need to be set to .caster onCast method.

set .u = CreateUnit(s.owner, DUMMY_ID, .x, .y, 0)
->
set .u = CreateUnit(.owner, DUMMY_ID, .x, .y, 0)
the s. is wrong here, it shold be this or nothing.

The usage of multiple different effect strings is a good idea, and it really fits the theme well!
 
Level 22
Joined
Feb 6, 2014
Messages
2,466
There could be a MAX_HEIGHT for texttags.
I've set the delay to a bigger number for fun, and it looked weird,
because the texttag just never stops in rising upward.

The texttag looks neat, but it would be a bit user-friendlier if it would not necesaarily create them.
Doing them optional would be optimal.
Got it, my mistake in the TextTagVelocity equation, it should be divided by .delay not multiplied.

In this resource it would make perfec sense to me if the dummy has some sight radius vision. Maybe ~500 (?), idk exactly
Because otherwise you would not even see the firework if no other unit is next to it.
And then this would also mean that the owner of recyled missle need to be set to .caster onCast method.
Yeah, makes sense

set .u = CreateUnit(s.owner, DUMMY_ID, .x, .y, 0)
->
set .u = CreateUnit(.owner, DUMMY_ID, .x, .y, 0)
the s. is wrong here, it shold be this or nothing.
You're right.
 
Good way you are using with solving the text height problem.
All looks good.

It doesn't really matter, but I just want to catch your attention that you might
have forgotten to remove testing conditions for the activation delay:
JASS:
private function SpellActivationDelay takes integer lvl returns real
    return 5.0//-lvl*0.25 + 3.25 //3, 2.75, 2.5
endfunction
The 5 was probably for testing, and the outcommented formula the defaul one.
Anyways, as mentioned it doesn't matter at all.

Approved.
 
Top