• Listen to a special audio message from Bill Roper to the Hive Workshop community (Bill is a former Vice President of Blizzard Entertainment, Producer, Designer, Musician, Voice Actor) 🔗Click here to hear his message!
  • Read Evilhog's interview with Gregory Alper, the original composer of the music for WarCraft: Orcs & Humans 🔗Click here to read the full interview.

Mana Charge

This bundle is marked as useful / simple. Simplicity is bliss, low effort and/or may contain minor bugs.
  • Like
Reactions: Mortimm
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...

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
 
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.
 
Top