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

Spiraling Trails

This bundle is marked as useful / simple. Simplicity is bliss, low effort and/or may contain minor bugs.
Spiraling Trails - casts magical stars that leave spiraling trails

JASS:
scope SpiralingTrails initializer init

globals
    private constant integer ABILITY_ID = 'A000'

    private constant integer DUMMY_ID = 'e000'
    private constant string DUMMY_MODEL = "Abilities\\Weapons\\FaerieDragonMissile\\FaerieDragonMissile.mdx"

    private constant real DT = 1.0 / 32.0

    private constant real Z_OFFSET = 80.0

    // the target point is considered reached if we are this many units away from it
    private constant real TARGET_DISTANCE_THRESHOLD = 16.0

    // if we are this many or less units from the terrain we stop
    private constant real TERRAIN_DISTANCE_THRESHOLD = 16.0

    // UnitDamageTarget arguments
    private constant boolean MELEE_ATTACK = false
    private constant boolean RANGE_ATTACK = true
    private constant attacktype ATTACK_TYPE = ATTACK_TYPE_NORMAL // spell
    private constant damagetype DAMAGE_TYPE = DAMAGE_TYPE_UNIVERSAL // ignore armor value
    private constant weapontype WEAPON_TYPE = WEAPON_TYPE_WHOKNOWS
endglobals

private function unit_filter takes unit u returns boolean
    // e.g: return false if the unit is invulnerable
    return true
endfunction

private function linear_speed takes integer level returns real
    return 500.0
endfunction
private function distance takes integer level returns real
    return 900.0
endfunction
private function dummy_scale takes integer level returns real
    return 0.5 + 0.5 * level
endfunction
private function spiral_radius takes integer level returns real
    return 64.0
endfunction
private function damage takes integer level returns real
    return 100.0 * level
endfunction


// NOTE: you can rewrite these functions to use your dummy recycling scripts!
globals
    private unit created_dummy
endglobals
private function dummy_create takes player p returns unit
    set created_dummy = CreateUnit(p, DUMMY_ID, 0.0, 0.0, 270.0)

    call UnitAddAbility(created_dummy, 'Amrf')
    call UnitRemoveAbility(created_dummy, 'Amrf')
    call UnitAddAbility(created_dummy, 'Aloc')
    call UnitRemoveAbility(created_dummy, 'Aloc')

    return created_dummy
endfunction
private function dummy_destroy takes unit u returns nothing
    call RemoveUnit(u)
endfunction


globals
    private location loc = Location(0.0, 0.0)
endglobals
private function GetTerrainZ takes real x, real y returns real
    call MoveLocation(loc, x, y)
    return GetLocationZ(loc)
endfunction
private function GetUnitZ takes unit u returns real
    call MoveLocation(loc, GetUnitX(u), GetUnitY(u))
    return GetLocationZ(loc) + GetUnitFlyHeight(u)
endfunction
private function SetUnitZ takes unit u, real z returns nothing
    call MoveLocation(loc, GetUnitX(u), GetUnitY(u))
    call SetUnitFlyHeight(u, z - GetLocationZ(loc), 0.0)
endfunction

native UnitAlive takes unit u returns boolean

globals
    private constant real TADT_POW_2 = TARGET_DISTANCE_THRESHOLD * TARGET_DISTANCE_THRESHOLD
    private constant real TEDT = TERRAIN_DISTANCE_THRESHOLD

    private real dx
    private real dy
    private real dz
    private real v_len

    private real ax
    private real ay
    private real az

    private group G = CreateGroup()
    private unit U
endglobals

private struct SpiralingTrail
    static timer tmr = CreateTimer()
    static thistype array update_list
    static integer update_count = 0

    player caster_owner
    unit caster
    unit dummy
    effect dummy_model
    real dummy_scale
    real damage

    // current position
    real px
    real py
    real pz

    // linear velocity
    real dpx
    real dpy
    real dpz

     // target point
    real tx
    real ty
    real tz

    real bvx_x
    real bvx_y
    real bvx_z

    real bvy_x
    real bvy_y
    real bvy_z

    real bvz_x
    real bvz_y
    real bvz_z

    real spiral_radius
    real ang_xy
    real ang_xz
    real ang_xz_inc

    real x
    real y
    real z

    static method create takes unit caster, string dummy_model, real px, real py, real pz, real tx, real ty, real tz, real ang_xz_initial, integer inc_sign, real speed, real scale, real p_spiral_radius, real p_damage returns thistype
        local thistype this = allocate()
        local real ang

        local real y_axis_deg
        local real bvz_ang_xy
        local real bvz_ang_xz

        set this.caster_owner = GetOwningPlayer(caster)
        set this.caster = caster
        set this.dummy = dummy_create(this.caster_owner)
        set this.dummy_model = AddSpecialEffectTarget(dummy_model, this.dummy, "origin")
        set this.dummy_scale = scale
        call SetUnitScale(this.dummy, scale, scale, scale)
        set this.damage = p_damage

        set this.px = px
        set this.py = py
        set this.pz = pz

        set this.tx = tx
        set this.ty = ty
        set this.tz = tz

        set dx = this.tx - this.px
        set dy = this.ty - this.py
        set dz = this.tz - this.pz
        set v_len = SquareRoot(dx * dx + dy * dy + dz * dz)
        set dx = dx / v_len
        set dy = dy / v_len
        set dz = dz / v_len

        set this.dpx = dx * speed * DT
        set this.dpy = dy * speed * DT
        set this.dpz = dz * speed * DT

        set ang = 2.0 * bj_PI - Atan2(dy, dx)
        set bvx_x = Cos(ang - bj_PI / 2.0)
        set bvx_y = Sin(ang - bj_PI / 2.0)
        set bvx_z = 0.0
        set bvy_x = Cos(ang)
        set bvy_y = Sin(ang)
        set bvy_z = 0.0
        set bvz_x = 0.0
        set bvz_y = 0.0
        set bvz_z = 1.0

        set this.ang_xy = 0.0
        set this.ang_xz = ang_xz_initial
        set this.spiral_radius = p_spiral_radius
        set this.ang_xz_inc = inc_sign * 2.0 * bj_PI / 15.0

        call SetUnitX(this.dummy, this.px)
        call SetUnitY(this.dummy, this.py)
        call SetUnitZ(this.dummy, this.pz)
        call SetUnitFacing(this.dummy, Atan2(dy, dx) * bj_RADTODEG)
        call SetUnitAnimationByIndex(this.dummy, R2I(Atan2(dz, SquareRoot(dx * dx  + dy * dy)) * bj_RADTODEG + 90.0))

        return this
    endmethod

    method destroy2 takes integer i returns nothing
        set update_list[i] = update_list[update_count]
        set update_count = update_count - 1

        call DestroyEffect(this.dummy_model)
        call dummy_destroy(this.dummy)
        set this.caster = null
        set this.dummy = null
        call this.destroy()
    endmethod

    static method move takes nothing returns nothing
        local integer i
        local thistype this
        local real terrain_z
        local boolean hit_unit

        set i = 1
        loop
            exitwhen i > update_count
            set this = update_list[i]

            set dx = this.tx - this.px
            set dy = this.ty - this.py
            set dz = this.tz - this.pz

            if dx * dx + dy * dy + dz * dz <= TADT_POW_2 then
                // we reached the target point
                //
                set this.px = this.tx
                set this.py = this.ty
                set this.pz = this.tz
                call SetUnitX(this.dummy, this.px)
                call SetUnitY(this.dummy, this.py)
                call SetUnitZ(this.dummy, this.pz)
                call this.destroy2(i)
                set i = i - 1

            else
                set this.px = this.px + this.dpx
                set this.py = this.py + this.dpy
                set this.pz = this.pz + this.dpz

                set ax = this.spiral_radius * Sin(this.ang_xz) * Cos(this.ang_xy)
                set ay = this.spiral_radius * Sin(this.ang_xz) * Sin(this.ang_xy)
                set az = this.spiral_radius * Cos(this.ang_xz)

                set this.x = this.px + this.bvx_x * ax + this.bvx_y * ay + this.bvx_z * az
                set this.y = this.py + this.bvy_x * ax + this.bvy_y * ay + this.bvy_z * az
                set this.z = this.pz + this.bvz_x * ax + this.bvz_y * ay + this.bvz_z * az

                call SetUnitX(this.dummy, this.x)
                call SetUnitY(this.dummy, this.y)
                call SetUnitZ(this.dummy, this.z)

                set this.ang_xz = this.ang_xz + this.ang_xz_inc

                // check for enemy units
                set hit_unit = false
                call GroupEnumUnitsInRange(G, this.x, this.y, this.dummy_scale * 64.0, null)
                loop
                    set U = FirstOfGroup(G)
                    exitwhen U == null
                    call GroupRemoveUnit(G, U)

                    if UnitAlive(U) and IsUnitEnemy(U, this.caster_owner) and not hit_unit and unit_filter(U) then
                        set hit_unit = true

                        if GetWidgetLife(U) - this.damage <= 0.405 then
                            call SetUnitExploded(U, true)
                            call UnitDamageTarget(this.caster, U, this.damage, MELEE_ATTACK, RANGE_ATTACK, ATTACK_TYPE, DAMAGE_TYPE, WEAPON_TYPE)
                        else
                            call UnitDamageTarget(this.caster, U, this.damage, MELEE_ATTACK, RANGE_ATTACK, ATTACK_TYPE, DAMAGE_TYPE, WEAPON_TYPE)
                        endif

                        call this.destroy2(i)
                        set i = i - 1

                        exitwhen true
                    endif
                endloop

                if not hit_unit then
                    // check for terrain/cliffs
                    call MoveLocation(loc, this.x, this.y)
                    set terrain_z = GetLocationZ(loc)
                    if this.pz - terrain_z <= TEDT then
                        call this.destroy2(i)
                        set i = i - 1
                    endif
                endif

            endif

            set i = i + 1
        endloop
    endmethod

    method start_moving takes nothing returns nothing
        set update_count = update_count + 1
        set update_list[update_count] = this

        if update_count == 1 then
            call TimerStart(tmr, DT, true, function thistype.move)
        endif
    endmethod
endstruct

private struct SpiralingTrails extends array

    static method cast takes nothing returns nothing
        local SpiralingTrail st
        local unit caster = GetTriggerUnit()
        local integer level = GetUnitAbilityLevel(caster, ABILITY_ID)
        local real dist = distance(level)
        local real speed = linear_speed(level)
        local real scale = dummy_scale(level)
        local real p_spiral_radius = spiral_radius(level)
        local real p_damage = damage(level)
        local real cx = GetUnitX(caster)
        local real cy = GetUnitY(caster)
        local real cz = GetUnitZ(caster) + Z_OFFSET
        local real tx = GetSpellTargetX()
        local real ty = GetSpellTargetY()
        local real tz = cz // GetTerrainZ(tx, ty)
        // call MoveLocation(loc, tx, ty)
        // set tz = GetLocationZ(loc)

        set dx = tx - cx
        set dy = ty - cy
        set dz = tz - cz
        set v_len = SquareRoot(dx * dx + dy * dy + dz * dz)
        set dx = dx / v_len
        set dy = dy / v_len
        set dz = dz / v_len

        set tx = cx + dx * dist
        set ty = cy + dy * dist
        set tz = cz + dz * dist

        set st = SpiralingTrail.create(caster, DUMMY_MODEL, cx, cy, cz, tx, ty, tz, 0.0, 1, speed, scale, p_spiral_radius, p_damage)
        call st.start_moving()
        set st = SpiralingTrail.create(caster, DUMMY_MODEL, cx, cy, cz, tx, ty, tz, bj_PI / 4.0, -1, speed, scale, p_spiral_radius, p_damage)
        call st.start_moving()

        set caster = null
    endmethod

endstruct

private function init takes nothing returns nothing
    local trigger t = CreateTrigger()
    call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_CAST)
    call TriggerAddAction(t, function SpiralingTrails.cast)
endfunction

endscope
Contents

Spiraling Trails (Map)

Level 37
Joined
Jul 22, 2015
Messages
3,485
Please add a "How to Install" in your description + map. Could you also describe the spell a little more? I feel I'm missing some actual spell mechanics from "Casts magical stars that leave spiraling trails"
  • If you used static method onInit inside the struct, you can get rid of that ugly initializer init
  • EVENT_PLAYER_UNIT_SPELL_CAST > EVENT_PLAYER_UNIT_SPELL_EFFECT
  • It would be better if static method cast returned a boolean and you used TriggerAddCondition() instead of TriggerAddAction()
  • Those method create arguments... lord have mercy. Do you really need to seperate the structs? That looks horrendous. On a side note, I see no reason for you to have seperate structs if you aren't going to encapsulate anything
  • The number of spiral effects created should be configurable
  • private function unit_filter should have all the filters of the group enumeration
I didn't really feel like looking too far into the code. At quick glance, I can see there are a lot of numbers that look arbitary to me. I'd love for you to add some comments that describe your reasoning behind them (ex: SquareRoot(dx * dx + dy * dy)) * bj_RADTODEG + 90.0 & bj_PI / 4.0)

Awaiting Update
 
Top