library Prediction /*
****************************************************************************************************
*
* P R E D I C T I O N
*
* ~ version 1.0
* ~ coded by Mr_Bean
*
****************************************************************************************************
*
* */ requires /*
* ¯¯¯¯¯¯¯¯
* */ TimerUtils, /* http://www.wc3c.net/showthread.php?t=101322
* */ xemissile, /* http://www.wc3c.net/showthread.php?t=101150
*
* Optional
* ¯¯¯¯¯¯¯¯
* */ optional SpellEffectEvent /* http://www.hiveworkshop.com/forums/jass-resources-412/snippet-spelleffectevent-187193
*
****************************************************************************************************
*
* Installation
* ¯¯¯¯¯¯¯¯¯¯¯¯
* - Make sure you've got (and installed properly) the above systems.
* - Copy the custom ability. Set SPELL_ID (below) to the raw ID of this ability.
* - Go through the configurables and change what you want to.
*
****************************************************************************************************
*
* Configuration
* ¯¯¯¯¯¯¯¯¯¯¯¯¯
*/
globals
//==================================================
//=== RAW IDS
private constant integer SPELL_ID = 'A000' // Raw ID of the ability.
//==================================================
//=== GENERAL
private constant real INITIAL_DELAY = 0.03 // Delay between cast and prediction. Shouldn't need to be changed.
//==================================================
//=== CENTRE AREA EFFECT CONFIGURATION
// This effect is created at the centre of the prediction area.
private constant real L_FX_SCALE = 1.0 // Model scale.
private constant real L_FX_HEIGHT = 25.0 // Height.
// Model path:
private constant string INDICATOR_FX = "Abilities\\Spells\\Items\\VampiricPotion\\VampPotionCaster.mdl"
//==================================================
//=== PERIMITER AREA EFFECT CONFIGURATION
// Multiple of these effects are created around the perimeter of the prediction area.
private constant integer NUM_EFFECTS = 8 // Number of effects.
private constant real EFFECT_SCALE = 1.0 // Model scale.
private constant real EFFECT_HEIGHT = 25.0 // Height.
// Model path:
private constant string INDICATOR_ART = "Abilities\\Weapons\\BloodElfMissile\\BloodElfMissile.mdl"
//==================================================
//=== EXPLOSION CONFIGURATION
// Created at prediction area when the time expires:
private constant string EXPLODE_EFFECT = "Abilities\\Spells\\Other\\Doom\\DoomDeath.mdl"
// Created at prediction area when the target is in the area:
private constant string HIT_EFFECT = "Abilities\\Spells\\Orc\\WarStomp\\WarStompCaster.mdl"
// Created at prediction area when the target is not in the area:
private constant string MISS_EFFECT = "Abilities\\Spells\\Human\\Polymorph\\PolyMorphTarget.mdl"
//==================================================
//=== MANA MISSILE CONFIGURATION
private constant real MAX_DISTANCE = 1000.0 // No mana is burned if the target is this far from the prediction area.
private constant real MISSILE_SCALE = 1.0 // Scale.
private constant real MISSILE_SPEED = 750.0 // Speed.
private constant real MISSILE_ARC = 0.2 // Arc.
private constant real MISSILE_HEIGHT = 50.0 // Height.
// Model:
private constant string MISSILE_ART = "Abilities\\Weapons\\SpiritOfVengeanceMissile\\SpiritOfVengeanceMissile.mdl"
// Effect created on target when mana is burned:
private constant string MANA_BURN_FX = "Abilities\\Spells\\Human\\Feedback\\ArcaneTowerAttack.mdl"
// Attachment point for the above effect:
private constant string BURN_FX_ATTACH = "origin"
//==================================================
//=== FLOATING TEXT CONFIGURATION
private constant boolean DISPLAY_TEXT = true // Display floating text showing damage dealt?
private constant real TEXT_ANGLE = 90.0 // Movement angle.
private constant real TEXT_SPEED = 64.0 // Movement speed.
private constant real TEXT_OFFSET = 50.0 // Height above target.
private constant real TEXT_DURATION = 2.0 // Duration.
private constant real TEXT_FADEPOINT = 1.0 // Fade time.
private constant real TEXT_HEIGHT = 0.023 // Size.
// Colour of text when showing damage:
private constant string COLOUR_DAMAGE = "|cffff0000"
// Colour of text when showing mana burned:
private constant string COLOUR_MANA = "|cff8000FF"
endglobals
//==================================================
//=== PREDICTS TARGET'S POSITION IN THIS MANY SECONDS
private function GetDelay takes integer level returns real
return 2.0
endfunction
//==================================================
//=== HIT AREA OF EFFECT
private function GetArea takes integer level returns real
return (25.0 * level) + 125.0
endfunction
//==================================================
//=== DAMAGE DEALT ON HIT
private function GetDamage takes integer level returns real
return (100.0 * level) + 50.0
endfunction
//==================================================
//=== MANA BURNED ON MISS
private function GetManaBurn takes integer level returns real
return 100.0 * level
endfunction
//==================================================
//=== DAMAGE FUNCTION SETUP
// This is if you want to change attack/damage types or if you use a custom
// function for damage detection or something similar.
private function DealDamage takes unit source, unit target, real amount returns nothing
call UnitDamageTarget(source, target, amount, false, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_NORMAL, null)
endfunction
/***************************************************************************************************
*
* END OF CONFIGURATION AND DOCUMENTATION
*
***************************************************************************************************/
globals
private constant real TEXT_X_VELOCITY = (Cos(TEXT_ANGLE * bj_DEGTORAD) * TEXT_SPEED) * 0.071 / 128.0
private constant real TEXT_Y_VELOCITY = (Sin(TEXT_ANGLE * bj_DEGTORAD) * TEXT_SPEED) * 0.071 / 128.0
endglobals
static if (DISPLAY_TEXT) then
private function DisplayText takes unit target, string text returns nothing
local texttag tt
if (IsVisibleToPlayer(GetUnitX(target), GetUnitY(target), GetLocalPlayer())) then
set tt = CreateTextTag()
call SetTextTagText(tt, text, TEXT_HEIGHT)
call SetTextTagPosUnit(tt, target, TEXT_OFFSET)
call SetTextTagVelocity(tt, TEXT_X_VELOCITY, TEXT_Y_VELOCITY)
call SetTextTagPermanent(tt, false)
call SetTextTagLifespan(tt, TEXT_DURATION)
call SetTextTagFadepoint(tt, TEXT_FADEPOINT)
call SetTextTagVisibility(tt, true)
set tt = null
endif
endfunction
endif
private struct ManaMissile extends xemissile
real amount
private method onHit takes nothing returns nothing
local real mana
if (not IsUnitType(targetUnit, UNIT_TYPE_DEAD) /*
*/ and not IsUnitType(targetUnit, UNIT_TYPE_MAGIC_IMMUNE) /*
*/ and not IsUnitType(targetUnit, UNIT_TYPE_STRUCTURE)) then
set mana = GetUnitState(targetUnit, UNIT_STATE_MANA)
if (mana > 0.0) then
call SetUnitState(targetUnit, UNIT_STATE_MANA, mana - amount)
call DestroyEffect(AddSpecialEffectTarget(MANA_BURN_FX, targetUnit, BURN_FX_ATTACH))
static if (DISPLAY_TEXT) then
call DisplayText(targetUnit, COLOUR_MANA + "-" + I2S(R2I(amount)) + "|r")
endif
endif
endif
endmethod
static method create takes real sx, real sy, unit t, real amt returns thistype
local thistype this = allocate(sx, sy, 0.0, GetUnitX(t), GetUnitY(t), MISSILE_HEIGHT)
set amount = amt
set targetUnit = t
set fxpath = MISSILE_ART
set scale = MISSILE_SCALE
call launch(MISSILE_SPEED, MISSILE_ARC)
return this
endmethod
endstruct
// I had to write this because I can't set xefx objects to be
// visible to only certain players.
private struct Effect
unit dummy
effect fx
method destroy takes nothing returns nothing
call DestroyEffect(fx)
set dummy = null
set fx = null
call deallocate()
endmethod
static method create takes player p, real x, real y, real s, real h, string e returns thistype
local thistype this = allocate()
set dummy = CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), XE_DUMMY_UNITID, x, y, 0.0)
call SetUnitScale(dummy, s, s, s)
set fx = AddSpecialEffectTarget(e, dummy, "origin")
call ShowUnit(dummy, IsPlayerAlly(GetLocalPlayer(), p))
if (h != 0.0) then
call UnitAddAbility(dummy, 'Amrf')
call UnitRemoveAbility(dummy, 'Amrf')
call SetUnitFlyHeight(dummy, h, 99999.0)
endif
return this
endmethod
endstruct
private type fxList extends Effect array[NUM_EFFECTS]
private struct Prediction extends array // I made it an array struct because I wanted to try it :)
static constant real ANGLE_DIV = (2.0 * bj_PI) / NUM_EFFECTS
//-----
static integer numInstances = 0
static integer numRecycles = 0
//-----
static integer array recycled
//-----
unit caster
unit target
integer level
real tx
real ty
real area
fxList fx
Effect locFx
//-----
/**
* Removes the area effects and cleans up.
*/
private method destroy takes nothing returns nothing
local integer i = 0
loop
exitwhen i == NUM_EFFECTS
call fx[i].destroy()
set i = i + 1
endloop
call fx.destroy()
call locFx.destroy()
set caster = null
set target = null
set recycled[numRecycles] = this
set numRecycles = numRecycles + 1
endmethod
/**
* Checks to see whether the target is in the area. Damages or burns
* mana accordingly.
*/
private static method explode takes nothing returns nothing
local thistype this = GetTimerData(GetExpiredTimer())
local real newX = GetUnitX(target)
local real newY = GetUnitY(target)
local real distance = SquareRoot((newX - tx) * (newX - tx) + (newY - ty) * (newY - ty))
call ReleaseTimer(GetExpiredTimer())
call DestroyEffect(AddSpecialEffect(EXPLODE_EFFECT, tx, ty))
// Target is in area:
if (distance <= area) then
call DealDamage(caster, target, GetDamage(level))
call DestroyEffect(AddSpecialEffect(HIT_EFFECT, tx, ty))
static if (DISPLAY_TEXT) then
call DisplayText(target, COLOUR_DAMAGE + I2S(R2I(GetDamage(level))) + "!|r")
endif
// Target is outside area:
else
// Target is within max distance:
if (distance <= MAX_DISTANCE) then
call ManaMissile.create(tx, ty, target, ((MAX_DISTANCE - distance) / MAX_DISTANCE) * GetManaBurn(level))
endif
call DestroyEffect(AddSpecialEffect(MISS_EFFECT, tx, ty))
endif
call destroy()
endmethod
/**
* Displays the estimated area.
*/
private method displayArea takes nothing returns nothing
local player owner = GetOwningPlayer(caster)
local integer i = 0
set fx = fxList.create()
loop
exitwhen i == NUM_EFFECTS
set fx[i] = Effect.create(owner, tx + area * Cos(i * ANGLE_DIV), ty + area * Sin(i * ANGLE_DIV), /*
*/ EFFECT_SCALE, EFFECT_HEIGHT, INDICATOR_ART)
set i = i + 1
endloop
set locFx = Effect.create(owner, tx, ty, L_FX_SCALE, L_FX_HEIGHT, INDICATOR_FX)
set owner = null
endmethod
/**
* Checks whether the target moved. Creates the estimated area accordingly.
*/
private static method checkPosition takes nothing returns nothing
local thistype this = GetTimerData(GetExpiredTimer())
local real newX
local real newY
local real angle
local real distance
set newX = GetUnitX(target)
set newY = GetUnitY(target)
// Target has moved:
if (newX != tx and newY != ty) then
set angle = Atan2(tx - newX, ty - newY)
set distance = SquareRoot((newX - tx) * (newX - tx) + (newY - ty) * (newY - ty)) / INITIAL_DELAY * GetDelay(level)
set tx = tx + distance * Cos(angle)
set ty = ty + distance * Sin(angle)
endif
call displayArea()
call TimerStart(GetExpiredTimer(), GetDelay(level), false, function thistype.explode)
endmethod
/**
* Allocate a new instance and save the target's position. Start a timer to check
* whether it is moving.
*/
private static method create takes nothing returns thistype
local thistype this
if (numRecycles == 0) then
set numInstances = numInstances + 1
set this = numInstances
else
set numRecycles = numRecycles - 1
set this = recycled[numRecycles]
endif
set caster = GetTriggerUnit()
set level = GetUnitAbilityLevel(caster, SPELL_ID)
set area = GetArea(level)
set target = GetSpellTargetUnit()
set tx = GetUnitX(target)
set ty = GetUnitY(target)
call TimerStart(NewTimerEx(this), INITIAL_DELAY, false, function thistype.checkPosition)
return this
endmethod
static if (not LIBRARY_SpellEffectEvent) then
private static method checkSpell takes nothing returns boolean
if (GetSpellAbilityId() == SPELL_ID) then
call create()
endif
return false
endmethod
endif
/**
* Register the spell and preload effects.
*/
private static method onInit takes nothing returns nothing
static if (LIBRARY_SpellEffectEvent) then
call RegisterSpellEffectEvent(SPELL_ID, function thistype.create)
else
local trigger t = CreateTrigger()
call TriggerAddCondition(t, Condition(function thistype.checkSpell))
call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_EFFECT)
endif
call Preload(INDICATOR_FX)
call Preload(INDICATOR_ART)
call Preload(EXPLODE_EFFECT)
call Preload(HIT_EFFECT)
call Preload(MISS_EFFECT)
call Preload(MISSILE_ART)
call Preload(MANA_BURN_FX)
endmethod
endstruct
endlibrary