(Required) • TimerUtils • Stack • WorldBounds • UnitIndexer • UnitZ • RSound (Optional) • AutoFly | by Vexorian by Nestharus by Nestharus by Nestharus by Garfield1337 by Quilnez by Nestharus | | wc3c.net/showthread.php?t=101322 | github.com/nestharus/JASS/tree/master/jass/Data%20Structures/Stack | github.com/nestharus/JASS/tree/master/jass/Systems/WorldBounds | hiveworkshop.com/forums/spells-569/unit-indexer-v5-3-0-1-a-260859/ | hiveworkshop.com/forums/jass-resources-412/snippet-getterrainz-unitz-236942/ | hiveworkshop.com/threads/snippet-rapidsound.258991/ | github.com/nestharus/JASS/tree/master/jass/Systems/AutoFly |
scope Frostmyr
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// Frostmyr v1.7
// ¯¯¯¯¯¯¯¯¯¯¯¯¯
// Created by: Quilnez
//
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// 1. Description
// The caster summons and controls 5 freezing projectiles which will periodically
// combusts his mana as cost. Every projectile deals damage periodically to nearby
// opponents. While controlling these projectiles, the caster will be unable to
// move and attack until it's ordered to stop. The spell will also stop once the
// caster's mana is depleted or it's dead.
//
// Level 1 - Burns 20 mana points per second. Deals 80 damage per second.
// Level 2 - Burns 30 mana points per second. Deals 160 damage per second.
// Level 3 - Burns 40 mana points per second. Deals 240 damage per second.
//
// To control the projectile you need to have the caster in your selection. Then
// simply right-click on the target location/unit. If you have a unit as target,
// the projectile will automatically follow it until it's dead.
//
// 2. External scripts
// (Required)
// • TimerUtils by Vexorian | wc3c.net/showthread.php?t=101322
// • Stack by Nestharus | github.com/nestharus/JASS/tree/master/jass/Data%20Structures/Stack
// • World Bounds by Nestharus | github.com/nestharus/JASS/tree/master/jass/Systems/WorldBounds
// • Unit Indexer by Nestharus | hiveworkshop.com/forums/spells-569/unit-indexer-v5-3-0-1-a-260859/
// • UnitZ by Garfield1337 | hiveworkshop.com/forums/jass-resources-412/snippet-getterrainz-unitz-236942/
// • RapidSound by Quilnez | hiveworkshop.com/threads/snippet-rapidsound.258991/
// (Optional)
// • AutoFly by Nestharus | github.com/nestharus/JASS/tree/master/jass/Systems/AutoFly
//
// 3. How to Install
// • Export all files in Import Manager to your map
// • Copy dummy unit, main spell, and cancel spell to your map
// • Copy Frostmyr and Scripts folders at Trigger Editor to your map
// • Just delete duplicated external scripts
// • Install all used scripts (libraries) properly
// • Configure the spell properly (spell id, dummy id, etc.)
//
// 4. Credits
// • BTNIce_by67chrome.blp by 67chrome
// • Mini Comet.mdx by 00110000
// • dummy.mdx by Vexorian
// • Cold Wind ambient by Google.com
//
// 5. Link
// Visit this link to check out the newest update of this spell:
// hiveworkshop.com/forums/spells.php?id=ixhbcf
//
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// CONFIGURATION
// ¯¯¯¯¯¯¯¯¯¯¯¯¯
globals
//
// A. General
//
// 1. Main spell's raw code
private constant integer SPELL_ID = 'A000'
//
// 2. Cancel spell's raw code
private constant integer CANCEL_ID = 'A001'
//
// 3. Dummy unit's raw code
private constant integer DUMMY_ID = 'h000'
//
// 4. Order id used to give order to missiles
private constant integer ORDER_ID = 851971 //smart
//
// 5. Played animation for caster
private constant string ANIMATION = "channel"
//
// 6. Created special effect whenever a unit takes damage from spell
private constant string DAMAGE_SFX = "war3mapImported\\ForstmyrTarget.mdx"
private constant string DAMAGE_SFX_PT = "chest"
//
// 7. Played sound effect whenever a unit takes damage from spell
private constant string DAMAGE_SOUND = "war3mapImported\\FrostmyrDamage.wav"
private constant integer DSOUND_VOLUME = 127
//
// 8. Times given for special effects to decay
private constant real SFX_DECAY_TIME = 5.0
//
// 9. Sound effect for missiles
private constant string SOUND_PATH = "war3mapImported\\FrostmyrAmbient.wav"
private constant real PITCH = 0.5
private constant integer VOLUME = 65
//
// 10. Dealt damage configurations
private constant attacktype ATTACK_TYPE = ATTACK_TYPE_NORMAL
private constant damagetype DAMAGE_TYPE = DAMAGE_TYPE_COLD
private constant weapontype WEAPON_TYPE = WEAPON_TYPE_WHOKNOWS
//
// B. Missile
//
// 1. Missile's model filepath
private constant string MODEL_PATH = "war3mapImported\\Mini Comet.mdx"
//
// 2. If true missiles will be able to enter idle state
// - Makes it easier to control
// - Increases damage efficiency
private constant boolean ALLOW_IDLE = true
//
// 3. Default fly height for missiles
private constant real Z_OFFSET = 125.0
//
// 4. Missile trajectory configurations
private constant real LENGTH = 100.0
private constant real WIDTH = 100.0
private constant real HEIGHT = 100.0
//
// 5. Movespeed for missiles
private constant real MOVE_RATE = 13.0
//
// 6. Turn rate for missiles
private constant real TURN_RATE = 3.0*bj_DEGTORAD
//
// 7. Initial offset for missiles
private constant real SPAWN_OFFSET = 100.0
//
// 8. Rotate (spin) rate for missiles
// - Use negative value to inverse the rotation
private constant real SPIN_RATE = 5*bj_DEGTORAD
//
// 9. Speed factor when missiles deaccelerate
// - Only takes effect if ALLOW_IDLE is true
// - Must be between 0 ~ 1
// - The higher the slower
private constant real SLOW_RATE = 0.95
//
// 10. Maximum range between missile and target to deal damage
private constant real COLLISION_SIZE = 120.0
//
// Better not to touch this
private constant real INTERVAL = 0.03125
//
endglobals
//
// C. Non-constant
//
// 1. Created missile per cast
private function MissileCount takes integer level returns integer
return 5
endfunction
//
// 2. Damage dealt by every single missile
private function DamageAmount takes integer level returns real
return 10.0 * level
endfunction
//
// 3. Delay before next damage will be dealt
private function DamageDelay takes integer level returns real
return 0.125
endfunction
//
// 4. Combusted mana amount every certain times
private function ManaCost takes integer level returns real
return 2.5 + 2.5 *level
endfunction
//
// 5. Delay between mana reduction
private function CostDelay takes integer level returns real
return 0.25
endfunction
//
// 6. Target classification that can be hit by this spell
private function filterTarget takes player caster, unit target returns boolean
return IsUnitEnemy(target, caster) and not(IsUnitType(target, UNIT_TYPE_STRUCTURE) or IsUnitType(target, UNIT_TYPE_MECHANICAL))
endfunction
//
// 7. This function let you to set up what condition to stop the channel
// The caster will stop channeling when this function returns true
private function stopChannel takes unit caster returns boolean
return GetUnitAbilityLevel(caster, 'BSTN') > 0 // Stop when the caster is stunned
endfunction
//
//
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// EVENT CATCHER
// ¯¯¯¯¯¯¯¯¯¯¯¯¯
private module EventCatcher
// Called whenever a unit takes damage from this spell
// - caster : caster of the spell (damage source)
// - target : damaged unit
// - level : caster's Frostmyr ability level
static method onDamage takes unit caster, unit target, integer level returns nothing
endmethod
endmodule
//
//
//
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
native UnitAlive takes unit id returns boolean
private module InitModule
private static method onInit takes nothing returns nothing
call init()
endmethod
endmodule
private struct FrostmyrMissile extends array
effect sfx
unit missile
real hOffset
real vOffset
real hMax
real vMax
implement Stack
endstruct
private struct Frostmyr
real locX
real locY
real targetX
real targetY
real missileX
real missileY
real angle
real hOffset
real vOffset
real damage
real damageDelay
real damageDelayX
real manaCost
real costDelay
real costDelayX
real missileSpin
real missileDistance
real missileDistanceX
real missileHPos
real missileVPos
real missileAngle
boolean idle
integer count
integer level
sound sfx
player owner
unit caster
unit target
timer tick
FrostmyrMissile stack
static RSound SoundFx
static trigger OrderTrigg= CreateTrigger()
static group TempGroup = CreateGroup()
static integer array Index
static constant real TAU = bj_PI*2
static constant real HP = bj_PI/2
implement optional EventCatcher
method destroy takes nothing returns nothing
local FrostmyrMissile node = .stack.first
// Kill missiles
loop
exitwhen node == 0
call UnitApplyTimedLife(node.missile, 'BTLF', SFX_DECAY_TIME)
call DestroyEffect(node.sfx)
set node.missile = null
set node.sfx = null
set node = node.next
endloop
call ReleaseTimer(.tick)
call StopSound(.sfx, true, true)
call .stack.destroy()
call deallocate()
// Remove leaks
set .caster = null
set .target = null
set .tick = null
set .sfx = null
endmethod
method stop takes nothing returns nothing
// Reset animation if not dead
if UnitAlive(.caster) then
call SetUnitAnimation(.caster, "stand")
endif
set Index[GetUnitUserData(.caster)] = 0
call UnitRemoveType(.caster, UNIT_TYPE_PEON)
call UnitRemoveAbility(.caster, CANCEL_ID)
call SetUnitAbilityLevel(.caster, SPELL_ID, .level)
call destroy()
endmethod
static method onPeriodic takes nothing returns nothing
local FrostmyrMissile node
local thistype this = GetTimerData(GetExpiredTimer())
local boolean b
local unit u
local real a
local real a2
local real as
local real d
local real f
local real h
local real m
local real v
local real x
local real y
local real z
local real x2
local real y2
local real z2
local real sf
// If have to stop channeling
if not UnitAlive(.caster) or stopChannel(.caster) then
call stop()
return
endif
if .target != null then
if UnitAlive(.target) then
set .targetX = GetUnitX(.target)
set .targetY = GetUnitY(.target)
else
set .target = null
endif
endif
// Check if the projectiles need to move
set b = not ALLOW_IDLE or (.targetX-.locX)*(.targetX-.locX)+(.targetY-.locY)*(.targetY-.locY) >= MOVE_RATE*MOVE_RATE
if b then
set a = Atan2(.targetY-.locY, .targetX-.locX)
if TURN_RATE > 0 and Cos(.angle-a) < Cos(TURN_RATE) then
if Sin(a-.angle) >= 0 then
set .angle = .angle + TURN_RATE
else
set .angle = .angle - TURN_RATE
endif
else
set .angle = a
endif
set .locX = .locX + MOVE_RATE * Cos(.angle)
set .locY = .locY + MOVE_RATE * Sin(.angle)
// Stop if the order point exceeds map boundaries
if .locX >= WorldBounds.maxX or .locX <= WorldBounds.minX or .locY >= WorldBounds.maxY or .locY <= WorldBounds.minY then
call stop()
return
endif
call SetSoundPosition(.sfx, .locX, .locY, Z_OFFSET)
endif
set .damageDelay = .damageDelay - INTERVAL
set .costDelay = .costDelay - INTERVAL
if .costDelay <= 0 then
set m = GetUnitState(.caster, UNIT_STATE_MANA) - .manaCost
call SetUnitState(.caster, UNIT_STATE_MANA, m)
set .costDelay = .costDelayX
if m <= 0 then
call stop()
return
endif
endif
// If idle calculate deaccelerated speed
if .idle then
set sf = missileDistanceX/2
set sf = 1-RAbsBJ(.missileDistance-sf)/sf*SLOW_RATE
else
set sf = 1
endif
set .missileDistance = .missileDistance + MOVE_RATE*sf
if .missileDistance > .missileDistanceX then
set .missileDistance = .missileDistanceX
endif
// Adjust control angle
set a = Atan2(.locY-.missileY, .locX-.missileX)
if TURN_RATE > 0 and Cos(.missileAngle-a) < Cos(TURN_RATE) then
if Sin(a-.missileAngle) >= 0 then
set .missileAngle = .missileAngle + TURN_RATE
else
set .missileAngle = .missileAngle - TURN_RATE
endif
else
set .missileAngle = a
endif
// Update missiles
set .missileSpin = .missileSpin + SPIN_RATE*.missileHPos
set f = (.missileDistanceX-.missileDistance)*(.missileDistance/.missileDistanceX)
set node = .stack.first
set as = TAU/.count
set a = as /2
loop
exitwhen node == 0
set node.hMax = WIDTH *Sin(a+.missileSpin)
set node.vMax = HEIGHT*Cos(a+.missileSpin)
set h = (4*node.hMax/.missileDistanceX)*f
set v = (4*node.vMax/.missileDistanceX)*f
set d = SquareRoot(.missileDistance*.missileDistance+h*h)
set a2 = .missileAngle-Atan(h/.missileDistance)
set x = .missileX + d * Cos(a2)
set y = .missileY + d * Sin(a2)
if x < WorldBounds.maxX and x > WorldBounds.minX and y < WorldBounds.maxY and y > WorldBounds.minY then
call ShowUnit(node.missile, true)
call SetUnitX(node.missile, x)
call SetUnitY(node.missile, y)
call SetUnitFacing(node.missile, .missileAngle*bj_RADTODEG)
call SetUnitFlyHeight(node.missile, Z_OFFSET+v*.missileVPos, 0)
set z = GetUnitZ(node.missile)
if .damageDelay <= 0 then
call GroupEnumUnitsInRange(TempGroup, x, y, COLLISION_SIZE, null)
loop
set u = FirstOfGroup(TempGroup)
exitwhen u == null
call GroupRemoveUnit(TempGroup, u)
if UnitAlive(u) and filterTarget(.owner, u) then
set x2 = GetUnitX(u)
set y2 = GetUnitY(u)
set z2 = GetUnitZ(u)
set d = SquareRoot((x-x2)*(x-x2)+(y-y2)*(y-y2))
set h = RAbsBJ(z2-z)
// Spherical collision
if d*d+h*h < COLLISION_SIZE*COLLISION_SIZE then
static if thistype.onDamage.exists then
call onDamage(.caster, u, .level)
endif
call SoundFx.play(x2, y2, z2, DSOUND_VOLUME)
call UnitDamageTarget(.caster, u, .damage, false, false, ATTACK_TYPE, DAMAGE_TYPE, WEAPON_TYPE)
call DestroyEffect(AddSpecialEffectTarget(DAMAGE_SFX, u, DAMAGE_SFX_PT))
endif
endif
endloop
endif
else
// Temporary hide missile if exceeds map boundaries
call ShowUnit(node.missile, false)
endif
set a = a + as
set node = node.next
endloop
if .damageDelay <= 0 then
set .damageDelay = .damageDelayX
endif
if .missileDistance == .missileDistanceX then
set .missileX = x
set .missileY = y
set .missileAngle = Atan2(.locY-.missileY, .locX-.missileX)
set .missileDistance = 0
set .missileVPos = -.missileVPos
if b then
set .idle = false
set .missileHPos = -.missileHPos
else
set .idle = true
set .missileHPos = 1
endif
endif
endmethod
static method orderStop takes nothing returns nothing
local timer t = GetExpiredTimer()
local thistype this = GetTimerData(t)
call DisableTrigger(OrderTrigg)
call IssueImmediateOrder(.caster, "stop")
call SetUnitAnimation(.caster, ANIMATION)
call EnableTrigger(OrderTrigg)
call ReleaseTimer(t)
set t = null
endmethod
static method onOrder takes nothing returns boolean
local thistype this = Index[GetUnitUserData(GetTriggerUnit())]
local integer id
local widget w
local unit u
// If unit is casting Frostmyr
if this != 0 then
set id = GetIssuedOrderId()
if id == ORDER_ID then
set u = GetOrderTargetUnit()
if u == null then
set w = GetOrderTarget()
if w == null then
set .targetX = GetOrderPointX()
set .targetY = GetOrderPointY()
else
set .targetX = GetWidgetX(w)
set .targetY = GetWidgetY(w)
endif
set .target = null
set w = null
else
set .target = u
set u = null
endif
set .idle = false
endif
// This is done because seems like immediate (no target) order
// is somehow unable to prevent attack move order immediately
if id == 851983 then // If order is "attack"
call IssuePointOrder(.caster, "move", 0, 0)
else
call TimerStart(NewTimerEx(this), 0, false, function thistype.orderStop)
endif
endif
return false
endmethod
static method onCast takes nothing returns boolean
local FrostmyrMissile node
local thistype this
local integer data = GetUnitUserData(GetTriggerUnit())
local integer id = GetSpellAbilityId()
local integer i
local integer h
local integer v
local real a
local real as
if id == SPELL_ID and Index[data] == 0 then
set this = allocate()
set .caster = GetTriggerUnit()
set .owner = GetTriggerPlayer()
set .angle = GetUnitFacing(.caster)*bj_DEGTORAD
set .missileX = GetUnitX(.caster) + SPAWN_OFFSET * Cos(.angle)
set .missileY = GetUnitY(.caster) + SPAWN_OFFSET * Sin(.angle)
set .missileAngle= .angle
set .locX = .missileX + LENGTH * Cos(.angle)
set .locY = .missileY + LENGTH * Sin(.angle)
set .targetX = .locX
set .targetY = .locY
set .idle = ALLOW_IDLE
set .hOffset = 0
set .vOffset = 0
set .level = GetUnitAbilityLevel(.caster, SPELL_ID)
set .damage = DamageAmount(.level)
set .damageDelayX= DamageDelay (.level)
set .manaCost = ManaCost (.level)
set .costDelayX = CostDelay (.level)
set .count = MissileCount(.level)
set .stack = FrostmyrMissile.create()
set .damageDelay = .damageDelayX
set .costDelay = .costDelayX
set as = TAU/.count
set a = as/2
set i = .count
set .missileSpin = 0
loop
set i = i - 1
set node = .stack.push()
set node.missile = CreateUnit(.owner, DUMMY_ID, .missileX, .missileY, .angle*bj_RADTODEG)
set node.sfx = AddSpecialEffectTarget(MODEL_PATH, node.missile, "origin")
set node.hMax = WIDTH *Sin(a)
set node.vMax = HEIGHT*Cos(a)
static if not LIBRARY_AutoFly then
if UnitAddAbility(node.missile, 'Amrf') and UnitRemoveAbility(node.missile, 'Amrf') then
endif
endif
call PauseUnit(node.missile, true)
call SetUnitFlyHeight(node.missile, Z_OFFSET, 0)
set a = a + as
exitwhen i == 0
endloop
set .locX = .targetX
set .locY = .targetY
set .missileDistanceX = LENGTH*2
set .missileDistance = 0
set .missileHPos = 1
set .missileVPos = 1
set .tick = NewTimerEx(this)
set .sfx = CreateSound(SOUND_PATH, true, true, false, 10, 10, "")
set Index[GetUnitUserData(.caster)] = this
call SetSoundVolume(.sfx, VOLUME)
call SetSoundPitch(.sfx, PITCH)
call StartSound(.sfx)
call UnitAddType(.caster, UNIT_TYPE_PEON)
call SetUnitAnimation(.caster, ANIMATION)
// Credits to Lambdadelta for this trick to hide main spell
call SetUnitAbilityLevel(.caster, SPELL_ID, 999)
call IncUnitAbilityLevel(.caster, SPELL_ID)
call UnitAddAbility(.caster, CANCEL_ID)
call TimerStart(.tick, INTERVAL, true, function thistype.onPeriodic)
elseif id == CANCEL_ID and Index[data] != 0 then
set this = Index[data]
call stop()
endif
return false
endmethod
// Needed so that the sound effect will appear since the first cast
static method preloadSound takes nothing returns nothing
local sound s = CreateSound(SOUND_PATH, false, false, true, 12700, 12700, "")
call StartSound(s)
call StopSound(s, true, false)
call ReleaseTimer(GetExpiredTimer())
set s = null
endmethod
static method init takes nothing returns nothing
local trigger t = CreateTrigger()
local integer i = 0
local player p
loop
exitwhen i > 11
set p = Player(i)
call TriggerRegisterPlayerUnitEvent(t, p, EVENT_PLAYER_UNIT_SPELL_ENDCAST, null)
call TriggerRegisterPlayerUnitEvent(OrderTrigg, p, EVENT_PLAYER_UNIT_ISSUED_TARGET_ORDER, null)
call TriggerRegisterPlayerUnitEvent(OrderTrigg, p, EVENT_PLAYER_UNIT_ISSUED_POINT_ORDER, null)
call TriggerRegisterPlayerUnitEvent(OrderTrigg, p, EVENT_PLAYER_UNIT_ISSUED_ORDER, null)
set i = i + 1
endloop
call TriggerAddCondition(t, Condition(function thistype.onCast))
call TriggerAddCondition(OrderTrigg, Condition(function thistype.onOrder))
call TimerStart(NewTimer(), 0, false, function thistype.preloadSound)
set SoundFx = RSound.create(DAMAGE_SOUND, true, true, 12700, 12700)
set t = null
endmethod
implement InitModule
endstruct
endscope
• BTNIce_by67chrome.blp • Mini Comet.mdx • dummy.mdx • Cold Wind ambient | by 67chrome by 00110000 by Vexorian by Google.com |