Name | Type | is_array | initial_value |
//TESH.scrollpos=0
//TESH.alwaysfold=0
library_once TimerUtils initializer init
//*********************************************************************
//* TimerUtils (Blue flavor for 1.23b or later)
//* ----------
//*
//* To implement it , create a custom text trigger called TimerUtils
//* and paste the contents of this script there.
//*
//* To copy from a map to another, copy the trigger holding this
//* library to your map.
//*
//* (requires vJass) More scripts: htt://www.wc3campaigns.net
//*
//* For your timer needs:
//* * Attaching
//* * Recycling (with double-free protection)
//*
//* set t=NewTimer() : Get a timer (alternative to CreateTimer)
//* ReleaseTimer(t) : Relese a timer (alt to DestroyTimer)
//* SetTimerData(t,2) : Attach value 2 to timer
//* GetTimerData(t) : Get the timer's value.
//* You can assume a timer's value is 0
//* after NewTimer.
//*
//* Blue Flavor: Slower than the red flavor, it got a 408000 handle id
//* limit, which means that if more than 408000 handle ids
//* are used in your map, TimerUtils might fail, this
//* value is quite big and it is much bigger than the
//* timer limit in Red flavor.
//*
//********************************************************************
//==================================================================================================
globals
private hashtable hasht //I <3 blizz
endglobals
//It is dependent on jasshelper's recent inlining optimization in order to perform correctly.
function SetTimerData takes timer t, integer value returns nothing
call SaveInteger(hasht,0, GetHandleId(t), value)
endfunction
function GetTimerData takes timer t returns integer
return LoadInteger(hasht, 0, GetHandleId(t))
endfunction
//==========================================================================================
globals
private timer array tT
private integer tN = 0
private constant integer HELD=0x28829022
//use a totally random number here, the more improbable someone uses it, the better.
endglobals
//==========================================================================================
function NewTimer takes nothing returns timer
if (tN==0) then
set tT[0]=CreateTimer()
else
set tN=tN-1
endif
call SetTimerData(tT[tN],0)
return tT[tN]
endfunction
//==========================================================================================
function ReleaseTimer takes timer t returns nothing
if(t==null) then
debug call BJDebugMsg("Warning: attempt to release a null timer")
return
endif
if (tN==8191) then
debug call BJDebugMsg("Warning: Timer stack is full, destroying timer!!")
//stack is full, the map already has much more troubles than the chance of bug
call DestroyTimer(t)
else
call PauseTimer(t)
if(GetTimerData(t)==HELD) then
debug call BJDebugMsg("Warning: ReleaseTimer: Double free!")
return
endif
call SetTimerData(t,HELD)
set tT[tN]=t
set tN=tN+1
endif
endfunction
private function init takes nothing returns nothing
set hasht = InitHashtable()
endfunction
endlibrary
//TESH.scrollpos=34
//TESH.alwaysfold=0
History
Versions 1.0, 1.2, 1.3, 1.4:
- This versions all had different approaches to try solving the same problem "How to make a lightning spell ?" but they all proved either to be inefficient or to fail
- Yes, I actually remade from zero the spell 3 times (without counting with version 1.0)
Version 1.5:
- First release to the spell Olympics
- Math formula improved
- Removed a useless timer
- Cleaned and commented the code
Version 1.6:
- The Math formula for damage reduction was still, wrong, so now it is corrected and now most things work as they should
Version 1.7:
- Code optimizations
- Fixed a glitch with the dummy unit, now it actually dies
Version 1.8:
- Made the function Targets easier to use
- Now the code has its own algorithm and re uses groups
- Added new function SetProjectile
- Many other minor code fixes and updates were done
- Added Anitarf to the credits
Version 1.9
- Changed the model of the missile to make it look better and now it works with flying units by changing its flying height properly
- Now the missile always faces the direction of the victim
- Improved the map and the terrain
- Changed the algorithm for picking units, now he picks close units to the target
- Now the effects appear on the units
- Now I also preload the units missile and the dummy
Version 2.0
- Now the abilities for the dummy unit are also preloaded
- Replaced SetUnitPosition with SetUnitX and SetUnitY, now the spell is faster!
Version 2.1
- Replaced some variables in order to make the preloading of the dummy unit faster.
- Cleaned the code and deleted old comments and code fragments as well as eliminated the "data = this" laziness
Version 2.2
- Now the code really works well with 1 single timer. Thx Pyrogasm and Anitarf!
- This spell would have not been possible without the people on the credits, Anitarf, Daelin, Deaod and Pyrogasm, thx to you all!
Version 2.3:
- Fixed a leak in method NextTarget
- Moved the timer code to the struct
- Tranformed some of the SETUP functions into constants
- Fixed a spelling mistake , replaced TIMER_CICLE, by TIMER_CYCLE
- Added JESP document
Version 2.31:
- Fixed the tooltips of the spell
Version 2.32:
- Updated for patch 1.24
//TESH.scrollpos=0
//TESH.alwaysfold=0
This is the JESP standard document, if a map contains this document it means that there are
spells that follow this standard.
Spells of this map that follow the standard:
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
- "Dark Lightning"
Advantages of the Standard
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
- Implementing spells that follow the standard is relatively easier than implementing JASS
spells that don't follow the standard.
- Configuring/Balancing spells that follow the standard is relatively easier than
implementing JASS spells that don't follow the standard.
- Users may do the following procedure to make a new ability that uses the spell's script :
* Create a new Trigger with a name (case sensitive)
* Convert that trigger to custom text.
* Copy the spell's script to a text editor like Notepad or your OS equivalent.
* Replace the spell's Code name with the name you used on the trigger.
* Copy the new text to the new trigger
* Duplicate the Spell's original objects to have new ones for the new spell script.
You are now able to use that new version of the spell.
- In case two guys give the same name to 2 different spells, there are no conflict problems
because you can easily change the name of one of them
What is the JESP Standard?
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
The JESP standard was designed to make spell sharing much better. And to make sure JASS
enhanced spells follow a rule, to prevent chaos.
What does JESP Standard stands for?
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
JASS
Enhanced
Spell
Pseudotemplate
Requirements for a spell to follow the JESP Standard
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
- The spell is written in JASS
- The spell is 100% multi instanceable.
- The spell script is ready to support spells of any number of levels.
(default config header is not required to support all of them)
- The Spell has an specific code name.
- The Spell's trigger must have the spell's codename as name
- The Spell's InitTrig function must be named: InitTrig_<CodeName>
- The spell has a configuration header.
- It is mandatory that rawcodes of objects are configurable in the header.
- All the spell's specific code is inside the spell's "Trigger" (Trigger== that custom text
slot that world editor calls Trigger, the spell may use as many 'trigger' OBJECTS as needed)
- Every spell-specific single identifier or key works in such a way that reproducing the
spell's trigger but after performing a text-replace of codename with another name (and thus
renaming the cloned trigger to the new code name) it won't cause compile errors / conflicts
when playing the map.
- There is no code inside the spell's "Trigger" that is not specific to the spell.
- There are no requirements for GUI variables that are specific to the spell. If a system
used by the spell requires GUI variables the code for the system must be outside the "Trigger"
- Eyecandy and spell's balance have to be easy to configure
- The name of the author should be included in the spell's script.
- The reason to exist of this standard is spell sharing. This document should be included within the map. And it should specify which spell follows the standard, in the top list.
//TESH.scrollpos=0
//TESH.alwaysfold=0
//===========================================================================
//A JESP spell that allows the user to create lightning spells with any model
//he desires. In this sample, the caster sends a purple projectile which will
//damage enemy units and heal the caster by an amount of damage they received.
//If an enemy unit dies due this ability, it will return as an Undead to aid
//the caster, unless it is a summon, a hero or a flying unit.
//
//Requires TimerUtils
//
//@author Flame_Phoenix
//
//@credits
//- Deaod, for all hi help in the code, with math formulas and advices
//- Anitarf, for math formulas and advices for efficiency
//- Pyrogasm, for giving me the algorithm for making the spell with 1 timer only
//- Daelin, for the math formulas on his outdated spells
//- Vexorian, for the idea of preloading units and abilities and for TimerUtils
//
//@version 2.32
//===========================================================================
scope DarkLightning initializer Init
//===========================================================================
//=============================SETUP START===================================
//===========================================================================
globals
private constant integer AID = 'A000' //rw of teh ability
private constant real SPEED = 700. //speed of the missile
private constant integer MISSILE_ID = 'h000' //rw of the missile
private constant integer DUM_ID = 'h001' //rw of the dummy unit
private constant integer DUM_AB = 'A001' //Ability of the dumy unit (animated dead)
private constant string DUM_ORDER = "animatedead" //string order of the dummy unit
private constant real TIMER_CYCLE = 0.03 //cicles of the timer
private constant string DRAIN_EFFECT = "Abilities\\Spells\\Demon\\DarkPortal\\DarkPortalTarget.mdl"
private constant string BLOOD_EFFECT = "Objects\\Spawnmodels\\Orc\\Orcblood\\BattrollBlood.mdl"
private constant attacktype A_TYPE = ATTACK_TYPE_MAGIC //the attack type of the spell
private constant damagetype D_TYPE = DAMAGE_TYPE_UNIVERSAL //the damage type of the spell
endglobals
private constant function Range takes integer level returns real
//If there is more than one Target, a next target will be picked in a 500
//AOE from the first
return 500. + (level * 0)
endfunction
private constant function Damage takes integer level returns real
//Damage each Target will take
return 100. * level
endfunction
private constant function Heal takes integer level, real damage returns real
//the heal the caster will get when damaging enemies
// in this case in level 1 caster gains 33% of damage done, in level
//2 he gains 66% of damage done and in level 3 he gains 99% of the
//damage deal to the target
return level * 0.33 * damage
endfunction
private constant function Reduction takes integer level returns real
//Damage reduction per Target
return 0.15 + (level * 0)
endfunction
private constant function TargetsNumber takes integer level returns integer
//The number of targets
return 4 + (level * 1)
endfunction
private function Targets takes unit caster, unit target returns boolean
//"caster" is the caster of the spell, and "target" is the target being evauated
return IsUnitEnemy(target, GetOwningPlayer(caster)) and (IsUnitType(target, UNIT_TYPE_STRUCTURE) == false) and (IsUnitType(target, UNIT_TYPE_MAGIC_IMMUNE) == false) and (GetWidgetLife(target) > 0.405)
endfunction
//===========================================================================
//=============================SETUP END=====================================
//===========================================================================
globals
private group g
private boolexpr b
private unit tmpCaster = null
private timer t
private integer instancesCount
endglobals
//===========================================================================
private function AceptedTargets takes nothing returns boolean
return Targets(tmpCaster, GetFilterUnit())
endfunction
//===========================================================================
private struct SpellData
//static SpellData array datas //an array which will contain the instances
unit caster //our caster !
unit vic //the current victim
integer level //the level of the ability
group picked //saves all targeted units so far, so they don't get picked again
unit missile //the missile
real wait //tells us how much time we must wait
integer targNum //Current number of the target
real lastDamage //this tells us the last amount of damage a unit received. NOTE: this is NOT the damage a unit is taking.
boolean done
static method create takes unit caster, unit vic returns SpellData
local SpellData data = SpellData.allocate()
//setting variables
set data.caster = caster
set data.vic = vic
set data.level = GetUnitAbilityLevel(caster, AID)
set data.missile = CreateUnit(GetOwningPlayer(caster), MISSILE_ID, GetUnitX(caster), GetUnitY(caster), 0)
set data.wait = 0.
set data.targNum = 0
set data.done = false
//we recycle the group
if data.picked == null then
set data.picked = CreateGroup()
endif
return data
endmethod
method SetProjectile takes nothing returns nothing
local real a = GetUnitX(.missile) - GetUnitX(.vic)
local real b = GetUnitY(.missile) - GetUnitY(.vic)
local real d = SquareRoot(a*a + b*b) //the distance between "a" and "b"
set .wait = d / SPEED
//we adapt the fly height of the missile to the height of the target!
call SetUnitFlyHeight(.missile, GetUnitFlyHeight(.vic), (GetUnitFlyHeight(.missile) - GetUnitFlyHeight(.vic)) / .wait)
endmethod
method TargetEffect takes nothing returns nothing
local unit dum
//the hp the enemies will lose and that the caster will win
if (.targNum == 0) then
set .lastDamage = Damage(.level)
else
set .lastDamage = .lastDamage - (.lastDamage * Reduction(.level))
endif
//here we damage the bad guy ! Die you bastard !!!!
//we also created the effects for both targets and caster
call UnitDamageTarget(.caster, .vic, .lastDamage, true, false, A_TYPE, D_TYPE, null)
call DestroyEffect(AddSpecialEffectTarget(DRAIN_EFFECT, .vic, "origin"))
call DestroyEffect(AddSpecialEffectTarget(BLOOD_EFFECT, .caster, "origin"))
//Heal the caster
call SetWidgetLife(.caster, GetWidgetLife(.caster) + Heal(.level, .lastDamage))
if (GetWidgetLife(.vic) < 0.405) and (IsUnitType(.vic, UNIT_TYPE_HERO) == false) and (IsUnitType(.vic, UNIT_TYPE_SUMMONED) == false) and (IsUnitType(.vic, UNIT_TYPE_FLYING) == false) then
set dum = CreateUnit(GetOwningPlayer(.caster), DUM_ID, GetUnitX(.vic), GetUnitY(.vic), 0)
call UnitAddAbility(dum, DUM_AB)
call SetUnitAbilityLevel(dum, DUM_AB, .level)
call IssueImmediateOrder(dum, DUM_ORDER)
call UnitApplyTimedLife(dum, 'BTLF', 1.)
endif
set dum = null
endmethod
method NextTarget takes nothing returns nothing
local unit f = null
local unit ret = null
//the position of the current target
local real cX = GetUnitX(.vic)
local real cY = GetUnitY(.vic)
//the position of our new target
local real nX
local real nY
//by saving the minimal distance, this will help us choose
//the closest new target to our current target
//note we start it with the biggest value possible, so
//we can find the minimum after
//Also note that I am calculating minDist^2 so I can avoid
//the use of a squareroot which will save speed. Know that if
//minDist <, >, <=, >= d then minDis^2 <, >, <=, >= d^2 and vice-versa
local real minDist = Range(.level)*Range(.level)
local real d //this will be a temporary variable for the distances we will calculate
//here we pick all units near the last target
set tmpCaster = .caster
call GroupEnumUnitsInRange(g, GetUnitX(.vic), GetUnitY(.vic), Range(.level), b)
loop
set f = FirstOfGroup(g)
exitwhen(f == null)
call GroupRemoveUnit(g, f)
if (IsUnitInGroup(f, .picked) == false) then
set nX = GetUnitX(f)
set nY = GetUnitY(f)
//now we calculate the distance between f and our current target
//note that: d^2 = (x2-x1)^2 + (y2-y1)^2
//I avoid using a square to make this faster
set d = (nX - cX)*(nX - cX) + (nY - cY)*(nY - cY)
//if this new distance is the smallest, then we update our target
if (d < minDist) then
set minDist = d
set ret = f
endif
endif
endloop
set .vic = ret
set ret = null
endmethod
method ChainEffect takes nothing returns nothing
//we add the victim to the victims groups, so we won't pick it twice
call GroupAddUnit(.picked, .vic)
//here we call the function responsable for the bad things we do to the bad guys xD
call .TargetEffect()
//now we increase the counter to know how many units we hit
set .targNum = .targNum + 1
//if the number of our current target is lower than the maximum amount
//of targets we can hit, we continue, else we end everything
if (.targNum < TargetsNumber(.level)) then
//pick new target !
call .NextTarget()
//if the new unit is not null, we repeat this step, else we end
if (.vic != null) then
call .SetProjectile()
else
set .done = true
endif
else
set .done = true
endif
endmethod
method MoveMissile takes nothing returns nothing
local real x1 = GetUnitX(.missile)
local real x2 = GetUnitX(.vic)
local real y1 = GetUnitY(.missile)
local real y2 = GetUnitY(.vic)
local real dx = TIMER_CYCLE * (x2 - x1) / .wait
local real dy = TIMER_CYCLE * (y2 - y1) / .wait
call SetUnitX(.missile, x1 + dx)
call SetUnitY(.missile, y1 + dy)
//here we set the facing of the missile
call SetUnitFacing(.missile, bj_RADTODEG * Atan2(y2 - y1, x2 - x1))
set .wait = .wait - TIMER_CYCLE
//this is when the missile gets to the unit
if .wait < TIMER_CYCLE then
call SetUnitX(.missile, x2)
call SetUnitY(.missile, y2)
//This runs the ChainEffect function again!
call .ChainEffect()
endif
endmethod
method onDestroy takes nothing returns nothing
//we clear the gorup so we can use it later
call GroupClear(.picked)
//we destroy the projectile
call ShowUnit(.missile, false)
call KillUnit(.missile)
endmethod
endstruct
//===========================================================================
globals
private SpellData array datas
endglobals
//===========================================================================
private function Periodic takes nothing returns nothing
local integer currentIndex = 0
local SpellData currentInstance
loop
set currentInstance = datas[currentIndex]
call currentInstance.MoveMissile()
//if our instance is done, we decrement the number of total instances
//and then we check if there are any more instances being run
if (currentInstance.done) then
set instancesCount = instancesCount - 1
//if there are, then we update our instance to the next of the array
//and we correct the index
if (instancesCount > 0) then
set datas[currentIndex] = datas[instancesCount]
set currentIndex = currentIndex - 1
//else we just release the timer
else
call ReleaseTimer(t)
endif
//now before we leave current instance for good, we destroy it!
call currentInstance.destroy()
endif
set currentIndex = currentIndex + 1
exitwhen currentIndex >= instancesCount
endloop
endfunction
//===========================================================================
private function Conditions takes nothing returns boolean
if GetSpellAbilityId() == AID then
//if there are no instances of the spell then it means the timer does not
//exist, so we create it and start it!
if instancesCount == 0 then
set t = NewTimer()
call TimerStart(t, TIMER_CYCLE, true, function Periodic)
endif
set datas[instancesCount] = SpellData.create(GetTriggerUnit(), GetSpellTargetUnit())
call datas[instancesCount].SetProjectile()
set instancesCount = instancesCount + 1
endif
return false
endfunction
//===========================================================================
private function Init takes nothing returns nothing
local trigger LightningTrg = CreateTrigger( )
call TriggerRegisterAnyUnitEventBJ( LightningTrg, EVENT_PLAYER_UNIT_SPELL_EFFECT )
call TriggerAddCondition( LightningTrg, Condition( function Conditions ) )
//setting globals
set b = Condition(function AceptedTargets)
set g = CreateGroup()
set instancesCount = 0
//Preload the effects
call Preload(DRAIN_EFFECT)
call Preload(BLOOD_EFFECT)
//preloading units and spells
set bj_lastCreatedUnit = CreateUnit(Player(0), DUM_ID, 0, 0, 0)
call UnitAddAbility(bj_lastCreatedUnit, DUM_AB)
call KillUnit(bj_lastCreatedUnit)
call KillUnit(CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), MISSILE_ID, 0, 0, 0))
endfunction
endscope