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

Mana Charge

This bundle is marked as useful / simple. Simplicity is bliss, low effort and/or may contain minor bugs.
Mana particles can be conjured by a caster to form a charge that can be instantly discharged onto unsuspecting foes from a distance or the ground itself in which case it acts as a propeller. A Mana Charge cannot be sustained for long periods of time due to the damage it causes to the caster.

JASS:
library withmanacharge requires /*
    */Ht, /* http://www.hiveworkshop.com/threads/t-real-x-vs-t-x-real.288833/
    */ updatelist, /* http://www.hiveworkshop.com/threads/fire-and-ice-system.290464/#post-3120100
    */ v3 /* in the map */

globals
    private constant integer MANA_CHARGE = 'A002'
    private constant integer MANA_CHARGE_ON = 852589
    private constant integer MANA_CHARGE_OFF = 852590
    private constant string MANA_CHARGE_HK = "X"

    private constant integer MANA_DISCHARGE = 'A001'

    private constant real MIN_REQUIRED_MANA = 50.0
    private constant real MAX_MANA_CHARGE = 100.0

    private constant real MANA_CHARGE_TEXT_SIZE = 9.0 * 0.0023

    // if the mana-discharge is casted on a point within the circle with a center point
    // the position of the caster and with `this` radius the mana-discharge will act
    // as a propeller for the caster and would move them in the opposite direction,
    // otherwise the mana-discharge acts as an instant linear projectile with a limited range
    private constant real DISCHARGE_GROUND_RADIUS = 384.0

    private constant real GRAVITY = -50.0
    private constant real DT = 1.0 / 32.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

// it takes `this` many seconds to charge up to `max-mana-charge`
private function max_mana_charge_time_for_level takes integer level returns real
    return 5.0 - (level - 1)
endfunction

// the caster can only hold the max-mana-charge for `this` many seconds
// after that the charge would be discharged onto the caster
private function max_mana_charge_hold_time_for_level takes integer level returns real
    return 10.0 + 2.0 * (level - 1)
endfunction

private function charge_to_velocity takes real charge returns real
    return 15.0 * charge
endfunction

private function charge_to_self_damage takes real charge, integer level returns real
    return (2.6667 + 0.0 * level) * charge
endfunction

private function charge_to_damage takes real charge, integer level returns real
    return (1.0 + level) * charge
endfunction

private function max_hit_distance_for_level takes integer level returns real
    return 800.0 + (level - 1) * 100.0
endfunction

private keyword Mana_Charge
native UnitAlive takes unit u returns boolean

private function targets_allowed takes Mana_Charge mc, unit u, real z_hit returns boolean
    local real uz
    local real z_diff

    if not UnitAlive(u) then
        return false
    endif

    if not IsUnitEnemy(u, mc.owner) then
         return false
    endif

    set v3_loc_x = GetUnitX(u)
    set v3_loc_y = GetUnitY(u)
    call MoveLocation(v3_loc, v3_loc_x, v3_loc_y)
    set uz = GetLocationZ(v3_loc) + GetUnitFlyHeight(u)
    set z_diff = uz - z_hit
    if z_diff < 0.0 then
        set z_diff = -z_diff
    endif
    if z_diff > 128.0 then
        return false
    endif

    return true
endfunction


globals
    private effect effect_result
endglobals
private function AddSpecialEffectZ takes string effec_path, real x, real y, real z returns effect
    local destructable platform = CreateDestructableZ('OTip', x, y, z, 0, 1, 0)
    set effect_result = AddSpecialEffect(effec_path, x, y)
    call RemoveDestructable(platform)
    set platform = null
    return effect_result
endfunction

private struct Mana_Charge
    static real ul_timer_frequency = DT
    static code ul_update_handler
    implement updatelist

    static real map_min_x
    static real map_max_x
    static real map_min_y
    static real map_max_y

    static Ht ht
    static group G = CreateGroup()

    static constant integer STATE_WAIT_TURN_OFF = 0
    static constant integer STATE_TURN_OFF = 1
    static constant integer STATE_CHARGING = 2
    static constant integer STATE_MAX_CHARGED = 3
    static constant integer STATE_DISCHARGING_GROUND = 4
    static constant integer STATE_LIGHTNING_FADE = 5
    integer state

    unit caster
    player owner
    integer level

    // turn-off
    integer turn_off_delay_ticks

    // charging
    real mana_dec
    real mana_charge
    texttag tt

    // max-charged
    integer charge_hold_ticks

    // discharging-ground
    v3 p
    v3 tp
    v3 dp
    v3 ddp
    real dp_rot

    // lightning-fade
    integer li_max_ticks
    integer li_ticks
    lightning li
    effect li_effect

    private static code on_event_handler
    private static method onInit takes nothing returns nothing
        local trigger t
        local rect r = GetPlayableMapRect()

        set map_min_x = GetRectMinX(r) + 64.0
        set map_max_x = GetRectMaxX(r) - 64.0
        set map_min_y = GetRectMinY(r) + 64.0
        set map_max_y = GetRectMaxY(r) - 64.0

        set ht = Ht.create()

        call ExecuteFunc("s__" + "thistype" + "_set_handlers")

        set t = CreateTrigger()
        call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_ISSUED_ORDER)
        call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_EFFECT)
        call TriggerAddAction(t, on_event_handler)
    endmethod

    method mana_charge_off takes nothing returns nothing
        // selecting a unit and calling ForceUIKey doesn't happen instantly/synchronously
        // so we transition to this wait/do-nothing state until the turn-off order is issued
        set this.state = STATE_WAIT_TURN_OFF

        if this.owner == GetLocalPlayer() then
            call SelectUnit(this.caster, true)
            // issue MANA_CHARGE_OFF order without interrupting the caster's current order
            call ForceUIKey(MANA_CHARGE_HK)
        endif
    endmethod

    method mana_charge_tt_update_pos takes nothing returns nothing
        // call SetTextTagPosUnit(this.tt, this.caster, /*height-offset:*/ 32.0)
        call SetTextTagPos(this.tt, GetUnitX(this.caster) - 24.0, GetUnitY(this.caster), /*height-offset:*/ 144.0)
    endmethod

    method destroy takes nothing returns nothing
        call ht[ht.h2i(this.caster)].int_d()
        set this.caster = null
        set this.owner = null
        call this.ul_remove()
        call this.deallocate()
    endmethod

    private static method on_event takes nothing returns nothing
        local eventid ee = GetTriggerEventId()
        local integer o
        local real lightning_fade_time = 0.5
        local real charge
        local real velocity
        local real dp_aoa
        local real dist_xy
        local real max_hit_distance
        local real hit_radius = 128.0
        local thistype this
        local v3 tpp
        local unit u
        local v3 curr_p
        local v3 step
        local integer i
        local v3 n
        local v3 a
        local v3 up
        local v3 ua
        local v3 ul
        local real z

        if ee == EVENT_PLAYER_UNIT_ISSUED_ORDER then
            set o = GetIssuedOrderId()

            if o == MANA_CHARGE_ON then
                set this = allocate()
                set this.caster = GetTriggerUnit()
                set this.owner = GetOwningPlayer(this.caster)

                if GetUnitState(this.caster, UNIT_STATE_MANA) < MIN_REQUIRED_MANA then
                    // ForceUIKey needs a bit of delay
                    set this.state = STATE_TURN_OFF
                    set this.turn_off_delay_ticks = R2I(0.1 / DT)

                else
                    set this.state = STATE_CHARGING
                    set this.level = GetUnitAbilityLevel(this.caster, MANA_CHARGE)

                    set this.mana_dec = (MAX_MANA_CHARGE / max_mana_charge_time_for_level(this.level)) * DT
                    set this.mana_charge = 0.0

                    set this.tt = CreateTextTag()
                    call SetTextTagText(this.tt, "0 mc", MANA_CHARGE_TEXT_SIZE)
                    call this.mana_charge_tt_update_pos()
                    call SetTextTagColor(this.tt, /*R:*/ 0x0E, /*G:*/ 0x2F, /*B:*/ 0xA7, /*A:*/ 0xFF)
                    call SetTextTagVisibility(this.tt, IsPlayerAlly(this.owner, GetLocalPlayer()))
                    call SetTextTagPermanent(this.tt, true)

                    call UnitAddAbility(this.caster, MANA_DISCHARGE)
                endif

                set ht.select()[ht.h2i(this.caster)].int = this
                call this.ul_add()

            elseif o == MANA_CHARGE_OFF then
                set this = ht.select()[ht.h2i(GetTriggerUnit())].int
                call DestroyTextTag(this.tt)
                set this.tt = null
                call UnitRemoveAbility(this.caster, MANA_DISCHARGE)
                call this.destroy()
            endif

        elseif ee == EVENT_PLAYER_UNIT_SPELL_EFFECT then
            if GetSpellAbilityId() != MANA_DISCHARGE then
                return
            endif

            set this = ht.select()[ht.h2i(GetTriggerUnit())].int

            call DestroyTextTag(this.tt)
            set this.tt = null

            set this.p = v3_from_unit(this.caster)
            set this.tp = v3_from_spell_target()
            set tpp = v3s(this.p, this.tp)

            set dist_xy = tpp.len_xy()
            if dist_xy <= DISCHARGE_GROUND_RADIUS then
                set this.state = STATE_DISCHARGING_GROUND

                set this.p = this.p.to_owned()
                set this.tp = this.tp.to_owned()

                set this.dp_rot = tpp.ang_rot()
                set dp_aoa = v3_map(dist_xy, DISCHARGE_GROUND_RADIUS, 0.0, 60.0, 90.0)
                set charge = v3_map(this.mana_charge, 0.0, MAX_MANA_CHARGE, 0.0, 100.0)
                set velocity = charge_to_velocity(charge)
                set this.dp = v3_from_spherical(this.dp_rot, bj_DEGTORAD * dp_aoa, velocity * DT).to_owned()

                set this.ddp = v3sc(DT, v3c(0.0, 0.0, GRAVITY)).to_owned()

                set this.li = AddLightningEx("DRAM", false, this.p.x, this.p.y, this.p.z, tp.x, tp.y, tp.z)
                set this.li_max_ticks = R2I(lightning_fade_time / DT)
                set this.li_ticks = this.li_max_ticks

                set this.dp_rot = bj_RADTODEG * this.dp_rot
                call SetUnitFacing(this.caster, this.dp_rot)
                call SetUnitPropWindow(this.caster, 0.0) // disable caster movement

                call UnitAddAbility(this.caster, 'Amrf')
                call UnitRemoveAbility(this.caster, 'Amrf')

            else
                set this.state = STATE_LIGHTNING_FADE
                set this.li_max_ticks = R2I(lightning_fade_time / DT)
                set this.li_ticks = this.li_max_ticks

                set max_hit_distance = max_hit_distance_for_level(this.level)
                set i = R2I(max_hit_distance / hit_radius + 0.5)
                set curr_p = v3c(this.p.x, this.p.y, 0.0)
                set step = v3_from_polar(tpp.ang_rot() + bj_PI, hit_radius)
                set z = this.p.z + 64.0
                loop
                    exitwhen i <= 0

                    call GroupEnumUnitsInRange(G, curr_p.x, curr_p.y, hit_radius, null)
                    loop
                        set u = FirstOfGroup(G)
                        exitwhen u == null
                        call GroupRemoveUnit(G, u)
                        if targets_allowed(this, u, z) then
                            exitwhen true
                        endif
                    endloop
                    if u != null then
                        call GroupClear(G)
                        exitwhen true
                    endif

                    call curr_p.set_eq(v3a(curr_p, step))
                    set i = i - 1
                endloop

                if u != null then
                    set n = v3n(step)
                    set n.z = 0.0
                    set a = v3c(this.p.x, this.p.y, 0.0)
                    set up = v3c(GetUnitX(u), GetUnitY(u), 0.0)
                    set ua = v3s(a, up)
                    set ul = v3a(up, v3s(ua, v3sc(ua.dot(n), n)))

                    set this.li_effect = AddSpecialEffectZ("Abilities\\Spells\\Orc\\Purge\\PurgeBuffTarget.mdl", ul.x, ul.y, z)
                    set this.li = AddLightningEx("DRAM", false, this.p.x, this.p.y, z, ul.x, ul.y, z)

                    call UnitDamageTarget(this.caster, u, charge_to_damage(this.mana_charge, this.level), MELEE_ATTACK, RANGE_ATTACK, ATTACK_TYPE, DAMAGE_TYPE, WEAPON_TYPE)
                    set u = null
                else
                    set this.li = AddLightningEx("DRAM", false, this.p.x, this.p.y, z, curr_p.x, curr_p.y, z)
                endif
            endif

            // it would be better if we could disable/silence the mana charge ability while
            // the caster is discharging on the ground instead of removing/hiding it
            call UnitRemoveAbility(this.caster, MANA_CHARGE)

            // the mana discharge ability can only be used once after the mana charge
            // is successfully (having enough mana) turned on, again it would be nicer to
            // be able to disable/silence it instead of removing/hiding it
            // note: this needs to be called after GetSpellTargetX|Y otherwise we get 0.0
            call UnitRemoveAbility(this.caster, MANA_DISCHARGE)
        endif
    endmethod

    private static method ul_update takes nothing returns nothing
        local thistype this
        local real mana
        local integer i

        set i = 1
        loop
            exitwhen i > ul_count
            set this = ul_list[i]

            if this.state == STATE_WAIT_TURN_OFF then
                // do nothing while mana charge is turning off

            elseif this.state == STATE_TURN_OFF then
                set this.turn_off_delay_ticks = this.turn_off_delay_ticks - 1
                if this.turn_off_delay_ticks <= 0 then
                    call this.mana_charge_off()
                endif

            elseif this.state == STATE_CHARGING then
                set this.mana_charge = this.mana_charge + this.mana_dec
                set mana = GetUnitState(this.caster, UNIT_STATE_MANA) - this.mana_dec
                call SetUnitState(this.caster, UNIT_STATE_MANA, mana)

                if mana < 1.0 or this.mana_charge > MAX_MANA_CHARGE then
                    set this.state = STATE_MAX_CHARGED
                    set this.charge_hold_ticks = R2I(max_mana_charge_hold_time_for_level(this.level) / DT)

                    if this.mana_charge > MAX_MANA_CHARGE then
                        set this.mana_charge = MAX_MANA_CHARGE
                    endif
                endif

                call SetTextTagText(this.tt, I2S(R2I(this.mana_charge)) + " mc", MANA_CHARGE_TEXT_SIZE)
                call this.mana_charge_tt_update_pos()

            elseif this.state == STATE_MAX_CHARGED then
                call this.mana_charge_tt_update_pos()

                set this.charge_hold_ticks = charge_hold_ticks - 1
                if this.charge_hold_ticks == 0 then
                    call DestroyEffect(AddSpecialEffect("Units\\NightElf\\Wisp\\WispExplode.mdl", GetUnitX(this.caster), GetUnitY(this.caster)))
                    call UnitDamageTarget(this.caster, this.caster, charge_to_self_damage(this.mana_charge, this.level), MELEE_ATTACK, RANGE_ATTACK, ATTACK_TYPE, DAMAGE_TYPE, WEAPON_TYPE)
                    call this.mana_charge_off()
                endif

            elseif this.state == STATE_DISCHARGING_GROUND then
                call this.dp.set_eq(v3a(this.dp, this.ddp))
                call this.p.set_eq(v3a(this.p, this.dp))

                if this.p.x < map_min_x then
                    set this.p.x = map_min_x
                elseif this.p.x > map_max_x then
                    set this.p.x = map_max_x
                endif
                if this.p.y < map_min_y then
                    set this.p.y = map_min_y
                elseif this.p.y > map_max_y then
                    set this.p.y = map_max_y
                endif

                call SetUnitX(this.caster, this.p.x)
                call SetUnitY(this.caster, this.p.y)
                call MoveLocation(v3_loc, this.p.x, this.p.y)
                set v3_loc_z = GetLocationZ(v3_loc)
                call SetUnitFlyHeight(this.caster, this.p.z - v3_loc_z, 0.0)

                call SetUnitFacing(this.caster, this.dp_rot)

                call MoveLightningEx(this.li, false, this.tp.x, this.tp.y, this.tp.z, this.p.x, this.p.y, this.p.z)

                if this.p.z < v3_loc_z then
                    set this.state = STATE_LIGHTNING_FADE

                    call this.p.destroy()
                    call this.tp.destroy()
                    call this.dp.destroy()
                    call this.ddp.destroy()

                    call SetUnitFlyHeight(this.caster, 0.0, 0.0)
                    call SetUnitPropWindow(this.caster, bj_DEGTORAD * GetUnitDefaultPropWindow(this.caster))
                endif

            elseif this.state == STATE_LIGHTNING_FADE then
                set this.li_ticks = this.li_ticks - 1
                if this.li_ticks <= 0 then
                    call DestroyLightning(this.li)
                    set this.li = null

                    if this.li_effect != null then
                        call DestroyEffect(this.li_effect)
                        set this.li_effect = null
                    endif

                    call UnitAddAbility(this.caster, MANA_CHARGE)
                    call SetUnitAbilityLevel(this.caster, MANA_CHARGE, this.level)

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

                else
                    call SetLightningColor(this.li, 1.0, 1.0, 1.0, I2R(this.li_ticks) / this.li_max_ticks)
                endif

            endif

            set i = i + 1
        endloop
    endmethod

    private static method set_handlers takes nothing returns nothing
        set on_event_handler = function thistype.on_event
        set ul_update_handler = function thistype.ul_update
    endmethod

endstruct

endlibrary
Contents

Mana Charge (Map)

Reviews
KILLCIDE
Needs Fixed Include import instructions and credits (if there are any) in both the description and ingame Have a proper ingame tooltip Any external libraries / system used must be approved in the database Suggestions For the sanity of the Code...
Level 37
Joined
Jul 22, 2015
Messages
3,485

Needs Fixed

  • Include import instructions and credits (if there are any) in both the description and ingame
  • Have a proper ingame tooltip
  • Any external libraries / system used must be approved in the database

Suggestions

  • For the sanity of the Code Moderators and Spell Reviewers, please use JPAG
  • The demo map is definitely one of the worse I've seen

Status

Awaiting Update
 
Level 13
Joined
Nov 7, 2014
Messages
571
For the sanity of the Code Moderators and Spell Reviewers, please use JPAG
What's not JPAG-ish about it?

The demo map is definitely one of the worse I've seen
How come (its a spell demo map you shoudn't expect to it to make the director's cut list)?

Have a proper ingame tooltip
I am not intrested in that.

Any external libraries / system used must be approved in the database
I like to use my own stuff.

Include import instructions and credits (if there are any) in both the description and ingame
I would've written them in the post if there were any.
 
Level 37
Joined
Jul 22, 2015
Messages
3,485
Top