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

Armageddon v.2.4.1

  • Like
Reactions: Lord Alyx

Armageddon


This terrible force of nature's vengeance rains down flaming stones around the caster,
pummeling any opponents foolish enough to be caught in its fury.
©Blizzard Entertainment, Diablo II.



Obligatory RequirementsOptional Requirements


Armageddon uses library Missile to create each single Meteor, hence you have full access to the Missile API.
Please go without setting an acceleration, as it probably breaks the speed calculation for meteors.
Physiks for the free fall may please your inner self, but doesn't change the ingame experience for the end user.


Armageddon

JASS:
scope Armageddon initializer Init /* v2.4.1
*************************************************************************************
*
*   This terrible force of nature's vengeance rains down flaming stones around the caster,
*   pummeling any opponents foolish enough to be caught in its fury.
*   ©Blizzard Entertainment, Diablo II
*
*************************************************************************************
*
*   Requires Missile, TimerUtils, SpellEffectEvent and RegisterPlayerUnitEvent.
*       - Credits to Bribe, Magtheridon96 and Vexorian
*   
*   Optionally IsDestructableTree to run an onDestructable collision.
*
*************************************************************************************/
//**
//*  User settings:
//*  ==============
    // One declaration of native "UnitAlive" per map script is enough.
    native UnitAlive takes unit id returns boolean

    globals
        private constant integer ARMAGEDDON_ABILITY    = 'A001'
        
        // Damage options.
        private constant attacktype ATTACK_TYPE        = ATTACK_TYPE_NORMAL
        private constant damagetype DAMAGE_TYPE        = DAMAGE_TYPE_FIRE
        
        // Missile constants.
        private constant real METEOR_START_HEIGHT      = 1300.
        
        // Effect options.
                                // EVENT_PLAYER_UNIT_SPELL_EFFECT.
        private constant string ON_EFFECT_EVENT_FX     = "Abilities\\Spells\\Human\\MarkOfChaos\\MarkOfChaosTarget.mdl"
        private constant string ON_CASTER_FX           = "Abilities\\Spells\\Human\\FlameStrike\\FlameStrikeTarget.mdl"
        private constant string CASTER_FX_ATTACH_POINT = "origin"
        private constant string ON_DAMAGE_UNIT_FX      = "Abilities\\Weapons\\FireBallMissile\\FireBallMissile.mdl"
        private constant string DAMAGE_FX_ATTACH_POINT = "chest"
        
        // Spell concept.
                                 // EVENT_PLAYER_UNIT_ISSUED_ORDER or EVENT_PLAYER_UNIT_ISSUED_POINT_ORDER.
        private constant boolean IS_IMMEDIATE_ORDER    = true
        private constant boolean IMPACT_DAMAGES_TREES  = true// Requires library IsDestructableTree.
        private constant boolean DAMAGE_FLYING_UNITS   = true// Requires USE_COLLISION_Z_FILTER = true in library Missile.
    endglobals
    
    // Set if the spell moves along with the caster. 
    private function IsSpellMovingWithCaster takes unit caster, integer level returns boolean
        return true
    endfunction
    
    // Set if the caster has to channel the spell.
    // In case the spell is channeling, make sure the "follow through time" field of the ability fits the spell duration.
    private function IsSpellChanneling takes unit caster, integer level returns boolean
        return true
    endfunction
    
    // Set how many meteor should be created per second. 
    // This function is re-evaluated each second, therefore a GetRandomInt() also makes sense here.
    private constant function GetMeteorsPerSecond takes integer level returns integer
        return 1 + (2*level)
    endfunction
    
    // Set the total spell duration.
    private constant function GetSpellDuration takes integer level returns real
        return 6. + (2*level)
    endfunction
    
    // Set the maximum field size in which effects take place.
    private constant function GetSpellFieldSize takes integer level returns real
        return 500. + (50*level)
    endfunction
    
    // Set the expected fly time per meteor. 
    private function GetMeteorFlyTime takes integer level returns real
        return GetRandomReal(2., 3)
    endfunction
    
    // Filter valid target units. Runs on unit collision.
    // Do NOT filter for flying units. This is done internally inside the spell script.
    private function FilterUnits takes unit target, player p returns boolean
        return UnitAlive(target) and IsUnitEnemy(target, p)
    endfunction
    
    // Runs each time before a new meteor is created.
    private function GlobalCasterCondition takes unit caster returns boolean
        return UnitAlive(caster)
    endfunction
    
    // Customize all missile members to your needs. "speed", "source", "owner" and "acceleration" are reserved and will be overriden.
    private function CustomizeMeteor takes Missile missile, unit caster, integer level returns nothing
        local real scale = GetRandomReal(.8, 1.3)
        set missile.scale = scale
        set missile.damage = 55. + 20.*level*scale
        set missile.collision = 96.*scale
        set missile.collisionZ = 96.*scale// Only required for if you want to damage flying units in mid air.
        set missile.model = "Abilities\\Spells\\Other\\Volcano\\VolcanoMissile.mdl"
    endfunction
    
    // You may delete the following content and return null for no sound.
    private function GetSoundHandle takes nothing returns sound
        local string file = "Abilities\\Spells\\Demon\\RainOfFire\\RainOfFireLoop1.wav"
        local sound snd = CreateSound(file, true, true, true, 10, 10, "")
        call SetSoundChannel(snd, 5)
        call SetSoundVolume(snd, 127)
        call SetSoundDistances(snd, 600, 10000)
        call SetSoundDistanceCutoff(snd, 3000)
        call SetSoundConeAngles(snd, 0, 0, 127)
        call SetSoundConeOrientation(snd, 0, 0, 0)
        call StartSound(snd)
        set bj_lastPlayedSound = snd
        set snd = null
        return bj_lastPlayedSound// null
    endfunction

//========================================================================
// Armageddon code. Make changes carefully.
//========================================================================
    
    // Uses Missile's API.
    private struct Meteor extends array
        
        // Runs on any destructable collision. You'll need IsDestructableTree.
        static if LIBRARY_IsDestructableTree and IMPACT_DAMAGES_TREES then
            static method onDestructable takes Missile missile, destructable hit returns boolean
                if IsTreeAlive(hit) then
                    if (missile.damage > (GetWidgetLife(hit) + .405)) then
                        call KillDestructable(hit)
                    else
                        call SetWidgetLife(hit, GetWidgetLife(hit) - missile.damage)
                        call SetDestructableAnimation(hit, "stand hit")
                    endif
                endif
                return false
            endmethod
        endif
        
        // Runs on any unit collision. Written to hit flying units in mid air.
        // Requires Missile_USE_COLLISION_Z_FILTER = true.
        static if Missile_USE_COLLISION_Z_FILTER and DAMAGE_FLYING_UNITS then
            static method onCollide takes Missile missile, unit hit returns boolean
                if IsUnitType(hit, UNIT_TYPE_FLYING) and FilterUnits(hit, missile.owner) then
                    if UnitDamageTarget(missile.source, hit, missile.damage, false, false, ATTACK_TYPE, DAMAGE_TYPE, null) then
                        call DestroyEffect(AddSpecialEffectTarget(ON_DAMAGE_UNIT_FX, hit, DAMAGE_FX_ATTACH_POINT))
                    endif
                endif
                return false
            endmethod
        endif
        
        // Runs when a missile launched from the Armageddon struct is deallocated. 
        // Written to hit non flying units in collision range.
        static method onRemove takes Missile missile returns boolean
            local unit u
            call GroupEnumUnitsInRange(bj_lastCreatedGroup, missile.x, missile.y, missile.collision + Missile_MAXIMUM_COLLISION_SIZE, null)
            loop
                set u = FirstOfGroup(bj_lastCreatedGroup)
                exitwhen u == null
                call GroupRemoveUnit(bj_lastCreatedGroup, u)
                if IsUnitInRange(missile.dummy, u, missile.collision) and not IsUnitType(u, UNIT_TYPE_FLYING) and FilterUnits(u, missile.owner) then
                    if UnitDamageTarget(missile.source, u, missile.damage, false, false, ATTACK_TYPE, DAMAGE_TYPE, null) then
                        call DestroyEffect(AddSpecialEffectTarget(ON_DAMAGE_UNIT_FX, u, DAMAGE_FX_ATTACH_POINT))
                    endif
                endif
            endloop
            return true
        endmethod
        
        // Enables the Missile interface for this struct.
        implement MissileStruct
    endstruct

    private function Random takes nothing returns real
        return GetRandomReal(0., 1.)
    endfunction
    
    private function GetRandomRange takes real radius returns real
        local real r = Random() + Random()
        if r > 1. then 
            return (2 - r)*radius
        endif
        return r*radius
    endfunction
    
    private function CreateMeteor takes unit source, player owner, real centerX, real centerY, real maxRange, integer level returns nothing
        // Get point of creation.
        local real theta = 2*bj_PI*Random()
        local real radius = GetRandomRange(maxRange)
        local real posX = centerX + radius*Cos(theta)
        local real posY = centerY + radius*Sin(theta)
        
        // Get the fly angle and maximum distance towards it.
        local real angle = 2*bj_PI*Random()
        local real maxX = centerX + maxRange*Cos(angle)
        local real maxY = centerY + maxRange*Sin(angle)
        
        // Get the maximum distance without leaving the field.
        local real dX = posX - maxX
        local real dY = posY - maxY                                                    // Between 0 and max distance.
        local Missile missile = Missile.create(posX, posY, METEOR_START_HEIGHT, angle, SquareRoot(dX*dX + dY*dY)*Random(), 0.)
        
        // Allow user customization.
        call CustomizeMeteor(missile, source, level)
        // Set or override important missile fields.
        set missile.source = source
        set missile.owner = owner
        set missile.acceleration = 0.
        call missile.flightTime2Speed(GetMeteorFlyTime(level))
        call Meteor.launch(missile)
    endfunction
    
    private struct Armageddon// extends array
        // implement Alloc
        //
        // Members.
        unit    source
        player  user
        timer   clock
        effect  fx 
        sound   snd
        //
        real    centerX
        real    centerY
        real    size
        real    time
        real    interval
        integer count
        integer order
        integer level
        // Spell concept members.
        boolean move
        boolean channel
        
        method clear takes nothing returns nothing            
            if snd != null then
                call StopSound(snd, true, true)
                set snd = null
            endif
            call DestroyEffect(fx)
            call ReleaseTimer(clock)
            call deallocate()
            
            set clock = null
            set source = null
            set user = null
            set fx = null
        endmethod
        
        static method onPeriodic takes nothing returns nothing
            local thistype this = GetTimerData(GetExpiredTimer())
            local boolean fire = (count > 0)
            // Evaluate global conditions.
            if not GlobalCasterCondition(source) or time <= 0. or (channel and (order != GetUnitCurrentOrder(source))) then
                call clear()
            else
                // Update time members.
                set time  = time - interval
                set count = count - 1
                if count <= 0 then
                    set count = GetMeteorsPerSecond(level)
                    set interval = 1./IMaxBJ(1, count)
                    call TimerStart(clock, interval, true, function thistype.onPeriodic)
                endif
                // Update the center position.
                if move then 
                    set centerX = GetUnitX(source)
                    set centerY = GetUnitY(source)
                    if (snd != null) then
                        call SetSoundPosition(snd, centerX, centerY, 0.)
                    endif
                endif
                if fire then
                    // Create a new meteor.
                    call CreateMeteor(source, user, centerX, centerY, size, level)
                endif
            endif
        endmethod
    endstruct
    
                     // EVENT_PLAYER_UNIT_SPELL_EFFECT.
    private function OnEffect takes nothing returns nothing
        local unit source = GetTriggerUnit()
        local integer level = GetUnitAbilityLevel(source, ARMAGEDDON_ABILITY) 
        local Armageddon dex = Armageddon.create()
    
        set dex.source  = source 
        set dex.level   = level
        set dex.clock   = NewTimerEx(dex)
        set dex.user    = GetTriggerPlayer()
        set dex.order   = GetUnitCurrentOrder(source)
        set dex.time    = GetSpellDuration(level)
        set dex.count   = GetMeteorsPerSecond(level)
        set dex.move    = IsSpellMovingWithCaster(source, level)
        set dex.size    = GetSpellFieldSize(level)*.5
        set dex.channel = IsSpellChanneling(source, level)
        
        static if IS_IMMEDIATE_ORDER then
            set dex.centerX = GetUnitX(source)
            set dex.centerY = GetUnitY(source)
        else
            set dex.centerX = GetSpellTargetX()
            set dex.centerY = GetSpellTargetY()
        endif
        // Run effects.
        set dex.snd = GetSoundHandle()
        if dex.snd != null then
            call SetSoundPosition(dex.snd, dex.centerX, dex.centerY, 0.)
        endif
        call DestroyEffect(AddSpecialEffect(ON_EFFECT_EVENT_FX, dex.centerX, dex.centerY))
        set dex.fx = AddSpecialEffectTarget(ON_CASTER_FX, source, CASTER_FX_ATTACH_POINT)
        // Start the spell.
        set dex.interval = 1./IMaxBJ(1, dex.count)
        call TimerStart(dex.clock, dex.interval, true, function Armageddon.onPeriodic)
        
        set source = null
    endfunction
    
    private function Init takes nothing returns nothing
        call RegisterSpellEffectEvent(ARMAGEDDON_ABILITY, function OnEffect)
    endfunction
endscope

Keywords:
Fire, Armageddon, Firestorm
Contents

Armageddon (Map)

Reviews
Armageddon v1.0.0.0 | Reviewed by Maker | 16.01.14 Concept[/COLOR]] The spell is like a non channeled Starfall and the area of effect follows the caster It might not be that original but it looks good and works well Somehow it lacks something...

Moderator

M

Moderator


Armageddon v1.0.0.0 | Reviewed by Maker | 16.01.14

[COLOR="gray"

[COLOR="gray"

[COLOR="gray"

[COLOR="gray"

[COLOR="gray"

Concept[/COLOR]]
126248-albums6177-picture66521.png
The spell is like a non channeled Starfall and the area of effect follows the caster
It might not be that original but it looks good
and works well
Somehow it lacks something that would really make it live up to its name
Triggers[/COLOR]]
126248-albums6177-picture66521.png
You did a great job with the triggering
Objects[/COLOR]]
126248-albums6177-picture66521.png
No complaints here, everything is as it should be
Effects[/COLOR]]
126248-albums6177-picture66521.png
The effects are what one could expect from a spell with this theme
Rating[/COLOR]]
CONCEPTTRIGGERSOBJECTSEFFECTSRATINGSTATUS
126248-albums6177-picture75358.jpg
126248-albums6177-picture75360.jpg
126248-albums6177-picture75360.jpg
126248-albums6177-picture75359.jpg
126248-albums6177-picture75359.jpg
APPROVED
 
Level 18
Joined
Sep 14, 2012
Messages
3,413
I'll say the same thing like the last thread :
Use constant function \o/ !

You're double privating :
JASS:
    private struct Main extends array
        private static constant real TIMEOUT = 0.031250000//Timer timeout
        private static constant real TWO_PI  = 6.283185307//Create new meteors in any potential angle
That's not needed ;)

Otherwise the code looks good even if it needs tons of things xD!
I'll see the map later and rate :)
 

Cokemonkey11

Code Reviewer
Level 29
Joined
May 9, 2006
Messages
3,516
But in the long run Table, RegisterPlayerUnitEvent, UnitIndexer, WorldBounds and maybe even CTL can be found in nearly any map, which is not completly written in GUI.

I literally don't use any of those except Table, and that's not even the Table you're thinking of...

What is with all of the private constant functions...

Simply declare it in the constant variable in the scope.

No reason to add extra functions.

That's just like, your opinion, man
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
When extending array Alloc is needed. The optional thing for that will break the script when it is run if you do not have Alloc
:grin:
JASS:
            static if (thistype.allocate.exists) then
                local thistype this = allocate()
            else
                local thistype this = rn[0]
                if this == 0 then
                    set ic = ic + 1
                    set this = ic
                else
                    set rn[0] = rn[this]
                endif
            endif
I literally don't use any of those except Table, and that's not even the Table you're thinking of...

Yes I know, but I guess you don't CnP spell resources into your maps, but rather re-write them into you own coding-style.

Edit:
What is with all of the private constant functions...
Because it offers you more capabilities. For instance damage calculation --> BASE_AMOUNT + LEVEL_INCREASEMENT in a global block

while within the function you can do 20 + level*10 + GetHeroStat(caster, strenght) + ...
 
Level 29
Joined
Oct 24, 2012
Messages
6,543
:grin:
JASS:
            static if (thistype.allocate.exists) then
                local thistype this = allocate()
            else
                local thistype this = rn[0]
                if this == 0 then
                    set ic = ic + 1
                    set this = ic
                else
                    set rn[0] = rn[this]
                endif
            endif


Yes I know, but I guess you don't CnP spell resources into your maps, but rather re-write them to you own style.

oops i must be tired lol.

@Cokemonkey11
Having constants like that looks tacky to me.
Why create extra functions when they are not needed ?
 

Cokemonkey11

Code Reviewer
Level 29
Joined
May 9, 2006
Messages
3,516
@Cokemonkey11
Having constants like that looks tacky to me.
Why create extra functions when they are not needed ?

It was a design preference. I also don't like them, but since we're not the designers, our preferences have no place in the matter.

When you program other languages the number of design preferences and semantically equivalent constructs will increase exponentially.

I don't like if ( s > 5 ) then and prefer if s>5 then but I don't tell everyone to mimic my whitespace.

On the other hand, if (! a) and (! b) then can be simplified with demorgan's laws to if ! (a or b) then, so you might have a reason to say something in that case.

constant functions are inlined the same way as constant fields so there is no reason to complain about the difference
 
But you do a static check for GROUND_LEVEL and it should be dynamic on every position of the map. Create a hill somewhere and spawn meteors. Instead of exploding at top it waits until it would normally reach the other ground level.

JASS:
                        if (cur.z < GROUND_LEVEL) then//Check if the meteor hit the ground
                            call GroupEnumUnitsInRange(enu, GetUnitX(cur.dum.unit), GetUnitY(cur.dum.unit), cur.aoe, null)
                            loop
                                set u = FirstOfGroup(enu)
                                exitwhen u == null
                                call GroupRemoveUnit(enu, u)
                       
                                if TargetFilter(.owner, u) then
                                    call UnitDamageTarget(.cast, u, .dmg, false, false, ATTACK_TYPE, DAMAGE_TYPE, null)
                                endif
                            endloop
                            /*
                            *   We have to use a little trick to display the full
                            *   animation of the meteor, because Dummy does
                            *   call SetUnitPosition(dummies[this], 2147483647, 2147483647) on destroy.
                            *   That's why we create a little waiting time, before recycling the dummy.
                            *   By checking if cur.model == null.
                            */
                            call DestroyEffect(cur.model)
                            set cur.model = null
                            set cur.z = 0//Reuse cur.z for the METEOR_EX_TIME check
                        endif
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
Just did you are wrong, because I create each meteor on the ground and then put them 1300 units in the air, relative to the position it has been created.

JASS:
set .z = METEOR_HEIGHT
set .dum = Dummy.create(x + field*Cos(angle), y + field*Sin(angle), 0)
call SetUnitScale(.dum.unit, scale, 1, 1)
set .model = AddSpecialEffectTarget(METEOR_MODEL, .dum.unit, "origin")
call SetUnitFlyHeight(.dum.unit, .z, 0)

In case you do platform deformations during the cast it will not work properly.

Edit: I think it works with platform deformations during the cast (untested), but if you raise the terrain while a meteor is falling it will stop falling for one loop or even move upwards. The explosion though should be displayed correctly.
 
Last edited:
Level 19
Joined
Mar 18, 2012
Messages
1,716
Table can most likely be found in every map which uses vJass resources and so is CTL.
Dummy is a personal preference over Missle and XE not just because of the sorting mechanics dummy has. In my opinion it also has a better API.
The CreateUnit native is a rather heavy operation, hence reducing it to a minumum is neat. However the avoided leak is negligible in the current patch version.

All other resources are optional.

The spell uses a lot of missles, which can influence the game experience by dropping the fps. Especially for those who run wc3 barely above requirements.
The more effects your resource uses the better the code should be. Optimization to a certain extend is something I really recommend.
That is the major reason those extra resources are required.
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
Yeah for Dummy that is true. Sometimes I get the feeling I'm the only one using it.
Lately submissions follow the trend to run autonomous, which I agree is neat if you consider it as single resource.
When your map has 50 of those spells that might be different.
In general I guess the only two commonly used resources are TimerUtils and Table (Vexorians?).

The spell is well outlined and it should be easy to re-code it to ones needs, if you really dislike one of the requirements.
On the other side I think many people simply don't know how to use i.e CTL and now they have another nice illustration for proper usage. :thumbs_up:
 
Level 18
Joined
Sep 14, 2012
Messages
3,413
Yeah Dummy isn't used ^^
I only use those resources when I code :
- Table by Vex
- My Fear System (just when needed)
- BoundSentinel by Vex
And I think I'm already done ;)
Things that I might use if needed :
- GetUnitZ
- IsTerrainPathable
- IsDestructableTree

Anyway you're right that this code show how to use CTL even if I don't get the purpose of this like the purpose of Alloc :/
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
I don't get the purpose of this like the purpose of Alloc :/
Alloc just makes your life easy, because it is so fast to type --> implement Alloc
Like the textmacro you used in your fear explosion, it is for lazy people ^^.
Also the allocation is very optimized by using just one integer array "recycle" and no static integer "ic".
In theory it has bonus debug features to calculate used memory, but I never used that one.

Easy spoken, CTL is a linked list using one timer. It only runs if you have an active instance.
It registers the functions via TriggerCondition to one global trigger and fires them when the timer expires.
When you destroy the instance the TriggerCondition is removed. When the no instances are active
the timer is stopped. I might missed something, because the CTL library is already optimized to variables with 1 or 2 chars.
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
Changelog from v.1.0 to v.2.0

  • Now uses Missile, hence enables Missile API for falling Meteors.
  • Position and fly angle are now properly randomized.
  • Meteors do no longer fall vertical, but can now have a variety of flying angles.
  • Implemented optional destructable ( tree ) collision.
  • Implemented more options to customize the spell ( channeling, moving, .. )
  • More effects added for better in game experience.
  • Changed documentation style.
Changelog from v.2.0 to v.2.1
  • Updated Missile inside the demo map to version 2.0.2
  • Armageddon can now hit flying units if they collide in their fly height with a Meteor. ( z - axis collision )
    This is an optional toogle and requires DAMAGE_FLYING_UNITS = true in the Armageddon configuration.
Changelog from v.2.1 to v.2.2
  • Updated Missile inside the demo map to version 2.0.2.1
  • Spiced up the spell with a sound file.
  • Improved documentation.
  • Does no longer crash the thread when 0 is returned for meteors per seconds.
  • Improved collision detection onRemove.
Changelog from v.2.2 to v.2.3
  • Updated Missile inside the demo map to version 2.0.2.
  • Passing 0 for MeteorsPerSecond now creates 0 missiles for that 1 second interval. ( Before it was still 1 )
Changelog from v.2.3 to v.2.4
  • The on damage fx now only spawns, if the target unit was actually damaged.
  • Fixed a very rare case, where GetRandomReal could recieve a higher min than max bound argument.
Changelog 2.4.1
  • Improved the way how meteors spawn.
 
Last edited:
Top