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

PurplePoot - Prismatic Spray

My entry for Zephyr Contest #15 - Model-based. Uses Direfury's Gnome Arcanist and Vexorian's dummy.mdx and TimerUtils.

To import this, simply copy+paste the object data into your map and then change the appropriate configuration variables (every four-digit code between single quotes, such as 'A000'). You may also wish to read the more general spell importing instructions.

Main code

Configuration

Rays

Geometry


JASS:
scope PrismaticSpray initializer init
globals
    private constant hashtable timers = InitHashtable()
endglobals

private function CastSpell takes unit caster, unit target, integer level, integer spell, integer order returns nothing
    local unit dumm = CreateUnit(GetOwningPlayer(caster),PrismaticSprayConfig_DUMMY,GetUnitX(caster),GetUnitY(caster),Atan2(GetUnitY(target)-GetUnitY(caster),GetUnitX(target)-GetUnitX(caster))*bj_RADTODEG)
    call UnitApplyTimedLife(dumm,'BTLF',5.)
    call SetUnitExploded(dumm,true)
    call UnitAddAbility(dumm,spell)
    call SetUnitAbilityLevel(dumm,spell,level)
    call IssueTargetOrderById(dumm,order,target)
    set dumm = null
endfunction

private function RayOfFrostInitial takes unit source, unit target, integer level returns nothing
    if IsPlayerEnemy(GetOwningPlayer(target),GetOwningPlayer(source)) then
        call CastSpell(source,target,level,PrismaticSprayConfig_RAY_OF_FROST_SLOW_ID,PrismaticSprayConfig_RAY_OF_FROST_SLOW_ORDER_ID)
    endif
endfunction
private function RayOfFrostOngoing takes unit source, unit target, integer level returns nothing
    if IsPlayerEnemy(GetOwningPlayer(target),GetOwningPlayer(source)) then
        call UnitDamageTarget(source,target,PrismaticSprayConfig_RayOfFrostDamageTick(level),false,true,ATTACK_TYPE_NORMAL,DAMAGE_TYPE_NORMAL,WEAPON_TYPE_WHOKNOWS)
    endif
endfunction

private function RayOfFireInitial takes unit source, unit target, integer level returns nothing
    if IsPlayerEnemy(GetOwningPlayer(target),GetOwningPlayer(source)) then
        call CastSpell(source,target,level,PrismaticSprayConfig_RAY_OF_FIRE_BURN_ID,PrismaticSprayConfig_RAY_OF_FIRE_BURN_ORDER_ID)
    endif
endfunction
private function RayOfFireOngoing takes unit source, unit target, integer level returns nothing
    if IsPlayerEnemy(GetOwningPlayer(target),GetOwningPlayer(source)) then
        call UnitDamageTarget(source,target,PrismaticSprayConfig_RayOfFireDamageTick(level),false,true,ATTACK_TYPE_NORMAL,DAMAGE_TYPE_NORMAL,WEAPON_TYPE_WHOKNOWS)
    endif
endfunction

private function HolyRayOngoing takes unit source, unit target, integer level returns nothing
    if IsPlayerAlly(GetOwningPlayer(target),GetOwningPlayer(source)) then
        call SetWidgetLife(target,GetWidgetLife(target) + PrismaticSprayConfig_HolyRayHealingTick(level))
    endif
endfunction

private function DarkRayInitial takes unit source, unit target, integer level returns nothing
    if IsPlayerEnemy(GetOwningPlayer(target),GetOwningPlayer(source)) then
        if GetWidgetLife(target) < PrismaticSprayConfig_DARK_RAY_KILL_THRESHOLD then
            call UnitDamageTarget(source,target,99999999.,false,true,ATTACK_TYPE_NORMAL,DAMAGE_TYPE_NORMAL,WEAPON_TYPE_WHOKNOWS)
        else
            call CastSpell(source,target,level,PrismaticSprayConfig_DARK_RAY_STUN_ID,PrismaticSprayConfig_DARK_RAY_STUN_ORDER_ID)
        endif
    endif
endfunction

private function ArcaneRayInitial takes unit source, unit target, integer level returns nothing
    if IsPlayerEnemy(GetOwningPlayer(target),GetOwningPlayer(source)) then
        call CastSpell(source,target,level,PrismaticSprayConfig_ARCANE_RAY_SLOW_ID,PrismaticSprayConfig_ARCANE_RAY_SLOW_ORDER_ID)
    elseif IsPlayerAlly(GetOwningPlayer(target),GetOwningPlayer(source)) then
        call CastSpell(source,target,level,PrismaticSprayConfig_ARCANE_RAY_HASTE_ID,PrismaticSprayConfig_ARCANE_RAY_HASTE_ORDER_ID)
    endif
endfunction

private struct Data
    unit caster
    integer level
    real angle
    boolean roundEnd
    method onDestroy takes nothing returns nothing
        set this.caster = null
    endmethod
endstruct

private function ShootRays takes Data dat returns nothing
    local real sourcex = GetUnitX(dat.caster) + PrismaticSprayConfig_EFFECT_POINT_XY_FORWARD * Cos(dat.angle) + PrismaticSprayConfig_EFFECT_POINT_XY_PERP * Cos(dat.angle + bj_PI/2.)
    local real sourcey = GetUnitY(dat.caster) + PrismaticSprayConfig_EFFECT_POINT_XY_FORWARD * Sin(dat.angle) + PrismaticSprayConfig_EFFECT_POINT_XY_PERP * Sin(dat.angle + bj_PI/2.)
    local integer numRays = PrismaticSprayConfig_RaysPerLevel(dat.level)
    local real angle = dat.angle - PrismaticSprayConfig_RAY_EFFECT_ARC/2.
    local real angleIncrement = PrismaticSprayConfig_RAY_EFFECT_ARC/I2R(numRays - 1)
    local integer rayType
    local PrismaticSprayRays_RayEffect initialEffect
    local PrismaticSprayRays_RayEffect ongoingEffect
    local string sfx
    loop
        exitwhen numRays == 0
        set rayType = GetRandomInt(0,4)
        if rayType == 0 then //Fire
            set initialEffect = RayOfFireInitial
            set ongoingEffect = RayOfFireOngoing
            set sfx = PrismaticSprayConfig_RAY_OF_FIRE_EFFECT
        elseif rayType == 1 then //Frost
            set initialEffect = RayOfFrostInitial
            set ongoingEffect = RayOfFireOngoing
            set sfx = PrismaticSprayConfig_RAY_OF_FROST_EFFECT
        elseif rayType == 2 then //Holy
            set initialEffect = PrismaticSprayRays_NoInitialEffect
            set ongoingEffect = HolyRayOngoing
            set sfx = PrismaticSprayConfig_HOLY_RAY_EFFECT
        elseif rayType == 3 then //Dark
            set initialEffect = DarkRayInitial
            set ongoingEffect = PrismaticSprayRays_NoOngoingEffect
            set sfx = PrismaticSprayConfig_DARK_RAY_EFFECT
        elseif rayType == 4 then //Arcane
            set initialEffect = ArcaneRayInitial
            set ongoingEffect = PrismaticSprayRays_NoOngoingEffect
            set sfx = PrismaticSprayConfig_ARCANE_RAY_EFFECT
        endif
        call FireRay(dat.caster,sfx,sourcex,sourcey,angle,PrismaticSprayConfig_EFFECT_POINT_Z,dat.level,initialEffect,ongoingEffect)
        set numRays = numRays - 1
        set angle = angle + angleIncrement
    endloop
endfunction

private function TimerExpires takes nothing returns nothing
    local timer t = GetExpiredTimer()
    local Data dat = GetTimerData(t)
    //Create rays if appropriate, then go to next iteration
    if dat.roundEnd then
        call TimerStart(t,PrismaticSprayConfig_EFFECT_POINT_TIME,false,function TimerExpires)
    else
        call ShootRays(dat)
        call TimerStart(t,PrismaticSprayConfig_EFFECT_END_TIME - PrismaticSprayConfig_EFFECT_POINT_TIME,false,function TimerExpires)
    endif
    set dat.roundEnd = not dat.roundEnd
    set t = null
endfunction

private function Actions takes nothing returns nothing
    local Data dat = Data.create()
    local timer t = NewTimer()
    local real x = GetUnitX(GetTriggerUnit())
    local real y = GetUnitY(GetTriggerUnit())
    set dat.caster = GetTriggerUnit()
    set dat.level = GetUnitAbilityLevel(dat.caster,GetSpellAbilityId())
    set dat.angle = Atan2(GetSpellTargetY() - y,GetSpellTargetX() - x)
    set dat.roundEnd = false
    call SaveTimerHandle(timers,GetHandleId(dat.caster),0,t)
    call SetTimerData(t,dat)
    call TimerStart(t,PrismaticSprayConfig_EFFECT_POINT_TIME,false,function TimerExpires)
    set t = null
endfunction

private function EndActions takes nothing returns nothing
    local timer t = LoadTimerHandle(timers,GetHandleId(GetTriggerUnit()),0)
    local Data dat = GetTimerData(t)
    call FlushChildHashtable(timers,GetHandleId(GetTriggerUnit()))
    call dat.destroy()
    call PauseTimer(t)
    call ReleaseTimer(t)
    set t = null
endfunction

private function Conditions takes nothing returns boolean
    return GetSpellAbilityId() == PrismaticSprayConfig_ABILITY_ID
endfunction

private function init takes nothing returns nothing
    local trigger t = CreateTrigger()
    call TriggerRegisterAnyUnitEventBJ(t,EVENT_PLAYER_UNIT_SPELL_EFFECT)
    call TriggerAddCondition(t,Condition(function Conditions))
    call TriggerAddAction(t,function Actions)
 
    set t = CreateTrigger()
    call TriggerRegisterAnyUnitEventBJ(t,EVENT_PLAYER_UNIT_SPELL_FINISH)
    call TriggerRegisterAnyUnitEventBJ(t,EVENT_PLAYER_UNIT_SPELL_ENDCAST)
    call TriggerAddCondition(t,Condition(function Conditions))
    call TriggerAddAction(t,function EndActions)
endfunction
endscope

JASS:
library PrismaticSprayConfig
//===========================================================================================
//=                                      Configuration                                      =
//===========================================================================================
globals
    public constant integer ABILITY_ID = 'A000'
    public constant integer DUMMY = 'e000'
 
    //The point after the beginning of the animation at which the rays should fire (animation-based)
    public constant real EFFECT_POINT_TIME = 0.45
    //The point after which each "round" of the spell ends
    //The spell duration should be a multiple of this
    public constant real EFFECT_END_TIME = 1.208
    //The offset towards the target from the centre of the hero where the effect should begin (model-based)
    public constant real EFFECT_POINT_XY_FORWARD = 75.
    //The offset perpendicular to the target from the centre of the model where the effect should begin (model-based)
    public constant real EFFECT_POINT_XY_PERP = 0.
    //The height at which the effect should begin (model-based)
    public constant real EFFECT_POINT_Z = 30.
 
    //The number of SFX displayed in a line per ray
    public constant integer RAY_EFFECT_COUNT = 15
    //The length of each ray
    public constant real RAY_EFFECT_LENGTH = 500.
    //The width of each ray's effect
    public constant real RAY_EFFECT_WIDTH = 100.
    //The elevation of each ray's special effects
    public constant real RAY_EFFECT_HEIGHT = 30.
    //The length of time that each ray applies its effect
    //.9s is appropriate for most sfx.
    public constant real RAY_EFFECT_DURATION = .9
    //Frequency at which ray effects are checked
    public constant real RAY_TIMER_TICK = .03
 
    //The size of the arc (in radians) in which rays are emitted
    public constant real RAY_EFFECT_ARC = bj_PI/2.
 
    //Enemies below this much health die instantly when hit by a dark ray.
    public constant real DARK_RAY_KILL_THRESHOLD = 200.
 
    //Special effect models
    public constant string RAY_OF_FIRE_EFFECT = "Abilities\\Weapons\\PhoenixMissile\\Phoenix_Missile_mini.mdl"
    public constant string RAY_OF_FROST_EFFECT = "Abilities\\Weapons\\ZigguratFrostMissile\\ZigguratFrostMissile.mdl"
    public constant string HOLY_RAY_EFFECT = "Abilities\\Weapons\\FaerieDragonMissile\\FaerieDragonMissile.mdl"
    public constant string DARK_RAY_EFFECT = "Abilities\\Weapons\\BansheeMissile\\BansheeMissile.mdl"
    public constant string ARCANE_RAY_EFFECT = "Abilities\\Weapons\\AvengerMissile\\AvengerMissile.mdl"
 
    //Spell IDs and OrderIDs
    public constant integer ARCANE_RAY_HASTE_ID  = 'A001'
    public constant integer ARCANE_RAY_SLOW_ID   = 'A002'
    public constant integer RAY_OF_FROST_SLOW_ID = 'A003'
    public constant integer RAY_OF_FIRE_BURN_ID  = 'A004'
    public constant integer DARK_RAY_STUN_ID     = 'A005'
 
    public constant integer ARCANE_RAY_HASTE_ORDER_ID  = 852101
    public constant integer ARCANE_RAY_SLOW_ORDER_ID   = 852075
    public constant integer RAY_OF_FROST_SLOW_ORDER_ID = 852226
    public constant integer RAY_OF_FIRE_BURN_ORDER_ID  = 852662
    public constant integer DARK_RAY_STUN_ORDER_ID     = 852095
endglobals

//Level-based effects
public function RayOfFrostDamageTick takes integer level returns real
    return 2. + I2R(level)
endfunction
public function RayOfFireDamageTick takes integer level returns real
    return 3. + 2.*I2R(level)
endfunction
public function HolyRayHealingTick takes integer level returns real
    return 5. + 3.*I2R(level)
endfunction

//You can use GetRandomInt to randomize the number of rays somewhat
public function RaysPerLevel takes integer level returns integer
    return GetRandomInt(3,5) + level
endfunction

public function TargetFilter takes nothing returns boolean
    return GetWidgetLife(GetFilterUnit()) > 0. and not (IsUnitType(GetFilterUnit(),UNIT_TYPE_FLYING) or IsUnitType(GetFilterUnit(),UNIT_TYPE_STRUCTURE))
endfunction
endlibrary

JASS:
library PrismaticSprayRays requires PrismaticSprayGeometry, PrismaticSprayConfig
globals
    private constant hashtable rays     = InitHashtable()
    private constant timer     T        = CreateTimer()
    private constant group     G        = CreateGroup()
    private          integer   numRays  = 0
 
    private constant real      RADIUS   = 2.*RMaxBJ(PrismaticSprayConfig_RAY_EFFECT_LENGTH,PrismaticSprayConfig_RAY_EFFECT_WIDTH)
endglobals

public function interface RayEffect takes unit source, unit target, integer level returns nothing

public function NoInitialEffect takes unit source, unit target, integer level returns nothing
endfunction
public function NoOngoingEffect takes unit source, unit target, integer level returns nothing
endfunction

private struct RayStruct
    Rectangle rayRect
    unit owner
    real lifespan
    group targetedUnits
    integer level
    RayEffect callbackInitial
    RayEffect callbackOngoing
    static method create takes nothing returns thistype
        local thistype this = thistype.allocate()
        set this.targetedUnits = CreateGroup()
        return this
    endmethod
    method onDestroy takes nothing returns nothing
        call GroupClear(this.targetedUnits)
        call DestroyGroup(this.targetedUnits)
        set this.targetedUnits = null
        set this.owner = null
        call this.rayRect.destroy()
    endmethod
endstruct

private function RayTimerLoop takes nothing returns nothing
    local RayStruct current
    local integer i = 0
    local unit u
    loop
        exitwhen i == numRays
        set current = LoadInteger(rays,i,0)
        call GroupEnumUnitsInRange(G, current.rayRect.p1x, current.rayRect.p1y, RADIUS, Filter(function PrismaticSprayConfig_TargetFilter))
        loop
            set u = FirstOfGroup(G)
            exitwhen u == null
            if current.rayRect.containsPoint(GetUnitX(u),GetUnitY(u)) then
                if not IsUnitInGroup(u, current.targetedUnits) then
                    call current.callbackInitial.evaluate(current.owner,u,current.level)
                    call GroupAddUnit(current.targetedUnits, u)
                endif
                call current.callbackOngoing.evaluate(current.owner,u,current.level)
            endif
            call GroupRemoveUnit(G,u)
        endloop
        set current.lifespan = current.lifespan - PrismaticSprayConfig_RAY_TIMER_TICK
        if current.lifespan <= 0. then
            call current.destroy()
            call SaveInteger(rays,i,0,LoadInteger(rays,0,numRays - 1))
            set numRays = numRays - 1
            call FlushChildHashtable(rays,numRays)
        else
            set i = i + 1
        endif
    endloop
    if numRays == 0 then
        call PauseTimer(T)
    endif
endfunction

function FireRay takes unit owner, string sfx, real sourcex, real sourcey, real angle, real height, integer level, RayEffect initialEffect, RayEffect ongoingEffect returns nothing
    local RayStruct ray = RayStruct.create()
    set ray.rayRect = Rectangle.createFacing(sourcex,sourcey,PrismaticSprayConfig_RAY_EFFECT_WIDTH,PrismaticSprayConfig_RAY_EFFECT_LENGTH,angle)
    set ray.owner = owner
    set ray.callbackInitial = initialEffect
    set ray.callbackOngoing = ongoingEffect
    set ray.lifespan = PrismaticSprayConfig_RAY_EFFECT_DURATION
    set ray.level = level
    call DisplayEffectInLine(sfx,sourcex,sourcey,PrismaticSprayConfig_RAY_EFFECT_LENGTH,angle,PrismaticSprayConfig_RAY_EFFECT_HEIGHT,PrismaticSprayConfig_RAY_EFFECT_COUNT)
 
    call SaveInteger(rays,numRays,0,ray)
    set numRays = numRays + 1
    if numRays == 1 then
        call TimerStart(T,PrismaticSprayConfig_RAY_TIMER_TICK,true,function RayTimerLoop)
    endif
endfunction
endlibrary

JASS:
library PrismaticSprayGeometry requires PrismaticSprayConfig

struct Rectangle
    real p1x
    real p2x
    real p3x
    real p4x
    real p1y
    real p2y
    real p3y
    real p4y
 
    real x12diff
    real x23diff
    real x34diff
    real x41diff
    real y12diff
    real y23diff
    real y34diff
    real y41diff
 
    //1 and 3 (and also 2 and 4) are opposite corners
    static method create takes real x1, real y1, real x2, real y2, real x3, real y3, real x4, real y4 returns thistype
        local thistype this = thistype.allocate()
        set this.p1x = x1
        set this.p2x = x2
        set this.p3x = x3
        set this.p4x = x4
        set this.p1y = y1
        set this.p2y = y2
        set this.p3y = y3
        set this.p4y = y4
        set this.x12diff = x2 - x1
        set this.x23diff = x3 - x2
        set this.x34diff = x4 - x3
        set this.x41diff = x1 - x4
        set this.y12diff = y2 - y1
        set this.y23diff = y3 - y2
        set this.y34diff = y4 - y3
        set this.y41diff = y1 - y4
        return this
    endmethod
 
    static method createFacing takes real px, real py, real width, real length, real angle returns thistype
        local real perpAngle = angle + bj_PI/2.
        local real offsetCosP = width/2. * Cos(perpAngle)
        local real offsetSinP = width/2. * Sin(perpAngle)
        local real lenCos = length * Cos(angle)
        local real lenSin = length * Sin(angle)
        local real p1x = px - offsetCosP
        local real p1y = py - offsetSinP
        local real p2x = px + offsetCosP
        local real p2y = py + offsetSinP
        return thistype.create(p1x, p1y, p2x, p2y, p2x + lenCos, p2y + lenSin, p1x + lenCos, p1y + lenSin)
    endmethod
 
    method containsPoint takes real px, real py returns boolean
      return (py - this.p1y)*this.x12diff <= (px - this.p1x)*this.y12diff and (py - this.p2y)*this.x23diff <= (px - this.p2x)*this.y23diff and (py - this.p3y)*this.x34diff <= (px - this.p3x)*this.y34diff and (py - this.p4y)*this.x41diff <= (px - this.p4x)*this.y41diff
    endmethod
endstruct

//Requires Count >= 2
function DisplayEffectInLine takes string model, real px, real py, real distance, real angle, real height, integer count returns nothing
    local real dx = px + distance * Cos(angle)
    local real dy = py + distance * Sin(angle)
    local real dist = SquareRoot((dx - px)*(dx - px) + (dy - py)*(dy - py))
    local real vx = distance/I2R(count - 1) * Cos(angle)
    local real vy = distance/I2R(count - 1) * Sin(angle)
    local unit u
    loop
        set count = count - 1
        exitwhen count <= 0
        set u = CreateUnit(Player(15),PrismaticSprayConfig_DUMMY,px,py,270.)
        call UnitApplyTimedLife(u,'BTLF',2.)
        call SetUnitFlyHeight(u,height,0.)
        call SetUnitExploded(u,true)
        call DestroyEffect(AddSpecialEffectTarget(model,u,"origin"))
        set px = px + vx
        set py = py + vy
    endloop
    set u = null
endfunction
endlibrary
Contents

PurplePoot - Prismatic Spray (Map)

Reviews
KILLCIDE
I took a look through IcemanBo's judge review for your spell and did not notice anything game breaking, so I have no reason not to approve this resource. It would be great if you can fix the potential thread crash he mentioned though. Needs Fixed...
IcemanBo
^These two things would be good to be fixed.
It's pretty new, but now it's in rules in new contests having to submit the submission in the resource section, rather then only having it in the contets thread.
The major reason was that with that hive can directly ensure to have it in the resource data base, as it seems many people didn't submit their contest submissions by them selves here.
 
Level 22
Joined
Feb 6, 2014
Messages
2,466
It's pretty new, but now it's in rules in new contests having to submit the submission in the resource section, rather then only having it in the contets thread.
Ahh ok. I support this because some good contest entries (model, icon, spell, etc) are really good but not in the resource section so when I searched for something, it won't show because it's not in the resource section so I end up missing that good resource.

as it seems many people didn't submit their contest submissions by them selves here.
Maybe they did not want to? Maybe they did that for the purpose of the contest only?
 
  • The "EndActions" function does run twice under normal conidtions, once for "Finish Cast" event, and once for "End Cast" event. The result is wrong attempt to destroy and to deallocate structs, and the not required clear of hashtable entries.
    There must be a safety check to only run the removal operations if the (Triggering Unit) is ensured to be still caster of a prismatical spray. (-0.5)

  • In
    function ShootRays
    should be a tiny safety check in case user defines "1" as amount of rays , because we should not devide through "0" here in:
    PrismaticSprayConfig_RAY_EFFECT_ARC/I2R(numRays - 1)

    The result would be a thread crash.
^These two things would be good to be fixed.
 
Level 9
Joined
Sep 5, 2015
Messages
369
we just change the trigger pages contexts to fit our spells ID's, correct?

this:

//Spell IDs and OrderIDs
public constant integer ARCANE_RAY_HASTE_ID = 'A001'
public constant integer ARCANE_RAY_SLOW_ID = 'A002'
public constant integer RAY_OF_FROST_SLOW_ID = 'A003'
public constant integer RAY_OF_FIRE_BURN_ID = 'A004'
public constant integer DARK_RAY_STUN_ID = 'A005'
 
Last edited:
Top