unsafeFrameScale = BlzFrameSetScale
BlzFrameSetScale = function() end
Name | Type | is_array | initial_value |
do; local _, codeLoc = pcall(error, "", 2) --get line number where DebugUtils begins.
Debug = {
--BEGIN OF SETTINGS--
settings = {
SHOW_TRACE_ON_ERROR = true ---Set to true to show a stack trace on every error in addition to the regular message (msg sources: automatic error handling, Debug.try, Debug.throwError, ...)
, USE_TRY_ON_TRIGGERADDACTION = true ---Set to true for automatic error handling on TriggerAddAction (applies Debug.try on every trigger action).
, USE_TRY_ON_CONDITION = true ---Set to true for automatic error handling on boolexpressions created via Condition() or Filter() (essentially applies Debug.try on every trigger condition).
, USE_TRY_ON_TIMERSTART = true ---Set to true for automatic error handling on TimerStart (applies Debug.try on every timer callback).
, USE_TRY_ON_COROUTINES = true ---Set to true for improved stack traces on errors within coroutines (applies Debug.try on coroutine.create and coroutine.wrap). This lets stack traces point to the erroneous function executed within the coroutine (instead of the function creating the coroutine).
, ALLOW_INGAME_CODE_EXECUTION = true ---Set to true to enable IngameConsole and -exec command.
, WARNING_FOR_UNDECLARED_GLOBALS = true ---Set to true to print warnings upon accessing undeclared globals (i.e. globals with nil-value). This is technically the case after having misspelled on a function name (like CraeteUnit instead of CreateUnit).
, SHOW_TRACE_FOR_UNDECLARED_GLOBALS = false ---Set to true to include a stack trace into undeclared global warnings. Only takes effect, if WARNING_FOR_UNDECLARED_GLOBALS is also true.
, USE_PRINT_CACHE = true ---Set to true to let print()-calls during loading screen be cached until the game starts.
, PRINT_DURATION = nil ---Adjust the duration in seconds that values printed by print() last on screen. Set to nil to use default duration (which depends on string length).
, USE_NAME_CACHE = true ---Set to true to let tostring/print output the string-name of an object instead of its memory location (except for booleans/numbers/strings). E.g. print(CreateUnit) will output "function: CreateUnit" instead of "function: 0063A698".
, AUTO_REGISTER_NEW_NAMES = true ---Automatically adds new names from global scope (and subtables of _G up to NAME_CACHE_DEPTH) to the name cache by adding metatables with the __newindex metamethod to ALL tables accessible from global scope.
, NAME_CACHE_DEPTH = 0 ---Set to 0 to only affect globals. Experimental feature: Set to an integer > 0 to also cache names for subtables of _G (up to the specified depth). Warning: This will alter the __newindex metamethod of subtables of _G (but not break existing functionality).
}
--END OF SETTINGS--
--START OF CODE--
, data = {
nameCache = {} ---@type table<any,string> contains the string names of any object in global scope (random for objects that have multiple names)
, nameCacheMirror = {} ---@type table<string,any> contains the (name,object)-pairs of all objects in the name cache. Used to prevent name duplicates that might otherwise occur upon reassigning globals.
, nameDepths = {} ---@type table<any,integer> contains the depth of the name used by by any object in the name cache (i.e. the depth within the parentTable).
, autoIndexedTables = {} ---@type table<table,boolean> contains (t,true), if DebugUtils already set a __newindex metamethod for name caching in t. Prevents double application.
, paramLog = {} ---@type table<string,string> saves logged information per code location. to be filled by Debug.log(), to be printed by Debug.try()
, sourceMap = {{firstLine= 1,file='DebugUtils'}} ---@type table<integer,{firstLine:integer,file:string,lastLine?:integer}> saves lines and file names of all documents registered via Debug.beginFile().
, printCache = {n=0} ---@type string[] contains the strings that were attempted to print during loading screen.
}
}
--localization
local settings, paramLog, nameCache, nameDepths, autoIndexedTables, nameCacheMirror, sourceMap, printCache = Debug.settings, Debug.data.paramLog, Debug.data.nameCache, Debug.data.nameDepths, Debug.data.autoIndexedTables, Debug.data.nameCacheMirror, Debug.data.sourceMap, Debug.data.printCache
--Write DebugUtils first line number to sourceMap:
---@diagnostic disable-next-line: need-check-nil
Debug.data.sourceMap[1].firstLine = tonumber(codeLoc:match(":\x25d+"):sub(2,-1))
-------------------------------------------------
--| File Indexing for local Error Msg Support |--
-------------------------------------------------
-- Functions for war3map.lua -> local file conversion for error messages.
---Returns the line number in war3map.lua, where this is called (for depth = 0).
---Choose a depth > 0 to instead return the line, where the corresponding function in the stack leading to this call is executed.
---@param depth? integer default: 0.
---@return number?
function Debug.getLine(depth)
depth = depth or 0
local _, location = pcall(error, "", depth + 3) ---@diagnostic disable-next-line: need-check-nil
local line = location:match(":\x25d+") --extracts ":1000" from "war3map.lua:1000:..."
return tonumber(line and line:sub(2,-1)) --check if line is nil before applying string.sub to prevent errors (nil can result from string.match above, although it should never do so in our case)
end
function Debug.beginFile(fileName, depth, lastLine)
depth, fileName = depth or 0, fileName or '' --filename is not actually optional, we just default to '' to prevent crashes.
local line = Debug.getLine(depth + 1)
if line then --for safety reasons. we don't want to add a non-existing line to the sourceMap
table.insert(sourceMap, {firstLine = line, file = fileName, lastLine = lastLine}) --automatically sorted list, because calls of Debug.beginFile happen logically in the order of the map script.
end
end
function Debug.benchmark(func, iterations, ...)
local start = os.clock()
for i = 1, iterations do
func(...)
end
local duration = os.clock() - start
print("Benchmarked: "..tostring(func).."\nTotal Seconds: "..tostring(duration).."\nTotal MS: "..tostring(duration * 1000).."\nIterations: "..tostring(iterations).."\nAverage MS: "..tostring((duration * 1000) / iterations))
end
function Debug.endFile(depth)
depth = depth or 0
local line = Debug.getLine(depth + 1)
sourceMap[#sourceMap].lastLine = line
end
function Debug.getLocalErrorMsg(errorMsg)
local startPos, endPos = errorMsg:find(":\x25d*") --start and end position of line number. The part before that is the document, part after the error msg.
if startPos and endPos then --can be nil, if input string was not of the desired form "<document>:<linenumber><RestOfMsg>".
local document, line, rest = errorMsg:sub(1, startPos), tonumber(errorMsg:sub(startPos+1, endPos)), errorMsg:sub(endPos+1, -1) --get error line in war3map.lua
if document == 'war3map.lua:' and line then --only convert war3map.lua-references to local position. Other files such as Blizzard.j.lua are not converted (obiously).
for i = #sourceMap, 1, -1 do --find local file containing the war3map.lua error line.
if line >= sourceMap[i].firstLine then --war3map.lua line is part of sourceMap[i].file
if not sourceMap[i].lastLine or line <= sourceMap[i].lastLine then --if lastLine is given, we must also check for it
return sourceMap[i].file .. ":" .. (line - sourceMap[i].firstLine + 1) .. rest
else --if line is larger than firstLine and lastLine of sourceMap[i], it is not part of a tracked file -> return global war3map.lua position.
break --prevent return within next step of the loop ("line >= sourceMap[i].firstLine" would be true again, but wrong file)
end
end
end
end
end
return errorMsg
end
local convertToLocalErrorMsg = Debug.getLocalErrorMsg
local concat
concat = function(firstParam, ...)
if select('#', ...) == 0 then
return tostring(firstParam)
end
return tostring(firstParam) .. ' ' .. concat(...)
end
local function getStackTrace(startDepth, endDepth)
local trace, separator = "", ""
local _, lastFile, tracePiece, lastTracePiece
for loopDepth = startDepth, endDepth do --get trace on different depth level
_, tracePiece = pcall(error, "", loopDepth) ---@type boolean, string
tracePiece = convertToLocalErrorMsg(tracePiece)
if #tracePiece > 0 and lastTracePiece ~= tracePiece then --some trace pieces can be empty, but there can still be valid ones beyond that
trace = trace .. separator .. ((tracePiece:match("^.-:") == lastFile) and tracePiece:match(":\x25d+"):sub(2,-1) or tracePiece:match("^.-:\x25d+"))
lastFile, lastTracePiece, separator = tracePiece:match("^.-:"), tracePiece, " <- "
end
end
return trace
end
local function errorHandler(errorMsg, startDepth)
startDepth = startDepth or 4 --xpcall doesn't specify this param, so it defaults to 4 in this case
errorMsg = convertToLocalErrorMsg(errorMsg)
--Print original error message and stack trace.
print("|cffff5555ERROR at " .. errorMsg .. "|r")
if settings.SHOW_TRACE_ON_ERROR then
print("|cffff5555Traceback (most recent call first):|r")
print("|cffff5555" .. getStackTrace(startDepth,200) .. "|r")
end
--Also print entries from param log, if there are any.
for location, loggedParams in pairs(paramLog) do
print("|cff888888Logged at " .. convertToLocalErrorMsg(location) .. loggedParams .. "|r")
paramLog[location] = nil
end
end
function Debug.try(funcToExecute, ...)
return select(2, xpcall(funcToExecute, errorHandler,...))
end
try = Debug.try
function Debug.throwError(...)
errorHandler(getStackTrace(4,4) .. ": " .. concat(...), 5)
end
function Debug.assert(condition, errorMsg, ...)
if condition then
return ...
else
errorHandler(getStackTrace(4,4) .. ": " .. errorMsg, 5)
end
end
function Debug.traceback()
return getStackTrace(3,200)
end
function Debug.log(...)
local _, location = pcall(error, "", 3) ---@diagnostic disable-next-line: need-check-nil
paramLog[location or ''] = concat(...)
end
local skipType = {boolean = true, string = true, number = true, ['nil'] = true}
--Set weak keys to nameCache and nameDepths and weak values for nameCacheMirror to prevent garbage collection issues
setmetatable(nameCache, {__mode = 'k'})
setmetatable(nameDepths, getmetatable(nameCache))
setmetatable(nameCacheMirror, {__mode = 'v'})
local function removeNameIfNecessary(name)
if nameCacheMirror[name] then
nameCache[nameCacheMirror[name]] = nil
nameCacheMirror[name] = nil
end
end
function Debug.registerName(whichObject, name)
if not skipType[type(whichObject)] then
removeNameIfNecessary(name)
nameCache[whichObject] = name
nameCacheMirror[name] = whichObject
nameDepths[name] = 0
end
end
local function addNameToCache(parentTableName, key, object, parentTableDepth)
parentTableDepth = parentTableDepth or -1
--Don't overwrite existing names for the same object, don't add names for primitive types.
if nameCache[object] or skipType[type(object)] then
return
end
local name
--apply dot-syntax for string keys without whitespace
if type(key) == 'string' and not string.find(key, "\x25s") then
if parentTableName == "" then
name = key
nameDepths[object] = 0
else
name = parentTableName .. "." .. key
nameDepths[object] = parentTableDepth + 1
end
--apply bracket-syntax for all other keys. This requires a parentTableName.
elseif parentTableName ~= "" then
name = type(key) == 'string' and ('"' .. key .. '"') or key
name = parentTableName .. "[" .. tostring(name) .. "]"
nameDepths[object] = parentTableDepth + 1
end
--Stop in cases without valid name (like parentTableName = "" and key = [1])
if name then
removeNameIfNecessary(name)
nameCache[object] = name
nameCacheMirror[name] = object
end
end
local function registerAllObjectsInTable(parentTable, parentTableName)
parentTableName = parentTableName or nameCache[parentTable] or ""
--Register all call-by-ref-objects in parentTable
for key, object in pairs(parentTable) do
addNameToCache(parentTableName, key, object, nameDepths[parentTable])
end
end
function Debug.registerNamesFrom(parentTable, parentTableName, depth)
--Support overloaded definition fun(parentTable:table, depth:integer)
if type(parentTableName) == 'number' then
depth = parentTableName
parentTableName = nil
end
--Apply default values
depth = depth or 1
parentTableName = parentTableName or nameCache[parentTable] or ""
--add name of T in case it hasn't already
if not nameCache[parentTable] and parentTableName ~= "" then
Debug.registerName(parentTable, parentTableName)
end
--Register all call-by-ref-objects in parentTable. To be preferred over simple recursive approach to ensure that top level names are preferred.
registerAllObjectsInTable(parentTable, parentTableName)
--if depth > 1 was specified, also register Names from subtables.
if depth > 1 then
for _, object in pairs(parentTable) do
if type(object) == 'table' then
Debug.registerNamesFrom(object, nil, depth - 1)
end
end
end
end
-------------------------------------------
--| Name Caching (Loading Screen setup) |--
-------------------------------------------
---Registers all existing object names from global scope and Lua incorporated libraries to be used by tostring() overwrite below.
local function registerNamesFromGlobalScope()
--Add all names from global scope to the name cache.
Debug.registerNamesFrom(_G, "")
--Add all names of Warcraft-enabled Lua libraries as well:
--Could instead add a depth to the function call above, but we want to ensure that these libraries are added even if the user has chosen depth 0.
for _, lib in ipairs({coroutine, math, os, string, table, utf8, Debug}) do
Debug.registerNamesFrom(lib)
end
--Add further names that are not accessible from global scope:
--Player(i)
for i = 0, GetBJMaxPlayerSlots() - 1 do
Debug.registerName(Player(i), "Player(" .. i .. ")")
end
end
--Set empty metatable to _G. __index is added when game starts (for "attempt to read undeclared global"-errors), __newindex is added right below (for building the name cache).
setmetatable(_G, getmetatable(_G) or {}) --getmetatable(_G) should always return nil provided that DebugUtils is the topmost script file in the trigger editor, but we still include this for safety-
-- Save old tostring into Debug Library before overwriting it.
Debug.oldTostring = tostring
if settings.USE_NAME_CACHE then
local oldTostring = tostring
tostring = function(obj) --new tostring(CreateUnit) prints "function: CreateUnit"
--tostring of non-primitive object is NOT guaranteed to be like "<type>:<hex>", because it might have been changed by some __tostring-metamethod.
if settings.USE_NAME_CACHE then --return names from name cache only if setting is enabled. This allows turning it off during runtime (via Ingame Console) to revert to old tostring.
return nameCache[obj] and ((oldTostring(obj):match("^.-: ") or (oldTostring(obj) .. ": ")) .. nameCache[obj]) or oldTostring(obj)
end
return Debug.oldTostring(obj)
end
--Add names to Debug.data.objectNames within Lua root. Called below the other Debug-stuff to get the overwritten versions instead of the original ones.
registerNamesFromGlobalScope()
--Prepare __newindex-metamethod to automatically add new names to the name cache
if settings.AUTO_REGISTER_NEW_NAMES then
local nameRegisterNewIndex
---__newindex to be used for _G (and subtables up to a certain depth) to automatically register new names to the nameCache.
---Tables in global scope will use their own name. Subtables of them will use <parentName>.<childName> syntax.
---Global names don't support container[key]-notation (because "_G[...]" is probably not desired), so we only register string type keys instead of using prettyTostring.
---@param t table
---@param k any
---@param v any
---@param skipRawset? boolean set this to true when combined with another __newindex. Suppresses rawset(t,k,v) (because the other __newindex is responsible for that).
nameRegisterNewIndex = function(t,k,v, skipRawset)
local parentDepth = nameDepths[t] or 0
--Make sure the parent table has an existing name before using it as part of the child name
if t == _G or nameCache[t] then
local existingName = nameCache[v]
if not existingName then
addNameToCache((t == _G and "") or nameCache[t], k, v, parentDepth)
end
--If v is a table and the parent table has a valid name, inherit __newindex to v's existing metatable (or create a new one), if that wasn't already done.
if type(v) == 'table' and nameDepths[v] < settings.NAME_CACHE_DEPTH then
if not existingName then
--If v didn't have a name before, also add names for elements contained in v by construction (like v = {x = function() end} ).
Debug.registerNamesFrom(v, settings.NAME_CACHE_DEPTH - nameDepths[v])
end
--Apply __newindex to new tables.
if not autoIndexedTables[v] then
autoIndexedTables[v] = true
local mt = getmetatable(v)
if not mt then
mt = {}
setmetatable(v, mt) --only use setmetatable when we are sure there wasn't any before to prevent issues with "__metatable"-metamethod.
end
local existingNewIndex = mt.__newindex
local isTable_yn = (type(existingNewIndex) == 'table')
--If mt has an existing __newindex, add the name-register effect to it (effectively create a new __newindex using the old)
if existingNewIndex then
mt.__newindex = function(t,k,v)
nameRegisterNewIndex(t,k,v, true) --setting t[k] = v might not be desired in case of existing newindex. Skip it and let existingNewIndex make the decision.
if isTable_yn then
existingNewIndex[k] = v
else
return existingNewIndex(t,k,v)
end
end
else
--If mt doesn't have an existing __newindex, add one that adds the object to the name cache.
mt.__newindex = nameRegisterNewIndex
end
end
end
end
--Set t[k] = v.
if not skipRawset then
rawset(t,k,v)
end
end
--Apply metamethod to _G.
local existingNewIndex = getmetatable(_G).__newindex --should always be nil provided that DebugUtils is the topmost script in your trigger editor. Still included for safety.
local isTable_yn = (type(existingNewIndex) == 'table')
if existingNewIndex then
getmetatable(_G).__newindex = function(t,k,v)
nameRegisterNewIndex(t,k,v, true)
if isTable_yn then
existingNewIndex[k] = v
else
existingNewIndex(t,k,v)
end
end
else
getmetatable(_G).__newindex = nameRegisterNewIndex
end
end
end
------------------------------------------------------
--| Native Overwrite for Automatic Error Handling |--
------------------------------------------------------
--A table to store the try-wrapper for each function. This avoids endless re-creation of wrapper functions within the hooks below.
--Weak keys ensure that garbage collection continues as normal.
local tryWrappers = setmetatable({}, {__mode = 'k'}) ---@type table<function,function>
local try = Debug.try
---Takes a function and returns a wrapper executing the same function within Debug.try.
---Wrappers are permanently stored (until the original function is garbage collected) to ensure that they don't have to be created twice for the same function.
---@param func? function
---@return function
local function getTryWrapper(func)
if func then
tryWrappers[func] = tryWrappers[func] or function(...) return try(func, ...) end
end
return tryWrappers[func] --returns nil for func = nil (important for TimerStart overwrite below)
end
--Overwrite TriggerAddAction, TimerStart, Condition and Filter natives to let them automatically apply Debug.try.
--Also overwrites coroutine.create and coroutine.wrap to let stack traces point to the function executed within instead of the function creating the coroutine.
if settings.USE_TRY_ON_TRIGGERADDACTION then
local originalTriggerAddAction = TriggerAddAction
TriggerAddAction = function(whichTrigger, actionFunc)
return originalTriggerAddAction(whichTrigger, getTryWrapper(actionFunc))
end
end
if settings.USE_TRY_ON_TIMERSTART then
local originalTimerStart = TimerStart
TimerStart = function(whichTimer, timeout, periodic, handlerFunc)
originalTimerStart(whichTimer, timeout, periodic, getTryWrapper(handlerFunc))
end
end
if settings.USE_TRY_ON_CONDITION then
local originalCondition = Condition
Condition = function(func)
return originalCondition(getTryWrapper(func))
end
Filter = Condition
end
if settings.USE_TRY_ON_COROUTINES then
local originalCoroutineCreate = coroutine.create
---@diagnostic disable-next-line: duplicate-set-field
coroutine.create = function(f)
return originalCoroutineCreate(getTryWrapper(f))
end
local originalCoroutineWrap = coroutine.wrap
---@diagnostic disable-next-line: duplicate-set-field
coroutine.wrap = function(f)
return originalCoroutineWrap(getTryWrapper(f))
end
end
------------------------------------------
--| Cache prints during Loading Screen |--
------------------------------------------
-- Apply the duration as specified in the settings.
if settings.PRINT_DURATION then
local display, getLocalPlayer, dur = DisplayTimedTextToPlayer, GetLocalPlayer, settings.PRINT_DURATION
print = function(...)
display(getLocalPlayer(), 0, 0, dur, concat(...))
end
end
-- Delay loading screen prints to after game start.
if settings.USE_PRINT_CACHE then
local oldPrint = print
--loading screen print will write the values into the printCache
print = function(...)
if bj_gameStarted then
oldPrint(...)
else --during loading screen only: concatenate input arguments 4-space-separated, implicitely apply tostring on each, cache to table
printCache.n = printCache.n + 1
printCache[printCache.n] = concat(...)
end
end
end
-------------------------
--| Modify Game Start |--
-------------------------
local originalMarkGameStarted = MarkGameStarted
--Hook certain actions into the start of the game.
MarkGameStarted = function()
originalMarkGameStarted()
if settings.WARNING_FOR_UNDECLARED_GLOBALS then
local existingIndex = getmetatable(_G).__index
local isTable_yn = (type(existingIndex) == 'table')
getmetatable(_G).__index = function(t, k) --we made sure that _G has a metatable further above.
--if string.sub(tostring(k),1,3) ~= 'bj_' then
print("Trying to read undeclared global at " .. getStackTrace(4,4) .. ": " .. tostring(k)
.. (settings.SHOW_TRACE_FOR_UNDECLARED_GLOBALS and "\nTraceback (most recent call first):\n" .. getStackTrace(4,200) or ""))
--end
if existingIndex then
if isTable_yn then
return existingIndex[k]
end
return existingIndex(t,k)
end
return rawget(t,k)
end
end
--Add names to Debug.data.objectNames again to ensure that overwritten natives also make it to the name cache.
--Overwritten natives have a new value, but the old key, so __newindex didn't trigger. But we can be sure that objectNames[v] doesn't yet exist, so adding again is safe.
if settings.USE_NAME_CACHE then
for _,v in pairs(_G) do
nameCache[v] = nil
end
registerNamesFromGlobalScope()
end
--Print messages that have been cached during loading screen.
if settings.USE_PRINT_CACHE then
--Note that we don't restore the old print. The overwritten variant only applies caching behaviour to loading screen prints anyway and "unhooking" always adds other risks.
for _, str in ipairs(printCache) do
print(str)
end
printCache = nil --frees reference for the garbage collector
end
--Create triggers listening to "-console" and "-exec" chat input.
if settings.ALLOW_INGAME_CODE_EXECUTION and IngameConsole then
IngameConsole.createTriggers()
end
end
---------------------
--| Other Utility |--
---------------------
do
---Returns the type of a warcraft object as string, e.g. "unit" upon inputting a unit.
---@param input any
---@return string
function Debug.wc3Type(input)
local typeString = type(input)
if typeString == 'userdata' then
typeString = tostring(input) --tostring returns the warcraft type plus a colon and some hashstuff.
return typeString:sub(1, (typeString:find(":", nil, true) or 0) -1) --string.find returns nil, if the argument is not found, which would break string.sub. So we need to replace by 0.
else
return typeString
end
end
Wc3Type = Debug.wc3Type --for backwards compatibility
local conciseTostring, prettyTostring
---Translates a table into a comma-separated list of its (key,value)-pairs. Also translates subtables up to the specified depth.
---E.g. {"a", 5, {7}} will display as '{(1, "a"), (2, 5), (3, {(1, 7)})}'.
---@param object any
---@param depth? integer default: unlimited. Unlimited depth will throw a stack overflow error on self-referential tables.
---@return string
conciseTostring = function (object, depth)
depth = depth or -1
if type(object) == 'string' then
return '"' .. object .. '"'
elseif depth ~= 0 and type(object) == 'table' then
local elementArray = {}
local keyAsString
for k,v in pairs(object) do
keyAsString = type(k) == 'string' and ('"' .. tostring(k) .. '"') or tostring(k)
table.insert(elementArray, '(' .. keyAsString .. ', ' .. conciseTostring(v, depth -1) .. ')')
end
return '{' .. table.concat(elementArray, ', ') .. '}'
end
return tostring(object)
end
---Creates a list of all (key,value)-pairs from the specified table. Also lists subtable entries up to the specified depth.
---Major differences to concise print are:
--- * Format: Linebreak-formatted instead of one-liner, uses "[key] = value" instead of "(key,value)"
--- * Will also unpack tables used as keys
--- * Also includes the table's memory position as returned by tostring(table).
--- * Tables referenced multiple times will only be unpacked upon first encounter and abbreviated on subsequent encounters
--- * As a consequence, pretty version can be executed with unlimited depth on self-referential tables.
---@param object any
---@param depth? integer default: unlimited.
---@param constTable table
---@param indent string
---@return string
prettyTostring = function(object, depth, constTable, indent)
depth = depth or -1
local objType = type(object)
if objType == "string" then
return '"'..object..'"' --wrap the string in quotes.
elseif objType == 'table' and depth ~= 0 then
if not constTable[object] then
constTable[object] = tostring(object):gsub(":","")
if next(object)==nil then
return constTable[object]..": {}"
else
local mappedKV = {}
for k,v in pairs(object) do
table.insert(mappedKV, '\n ' .. indent ..'[' .. prettyTostring(k, depth - 1, constTable, indent .. " ") .. '] = ' .. prettyTostring(v, depth - 1, constTable, indent .. " "))
end
return constTable[object]..': {'.. table.concat(mappedKV, ',') .. '\n'..indent..'}'
end
end
end
return constTable[object] or tostring(object)
end
---Creates a list of all (key,value)-pairs from the specified table. Also lists subtable entries up to the specified depth.
---Supports concise style and pretty style.
---Concise will display {"a", 5, {7}} as '{(1, "a"), (2, 5), (3, {(1, 7)})}'.
---Pretty is linebreak-separated, so consider table size before converting. Pretty also abbreviates tables referenced multiple times.
---Can be called like table.tostring(T), table.tostring(T, depth), table.tostring(T, pretty_yn) or table.tostring(T, depth, pretty_yn).
---table.tostring is not multiplayer-synced.
---@param whichTable table
---@param depth? integer default: unlimited
---@param pretty_yn? boolean default: false (concise)
---@return string
---@overload fun(whichTable:table, pretty_yn?:boolean):string
function table.tostring(whichTable, depth, pretty_yn)
--reassign input params, if function was called as table.tostring(whichTable, pretty_yn)
if type(depth) == 'boolean' then
pretty_yn = depth
depth = -1
end
return pretty_yn and prettyTostring(whichTable, depth, {}, "") or conciseTostring(whichTable, depth)
end
---Prints a list of (key,value)-pairs contained in the specified table and its subtables up to the specified depth.
---Supports concise style and pretty style. Pretty is linebreak-separated, so consider table size before printing.
---Can be called like table.print(T), table.print(T, depth), table.print(T, pretty_yn) or table.print(T, depth, pretty_yn).
---@param whichTable table
---@param depth? integer default: unlimited
---@param pretty_yn? boolean default: false (concise)
---@overload fun(whichTable:table, pretty_yn?:boolean)
function table.print(whichTable, depth, pretty_yn)
print(table.tostring(whichTable, depth, pretty_yn))
end
end
end
Debug.endFile()
------------------------
----| String Width |----
------------------------
--[[
offers functions to measure the width of a string (i.e. the space it takes on screen, not the number of chars). Wc3 font is not monospace, so the system below has protocolled every char width and simply sums up all chars in a string.
output measures are:
1. Multiboard-width (i.e. 1-based screen share used in Multiboards column functions)
2. Line-width for screen prints
every unknown char will be treated as having default width (see constants below)
--]]
do
----------------------------
----| String Width API |----
----------------------------
local multiboardCharTable = {} ---@type table -- saves the width in screen percent (on 1920 pixel width resolutions) that each char takes up, when displayed in a multiboard.
local DEFAULT_MULTIBOARD_CHAR_WIDTH = 1. / 128. ---@type number -- used for unknown chars (where we didn't define a width in the char table)
local MULTIBOARD_TO_PRINT_FACTOR = 1. / 36. ---@type number -- 36 is actually the lower border (longest width of a non-breaking string only consisting of the letter "i")
---Returns the width of a char in a multiboard, when inputting a char (string of length 1) and 0 otherwise.
---also returns 0 for non-recorded chars (like ` and ´ and ß and § and €)
---@param char string | integer integer bytecode representations of chars are also allowed, i.e. the results of string.byte().
---@param textlanguage? '"ger"'| '"eng"' (default: 'eng'), depending on the text language in the Warcraft 3 installation settings.
---@return number
function string.charMultiboardWidth(char, textlanguage)
return multiboardCharTable[textlanguage or 'eng'][char] or DEFAULT_MULTIBOARD_CHAR_WIDTH
end
---returns the width of a string in a multiboard (i.e. output is in screen percent)
---unknown chars will be measured with default width (see constants above)
---@param multichar string
---@param textlanguage? '"ger"'| '"eng"' (default: 'eng'), depending on the text language in the Warcraft 3 installation settings.
---@return number
function string.multiboardWidth(multichar, textlanguage)
local chartable = table.pack(multichar:byte(1,-1)) --packs all bytecode char representations into a table
local charWidth = 0.
for i = 1, chartable.n do
charWidth = charWidth + string.charMultiboardWidth(chartable[i], textlanguage)
end
return charWidth
end
---The function should match the following criteria: If the value returned by this function is smaller than 1.0, than the string fits into a single line on screen.
---The opposite is not necessarily true (but should be true in the majority of cases): If the function returns bigger than 1.0, the string doesn't necessarily break.
---@param char string | integer integer bytecode representations of chars are also allowed, i.e. the results of string.byte().
---@param textlanguage? '"ger"'| '"eng"' (default: 'eng'), depending on the text language in the Warcraft 3 installation settings.
---@return number
function string.charPrintWidth(char, textlanguage)
return string.charMultiboardWidth(char, textlanguage) * MULTIBOARD_TO_PRINT_FACTOR
end
---The function should match the following criteria: If the value returned by this function is smaller than 1.0, than the string fits into a single line on screen.
---The opposite is not necessarily true (but should be true in the majority of cases): If the function returns bigger than 1.0, the string doesn't necessarily break.
---@param multichar string
---@param textlanguage? '"ger"'| '"eng"' (default: 'eng'), depending on the text language in the Warcraft 3 installation settings.
---@return number
function string.printWidth(multichar, textlanguage)
return string.multiboardWidth(multichar, textlanguage) * MULTIBOARD_TO_PRINT_FACTOR
end
----------------------------------
----| String Width Internals |----
----------------------------------
---@param charset '"ger"'| '"eng"' (default: 'eng'), depending on the text language in the Warcraft 3 installation settings.
---@param char string|integer either the char or its bytecode
---@param lengthInScreenWidth number
local function setMultiboardCharWidth(charset, char, lengthInScreenWidth)
multiboardCharTable[charset] = multiboardCharTable[charset] or {}
multiboardCharTable[charset][char] = lengthInScreenWidth
end
---numberPlacements says how often the char can be placed in a multiboard column, before reaching into the right bound.
---@param charset '"ger"'| '"eng"' (default: 'eng'), depending on the text language in the Warcraft 3 installation settings.
---@param char string|integer either the char or its bytecode
---@param numberPlacements integer
local function setMultiboardCharWidthBase80(charset, char, numberPlacements)
setMultiboardCharWidth(charset, char, 0.8 / numberPlacements) --1-based measure. 80./numberPlacements would result in Screen Percent.
setMultiboardCharWidth(charset, char:byte(1,-1), 0.8 / numberPlacements)
end
-- Set Char Width for all printable ascii chars in screen width (1920 pixels). Measured on a 80percent screen width multiboard column by counting the number of chars that fit into it.
-- Font size differs by text install language and patch (1.32- vs. 1.33+)
if BlzGetUnitOrderCount then --identifies patch 1.33+
--German font size for patch 1.33+
setMultiboardCharWidthBase80('ger', "a", 144)
setMultiboardCharWidthBase80('ger', "b", 131)
setMultiboardCharWidthBase80('ger', "c", 144)
setMultiboardCharWidthBase80('ger', "d", 120)
setMultiboardCharWidthBase80('ger', "e", 131)
setMultiboardCharWidthBase80('ger', "f", 240)
setMultiboardCharWidthBase80('ger', "g", 120)
setMultiboardCharWidthBase80('ger', "h", 131)
setMultiboardCharWidthBase80('ger', "i", 288)
setMultiboardCharWidthBase80('ger', "j", 288)
setMultiboardCharWidthBase80('ger', "k", 144)
setMultiboardCharWidthBase80('ger', "l", 288)
setMultiboardCharWidthBase80('ger', "m", 85)
setMultiboardCharWidthBase80('ger', "n", 131)
setMultiboardCharWidthBase80('ger', "o", 120)
setMultiboardCharWidthBase80('ger', "p", 120)
setMultiboardCharWidthBase80('ger', "q", 120)
setMultiboardCharWidthBase80('ger', "r", 206)
setMultiboardCharWidthBase80('ger', "s", 160)
setMultiboardCharWidthBase80('ger', "t", 206)
setMultiboardCharWidthBase80('ger', "u", 131)
setMultiboardCharWidthBase80('ger', "v", 131)
setMultiboardCharWidthBase80('ger', "w", 96)
setMultiboardCharWidthBase80('ger', "x", 144)
setMultiboardCharWidthBase80('ger', "y", 131)
setMultiboardCharWidthBase80('ger', "z", 144)
setMultiboardCharWidthBase80('ger', "A", 103)
setMultiboardCharWidthBase80('ger', "B", 120)
setMultiboardCharWidthBase80('ger', "C", 111)
setMultiboardCharWidthBase80('ger', "D", 103)
setMultiboardCharWidthBase80('ger', "E", 144)
setMultiboardCharWidthBase80('ger', "F", 160)
setMultiboardCharWidthBase80('ger', "G", 96)
setMultiboardCharWidthBase80('ger', "H", 96)
setMultiboardCharWidthBase80('ger', "I", 240)
setMultiboardCharWidthBase80('ger', "J", 240)
setMultiboardCharWidthBase80('ger', "K", 120)
setMultiboardCharWidthBase80('ger', "L", 144)
setMultiboardCharWidthBase80('ger', "M", 76)
setMultiboardCharWidthBase80('ger', "N", 96)
setMultiboardCharWidthBase80('ger', "O", 90)
setMultiboardCharWidthBase80('ger', "P", 131)
setMultiboardCharWidthBase80('ger', "Q", 90)
setMultiboardCharWidthBase80('ger', "R", 120)
setMultiboardCharWidthBase80('ger', "S", 131)
setMultiboardCharWidthBase80('ger', "T", 144)
setMultiboardCharWidthBase80('ger', "U", 103)
setMultiboardCharWidthBase80('ger', "V", 120)
setMultiboardCharWidthBase80('ger', "W", 76)
setMultiboardCharWidthBase80('ger', "X", 111)
setMultiboardCharWidthBase80('ger', "Y", 120)
setMultiboardCharWidthBase80('ger', "Z", 120)
setMultiboardCharWidthBase80('ger', "1", 144)
setMultiboardCharWidthBase80('ger', "2", 120)
setMultiboardCharWidthBase80('ger', "3", 120)
setMultiboardCharWidthBase80('ger', "4", 120)
setMultiboardCharWidthBase80('ger', "5", 120)
setMultiboardCharWidthBase80('ger', "6", 120)
setMultiboardCharWidthBase80('ger', "7", 131)
setMultiboardCharWidthBase80('ger', "8", 120)
setMultiboardCharWidthBase80('ger', "9", 120)
setMultiboardCharWidthBase80('ger', "0", 120)
setMultiboardCharWidthBase80('ger', ":", 288)
setMultiboardCharWidthBase80('ger', ";", 288)
setMultiboardCharWidthBase80('ger', ".", 288)
setMultiboardCharWidthBase80('ger', "#", 120)
setMultiboardCharWidthBase80('ger', ",", 288)
setMultiboardCharWidthBase80('ger', " ", 286) --space
setMultiboardCharWidthBase80('ger', "'", 180)
setMultiboardCharWidthBase80('ger', "!", 180)
setMultiboardCharWidthBase80('ger', "$", 131)
setMultiboardCharWidthBase80('ger', "&", 90)
setMultiboardCharWidthBase80('ger', "/", 180)
setMultiboardCharWidthBase80('ger', "(", 240)
setMultiboardCharWidthBase80('ger', ")", 240)
setMultiboardCharWidthBase80('ger', "=", 120)
setMultiboardCharWidthBase80('ger', "?", 144)
setMultiboardCharWidthBase80('ger', "^", 144)
setMultiboardCharWidthBase80('ger', "<", 144)
setMultiboardCharWidthBase80('ger', ">", 144)
setMultiboardCharWidthBase80('ger', "-", 180)
setMultiboardCharWidthBase80('ger', "+", 120)
setMultiboardCharWidthBase80('ger', "*", 180)
setMultiboardCharWidthBase80('ger', "|", 287) --2 vertical bars in a row escape to one. So you could print 960 ones in a line, 480 would display. Maybe need to adapt to this before calculating string width.
setMultiboardCharWidthBase80('ger', "~", 111)
setMultiboardCharWidthBase80('ger', "{", 240)
setMultiboardCharWidthBase80('ger', "}", 240)
setMultiboardCharWidthBase80('ger', "[", 240)
setMultiboardCharWidthBase80('ger', "]", 240)
setMultiboardCharWidthBase80('ger', "_", 144)
setMultiboardCharWidthBase80('ger', "\x25", 103) --percent
setMultiboardCharWidthBase80('ger', "\x5C", 205) --backslash
setMultiboardCharWidthBase80('ger', "\x22", 120) --double quotation mark
setMultiboardCharWidthBase80('ger', "\x40", 90) --at sign
setMultiboardCharWidthBase80('ger', "\x60", 144) --Gravis (Accent)
--English font size for patch 1.33+
setMultiboardCharWidthBase80('eng', "a", 144)
setMultiboardCharWidthBase80('eng', "b", 120)
setMultiboardCharWidthBase80('eng', "c", 131)
setMultiboardCharWidthBase80('eng', "d", 120)
setMultiboardCharWidthBase80('eng', "e", 120)
setMultiboardCharWidthBase80('eng', "f", 240)
setMultiboardCharWidthBase80('eng', "g", 120)
setMultiboardCharWidthBase80('eng', "h", 120)
setMultiboardCharWidthBase80('eng', "i", 288)
setMultiboardCharWidthBase80('eng', "j", 288)
setMultiboardCharWidthBase80('eng', "k", 144)
setMultiboardCharWidthBase80('eng', "l", 288)
setMultiboardCharWidthBase80('eng', "m", 80)
setMultiboardCharWidthBase80('eng', "n", 120)
setMultiboardCharWidthBase80('eng', "o", 111)
setMultiboardCharWidthBase80('eng', "p", 111)
setMultiboardCharWidthBase80('eng', "q", 111)
setMultiboardCharWidthBase80('eng', "r", 206)
setMultiboardCharWidthBase80('eng', "s", 160)
setMultiboardCharWidthBase80('eng', "t", 206)
setMultiboardCharWidthBase80('eng', "u", 120)
setMultiboardCharWidthBase80('eng', "v", 144)
setMultiboardCharWidthBase80('eng', "w", 90)
setMultiboardCharWidthBase80('eng', "x", 131)
setMultiboardCharWidthBase80('eng', "y", 144)
setMultiboardCharWidthBase80('eng', "z", 144)
setMultiboardCharWidthBase80('eng', "A", 103)
setMultiboardCharWidthBase80('eng', "B", 120)
setMultiboardCharWidthBase80('eng', "C", 103)
setMultiboardCharWidthBase80('eng', "D", 96)
setMultiboardCharWidthBase80('eng', "E", 131)
setMultiboardCharWidthBase80('eng', "F", 160)
setMultiboardCharWidthBase80('eng', "G", 96)
setMultiboardCharWidthBase80('eng', "H", 90)
setMultiboardCharWidthBase80('eng', "I", 240)
setMultiboardCharWidthBase80('eng', "J", 240)
setMultiboardCharWidthBase80('eng', "K", 120)
setMultiboardCharWidthBase80('eng', "L", 131)
setMultiboardCharWidthBase80('eng', "M", 76)
setMultiboardCharWidthBase80('eng', "N", 90)
setMultiboardCharWidthBase80('eng', "O", 85)
setMultiboardCharWidthBase80('eng', "P", 120)
setMultiboardCharWidthBase80('eng', "Q", 85)
setMultiboardCharWidthBase80('eng', "R", 120)
setMultiboardCharWidthBase80('eng', "S", 131)
setMultiboardCharWidthBase80('eng', "T", 144)
setMultiboardCharWidthBase80('eng', "U", 96)
setMultiboardCharWidthBase80('eng', "V", 120)
setMultiboardCharWidthBase80('eng', "W", 76)
setMultiboardCharWidthBase80('eng', "X", 111)
setMultiboardCharWidthBase80('eng', "Y", 120)
setMultiboardCharWidthBase80('eng', "Z", 111)
setMultiboardCharWidthBase80('eng', "1", 103)
setMultiboardCharWidthBase80('eng', "2", 111)
setMultiboardCharWidthBase80('eng', "3", 111)
setMultiboardCharWidthBase80('eng', "4", 111)
setMultiboardCharWidthBase80('eng', "5", 111)
setMultiboardCharWidthBase80('eng', "6", 111)
setMultiboardCharWidthBase80('eng', "7", 111)
setMultiboardCharWidthBase80('eng', "8", 111)
setMultiboardCharWidthBase80('eng', "9", 111)
setMultiboardCharWidthBase80('eng', "0", 111)
setMultiboardCharWidthBase80('eng', ":", 288)
setMultiboardCharWidthBase80('eng', ";", 288)
setMultiboardCharWidthBase80('eng', ".", 288)
setMultiboardCharWidthBase80('eng', "#", 103)
setMultiboardCharWidthBase80('eng', ",", 288)
setMultiboardCharWidthBase80('eng', " ", 286) --space
setMultiboardCharWidthBase80('eng', "'", 360)
setMultiboardCharWidthBase80('eng', "!", 288)
setMultiboardCharWidthBase80('eng', "$", 131)
setMultiboardCharWidthBase80('eng', "&", 120)
setMultiboardCharWidthBase80('eng', "/", 180)
setMultiboardCharWidthBase80('eng', "(", 206)
setMultiboardCharWidthBase80('eng', ")", 206)
setMultiboardCharWidthBase80('eng', "=", 111)
setMultiboardCharWidthBase80('eng', "?", 180)
setMultiboardCharWidthBase80('eng', "^", 144)
setMultiboardCharWidthBase80('eng', "<", 111)
setMultiboardCharWidthBase80('eng', ">", 111)
setMultiboardCharWidthBase80('eng', "-", 160)
setMultiboardCharWidthBase80('eng', "+", 111)
setMultiboardCharWidthBase80('eng', "*", 144)
setMultiboardCharWidthBase80('eng', "|", 479) --2 vertical bars in a row escape to one. So you could print 960 ones in a line, 480 would display. Maybe need to adapt to this before calculating string width.
setMultiboardCharWidthBase80('eng', "~", 144)
setMultiboardCharWidthBase80('eng', "{", 160)
setMultiboardCharWidthBase80('eng', "}", 160)
setMultiboardCharWidthBase80('eng', "[", 206)
setMultiboardCharWidthBase80('eng', "]", 206)
setMultiboardCharWidthBase80('eng', "_", 120)
setMultiboardCharWidthBase80('eng', "\x25", 103) --percent
setMultiboardCharWidthBase80('eng', "\x5C", 180) --backslash
setMultiboardCharWidthBase80('eng', "\x22", 180) --double quotation mark
setMultiboardCharWidthBase80('eng', "\x40", 85) --at sign
setMultiboardCharWidthBase80('eng', "\x60", 206) --Gravis (Accent)
else
--German font size up to patch 1.32
setMultiboardCharWidthBase80('ger', "a", 144)
setMultiboardCharWidthBase80('ger', "b", 144)
setMultiboardCharWidthBase80('ger', "c", 144)
setMultiboardCharWidthBase80('ger', "d", 131)
setMultiboardCharWidthBase80('ger', "e", 144)
setMultiboardCharWidthBase80('ger', "f", 240)
setMultiboardCharWidthBase80('ger', "g", 120)
setMultiboardCharWidthBase80('ger', "h", 144)
setMultiboardCharWidthBase80('ger', "i", 360)
setMultiboardCharWidthBase80('ger', "j", 288)
setMultiboardCharWidthBase80('ger', "k", 144)
setMultiboardCharWidthBase80('ger', "l", 360)
setMultiboardCharWidthBase80('ger', "m", 90)
setMultiboardCharWidthBase80('ger', "n", 144)
setMultiboardCharWidthBase80('ger', "o", 131)
setMultiboardCharWidthBase80('ger', "p", 131)
setMultiboardCharWidthBase80('ger', "q", 131)
setMultiboardCharWidthBase80('ger', "r", 206)
setMultiboardCharWidthBase80('ger', "s", 180)
setMultiboardCharWidthBase80('ger', "t", 206)
setMultiboardCharWidthBase80('ger', "u", 144)
setMultiboardCharWidthBase80('ger', "v", 131)
setMultiboardCharWidthBase80('ger', "w", 96)
setMultiboardCharWidthBase80('ger', "x", 144)
setMultiboardCharWidthBase80('ger', "y", 131)
setMultiboardCharWidthBase80('ger', "z", 144)
setMultiboardCharWidthBase80('ger', "A", 103)
setMultiboardCharWidthBase80('ger', "B", 131)
setMultiboardCharWidthBase80('ger', "C", 120)
setMultiboardCharWidthBase80('ger', "D", 111)
setMultiboardCharWidthBase80('ger', "E", 144)
setMultiboardCharWidthBase80('ger', "F", 180)
setMultiboardCharWidthBase80('ger', "G", 103)
setMultiboardCharWidthBase80('ger', "H", 103)
setMultiboardCharWidthBase80('ger', "I", 288)
setMultiboardCharWidthBase80('ger', "J", 240)
setMultiboardCharWidthBase80('ger', "K", 120)
setMultiboardCharWidthBase80('ger', "L", 144)
setMultiboardCharWidthBase80('ger', "M", 80)
setMultiboardCharWidthBase80('ger', "N", 103)
setMultiboardCharWidthBase80('ger', "O", 96)
setMultiboardCharWidthBase80('ger', "P", 144)
setMultiboardCharWidthBase80('ger', "Q", 90)
setMultiboardCharWidthBase80('ger', "R", 120)
setMultiboardCharWidthBase80('ger', "S", 144)
setMultiboardCharWidthBase80('ger', "T", 144)
setMultiboardCharWidthBase80('ger', "U", 111)
setMultiboardCharWidthBase80('ger', "V", 120)
setMultiboardCharWidthBase80('ger', "W", 76)
setMultiboardCharWidthBase80('ger', "X", 111)
setMultiboardCharWidthBase80('ger', "Y", 120)
setMultiboardCharWidthBase80('ger', "Z", 120)
setMultiboardCharWidthBase80('ger', "1", 288)
setMultiboardCharWidthBase80('ger', "2", 131)
setMultiboardCharWidthBase80('ger', "3", 144)
setMultiboardCharWidthBase80('ger', "4", 120)
setMultiboardCharWidthBase80('ger', "5", 144)
setMultiboardCharWidthBase80('ger', "6", 131)
setMultiboardCharWidthBase80('ger', "7", 144)
setMultiboardCharWidthBase80('ger', "8", 131)
setMultiboardCharWidthBase80('ger', "9", 131)
setMultiboardCharWidthBase80('ger', "0", 131)
setMultiboardCharWidthBase80('ger', ":", 480)
setMultiboardCharWidthBase80('ger', ";", 360)
setMultiboardCharWidthBase80('ger', ".", 480)
setMultiboardCharWidthBase80('ger', "#", 120)
setMultiboardCharWidthBase80('ger', ",", 360)
setMultiboardCharWidthBase80('ger', " ", 288) --space
setMultiboardCharWidthBase80('ger', "'", 480)
setMultiboardCharWidthBase80('ger', "!", 360)
setMultiboardCharWidthBase80('ger', "$", 160)
setMultiboardCharWidthBase80('ger', "&", 96)
setMultiboardCharWidthBase80('ger', "/", 180)
setMultiboardCharWidthBase80('ger', "(", 288)
setMultiboardCharWidthBase80('ger', ")", 288)
setMultiboardCharWidthBase80('ger', "=", 160)
setMultiboardCharWidthBase80('ger', "?", 180)
setMultiboardCharWidthBase80('ger', "^", 144)
setMultiboardCharWidthBase80('ger', "<", 160)
setMultiboardCharWidthBase80('ger', ">", 160)
setMultiboardCharWidthBase80('ger', "-", 144)
setMultiboardCharWidthBase80('ger', "+", 160)
setMultiboardCharWidthBase80('ger', "*", 206)
setMultiboardCharWidthBase80('ger', "|", 480) --2 vertical bars in a row escape to one. So you could print 960 ones in a line, 480 would display. Maybe need to adapt to this before calculating string width.
setMultiboardCharWidthBase80('ger', "~", 144)
setMultiboardCharWidthBase80('ger', "{", 240)
setMultiboardCharWidthBase80('ger', "}", 240)
setMultiboardCharWidthBase80('ger', "[", 240)
setMultiboardCharWidthBase80('ger', "]", 288)
setMultiboardCharWidthBase80('ger', "_", 144)
setMultiboardCharWidthBase80('ger', "\x25", 111) --percent
setMultiboardCharWidthBase80('ger', "\x5C", 206) --backslash
setMultiboardCharWidthBase80('ger', "\x22", 240) --double quotation mark
setMultiboardCharWidthBase80('ger', "\x40", 103) --at sign
setMultiboardCharWidthBase80('ger', "\x60", 240) --Gravis (Accent)
--English Font size up to patch 1.32
setMultiboardCharWidthBase80('eng', "a", 144)
setMultiboardCharWidthBase80('eng', "b", 120)
setMultiboardCharWidthBase80('eng', "c", 131)
setMultiboardCharWidthBase80('eng', "d", 120)
setMultiboardCharWidthBase80('eng', "e", 131)
setMultiboardCharWidthBase80('eng', "f", 240)
setMultiboardCharWidthBase80('eng', "g", 120)
setMultiboardCharWidthBase80('eng', "h", 131)
setMultiboardCharWidthBase80('eng', "i", 360)
setMultiboardCharWidthBase80('eng', "j", 288)
setMultiboardCharWidthBase80('eng', "k", 144)
setMultiboardCharWidthBase80('eng', "l", 360)
setMultiboardCharWidthBase80('eng', "m", 80)
setMultiboardCharWidthBase80('eng', "n", 131)
setMultiboardCharWidthBase80('eng', "o", 120)
setMultiboardCharWidthBase80('eng', "p", 120)
setMultiboardCharWidthBase80('eng', "q", 120)
setMultiboardCharWidthBase80('eng', "r", 206)
setMultiboardCharWidthBase80('eng', "s", 160)
setMultiboardCharWidthBase80('eng', "t", 206)
setMultiboardCharWidthBase80('eng', "u", 131)
setMultiboardCharWidthBase80('eng', "v", 144)
setMultiboardCharWidthBase80('eng', "w", 90)
setMultiboardCharWidthBase80('eng', "x", 131)
setMultiboardCharWidthBase80('eng', "y", 144)
setMultiboardCharWidthBase80('eng', "z", 144)
setMultiboardCharWidthBase80('eng', "A", 103)
setMultiboardCharWidthBase80('eng', "B", 120)
setMultiboardCharWidthBase80('eng', "C", 103)
setMultiboardCharWidthBase80('eng', "D", 103)
setMultiboardCharWidthBase80('eng', "E", 131)
setMultiboardCharWidthBase80('eng', "F", 160)
setMultiboardCharWidthBase80('eng', "G", 103)
setMultiboardCharWidthBase80('eng', "H", 96)
setMultiboardCharWidthBase80('eng', "I", 288)
setMultiboardCharWidthBase80('eng', "J", 240)
setMultiboardCharWidthBase80('eng', "K", 120)
setMultiboardCharWidthBase80('eng', "L", 131)
setMultiboardCharWidthBase80('eng', "M", 76)
setMultiboardCharWidthBase80('eng', "N", 96)
setMultiboardCharWidthBase80('eng', "O", 85)
setMultiboardCharWidthBase80('eng', "P", 131)
setMultiboardCharWidthBase80('eng', "Q", 85)
setMultiboardCharWidthBase80('eng', "R", 120)
setMultiboardCharWidthBase80('eng', "S", 131)
setMultiboardCharWidthBase80('eng', "T", 144)
setMultiboardCharWidthBase80('eng', "U", 103)
setMultiboardCharWidthBase80('eng', "V", 120)
setMultiboardCharWidthBase80('eng', "W", 76)
setMultiboardCharWidthBase80('eng', "X", 111)
setMultiboardCharWidthBase80('eng', "Y", 120)
setMultiboardCharWidthBase80('eng', "Z", 111)
setMultiboardCharWidthBase80('eng', "1", 206)
setMultiboardCharWidthBase80('eng', "2", 131)
setMultiboardCharWidthBase80('eng', "3", 131)
setMultiboardCharWidthBase80('eng', "4", 111)
setMultiboardCharWidthBase80('eng', "5", 131)
setMultiboardCharWidthBase80('eng', "6", 120)
setMultiboardCharWidthBase80('eng', "7", 131)
setMultiboardCharWidthBase80('eng', "8", 111)
setMultiboardCharWidthBase80('eng', "9", 120)
setMultiboardCharWidthBase80('eng', "0", 111)
setMultiboardCharWidthBase80('eng', ":", 360)
setMultiboardCharWidthBase80('eng', ";", 360)
setMultiboardCharWidthBase80('eng', ".", 360)
setMultiboardCharWidthBase80('eng', "#", 103)
setMultiboardCharWidthBase80('eng', ",", 360)
setMultiboardCharWidthBase80('eng', " ", 288) --space
setMultiboardCharWidthBase80('eng', "'", 480)
setMultiboardCharWidthBase80('eng', "!", 360)
setMultiboardCharWidthBase80('eng', "$", 131)
setMultiboardCharWidthBase80('eng', "&", 120)
setMultiboardCharWidthBase80('eng', "/", 180)
setMultiboardCharWidthBase80('eng', "(", 240)
setMultiboardCharWidthBase80('eng', ")", 240)
setMultiboardCharWidthBase80('eng', "=", 111)
setMultiboardCharWidthBase80('eng', "?", 180)
setMultiboardCharWidthBase80('eng', "^", 144)
setMultiboardCharWidthBase80('eng', "<", 131)
setMultiboardCharWidthBase80('eng', ">", 131)
setMultiboardCharWidthBase80('eng', "-", 180)
setMultiboardCharWidthBase80('eng', "+", 111)
setMultiboardCharWidthBase80('eng', "*", 180)
setMultiboardCharWidthBase80('eng', "|", 480) --2 vertical bars in a row escape to one. So you could print 960 ones in a line, 480 would display. Maybe need to adapt to this before calculating string width.
setMultiboardCharWidthBase80('eng', "~", 144)
setMultiboardCharWidthBase80('eng', "{", 240)
setMultiboardCharWidthBase80('eng', "}", 240)
setMultiboardCharWidthBase80('eng', "[", 240)
setMultiboardCharWidthBase80('eng', "]", 240)
setMultiboardCharWidthBase80('eng', "_", 120)
setMultiboardCharWidthBase80('eng', "\x25", 103) --percent
setMultiboardCharWidthBase80('eng', "\x5C", 180) --backslash
setMultiboardCharWidthBase80('eng', "\x22", 206) --double quotation mark
setMultiboardCharWidthBase80('eng', "\x40", 96) --at sign
setMultiboardCharWidthBase80('eng', "\x60", 206) --Gravis (Accent)
end
end
Debug.beginFile("IngameConsole")
--[[
--------------------------
----| Ingame Console |----
--------------------------
/**********************************************
* Allows you to use the following ingame commands:
* "-exec <code>" to execute any code ingame.
* "-console" to start an ingame console interpreting any further chat input as code and showing both return values of function calls and error messages. Furthermore, the print function will print
* directly to the console after it got started. You can still look up all print messages in the F12-log.
***********************
* -------------------
* |Using the console|
* -------------------
* Any (well, most) chat input by any player after starting the console is interpreted as code and directly executed. You can enter terms (like 4+5 or just any variable name), function calls (like print("bla"))
* and set-statements (like y = 5). If the code has any return values, all of them are printed to the console. Erroneous code will print an error message.
* Chat input starting with a hyphen is being ignored by the console, i.e. neither executed as code nor printed to the console. This allows you to still use other chat commands like "-exec" without prompting errors.
***********************
* ------------------
* |Multiline-Inputs|
* ------------------
* You can prevent a chat input from being immediately executed by preceeding it with the '>' character. All lines entered this way are halted, until any line not starting with '>' is being entered.
* The first input without '>' will execute all halted lines (and itself) in one chunk.
* Example of a chat input (the console will add an additional '>' to every line):
* >function a(x)
* >return x
* end
***********************
* Note that multiline inputs don't accept pure term evaluations, e.g. the following input is not supported and will prompt an error, while the same lines would have worked as two single-line inputs:
* >x = 5
* x
***********************
* -------------------
* |Reserved Keywords|
* -------------------
* The following keywords have a reserved functionality, i.e. are direct commands for the console and will not be interpreted as code:
* - 'help' - will show a list of all reserved keywords along very short explanations.
* - 'exit' - will shut down the console
* - 'share' - will share the players console with every other player, allowing others to read and write into it. Will force-close other players consoles, if they have one active.
* - 'clear' - will clear all text from the console, except the word 'clear'
* - 'lasttrace' - will show the stack trace of the latest error that occured within IngameConsole
* - 'show' - will show the console, after it was accidently hidden (you can accidently hide it by showing another multiboard, while the console functionality is still up and running).
* - 'printtochat' - will let the print function return to normal behaviour (i.e. print to the chat instead of the console).
* - 'printtoconsole'- will let the print function print to the console (which is default behaviour).
* - 'autosize on' - will enable automatic console resize depending on the longest string in the display. This is turned on by default.
* - 'autosize off' - will disable automatic console resize and instead linebreak long strings into multiple lines.
* - 'textlang eng' - lets the console use english Wc3 text language font size to compute linebreaks (look in your Blizzard launcher settings to find out)
* - 'textlang ger' - lets the console use german Wc3 text language font size to compute linebreaks (look in your Blizzard launcher settings to find out)
***********************
* --------------
* |Paste Helper|
* --------------
* @Luashine has created a tool that simplifies pasting multiple lines of code from outside Wc3 into the IngameConsole.
* This is particularly useful, when you want to execute a large chunk of testcode containing several linebreaks.
* Goto: https://github.com/Luashine/wc3-debug-console-paste-helper#readme
*
*************************************************/
--]]
----------------
--| Settings |--
----------------
---@class IngameConsole
IngameConsole = {
--Settings
numRows = 20 ---@type integer Number of Rows of the console (multiboard), excluding the title row. So putting 20 here will show 21 rows, first being the title row.
, autosize = true ---@type boolean Defines, whether the width of the main Column automatically adjusts with the longest string in the display.
, currentWidth = 0.5 ---@type number Current and starting Screen Share of the console main column.
, mainColMinWidth = 0.3 ---@type number Minimum Screen share of the console main column.
, mainColMaxWidth = 0.8 ---@type number Maximum Scren share of the console main column.
, tsColumnWidth = 0.06 ---@type number Screen Share of the Timestamp Column
, linebreakBuffer = 0.008 ---@type number Screen Share that is added to longest string in display to calculate the screen share for the console main column. Compensates for the small inaccuracy of the String Width function.
, maxLinebreaks = 8 ---@type integer Defines the maximum amount of linebreaks, before the remaining output string will be cut and not further displayed.
, printToConsole = false ---@type boolean defines, if the print function should print to the console or to the chat
, sharedConsole = false ---@type boolean defines, if the console is displayed to each player at the same time (accepting all players input) or if all players much start their own console.
, showTraceOnError = false ---@type boolean defines, if the console shows a trace upon printing errors. Usually not too useful within console, because you have just initiated the erroneous call.
, textLanguage = 'eng' ---@type string text language of your Wc3 installation, which influences font size (look in the settings of your Blizzard launcher). Currently only supports 'eng' and 'ger'.
, colors = {
timestamp = "bbbbbb" ---@type string Timestamp Color
, singleLineInput = "ffffaa" ---@type string Color to be applied to single line console inputs
, multiLineInput = "ffcc55" ---@type string Color to be applied to multi line console inputs
, returnValue = "00ffff" ---@type string Color applied to return values
, error = "ff5555" ---@type string Color to be applied to errors resulting of function calls
, keywordInput = "ff00ff" ---@type string Color to be applied to reserved keyword inputs (console reserved keywords)
, info = "bbbbbb" ---@type string Color to be applied to info messages from the console itself (for instance after creation or after printrestore)
}
--Privates
, numCols = 2 ---@type integer Number of Columns of the console (multiboard). Adjusting this requires further changes on code base.
, player = nil ---@type player player for whom the console is being created
, currentLine = 0 ---@type integer Current Output Line of the console.
, inputload = '' ---@type string Input Holder for multi-line-inputs
, output = {} ---@type string[] Array of all output strings
, outputTimestamps = {} ---@type string[] Array of all output string timestamps
, outputWidths = {} ---@type number[] remembers all string widths to allow for multiboard resize
, trigger = nil ---@type trigger trigger processing all inputs during console lifetime
, multiboard = nil ---@type multiboard
, timer = nil ---@type timer gets started upon console creation to measure timestamps
, errorHandler = nil ---@type fun(errorMsg:string):string error handler to be used within xpcall. We create one per console to make it compatible with console-specific settings.
, lastTrace = '' ---@type string trace of last error occured within console. To be printed via reserved keyword "lasttrace"
--Statics
, keywords = {} ---@type table<string,function> saves functions to be executed for all reserved keywords
, playerConsoles = {} ---@type table<player,IngameConsole> Consoles currently being active. up to one per player.
, originalPrint = print ---@type function original print function to restore, after the console gets closed.
}
IngameConsole.__index = IngameConsole
IngameConsole.__name = 'IngameConsole'
------------------------
--| Console Creation |--
------------------------
---Creates and opens up a new console.
---@param consolePlayer player player for whom the console is being created
---@return IngameConsole
function IngameConsole.create(consolePlayer)
local new = {} ---@type IngameConsole
setmetatable(new, IngameConsole)
---setup Object data
new.player = consolePlayer
new.output = {}
new.outputTimestamps = {}
new.outputWidths = {}
--Timer
new.timer = CreateTimer()
TimerStart(new.timer, 3600., true, nil) --just to get TimeElapsed for printing Timestamps.
--Trigger to be created after short delay, because otherwise it would fire on "-console" input immediately and lead to stack overflow.
new:setupTrigger()
--Multiboard
new:setupMultiboard()
--Create own error handler per console to be compatible with console-specific settings
new:setupErrorHandler()
--Share, if settings say so
if IngameConsole.sharedConsole then
new:makeShared() --we don't have to exit other players consoles, because we look for the setting directly in the class and there just logically can't be other active consoles.
end
--Welcome Message
new:out('info', 0, false, "Console started. Any further chat input will be executed as code, except when beginning with \x22-\x22.")
return new
end
---Creates the multiboard used for console display.
function IngameConsole:setupMultiboard()
self.multiboard = CreateMultiboard()
MultiboardSetRowCount(self.multiboard, self.numRows + 1) --title row adds 1
MultiboardSetColumnCount(self.multiboard, self.numCols)
MultiboardSetTitleText(self.multiboard, "Console")
local mbitem
for col = 1, self.numCols do
for row = 1, self.numRows + 1 do --Title row adds 1
mbitem = MultiboardGetItem(self.multiboard, row -1, col -1)
MultiboardSetItemStyle(mbitem, true, false)
MultiboardSetItemValueColor(mbitem, 255, 255, 255, 255) -- Colors get applied via text color code
MultiboardSetItemWidth(mbitem, (col == 1 and self.tsColumnWidth) or self.currentWidth )
MultiboardReleaseItem(mbitem)
end
end
mbitem = MultiboardGetItem(self.multiboard, 0, 0)
MultiboardSetItemValue(mbitem, "|cffffcc00Timestamp|r")
MultiboardReleaseItem(mbitem)
mbitem = MultiboardGetItem(self.multiboard, 0, 1)
MultiboardSetItemValue(mbitem, "|cffffcc00Line|r")
MultiboardReleaseItem(mbitem)
self:showToOwners()
end
---Creates the trigger that responds to chat events.
function IngameConsole:setupTrigger()
self.trigger = CreateTrigger()
TriggerRegisterPlayerChatEvent(self.trigger, self.player, "", false) --triggers on any input of self.player
TriggerAddCondition(self.trigger, Condition(function() return string.sub(GetEventPlayerChatString(),1,1) ~= '-' end)) --console will not react to entered stuff starting with '-'. This still allows to use other chat orders like "-exec".
TriggerAddAction(self.trigger, function() self:processInput(GetEventPlayerChatString()) end)
end
---Creates an Error Handler to be used by xpcall below.
---Adds stack trace plus formatting to the message.
function IngameConsole:setupErrorHandler()
self.errorHandler = function(errorMsg)
errorMsg = Debug.getLocalErrorMsg(errorMsg)
local _, tracePiece, lastFile = nil, "", errorMsg:match("^.-:") or "<unknown>" -- errors on objects created within Ingame Console don't have a file and linenumber. Consider "x = {}; x[nil] = 5".
local fullMsg = errorMsg .. "\nTraceback (most recent call first):\n" .. (errorMsg:match("^.-:\x25d+") or "<unknown>")
--Get Stack Trace. Starting at depth 5 ensures that "error", "messageHandler", "xpcall" and the input error message are not included.
for loopDepth = 5, 50 do --get trace on depth levels up to 50
---@diagnostic disable-next-line: cast-local-type, assign-type-mismatch
_, tracePiece = pcall(error, "", loopDepth) ---@type boolean, string
tracePiece = Debug.getLocalErrorMsg(tracePiece)
if #tracePiece > 0 then --some trace pieces can be empty, but there can still be valid ones beyond that
fullMsg = fullMsg .. " <- " .. ((tracePiece:match("^.-:") == lastFile) and tracePiece:match(":\x25d+"):sub(2,-1) or tracePiece:match("^.-:\x25d+"))
lastFile = tracePiece:match("^.-:")
end
end
self.lastTrace = fullMsg
return "ERROR: " .. (self.showTraceOnError and fullMsg or errorMsg)
end
end
---Shares this console with all players.
function IngameConsole:makeShared()
local player
for i = 0, GetBJMaxPlayers() -1 do
player = Player(i)
if (GetPlayerSlotState(player) == PLAYER_SLOT_STATE_PLAYING) and (IngameConsole.playerConsoles[player] ~= self) then --second condition ensures that the player chat event is not added twice for the same player.
IngameConsole.playerConsoles[player] = self
TriggerRegisterPlayerChatEvent(self.trigger, player, "", false) --triggers on any input
end
end
self.sharedConsole = true
end
---------------------
--| In |--
---------------------
---Processes a chat string. Each input will be printed. Incomplete multiline-inputs will be halted until completion. Completed inputs will be converted to a function and executed. If they have an output, it will be printed.
---@param inputString string
function IngameConsole:processInput(inputString)
--if the input is a reserved keyword, conduct respective actions and skip remaining actions.
if IngameConsole.keywords[inputString] then --if the input string is a reserved keyword
self:out('keywordInput', 1, false, inputString)
IngameConsole.keywords[inputString](self) --then call the method with the same name. IngameConsole.keywords["exit"](self) is just self.keywords:exit().
return
end
--if the input is a multi-line-input, queue it into the string buffer (inputLoad), but don't yet execute anything
if string.sub(inputString, 1, 1) == '>' then --multiLineInput
inputString = string.sub(inputString, 2, -1)
self:out('multiLineInput',2, false, inputString)
self.inputload = self.inputload .. inputString .. '\r' --carriage return
else --if the input is either singleLineInput OR the last line of multiLineInput, execute the whole thing.
self:out(self.inputload == '' and 'singleLineInput' or 'multiLineInput', 1, false, inputString)
self.inputload = self.inputload .. inputString
local loadedFunc, errorMsg = load("return " .. self.inputload) --adds return statements, if possible (works for term statements)
if loadedFunc == nil then
loadedFunc, errorMsg = load(self.inputload)
end
self.inputload = '' --empty inputload before execution of pcall. pcall can break (rare case, can for example be provoked with metatable.__tostring = {}), which would corrupt future console inputs.
--manually catch case, where the input did not define a proper Lua statement (i.e. loadfunc is nil)
local results = loadedFunc and table.pack(xpcall(loadedFunc, self.errorHandler)) or {false, "Input is not a valid Lua-statement: " .. errorMsg}
--output error message (unsuccessful case) or return values (successful case)
if not results[1] then --results[1] is the error status that pcall always returns. False stands for: error occured.
self:out('error', 0, true, results[2]) -- second result of pcall is the error message in case an error occured
elseif results.n > 1 then --Check, if there was at least one valid output argument. We check results.n instead of results[2], because we also get nil as a proper return value this way.
self:out('returnValue', 0, true, table.unpack(results, 2, results.n))
end
end
end
----------------------
--| Out |--
----------------------
-- split color codes, split linebreaks, print lines separately, print load-errors, update string width, update text, error handling with stack trace.
---Duplicates Color coding around linebreaks to make each line printable separately.
---Operates incorrectly on lookalike color codes invalidated by preceeding escaped vertical bar (like "||cffffcc00bla|r").
---Also operates incorrectly on multiple color codes, where the first is missing the end sequence (like "|cffffcc00Hello |cff0000ffWorld|r")
---@param inputString string
---@return string, integer
function IngameConsole.spreadColorCodes(inputString)
local replacementTable = {} --remembers all substrings to be replaced and their replacements.
for foundInstance, color in inputString:gmatch("((|c\x25x\x25x\x25x\x25x\x25x\x25x\x25x\x25x).-|r)") do
replacementTable[foundInstance] = foundInstance:gsub("(\r?\n)", "|r\x251" .. color)
end
return inputString:gsub("((|c\x25x\x25x\x25x\x25x\x25x\x25x\x25x\x25x).-|r)", replacementTable)
end
---Concatenates all inputs to one string, spreads color codes around line breaks and prints each line to the console separately.
---@param colorTheme? '"timestamp"'| '"singleLineInput"' | '"multiLineInput"' | '"result"' | '"keywordInput"' | '"info"' | '"error"' | '"returnValue"' Decides about the color to be applied. Currently accepted: 'timestamp', 'singleLineInput', 'multiLineInput', 'result', nil. (nil equals no colorTheme, i.e. white color)
---@param numIndentations integer Number of '>' chars that shall preceed the output
---@param hideTimestamp boolean Set to false to hide the timestamp column and instead show a "->" symbol.
---@param ... any the things to be printed in the console.
function IngameConsole:out(colorTheme, numIndentations, hideTimestamp, ...)
local inputs = table.pack(...)
for i = 1, inputs.n do
inputs[i] = tostring(inputs[i]) --apply tostring on every input param in preparation for table.concat
end
--Concatenate all inputs (4-space-separated)
local printOutput = table.concat(inputs, ' ', 1, inputs.n)
printOutput = printOutput:find("(\r?\n)") and IngameConsole.spreadColorCodes(printOutput) or printOutput
local substrStart, substrEnd = 1, 1
local numLinebreaks, completePrint = 0, true
repeat
substrEnd = (printOutput:find("(\r?\n)", substrStart) or 0) - 1
numLinebreaks, completePrint = self:lineOut(colorTheme, numIndentations, hideTimestamp, numLinebreaks, printOutput:sub(substrStart, substrEnd))
hideTimestamp = true
substrStart = substrEnd + 2
until substrEnd == -1 or numLinebreaks > self.maxLinebreaks
if substrEnd ~= -1 or not completePrint then
self:lineOut('info', 0, false, 0, "Previous value not entirely printed after exceeding maximum number of linebreaks. Consider adjusting 'IngameConsole.maxLinebreaks'.")
end
self:updateMultiboard()
end
---Prints the given string to the console with the specified colorTheme and the specified number of indentations.
---Only supports one-liners (no \n) due to how multiboards work. Will add linebreaks though, if the one-liner doesn't fit into the given multiboard space.
---@param colorTheme? '"timestamp"'| '"singleLineInput"' | '"multiLineInput"' | '"result"' | '"keywordInput"' | '"info"' | '"error"' | '"returnValue"' Decides about the color to be applied. Currently accepted: 'timestamp', 'singleLineInput', 'multiLineInput', 'result', nil. (nil equals no colorTheme, i.e. white color)
---@param numIndentations integer Number of greater '>' chars that shall preceed the output
---@param hideTimestamp boolean Set to false to hide the timestamp column and instead show a "->" symbol.
---@param numLinebreaks integer
---@param printOutput string the line to be printed in the console.
---@return integer numLinebreaks, boolean hasPrintedEverything returns true, if everything could be printed. Returns false otherwise (can happen for very long strings).
function IngameConsole:lineOut(colorTheme, numIndentations, hideTimestamp, numLinebreaks, printOutput)
--add preceeding greater chars
printOutput = ('>'):rep(numIndentations) .. printOutput
--Print a space instead of the empty string. This allows the console to identify, if the string has already been fully printed (see while-loop below).
if printOutput == '' then
printOutput = ' '
end
--Compute Linebreaks.
local linebreakWidth = ((self.autosize and self.mainColMaxWidth) or self.currentWidth )
local partialOutput = nil
local maxPrintableCharPosition
local printWidth
while string.len(printOutput) > 0 and numLinebreaks <= self.maxLinebreaks do --break, if the input string has reached length 0 OR when the maximum number of linebreaks would be surpassed.
--compute max printable substring (in one multiboard line)
maxPrintableCharPosition, printWidth = IngameConsole.getLinebreakData(printOutput, linebreakWidth - self.linebreakBuffer, self.textLanguage)
--adds timestamp to the first line of any output
if numLinebreaks == 0 then
partialOutput = printOutput:sub(1, numIndentations) .. ((IngameConsole.colors[colorTheme] and "|cff" .. IngameConsole.colors[colorTheme] .. printOutput:sub(numIndentations + 1, maxPrintableCharPosition) .. "|r") or printOutput:sub(numIndentations + 1, maxPrintableCharPosition)) --Colorize the output string, if a color theme was specified. IngameConsole.colors[colorTheme] can be nil.
table.insert(self.outputTimestamps, "|cff" .. IngameConsole.colors['timestamp'] .. ((hideTimestamp and ' ->') or IngameConsole.formatTimerElapsed(TimerGetElapsed(self.timer))) .. "|r")
else
partialOutput = (IngameConsole.colors[colorTheme] and "|cff" .. IngameConsole.colors[colorTheme] .. printOutput:sub(1, maxPrintableCharPosition) .. "|r") or printOutput:sub(1, maxPrintableCharPosition) --Colorize the output string, if a color theme was specified. IngameConsole.colors[colorTheme] can be nil.
table.insert(self.outputTimestamps, ' ..') --need a dummy entry in the timestamp list to make it line-progress with the normal output.
end
numLinebreaks = numLinebreaks + 1
--writes output string and width to the console tables.
table.insert(self.output, partialOutput)
table.insert(self.outputWidths, printWidth + self.linebreakBuffer) --remember the Width of this printed string to adjust the multiboard size in case. 0.5 percent is added to avoid the case, where the multiboard width is too small by a tiny bit, thus not showing some string without spaces.
--compute remaining string to print
printOutput = string.sub(printOutput, maxPrintableCharPosition + 1, -1) --remaining string until the end. Returns empty string, if there is nothing left
end
self.currentLine = #self.output
return numLinebreaks, string.len(printOutput) == 0 --printOutput is the empty string, if and only if everything has been printed
end
---Lets the multiboard show the recently printed lines.
function IngameConsole:updateMultiboard()
local startIndex = math.max(self.currentLine - self.numRows, 0) --to be added to loop counter to get to the index of output table to print
local outputIndex = 0
local maxWidth = 0.
local mbitem
for i = 1, self.numRows do --doesn't include title row (index 0)
outputIndex = i + startIndex
mbitem = MultiboardGetItem(self.multiboard, i, 0)
MultiboardSetItemValue(mbitem, self.outputTimestamps[outputIndex] or '')
MultiboardReleaseItem(mbitem)
mbitem = MultiboardGetItem(self.multiboard, i, 1)
MultiboardSetItemValue(mbitem, self.output[outputIndex] or '')
MultiboardReleaseItem(mbitem)
maxWidth = math.max(maxWidth, self.outputWidths[outputIndex] or 0.) --looping through non-defined widths, so need to coalesce with 0
end
--Adjust Multiboard Width, if necessary.
maxWidth = math.min(math.max(maxWidth, self.mainColMinWidth), self.mainColMaxWidth)
if self.autosize and self.currentWidth ~= maxWidth then
self.currentWidth = maxWidth
for i = 1, self.numRows +1 do
mbitem = MultiboardGetItem(self.multiboard, i-1, 1)
MultiboardSetItemWidth(mbitem, maxWidth)
MultiboardReleaseItem(mbitem)
end
self:showToOwners() --reshow multiboard to update item widths on the frontend
end
end
---Shows the multiboard to all owners (one or all players)
function IngameConsole:showToOwners()
if self.sharedConsole or GetLocalPlayer() == self.player then
MultiboardDisplay(self.multiboard, true)
MultiboardMinimize(self.multiboard, false)
end
end
---Formats the elapsed time as "mm: ss. hh" (h being a hundreds of a sec)
function IngameConsole.formatTimerElapsed(elapsedInSeconds)
return string.format("\x2502d: \x2502.f. \x2502.f", elapsedInSeconds // 60, math.fmod(elapsedInSeconds, 60.) // 1, math.fmod(elapsedInSeconds, 1) * 100)
end
---Computes the max printable substring for a given string and a given linebreakWidth (regarding a single line of console).
---Returns both the substrings last char position and its total width in the multiboard.
---@param stringToPrint string the string supposed to be printed in the multiboard console.
---@param linebreakWidth number the maximum allowed width in one line of the console, before a string must linebreak
---@param textLanguage string 'ger' or 'eng'
---@return integer maxPrintableCharPosition, number printWidth
function IngameConsole.getLinebreakData(stringToPrint, linebreakWidth, textLanguage)
local loopWidth = 0.
local bytecodes = table.pack(string.byte(stringToPrint, 1, -1))
for i = 1, bytecodes.n do
loopWidth = loopWidth + string.charMultiboardWidth(bytecodes[i], textLanguage)
if loopWidth > linebreakWidth then
return i-1, loopWidth - string.charMultiboardWidth(bytecodes[i], textLanguage)
end
end
return bytecodes.n, loopWidth
end
-------------------------
--| Reserved Keywords |--
-------------------------
---Exits the Console
---@param self IngameConsole
function IngameConsole.keywords.exit(self)
DestroyMultiboard(self.multiboard)
DestroyTrigger(self.trigger)
DestroyTimer(self.timer)
IngameConsole.playerConsoles[self.player] = nil
if next(IngameConsole.playerConsoles) == nil then --set print function back to original, when no one has an active console left.
print = IngameConsole.originalPrint
end
end
---Lets the console print to chat
---@param self IngameConsole
function IngameConsole.keywords.printtochat(self)
self.printToConsole = false
self:out('info', 0, false, "The print function will print to the normal chat.")
end
---Lets the console print to itself (default)
---@param self IngameConsole
function IngameConsole.keywords.printtoconsole(self)
self.printToConsole = true
self:out('info', 0, false, "The print function will print to the console.")
end
---Shows the console in case it was hidden by another multiboard before
---@param self IngameConsole
function IngameConsole.keywords.show(self)
self:showToOwners() --might be necessary to do, if another multiboard has shown up and thereby hidden the console.
self:out('info', 0, false, "Console is showing.")
end
---Prints all available reserved keywords plus explanations.
---@param self IngameConsole
function IngameConsole.keywords.help(self)
self:out('info', 0, false, "The Console currently reserves the following keywords:")
self:out('info', 0, false, "'help' shows the text you are currently reading.")
self:out('info', 0, false, "'exit' closes the console.")
self:out('info', 0, false, "'lasttrace' shows the stack trace of the latest error that occured within IngameConsole.")
self:out('info', 0, false, "'share' allows other players to read and write into your console, but also force-closes their own consoles.")
self:out('info', 0, false, "'clear' clears all text from the console.")
self:out('info', 0, false, "'show' shows the console. Sensible to use, when displaced by another multiboard.")
self:out('info', 0, false, "'printtochat' lets Wc3 print text to normal chat again.")
self:out('info', 0, false, "'printtoconsole' lets Wc3 print text to the console (default).")
self:out('info', 0, false, "'autosize on' enables automatic console resize depending on the longest line in the display.")
self:out('info', 0, false, "'autosize off' retains the current console size.")
self:out('info', 0, false, "'textlang eng' will use english text installation font size to compute linebreaks (default).")
self:out('info', 0, false, "'textlang ger' will use german text installation font size to compute linebreaks.")
self:out('info', 0, false, "Preceeding a line with '>' prevents immediate execution, until a line not starting with '>' has been entered.")
end
---Clears the display of the console.
---@param self IngameConsole
function IngameConsole.keywords.clear(self)
self.output = {}
self.outputTimestamps = {}
self.outputWidths = {}
self.currentLine = 0
self:out('keywordInput', 1, false, 'clear') --we print 'clear' again. The keyword was already printed by self:processInput, but cleared immediately after.
end
---Shares the console with other players in the same game.
---@param self IngameConsole
function IngameConsole.keywords.share(self)
for _, console in pairs(IngameConsole.playerConsoles) do
if console ~= self then
IngameConsole.keywords['exit'](console) --share was triggered during console runtime, so there potentially are active consoles of others players that need to exit.
end
end
self:makeShared()
self:showToOwners() --showing it to the other players.
self:out('info', 0,false, "The console of player " .. GetConvertedPlayerId(self.player) .. " is now shared with all players.")
end
---Enables auto-sizing of console (will grow and shrink together with text size)
---@param self IngameConsole
IngameConsole.keywords["autosize on"] = function(self)
self.autosize = true
self:out('info', 0,false, "The console will now change size depending on its content.")
end
---Disables auto-sizing of console
---@param self IngameConsole
IngameConsole.keywords["autosize off"] = function(self)
self.autosize = false
self:out('info', 0,false, "The console will retain the width that it currently has.")
end
---Lets linebreaks be computed by german font size
---@param self IngameConsole
IngameConsole.keywords["textlang ger"] = function(self)
self.textLanguage = 'ger'
self:out('info', 0,false, "Linebreaks will now compute with respect to german text installation font size.")
end
---Lets linebreaks be computed by english font size
---@param self IngameConsole
IngameConsole.keywords["textlang eng"] = function(self)
self.textLanguage = 'eng'
self:out('info', 0,false, "Linebreaks will now compute with respect to english text installation font size.")
end
---Prints the stack trace of the latest error that occured within IngameConsole.
---@param self IngameConsole
IngameConsole.keywords["lasttrace"] = function(self)
self:out('error', 0,false, self.lastTrace)
end
--------------------
--| Main Trigger |--
--------------------
do
--Actions to be executed upon typing -exec
local function execCommand_Actions()
local input = string.sub(GetEventPlayerChatString(),7,-1)
print("Executing input: |cffffff44" .. input .. "|r")
--try preceeding the input by a return statement (preparation for printing below)
local loadedFunc, errorMsg = load("return ".. input)
if not loadedFunc then --if that doesn't produce valid code, try without return statement
loadedFunc, errorMsg = load(input)
end
--execute loaded function in case the string defined a valid function. Otherwise print error.
if errorMsg then
print("|cffff5555Invalid Lua-statement: " .. Debug.getLocalErrorMsg(errorMsg) .. "|r")
else
---@diagnostic disable-next-line: param-type-mismatch
local results = table.pack(Debug.try(loadedFunc))
if results[1] ~= nil or results.n > 1 then
for i = 1, results.n do
results[i] = tostring(results[i])
end
--concatenate all function return values to one colorized string
print("|cff00ffff" .. table.concat(results, ' ', 1, results.n) .. "|r")
end
end
end
local function execCommand_Condition()
return string.sub(GetEventPlayerChatString(), 1, 6) == "-exec "
end
local function startIngameConsole()
--if the triggering player already has a console, show that console and stop executing further actions
if IngameConsole.playerConsoles[GetTriggerPlayer()] then
IngameConsole.playerConsoles[GetTriggerPlayer()]:showToOwners()
return
end
--create Ingame Console object
IngameConsole.playerConsoles[GetTriggerPlayer()] = IngameConsole.create(GetTriggerPlayer())
--overwrite print function
print = function(...)
IngameConsole.originalPrint(...) --the new print function will also print "normally", but clear the text immediately after. This is to add the message to the F12-log.
if IngameConsole.playerConsoles[GetLocalPlayer()] and IngameConsole.playerConsoles[GetLocalPlayer()].printToConsole then
ClearTextMessages() --clear text messages for all players having an active console
end
for player, console in pairs(IngameConsole.playerConsoles) do
if console.printToConsole and (player == console.player) then --player == console.player ensures that the console only prints once, even if the console was shared among all players
console:out(nil, 0, false, ...)
end
end
end
end
---Creates the triggers listening to "-console" and "-exec" chat input.
---Being executed within DebugUtils (MarkGameStart overwrite).
function IngameConsole.createTriggers()
--Exec
local execTrigger = CreateTrigger()
TriggerAddCondition(execTrigger, Condition(execCommand_Condition))
TriggerAddAction(execTrigger, execCommand_Actions)
--Real Console
local consoleTrigger = CreateTrigger()
TriggerAddAction(consoleTrigger, startIngameConsole)
--Events
for i = 0, GetBJMaxPlayers() -1 do
TriggerRegisterPlayerChatEvent(execTrigger, Player(i), "-exec ", false)
TriggerRegisterPlayerChatEvent(consoleTrigger, Player(i), "-console", true)
end
end
end
Debug.endFile()
do
Config = {}
DEFAULT_TIMEOUT = 0.03125
FAST_TIMEOUT = 0.01
SFX_OFFSET = 16.0
PASSIVE_PLAYER = 0
ABILITY_LOCUST = FourCC('Aloc')
ABILITY_MOVE = FourCC('Amov')
ABILITY_PREVENT_ATTACK = FourCC('Abun')
ART_LEVELUP = "Abilities\\Spells\\Other\\Levelup\\LevelupCaster"
GAME_TIME = 0.0
DEFAULT_TIMER = 0
UNPAUSEABLE_TIMER = 0
MAX_PLAYERS = 1
SAVE_KEY = "abcdefghijklmnopqrstuvwyxz123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$"
MAP_NAME = "HallZ of Torment"
local function update_time()
GAME_TIME = GAME_TIME + 0.01
end
local function mathround(n)
return (n + 0.5 - (n % 1)) // 1
end
function Config.Init()
PASSIVE_PLAYER = Player(PLAYER_NEUTRAL_PASSIVE)
EFFECT_TIMER = TimerQueue:create()
DEFAULT_TIMER = TimerQueue:create()
UNPAUSEABLE_TIMER = TimerQueue:create()
SPAWN_TIMER = TimerQueue:create()
DEFAULT_TIMER:callPeriodically(0.01, nil, update_time)
EnablePreSelect(false,false)
--[[local clock = os.clock()
for i =1, 1000000, 1 do
local i = MathRound(5.5)
end
print(os.clock()-clock)
clock = os.clock()
for i =1, 1000000, 1 do
local i = (5.5 + 0.5 - (5.5 % 1)) // 1
end
print(os.clock()-clock)]]--
end
end
do
Alias_Table = {}
function Alias_Table:new(weights)
local total = 0
for i = 1, #weights do
total = total + weights[i]
end
local normalize = #weights / total
local norm = NewKeyTable()
local small_stack = NewKeyTable()
local big_stack = NewKeyTable()
for i = 1, #weights do
local w = weights[i]
norm[i] = w * normalize
if norm[i] < 1 then
small_stack[#small_stack+1] = i
else
big_stack[#big_stack+1] = i
end
end
local prob = NewKeyTable()
local alias = NewKeyTable()
while small_stack[1] and big_stack[1] do -- both non-empty
small = table.remove(small_stack)
large = table.remove(big_stack)
prob[small] = norm[small]
alias[small] = large
norm[large] = norm[large] + norm[small] - 1
if norm[large] < 1 then
small_stack[#small_stack+1] = large
else
big_stack[#big_stack+1] = large
end
end
for i = 1, #big_stack do prob[big_stack[i]] = 1 end
for i = 1, #small_stack do prob[small_stack[i]] = 1 end
ReleaseKeyTable(norm)
ReleaseKeyTable(small_stack)
ReleaseKeyTable(big_stack)
self.__index = self
local tbl = NewTable()
tbl.alias = alias
tbl.prob = prob
tbl.n = #weights
return setmetatable(tbl, self)
end
function Alias_Table:destroy()
ReleaseKeyTable(self.alias)
ReleaseKeyTable(self.prob)
self.alias = nil
self.prob = nil
self.n = nil
setmetatable(self,nil)
ReleaseTable(self)
end
function Alias_Table:__call()
local index = math.random(self.n)
return math.random() < self.prob[index] and index or self.alias[index]
end
end
--[[
Noise Lua v1.2.1
Port by Glint, https://github.com/eGlint/Warcraft-III-Noise-Library
Perlin Noise by Kenneth Perlin, https://mrl.nyu.edu/~perlin/noise/
Open Simplex by Kurt Spencer, https://gist.github.com/KdotJPG/b1270127455a94ac5d19
]]--
do
Noise = {}
Noise.version = "1.2.1"
Noise.permutation = {}
local function floor(value)
local n = R2I(value)
if value < 0. and value - n ~= 0. then n = n - 1 end
return n
end
local function fade(t)
return t * t * t * (t * (t * 6. - 15.) + 10.)
end
local function lerp(t, a, b)
return a + t * (b - a)
end
local function grad1D(hash, x)
local h = hash & 15
return (h & 1 == 0 and x or -x)
end
function Noise.perlin1D (x)
local X = floor(x) & 255
x = x - floor(x)
return lerp(fade(x), grad1D(Noise.permutation[X], x), grad1D(Noise.permutation[X + 1], x - 1)) * 2
end
local function grad2D(hash, x, y)
local h = hash & 15
local u, v = h < 8 and x or y, h < 4 and y or x
return (h & 1 == 0 and u or -u) + (h & 2 == 0 and v or -v)
end
function Noise.perlin2D (x, y)
local X, Y = floor(x) & 255, floor(y) & 255
x, y = x - floor(x), y - floor(y)
local u, v = fade(x), fade(y)
local A = Noise.permutation[X] + Y
local B = Noise.permutation[X + 1] + Y
local a1 = lerp(u, grad2D(Noise.permutation[A], x, y), grad2D(Noise.permutation[B], x - 1, y))
local a2 = lerp(u, grad2D(Noise.permutation[A + 1], x, y - 1), grad2D(Noise.permutation[B + 1], x - 1, y - 1))
return lerp(v, a1, a2)
end
local function grad3D(hash, x, y, z)
local h = hash & 15
local u, v = h < 8 and x or y, h < 4 and y or ((h == 12 or h == 14) and x or z)
return (h & 1 == 0 and u or -u) + (h & 2 == 0 and v or -v)
end
function Noise.perlin3D (x, y, z)
local X, Y, Z = floor(x) & 255, floor(y) & 255, floor(z) & 255
x, y, z = x - floor(x), y - floor(y), z - floor(z)
local u, v, w = fade(x), fade(y), fade(z)
local A = Noise.permutation[X] + Y
local AA = Noise.permutation[A] + Z
local AB = Noise.permutation[A + 1] + Z
local B = Noise.permutation[X + 1] + Y
local BA = Noise.permutation[B] + Z
local BB = Noise.permutation[B + 1] + Z
local a1 = lerp(u, grad3D(Noise.permutation[AA], x, y, z), grad3D(Noise.permutation[BA], x - 1, y, z))
local a2 = lerp(u, grad3D(Noise.permutation[AB], x, y - 1, z), grad3D(Noise.permutation[BB], x - 1, y - 1, z))
local b1 = lerp(u, grad3D(Noise.permutation[AA + 1], x, y, z - 1), grad3D(Noise.permutation[BA + 1], x - 1, y, z - 1))
local b2 = lerp(u, grad3D(Noise.permutation[AB + 1], x, y - 1, z - 1), grad3D(Noise.permutation[BB + 1], x - 1, y - 1, z - 1))
return lerp(w, lerp(v, a1, a2), lerp(v, b1, b2))
end
-- Deprecated as of 1.2.0, use Noise.generatePermutationTable() instead.
function Noise.permutationInit ()
print("Noise.permutationInit() is deprecated. Use Noise.generatePermutationTable() instead.")
Noise.generatePermutationTable()
end
function Noise.generatePermutationTable(getRandomIntInterface)
for i = 0, 255 do
Noise.permutation[i] = type(getRandomIntInterface) == "function" and getRandomIntInterface(0, 255) or GetRandomInt(0, 255)
Noise.permutation[i + 256] = Noise.permutation[i]
end
end
Noise.STRETCH_CONSTANT_2D = -0.211324865405187
Noise.SQUISH_CONSTANT_2D = 0.366025403784439
Noise.NORM_CONSTANT_2D = 47
Noise.PMASK = 255
Noise.SQUISH_CONSTANT_2D_X, Noise.SQUISH_CONSTANT_2D_Y = 2. * Noise.SQUISH_CONSTANT_2D, 2. * Noise.SQUISH_CONSTANT_2D
Noise.gradTable2D =
{
[0] =
{ 5, 2}, { 2, 5},
{-2, 5}, {-5, 2},
{-5, -2}, {-2, -5},
{ 2, -5}, { 5, -2}
}
local function extrapolate2D(xsb, ysb, dx, dy)
local index = Noise.permutation[Noise.permutation[xsb & Noise.PMASK] ~ (ysb & Noise.PMASK)] & 7
return Noise.gradTable2D[index][1] * dx + Noise.gradTable2D[index][2] * dy
end
function Noise.openSimplex2D(x, y,scale)
x = x * scale
y = y * scale
local strechOffset = (x + y) * Noise.STRETCH_CONSTANT_2D
local xs = x + strechOffset
local ys = y + strechOffset
local xsb = floor(xs)
local ysb = floor(ys)
local squishOffset = (xsb + ysb) * Noise.SQUISH_CONSTANT_2D
local xb = xsb + squishOffset
local yb = ysb + squishOffset
local xins = xs - xsb
local yins = ys - ysb
local inSum = xins + yins
local dx0 = x - xb
local dy0 = y - yb
local dx_ext
local dy_ext
local xsv_ext
local ysv_ext
local value = 0.
local dx1 = dx0 - 1. - Noise.SQUISH_CONSTANT_2D
local dy1 = dy0 - Noise.SQUISH_CONSTANT_2D
local attn1 = 2. - dx1 * dx1 - dy1 * dy1
local dx2 = dx0 - Noise.SQUISH_CONSTANT_2D
local dy2 = dy0 - 1. - Noise.SQUISH_CONSTANT_2D
local attn2 = 2. - dx2 * dx2 - dy2 * dy2
local ins
local attn0
local attn_ext
if attn1 > 0. then
attn1 = attn1 * attn1
value = attn1 * attn1 * extrapolate2D(xsb + 1, ysb, dx1, dy1)
end
if attn2 > 0. then
attn2 = attn2 * attn2
value = value + attn2 * attn2 * extrapolate2D(xsb, ysb + 1, dx2, dy2)
end
if inSum <= 1 then
zins = 1. - inSum
if zins > xins or zins > yins then
if xins > yins then
xsv_ext = xsb + 1
ysv_ext = ysb - 1
dx_ext = dx0 - 1.
dy_ext = dy0 + 1.
else
xsv_ext = xsb - 1
ysv_ext = ysb + 1
dx_ext = dx0 + 1.
dy_ext = dy0 - 1.
end
else
xsv_ext = xsb + 1
ysv_ext = ysb + 1
dx_ext = dx0 - 1. - Noise.SQUISH_CONSTANT_2D_X
dy_ext = dy0 - 1. - Noise.SQUISH_CONSTANT_2D_Y
end
else
zins = 2. - inSum
if zins < xins or zins < yins then
if xins > yins then
xsv_ext = xsb + 2
ysv_ext = ysb
dx_ext = dx0 - 2. - Noise.SQUISH_CONSTANT_2D_X
dy_ext = dy0 - Noise.SQUISH_CONSTANT_2D_Y
else
xsv_ext = xsb
ysv_ext = ysb + 2
dx_ext = dx0 - Noise.SQUISH_CONSTANT_2D_X
dy_ext = dy0 - 2. - Noise.SQUISH_CONSTANT_2D_Y
end
else
dx_ext = dx0
dy_ext = dy0
xsv_ext = xsb
ysv_ext = ysb
end
xsb = xsb + 1
ysb = ysb + 1
dx0 = dx0 - 1. - Noise.SQUISH_CONSTANT_2D_X
dy0 = dy0 - 1. - Noise.SQUISH_CONSTANT_2D_Y
end
attn0 = 2. - dx0 * dx0 - dy0 * dy0
if attn0 > 0. then
attn0 = attn0 * attn0
value = value + attn0 * attn0 * extrapolate2D(xsb, ysb, dx0, dy0)
end
attn_ext = 2. - dx_ext * dx_ext - dy_ext * dy_ext
if attn_ext > 0. then
attn_ext = attn_ext * attn_ext
value = value + attn_ext * attn_ext * extrapolate2D(xsv_ext, ysv_ext, dx_ext, dy_ext)
end
return value / Noise.NORM_CONSTANT_2D
end
function Noise.initialize()
Noise.generatePermutationTable()
end
Noise.initialize()
end
do
function Noise.octavePerlin1D(x, octaves, persistence)
local total, frequency, amplitude, maxValue = 0., 1., 1., 0.
for i = 0, octaves - 1 do
total = Noise.perlin1D(x * frequency) * amplitude
maxValue = maxValue + amplitude
amplitude = amplitude * persistence
frequency = frequency * 2
end
return total / maxValue
end
function Noise.octavePerlin2D(x, y, octaves, persistence)
local total, frequency, amplitude, maxValue = 0., 1., 1., 0.
for i = 0, octaves - 1 do
total = Noise.perlin2D(x * frequency, y * frequency) * amplitude
maxValue = maxValue + amplitude
amplitude = amplitude * persistence
frequency = frequency * 2
end
return total / maxValue
end
function Noise.octavePerlin3D(x, y, z, octaves, persistence)
local total, frequency, amplitude, maxValue = 0., 1., 1., 0.
for i = 0, octaves - 1 do
total = Noise.perlin3D(x * frequency, y * frequency, z * frequency) * amplitude
maxValue = maxValue + amplitude
amplitude = amplitude * persistence
frequency = frequency * 2
end
return total / maxValue
end
end
do
local d, m
RNG = {
seed = 0,
mod = 2147483647,
inc = 0,
mult = 16807
}
RNG.__index = RNG
function RNG.new(tbl)
local self = tbl or NewTable()
setmetatable(self,RNG)
return self
end
function RNG:set_seed(val)
self.seed = math.floor(wrap(val,0,self.mod))
end
function RNG:destroy()
self.seed = nil
self.mult = nil
self.mod = nil
self.inc = nil
setmetatable(self,nil)
ReleaseTable(self)
end
function RNG:push()
d = self.seed * self.mult + self.inc
m = d - math.floor(d / self.mod) * self.mod
if m < 0 then
m = m + self.mod
end
self.seed = m
end
function RNG:random(low,high)
self:push()
m = self.seed / self.mod
if low then
if high then
m = m*(high-low)+low
else
m = m *(low-1) + 1
end
if math.type(low) == "integer" then
m = m>=0 and math.floor(m+0.5) or math.ceil(m-0.5)
end
end
return m
end
end
do
local map = {
I = 1,
V = 5,
X = 10,
L = 50,
C = 100,
D = 500,
M = 1000,
}
local numbers = { 1, 5, 10, 50, 100, 500, 1000 }
local chars = { "I", "V", "X", "L", "C", "D", "M" }
local RomanNumerals = { }
function ToRomanNumerals(s)
s = math.floor(tonumber(s))
if s <= 0 then return s end
local ret = ""
for i = #numbers, 1, -1 do
local num = numbers[i]
while s - num >= 0 and s > 0 do
ret = ret .. chars[i]
s = s - num
end
for j = 1, i - 1 do
local n2 = numbers[j]
if s - (num - n2) >= 0 and s < num and s > 0 and num - n2 ~= n2 then
ret = ret .. chars[j] .. chars[i]
s = s - (num - n2)
break
end
end
end
return ret
end
function RomanNumerals.ToNumber(s)
s = s:upper()
local ret = 0
local i = 1
while i <= s:len() do
local c = s:sub(i, i)
if c ~= " " then
local m = map[c]
local next = s:sub(i + 1, i + 1)
local nextm = map[next]
if next and nextm then
if nextm > m then
ret = ret + (nextm - m)
i = i + 1
else
ret = ret + m
end
else
ret = ret + m
end
end
i = i + 1
end
return ret
end
end
if Debug and Debug.beginFile then Debug.beginFile("TimerQueue") end
--[[------------------------------------------------------------------------------------------------------------------------------------------------------------
*
* --------------------------------
* | TimerQueue and Stopwatch 1.1 |
* --------------------------------
*
* - by Eikonium and AGD
*
* -> https://www.hiveworkshop.com/threads/timerqueue-stopwatch.339411/
* - This is basically the enhanced and instancifiable version of ExecuteDelayed 1.0.4 by AGD https://www.hiveworkshop.com/threads/lua-delayedaction.321072/
*
* --------------------
* | TimerQueue class |
* --------------------
* - A TimerQueue is an object used to execute delayed function calls. It can queue any number of function calls at the same time, while being based on a single timer. This offers much better performance than starting many separate timers.
* - The class also provides methods to pause, resume, reset and destroy a TimerQueue - and even includes error handling.
* - As such, you can create as many independent TimerQueues as you like, which you can individually use, pause, reset, etc.
* - All methods can also be called on the class directly, which frees you from needing to create a TimerQueue object in the first place. You still need colon-notation!
* TimerQueue.create() --> TimerQueue
* - Creates a new TimerQueue with its own independent timer and function queue.
* <TimerQueue>:callDelayed(number delay, function callback, ...)
* - Calls the specified function (or callable table) after the specified delay (in seconds) with the specified arguments (...). Does not delay the following lines of codes.
* <TimerQueue>:callPeriodically(number delay, function|nil stopCondition, function callback, ...)
* - Periodically calls the specified function (or callable table) after the specified delay (in seconds) with the specified arguments (...). Stops, when the specified condition resolves to true.
* - The stop-condition must be a function returning a boolean. It is checked after each callback execution and is passed the same arguments as the callback (...) (which you can still ignore).
* - You can pass nil instead of a function to let the periodic execution repeat forever.
* <TimerQueue>:reset()
* - Discards all queued function calls from the Timer Queue. Discarded function calls are not executed.
* - You can continue to use <TimerQueue>:callDelayed after resetting it.
* <TimerQueue>:pause()
* - Pauses the TimerQueue at its current point in time, effectively freezing all delayed function calls that it currently holds, until the queue is resumed.
* - Using <TimerQueue>:callDelayed on a paused queue will correctly add the new callback to the queue, but time will start ticking only after resuming the queue.
* <TimerQueue>:isPaused() --> boolean
* - Returns true, if the TimerQueue is paused, and false otherwise.
* <TimerQueue>:resume()
* - Resumes a TimerQueue that was previously paused. Has no effect on TimerQueues that are not paused.
* <TimerQueue>:destroy()
* - Destroys the Timer Queue. Remaining function calls are discarded and not being executed.
* <TimerQueue>.debugMode : boolean
* - TimerQueues come with their own error handling in case you are not using DebugUtils (https://www.hiveworkshop.com/threads/debug-utils-ingame-console-etc.330758/).
* - Set to true to let erroneous function calls through <TimerQueue>:callDelayed print error messages on screen (only takes effect, if Debug Utils is not present. Otherwise you get Debug Utils error handling, which is even better).
* - Set to false to not trigger error messages after erroneous callbacks. Do this before map release.
* - Default: true.
* -------------------
* | Stopwatch class |
* -------------------
* - Stopwatches count upwards, i.e. they measure the time passed since you've started them. Thus, they can't trigger any callbacks (use normal timers or TimerQueues for that).
* Stopwatch.create(boolean startImmediately_yn) --> Stopwatch
* - Creates a Stopwatch. Set boolean param to true to start it immediately.
* <Stopwatch>:start()
* - Starts or restarts a Stopwatch, i.e. resets the elapsed time of the Stopwatch to zero and starts counting upwards.
* <Stopwatch>:getElapsed() --> number
* - Returns the time in seconds that a Stopwatch is currently running, i.e. the elapsed time since start.
* <Stopwatch>:pause()
* - Pauses a Stopwatch, so it will retain its current elapsed time, until resumed.
* <Stopwatch>:resume()
* - Resumes a Stopwatch after having been paused.
* <Stopwatch>:destroy()
* - Destroys a Stopwatch. Maybe necessary to prevent memory leaks. Not sure, if lua garbage collection also collects warcraft objects...
---------------------------------------------------------------------------------------------------------------------------------------------------------]]
do
---@class TimerQueueElement
---@field [integer] any arguments to be passed to callback
TimerQueueElement = {
next = nil ---@type TimerQueueElement next TimerQueueElement to expire after this one
, timeout = 0. ---@type number time between previous callback and this one
, callback = function() end ---@type function callback to be executed
, n = 0 ---@type integer number of arguments passed
}
TimerQueueElement.__index = TimerQueueElement
TimerQueueElement.__name = 'TimerQueueElement'
function TimerQueueElement.create(timeout, callback, ...)
local new = setmetatable({timeout = timeout, callback = callback, n = select('#', ...), ...}, TimerQueueElement)
new.next = new
return new
end
---@class TimerQueue
TimerQueue = {
timer = nil ---@type timer the single timer this system is based on (one per instance of course)
, queue = TimerQueueElement.create() -- queue of waiting callbacks to be executed in the future
, n = 0 ---@type integer number of elements in the queue
, on_expire = function() end ---@type function callback to be executed upon timer expiration (defined further below).
, debugMode = true ---@type boolean setting this to true will print error messages, when the input function couldn't be executed properly. Set this to false before releasing your map.
, paused = false ---@type boolean whether the queue is paused or not
}
TimerQueue.__index = TimerQueue
TimerQueue.__name = 'TimerQueue'
--Creates a timer on first access of the static TimerQueue:callDelayed method. Avoids timer creation inside the Lua root.
setmetatable(TimerQueue, {__index = function(t,k) if k == 'timer' then t[k] = CreateTimer() end; return rawget(t,k) end})
local unpack, max, timerStart, timerGetElapsed, pauseTimer = table.unpack, math.max, TimerStart, TimerGetElapsed, PauseTimer
---@param timerQueue TimerQueue
local function on_expire(timerQueue)
local queue, timer = timerQueue.queue, timerQueue.timer
local topOfQueue = queue.next
queue.next = topOfQueue.next
timerQueue.n = timerQueue.n - 1
if timerQueue.n > 0 then
timerStart(timer, queue.next.timeout, false, timerQueue.on_expire)
else
-- These two functions below may not be necessary
timerStart(timer, 0, false, nil) --don't put in on_expire as handlerFunc, because it can still expire and reduce n to a value < 0.
pauseTimer(timer)
end
if Debug and Debug.try then
Debug.try(topOfQueue.callback, unpack(topOfQueue, 1, topOfQueue.n))
else
local errorStatus, errorMessage = pcall(topOfQueue.callback, unpack(topOfQueue, 1, topOfQueue.n))
if timerQueue.debugMode and not errorStatus then
print("|cffff5555ERROR during TimerQueue callback: " .. errorMessage .. "|r")
end
end
end
TimerQueue.on_expire = function() on_expire(TimerQueue) end
function TimerQueue.create()
local new = {}
setmetatable(new, TimerQueue)
new.timer = CreateTimer()
new.queue = TimerQueueElement.create()
new.on_expire = function() on_expire(new) end
return new
end
function TimerQueue:callDelayed(timeout, callback, ...)
timeout = math.max(timeout, 0.)
local queue = self.queue
self.n = self.n + 1
local current = queue
local current_timeout = current.next.timeout - max(timerGetElapsed(self.timer), 0.) -- don't use TimerGetRemaining to prevent bugs for expired and previously paused timers.
while current.next ~= queue and timeout >= current_timeout do --there is another element in the queue and the new element shall be executed later than the current
timeout = timeout - current_timeout
current = current.next
current_timeout = current.next.timeout
end
local new = TimerQueueElement.create(timeout, callback, ...)
new.next = current.next
current.next = new
if current == queue then --New callback is the next to expire
new.next.timeout = max(current_timeout - timeout, 0.) --adapt element that was previously on top. Subtract new timeout and subtract timer elapsed time to get new timeout.
timerStart(self.timer, timeout, false, self.on_expire)
if self.paused then
self:pause()
end
else
new.next.timeout = max(new.next.timeout - timeout, 0.) --current.next might be the root element (queue), so prevent that from dropping below 0. (although it doesn't really matter)
end
end
function TimerQueue:callPeriodically(timeout, stopCondition, callback, ...)
local func
func = function(...)
callback(...)
if not (stopCondition and stopCondition(...)) then
self:callDelayed(timeout, func, ...)
end
end
self:callDelayed(timeout, func, ...)
end
function TimerQueue:reset()
timerStart(self.timer, 0., false, nil) --dont't put in on_expire as handlerFunc. callback can still expire after pausing and resuming the empty queue, which would set n to a value < 0.
pauseTimer(self.timer)
self.n = 0
self.queue = TimerQueueElement.create()
end
function TimerQueue:pause()
self.paused = true
pauseTimer(self.timer)
end
function TimerQueue:isPaused()
return self.paused
end
---Resumes a TimerQueue that was paused previously. Has no effect on running TimerQueues.
function TimerQueue:resume()
if self.paused then
self.paused = false
self.queue.next.timeout = self.queue.next.timeout - timerGetElapsed(self.timer) --need to restart from 0, because TimerGetElapsed(resumedTimer) is doing so as well after a timer is resumed.
ResumeTimer(self.timer)
end
end
---Destroys the timer object behind the TimerQueue. The Lua object will be automatically garbage collected once you ensure that there is no more reference to it.
function TimerQueue:destroy()
pauseTimer(self.timer) --https://www.hiveworkshop.com/threads/issues-with-timer-functions.309433/ suggests that non-paused destroyed timers can still execute their callback
DestroyTimer(self.timer)
end
---Prints the queued callbacks within the TimerQueue. For debugging purposes.
---@return string
function TimerQueue:tostring()
local current, result, i = self.queue.next, {}, 0
local args = {}
while current ~= self.queue do
i = i + 1
for j = 1, current.n do
args[j] = tostring(current[j])
end
result[i] = '(i=' .. i .. ',timeout=' .. current.timeout .. ',f=' .. tostring(current.callback) .. ',args={' .. table.concat(args, ',',1,current.n) .. '})'
current = current.next
end
return '{n = ' .. self.n .. ',queue=(' .. table.concat(result, ',', 1, i) .. ')}'
end
---@class Stopwatch
Stopwatch = {
timer = {} ---@type timer the countdown-timer permanently cycling
, elapsed = 0. ---@type number the number of times the timer reached 0 and restarted
, increaseElapsed = function() end ---@type function timer callback function to increase numCycles by 1 for a specific Stopwatch.
}
Stopwatch.__index = Stopwatch
local CYCLE_LENGTH = 3600. --time in seconds that a timer needs for one cycle. doesn't really matter.
---Creates a Stopwatch.
---@param startImmediately_yn boolean Set to true to start immediately. If not specified or set to false, the Stopwatch will not start to count upwards.
function Stopwatch.create(startImmediately_yn)
local new = {}
setmetatable(new, Stopwatch)
new.timer = CreateTimer()
new.elapsed = 0.
new.increaseElapsed = function() new.elapsed = new.elapsed + CYCLE_LENGTH end
if startImmediately_yn then
new:start()
end
return new
end
---Starts or restarts a Stopwatch, i.e. resets the elapsed time of the Stopwatch to zero and starts counting upwards.
function Stopwatch:start()
self.elapsed = 0.
TimerStart(self.timer, CYCLE_LENGTH, true, self.increaseElapsed)
end
---Returns the time in seconds that a Stopwatch is currently running, i.e. the elapsed time since start.
---@return number
function Stopwatch:getElapsed()
return self.elapsed + TimerGetElapsed(self.timer)
end
---Pauses a Stopwatch, so it will retain its current elapsed time, until resumed.
function Stopwatch:pause()
PauseTimer(self.timer)
end
---Resumes a Stopwatch after having been paused.
function Stopwatch:resume()
self.elapsed = self.elapsed + TimerGetElapsed(self.timer)
TimerStart(self.timer, CYCLE_LENGTH, true, self.increaseElapsed) --not using ResumeTimer here, as it actually starts timer from new with the remaining time and thus screws up TimerGetElapsed().
end
---Destroys the timer object behind the Stopwatch. The Lua object will be automatically garbage collected once you ensure that there is no more reference to it.
function Stopwatch:destroy()
DestroyTimer(self.timer)
end
end
if Debug and Debug.endFile then Debug.endFile() end
do
Polygon = {}
Polygon.__index = Polygon
local function get_bouding_box(points,j)
local minX,minY,maxX,maxY = points[1],points[2],points[1],points[2]
for i = 1, j, 2 do
local qx = points[i]
local qy = points[i+1]
minX,maxX,minY,maxY = math.min(qx,minX),math.max(qx,maxX),math.min(qy,minY),math.max(qy,maxY)
end
return minX,maxX,minY,maxY
end
function Polygon.get_bounding_box(self)
self.minX, self.maxX, self.minY, self.maxY = get_bouding_box(self.points,#self.points)
self.is_dirty = false
end
function Polygon.new(origin_x,origin_y,...)
local self = setmetatable({
origin_x = origin_x,
origin_y = origin_y,
points = {...},
}, Polygon)
self:get_bounding_box()
return self
end
function Polygon.move_to(self,x,y)
self.origin_x = x
self.origin_y = y
end
function Polygon.rotate(self,angle)
local cos_a = math.cos(angle)
local sin_a = math.sin(angle)
for i = 1, #self.points, 2 do
local x = self.points[i]
local y = self.points[i+1]
self.points[i] = x * cos_a - y * sin_a
self.points[i+1] = x * sin_a + y * cos_a
end
self.is_dirty = true
end
function Polygon.scale(self,scale_x,scale_y)
for i = 1, #self.points, 2 do
self.points[i] = self.points[i] * scale_x
self.points[i+1] = self.points[i+1] * scale_y
end
end
local function inside(x,y,points,j)
local is_inside = false
local l = j -1
for i = 1, j, 2 do
local qx = points[i]
local qy = points[i+1]
local ay = points[l+1]
if (qy > y) ~= (ay > y) and x < (points[l] - qx) * (y - qy) / (ay - qy) + qx then
is_inside = not(is_inside)
end
l = i
end
return is_inside
end
function Polygon.inside(self,x,y)
x = x - self.origin_x
y = y - self.origin_y
if self.is_dirty then
self:get_bounding_box()
end
if x < self.minX or x > self.maxX or y < self.minY or y > self.maxY then
return false
end
return inside(x,y,self.points,#self.points)
end
local ex_tbl = {}
function Polygon.insideEx(self,x,y,angle,scale_x,scale_y)
x = x - self.origin_x
y = y - self.origin_y
local cos_a = math.cos(angle)
local sin_a = math.sin(angle)
for i = 1, #self.points, 2 do
local x1 = self.points[i] * scale_x
local y1 = self.points[i+1] * scale_y
ex_tbl[i] = x1 * cos_a - y1 * sin_a
ex_tbl[i+1] = x1 * sin_a + y1 * cos_a
end
local minX,maxX,minY,maxY = get_bouding_box(ex_tbl,#self.points)
if x < minX or x > maxX or y < minY or y > maxY then
return false
end
return inside(x,y,ex_tbl,#self.points)
end
function Polygon.debug(self,state)
self.debug_data = self.debug_data or {}
if state then
local points = self.points
local j = #points
local l = j -1
local ox = self.origin_x
local oy = self.origin_y
for i = 1, j, 2 do
local qx = points[i] + ox
local qy = points[i+1] + oy
local ax = points[l] + ox
local ay = points[l+1] + oy
table.insert(self.debug_data,
AddLightningEx("FORK", false, qx, qy,0, ax, ay,0)
)
l = i
end
else
for i = 1, #self.debug_data, 1 do
DestroyLightning(self.debug_data[i])
self.debug_data[i] = nil
end
end
end
end
do
local vx = {}
local vy = {}
local vz = {}
local vvallocNext = -1
local vvx = {}
local vvy = {}
local vvz = {}
local vvw = {}
local mallocNext = -1
local mElems = {}
local gLoc
local function vVec(x, y, z)
vvallocNext = vvallocNext + 1
if 1024 == vvallocNext then vvallocNext = -1 end
vx[vvallocNext], vy[vvallocNext], vz[vvallocNext] = x, y, z
return vvallocNext
end
local function vSub( a, b)
return vVec(vx[a] - vx[b], vy[a] - vy[b], vz[a] - vz[b])
end
local function vNorm ( v )
local l = 1.0 / math.sqrt(vx[v] * vx[v] + vy[v] * vy[v] + vz[v] * vz[v])
return vVec(l * vx[v], l * vy[v], l * vz[v])
end
local function vDot( a, b)
return vx[a] * vx[b] + vy[a] * vy[b] + vz[a] * vz[b]
end
local function vCross( a, b)
return vVec(vy[a] * vz[b] - vz[a] * vy[b], vz[a] * vx[b] - vx[a] * vz[b], vx[a] * vy[b] - vy[a] * vx[b])
end
local function vNearlyParallel( a, b, epsilon)
local s0 = vx[a] * vx[b] + vy[a] * vy[b] + vz[a] * vz[b]
local s1 = vx[a] * vx[a] + vy[a] * vy[a] + vz[a] * vz[a]
local s2 = vx[b] * vx[b] + vy[b] * vy[b] + vz[b] * vz[b]
local d = s0 * s0 - s1 * s2
if d < 0 then
d = -d
end
return d < epsilon
end
local function vvVec( x, y, z, w)
vvallocNext = vvallocNext + 1
if 1024 == vvallocNext then vvallocNext = -1 end
vvx[vvallocNext] = x
vvy[vvallocNext] = y
vvz[vvallocNext] = z
vvw[vvallocNext] = w
return vvallocNext
end
function mZero()
mallocNext = mallocNext + 1
if 1024 == mallocNext then mallocNext = 0 end
local a = 0 - 1
local m16 = 16 * mallocNext
while 16 ~= a do
a = a + 1
mElems[m16 + a] = 0.0
end
return mallocNext
end
local function mLookAt1 ( f, s, u, eye )
mallocNext = mallocNext + 1
if 1024 == mallocNext then mallocNext = 0 end
local m16 = 16 * mallocNext
mElems[m16] = vx[s]
mElems[m16 + 4] = vx[u]
mElems[m16 + 8] = -vx[f]
mElems[m16 + 12] = 0.0
mElems[m16 + 1] = vy[s]
mElems[m16 + 5] = vy[u]
mElems[m16 + 9] = -vy[f]
mElems[m16 + 13] = 0.0
mElems[m16 + 2] = vz[s]
mElems[m16 + 6] = vz[u]
mElems[m16 + 10] = -vz[f]
mElems[m16 + 14] = 0.0
mElems[m16 + 3] = -vDot(s, eye)
mElems[m16 + 7] = -vDot(u, eye)
mElems[m16 + 11] = vDot(f, eye)
mElems[m16 + 15] = 1.0
return mallocNext
end
local function mLookAt_RH( eye, target, up )
local f = vNorm(vSub(target, eye))
local s = vNorm(vCross(f, up))
local u = vCross(s, f)
return mLookAt1(f, s, u, eye)
end
local function getCotangent ( fov, aspect)
return 1.0 / math.tan(0.5 * fov / math.sqrt(aspect*aspect + 1.0))
end
local function mPerspective_RH_NO ( fov, aspect, near, far )
local m = mZero()
local m16 = m * 16
local cotangent = getCotangent(fov, aspect)
mElems[m16] = cotangent / aspect
mElems[m16 + 5] = cotangent
mElems[m16 + 10] = (near + far) / (near - far)
mElems[m16 + 11] = (2.0 * near * far) / (near - far)
mElems[m16 + 14] = -1.0
return m
end
local function mMulM ( a, b )
mallocNext = mallocNext + 1
if 1024 == mallocNext then mallocNext = 0 end
local a16 = 16 * a
local b16 = 16 * b
local m16 = 16 * mallocNext
mElems[m16] = mElems[a16] * mElems[b16] + mElems[a16 + 1] * mElems[b16 + 4] + mElems[a16 + 2] * mElems[b16 + 8] + mElems[a16 + 3] * mElems[b16 + 12]
mElems[m16 + 1] = mElems[a16] * mElems[b16 + 1] + mElems[a16 + 1] * mElems[b16 + 5] + mElems[a16 + 2] * mElems[b16 + 9] + mElems[a16 + 3] * mElems[b16 + 13]
mElems[m16 + 2] = mElems[a16] * mElems[b16 + 2] + mElems[a16 + 1] * mElems[b16 + 6] + mElems[a16 + 2] * mElems[b16 + 10] + mElems[a16 + 3] * mElems[b16 + 14]
mElems[m16 + 3] = mElems[a16] * mElems[b16 + 3] + mElems[a16 + 1] * mElems[b16 + 7] + mElems[a16 + 2] * mElems[b16 + 11] + mElems[a16 + 3] * mElems[b16 + 15]
mElems[m16 + 4] = mElems[a16 + 4] * mElems[b16] + mElems[a16 + 5] * mElems[b16 + 4] + mElems[a16 + 6] * mElems[b16 + 8] + mElems[a16 + 7] * mElems[b16 + 12]
mElems[m16 + 5] = mElems[a16 + 4] * mElems[b16 + 1] + mElems[a16 + 5] * mElems[b16 + 5] + mElems[a16 + 6] * mElems[b16 + 9] + mElems[a16 + 7] * mElems[b16 + 13]
mElems[m16 + 6] = mElems[a16 + 4] * mElems[b16 + 2] + mElems[a16 + 5] * mElems[b16 + 6] + mElems[a16 + 6] * mElems[b16 + 10] + mElems[a16 + 7] * mElems[b16 + 14]
mElems[m16 + 7] = mElems[a16 + 4] * mElems[b16 + 3] + mElems[a16 + 5] * mElems[b16 + 7] + mElems[a16 + 6] * mElems[b16 + 11] + mElems[a16 + 7] * mElems[b16 + 15]
mElems[m16 + 8] = mElems[a16 + 8] * mElems[b16] + mElems[a16 + 9] * mElems[b16 + 4] + mElems[a16 + 10] * mElems[b16 + 8] + mElems[a16 + 11] * mElems[b16 + 12]
mElems[m16 + 9] = mElems[a16 + 8] * mElems[b16 + 1] + mElems[a16 + 9] * mElems[b16 + 5] + mElems[a16 + 10] * mElems[b16 + 9] + mElems[a16 + 11] * mElems[b16 + 13]
mElems[m16 + 10] = mElems[a16 + 8] * mElems[b16 + 2] + mElems[a16 + 9] * mElems[b16 + 6] + mElems[a16 + 10] * mElems[b16 + 10] + mElems[a16 + 11] * mElems[b16 + 14]
mElems[m16 + 11] = mElems[a16 + 8] * mElems[b16 + 3] + mElems[a16 + 9] * mElems[b16 + 7] + mElems[a16 + 10] * mElems[b16 + 11] + mElems[a16 + 11] * mElems[b16 + 15]
mElems[m16 + 12] = mElems[a16 + 12] * mElems[b16] + mElems[a16 + 13] * mElems[b16 + 4] + mElems[a16 + 14] * mElems[b16 + 8] + mElems[a16 + 15] * mElems[b16 + 12]
mElems[m16 + 13] = mElems[a16 + 12] * mElems[b16 + 1] + mElems[a16 + 13] * mElems[b16 + 5] + mElems[a16 + 14] * mElems[b16 + 9] + mElems[a16 + 15] * mElems[b16 + 13]
mElems[m16 + 14] = mElems[a16 + 12] * mElems[b16 + 2] + mElems[a16 + 13] * mElems[b16 + 6] + mElems[a16 + 14] * mElems[b16 + 10] + mElems[a16 + 15] * mElems[b16 + 14]
mElems[m16 + 15] = mElems[a16 + 12] * mElems[b16 + 3] + mElems[a16 + 13] * mElems[b16 + 7] + mElems[a16 + 14] * mElems[b16 + 11] + mElems[a16 + 15] * mElems[b16 + 15]
return mallocNext
end
local function mMulVv ( m, vv )
local m16 = 16 * m
local x = vvx[vv] * mElems[m16]
local y = vvx[vv] * mElems[m16 + 4]
local z = vvx[vv] * mElems[m16 + 8]
local w = vvx[vv] * mElems[m16 + 12]
x = x + vvy[vv] * mElems[m16 + 1]
y = y + vvy[vv] * mElems[m16 + 5]
z = z + vvy[vv] * mElems[m16 + 9]
w = w + vvy[vv] * mElems[m16 + 13]
x = x + vvz[vv] * mElems[m16 + 2]
y = y + vvz[vv] * mElems[m16 + 6]
z = z + vvz[vv] * mElems[m16 + 10]
w = w + vvz[vv] * mElems[m16 + 14]
x = x + vvw[vv] * mElems[m16 + 3]
y = y + vvw[vv] * mElems[m16 + 7]
z = z + vvw[vv] * mElems[m16 + 11]
w = w + vvw[vv] * mElems[m16 + 15]
return vvVec(x, y, z, w)
end
local function afterInitGetZ(x,y)
MoveLocation(gLoc, x, y)
return GetLocationZ(gLoc)
end
local function getTerrainZ ( x, y )
gLoc = Location(x, y)
getTerrainZ = afterInitGetZ
return GetLocationZ(gLoc)
end
local c1 = (0.96 + 1.54)
local c2 = (0.98 + 0.99)
local aspect = 0.8 / (0.58 - 0.13)
local fov = 0
local near = 0
local far = 0
local cacheCE
local cacheCT
local cacheLast = 0.0
local ce = vVec(0.0, 0.0, 0.0)
local ct = vVec(0.0, 0.0, 0.0)
local pitch = 0
function GetScreenXY( mwx, mwy, z_offset )
local mwz = getTerrainZ(mwx, mwy) + (z_offset or 0)
if cacheLast ~= GAME_TIME then
fov = GetCameraField(CAMERA_FIELD_FIELD_OF_VIEW)
near = GetCameraField(CAMERA_FIELD_NEARZ)
far = GetCameraField(CAMERA_FIELD_FARZ)
pitch = GetCameraField(CAMERA_FIELD_ANGLE_OF_ATTACK)
ce = vVec(GetCameraEyePositionX(), GetCameraEyePositionY(), GetCameraEyePositionZ())
ct = vVec(GetCameraTargetPositionX(), GetCameraTargetPositionY(), GetCameraTargetPositionZ())
cacheLast = GAME_TIME
else
ce = vVec(vx[ce], vy[ce], vz[ce])
ct = vVec(vx[ct], vy[ct], vz[ct])
end
local up = vVec(0.0, 0.0, 1.0)
if vNearlyParallel(vSub(ct, ce), up, 1000.0) then up = vVec(0.0, 1.0, 0.0) end
local m0 = mLookAt_RH(ce, ct, up)
local m1 = mPerspective_RH_NO(fov, aspect, near, far)
local vv = mMulVv(mMulM(m1, m0), vvVec(mwx, mwy, mwz, 1.0))
local x,y
if 1.55334281 < pitch and pitch < 4.69493501 then
x,y = vvx[vv] / vvw[vv], -(vvy[vv] / vvw[vv])
else
x,y = vvx[vv] / vvw[vv], vvy[vv] / vvw[vv]
end
local yadjust = (pitch - 5.305801) * 0.08
return ((x + 0.99)*0.8) / c2, ((y + 1.54)*0.58)/c1 - yadjust
end
end
if Debug then Debug.beginFile "FastWorld2ScreenTransform" end
do
--[[
=============================================================================================================================================================
Fast World2Screen Transform
by Antares
Transform from world to screen coordinates and back.
Requires:
TotalInitialization https://www.hiveworkshop.com/threads/.317099/
PrecomputedHeightMap (optional) https://www.hiveworkshop.com/threads/.353477/
=============================================================================================================================================================
A P I
=============================================================================================================================================================
World2Screen(x, y, z, player) Takes the world coordinates x, y, and z and returns the screen positions x and y of that point on the
specified player's screen. The third return value specifies if the point is on the screen.
Screen2World(x, y, player) Takes the screen coordinates x and y and the player for which the calculation should be performed and
returns the x, y, and z world coordinates. The fourth return value specifies whether or not the search for
the world intersection point converged. This function may fail when a ray pointing from the camera to the
world intersects at multiple points with the terrain.
=============================================================================================================================================================
C O N F I G
=============================================================================================================================================================
]]
--The interval between updates of each player's camera parameters.
local TIME_STEP = 0.02 ---@type number
--[[
=============================================================================================================================================================
E N D O F C O N F I G
=============================================================================================================================================================
]]
local cos = math.cos
local sin = math.sin
local sqrt = math.sqrt
local preCalc = false ---@type boolean
local eyeX = 0 ---@type number
local eyeY = 0 ---@type number
local eyeZ = 0 ---@type number
local cosRot = 0 ---@type number
local sinRot = 0 ---@type number
local cosAttack = 0 ---@type number
local sinAttack = 0 ---@type number
local angleOfAttack = 0 ---@type number
local rotation = 0 ---@type number
local cosAttackCosRot = 0 ---@type number
local cosAttackSinRot = 0 ---@type number
local sinAttackCosRot = 0 ---@type number
local sinAttackSinRot = 0 ---@type number
local yCenterScreenShift = 0 ---@type number
local scaleFactor = 0 ---@type number
local GetTerrainZ ---@type function
local moveableLoc ---@type location
local function CameraPrecalc(player)
eyeX = GetCameraEyePositionX()
eyeY = GetCameraEyePositionY()
eyeZ = GetCameraEyePositionZ()
angleOfAttack = GetCameraField(CAMERA_FIELD_ANGLE_OF_ATTACK)
rotation = GetCameraField(CAMERA_FIELD_ROTATION)
local fieldOfView = GetCameraField(CAMERA_FIELD_FIELD_OF_VIEW)
cosAttack = cos(angleOfAttack)
sinAttack = sin(angleOfAttack)
cosRot = cos(rotation)
sinRot = sin(rotation)
yCenterScreenShift = 0.1284*cosAttack
scaleFactor = 0.0524*fieldOfView^3 - 0.0283*fieldOfView^2 + 1.061*fieldOfView
cosAttackCosRot = cosAttack*cosRot
cosAttackSinRot = cosAttack*sinRot
sinAttackCosRot = sinAttack*cosRot
sinAttackSinRot = sinAttack*sinRot
preCalc = true
end
---Takes the world coordinates x, y, and z and returns the screen positions x and y of that point on the specified player's screen. The third return value specifies if the point is on the screen.
---@param x number
---@param y number
---@param z number
---@return number, number, boolean
function World2Screen(x, y, z)
if not preCalc then
CameraPrecalc()
end
local dx = x - eyeX
local dy = y - eyeY
local dz = z - eyeZ
local xPrime = scaleFactor*(-cosAttackCosRot*dx - cosAttackSinRot*dy - sinAttack*dz)
local xs = 0.4 + (cosRot*dy - sinRot*dx)/xPrime
local ys = 0.42625 - yCenterScreenShift + (sinAttackCosRot*dx + sinAttackSinRot*dy - cosAttack*dz)/xPrime
return xs, ys, xPrime < 0 and xs > -0.1333 and xs < 0.9333 and ys > 0 and ys < 0.6
end
---Takes the screen coordinates x and y and the player for which the calculation should be performed and returns the x, y, and z world coordinates. The fourth return value specifies whether or not the search for the world intersection point converged. This function may fail when a ray pointing from the camera to the world intersects at multiple points with the terrain.
---@param x number
---@param y number
---@return number, number, number, boolean
function Screen2World(x, y)
if not preCalc then
CameraPrecalc()
end
local a = (x - 0.4)*scaleFactor
local b = (0.42625 - yCenterScreenShift - y)*scaleFactor
--This is the unit vector pointing towards the mouse cursor in the camera's coordinate system.
local nx = 1/sqrt(1 + a*a + b*b)
local ny = sqrt(1 - (1 + b*b)*nx*nx)
local nz = sqrt(1 - nx*nx - ny*ny)
if a > 0 then
ny = -ny
end
if b < 0 then
nz = -nz
end
--Constructs the unit vector pointing from the camera eye position to the mouse cursor.
local nxPrime = cosAttackCosRot*nx - sinRot*ny + sinAttackCosRot*nz
local nyPrime = cosAttackSinRot*nx + cosRot*ny + sinAttackSinRot*nz
local nzPrime = -sinAttack*nx + cosAttack*nz
--Try to find intersection point of vector with terrain.
local zGuess = GetTerrainZ(eyeX, eyeY)
local xGuess = eyeX + nxPrime*(eyeZ - zGuess)/nzPrime
local yGuess = eyeY + nyPrime*(eyeZ - zGuess)/nzPrime
local zWorld = GetTerrainZ(xGuess, yGuess)
local deltaZ = zWorld - zGuess
zGuess = zWorld
local zWorldOld, deltaZOld
local i = 0
while (deltaZ > 1 or deltaZ < -1) and i < 50 do
zWorldOld = zWorld
deltaZOld = deltaZ
xGuess = eyeX + nxPrime*(eyeZ - zGuess)/nzPrime
yGuess = eyeY + nyPrime*(eyeZ - zGuess)/nzPrime
zWorld = GetTerrainZ(xGuess, yGuess)
deltaZ = zWorld - zGuess
zGuess = (deltaZOld*zWorld - deltaZ*zWorldOld)/(deltaZOld - deltaZ)
i = i + 1
end
return xGuess, yGuess, zWorld, i < 50
end
function InitWorld2Screen2()
moveableLoc = Location(0, 0)
GetTerrainZ = function(x, y)
MoveLocation(moveableLoc, x, y)
return GetLocationZ(moveableLoc)
end
TimerStart(CreateTimer(), TIME_STEP, true, function()
preCalc = false
end)
end
end
if Debug then Debug.endFile() end
if Debug and Debug.beginFile then Debug.beginFile("MDTable") end
--[[-----------------------------------------------------------------------------------------------------------------------------------------------
* ----------------------------------------
* | Multidimensional Table (X-Lite) v1.1 |
* ----------------------------------------
*
* Creator: Eikonium
* Contributors: Bribe
* --> https://www.hiveworkshop.com/threads/multidimensional-table.339498/
*
* - Multidimensional tables are tables that allow for direct access of multiple table dimensions without the need of manual subtable creation.
* - This X-Lite-version only offers the ability to create multi-dimensional tables, without any additional API perks of the standard MDTable or even MDTable Lite.
*
* table.createMD(numDimensions: integer) --> table
* Creates an MDTable with the specified number of dimensions. E.g. "T = table.createMD(3)" will allow you to read from and write to T[key1][key2][key3] for any arbitrary set of 3 keys.
* You can think of it like a tree with constant depth that only allows you to write into the "leaf level" (using exactly the number of keys as dimensions specified).
* - In the example with 3 dimensions, you should only write to T[key1][key2][key3], never to T[key1] or T[key1][key2].
* Reading is fine on every level, but you are probably not interested in the subtable stored in T[key1].
* You can however manually save further tables in T[key1][key2][key3] (at leaf level).
* - MDTables will automatically create subtables on demand, i.e. upon reading from or writing to a combination of keys.
-------------------------------------------------------------------------------------------------------------------------------------------------]]
do
local dimensionStorage = setmetatable({}, {__mode = 'k'})
local repeater = {__index = function(self, key)
local new = dimensionStorage[self] > 2 and table.createMD(dimensionStorage[self] - 1) or {}
rawset(self, key, new)
return new
end}
---Create a new Multidimensional Table.
---@param numDimensions integer
function table.createMD(numDimensions)
local newTable = {}
dimensionStorage[newTable] = numDimensions
return setmetatable(newTable, repeater)
end
end
if Debug and Debug.endFile then Debug.endFile() end
do
local SUN_ZENITH_ANGLE = 45 ---@type number
local SUN_AZIMUTH_ANGLE = 45 ---@type number
local MAXIMUM_ALPHA = 200 ---@type integer
local SHADOW_ATTENUATION = 0.15 ---@type number
local mt = {__mode = "k"}
local shadowOffsetX = setmetatable({}, mt) ---@type number[]
local shadowOffsetY = setmetatable({}, mt) ---@type number[]
local shadowAttenuation = setmetatable({}, mt) ---@type number[]
local currentWidth = setmetatable({}, mt) ---@type number[]
local currentHeight = setmetatable({}, mt) ---@type number[]
local zenithOffsetX = 1/math.tan(bj_DEGTORAD*SUN_ZENITH_ANGLE)*math.cos(bj_DEGTORAD*SUN_AZIMUTH_ANGLE)
local zenithOffsetY = 1/math.tan(bj_DEGTORAD*SUN_ZENITH_ANGLE)*math.sin(bj_DEGTORAD*SUN_AZIMUTH_ANGLE)
UNIT_SHADOW_PATH = "ReplaceableTextures\\Shadows\\Shadow.blp"
FLYER_SHADOW_PATH = "ReplaceableTextures\\Shadows\\ShadowFlyer.blp"
function AddShadowedEffect(x, y, shadowPath, shadowWidth, shadowHeight, xOffset, yOffset)
local img = CreateImage(shadowPath, shadowWidth, shadowHeight, 0, x + (xOffset or 0) - 0.5*shadowWidth, y + (yOffset or 0) - 0.5*shadowHeight, 0, 0, 0, 0, 1)
SetImageRenderAlways(img, true)
SetImageColor(img, 255, 255, 255, MAXIMUM_ALPHA)
SetImageAboveWater(img, false, true)
currentWidth[img] = shadowWidth
currentHeight[img] = shadowHeight
shadowOffsetX[img] = xOffset or 0
shadowOffsetY[img] = yOffset or 0
shadowAttenuation[img] = SHADOW_ATTENUATION/math.max(shadowHeight, shadowWidth)
return img
end
function DestroyShadowedEffect(img)
currentWidth[img] = nil
currentHeight[img] = nil
shadowOffsetX[img] = nil
shadowOffsetY[img] = nil
shadowAttenuation[img] = nil
DestroyImage(img)
end
function SetShadowedEffectPosition(img, x, y, z, alpha)
alpha = ((alpha or MAXIMUM_ALPHA)*(1 - (shadowAttenuation[img]*z))) // 1
if alpha < 0 then
alpha = 0
end
SetImageColor(img, 255, 255, 255, alpha)
x = x + shadowOffsetX[img] + zenithOffsetX*z
y = y + shadowOffsetY[img] + zenithOffsetY*z
SetImagePosition(img, x + shadowOffsetX[img] - 0.5*currentWidth[img], y + shadowOffsetY[img] - 0.5*currentHeight[img], 0)
end
end
do
HALF_PI = math.pi * 0.5
PI = math.pi
TAU = math.pi * 2.0
THIRD_TAU = TAU / 3
local mathabs = math.abs
local mathfloor = math.floor
local mathmin = math.min
local mathmax = math.max
local mathsqrt = math.sqrt
local mathatan = math.atan
local mathcos = math.cos
local mathsin = math.sin
function floorWithProbabilisticRemainder(x)
local floorValue = mathfloor(x)
if math.random() < x - floorValue then
return floorValue + 1
else
return floorValue
end
end
function lerp_angle(a, b, t)
local delta = (b - a) %% TAU
if delta > PI then
delta = delta - TAU
elseif delta < -PI then
delta = delta + TAU
end
return a + delta * t
end
function GetRandomLocationInRange(x,y,range,size,filter)
local half = size * 0.5
local r = 16
for i = 1, 100 do
r = math.min(r+16,range)
local x2,y2 = GetRandomReal(-r,r) + x,GetRandomReal(-r,r) + y
if not unit_collision_hash:cell_occupied(x2-half, y2-half, size, size,filter) then
return x2,y2
end
end
return x,y
end
function GetRandomLocationStrictRange(x,y,range,size,filter)
local half = size * 0.5
for i = 1, 100 do
local x2,y2 = GetRandomReal(-range,range) + x,GetRandomReal(-range,range) + y
if not unit_collision_hash:cell_occupied(x2-half, y2-half, size, size,filter) then
return x2,y2
end
end
return x,y
end
function GetRandomLocationNearby(x,y,size,filter)
local range = 16
local half = size * 0.5
for i = 1, 100 do
range = range + 16
local x2,y2 = GetRandomReal(-range,range) + x,GetRandomReal(-range,range) + y
if not unit_collision_hash:cell_occupied(x2-half, y2-half, size, size,filter) then
return x2,y2
end
end
return x,y
end
function ParabolicArc(a, b, t)
local parabolicT = 1 - (t*2 - 1)^2
return a + b * parabolicT
end
function lerp_figure8(t)
t = t%%1
local angle = t * 4 * PI + HALF_PI
if t < 0.5 then
return mathcos(angle) + 0.5, 1 - mathsin(angle), angle
else
return mathcos(angle) + 0.5, mathsin(angle) - 1, angle
end
end
function PitchBetween(x1,y1,z1,x2,y2,z2)
dx = x1-x2
dy = y1-y2
dz = z1-z2
return mathatan(mathsqrt(dy*dy + dx*dx), dz)
end
function asymptotic(val,add)
return val/(val+add)
end
function step(x, threshold)
if x < threshold then
return 0
else
return 1
end
end
function mathsign(x)
return x>0 and 1 or x<0 and -1 or 0
end
function mathsignOr(x)
return x>=0 and 1 or x<0 and -1
end
function ReduceDamage(damage,defense)
local dsign = defense>0 and 1 or defense<0 and -1 or 0
local dadj = 0.004 * defense
local dmin = (0.4 < dadj) and 0.4 or dadj
local dabs = (defense < 0) and -defense or defense
return (1.0-(dsign * (0.6 - 24/(dabs+40)) + dmin)) * damage
end
function BlockDamage(damage,blockStrength)
local div = blockStrength/damage
if GetRandomReal(0.0,1.0) <= mathmin(0.5 * div,0.5 * mathsqrt(div),1.0) then
return 0.0
else
return damage
end
end
function CreateBlockEffect(x,y)
local sfx = AddSpecialEffect("BlockEffect3.mdx",x-SFX_OFFSET+GetRandomReal(-16,16),y-SFX_OFFSET+GetRandomReal(-16,16))
BlzSetSpecialEffectScale(sfx,0.65)
DestroyEffect(sfx)
SOUND_BLOCK:random()(GetRandomReal(0.9,1.1),nil,x,y)
end
function calculateSquareIntersection(square1, square2)
local halfSize1 = square1.half_size
local halfSize2 = square2.half_size
local intersectionLeft = mathmax(square1.position[1] - halfSize1, square2.position[1] - halfSize2)
local intersectionRight = mathmin(square1.position[1] + halfSize1, square2.position[1] + halfSize2)
local intersectionTop = mathmin(square1.position[2] + halfSize1, square2.position[2] + halfSize2)
local intersectionBottom = mathmax(square1.position[2] - halfSize1, square2.position[2] - halfSize2)
return (intersectionLeft + intersectionRight) * 0.5,
(intersectionTop + intersectionBottom) * 0.5
end
function angleBetween(x1,y1,x2,y2)
return mathatan(y2 - y1, x2 - x1)
end
function crossProduct(ax, ay, bx, by)
return ax * by - ay * bx
end
function isPointInTrapezium(px, py, ax, ay, bx, by, cx, cy, dx, dy)
local apx, apy = ax - px, ay - py
local bpx, bpy = bx - px, by - py
local cpx, cpy = cx - px, cy - py
local dpx, dpy = dx - px, dy - py
local ab = apx * bpy - apy * bpx
local bc = bpx * cpy - bpy * cpx
local cd = cpx * dpy - cpy * dpx
local da = dpx * apy - dpy * apx
return (ab >= 0 and bc >= 0 and cd >= 0 and da >= 0) or (ab <= 0 and bc <= 0 and cd <= 0 and da <= 0)
end
function lerp(v0, v1, t)
return v0 + t * (v1 - v0)
end
function point_in_range_of_line(x1, y1, x2, y2, x3, y3,range)
local px = x2-x1
local py = y2-y1
local u = ((x3 - x1) * px + (y3 - y1) * py) / (px*px + py*py)
if u > 1 then
u = 1
elseif u < 0 then
u = 0
end
local dx = (x1 + u * px) - x3
local dy = (y1 + u * py) - y3
return dx*dx + dy*dy <= range * range
end
function angle_difference(angle1, angle2)
return (angle2 - angle1 + PI) %% TAU - PI
end
function wrap(value, min, max)
local range = max - min + 1
return min + (value - min) %% range
end
function reverseWrap(input, min, max)
local zigzagPosition = (input - min) %% ((max - min) * 2)
return zigzagPosition < (max - min) and min + zigzagPosition or max - (zigzagPosition - (max - min))
end
function clamp(value, min, max)
return (value < min) and min or ((value > max) and max or value)
end
function wrappedAngleDifference(theta1, theta2)
local deltaThetaAdjusted = (theta2 - theta1 + PI) %% TAU - PI
if deltaThetaAdjusted == -PI then
return PI
else
return deltaThetaAdjusted
end
end
function radDotProduct(theta1, theta2)
return mathcos(theta1) * mathcos(theta2) + mathsin(theta1) * mathsin(theta2)
end
function change_angle_bounded(from, to, amount)
local diff = angle_difference(from,to)
if diff > 0.0 then
diff = mathmin(diff,amount)
else
diff = mathmax(diff,-amount)
end
return from + diff, diff
end
function isPointInsideCone(coneX, coneY, coneDirection, coneWidth, targetX, targetY)
local angleToTarget = mathatan(targetY - coneY, targetX - coneX)
local angleDifference = mathabs(angle_difference(angleToTarget,coneDirection))
return angleDifference <= coneWidth * 0.5
end
function VectorNormalize(x, y)
local magnitude = mathsqrt(x * x + y * y)
if magnitude ~= 0 then
return x / magnitude, y / magnitude
else
return 0, 0
end
end
function VectorAngle(x1, y1, x2, y2)
local dx = x1 - x2
local dy = y1 - y2
local r = mathsqrt(dx * dx + dy * dy)
if r == 0.0 then
return 0,0
end
return dx/r, dy/r
end
function PushCalc(x1, y1, x2, y2, radius1, radius2)
local dx = x1-x2
local dy = y1-y2
local distance = mathsqrt(dx * dx + dy * dy)
local overlap = radius1 + radius2 - distance
if overlap > 0 then
local push_distance = overlap / distance
return dx * push_distance, dy * push_distance
end
return 0,0
end
function VectorAngleSquare(center_x, center_y, point_x, point_y)
local dx = point_x - center_x
local dy = point_y - center_y
if mathabs(dx) >= mathabs(dy) then
if dx > 0 then
return 1,0
else
return -1,0
end
else
if dy > 0 then
return 0,1
else
return 0, -1
end
end
end
function intersect_with_bounds(min_x, max_x, min_y, max_y, angle)
local cx, cy = (min_x + max_x) / 2, (min_y + max_y) / 2
local dx, dy = mathcos(angle), mathsin(angle)
local x, y
if mathabs(dx) > mathabs(dy) then
x = dx > 0 and max_x or min_x
y = cy + (x - cx) * dy / dx
else
y = dy > 0 and max_y or min_y
x = cx + (y - cy) * dx / dy
end
if x < min_x or x > max_x or y < min_y or y > max_y then
local adjacent_x, adjacent_y
if x < min_x then
adjacent_x, adjacent_y = min_x, cy + (min_x - cx) * dy / dx
elseif x > max_x then
adjacent_x, adjacent_y = max_x, cy + (max_x - cx) * dy / dx
elseif y < min_y then
adjacent_x, adjacent_y = cx + (min_y - cy) * dx / dy, min_y
else
adjacent_x, adjacent_y = cx + (max_y - cy) * dx / dy, max_y
end
x, y = adjacent_x, adjacent_y
end
return x, y
end
function IsInRange(x1,y1,x2,y2,range)
local dx = x1 - x2
local dy = y1 - y2
return (dx * dx + dy * dy) < range * range
end
function DistanceTo(x1,y1,x2,y2)
local dx = x1 - x2
local dy = y1 - y2
return mathsqrt(dx * dx + dy * dy)
end
function DistanceSquaredTo(x1,y1,x2,y2)
local dx = x1 - x2
local dy = y1 - y2
return dx * dx + dy * dy
end
function easeInOutSine(x)
return -(mathcos(PI * x) - 1) / 2;
end
function easeInSine(x)
return 1 - mathcos((x * PI) / 2);
end
function easeOutSine(x)
return mathsin((x*PI)/2)
end
function easeInCubic(x)
return x * x * x
end
function easeInX(x,y)
return x^y
end
function easeInSextic(x)
local x2 = x * x
return x2 * x2 * x2
end
function easeOutCubic(x)
local x = 1 - x
return 1 - x * x * x
end
function easeOutX(x,y)
return 1 - (1 - x) ^ y
end
function easeOutSextic(x)
local x2 = 1 - x
x2 = x2 * x2
return 1- x2 * x2 * x2
end
function EaseBias(x,bias)
local k = (1 - bias) ^ 3
return (x * k) / (x * k - x + 1)
end
local c1 = 1.70158
local c3 = c1 + 1
function easeInBack(x)
local x2 = x * x
return c3 * x2 * x - c1 * x2
end
function easeOutBack(x)
x = x-1
local x2 = x * x
return 1 + c3 * x2 * x + c1 * x2;
end
local c5 = TAU / 4.5
function easeInOutElastic(x)
if x == 0 then
return 0
elseif x == 1 then
return 1
elseif x < 0.5 then
return -(2^(20 * x - 10) * mathsin((20 * x - 11.125) * c5)) / 2
else
return (2^(-20 * x + 10) * mathsin((20 * x - 11.125) * c5)) / 2 + 1
end
end
function DisableClicks()
CreateLeaderboardBJ(bj_FORCE_ALL_PLAYERS, "title")
BlzFrameSetSize(BlzGetFrameByName("Leaderboard", 0), 0, 0)
BlzFrameSetVisible(BlzGetFrameByName("LeaderboardTitle", 0), false)
BlzFrameSetVisible(BlzGetFrameByName("LeaderboardBackdrop", 0), false)
local frame = BlzGetFrameByName("LeaderboardBackdrop", 0)
BlzFrameSetVisible(frame, true)
BlzFrameClearAllPoints(frame)
BlzFrameSetSize(frame, 3.6, 0.6)
BlzFrameSetPoint(frame,FRAMEPOINT_TOP,BlzGetOriginFrame(ORIGIN_FRAME_GAME_UI,0),FRAMEPOINT_TOP,0,0)
BlzFrameSetLevel(frame,-10)
BlzFrameSetAlpha(frame,0)
BlzFrameSetEnable(frame,false)
end
end
do
SYMBOL_FROST = "NumberFrost.mdx"
SYMBOL_BURN = "NumberBurn.mdx"
SYMBOL_ELECTRIFY = "NumberElectrify.mdx"
SYMBOL_CRIT = "NumberCrit.mdx"
local numbers = {}
for i = 0, 9, 1 do
numbers[tostring(i)] = "Number"..tostring(i)..".mdx"
end
local scale = 0.2
local base_size = 112
local actual_size = scale * base_size
local speed = 2.0
--[[function CreateFloatingText(txt,r,g,b,x,y)
local t = CreateTextTag()
SetTextTagText(t,txt,0.015)
SetTextTagPermanent(t,false)
SetTextTagLifespan(t,1.0)
SetTextTagFadepoint(t,0.85)
SetTextTagColor(t,r,g,b,100)
SetTextTagVelocity(t,0,0.1)
SetTextTagPos(t,x-string.len(txt) * 14,y,0)
end
]]--
local function destroyText(sfx)
DestroyEffect(sfx)
end
local size_cache = {}
local len_cache = {}
for i = 1, 100, 1 do
size_cache[i] = actual_size*i
len_cache[i] = size_cache[i] * 0.5
end
local stringlen = string.len
local stringsub = string.sub
function CreateFloatingText(txt,r,g,b,x,y,symbol)
local str = tostring(txt)
local len = stringlen(str)
local sfx
x = x - len_cache[len] + GetRandomReal(-32,32)
y = y + GetRandomReal(-32,32)
if symbol then
sfx = AddSpecialEffect(symbol,x,y)
BlzSetSpecialEffectScale(sfx,scale)
BlzSetSpecialEffectTimeScale(sfx,speed)
BlzSetSpecialEffectColor(sfx,r,g,b)
DestroyEffect(sfx)
end
for i = 1, len, 1 do
local path = numbers[stringsub(str,i,i)]
if path == nil then return end
sfx = AddSpecialEffect(path,x+size_cache[i],y)
BlzSetSpecialEffectScale(sfx,scale)
BlzSetSpecialEffectTimeScale(sfx,speed)
BlzSetSpecialEffectColor(sfx,r,g,b)
DestroyEffect(sfx)
end
end
end
do
shash = {}
shash.__index = shash
local mathfloor = math.floor
function shash.new(cellsize)
local self = setmetatable(NewTable(), shash)
cellsize = cellsize or 64
self.cellsize = cellsize
self.cells = {}
self.entities = {}
self.tablepool = {}
return self
end
local function coord_to_key(x, y)
return x + y * 1e7
end
function shash:add(obj, x, y, w, h)
-- Create entity. The table is used as an array as this offers a noticable
-- performance increase on LuaJIT; the indices are as follows:
-- [1] = left, [2] = top, [3] = right, [4] = bottom, [5] = object
local e = NewTable()
e[1] = x
e[2] = y
e[3] = x + w
e[4] = y + h
e[5] = obj
-- Add to main entities table
self.entities[obj] = e
-- Add to cells
local cellsize = self.cellsize
local ax1, ay1 = e[1] // cellsize, e[2] // cellsize
local ax2, ay2 = e[3] // cellsize, e[4] // cellsize
for y1 = ay1, ay2 do
for x1 = ax1, ax2 do
local idx = x1 + y1 * 1e7
local tbl = self.cells[idx]
if not tbl then
local t = NewTable()
t[1] = e
self.cells[idx] = t
else
tbl[#tbl+1] = e
end
end
end
end
function shash:remove(obj)
-- Get entity of obj
local e = self.entities[obj]
-- Remove from main entities table
self.entities[obj] = nil
-- Remove from cells
local cellsize = self.cellsize
local ax1, ay1 = e[1] // cellsize, e[2] // cellsize
local ax2, ay2 = e[3] // cellsize, e[4] // cellsize
for y1 = ay1, ay2 do
for x1 = ax1, ax2 do
local idx = x1 + y1 * 1e7
local t = self.cells[idx]
local n = #t
-- Only one entity? Remove entity from cell and remove cell
if n == 1 then
ReleaseTable(self.cells[idx])
self.cells[idx] = nil
goto continue
end
-- Find and swap-remove entity
for i, v in ipairs(t) do
if v == e then
t[i] = t[n]
t[n] = nil
goto continue
end
end
::continue::
end
end
ReleaseTable(e)
end
function shash:update(obj, x, y, w, h)
-- Get entity from obj
local e = self.entities[obj]
-- No width/height specified? Get width/height from existing bounding box
w = w or e[3] - e[1]
h = h or e[4] - e[2]
-- Check the entity has actually changed cell-position, if it hasn't we don't
-- need to touch the cells at all
local cellsize = self.cellsize
local ax1, ay1 = e[1] // cellsize, e[2] // cellsize
local ax2, ay2 = e[3] // cellsize, e[4] // cellsize
local bx1, by1 = x // cellsize, y // cellsize
local bx2, by2 = (x+w) // cellsize, (y+h) // cellsize
-- Update entity
e[1], e[2], e[3], e[4] = x, y, x + w, y + h
if ax1 ~= bx1 or ay1 ~= by1 or ax2 ~= bx2 or ay2 ~= by2 then
-- Remove from old cells
for y1 = ay1, ay2 do
for x1 = ax1, ax2 do
local idx = x1 + y1 * 1e7
local t = self.cells[idx]
local n = #t
-- Only one entity? Remove entity from cell and remove cell
if n == 1 then
ReleaseTable(self.cells[idx])
self.cells[idx] = nil
goto continue
end
-- Find and swap-remove entity
for i, v in ipairs(t) do
if v == e then
t[i] = t[n]
t[n] = nil
goto continue
end
end
::continue::
end
end
-- Add to new cells
ax1, ay1 = e[1] // cellsize, e[2] // cellsize
ax2, ay2 = e[3] // cellsize, e[4] // cellsize
for y1 = ay1, ay2 do
for x1 = ax1, ax2 do
local idx = x1 + y1 * 1e7
local tbl = self.cells[idx]
if not tbl then
local t = NewTable()
t[1] = e
self.cells[idx] = t
else
tbl[#tbl+1] = e
end
end
end
end
end
shash.maxEnum = math.huge
local function each_overlapping_entity(self, e, fn, ...)
local tbl = self.tablepool
local s = #tbl
local set = tbl[s] or {}
tbl[s] = nil
s = s + 1
local keyset = tbl[s] or {}
local keypos = 0
tbl[s] = nil
local cellsize = self.cellsize
local d1 = e[1]
local d2 = e[2]
local d3 = e[3]
local d4 = e[4]
local sx, sy = d1 // cellsize, d2 // cellsize
local ex, ey = d3 // cellsize, d4 // cellsize
for y = sy, ey do
for x = sx, ex do
local idx = x + y * 1e7
local t = self.cells[idx]
if t then
for i, v in ipairs(t) do
if e ~= v and not set[v] and d3 > v[1] and d1 < v[3] and d4 > v[2] and d2 < v[4] then
fn(v[5], ...)
set[v] = true
keypos = keypos + 1
keyset[keypos] = v
shash.maxEnum = shash.maxEnum - 1
if shash.maxEnum == 0 then goto continue end
end
end
end
end
end
::continue::
s = #tbl
for i = 1, keypos do
local v = keyset[i]
set[v] = nil
keyset[i] = nil
end
tbl[s+1] = set
tbl[s+2] = keyset
end
local temp = {0,0,0,0}
--// fn can be a table or a function, a table will have results appended to it
function shash:each(x, y, w, h, fn, ...)
local e = self.entities[x]
if e then
each_overlapping_entity(self, e, y, w, h, fn, ...)
else
temp[1] = x
temp[2] = y
temp[3] = x + w
temp[4] = y + h
each_overlapping_entity(self, temp, fn, ...)
end
end
function shash:get_of_type(x1,y1,w,h,whichType)
local results = NewTable()
local j = 0
local tbl = self.tablepool
local s = #tbl
local set = tbl[s] or {}
tbl[s] = nil
s = s + 1
local keyset = tbl[s] or {}
tbl[s] = nil
local cellsize = self.cellsize
local d3 = x1 + w
local d4 = y1 + h
local sx, sy = x1 // cellsize, y1 // cellsize
local ex, ey = d3 // cellsize, d4 // cellsize
for y = sy, ey do
for x = sx, ex do
local idx = x + y * 1e7
local t = self.cells[idx]
if t then
for i, v in ipairs(t) do
local e = v[5]
if not set[v] and d3 > v[1] and x1 < v[3] and d4 > v[2] and y1 < v[4] and Types.is(e,whichType) then
set[v] = true
j = j + 1
keyset[j] = v
results[j] = e
shash.maxEnum = shash.maxEnum - 1
if shash.maxEnum == 0 then goto continue end
end
end
end
end
end
::continue::
s = #tbl
for i = 1, j do
local v = keyset[i]
set[v] = nil
keyset[i] = nil
end
tbl[s+1] = set
tbl[s+2] = keyset
return results
end
function shash:count_of_type(x1,y1,w,h,whichType)
local j = 0
local tbl = self.tablepool
local s = #tbl
local set = tbl[s] or {}
tbl[s] = nil
s = s + 1
local keyset = tbl[s] or {}
tbl[s] = nil
local cellsize = self.cellsize
local d3 = x1 + w
local d4 = y1 + h
local sx, sy = x1 // cellsize, y1 // cellsize
local ex, ey = d3 // cellsize, d4 // cellsize
local cells = self.cells
for y = sy, ey do
for x = sx, ex do
local idx = x + y * 1e7
local t = cells[idx]
local size = t and #t or 0
for i = 1, size do
local v = t[i]
if not set[v] and d3 > v[1] and x1 < v[3] and d4 > v[2] and y1 < v[4] and Types.is(v[5],whichType) then
set[v] = true
j = j + 1
keyset[j] = v
shash.maxEnum = shash.maxEnum - 1
if shash.maxEnum == 0 then goto continue end
end
end
end
end
::continue::
s = #tbl
for i = 1, j do
local v = keyset[i]
set[v] = nil
keyset[i] = nil
end
tbl[s+1] = set
tbl[s+2] = keyset
return j
end
function shash:cell_occupied(x1, y1, w, h,whichType)
local d3 = x1 + w
local d4 = y1 + h
local cellsize = self.cellsize
local sx, sy = x1 // cellsize, y1 // cellsize
local ex, ey = d3 // cellsize, d4 // cellsize
local cells = self.cells
for y = sy, ey do
for x = sx, ex do
local t = cells[x + y * 1e7]
local size = t and #t or 0
for i = 1, size do
local v = t[i]
if d3 > v[1] and x1 < v[3] and d4 > v[2] and y1 < v[4] and Types.is(v[5],whichType) then
return true
end
end
end
end
return false
end
function shash:point_inside_object(obj,x,y)
local e = self.entities[obj]
return e[3] > x and e[1] < x and e[4] > y and e[2] < y
end
function shash:get_first(x1, y1, w, h,whichType)
local j = 0
local tbl = self.tablepool
local s = #tbl
local set = tbl[s] or {}
tbl[s] = nil
s = s + 1
local keyset = tbl[s] or {}
tbl[s] = nil
local d3 = x1 + w
local d4 = y1 + h
local cellsize = self.cellsize
local sx, sy = x1 // cellsize, y1 // cellsize
local ex, ey = d3 // cellsize, d4 // cellsize
local cells = self.cells
local next = NewTable()
local cur = NewTable()
local x,y = x1+w*0.5, y1+h*0.5
next[1] = Vector(MathRound((x)/cellsize), MathRound((y)/cellsize))
local which
local curDist = math.huge
local iter_since_found = 0
while #next > 0 do
cur,next = next,cur
ClearVectorTable(next)
if which then iter_since_found = iter_since_found + 1 end
while #cur > 0 do
local pos = cur[#cur]
cur[#cur] = nil
local idx = pos.x + pos.y * 1e7
local t = cells[idx]
local size = t and #t or 0
set[idx] = true
for i = 1, size do
local v = t[i]
local target = v[5]
if d3 > v[1] and x1 < v[3] and d4 > v[2] and y1 < v[4] and Types.is(target,whichType) then
local dist = DistanceSquaredTo(x,y,target.position[1],target.position[2])
if dist < curDist then
curDist = dist
which = target
end
if iter_since_found == 2 then
goto continue
end
end
end
for l = 1, 4 do
local x3 = Vector.neighbors[l].x + pos.x
local y3 = Vector.neighbors[l].y + pos.y
local idx2 = x3 + y3 * 1e7
if not set[idx2] and x3 >= sx and x3 <= ex and y3 >= sy and y3 <= ey then
j = j + 1
keyset[j] = idx2
set[idx2] = true
next[#next+1] = Vector(x3,y3)
end
end
end
end
::continue::
ReleaseVectorTable(next)
ReleaseVectorTable(cur)
s = #tbl
for i = 1, j do
local v = keyset[i]
set[v] = nil
keyset[i] = nil
end
tbl[s+1] = set
tbl[s+2] = keyset
return which
end
function shash:info(opt, ...)
if opt == "cells" or opt == "entities" then
local n = 0
for k in pairs(self[opt]) do
n = n + 1
end
return n
end
if opt == "cell" then
local t = self.cells[ coord_to_key(...) ]
return t and #t or 0
end
end
end
do
FlowField = {}
FlowField.__index = FlowField
local mathfloor = math.floor
local atan = math.atan
local function coord_to_key(x, y)
return mathfloor(x + y * 128 + 1e6)
end
function FlowField.new(maxX,minX,maxY,minY,cell_size,shash)
local self = setmetatable(NewTable(),FlowField)
self.maxX = maxX
self.minX = minX
self.maxY = maxY
self.minY = minY
self.shash = shash
self.cell_size = cell_size
self.neighbors = NewTable()
self.neighbors[1] = NewTable()
self.neighbors[1][1] = cell_size
self.neighbors[1][2] = 0
self.neighbors[2] = NewTable()
self.neighbors[2][1] = 0
self.neighbors[2][2] = cell_size
self.neighbors[3] = NewTable()
self.neighbors[3][1] = -cell_size
self.neighbors[3][2] = 0
self.neighbors[4] = NewTable()
self.neighbors[4][1] = 0
self.neighbors[4][2] = -cell_size
self.neighbors[5] = NewTable()
self.neighbors[5][1] = cell_size
self.neighbors[5][2] = cell_size
self.neighbors[6] = NewTable()
self.neighbors[6][1] = -cell_size
self.neighbors[6][2] = cell_size
self.neighbors[7] = NewTable()
self.neighbors[7][1] = cell_size
self.neighbors[7][2] = -cell_size
self.neighbors[8] = NewTable()
self.neighbors[8][1] = -cell_size
self.neighbors[8][2] = -cell_size
self.vectorDir = NewTable()
self.originX = 0
self.originY = 0
self.open_cells = 0
self.mixedDir = NewTable()
self.indextbl = NewTable()
return self
end
function FlowField:destroy()
setmetatable(self,nil)
self.maxX = nil
self.minX = nil
self.maxY = nil
self.minY = nil
self.shash = nil
self.cell_size = nil
for i, v in ipairs(self.neighbors) do
ReleaseTable(v)
end
ReleaseTable(self.neighbors)
self.neighbors = nil
for i = 1, #self.indextbl do
local v = self.indextbl[i]
self.indextbl[i] = nil
self.vectorDir[v] = nil
self.mixedDir[v] = nil
end
ReleaseTable(self.vectorDir)
ReleaseTable(self.mixedDir)
self.vectorDir = nil
self.mixedDir = nil
self.originX = nil
self.originY = nil
self.open_cells = nil
ReleaseTable(self.indextbl)
self.indextbl = nil
ReleaseTable(self)
end
function FlowField:get_dir(x, y)
local cellsize = self.cell_size
local x2,y2 = MathRound(x / cellsize)*cellsize, MathRound(y / cellsize)*cellsize
local ind = coord_to_key(x2, y2)
local dir = atan(self.originY-y,self.originX-x)
if self.vectorDir[ind] ~= nil then
if not self.mixedDir[ind] then
local cos = math.cos(self.vectorDir[ind])
local sin = math.sin(self.vectorDir[ind])
for j, n in ipairs(self.neighbors) do
local ind2 = coord_to_key(x2 + n[1],y2 + n[2])
local v = (self.vectorDir[ind2] or self.vectorDir[ind])
cos = cos + math.cos(v)
sin = sin + math.sin(v)
end
self.mixedDir[ind] = atan(sin/9,cos/9)
end
return self.mixedDir[ind], dir
else
return dir+PI, dir+PI
end
end
function FlowField:cell_valid(x,y)
local cellsize = self.cell_size
local x2,y2 = MathRound(x / cellsize)*cellsize, MathRound(y / cellsize)*cellsize
local ind = coord_to_key(x2, y2)
return self.vectorDir[ind] ~= nil
end
function FlowField:generate(x1,y1)
local cellsize = self.cell_size
self.open_cells = 1
x1 = MathRound(x1/ cellsize) * cellsize
y1 = MathRound(y1/ cellsize) * cellsize
self.originX = x1
self.originY = y1
local double = cellsize * 2
local maxX = x1 + self.maxX
local minX = x1 + self.minX
local maxY = y1 + self.maxY
local minY = y1 + self.minY
local nextX = NewTable()
local nextY = NewTable()
local curX = NewTable()
local curY = NewTable()
local neighbors = self.neighbors
local dirV = self.vectorDir
local mixed = self.mixedDir
local shash = self.shash
local indextbl = self.indextbl
for i = 1, #indextbl do
local v = indextbl[i]
indextbl[i] = nil
dirV[v] = nil
mixed[v] = nil
end
local index = 0
nextX[1] = x1
nextY[1] = y1
while #nextX > 0 do
for i = 1, #curX, 1 do
curX[i] = nil
curY[i] = nil
end
local n1 = curX
local n2 = curY
curX = nextX
curY = nextY
nextX = n1
nextY = n2
for i = 1, #curX, 1 do
local x = curX[i]
local y = curY[i]
for j, n in ipairs(neighbors) do
local dx = x + n[1]
local dy = y + n[2]
local ind = coord_to_key(dx,dy)
if not dirV[ind] and dx < maxX and dx > minX and dy < maxY and dy > minY then
if not shash:cell_occupied(dx- cellsize, dy - cellsize, double, double,Types.Collidable) then
table.insert(nextX,dx)
table.insert(nextY,dy)
self.open_cells = self.open_cells + 1
end
index = index + 1
indextbl[index] = ind
dirV[ind] = atan(y-dy,x-dx)
--local sfx = AddSpecialEffect("units\\human\\Peasant\\Peasant",dx,dy)
--BlzSetSpecialEffectYaw(sfx,dirV[ind])
--DestroyEffect(sfx)
end
end
end
end
ReleaseTable(nextX)
ReleaseTable(nextY)
ReleaseTable(curX)
ReleaseTable(curY)
end
end
do
local freeTable = {}
local tblTracker = {}
function NewTable()
local tbl = freeTable[#freeTable] or {}
tblTracker[tbl] = nil
freeTable[#freeTable] = nil
return tbl
end
function table.pop_back(tbl)
return table.remove(tbl, #tbl)
end
function table.removeobject(tbl, object)
for i = #tbl, 1, -1 do
if tbl[i] == object then
tbl[i] = tbl[#tbl]
tbl[#tbl] = nil
end
end
end
function ReleaseTable(tbl)
if not tbl or tblTracker[tbl] then return end
tblTracker[tbl] = true
for i = 1, #tbl, 1 do
tbl[i] = nil
end
for k in pairs (tbl) do
print("Error: bad table release",k)
rawset(tbl, k, nil)
end
freeTable[#freeTable+1] = tbl
end
function ClearTable(tbl)
if not tbl then return end
for i = 1, #tbl, 1 do
tbl[i] = nil
end
for k in pairs (tbl) do
print("Error: bad table clear",k)
rawset(tbl, k, nil)
end
end
function ReleaseTableFast(tbl)
freeTable[#freeTable+1] = tbl
end
function ReleaseKeyTable(tbl)
if not tbl then return end
local list = tbl._list
setmetatable(tbl,nil)
for i = 1, #list, 1 do
local key = list[i]
tbl[key] = nil
list[i] = nil
end
tbl._list = nil
ReleaseTable(list)
--freeTable[#freeTable+1] = list
ReleaseTable(tbl)
--freeTable[#freeTable+1] = tbl
end
function ClearKeyTable(tbl)
local list = tbl._list
for i = 1, #list, 1 do
local key = list[i]
tbl[key] = nil
list[i] = nil
end
end
metaKeyTable = {
__newindex = function (t,k,v)
rawset(t,k,v)
t._list[#t._list+1] = k
end
}
function NewKeyTable(o)
o = o or NewTable()
o._list = NewTable()
return setmetatable(o,metaKeyTable)
end
function CopyKeyTable(base)
local new = NewTable()
new._list = NewTable()
local newList = new._list
for i, key in ipairs(base._list) do
newList[i] = key
new[key] = base[key]
end
setmetatable(new,metaKeyTable)
end
function varagToTable(...)
local tbl = NewTable()
for i = select("#",...), 1, -1 do
tbl[i] = select(i,...)
end
return tbl
end
end
if Debug and Debug.beginFile then Debug.beginFile("Set/Group") end
do
local DEBUG_MODE = true
local CUSTOM_DEFEND_ABICODE = nil
Set = {
data = {} ---@type table data structure saving the actual elements
, orderedKeys = {} ---@type any[] to keep iteration order synchronized in multiplayer
, n = 0 ---@type number number of elements in the Set
}
Set.__index = Set
Set.__tostring = function(x)
setmetatable(x,nil) --detach metatable from object to allow using normal tostring (below) without recursive call of Set.__tostring
local result = tostring(x)
result = 'Set' .. string.sub(result, string.find(result, ':', nil, true), -1)
setmetatable(x,Set)
return result
end
local throwError = function(message) print("|cffff5555Error: " .. message .. "|r") end
local checkColonNotation = function(methodName, pseudoSelf)
if getmetatable(pseudoSelf) ~= Set then
throwError("Method " .. methodName .. " used with .-notation instead of :-notation.")
end
end
local checkDotNotation = function(methodName, firstArgumentOfMethod)
if firstArgumentOfMethod == Set then
throwError("Method " .. methodName .. " used with :-notation instead of .-notation.")
end
end
local wc3Type = function (input)
local typeString = type(input)
if typeString == 'userdata' then
typeString = tostring(input) --toString returns the warcraft type plus a colon and some hashstuff.
return string.sub(typeString, 1, (string.find(typeString, ":", nil, true) or 0) -1) --string.find returns nil, if the argument is not found, which would break string.sub. So we need or as coalesce.
else
return typeString
end
end
function Set.create(...)
if DEBUG_MODE then checkDotNotation("Set.create(...)", ...) end
local new = NewTable()
new.data = NewTable() --place to save the actual elements of the set. Elements can't be saved in self, because they might conflict with function names of the class (adding the element "add" would prevent future access to the add-method).
new.orderedKeys = NewTable()
setmetatable(new, Set)
new:add(...)
return new
end
function Set:destroy()
for element in self:elements() do
self:removeSingle(element)
end
setmetatable(self,nil)
setmetatable(self.data,nil)
setmetatable(self.orderedKeys,nil)
ReleaseTable(self.data)
ReleaseTable(self.orderedKeys)
self.data = nil
self.orderedKeys = nil
self.n = nil
ReleaseTable(self)
end
function Set.isSet(anything)
if DEBUG_MODE then checkDotNotation("Set.isSet(anything)", anything) end
return getmetatable(anything) == Set
end
function Set:addSingle(element)
if DEBUG_MODE then checkColonNotation("Set:addSingle(element)", self) end
if element ~=nil and not self.data[element] then
self.n = self.n + 1
self.data[element] = self.n
self.orderedKeys[self.n] = element
end
return self
end
function Set:add(...)
if DEBUG_MODE then checkColonNotation("Set:add(...)", self) end
for i = 1, select('#', ...) do
self:addSingle(select(i, ...))
end
return self
end
function Set:removeSingle(element)
if DEBUG_MODE then checkColonNotation("Set:removeSingle(element)", self) end
if self.data[element] then
local i,n = self.data[element], self.n
self.data[self.orderedKeys[n]] = i --last element takes iteration slot of removed element
self.orderedKeys[i] = self.orderedKeys[n]
self.orderedKeys[n] = nil
self.data[element] = nil
self.n = self.n - 1
end
return self
end
function Set:remove(...)
if DEBUG_MODE then checkColonNotation("Set:remove(...)", self) end
for i = 1, select('#', ...) do
self:removeSingle(select(i, ...))
end
return self
end
function Set:contains(element)
if DEBUG_MODE then checkColonNotation("Set:contains(element)", self) end
return self.data[element] ~= nil
end
function Set:element_index(element)
return self.data[element]
end
function Set:get_element_by_index(index)
return self.orderedKeys[index]
end
function Set:retainAll(container)
if DEBUG_MODE then checkColonNotation("Set:retainAll(container)", self) end
local typeString = wc3Type(container)
local containerAsSet = Set.create()
if typeString == 'group' then --Case 1: container is group
ForGroup(container, function () containerAsSet:addSingle(GetEnumUnit()) end)
elseif typeString == 'force' then --Case 2: container is force
ForForce(container, function () containerAsSet:addSingle(GetEnumPlayer()) end)
elseif (getmetatable(container) == getmetatable(self)) then --Case 3: container is Set
containerAsSet = container
elseif SyncedTable and SyncedTable.isSyncedTable(container) then --Case 4: container is SyncedTable
for _, element in pairs(container) do --pairs-function is multiplayer synced for SyncedTables.
containerAsSet:addSingle(element)
end
elseif(typeString == 'table') then --Case 5: container is a Table. We then assume, it's an array.
for _, element in ipairs(container) do
containerAsSet:addSingle(element)
end
else --Case 6: invalid input.
throwError("retainAll is only compatible with a Set, SyncedTable, array, force or group")
---@diagnostic disable-next-line: missing-return-value
return
end
-- do intersection
for element in self:elements() do
if not containerAsSet:contains(element) then self:removeSingle(element) end
end
return self
end
function Set:removeAll(container)
if DEBUG_MODE then checkColonNotation("Set:removeAll(container)", self) end
local typeString = wc3Type(container)
if typeString == 'group' then --Case 1: container is a group
ForGroup(container, function () self:removeSingle(GetEnumUnit()) end)
elseif typeString == 'force' then --Case 2: container is a force
ForForce(container, function () self:removeSingle(GetEnumPlayer()) end)
elseif (getmetatable(container) == getmetatable(self)) then --Case 3: container is a Set
for element in container:elements() do
self:removeSingle(element)
end
elseif SyncedTable and SyncedTable.isSyncedTable(container) then --Case 4: container is SyncedTable
for _, element in pairs(container) do --pairs-function is multiplayer synced for SyncedTables.
self:removeSingle(element)
end
elseif(type(container) == 'table') then --Case 5: container is a table. We then assume, it's a sequence.
for _, element in ipairs(container) do
self:removeSingle(element)
end
else --Case 6: invalid input.
throwError("removeAll is only compatible with a Set, SyncedTable, array, force or group")
end
return self
end
function Set:addAll(container)
if DEBUG_MODE then checkColonNotation("Set:addAll(container)", self) end
local typeString = wc3Type(container)
if typeString == 'group' then --Case 1: container is Group
ForGroup(container, function () self:addSingle(GetEnumUnit()) end)
elseif typeString == 'force' then --Case 2: container is Force
ForForce(container, function () self:addSingle(GetEnumPlayer()) end)
elseif (getmetatable(container) == getmetatable(self)) then --Case 3: container is Set
for element in container:elements() do
self:addSingle(element)
end
elseif SyncedTable and SyncedTable.isSyncedTable(container) then --Case 4: container is SyncedTable
for _, element in pairs(container) do --pairs-function is multiplayer synced for SyncedTables.
self:addSingle(element)
end
elseif typeString == 'table' then --Case 5: container is table (and we then assume it's an array)
for _, element in ipairs(container) do
self:addSingle(element)
end
else --Case 6: invalid input.
throwError("addAll is only compatible with a Set, SyncedTable, array, force or group")
end
return self
end
function Set:addAllKeys(whichTable)
if DEBUG_MODE then checkColonNotation("Set:addAllKeys(container)", self) end
if(type(whichTable) == 'table') then
for key, _ in pairs(whichTable) do --pairs-function is multiplayer synced for SyncedTables.
self:add(key)
end
else
throwError("AddAllKeys only compatible with SyncedTables")
end
return self
end
--[[function Set:elements()
if DEBUG_MODE then checkColonNotation("Set:elements()", self) end
local i = 0
local lastKey
local orderedKeys = self.orderedKeys
return function()
if lastKey == orderedKeys[i] then
i = i+1 --only increase i, if the last key in loop is still in place. If not, it means that the element has been removed and we need to stay at i.
end
lastKey = orderedKeys[i]
return lastKey
end
end]]--
local function setIterator(state)
local i = state.i
if state.lastKey == state.orderedKeys[i] then
i = i + 1
state.i = i
end
state.lastKey = state.orderedKeys[i]
if not state.lastKey then
state.orderedKeys = nil
state.i = nil
ReleaseTable(state)
end
return state.lastKey
end
function Set:elements()
local state = NewTable()
state.i = 0
state.orderedKeys = self.orderedKeys
return setIterator, state
end
function Set:size()
if DEBUG_MODE then checkColonNotation("Set:size()", self) end
return self.n
end
---returns true, when the set is empty and false otherwise
---@return boolean
function Set:isEmpty()
if DEBUG_MODE then checkColonNotation("Set:isEmpty()", self) end
return self:size() == 0
end
---Returns a random element from the Set.
function Set:random()
if DEBUG_MODE then checkColonNotation("Set:random()", self) end
return self.orderedKeys[math.random(self.n)]
end
function Set:popBack()
if DEBUG_MODE then checkColonNotation("Set:popBack()", self) end
local element = self.orderedKeys[self.n]
if self.data[element] then
local i,n = self.data[element], self.n
self.data[self.orderedKeys[n]] = i --last element takes iteration slot of removed element
self.orderedKeys[i] = self.orderedKeys[n]
self.orderedKeys[n] = nil
self.data[element] = nil
self.n = self.n - 1
end
return element, self.n
end
---removes all Elements from the set
---@return Set self
function Set:clear()
if DEBUG_MODE then checkColonNotation("Set:clear()", self) end
self.data = {}
self.orderedKeys = {}
self.n = 0
return self
end
---Returns an array with exactly the elements of the Set. Only do this, when another function input needs an array, because why should you use a Set, when you convert it to an array anyway?
---@return any[] array
function Set:toArray()
if DEBUG_MODE then checkColonNotation("Set:toArray()", self) end
local i,result = 1,{}
for element in self:elements() do
result[i] = element
i = i+1
end
return result
end
function Set:toString()
if DEBUG_MODE then checkColonNotation("Set:toString()", self) end
local elementsToString = {}
for i = 1, self.n do
elementsToString[i] = tostring(self.orderedKeys[i]) --must be translated to strings, else table.concat wouldn't work.
end
return '{' .. table.concat(elementsToString, ', ', 1, self.n) .. '}'
end
function Set:print()
if DEBUG_MODE then checkColonNotation("Set:print()", self) end
print(self:toString())
end
function Set:intersects(otherSet)
if DEBUG_MODE then checkColonNotation("Set:intersects(otherSet)", self) end
for element in self:elements() do
if otherSet.data[element] then
return true
end
end
return false
end
function Set:copy()
if DEBUG_MODE then checkColonNotation("Set:copy()", self) end
return Set.create():addAll(self)
end
function Set.union(...)
if DEBUG_MODE then checkDotNotation("Set.union(...)", ...) end
local resultSet = Set.create()
for i = 1, select('#',...) do
resultSet:addAll(select(i, ...))
end
return resultSet
end
function Set.intersection(...)
if DEBUG_MODE then checkDotNotation("Set.intersection(...)", ...) end
local n = select('#',...)
local resultSet = Set.create()
if n > 0 then resultSet:addAll(...) end --actually only adds the first container (addAll only supports one param)
for i = 2, n do
resultSet:retainAll(select(i,...))
end
return resultSet
end
function Set.except(containerA, containerB)
if DEBUG_MODE then checkDotNotation("Set.except(A,B)", containerA) end
return Set.create():addAll(containerA):removeAll(containerB)
end
function Set.fromGroup(unitgroup)
if DEBUG_MODE then checkDotNotation("Set.fromGroup(unitgroup)", unitgroup) end
local unitSet = Set.create()
ForGroup(unitgroup, function () unitSet:addSingle(GetEnumUnit()) end)
return unitSet
end
function Set.fromForce(playergroup)
if DEBUG_MODE then checkDotNotation("Set.fromForce(playergroup)", playergroup) end
local playerSet = Set.create()
ForForce(playergroup, function () playerSet:addSingle(GetEnumPlayer()) end)
return playerSet
end
function Set.fromTable(whichTable)
if DEBUG_MODE then checkDotNotation("Set.fromTable(whichTable)", whichTable) end
return Set.create():addAll(whichTable)
end
--------------------------
-- | SetUtils Library | --
--------------------------
--Mimick existing pick natives, but create Sets instead of groups, forces, destructable
SetUtils = {}
local autoUnitRemoveSubscriptions = Set.create() --Set of all Sets that are subscribed to automatic unit removal. Only in use, when a custom defend ability was provided.
local getUnitTypeId, unitAddAbility, unitMakeAbilityPermanent = GetUnitTypeId, UnitAddAbility, UnitMakeAbilityPermanent --localize natives for quicker access
---Removes the specified unit from all sets subscribed to auto-removal of invalid unit references.
---Called upon any unit leaving the game.
---@param unitToRemove unit
local function removeUnitFromSubscribedSets(unitToRemove)
for set in autoUnitRemoveSubscriptions:elements() do
set:removeSingle(unitToRemove)
end
end
local checkDotNotation = function(firstArgumentOfMethod)
if firstArgumentOfMethod == SetUtils then
throwError("SetUtils method used with :-notation instead of .-notation.")
end
end
--------UnitGroups---------
---Executes the specified enumFunc to create a unitgroup and converts the output to a set.
---@param enumFunc function
---@param destroyCondition? boolexpr pass a boolean expr if you want it to be destroyed after the enumeration.
---@param ... any the parameters to pass to enumFunc
---@return Set
local function enumGroupToSet(enumFunc, destroyCondition, ...)
if DEBUG_MODE then checkDotNotation(...) end
local unitgroup = CreateGroup()
enumFunc(unitgroup, ...)
local unitSet = Set.fromGroup(unitgroup)
DestroyGroup(unitgroup)
if destroyCondition then
DestroyBoolExpr(destroyCondition)
end
return unitSet
end
---Returns Condition(func), if the input a is a function. Returns the input otherwise.
---@param func? function | boolexpr
---@return boolexpr condition, boolean converted_yn
local function conditionIfNecessary(func)
local converted_yn = type(func) == 'function' ---@diagnostic disable-next-line: return-type-mismatch
return (converted_yn and Condition(func)) or func, converted_yn --real boolexpr have type userdata
end
---Returns the Set of all units in a specified rect that match a specified condition.
---Use GetFilterUnit() to refer to the unit being checked by the condition.
---@param whichRect rect
---@param condition? function | boolexpr
---@return Set
function SetUtils.getUnitsInRectMatching(whichRect, condition)
local convertedCondition, converted_yn = conditionIfNecessary(condition)
return enumGroupToSet(GroupEnumUnitsInRect, converted_yn and convertedCondition or nil, whichRect, convertedCondition)
end
---Returns the Set of all units in a specified rect.
---@param whichRect rect
---@return Set
function SetUtils.getUnitsInRect(whichRect)
return SetUtils.getUnitsInRectMatching(whichRect)
end
---Returns the Set of all units within a specified radius of the specified coordinates matching the specified condition.
---Use GetFilterUnit() to refer to the unit being checked by the condition.
---@param x real
---@param y real
---@param radius real
---@param condition? function | boolexpr
---@return Set
function SetUtils.getUnitsInRangeMatching(x, y, radius, condition)
local convertedCondition, converted_yn = conditionIfNecessary(condition)
return enumGroupToSet(GroupEnumUnitsInRange, converted_yn and convertedCondition or nil, x, y, radius, convertedCondition)
end
---Returns the Set of all units within a specified radius of the specified coordinates.
---@param x real
---@param y real
---@param radius real
---@return Set
function SetUtils.getUnitsInRange(x, y, radius)
return SetUtils.getUnitsInRangeMatching(x, y, radius)
end
---Returns the Set of all units owned by the specified player and matching the specified condition.
---Use GetFilterUnit() to refer to the unit being checked by the condition.
---@param whichPlayer player
---@param condition? function | boolexpr
---@return Set
function SetUtils.getUnitsOfPlayerMatching(whichPlayer, condition)
local convertedCondition, converted_yn = conditionIfNecessary(condition)
return enumGroupToSet(GroupEnumUnitsOfPlayer, converted_yn and convertedCondition or nil, whichPlayer, convertedCondition)
end
---Returns the Set of all units owned by the specified player.
---@param whichPlayer player
---@return Set
function SetUtils.getUnitsOfPlayer(whichPlayer)
return SetUtils.getUnitsOfPlayerMatching(whichPlayer)
end
--Permanently stores conditions for retreiving typeIds and the worldBounds rect. Both are created on demand. Prevents continous re-creation of conditionfuncs from anonymous functions.
--TypeIds are static and limited in number, so there's not much chance of creating unused objects with this system.
local storage = setmetatable({}, {})
getmetatable(storage).__index = function(t,k)
if k == 'worldBounds' then
t[k] = GetWorldBounds()
elseif k == 'returnTrue' then
t[k] = Condition(function() return true end)
else
t[k] = Condition(function() return GetUnitTypeId(GetFilterUnit()) == k end)
end
return rawget(t,k)
end
---Returns the Set of all units having the specified unitType and matching the specified condition.
---Use GetFilterUnit() to refer to the unit being checked by the condition.
---@param typeId integer
---@param condition? function | boolexpr
---@return Set
function SetUtils.getUnitsOfTypeIdMatching(typeId, condition)
condition = condition or storage['returnTrue']
local convertedCondition, converted_yn = conditionIfNecessary(condition)
local logicalAnd = And(storage[typeId], convertedCondition)
local returnSet = enumGroupToSet(GroupEnumUnitsInRect, logicalAnd, storage['worldBounds'], logicalAnd)
if converted_yn then DestroyBoolExpr(convertedCondition) end
return returnSet
end
---Returns the Set of all units owned by the specified player and having the specified unitType.
---Use GetFilterUnit() to refer to the unit being checked by the condition.
---@param whichPlayer player
---@param typeId integer
---@return Set
function SetUtils.getUnitsOfPlayerAndTypeId(whichPlayer, typeId)
return enumGroupToSet(GroupEnumUnitsOfPlayer, nil, whichPlayer, storage[typeId])
end
---Returns the Set of all units having the specified unitType.
---@param typeId integer
---@return Set
function SetUtils.getUnitsOfTypeId(typeId)
return SetUtils.getUnitsOfTypeIdMatching(typeId)
end
---Returns the Set of all units being currently selected by a player and matching the specified condition.
---@param whichPlayer player
---@param condition? function | boolexpr
---@return Set
function SetUtils.getUnitsSelected(whichPlayer, condition)
SyncSelections() --important to prevent desyncs, as selections are saved locally.
local convertedCondition, converted_yn = conditionIfNecessary(condition)
return enumGroupToSet(GroupEnumUnitsSelected, converted_yn and convertedCondition or nil, whichPlayer, convertedCondition)
end
SetUtils.getUnitsSelectedMatching = SetUtils.getUnitsSelected
--------PlayerGroups---------
---Returns the Set of all players.
---Contains players that were present during game start, including computer players.
---@return Set
function SetUtils.getPlayersAll()
return Set.fromForce(GetPlayersAll()) --global wc3 var, doesn't produce memory leaks
end
---Returns the Set of all active players (including computer players) matching the specified condition.
---Only contains players that were present during game start, including computer players.
---Use GetFilterPlayer() to refer to the player being checked in the condition.
---@param condition function | boolexpr
---@return Set
function SetUtils.getPlayersMatching(condition)
local playergroup = CreateForce()
local convertedCondition, converted_yn = conditionIfNecessary(condition)
ForceEnumPlayers(playergroup, convertedCondition)
local playerSet = Set.fromForce(playergroup)
DestroyForce(playergroup)
if converted_yn then
DestroyBoolExpr(convertedCondition)
end
return playerSet
end
--------DestructableGroups---------
---Returns the Set of all destructables in the specified rect matching the specified condition.
---Use GetFilterDestructable() to refer to the destructable being checked in the condition.
---@param whichRect rect
---@param condition? function | boolexpr
---@return Set
function SetUtils.getDestructablesInRectMatching(whichRect, condition)
local destructableSet = Set.create()
local convertedCondition, converted_yn = conditionIfNecessary(condition)
EnumDestructablesInRect(whichRect, convertedCondition, function() destructableSet:add(GetEnumDestructable()) end)
if converted_yn then
DestroyBoolExpr(convertedCondition)
end
return destructableSet
end
---Returns the Set of all destructables in the specified rect.
---@param whichRect rect
---@return Set
function SetUtils.getDestructablesInRect(whichRect)
return SetUtils.getDestructablesInRectMatching(whichRect)
end
--------ItemGroups---------
---Returns the Set of all items in the specified rect matching the specified condition.
---Use GetFilterItem() to refer to the item being checked in the condition.
---@param whichRect rect
---@param condition function | boolexpr
---@return Set
function SetUtils.getItemsInRectMatching(whichRect, condition)
local itemSet = Set.create()
local convertedCondition, converted_yn = conditionIfNecessary(condition)
EnumItemsInRect(whichRect, convertedCondition, function() itemSet:add(GetEnumItem()) end)
if converted_yn then
DestroyBoolExpr(convertedCondition)
end
return itemSet
end
---Returns the Set of all items in the specified rect.
---@param whichRect rect
---@return Set
function SetUtils.getItemsInRect(whichRect)
return SetUtils.getItemsInRectMatching(whichRect)
end
--------Utility---------
---Removes all invalid unit references from the specified Set, i.e. units that have already been removed from the game.
---If your Set contains non-unit elements, you must set the second parameter to true to avoid crashes.
---@param whichSet Set the Set that might contain references to removed units
---@param checkIfUnit? boolean default: false. Set to true to avoid crashes, if the Set contains non-unit elements.
function SetUtils.clearInvalidUnitRefs(whichSet, checkIfUnit)
for element in whichSet:elements() do
if not checkIfUnit or wc3Type(element) == 'unit' then
if getUnitTypeId(element) == 0 then
whichSet:removeSingle(element)
end
end
end
end
---Subscribes the specified set to automatic removal of invalid unit references, i.e. for the rest of the game, units that are removed from the game will also be removed from the specified set.
---Set the second parameter to false (default true) to unsubscribe the specified Set from the automatic unit cleaning.
---As long as a Set is subscribed, it will not be garbage collected.
---This function requires CUSTOM_DEFEND_ABICODE to be set.
---@param whichSet Set
---@param subscribe_yn? boolean default: true. true to subscribe. false to unsubscribe.
function SetUtils.subscribeSetToAutoUnitRemoval(whichSet, subscribe_yn)
if CUSTOM_DEFEND_ABICODE then
if subscribe_yn or subscribe_yn == nil then
SetUtils.clearInvalidUnitRefs(whichSet, true)
autoUnitRemoveSubscriptions:addSingle(whichSet)
else
autoUnitRemoveSubscriptions:removeSingle(whichSet)
end
else
throwError("You can't use SetUtils.subscribeToAutoUnitRemoval, until you have provided a custom defend ability.")
end
end
local hasUnitBeenRemovedCondition ---@type conditionfunc initialized in SetUtils.createTriggers() below.
---Adds the event "unit is removed from the game" to the specified trigger.
---This event is not compatible with other events, so don't use it on triggers with multiple events (other events will simply be invalidated).
---Requires CUSTOM_DEFEND_ABICODE to be set.
---@param whichTrigger trigger
function SetUtils.triggerRegisterAnyUnitRemoveEvent(whichTrigger)
if CUSTOM_DEFEND_ABICODE then
TriggerRegisterAnyUnitEventBJ(whichTrigger, EVENT_PLAYER_UNIT_ISSUED_ORDER)
TriggerAddCondition(whichTrigger, hasUnitBeenRemovedCondition)
else
throwError("You can't use SetUtils.triggerRegisterAnyUnitRemoveEvent, until you have provided a custom defend ability.")
end
end
--------Triggers for unit auto removal---------
function SetUtils.createTriggers()
--Init unit reference cleanup methods
--Add the custom defend ability to all units entering the map, if it was provided.
if CUSTOM_DEFEND_ABICODE then
for i = 0, GetBJMaxPlayers() - 1 do
SetPlayerAbilityAvailable(Player(i), CUSTOM_DEFEND_ABICODE, false)
end
local enterTrigger = CreateTrigger()
TriggerRegisterEnterRectSimple( enterTrigger, bj_mapInitialPlayableArea )
local function prepareNewUnit()
local u = GetTriggerUnit()
unitAddAbility(u, CUSTOM_DEFEND_ABICODE)
unitMakeAbilityPermanent(u, true, CUSTOM_DEFEND_ABICODE)
end
TriggerAddAction(enterTrigger, prepareNewUnit)
--Initialize upvalue. We don't set hasUnitBeenRemovedCondition earlier to prevent using Wc3 natives in the Lua root.
hasUnitBeenRemovedCondition = Condition(function() return (GetIssuedOrderId() == 852056) and GetUnitAbilityLevel(GetTriggerUnit(), CUSTOM_DEFEND_ABICODE) == 0 end) --undefend order. This one is issued upon units leaving the game, but also under other circumstances. Ability-Level == 0 proves the removed from the game event.
local removeTrigger = CreateTrigger()
SetUtils.triggerRegisterAnyUnitRemoveEvent(removeTrigger)
TriggerAddAction(removeTrigger, function() removeUnitFromSubscribedSets(GetTriggerUnit()) end)
end
end
---@diagnostic disable-next-line: undefined-global
if OnInit and OnInit.trig then OnInit.trig(SetUtils.createTriggers) end --use TotalInit library, if available.
end
if Debug and Debug.endFile then Debug.endFile() end
do
WorldBounds = {}
PlayableBounds = {}
TeleportBounds = {}
TeleportBounds.point = 0.5
function WorldBounds.Init()
PlayableBounds.r = bj_mapInitialPlayableArea
PlayableBounds.maxX = GetRectMaxX(PlayableBounds.r)
PlayableBounds.maxY = GetRectMaxY(PlayableBounds.r)
PlayableBounds.minX = GetRectMinX(PlayableBounds.r)
PlayableBounds.minY = GetRectMinY(PlayableBounds.r)
TeleportBounds.maxX = PlayableBounds.maxX * TeleportBounds.point
TeleportBounds.maxY = PlayableBounds.maxY * TeleportBounds.point
TeleportBounds.minX = PlayableBounds.minX * TeleportBounds.point
TeleportBounds.minY = PlayableBounds.minY * TeleportBounds.point
WorldBounds.r = GetWorldBounds()
WorldBounds.maxX = GetRectMaxX(WorldBounds.r)
WorldBounds.maxY = GetRectMaxY(WorldBounds.r)
WorldBounds.minX = GetRectMinX(WorldBounds.r)
WorldBounds.minY = GetRectMinY(WorldBounds.r)
WorldBounds.reg = CreateRegion()
WorldBounds.length = WorldBounds.maxX - WorldBounds.minX
WorldBounds.width = WorldBounds.maxY - WorldBounds.minY
PlayableBounds.length = PlayableBounds.maxX - PlayableBounds.minX
PlayableBounds.width = PlayableBounds.maxY - PlayableBounds.minY
PlayableBounds.cell_length = PlayableBounds.length // 128
PlayableBounds.cell_width = PlayableBounds.width // 128
PlayableBounds.half_length = PlayableBounds.length // 2
PlayableBounds.half_width = PlayableBounds.width // 2
RegionAddRect(WorldBounds.reg,WorldBounds.r)
end
function WorldBounds.ClampCords(x,y)
return clamp(x, WorldBounds.minX, WorldBounds.maxX), clamp(y, WorldBounds.minY, WorldBounds.maxY)
end
function PlayableBounds.ClampCords(x,y)
return clamp(x, PlayableBounds.minX, PlayableBounds.maxX), clamp(y, PlayableBounds.minY, PlayableBounds.maxY)
end
function DuplicateRect(r)
return Rect(GetRectMinX(r),GetRectMinY(r),GetRectMaxX(r),GetRectMaxY(r))
end
end
do
PlayerData = {}
PlayerUsers = {}
PlayerComps = {}
PlayerId2Data = {}
Player2Id = {}
PLAYER_COUNT = 0
function Player2Data(p)
return PlayerId2Data[GetPlayerId(p)]
end
function InitPlayerData()
LocalPlayer = GetLocalPlayer()
local t = CreateTrigger()
for i = 0, MAX_PLAYERS, 1 do
local p = Player(i)
Player2Id[p] = i
table.insert(PlayerData, {
id = i,
user = GetPlayerSlotState(p) == PLAYER_SLOT_STATE_PLAYING and GetPlayerController(p) == MAP_CONTROL_USER,
p = p,
name = GetPlayerName(p)
})
if LocalPlayer == p then
PlayerDataLocal = PlayerData[#PlayerData]
end
PlayerId2Data[i] = PlayerData[#PlayerData]
if PlayerData[#PlayerData].user then
PlayerData[#PlayerData].PlayerAlive = true
table.insert(PlayerUsers,p)
TriggerRegisterPlayerEvent(t,p,EVENT_PLAYER_LEAVE)
PLAYER_COUNT = PLAYER_COUNT + 1
else
table.insert(PlayerComps,p)
SetPlayerController(p, MAP_CONTROL_COMPUTER)
end
end
for i, p1 in ipairs(PlayerData) do
for j, p2 in ipairs(PlayerData) do
if p1 ~= p2 then
local ally = p1.user == p2.user or i>24 or j>24
local vision = p1.user and not p2.user
SetPlayerAlliance(p1.p,p2.p,ALLIANCE_PASSIVE,ally)
SetPlayerAlliance(p2.p,p1.p,ALLIANCE_PASSIVE,ally)
SetPlayerAlliance(p1.p,p2.p,ALLIANCE_SHARED_VISION,vision or ally)
SetPlayerAlliance(p2.p,p1.p,ALLIANCE_SHARED_VISION,not vision or ally)
end
end
end
InitSaveLoad()
TriggerAddAction(t,function()
PLAYER_COUNT = PLAYER_COUNT - 1
end)
end
end
do
local SoundPitch = {}
Sound = {
path = "",
pitch = 1,
duration = 0,
looping = false,
is3D = false,
outOfRangeStop = false,
minDist = 4000,
maxDist = 10000,
fadeIn = 1,
fadeOut = 1,
eaxSetting = "DefaultEAXON",
volume = 127,
cooldown = -1,
current_cd = 0
}
Sound.__index = Sound
local _AllSounds = {}
local _3DSounds = {}
function Sound.reset()
for index, value in ipairs(_AllSounds) do
_AllSounds[index].current_cd = nil
end
end
function Sound.move_all(offset_x,offset_y)
for index, value in ipairs(_3DSounds) do
local sound_data = _3DSounds[index]
sound_data.x = sound_data.x + offset_x
sound_data.y = sound_data.y + offset_x
SetSoundPosition(sound_data.sound,sound_data.x,sound_data.y,0)
end
end
function Sound:new(t)
t = t or {}
setmetatable(t,Sound)
t.instances = {}
table.insert(_AllSounds,t)
return t
end
local function recycle(self,sound_data)
table.insert(self.instances,sound_data.sound)
if sound_data.x then
table.removeobject(_3DSounds,sound_data)
sound_data.x = nil
sound_data.y = nil
end
sound_data.sound = nil
ReleaseTable(sound_data)
end
local function loading(snd,pitch,self,isLocal,x,y,target)
table.insert(self.instances,snd)
self(pitch,isLocal,x,y,target)
end
function Sound:stop(snd,fade)
StopSound(snd,false,fade)
table.insert(self.instances,snd)
end
function Sound:__call(pitch,isLocal,x,y,target)
if self.current_cd > GAME_TIME then return end
self.current_cd = GAME_TIME + self.cooldown
local pos = #self.instances
local snd
if pos == 0 then
snd = CreateSound(
self.path,
self.looping,
self.is3D,
self.outOfRangeStop,
self.fadeIn,
self.fadeOut,
self.eaxSetting
)
SetSoundVolume(snd,self.volume)
if self.is3D then
SetSoundDistances(snd,self.minDist,self.maxDist)
end
UNPAUSEABLE_TIMER:callDelayed(0.0, loading,snd,pitch,self,isLocal,x,y,target)
else
snd = self.instances[pos]
table.remove(self.instances,pos)
local data = NewTable()
data.sound = snd
if x ~= nil and y ~= nil then
SetSoundPosition(snd,x,y,0)
data.x = x
data.y = y
table.insert(_3DSounds,data)
elseif target then
AttachSoundToUnit(snd,target)
end
if isLocal == nil or isLocal then
StartSound(snd)
end
if not self.looping then
UNPAUSEABLE_TIMER:callDelayed(self.duration*SoundPitch[snd], recycle,self,data)
end
end
SetPitch(snd,pitch or self.pitch)
return snd
end
function SetPitch(snd,pitch)
if GetSoundIsPlaying(snd) or GetSoundIsLoading(snd) then
local last = SoundPitch[snd] or 1
SetSoundPitch(snd,1/last)
SetSoundPitch(snd,pitch)
elseif pitch == 1 then
SetSoundPitch(snd,1.0001)
pitch = 1.0001
else
SetSoundPitch(snd,pitch)
end
SoundPitch[snd] = pitch
end
end
do --//FadeFilter
local udg_cineFadeFinishTimer = {}
local udg_cineFadeContinueTimer = {}
local udg_cineFadeContinueRed = {}
local udg_cineFadeContinueGreen = {}
local udg_cineFadeContinueBlue = {}
local udg_cineFadeContinueTrans = {}
local udg_cineFadeContinueDuration = {}
local udg_cineFadeContinueTex = {}
function CinematicFadeCommonForPlayer(p,red,green, blue, duration, tex, startTrans, endTrans)
if duration == 0 then
startTrans = endTrans
end
if GetLocalPlayer() == p then
SetCineFilterTexture(tex)
SetCineFilterBlendMode(BLEND_MODE_BLEND)
SetCineFilterTexMapFlags(TEXMAP_FLAG_NONE)
SetCineFilterStartUV(0, 0, 1, 1)
SetCineFilterEndUV(0, 0, 1, 1)
SetCineFilterStartColor(PercentTo255(red), PercentTo255(green), PercentTo255(blue), PercentTo255(100-startTrans))
SetCineFilterEndColor(PercentTo255(red), PercentTo255(green), PercentTo255(blue), PercentTo255(100-endTrans))
SetCineFilterDuration(duration)
DisplayCineFilter(true)
end
end
function FinishCinematicFadeForPlayer()
local playerid = 0
for index = 0, 23, 1 do
if GetExpiredTimer() == udg_cineFadeFinishTimer[index] then
playerid = index
end
end
DestroyTimer(udg_cineFadeFinishTimer[playerid])
udg_cineFadeFinishTimer[playerid] = nil
if GetPlayerId(GetLocalPlayer()) == playerid then
DisplayCineFilter(false)
end
end
function FinishCinematicFadeAfterForPlayer(playerid,duration)
udg_cineFadeFinishTimer[playerid] = CreateTimer()
TimerStart(udg_cineFadeFinishTimer[playerid], duration, false, FinishCinematicFadeForPlayer)
end
function ContinueCinematicFadeForPlayer()
local playerid = 0
for index = 0, 23, 1 do
if GetExpiredTimer() == udg_cineFadeContinueTimer[index] then
playerid = index
end
end
DestroyTimer(udg_cineFadeContinueTimer[playerid])
udg_cineFadeContinueTimer[playerid] = nil
CinematicFadeCommonForPlayer(Player(playerid), udg_cineFadeContinueRed[playerid], udg_cineFadeContinueGreen[playerid], udg_cineFadeContinueBlue[playerid], udg_cineFadeContinueDuration[playerid], udg_cineFadeContinueTex[playerid], udg_cineFadeContinueTrans[playerid], 100)
end
function ContinueCinematicFadeAfterForPlayer( playerid, duration, red, green, blue, trans, tex)
udg_cineFadeContinueRed[playerid] = red
udg_cineFadeContinueGreen[playerid] = green
udg_cineFadeContinueBlue[playerid] = blue
udg_cineFadeContinueTrans[playerid] = trans
udg_cineFadeContinueDuration[playerid] = duration
udg_cineFadeContinueTex[playerid] = tex
udg_cineFadeContinueTimer[playerid] = CreateTimer()
TimerStart(udg_cineFadeContinueTimer[playerid], duration, false, ContinueCinematicFadeForPlayer)
end
function AbortCinematicFadeForPlayer(playerid)
if udg_cineFadeContinueTimer[playerid] ~= nil then
DestroyTimer(udg_cineFadeContinueTimer[playerid])
end
if udg_cineFadeFinishTimer[playerid] ~= nil then
DestroyTimer(udg_cineFadeFinishTimer[playerid])
end
end
function CinematicFadeForPlayer(p, fadetype, duration, tex, red, green, blue, trans)
local playerid = GetPlayerId(p)
if (fadetype == bj_CINEFADETYPE_FADEOUT) then
AbortCinematicFadeForPlayer(playerid)
CinematicFadeCommonForPlayer(p, red, green, blue, duration, tex, 100, trans)
elseif (fadetype == bj_CINEFADETYPE_FADEIN) then
AbortCinematicFadeForPlayer(playerid)
CinematicFadeCommonForPlayer(p, red, green, blue, duration, tex, trans, 100)
FinishCinematicFadeAfterForPlayer(playerid, duration)
elseif (fadetype == bj_CINEFADETYPE_FADEOUTIN) then
if (duration > 0) then
AbortCinematicFadeForPlayer(playerid)
CinematicFadeCommonForPlayer(p, red, green, blue, duration * 0.5, tex, 100, trans)
ContinueCinematicFadeAfterForPlayer(playerid, duration * 0.5, red, green, blue, trans, tex)
FinishCinematicFadeAfterForPlayer(playerid, duration)
end
end
end
function CinematicFade( fadetype, duration, tex, red, green, blue, trans)
local p
for i = 0, 23, 1 do
p = Player(i)
if IsPlayerInForce(p,udg_AllPlayers) then
CinematicFadeForPlayer(p,fadetype,duration,tex,red,green,blue,trans)
end
end
end
end
do
Heap = {}
Heap.__index = Heap
local function findLowest( a, b )
return a < b
end
local function newHeap( template, compare )
local tbl = NewTable()
tbl.Data = NewTable()
tbl.Compare = compare or findLowest
tbl.Size = 0
return setmetatable(tbl,template)
end
function Heap:Release()
ReleaseTable(self.Data)
self.Compare = nil
self.Data = nil
self.Size = nil
setmetatable(self, nil)
ReleaseTable(self)
end
local function sortUp( heap, index )
if index <= 1 then return end
local pIndex = index % 2 == 0 and MathRound(index / 2) or MathRound(( index - 1 ) / 2)
if not heap.Compare( heap.Data[pIndex], heap.Data[index] ) then
heap.Data[pIndex], heap.Data[index] = heap.Data[index], heap.Data[pIndex]
sortUp( heap, pIndex )
end
end
local function sortDown( heap, index )
local leftIndex, rightIndex, minIndex
leftIndex = index * 2
rightIndex = leftIndex + 1
if rightIndex > heap.Size then
if leftIndex > heap.Size then return
else minIndex = leftIndex end
else
if heap.Compare( heap.Data[leftIndex], heap.Data[rightIndex] ) then minIndex = leftIndex
else minIndex = rightIndex end
end
if not heap.Compare( heap.Data[index], heap.Data[minIndex] ) then
heap.Data[index], heap.Data[minIndex] = heap.Data[minIndex], heap.Data[index]
sortDown( heap, minIndex )
end
end
function Heap:Empty()
return self.Size == 0
end
function Heap:Clear()
ReleaseTable(self.Data)
self.Data, self.Size, self.Compare = NewTable(), 0, self.Compare or findLowest
return self
end
function Heap:Push( item )
if item then
self.Size = self.Size + 1
self.Data[self.Size] = item
sortUp( self, self.Size )
end
return self
end
function Heap:Pop()
local root
if self.Size > 0 then
root = self.Data[1]
self.Data[1] = self.Data[self.Size]
self.Data[self.Size] = nil
self.Size = self.Size - 1
if self.Size > 1 then
sortDown( self, 1 )
end
end
return root
end
setmetatable( Heap, { __call = function( self, ... ) return newHeap( self, ... ) end } )
end
do
Vector = {}
Vector.__index = Vector
local vectorTable = {}
local function newVector( x, y )
local newV = vectorTable[#vectorTable] or {}
setmetatable( newV, Vector )
vectorTable[#vectorTable] = nil
newV.x = x or 0
newV.y = y or 0
return newV
end
function ReleaseVector(v)
table.insert(vectorTable,v)
end
function DuplicateVectorTable(tbl)
local new = NewTable()
for i, vec in ipairs(tbl) do
new[i] = vec
end
return new
end
function ReleaseVectorTable(tbl)
for i, vec in ipairs(tbl) do
table.insert(vectorTable,vec)
tbl[i] = nil
end
ReleaseTable(tbl)
end
function ClearVectorTable(tbl)
for i, vec in ipairs(tbl) do
table.insert(vectorTable,vec)
tbl[i] = nil
end
end
function isvector( vTbl )
return getmetatable( vTbl ) == Vector
end
function Vector.__unm( vTbl )
return newVector( -vTbl.x, -vTbl.y )
end
function Vector.__add( a, b )
return newVector( a.x + b.x, a.y + b.y )
end
function Vector.__sub( a, b )
return newVector( a.x - b.x, a.y - b.y )
end
function Vector.__mul( a, b )
if type( a ) == "number" then
return newVector( a * b.x, a * b.y )
elseif type( b ) == "number" then
return newVector( a.x * b, a.y * b )
else
return newVector( a.x * b.x, a.y * b.y )
end
end
function Vector.__div( a, b )
return newVector( a.x / b, a.y / b )
end
function Vector.__eq( a, b )
return a.x == b.x and a.y == b.y
end
function Vector:__tostring()
return "(" .. self.x .. ", " .. self.y .. ")"
end
function Vector:ID()
if self._ID == nil then
local x, y = self.x, self.y
self._ID = 0.5 * ( ( x + y ) * ( x + y + 1 ) + y )
end
return self._ID
end
setmetatable( Vector, { __call = function( _, ... ) return newVector( ... ) end } )
Vector.neighbors = {Vector(1,0), Vector(0,1), Vector(-1,0), Vector(0,-1)}
end
do
Astar = {}
Astar.__index = Astar
-- This instantiates a new Astar class for usage later.
-- "start" and "finish" should both be 2 dimensional vectors, or just a table with "x" and "y" keys.
-- positionOpenCheck can be a function or a table.
-- If it's a function it must have a return value of true or false depending on whether or not the position is open.
-- If it's a table it should simply be a table of values such as "pos[x][y] = true".
function Astar:Initialize( start, finish, positionOpenCheck, max )
local tbl = NewTable()
local newPath = setmetatable( tbl, Astar )
tbl.max = max or 100
tbl.PositionOpenCheck = positionOpenCheck
tbl.Start = start
tbl.Finish = finish
newPath:CalculatePath(tbl.max)
return newPath
end
function Astar:Release()
if self.touched then
for _, vec in ipairs(self.touched) do
vec.gScore = nil
vec.hScore = nil
vec.fScore = nil
vec.closed = nil
vec.touched = nil
vec.previous = nil
ReleaseVector(vec)
end
ReleaseTable(self.touched)
end
self.touched = nil
self.Start = nil
self.Finish = nil
self.max = nil
self.PositionOpenCheck = nil
ReleaseTable(self.Path)
self.Path = nil
ReleaseTable(self)
setmetatable(self, nil)
end
function Astar:CleanPath()
local vecLast
local path = self.Path
local i = 1
local remove = NewTable()
while i < #path do
local vec = path[i]
if i > 1 and i < #path then
local difX=math.abs(path[i-1].x-path[i+1].x)
local difY=math.abs(path[i-1].y-path[i+1].y)
if difX==1 and difY==1 then
table.insert(remove,i)
end
end
i = i + 1
end
for i = #remove, 1, -1 do
table.remove(path,remove[i])
end
ReleaseTable(remove)
end
local function distance( start, finish )
local dx = start.x - finish.x
local dy = start.y - finish.y
return dx * dx + dy * dy
end
local positionIsOpen
local function positionIsOpenTable( pos, check ) return check[pos.x] and check[pos.x][pos.y] end
local function positionIsOpenCustom( pos, check ) return check( pos ) end
local adjacentPositions = {
Vector( 0, -128 ),
Vector( -128, 0 ),
Vector( 0, 128 ),
Vector( 128, 0 ),
Vector( -128, -128 ),
Vector( 128, 128 ),
Vector( -128, 128 ),
Vector( 128, -128 ),
}
local maxAdjacents = #adjacentPositions
local function fetchOpenAdjacentNodes( pos, positionOpenCheck )
local result = NewTable()
for i = 1, #adjacentPositions do
local adjacentPos = pos + adjacentPositions[i]
if adjacentPos and positionIsOpen( adjacentPos, positionOpenCheck ) then
table.insert( result, adjacentPos )
else
ReleaseVector(adjacentPos)
end
end
return result
end
local function compareFunc(a,b)
return a.fScore < b.fScore
end
function Astar:GetClosestOpenCell(vec)
local positionOpenCheck = self.PositionOpenCheck
positionIsOpen = type( positionOpenCheck ) == "table" and positionIsOpenTable or positionIsOpenCustom
local listX = NewTable()
local listY = NewTable()
local listNextX = NewTable()
local listNextY = NewTable()
local closed = NewKeyTable()
local ret
listNextX[1] = vec.x
listNextY[1] = vec.y
while #listNextX > 0 do
listX, listNextX = listNextX, listX
listY, listNextY = listNextY, listY
local max = #listX
for i=1, max do
local x = listX[i]
local y = listY[i]
local v = Vector(x,y)
if positionIsOpen(v,positionOpenCheck) then
for j = i, max do
listX[j] = nil
listY[j] = nil
end
ReleaseTable(listX)
ReleaseTable(listY)
ReleaseTable(listNextX)
ReleaseTable(listNextY)
ReleaseKeyTable(closed)
return v
else
ReleaseVector(v)
for j = 1, 4 do
local x2, y2 = x + adjacentPositions[j].x, y + adjacentPositions[j].y
local cellId = x2 + y2 * 1e7
if not closed[cellId] then
closed[cellId] = true
table.insert(listNextX,x2)
table.insert(listNextY,y2)
end
end
end
listX[i] = nil
listY[i] = nil
end
end
ReleaseTable(listX)
ReleaseTable(listY)
ReleaseTable(listNextX)
ReleaseTable(listNextY)
ReleaseKeyTable(closed)
end
-- This is the function used to actually calculate the path.
-- It returns the calcated path table, or nil if it cannot find a path.
function Astar:CalculatePath(max)
local start, finish, positionOpenCheck = self.Start, self.Finish, self.PositionOpenCheck
if not positionOpenCheck then return end
positionIsOpen = type( positionOpenCheck ) == "table" and positionIsOpenTable or positionIsOpenCustom
if not positionIsOpen( finish, positionOpenCheck ) then return end
local open = Heap()
local touched = NewTable()
touched[1] = start
self.touched = touched
start.touched = true
start.gScore = 0
start.hScore = distance( start, finish )
start.fScore = start.hScore
open.Compare = compareFunc
open:Push( start )
while not open:Empty() do
max = max - 1
if max == 0 then
open:Release()
return
end
local current = open:Pop()
if not current.closed then
if current == finish then
local path = NewTable()
while true do
if current.previous then
table.insert( path, 1, current )
current = current.previous
else
self.Path = path
open:Release()
return path
end
end
end
current.closed = true
for i = 1, maxAdjacents do
local adjacent = current + adjacentPositions[i]
if adjacent and positionIsOpen( adjacent, positionOpenCheck ) then
if not adjacent.closed then
local added_gScore = current.gScore + distance( current, adjacent )
if not adjacent.gScore or added_gScore < adjacent.gScore then
if not adjacent.touched then
table.insert(touched,adjacent)
adjacent.touched = true
end
adjacent.gScore = added_gScore
if not adjacent.hScore then
adjacent.hScore = distance( adjacent, finish )
end
adjacent.fScore = added_gScore + adjacent.hScore
open:Push( adjacent )
adjacent.previous = current
end
end
else
ReleaseVector(adjacent)
end
end
end
end
for _, vec in ipairs(touched) do
vec.gScore = nil
vec.hScore = nil
vec.fScore = nil
vec.closed = nil
vec.touched = nil
vec.previous = nil
end
ReleaseTable(touched)
open:Release()
end
function Astar:GetPath()
return self.Path
end
function Astar:GetDistance()
local path = self.Path
if not path then return end
return distance( path[1], path[#path] )
end
function Astar:GetTiles()
local path = self.Path
if not path then return end
return #path
end
function Astar:__tostring()
local path = self.Path
local string = ""
if path then
for k, v in ipairs( path ) do
local formatted = ( k .. ": " .. v )
string = k == 1 and formatted or string .. "\n" .. formatted
end
end
return string
end
setmetatable( Astar, { __call = function( self, ... ) return self:Initialize( ... ) end } )
end
do
Event = {}
Event.__index = Event
EventAction = {}
EventAction.__index = EventAction
function Event.new()
local self = setmetatable(NewTable(), Event)
self.actions = NewTable()
return self
end
function EventAction.new(event,...)
local self = setmetatable(NewTable(), EventAction)
local args = varagToTable(...)
self.func = args[1]
table.remove(args,1)
self.args = args
self.event = event
table.insert(event.actions,self)
return self
end
function EventAction.destroy(self)
self.func = nil
ReleaseTable(self.args)
table.removeobject(self.event.actions,self)
self.event = nil
self.args = nil
setmetatable(self,nil)
ReleaseTable(self)
end
function Event.add_action(self,...)
return EventAction.new(self,...)
end
function Event.destroy(self)
for i = #self.actions, 1, -1 do
self.actions[i]:destroy()
end
ReleaseTable(self.actions)
setmetatable(self,nil)
self.actions = nil
ReleaseTable(self)
end
function Event.fire(self,...)
local args = varagToTable(...)
for i = 1, #self.actions, 1 do
local action = self.actions[i]
if #action.args > 0 then
local tbl = NewTable()
for j = 1, #action.args do
tbl[j] = action.args[j]
end
for j = 1, #args do
table.insert(tbl,args[j])
end
action.func(table.unpack(tbl))
ReleaseTable(tbl)
else
action.func(...)
end
end
ReleaseTable(args)
end
end
if Debug then Debug.beginFile "FileIO" end
--[[
FileIO v1a
Provides functionality to read and write files, optimized with lua functionality in mind.
API:
FileIO.Save(filename, data)
- Write string data to a file
FileIO.Load(filename) -> string?
- Read string data from a file. Returns nil if file doesn't exist.
FileIO.SaveAsserted(filename, data, onFail?) -> bool
- Saves the file and checks that it was saved successfully.
If it fails, passes (filename, data, loadResult) to onFail.
FileIO.enabled : bool
- field that indicates that files can be accessed correctly.
Optional requirements:
DebugUtils by Eikonium @ https://www.hiveworkshop.com/threads/330758/
Total Initialization by Bribe @ https://www.hiveworkshop.com/threads/317099/
Inspired by:
- TriggerHappy's Codeless Save and Load @ https://www.hiveworkshop.com/threads/278664/
- ScrewTheTrees's Codeless Save/Sync concept @ https://www.hiveworkshop.com/threads/325749/
- Luashine's LUA variant of TH's FileIO @ https://www.hiveworkshop.com/threads/307568/post-3519040
- HerlySQR's LUA variant of TH's Save/Load @ https://www.hiveworkshop.com/threads/331536/post-3565884
Updated: 8 Mar 2023
--]]
function InitFileIO()
local RAW_PREFIX = ']]i([['
local RAW_SUFFIX = ']])--[['
local RAW_SIZE = 256 - #RAW_PREFIX - #RAW_SUFFIX
local LOAD_ABILITY = FourCC('ANdc')
local LOAD_EMPTY_KEY = '!@#$, empty data'
local function open(filename)
name = filename
PreloadGenClear()
Preload('")\nendfunction\n//!beginusercode\nlocal p={} local i=function(s) table.insert(p,s) end--[[')
end
local function write(s)
for i = 1, #s, RAW_SIZE do
Preload(RAW_PREFIX .. s:sub(i, i + RAW_SIZE - 1) .. RAW_SUFFIX)
end
end
local function close()
Preload(']]BlzSetAbilityTooltip(' .. LOAD_ABILITY .. ', table.concat(p), 0)\n//!endusercode\nfunction a takes nothing returns nothing\n//')
PreloadGenEnd(name)
name = nil
end
---
---@param filename string
---@param data string
local function savefile(filename, data)
open(filename)
write(data)
close()
end
---@param filename string
---@return string?
local function loadfile(filename)
local s = BlzGetAbilityTooltip(LOAD_ABILITY, 0)
BlzSetAbilityTooltip(LOAD_ABILITY, LOAD_EMPTY_KEY, 0)
Preloader(filename)
local loaded = BlzGetAbilityTooltip(LOAD_ABILITY, 0)
BlzSetAbilityTooltip(LOAD_ABILITY, s, 0)
if loaded == LOAD_EMPTY_KEY then
return nil
end
return loaded
end
---@param filename string
---@param data string
---@param onFail function?
---@return boolean
local function saveAsserted(filename, data, onFail)
savefile(filename, data)
local res = loadfile(filename)
if res == data then
return true
end
if onFail then
onFail(filename, data, res)
end
return false
end
local fileIO_enabled = saveAsserted('TestFileIO.pld', 'FileIO is Enabled')
FileIO = {
Save = savefile,
Load = loadfile,
SaveAsserted = saveAsserted,
enabled = fileIO_enabled,
}
end
if Debug then Debug.endFile() end
do
Encoder = {}
local b
local function encHelper1(x)
local r,b='',x:byte()
for i=8,1,-1 do r=r..(b%%2^i-b%%2^(i-1)>0 and '1' or '0') end
return r;
end
local function encHelper2(x)
if (#x < 6) then return '' end
local c=0
for i=1,6 do c=c+(x:sub(i,i)=='1' and 2^(6-i) or 0) end
return b:sub(c+1,c+1)
end
function enc(data,key)
b = key
return ((data:gsub('.', encHelper1)..'0000'):gsub('%%d%%d%%d?%%d?%%d?%%d?', encHelper2)..({ '', '==', '=' })[#data%%3+1])
end
local function decHelper1(x)
if (x == '=') then return '' end
local r,f='',(b:find(x)-1)
for i=6,1,-1 do r=r..(f%%2^i-f%%2^(i-1)>0 and '1' or '0') end
return r;
end
local function decHelper2(x)
if (#x ~= 8) then return '' end
local c=0
for i=1,8 do c=c+(x:sub(i,i)=='1' and 2^(8-i) or 0) end
return string.char(c)
end
function dec(data,key)
b = key
data = string.gsub(data, '[^'..b..'=]', '')
return (data:gsub('.', decHelper1):gsub('%%d%%d%%d?%%d?%%d?%%d?%%d?%%d?', decHelper2))
end
function Convert2Data(str)
local data = {}
while true do
local s = string.match(str,"%%d+")
if s then
str = string.gsub(str,s,"",1)
if tonumber(s) > 2147483647 then
table.insert(data,0)
else
table.insert(data,tonumber(s))
end
else
break
end
end
return data
end
function Encoder.EncodePlayer(p,data)
data = math.abs(StringHash(Player2Data(p).name)).." "..data
return enc(data,SAVE_KEY)
end
function Encoder.DecodePlayer(p,data)
local data = Convert2Data(dec(data,SAVE_KEY))
if data[1] == math.abs(StringHash(Player2Data(p).name)) then
table.remove(data,1)
return data
else
return {}
end
end
end
do --//SaveLoad
local filePath
local SaveValues = {"GamesPlayed"}
player_load_event = Event.new()
function WriteSave()
local saveData = ""
local pdata = PlayerDataLocal
for j = 1, #SaveValues do
local val = SaveValues[j]
if pdata[val] then
saveData = saveData..math.floor(pdata[val]).." "
else
saveData = saveData.."0 "
end
end
saveData = Encoder.EncodePlayer(LocalPlayer,saveData)
FileIO.Save(filePath,saveData)
end
function AddSaveKey(strKey)
for i, pdata in ipairs(PlayerData) do
pdata[strKey] = 0
end
table.insert(SaveValues,strKey)
end
function LoadValueQueue(p,s)
local pdata = Player2Data(p)
local data = Encoder.DecodePlayer(p,s)
for i = 1, #SaveValues do
pdata[SaveValues[i]] = data[i] or 0
--print("Loaded "..SaveValues[i]..": "..pdata[SaveValues[i]])
end
pdata.GamesPlayed = pdata.GamesPlayed + 1
player_load_event:fire(pdata)
end
function OnSyncLoad()
local p = GetTriggerPlayer()
local data = BlzGetTriggerSyncData()
local pdata = Player2Data(p)
if data == "start" then
pdata.code = ""
elseif data == "end" then
LoadValueQueue(p,pdata.code)
else
pdata.code = pdata.code..data
end
end
function InitSaveLoad()
InitFileIO()
local MenuFrame = BlzGetFrameByName("UpperButtonBarMenuButton", 0)
local LastSave = 0
filePath = MAP_NAME.."\\"..PlayerDataLocal.name.."\\PlayerData.pld"
local t = CreateTrigger()
TriggerAddAction(t,OnSyncLoad)
for i = 1, #PlayerUsers do
local p = PlayerUsers[i]
BlzTriggerRegisterPlayerSyncEvent(t,p,"OnSyncLoad", false)
end
local s = FileIO.Load(filePath) or ""
BlzSendSyncData("OnSyncLoad", "start")
for i = 1, #s, 255 do
BlzSendSyncData("OnSyncLoad",s:sub(i, math.min(i + 254, #s)))
end
BlzSendSyncData("OnSyncLoad", "end")
TimerStart(CreateTimer(),1.0,true,function()
WriteSave()
end)
end
end
do
Types = {}
Types.Damageable = 1
Types.Effectable = 2
Types.PlayerUnit = 4
Types.ExpOrb = 8
Types.Collidable = 16
Types.Ouchie = 32
Types.Summon = 64
Types.ProjectileStopper = 128
Types.PushesPlayer = 256
Types.Uninfested = 512
BossTypes = {}
BossTypes.Unique = 1
BossTypes.Random = 2
BossTypes.Legendary = 4
function BossTypes.is(source,check_type)
return source.bossType and source.bossType & check_type ~= 0
end
function Types.is(source,check_type)
return source.type and source.type & check_type ~= 0
end
end
do
Destructable = {}
Destructable.__index = Destructable
local rng = RNG.new()
destructables = Set.create()
local serialized = {}
function destructables.reset()
serialized = {}
end
function Destructable:get_scale()
return self.scale
end
function Destructable:serialize()
serialized[self.world_pos] = self.health
end
function Destructable.deserialize(x,y)
x,y = WorldGeneration.cordsToWorld(x,y)
local s = string.format("%%f,%%f", x, y)
return serialized[s], s
end
function Destructable.receive_damage(self,amount)
if self.health == nil then return end
self.health = self.health - amount
if self.health <= 0.0 then
if self.drops then
local it = self.drops:pop()
if it ~= false then
Item.new(it,self.position[1],self.position[2])
end
end
if self.deathSound then
self.deathSound(GetRandomReal(0.9,1.1),true,self.position[1],self.position[2])
end
self:destroy()
end
return amount
end
function Destructable.new(data,x,y,rotation)
local serial, world_pos = Destructable.deserialize(x,y)
if serial ~= nil and serial <= 0 then return end
local self = setmetatable(NewTable(), Destructable)
self.sfx = AddSpecialEffect(data.model,x-SFX_OFFSET,y-SFX_OFFSET)
self.name = data.name
self.position = NewTable()
self.position[1] = x
self.position[2] = y
self.world_pos = world_pos
self.scale = data.scale or 1.0
self.marked = 0.0
self.square = data.square
self.health = serial or data.health
self.health_max = data.health
self.drops = data.drops
self.deathSound = data.deathSound
self.type = data.type or 0
BlzSetSpecialEffectYaw(self.sfx,rotation)
BlzSetSpecialEffectScale(self.sfx,self.scale)
BlzSetSpecialEffectZ(self.sfx,data.height or 0)
if data.size then
self.type = self.type + Types.Collidable
self.size = data.size
self.half_size = data.size * 0.5
unit_collision_hash:add(self, x - self.half_size, y - self.half_size, self.size, self.size)
end
if self.health ~= nil then
self.type = self.type + Types.Damageable
end
destructables:add(self)
return self
end
function Destructable.destroy(self,quick)
if self.health ~= nil then
self:serialize()
end
if quick or self.health == nil then
BlzSetSpecialEffectZ(self.sfx,-99999)
DestroyEffect(self.sfx)
else
DestroyEffectEx(self.sfx)
end
self.sfx = nil
ReleaseTable(self.position)
if self.size then
self.size = nil
unit_collision_hash:remove(self)
end
self.half_size = nil
self.position = nil
self.scale = nil
self.name = nil
self.world_pos = nil
self.marked = nil
self.health = nil
self.health_max = nil
self.deathSound = nil
self.type = nil
self.square = nil
self.drops = nil
self.old = nil
setmetatable(self,nil)
ReleaseTable(self)
destructables:remove(self)
end
end
do
local MAX_SPAWN = 800
local side = 1
local SpawnPos = {}
local spawn_range = 64.
UnitSpawner = {}
local SpawnTable = Set.create()
function AddWaveType(which,rate,stops_at,amount)
local self = NewTable()
self.type = which
self.amount = amount or math.huge
self.rate = rate
self.stops_at = stops_at or math.huge
self.last_spawn = 0.0
SpawnTable:add(self)
end
function RemoveWaveType(self)
SpawnTable:remove(self)
self.type = nil
self.amount = nil
self.rate = nil
self.stops_at = nil
self.last_spawn = nil
ReleaseTable(self)
end
function ResetWaveTypes()
SPAWN_TIMER:reset()
for data in SpawnTable:elements() do
RemoveWaveType(data)
end
end
local side = {}
function GetRandomSpawnPosition(x,y,x2,y2,target)
local pathing = target.pathing
for i = 1, 10 do
local t = GetRandomReal(0.0,1.0)
if x == 0 and y == 0 and i > 0 then
side[1] = GetRandomInt(1,4)
else
if x > 0 then
table.insert(side,2)
elseif x < 0 then
table.insert(side,4)
end
if y > 0 then
table.insert(side,1)
elseif y < 0 then
table.insert(side,3)
end
end
local v = side[GetRandomInt(1,#side)]
side[1] = nil
side[2] = nil
local rx, ry
if v == 1 then -- top
rx,ry = lerp(SpawnPos.ax, SpawnPos.bx,t), lerp(SpawnPos.ay, SpawnPos.by,t) + GetRandomReal(-spawn_range,spawn_range)
elseif v == 2 then -- right
rx,ry = lerp(SpawnPos.bx, SpawnPos.cx,t) + GetRandomReal(-spawn_range,spawn_range), lerp(SpawnPos.by, SpawnPos.cy,t)
elseif v == 3 then -- bottom
rx, ry = lerp(SpawnPos.cx, SpawnPos.dx,t), lerp(SpawnPos.cy, SpawnPos.dy,t) + GetRandomReal(-spawn_range,spawn_range)
else -- left
rx, ry = lerp(SpawnPos.dx, SpawnPos.ax,t) + GetRandomReal(-spawn_range,spawn_range), lerp(SpawnPos.dy, SpawnPos.ay,t)
end
rx,ry = PlayableBounds.ClampCords(rx,ry)
if pathing:cell_valid(rx + x2,ry+y2) and not unit_collision_hash:cell_occupied(rx + x2, ry+y2,0,0,Types.Collidable + Types.Ouchie) then
return rx,ry
end
end
end
local function spawnTime()
local target = playerunits:random()
if target == nil then return end
local pos = target.position
local x2,y2 = pos[1],pos[2]
for data in SpawnTable:elements() do
if units:size() < target.pathing.open_cells*0.18 and data.last_spawn <= GAME_TIME then
data.amount = data.amount - 1
data.last_spawn = data.rate + GAME_TIME
local x1,y1 = GetRandomSpawnPosition(target.move_dir[1],target.move_dir[2],x2,y2,target)
if x1 == nil then goto continue end
x1,y1 = x2+x1, y2+y1
local u = Unit.new(data.type,x1,y1,math.atan(y2-y1,x2-x1))
u:set_target(target)
if data.amount <= 0 or data.stops_at <= GAME_TIME then
RemoveWaveType(data)
end
::continue::
end
end
end
function UnitSpawner.Init()
table.sort(Camera)
for key in pairs(Camera) do
if string.len(key) == 2 then
Camera[key] = Camera[key] * 1.02
SpawnPos[key] = Camera[key] * 0.95
end
end
DEFAULT_TIMER:callPeriodically(0.01, nil, spawnTime)
end
end
do
unit_collision_hash = shash.new(196)
units = Set.create()
Unit = {}
Unit.__index = Unit
local destroy_queue = {}
local remove_queue = {}
local mathabs = math.abs
Unit.kill_event = Event.new()
Unit.spawn_event = Event.new()
local lastRandomBoss = 0
function Unit.reset_random_boss_timer()
lastRandomBoss = GAME_TIME + GetRandomReal(120.,240.)
end
local default_type = Types.Damageable + Types.Effectable + Types.Ouchie + Types.Uninfested
function Unit.new(data,x,y,rotation)
local self = setmetatable(NewTable(), Unit)
self.sfx = AddSpecialEffect(data.model,x-SFX_OFFSET,y-SFX_OFFSET)
self.type = default_type
self.shadow = AddShadowedEffect(x, y, UNIT_SHADOW_PATH, data.size*2, data.size*2, 0, 0)
self.target = false
self.death_event = Event.new()
self.damage_event = Event.new()
self.position = NewTable()
self.push = NewTable()
self.push[1] = 0.0
self.push[2] = 0.0
self.position[1] = x
self.position[2] = y
self.position[3] = 0
self.marked = 0.0
self.name = data.name
self.scale = data.scale
self.size = data.size
self.half_size = data.size * 0.5
self.speed = data.speed
self.collision_enabled = data.collision_enabled or true
if rotation then
self.yaw = rotation
elseif data.facing then
self.yaw = data.facing
else
self.yaw = GetRandomReal(-PI,PI)
end
self.turn_speed = data.turn_speed
self.scale_mult = 1.0
self.anim = ANIM_TYPE_STAND
self.exp = data.exp
self.health = data.health
self.colliding = 0
self.slow_mult = 1.0
self.knockbackResist = data.knockbackResist or 1.0
self.damage = data.damage
self.effects_sfx = NewTable()
self.effects_dur = NewTable()
self.effects_stacks = NewTable()
self.spawn_time = GAME_TIME
self.bossType = data.bossType
self.angle_move_func = data.angle_move_func or unit_angle_move_normal
self.speed_move_func = data.speed_move_func or unit_speed_move_normal
self.damage_func = data.damage_func
self.death_func = data.death_func
BlzSetSpecialEffectScale(self.sfx,self.scale)
BlzSetSpecialEffectYaw(self.sfx,self.yaw)
BlzSetSpecialEffectColorByPlayer(self.sfx,PASSIVE_PLAYER)
unit_collision_hash:add(self, x - self.half_size, y - self.half_size, self.size, self.size)
units:add(self)
self.bossType = self.bossType or (lastRandomBoss < GAME_TIME and BossTypes.Random or 0)
if BossTypes.is(self,BossTypes.Unique+BossTypes.Random) then
if BossTypes.is(self,BossTypes.Random) then
Unit.reset_random_boss_timer()
self:set_scale(1.6)
self.outlinesfx = AddSpecialEffect(data.model,x-SFX_OFFSET,y-SFX_OFFSET)
self.outline_offset = 0
self.name = "|cff00c8ff" .. self.name .. "|r"
BlzSetSpecialEffectScale(self.outlinesfx,(self.scale + 0.1)*self.scale_mult)
BlzSetSpecialEffectYaw(self.outlinesfx,self.yaw)
BlzSetSpecialEffectColorByPlayer(self.outlinesfx,PASSIVE_PLAYER)
BlzSetSpecialEffectAlpha(self.outlinesfx,100)
BlzSetSpecialEffectColor(self.outlinesfx,0,200,255)
self.speed = self.speed * 1.3
self.turn_speed = self.turn_speed * 1.3
self.health = self.health * 50
self.damage = self.damage * 3
self.exp = self.exp * 15
if not data.attack then
local i = GetRandomInt(1,3)
if i == 1 then
self.attack = UnitAttack.new(unit_attack_tri_skulls,self)
elseif i == 2 then
self.attack = UnitAttack.new(unit_attack_homing_skulls,self)
else
self.attack = UnitAttack.new(unit_attack_jump,self)
end
end
SOUND_UI_BOSS_RANDOM()
else
SOUND_UI_BOSS_UNIQUE()
self.name = "|cffffcc00" .. self.name .. "|r"
end
end
self.health_max = self.health
if data.outline then
self.outlinesfx = AddSpecialEffect(data.model,x-SFX_OFFSET,y-SFX_OFFSET)
self.outline_offset = data.outline_offset or 0
BlzSetSpecialEffectScale(self.outlinesfx,(self.scale + (data.outline_scale or 0.05))*self.scale_mult)
BlzSetSpecialEffectYaw(self.outlinesfx,self.yaw)
BlzSetSpecialEffectColorByPlayer(self.outlinesfx,PASSIVE_PLAYER)
BlzSetSpecialEffectAlpha(self.outlinesfx,data.outline_alpha or 150)
BlzSetSpecialEffectColor(self.outlinesfx,data.outline_r or 255,data.outline_b or 255,data.outline_b or 255)
end
if data.attack then
self.attack = UnitAttack.new(data.attack,self)
end
if data.on_create_func then
data.on_create_func(self)
end
Unit.spawn_event:fire(self)
return self
end
function Unit:subanimation(anim,add)
if add then
BlzSpecialEffectAddSubAnimation(self.sfx,anim)
if self.outlinesfx then
BlzSpecialEffectAddSubAnimation(self.outlinesfx,anim)
end
else
BlzSpecialEffectRemoveSubAnimation(self.sfx,anim)
if self.outlinesfx then
BlzSpecialEffectRemoveSubAnimation(self.outlinesfx,anim)
end
end
end
function Unit.move(self,x,y)
self.position[1] = x
self.position[2] = y
local z = self.position[3]
x,y = x-SFX_OFFSET,y-SFX_OFFSET
SetShadowedEffectPosition(self.shadow, x, y, z)
BlzSetSpecialEffectPosition(self.sfx,x,y,z)
unit_collision_hash:update(self,x-self.half_size,y-self.half_size)
if self.outlinesfx then
BlzSetSpecialEffectPosition(self.outlinesfx,x,y+self.outline_offset,z)
end
end
function Unit.set_facing(self,yaw)
self.yaw = yaw
BlzSetSpecialEffectYaw(self.sfx,yaw)
if self.outlinesfx then
BlzSetSpecialEffectYaw(self.outlinesfx,yaw)
end
end
function Unit.set_scale(self,scale)
self.scale_mult = scale
self.half_size = self.size * scale * 0.5
BlzSetSpecialEffectScale(self.sfx,self.scale * scale)
if self.outlinesfx then
BlzSetSpecialEffectScale(self.outlinesfx,self.scale*scale)
end
unit_collision_hash:update(self,self.position[1] - self.half_size*scale,self.position[2]-self.half_size*scale,self.size*scale,self.size*scale)
end
function Unit:get_scale()
return self.scale_mult * self.scale
end
function Unit.set_target(self,vec2)
self.target = vec2
end
function Unit.receive_damage(self,amount,unblockable)
if self.damage_func then
amount = self.damage_func(self,amount,unblockable)
end
amount = amount / self.scale_mult
self.health = self.health - amount
if self.health <= 0.0 then
self:trigger_frost()
if BossTypes.is(self,BossTypes.Unique) then
Item.new(item_ability_scroll,self.position[1],self.position[2])
end
if BossTypes.is(self,BossTypes.Random) then
Item.new(item_equipment_chest,self.position[1], self.position[2])
end
Unit.kill_event:fire(self)
if self.death_func then
self:death_func()
end
self.death_event:fire(self)
self:destroy()
elseif self.healthbar then
self.damage_event:fire(self,amount)
BlzSetSpecialEffectTime(self.healthbar,1.0 - self.health/self.health_max)
end
return amount
end
function Unit.destroy(self)
if self.remove_func then
self.remove_func(self)
self.remove_func = nil
end
if self.bossType == 0 then
BlzSetSpecialEffectTimeScale(self.sfx,4.0)
DEFAULT_TIMER:callDelayed(1.0, BlzPlaySpecialEffect,self.sfx,ANIM_TYPE_DECAY)
else
DEFAULT_TIMER:callDelayed(4.0, BlzPlaySpecialEffect,self.sfx,ANIM_TYPE_DECAY)
end
if self.exp ~= nil then
Orb.new(self.position[1],self.position[2],(self.exp + GAME_TIME*0.001)*self.scale_mult)
self.exp = nil
end
self.death_event:destroy()
self.damage_event:destroy()
self.death_event = nil
self.damage_event = nil
self.health_max = nil
self.name = nil
DestroyShadowedEffect(self.shadow)
self.shadow = nil
self.target = nil
DestroyEffectEx(self.sfx)
DestroyEffectEx(self.outlinesfx)
self.outlinesfx = nil
self.sfx = nil
ReleaseTable(self.position)
ReleaseTable(self.push)
self.push = nil
self.outline_offset = nil
self.collision_enabled = nil
self.size = nil
self.damage_func = nil
self.half_size = nil
self.speed = nil
self.anim = nil
self.marked = nil
self.yaw = nil
self.health = nil
self.scale = nil
self.bossType= nil
self.slow_mult = nil
self.type = nil
self.angle_move_func = nil
self.speed_move_func = nil
self.death_func = nil
self.colliding = nil
self.disabled_movement = nil
self.spawn_time = nil
self.turn_speed = nil
self.knockbackResist = nil
self.scale_mult = nil
self.damage = nil
self.position = nil
self.effect_source= nil
for i = 1, MAX_EFFECTS, 1 do
DestroyEffect(self.effects_sfx[i])
self.effects_sfx[i] = nil
self.effects_dur[i] = nil
self.effects_stacks[i] = nil
end
ReleaseTable(self.effects_sfx)
ReleaseTable(self.effects_dur)
ReleaseTable(self.effects_stacks)
if self.attack then
self.attack:destroy()
self.attack = nil
end
self.effects_sfx = nil
self.effects_dur = nil
self.effects_stacks = nil
self.stunned = nil
units:remove(self)
setmetatable(self,nil)
table.insert(destroy_queue,self)
end
function Unit.resolve_animation(self,moving)
local next
if self.attack ~= nil and self.attack:is_attacking() then
next = ANIM_TYPE_ATTACK
elseif self.target == false or not moving then
next = ANIM_TYPE_STAND
else
next = ANIM_TYPE_WALK
end
if next ~= self.anim then
self.anim = next
local outline = self.outlinesfx
local sfx = self.sfx
local tscale = 1.0
local t = 0
if next == ANIM_TYPE_WALK then
local t = GetRandomReal(0,1)
elseif next == ANIM_TYPE_ATTACK then
tscale = 1.0/self.attack.attack_time
end
local clock = os.clock()
BlzPlaySpecialEffect(sfx,next)
BlzSetSpecialEffectTime(sfx,t)
BlzSetSpecialEffectTimeScale(sfx,tscale)
if outline then
BlzPlaySpecialEffect(outline,next)
BlzSetSpecialEffectTime(outline,t)
BlzSetSpecialEffectTimeScale(outline,tscale)
end
end
end
local unitCopy = Set.create()
local function check_bounds()
local inside = false
if unitCopy:size() == 0 then
unitCopy:destroy()
unitCopy = units:copy()
end
local u = unitCopy:popBack()
if not u or not u.position or u.marked > GAME_TIME or getmetatable(u.target) ~= PlayerUnit then return end
local target = u.target
local x,y = target.position[1], target.position[2]
local px = u.position[1] - x
local py = u.position[2] - y
if not isPointInTrapezium(px,py, Camera.ax, Camera.ay, Camera.bx, Camera.by, Camera.cx, Camera.cy, Camera.dx, Camera.dy) then
px,py = GetRandomSpawnPosition(target.move_dir[1],target.move_dir[2],x,y,target)
if px == nil then goto continue end
px,py = x+px,y+py
u:set_facing(math.atan(y-py,x-px))
u:move(px,py)
u.push[1] = 0
u.push[2] = 0
::continue::
end
end
local sampleSize = 0
local sampleTime = 0
local function collisions()
local i = 1
while i <= #remove_queue do
local u = remove_queue[i]
if u.time < GAME_TIME then
remove_queue[i] = remove_queue[#remove_queue]
remove_queue[#remove_queue] = nil
u.time = nil
ReleaseTable(u)
else
i = i + 1
end
end
for u in units:elements() do
if u.type == nil then goto next end
local e = unit_collision_hash.entities[u]
local cur_x, cur_y = u.position[1],u.position[2]
local scale_mult = u.scale_mult
local half_size = u.half_size
local enabled = u.collision_enabled
local tbl = unit_collision_hash.tablepool
local s = #tbl
local set = tbl[s] or {}
tbl[s] = nil
s = s + 1
local keyset = tbl[s] or {}
local keypos = 0
tbl[s] = nil
local cellsize = unit_collision_hash.cellsize
local d1 = e[1]
local d2 = e[2]
local d3 = e[3]
local d4 = e[4]
local sx, sy = d1 // cellsize, d2 // cellsize
local ex, ey = d3 // cellsize, d4 // cellsize
local speed = u.speed
local cells = unit_collision_hash.cells
for y = sy, ey do
for x = sx, ex do
local idx = x + y * 1e7
local t = cells[idx]
local size = t and #t or 0
for i = 1, size do
local v = t[i]
local obj = v[5]
if e ~= v and obj.type and not set[v] and d3 > v[1] and d1 < v[3] and d4 > v[2] and d2 < v[4] then
set[v] = true
keypos = keypos + 1
keyset[keypos] = v
local obj_half_size = obj.half_size
local x1,y1 = obj.position[1],obj.position[2]
local collidable = obj.type & Types.Collidable > 0
if not obj.ignoreCollision and (obj.type & Types.Ouchie == 0 or half_size <= obj_half_size + 0.5) and (not collidable or obj.square or IsInRange(x1, y1, cur_x, cur_y, obj_half_size + scale_mult * half_size)) then
u.colliding = u.colliding + (collidable and 36 or 0)
x1, y1 = VectorAngle(cur_x, cur_y, x1, y1, half_size, obj_half_size)
u.push[1] = u.push[1] + (collidable and x1 * speed * 6 or enabled and x1 * speed * 3 or 0)
u.push[2] = u.push[2] + (collidable and y1 * speed * 6 or enabled and y1 * speed * 3 or 0)
end
end
end
end
end
u.colliding = math.min(100,u.colliding)
s = #tbl
for i = 1, keypos do
local v = keyset[i]
set[v] = nil
keyset[i] = nil
end
tbl[s+1] = set
tbl[s+2] = keyset
::next::
end
end
function table.queueFree(tbl)
table.insert(remove_queue,tbl)
tbl.time = GAME_TIME + 15.0
end
local function update()
for i = 1, #destroy_queue, 1 do
local u = destroy_queue[i]
unit_collision_hash:remove(u)
destroy_queue[i] = nil
table.queueFree(u)
end
for u in units:elements() do
if not u.disabled_movement then
local cur_x = u.position[1]
local cur_y = u.position[2]
local push = u.push
push[1] = push[1] * 0.65
push[2] = push[2] * 0.65
local x,y = 0,0
if getmetatable(u.target) == PlayerUnit and not u.stunned then
local move_rot,target_rot = u.target.pathing:get_dir(cur_x,cur_y)
local next_rot = u.angle_move_func(u,move_rot,target_rot)
local yaw
if u.attack == nil then
yaw = change_angle_bounded(u.yaw,next_rot,u.turn_speed)
x,y = math.cos(yaw), math.sin(yaw)
elseif not u.attack:is_attacking() and not u.attack:try_attack() then
yaw = change_angle_bounded(u.yaw,next_rot,u.turn_speed)
x,y = math.cos(yaw), math.sin(yaw)
else
yaw = change_angle_bounded(u.yaw,target_rot,u.turn_speed)
end
u:set_facing(yaw)
end
u.colliding = u.colliding > 0 and u.colliding - 1 or 0
if mathabs(x + push[1]) > 0.001 or mathabs(y + push[2]) > 0.001 then
local speed = u.speed_move_func(u,(u.speed * u.scale_mult)*u.slow_mult)
x,y = VectorNormalize(x * speed + push[1],y * speed + push[2])
u:move(cur_x+x * speed,cur_y+y * speed)
u:resolve_animation(speed > 0)
else
u:resolve_animation(false)
end
end
end
end
local function push_iter(obj,cur_x,cur_y,strength,range)
if getmetatable(obj) == Unit then
strength = strength/obj.knockbackResist
local x,y = obj.position[1],obj.position[2]
if IsInRange(cur_x,cur_y,x,y,range*0.5)then
local push = obj.push
x,y = VectorAngle(x,y,cur_x,cur_y)
push[1] = push[1] + x * strength
push[2] = push[2] + y * strength
end
end
end
function Unit:knockback(dir_x,dir_y,strength)
strength = strength/self.knockbackResist
local push = self.push
push[1] = push[1] + dir_x * strength
push[2] = push[2] + dir_y * strength
end
function Unit.push_away_from_point(area,x,y,strength)
local half = area * 0.5
unit_collision_hash:each(x-half,y-half,area,area, push_iter,x,y,strength,area)
end
function Unit.Init()
DEFAULT_TIMER:callPeriodically(DEFAULT_TIMEOUT*4, nil, collisions)
DEFAULT_TIMER:callPeriodically(DEFAULT_TIMEOUT, nil, update)
DEFAULT_TIMER:callPeriodically(0.01, nil, check_bounds)
end
end
do
UnitAttack = {}
UnitAttack.__index = UnitAttack
function UnitAttack.new(data,owner,ability)
local self = setmetatable(NewTable(),UnitAttack)
self.range = data.range
self.in_range_function = data.in_range_function
self.begin_function = data.begin_function
self.cooldown = data.cooldown
self.attack_time = data.attack_time or 1.0
self.attacking = 0.0
self.delay = data.delay or 0.0
self.cur_cooldown = 0.0
self.owner = owner
self.damage = data.damage
self.ability = ability
return self
end
function UnitAttack:get_attack_time()
local abil = self.ability
if abil then
return self.attack_time / (abil.owner.attackSpeedMult + abil.attackSpeedMult + abil.owner.summonAttackSpeedMult)
else
return self.attack_time
end
end
function UnitAttack:get_cooldown()
local abil = self.ability
if abil then
return self.cooldown / (abil.owner.attackSpeedMult + abil.attackSpeedMult + abil.owner.summonAttackSpeedMult)
else
return self.cooldown
end
end
function UnitAttack:get_range()
local abil = self.ability
if abil then
return self.range * (abil.owner.rangeMult + abil.rangeMult)
else
return self.range
end
end
function UnitAttack.launch(self)
if self.resetting then
self.resetting = false
return
end
local owner = self.owner
if getmetatable(self) ~= UnitAttack or owner.type == nil or owner.target.position == nil then return end
local count = 1
if self.ability then
count = self.ability:increment_count()
end
self.cur_cooldown = GAME_TIME + self:get_cooldown()
self.in_range_function(self,count)
end
function UnitAttack:try_attack()
local o = self.owner
local x1,y1 = o.target.position[1],o.target.position[2]
local x2,y2 = o.position[1],o.position[2]
if self.cur_cooldown < GAME_TIME and IsInRange(x1,y1,x2,y2,self:get_range()) then
self.resetting = false
local atk_time = self:get_attack_time()
self.attacking = GAME_TIME + atk_time
DEFAULT_TIMER:callDelayed(self.delay * atk_time, UnitAttack.launch,self)
if self.begin_function then
self:begin_function(o)
end
return true
end
return false
end
function UnitAttack:reset()
self.attacking = 0
self.resetting = true
end
function UnitAttack:is_attacking()
return self.attacking > GAME_TIME
end
function UnitAttack:destroy()
self.range = nil
self.in_range_function = nil
self.cooldown = nil
self.delay = nil
self.cur_cooldown = nil
self.owner = nil
self.attacking = nil
self.begin_function = nil
self.damage = nil
self.attack_time = nil
self.resetting = nil
self.ability = nil
setmetatable(self,nil)
ReleaseTable(self)
end
end
do
orb_collision_hash = shash.new(64)
Orb = {}
Orb.__index = Orb
local DEFAULT_SCALE = 0.15
local DEFAULT_HEIGHT = 32.0
local DEFAULT_SPEED = 11.5
captured_orbs = Set.create()
local destroy_queue = {}
local mathmin = math.min
local mathmax = math.max
local mathlog = math.log
local mathceil = math.ceil
all_exp = Set.create()
local orb_models = {
"ExpOrb1", "ExpOrb2", "ExpOrb3", "ExpOrb4", "ExpOrb5", "ExpOrb6",
"ExpOrb7", "ExpOrb8", "ExpOrb9", "ExpOrb10"
}
local function get_orb_model(exp_amount)
local val = mathmin(#orb_models,mathceil(mathlog(mathmax(exp_amount,1.0)+0.0001,2.0)))
return orb_models[val]
end
function Orb.move(self,x,y)
orb_collision_hash:update(self,x-16,y-16,32,32)
self.position[1] = x
self.position[2] = y
BlzSetSpecialEffectPosition(self.sfx,x-SFX_OFFSET,y-SFX_OFFSET,DEFAULT_HEIGHT)
end
function Orb.new(x,y,exp_amount)
local self = setmetatable(NewTable(), Orb)
self.sfx = AddSpecialEffect(get_orb_model(exp_amount),x-SFX_OFFSET,y-SFX_OFFSET)
self.xp = exp_amount
self.position = NewTable()
self.position[1] = x
self.position[2] = y
self.type = Types.ExpOrb
orb_collision_hash:add(self, x - 16, y - 16, 32, 32)
BlzSetSpecialEffectScale(self.sfx,DEFAULT_SCALE)
BlzSetSpecialEffectZ(self.sfx,DEFAULT_HEIGHT)
all_exp:add(self)
return self
end
function Orb.mark_captured(self,target)
if self.start == nil then
all_exp:remove(self)
table.insert(destroy_queue,self)
captured_orbs:add(self)
self.start = NewTable()
self.start[1] = self.position[1]
self.start[2] = self.position[2]
self.target = target
self.time = 0.0
local d = DistanceTo(self.position[1],self.position[2],target.position[1],target.position[2])
self.target_time = d/(DEFAULT_SPEED / DEFAULT_TIMEOUT)
end
end
function Orb.destroy(self)
if self.start ~= nil then
ReleaseTable(self.start)
self.start = nil
self.target = nil
end
DestroyEffect(self.sfx)
ReleaseTable(self.position)
self.position = nil
self.sfx = nil
self.xp = nil
self.type = nil
self.time = nil
self.target_time = nil
ReleaseTable(self)
setmetatable(self,nil)
captured_orbs:remove(self)
all_exp:remove(self)
end
local function pick_captured()
for i = 1, #destroy_queue, 1 do
local orb = destroy_queue[i]
orb_collision_hash:remove(orb)
destroy_queue[i] = nil
end
for orb in captured_orbs:elements() do
if getmetatable(orb.target) == PlayerUnit then
orb.time = mathmin(orb.time + DEFAULT_TIMEOUT,orb.target_time)
local t = orb.time/orb.target_time
local t2 = easeInBack(t)
local x = lerp(orb.start[1],orb.target.position[1],t2)
local y = lerp(orb.start[2],orb.target.position[2],t2)
local z = ParabolicArc(DEFAULT_HEIGHT,256, t)
orb.position[1] = x
orb.position[2] = y
BlzSetSpecialEffectPosition(orb.sfx,x-SFX_OFFSET,y-SFX_OFFSET,z)
if orb.time == orb.target_time then
orb.target:gain_exp(orb.xp)
orb:destroy()
end
else
orb:destroy()
end
end
end
function Orb.Init()
DEFAULT_TIMER:callPeriodically(DEFAULT_TIMEOUT, nil, pick_captured)
end
end
do
summons = Set.create()
Summon = {}
Summon.__index = Summon
local destroy_queue = {}
local mathabs = math.abs
local atan = math.atan
Summon.kill_event = Event.new()
function Summon.new(data,x,y,owner,ability)
local self = setmetatable(NewTable(), Summon)
self.sfx = AddSpecialEffect(data.model,x-SFX_OFFSET,y-SFX_OFFSET)
self.shadow = AddShadowedEffect(x, y, UNIT_SHADOW_PATH, data.size*2, data.size*2, 0, 0)
self.type = Types.Summon
self.position = NewTable()
self.push = NewTable()
self.push[1] = 0.0
self.push[2] = 0.0
self.position[1] = x
self.position[2] = y
self.wall = data.wall
self.size = data.size
self.half_size = data.size * 0.5
self.speed = data.speed
self.yaw = GetRandomReal(-PI,PI)
self.turn_speed = data.turn_speed
self.anim = ANIM_TYPE_STAND
self.health = data.health
self.colliding = 0
self.owner = owner
self.moveSpeedMult = data.moveSpeedMult or 1.0
if ability then
ability:update_summoned(1)
self.ability = ability
end
self.damage = data.damage
self.spawn_time = GAME_TIME
self.angle_move_func = data.angle_move_func or unit_angle_move_normal
self.speed_move_func = data.speed_move_func or unit_speed_move_normal
self.path_pos = 0
self.dmg_cd = 0
self.pathtargetX = 0
self.pathtargetY = 0
self.target = owner
self.targetTime = 0
self.touchAttackCD = 0
self.curSpeed = 0
self.timedLife = data.timedLife and GAME_TIME + data.timedLife or math.huge
self.canMove = true
self.canAnimate = true
self.attack_on_touch = data.attack_on_touch
self.ignoreCollision = data.ignoreCollision
BlzSetSpecialEffectScale(self.sfx,data.scale)
BlzSetSpecialEffectYaw(self.sfx,self.yaw)
BlzSetSpecialEffectColorByPlayer(self.sfx,PASSIVE_PLAYER)
unit_collision_hash:add(self, x - self.half_size, y - self.half_size, self.size, self.size)
summons:add(self)
if data.attack then
self.attack = UnitAttack.new(data.attack,self,ability)
end
return self
end
function Summon.move(self,x,y)
self.position[1] = x
self.position[2] = y
BlzSetSpecialEffectPosition(self.sfx,x-SFX_OFFSET,y-SFX_OFFSET,0)
SetShadowedEffectPosition(self.shadow, x, y, 0)
unit_collision_hash:update(self,x-self.half_size,y-self.half_size)
end
function Summon.receive_damage(self,amount)
self.health = self.health - amount
if self.health <= 0.0 then
self:destroy()
end
end
function Summon.destroy(self)
Summon.kill_event:fire(self)
self.target = nil
DestroyEffectEx(self.sfx)
self.sfx = nil
ReleaseTable(self.position)
ReleaseTable(self.push)
self.push = nil
self.curSpeed = nil
self.size = nil
self.half_size = nil
self.speed = nil
self.anim = nil
self.canMove = nil
self.yaw = nil
self.health = nil
DestroyShadowedEffect(self.shadow)
self.shadow = nil
self.type = nil
self.attack_on_touch = nil
self.dmg_cd = nil
self.touchAttackCD = nil
self.angle_move_func = nil
self.targetTime = nil
self.speed_move_func = nil
self.moveSpeedMult = nil
self.colliding = nil
self.timedLife = nil
self.spawn_time = nil
self.turn_speed = nil
self.attacks_before_death = nil
self.damage = nil
self.position = nil
self.owner = nil
self.pathtargetX = nil
self.wall = nil
self.pathtargetY = nil
self.getTargetFlag = nil
self.canAnimate = nil
if self.ability then
self.ability:update_summoned(-1)
self.ability = nil
end
self.ignoreCollision = nil
if self.path then
self.path:Release()
self.path = nil
end
self.path_pos = nil
if self.attack then
self.attack:destroy()
self.attack = nil
end
summons:remove(self)
setmetatable(self,nil)
table.insert(destroy_queue,self)
end
function Summon.resolve_animation(self,moving)
if not self.canAnimate then return end
local next
if self.attack ~= nil and self.attack:is_attacking() then
next = ANIM_TYPE_ATTACK
elseif self.target == false or not moving then
next = ANIM_TYPE_STAND
else
next = ANIM_TYPE_WALK
end
if next ~= self.anim then
self.anim = next
BlzPlaySpecialEffect(self.sfx,next)
if next == ANIM_TYPE_WALK then
BlzSetSpecialEffectTime(self.sfx,GetRandomReal(0,1))
BlzSetSpecialEffectTimeScale(self.sfx,1.0)
elseif next == ANIM_TYPE_ATTACK then
BlzSetSpecialEffectTimeScale(self.sfx,1.0/self.attack:get_attack_time())
else
BlzSetSpecialEffectTimeScale(self.sfx,1.0)
end
end
end
local dir_tbl = {0,0,0}
local function collision_iter(obj,cur_x,cur_y,u,push, speed)
if u.type == nil or obj.type == nil then return end
if not Types.is(obj,Types.Ouchie) then
local x,y = obj.position[1],obj.position[2]
if (Types.is(obj,Types.Collidable) and not obj.square and not IsInRange(x,y,cur_x,cur_y,obj.half_size)) or obj.ignoreCollision then
return
else
x,y = VectorAngle(cur_x,cur_y,x,y)
if Types.is(obj,Types.Collidable) then
push[1] = push[1] + x * speed
push[2] = push[2] + y * speed
u.colliding = u.colliding + 24
else
push[1] = push[1] + x * speed
push[2] = push[2] + y * speed
end
end
elseif u.dmg_cd < GAME_TIME and Types.is(obj,Types.Ouchie) then
u.dmg_cd = GAME_TIME + 0.1
u:receive_damage(obj.damage)
end
if u.type and Types.is(obj,Types.Damageable) and u.attack_on_touch then
local abil = u.ability
if u.touchAttackCD < GAME_TIME then
u.touchAttackCD = GAME_TIME + abil:get_attack_speed()/abil:get_attack_count()
abil:deal_damage(obj)
if u.attacks_before_death then
u.attacks_before_death = u.attacks_before_death - 1
if u.attacks_before_death <= 0 then
u.attacks_before_death = nil
u:destroy()
end
end
end
end
end
local function Cord2Cell(x,y)
return Vector((x//128) * 128, (y//128) * 128)
end
local function IsCellValidEmpty(vec2)
return not unit_collision_hash:cell_occupied(vec2.x- 64, vec2.y - 64, 128, 128,Types.Collidable)
end
local function collisions()
for u in summons:elements() do
unit_collision_hash:each(u, collision_iter,u.position[1],u.position[2],u,u.push,math.max(u.curSpeed,4))
end
end
Summon.getTargetFlag = Types.Damageable
function Summon:force_path_check()
local x2,y2 = self.owner.position[1], self.owner.position[2]
local x = self.position[1]
local y = self.position[2]
local px = x-x2
local py = y-y2
if not isPointInTrapezium(px,py, Camera.ax, Camera.ay, Camera.bx, Camera.by, Camera.cx, Camera.cy, Camera.dx, Camera.dy) then
local px,py = GetRandomSpawnPosition(self.owner.move_dir[1],self.owner.move_dir[2],x2,y2,self.owner)
if px ~= nil then
x,y = x2+px,y2+py
self.yaw = math.atan(y2-py,x2-px)
self:move(x,y)
self.push[1] = 0
self.push[2] = 0
end
end
if not Types.is(self.target,self.getTargetFlag) or self.targetTime < GAME_TIME then
local closest_unit = unit_collision_hash:get_first(x - 512, y - 512, 1024, 1024,self.getTargetFlag)
if not closest_unit then
closest_unit = self.owner
end
self.target = closest_unit
self.pathtargetX = 133.7
self.pathtargetY = 133.7
self.targetTime = GAME_TIME + 1.0
end
if self.speed > 0 then
local end_pos = Cord2Cell(self.target.position[1],self.target.position[2])
if end_pos.x ~= self.pathtargetX or end_pos.y ~= self.pathtargetY then
self.pathtargetX = end_pos.x
self.pathtargetY = end_pos.y
self.path_pos = 1
ReleaseVector(end_pos)
end_pos = Cord2Cell(GetRandomLocationInRange(self.target.position[1],self.target.position[2],256,128,Types.Collidable))
if self.path then
self.path:Release()
end
local start_pos = Cord2Cell(GetRandomLocationInRange(x,y,256,128,Types.Collidable))
self.path = Astar( start_pos, end_pos, IsCellValidEmpty,50)
if self.path and (self.path.Path == nil or #self.path.Path==0) then
self.path:Release()
self.path = nil
end
ReleaseVector(start_pos)
end
ReleaseVector(end_pos)
end
end
local function check_targets()
for u in summons:elements() do
if u.timedLife < GAME_TIME then
u:destroy()
goto next
end
u:force_path_check()
::next::
end
end
function Summon:get_dir(x,y,speed)
local pos = self.target.position
local dir = atan(pos[2]-y,pos[1]-x)
if self.path ~= nil and self.path.Path ~= nil then
local path = self.path.Path
local pos2 = path[self.path_pos]
if IsInRange(x,y,pos2.x,pos2.y,speed) then
self.path_pos = self.path_pos + 1
if self.path_pos > #path then
self.path:Release()
self.path = nil
return dir,dir
end
end
pos2 = path[self.path_pos]
return atan(pos2.y-y,pos2.x-x),dir
else
return dir, dir
end
end
local function update()
for i = 1, #destroy_queue, 1 do
local u = destroy_queue[i]
unit_collision_hash:remove(u)
table.queueFree(u)
destroy_queue[i] = nil
end
for u in summons:elements() do
local cur_x = u.position[1]
local cur_y = u.position[2]
local push = u.push
push[1] = push[1] * 0.6
push[2] = push[2] * 0.6
local x,y = push[1], push[2]
u.curSpeed = u.speed_move_func(u,u.speed * (u.moveSpeedMult + u.owner.summonMoveSpeedMult))
if u.target and u.target.type ~= nil and u.canMove then
local move_rot,target_rot = u:get_dir(cur_x,cur_y,u.curSpeed)
local next_rot = u.angle_move_func(u,move_rot,target_rot)
local yaw
if u.attack == nil or u.target == u.owner then
yaw = change_angle_bounded(u.yaw,next_rot,u.turn_speed)
x,y = math.cos(yaw) + x, math.sin(yaw) + y
elseif not u.attack:is_attacking() and not u.attack:try_attack() then
yaw = change_angle_bounded(u.yaw,next_rot,u.turn_speed)
x,y = math.cos(yaw) + x, math.sin(yaw) + y
else
yaw = change_angle_bounded(u.yaw,target_rot,u.turn_speed)
end
u.yaw = yaw
BlzSetSpecialEffectYaw(u.sfx,yaw)
end
u.colliding = math.max(u.colliding - 1,0)
if u.type ~= nil then
if not u.wall and (mathabs(x + push[1]) > 0.001 or mathabs(y + push[2]) > 0.001) then
x,y = VectorNormalize(x,y)
x,y = x * u.curSpeed + push[1], y * u.curSpeed + push[2]
u:move(cur_x+x,cur_y+y)
u:resolve_animation(u.curSpeed > 0)
else
u:resolve_animation(false)
end
end
end
end
function Summon.Init()
DEFAULT_TIMER:callPeriodically(DEFAULT_TIMEOUT*4, nil, collisions)
DEFAULT_TIMER:callPeriodically(DEFAULT_TIMEOUT, nil, update)
DEFAULT_TIMER:callPeriodically(0.15, nil, check_targets)
end
end
do
Mouse = {}
Mouse.press_event = Event.new()
Mouse.move_event = Event.new()
function Mouse.move()
local p = GetTriggerPlayer()
local x = BlzGetTriggerPlayerMouseX()
local y = BlzGetTriggerPlayerMouseY()
if x == 0.0 and y == 0.0 then return end
local pdata = Player2Data(p)
pdata.mouse_pos[1] = x
pdata.mouse_pos[2] = y
pdata.mouse_screen_pos[1], pdata.mouse_screen_pos[2] = World2Screen( x, y, 0 )
Mouse.move_event:fire(pdata,x,y)
if pdata.playerUnit ~= nil then
pdata.mouse_angle = math.atan(pdata.mouse_pos[2]-pdata.playerUnit.position[2],pdata.mouse_pos[1]-pdata.playerUnit.position[1])
end
end
function Mouse.click_evaluate(pdata,state)
local but = BlzGetTriggerPlayerMouseButton()
local pdata = Player2Data(GetTriggerPlayer())
pdata[but] = state
if but == MOUSE_BUTTON_TYPE_LEFT then
Mouse.press_event:fire(pdata,state)
end
end
function Mouse.press()
Mouse.click_evaluate(Player2Data(GetTriggerPlayer()),true)
end
function Mouse.release()
Mouse.click_evaluate(Player2Data(GetTriggerPlayer()),false)
end
function Mouse.ResetInputForPData(pdata)
pdata[MOUSE_BUTTON_TYPE_LEFT] = false
pdata[MOUSE_BUTTON_TYPE_MIDDLE] = false
pdata[MOUSE_BUTTON_TYPE_RIGHT] = false
Mouse.press_event:fire(pdata,false)
end
function Mouse.Init()
local t = CreateTrigger()
local t2 = CreateTrigger()
local t3 = CreateTrigger()
FogEnable(false)
FogMaskEnable(false)
for i, p in ipairs(PlayerUsers) do
local pdata = Player2Data(p)
pdata.mouse_pos = NewTable()
pdata.mouse_screen_pos = NewTable()
pdata[MOUSE_BUTTON_TYPE_LEFT] = false
pdata[MOUSE_BUTTON_TYPE_MIDDLE] = false
pdata[MOUSE_BUTTON_TYPE_RIGHT] = false
pdata.mouse_pos[1] = 0.0
pdata.mouse_pos[2] = 0.0
pdata.mouse_screen_pos[1] = 0.4
pdata.mouse_screen_pos[2] = 0.3
pdata.mouse_angle = 0.0
TriggerRegisterPlayerEvent(t,p,EVENT_PLAYER_MOUSE_MOVE)
TriggerRegisterPlayerEvent(t2,p,EVENT_PLAYER_MOUSE_DOWN)
TriggerRegisterPlayerEvent(t3,p,EVENT_PLAYER_MOUSE_UP)
end
TriggerAddAction(t,Mouse.move)
TriggerAddAction(t2,Mouse.press)
TriggerAddAction(t3,Mouse.release)
end
end
do
local PLAYER_UNIT_DELTA = 0.01
COLLISION_HIT_COOLDOWN = 0.2
PlayerUnit = {}
PlayerUnit.__index = PlayerUnit
playerunits = Set.create()
player_collision_hash = shash.new(512)
PlayerUnit.hit_event = Event.new()
PlayerUnit.damage_event = Event.new()
PlayerUnit.about_to_die = Event.new()
PlayerUnit.death_event = Event.new()
PlayerUnit.heal_event = Event.new()
function GetExpRequired(level)
return MathRound( 0.04 * (level^3) + 0.8 * (level^2) + 2 * level)
end
local function levelUpPush(self)
if self.position == nil then return end
Unit.push_away_from_point(762,self.position[1],self.position[2],24.0)
end
function PlayerUnit:get_movespeed()
return self.speed * self.moveSpeedMult
end
function PlayerUnit:get_max_health()
return MathRound(self.max_health * self.healthMult)
end
function PlayerUnit:get_pickup_range()
return 300. * self.pickupRangeMult
end
local function buffEnd(playerUnit, whichStat,name,sfx)
local bonusName = name.."bonus"
if not playerUnit.buffs[name] then return end
local remain = playerUnit.buffs[name] - GAME_TIME
if remain > 0.0 then
DEFAULT_TIMER:callDelayed(remain,buffEnd,playerUnit, whichStat,name,sfx)
else
DestroyEffect(sfx)
playerUnit.buffs[name] = nil
playerUnit[whichStat] = tonumber(string.format("%%0.3f",playerUnit[whichStat] - playerUnit.buffs[bonusName]))
playerUnit.buffs[bonusName] = nil
end
end
function PlayerUnit:buff(whichStat,name,effect_model,attach_point,amount,duration,stackDur,stackBonus)
local bonusName = name.."bonus"
if stackDur then
duration = math.max(GAME_TIME,(self.buffs[name] or 0)) - GAME_TIME + duration
end
if not self.buffs[name] then
local sfx = AddSpecialEffectTarget(effect_model,self.sfx,attach_point)
DEFAULT_TIMER:callDelayed(duration,buffEnd,self,whichStat,name,sfx)
end
self.buffs[name] = GAME_TIME + duration
if stackBonus then
self.buffs[bonusName] = (self.buffs[bonusName] or 0) + amount
else
if self.buffs[bonusName] then
self.buffs[whichStat] = tonumber(string.format("%%0.3f",self.buffs[whichStat] - self.buffs[bonusName]))
end
self.buffs[bonusName] = math.max(amount, (self.buffs[bonusName] or 0))
end
self[whichStat] = self[whichStat] + amount
end
function PlayerUnit.level_up(self)
levelUpPush(self)
DEFAULT_TIMER:callDelayed(0.25, levelUpPush,self)
DEFAULT_TIMER:callDelayed(0.5, levelUpPush,self)
DEFAULT_TIMER:callDelayed(0.75, levelUpPush,self)
DEFAULT_TIMER:callDelayed(1.0, levelUpPush,self)
DEFAULT_TIMER:callDelayed(0.75,UI.ShowLevelUpUI,self)
DestroyEffect(AddSpecialEffectTarget(ART_LEVELUP,self.sfx,"origin"))
self.level = self.level + 1
if self.pdata == PlayerDataLocal then
BlzFrameSetText(UI.LevelText, "|cffd7d9feLevel "..tostring(self.level).."|r")
end
end
function PlayerUnit.set_max_health(self,amount)
local max = self:get_max_health()
local per = self.health/max
self.max_health = amount
max = self:get_max_health()
self.health = max * per
if self.pdata == PlayerDataLocal then
UI.Healthbar_Update(self.health,max)
end
end
function PlayerUnit:update_health()
local max = self:get_max_health()
self.health = math.min(self.health,max)
if self.pdata == PlayerDataLocal then
UI.Healthbar_Update(MathRound(self.health),max)
end
end
function PlayerUnit.set_healthMult(self,val)
local max = self:get_max_health()
local per = self.health/max
self.healthMult = val
max = self:get_max_health()
self.health = max * per
if self.pdata == PlayerDataLocal then
UI.Healthbar_Update(self.health,max)
end
end
function PlayerUnit.add_health(self,amount)
self:set_max_health(self.max_health+amount)
end
function PlayerUnit.gain_exp(self,amount)
amount = amount * self.expMult
self.exp_cur = self.exp_cur + amount
if self.exp_cur >= self.exp_max then
self.exp_cur = self.exp_cur - self.exp_max
self:level_up()
if self.level_function ~= nil then
self.level_function(self)
end
self.exp_max = GetExpRequired(self.level)
if self.exp_cur >= self.exp_max then
DEFAULT_TIMER:callDelayed(0.0, PlayerUnit.gain_exp,self,0.0)
end
end
SOUND_ORB_COLLECT(0.75+(self.exp_cur/self.exp_max))
if self.pdata == PlayerDataLocal then
UI.Expbar_Update(self.exp_cur,self.exp_max)
end
end
local function pickup_iter(orb,target)
if orb.type == Types.ExpOrb then
orb:mark_captured(target)
end
end
function PlayerUnit.new(pdata,x,y,data)
local self = setmetatable(NewTable(), PlayerUnit)
local x2,y2 = GetRandomLocationInRange(x,y+128,512,64,Types.Collidable)
Item.new(item_ability_scroll,x2,y2)
if PlayerDataLocal == data.pdata then
BlzFrameSetText(UI.LevelText, "|cffd7d9feLevel 1|r")
end
self.sfx = CreateUnit(PASSIVE_PLAYER,data.model,x,y,270)
SetUnitColor(self.sfx,GetPlayerColor(pdata.p))
UnitAddAbility(self.sfx,ABILITY_LOCUST)
UnitRemoveAbility(self.sfx,ABILITY_MOVE)
UnitAddAbility(self.sfx,ABILITY_PREVENT_ATTACK)
PauseUnit(self.sfx,true)
self.lookat = CreateUnit(PASSIVE_PLAYER,FourCC('h000'),x,y,0)
if data.bone_height then
SetUnitFlyHeight(self.lookat,data.bone_height,0.0)
end
PauseUnit(self.lookat,true)
self.type = Types.PlayerUnit
self.position = NewTable()
self.position[1] = x
self.position[2] = y
self.scale = data.scale
self.size = data.size
self.half_size = data.size * 0.5
self.speed = data.speed
self.yaw = 4.712388
self.buffs = NewKeyTable()
self.anim = data.anims.stand
self.anim_lib = data.anims
self.pdata = pdata
self.attacks = NewTable()
self.invulnerable = 0
self.move_dir = pdata.move_vec
Attack.new(data.attack,self)
self.attacking = 0.0
self.summon_count = 0
self.moving = false
self.bone_angle_offset = data.bone_angle_offset or 0.0
self.health = data.max_health
self.baseHealthRegen = data.baseHealthRegen
self.max_health = self.health
self.collision_cooldown = 0.0
self.level = 1
BlzFrameSetText(UI.LevelText, "|cffd7d9feLevel "..tostring(self.level).."|r")
self.exp_cur = 0.0
self.prev_positions = NewTable()
self.time_scale = 1.0
self.healthMult = 1.0
self.expMult = data.expMult or 1.0
self.exp_max = GetExpRequired(self.level)
self.rangeMult = data.rangeMult or 1.0
self.pickupRangeMult = data.pickupRangeMult or 1.0
self.areaMult = data.areaMult or 1.0
self.attackSpeedMult = data.attackSpeedMult or 1.0
self.moveSpeedMult = data.moveSpeedMult or 1.0
self.summonCountMult = data.summonCountMult or 0.0
self.summonForceMult = data.summonForceMult or 0.0
self.summonDamageMult = data.summonDamageMult or 0.0
self.summonMoveSpeedMult = data.summonMoveSpeedMult or 0.0
self.summonAttackSpeedMult = data.summonAttackSpeedMult or 0.0
self.damageMult = data.damageMult or 1.0
self.baseDamageBonus = data.baseDamageBonus or 0.0
self.baseCritChance = data.baseCritChance or 0.0
self.critBonus = data.critBonus or 1.5
self.critMult = data.critMult or 1.0
self.regenAccum = 0.0
self.multistrike = data.multistrike or 1.0
self.forceMult = data.forceMult or 1.0
self.block = data.block or 0.0
self.blockMult = 1.0
self.defense = data.defense or 0.0
self.frailBonus = data.frailBonus or 0.0
self.afflictBonus = data.afflictBonus or 0.0
self.slowBonus = data.slowBonus or 0.0
self.electricBonus = data.electricBonus or 0.0
self.burnBonus = data.burnBonus or 0.0
self.frostBonus = data.frostBonus or 0.0
self.regenMult = data.regenMult or 1.0
self.defenseMult = 1.0
self.level_function = data.level_function
self.pathing = FlowField.new(Camera.maxX*1.5,Camera.minX*1.5,Camera.maxY*1.5,Camera.minY*1.5,64,unit_collision_hash)
player_collision_hash:add(self, x - 32, y - 32, 64, 64)
SetUnitLookAt(self.sfx,"Bone_Chest",self.lookat,0,0,0.0)
playerunits:add(self)
UI.Healthbar_Update(self.health,self:get_max_health())
UI.Expbar_Update(self.exp_cur,self.exp_max)
if data.class_traits then
for _, trait in ipairs(data.class_traits) do
trait:add_to_pool()
end
end
self.damageMultTable = NewTable()
self.areaMultTable = NewTable()
self.critMultTable = NewTable()
self.multistrikeMultTable = NewTable()
self.attackSpeedTable = NewTable()
setmetatable(self.damageMultTable,base_damage_type_damage)
setmetatable(self.areaMultTable,base_damage_type_damage)
setmetatable(self.critMultTable,base_damage_type_damage)
setmetatable(self.multistrikeMultTable,base_damage_type_damage)
setmetatable(self.attackSpeedTable,base_damage_type_damage)
for i = 0, DamageTypes.Max - 1, 1 do
local j = 2^i
if data.damageMultTable then self.damageMultTable[j] = data.damageMultTable[j] or 0.0 end
if data.areaMultTable then self.areaMultTable[j] = data.areaMultTable[j] or 0.0 end
if data.critMultTable then self.critMultTable[j] = data.critMultTable[j] or 0.0 end
if data.multistrikeMultTable then self.multistrikeMultTable[j] = data.multistrikeMultTable[j] or 0.0 end
if data.attackSpeedTable then self.attackSpeedTable[j] = data.attackSpeedTable[j] or 0.0 end
end
self.event_actions = NewTable()
return self
end
function PlayerUnit.destroy(self)
DestroyAllEquipment()
for _, attack in ipairs(self.attacks) do
attack:destroy()
end
SOUND_DEATH()
Unit.ClearEffectsForPlayer(self)
playerunits:remove(self)
KillUnit(self.sfx)
self.sfx = nil
RemoveUnit(self.lookat)
self.lookat = nil
self.type = nil
ReleaseTable(self.position)
self.pdata.playerUnit = nil
self.health = nil
self.max_health = nil
self.exp_cur = nil
self.exp_max = nil
ReleaseKeyTable(self.buffs)
self.buffs = nil
self.level = nil
self.position = nil
self.scale = nil
self.defenseMult = nil
self.blockMult = nil
self.size = nil
self.half_size = nil
self.regenMult = nil
self.speed = nil
self.expMult = nil
self.yaw = nil
self.anim = nil
self.anim_lib = nil
self.time_scale = nil
self.rangeMult = nil
self.baseDamageBonus = nil
self.summonAttackSpeedMult = nil
self.summonMoveSpeedMult = nil
self.healthMult = nil
self.pickupRangeMult = nil
self.summon_count = nil
self.areaMult = nil
self.bone_angle_offset = nil
self.attackSpeedMult = nil
self.moveSpeedMult = nil
self.damageMult = nil
for i,v in ipairs(self.prev_positions) do
ReleaseUpdatingVector(v)
end
ReleaseTable(self.prev_positions)
self.prev_positions = nil
self.level_function = nil
self.baseCritChance = nil
self.critBonus = nil
self.critMult = nil
self.baseHealthRegen = nil
self.regenAccum = nil
self.summonCountMult = nil
self.summonForceMult = nil
self.summonDamageMult = nil
self.multistrike = nil
self.forceMult = nil
self.frailBonus = nil
self.afflictBonus = nil
self.slowBonus = nil
self.electricBonus = nil
self.invulnerable = nil
self.burnBonus = nil
self.frostBonus = nil
if self.pdata == PlayerDataLocal then
Camera.target = nil
end
self.pdata = nil
ReleaseTable(self.attacks)
self.attacks = nil
self.move_dir = nil
self.attacking = nil
self.moving = nil
self.block = nil
self.defense = nil
self.collision_cooldown = nil
self.pathing:destroy()
self.pathing = nil
for i = 1, #self.event_actions, 1 do
self.event_actions[i]:destroy()
end
ReleaseTable(self.event_actions)
self.event_actions = nil
player_collision_hash:remove(self)
for i = 0, DamageTypes.Max - 1, 1 do
local j = 2^i
self.areaMultTable[j] = nil
self.critMultTable[j] = nil
self.damageMultTable[j] = nil
self.attackSpeedTable[j] = nil
end
setmetatable(self.damageMultTable,nil)
setmetatable(self.areaMultTable,nil)
setmetatable(self.critMultTable,nil)
setmetatable(self.multistrikeMultTable,nil)
setmetatable(self.attackSpeedTable,nil)
ReleaseTable(self.multistrikeMultTable)
ReleaseTable(self.areaMultTable)
ReleaseTable(self.critMultTable)
ReleaseTable(self.damageMultTable)
ReleaseTable(self.attackSpeedTable)
self.multistrikeMultTable = nil
self.damageMultTable = nil
self.critMultTable = nil
self.areaMultTable = nil
self.attackSpeedTable = nil
setmetatable(self,nil)
ReleaseTable(self)
end
function PlayerUnit:damage_self(amount)
if self.invulnerable > GAME_TIME then return end
self.health = self.health - amount
if self.pdata == PlayerDataLocal then
UI.Healthbar_Update(self.health,self:get_max_health())
end
if self.health <= 0.0 then
PlayerUnit.about_to_die:fire(self)
if self.health <= 0.0 then
self:destroy()
end
end
end
function PlayerUnit.damage(self,source,amount)
if self.invulnerable > GAME_TIME then return end
amount = BlockDamage(amount,self.block*self.blockMult)
if amount == 0.0 then
CreateBlockEffect(self.position[1],self.position[2])
else
CinematicFadeForPlayer(self.pdata.p, bj_CINEFADETYPE_FADEOUTIN, .5, "ReplaceableTextures\\CameraMasks\\HazeFilter_mask.blp", 255, 0, 0,75)
SOUND_HIT:random()(GetRandomReal(0.9,1.1))
DestroyEffect(AddSpecialEffect("Objects\\Spawnmodels\\Critters\\Albatross\\CritterBloodAlbatross.mdl",self.position[1]-SFX_OFFSET,self.position[2]-SFX_OFFSET))
amount = ReduceDamage(amount,self.defense*self.defenseMult)
self.health = self.health - amount
if self.pdata == PlayerDataLocal then
UI.Healthbar_Update(self.health,self:get_max_health())
end
if self.health <= 0.0 then
PlayerUnit.about_to_die:fire(self)
if self.health <= 0.0 then
PlayerUnit.death_event:fire(self)
self:destroy()
return
end
end
end
PlayerUnit.hit_event:fire(self,amount,source)
if amount > 0 then
PlayerUnit.damage_event:fire(self,amount,source)
end
end
function PlayerUnit.check_bounds(self,x,y)
local dir_x = 0
local dir_y = 0
local offset_x = 0
local offset_y = 0
if x > TeleportBounds.maxX then
dir_x = TeleportBounds.point
offset_x = TeleportBounds.minX
elseif x < TeleportBounds.minX then
dir_x = -TeleportBounds.point
offset_x = TeleportBounds.maxX
end
if y > TeleportBounds.maxY then
dir_y = TeleportBounds.point
offset_y = TeleportBounds.minY
elseif y < TeleportBounds.minY then
dir_y = -TeleportBounds.point
offset_y = TeleportBounds.maxY
end
offset_x = offset_x * 2
offset_y = offset_y * 2
if dir_x ~= 0 or dir_y ~= 0 then
WorldGeneration.push(dir_x,dir_y,offset_x,offset_y)
end
end
function PlayerUnit.move(self,x,y)
self:check_bounds(x,y)
self.position[1] = x
self.position[2] = y
SetUnitX(self.sfx,x)
SetUnitY(self.sfx,y)
player_collision_hash:update(self, x - 32, y - 32, 64, 64)
end
function PlayerUnit.set_time_scale(self,speed)
self.time_scale = speed
SetUnitTimeScale(self.sfx,speed)
end
function PlayerUnit.resolve_animation(self)
local next
if self.attacking > GAME_TIME then return end
if self.moving then
next = self.anim_lib.walk
else
next = self.anim_lib.stand
end
if next ~= self.anim or self.attacking then
self:set_time_scale(1.0)
SetUnitAnimationByIndex(self.sfx,next[1])
self.anim = next
end
end
function PlayerUnit.animate_attack(self,dur)
self.attacking = dur + GAME_TIME
local speed = self.anim_lib.attack[2]/dur
self:set_time_scale(speed)
SetUnitAnimationByIndex(self.sfx,self.anim_lib.attack[1])
end
local dir_tbl = {0,0,0}
local function dest_iter(dest,cur_x,cur_y,self)
if Types.is(dest,Types.Collidable) or Types.is(dest,Types.PushesPlayer) then
local pushStr = dest.pushStr or 12.0
local x,y = dest.position[1],dest.position[2]
if dest.square then
x,y = VectorAngle(cur_x,cur_y,x,y)
dir_tbl[1] = dir_tbl[1] + x * pushStr
dir_tbl[2] = dir_tbl[2] + y * pushStr
dir_tbl[3] = dir_tbl[3] + pushStr
elseif IsInRange(cur_x,cur_y,x,y,dest.half_size+self.half_size) then
x,y = VectorAngle(cur_x,cur_y,x,y)
dir_tbl[1] = dir_tbl[1] + x * pushStr
dir_tbl[2] = dir_tbl[2] + y * pushStr
dir_tbl[3] = dir_tbl[3] + pushStr
end
end
end
local function hit_iter(u,self)
if getmetatable(self) ~= PlayerUnit then return end
if Types.is(u,Types.Ouchie) and self.collision_cooldown < GAME_TIME and not u.disabled_movement then
self.collision_cooldown = GAME_TIME + COLLISION_HIT_COOLDOWN
self:damage(u,u.damage)
end
end
function PlayerUnit.regen_tick(self)
local max = self:get_max_health()
if self.health < max then
self.regenAccum = self.regenAccum + (self.baseHealthRegen * PLAYER_UNIT_DELTA * self.regenMult)
if self.regenAccum >= 1 then
RegenText.new()
self.health = math.min(max,self.health + 1)
self.regenAccum = self.regenAccum - 1
UI.Healthbar_Update(self.health,max)
end
end
end
function PlayerUnit:heal(amount)
max = self:get_max_health()
local prev = self.health
self.health = math.min(self.health + amount,max)
for i = 1, math.floor(self.health-prev) do
RegenText.new()
end
if self.pdata == PlayerDataLocal then
UI.Healthbar_Update(self.health,max)
end
PlayerUnit.heal_event:fire(self,amount)
end
local function item_iter(it,self)
it:pickup(self)
end
function PlayerUnit.check_move(self)
local x = self.position[1]
local y = self.position[2]
self:regen_tick()
self.moving = self.move_dir ~= nil and (self.move_dir[1] ~= 0.0 or self.move_dir[2] ~= 0.0)
local speed = self:get_movespeed()
dir_tbl[1] = self.move_dir[1]
dir_tbl[2] = self.move_dir[2]
dir_tbl[3] = 1
unit_collision_hash:each(x - self.half_size, y - self.half_size, self.size, self.size, dest_iter,x,y,self)
dir_tbl[1] = dir_tbl[1]/dir_tbl[3]
dir_tbl[2] = dir_tbl[2]/dir_tbl[3]
local x1 = x + dir_tbl[1] * speed
local y1 = y + dir_tbl[2] * speed
self:move(x1,y1)
if self.moving then
SetUnitFacing(self.sfx,self.pdata.move_angle)
else
SetUnitFacing(self.sfx,self.yaw*bj_RADTODEG)
end
self:resolve_animation()
self.yaw = change_angle_bounded(self.yaw,self.pdata.mouse_angle,0.1)
x,y = WorldBounds.ClampCords(x+Cos(self.yaw + self.bone_angle_offset)*1000.0,y+Sin(self.yaw + self.bone_angle_offset)*1000.0)
SetUnitX(self.lookat,x)
SetUnitY(self.lookat,y)
x,y = self.position[1],self.position[2]
item_collision_hash:each(x-ITEM_HALF_SIZE,y-ITEM_HALF_SIZE,ITEM_SIZE,ITEM_SIZE,item_iter,self)
unit_collision_hash:each(x-self.half_size,y-self.half_size,self.size, self.size,hit_iter,self)
end
function PlayerUnit:project_position(time)
local sum_dx, sum_dy = 0, 0
time = time * 10
local speed = self:get_movespeed() * time
local cur_x = self.position[1]
local cur_y = self.position[2]
local prev_positions = self.prev_positions
local v2 = Vector(self.move_dir[1] * speed + cur_x,self.move_dir[2] * speed + cur_y)
table.insert(prev_positions,v2)
local size = #prev_positions
for i = 2, #prev_positions do
sum_dx = sum_dx + (prev_positions[i].x - prev_positions[i-1].x)
sum_dy = sum_dy + (prev_positions[i].y - prev_positions[i-1].y)
end
table.pop_back(prev_positions)
sum_dx = sum_dx / size
sum_dy = sum_dy / size
return cur_x + sum_dx * time, cur_y + sum_dy * time
end
local function collect_xp()
for u in playerunits:elements() do
local prev = u.prev_positions
local pos = GetUpdatingVector(u.position[1],u.position[2])
table.insert(prev,pos)
if #prev > 5 then
ReleaseUpdatingVector(table.remove(prev,1))
end
u.pathing:generate(pos.x,pos.y)
local range = u:get_pickup_range()
local half = range * 0.5
orb_collision_hash:each(u.position[1]-half,u.position[2]-half,range,range,pickup_iter,u)
end
end
local function movement()
for u in playerunits:elements() do
u:check_move()
end
end
local key_dir = {}
local function press()
local pdata = Player2Data(GetTriggerPlayer())
local key = BlzGetTriggerPlayerKey()
local pressed = -1.0
local down = BlzGetTriggerPlayerIsKeyDown()
local last = pdata[key]
pdata[key] = down
if down then pressed = 1.0 end
if last == down then return end
local dir = key_dir[key]
local x = clamp(pdata.move_dir[1] + dir[1] * pressed,-1.0,1.0)
local y = clamp(pdata.move_dir[2] + dir[2] * pressed,-1.0,1.0)
pdata.move_dir[1], pdata.move_dir[2] = x,y
pdata.move_vec[1], pdata.move_vec[2] = VectorNormalize(x, y)
pdata.move_angle = math.atan(y,x)*bj_RADTODEG
end
function add_trigger_event_key(t,p,key)
for metaKey = 0,15,1 do
BlzTriggerRegisterPlayerKeyEvent(t,p,key,metaKey,true)
BlzTriggerRegisterPlayerKeyEvent(t,p,key,metaKey,false)
end
end
function PlayerUnit.Init()
SwordsmanInit()
ArcherInit()
ExterminatorInit()
WarlockInit()
SorceressInit()
ClericInit()
DEFAULT_TIMER:callPeriodically(PLAYER_UNIT_DELTA, nil, movement)
DEFAULT_TIMER:callPeriodically(0.1, nil, collect_xp)
key_dir[OSKEY_W] = {0.0,1.0}
key_dir[OSKEY_S] = {0.0,-1.0}
key_dir[OSKEY_D] = {1.0,0.0}
key_dir[OSKEY_A] = {-1.0,0.0}
local t = CreateTrigger()
for i, p in ipairs(PlayerUsers) do
local pdata = Player2Data(p)
pdata.move_dir = {0.0,0.0}
pdata.move_vec = {0.0,0.0}
pdata.move_angle = 0.0
add_trigger_event_key(t,p,OSKEY_W)
add_trigger_event_key(t,p,OSKEY_A)
add_trigger_event_key(t,p,OSKEY_S)
add_trigger_event_key(t,p,OSKEY_D)
end
TriggerAddAction(t,press)
end
end
do
all_missiles = Set.create()
Missile = {}
Missile.__index = Missile
local delayed_destroy = Set.create()
local DEFAULT_HEIGHT = 50.0
local function dmg_iter(u,attack,list,missile)
if list[u] == nil then
list[u] = true
if missile.hits > 0 and getmetatable(missile.owner) == PlayerUnit and not Types.is(u,Types.Summon) then
local prev = attack.always_crit
attack.always_crit = missile.always_crit
local crit,_,dmg = attack:deal_damage(u)
attack.always_crit = prev
missile.hits = missile.hits - 1
if dmg == 0 then
missile.hits = 0
end
if attack.missile_hit_function then
attack.missile_hit_function(attack,missile,crit,u)
end
if missile.hits <= 0 then
delayed_destroy:add(missile)
end
end
end
end
function missile_player_dmg_iter(u,_,list,missile)
if list[u] == nil and u.type ~= nil then
list[u] = true
if missile.hits > 0 then
missile.hits = missile.hits - 1
u:damage(missile.owner,missile.damage)
if missile.hits <= 0 then
delayed_destroy:add(missile)
end
end
end
end
function Missile:get_angle()
return math.atan(self.dir_vec[2],self.dir_vec[1])
end
function Missile.new(model,origin_x,origin_y,dir,speed,owner,attack,dist,hits,area,height)
local self = setmetatable(NewTable(), Missile)
self.sfx = AddSpecialEffect(model,origin_x,origin_y)
self.height = height or DEFAULT_HEIGHT
BlzSetSpecialEffectZ(self.sfx,self.height)
BlzSetSpecialEffectYaw(self.sfx,dir)
self.position = NewTable()
self.position[1] = origin_x
self.position[2] = origin_y
self.dir_vec = NewTable()
self.dir_vec[1] = math.cos(dir) * speed
self.dir_vec[2] = math.sin(dir) * speed
self.owner = owner
self.attack = attack
self.dist = 0.0
self.max_dist = dist
self.speed = speed
self.area = area or attack:get_area()
self.hit_list = NewKeyTable()
self.hits = hits or attack:get_force()
self.dmg_func = dmg_iter
self.hash = unit_collision_hash
BlzSetSpecialEffectScale(self.sfx,attack:get_scale())
all_missiles:add(self)
return self
end
function Missile.newUnitMissile(model,origin_x,origin_y,dir,speed,area,owner,damage,dist,scale,height)
local self = setmetatable(NewTable(), Missile)
self.sfx = AddSpecialEffect(model,origin_x,origin_y)
self.height = height or DEFAULT_HEIGHT
BlzSetSpecialEffectZ(self.sfx,self.height)
BlzSetSpecialEffectYaw(self.sfx,dir)
self.position = NewTable()
self.position[1] = origin_x
self.position[2] = origin_y
self.dir_vec = NewTable()
self.dir_vec[1] = Cos(dir) * speed
self.dir_vec[2] = Sin(dir) * speed
self.dist = 0.0
self.owner = owner
self.damage = damage
self.max_dist = dist
self.speed = speed
self.area = area
self.hit_list = NewKeyTable()
self.hits = 1
self.hash = player_collision_hash
self.dmg_func = missile_player_dmg_iter
BlzSetSpecialEffectScale(self.sfx,scale or 1.0)
all_missiles:add(self)
return self
end
function Missile.destroy(self)
if self.attack then
if self.attack.missile_destroy_function then
self.attack.missile_destroy_function(self.attack,self)
end
self.attack = nil
end
DestroyEffect(self.sfx)
ReleaseTable(self.position)
ReleaseTable(self.dir_vec)
self.position = nil
self.dir_vec = nil
self.sfx = nil
self.area = nil
self.owner = nil
self.dist = nil
self.max_dist = nil
self.height = nil
self.speed = nil
self.hits = nil
self.hash = nil
self.always_crit = nil
self.damage = nil
ReleaseKeyTable(self.hit_list)
self.hit_list = nil
self.dmg_func = nil
self.custom_data = nil
all_missiles:remove(self)
setmetatable(self,nil)
ReleaseTable(self)
end
function Missile.move(self,x,y)
self.position[1] = x
self.position[2] = y
BlzSetSpecialEffectPosition(self.sfx,x,y,self.height)
end
local function movement()
for m in delayed_destroy:elements() do
m:destroy()
end
delayed_destroy:clear()
for m in all_missiles:elements() do
local x,y = m.position[1] + m.dir_vec[1],m.position[2] + m.dir_vec[2]
m.dist = m.dist + m.speed
m:move(x,y)
local half = m.area * 0.5
if m.dist >= m.max_dist or unit_collision_hash:cell_occupied(x, y,0,0,Types.ProjectileStopper) then
m:destroy()
else
m.hash:each(x - half, y - half, m.area, m.area, m.dmg_func,m.attack,m.hit_list,m)
end
end
end
function Missile.check_destroyed_attack(attack)
for m in all_missiles:elements() do
if m.attack == attack then
m:destroy()
end
end
end
function Missile.Init()
DEFAULT_TIMER:callPeriodically(DEFAULT_TIMEOUT, nil, movement)
end
end
do
Attack = {}
Attack.__index = Attack
AttackTypes = {
Missile = 1,
Circle = 2,
Cone = 3,
Unique = 4,
}
AttackProperties = {
Projectile = 1,
Melee = 2,
Summon = 4,
Area = 8,
Elemental = 16,
}
DamageTypes = {
Physical = 1,
Magical = 2,
Fire = 4,
Lightning = 8,
Ice = 16,
--ELEMENTAL EFFECTS
Burn = 32,
Electrify = 64,
Frost = 128,
}
Attack.hit_event = Event.new()
Attack.main_attack_fire = Event.new()
Attack.gain_event = Event.new()
function AttackProperties.is(whichAttack,whichProperty)
if whichAttack.properties == nil then return false end
return whichAttack.properties & whichProperty == whichProperty
end
function AttackProperties.add(whichAttack,whichProperty)
whichAttack.properties = whichAttack.properties | whichProperty
end
DamageTypes.Max = 8
base_damage_type_damage = {}
base_damage_type_damage[DamageTypes.Physical] = 0.0
base_damage_type_damage[DamageTypes.Magical] = 0.0
base_damage_type_damage[DamageTypes.Fire] = 0.0
base_damage_type_damage[DamageTypes.Lightning] = 0.0
base_damage_type_damage[DamageTypes.Ice] = 0.0
base_damage_type_damage[DamageTypes.Burn] = 0.0
base_damage_type_damage[DamageTypes.Electrify] = 0.0
base_damage_type_damage[DamageTypes.Frost] = 0.0
base_damage_type_damage.__index = base_damage_type_damage
all_attacks = Set.create()
function Attack.get_speed(self)
return self.speed * (self.speedMult + 1)
end
function Attack:update_summoned(count,hordeSummon)
if not hordeSummon then
self.owner.summon_count = self.owner.summon_count + count
end
self.summoned_count = self.summoned_count + count
if self.summoned_count == 0 then
SetCooldownIconText(self,"")
else
SetCooldownIconText(self,self.summoned_count)
end
end
function Attack.get_damage(self,target,dmgTypeOverride)
local owner = self.owner
local dmgType = dmgTypeOverride or self.damageType
local fragile = 0
if target and target.get_fragility then
fragile = target:get_fragility()
end
if self.summon_count then
fragile = fragile + owner.summonDamageMult
end
return (self.damage + owner.baseDamageBonus) * (owner.damageMult + self.damageMult + self.damageMultTable[dmgType] + owner.damageMultTable[dmgType] + fragile)
end
function Attack.get_range(self)
return (self.owner.rangeMult + self.rangeMult) * self.range
end
function Attack.get_area(self)
return self.area * (self.owner.areaMult + self.areaMult + self.owner.areaMultTable[self.damageType])
end
function Attack.get_attack_speed(self)
return self.cooldown / (self.owner.attackSpeedMult + self.attackSpeedMult + self.owner.attackSpeedTable[self.damageType])
end
function Attack.get_attack_speed_scaler(self)
return (self.owner.attackSpeedMult + self.attackSpeedMult + self.owner.attackSpeedTable[self.damageType])
end
function Attack.get_attack_count(self)
return self.attack_count * (self.owner.multistrike + self.multistrikeMult + self.owner.multistrikeMultTable[self.damageType])
end
function Attack.get_summon_count(self)
return self.summon_count * (self.owner.multistrike + self.multistrikeMult + self.owner.summonCountMult)
end
function Attack.get_on_hit_chance(self)
return self.effectChance * self.effectChanceMult
end
function Attack.get_scale(self)
return self.base_scale * (self.owner.areaMult + self.areaMult + self.owner.areaMultTable[self.damageType])
end
function Attack.get_force(self)
if self.summon_count then
return self.force * (self.owner.forceMult + self.forceMult + self.owner.summonForceMult)
else
return self.force * (self.owner.forceMult + self.forceMult)
end
end
function Attack.get_crit_chance(self,target)
if self.always_crit then
return math.max(1.0,self.owner.baseCritChance + self.critChanceBonus + self.owner.critMultTable[self.damageType])
else
return self.owner.baseCritChance + self.critChanceBonus + self.owner.critMultTable[self.damageType]
end
end
function Attack.get_cone_angle(self)
return self.cone_angle * (self.owner.areaMult + self.areaMult + self.owner.areaMultTable[self.damageType])
end
function Attack.get_crit_bonus(self)
return (self.owner.critBonus + self.critDamageBonus) * self.owner.critMult
end
function Attack.deal_damage(self,target,reduction,override_damage,dmgTypeOverride,effect_override)
local playerUnit = self.owner
local crit_chance = self:get_crit_chance(target)
local crit_hit = false
local effect_chance = false
local actual_dmg = 1
if Types.is(target,Types.Damageable) then
local x,y = target.position[1],target.position[2]
if self.hit_sound then
self.hit_sound:random()(GetRandomReal(0.9,1.1),true,x,y)
end
local dmg = (override_damage or self:get_damage(target,dmgTypeOverride)) * (reduction or 1.0)
local crit_roll = GetRandomReal(0.0,1.0)
crit_hit = crit_roll < crit_chance
if crit_hit then
local r = crit_chance - math.floor(crit_chance)
if crit_roll < r then
crit_chance = math.ceil(crit_chance)
else
crit_chance = math.floor(crit_chance)
end
dmg = dmg * self:get_crit_bonus() * crit_chance
CreateFloatingText(math.floor(dmg),200,0,0,target.position[1],target.position[2],SYMBOL_CRIT)
else
CreateFloatingText(math.floor(dmg),200,200,200,target.position[1],target.position[2])
end
actual_dmg = target:receive_damage(dmg,self.unblockable)
Attack.hit_event:fire(self,target,actual_dmg,x,y)
if Types.is(target,Types.Effectable) and actual_dmg > 0 then
local efffect_func = effect_override or self.effect_function
if efffect_func then
effect_chance = efffect_func(target,self,self:get_on_hit_chance())
if target.type == nil then return end
end
if self.on_hit_function ~= nil then
self.on_hit_function(target,self,crit_hit)
end
end
end
return crit_hit, effect_chance, actual_dmg
end
function Attack.basic_damage(self,target,dmg)
if Types.is(target,Types.Damageable) then
CreateFloatingText(math.floor(dmg),200,200,200,target.position[1],target.position[2])
target:receive_damage(dmg)
end
end
local function cone_iter(u,attack,x,y,angle,cone_angle,range)
if u.type == nil then return end
local x1 = u.position[1]
local y1 = u.position[2]
if isPointInsideCone(x, y, angle, cone_angle, x1, y1) and IsInRange(x1, y1,x,y,range) then
attack:deal_damage(u)
end
end
local function circle_iter(u,attack,x,y,area)
if u.type == nil then return end
if IsInRange(u.position[1], u.position[2],x,y,area) then
attack:deal_damage(u)
end
end
AttackFunctions = {}
AttackFunctions[AttackTypes.Circle] = function(self,playerUnit)
local x,y = playerUnit.position[1],playerUnit.position[2]
local area = self.get_area()
local half = area * 0.5
unit_collision_hash:each(x - half, y - half, area, area, circle_iter,self,x,y,area)
AreaIndicator.new(AreaIndicatorTypes.Cone,x,y,half,0,TAU):destroy(0.1)
end
AttackFunctions[AttackTypes.Cone] = function(self,playerUnit)
local x,y = playerUnit.position[1],playerUnit.position[2]
local half = self:get_range()
local area = half * 2.0
local dir = playerUnit.yaw
local cone_angle = self:get_cone_angle()
unit_collision_hash:each(x - half, y - half, area, area, cone_iter,self,x,y,dir,cone_angle,half)
AreaIndicator.new(AreaIndicatorTypes.Cone,x,y,half,dir,cone_angle):destroy(0.1)
end
AttackFunctions[AttackTypes.Missile] = function(self,playerUnit,count)
local x,y = playerUnit.position[1],playerUnit.position[2]
local dir = playerUnit.yaw
local range = self:get_range()
local speed = self:get_speed()
if count == 1 then
local m = Missile.new(self.model,x,y,dir,speed,self.owner,self,range)
m.always_crit = self.always_crit
else
local a = math.min(0.4363323 + (0.03490659*count),TAU)
local angleBetweenProjectiles = a / (count - 1)
local startingAngle = dir - (a / 2)
for i = 0, count - 1 do
local dir2 = startingAngle + (angleBetweenProjectiles * i)
local m = Missile.new(self.model,x,y,dir2,speed,self.owner,self,range)
m.always_crit = self.always_crit
end
end
end
function Attack.increment_count(self)
local count = (self.summon_is_attack and self:get_summon_count() or self:get_attack_count()) + self.attack_leftover
local actual = math.floor(count)
self.attack_leftover = count - actual
return actual
end
function Attack.finished(self,sfx)
local u = self.owner
local count = self:increment_count()
if self.type ~= AttackTypes.Missile then
for i = 1, count, 1 do
local delay = (i-1) * 0.15
self.timer:callDelayed(delay, AttackFunctions[self.type],self,u)
end
else
AttackFunctions[self.type](self,u,count)
end
end
function Attack:is_on_cooldown()
return self.cooldown_cur > GAME_TIME
end
local function updateCooldown(self,frame)
self.curCooldown = self.curCooldown + 0.05
local t = math.min(1,self.curCooldown/self.maxCooldown)
BlzFrameSetValue(frame,t)
if t == 1 then
self.curCooldown = nil
self.maxCooldown = nil
return
end
self.timer:callDelayed(0.05,updateCooldown,self,frame)
end
function Attack:get_new_cooldown()
return self:get_attack_speed()
end
function Attack:start_cooldown()
local t = self:get_new_cooldown()
local frame = self.cooldownIcon.cd.frame
if not self.maxCooldown then
self.timer:callDelayed(0.05,updateCooldown,self,frame)
end
self.curCooldown = 0.0
self.maxCooldown = t
self.cooldown_cur = GAME_TIME + t
BlzFrameSetValue(frame,1.0)
return t
end
function Attack.fire(self)
self.attacking = true
local cooldown = self:start_cooldown()
if self.main_attack then
Attack.main_attack_fire:fire(self)
end
if self.on_begin_function then
self.on_begin_function(self)
end
self.timer:callDelayed(cooldown * self.delay, self.finished,self,self.owner.sfx)
if self.main_attack then
self.owner:animate_attack(cooldown)
end
end
function Attack.periodic_check(self)
if not self.held then
self.attacking = false
return
end
self.timer:callDelayed(self:get_new_cooldown(), self.periodic_check,self)
self:fire()
end
function Attack.start(self,pdata,state)
if pdata.playerUnit ~= self.owner then return end
self.held = state
if not self.attacking and state then
self.timer:callDelayed(self:get_new_cooldown(), self.periodic_check,self)
self.attacking = true
self.timer:callDelayed(0,self.fire,self)
end
end
function Attack.move(x,y)
end
Attack.added_summon_trait = false
function Attack.new(data,owner,abilitySource,gain_arg)
local self = setmetatable(NewTable(), Attack)
self.name = data.name
self.cooldown = data.cooldown
self.cooldown_cur = 0.0
self.area = data.area
self.damage = data.damage
self.range = data.range
self.type = data.type
self.timer = TimerQueue.create()
self.owner = owner
self.delay = data.delay
self.main_attack = data.main_attack
self.cone_angle = data.cone_angle
self.event_actions = NewTable()
self.attack_count = data.attack_count or 1
self.abilitySource = abilitySource
self.icon = abilitySource and abilitySource.icon or "ReplaceableTextures\\CommandButtons\\BTNAttack.blp"
self.attack_leftover = 0.0
self.model = data.model
if data.summon_count then
self.summon_count = data.summon_count
self.summoned_count = 0
end
if data.summon_count and not Attack.added_summon_trait then
Attack.added_summon_trait = true
trait_elemental_surge:add_to_pool()
trait_demonic_exchange:add_to_pool()
end
self.base_scale = data.base_scale or 1
self.update_func = data.update_func or attack_update_blank
self.destroy_func = data.destroy_func or attack_update_blank
self.speed= data.speed
self.properties = data.properties
self.attackSpeedMult = data.attackSpeedMult or 0.0
self.speedMult = data.speedMult or 0.0
self.rangeMult = data.rangeMult or 0.0
self.damageMult = data.damageMult or 0.0
self.areaMult = data.areaMult or 0.0
self.critChanceBonus = data.critChanceBonus or 0.0
self.critDamageBonus = data.critDamageBonus or 0.0
self.multistrikeMult = data.multistrikeMult or 0.0
self.effectChance = data.effectChance or 0.0
self.effectChanceMult = data.effectChanceMult or 1.0
self.effect_function = data.effect_function
self.on_hit_function = data.on_hit_function
self.on_begin_function = data.on_begin_function
self.hit_sound = data.hit_sound
self.level = 1
self.finished = data.finished
self.force = data.force or 1
self.forceMult = data.forceMult or 0.0
self.damageType = data.damageType or DamageTypes.Physical
self.damageMultTable = NewTable()
setmetatable(self.damageMultTable,base_damage_type_damage)
if data.damageMultTable then
for i = 0, DamageTypes.Max - 1, 1 do
local j = 2^i
if data.damageMultTable[j] then
self.damageMultTable[j] = data.damageMultTable[j]
end
end
end
if self.cooldown or data.summon_count then
AssignCooldownIconToAttack(self)
end
if data.gain_action then
data.gain_action(self,data,gain_arg)
end
if abilitySource and abilitySource.upgrades then
self.upgrades = abilitySource.upgrades:copy()
self.upgrades_max = 0
self.upgrades_cur = 0
end
if GameAction.IsPaused() then
self.timer:pause()
end
all_attacks:add(self)
self.update_func(self)
self.move = data.move or Attack.move
table.insert(self.owner.attacks,self)
Attack.gain_event:fire(self)
return self
end
function Attack.destroy(self)
if self.cooldown or self.summon_count then
FreeAttackCooldownIcon(self)
self.cooldown = nil
end
Missile.check_destroyed_attack(self)
self.destroy_func(self)
self.name = nil
self.destroy_func = nil
self.update_func = nil
self.cooldown_cur = nil
self.area = nil
self.damage = nil
self.range = nil
self.owner = nil
self.delay = nil
self.cone_angle = nil
self.hit_sound = nil
self.main_attack = nil
self.curCooldown = nil
self.maxCooldown = nil
self.attack_count = nil
self.attack_leftover = nil
self.summoned_count = nil
self.base_scale = nil
self.properties= nil
self.model = nil
self.speed = nil
self.timer:destroy()
self.timer = nil
self.level = nil
self.speedMult = nil
self.rangeMult = nil
self.damageMult = nil
self.summon_count = nil
self.areaMult = nil
self.critChanceBonus = nil
self.icon = nil
self.critDamageBonus = nil
self.held = nil
self.attackSpeedMult = nil
self.on_begin_function = nil
self.multistrikeMult = nil
self.effectChance = nil
self.effectChanceMult = nil
self.effect_source = nil
self.effect_function = nil
self.on_hit_function = nil
self.finished = nil
self.force = nil
self.forceMult = nil
self.move = nil
self.type = nil
self.attacking = nil
self.abilitySource = nil
self.damageType = nil
for i = 0, DamageTypes.Max - 1, 1 do
local j = 2^i
self.damageMultTable[j] = nil
end
setmetatable(self.damageMultTable,nil)
ReleaseTable(self.damageMultTable)
self.damageMultTable = nil
if self.upgrades then
self.upgrades:destroy()
self.upgrades = nil
self.upgrades_max = nil
self.upgrades_cur = nil
end
for i = 1, #self.event_actions, 1 do
self.event_actions[i]:destroy()
end
ReleaseTable(self.event_actions)
self.event_actions = nil
setmetatable(self,nil)
ReleaseTable(self)
all_attacks:remove(self)
end
end
do
Camera = {}
Camera.ax = -1850
Camera.ay = 950
Camera.bx = 1850
Camera.by = 950
Camera.cx = 1200
Camera.cy = -850
Camera.dx = -1200
Camera.dy = -850
Camera.Offset = {x=0,y=0}
Camera.position = {x=0,y=0}
Camera.maxX = math.max(Camera.ax,Camera.bx,Camera.cx,Camera.dx)
Camera.minX = math.min(Camera.ax,Camera.bx,Camera.cx,Camera.dx)
Camera.maxY = math.max(Camera.ay,Camera.by,Camera.cy,Camera.dy)
Camera.minY = math.min(Camera.ay,Camera.by,Camera.cy,Camera.dy)
local u
function Camera.offset(offsetX,offsetY)
Camera.position.x = Camera.position.x + offsetX
Camera.position.y = Camera.position.y + offsetY
end
function Camera.update()
if CharacterSelect.selecting then
CameraSetupApplyForceDuration(gg_cam_CharacterSelect,true,0.0)
return
end
if getmetatable(PlayerDataLocal.playerUnit) == nil then return end
local pos = PlayerDataLocal.playerUnit.position
local lastX = Camera.position.x
local lastY = Camera.position.y
Camera.position.x = lerp(Camera.position.x-Camera.Offset.x,pos[1],0.015)
Camera.position.y = lerp(Camera.position.y-Camera.Offset.y,pos[2],0.015)
local mpos = PlayerDataLocal.mouse_pos
mpos[1] = mpos[1] + Camera.position.x - lastX
mpos[2] = mpos[2] + Camera.position.y - lastY
--SetCameraTargetController(u,Camera.position.x,Camera.position.y,false)
SetCameraTargetController(PlayerDataLocal.playerUnit.sfx,0,0,false)
end
function Camera.Init()
SetCameraBounds(99999,99999,-99999,99999,-99999,-99999,99999,-99999)
u = CreateUnit(Player(0),FourCC('hpea'),0, 0,0)
PauseUnit(u,true)
ShowUnit(u,false)
TimerQueue.create():callPeriodically(0.005, nil, Camera.update)
end
end
do
AreaIndicator = {}
AreaIndicator.__index = AreaIndicator
AreaIndicatorTypes = {
Cone = 1,
Circle = 2,
}
local AREA_EFFECTS = {
"AreaIndicator6.mdx"
}
function AreaIndicator.new(area_type,x,y,scale,dir,cone_angle)
local self = setmetatable(NewTable(), AreaIndicator)
self.sfx = AddSpecialEffect(AREA_EFFECTS[area_type],x-SFX_OFFSET,y-SFX_OFFSET)
BlzSetSpecialEffectScale(self.sfx,scale)
if area_type == AreaIndicatorTypes.Cone then
BlzSetSpecialEffectTime(self.sfx,1.0-cone_angle/TAU)
BlzSetSpecialEffectTimeScale(self.sfx,0.0)
BlzSetSpecialEffectYaw(self.sfx, dir - cone_angle * 0.5)
elseif area_type == AreaIndicatorTypes.Circle then
BlzSetSpecialEffectTime(self.sfx,1.0)
BlzSetSpecialEffectTimeScale(self.sfx,0.0)
BlzSetSpecialEffectYaw(self.sfx, dir)
end
return self
end
local function finish_destroy(self)
BlzSetSpecialEffectZ(self.sfx,-9999)
DestroyEffect(self.sfx)
self.sfx = nil
setmetatable(self,nil)
ReleaseTable(self)
end
function AreaIndicator.destroy(self,time)
if time == nil or time == 0.0 then
finish_destroy(self)
else
DEFAULT_TIMER:callDelayed(time,finish_destroy,self)
end
return self
end
end
do
Wearable = {}
Wearable.__index = Wearable
Wearable.Type = {
BOOTS = 1,
CHEST = 2,
GLOVES = 3,
HEADWEAR = 4,
NECKLACE = 5,
RING = 6,
POTION = 7,
}
WearablesAll = Set.create()
ExistingWearables = Set.create()
local byName = {}
function Wearable.new(self)
setmetatable(self,Wearable)
if self.type ~= Wearable.Type.POTION then
WearablesAll:add(self)
end
byName[self.name] = self
return self
end
function Wearable:action()
end
function Wearable:instance()
local new = NewTable()
setmetatable(new,Wearable)
new.icon = self.icon
new.name = self.name
new.description = self.description
new.type = self.type
new.attack_type = self.attack_type
new.source = self
new.data = NewKeyTable()
new.action = self.action
if self.type == Wearable.Type.POTION then
new.noSlotUse = true
end
ExistingWearables:add(self)
return new
end
function Wearable:equip(owner,slot)
self.slot = slot
if self.attack_type then
self.attack = Attack.new(self.attack_type,owner,self,self)
end
end
function Wearable:unequip()
self.slot = nil
if self.attack then
local attacks = self.attack.owner.attacks
for i, attack in ipairs(attacks) do
if attack == self.attack then
table.remove(attacks,i)
break
end
end
self.attack:destroy()
self.attack = nil
end
end
function Wearable:destroy()
ExistingWearables:remove(self.source)
if self.slot then
SlotClearWearable(self.slot)
self.slot = nil
end
self.icon = nil
self.name = nil
self.noSlotUse = nil
self.description = nil
self.type = nil
self.attack_type = nil
self.source = nil
self.action = nil
if self.attack then
local attacks = self.attack.owner.attacks
for i, attack in ipairs(attacks) do
if attack == self.attack then
table.remove(attacks,i)
break
end
end
self.attack:destroy()
self.attack = nil
end
ReleaseKeyTable(self.data)
self.data = nil
setmetatable(self,nil)
ReleaseTable(self)
end
function Wearable.instanceByName(name)
local w = byName[name]
return w:instance()
end
function Wearable.instanceRandom()
local list = Set.create()
for w in WearablesAll:elements() do
if not ExistingWearables:contains(w) then
list:add(w)
end
end
local ret = list:random()
list:destroy()
return ret:instance()
end
end
do
Frame = {}
Frame.__index = Frame
frame_hash = shash.new(0.1)
local frame2Frame = {}
function Frame.getFrame(frame)
return frame2Frame[frame]
end
function Frame.new(self)
if self.frameParent then
self.parent = self.frameParent.frame
if self.frameParent.children then
table.insert(self.frameParent.children,self)
else
self.frameParent.children = {self}
end
self.order = self.frameParent.order + 1
else
self.order = 1
end
if self.simpleFrame then
self.frame = BlzCreateSimpleFrame(self.typeName,self.parent,self.createContext)
elseif self.byType then
self.frame = BlzCreateFrameByType(self.typeName,"",self.parent,"",self.createContext)
else
self.frame = BlzCreateFrame(self.typeName, self.parent, 0, self.createContext)
end
frame2Frame[self.frame] = self
if self.text then
BlzFrameSetText(self.frame,self.text)
end
self.originx1 = self.x1
self.originy1 = self.y1
self.originx2 = self.x2
self.originy2 = self.y2
self.localScaler = self.localScaler or Vector(1,1)
BlzFrameSetAbsPoint(self.frame, FRAMEPOINT_TOPLEFT, self.x1, self.y1)
BlzFrameSetAbsPoint(self.frame, FRAMEPOINT_BOTTOMRIGHT, self.x2, self.y2)
if self.trackHover or self.moveable then
local x = (self.x1 + self.x2)/2
local y = (self.y1 + self.y2)/2
local w = math.abs(self.x1-self.x2)
local h = math.abs(self.y1-self.y2)
frame_hash:add(self, x - w * 0.5, y - h * 0.5, w, h)
end
if self.texture then
BlzFrameSetTexture(self.frame,self.texture,0,true)
end
if self.justifyH then
BlzFrameSetTextAlignment(self.frame,self.justifyV,self.justifyH)
end
if self.scale then
self.scaleX = self.scale
self.scaleY = self.scale
unsafeFrameScale(self.frame,self.scale)
else
self.scaleX = self.scaleX or 1.0
self.scaleY = self.scaleY or 1.0
end
if self.level then
BlzFrameSetLevel(self.frame,self.level)
end
if self.enabled then
BlzFrameSetEnable(self.frame,self.enabled)
end
if self.model then
BlzFrameSetModel(self.frame,self.model,0)
end
if self.visible == false then
BlzFrameSetVisible(self.frame,false)
else
self.visible = true
end
setmetatable(self,Frame)
return self
end
function Frame:set_text(txt)
self.text = txt
BlzFrameSetText(self.frame,txt)
end
function Frame:set_visible(state)
self.visible = state
BlzFrameSetVisible(self.frame,state)
end
function Frame:set_disabled(state)
self.disabled = state
if self.hoverIcon then
self.hoverIcon:set_disabled(state)
else
if state then
BlzFrameSetTexture(self.frame,"DisabledFrame.dds",0,true)
else
BlzFrameSetTexture(self.frame,self.texture,0,true)
end
end
end
local function updateFramePosition(frame,x1,y1,x2,y2)
frame.x1 = x1
frame.y1 = y1
frame.x2 = x2
frame.y2 = y2
local cx = (x1 + x2) * 0.5
local cy = (y1 + y2) * 0.5
local nw = (x2 - x1) * frame.localScaler.x * 0.5
local nh = (y2 - y1) * frame.localScaler.y * 0.5
if frame.trackHover or frame.moveable then
local w = math.abs(x1-x2)
local h = math.abs(y1-y2)
frame_hash:update(frame, cx - w * 0.5 * frame.localScaler.x, cy - h * 0.5 * frame.localScaler.x, w, h)
end
BlzFrameSetAbsPoint(frame.frame,FRAMEPOINT_TOPLEFT,cx - nw,cy - nh)
BlzFrameSetAbsPoint(frame.frame,FRAMEPOINT_BOTTOMRIGHT,cx + nw,cy + nh)
end
local function moveChildren(self,x1,y1,x2,y2)
local dx = (x2 - x1) / (self.x2 - self.x1)
local dy = (y2 - y1) / (self.y2 - self.y1)
if self.children then
for i, child in ipairs(self.children) do
local child_x1 = x1 + (child.x1 - self.x1) * dx
local child_y1 = y1 + (child.y1 - self.y1) * dy
local child_x2 = x1 + (child.x2 - self.x1) * dx
local child_y2 = y1 + (child.y2 - self.y1) * dy
if child.children then
moveChildren(child,child_x1,child_y1,child_x2,child_y2)
end
updateFramePosition(child,child_x1,child_y1,child_x2,child_y2)
end
end
end
function Frame:set_position(x1,y1,x2,y2,excludeChildren)
if not excludeChildren then
moveChildren(self,x1,y1,x2,y2)
end
updateFramePosition(self,x1,y1,x2,y2)
end
function Frame:set_center(x, y)
local width = self.x2 - self.x1
local height = self.y2 - self.y1
local x1 = x - width / 2
local y1 = y - height / 2
local x2 = x + width / 2
local y2 = y + height / 2
self:set_position(x1, y1, x2, y2)
end
function Frame:move(moveX,moveY,excludeChildren)
self:set_position(self.x1+moveX,self.y1+moveY,self.x2+moveX,self.y2+moveY,excludeChildren)
end
function Frame:scaleXY(scaleFactorX, scaleFactorY)
self.scaleX = scaleFactorX
self.scaleY = scaleFactorY
local x1, y1, x2, y2 = self.x1, self.y1, self.x2, self.y2
local width, height = x2 - x1, y2 - y1
local centerX, centerY = x1 + width / 2, y1 + height / 2
local newWidth, newHeight = width * scaleFactorX, height * scaleFactorY
local newX1, newY1 = centerX - newWidth / 2, centerY - newHeight / 2
local newX2, newY2 = centerX + newWidth / 2, centerY + newHeight / 2
self:set_position(newX1, newY1, newX2, newY2)
end
local insideFrames = Set.create()
local pickedFrames = Set.create()
local eventFrames = Set.create()
local mouseScreenX = 0
local mouseScreenY = 0
local mouseHeld = false
local function frameEnter(frame)
if not BlzFrameIsVisible(frame.frame) then return end
frame.picked = true
pickedFrames:add(frame)
if not frame.inside and not frame.disabled then
frame.inside = true
insideFrames:add(frame)
eventFrames:add(frame)
end
end
local function mouseCheck()
if mouseHeld then return end
frame_hash:each(mouseScreenX, mouseScreenY, 0.0, 0.0, frameEnter)
for frame in insideFrames:elements() do
if not frame.picked or frame.disabled then
frame.inside = nil
insideFrames:remove(frame)
if frame.trackHover then
BlzFrameSetTexture(frame.frame,frame.texture,0,true)
if type(frame.trackHover) == "function" then
frame.trackHover(frame,false)
end
end
end
end
for frame in eventFrames:elements() do
eventFrames:remove(frame)
if frame.trackHover and not frame.disabled then
if frame.textureHover then
(frame.hoverSound and frame.hoverSound or SOUND_UI_HOVER)()
BlzFrameSetTexture(frame.frame,frame.textureHover,0,true)
end
if type(frame.trackHover) == "function" then
frame.trackHover(frame,true)
end
end
end
for frame in pickedFrames:elements() do
frame.picked = nil
pickedFrames:remove(frame)
end
end
local function mouseMove(pdata,x,y)
local lastX = mouseScreenX
local lastY = mouseScreenY
mouseScreenX,mouseScreenY = PlayerDataLocal.mouse_screen_pos[1],PlayerDataLocal.mouse_screen_pos[2]
mouseCheck(pdata)
if mouseHeld then
local diffX = mouseScreenX - lastX
local diffY = mouseScreenY - lastY
for frame in insideFrames:elements() do
if frame.moveable then
frame:move(diffX,diffY)
end
end
end
end
local highestOrder = 0
local function frameGetOrder(frame)
if frame.trackClick then
highestOrder = math.max(highestOrder,frame.order)
end
end
local function afterPress(frame)
if frame.disabled then
if frame.hoverIcon then
BlzFrameSetTexture(frame.hoverIcon.frame,"DisabledFrame.dds",0,true)
else
BlzFrameSetTexture(frame.frame,"DisabledFrame.dds",0,true)
end
elseif frame.pressed then
BlzFrameSetTexture(frame.frame,frame.texturePressed or frame.texture,0,true)
elseif frame.inside then
BlzFrameSetTexture(frame.frame,frame.textureHover or frame.texture,0,true)
else
BlzFrameSetTexture(frame.frame,frame.texture,0,true)
end
end
local function mouseClick(pdata,state)
mouseHeld = state
highestOrder = 0
for frame in insideFrames:elements() do
if frame.trackClick and not frame.disabled then
highestOrder = math.max(highestOrder,frame.order)
end
end
for frame in insideFrames:elements() do
if frame.order < highestOrder or frame.disabled then goto next end
if state then
if frame.texturePressed then
BlzFrameSetTexture(frame.frame,frame.texturePressed,0,true)
end
if frame.trackClickStart then
frame:trackClickStart()
end
else
if frame_hash:point_inside_object(frame,mouseScreenX,mouseScreenY) then
(frame.pressSound and frame.pressSound or SOUND_UI_PRESS)()
UNPAUSEABLE_TIMER:callDelayed(0.1,afterPress,frame)
if frame.trackClick then
frame:trackClick(pdata)
end
else
afterPress(frame)
end
end
::next::
end
end
Mouse.move_event:add_action(mouseMove)
Mouse.press_event:add_action(mouseClick)
function Frame.Init()
UNPAUSEABLE_TIMER:callPeriodically(0.03125,nil,mouseCheck)
end
end
do
local DEFAULT_MAX_X = 0.8
local DEFAULT_MAX_Y = 0.6
local DEFAULT_ASPECT_RATIO = DEFAULT_MAX_X / DEFAULT_MAX_Y
Resolution = {
min_x = 0.0,
min_y = 0.0,
max_x = DEFAULT_MAX_X,
max_y = DEFAULT_MAX_Y,
}
local last_width
local last_height
local function calculate_extents()
UNPAUSEABLE_TIMER:callDelayed(0.1,calculate_extents)
local width = math.max(BlzGetLocalClientWidth(),1)
local height = math.max(BlzGetLocalClientHeight(),1)
if width ~= last_width or height ~= last_height then
last_width = width
last_height = height
return
end
local new_aspect_ratio = width / height
local new_max_x = DEFAULT_MAX_X * (new_aspect_ratio / DEFAULT_ASPECT_RATIO)
local new_max_y = DEFAULT_MAX_Y * (new_aspect_ratio / DEFAULT_ASPECT_RATIO)
if new_aspect_ratio > DEFAULT_ASPECT_RATIO then
new_min_x = -((new_max_x - DEFAULT_MAX_X) / 2)
new_min_y = 0.0
elseif new_aspect_ratio < DEFAULT_ASPECT_RATIO then
new_min_x = 0.0
new_min_y = -((new_max_y - DEFAULT_MAX_Y) / 2)
end
Resolution.min_x = new_min_x
Resolution.min_y = new_min_y
Resolution.max_x = new_max_x + new_min_x
Resolution.max_y = 0.6
Resolution.ratio = new_aspect_ratio
end
function Resolution.Init()
calculate_extents()
UNPAUSEABLE_TIMER:callDelayed(0.1,calculate_extents)
end
end
do
local DURATION = 1.0
local MAX_Y = 0.05
RegenText = {}
RegenText.__index = RegenText
local regenTextPool = {}
local regenCurrentPool = Set.create()
function RegenText.new()
local self = regenTextPool[#regenTextPool]
local x,y = GetRandomReal(-0.16,0.16), 0
if self == nil then
self = setmetatable(NewTable(), RegenText)
self.frame = BlzCreateFrameByType("TEXT", "name", BlzGetOriginFrame(ORIGIN_FRAME_GAME_UI, 0), "", 0)
BlzFrameSetText(self.frame, "|cff40f040+1|r")
BlzFrameSetEnable(self.frame, false)
unsafeFrameScale(self.frame, 2.0)
BlzFrameSetTextAlignment(self.frame, TEXT_JUSTIFY_CENTER, TEXT_JUSTIFY_MIDDLE)
else
regenTextPool[#regenTextPool] = nil
end
BlzFrameSetPoint(self.frame,FRAMEPOINT_CENTER,UI.Healthbar,FRAMEPOINT_CENTER,x,y)
BlzFrameSetAlpha(self.frame,255)
self.time = 0.0
self.x = x
self.y = y
regenCurrentPool:add(self)
return self
end
local function update_text()
for self in regenCurrentPool:elements() do
self.time = math.min(self.time + DEFAULT_TIMEOUT,DURATION)
local t = easeOutCubic(self.time/DURATION)
BlzFrameSetPoint(self.frame,FRAMEPOINT_CENTER,UI.Healthbar,FRAMEPOINT_CENTER,self.x,self.y+lerp(0.0,MAX_Y,t))
BlzFrameSetAlpha(self.frame,MathRound(lerp(255.0,0.0,t)))
if self.time == DURATION then
table.insert(regenTextPool,self)
regenCurrentPool:remove(self)
end
end
end
function RegenText.Init()
DEFAULT_TIMER:callPeriodically(DEFAULT_TIMEOUT, nil, update_text)
end
end
do
UI = {}
local hp_per = 1.0
local hp_cur = 0.0
local xp_per = 0.000001
local xp_cur = 0.0
local bossHp_cur = 0.0
local bossHp_per = 0.0
function UI.Healthbar_Update(cur_hp,max_hp)
cur_hp = math.max(0.0,cur_hp)
hp_per = cur_hp/max_hp
BlzFrameSetText(UI.HealthbarText, "|cffffb8b8"..tostring(math.floor(cur_hp)).."/"..tostring(math.floor(max_hp)).."|r")
end
function UI.Expbar_Update(cur_xp,max_xp)
xp_per = cur_xp/max_xp
if xp_per < xp_cur then
xp_cur = 0.0
end
BlzFrameSetText(UI.ExpBarText, "|cffd7d9fe"..tostring(math.floor(cur_xp)).."/"..tostring(max_xp).."|r")
end
function UI.update()
hp_cur = lerp(hp_cur,hp_per,0.1)
xp_cur = lerp(xp_cur,xp_per,0.1)
bossHp_cur = lerp(bossHp_cur+0.005,bossHp_per-0.005,0.1)
BlzFrameSetValue(UI.Healthbar,hp_cur)
BlzFrameSetValue(UI.ExpBar,xp_cur)
BlzFrameSetValue(UI.BossBar,bossHp_cur)
BlzFrameSetVisible(UI.BossBar,bossHp_per > 0.0)
end
function UI.Bossbar_Update(cur_hp,max_hp)
cur_hp = math.max(0.0,cur_hp)
bossHp_per = cur_hp/max_hp
end
local function fadeTime(ui,time,total,easing)
time = math.min(time + 0.03125,total)
local t = time/total
if t < 1.0 then
UNPAUSEABLE_TIMER:callDelayed(0.03125,fadeTime, ui,time,total,easing)
end
BlzFrameSetAlpha(ui,math.ceil(lerp(0,255,easing(t))))
end
function UI.FadeIn(ui,time,easing)
BlzFrameSetAlpha(ui,0)
UNPAUSEABLE_TIMER:callDelayed(0.03125, fadeTime,ui,0,time,easing)
end
local defaultVisible = true
function UI.toggle_default()
defaultVisible = not defaultVisible
BlzFrameSetVisible(UI.Healthbar,defaultVisible)
BlzFrameSetVisible(UI.ExpBar,defaultVisible)
BlzFrameSetVisible(UI.HealthbarText,defaultVisible)
BlzFrameSetVisible(UI.ExpBarText,defaultVisible)
BlzFrameSetVisible(UI.LevelText,defaultVisible)
end
local function createCooldown(posX,posY,size)
local adj = size * 0.11
local frame4 = Frame.new({
typeName = "BACKDROP",
parent = ConsoleUIBackdrop,
createContext = 0,
x1 = posX + adj,
y1 = posY - adj,
x2 = posX - adj + size,
y2 = posY + adj - size,
byType = true,
texture = "BlackBackdrop.dds",
enabled = false,
})
local frame3 = Frame.new({
typeName = "BACKDROP",
frameParent = frame4,
createContext = 0,
x1 = frame4.x1,
y1 = frame4.y1,
x2 = frame4.x2,
y2 = frame4.y2,
byType = true,
texture = "ReplaceableTextures\\CommandButtons\\BTNAttack.blp",
enabled = false,
})
local frame1 = Frame.new({
typeName = "STATUSBAR",
frameParent = frame3,
createContext = 0,
x1 = frame3.x1,
y1 = frame3.y1,
x2 = frame3.x2,
y2 = frame3.y2,
byType = true,
texture = "",
model = "UI\\Feedback\\Cooldown\\UI-Cooldown-Indicator.mdx",
text = "",
enabled = false,
})
frame4.cd = frame1
frame4.tx = frame3
local frame2 = Frame.new({
typeName = "BACKDROP",
frameParent = frame1,
createContext = 0,
x1 = posX,
y1 = posY,
x2 = posX + size,
y2 = posY - size,
byType = true,
texture = "cooldownframe.dds",
enabled = false,
})
local frame5 = Frame.new({
typeName = "TEXT",
parent = ConsoleUIBackdrop,
createContext = 1,
x1 = frame2.x1,
y1 = frame2.y1,
x2 = frame2.x2,
y2 = frame2.y2,
byType = true,
text = "",
justifyH = TEXT_JUSTIFY_CENTER,
justifyV = TEXT_JUSTIFY_CENTER,
scale = 1.5,
enabled = false,
})
frame4.txt = frame5
frame5:set_visible(false)
BlzFrameSetMinMaxValue(frame1.frame, 0.0, 1.0)
BlzFrameSetSize(frame1.frame, 0.00001, 0.00001)
unsafeFrameScale(frame1.frame,26.5 * size * 0.8)
frame4:set_visible( false)
return frame4
end
local cooldownPool = {}
local displayedCooldowns = {}
local COOLDOWN_SIZE = 0.035
local COOLDOWN_SIZE_HALF = COOLDOWN_SIZE * 0.5
local COOLDOWN_ORIGIN_X = 0.4
local COOLDOWN_ORIGIN_Y = 0.075
local function updateCooldownDisplay()
local count = #displayedCooldowns
local offset = count * COOLDOWN_SIZE_HALF
for i, f in ipairs(displayedCooldowns) do
local x,y = COOLDOWN_ORIGIN_X-offset + i * COOLDOWN_SIZE-COOLDOWN_SIZE_HALF,COOLDOWN_ORIGIN_Y-COOLDOWN_SIZE_HALF
f:set_center(x,y)
f.txt:set_center(x,y)
end
end
function FreeAttackCooldownIcon(attack)
if attack.cooldownIcon then
local which = attack.cooldownIcon
which:set_visible(false)
which.txt:set_visible(false)
table.insert(cooldownPool,which)
table.removeobject(displayedCooldowns,which)
updateCooldownDisplay()
attack.cooldownIcon = nil
end
end
function AssignCooldownIconToAttack(attack)
local f = table.pop_back(cooldownPool)
local tx = f.tx
local x1,y1,x2,y2 = tx.x1,tx.y1,tx.x2,tx.y2
if getmetatable(attack.abilitySource) == Wearable then
tx.localScaler.y = 1.66666
else
tx.localScaler.y = 1.0
end
BlzFrameSetTexture(tx.frame,attack.icon or "",0,true)
BlzFrameSetValue(f.cd.frame,1.0)
f:set_visible(true)
f.txt:set_visible(true)
f.txt:set_text("")
table.insert(displayedCooldowns,f)
updateCooldownDisplay()
attack.cooldownIcon = f
end
function SetCooldownIconText(attack,text)
local f = attack.cooldownIcon
if f == nil then return end
f.txt:set_text(text)
end
local function MyBarCreate()
for i = 1, 20 do
table.insert(cooldownPool,createCooldown(COOLDOWN_ORIGIN_X-COOLDOWN_SIZE_HALF,COOLDOWN_ORIGIN_Y,COOLDOWN_SIZE))
end
UI.BossBar = BlzCreateSimpleFrame("MyBar", ConsoleUIBackdrop, 0)
BlzFrameSetTexture(BlzGetFrameByName("MyBarBackground",0), "BossBarEmpty.dds", 0,true)
BlzFrameSetAbsPoint(UI.BossBar, FRAMEPOINT_TOPLEFT, 0.200000, 0.598520)
BlzFrameSetAbsPoint(UI.BossBar, FRAMEPOINT_BOTTOMRIGHT, 0.600000, 0.531860)
BlzFrameSetTexture(UI.BossBar, "BossBarFull.dds", 0, true)
BlzFrameSetMinMaxValue(UI.BossBar, 0.0, 1.0)
BlzFrameSetValue(UI.BossBar,0.0)
BlzFrameSetVisible(UI.BossBar,false)
UI.BossBarText = BlzGetFrameByName("MyBarText",0)
BlzFrameSetAbsPoint(UI.BossBarText, FRAMEPOINT_TOPLEFT, 0.249240, 0.594420-0.005)
BlzFrameSetAbsPoint(UI.BossBarText, FRAMEPOINT_BOTTOMRIGHT, 0.564150, 0.550930-0.005)
BlzFrameSetText(UI.BossBarText, "|cffFFCC00Lord of Bones|r")
BlzFrameSetScale(UI.BossBarText, 3.43)
UI.Healthbar = BlzCreateSimpleFrame("MyBar", ConsoleUIBackdrop, 0)
BlzFrameSetText(BlzGetFrameByName("MyBarText",0), "")
BlzFrameSetPoint(UI.Healthbar,FRAMEPOINT_CENTER,ConsoleUIBackdrop,FRAMEPOINT_BOTTOM,0,0.025)
BlzFrameSetTexture(BlzGetFrameByName("MyBarBackground",0), "HealthBarEmpty.dds", 0,true)
BlzFrameSetTexture(UI.Healthbar, "HealthBarFull.dds", 0,true)
BlzFrameSetSize(UI.Healthbar, 0.64, 0.03)
BlzFrameSetMinMaxValue(UI.Healthbar, 0.0, 1.0)
BlzFrameSetValue(UI.Healthbar,0.5)
UI.ExpBar = BlzCreateSimpleFrame("MyBar", ConsoleUIBackdrop, 0)
BlzFrameSetText(BlzGetFrameByName("MyBarText",0), "")
BlzFrameSetPoint(UI.ExpBar,FRAMEPOINT_CENTER,ConsoleUIBackdrop,FRAMEPOINT_BOTTOM,0,0.005)
BlzFrameSetTexture(BlzGetFrameByName("MyBarBackground",0), "exp_bar_empty2", 0,true)
BlzFrameSetTexture(UI.ExpBar, "exp_bar_full2", 0,true)
BlzFrameSetSize(UI.ExpBar, 0.64, 0.01)
BlzFrameSetMinMaxValue(UI.ExpBar, 0.0, 1.0)
BlzFrameSetValue(UI.ExpBar,0.5)
UI.Background = BlzCreateFrameByType("STATUSBAR","",ConsoleUIBackdrop,"",0)
BlzFrameSetAbsPoint(UI.Background,FRAMEPOINT_CENTER,0,0)
BlzFrameSetModel(UI.Background,"background_model3.mdx",0)
BlzFrameSetSize(UI.Background,0.0000001,0.0000001)
BlzFrameSetScale(UI.Background,2.0)
BlzFrameSetLevel(UI.Background,-1)
BlzFrameSetVisible(UI.Background,false)
UI.HealthbarText = BlzCreateFrameByType("TEXT", "name", OriginFrameGameUI, "", 0)
BlzFrameSetPoint(UI.HealthbarText,FRAMEPOINT_CENTER,UI.Healthbar,FRAMEPOINT_CENTER,0,0)
BlzFrameSetText(UI.HealthbarText, "|cffffb8b8100|r")
BlzFrameSetEnable(UI.HealthbarText, false)
unsafeFrameScale(UI.HealthbarText, 1.57)
BlzFrameSetTextAlignment(UI.HealthbarText, TEXT_JUSTIFY_CENTER, TEXT_JUSTIFY_MIDDLE)
UI.ExpBarText = BlzCreateFrameByType("TEXT", "name", OriginFrameGameUI, "", 0)
BlzFrameSetPoint(UI.ExpBarText,FRAMEPOINT_CENTER,UI.ExpBar,FRAMEPOINT_CENTER,0,0)
BlzFrameSetText(UI.ExpBarText, "|cffd7d9fe100|r")
BlzFrameSetEnable(UI.ExpBarText, false)
BlzFrameSetScale(UI.ExpBarText, 1.00)
BlzFrameSetLevel(UI.ExpBarText, 1)
BlzFrameSetTextAlignment(UI.ExpBarText, TEXT_JUSTIFY_CENTER, TEXT_JUSTIFY_MIDDLE)
UI.LevelText = BlzCreateFrameByType("TEXT", "name", OriginFrameGameUI, "", 0)
BlzFrameSetPoint(UI.LevelText,FRAMEPOINT_LEFT,UI.ExpBar,FRAMEPOINT_LEFT,0.0025,-0.005)
BlzFrameSetText(UI.LevelText, "|cffd7d9feLevel 1|r")
BlzFrameSetEnable(UI.LevelText, false)
unsafeFrameScale(UI.LevelText, 1.00)
BlzFrameSetTextAlignment(UI.LevelText, TEXT_JUSTIFY_BOTTOM, TEXT_JUSTIFY_LEFT)
end
function UI.Init()
ConsoleUIBackdrop = BlzGetFrameByName("ConsoleUIBackdrop", 0)
OriginFrameGameUI = BlzGetOriginFrame(ORIGIN_FRAME_GAME_UI, 0)
BlzLoadTOCFile("war3mapImported\\MyBar.TOC")
-- Hide most default UI
BlzHideOriginFrames(true)
-- Get Inventory, Group Selection, Item on Ground, Trainer Back but still hide Single Unit selection.
local parent = BlzCreateFrameByType("SIMPLEFRAME", "", BlzGetFrameByName("ConsoleUI", 0), "", 0)
BlzFrameSetVisible(parent, false)
BlzFrameSetVisible(BlzFrameGetParent(BlzGetFrameByName("SimpleInfoPanelUnitDetail", 0)), true)
BlzFrameSetParent(BlzGetFrameByName("SimpleInfoPanelUnitDetail", 0), parent)
-- Show Quest/Menu/Alli/Chat
BlzFrameSetVisible(BlzGetFrameByName("UpperButtonBarFrame", 0), true)
BlzFrameSetVisible(BlzGetFrameByName("UpperButtonBarQuestsButton", 0),false)
BlzFrameSetVisible(BlzGetFrameByName("UpperButtonBarAlliesButton", 0),false)
BlzFrameSetVisible(BlzGetFrameByName("UpperButtonBarChatButton", 0),false)
BlzFrameSetAbsPoint(BlzGetFrameByName("UpperButtonBarMenuButton", 0), FRAMEPOINT_TOPLEFT, 0.85, 0.6)
-- Show Gold/Lumber/Food
local ResourceBar = BlzGetFrameByName("ResourceBarFrame", 0)
local GoldText = BlzGetFrameByName("ResourceBarGoldText", 0)
local LumberText = BlzGetFrameByName("ResourceBarLumberText", 0)
local FoodText = BlzGetFrameByName("ResourceBarSupplyText", 0)
local UpkeepText = BlzGetFrameByName("ResourceBarUpkeepText", 0)
for i = 0, 23, 1 do
BlzFrameSetVisible(BlzGetFrameByName("ResourceBarGoldIcon", i),false)
end
BlzFrameClearAllPoints(GoldText)
BlzFrameSetAbsPoint(GoldText, FRAMEPOINT_BOTTOMLEFT, -999.0, -999.0)
BlzFrameClearAllPoints(LumberText)
BlzFrameSetAbsPoint(LumberText, FRAMEPOINT_BOTTOMLEFT, -999.0, -999.0)
BlzFrameClearAllPoints(FoodText)
BlzFrameSetAbsPoint(FoodText, FRAMEPOINT_BOTTOMLEFT, -999.0, -999.0)
BlzFrameClearAllPoints(UpkeepText)
BlzFrameSetAbsPoint(UpkeepText, FRAMEPOINT_BOTTOMLEFT, -999.0, -999.0)
-- Show HeroButtons
BlzFrameSetVisible(BlzFrameGetParent(BlzGetOriginFrame(ORIGIN_FRAME_HERO_BUTTON, 0)), false)
-- Reforged Stuff
-- "Hide" Black Bottom Background
BlzFrameSetSize(BlzGetFrameByName("ConsoleUIBackdrop",0), 0, 0.0001)
-- "Hide" Inventory Cover
BlzFrameSetAlpha( BlzGetFrameByName("SimpleInventoryCover", 0), 0)
-- Hide Mouse Dead Zone at Command Bar
BlzFrameSetVisible(BlzFrameGetChild(BlzGetFrameByName("ConsoleUI", 0), 5), false)
-- Show Idle Worker
BlzFrameSetVisible(BlzFrameGetChild(BlzGetFrameByName("ConsoleUI", 0), 7), false)
-- Show Day Time Clock
BlzFrameSetVisible(BlzFrameGetChild(BlzFrameGetChild(BlzGetOriginFrame(ORIGIN_FRAME_GAME_UI, 0), 5),0), false)
BlzFrameSetVisible(BlzGetFrameByName("ResourceBarFrame",0),true)
BlzFrameSetVisible(BlzGetFrameByName("ConsoleTopBar", 0), false)
BlzFrameSetVisible(BlzGetFrameByName("ConsoleBottomBar", 0), false)
BlzFrameSetVisible(BlzGetFrameByName("ConsoleBottomBarOverlay", 0), false)
MyBarCreate()
DEFAULT_TIMER:callPeriodically(0.03125, nil, UI.update)
UI.CreateLevelUp()
UI.AbilitySelectionInit()
RegenText.Init()
Equipment.Init()
InitTooltip()
UI.ChooseItemInit()
UI.HoverInit()
end
end
do
local HEALTHBAR_FULL = "UnitHealthbarFull.dds"
local HEALTHBAR_EMPTY = "UnitHealthbarEmpty.dds"
function normalizeRange(r, min_range, max_range)
return min_range + (max_range - min_range) * r
end
function UI.HoverShow(state)
UI.UnitHealthBar:set_visible(state)
end
function UI.HoverInit()
local curPosX = 0.0
local curPosY = 0.0
local lastTarget = {}
local lastSFX = 0
local alpha = 0
local percentLast = 0
UI.UnitHealthBar = Frame.new({
typeName = "MyBar",
simpleFrame = true,
parent = ConsoleUIBackdrop,
createContext = 0,
x1 = 0.2,
y1 = 0.59,
x2 = 0.3,
y2 = 0.57,
texture = HEALTHBAR_FULL,
})
BlzFrameSetTexture(BlzGetFrameByName("MyBarBackground",0), HEALTHBAR_EMPTY, 0,true)
BlzFrameSetMinMaxValue(UI.UnitHealthBar.frame, 0.0, 1.0)
BlzFrameSetValue(UI.UnitHealthBar.frame,0.5)
UI.UnitHealthBarText = BlzGetFrameByName("MyBarText",0)
BlzFrameSetFont(UI.UnitHealthBarText,"fonts\\BLQ55Web.ttf",0.011,0)
BlzFrameSetText(UI.UnitHealthBarText, "|cffFFCC00Lord of Boners|r")
BlzFrameSetScale(UI.UnitHealthBarText, 0.7)
local function orb_iter(u,attack,x,y,list,range)
if Types.is(u,Types.Damageable) and list[u] == nil and IsInRange(x,y,u.position[1],u.position[2],range+u.half_size) then
list[u] = true
attack:deal_damage(u)
end
end
function UI.HoverUpdate()
local pos = PlayerDataLocal.mouse_pos
local next = unit_collision_hash:get_first(pos[1]-64, pos[2]-64, 128, 128,Types.Damageable)
if not next and lastTarget.sfx == lastSFX then
next = lastTarget
end
if next then
pos = next.position
local x,y = World2Screen( pos[1], pos[2]-64, 0 )
local p = next.health / next.health_max
BlzFrameSetText(UI.UnitHealthBarText, next.name)
if next ~= lastTarget or DistanceTo(x,y,curPosX,curPosY) > 0.4 then
curPosX, curPosY = x, y
percentLast = p
end
curPosX = lerp(curPosX,x,0.5)
curPosY = lerp(curPosY,y,0.5)
percentLast = lerp(percentLast,p,0.15)
BlzFrameSetValue(UI.UnitHealthBar.frame,normalizeRange(percentLast, 0.05, 0.95))
alpha = MathRound(lerp(alpha,255,0.15))
UI.UnitHealthBar:set_center(curPosX,curPosY)
lastTarget = next
lastSFX = next.sfx
else
alpha = MathRound(lerp(alpha,0,0.3))
end
BlzFrameSetAlpha(UI.UnitHealthBar.frame,alpha)
end
BlzFrameSetAlpha(UI.UnitHealthBar.frame,0)
DEFAULT_TIMER:callPeriodically(DEFAULT_TIMEOUT, nil, UI.HoverUpdate)
end
end
do
Potions = {}
local function showPotionTooltip(frame,state)
if state then
SetTooltipTarget(frame,frame.potionName,frame.potionTooltip)
else
HideTooltip()
end
end
function Potions.update(frame)
frame.stack = math.max(math.min(frame.stack,frame.maxStack),0)
frame:set_disabled(frame.stack <= 0)
BlzFrameSetText(frame.stackFrame.frame,tostring(frame.stack) .. " / " .. tostring(frame.maxStack))
end
local potionNames = {}
function Potions.reset(pdata)
for ind, frame in ipairs(potionNames) do
local name = frame.potionName
frame.stack = pdata[name]
Potions.update(frame)
end
end
function Potions.increment(pdata,frame)
local name = frame.potionName
pdata[name] = math.min(pdata[name] + 1,frame.maxStack)
frame.stack = frame.stack + 1
Potions.update(frame)
end
function Potions.newButton(parent,x1,y1,x2,y2, texture, name, tooltip,max,action)
AddSaveKey(name)
local frame1 = Frame.new({
typeName = "BACKDROP",
frameParent = parent,
createContext = 0,
x1 = x1,
y1 = y1,
x2 = x2,
y2 = y2,
byType = true,
texture = "ItemEmptyLarge.dds",
trackHover = showPotionTooltip,
trackClick = action,
pressSound = SOUND_POTION,
})
local frame2 = Frame.new({
typeName = "BACKDROP",
frameParent = frame1,
createContext = 0,
x1 = x1+0.005,
y1 = y1-0.005,
x2 = x2-0.005,
y2 = y2+0.005,
byType = true,
texture = texture,
})
frame1.stackFrame = Frame.new({
typeName = "TEXT",
frameParent = frame1,
createContext = 1,
x1 = x1,
y1 = y1,
x2 = x2,
y2 = y2+0.005,
byType = true,
text = "0/1",
justifyH = TEXT_JUSTIFY_CENTER,
justifyV = TEXT_JUSTIFY_BOTTOM,
scale = 1.5,
enabled = false,
})
local frame3 = Frame.new({
typeName = "BACKDROP",
frameParent = frame1.stackFrame,
createContext = 0,
x1 = x1,
y1 = y1,
x2 = x2,
y2 = y2,
texture="alphatexture.dds",
textureHover = "human-multipleselection-border-large.dds",
texturePressed = "human-multipleselection-border-large-pressed.dds",
trackHover = true,
byType = true,
})
frame1.icon = frame2
frame1.hoverIcon = frame3
frame1.potionName = name
frame1.potionTooltip = tooltip
frame1.maxStack = max
frame1.stack = 0
Potions.update(frame1)
table.insert(potionNames,frame1)
return frame1
end
player_load_event:add_action(function(pdata)
for ind, frame in ipairs(potionNames) do
local name = frame.potionName
frame.stack = pdata[name]
Potions.update(frame)
end
end)
end
do
local uiAbilities = {}
local can_select_ability = false
function UI.endAbilitySelect(pdata)
UI.AbilitySelectBackground:set_visible(false)
GameAction.LockPause(false)
GameAction.Pause(false)
end
function UI.get_max_abilities()
return #uiAbilities
end
function UI.hide_abilities()
for _, trait in ipairs(uiAbilities) do
trait:set_visible(false)
end
end
function UI.GetAbilityByIndex(ind)
return uiAbilities[ind]
end
function UI.AbilityDisplay(self,whichAbil)
BlzFrameSetTexture(self.icon.frame,whichAbil.icon,0,true)
self.index = nil
if type(whichAbil.name) == "table" then
self.index = GetRandomInt(1,#whichAbil.name)
local i = self.index
BlzFrameSetText(self.description.frame,"|cfff55e21"..string.upper(whichAbil.name[i])..":|r|n"..whichAbil.description[i])
else
BlzFrameSetText(self.description.frame,"|cfff55e21"..string.upper(whichAbil.name)..":|r|n"..whichAbil.description)
end
self:set_visible(true)
self.abil = whichAbil
end
function UI.AbilitySelected(uiabil,pdata)
if not can_select_ability then return end
can_select_ability = false
local abil = uiabil.abil
abil:apply(pdata.playerUnit,uiabil.index)
UNPAUSEABLE_TIMER:callDelayed(0.1,UI.endAbilitySelect,pdata)
UI.hide_abilities()
end
function UI.AbilityNew(x1,y1,x2,y2)
local frame1 = Frame.new({
typeName = "BACKDROP",
frameParent = UI.AbilitySelectBackground,
createContext = 0,
x1 = x1,
y1 = y1,
x2 = x2,
y2 = y2,
byType = true,
texture = "AbilitySelectionBackdrop.dds",
trackHover = true,
trackClick = UI.AbilitySelected,
})
frame1.icon = Frame.new({
typeName = "BACKDROP",
frameParent = frame1,
createContext = 0,
x1 = x1+0.035,
y1 = y1-0.035,
x2 = x2-0.285,
y2 = y2+0.035,
byType = true,
texture = "AbilityAstronomersOrbs.dds",
})
local frame2 = Frame.new({
typeName = "BACKDROP",
frameParent = frame1,
createContext = 0,
x1 = x1,
y1 = y1,
x2 = x2,
y2 = y2,
texture="alphatexture.dds",
textureHover = "buttonHover.dds",
texturePressed = "buttonPress.dds",
trackHover = true,
byType = true,
})
frame1.description = Frame.new({
typeName = "TEXT",
frameParent = frame1,
createContext = 1,
x1 = x1+0.13208,
y1 = y1-0.01855,
x2 = x2-0.01792,
y2 = y2+0.02145,
byType = true,
text = "|cffd6d6d6Eats ass for 69 damage|r",
justifyH = TEXT_JUSTIFY_LEFT,
justifyV = TEXT_JUSTIFY_CENTER,
scale = 1.14,
enabled = false,
})
frame1.border = Frame.new({
typeName = "BACKDROP",
frameParent = frame1.icon,
createContext = 0,
x1 = frame1.icon.x1-0.015,
y1 = frame1.icon.y1+0.015,
x2 = frame1.icon.x2+0.015,
y2 = frame1.icon.y2-0.015,
byType = true,
texture = "AbilityBorder.dds",
})
table.insert(uiAbilities,frame1)
return frame1
end
function UI.AbilitySelectionInit()
UI.AbilitySelectBackground = Frame.new({
typeName = "BACKDROP",
parent = BlzGetOriginFrame(ORIGIN_FRAME_GAME_UI, 0),
createContext = 0,
x1 = -0.13379,
y1 = 0.60223,
x2 = 0.93602,
y2 = 0,
byType = true,
texture = "alphatexture.dds",
})
UI.AbilitySelectBackground:set_visible(false)
Frame.new({
typeName = "TEXT",
frameParent = UI.AbilitySelectBackground,
createContext = 1,
x1 = UI.AbilitySelectBackground.x1+0.19,
y1 = UI.AbilitySelectBackground.y1+0.60201,
x2 = UI.AbilitySelectBackground.x2+0.69,
y2 = UI.AbilitySelectBackground.y2+0.54201,
byType = true,
text = "|cffFFCC00Choose An Ability!|r",
justifyH = TEXT_JUSTIFY_CENTER,
justifyV = TEXT_JUSTIFY_MIDDLE,
scale = 3.86,
enabled = false,
})
UI.AbilitySelectBackground:set_visible(false)
local firstAbility = UI.AbilityNew(0.240000, 0.520000,0.640000, 0.370000)
UI.AbilityNew(0.240000, 0.360000,0.640000, 0.210000)
UI.AbilityNew(0.239190, 0.202490,0.639190, 0.0524900)
Renewal_PotionButton = Potions.newButton(UI.AbilitySelectBackground,firstAbility.x1-0.1, firstAbility.y1, firstAbility.x1, firstAbility.y1 - 0.15,
"IconPotion3.dds",
"Potion of Renewal",
"Rerolls the complete selection of abilties.",
3,
function(frame,pdata)
frame.stack = frame.stack - 1
Potions.update(frame)
ShowAbilities(pdata.playerUnit)
end
)
UI.hide_abilities()
end
local function enableSelection()
can_select_ability = true
end
local lastShown = 0.0
function UI.ShowAbilitySelect(playerUnit)
can_select_ability = false
GameAction.Pause(true)
GameAction.LockPause(true)
UI.AbilitySelectBackground:set_visible(true)
ShowAbilities(playerUnit)
if lastShown < GAME_TIME then
lastShown = GAME_TIME + 1.5
UI.FadeIn(UI.AbilitySelectBackground.frame,1.5,easeOutCubic)
end
UNPAUSEABLE_TIMER:callDelayed(0.75,enableSelection)
end
end
do
local can_select_trait = false
function UI.endLevelUp(pdata)
BlzFrameSetVisible(UI.LevelUpBackground.frame,false)
GameAction.LockPause(false)
GameAction.Pause(false)
end
function UI.TraitSelected(traitframe,pdata)
if not can_select_trait then return end
local multiplyer = traitframe.multiplyer
if Potions.ActivePotion == Oblivion_PotionButton then
traitframe:set_visible(false)
TraitPool:remove(traitframe.trait)
MemoryTraitPool:remove(traitframe.trait)
FillTraitIndex(pdata.playerUnit, traitframe.traitIndex)
Potions.ActivePotion = nil
Potions.update(Oblivion_PotionButton)
return
elseif Potions.ActivePotion == Memory_PotionButton then
traitframe:set_visible(false)
MemoryTraitPool:add(traitframe.trait)
FillTraitIndex(pdata.playerUnit, traitframe.traitIndex,true)
Potions.ActivePotion = nil
Potions.update(Memory_PotionButton)
return
elseif Potions.ActivePotion == Reverberant_PotionButton then
multiplyer = multiplyer * 2
Potions.ActivePotion = nil
Potions.update(Reverberant_PotionButton)
end
can_select_trait = false
local trait = traitframe.trait
trait:apply(pdata.playerUnit,multiplyer,traitframe.multi_trait)
for attack in all_attacks:elements() do
attack.update_func(attack)
end
UNPAUSEABLE_TIMER:callDelayed(0.1,UI.endLevelUp,pdata)
UI.hide_traits()
end
local traits = {}
function UI.get_max_traits()
return #traits
end
function UI.hide_traits()
for _, trait in ipairs(traits) do
trait:set_visible(false)
end
end
function UI.GetTraitByIndex(ind)
return traits[ind]
end
function UI.TraitDisplay(self,whichTrait)
BlzFrameSetTexture(self.border.frame, "classtrait_border.dds", 0, true)
local icon = whichTrait.icon
self.multiplyer = 1
self.multi_trait = nil
if whichTrait.multi_trait then
local which = GetRandomInt(1,whichTrait.multi_trait)
local subname = whichTrait["subname"..tostring(which)]
local name = whichTrait.name.." "..subname
local desc = whichTrait["descriptions"..tostring(which)]
if type(desc) == "table" then
desc = desc[whichTrait.level]
end
if type(icon) == "table" then
icon = icon[which]
end
self.multi_trait = which
BlzFrameSetText(self.description.frame,name.." "..ToRomanNumerals(whichTrait.level)..":|n"..desc)
elseif whichTrait.ability == nil then
BlzFrameSetTexture(self.border.frame, "trait_border.dds", 0, true)
BlzFrameSetText(self.description.frame,whichTrait.name.." "..ToRomanNumerals(whichTrait.level)..":|n"..whichTrait.description)
else
BlzFrameSetTexture(self.border.frame, "abiltrait_border.dds", 0, true)
if whichTrait.x2 ~= nil and whichTrait.ability.level %% whichTrait.x2 == 0.0 then
self.multiplyer = 2
BlzFrameSetText(self.description.frame,"|cff00ffff"..whichTrait.ability.name.." "..ToRomanNumerals(whichTrait.ability.level).." ("..whichTrait.name..") x2:|r|n"..whichTrait.description)
else
BlzFrameSetText(self.description.frame,whichTrait.ability.name.." "..ToRomanNumerals(whichTrait.ability.level).." ("..whichTrait.name.."):|n"..whichTrait.description)
end
end
BlzFrameSetTexture(self.icon.frame,icon,0,true)
BlzFrameSetVisible(self.frame,true)
self.trait = whichTrait
end
function UI.TraitNew(x1,y1,x2,y2)
local frame1 = Frame.new({
typeName = "BACKDROP",
frameParent = UI.LevelUpBackground,
createContext = 0,
x1 = x1,
y1 = y1,
x2 = x2,
y2 = y2,
byType = true,
texture = "ui_select_option_base.dds",
trackHover = true,
trackClick = UI.TraitSelected,
})
frame1.icon = Frame.new({
typeName = "BACKDROP",
frameParent = frame1,
createContext = 0,
x1 = x1+0.03,
y1 = y1-0.03,
x2 = x2-0.41,
y2 = y2+0.035,
byType = true,
texture = "TraitRange.dds",
})
local frame2 = Frame.new({
typeName = "BACKDROP",
frameParent = frame1,
createContext = 0,
x1 = x1,
y1 = y1,
x2 = x2,
y2 = y2,
texture="alphatexture.dds",
textureHover = "buttonHover.dds",
texturePressed = "buttonPress.dds",
trackHover = true,
byType = true,
})
frame1.description = Frame.new({
typeName = "TEXT",
frameParent = frame1,
createContext = 1,
x1 = x1+0.1+.05,
y1 = y1-0.0175,
x2 = x2-0.02+.05,
y2 = y2+0.0175,
byType = true,
text = "|cffd6d6d6Eats ass for 69 damage|r",
justifyH = TEXT_JUSTIFY_LEFT,
justifyV = TEXT_JUSTIFY_TOP,
scale = 1.57,
enabled = false,
})
frame1.border = Frame.new({
typeName = "BACKDROP",
frameParent = frame1.icon,
createContext = 0,
x1 = frame1.icon.x1-0.0275,
y1 = frame1.icon.y1+0.0275,
x2 = frame1.icon.x2+0.0325,
y2 = frame1.icon.y2-0.0325,
byType = true,
texture = "trait_border.dds",
})
table.insert(traits,frame1)
frame1.traitIndex = #traits
return frame1
end
function UI.CreateLevelUp()
UI.LevelUpBackground = Frame.new({
typeName = "BACKDROP",
parent = OriginFrameGameUI,
createContext = 0,
x1 = -0.13379,
y1 = 0.60223,
x2 = 0.93602,
y2 = 0,
byType = true,
texture = "alphatexture.dds",
})
UI.LevelUpBackground:set_visible(false)
Frame.new({
typeName = "TEXT",
frameParent = UI.LevelUpBackground,
createContext = 1,
x1 = 0.19,
y1 = 0.60201+0.01,
x2 = 0.69,
y2 = 0.54201+0.01,
byType = true,
text = "|cffFFCC00Level Up|r",
justifyH = TEXT_JUSTIFY_CENTER,
justifyV = TEXT_JUSTIFY_MIDDLE,
scale = 3.86,
enabled = false,
})
local offsetY = 0.02
local firstTrait = UI.TraitNew(0.190000, 0.535000+offsetY,0.690000, 0.410000+offsetY)
local secondTrait = UI.TraitNew(0.190000, 0.405000+offsetY,0.690000, 0.280000+offsetY)
local thirdTrait = UI.TraitNew(0.190000, 0.275000+offsetY,0.690000, 0.150000+offsetY)
local fourthTrait = UI.TraitNew(0.190000, 0.145000+offsetY,0.690000, 0.0200000+offsetY)
UI.hide_traits()
StrongWine_PotionButton = Potions.newButton(UI.LevelUpBackground,firstTrait.x1-0.0833, firstTrait.y1, firstTrait.x1, firstTrait.y1 - 0.125,
"IconPotion5.dds",
"Strong Wine",
"Rerolls the complete selection of traits.",
10,
function(frame,pdata)
frame.stack = frame.stack - 1
Potions.update(frame)
ShowTraits(pdata.playerUnit)
end
)
Oblivion_PotionButton = Potions.newButton(UI.LevelUpBackground,secondTrait.x1-0.0833, secondTrait.y1, secondTrait.x1, secondTrait.y1 - 0.125,
"IconPotion1.dds",
"Potion of Oblivion",
"Select a trait when used and that trait and all its follow up traits will never be shown again in this run. You can still choose a trait afterwards.",
8,
function(frame,pdata)
if Potions.ActivePotion ~= nil then return end
frame.stack = frame.stack - 1
Potions.update(frame)
frame:set_disabled(true)
Potions.ActivePotion = frame
end
)
Memory_PotionButton = Potions.newButton(UI.LevelUpBackground,thirdTrait.x1-0.0833, thirdTrait.y1, thirdTrait.x1, thirdTrait.y1 - 0.125,
"IconPotion6.dds",
"Potion of Memories",
"Select a trait when used and that trait will stay in the selection until you take it. You can still choose a trait afterwards.",
8,
function(frame,pdata)
if Potions.ActivePotion ~= nil then return end
frame.stack = frame.stack - 1
Potions.update(frame)
frame:set_disabled(true)
Potions.ActivePotion = frame
end
)
Reverberant_PotionButton = Potions.newButton(UI.LevelUpBackground,fourthTrait.x1-0.0833, fourthTrait.y1, fourthTrait.x1, fourthTrait.y1 - 0.125,
"IconPotion4.dds",
"Reverberant Tinkture",
"Select a trait when used and that trait will be applied twice.",
3,
function(frame,pdata)
if Potions.ActivePotion ~= nil then return end
frame.stack = frame.stack - 1
Potions.update(frame)
frame:set_disabled(true)
Potions.ActivePotion = frame
end
)
end
local function enableSelection()
can_select_trait = true
end
local lastShown = 0
function UI.ShowLevelUpUI(playerUnit)
if getmetatable(playerUnit) ~= PlayerUnit then return end
can_select_trait = false
GameAction.Pause(true)
GameAction.LockPause(true)
UI.LevelUpBackground:set_visible(true)
ShowTraits(playerUnit)
if lastShown < GAME_TIME then
lastShown = GAME_TIME + 1.5
UI.FadeIn(UI.LevelUpBackground.frame,1.5,easeOutCubic)
end
UNPAUSEABLE_TIMER:callDelayed(0.75,enableSelection)
end
end
do
Equipment = {}
local slots = Set.create()
function Equipment.set_visible(state)
if state then
LerpFramePosition(Equipment.backdrop, 0.00022, 0.60223, 0.80202,0,0.5,easeOutBack)
LerpFrameAlpha(Equipment.backdrop,0,255,0.5,easeInCubic,true)
else
LerpFramePosition(Equipment.backdrop, 0.00022, 1.60223, 0.80202,1,0.5,easeInBack)
LerpFrameAlpha(Equipment.backdrop,255,0,0.5,easeOutCubic,true)
end
end
local lastTooltipTarget
local function showItemTooltip(frame,state)
if lastTooltipTarget and lastTooltipTarget.exit then
lastTooltipTarget.exit:set_visible(false)
end
if frame.exit then
frame.exit:set_visible(state and frame.wearable)
end
if state and frame.wearable then
lastTooltipTarget = frame
SetTooltipTarget(frame,frame.wearable.name,frame.wearable.description)
else
HideTooltip()
end
end
local function hoverDestroy(frame,state)
frame.clickCount = 0
if lastTooltipTarget == frame.destroyTarget and frame.destroyTarget.wearable then
SetTooltipTarget(frame.destroyTarget,frame.destroyTarget.wearable.name,frame.destroyTarget.wearable.description)
end
end
function GetEquipedItems()
local list = NewTable()
for frame in slots:elements() do
if frame.type ~= nil and frame.wearable ~= nil then
table.insert(list,frame.wearable)
end
end
return list
end
function GetOpenInventorySlot(whichType,ignoreEquipment)
for frame in slots:elements() do
if ignoreEquipment then
if frame.type == nil and frame.wearable == nil then
return frame
end
elseif (frame.type == nil or frame.type == whichType) and frame.wearable == nil then
return frame
end
end
return false
end
function GetSlotByType(whichType)
local valid
for frame in slots:elements() do
if frame.type == whichType then
if not valid or frame.wearable == nil then
valid = frame
end
end
end
return valid
end
function SlotGainWearable(pdata,slot,wearable)
slot.wearable = wearable
if slot.type == wearable.type and slot.type then
slot.wearable:equip(pdata.playerUnit,slot)
end
BlzFrameSetTexture(slot.icon.frame,wearable.icon,0,true)
end
function GainWearable(pdata,wearable)
pdata = pdata or PlayerDataLocal
if wearable.noSlotUse then
wearable:action(pdata)
wearable:destroy()
else
local slot = GetOpenInventorySlot(wearable.type)
if slot then
SlotGainWearable(PlayerDataLocal,slot,wearable)
else
wearable:destroy()
end
end
end
function DestroyAllEquipment()
for frame in slots:elements() do
if frame.wearable then
frame.wearable:destroy()
end
end
end
function SlotRemoveWearable(slot)
if not slot.wearable then return end
slot.wearable:unequip()
slot.wearable = nil
BlzFrameSetTexture(slot.icon.frame,slot.icon.texture,0,true)
end
function SlotDestroyWearable(slot)
if slot.wearable ~= nil and slot.wearable.destroy ~= nil then
slot.wearable:destroy()
end
slot.wearable = nil
BlzFrameSetTexture(slot.icon.frame,slot.icon.texture,0,true)
showItemTooltip(slot,true)
end
function SlotClearWearable(slot)
slot.wearable = nil
BlzFrameSetTexture(slot.icon.frame,slot.icon.texture,0,true)
end
local function click(frame,pdata)
BlzFrameSetFocus(frame.frame,false)
if frame.wearable then
local wearable = frame.wearable
if wearable.noSlotUse then
GainWearable(pdata,wearable)
UI.EndChooseItem()
elseif frame.type or frame.chooseSlot then
local slot = frame.chooseSlot and GetOpenInventorySlot(wearable.type,false) or GetOpenInventorySlot(frame.type,true)
if slot then
SlotRemoveWearable(frame)
SlotGainWearable(pdata,slot,wearable)
if frame.chooseSlot then
UI.EndChooseItem()
end
end
else
local slot = GetSlotByType(wearable.type)
local slotWearable = slot.wearable
SlotRemoveWearable(frame)
if slotWearable then
SlotRemoveWearable(slot)
SlotGainWearable(pdata,frame,slotWearable)
end
SlotGainWearable(pdata,slot,wearable)
end
end
showItemTooltip(frame,true)
end
local function destroyClick(frame,pdata)
frame.clickCount = (frame.clickCount or 0) + 1
if frame.clickCount >= 2 then
SlotDestroyWearable(frame.destroyTarget)
frame.clickCount = 0
else
SetTooltipTarget(frame.destroyTarget,"|cffff0000CLICK AGAIN TO DELETE: "..frame.destroyTarget.wearable.name.."|r",frame.destroyTarget.wearable.description)
end
end
function Equipment.newButton(parent,x1,y1,x2,y2, texture,large,whichType,invalidSlot)
local frame1 = Frame.new({
typeName = "BACKDROP",
frameParent = parent,
createContext = 0,
x1 = x1,
y1 = y1,
x2 = x2,
y2 = y2,
byType = true,
texture = (large and "ItemEmptyLarge.dds" or "ItemEmptySmall.dds"),
trackHover = showItemTooltip,
trackClick = click,
type = whichType,
})
local frame2 = Frame.new({
typeName = "BACKDROP",
frameParent = frame1,
createContext = 0,
x1 = x1+0.005,
y1 = y1-0.005 + (large and 0 or 0.025),
x2 = x2-0.005,
y2 = y2+0.005 + (large and 0 or -0.025),
byType = true,
texture = texture,
})
local frame3 = Frame.new({
typeName = "BACKDROP",
frameParent = frame2,
createContext = 0,
x1 = x1,
y1 = y1,
x2 = x2,
y2 = y2,
texture="alphatexture.dds",
textureHover = (large and "human-multipleselection-border-large.dds" or "_hd.w3mod:ui\\widgets\\console\\human\\commandbutton\\human-multipleselection-border.dds"),
texturePressed = (large and "human-multipleselection-border-large-pressed.dds" or "human-multipleselection-border-pressed.dds"),
trackHover = true,
byType = true,
})
if not invalidSlot then
slots:add(frame1)
frame1.exit = Frame.new({
typeName = "BACKDROP",
byType = true,
frameParent = frame3,
createContext = 0,
x1 = x2 - 0.03,
y1 = y1,
x2 = x2,
y2 = y1 - 0.03,
trackClick = destroyClick,
trackHover = hoverDestroy,
texture = "UICancelNormal.dds",
textureHover = "UICancelHover.dds",
texturePressed = "UICancelPressed.dds",
visible = false
})
frame1.exit.destroyTarget = frame1
end
frame1.icon = frame2
frame1.hoverIcon = frame3
return frame1
end
local function moveFrame(frame, x1, y1, x2, y2, x3, y3, x4, y4, dur, durMax,easing)
dur = math.min(dur + 0.03125,durMax)
local t = dur/durMax
if easing then
t = easing(t)
end
frame:set_position(lerp(x1,x3,t), lerp(y1,y3,t),lerp(x2,x4,t), lerp(y2,y4,t))
if dur ~= durMax then
UNPAUSEABLE_TIMER:callDelayed(0.03125,moveFrame,frame, x1, y1, x2, y2, x3, y3, x4, y4, dur, durMax,easing)
else
BlzFrameSetParent(frame.frame,frame.parent)
end
end
function LerpFramePosition(frame, x1, y1,x2, y2, dur, easing)
BlzFrameSetParent(frame.frame,ConsoleUIBackdrop)
UNPAUSEABLE_TIMER:callDelayed(0.03125,moveFrame,frame,frame.x1,frame.y1,frame.x2,frame.y2,x1,y1,x2,y2,0,dur,easing)
end
local function alphaFrame(frame,startAlpha,endAlpha,dur,durMax,easing,disableAtZero)
dur = math.min(dur + 0.03125,durMax)
local t = dur/durMax
if easing then
t = easing(t)
end
local alpha = MathRound(lerp(startAlpha,endAlpha,t))
BlzFrameSetAlpha(frame.frame,alpha)
if disableAtZero then
BlzFrameSetVisible(frame.frame,alpha>0)
BlzFrameSetEnable(frame.frame, alpha >0)
end
if dur ~= durMax then
UNPAUSEABLE_TIMER:callDelayed(0.03125,alphaFrame,frame,startAlpha,endAlpha,dur,durMax,easing,disableAtZero)
end
end
function LerpFrameAlpha(frame,startAlpha,endAlpha,dur,easing,disableAtZero)
BlzFrameSetAlpha(frame.frame,startAlpha)
UNPAUSEABLE_TIMER:callDelayed(0.03125,alphaFrame,frame,startAlpha,endAlpha,0,dur,easing,disableAtZero)
end
Equipment.Init = function()
Equipment.backdrop = Frame.new({
typeName = "BACKDROP",
parent = ConsoleUIBackdrop,
createContext = 1,
x1 = 0.000220000,
y1 = 0.602230,
x2 = 0.802020,
y2 = 0.00000,
byType = true,
texture = "alphatexture.dds",
})
Equipment.backdrop.parent = OriginFrameGameUI
local offsetX = 0.1225
local offsetY = 0.01
Equipment.newButton(Equipment.backdrop,0.150000, 0.360000,0.250000, 0.210000, "EquipmentSlotChest.dds",true,Wearable.Type.CHEST)
Equipment.newButton(Equipment.backdrop,0.150000, 0.560000,0.250000, 0.460000, "EquipmentSlotHelmet.dds",false,Wearable.Type.HEADWEAR)
Equipment.newButton(Equipment.backdrop,0.150000, 0.460000,0.250000, 0.360000, "EquipmentSlotNecklace.dds",false,Wearable.Type.NECKLACE)
Equipment.newButton(Equipment.backdrop,0.0500000, 0.390000,0.150000, 0.240000, "EquipmentSlotBoots.dds",true,Wearable.Type.BOOTS)
Equipment.newButton(Equipment.backdrop,0.250000, 0.390000,0.350000, 0.240000, "EquipmentSlotGloves.dds",true,Wearable.Type.GLOVES)
Equipment.newButton(Equipment.backdrop,0.250000, 0.490000,0.350000, 0.390000, "EquipmentSlotRing.dds",false,Wearable.Type.RING)
Equipment.newButton(Equipment.backdrop,0.0500000, 0.490000,0.150000, 0.390000, "EquipmentSlotRing.dds",false,Wearable.Type.RING)
offsetY = 0.04
Equipment.newButton(Equipment.backdrop,0.00000+offsetX, 0.150000+offsetY,0.100000+offsetX, 0.00000+offsetY, "alphatexture.dds",true)
Equipment.newButton(Equipment.backdrop,0.100000+offsetX, 0.150000+offsetY,0.200000+offsetX, 0.00000+offsetY, "alphatexture.dds",true)
Equipment.newButton(Equipment.backdrop,0.200000+offsetX, 0.150000+offsetY,0.300000+offsetX, 0.00000+offsetY, "alphatexture.dds",true)
Equipment.newButton(Equipment.backdrop,0.300000+offsetX, 0.150000+offsetY,0.400000+offsetX, 0.00000+offsetY, "alphatexture.dds",true)
Frame.new({
typeName = "TEXT",
frameParent = Equipment.backdrop,
createContext = 1,
x1 = 0.100000,
y1 = 0.595000,
x2 = 0.300000,
y2 = 0.545000,
byType = true,
text = "|cffffffffEQUIPMENT|r",
justifyH = TEXT_JUSTIFY_MIDDLE,
justifyV = TEXT_JUSTIFY_TOP,
scale = 3.0,
enabled = false,
})
Equipment.backdrop:move(0,1)
BlzFrameSetVisible(Equipment.backdrop.frame,false)
end
end
do
local function click_discard(frame)
UI.EndChooseItem()
end
function UI.ChooseItemInit()
UI.ItemChoice = Frame.new({
typeName = "CheckListBox",
parent = ConsoleUIBackdrop,
createContext = 0,
x1 = 0.35,
y1 = 0.6,
x2 = 0.8,
y2 = 0.19,
})
Frame.new({
typeName = "TEXT",
frameParent = UI.ItemChoice,
createContext = 1,
x1 = UI.ItemChoice.x1,
y1 = UI.ItemChoice.y1,
x2 = UI.ItemChoice.x2,
y2 = UI.ItemChoice.y2,
byType = true,
text = "|cffffffff\nChoose One Item|r",
justifyH = TEXT_JUSTIFY_MIDDLE,
justifyV = TEXT_JUSTIFY_TOP,
scale = 2.43,
enabled = false,
})
local originX1 = 0.525
local originY1 = 0.37
local originX2 = 0.625
local originY2 = 0.22
local slots = Set.create()
slots:add(Equipment.newButton(UI.ItemChoice,originX1,originY1,originX2,originY2, "alphatexture.dds",true,nil,true))
slots:add(Equipment.newButton(UI.ItemChoice,originX1,originY1,originX2,originY2, "alphatexture.dds",true,nil,true))
slots:add(Equipment.newButton(UI.ItemChoice,originX1,originY1,originX2,originY2, "alphatexture.dds",true,nil,true))
local discardButton = Frame.new({
typeName = "BACKDROP",
frameParent = UI.ItemChoice,
createContext = 1,
x1 = 0.575-.04,
y1 = 0.22,
x2 = 0.575+.04,
y2 = 0.22-.02,
byType = true,
texture = "UIRedButtonNormal.dds",
textureHover = "UIRedButtonHover.dds",
texturePressed = "UIRedButtonPressed.dds",
trackHover = true,
trackClick = click_discard,
})
Frame.new({
typeName = "TEXT",
frameParent = UI.ItemChoice,
createContext = 1,
x1 = discardButton.x1,
y1 = discardButton.y1,
x2 = discardButton.x2,
y2 = discardButton.y2,
byType = true,
text = "Discard",
justifyH = TEXT_JUSTIFY_CENTER,
justifyV = TEXT_JUSTIFY_MIDDLE,
scale = 1.0,
enabled = false,
})
for slot in slots:elements() do
slot.chooseSlot = true
BlzFrameSetVisible(slot.frame,false)
end
local lastCount
local lastIsPotion
local function displayNewItems(count,potion,move)
local i = 0
local offset = (count-1) * 0.055
for slot in slots:elements() do
BlzFrameSetVisible(slot.frame,true)
if move then
slot:set_position(originX1+1,originY1,originX2+1,originY2)
slot:move(-offset + i * 0.11,0)
else
SlotDestroyWearable(slot)
end
local w
if potion then
w = potion_drop_list:pop():instance()
else
w = Wearable.instanceRandom()
end
SlotGainWearable(PlayerDataLocal,slot,w)
i = i + 1
if i >= count then break end
end
end
Hallucinogenic_PotionButton = Potions.newButton(UI.ItemChoice,0.5225, 0.19, 0.5225 + 0.1, 0.04,
"IconPotion2.dds",
"Hallucinogenic Elixir",
"Rerolls the selection of items.",
3,
function(frame,pdata)
frame.stack = frame.stack - 1
Potions.update(frame)
displayNewItems(lastCount,lastIsPotion,false)
end
)
BlzFrameSetVisible(UI.ItemChoice.frame,false)
UI.ItemChoice:move(1,0)
function UI.ChooseItem(count,potion)
GameAction.Pause(true)
GameAction.LockPause(true)
Equipment.set_visible(true)
SOUND_CHEST:random()()
lastIsPotion = potion
lastCount = count
displayNewItems(count,potion,true)
LerpFramePosition(UI.ItemChoice, 0.35, 0.6, 0.8,0.19,0.5,easeOutBack)
LerpFrameAlpha(UI.ItemChoice,0,255,0.5,easeInCubic,true)
end
function UI.EndChooseItem()
LerpFramePosition(UI.ItemChoice, 1.35, 0.6, 1.8,0.19,0.5,easeInBack)
LerpFrameAlpha(UI.ItemChoice,255,0,0.5,easeOutCubic,true)
GameAction.LockPause(false)
GameAction.Pause(false)
Equipment.set_visible(false)
for slot in slots:elements() do
SlotDestroyWearable(slot)
BlzFrameSetVisible(slot.frame,false)
end
end
end
end
do
CharacterSelect = {}
local whichClass
local lastDisplay
local lookat
local function click_confirm(frame)
BlzShowTerrain(false)
LerpFrameAlpha(CharacterSelect.loadingScreen,0,255,0.5,easeInCubic,true)
CharacterSelect.loadingScreen:set_visible(true)
BlzFrameClearAllPoints(CharacterSelect.loadingScreen.frame)
BlzFrameSetAbsPoint(CharacterSelect.loadingScreen.frame,FRAMEPOINT_TOPLEFT,Resolution.min_x,Resolution.max_y)
BlzFrameSetAbsPoint(CharacterSelect.loadingScreen.frame,FRAMEPOINT_BOTTOMRIGHT,Resolution.max_x,Resolution.min_y)
WorldGeneration.start_fresh(GenerateUnderground)
AddDefaultTraitsToPool()
AddAbilitiesToPool()
RemoveUnit(lastDisplay)
RemoveUnit(lookat)
lastDisplay = false
lookat = false
CharacterSelectShow(false)
end
Backdrop01 = nil
local function displayClass()
if lastDisplay then
RemoveUnit(lastDisplay)
end
if not lookat then
lookat = CreateUnit(Player(0),FourCC('h000'),0,0,0)
end
lastDisplay = CreateUnit(Player(0),whichClass.model,CameraSetupGetDestPositionX(gg_cam_CharacterSelect),CameraSetupGetDestPositionY(gg_cam_CharacterSelect),CameraSetupGetField(gg_cam_CharacterSelect,CAMERA_FIELD_ROTATION)+180)
SetUnitLookAt(lastDisplay,"chest",lookat,0,0,0)
SetUnitPosition(lookat,GetCameraEyePositionX(),GetCameraEyePositionY())
SetUnitFlyHeight(lookat,GetCameraEyePositionZ(),0)
local name = whichClass.name
local description = whichClass.description(whichClass)
CharacterSelect.namePlateText:set_text(name)
CharacterSelect.description:set_text(description)
end
local function click_left(frame)
local ind = PlayerClasses:element_index(whichClass)
ind = ind -1
if ind < 1 then
ind = PlayerClasses:size()
end
whichClass = PlayerClasses:get_element_by_index(ind)
displayClass()
end
local function click_right(frame)
local ind = PlayerClasses:element_index(whichClass)
ind = ind + 1
if ind > PlayerClasses:size() then
ind = 1
end
whichClass = PlayerClasses:get_element_by_index(ind)
displayClass()
end
local function finishClass()
UI.toggle_default()
BlzShowTerrain(true)
LerpFrameAlpha(CharacterSelect.loadingScreen,255,0,0.5,easeOutCubic,true)
PlayerDataLocal.playerUnit = PlayerUnit.new(PlayerDataLocal,0,0,whichClass)
Camera.position.x = PlayerDataLocal.playerUnit.position[1]
Camera.position.y = PlayerDataLocal.playerUnit.position[2]
WorldGeneration.spawnFunction()
GAME_TIME = 0.0
Sound.reset()
Unit.reset_random_boss_timer()
ResetEffectTraits()
WorldGeneration.add_scrolls(2)
Potions.reset(PlayerDataLocal)
GameAction.LockPause(false)
end
local function createPlayer(newWorld)
if newWorld then
Unit.StunInit()
UNPAUSEABLE_TIMER:callDelayed(2.0,finishClass)
end
end
local showDest = false
local function pickDest()
ShowDestructable(GetEnumDestructable(),showDest)
end
function CharacterSelectShow(state)
showDest = state
CharacterSelect.selecting = state
EnumDestructablesInRect(GetPlayableMapRect(),nil,pickDest)
CharacterSelect.backdrop:set_visible(state)
if state then
GameAction.LockPause(true)
UI.toggle_default()
SetTerrainType(-1800, -2500, FourCC('Dsqd'), -1, 20, 0)
CameraSetupApplyForceDuration(gg_cam_CharacterSelect,true,0.0)
if not whichClass then
whichClass = PlayerClasses:random()
end
UNPAUSEABLE_TIMER:callDelayed(0.1,displayClass)
else
ResetToGameCamera(0.0)
end
end
local function finish_restart(pdata)
WorldGeneration.destroy()
CharacterSelectShow(true)
end
local function restart(playerUnit)
UNPAUSEABLE_TIMER:callDelayed(5.0,finish_restart,playerUnit.pdata)
end
function CharacterSelect.init()
WorldGeneration.doneLoading:add_action(createPlayer)
PlayerUnit.death_event:add_action(restart)
CharacterSelect.backdrop = Frame.new({
typeName = "BACKDROP",
parent = ConsoleUIBackdrop,
createContext = 1,
x1 = 0.41,
y1 = 0.57,
x2 = 0.86,
y2 = 0.12,
byType = true,
texture = "UICharacterSheet.dds",
})
CharacterSelect.backdrop:set_visible(false)
CharacterSelect.namePlate = Frame.new({
typeName = "BACKDROP",
frameParent = CharacterSelect.backdrop,
createContext = 1,
x1 = 0.535,
y1 = 0.49,
x2 = 0.735,
y2 = 0.44,
byType = true,
texture = "UINamePlate.dds",
})
CharacterSelect.description = Frame.new({
typeName = "TEXT",
frameParent = CharacterSelect.backdrop,
createContext = 1,
x1 = 0.52,
y1 = 0.435 - 0.015,
x2 = 0.75,
y2 = 0.205,
byType = true,
text = "Describe Me",
justifyH = TEXT_JUSTIFY_LEFT,
justifyV = TEXT_JUSTIFY_TOP,
scale = 1.0,
enabled = false,
})
CharacterSelect.confirmButton = Frame.new({
typeName = "BACKDROP",
frameParent = CharacterSelect.backdrop,
createContext = 1,
x1 = 0.585,
y1 = 0.2,
x2 = 0.685,
y2 = 0.175,
byType = true,
texture = "UIRedButtonNormal.dds",
textureHover = "UIRedButtonHover.dds",
texturePressed = "UIRedButtonPressed.dds",
trackHover = true,
trackClick = click_confirm,
pressSound = SOUND_UI_START_GAME
})
CharacterSelect.leftButton = Frame.new({
typeName = "BACKDROP",
frameParent = CharacterSelect.backdrop,
createContext = 1,
x1 = 0.486,
y1 = 0.38+0.14,
x2 = 0.536,
y2 = 0.28+0.14,
byType = true,
texture = "UIArrowLeftNormal.dds",
textureHover = "UIArrowLeftHover.dds",
texturePressed = "UIArrowLeftPressed.dds",
trackHover = true,
trackClick = click_left,
})
CharacterSelect.rightButton = Frame.new({
typeName = "BACKDROP",
frameParent = CharacterSelect.backdrop,
createContext = 1,
x1 = 0.736-0.0015,
y1 = 0.38+0.14,
x2 = 0.786-0.0015,
y2 = 0.28+0.14,
byType = true,
texture = "UIArrowRightNormal.dds",
textureHover = "UIArrowRightHover.dds",
texturePressed = "UIArrowRightPressed.dds",
trackHover = true,
trackClick = click_right,
})
CharacterSelect.namePlateText = Frame.new({
typeName = "TEXT",
frameParent = CharacterSelect.backdrop,
createContext = 1,
x1 = 0.535,
y1 = 0.49,
x2 = 0.735,
y2 = 0.44,
byType = true,
text = "Warrior",
justifyH = TEXT_JUSTIFY_CENTER,
justifyV = TEXT_JUSTIFY_MIDDLE,
scale = 2.5,
enabled = false,
})
CharacterSelect.confirmText = Frame.new({
typeName = "TEXT",
frameParent = CharacterSelect.backdrop,
createContext = 1,
x1 = 0.585,
y1 = 0.2,
x2 = 0.685,
y2 = 0.175,
byType = true,
text = "Confirm",
justifyH = TEXT_JUSTIFY_CENTER,
justifyV = TEXT_JUSTIFY_MIDDLE,
scale = 1.0,
enabled = false,
})
CharacterSelect.loadingScreen = Frame.new({
typeName = "BACKDROP",
parent = ConsoleUIBackdrop,
createContext = 1,
x1 = Resolution.min_x,
y1 = Resolution.max_y,
x2 = Resolution.max_x,
y2 = Resolution.min_y,
byType = true,
texture = "Fullscreen.dds",
})
CharacterSelect.loadingText = Frame.new({
typeName = "TEXT",
frameParent = CharacterSelect.loadingScreen,
createContext = 1,
x1 = Resolution.min_x,
y1 = Resolution.max_y,
x2 = Resolution.max_x,
y2 = Resolution.min_y,
byType = true,
text = "L O A D I N G",
justifyH = TEXT_JUSTIFY_CENTER,
justifyV = TEXT_JUSTIFY_BOTTOM,
scale = 5.0,
enabled = false,
})
CharacterSelect.loadingScreen:set_visible(false)
CharacterSelectShow(true)
end
end
do
local tooltipTarget = 0
function GetTooltipTarget()
return tooltipTarget
end
function InitTooltip()
local tooltipFrameBackGroundTop = BlzCreateFrame("BoxedText", ConsoleUIBackdrop, 0, 0)
local tooltipFrameTextTop = BlzCreateFrameByType("TEXT", "MyScriptDialogButtonTooltip1", tooltipFrameBackGroundTop, "", 0)
BlzFrameSetSize(tooltipFrameTextTop, 0.25, 0)
unsafeFrameScale(tooltipFrameTextTop,1.5)
BlzFrameSetPoint(tooltipFrameBackGroundTop, FRAMEPOINT_BOTTOMLEFT, tooltipFrameTextTop, FRAMEPOINT_BOTTOMLEFT, -0.01, -0.01)
BlzFrameSetPoint(tooltipFrameBackGroundTop, FRAMEPOINT_TOPRIGHT, tooltipFrameTextTop, FRAMEPOINT_TOPRIGHT, 0.01, 0.01)
BlzFrameSetEnable(tooltipFrameTextTop, false)
unsafeFrameScale(BlzFrameGetChild(tooltipFrameBackGroundTop,0),1.6)
local tooltipFrameBackGroundBot = BlzCreateFrame("BoxedText", ConsoleUIBackdrop, 0, 0)
local tooltipFrameTextBot = BlzCreateFrameByType("TEXT", "MyScriptDialogButtonTooltip2", tooltipFrameBackGroundBot, "", 0)
BlzFrameSetSize(tooltipFrameTextBot, 0.25, 0)
unsafeFrameScale(tooltipFrameTextBot,1.5)
BlzFrameSetPoint(tooltipFrameBackGroundBot, FRAMEPOINT_BOTTOMLEFT, tooltipFrameTextBot, FRAMEPOINT_BOTTOMLEFT, -0.01, -0.01)
BlzFrameSetPoint(tooltipFrameBackGroundBot, FRAMEPOINT_TOPRIGHT, tooltipFrameTextBot, FRAMEPOINT_TOPRIGHT, 0.01, 0.01)
BlzFrameSetEnable(tooltipFrameTextBot, false)
unsafeFrameScale(BlzFrameGetChild(tooltipFrameBackGroundBot,0),1.6)
local tooltipTarget = 0
function SetTooltipTarget(frame,title,text)
local tooltipFrameText
local tooltipFrameBackGround
tooltipTarget = frame
local offY = 0.005
if frame.y1 > 0.3 then
tooltipFrameText = tooltipFrameTextBot
tooltipFrameBackGround = tooltipFrameBackGroundBot
offY = -0.005
else
tooltipFrameText = tooltipFrameTextTop
tooltipFrameBackGround = tooltipFrameBackGroundTop
end
BlzFrameClearAllPoints(tooltipFrameText)
BlzFrameSetParent(tooltipFrameBackGround,OriginFrameGameUI)
BlzFrameSetLevel(tooltipFrameBackGround,10)
BlzFrameSetParent(tooltipFrameBackGround,frame.frame)
BlzFrameSetVisible(tooltipFrameBackGround,true)
local maxX = math.max(frame.x1,frame.x2)
local minX = math.min(frame.x1,frame.x2)
if maxX > 0.6 then
BlzFrameSetPoint(tooltipFrameText, FRAMEPOINT_RIGHT, frame.frame, FRAMEPOINT_LEFT, -0.005, 0)
elseif minX < 0.2 then
BlzFrameSetPoint(tooltipFrameText, FRAMEPOINT_LEFT, frame.frame, FRAMEPOINT_RIGHT, 0.005, 0)
elseif frame.y1 > 0.3 then
BlzFrameSetPoint(tooltipFrameText, FRAMEPOINT_TOP, frame.frame, FRAMEPOINT_BOTTOM, 0, offY)
else
BlzFrameSetPoint(tooltipFrameText, FRAMEPOINT_BOTTOM, frame.frame, FRAMEPOINT_TOP, 0, offY)
end
BlzFrameSetText(tooltipFrameText, "|n|n"..text)
BlzFrameSetText(BlzFrameGetChild(tooltipFrameBackGround,0)," "..title.."|n")
end
function HideTooltip()
BlzFrameSetVisible(tooltipFrameBackGroundTop,false)
BlzFrameSetVisible(tooltipFrameBackGroundBot,false)
end
HideTooltip()
end
end
do
Trait = {}
Trait.__index = Trait
TraitPool = Set.create()
MemoryTraitPool = Set.create()
function TraitLevelFunction_AnyLevel(lvl,playerLevel)
return true
end
function AbilityLevelFunction_false_true_true(x)
return (x - 1) %% 3 ~= 0
end
function AbilityLevelFunction_true_true_false(x)
return x %% 3 ~= 0
end
function AbilityLevelFunction_true_false(x)
return x %% 2 ~= 0
end
function AbilityLevelFunction_false_true(x)
return x %% 2 == 0
end
function AbilityLevelFunction_three_six_ten(x)
local index = (x - 1) %% 10
return index == 2 or index == 5 or index == 9
end
--//0,5,15,30,50
function TraitLevelFunction_Default(lvl,playerLevel)
return 2.5 * lvl * lvl - 2.5 * lvl <= playerLevel
end
function TraitLevelFunction_Fifteen(lvl,playerLevel)
return 15 * (2^(lvl-1)) <= playerLevel
end
function TraitLevelFunction_Eight(lvl,playerLevel)
return 8.0 * lvl <= playerLevel
end
function TraitLevelFunction_Five_Ten_Twenty(lvl,playerLevel)
return 5 * (2^(lvl-1)) <= playerLevel
end
function TraitLevelFunction_Ten_Twenty_Thirtyfive(lvl,playerLevel)
return 5 * lvl + 5 * (lvl == 1 and 1 or lvl == 2 and 2 or 4) <= playerLevel
end
function TraitLevelFunction_Thirty(lvl,playerLevel)
return 30 * lvl <= playerLevel
end
function Trait.new(self)
return setmetatable(self,Trait)
end
function Trait.apply(self,playerUnit,multiplyer,multi_trait)
MemoryTraitPool:remove(self)
if multi_trait ~= nil then
local which = self["actions"..multi_trait]
if type(which) == "table" then
which[self.level](self,playerUnit)
else
which(self,playerUnit)
end
else
for i = 1, multiplyer do
self.action(self,playerUnit,self.ability)
end
if self.ability ~= nil then
self.ability.level = self.ability.level + 1
if (self.ability.level == 4 or self.ability.level == 7) and self.ability.upgrades ~= nil then
self.ability.upgrades_max = self.ability.upgrades_max + 1
if self.ability.upgrades_max == self.ability.upgrades_cur + 1 then
for upgrade in self.ability.upgrades:elements() do
upgrade:add_to_pool()
end
end
end
end
end
self.level = self.level + 1
if self.level > self.level_max then
TraitPool:remove(self)
end
end
local tempPool
local tempMemoryPool
local maxTraits
local toDisplay
function FillTraitIndex(playerUnit,index,ignoreMemory)
local whichPool = tempPool
if tempMemoryPool:size() > 0 and not ignoreMemory then
whichPool = tempMemoryPool
end
local trait = whichPool:random()
if trait.ability ~= nil then
if trait.ability.level > trait.level_max then
TraitPool:remove(trait)
elseif trait.level_func(trait.ability.level,playerUnit.level) and trait.ability_level_func(trait.ability.level) then
UI.TraitDisplay(UI.GetTraitByIndex(index),trait)
index = index + 1
end
elseif trait.level_func(trait.level,playerUnit.level) then
UI.TraitDisplay(UI.GetTraitByIndex(index),trait)
index = index + 1
end
whichPool:remove(trait)
return index
end
function ShowTraits(playerUnit)
maxTraits = UI.get_max_traits()
toDisplay = 1
tempPool = TraitPool:copy()
tempMemoryPool = MemoryTraitPool:copy()
for t in tempMemoryPool:elements() do
tempPool:remove(t)
end
while toDisplay <= maxTraits and tempPool:size() + tempMemoryPool:size() > 0 do
toDisplay = FillTraitIndex(playerUnit,toDisplay)
end
if toDisplay == 1 then
UI.endLevelUp()
end
end
function Trait.add_to_pool(self,abil)
self.ability = abil
self.level = 1
TraitPool:add(self)
return self
end
end
do
Item = {}
Item.__index = Item
item_collision_hash = shash.new(64)
ITEM_SIZE = 64
ITEM_HALF_SIZE = ITEM_SIZE * 0.5
all_items = Set.create()
Item.gain_event = Event.new()
local tracked_items = Set.create()
local recycledTrackers = {}
local TRACKER_SIZE = 0.015
function Item:hide_tracker()
if not self.tracker then return end
recycledTrackers[#recycledTrackers+1] = self.tracker
BlzFrameSetVisible(self.tracker.frame,false)
self.tracker = nil
end
function Item:get_track_frame()
self.tracker = recycledTrackers[#recycledTrackers] or
Frame.new({
typeName = "BACKDROP",
parent = OriginFrameGameUI,
createContext = 0,
x1 = -TRACKER_SIZE+0.4,
y1 = TRACKER_SIZE+0.3,
x2 = TRACKER_SIZE+0.4,
y2 = -TRACKER_SIZE+0.3,
byType = true,
})
recycledTrackers[#recycledTrackers] = nil
BlzFrameSetParent(self.tracker.frame,ConsoleUIBackdrop)
BlzFrameSetLevel(self.tracker.frame,10)
BlzFrameSetTexture(self.tracker.frame,self.tracked,0,true)
BlzFrameSetVisible(self.tracker.frame,true)
end
function Item.move(self,x,y)
self.position[1] = x
self.position[2] = y
BlzSetSpecialEffectPosition(self.sfx,x-SFX_OFFSET,y-SFX_OFFSET,0)
item_collision_hash:update(self, x - ITEM_HALF_SIZE, y - ITEM_HALF_SIZE)
end
function Item.new(data,x,y)
local self = setmetatable(NewTable(),Item)
self.position = NewTable()
self.position[1]= x
self.position[2]= y
self.sfx = AddSpecialEffect(data.model,x-SFX_OFFSET,y-SFX_OFFSET)
self.tracked = data.tracked
self.itemType = data
BlzSetSpecialEffectYaw(self.sfx,data.rotation or TAU*0.75)
self.delayDestroyEffect = data.delayDestroyEffect
if self.tracked then
tracked_items:add(self)
end
BlzSetSpecialEffectScale(self.sfx,data.scale or 1)
self.pickup_func = data.pickup_func
item_collision_hash:add(self, x - ITEM_HALF_SIZE, y - ITEM_HALF_SIZE, ITEM_SIZE, ITEM_SIZE)
all_items:add(self)
if data.spawn_func then
data.spawn_func(self)
end
return self
end
function Item.pickup(self,playerUnit)
Item.gain_event:fire(playerUnit,self)
self.pickup_func(playerUnit,self)
self:destroy()
end
function Item.destroy(self)
if self.tracked then
tracked_items:remove(self)
end
self:hide_tracker()
if self.delayDestroyEffect then
DEFAULT_TIMER:callDelayed(self.delayDestroyEffect,DestroyEffect,self.sfx)
self.delayDestroyEffect = nil
else
DestroyEffect(self.sfx)
end
ReleaseTable(self.position)
self.position = nil
self.sfx = nil
self.pickup_func = nil
self.tracked = nil
self.itemType = nil
item_collision_hash:remove(self)
setmetatable(self,nil)
ReleaseTable(self)
all_items:remove(self)
end
local function check_tracked()
local x,y
local playerUnit = PlayerDataLocal.playerUnit
if playerUnit then
x,y = playerUnit.position[1], playerUnit.position[2]
else
x,y = GetCameraTargetPositionX(), GetCameraTargetPositionY()
end
for it in tracked_items:elements() do
local px = it.position[1] - x
local py = it.position[2] - y
if not isPointInTrapezium(px,py, Camera.ax*.83, Camera.ay*.85, Camera.bx*.83, Camera.by*.85, Camera.cx*.83, Camera.cy*.85, Camera.dx*.83, Camera.dy*.85) then
if not it.tracker then
it:get_track_frame()
end
local angle_rad = math.atan(it.position[2]-y, it.position[1]-x)
local x2,y2 = intersect_with_bounds(Resolution.min_x+TRACKER_SIZE, Resolution.max_x-TRACKER_SIZE, Resolution.min_y+TRACKER_SIZE,Resolution.max_y-TRACKER_SIZE, angle_rad)
it.tracker:set_center(x2, y2)
elseif it.tracker then
it:hide_tracker()
end
end
end
function Item.init()
DEFAULT_TIMER:callPeriodically(DEFAULT_TIMEOUT, nil, check_tracked)
end
end
do
AbilityUpgradePool = Set.create()
Ability = {}
Ability.__index = Ability
AbilityPool = Set.create()
AllAbilities = Set.create()
function Ability.new(self)
setmetatable(self,Ability)
AllAbilities:add(self)
if self.upgradeAbility then
self.upgradeAbility.upgrades = self.upgradeAbility.upgrades or Set.create()
self.upgradeAbility.upgrades:add(self)
self.icon = self.icon or self.upgradeAbility.icon
end
return self
end
function TestAbility(abil)
local playerUnit = PlayerDataLocal.playerUnit
if type(abil.action) == "table" then
local i = GetRandomInt(1,#abil.action)
abil.action[i](abil,playerUnit)
else
abil:action(playerUnit)
end
if abil.upgrades then
for upgrade in abil.upgrades:elements() do
for i, attack in ipairs(playerUnit.attacks) do
if upgrade.upgradeAbility.name == attack.name and attack.name ~= nil then
if type(upgrade.action) == "table" then
local i = GetRandomInt(1,#upgrade.action)
upgrade.action[i](upgrade,playerUnit,attack)
else
upgrade:action(playerUnit,attack)
end
end
end
end
end
end
function TestAbilities()
local playerUnit = PlayerDataLocal.playerUnit
for abil in AllAbilities:elements() do
if abil.upgradeAbility then
for i, attack in ipairs(playerUnit.attacks) do
if abil.upgradeAbility.name == attack.name and attack.name ~= nil then
if type(abil.action) == "table" then
local i = GetRandomInt(1,#abil.action)
abil.action[i](abil,playerUnit,attack)
else
abil:action(playerUnit,attack)
end
end
end
else
if type(abil.action) == "table" then
local i = GetRandomInt(1,#abil.action)
abil.action[i](abil,playerUnit)
else
abil:action(playerUnit)
end
end
end
end
function Ability.apply(self,playerUnit,index)
if self.upgradeAbility then
for i, attack in ipairs(playerUnit.attacks) do
if self.upgradeAbility.name == attack.name and attack.name ~= nil then
if index then
self.action[index](self,playerUnit,attack)
else
self.action(self,playerUnit,attack)
end
if attack.upgrades then
attack.upgrades_cur = attack.upgrades_cur + 1
if attack.upgrades_cur >= attack.upgrades_max then
for abil in attack.upgrades:elements() do
AbilityUpgradePool:remove(abil)
end
end
attack.upgrades:remove(self)
end
end
end
AbilityUpgradePool:remove(self)
else
if index then
self.action[index](self,playerUnit)
else
self:action(playerUnit)
end
AbilityPool:remove(self)
end
end
function ShowAbilities(playerUnit)
local max = UI.get_max_abilities()
local toDisplay = 1
local tempPool = AbilityPool:copy()
local tempUpgradePool = AbilityUpgradePool:copy()
local upgrade_chance = 1.0
while toDisplay <= max and (tempPool:size() > 0 or tempUpgradePool:size() > 0) do
local abil
if GetRandomReal(0,1) <= upgrade_chance and tempUpgradePool:size() > 0 then
abil = tempUpgradePool:random()
tempUpgradePool:remove(abil)
upgrade_chance = upgrade_chance * 0.35
else
abil = tempPool:random()
tempPool:remove(abil)
end
if abil then
UI.AbilityDisplay(UI.GetAbilityByIndex(toDisplay),abil)
toDisplay = toDisplay + 1
end
end
if toDisplay == 1 then
UI.endAbilitySelect()
end
tempPool:destroy()
tempUpgradePool:destroy()
end
function Ability.add_to_pool(self)
self.level = 1
if self.upgradeAbility then
AbilityUpgradePool:add(self)
else
AbilityPool:add(self)
end
return self
end
end
do
GameAction = {}
local paused = false
local pause_locked = 0
function GameAction.LockPause(state)
pause_locked = pause_locked + (state and 1 or -1)
end
function GameAction.Pause(state)
paused = state
UI.HoverShow(not state)
if state then
BlzFrameSetVisible(UI.BossBar,false)
DEFAULT_TIMER:pause()
SPAWN_TIMER:pause()
EFFECT_TIMER:pause()
for attack in all_attacks:elements() do
attack.timer:pause()
end
for unit in units:elements() do
BlzSetSpecialEffectTimeScale(unit.sfx,0.0)
BlzSetSpecialEffectTimeScale(unit.outlinesfx,0.0)
end
for dest in destructables:elements() do
BlzSetSpecialEffectTimeScale(dest.sfx,0.0)
end
for punit in playerunits:elements() do
SetUnitTimeScale(punit.sfx,0.0)
end
for unit in summons:elements() do
BlzSetSpecialEffectTimeScale(unit.sfx,0.0)
end
else
DEFAULT_TIMER:resume()
SPAWN_TIMER:resume()
EFFECT_TIMER:resume()
for attack in all_attacks:elements() do
attack.timer:resume()
end
for unit in units:elements() do
BlzSetSpecialEffectTimeScale(unit.sfx,1.0)
BlzSetSpecialEffectTimeScale(unit.outlinesfx,1.0)
end
for dest in destructables:elements() do
BlzSetSpecialEffectTimeScale(dest.sfx,1.0)
end
for punit in playerunits:elements() do
SetUnitTimeScale(punit.sfx,punit.time_scale)
end
for unit in summons:elements() do
BlzSetSpecialEffectTimeScale(unit.sfx,1.0)
end
end
end
function GameAction.IsPaused()
return paused
end
local function press()
local pdata = Player2Data(GetTriggerPlayer())
local key = BlzGetTriggerPlayerKey()
if BlzGetTriggerPlayerIsKeyDown() and key == OSKEY_SPACE and pause_locked == 0 then
paused = not paused
GameAction.Pause(paused)
Equipment.set_visible(paused)
--BlzFrameSetVisible(UI.Background,paused)
end
end
function GameAction.Init()
local t = CreateTrigger()
for i, p in ipairs(PlayerUsers) do
add_trigger_event_key(t,p,OSKEY_SPACE)
end
TriggerAddAction(t,press)
end
end
do
local STUN_MODEL = "Abilities\\Spells\\Human\\Thunderclap\\ThunderclapTarget.mdl"
local ELECTRIFY_MODEL = "Abilities\\Spells\\Items\\AIlb\\AIlbTarget.mdl"
local BURN_MODEL = "Abilities\\Spells\\Other\\BreathOfFire\\BreathOfFireDamage.mdl"
local FROST_MODEL = "Abilities\\Spells\\Items\\AIob\\AIobtarget.mdl"
local SLOW_MODEL = "WaterSlow.mdx"
local FRAGILE_MODEL = "Abilities\\Spells\\Other\\HowlOfTerror\\HowlTarget.mdl"
local AFFLICTION_MODEL = "Abilities\\Spells\\Orc\\SpiritLink\\SpiritLinkTarget.mdl"
local OVERHEAD_HEIGHT = 96.0
local CHEST_HEIGHT = 32.0
local MAX_STACKS = 20
EFFECT_TYPES ={
Stun = 1,
Electrify = 2,
Burn = 3,
Frost = 4,
Slow = 5,
Fragile = 6,
Affliction = 7,
}
MAX_EFFECTS = 7
local effect_offsets = {
OVERHEAD_HEIGHT,
CHEST_HEIGHT,
0,
CHEST_HEIGHT,
CHEST_HEIGHT,
CHEST_HEIGHT,
CHEST_HEIGHT,
}
function Unit:get_debuff_effect_stacks()
return (self.effects_dur[EFFECT_TYPES.Slow] or 0)/2 +
(self.effects_stacks[EFFECT_TYPES.Fragile] or 0) +
(self.effects_stacks[EFFECT_TYPES.Affliction] or 0)
end
local function attach_effect(self,model,height,scale_mult)
local sfx = AddSpecialEffect(model,self.position[1]-SFX_OFFSET,self.position[2]-SFX_OFFSET)
local scale = self:get_scale() * (scale_mult or 1)
BlzSetSpecialEffectZ(sfx,height * scale)
BlzSetSpecialEffectScale(sfx,scale)
return sfx
end
function Unit.get_fragility(self)
return (self.effects_stacks[EFFECT_TYPES.Fragile] or 0) * 0.05
end
local function fragile_tick(self,sfx)
if self.sfx ~= sfx then return end
local dur = (self.effects_stacks[EFFECT_TYPES.Fragile] or 0) - 1
if dur <= 0.0 then
DestroyEffect(self.effects_sfx[EFFECT_TYPES.Fragile])
self.effects_sfx[EFFECT_TYPES.Fragile] = nil
else
self.effects_stacks[EFFECT_TYPES.Fragile] = dur
EFFECT_TIMER:callDelayed(5.0 - dur * 0.05, fragile_tick,self,sfx)
end
end
function Unit.frail(self,source,stacks)
stacks = stacks + source.frailBonus
if GetRandomReal(0.0,1.0) <= stacks%%1 then
stacks = math.ceil(stacks)
else
stacks = math.floor(stacks)
if stacks <= 0 then return 0 end
end
self.effects_stacks[EFFECT_TYPES.Fragile] = (self.effects_stacks[EFFECT_TYPES.Fragile] or 0) + stacks
local dur = self.effects_stacks[EFFECT_TYPES.Fragile]
if self.effects_sfx[EFFECT_TYPES.Fragile] == nil then
self.effect_source = source
self.effects_sfx[EFFECT_TYPES.Fragile] = attach_effect(self,FRAGILE_MODEL,CHEST_HEIGHT)
EFFECT_TIMER:callDelayed(5.0 - dur * 0.05, fragile_tick,self,self.sfx)
end
return stacks
end
local function affliction_tick(self,sfx)
if self.sfx ~= sfx then return end
local dur = (self.effects_stacks[EFFECT_TYPES.Affliction] or 0) - 1
if dur <= 0.0 then
DestroyEffect(self.effects_sfx[EFFECT_TYPES.Affliction])
self.effects_stacks[EFFECT_TYPES.Affliction] = dur
self.effects_sfx[EFFECT_TYPES.Affliction] = nil
else
self.effects_stacks[EFFECT_TYPES.Affliction] = dur
EFFECT_TIMER:callDelayed(5.0 - dur * 0.05, affliction_tick,self,sfx)
end
end
function Unit.afflict(self,source,stacks)
stacks = stacks + source.afflictBonus
if GetRandomReal(0.0,1.0) <= stacks%%1 then
stacks = math.ceil(stacks)
else
stacks = math.floor(stacks)
if stacks <= 0 then return 0 end
end
self.effects_stacks[EFFECT_TYPES.Affliction] = (self.effects_stacks[EFFECT_TYPES.Affliction] or 0) + stacks
local dur = self.effects_stacks[EFFECT_TYPES.Affliction]
if self.effects_sfx[EFFECT_TYPES.Affliction] == nil then
self.effect_source = source
self.effects_sfx[EFFECT_TYPES.Affliction] = attach_effect(self,AFFLICTION_MODEL,CHEST_HEIGHT)
EFFECT_TIMER:callDelayed(5.0 - dur * 0.05, affliction_tick,self,self.sfx)
end
return stacks
end
local function slow_check_end(self,sfx)
if self.sfx ~= sfx then return end
self.effects_dur[EFFECT_TYPES.Slow] = self.effects_dur[EFFECT_TYPES.Slow] - 2
self.slow_mult = 0.8^(self.effects_dur[EFFECT_TYPES.Slow] / 2)
if self.effects_dur[EFFECT_TYPES.Slow] == 0 then
DestroyEffect(self.effects_sfx[EFFECT_TYPES.Slow])
self.effects_sfx[EFFECT_TYPES.Slow] = nil
else
EFFECT_TIMER:callDelayed(2.0, slow_check_end,self,sfx)
end
end
function Unit.get_effect_stacks(self,whichType)
if whichType == EFFECT_TYPES.Slow then
return (self.effects_dur[whichType] or 0)/2
else
return (self.effects_stacks[whichType] or 0)
end
end
function Unit.slow(self,source,stacks)
stacks = stacks + source.slowBonus
if GetRandomReal(0.0,1.0) <= stacks%%1 then
stacks = math.ceil(stacks)
else
stacks = math.floor(stacks)
if stacks <= 0 then return 0 end
end
self.effects_dur[EFFECT_TYPES.Slow] = math.min((self.effects_dur[EFFECT_TYPES.Slow] or 0) + stacks * 2,20)
self.slow_mult = 0.8^(self.effects_dur[EFFECT_TYPES.Slow] / 2)
if self.effects_sfx[EFFECT_TYPES.Slow] == nil then
self.effect_source = source
self.effects_sfx[EFFECT_TYPES.Slow] = attach_effect(self,SLOW_MODEL,0,0.5)
EFFECT_TIMER:callDelayed(2.0, slow_check_end,self,self.sfx)
end
return stacks
end
local function stun_check_end(self)
if self.effects_dur == nil then return end
if self.stunned and self.effects_dur[EFFECT_TYPES.Stun] <= GAME_TIME then
self.stunned = nil
DestroyEffect(self.effects_sfx[EFFECT_TYPES.Stun])
self.effects_sfx[EFFECT_TYPES.Stun] = nil
end
end
function Unit.stun(self,source,duration)
if self.bossType then
duration = duration * 0.5
end
local next = GAME_TIME + duration
self.effects_dur[EFFECT_TYPES.Stun] = math.max(self.effects_dur[EFFECT_TYPES.Stun] or GAME_TIME,GAME_TIME + duration - 0.02)
self.stunned = true
if self.effects_sfx[EFFECT_TYPES.Stun] == nil then
self.effect_source = source
self.effects_sfx[EFFECT_TYPES.Stun] = attach_effect(self,STUN_MODEL,OVERHEAD_HEIGHT)
if self.attack ~= nil and not self.bossType then
self.attack:reset()
end
end
EFFECT_TIMER:callDelayed(duration, stun_check_end,self)
end
local function electrify_damage(self)
local dur = self.effects_stacks[EFFECT_TYPES.Electrify]
local source = self.effect_source
local afflict_dmg = (self.effects_stacks[EFFECT_TYPES.Affliction] or 0) * 0.05
local base_damage = ((25 * dur)/(0.9^dur)) * (source.damageMult + source.damageMultTable[DamageTypes.Electrify] + afflict_dmg)
CreateFloatingText(math.floor(base_damage),255,255,0,self.position[1],self.position[2],SYMBOL_ELECTRIFY)
self.effects_stacks[EFFECT_TYPES.Electrify] = dur - 1
self:receive_damage(base_damage)
end
local function electrify_tick(self,sfx)
if self.sfx ~= sfx then return end
local dur = self.effects_stacks[EFFECT_TYPES.Electrify]
if dur == nil then return end
if dur <= 0.0 then
DestroyEffect(self.effects_sfx[EFFECT_TYPES.Electrify])
self.effects_sfx[EFFECT_TYPES.Electrify] = nil
else
EFFECT_TIMER:callDelayed(0.9 ^ dur, electrify_tick,self,sfx)
electrify_damage(self)
end
end
function Unit.electrify(self,source,stacks)
stacks = stacks + source.electricBonus
if GetRandomReal(0.0,1.0) <= stacks%%1 then
stacks = math.ceil(stacks)
else
stacks = math.floor(stacks)
if stacks <= 0 then return 0 end
end
self.effects_stacks[EFFECT_TYPES.Electrify] = (self.effects_stacks[EFFECT_TYPES.Electrify] or 0) + stacks
local dur = self.effects_stacks[EFFECT_TYPES.Electrify]
if self.effects_sfx[EFFECT_TYPES.Electrify] == nil then
self.effect_source = source
self.effects_sfx[EFFECT_TYPES.Electrify] = attach_effect(self,ELECTRIFY_MODEL,CHEST_HEIGHT)
EFFECT_TIMER:callDelayed(0.9 ^ dur, electrify_tick,self,self.sfx)
end
while self.effects_stacks and self.effects_stacks[EFFECT_TYPES.Electrify] > MAX_STACKS do
electrify_damage(self)
end
return stacks
end
local function burn_damage(self)
local source = self.effect_source
local afflict_dmg = (self.effects_stacks[EFFECT_TYPES.Affliction] or 0) * 0.05
local base_damage = (12.5 * self.effects_stacks[EFFECT_TYPES.Burn]) * (source.damageMult + source.damageMultTable[DamageTypes.Burn] + afflict_dmg)
CreateFloatingText(math.floor(base_damage),212,94,25,self.position[1],self.position[2],SYMBOL_BURN)
self:receive_damage(base_damage)
end
local function check_burn(self,sfx)
if self.sfx ~= sfx then return end
if self.effects_dur[EFFECT_TYPES.Burn] ~= nil and self.effects_dur[EFFECT_TYPES.Burn] >= GAME_TIME then
burn_damage(self)
EFFECT_TIMER:callDelayed(1.0, check_burn,self,sfx)
else
DestroyEffect(self.effects_sfx[EFFECT_TYPES.Burn])
self.effects_sfx[EFFECT_TYPES.Burn] = nil
self.effects_stacks[EFFECT_TYPES.Burn] = nil
end
end
function Unit.burn(self,source,stacks)
stacks = stacks + source.burnBonus
if GetRandomReal(0.0,1.0) <= stacks%%1 then
stacks = math.ceil(stacks)
else
stacks = math.floor(stacks)
if stacks <= 0 then return 0 end
end
self.effects_stacks[EFFECT_TYPES.Burn] = (self.effects_stacks[EFFECT_TYPES.Burn] or 0) + stacks
self.effects_dur[EFFECT_TYPES.Burn] = GAME_TIME + 5.01
if self.effects_sfx[EFFECT_TYPES.Burn] == nil then
self.effect_source = source
self.effects_sfx[EFFECT_TYPES.Burn] = attach_effect(self,BURN_MODEL,0)
EFFECT_TIMER:callDelayed(1.0, check_burn,self,self.sfx)
end
while self.effects_stacks ~= nil and self.effects_stacks[EFFECT_TYPES.Burn] > MAX_STACKS do
burn_damage(self)
if self.effects_stacks ~= nil then
self.effects_stacks[EFFECT_TYPES.Burn] = self.effects_stacks[EFFECT_TYPES.Burn] - 1
end
end
return stacks
end
local function frost_iter(u,source,dmg,str)
if Types.is(u,Types.Damageable) then
CreateFloatingText(str,128,128,255,u.position[1],u.position[2],SYMBOL_FROST)
u:receive_damage(dmg)
end
end
local function frost_damage(area,dmg,x,y,source)
local half = area * 0.5
local str = math.floor(dmg)
unit_collision_hash:each(x - half, y - half, area, area, frost_iter,source,dmg,str)
end
function Unit:trigger_frost()
if self.effects_sfx[EFFECT_TYPES.Frost] ~= nil then
local source = self.effect_source
local area = (self.size * self.scale_mult + 96) * source.areaMult
local afflict_dmg = (self.effects_stacks[EFFECT_TYPES.Affliction] or 0) * 0.05
local dmg = (25 * self.effects_stacks[EFFECT_TYPES.Frost]) * (source.damageMult + source.damageMultTable[DamageTypes.Frost] + afflict_dmg)
self.effects_stacks[EFFECT_TYPES.Frost] = nil
EFFECT_TIMER:callDelayed(0.1, frost_damage,area,dmg,self.position[1],self.position[2],source)
DestroyEffect(self.effects_sfx[EFFECT_TYPES.Frost])
local sfx = AddSpecialEffect("Abilities\\Spells\\Undead\\FrostNova\\FrostNovaTarget.mdl",self.position[1],self.position[2])
BlzSetSpecialEffectScale(sfx,area/196.0)
DestroyEffect(sfx)
self.effects_sfx[EFFECT_TYPES.Frost] = nil
end
end
function Unit.frost(self,source,stacks)
stacks = stacks + source.frostBonus
if GetRandomReal(0.0,1.0) <= stacks%%1 then
stacks = math.ceil(stacks)
else
stacks = math.floor(stacks)
if stacks <= 0 then return 0 end
end
self.effects_stacks[EFFECT_TYPES.Frost] = (self.effects_stacks[EFFECT_TYPES.Frost] or 0) + stacks
self.effects_dur[EFFECT_TYPES.Frost] = GAME_TIME + 10.0
if self.effects_sfx[EFFECT_TYPES.Frost] == nil then
self.effect_source = source
self.effects_sfx[EFFECT_TYPES.Frost] = attach_effect(self,FROST_MODEL,0)
end
if self.effects_stacks[EFFECT_TYPES.Frost] > MAX_STACKS then
self:trigger_frost()
end
return stacks
end
function Unit:get_elemental_effect_stacks()
return (self.effects_stacks[EFFECT_TYPES.Frost] or 0) +
(self.effects_stacks[EFFECT_TYPES.Burn] or 0) +
(self.effects_stacks[EFFECT_TYPES.Electrify] or 0)
end
local function update()
for self in units:elements() do
if not self.effect_source then goto continue end
local x,y = self.position[1] - SFX_OFFSET, self.position[2] - SFX_OFFSET
local z = self.position[3]
local scale = self:get_scale()
for i = 1, MAX_EFFECTS, 1 do
if self.effects_sfx[i] then
BlzSetSpecialEffectPosition(self.effects_sfx[i],x,y,effect_offsets[i] * scale + z)
end
end
::continue::
end
end
local function update_frost()
for self in units:elements() do
if self.effects_dur[EFFECT_TYPES.Frost] and self.effects_dur[EFFECT_TYPES.Frost] < GAME_TIME then
DestroyEffect(self.effects_sfx[EFFECT_TYPES.Frost])
self.effects_sfx[EFFECT_TYPES.Frost] = nil
self.effects_dur[EFFECT_TYPES.Frost] = nil
self.effects_stacks[EFFECT_TYPES.Frost] = nil
end
end
end
function Unit.ClearEffectsForPlayer(playerUnit)
EFFECT_TIMER:reset()
for self in units:elements() do
if self.effect_source == playerUnit then
self.stunned = nil
for i = 1, MAX_EFFECTS, 1 do
DestroyEffect(self.effects_sfx[i])
self.effects_sfx[i] = nil
self.effects_dur[i] = nil
self.effects_stacks[i] = nil
end
end
end
end
function GetEffectCountTotal(whichEffect)
local count = 0
for self in units:elements() do
if self.effects_sfx[whichEffect] then
count = count + 1
end
end
return count
end
local defaultBurn = Unit.burn
local function burn_add_trait(self,source,stacks)
trait_fire_affinity:add_to_pool()
Unit.burn = defaultBurn
return self:burn(source,stacks)
end
local defaultFrost = Unit.frost
local function frost_add_trait(self,source,stacks)
trait_frost_affinity:add_to_pool()
Unit.frost = defaultFrost
return self:frost(source,stacks)
end
local defaultElectrify = Unit.electrify
local function electrify_add_trait(self,source,stacks)
trait_lightning_affinity:add_to_pool()
Unit.electrify = defaultElectrify
return self:electrify(source,stacks)
end
local defaultFragile = Unit.frail
local defaultAffliction = Unit.afflict
local function frail_add_trait(self,source,stacks)
trait_false_blessing:add_to_pool()
Unit.frail = defaultFragile
Unit.afflict = defaultAffliction
return self:frail(source,stacks)
end
local function afflict_add_trait(self,source,stacks)
trait_false_blessing:add_to_pool()
Unit.frail = defaultFragile
Unit.afflict = defaultAffliction
return self:afflict(source,stacks)
end
local defaultSlow = Unit.slow
local function slow_add_trait(self,source,stacks)
trait_crippling_strikes:add_to_pool()
Unit.slow = defaultSlow
return self:slow(source,stacks)
end
function Unit.StunInit()
EFFECT_TIMER:callPeriodically(DEFAULT_TIMEOUT, nil, update)
EFFECT_TIMER:callPeriodically(1.0, nil, update_frost)
end
function ResetEffectTraits()
Unit.burn = burn_add_trait
Unit.frost = frost_add_trait
Unit.electrify = electrify_add_trait
Unit.frail = frail_add_trait
Unit.afflict = afflict_add_trait
Unit.slow = slow_add_trait
Attack.added_summon_trait = false
end
end
do
WeightedTable = {}
WeightedTable.__index = WeightedTable
function WeightedTable.new()
local self = setmetatable(NewTable(),WeightedTable)
self.dirty = true
self.weights = NewTable()
self.objects = NewTable()
return self
end
function WeightedTable:destroy()
if self.alias_table ~= nil then
self.alias_table:destroy()
self.alias_table = nil
end
self.dirty = nil
ReleaseTable(self.weights)
ReleaseTable(self.objects)
self.weights = nil
self.objects = nil
setmetatable(self,nil)
ReleaseTable(self)
end
function WeightedTable:add(object,weight)
table.insert(self.weights,weight)
table.insert(self.objects,object)
self.dirty = true
end
function WeightedTable:remove(object)
local i = table.removeobject(self.objects, object)
table.remove(self.weights,i)
self.dirty = true
end
function WeightedTable:pop()
if self.dirty then
self.dirty = false
if self.alias_table ~= nil then
self.alias_table:destroy()
end
self.alias_table = Alias_Table:new(self.weights)
end
return self.objects[self.alias_table()]
end
end
function AstronomerOrbInit()
abiltrait_astronomer_speed = Trait.new({
name = "Speed",
description = "|cff00ff00+10%% Speed (Ability)|n+5%% Range|n+15%% Damage (Ability)|r",
icon = "AbilityAstronomersOrbs.dds",
action = function(trait,playerUnit,abil)
abil.speedMult = abil.speedMult + 0.1
abil.rangeMult = abil.rangeMult + 0.05
abil.damageMult = abil.damageMult + 0.15
end,
level_max = 10,
x2 = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_true_true_false,
})
abiltrait_astronomer_roughness = Trait.new({
name = "Roughness",
description = "|cff00ff00+15%% Base Crit Chance (Ability)|n+5%% Crit Damage (Ability)|r",
icon = "AbilityAstronomersOrbs.dds",
action = function(trait,playerUnit,abil)
abil.critDamageBonus = abil.critDamageBonus + 0.05
abil.critChanceBonus = abil.critChanceBonus + 0.15
end,
x2 = 9,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_false_true_true,
})
abiltrait_astronomer_mass = Trait.new({
name = "Mass",
description = "|cff00ff00+20%% Damage (Ability)|n+5%% Area|r|n|cffff0000-15%% Range (Ability)|r",
icon = "AbilityAstronomersOrbs.dds",
action = function(trait,playerUnit,abil)
abil.damageMult = abil.damageMult + 0.2
abil.areaMult = abil.areaMult + 0.05
abil.rangeMult = abil.rangeMult - 0.15
end,
level_max = 10,
x2 = 9,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_true_false,
})
abiltrait_astronomer_density = Trait.new({
name = "Density",
description = "|cff00ff00+15%% Damage (Ability)|n+5%% Crit Damage|r|n|cffff0000-10%% Area (Ability)|r",
icon = "AbilityAstronomersOrbs.dds",
action = function(trait,playerUnit,abil)
abil.damageMult = abil.damageMult + 0.15
abil.areaMult = abil.areaMult - 0.1
abil.critDamageBonus = abil.critDamageBonus + 0.05
end,
level_max = 10,
x2 = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_false_true,
})
abiltrait_astronomer_bodies = Trait.new({
name = "Bodies",
description = "|cff00ff00+1 Orb (Ability)|r|n|cffff0000-10%% Area|n-25%% Damage (Ability)|r",
icon = "AbilityAstronomersOrbs.dds",
action = function(trait,playerUnit,abil)
abil.damageMult = abil.damageMult - 0.25
abil.attack_count = abil.attack_count + 1
abil.areaMult = abil.areaMult - 0.1
end,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_three_six_ten,
})
abiltrait_astronomer_material = Trait.new({
name = "Material",
description = "|cff00ff00+6 Base Damage (Ability)|r",
icon = "AbilityAstronomersOrbs.dds",
action = function(trait,playerUnit,abil)
abil.damage = abil.damage + 6
end,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_three_six_ten,
})
ability_astronomers_orbs = Ability.new({
name = "Astronomer's Orbs",
description = "Summons metal orbs that circle around you and damage enemies. Orbs are indestructable and permanent. Orb's speed increases with your movement speed.",
icon = "AbilityAstronomersOrbs.dds",
action = function(abil,playerUnit)
local attack = Attack.new(astronomer_orb_attack,playerUnit,ability_astronomers_orbs)
abiltrait_astronomer_speed:add_to_pool(attack)
abiltrait_astronomer_roughness:add_to_pool(attack)
abiltrait_astronomer_mass:add_to_pool(attack)
abiltrait_astronomer_density:add_to_pool(attack)
abiltrait_astronomer_bodies:add_to_pool(attack)
abiltrait_astronomer_material:add_to_pool(attack)
end,
})
local function get_damage(self,target)
local dmg = Attack.get_damage(self)
local bonus
if self.electrifiedOrbs then
bonus = 1.0 + self:get_speed()
end
if self.orbitalShift then
bonus = bonus + DistanceTo(self.owner.position[1],self.owner.position[2],target.position[1],target.position[2])/300
end
return dmg * bonus
end
local function get_on_hit_chance(self)
return Attack.get_on_hit_chance(self) + self:get_speed() * 2.0
end
abilityupgrade_innerorbit = Ability.new({
upgradeAbility = ability_astronomers_orbs,
name = "Inner Orbit",
description = "Summons an additional orbit lane that spins in the opposite direction.",
action = function(abil,playerUnit,attack)
attack.innerOrbit = true
attack:update_func()
end,
})
abilityupgrade_electrifiedorbs = Ability.new({
upgradeAbility = ability_astronomers_orbs,
name = "Electrified Orbs",
description = "Orbs deal additional lightning damage based on their movement speed. Orbs can electrify enemies on hit based on movement speed.",
action = function(abil,playerUnit,attack)
attack.effect_function = attack_effect_electrify
attack.effectChance = 0.15
attack.get_on_hit_chance = get_on_hit_chance
attack.electrifiedOrbs = true
attack.get_damage = get_damage
attack.properties = attack.properties + AttackProperties.Elemental
end,
})
abilityupgrade_orbitalshift = Ability.new({
upgradeAbility = ability_astronomers_orbs,
name = "Orbital Shift",
description = "Orbs in the original lane move on eccentric orbits. Additional damage bonus depends on the distance between you and an orb.",
action = function(abil,playerUnit,attack)
attack.orbitalShift = true
attack.get_damage = get_damage
attack:update_func()
end,
})
function orb_iter(u,attack,x,y,list,range)
if Types.is(u,Types.Damageable) and list[u] == nil and IsInRange(x,y,u.position[1],u.position[2],range+u.half_size) then
list[u] = true
attack:deal_damage(u)
end
end
function orb_get_speed(self)
return (self.owner.speed * self.owner.moveSpeedMult + Attack.get_speed(self)) * 0.0075
end
local function orb_orbit(self)
local playerUnit = self.owner
local x,y = playerUnit.position[1],playerUnit.position[2]
local speed = self:get_speed()
self.rotation = self.rotation + speed
if self.rotation > PI then
self.rotation = self.rotation - TAU
for i = 1, #self.hit_lists, 1 do
ClearKeyTable(self.hit_lists[i])
end
end
local offset_angle = TAU / #self.orbs
local area = self:get_area()
local half = area * 0.5
local range = self:get_range()
local half_count = #self.orbs
local shift = self.orbitalShift
if shift then
self.shift_rotation = self.shift_rotation + 0.03
if self.shift_rotation > PI then
self.shift_rotation = self.shift_rotation - TAU
end
end
if self.innerOrbit then
self.inner_rotation = self.inner_rotation - speed
if self.inner_rotation < -PI then
self.inner_rotation = TAU + self.inner_rotation
end
half_count = half_count / 2
offset_angle = offset_angle * 2
end
for i = 1, #self.orbs, 1 do
local x1,y1,r
local t_range = range
if i > half_count then
r = offset_angle * (i - half_count) + self.inner_rotation
t_range = t_range * 0.5
else
r = offset_angle * i + self.rotation
end
if shift then
local sim = (math.max(math.abs(wrappedAngleDifference(r,self.shift_rotation)),HALF_PI)-0.785398) * 0.75
local diff = lerp(sim,1.0, easeInCubic(reverseWrap(GAME_TIME*0.5,0.0,1.0)))
t_range = t_range * diff
end
x1,y1 = x + Cos(r) * t_range,y + Sin(r) * t_range
BlzSetSpecialEffectPosition(self.orbs[i],x1-SFX_OFFSET,y1-SFX_OFFSET,50)
unit_collision_hash:each(x1 - half, y1 - half, area, area, orb_iter,self,x1,y1,self.hit_lists[i],half)
end
end
astronomer_orb_attack = {
name = "Astronomer's Orbs",
model = "AstronomerOrb.mdx",
speed = 10.0,
area = 96.0 ,
damage = 30.0,
range = 325.0,
base_scale = 0.35,
critDamageBonus = 0.45,
critChanceBonus = 0.1,
summon_count = 3.0,
properties = AttackProperties.Summon + AttackProperties.Projectile,
type = AttackTypes.Unique,
gain_action = function(self)
self.timer:callPeriodically(DEFAULT_TIMEOUT, nil, orb_orbit,self)
self.get_speed = orb_get_speed
end,
update_func = function(self)
self.orbs = self.orbs or NewTable()
self.hit_lists = self.hit_lists or NewTable()
self.rotation = self.rotation or 0.0
local count = MathRound(self:get_summon_count())
if self.innerOrbit then
count = count * 2
self.inner_rotation = self.inner_rotation or 0.0
end
if self.orbitalShift then
self.shift_rotation = self.shift_rotation or 0.0
end
while count ~= #self.orbs do
if count > #self.orbs then
table.insert(self.orbs,AddSpecialEffect(self.model,0,0))
table.insert(self.hit_lists,NewKeyTable())
self:update_summoned(1)
else
local pos = #self.orbs
DestroyEffect(self.orbs[pos])
ReleaseKeyTable(self.hit_lists[pos])
self.hit_lists[pos] = nil
self.orbs[pos] = nil
self:update_summoned(-1)
end
end
local scale = self:get_scale()
for i = 1, #self.orbs, 1 do
BlzSetSpecialEffectPosition(self.orbs[i],0,0,-500)
BlzSetSpecialEffectScale(self.orbs[i],scale)
end
end,
destroy_func = function(self)
self:update_summoned(-#self.orbs)
for i = 1, #self.orbs, 1 do
DestroyEffect(self.orbs[i])
ReleaseKeyTable(self.hit_lists[i])
self.hit_lists[i] = nil
end
self.rotation = nil
self.orbs = nil
ReleaseTable(self.orbs)
ReleaseTable(self.hit_lists)
self.hit_lists = nil
self.orbitalShift = nil
self.electrifiedOrbs = nil
self.innerOrbit = nil
self.get_speed = nil
self.inner_rotation = nil
self.shift_rotation = nil
self.get_damage = nil
self.get_on_hit_chance = nil
end,
}
end
function LightningStrikesInit()
abiltrait_lightningstrikes_conductivity = Trait.new({
name = "Conductivity",
description = "|cff00ff00+10%% Base Crit Chance (Ability)|n+15%% Area (Ability)|r|n|cffff0000-5%% Attack Speed (Ability)|r",
icon = "AbilityLightningStrikes.dds",
action = function(trait,playerUnit,abil)
abil.critChanceBonus = abil.critChanceBonus + 0.1
abil.areaMult = abil.areaMult + 0.15
abil.attackSpeedMult = abil.attackSpeedMult - 0.05
end,
level_max = 10,
x2 = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_true_true_false,
})
abiltrait_lightningstrikes_conversion = Trait.new({
name = "Conversion",
description = "|cff00ff00+15%% Attack Speed (Ability)|r",
icon = "AbilityLightningStrikes.dds",
action = function(trait,playerUnit,abil)
abil.attackSpeedMult = abil.attackSpeedMult + 0.15
end,
x2 = 9,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_false_true_true,
})
abiltrait_lightningstrikes_intensity = Trait.new({
name = "Intensity",
description = "|cff00ff00+20%% Damage (Ability)|n+20%% Crit Damage (Ability)|r|n|cffff0000-10%% Area (Ability)|r",
icon = "AbilityLightningStrikes.dds",
action = function(trait,playerUnit,abil)
abil.damageMult = abil.damageMult + 0.2
abil.critDamageBonus = abil.critDamageBonus + 0.2
abil.areaMult = abil.areaMult - 0.1
end,
level_max = 10,
x2 = 9,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_true_false,
})
abiltrait_lightningstrikes_charges = Trait.new({
name = "Charges",
description = "|cff00ff00+0.5 Lightning Strike (Ability)|r|n|cffff0000-10%% Damage (Ability)|r",
icon = "AbilityLightningStrikes.dds",
action = function(trait,playerUnit,abil)
abil.attack_count = abil.attack_count + 0.5
abil.damageMult = abil.damageMult - 0.1
end,
level_max = 10,
x2 = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_false_true,
})
abiltrait_lightningstrikes_capacity = Trait.new({
name = "Capacity",
description = "|cff00ff00+100 Base Damage (Ability)|r|n|cffff0000-50%% Effect On Hit Chance (Ability)|r",
icon = "AbilityLightningStrikes.dds",
action = function(trait,playerUnit,abil)
abil.damage = abil.damage + 100
abil.effectChanceMult = abil.effectChanceMult - 0.5
end,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_three_six_ten,
})
abiltrait_lightningstrikes_repercussion = Trait.new({
name = "Repercussion",
description = "|cff00ff00+100%% Electrify Chance (Ability)|r|n|cffff0000-50 Base Damage (Ability)|r",
icon = "AbilityLightningStrikes.dds",
action = function(trait,playerUnit,abil)
abil.effectChanceMult = abil.effectChanceMult + 1.0
abil.damage = abil.damage - 50
end,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_three_six_ten,
})
ability_lightningstrikes = Ability.new({
name = "Lightning Strikes",
description = "Strikes random enemies with lightning and stuns surrounding enemies around the target. Hit enemies get electrified.",
icon = "AbilityLightningStrikes.dds",
action = function(abil,playerUnit)
local attack = Attack.new(lightning_strikes_attack,playerUnit,ability_lightningstrikes)
abiltrait_lightningstrikes_conductivity:add_to_pool(attack)
abiltrait_lightningstrikes_conversion:add_to_pool(attack)
abiltrait_lightningstrikes_intensity:add_to_pool(attack)
abiltrait_lightningstrikes_charges:add_to_pool(attack)
abiltrait_lightningstrikes_capacity:add_to_pool(attack)
abiltrait_lightningstrikes_repercussion:add_to_pool(attack)
end,
})
local function on_hit_electrify(target,attack)
target:stun(attack.owner,1.0)
if GetRandomReal(0.0,1.0)<= 0.4 then
target:electrify(attack.owner,1)
end
end
abilityupgrade_electrifyingstrikes = Ability.new({
upgradeAbility = ability_lightningstrikes,
name = "Electrifying Strikes",
description = "Enemies stunned by Lightning Strikes have a 40%% chance to get electrified.",
action = function(abil,playerUnit,attack)
attack.electrifyingStrikes = true
attack.on_hit_function = on_hit_electrify
end,
})
abilityupgrade_explosivestrikes = Ability.new({
upgradeAbility = ability_lightningstrikes,
name = "Explosive Strikes",
description = "When a Lightning Strike deals a critical hit, it causes an explosion that deals fire damage based on the strikes strength (30%%).",
action = function(abil,playerUnit,attack)
attack.explosiveStrikes = true
end,
})
abilityupgrade_concentratedstrikes = Ability.new({
upgradeAbility = ability_lightningstrikes,
name = "Concentrated Strikes",
description = "Focusing strikes on the foe with the highest Health and closest enemies to it. +5%% damage bonus for each elemental stack on striked enemies.",
action = function(abil,playerUnit,attack)
attack.concentratedStrikes = true
end,
})
local strike_list = {}
local hit_crit
local function lightningstrike_iter(u)
if Types.is(u,Types.Damageable) then
table.insert(strike_list,u)
end
end
local function get_closest_highest_health(list,x,y)
local bestScore = math.huge
local bestUnit
local index
for i = 1, #list, 1 do
local u = list[i]
if not Types.is(u,Types.Damageable) then goto continue end
local score = DistanceSquaredTo(x,y,u.position[1],u.position[2])/u.health
if score < bestScore then
bestUnit = u
bestScore = score
index = i
end
::continue::
end
if bestUnit then
list[index], list[#list] = list[#list], list[index]
table.remove(list)
return bestUnit
else
return
end
end
local function lightningstrike_damage_iter(u,attack)
local crit
if attack.concentratedStrikes and Types.is(u,Types.Effectable) then
local stacks = u:get_elemental_effect_stacks() * 0.05 + 1.0
crit = attack:deal_damage(u,nil,attack:get_damage() * stacks)
else
crit = attack:deal_damage(u)
end
if crit then
hit_crit = true
end
end
local function lightningstrike_fire_damage_iter(u,attack)
attack:deal_damage(u,nil,attack:get_damage(u,DamageTypes.Fire) * 0.3)
end
local function lightning_strike_period(self)
local big_area = self:get_range()
local half_big = big_area * 0.5
local area = self:get_area()
local half = area * 0.5
local count = self:get_attack_count()
local scale = self:get_scale()
local x1,y1 = self.owner.position[1], self.owner.position[2]
unit_collision_hash:each(x1 - half_big, y1 - half_big, big_area, big_area, lightningstrike_iter)
local count = math.min(self:increment_count(),#strike_list)
for i = 1, count, 1 do
local target
if self.concentratedStrikes then
target = get_closest_highest_health(strike_list,x1,y1)
else
local pos = GetRandomInt(1,#strike_list)
target = strike_list[pos]
strike_list[pos] = strike_list[#strike_list]
strike_list[#strike_list] = nil
end
if target == nil or not Types.is(target,Types.Damageable) then
count = count + 1
else
local x,y = target.position[1],target.position[2]
local sfx = AddSpecialEffect(self.model,x-SFX_OFFSET,y-SFX_OFFSET)
BlzSetSpecialEffectScale(sfx,scale)
DestroyEffectEx(sfx)
hit_crit = false
unit_collision_hash:each(x - half, y - half, area, area, lightningstrike_damage_iter,self)
if hit_crit and self.explosiveStrikes then
local sfx2 = AddSpecialEffect("Objects\\Spawnmodels\\Human\\HCancelDeath\\HCancelDeath.mdl",x-SFX_OFFSET,y-SFX_OFFSET)
BlzSetSpecialEffectScale(sfx2,BlzGetSpecialEffectScale(sfx)*0.5)
DestroyEffect(sfx2)
unit_collision_hash:each(x - half*2, y - half*2, area*2, area*2, lightningstrike_fire_damage_iter,self)
end
end
end
ClearTable(strike_list)
self.timer:callDelayed(self:start_cooldown(), lightning_strike_period,self)
end
lightning_strikes_attack = {
name = "Lightning Strikes",
model = "Abilities\\Spells\\Other\\Monsoon\\MonsoonBoltTarget",
cooldown = 2.0,
area = 96.0,
damage = 200.0,
range = 1400.0,
base_scale = 1.0,
attack_count = 3.0,
effectChance = 1.0,
properties = AttackProperties.Elemental,
effect_function = attack_effect_electrify,
damageType = DamageTypes.Lightning,
on_hit_function = function(target,attack)
target:stun(attack.owner,1.0)
end,
type = AttackTypes.Unique,
gain_action = function(self)
self.timer:callDelayed(self:start_cooldown(), lightning_strike_period,self)
end,
destroy_func = function(self)
self.electrifyingStrikes = nil
self.explosiveStrikes = nil
self.concentratedStrikes = nil
end,
}
end
function PhantomNeedlesInit()
abiltrait_phantomneedles_firerate = Trait.new({
name = "Firerate",
description = "|cff00ff00+15%% Attack Speed (Ability)|r",
icon = "AbilityPhantomNeedles.dds",
action = function(trait,playerUnit,abil)
abil.attackSpeedMult = abil.attackSpeedMult + 0.15
end,
level_max = 10,
x2 = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_true_true_false,
})
abiltrait_phantomneedles_sharpness = Trait.new({
name = "Sharpness",
description = "|cff00ff00+20%% Damage (Ability)|r",
icon = "AbilityPhantomNeedles.dds",
action = function(trait,playerUnit,abil)
abil.damageMult = abil.damageMult + 0.2
end,
x2 = 9,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_false_true_true,
})
abiltrait_phantomneedles_spin = Trait.new({
name = "Spin",
description = "|cff00ff00+10%% Crit Damage (Ability)|r",
icon = "AbilityPhantomNeedles.dds",
action = function(trait,playerUnit,abil)
abil.critDamageBonus = abil.critDamageBonus + 0.1
end,
level_max = 10,
x2 = 9,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_true_false,
})
abiltrait_phantomneedles_puncture = Trait.new({
name = "Puncture",
description = "|cff00ff00+25%% Range (Ability)|n+25%% Force (Ability)|r",
icon = "AbilityPhantomNeedles.dds",
action = function(trait,playerUnit,abil)
abil.rangeMult = abil.rangeMult + 0.25
abil.forceMult = abil.forceMult + 0.25
end,
level_max = 10,
x2 = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_false_true,
})
abiltrait_phantomneedles_pinpoint = Trait.new({
name = "Pinpoint",
description = "|cff00ff00+50%% Crit Chance (Ability)|r",
icon = "AbilityPhantomNeedles.dds",
action = function(trait,playerUnit,abil)
abil.critChanceBonus = abil.critChanceBonus + 0.5
end,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_three_six_ten,
})
abiltrait_phantomneedles_power = Trait.new({
name = "Power",
description = "|cff00ff00+8 Base Damage (Ability)|r",
icon = "AbilityPhantomNeedles.dds",
action = function(trait,playerUnit,abil)
abil.damage = abil.damage + 8.0
end,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_three_six_ten,
})
ability_phantomneedles = Ability.new({
name = "Phantom Needles",
icon = "AbilityPhantomNeedles.dds",
description = "Fires needles at random nearby enemies in quick succession. Multistrike increases fire rate.",
action = function(abil,playerUnit)
local attack = Attack.new(phantom_needles_attack,playerUnit,ability_phantomneedles)
abiltrait_phantomneedles_firerate:add_to_pool(attack)
abiltrait_phantomneedles_sharpness:add_to_pool(attack)
abiltrait_phantomneedles_spin:add_to_pool(attack)
abiltrait_phantomneedles_puncture:add_to_pool(attack)
abiltrait_phantomneedles_pinpoint:add_to_pool(attack)
abiltrait_phantomneedles_power:add_to_pool(attack)
end,
})
abilityupgrade_phantomsplit = Ability.new({
upgradeAbility = ability_phantomneedles,
name = "Phantom Split",
description = "Critical hits split needles into two new projectiles. Both new needles are a copy of the original with its remaining hit count. Increases the base damage by 15.",
action = function(abil,playerUnit,attack)
attack.phantomSplit = true
attack.damage = attack.damage + 15
end,
})
abilityupgrade_phantomrift = Ability.new({
upgradeAbility = ability_phantomneedles,
name = "Phantom Rift",
description = "When needles disappear, they deal magic damage in an area. Damage is increased for each remaining piercing multiplied by damage and crit bonus.",
action = function(abil,playerUnit,attack)
attack.phatomRift = true
end,
})
local function get_crit_chance(self,target)
if Types.is(target,Types.Effectable) then
return Attack.get_crit_chance(self,target) + target:get_effect_stacks(EFFECT_TYPES.Slow) * 0.15
else
return Attack.get_crit_chance(self,target)
end
end
abilityupgrade_phantomfetters = Ability.new({
upgradeAbility = ability_phantomneedles,
name = "Phantom Fetters",
description = "Needles apply Slow on hit. Apply chance is reduced for each enemy hit by the projectile. For each Slow stack on enemies needles have an additional flat 15%% Crit Chance.",
action = function(abil,playerUnit,attack)
attack.phantomFetters = true
attack.get_crit_chance = get_crit_chance
end,
})
function Attack_get_new_cooldown_multi(self)
return self:get_attack_speed()/self:get_attack_count(self)
end
local function strike_period(self)
local big_area = self:get_range() * 2
local half_big = big_area * 0.5
local scale = self:get_scale()
local x,y = self.owner.position[1], self.owner.position[2]
local results = unit_collision_hash:get_of_type(x - half_big, y - half_big, big_area, big_area,Types.Damageable)
if #results > 0 then
local target = results[GetRandomInt(1,#results)]
Missile.new(self.model,x,y,angleBetween(x,y,target.position[1],target.position[2]),self:get_speed(),self.owner,self,half_big + (target.size + self:get_area()))
end
ReleaseTable(results)
self.timer:callDelayed(self:start_cooldown(), strike_period,self)
end
local function rift_iter(u,attack,dmg)
attack:basic_damage(u,dmg)
end
phantom_needles_attack = {
name = "Phantom Needles",
model = "Needle.MDX",
cooldown = 0.4,
area = 48.0,
damage = 40.0,
range = 375.,
base_scale = 1.0,
speed = 30.0,
force = 5,
attack_count = 1.0,
properties = AttackProperties.Projectile,
type = AttackTypes.Unique,
gain_action = function(self,data)
self.missile_hit_function = data.missile_hit_function
self.missile_destroy_function = data.missile_destroy_function
self.get_new_cooldown = Attack_get_new_cooldown_multi
self.timer:callDelayed(self:start_cooldown(), strike_period,self)
end,
destroy_func = function(self)
self.phantomSplit = nil
self.phatomRift = nil
self.phantomFetters = nil
self.get_crit_chance = nil
self.missile_hit_function = nil
self.missile_destroy_function = nil
self.get_new_cooldown = nil
end,
missile_hit_function = function(self,missile,crit,target)
if self.phantomSplit and missile.custom_data == nil and crit then
missile.custom_data = true
local angle = missile:get_angle()
local mis = Missile.new(self.model,missile.position[1],missile.position[2],angle + PI * 0.25,missile.speed,self.owner,self,missile.max_dist,missile.hits)
mis.custom_data = true
mis = Missile.new(self.model,missile.position[1],missile.position[2],angle - PI * 0.25,missile.speed,self.owner,self,missile.max_dist,missile.hits)
mis.custom_data = true
end
if self.phantomFetters and Types.is(target,Types.Effectable) then
local chance = missile.hits / 5
if GetRandomReal(0,1) <= chance then
target:slow(self.owner,math.ceil(chance))
end
end
end,
missile_destroy_function = function(self,missile)
if self.phatomRift and missile.hits > 0 then
local x,y = missile.position[1],missile.position[2]
local area = self:get_area() * 4.5
local half = area * 0.5
local dmg = missile.hits * self:get_damage() * self:get_crit_bonus() * 0.15
local sfx = AddSpecialEffect("NovaModel.mdx",x-SFX_OFFSET,y-SFX_OFFSET)
BlzSetSpecialEffectScale(sfx,area/128)
BlzSetSpecialEffectColor(sfx,200,0,255)
BlzSetSpecialEffectTimeScale(sfx,3.0)
DestroyEffect(sfx)
DestroyEffect(AddSpecialEffect("Abilities\\Spells\\Human\\Feedback\\ArcaneTowerAttack.mdl",x-SFX_OFFSET,y-SFX_OFFSET))
unit_collision_hash:each(x - half, y - half, area, area, rift_iter,self,dmg)
end
end,
}
end
function ArcaneSplintersInit()
abiltrait_arcanesplinters_charging = Trait.new({
name = "Charging",
description = "|cff00ff00+20%% Attack Speed (Ability)|r",
icon = "AbilityArcaneSplinters.dds",
action = function(trait,playerUnit,abil)
abil.attackSpeedMult = abil.attackSpeedMult + 0.2
end,
level_max = 10,
x2 = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_true_true_false,
})
abiltrait_arcanesplinters_power = Trait.new({
name = "Power",
description = "|cff00ff00+30%% Damage (Ability)|r",
icon = "AbilityArcaneSplinters.dds",
action = function(trait,playerUnit,abil)
abil.damageMult = abil.damageMult + 0.3
end,
x2 = 9,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_false_true_true,
})
abiltrait_arcanesplinters_focus = Trait.new({
name = "Focus",
description = "|cff00ff00+30%% Range (Ability)|r|n|cffff0000-10%% Area (Ability)|r",
icon = "AbilityArcaneSplinters.dds",
action = function(trait,playerUnit,abil)
abil.rangeMult = abil.rangeMult + 0.3
abil.areaMult = abil.areaMult - 0.1
end,
level_max = 10,
x2 = 9,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_true_false,
})
abiltrait_arcanesplinters_piercing = Trait.new({
name = "Piercing",
description = "|cff00ff00+5%% Crit Chance (Ability)|n+30%% Crit Damage (Ability)|r",
icon = "AbilityArcaneSplinters.dds",
action = function(trait,playerUnit,abil)
abil.critDamageBonus = abil.critDamageBonus + 0.05
abil.critChanceBonus = abil.critChanceBonus + 0.3
end,
level_max = 10,
x2 = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_false_true,
})
abiltrait_arcanesplinters_fading = Trait.new({
name = "Fading",
description = "|cff00ff00+1.5 Base Force (Ability)|n+10%% Area (Ability)|r|n|cffff0000-10%% Range (Ability)|r",
icon = "AbilityArcaneSplinters.dds",
action = function(trait,playerUnit,abil)
abil.force = abil.force + 1.5
abil.areaMult = abil.areaMult + 0.1
abil.rangeMult = abil.rangeMult - 0.1
end,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_three_six_ten,
})
abiltrait_arcanesplinters_split = Trait.new({
name = "Split",
description = "|cff00ff00+1 Base Multistrike (Ability)|n+10%% Area (Ability)|r",
icon = "AbilityArcaneSplinters.dds",
action = function(trait,playerUnit,abil)
abil.areaMult = abil.areaMult + 0.1
abil.multistrikeMult = abil.multistrikeMult + 1.0
end,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_three_six_ten,
})
ability_arcanesplinters = Ability.new({
name = "Arcane Splinters",
description = "Shoots short range projectiles in a vertical spread pattern. Projectiles remain for a short while dealing damage to enemies that touch them.",
icon = "AbilityArcaneSplinters.dds",
action = function(abil,playerUnit)
local attack = Attack.new(arcane_splinters_attack,playerUnit,ability_arcanesplinters)
abiltrait_arcanesplinters_charging:add_to_pool(attack)
abiltrait_arcanesplinters_power:add_to_pool(attack)
abiltrait_arcanesplinters_focus:add_to_pool(attack)
abiltrait_arcanesplinters_piercing:add_to_pool(attack)
abiltrait_arcanesplinters_fading:add_to_pool(attack)
abiltrait_arcanesplinters_split:add_to_pool(attack)
end,
})
abilityupgrade_arcaneunrest = Ability.new({
upgradeAbility = ability_arcanesplinters,
name = "Arcane Unrest",
description = "Arcane Splinters will never entirely stop moving and instead of losing damage over time, they gain damage. +20 Base Damage.",
action = function(abil,playerUnit,attack)
attack.damage = attack.damage + 20
attack.arcaneUnrest = true
end,
})
abilityupgrade_arcaneshivers = Ability.new({
upgradeAbility = ability_arcanesplinters,
name = "Arcane Shivers",
description = "Reduces the number of splinters, but makes them burst into smaller ones after a short time. Thus, the covered area and the total amount of projectiles is increased.",
action = function(abil,playerUnit,attack)
attack.arcaneShivers = true
attack.attack_count = attack.attack_count - 1.5
end,
})
abilityupgrade_arcaneelements = Ability.new({
upgradeAbility = ability_arcanesplinters,
name = "Arcane Elements",
description = "Splinters will get elemental damage and can apply affects on hit.",
action = function(abil,playerUnit,attack)
attack.arcaneElements = true
attack.effectChance = 0.25
attack.properties = attack.properties + AttackProperties.Elemental
end,
})
splinter = {}
splinter.__index = splinter
function splinter.new(scale,x1,y1,x2,y2,move_dur,dur,attack,area,half,reducMult)
local self = setmetatable(NewTable(),splinter)
self.sfx = AddSpecialEffect(attack.model,x1-SFX_OFFSET,y1-SFX_OFFSET)
BlzSetSpecialEffectScale(self.sfx,scale)
BlzSetSpecialEffectZ(self.sfx,16.0)
self.x1 = x1
self.y1 = y1
self.x2 = x2
self.y2 = y2
self.reducMult = reducMult or 1.0
if attack.arcaneUnrest then
self.move_dur_max = dur
else
self.move_dur_max = move_dur
end
self.move_dur = 0.0
self.max_dur = dur
self.dur = 0.0
self.attack = attack
self.area = area
self.half = half
attack.splinter_set:add(self)
return self
end
function splinter.destroy(self)
self.attack.splinter_set:remove(self)
DestroyEffect(self.sfx)
self.x1 = nil
self.y1 = nil
self.x2 = nil
self.y2 = nil
self.reducMult = nil
self.move_dur_max = nil
self.move_dur = nil
self.max_dur = nil
self.dur = nil
self.attack = nil
setmetatable(self,nil)
self.area = nil
self.half = nil
self.split = nil
self.sfx = nil
self.stop = nil
ReleaseTable(self)
end
local base_angle1 = 1.570796
local base_angle2 = 4.712389
local offset_angle = 0.3926991 *2
local base_attack_rate = 0.25
local function hit_iter(u,splint,reduction)
if splint.attack.arcaneElements then
local integer i = GetRandomInt(1,3)
if i == 1 then
splint.attack:deal_damage(u,reduction,nil,DamageTypes.Lightning,attack_effect_electrify)
elseif i == 2 then
splint.attack:deal_damage(u,reduction,nil,DamageTypes.Fire,attack_effect_burn)
else
splint.attack:deal_damage(u,reduction,nil,DamageTypes.Ice,attack_effect_frost)
end
else
splint.attack:deal_damage(u,reduction*splint.reducMult)
end
end
function move_arcane_splinters(self)
self.dmg_cd = self.dmg_cd + DEFAULT_TIMEOUT
local reset_hits = self.dmg_cd >= base_attack_rate
if reset_hits then
self.dmg_cd = 0.0
end
for splint in self.splinter_set:elements() do
splint.dur = splint.dur + DEFAULT_TIMEOUT
splint.move_dur = math.min(splint.move_dur_max,splint.move_dur + DEFAULT_TIMEOUT)
local t = splint.move_dur/splint.move_dur_max
local t2 = splint.stop or easeOutCubic(t)
local reduc = easeInCubic(splint.dur/splint.max_dur)
local x = lerp(splint.x1,splint.x2,t2)
local y = lerp(splint.y1,splint.y2,t2)
if self.arcaneShivers and t > 0.4 and not splint.split then
splint.split = true
local range = self:get_range()
local scale = self:get_scale() * 0.75
local area = self:get_area() * 0.75
local half = area * 0.75
local dur = self:get_force()
if self.arcaneUnrest then
range = range * 1.5
end
SOUND_SPLINTER_SPAWN:random()(GetRandomReal(1.2,1.5),true,x,y)
for i = 1, 3, 1 do
local range_dif = range * GetRandomReal(0.6,1.0)
local a = GetRandomReal(0,TAU)
local x2,y2 = x + Cos(a) * range_dif, y + Sin(a) * range_dif
local s = splinter.new(scale,x,y,x2,y2,1.5,dur,self,area,half)
s.split = true
end
end
BlzSetSpecialEffectPosition(splint.sfx,x-SFX_OFFSET,y-SFX_OFFSET,0)
BlzSetSpecialEffectAlpha(splint.sfx,math.floor(lerp(255,0,reduc)))
if reset_hits then
if self.arcaneUnrest then
reduc = reduc + 1.0
else
reduc = 1.0 - reduc
end
unit_collision_hash:each(x - splint.half, y - splint.half, splint.area, splint.area, hit_iter,splint,reduc)
if not splint.stop and unit_collision_hash:cell_occupied(x - splint.half, y - splint.half, splint.area, splint.area,Types.ProjectileStopper) then
splint.stop = t2
end
end
if splint.dur >= splint.max_dur then
splint:destroy()
end
end
self.timer:callDelayed(DEFAULT_TIMEOUT, move_arcane_splinters,self)
end
local function spawn_splinter(self,x,y,dur,area,half,scale,range)
SOUND_SPLINTER_SPAWN:random()(GetRandomReal(0.9,1.1),true,x,y)
local dir = GetRandomReal(-offset_angle,offset_angle)
local range_dif = range * GetRandomReal(0.6,1.0)
local x2,y2 = x + Cos(dir+base_angle1) * range_dif, y + Sin(dir+base_angle1) * range_dif
splinter.new(scale,x,y,x2,y2,1.5,dur,self,area,half)
dir = GetRandomReal(-offset_angle,offset_angle)
range_dif = range * GetRandomReal(0.6,1.0)
x2,y2 = x + Cos(dir+base_angle2) * range_dif, y + Sin(dir+base_angle2) * range_dif
splinter.new(scale,x,y,x2,y2,1.5,dur,self,area,half)
end
local function strike_period(self)
local range = self:get_range()
if self.arcaneUnrest then
range = range * 1.5
end
local scale = self:get_scale()
local x,y = self.owner.position[1], self.owner.position[2]
local count = self:increment_count(self)
local dur = self:get_force()
local area = self:get_area()
local half = area * 0.5
for i = 1, count, 1 do
self.timer:callDelayed(i*0.1, spawn_splinter,self,x,y,dur,area,half,scale,range)
end
self.timer:callDelayed(self:start_cooldown(), strike_period,self)
end
arcane_splinters_attack = {
name = "Arcane Splinters",
model = "ArcaneSplinter2.mdx",
cooldown = 5.0,
area = 48.0,
damage = 40.0,
range = 500.,
base_scale = 0.7,
speed = 30.0,
force = 5,
attack_count = 4.0,
damageType = DamageTypes.Magical,
properties = AttackProperties.Projectile,
type = AttackTypes.Unique,
gain_action = function(self)
self.timer:callDelayed(self:start_cooldown(), strike_period,self)
self.timer:callDelayed(DEFAULT_TIMEOUT, move_arcane_splinters,self)
self.splinter_set = Set.create()
self.dmg_cd = 0.0
end,
destroy_func = function(self)
for splint in self.splinter_set:elements() do
splint:destroy()
end
self.splinter_set:destroy()
self.splinter_set = nil
self.dmg_cd = nil
self.arcaneElements = nil
self.arcaneShivers = nil
self.arcaneUnrest = nil
end,
move = function(self,x,y)
for splint in self.splinter_set:elements() do
splint.x1 = splint.x1 + x
splint.y1 = splint.y1 + y
splint.x2 = splint.x2 + x
splint.y2 = splint.y2 + y
end
end
}
end
function RadiantAuraInit()
abiltrait_radiantaura_focus = Trait.new({
name = "Focus",
description = "|cff00ff00+35%% Damage (Ability)|r|n|cffff0000-10%% Area (Ability)|r",
icon = "AbilityRadiantAura",
action = function(trait,playerUnit,abil)
abil.damageMult = abil.damageMult + 0.35
abil.areaMult = abil.areaMult - 0.1
end,
level_max = 10,
x2 = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_true_true_false,
})
abiltrait_radiantaura_blessing = Trait.new({
name = "Blessing",
description = "|cff00ff00+10%% Damage (Ability)|n+10%% Attack Speed (Ability)|n+5%% Area (Ability)|r",
icon = "AbilityRadiantAura",
action = function(trait,playerUnit,abil)
abil.damageMult = abil.damageMult + 0.1
abil.areaMult = abil.areaMult + 0.05
abil.attackSpeedMult = abil.attackSpeedMult + 0.1
end,
x2 = 9,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_false_true_true,
})
abiltrait_radiantaura_strength = Trait.new({
name = "Strength",
description = "|cff00ff00+20%% Area (Ability)|n+10%% Total Stacks (Ability)|r",
icon = "AbilityRadiantAura.dds",
action = function(trait,playerUnit,abil)
abil.totalStacks = abil.totalStacks + 0.1
abil.areaMult = abil.areaMult + 0.2
end,
level_max = 10,
x2 = 9,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_true_false,
})
abiltrait_radiantaura_force = Trait.new({
name = "Force",
description = "|cff00ff00+20%% Damage (Ability)|n+15 Force (Ability)|r",
icon = "AbilityRadiantAura.dds",
action = function(trait,playerUnit,abil)
abil.damageMult = abil.damageMult + 0.2
abil.force = abil.force + 15
end,
level_max = 10,
x2 = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_false_true,
})
abiltrait_radiantaura_haste = Trait.new({
name = "Haste",
description = "|cff00ff00+0.05 Base Attack Speed (Ability)|r|n|cffff0000-300 Base Damage (Ability)|r",
icon = "AbilityRadiantAura.dds",
action = function(trait,playerUnit,abil)
abil.cooldown = abil.cooldown - 0.05
abil.damage = abil.damage - 300
end,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_three_six_ten,
})
abiltrait_radiantaura_might = Trait.new({
name = "Might",
description = "|cff00ff00+600 Base Damage (Ability)|r",
icon = "AbilityRadiantAura.dds",
action = function(trait,playerUnit,abil)
abil.damage = abil.damage + 600
end,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_three_six_ten,
})
ability_radiantaura = Ability.new({
name = "Radiant Aura",
description = "Casts light that distributes its damage equally among all enemies in its range. Automatically cast in intervals.",
icon = "AbilityRadiantAura.dds",
action = function(abil,playerUnit)
local attack = Attack.new(radiant_aura_attack,playerUnit,ability_radiantaura)
abiltrait_radiantaura_focus:add_to_pool(attack)
abiltrait_radiantaura_blessing:add_to_pool(attack)
abiltrait_radiantaura_strength:add_to_pool(attack)
abiltrait_radiantaura_force:add_to_pool(attack)
abiltrait_radiantaura_haste:add_to_pool(attack)
abiltrait_radiantaura_might:add_to_pool(attack)
end,
})
abilityupgrade_punitivelight = Ability.new({
upgradeAbility = ability_radiantaura,
name = "Punitive Light",
description = "Emits an additional magic attack that can apply Fragile and Affliction to all enemies.",
action = function(abil,playerUnit,attack)
attack.punitiveLight = true
end,
})
abilityupgrade_sacredflame = Ability.new({
upgradeAbility = ability_radiantaura,
name = "Sacred Flame",
description = "Emits an additional fire based attack that will distribute a certain number of burn stacks among all enemies in range. Number of stacks can be increased with burn chance.",
action = function(abil,playerUnit,attack)
attack.sacredFlame = true
attack.properties = attack.properties + AttackProperties.Elemental
end,
})
abilityupgrade_echoinglight = Ability.new({
upgradeAbility = ability_radiantaura,
name = "Echoing Light",
description = "Enemies hit by Radiant Aura have a chance to relay the same damage to enemies in close proximity.",
action = function(abil,playerUnit,attack)
attack.echoingLight = true
end,
})
local function count_iter(u,inRangeSet)
if Types.is(u,Types.Damageable) then
inRangeSet:add(u)
end
end
local function echo_iter(u,self,dmg,source)
if u ~= source then
self:deal_damage(u,nil,dmg)
end
end
local function damage_action(self,inRangeSet,destroySet,punitiveLight,sacredFlame)
local x,y = self.owner.position[1], self.owner.position[2]
local sfx = AddSpecialEffect(self.model,x-SFX_OFFSET,y-SFX_OFFSET)
SOUND_RADIANT_AURA(GetRandomReal(0.9,1.1),true,x,y)
local sacredStacks = 0
local count = inRangeSet:size()
local area = self:get_area() * 0.25
local half = area * 0.5
local scale = self:get_scale()
local regular = false
BlzSetSpecialEffectScale(sfx,scale)
if punitiveLight then
BlzSetSpecialEffectColor(sfx,255,100,255)
elseif sacredFlame > 0 then
sacredStacks = math.ceil(sacredFlame/count)
BlzSetSpecialEffectColor(sfx,255,100,100)
else
regular = true
end
DestroyEffectEx(sfx)
local dmg = self:get_damage()/count
for u in inRangeSet:elements() do
self:deal_damage(u,nil,dmg)
if not Types.is(u,Types.Damageable) then
inRangeSet:remove(u)
goto continue
elseif Types.is(u,Types.Effectable) then
if punitiveLight then
local chance = self.totalStacks
if GetRandomReal(0.0,1.0) < self.totalStacks - math.floor(self.totalStacks) then
chance = math.ceil(chance)
end
u:frail(self.owner,1+chance)
u:afflict(self.owner,1+chance)
elseif sacredFlame > 0 then
local actualStacks = math.min(sacredFlame,sacredStacks)
sacredFlame = sacredFlame - actualStacks
u:burn(self.owner,actualStacks)
end
end
if regular and self.echoingLight and GetRandomReal(0.0,1.0) < 0.1 then
local x,y = u.position[1], u.position[2]
AddSpecialEffect(self.model,x-SFX_OFFSET,-SFX_OFFSETy)
BlzSetSpecialEffectScale(sfx,scale*0.25)
DestroyEffectEx(sfx)
unit_collision_hash:each(x - half, y - half, area, area, echo_iter,self,dmg,u)
end
::continue::
end
if destroySet then
inRangeSet:destroy()
end
end
local function move_indicator(self,curTime,maxTime)
curTime = math.min(maxTime,curTime + DEFAULT_TIMEOUT)
local p = self.owner.position
BlzSetSpecialEffectPosition(self.indicator,p[1]-SFX_OFFSET,p[2]-SFX_OFFSET,0)
BlzSetSpecialEffectTime(self.indicator,(curTime/maxTime) * 0.5)
BlzSetSpecialEffectScale(self.indicator,self:get_area()/128)
if curTime == maxTime then return end
self.timer:callDelayed(DEFAULT_TIMEOUT, move_indicator,self,curTime,maxTime)
end
local function strike_period(self)
local x,y = self.owner.position[1], self.owner.position[2]
local area = self:get_area()
local half = area * 0.5
local attacks = self:increment_count(self)
local inRangeSet = Set.create()
unit_collision_hash:each(x - half, y - half, area, area, count_iter,inRangeSet)
local delay = 0.0
if self.punitiveLight then
self.timer:callDelayed(delay, damage_action,self,inRangeSet,false,true,0)
delay = delay + 0.15
end
if self.sacredFlame then
self.timer:callDelayed(delay, damage_action,self,inRangeSet,false,false,20 * self.totalStacks)
delay = delay + 0.15
end
for i = 1, attacks, 1 do
self.timer:callDelayed(delay, damage_action,self,inRangeSet,i==attacks,false,0)
delay = delay + 0.15
end
local cd = self:start_cooldown()
move_indicator(self,0,cd)
self.timer:callDelayed(cd, strike_period,self)
end
radiant_aura_attack = {
name = "Radiant Aura",
model = "RadiantAura.mdx",
cooldown = 4.0,
area = 600.0,
damage = 3000.0,
base_scale = 600.0/128,
critDamageBonus = 0.3,
force = 5,
attack_count = 1.0,
damageType = DamageTypes.Magical,
properties = AttackProperties.Area,
type = AttackTypes.Unique,
gain_action = function(self)
self.totalStacks = 1.0
self.indicator = AddSpecialEffect("AreaDamageIndicator.mdx",self.owner.position[1]-SFX_OFFSET,self.owner.position[2]-SFX_OFFSET)
local cd = self:start_cooldown()
BlzSetSpecialEffectScale(self.indicator,self:get_area()/128)
BlzSetSpecialEffectTimeScale(self.indicator,0.0)
move_indicator(self,0,cd)
self.timer:callDelayed(cd, strike_period,self)
end,
destroy_func = function(self)
BlzSetSpecialEffectZ(self.indicator,-9999)
DestroyEffect(self.indicator)
self.indicator = nil
self.echoingLight = nil
self.sacredFlame = nil
self.punitiveLight = nil
self.totalStacks = nil
end,
}
end
function RingBladesInit()
abiltrait_ringblades_ripping = Trait.new({
name = "Ripping",
description = "|cff00ff00+10%% Base Crit Chance (Ability)|n+10%% Area (Ability)|r",
icon = "AbilityRingBlade.dds",
action = function(trait,playerUnit,abil)
abil.critChanceBonus = abil.critChanceBonus + 0.10
abil.areaMult = abil.areaMult + 0.1
end,
level_max = 10,
x2 = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_true_true_false,
})
abiltrait_ringblades_big = Trait.new({
name = "Big",
description = "|cff00ff00+15%% Damage (Ability)|n+10%% Area (Ability)|r",
icon = "AbilityRingBlade.dds",
action = function(trait,playerUnit,abil)
abil.damageMult = abil.damageMult + 0.15
abil.areaMult = abil.areaMult + 0.1
end,
x2 = 9,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_false_true_true,
})
abiltrait_ringblades_strong = Trait.new({
name = "Strong",
description = "|cff00ff00+25%% Damage (Ability)|n+10%% Cone Size (Ability)|r",
icon = "AbilityRingBlade.dds",
action = function(trait,playerUnit,abil)
abil.damageMult = abil.damageMult + 0.25
abil.angleMult = abil.angleMult + 0.1
end,
level_max = 10,
x2 = 9,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_true_false,
})
abiltrait_ringblades_fast = Trait.new({
name = "Fast",
description = "|cff00ff00+18%% Attack Speed (Ability)|n+10%% Cone Size (Ability)|r",
icon = "AbilityRingBlade.dds",
action = function(trait,playerUnit,abil)
abil.attackSpeedMult = abil.attackSpeedMult + 0.18
abil.angleMult = abil.angleMult + 0.1
end,
level_max = 10,
x2 = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_false_true,
})
abiltrait_ringblades_cruel = Trait.new({
name = "Cruel",
description = "|cff00ff00+15 Base Damage (Ability)|n+15%% Crit Damage (Ability)|r",
icon = "AbilityRingBlade.dds",
action = function(trait,playerUnit,abil)
abil.critDamageBonus = abil.critDamageBonus + 0.15
abil.damage = abil.damage + 15
end,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_three_six_ten,
})
abiltrait_ringblades_reaching = Trait.new({
name = "Reaching",
description = "|cff00ff00+40%% Range (Ability)|r",
icon = "AbilityRingBlade.dds",
action = function(trait,playerUnit,abil)
abil.rangeMult = abil.rangeMult + .4
end,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_three_six_ten,
})
ability_ringblades = Ability.new({
name = "Ring Blades",
description = "Throws blades left and right in alternation. Blades act like boomerangs passing you and then flying off screen. Multistrike increases the frequency of thrown blades.",
icon = "AbilityRingBlade.dds",
action = function(abil,playerUnit)
local attack = Attack.new(ring_blade_attack,playerUnit,ability_ringblades)
abiltrait_ringblades_ripping:add_to_pool(attack)
abiltrait_ringblades_big:add_to_pool(attack)
abiltrait_ringblades_strong:add_to_pool(attack)
abiltrait_ringblades_fast:add_to_pool(attack)
abiltrait_ringblades_cruel:add_to_pool(attack)
abiltrait_ringblades_reaching:add_to_pool(attack)
end,
})
abilityupgrade_cripplingblades = Ability.new({
upgradeAbility = ability_ringblades,
name = "Crippling Blades",
description = "Adds two projectiles per cycle. Additional blades can apply slow effect.",
action = function(abil,playerUnit,attack)
attack.cripplingBlades = true
end,
})
abilityupgrade_cyclone = Ability.new({
upgradeAbility = ability_ringblades,
name = "Cyclone",
description = "Blades start circling and fly 3 times as long as before.",
action = function(abil,playerUnit,attack)
attack.cyclone = true
end,
})
abilityupgrade_piercingblades = Ability.new({
upgradeAbility = ability_ringblades,
name = "Piercing Blades",
description = "Adds two projectiles per cycle. Additional blades deal magic damage and can apply fragile. Projectiles can't be blocked.",
action = function(abil,playerUnit,attack)
attack.piercingBlades = true
end,
})
ringblade = {}
ringblade.__index = ringblade
function ringblade.new(scale,x1,y1,x2,y2,attack,area,half,dur,dir,range,attack_effect)
local self = setmetatable(NewTable(),ringblade)
self.sfx = AddSpecialEffect(attack.model,x1-SFX_OFFSET,y1-SFX_OFFSET)
BlzSetSpecialEffectScale(self.sfx,scale)
BlzSetSpecialEffectZ(self.sfx,16.0)
self.x1 = x1
self.y1 = y1
self.x2 = x2
self.y2 = y2
self.dur = 0.0
if self.cyclone then
self.max_dur = dur * 3
else
self.max_dur = dur
end
self.attack_effect = attack_effect
self.dir = dir
self.range = range
self.attack = attack
self.switched_back = false
self.area = area
self.half = half
self.hit_list = NewKeyTable()
attack.blade_set:add(self)
return self
end
function ringblade:destroy()
self.attack.blade_set:remove(self)
DestroyEffect(self.sfx)
self.x1 = nil
self.y1 = nil
self.x2 = nil
self.y2 = nil
self.max_dur = nil
self.range = nil
self.dur = nil
self.dir = nil
self.attack_effect = nil
self.attack = nil
self.switched_back = nil
setmetatable(self,nil)
self.area = nil
self.half = nil
self.sfx = nil
ReleaseKeyTable(self.hit_list)
self.hit_list = nil
ReleaseTable(self)
end
local offset_angle = 0.1745329
local launchAngles = {PI,TAU}
local function hit_iter(u,blade,list)
if list[u] == nil then
list[u] = true
blade.attack:deal_damage(u,nil,nil,nil,blade.attack_effect)
end
end
local function move_blades(self)
for blade in self.blade_set:elements() do
local x,y
blade.dur = blade.dur + DEFAULT_TIMEOUT
local t = blade.dur/blade.max_dur
if self.cyclone then
blade.dir = blade.dir + 0.09
t = easeInOutSine(t)
local d = lerp(0,self.range * 2,t)
if blade.dir > PI then
ClearKeyTable(blade.hit_list)
blade.dir = blade.dir - TAU
end
x = blade.x1 + d * math.cos(blade.dir)
y = blade.y1 + d * math.sin(blade.dir)
else
if blade.dur > blade.max_dur then
t = easeInOutSine(t - 1.0) * 6
if not self.switched_back then
ClearKeyTable(blade.hit_list)
end
self.switched_back = true
x = lerp(blade.x2,blade.x1,t)
y = lerp(blade.y2,blade.y1,t)
else
t = easeInOutSine(t)
x = lerp(blade.x1,blade.x2,t)
y = lerp(blade.y1,blade.y2,t)
end
end
BlzSetSpecialEffectPosition(blade.sfx,x-SFX_OFFSET,y-SFX_OFFSET,16)
unit_collision_hash:each(x - blade.half, y - blade.half, blade.area, blade.area, hit_iter,blade,blade.hit_list)
if blade.dur >= blade.max_dur * 2 then
blade:destroy()
end
end
self.timer:callDelayed(DEFAULT_TIMEOUT, move_blades,self)
end
local function strike_period(self)
local range = self:get_range()
local x,y = self.owner.position[1], self.owner.position[2]
local area = self:get_area()
local offsetAngle = offset_angle * (self.angleMult + self.owner.areaMult + self.areaMult)
local dir = launchAngles[math.random(#launchAngles)]
local dir2 = dir + GetRandomReal(-offsetAngle,offsetAngle)
local x2,y2 = x + math.cos(dir2) * range, y + math.sin(dir2) * range
local force = self:get_force()
local scale = self:get_scale()
ringblade.new(scale,x,y,x2,y2,self,area,area * 0.5,force,dir2,range)
if self.cripplingBlades then
dir2 = dir + GetRandomReal(-offsetAngle,offsetAngle)
x2,y2 = x + math.cos(dir2) * range, y + math.sin(dir2) * range
local sfx = ringblade.new(scale,x+GetRandomReal(-64,64),y+GetRandomReal(-64,64),x2,y2,self,area,area * 0.5,force,dir2,range,attack_effect_slow).sfx
BlzSetSpecialEffectColor(sfx,0,255,255)
BlzSetSpecialEffectAlpha(sfx,100)
dir2 = dir + GetRandomReal(-offsetAngle,offsetAngle)
x2,y2 = x + math.cos(dir2) * range, y + math.sin(dir2) * range
sfx = ringblade.new(scale,x+GetRandomReal(-64,64),y+GetRandomReal(-64,64),x2,y2,self,area,area * 0.5,force,dir2,range,attack_effect_slow).sfx
BlzSetSpecialEffectColor(sfx,0,255,255)
BlzSetSpecialEffectAlpha(sfx,100)
end
if self.piercingBlades then
dir2 = dir + GetRandomReal(-offsetAngle,offsetAngle)
x2,y2 = x + math.cos(dir2) * range, y + math.sin(dir2) * range
local sfx = ringblade.new(scale,x+GetRandomReal(-64,64),y+GetRandomReal(-64,64),x2,y2,self,area,area * 0.5,force,dir2,range,attack_effect_fragile).sfx
BlzSetSpecialEffectColor(sfx,255,0,255)
BlzSetSpecialEffectAlpha(sfx,100)
dir2 = dir + GetRandomReal(-offsetAngle,offsetAngle)
x2,y2 = x + math.cos(dir2) * range, y + math.sin(dir2) * range
sfx = ringblade.new(scale,x+GetRandomReal(-64,64),y+GetRandomReal(-64,64),x2,y2,self,area,area * 0.5,force,dir2,range,attack_effect_fragile).sfx
BlzSetSpecialEffectColor(sfx,255,0,255)
BlzSetSpecialEffectAlpha(sfx,100)
end
self.timer:callDelayed(self:start_cooldown(), strike_period,self)
end
ring_blade_attack = {
name = "Ring Blades",
model = "RingBlade.mdx",
cooldown = 4.0,
area = 128.0,
damage = 120.0,
base_scale = 1.0,
range = 400.,
critDamageBonus = 0.65,
critChanceBonus = 0.3,
attack_count = 1.0,
effectChance = 1.0,
force = 2.5,
type = AttackTypes.Unique,
properties = AttackProperties.Projectile + AttackProperties.Melee,
gain_action = function(self)
self.angleMult = 1.0
self.blade_set = Set.create()
self.timer:callDelayed(DEFAULT_TIMEOUT, move_blades,self)
self.get_new_cooldown = Attack_get_new_cooldown_multi
self.timer:callDelayed(self:start_cooldown(), strike_period,self)
end,
destroy_func = function(self)
for blade in self.blade_set:elements() do
blade:destroy()
end
self.get_new_cooldown = nil
self.blade_set:destroy()
self.blade_set = nil
self.angleMult = nil
self.switched_back = nil
self.cripplingBlades = nil
self.cyclone = nil
self.piercingBlades = nil
end,
move = function(self,x,y)
for blade in self.blade_set:elements() do
blade.x1 = blade.x1 + x
blade.y1 = blade.y1 + y
blade.x2 = blade.x2 + x
blade.y2 = blade.y2 + y
end
end
}
end
function DragonsBreathInit()
abiltrait_dragonsbreath_coverage = Trait.new({
name = "Coverage",
description = "|cff00ff00+5%% Multistrike (Ability)|n+15%% Area (Ability)|r",
icon = "AbilityDragonsBreath.dds",
action = function(trait,playerUnit,abil)
abil.multistrikeMult = abil.multistrikeMult + 0.05
abil.areaMult = abil.areaMult + 0.15
end,
level_max = 10,
x2 = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_true_true_false,
})
abiltrait_dragonsbreath_reach = Trait.new({
name = "Reach",
description = "|cff00ff00+5%% Damage (Ability)|n+15%% Range (Ability)|r",
icon = "AbilityDragonsBreath.dds",
action = function(trait,playerUnit,abil)
abil.damageMult = abil.damageMult + 0.05
abil.rangeMult = abil.rangeMult + 0.15
end,
x2 = 9,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_false_true_true,
})
abiltrait_dragonsbreath_flammability = Trait.new({
name = "Flammability",
description = "|cff00ff00+20%% Burn Chance (Ability)|r",
icon = "AbilityDragonsBreath.dds",
action = function(trait,playerUnit,abil)
abil.effectChance = abil.effectChance + 0.2
end,
level_max = 10,
x2 = 9,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_true_false,
})
abiltrait_dragonsbreath_recovery = Trait.new({
name = "Recovery",
description = "|cff00ff00+12%% Attack Speed (Ability)|r",
icon = "AbilityDragonsBreath.dds",
action = function(trait,playerUnit,abil)
abil.attackSpeedMult = abil.attackSpeedMult + 0.12
end,
level_max = 10,
x2 = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_false_true,
})
abiltrait_dragonsbreath_agony = Trait.new({
name = "Agony",
description = "|cff00ff00+10%% Base Attack Speed (Ability)|r|n|cffff0000-10 Base Damage (Ability)|r",
icon = "AbilityDragonsBreath.dds",
action = function(trait,playerUnit,abil)
abil.cooldown = abil.cooldown - 0.5
abil.damage = abil.damage - 10
end,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_three_six_ten,
})
abiltrait_dragonsbreath_incineration = Trait.new({
name = "Incineration",
description = "|cff00ff00+25 Base Damage (Ability)|r|n|cffff0000-10%% Burn Chance (Ability)|r",
icon = "AbilityDragonsBreath.dds",
action = function(trait,playerUnit,abil)
abil.effectChance = abil.effectChance - 0.1
abil.damage = abil.damage + 25
end,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_three_six_ten,
})
ability_dragonsbreath = Ability.new({
name = "Dragon's Breath",
description = "Emits a wave of fire in the direction you're facing. The fire has a high chance to set enemies on fire.",
icon = "AbilityDragonsBreath.dds",
action = function(abil,playerUnit)
local attack = Attack.new(dragons_breath_attack,playerUnit,ability_dragonsbreath)
abiltrait_dragonsbreath_coverage:add_to_pool(attack)
abiltrait_dragonsbreath_reach:add_to_pool(attack)
abiltrait_dragonsbreath_flammability:add_to_pool(attack)
abiltrait_dragonsbreath_recovery:add_to_pool(attack)
abiltrait_dragonsbreath_agony:add_to_pool(attack)
abiltrait_dragonsbreath_incineration:add_to_pool(attack)
end,
})
abilityupgrade_heatblast = Ability.new({
upgradeAbility = ability_dragonsbreath,
name = "Heat Blast",
description = "Dragon's Breath knocks back enemies and increases direct damage by 100%%. Dragon's Breath now also counts as physical attack.",
action = function(abil,playerUnit,attack)
attack.heatBlast = true
attack.damageType = DamageTypes.Physical
attack.damageMult = attack.damageMult + 1.0
end,
})
abilityupgrade_streamoffire = Ability.new({
upgradeAbility = ability_dragonsbreath,
name = "Stream of Fire",
description = "Instead of a emitting a single moving wave of fire, Dragon's Breath emits a quick but continuous stream of fire.",
action = function(abil,playerUnit,attack)
attack.streamOfFire = true
attack.model = "FlameThrower1.mdx"
end,
})
abilityupgrade_refinedflames = Ability.new({
upgradeAbility = ability_dragonsbreath,
name = "Refined Flames",
description = "For every 200 hits, damage and attack speed increase by 1%% permanently.",
action = function(abil,playerUnit,attack)
attack.refinedFlames = true
attack.hits = 0
end,
})
dragonbreath = {}
dragonbreath.__index = dragonbreath
function dragonbreath.new(scale,yaw,x1,y1,x2,y2,attack,area,half,dur,dir_x,dir_y,range)
local self = setmetatable(NewTable(),dragonbreath)
self.sfx = AddSpecialEffect(attack.model,x1-SFX_OFFSET,y1-SFX_OFFSET)
BlzSetSpecialEffectZ(self.sfx,16.0)
BlzSetSpecialEffectYaw(self.sfx,yaw)
self.x1 = x1
self.y1 = y1
self.x2 = x2
self.y2 = y2
self.yaw = yaw
if attack.streamOfFire then
BlzSetSpecialEffectScale(self.sfx,range/768)
self.angleDiff = angle_difference(yaw, attack.owner.yaw)
else
BlzSetSpecialEffectScale(self.sfx,scale)
self.line_x = math.cos(yaw+HALF_PI)
self.line_y = math.sin(yaw+HALF_PI)
end
self.max_dur = dur
self.dur = 0.0
self.attack = attack
self.area = area
self.half = half
self.dir_x = dir_x
self.dir_y = dir_y
self.force = dur * 5
self.range = range
self.hit_list = NewKeyTable()
attack.breath_set:add(self)
return self
end
function dragonbreath:destroy()
self.attack.breath_set:remove(self)
DestroyEffect(self.sfx)
self.x1 = nil
self.y1 = nil
self.x2 = nil
self.y2 = nil
self.max_dur = nil
self.dur = nil
self.attack = nil
setmetatable(self,nil)
self.area = nil
self.line_x = nil
self.line_y = nil
self.half = nil
self.angleDiff = nil
self.sfx = nil
self.dir_x = nil
self.dir_y = nil
self.force = nil
self.range = nil
self.yaw = nil
ReleaseKeyTable(self.hit_list)
self.hit_list = nil
ReleaseTable(self)
end
local function hit_iter(u,breath,list,area,lx1,ly1,lx2,ly2,attack)
if not Types.is(u,Types.Damageable) then return end
if (list[u] == nil or list[u] < GAME_TIME) and point_in_range_of_line(lx1, ly1, lx2, ly2, u.position[1], u.position[2], area+ u.half_size) then
list[u] = GAME_TIME + 1.0
if attack.heatBlast and getmetatable(u) == Unit then
u:knockback(breath.dir_x,breath.dir_y,breath.force)
end
attack:deal_damage(u)
if attack.refinedFlames then
attack.hits = attack.hits + 1
if attack.hits == 200 then
attack.hits = 0
attack.attackSpeedMult = attack.attackSpeedMult + 0.01
attack.damageMult = attack.damageMult + 0.01
end
end
end
end
local function move_flame(self)
for breath in self.breath_set:elements() do
breath.dur = math.min(breath.dur + DEFAULT_TIMEOUT,breath.max_dur)
local x,y
if breath.attack.streamOfFire then
local range = breath.range
local half = range * 0.5
local owner = breath.attack.owner
breath.yaw = change_angle_bounded(breath.yaw,owner.yaw + breath.angleDiff,0.075)
local yaw = breath.yaw
x = owner.position[1]
y = owner.position[2]
local dir_x = math.cos(yaw)
local dir_y = math.sin(yaw)
local lx1 = x + dir_x * breath.range
local ly1 = y + dir_y * breath.range
local ox = x + dir_x * half
local oy = y + dir_y * half
breath.dir_x = dir_x
breath.dir_y = dir_y
unit_collision_hash:each(ox - half, oy - half, range, range, hit_iter,breath,breath.hit_list,range/4,x,y,lx1,ly1,self)
BlzSetSpecialEffectYaw(breath.sfx,yaw)
else
local t = easeOutSine(breath.dur/breath.max_dur)
local half = breath.half
x = lerp(breath.x1,breath.x2,t)
y = lerp(breath.y1,breath.y2,t)
local lx1 = x + breath.line_x * half
local ly1 = y + breath.line_y * half
local lx2 = x - breath.line_x * half
local ly2 = y - breath.line_y * half
unit_collision_hash:each(x - half, y - half, breath.area, breath.area, hit_iter,breath,breath.hit_list,half*0.25,lx1,ly1,lx2,ly2,self)
end
BlzSetSpecialEffectPosition(breath.sfx,x-SFX_OFFSET,y-SFX_OFFSET,16)
if breath.dur == breath.max_dur then
breath:destroy()
end
end
self.timer:callDelayed(DEFAULT_TIMEOUT, move_flame,self)
end
local function strike_period(self)
local range = self:get_range()
local x,y = self.owner.position[1], self.owner.position[2]
local area = self:get_area()
local half = area * 0.5
local count = self:increment_count(self)
local force = self:get_force()
local yaw = self.owner.yaw
local scale = self:get_scale()
SOUND_DRAGON_BREATH(GetRandomReal(0.9,1.1),true,x,y)
SOUND_FLAME_THROWER:random()(GetRandomReal(0.9,1.1),true,x,y)
if count == 1 then
local dir_x,dir_y = math.cos(yaw),math.sin(yaw)
local x2,y2 = x + dir_x * range, y + dir_y * range
dragonbreath.new(scale,yaw,x,y,x2,y2,self,area,half,force,dir_x,dir_y,range)
else
local a = math.min(0.4363323*count,TAU)
local angleBetweenProjectiles = a / (count - 1)
local startingAngle = yaw - (a / 2)
for i = 0, count - 1 do
yaw = startingAngle + (angleBetweenProjectiles * i)
local dir_x,dir_y = math.cos(yaw),math.sin(yaw)
local x2,y2 = x + dir_x * range, y + dir_y * range
dragonbreath.new(scale,yaw,x,y,x2,y2,self,area,half,force,dir_x,dir_y,range)
end
end
self.timer:callDelayed(self:start_cooldown(), strike_period,self)
end
dragons_breath_attack = {
name = "Dragon's Breath",
model = "Fire_Blast6.mdx",
cooldown = 5.0,
area = 386.0,
damage = 35.0,
base_scale = 1.0,
range = 400.,
attack_count = 1.0,
force = 3.0,
type = AttackTypes.Unique,
properties = AttackProperties.Area + AttackProperties.Elemental,
effectChance = 0.5,
damageType = DamageTypes.Fire,
effect_function = attack_effect_burn,
gain_action = function(self)
self.breath_set = Set.create()
self.timer:callDelayed(self:start_cooldown(), strike_period,self)
self.timer:callDelayed(DEFAULT_TIMEOUT, move_flame,self)
end,
destroy_func = function(self)
for breath in self.breath_set:elements() do
breath:destroy()
end
self.breath_set:destroy()
self.breath_set = nil
self.refinedFlames = nil
self.streamOfFire = nil
self.heatBlast = nil
self.hits = nil
end,
move = function(self,x,y)
for breath in self.breath_set:elements() do
breath.x1 = breath.x1 + x
breath.y1 = breath.y1 + y
breath.x2 = breath.x2 + x
breath.y2 = breath.y2 + y
end
end
}
end
function FrostAvalancheInit()
abiltrait_frostavalanche_frost = Trait.new({
name = "Frost",
description = "|cff00ff00+20%% Elemental Chance (Ability)|r",
icon = "AbilityFrostAvalanche.dds",
action = function(trait,playerUnit,abil)
abil.effectChance = abil.effectChance + 0.2
end,
level_max = 10,
x2 = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_true_true_false,
})
abiltrait_frostavalanche_impact = Trait.new({
name = "Impact",
description = "|cff00ff00+20%% Damage (Ability)|r",
icon = "AbilityFrostAvalanche.dds",
action = function(trait,playerUnit,abil)
abil.damageMult = abil.damageMult + 0.2
end,
x2 = 9,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_false_true_true,
})
abiltrait_frostavalanche_force = Trait.new({
name = "Force",
description = "|cff00ff00+10%% Force (Ability)|n+10%% Range (Ability)|n+10%% Damage (Ability)|r",
icon = "AbilityFrostAvalanche.dds",
action = function(trait,playerUnit,abil)
abil.forceMult = abil.forceMult + 0.1
abil.rangeMult = abil.rangeMult + 0.1
abil.damageMult = abil.damageMult + 0.1
end,
level_max = 10,
x2 = 9,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_true_false,
})
abiltrait_frostavalanche_freqence = Trait.new({
name = "Frequence",
description = "|cff00ff00+15%% Attack Speed (Ability)|r",
icon = "AbilityFrostAvalanche.dds",
action = function(trait,playerUnit,abil)
abil.attackSpeedMult = abil.attackSpeedMult + 0.15
end,
level_max = 10,
x2 = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_false_true,
})
abiltrait_frostavalanche_attraction = Trait.new({
name = "Attraction",
description = "|cff00ff00+25%% Force (Ability)|r|n|cffff0000-30 Base Knockback (Ability)|r",
icon = "AbilityFrostAvalanche.dds",
action = function(trait,playerUnit,abil)
abil.forceMult = abil.forceMult + 0.25
abil.knockback = abil.knockback - 30
end,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_three_six_ten,
})
abiltrait_frostavalanche_waves = Trait.new({
name = "Waves",
description = "|cff00ff00+0.8 Base Avalanche Waves (Ability)|r",
icon = "AbilityFrostAvalanche.dds",
action = function(trait,playerUnit,abil)
abil.attack_count = abil.attack_count + 0.8
end,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_three_six_ten,
})
ability_frostavalanche = Ability.new({
name = "Frost Avalanche",
description = "Emits 4 frost projectiles in diagonal directions. Deals mediocre direct damage but has a chance to apply frost effect.",
icon = "AbilityFrostAvalanche.dds",
action = function(abil,playerUnit)
local attack = Attack.new(frost_avalanche_attack,playerUnit,ability_frostavalanche)
abiltrait_frostavalanche_frost:add_to_pool(attack)
abiltrait_frostavalanche_impact:add_to_pool(attack)
abiltrait_frostavalanche_force:add_to_pool(attack)
abiltrait_frostavalanche_freqence:add_to_pool(attack)
abiltrait_frostavalanche_attraction:add_to_pool(attack)
abiltrait_frostavalanche_waves:add_to_pool(attack)
end,
})
abilityupgrade_quickfreeze = Ability.new({
upgradeAbility = ability_frostavalanche,
name = "Quick Freeze",
description = "Frost Avalanche will leave a trail of ice patches that slow down enemies. Patches may also apply frost effect.",
action = function(abil,playerUnit,attack)
attack.quickFreeze = true
end,
})
abilityupgrade_debrisavalanche = Ability.new({
upgradeAbility = ability_frostavalanche,
name = "Debris Avalanche",
description = "Frost Avalanche will spawn additional projectiles while moving.",
action = function(abil,playerUnit,attack)
attack.debrisAvalanche = true
end,
})
abilityupgrade_staticice = Ability.new({
upgradeAbility = ability_frostavalanche,
name = "Static Ice",
description = "Ice damage from all Avalanche attacks will get Lightning damage type and have a chance to electrify enemies.",
action = function(abil,playerUnit,attack)
attack.damageType = DamageTypes.Lightning
attack.effect_function = attack_effect_frost_electrify
end,
})
frostmeteor = {}
frostmeteor.__index = frostmeteor
function frostmeteor.new(scale,x1,y1,range,attack)
local self = setmetatable(NewTable(),frostmeteor)
local yaw = GetRandomReal(0,TAU)
self.sfx = AddSpecialEffect("OrbOfFrost.mdx",x1-SFX_OFFSET,y1-SFX_OFFSET)
BlzSetSpecialEffectYaw(self.sfx,yaw)
BlzSetSpecialEffectScale(self.sfx,scale)
self.x1 = x1
self.y1 = y1
self.x2 = x1 + math.cos(yaw) * range
self.y2 = y1 + math.sin(yaw) * range
self.max_dur = 2.25
self.dur = 0.0
self.attack = attack
self.area = scale * 96
self.half = self.area * 0.5
attack.meteor_set:add(self)
return self
end
function frostmeteor:destroy()
self.attack.meteor_set:remove(self)
DestroyEffect(self.sfx)
self.x1 = nil
self.y1 = nil
self.x2 = nil
self.y2 = nil
self.max_dur = nil
self.dur = nil
self.attack = nil
setmetatable(self,nil)
self.area = nil
self.half = nil
self.sfx = nil
ReleaseTable(self)
end
frostfloor = {}
frostfloor.__index = frostfloor
function frostfloor.new(model,scale,x,y,attack,dur,chance,hits)
local self = setmetatable(NewTable(),frostfloor)
self.x = x
self.y = y
self.sfx = AddSpecialEffect(model,x-SFX_OFFSET,y-SFX_OFFSET)
self.area = scale * 128
self.half = self.area * 0.5
self.attack = attack
self.hit_list = NewKeyTable()
self.dur = dur + GAME_TIME
self.chance = chance
self.hits = hits
attack.icefloor_set:add(self)
BlzSetSpecialEffectYaw(self.sfx,GetRandomReal(0,TAU))
BlzSetSpecialEffectScale(self.sfx,scale)
return self
end
function frostfloor:destroy()
self.attack.icefloor_set:remove(self)
DestroyEffect(self.sfx)
self.x = nil
self.y = nil
self.dur = nil
self.attack = nil
setmetatable(self,nil)
self.area = nil
self.half = nil
self.sfx = nil
self.hits = nil
self.chance = nil
ReleaseKeyTable(self.hit_list)
self.hit_list = nil
ReleaseTable(self)
end
frostavalanche = {}
frostavalanche.__index = frostavalanche
function frostavalanche.new(scale,yaw,x1,y1,x2,y2,attack,area,half,dur,dirx,diry,distPer)
local self = setmetatable(NewTable(),frostavalanche)
self.sfx = AddSpecialEffect(attack.model,x1-SFX_OFFSET,y1-SFX_OFFSET)
BlzSetSpecialEffectYaw(self.sfx,yaw)
BlzSetSpecialEffectScale(self.sfx,scale)
BlzSetSpecialEffectZ(self.sfx,32)
self.x1 = x1
self.y1 = y1
self.x2 = x2
self.y2 = y2
self.max_dur = dur
self.dur = 0.0
self.force = dur * 10
self.attack = attack
self.area = area
self.half = half
self.hit_list = NewKeyTable()
self.dir_x = dirx
self.dir_y = diry
self.distPer = distPer
self.dist = 0
attack.projectile_set:add(self)
return self
end
function frostavalanche:destroy()
self.attack.projectile_set:remove(self)
DestroyEffect(self.sfx)
self.x1 = nil
self.y1 = nil
self.x2 = nil
self.y2 = nil
self.max_dur = nil
self.dur = nil
self.attack = nil
setmetatable(self,nil)
self.area = nil
self.half = nil
self.sfx = nil
self.force = nil
self.dir_x = nil
self.dir_y = nil
self.distPer = nil
self.dist = nil
ReleaseKeyTable(self.hit_list)
self.hit_list = nil
ReleaseTable(self)
end
local function hit_iter(u,projectile,list,x,y,area,knockback_strength)
if not Types.is(u,Types.Damageable) then return end
if list == nil then
projectile.attack:deal_damage(u,0.5)
return
end
if (list[u] == nil or list[u] < GAME_TIME) and IsInRange(x,y,u.position[1],u.position[2],area+ u.half_size) then
list[u] = GAME_TIME + 1.0
if u.knockback ~= nil then
u:knockback(projectile.dir_x,projectile.dir_y,knockback_strength)
end
projectile.attack:deal_damage(u)
end
end
local function ice_iter(u,projectile,list)
if not Types.is(u,Types.Effectable) then return end
if list[u] == nil or list[u] < GAME_TIME then
list[u] = GAME_TIME + 2.0
if GetRandomReal(0,1) >= projectile.chance then
projectile.hits = projectile.hits - 1
u:slow(projectile.attack.owner,1)
end
end
end
function frostavalanche_check_ice(self)
for projectile in self.icefloor_set:elements() do
unit_collision_hash:each(projectile.x - projectile.half, projectile.y - projectile.half, projectile.area, projectile.area, ice_iter,projectile,projectile.hit_list)
if projectile.dur <= GAME_TIME or projectile.hits <= 0 then
projectile:destroy()
end
end
self.timer:callDelayed(0.1, frostavalanche_check_ice,self)
end
function zigzagLerp(startX, startY, endX, endY, t, amplitude, frequency)
local dx = endX - startX
local dy = endY - startY
local length = math.sqrt(dx * dx + dy * dy)
local normalX = -dy / length
local normalY = dx / length
local lerpX = startX + dx * t
local lerpY = startY + dy * t
local offset = amplitude * math.sin(t * frequency * TAU)
local finalX = lerpX + normalX * offset
local finalY = lerpY + normalY * offset
return finalX, finalY
end
local function move_projectiles(self)
local quickFreeze = self.quickFreeze
local debrisAvalanche = self.debrisAvalanche
for projectile in self.projectile_set:elements() do
projectile.dur = math.min(projectile.dur + DEFAULT_TIMEOUT,projectile.max_dur)
local t = easeOutSine(projectile.dur/projectile.max_dur)
local x,y = zigzagLerp(projectile.x1,projectile.y1,projectile.x2,projectile.y2,t,256,0.5)
projectile.dist = projectile.dist + projectile.distPer * (1.5-t)
if projectile.dist > 128 then
projectile.dist = 0
if quickFreeze then
frostfloor.new("icefloor2.mdx",self:get_scale()-0.85,x+GetRandomReal(-32,32),y+GetRandomReal(-32,32),self,5,1,100)
end
if debrisAvalanche then
frostmeteor.new(self:get_scale(),x,y,self:get_range()*0.25,self)
end
end
BlzSetSpecialEffectPosition(projectile.sfx,x-SFX_OFFSET,y-SFX_OFFSET,32)
local knockback_strength = (projectile.attack.knockback + projectile.force) * 0.6
destroyMe = false
unit_collision_hash:each(x - projectile.half, y - projectile.half, projectile.area, projectile.area, hit_iter,projectile,projectile.hit_list,x,y,projectile.half,knockback_strength)
if projectile.dur == projectile.max_dur or unit_collision_hash:cell_occupied(x, y,0,0,Types.ProjectileStopper) then
projectile:destroy()
end
end
for projectile in self.meteor_set:elements() do
projectile.dur = math.min(projectile.dur + DEFAULT_TIMEOUT,projectile.max_dur)
local t = projectile.dur/projectile.max_dur
local x = lerp(projectile.x1,projectile.x2,t)
local y = lerp(projectile.y1,projectile.y2,t)
local z = ParabolicArc(0, 600, t)
BlzSetSpecialEffectPosition(projectile.sfx,x-SFX_OFFSET,y-SFX_OFFSET,z)
BlzSetSpecialEffectPitch(projectile.sfx,lerp(-HALF_PI,HALF_PI,t))
if projectile.dur == projectile.max_dur then
local sfx = AddSpecialEffect("Abilities\\Weapons\\FrostWyrmMissile\\FrostWyrmMissile.mdl",x,y)
BlzSetSpecialEffectScale(sfx,BlzGetSpecialEffectScale(projectile.sfx)*0.5)
DestroyEffect(sfx)
unit_collision_hash:each(x - projectile.half, y - projectile.half, projectile.area, projectile.area, hit_iter,projectile,projectile.hit_list,x,y,projectile.half)
projectile:destroy()
end
end
self.timer:callDelayed(DEFAULT_TIMEOUT, move_projectiles,self)
end
local function strike_period(self)
local range = self:get_range()
local x,y = self.owner.position[1], self.owner.position[2]
local area = self:get_area()
local half = area * 0.5
local count = self:increment_count(self)
local force = self:get_force()
local scale = self:get_scale()
local angleBetweenProjectiles = TAU / count
local startingAngle = PI * 0.25
local distPer = (range/force) * DEFAULT_TIMEOUT
for i = 0, count - 1 do
local dir2 = startingAngle + (angleBetweenProjectiles * i)
local dirx = math.cos(dir2)
local diry = math.sin(dir2)
local x2,y2 = x + dirx * range, y + diry * range
frostavalanche.new(scale,dir2,x,y,x2,y2,self,area,half,force,dirx,diry,distPer)
end
self.timer:callDelayed(self:start_cooldown(), strike_period,self)
end
frost_avalanche_attack = {
name = "Frost Avalanche",
model = "FrostOrb.mdx",
cooldown = 5.0,
area = 196.0,
damage = 70.0,
base_scale = 1.5,
range = 800.,
effectChance = 0.5,
attack_count = 4.0,
properties = AttackProperties.Projectile + AttackProperties.Elemental,
force = 3.0,
knockback = 20.,
damageType = DamageTypes.Ice,
type = AttackTypes.Unique,
effect_function = attack_effect_frost,
gain_action = function(self,data)
self.projectile_set = Set.create()
self.icefloor_set = Set.create()
self.meteor_set = Set.create()
self.timer:callDelayed(self:start_cooldown(), strike_period,self)
self.timer:callDelayed(DEFAULT_TIMEOUT, move_projectiles,self)
self.timer:callDelayed(0.1, frostavalanche_check_ice,self)
self.knockback = data.knockback
end,
destroy_func = function(self)
for projectile in self.projectile_set:elements() do
projectile:destroy()
end
for projectile in self.icefloor_set:elements() do
projectile:destroy()
end
for projectile in self.meteor_set:elements() do
projectile:destroy()
end
self.projectile_set:destroy()
self.icefloor_set:destroy()
self.meteor_set:destroy()
self.meteor_set = nil
self.icefloor_set = nil
self.projectile_set = nil
self.knockback = nil
self.debrisAvalanche = nil
self.quickFreeze = nil
end,
move = function(self,x,y)
for projectile in self.projectile_set:elements() do
projectile.x1 = projectile.x1 + x
projectile.y1 = projectile.y1 + y
projectile.x2 = projectile.x2 + x
projectile.y2 = projectile.y2 + y
end
for projectile in self.meteor_set:elements() do
projectile.x1 = projectile.x1 + x
projectile.y1 = projectile.y1 + y
projectile.x2 = projectile.x2 + x
projectile.y2 = projectile.y2 + y
end
for projectile in self.icefloor_set:elements() do
projectile.x = projectile.x + x
projectile.y = projectile.y + y
BlzSetSpecialEffectPosition(projectile.sfx,projectile.x-SFX_OFFSET,projectile.y-SFX_OFFSET,0)
end
end,
}
end
function MeteorStrikeInit()
abiltrait_meteorstrike_blast = Trait.new({
name = "Blast",
description = "|cff00ff00+25%% Area (Ability)|r",
icon = "AbilityMeteorStrike.dds",
action = function(trait,playerUnit,abil)
abil.areaMult = abil.areaMult + 0.25
end,
level_max = 10,
x2 = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_true_true_false,
})
abiltrait_meteorstrike_impact = Trait.new({
name = "Impact",
description = "|cff00ff00+20%% Damage (Ability)|r",
icon = "AbilityMeteorStrike.dds",
action = function(trait,playerUnit,abil)
abil.damageMult = abil.damageMult + 0.2
end,
x2 = 9,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_false_true_true,
})
abiltrait_meteorstrike_frequency = Trait.new({
name = "Frequency",
description = "|cff00ff00+15%% Attack Speed (Ability)|r",
icon = "AbilityMeteorStrike.dds",
action = function(trait,playerUnit,abil)
abil.attackSpeedMult = abil.attackSpeedMult + 0.15
end,
level_max = 10,
x2 = 9,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_true_false,
})
abiltrait_meteorstrike_heat = Trait.new({
name = "Heat",
description = "|cff00ff00+30%% Crit Chance (Ability)|r",
icon = "AbilityMeteorStrike.dds",
action = function(trait,playerUnit,abil)
abil.critChanceBonus = abil.critChanceBonus + 0.3
end,
level_max = 10,
x2 = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_false_true,
})
abiltrait_meteorstrike_fusion = Trait.new({
name = "Fusion",
description = "|cff00ff00+50 Base Damage (Ability)|n+10%% Crit Chance (Ability)|r|n|cffff0000-10%% Multistrike (Ability)|r",
icon = "AbilityMeteorStrike.dds",
action = function(trait,playerUnit,abil)
abil.damage = abil.damage + 50
abil.critChanceBonus = abil.critChanceBonus + 0.1
abil.multistrikeMult = abil.multistrikeMult - 0.1
end,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_three_six_ten,
})
abiltrait_meteorstrike_shower = Trait.new({
name = "Shower",
description = "|cff00ff00+50%% Multistrike (Ability)|r|n|cffff0000-20 Base Damage (Ability)|n-30%% Area (Ability)|r",
icon = "AbilityMeteorStrike.dds",
action = function(trait,playerUnit,abil)
abil.multistrikeMult = abil.multistrikeMult + 0.5
abil.damage = abil.damage - 20
abil.areaMult = abil.areaMult - 0.3
end,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_three_six_ten,
})
ability_meteorstrike = Ability.new({
name = "Meteor Strike",
description = "Fires three projectiles in an arc in random directions. Aims within a certain distance range. Projectiles explode when hitting ground and deal area damage.",
icon = "AbilityMeteorStrike.dds",
action = function(abil,playerUnit)
local attack = Attack.new(meteor_strike_attack,playerUnit,ability_meteorstrike)
abiltrait_meteorstrike_blast:add_to_pool(attack)
abiltrait_meteorstrike_impact:add_to_pool(attack)
abiltrait_meteorstrike_frequency:add_to_pool(attack)
abiltrait_meteorstrike_heat:add_to_pool(attack)
abiltrait_meteorstrike_fusion:add_to_pool(attack)
abiltrait_meteorstrike_shower:add_to_pool(attack)
end,
})
abilityupgrade_burningcraters = Ability.new({
upgradeAbility = ability_meteorstrike,
name = "Burning Craters",
description = "Areas hit by Meteor Strike start burning including all enemies.",
action = function(abil,playerUnit,attack)
attack.burningCraters = true
end,
})
abilityupgrade_scattereddebris = Ability.new({
upgradeAbility = ability_meteorstrike,
name = "Scattered Debris",
description = "Meteor spawns 2.0 additional smaller projectiles upon impact. Bigger Area increases projectiles with 50.0% effectivity.",
action = function(abil,playerUnit,attack)
attack.scatteredDebris = true
end,
})
abilityupgrade_fireball = Ability.new({
upgradeAbility = ability_meteorstrike,
name = "Fireball",
description = "Meteor becomes a fireball that burns enemies in it's path. Explodes after a certain distance or after too many hits.",
action = function(abil,playerUnit,attack)
attack.fireball = true
end,
})
meteorstrike = {}
meteorstrike.__index = meteorstrike
function meteorstrike.new(scale,yaw,x1,y1,x2,y2,attack,area,half)
local self = setmetatable(NewTable(),meteorstrike)
self.sfx = AddSpecialEffect(attack.model,x1-SFX_OFFSET,y1-SFX_OFFSET)
BlzSetSpecialEffectYaw(self.sfx,yaw)
BlzSetSpecialEffectScale(self.sfx,scale)
self.x1 = x1
self.y1 = y1
self.x2 = x2
self.y2 = y2
self.max_dur = 1.75
self.dur = 0.0
self.attack = attack
self.hits = attack:get_force()
self.area = area
self.half = half
self.hit_list = NewKeyTable()
attack.projectile_set:add(self)
return self
end
function meteorstrike:destroy()
self.attack.projectile_set:remove(self)
DestroyEffect(self.sfx)
self.x1 = nil
self.y1 = nil
self.x2 = nil
self.y2 = nil
self.max_dur = nil
self.dur = nil
self.attack = nil
self.debris = nil
setmetatable(self,nil)
self.area = nil
self.half = nil
self.hits = nil
self.sfx = nil
ReleaseKeyTable(self.hit_list)
self.hit_list = nil
ReleaseTable(self)
end
meteorfire = {}
meteorfire.__index = meteorfire
function meteorfire.new(scale,x,y,attack,dur,chance)
local self = setmetatable(NewTable(),meteorfire)
self.x = x
self.y = y
self.sfx = AddSpecialEffect("FireMedium5.mdx",x-SFX_OFFSET,y-SFX_OFFSET)
self.area = scale * 128
self.half = self.area * 0.5
self.attack = attack
self.hit_list = NewKeyTable()
self.dur = dur + GAME_TIME
self.chance = chance
attack.fire_set:add(self)
BlzSetSpecialEffectYaw(self.sfx,GetRandomReal(0,TAU))
BlzSetSpecialEffectScale(self.sfx,scale)
return self
end
function meteorfire:destroy()
self.attack.fire_set:remove(self)
DestroyEffect(self.sfx)
self.x = nil
self.y = nil
self.dur = nil
self.attack = nil
setmetatable(self,nil)
self.area = nil
self.half = nil
self.chance =nil
self.sfx = nil
ReleaseKeyTable(self.hit_list)
self.hit_list = nil
ReleaseTable(self)
end
local function fire_iter(u,projectile,list)
if not Types.is(u,Types.Effectable) then return end
if list[u] == nil or list[u] < GAME_TIME then
list[u] = GAME_TIME + 1.5
if GetRandomReal(0,1) <= projectile.chance then
u:burn(projectile.attack.owner,1)
end
end
end
function meteorfire_check_fire(self)
for projectile in self.fire_set:elements() do
unit_collision_hash:each(projectile.x - projectile.half, projectile.y - projectile.half, projectile.area, projectile.area, fire_iter,projectile,projectile.hit_list)
if projectile.dur <= GAME_TIME then
projectile:destroy()
end
end
self.timer:callDelayed(0.1, meteorfire_check_fire,self)
end
local function hit_iter(u,projectile,x,y,area,reduc,hit_list)
if Types.is(u,Types.Damageable) and IsInRange(x,y,u.position[1],u.position[2],area) then
if hit_list then
if hit_list[u] ~= nil and hit_list[u] > GAME_TIME then
return
else
projectile.hits = projectile.hits - 1
hit_list[u] = GAME_TIME + 1.0
end
end
reduc = reduc or 1
if projectile.debris then
projectile.attack:deal_damage(u,0.5 * reduc)
else
projectile.attack:deal_damage(u, reduc)
end
end
end
local function move_projectiles(self)
local fireball = self.fireball
for projectile in self.projectile_set:elements() do
projectile.dur = math.min(projectile.dur + DEFAULT_TIMEOUT,projectile.max_dur)
local t = projectile.dur/projectile.max_dur
local x = lerp(projectile.x1,projectile.x2,t)
local y = lerp(projectile.y1,projectile.y2,t)
if fireball then
BlzSetSpecialEffectPosition(projectile.sfx,x-SFX_OFFSET,y-SFX_OFFSET,64)
local half = projectile.half * 0.5
local area = projectile.area * 0.5
unit_collision_hash:each(x - half, y - half, area, area, hit_iter,projectile,x,y,half,0.25,projectile.hit_list)
if unit_collision_hash:cell_occupied(x, y,0,0,Types.ProjectileStopper) then
projectile.hits = 0
end
else
BlzSetSpecialEffectPitch(projectile.sfx,lerp(-HALF_PI,HALF_PI,t))
BlzSetSpecialEffectPosition(projectile.sfx,x-SFX_OFFSET,y-SFX_OFFSET,ParabolicArc(0, 600, t))
end
if projectile.dur == projectile.max_dur or projectile.hits <= 0 then
local sfx = AddSpecialEffect("Objects\\Spawnmodels\\Human\\HCancelDeath\\HCancelDeath.mdl",x-SFX_OFFSET,y-SFX_OFFSET)
BlzSetSpecialEffectScale(sfx,BlzGetSpecialEffectScale(projectile.sfx)*0.5)
DestroyEffect(sfx)
unit_collision_hash:each(x - projectile.half, y - projectile.half, projectile.area, projectile.area, hit_iter,projectile,x,y,projectile.half)
local scale = self:get_scale()
if self.burningCraters then
if projectile.debris then
meteorfire.new(scale*0.25,x,y,self,6,1)
else
meteorfire.new(scale*0.5,x,y,self,6,1)
end
end
if self.scatteredDebris and projectile.debris == nil then
local range = self:get_range() * 0.35
local area = self:get_area() * 0.5
local half = area * 0.5
for i = 1, 2, 1 do
local yaw = GetRandomReal(0,TAU)
local x2,y2 = x + math.cos(yaw) * range, y + math.sin(yaw) * range
local m = meteorstrike.new(scale*0.5,yaw,x,y,x2,y2,self,area,half)
m.debris = true
end
end
projectile:destroy()
end
end
self.timer:callDelayed(DEFAULT_TIMEOUT, move_projectiles,self)
end
local function strike_period(self)
local range = self:get_range()
local x,y = self.owner.position[1], self.owner.position[2]
local area = self:get_area()
local half = area * 0.5
local count = self:increment_count(self)
local scale = self:get_scale()
for i = 1, count do
local x2,y2
local dir2
for j = 1, 500 do
local r2 = GetRandomReal(300,range)
dir2 = GetRandomReal(-PI,PI)
x2,y2 = x + math.cos(dir2) * r2, y + math.sin(dir2) * r2
if not unit_collision_hash:cell_occupied(x2, y2,0,0,Types.Collidable) then break end
end
meteorstrike.new(scale,dir2,x,y,x2,y2,self,area,half)
end
self.timer:callDelayed(self:start_cooldown(), strike_period,self)
end
meteor_strike_attack = {
name = "Meteor Strike",
model = "Abilities\\Weapons\\FireBallMissile\\FireBallMissile.mdl",
cooldown = 4.0,
area = 333.0,
damage = 250.0,
base_scale = 2.5,
range = 600.,
attack_count = 3.0,
critChanceBonus = 0.3,
force = 5.0,
damageType = DamageTypes.Fire,
properties = AttackProperties.Projectile + AttackProperties.Area + AttackProperties.Elemental,
type = AttackTypes.Unique,
gain_action = function(self,data)
self.projectile_set = Set.create()
self.timer:callDelayed(self:start_cooldown(), strike_period,self)
self.timer:callDelayed(DEFAULT_TIMEOUT, move_projectiles,self)
self.fire_set = Set.create()
self.timer:callDelayed(0.1, meteorfire_check_fire,self)
end,
destroy_func = function(self)
for projectile in self.projectile_set:elements() do
projectile:destroy()
end
for projectile in self.fire_set:elements() do
projectile:destroy()
end
self.fire_set:destroy()
self.projectile_set:destroy()
self.projectile_set = nil
self.fire_set = nil
self.fireball = nil
self.burningCraters = nil
self.scatteredDebris = nil
end,
move = function(self,x,y)
for projectile in self.projectile_set:elements() do
projectile.x1 = projectile.x1 + x
projectile.y1 = projectile.y1 + y
projectile.x2 = projectile.x2 + x
projectile.y2 = projectile.y2 + y
end
for projectile in self.fire_set:elements() do
projectile.x = projectile.x + x
projectile.y = projectile.y + y
BlzSetSpecialEffectPosition(projectile.sfx,projectile.x-SFX_OFFSET,projectile.y-SFX_OFFSET,0)
end
end
}
end
function DancingDaggersInit()
abiltrait_dancingdaggers_sharpness = Trait.new({
name = "Sharpness",
description = "|cff00ff00+20%% Damage (Ability)|r",
icon = "AbilityDancingDaggers.dds",
action = function(trait,playerUnit,abil)
abil.damageMult = abil.damageMult + 0.2
end,
level_max = 10,
x2 = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_true_true_false,
})
abiltrait_dancingdaggers_slicing = Trait.new({
name = "Slicing",
description = "|cff00ff00+20%% Crit Damage (Ability)|n+5%% Base Crit Chance (Ability)|r",
icon = "AbilityDancingDaggers.dds",
action = function(trait,playerUnit,abil)
abil.critDamageBonus = abil.critDamageBonus + 0.2
abil.critChanceBonus = abil.critChanceBonus + 0.05
end,
x2 = 9,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_false_true_true,
})
abiltrait_dancingdaggers_juggling = Trait.new({
name = "Juggling",
description = "|cff00ff00+15%% Attack Speed (Ability)|r",
icon = "AbilityDancingDaggers.dds",
action = function(trait,playerUnit,abil)
abil.attackSpeedMult = abil.attackSpeedMult + 0.15
end,
level_max = 10,
x2 = 9,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_true_false,
})
abiltrait_dancingdaggers_massive = Trait.new({
name = "Massive",
description = "|cff00ff00+25%% Area (Ability)|r",
icon = "AbilityDancingDaggers.dds",
action = function(trait,playerUnit,abil)
abil.areaMult = abil.areaMult + 0.25
end,
level_max = 10,
x2 = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_false_true,
})
abiltrait_dancingdaggers_catching = Trait.new({
name = "Catching",
description = "|cff00ff00+25%% Catching Area (Ability)|n+5%% Attack Speed (Ability)|r",
icon = "AbilityDancingDaggers.dds",
action = function(trait,playerUnit,abil)
abil.catchArea = abil.catchArea + 0.25
abil.attackSpeedMult = abil.attackSpeedMult + 0.05
end,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_three_six_ten,
})
abiltrait_dancingdaggers_lethality = Trait.new({
name = "Lethality",
description = "|cff00ff00+20 Base Damage (Ability)|r|n-10%% Attack Speed (Ability)|r",
icon = "AbilityDancingDaggers.dds",
action = function(trait,playerUnit,abil)
abil.damage = abil.damage + 20
abil.attackSpeedMult = abil.attackSpeedMult - 0.1
end,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_three_six_ten,
})
ability_dancingdaggers = Ability.new({
name = "Dancing Daggers",
description = "Launches a dagger at a random target and deal area damage. Then projectile will then fly back near towards the player; if caught, launch that dagger again.",
icon = "AbilityDancingDaggers.dds",
action = function(abil,playerUnit)
local attack = Attack.new(dancing_daggers_attack,playerUnit,ability_dancingdaggers)
abiltrait_dancingdaggers_sharpness:add_to_pool(attack)
abiltrait_dancingdaggers_slicing:add_to_pool(attack)
abiltrait_dancingdaggers_juggling:add_to_pool(attack)
abiltrait_dancingdaggers_massive:add_to_pool(attack)
abiltrait_dancingdaggers_catching:add_to_pool(attack)
abiltrait_dancingdaggers_lethality:add_to_pool(attack)
end,
})
abilityupgrade_hungryblades = Ability.new({
upgradeAbility = ability_dancingdaggers,
name = "Hungry Blades",
description = "Daggers will bounce to a second target before returning to the player.",
action = function(abil,playerUnit,attack)
attack.hungryBlades = true
end,
})
abilityupgrade_seekingblades = Ability.new({
upgradeAbility = ability_dancingdaggers,
name = "Seeking Blades",
description = "Dagger catch points will slowly move towards the player.",
action = function(abil,playerUnit,attack)
attack.seekingBlades = true
end,
})
abilityupgrade_phantomblades = Ability.new({
upgradeAbility = ability_dancingdaggers,
name = "Phantom Blades",
description = "Every catch of a dagger makes it deal 25%% more damage, but makes it fall 15%% faster.",
action = function(abil,playerUnit,attack)
attack.phantomBlades = true
end,
})
dancingdagger = {}
dancingdagger.__index = dancingdagger
function dancingdagger.new(scale,yaw,x1,y1,x2,y2,attack,area,half)
local self = setmetatable(NewTable(),dancingdagger)
self.sfx = AddSpecialEffect(attack.model,x1-SFX_OFFSET,y1-SFX_OFFSET)
BlzSetSpecialEffectZ(self.sfx,32)
BlzSetSpecialEffectYaw(self.sfx,yaw)
BlzSetSpecialEffectScale(self.sfx,scale)
self.x1 = x1
self.y1 = y1
self.x2 = x2
self.y2 = y2
self.max_dur = 0.75
self.dur = 0.0
self.attack = attack
self.area = area
self.half = half
self.catch = 0
attack.projectile_set:add(self)
return self
end
function dancingdagger:destroy()
self.attack.projectile_set:remove(self)
DestroyEffect(self.sfx)
DestroyEffect(self.returning)
self.x1 = nil
self.y1 = nil
self.x2 = nil
self.y2 = nil
self.max_dur = nil
self.dur = nil
self.attack = nil
self.returning = nil
self.second = nil
setmetatable(self,nil)
self.area = nil
self.half = nil
self.catch = nil
self.sfx = nil
ReleaseTable(self)
end
local function hit_iter(u,projectile)
local a,b,dmg = projectile.attack:deal_damage(u,1.0 + projectile.catch*0.25)
end
local found_player = false
local function find_player_iter(p,p2)
if p == p2 then
found_player = true
end
end
function area_has_player(x,y,area,p)
found_player = false
local half = area * 0.5
player_collision_hash:each(x - half, y - half, area, area,find_player_iter,p)
return found_player
end
local strike_list = {}
local strike_listPos = 0
local function random_iter(u)
if Types.is(u,Types.Damageable) then
strike_listPos = strike_listPos + 1
strike_list[strike_listPos] = u
end
end
local function move_projectiles(self)
for projectile in self.projectile_set:elements() do
projectile.dur = math.min(projectile.dur + DEFAULT_TIMEOUT,projectile.max_dur)
local t = projectile.dur/projectile.max_dur
local x = lerp(projectile.x1,projectile.x2,t)
local y = lerp(projectile.y1,projectile.y2,t)
if projectile.returning == nil then
BlzSetSpecialEffectPosition(projectile.sfx,x-SFX_OFFSET,y-SFX_OFFSET,32)
if unit_collision_hash:cell_occupied(x - projectile.half, y - projectile.half, projectile.area, projectile.area,Types.Damageable + Types.ProjectileStopper) then
local sfx = AddSpecialEffect(self.model3,x,y)
BlzSetSpecialEffectScale(sfx,self:get_scale() * 0.5)
DestroyEffectEx(sfx)
unit_collision_hash:each(x - projectile.half*3, y - projectile.half*3, projectile.area*3, projectile.area*3, hit_iter,projectile,x,y,projectile.half)
projectile.dur = 0.0
projectile.x1 = x
projectile.y1 = y
local success = false
if self.hungryBlades and projectile.second == nil then
local range = self:get_range() * 2
local half_range = range * 0.5
strike_listPos = 0
unit_collision_hash:each(x - half_range, y - half_range, range, range, random_iter)
if strike_listPos ~= 0 then
local target = strike_list[GetRandomInt(1,strike_listPos)]
local dir = math.atan(target.position[2]-y,target.position[1]-x)
half_range = half_range + (target.size + self:get_area())
projectile.x2 = projectile.x1 + math.cos(dir) * half_range
projectile.y2 = projectile.y1 + math.sin(dir) * half_range
BlzSetSpecialEffectYaw(projectile.sfx,dir)
projectile.second = true
success = true
end
end
if not success then
local x1,y1 = GetRandomLocationStrictRange(self.owner.position[1],self.owner.position[2],196,64,Types.Collidable)
projectile.returning = AddSpecialEffect(projectile.attack.model2,x1-SFX_OFFSET,y1-SFX_OFFSET)
BlzSetSpecialEffectYaw(projectile.returning,HALF_PI)
BlzSetSpecialEffectScale(projectile.returning,self.catchArea)
projectile.x2 = x1
projectile.y2 = y1
projectile.max_dur = 2.5 * .85^projectile.catch
end
elseif projectile.dur == projectile.max_dur then
projectile:destroy()
end
else
if self.seekingBlades then
local x2,y2 = VectorAngle(projectile.x2, projectile.y2, self.owner.position[1], self.owner.position[2])
projectile.x2 = projectile.x2 - x2 * 0.25
projectile.y2 = projectile.y2 - y2 * 0.25
BlzSetSpecialEffectPosition(projectile.returning,projectile.x2 - SFX_OFFSET,projectile.y2- SFX_OFFSET,0)
end
BlzSetSpecialEffectPosition(projectile.sfx,x-SFX_OFFSET,y-SFX_OFFSET,ParabolicArc(32, 600, t))
BlzSetSpecialEffectAlpha(projectile.returning,math.floor(lerp(0,255,t)))
if projectile.dur == projectile.max_dur then
local area = self.catchArea * 128
local half = area * 0.5
if area_has_player(x,y,area,self.owner) then
local range = self:get_range() * 2
local half_range = range * 0.5
strike_listPos = 0
unit_collision_hash:each(x - half_range, y - half_range, range, range, random_iter)
if strike_listPos ~= 0 then
local target = strike_list[GetRandomInt(1,strike_listPos)]
local dir = math.atan(target.position[2]-y,target.position[1]-x)
half_range = half_range + (target.size + self:get_area())
projectile.second = nil
projectile.x1 = x
projectile.y1 = y
projectile.x2 = x + math.cos(dir) * half_range
projectile.y2 = y + math.sin(dir) * half_range
projectile.dur = 0.0
projectile.max_dur = 0.75
if self.phantomBlades then
projectile.catch = projectile.catch + 1
end
BlzSetSpecialEffectYaw(projectile.sfx,dir)
BlzSetSpecialEffectColor(projectile.returning,0,255,0)
local sfx = AddSpecialEffect(self.model3,x-SFX_OFFSET,y-SFX_OFFSET)
BlzSetSpecialEffectScale(sfx,self:get_scale() * 0.5)
DestroyEffectEx(sfx)
unit_collision_hash:each(x - projectile.half*3, y - projectile.half*3, projectile.area*3, projectile.area*3, hit_iter,projectile,x,y,projectile.half)
else
projectile:destroy()
end
else
BlzSetSpecialEffectColor(projectile.returning,255,0,0)
projectile:destroy()
end
DestroyEffect(projectile.returning)
projectile.returning = nil
end
end
end
self.timer:callDelayed(DEFAULT_TIMEOUT, move_projectiles,self)
end
local function strike_period(self)
local range = self:get_range() * 2
local x,y = self.owner.position[1], self.owner.position[2]
local half_range = range * 0.5
local area = self:get_area()
local half = area * 0.5
local scale = self:get_scale()
strike_listPos = 0
unit_collision_hash:each(x - half_range, y - half_range, range, range, random_iter)
if strike_listPos ~= 0 then
local target = strike_list[GetRandomInt(1,strike_listPos)]
local dir = math.atan(target.position[2]-y,target.position[1]-x)
half_range = half_range + (target.size + area)
local x1 = x + math.cos(dir) * half_range
local y1 = y + math.sin(dir) * half_range
dancingdagger.new(scale,dir,x,y,x1,y1,self,area,half)
end
self.timer:callDelayed(self:start_cooldown(), strike_period,self)
end
dancing_daggers_attack = {
name = "Dancing Daggers",
model = "SkeletonAssassinDaggerMissile.mdx",
model2 = "CatchDaggerArea2.mdl",
model3 = "CullingSlash.mdx",
cooldown = 5.0,
area = 64.0,
damage = 165.0,
base_scale = 1.5,
range = 600.,
attack_count = 1.0,
critChanceBonus = 0.3,
properties = AttackProperties.Projectile,
type = AttackTypes.Unique,
gain_action = function(self,data)
self.projectile_set = Set.create()
self.timer:callDelayed(DEFAULT_TIMEOUT, move_projectiles,self)
self.model2 = data.model2
self.model3 = data.model3
self.catchArea = 1.0
self.get_new_cooldown = Attack_get_new_cooldown_multi
self.timer:callDelayed(self:start_cooldown(), strike_period,self)
end,
destroy_func = function(self)
for projectile in self.projectile_set:elements() do
projectile:destroy()
end
self.get_new_cooldown = nil
self.projectile_set:destroy()
self.projectile_set = nil
self.seekingBlades = nil
self.hungryBlades = nil
self.phantomBlades = nil
self.model2 = nil
self.model3 = nil
self.catchArea = nil
end,
move = function(self,x,y)
for projectile in self.projectile_set:elements() do
projectile.x1 = projectile.x1 + x
projectile.y1 = projectile.y1 + y
projectile.x2 = projectile.x2 + x
projectile.y2 = projectile.y2 + y
if projectile.returning then
BlzSetSpecialEffectPosition(projectile.returning,BlzGetLocalSpecialEffectX(projectile.returning)+x,BlzGetLocalSpecialEffectY(projectile.returning)+y,BlzGetLocalSpecialEffectZ(projectile.returning))
end
end
end
}
end
function FlameStrikeInit()
abiltrait_flamestrike_mighty = Trait.new({
name = "Mighty",
description = "|cff00ff00+20%% Damage (Ability)|r",
icon = "AbilityFlameStrike.dds",
action = function(trait,playerUnit,abil)
abil.damageMult = abil.damageMult + 0.2
end,
level_max = 10,
x2 = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_true_true_false,
})
abiltrait_flamestrike_fury = Trait.new({
name = "Fury",
description = "|cff00ff00+20%% Attack Speed (Ability)|r",
icon = "AbilityFlameStrike.dds",
action = function(trait,playerUnit,abil)
abil.attackSpeedMult = abil.attackSpeedMult + 0.2
end,
x2 = 9,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_false_true_true,
})
abiltrait_flamestrike_deadly = Trait.new({
name = "Deadly",
description = "|cff00ff00+50%% Crit Damage (Ability)|r",
icon = "AbilityFlameStrike.dds",
action = function(trait,playerUnit,abil)
abil.critDamageBonus = abil.critDamageBonus + 0.5
end,
level_max = 10,
x2 = 9,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_true_false,
})
abiltrait_flamestrike_heatedup = Trait.new({
name = "Heated Up",
description = "|cff00ff00+30%% Burn Chance (Ability)|r",
icon = "AbilityFlameStrike.dds",
action = function(trait,playerUnit,abil)
abil.effectChance = abil.effectChance + 0.3
end,
level_max = 10,
x2 = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_false_true,
})
abiltrait_flamestrike_flow = Trait.new({
name = "Flow",
description = "|cff00ff00-10%% Charge Duration (Ability)|r",
icon = "AbilityFlameStrike.dds",
action = function(trait,playerUnit,abil)
abil.stack_rate = abil.stack_rate + 0.05
end,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_three_six_ten,
})
abiltrait_flamestrike_overpowered = Trait.new({
name = "Overpowered",
description = "|cff00ff00+20%% Charge Multiplier (Ability)|r",
icon = "AbilityFlameStrike.dds",
action = function(trait,playerUnit,abil)
abil.stack_mult = abil.stack_mult + 0.2
end,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_three_six_ten,
})
ability_flamestrike = Ability.new({
name = "Flame Strike",
description = "Strong single target melee attack that hits the closest enemy. May inflict burn on hit. Damage and crit chance increase each second the attack did not trigger. Not affected by Force.",
icon = "AbilityFlameStrike.dds",
action = function(abil,playerUnit)
local attack = Attack.new(flame_strike_attack,playerUnit,ability_flamestrike)
abiltrait_flamestrike_mighty:add_to_pool(attack)
abiltrait_flamestrike_fury:add_to_pool(attack)
abiltrait_flamestrike_deadly:add_to_pool(attack)
abiltrait_flamestrike_heatedup:add_to_pool(attack)
abiltrait_flamestrike_flow:add_to_pool(attack)
abiltrait_flamestrike_overpowered:add_to_pool(attack)
end,
})
abilityupgrade_blazingemberfall = Ability.new({
upgradeAbility = ability_flamestrike,
name = "Blazing Emberfall",
description = "Flame wave that sets all nearby enemies on fire. Affected by range, area, and burn chance. Deals no direct damage. Range and burn chance increase for every second the attack was not triggered.",
action = function(abil,playerUnit,attack)
attack.blazingEmberfall = true
end,
})
abilityupgrade_scatteredsparks = Ability.new({
upgradeAbility = ability_flamestrike,
name = "Scattered Sparks",
description = "When the attack hits a burning enemy, it triggers an explosion. Explosion damage scales with the number of burn stacks and damage dealt. Deals splash damage and pushes enemies away.",
action = function(abil,playerUnit,attack)
attack.scatteredSparks = true
end,
})
abilityupgrade_froststrike = Ability.new({
upgradeAbility = ability_flamestrike,
name = "Frost Strike",
description = "Transforms the Flame Strike into Frost Strike. The Ability and Upgrades become Ice and work now with Frost.",
action = function(abil,playerUnit,attack)
attack.effect_type = EFFECT_TYPES.Frost
attack.frostStrike = true
attack.damageType = DamageTypes.Ice
attack.model = "EphemeralSlashBlue.mdx"
attack.model3 = "Abilities\\Weapons\\FrostWyrmMissile\\FrostWyrmMissile.mdl"
attack.effect_function = attack_effect_frost
end,
})
local closest_unit = 0
local closest_dist = 0
local function closest_iter(u,x,y)
if Types.is(u,Types.Damageable) then
local dist = DistanceSquaredTo(u.position[1],u.position[2],x,y)
if dist < closest_dist then
closest_dist = dist
closest_unit = u
end
end
end
local function hit_iter(u,attack,x,y,area,knockback_strength,reduc,closest)
if not Types.is(u,Types.Damageable) or u==closest then return end
local x1,y1 = u.position[1],u.position[2]
if IsInRange(x,y,x1,y1,area + u.half_size) then
if u.knockback ~= nil then
local dir_x,dir_y = VectorAngle(x1,y1,x,y)
u:knockback(dir_x,dir_y,knockback_strength)
end
attack:deal_damage(u,reduc,nil,nil,attack_effect_blank)
end
end
local function burn_iter(u,attack,x,y,area,effect_chance,closest)
if not Types.is(u,Types.Effectable) or not IsInRange(x,y,u.position[1],u.position[2],area + u.half_size) or u==closest then return end
local r = effect_chance - math.floor(effect_chance)
if GetRandomReal(0.0,1.0) < r then
effect_chance = math.ceil(effect_chance)
end
if effect_chance >= 1.0 then
attack.effect_function(u,attack,effect_chance)
end
end
local function strike_period(self)
local range = self:get_range() * 2
local x,y = self.owner.position[1], self.owner.position[2]
local half_range = range * 0.5
self.stacks = math.min(self.stacks + self.stack_rate,6.0)
closest_dist = math.huge
closest_unit = false
unit_collision_hash:each(x - half_range, y - half_range, range, range, closest_iter,x,y)
if closest_unit ~= false then
x,y = closest_unit.position[1], closest_unit.position[2]
local sfx = AddSpecialEffect(self.model,x-SFX_OFFSET,y-SFX_OFFSET)
BlzSetSpecialEffectZ(sfx,BlzGetLocalSpecialEffectZ(closest_unit.sfx))
local t = self.stacks/6
local alpha = math.floor(t * 255)
BlzSetSpecialEffectScale(sfx,closest_unit:get_scale()*0.5)
BlzSetSpecialEffectAlpha(sfx,alpha)
DestroyEffectEx(sfx)
if self.scatteredSparks and Types.is(closest_unit,Types.Effectable) then
local effectStacks = closest_unit:get_effect_stacks(self.effect_type)
if effectStacks == 0 then goto continue end
local t2 = effectStacks/20 + 0.5
local area = self:get_area() * math.min(t2 * 3,3)
local half = area * 0.5
local kb_strength = self:get_force() * 3 * t2
unit_collision_hash:each(x - half, y - half, area, area, hit_iter,self,x,y,half,kb_strength,t2*0.35,closest_unit)
sfx = AddSpecialEffect(self.model3,x-SFX_OFFSET,y-SFX_OFFSET)
BlzSetSpecialEffectYaw(sfx,GetRandomReal(0,TAU))
BlzSetSpecialEffectScale(sfx,area/256)
DestroyEffectEx(sfx)
end
::continue::
if self.blazingEmberfall then
t = t + 0.5
local area = self:get_area() * t * 2
local half = area * 0.5
local effect_chance = self:get_on_hit_chance() * t
unit_collision_hash:each(x - half, y - half, area, area, burn_iter,self,x,y,half,effect_chance,closest_unit)
sfx = AddSpecialEffect("NovaModel.mdx",x-SFX_OFFSET,y-SFX_OFFSET)
BlzSetSpecialEffectYaw(sfx,GetRandomReal(0,TAU))
BlzSetSpecialEffectTimeScale(sfx,2.5)
BlzSetSpecialEffectScale(sfx,area/128)
BlzSetSpecialEffectAlpha(sfx,alpha)
if self.frostStrike then
BlzSetSpecialEffectColor(sfx,175,175,255)
else
BlzSetSpecialEffectColor(sfx,255,100,50)
end
DestroyEffectEx(sfx)
end
self:deal_damage(closest_unit)
self.stacks = 0
end
self.timer:callDelayed(self:start_cooldown(), strike_period,self)
end
local function get_damage(self)
return Attack.get_damage(self) * (self.stacks * self.stack_mult)
end
local function get_crit_chance(self)
return Attack.get_crit_chance(self) + (self.stacks * self.stack_mult) * 0.08333
end
flame_strike_attack = {
name = "Flame Strike",
model = "EphemeralSlashOrange.mdx",
area = 256.0,
cooldown = 0.5,
damage = 200.0,
base_scale = 1.0,
range = 200.,
attack_count = 1.0,
effectChance = 1.0,
force = 3.0,
effect_function = attack_effect_burn,
damageType = DamageTypes.Fire,
properties = AttackProperties.Melee + AttackProperties.Elemental,
type = AttackTypes.Unique,
gain_action = function(self,data)
self.effect_type = EFFECT_TYPES.Burn
self.model3 = "Abilities\\Weapons\\RedDragonBreath\\RedDragonMissile.mdl"
self.stacks = 0
self.stack_rate = 0.5
self.stack_mult = 1.0
self.get_crit_chance = get_crit_chance
self.get_damage = get_damage
self.get_new_cooldown = Attack_get_new_cooldown_multi
self.timer:callDelayed(self:start_cooldown(), strike_period,self)
end,
destroy_func = function(self)
self.stacks = nil
self.effect_type = nil
self.get_crit_chance = nil
self.get_damage = nil
self.get_new_cooldown = nil
self.stack_rate = nil
self.stack_mult = nil
self.model3 = nil
self.scatteredSparks = nil
self.blazingEmberfall = nil
self.frostStrike = nil
end,
}
end
function SummonGolemInit()
abiltrait_summongolem_intertia = Trait.new({
name = "Intertia",
description = "|cff00ff00+20%% Roll Duration (Ability)|r",
icon = "AbilitySummonGolem.dds",
action = function(trait,playerUnit,abil)
abil.forceMult = abil.forceMult + 0.2
end,
level_max = 10,
x2 = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_true_true_false,
})
abiltrait_summongolem_acceleration = Trait.new({
name = "Acceleration",
description = "|cff00ff00+15%% Attack Speed (Ability)|n+10%% Movement Speed (Ability)|r",
icon = "AbilitySummonGolem.dds",
action = function(trait,playerUnit,abil)
abil.attackSpeedMult = abil.attackSpeedMult + 0.15
abil.moveSpeedMult = abil.moveSpeedMult + 0.1
for u in abil.summons:elements() do
u.moveSpeedMult = abil.moveSpeedMult
end
end,
x2 = 9,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_false_true_true,
})
abiltrait_summongolem_adjustment = Trait.new({
name = "Adjustment",
description = "|cff00ff00+40%% Crit Chance (Ability)|r",
icon = "AbilitySummonGolem.dds",
action = function(trait,playerUnit,abil)
abil.critChanceBonus = abil.critChanceBonus + 0.05
end,
level_max = 10,
x2 = 9,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_true_false,
})
abiltrait_summongolem_power = Trait.new({
name = "Power",
description = "|cff00ff00+20%% Damage (Ability)|n+5%% Crit Damage (Ability)|r",
icon = "AbilitySummonGolem.dds",
action = function(trait,playerUnit,abil)
abil.damageMult = abil.damageMult + 0.2
abil.critDamageBonus = abil.critDamageBonus + 0.05
end,
level_max = 10,
x2 = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_false_true,
})
abiltrait_summongolem_devastation = Trait.new({
name = "Devastation",
description = "|cff00ff00+15 Base Damage (Ability)|n+10%% Range (Ability)|n+10%% Area (Ability)|r",
icon = "AbilitySummonGolem.dds",
action = function(trait,playerUnit,abil)
abil.damage = abil.damage + 15
abil.rangeMult = abil.rangeMult + 0.1
abil.areaMult = abil.areaMult + 0.1
end,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_three_six_ten,
})
abiltrait_summongolem_inexorable = Trait.new({
name = "Inexorable",
description = "|cff00ff00-2s Base Roll Cooldown (Ability)|n+2s Base Roll Duration (Ability)|n+10%% Movement Speed (Ability)|r",
icon = "AbilitySummonGolem.dds",
action = function(trait,playerUnit,abil)
abil.force = abil.force + 2.0
abil.cooldown = abil.cooldown - 2.0
abil.moveSpeedMult = abil.moveSpeedMult + 0.1
for u in abil.summons:elements() do
u.moveSpeedMult = abil.moveSpeedMult
end
end,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_three_six_ten,
})
ability_summongolem = Ability.new({
name = "Summon Golem",
description = "Summons a clay golem that fights at your side. Starts rolling in a circular pattern and damages all enemies in its way. Rolling damage increases with rolling duration.",
icon = "AbilitySummonGolem.dds",
action = function(abil,playerUnit)
local attack = Attack.new(summon_golem_attack,playerUnit,ability_summongolem)
abiltrait_summongolem_intertia:add_to_pool(attack)
abiltrait_summongolem_acceleration:add_to_pool(attack)
abiltrait_summongolem_adjustment:add_to_pool(attack)
abiltrait_summongolem_power:add_to_pool(attack)
abiltrait_summongolem_devastation:add_to_pool(attack)
abiltrait_summongolem_inexorable:add_to_pool(attack)
end,
})
local function destroy_golem(self)
self.speedBuiltUp = nil
self.destroy = nil
self.ability.summons:remove(self)
Summon.destroy(self)
end
local function hit_iter(u,abil,hit_list)
if hit_list[u] == nil then
hit_list[u] = true
abil:deal_damage(u)
end
end
local function strike_period(self,golem,duration,lerpDur,angle,dist,sfx,hit_list)
if duration > GAME_TIME or unit_collision_hash:cell_occupied(golem.position[1] - golem.half_size, golem.position[2] - golem.half_size, golem.size, golem.size,Types.Collidable) then
lerpDur = math.min(lerpDur + DEFAULT_TIMEOUT,1.0)
local t = lerpDur/1.0
local range = lerp(dist,self:get_range(),t)
angle = angle + 0.09817
if angle > TAU then
ClearKeyTable(hit_list)
angle = angle - TAU
end
local x,y = self.owner.position[1] + range * math.cos(angle),self.owner.position[2] + range * math.sin(angle)
golem:move(x,y)
BlzSetSpecialEffectPosition(sfx,x-SFX_OFFSET,y-SFX_OFFSET,64)
unit_collision_hash:each(golem,hit_iter,self,hit_list)
self.timer:callDelayed(DEFAULT_TIMEOUT, strike_period,self,golem,duration,lerpDur,angle,dist,sfx,hit_list)
else
if self.magmaFists then
self:start_cooldown(4)
else
self:start_cooldown()
end
BlzSetSpecialEffectAlpha(golem.sfx,255)
DestroyEffect(sfx)
ReleaseKeyTable(hit_list)
golem.canMove = true
end
end
local function roll_attack(attack,count)
local self = attack.ability
local golem = attack.owner
if self.cooldown_cur < GAME_TIME then
golem.canMove = false
local x1,y1 = golem.position[1],golem.position[2]
local x2,y2 = self.owner.position[1],self.owner.position[2]
local angle = math.atan(y1-y2,x1-x2)
local dist = DistanceTo(x1,y1,x2,y2)
local hit_list = NewKeyTable()
local sfx = AddSpecialEffect("Abilities\\Weapons\\AncientProtectorMissile\\AncientProtectorMissile.mdl",x1,y1)
BlzSetSpecialEffectPosition(sfx,x1-SFX_OFFSET,y1-SFX_OFFSET,64)
BlzSetSpecialEffectScale(sfx,BlzGetSpecialEffectScale(golem.sfx)*2.5)
BlzSetSpecialEffectAlpha(golem.sfx,0)
if self.ferriteInfusion then
self.timer:callDelayed(DEFAULT_TIMEOUT, strike_period,self,golem,GAME_TIME + self:get_force() * 2,0,angle,dist,sfx,hit_list)
else
self.timer:callDelayed(DEFAULT_TIMEOUT, strike_period,self,golem,GAME_TIME + self:get_force(),0,angle,dist,sfx,hit_list)
end
sfx = AddSpecialEffect("Abilities\\Weapons\\AncientProtectorMissile\\AncientProtectorMissile.mdl",x1,y1)
BlzSetSpecialEffectPosition(sfx,x1,y1,64)
BlzSetSpecialEffectScale(sfx,BlzGetSpecialEffectScale(golem.sfx)*2.5)
DestroyEffect(sfx)
else
if self.magmaFists then
summon_cone_attack(attack,count,DamageTypes.Fire,2.0,attack_effect_burn)
else
summon_cone_attack(attack,count)
end
end
end
function electric_shockwave_iter(u,owner)
if Types.is(u,Types.Effectable) then
u:electrify(owner,1.0)
end
end
local function move_func(golem,speed)
golem.speedBuiltUp = golem.speedBuiltUp + speed
if golem.speedBuiltUp > 400.0 then
golem.speedBuiltUp = 0
local range = golem.ability:get_range() * 0.75
local half = range * 0.5
local x,y = golem.position[1],golem.position[2]
local sfx = AddSpecialEffect("ElectricSpark.mdx",x-SFX_OFFSET,y-SFX_OFFSET)
BlzSetSpecialEffectScale(sfx,range*0.0025)
BlzSetSpecialEffectAlpha(sfx,100)
DestroyEffect(sfx)
unit_collision_hash:each(x - half, y - half, range, range, electric_shockwave_iter,golem.owner)
end
return speed
end
local function create_golem(self)
local x,y = self.owner.position[1],self.owner.position[2]
local golem = Summon.new(summon_golem,x,y,self.owner,self)
self.summons:add(golem)
golem.moveSpeedMult = self.moveSpeedMult
golem.attack.in_range_function = roll_attack
golem.destroy = destroy_golem
if self.ferriteInfusion then
golem.speedBuiltUp = 0.0
golem.speed_move_func = move_func
end
end
abilityupgrade_magmafists = Ability.new({
upgradeAbility = ability_summongolem,
name = "Magma Fists",
description = "Golem's melee attacks deal fire damage and have a chance to inflict burn. +50%% Attack Area, +4s Roll Cooldown.",
icon = "AbilitySummonGolem.dds",
action = function(abil,playerUnit,attack)
attack.magmaFists = true
AttackProperties.add(attack,AttackProperties.Elemental)
end,
})
abilityupgrade_ferriteinfusion = Ability.new({
upgradeAbility = ability_summongolem,
name = "Ferrite Infusion",
description = "Golem sends out electrifying shockwaves when moving. x2.0 Roll Duration.",
icon = "AbilitySummonGolem.dds",
action = function(abil,playerUnit,attack)
attack.ferriteInfusion = true
AttackProperties.add(attack,AttackProperties.Elemental)
for u in attack.summons:elements() do
u.speedBuiltUp = 0.0
u.move_func = move_func
end
end,
})
abilityupgrade_doubletrouble = Ability.new({
upgradeAbility = ability_summongolem,
name = "Double Trouble",
description = "Creates another Clay Golem to assist you in battle.",
icon = "AbilitySummonGolem.dds",
action = function(abil,playerUnit,attack)
create_golem(attack)
end,
})
summon_golem_attack = {
name = "Summon Golem",
cooldown = 12.0,
damage = 200.0,
base_scale = 1.0,
range = 400.,
force = 4.0,
attack_count = 1.0,
critChanceBonus = 0.2,
cone_angle = PI*0.35,
effectChance = 0.5,
summon_count = 1.0,
properties = AttackProperties.Melee + AttackProperties.Summon,
type = AttackTypes.Unique,
gain_action = function(self,data)
self.summons = Set.create()
self.moveSpeedMult = 1.0
self:start_cooldown()
create_golem(self)
end,
destroy_func = function(self)
for u in self.summons:elements() do
u:destroy()
end
self.moveSpeedMult = nil
self.summons:destroy()
self.summons = nil
self.magmaFists = nil
self.ferriteInfusion = nil
end,
}
end
function TransfixionInit()
abiltrait_transfixion_spread = Trait.new({
name = "Spread",
description = "|cff00ff00+15%% Multistrike (Ability)|n+10%% Area (Ability)|r",
icon = "AbilityTransfixion.dds",
action = function(trait,playerUnit,abil)
abil.multistrikeMult = abil.multistrikeMult + 0.15
abil.areaMult = abil.areaMult + 0.1
end,
level_max = 10,
x2 = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_true_true_false,
})
abiltrait_transfixion_cadence = Trait.new({
name = "Cadence",
description = "|cff00ff00+15%% Attack Speed (Ability)|r",
icon = "AbilityTransfixion.dds",
action = function(trait,playerUnit,abil)
abil.attackSpeedMult = abil.attackSpeedMult + 0.15
end,
x2 = 9,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_false_true_true,
})
abiltrait_transfixion_effectivity = Trait.new({
name = "Effectivity",
description = "|cff00ff00+5%% Effect On Hit Chance (Ability)|r",
icon = "AbilityTransfixion.dds",
action = function(trait,playerUnit,abil)
abil.effectChance = abil.effectChance + 0.05
end,
level_max = 10,
x2 = 9,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_true_false,
})
abiltrait_transfixion_accuracy = Trait.new({
name = "Accuracy",
description = "|cff00ff00+10%% Crit Chance (Ability)|n+20%% Crit Damage (Ability)|r",
icon = "AbilityTransfixion.dds",
action = function(trait,playerUnit,abil)
abil.critDamageBonus = abil.critDamageBonus + 0.2
abil.critChanceBonus = abil.critChanceBonus + 0.1
end,
level_max = 10,
x2 = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_false_true,
})
abiltrait_transfixion_deadliness = Trait.new({
name = "Deadliness",
description = "|cff00ff00+10%% Base Attack Speed (Ability)|n+20%% Range (Ability)|r",
icon = "AbilityTransfixion.dds",
action = function(trait,playerUnit,abil)
abil.damage = abil.damage + 10
abil.rangeMult = abil.rangeMult + 0.2
end,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_three_six_ten,
})
abiltrait_transfixion_potency = Trait.new({
name = "Potency",
description = "|cff00ff00+33%% Base Crit Chance (Ability)|r",
icon = "AbilityTransfixion.dds",
action = function(trait,playerUnit,abil)
abil.critChanceBonus = abil.critChanceBonus + 0.33
end,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_three_six_ten,
})
ability_transfixion = Ability.new({
name = "Transfixion",
description = "Fires projectiles in the direction you're facing. Damage is reduced for every hit enemy. Chance to apply Fragile effect.",
icon = "AbilityTransfixion.dds",
action = function(abil,playerUnit)
local attack = Attack.new(transfixion_attack,playerUnit,ability_transfixion)
abiltrait_transfixion_spread:add_to_pool(attack)
abiltrait_transfixion_cadence:add_to_pool(attack)
abiltrait_transfixion_effectivity:add_to_pool(attack)
abiltrait_transfixion_accuracy:add_to_pool(attack)
abiltrait_transfixion_deadliness:add_to_pool(attack)
abiltrait_transfixion_potency:add_to_pool(attack)
end,
})
abilityupgrade_cripplingcuts = Ability.new({
upgradeAbility = ability_transfixion,
name = "Crippling Cuts",
description = "Fires projectiles behind you. All Transfixion projectiles get a chance to apply slow on hit enemies.",
action = function(abil,playerUnit,attack)
attack.cripplingCuts = true
end,
})
abilityupgrade_painfulincision = Ability.new({
upgradeAbility = ability_transfixion,
name = "Painful Incision",
description = " Fires projectiles behind you. All Transfixion projectiles get a chance to apply Affliction.",
action = function(abil,playerUnit,attack)
attack.painfulIncision = true
end,
})
abilityupgrade_amputation = Ability.new({
upgradeAbility = ability_transfixion,
name = "Amputation",
description = "Adds shots in backwards direction. Enemies hit by Transfixion take 10% additional damage for each debuff stack.",
action = function(abil,playerUnit,attack)
attack.amputation = true
end,
})
transfixionmissile = {}
transfixionmissile.__index = transfixionmissile
function transfixionmissile.new(scale,yaw,x1,y1,x2,y2,attack,area,half,dur,range)
local self = setmetatable(NewTable(),transfixionmissile)
SOUND_TRANSFIXION:random()(GetRandomReal(0.7,0.9),true,x1,y1)
self.sfx = AddSpecialEffect(attack.model,x1-SFX_OFFSET,y1-SFX_OFFSET)
BlzSetSpecialEffectZ(self.sfx,32)
BlzSetSpecialEffectYaw(self.sfx,yaw+HALF_PI)
BlzSetSpecialEffectScale(self.sfx,scale)
BlzSetSpecialEffectColor(self.sfx,255,0,0)
self.x1 = x1
self.y1 = y1
self.x2 = x2
self.y2 = y2
self.max_dur = dur
self.dur = 0.0
self.attack = attack
self.area = area
self.half = half
self.hits = 0
self.hit_list = NewKeyTable()
attack.projectile_set:add(self)
return self
end
function transfixionmissile:destroy()
self.attack.projectile_set:remove(self)
DestroyEffect(self.sfx)
self.x1 = nil
self.y1 = nil
self.x2 = nil
self.y2 = nil
self.max_dur = nil
self.dur = nil
self.attack = nil
setmetatable(self,nil)
self.area = nil
self.half = nil
self.angleDiff = nil
self.sfx = nil
self.hits = nil
ReleaseKeyTable(self.hit_list)
self.hit_list = nil
ReleaseTable(self)
end
local destroyMe = false
local function hit_iter(u,projectile,list,area,lx1,ly1,lx2,ly2,attack)
if u.position and list[u] == nil and point_in_range_of_line(lx1, ly1, lx2, ly2,u.position[1], u.position[2], area+u.half_size) then
if Types.is(u,Types.ProjectileStopper) then
destroyMe = true
return
end
list[u] = true
local reduc = 1.0
if attack.amputation and u.get_debuff_effect_stacks then
reduc = reduc + u:get_debuff_effect_stacks() * 0.1
end
local _, effect_proc, dmg = attack:deal_damage(u,(attack:get_force()^projectile.hits) * reduc)
if dmg <= 0 or reduc <= 0.1 then destroyMe = true end
projectile.hits = projectile.hits + 1
if effect_proc and effect_proc >= 1.0 then
if attack.painfulIncision then
u:afflict(attack.owner,effect_proc)
end
if attack.cripplingCuts then
u:slow(attack.owner,effect_proc)
end
end
end
end
local function move_projectile(self)
for projectile in self.projectile_set:elements() do
projectile.dur = math.min(projectile.dur + DEFAULT_TIMEOUT,projectile.max_dur)
local t = easeOutX(projectile.dur/projectile.max_dur,2)
local half = projectile.half
local x = lerp(projectile.x1,projectile.x2,t)
local y = lerp(projectile.y1,projectile.y2,t)
destroyMe = false
unit_collision_hash:each(x - half, y - half, projectile.area, projectile.area, hit_iter,projectile,projectile.hit_list,half,projectile.x1,projectile.y1,projectile.x2,projectile.y2,self)
BlzSetSpecialEffectPosition(projectile.sfx,x-SFX_OFFSET,y-SFX_OFFSET,32)
if projectile.dur == projectile.max_dur or destroyMe then
projectile:destroy()
end
end
self.timer:callDelayed(DEFAULT_TIMEOUT, move_projectile,self)
end
local function strike_period(self)
local range = self:get_range()
local x,y = self.owner.position[1], self.owner.position[2]
local area = self:get_area()
local half = area * 0.5
local count = self:increment_count(self)
local subcount = count-1
local dur = range/512
local yaw = self.owner.yaw
local scale = self:get_scale()
local mult = 1
local angleScale = 0.2792527 * scale
if self.cripplingCuts then mult = mult + 1 end
if self.painfulIncision then mult = mult + 1 end
if self.amputation then mult = mult + 1 end
local a = math.min(angleScale * count,TAU)
local angleBetweenProjectiles = a / (count - 1)
local startingAngle = yaw - (a / 2)
local j = 0
for i = 0, count - 1 do
a = startingAngle + (angleBetweenProjectiles * i)
local x2,y2 = x + math.cos(a) * range, y + math.sin(a) * range
self.timer:callDelayed(j,transfixionmissile.new,scale,a,x,y,x2,y2,self,area,half,dur,range)
j = j + 0.03
end
local t4 = subcount * 2
count = count*mult - count
a = math.min(angleScale * count,TAU)
angleBetweenProjectiles = a / (count - 1)
startingAngle = yaw - (a / 2) + PI
for i = 0, count - 1 do
a = startingAngle + (angleBetweenProjectiles * i)
local x2,y2 = x + math.cos(a) * range, y + math.sin(a) * range
self.timer:callDelayed(j,transfixionmissile.new,scale,a,x,y,x2,y2,self,area,half,dur,range)
j = j + 0.03
end
self.timer:callDelayed(self:start_cooldown(), strike_period,self)
end
transfixion_attack = {
name = "Transfixion",
model = "TransfixionModel9.mdx",
cooldown = 2.0,
area = 96.0,
damage = 70.0,
base_scale = 1.0,
range = 500.,
attack_count = 3.0,
critChanceBonus = 0.2,
force = 0.75,
properties = AttackProperties.Projectile,
type = AttackTypes.Unique,
effectChance = 0.25,
damageType = DamageTypes.Magical,
effect_function = attack_effect_fragile,
gain_action = function(self)
self.projectile_set = Set.create()
self.timer:callDelayed(self:start_cooldown(), strike_period,self)
self.timer:callDelayed(DEFAULT_TIMEOUT, move_projectile,self)
end,
destroy_func = function(self)
for projectile in self.projectile_set:elements() do
projectile:destroy()
end
self.projectile_set:destroy()
self.projectile_set = nil
self.cripplingCuts = nil
self.painfulIncision = nil
self.amputation = nil
self.hits = nil
end,
move = function(self,x,y)
for projectile in self.projectile_set:elements() do
projectile.x1 = projectile.x1 + x
projectile.y1 = projectile.y1 + y
projectile.x2 = projectile.x2 + x
projectile.y2 = projectile.y2 + y
end
end
}
end
function KugelblitzInit()
abiltrait_kugelblitz_capacity = Trait.new({
name = "Capacity",
description = "|cff00ff00+15%% Attack Speed (Ability)|r",
icon = "AbilityKugelblitz.dds",
action = function(trait,playerUnit,abil)
abil.attackSpeedMult = abil.attackSpeedMult + 0.15
end,
level_max = 10,
x2 = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_true_true_false,
})
abiltrait_kugelblitz_current = Trait.new({
name = "Current",
description = "|cff00ff00+10 Base Damage (Ability)|r",
icon = "AbilityKugelblitz.dds",
action = function(trait,playerUnit,abil)
abil.damage = abil.damage + 10
end,
x2 = 9,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_false_true_true,
})
abiltrait_kugelblitz_voltage = Trait.new({
name = "Voltage",
description = "|cff00ff00+4%% Electrify Chance (Ability)|r",
icon = "AbilityKugelblitz.dds",
action = function(trait,playerUnit,abil)
abil.effectChance = abil.effectChance + 0.04
end,
level_max = 10,
x2 = 9,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_true_false,
})
abiltrait_kugelblitz_discharge = Trait.new({
name = "Discharge",
description = "|cff00ff00+20%% Area (Ability)|r",
icon = "AbilityKugelblitz.dds",
action = function(trait,playerUnit,abil)
abil.areaMult = abil.areaMult + 0.2
end,
level_max = 10,
x2 = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_false_true,
})
abiltrait_kugelblitz_peak = Trait.new({
name = "Peak",
description = "|cff00ff00+25%% Crit Chance (Ability)|r",
icon = "AbilityKugelblitz.dds",
action = function(trait,playerUnit,abil)
abil.critChanceBonus = abil.critChanceBonus + 0.25
end,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_three_six_ten,
})
abiltrait_kugelblitz_resistance = Trait.new({
name = "Resistance",
description = "|cff00ff00+8 Base Damage (Ability)|r",
icon = "AbilityKugelblitz.dds",
action = function(trait,playerUnit,abil)
abil.damage = abil.damage + 8.0
abil.speedMult = abil.speedMult - 0.2
end,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_three_six_ten,
})
ability_kugelblitz = Ability.new({
name = "Kugelblitz",
icon = "AbilityKugelblitz.dds",
description = "Fires a slowly moving ball of electricity towards a random enemy. The ball emits an electric shockwave in regular intervals damaging enemies around it.",
action = function(abil,playerUnit)
local attack = Attack.new(kugelblitz_attack,playerUnit,ability_kugelblitz)
abiltrait_kugelblitz_capacity:add_to_pool(attack)
abiltrait_kugelblitz_current:add_to_pool(attack)
abiltrait_kugelblitz_voltage:add_to_pool(attack)
abiltrait_kugelblitz_discharge:add_to_pool(attack)
abiltrait_kugelblitz_peak:add_to_pool(attack)
abiltrait_kugelblitz_resistance:add_to_pool(attack)
end,
})
abilityupgrade_staticattraction = Ability.new({
upgradeAbility = ability_kugelblitz,
name = "Static Attraction",
description = "Kugelblitz changes direction and is attracted by nearby enemies.",
action = function(abil,playerUnit,attack)
attack.staticAttraction = true
end,
})
abilityupgrade_highvoltage = Ability.new({
upgradeAbility = ability_kugelblitz,
name = "High Voltage",
description = "Kugelblitz additionally casts chain lightning while moving past enemies.",
action = function(abil,playerUnit,attack)
attack.highVoltage = true
end,
})
abilityupgrade_finaldischarge = Ability.new({
upgradeAbility = ability_kugelblitz,
name = "Final Discharge",
description = "Maximum Range of Kugelblitz gets reduced. When the Kugelblitz is about to disappear, it explodes into a Shockwave that electrifies all enemies.",
action = function(abil,playerUnit,attack)
attack.finalDischarge = true
attack.force = attack.force - 1
end,
})
kugelblitz = {}
kugelblitz.__index = kugelblitz
function kugelblitz.new(scale,x,y,dir_x,dir_y,speed,dur,attack,area,half,as)
local self = setmetatable(NewTable(),kugelblitz)
self.sfx = AddSpecialEffect(attack.model,x-SFX_OFFSET,y-SFX_OFFSET)
BlzSetSpecialEffectScale(self.sfx,scale)
BlzSetSpecialEffectZ(self.sfx,32.0)
BlzSetSpecialEffectYaw(self.sfx,GetRandomReal(-PI,PI))
self.x = x
self.y = y
self.dir_x = dir_x
self.dir_y = dir_y
self.target_x = dir_x
self.target_y = dir_y
self.speed = speed
self.max_dur = dur
self.dur = 0.0
self.attack = attack
self.area = area
self.half = half
self.attackspeed = as
self.lastAttack = GAME_TIME + GetRandomReal(0,1)
self.hit_list = NewKeyTable()
attack.projectile_set:add(self)
return self
end
function kugelblitz.destroy(self)
self.attack.projectile_set:remove(self)
DestroyEffect(self.sfx)
self.x = nil
self.y = nil
self.target_x = nil
self.target_y = nil
self.dir_x = nil
self.dir_y = nil
self.attackspeed = nil
self.lastAttack = nil
self.speed = nil
self.max_dur = nil
self.dur = nil
self.attack = nil
self.area = nil
self.half = nil
self.sfx = nil
self.target = nil
ReleaseKeyTable(self.hit_list)
self.hit_list = nil
setmetatable(self,nil)
ReleaseTable(self)
end
local destroyMe = false
local function hit_iter(u,list,attack,reduc)
if (list[u] == nil or list[u] < GAME_TIME or reduc < 1) then
local _,_,dmg = attack:deal_damage(u,reduc)
if reduc == 1 then
list[u] = GAME_TIME + 1
if dmg <= 0 then destroyMe = true end
end
end
end
local function resetanim(proj)
if getmetatable(proj) == kugelblitz then
BlzPlaySpecialEffect(proj.sfx,ANIM_TYPE_STAND)
end
end
local function get_closest(results,x,y)
local closest
local closest_dist = math.huge
local ind
for i, u in ipairs(results) do
local d = DistanceSquaredTo(x,y,u.position[1],u.position[2])
if d < closest_dist then
closest = u
closest_dist = d
ind = i
end
end
return closest, ind
end
function chain_lightning(self,updatingVec,count,hit_list,damageMult)
local big_area = self:get_range() * 2
local half_big = big_area * 0.5
local x,y = updatingVec.x,updatingVec.y
local results = unit_collision_hash:get_of_type(x - half_big, y - half_big, big_area, big_area,Types.Damageable)
local target
while #results>0 and not target do
local test,i = results[1], 1
results[i] = results[#results]
results[#results] = nil
if hit_list[test] == nil then
target = test
hit_list[target] = true
end
end
if target then
count = count - 1
target.marked = target.marked + 0.2
if count > 0 then
self.timer:callDelayed(GetRandomReal(0.2,0.6), chain_lightning,self,GetUpdatingVector(target.position[1],target.position[2]),count,hit_list,damageMult)
else
local sfx = AddSpecialEffect("BlueBloodLustTarget2.mdx",target.position[1],target.position[2])
BlzSetSpecialEffectZ(sfx,32)
DestroyEffectEx(sfx)
end
local l = AddLightningEx('CLSB',true,x,y,32,target.position[1],target.position[2],32)
local sfx = AddSpecialEffect("BlueBloodLustTarget2.mdx",x,y)
BlzSetSpecialEffectZ(sfx,32)
DestroyEffectEx(sfx)
SOUND_ZAP:random()(GetRandomReal(0.9,1.1),true,x,y)
DEFAULT_TIMER:callDelayed(0.2,DestroyLightning,l)
self:deal_damage(target,damageMult)
else
count = 0
end
if count == 0 then
ReleaseKeyTable(hit_list)
end
ReleaseTable(results)
ReleaseUpdatingVector(updatingVec)
end
local function move_projectiles(self)
local staticAttraction = self.staticAttraction
local highVoltage = self.highVoltage
for proj in self.projectile_set:elements() do
local x,y = proj.x, proj.y
if staticAttraction then
if proj.target == nil or proj.target.type == nil then
local big_area = self:get_range() * 2
local half_big = big_area * 0.5
local results = unit_collision_hash:get_of_type(x - half_big, y - half_big, big_area, big_area,Types.Damageable)
proj.target = get_closest(results,x,y)
ReleaseTable(results)
end
if proj.target then
proj.target_x, proj.target_y = VectorAngle(proj.target.position[1],proj.target.position[2],x,y)
end
proj.dir_x = lerp(proj.dir_x, proj.target_x,0.05)
proj.dir_y = lerp(proj.dir_y, proj.target_y,0.05)
end
proj.dur = proj.dur + DEFAULT_TIMEOUT
x = x + proj.dir_x * proj.speed
y = y + proj.dir_y * proj.speed
local area = proj.area
local reduc = 1.0
local inc = 0
proj.x = x
proj.y = y
if proj.lastAttack < GAME_TIME or proj.dur >= proj.max_dur then
proj.lastAttack = GAME_TIME + proj.attackspeed
area = area * 3
if proj.dur >= proj.max_dur and self.finalDischarge then
BlzSetSpecialEffectScale(proj.sfx,BlzGetSpecialEffectScale(proj.sfx)*2)
inc = 0.5
area = area * 2
end
BlzPlaySpecialEffect(proj.sfx,ANIM_TYPE_DEATH)
self.timer:callDelayed(0.25,resetanim,proj)
reduc = 0.25
if highVoltage then
chain_lightning(self,GetUpdatingVector(x,y),math.ceil(proj.max_dur-proj.dur),NewKeyTable(),0.5)
end
end
local half = area * 0.5
self.effectChance = self.effectChance + inc
BlzSetSpecialEffectPosition(proj.sfx,x-SFX_OFFSET,y-SFX_OFFSET,32)
destroyMe = false
unit_collision_hash:each(x - half, y - half, area, area, hit_iter,proj.hit_list,self,reduc)
self.effectChance = self.effectChance - inc
if proj.dur >= proj.max_dur or destroyMe or unit_collision_hash:cell_occupied(x, y,0,0,Types.ProjectileStopper) then
proj:destroy()
end
end
self.timer:callDelayed(DEFAULT_TIMEOUT, move_projectiles,self)
end
local function strike_period(self)
local big_area = self:get_range() * 2
local half_big = big_area * 0.5
local scale = self:get_scale()
local x,y = self.owner.position[1], self.owner.position[2]
local results = unit_collision_hash:get_of_type(x - half_big, y - half_big, big_area, big_area,Types.Damageable)
local speed = self:get_speed()
local dur = self:get_force()
local attackspeed = self:start_cooldown()
local as = attackspeed / 5
local area = self:get_area()
local half = area * 0.5
local count = self:increment_count(self)
for i = 1, count do
if #results > 0 then
local ind = GetRandomInt(1,#results)
local target = results[ind]
results[ind] = results[#results]
results[#results] = nil
local dir_x,dir_y = VectorAngle(target.position[1],target.position[2],x,y)
local k = kugelblitz.new(scale,x,y,dir_x,dir_y,speed,dur,self,area,half,as)
k.target = target
end
end
ReleaseTable(results)
self.timer:callDelayed(attackspeed, strike_period,self)
end
kugelblitz_attack = {
name = "Kugelblitz",
model = "SuperLightningBall.mdx",
cooldown = 5.0,
area = 100.0,
damage = 80.0,
range = 425.,
base_scale = 0.6,
speed = 5.0,
force = 4,
attack_count = 1.0,
critDamageBonus = 0.3,
effectChance = 0.5,
damageType = DamageTypes.Lightning,
properties = AttackProperties.Projectile + AttackProperties.Area + AttackProperties.Elemental,
effect_function = attack_effect_electrify,
type = AttackTypes.Unique,
gain_action = function(self,data)
self.projectile_set = Set.create()
self.timer:callDelayed(self:start_cooldown(), strike_period,self)
self.timer:callDelayed(DEFAULT_TIMEOUT, move_projectiles,self)
end,
destroy_func = function(self)
for projectile in self.projectile_set:elements() do
projectile:destroy()
end
self.projectile_set:destroy()
self.projectile_set = nil
self.staticAttraction = nil
self.highVoltage = nil
self.finalDischarge = nil
end,
move = function(self,x,y)
for projectile in self.projectile_set:elements() do
projectile.x = projectile.x + x
projectile.y = projectile.y + y
end
end,
}
end
function HailstormInit()
abiltrait_hailstorm_ice = Trait.new({
name = "Ice",
description = "|cff00ff00+30%% Damage (Ability)|r|n|cffff0000-5%% Area (Ability)|r",
icon = "AbilityHailstorm.dds",
action = function(trait,playerUnit,abil)
abil.damageMult = abil.damageMult + 0.3
abil.areaMult = abil.areaMult - 0.05
end,
level_max = 10,
x2 = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_true_true_false,
})
abiltrait_hailstorm_snow = Trait.new({
name = "Snow",
description = "|cff00ff00+10%% Area (Ability)|r|n|cffff0000-10%% Range (Ability)|r",
icon = "AbilityHailstorm.dds",
action = function(trait,playerUnit,abil)
abil.areaMult = abil.areaMult + 0.1
abil.rangeMult = abil.rangeMult - 0.1
end,
x2 = 9,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_false_true_true,
})
abiltrait_hailstorm_cold = Trait.new({
name = "Cold",
description = "|cff00ff00+40%% Frost Chance (Ability)|r|n|cffff0000-10%% Area (Ability)|r",
icon = "AbilityHailstorm.dds",
action = function(trait,playerUnit,abil)
abil.effectChance = abil.effectChance + 0.4
abil.areaMult = abil.areaMult - 0.1
end,
level_max = 10,
x2 = 9,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_true_false,
})
abiltrait_hailstorm_wind = Trait.new({
name = "Wind",
description = "|cff00ff00+15%% Projectile Speed (Ability)|n+10%% Force (Ability)|r|n|cffff0000-0.05 Movement Speed Factor (Ability)|r",
icon = "AbilityHailstorm.dds",
action = function(trait,playerUnit,abil)
abil.speed = abil.speed + 0.00225
abil.forceMult = abil.forceMult + 0.1
abil.moveSpeedFactor = abil.moveSpeedFactor - 0.05
end,
level_max = 10,
x2 = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_false_true,
})
abiltrait_hailstorm_eyes = Trait.new({
name = "Eyes",
description = "|cff00ff00+0.3 Base Object Count (Ability)|r|n|cffff0000+10%% Charge Time (Ability)|r",
icon = "AbilityHailstorm.dds",
action = function(trait,playerUnit,abil)
abil.forceMult = abil.forceMult - 0.1
abil.attack_count = abil.attack_count + 0.3
end,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_three_six_ten,
})
abiltrait_hailstorm_updraft = Trait.new({
name = "Updraft",
description = "|cff00ff00+10%% Frost Chance (Ability)|n+150 Base Damage (Ability)|r|n|cffff0000+10%% Drop Limit (Ability)|r",
icon = "AbilityHailstorm.dds",
action = function(trait,playerUnit,abil)
abil.effectChance = abil.effectChance + 0.1
abil.damage = abil.damage + 150
abil.dropLimit = abil.dropLimit - 0.1
end,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_three_six_ten,
})
ability_hailstorm = Ability.new({
name = "Hailstorm",
description = "Generates glacial satellites that spin around you and grow while you're moving. Once you stop moving they crash down damaging enemies in an area.",
icon = "AbilityHailstorm.dds",
action = function(abil,playerUnit)
local attack = Attack.new(hailstorm_attack,playerUnit,ability_hailstorm)
abiltrait_hailstorm_ice:add_to_pool(attack)
abiltrait_hailstorm_snow:add_to_pool(attack)
abiltrait_hailstorm_cold:add_to_pool(attack)
abiltrait_hailstorm_wind:add_to_pool(attack)
abiltrait_hailstorm_eyes:add_to_pool(attack)
abiltrait_hailstorm_updraft:add_to_pool(attack)
end,
})
abilityupgrade_hailstormspike = Ability.new({
upgradeAbility = ability_hailstorm,
name = "Hailstorm Spikes",
description = "Every few meters you move dangerous spikes will shoot out under the satellites and damage all enemies that pass through them. Spawns decrease until the satellites are dropped.",
action = function(abil,playerUnit,attack)
attack.hailstormSpikes = true
end,
})
abilityupgrade_hailstormvortex = Ability.new({
upgradeAbility = ability_hailstorm,
name = "Hailstorm Vortex",
description = "Every few meters you move the ability will generate a freezing vortex under each satellite pulling enemies in. Gets weaker until the satellites are dropped.",
action = function(abil,playerUnit,attack)
attack.hailstormVortex = true
attack.model2 = "IceTornado.mdx"
end,
})
abilityupgrade_frozenfire = Ability.new({
upgradeAbility = ability_hailstorm,
name = "Frozen Fire",
description = "Every time Hailstorm applies frost, it also applies burn. All Hailstorm damage will be of the type Fire.",
action = function(abil,playerUnit,attack)
attack.frozenFire = true
attack.damageType = DamageTypes.Fire
attack.effect_function = attack_effect_frost_burn
end,
})
hailstorm = {}
hailstorm.__index = hailstorm
function hailstorm.new(x,y,angle,attack)
local self = setmetatable(NewTable(),hailstorm)
self.sfx = AddSpecialEffect(attack.model,x-SFX_OFFSET,y-SFX_OFFSET)
BlzSetSpecialEffectPitch(self.sfx,HALF_PI)
BlzSetSpecialEffectScale(self.sfx,0)
self.x = x
self.y = y
self.z = 16
self.angle = angle
self.step = 0.0
self.a = 0.0
self.step_max = attack.dropLimit
self.attack = attack
self.spikeLast = 0
attack.projectile_set:add(self)
return self
end
function hailstorm:destroy()
self.attack.projectile_set:remove(self)
DestroyEffectEx(self.sfx)
DestroyEffect(self.ind)
self.angle = nil
self.ind = nil
self.step_max = nil
self.step = nil
self.attack = nil
setmetatable(self,nil)
self.sfx = nil
self.dropping = nil
self.spikeLast = nil
self.x = nil
self.y = nil
self.z = nil
self.a = nil
ReleaseTable(self)
end
icespike = {}
icespike.__index = icespike
function icespike.new(x,y,attack,scale)
local self = setmetatable(NewTable(),icespike)
self.sfx = NewTable()
local count = 3
local angles = TAU/count
for i = 1, count do
local a = angles * i
local sfx = AddSpecialEffect("IceShard.mdx",x-SFX_OFFSET + math.cos(a) * 16,y-SFX_OFFSET+ math.sin(a) * 16)
BlzSetSpecialEffectYaw(sfx,a)
BlzSetSpecialEffectPitch(sfx,HALF_PI*0.5)
BlzSetSpecialEffectScale(sfx,scale*0.35)
self.sfx[i] = sfx
end
self.x = x
self.y = y
self.attack = attack
self.time = attack:get_force() * 6 + GAME_TIME
self.hit_list = NewKeyTable()
attack.ice_set:add(self)
return self
end
function icespike:destroy()
self.attack.ice_set:remove(self)
for i = 1, #self.sfx do
DestroyEffectEx(self.sfx[i])
self.sfx[i] = nil
end
ReleaseTable(self.sfx)
self.attack = nil
setmetatable(self,nil)
self.sfx = nil
self.x = nil
self.y = nil
self.time = nil
ReleaseKeyTable(self.hit_list)
self.hit_list = nil
ReleaseTable(self)
end
local function hit_iter(u,attack,x,y,area,reduc,hit_list)
if Types.is(u,Types.Damageable) and IsInRange(x,y,u.position[1],u.position[2],area+u.half_size) then
if hit_list then
if hit_list[u] then
return
else
hit_list[u] = true
end
end
attack:deal_damage(u,reduc)
end
end
local function pull_iter(u,attack,x,y,area,s)
if Types.is(u,Types.Damageable) and IsInRange(x,y,u.position[1],u.position[2],area+u.half_size) then
if u.knockback then
local dir_x,dir_y = VectorAngle(x,y,u.position[1],u.position[2])
u:knockback(dir_x,dir_y,s)
end
end
end
local function ice(self)
local area = self:get_area() * 0.15
local half = area * 0.5
for projectile in self.ice_set:elements() do
local x,y = projectile.x,projectile.y
unit_collision_hash:each(x - half, y - half, area, area, hit_iter,self,x,y,half,0.1,projectile.hit_list)
if projectile.time < GAME_TIME then
projectile:destroy()
end
end
self.timer:callDelayed(0.1, ice,self)
end
local function move_projectiles(self)
local owner = self.owner
local stepInc = (self:get_force() + (owner.moving and self.moveSpeedFactor * owner.speed * owner.moveSpeedMult or 0.0)) * 0.01
local range = self:get_range()
local speed = self:get_speed()
local hailstormVortex = self.hailstormVortex
local hailstormSpikes = self.hailstormSpikes
self.angle = self.angle + speed
for projectile in self.projectile_set:elements() do
local x,y
if projectile.dropping then
projectile.a = projectile.a + 9.8
x,y = projectile.x,projectile.y
projectile.z = math.max(projectile.z - projectile.a,0)
if projectile.z == 0 then
local t = projectile.step / projectile.step_max
local area = self:get_area() * t
local half = area * 0.5
unit_collision_hash:each(x - half, y - half, area, area, hit_iter,self,x,y,half,t)
self.orbitting = nil
projectile:destroy()
goto continue
end
else
projectile.step = math.min(projectile.step + stepInc,projectile.step_max)
local t = projectile.step / projectile.step_max
local r = range * t
local a = projectile.angle + self.angle
x,y = owner.position[1] + r * math.cos(a), owner.position[2] + r * math.sin(a)
projectile.x = x
projectile.y = y
projectile.z = projectile.step * 256 + 16
if projectile.step > 0.75 and not projectile.ind then
projectile.ind = AddSpecialEffect(self.model2,x,y)
if hailstormVortex then
BlzPlaySpecialEffect(projectile.ind,ANIM_TYPE_STAND)
end
end
local scale = self:get_scale()
if projectile.ind then
if hailstormVortex then
t = 1.5 - t
local area = self:get_area() * t
local half = area * 0.5
unit_collision_hash:each(x - half, y - half, area, area, pull_iter,self,x,y,half,t*0.5)
end
if hailstormSpikes and projectile.spikeLast < GAME_TIME then
projectile.spikeLast = GAME_TIME + 0.45
icespike.new(x+GetRandomReal(-32,32),y+GetRandomReal(-32,32),self,scale)
end
end
BlzSetSpecialEffectScale(projectile.sfx,projectile.step*scale)
if hailstormVortex then
BlzSetSpecialEffectScale(projectile.ind,projectile.step_max*0.25 * t * scale)
else
BlzSetSpecialEffectScale(projectile.ind,projectile.step*scale)
end
if projectile.ind and not owner.moving then
projectile.dropping = true
end
end
x = x - SFX_OFFSET
y = y - SFX_OFFSET
BlzSetSpecialEffectPosition(projectile.ind,x,y,0)
BlzSetSpecialEffectPosition(projectile.sfx,x,y,projectile.z)
::continue::
end
self.timer:callDelayed(DEFAULT_TIMEOUT, move_projectiles,self)
end
local function strike_period(self)
if not self.orbitting then
self.orbitting = true
local x,y = self.owner.position[1], self.owner.position[2]
local count = self:increment_count(self)
local aMult = TAU/count
for i = 1, count do
hailstorm.new(x,y,aMult * i,self)
end
end
self.timer:callDelayed(self:start_cooldown(), strike_period,self)
end
hailstorm_attack = {
name = "Hailstorm",
model = "FrostBoltV4.mdx",
cooldown = 1.0,
area = 512.0,
damage = 500.0,
base_scale = 1.0,
range = 525.,
attack_count = 2.0,
critChanceBonus = 0.1,
critDamageBonus = 0.5,
speed = 0.015,
force = 0.5,
effectChance = 1.0,
damageType = DamageTypes.Ice,
type = AttackTypes.Unique,
effect_function = attack_effect_frost,
properties = AttackProperties.Elemental,
gain_action = function(self,data)
self.moveSpeedFactor = 0.3
self.dropLimit = 2.0
self.angle = 0.0
self.model2 = "HailstormIndicator2.mdl"
self.projectile_set = Set.create()
self.ice_set = Set.create()
self.timer:callDelayed(self:start_cooldown(), strike_period,self)
self.timer:callDelayed(DEFAULT_TIMEOUT, move_projectiles,self)
self.timer:callDelayed(0.1, ice,self)
end,
destroy_func = function(self)
for projectile in self.projectile_set:elements() do
projectile:destroy()
end
for projectile in self.ice_set:elements() do
projectile:destroy()
end
self.ice_set:destroy()
self.ice_set = nil
self.projectile_set:destroy()
self.projectile_set = nil
self.frozenFire = nil
self.angle = nil
self.hailstormVortex = nil
self.hailstormSpikes = nil
self.moveSpeedFactor = nil
self.orbitting = nil
self.dropLimit = nil
self.model2 = nil
end,
move = function(self,x,y)
for projectile in self.projectile_set:elements() do
projectile.x = projectile.x + x
projectile.y = projectile.y + y
end
for projectile in self.ice_set:elements() do
projectile.x = projectile.x + x
projectile.y = projectile.y + y
for i,sfx in ipairs(projectile.sfx) do
BlzSetSpecialEffectPosition(sfx, BlzGetLocalSpecialEffectX(sfx)+x,BlzGetLocalSpecialEffectY(sfx)+y,0)
end
end
end
}
end
function MorningStarInit()
abiltrait_morningstar_weight = Trait.new({
name = "Weight",
description = "|cff00ff00+0.1 Base Force (Ability)|r",
icon = "AbilityMorningstar.dds",
action = function(trait,playerUnit,abil)
abil.force = abil.force + 0.1
end,
level_max = 10,
x2 = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_true_true_false,
})
abiltrait_morningstar_spikes = Trait.new({
name = "Spikes",
description = "|cff00ff00+12 Base Damage (Ability)|r",
icon = "AbilityMorningstar.dds",
action = function(trait,playerUnit,abil)
abil.damage = abil.damage + 12
end,
x2 = 9,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_false_true_true,
})
abiltrait_morningstar_mutlistar = Trait.new({
name = "Multistar",
description = "|cff00ff00+0.1 Base Multistrike (Ability)|r",
icon = "AbilityMorningstar.dds",
action = function(trait,playerUnit,abil)
abil.attack_count = abil.attack_count + 0.1
end,
level_max = 10,
x2 = 9,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_true_false,
})
abiltrait_morningstar_handle = Trait.new({
name = "Handle",
description = "|cff00ff00+10%% Base Attack Speed (Ability)|r",
icon = "AbilityMorningstar.dds",
action = function(trait,playerUnit,abil)
abil.cooldown = abil.cooldown - 0.4
end,
level_max = 10,
x2 = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_false_true,
})
abiltrait_morningstar_chain = Trait.new({
name = "Chain",
description = "|cff00ff00+6%% Base Range(Ability)|r",
icon = "AbilityMorningstar.dds",
action = function(trait,playerUnit,abil)
abil.range = abil.range + 12
end,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_three_six_ten,
})
abiltrait_morningstar_enchantment = Trait.new({
name = "Enchantment",
description = "|cff00ff00+10%% Damage, Force, Attack Speed, Multistrike (Ability)|r",
icon = "AbilityMorningstar.dds",
action = function(trait,playerUnit,abil)
abil.damageMult = abil.damageMult + 0.1
abil.forceMult = abil.forceMult + 0.1
abil.attackSpeedMult = abil.attackSpeedMult + 0.1
abil.multistrikeMult = abil.multistrikeMult + 0.1
end,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_three_six_ten,
})
ability_morningstar = Ability.new({
name = "Morning Star",
description = "Swings a heavy iron ball on a chain in a figure eight pattern. Force multiplies damage.",
icon = "AbilityMorningstar.dds",
action = function(abil,playerUnit)
local attack = Attack.new(morningstar_attack,playerUnit,ability_morningstar)
abiltrait_morningstar_weight:add_to_pool(attack)
abiltrait_morningstar_spikes:add_to_pool(attack)
abiltrait_morningstar_mutlistar:add_to_pool(attack)
abiltrait_morningstar_handle:add_to_pool(attack)
abiltrait_morningstar_chain:add_to_pool(attack)
abiltrait_morningstar_enchantment:add_to_pool(attack)
end,
})
abilityupgrade_spikedchain = Ability.new({
upgradeAbility = ability_morningstar,
name = "Spiked Chain",
description = "The chains of your Morning Stars also inflict damage and have a chance to apply slow on hit enemies.",
action = function(abil,playerUnit,attack)
attack.spikedChain = true
attack.effectChance = 0.5
attack.effect_function = attack_effect_slow
attack.r = 1
attack.g = 0.5
attack.b = 0.5
end,
})
abilityupgrade_unleashedstars = Ability.new({
upgradeAbility = ability_morningstar,
name = "Unleashed Stars",
description = "Each hit with the Morning Star has a 60%% chance to emit an unleashed Morning Star that bounces between enemies and deals damage.",
action = function(abil,playerUnit,attack)
attack.unleashedStars = true
end,
})
abilityupgrade_butterflyswing = Ability.new({
upgradeAbility = ability_morningstar,
name = "Butterfly Swing",
description = "Swing two Morning Stars in a butterfly pattern.",
action = function(abil,playerUnit,attack)
attack.butterflySwing = true
end,
})
local function get_damage(self,target,dmgTypeOverride)
return Attack.get_damage(self,target,dmgTypeOverride) * self:get_force()
end
morningstar = {}
morningstar.__index = morningstar
function morningstar.new(x,y,attack,offset)
local self = setmetatable(NewTable(),morningstar)
self.sfx = AddSpecialEffect(attack.model,x-SFX_OFFSET,y-SFX_OFFSET)
self.light = AddLightning('CHAN',false,x,y,x,y)
self.scale = attack:get_scale()
BlzSetSpecialEffectScale(self.sfx,self.scale)
SetLightningColor(self.light,attack.r,attack.g,attack.b,1)
self.area = attack:get_area()
self.half = self.area * 0.5
self.dur = 0.0
self.dur_max = attack:get_force() * 4
self.attack = attack
self.hit_list = NewKeyTable()
self.offset = offset
self.x = x
self.y = y
attack.projectile_set:add(self)
return self
end
function morningstar:destroy()
self.attack.projectile_set:remove(self)
DestroyEffect(self.sfx)
DestroyLightning(self.light)
self.light = nil
self.dur_max = nil
self.scale = nil
self.area = nil
self.half = nil
self.dur = nil
self.offset = nil
self.attack = nil
setmetatable(self,nil)
self.sfx = nil
self.x = nil
self.y = nil
ReleaseKeyTable(self.hit_list)
self.hit_list = nil
ReleaseTable(self)
end
bouncingstar = {}
bouncingstar.__index = bouncingstar
function bouncingstar.new(x,y,attack,vel_x,vel_y)
local self = setmetatable(NewTable(),bouncingstar)
self.sfx = AddSpecialEffect("MorningStar2.mdx",x-SFX_OFFSET,y-SFX_OFFSET)
self.scale = attack:get_scale() * 0.5
BlzSetSpecialEffectScale(self.sfx,self.scale)
BlzSetSpecialEffectYaw(self.sfx,math.atan(vel_y,vel_x))
BlzPlaySpecialEffect(self.sfx,ANIM_TYPE_ATTACK)
self.area = attack:get_area() * 0.5
self.half = self.area * 0.5
self.vel_x = vel_x
self.vel_y = vel_y
self.attack = attack
self.x = x
self.y = y
self.hit_list = NewKeyTable()
attack.bouncing_set:add(self)
return self
end
function bouncingstar:destroy()
self.attack.bouncing_set:remove(self)
BlzSetSpecialEffectTimeScale(self.sfx,1.0)
DestroyEffect(self.sfx)
self.scale = nil
self.area = nil
self.half = nil
self.attack = nil
setmetatable(self,nil)
self.sfx = nil
self.x = nil
self.y = nil
self.vel_x = nil
self.vel_y = nil
ReleaseKeyTable(self.hit_list)
self.hit_list = nil
ReleaseTable(self)
end
local was_hit = false
local function hit_iter(u,attack,x,y,area,list,x1,y1)
if (list[u] == nil or list[u] < GAME_TIME) and Types.is(u,Types.Damageable) then
local x2,y2 = u.position[1],u.position[2]
local half = u.half_size
if IsInRange(x,y,x2,y2,area+half) then
list[u] = GAME_TIME + 1.0
attack:deal_damage(u)
was_hit = true
elseif (x1 and point_in_range_of_line(x, y, x1, y1, x2, y2, half)) then
list[u] = GAME_TIME + 0.25
attack:deal_damage(u,0.25)
end
end
end
function get_midpoint(x1,y1,x2,y2)
return (x1+x2)/2, (y1+y2)/2
end
local function rotateVector2Rad(vx,vy, angleRad)
local cos = math.cos(angleRad)
local sin = math.sin(angleRad)
return cos * vx - sin * vy, sin * vx + cos * vy
end
local function bounce_hit_iter(u,attack,b,list)
if (list[u] == nil or list[u] < GAME_TIME) and not was_hit and u.type ~= nil then
list[u] = GAME_TIME + 0.25
was_hit = true
local normalX, normalY = VectorAngleSquare(u.position[1], u.position[2], b.x, b.y)
local dot = b.vel_x * normalX + b.vel_y * normalY
b.vel_x = (b.vel_x - 2 * dot * normalX) * 0.9
b.vel_y = (b.vel_y - 2 * dot * normalY) * 0.9
BlzSetSpecialEffectYaw(b.sfx,math.atan(b.vel_y,b.vel_x))
attack:deal_damage(u,0.25)
end
end
local function move_projectiles(self)
local owner = self.owner
local range = self:get_range()
local speed = self:get_speed()
local x1,y1 = owner.position[1],owner.position[2]
local butterflySwing = self.butterflySwing
local spikedChain = self.spikedChain
local unleashedStars = self.unleashedStars
for projectile in self.projectile_set:elements() do
projectile.dur = projectile.dur + DEFAULT_TIMEOUT
local t = (projectile.dur / projectile.dur_max)
local x,y, a = lerp_figure8(t * speed + projectile.offset)
if butterflySwing then
local r = range * 2
x,y = x * r + x1 - r * 0.5, y * range * 0.75 + y1
else
x,y = x * range + x1 - range * 0.5, y * range + y1
end
if a > 13.5 or a < 0.1 then
SOUND_MACE_SWING(GetRandomReal(1.0,1.2),true,x,y)
end
local vel_x = x-projectile.x
local vel_y = y-projectile.y
projectile.x = x
projectile.y = y
local trueRange = DistanceTo(x,y,x1,y1)
local z = projectile.scale*16
BlzSetSpecialEffectPosition(projectile.sfx,x- SFX_OFFSET,y- SFX_OFFSET,z)
MoveLightningEx(projectile.light,false,x1,y1,32,x- SFX_OFFSET,y- SFX_OFFSET,z)
BlzSetSpecialEffectYaw(projectile.sfx,math.atan(y-y1,x-x1))
local area = projectile.area
local half = projectile.half
was_hit = false
if spikedChain then
local area_range = trueRange+ half
local half_range = area_range * 0.5
local x2,y2 = get_midpoint(x,y,x1,y1)
unit_collision_hash:each(x2 - half_range, y2 - half_range, area_range, area_range, hit_iter,self,x,y,half,projectile.hit_list,x1,y1)
else
unit_collision_hash:each(x - half, y - half, area, area, hit_iter,self,x,y,half,projectile.hit_list)
end
if unleashedStars and was_hit and GetRandomReal(0,1) <= 0.6 then
bouncingstar.new(x,y,self,vel_x,vel_y)
end
if projectile.dur >= projectile.dur_max then
projectile:destroy()
end
end
for b in self.bouncing_set:elements() do
was_hit = false
b.vel_x, b.vel_y = b.vel_x * 0.975, b.vel_y * 0.975
local x,y = b.x + b.vel_x, b.y + b.vel_y
local area = b.area
local half = b.half
b.x = x
b.y = y
local absX = math.abs(b.vel_x)
local absY = math.abs(b.vel_y)
local z = b.scale*16
BlzSetSpecialEffectTimeScale(b.sfx,(absX+absY) * 0.25)
BlzSetSpecialEffectPosition(b.sfx,x,y,z)
unit_collision_hash:each(x - half, y - half, area, area, bounce_hit_iter,self,b,b.hit_list)
if absX < 1 and absY < 1 then
b:destroy()
end
end
self.timer:callDelayed(DEFAULT_TIMEOUT, move_projectiles,self)
end
local function strike_period(self)
local x,y = self.owner.position[1], self.owner.position[2]
morningstar.new(x,y,self,0)
if self.butterflySwing then
morningstar.new(x,y,self,0.5)
end
self.timer:callDelayed(self:start_cooldown(), strike_period,self)
end
morningstar_attack = {
name = "Morning Star",
model = "MorningStar.mdx",
cooldown = 4,
area = 96.0,
damage = 120.0,
base_scale = 3.0,
range = 200.,
attack_count = 1.0,
critChanceBonus = 0.5,
critDamageBonus = 0.5,
speed = 1.0,
force = 1.0,
damageType = DamageTypes.Physical,
properties = AttackProperties.Projectile + AttackProperties.Melee,
type = AttackTypes.Unique,
gain_action = function(self,data)
self.r = 1
self.g = 1
self.b = 1
self.get_damage = get_damage
self.projectile_set = Set.create()
self.bouncing_set = Set.create()
self.timer:callDelayed(DEFAULT_TIMEOUT, move_projectiles,self)
self.get_new_cooldown = Attack_get_new_cooldown_multi
self.timer:callDelayed(self:start_cooldown(), strike_period,self)
end,
destroy_func = function(self)
self.get_new_cooldown = nil
for projectile in self.projectile_set:elements() do
projectile:destroy()
end
self.projectile_set:destroy()
self.bouncing_set:destroy()
self.bouncing_set = nil
self.projectile_set = nil
self.unleashedStars = nil
self.butterflySwing = nil
self.spikedChain = nil
self.get_damage = nil
self.r = nil
self.g = nil
self.b = nil
end,
move = function(self,x,y)
for projectile in self.bouncing_set:elements() do
projectile.x = projectile.x + x
projectile.y = projectile.y + y
end
end
}
end
function SpiritWarriorInit()
abiltrait_spiritwarrior_strength = Trait.new({
name = "Strength",
description = "|cff00ff00+20%% Damage (Ability|r",
icon = "AbilitySpiritWarrior.dds",
action = function(trait,playerUnit,abil)
abil.damageMult = abil.damageMult + 0.2
end,
level_max = 10,
x2 = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_true_true_false,
})
abiltrait_spiritwarrior_cunning = Trait.new({
name = "Cunning",
description = "|cff00ff00+10%% Crit Chance (Ability)|r",
icon = "AbilitySpiritWarrior.dds",
action = function(trait,playerUnit,abil)
abil.critChanceBonus = abil.critChanceBonus + 0.1
end,
x2 = 9,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_false_true_true,
})
abiltrait_spiritwarrior_ruthless = Trait.new({
name = "Ruthless",
description = "|cff00ff00+30%% Crit Damage (Ability)|r",
icon = "AbilitySpiritWarrior.dds",
action = function(trait,playerUnit,abil)
abil.critDamageBonus = abil.critDamageBonus + 0.3
end,
level_max = 10,
x2 = 9,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_true_false,
})
abiltrait_spiritwarrior_coverage = Trait.new({
name = "Coverage",
description = "|cff00ff00+20%% Area (Ability)|n+10%% Range (Ability)|r",
icon = "AbilitySpiritWarrior.dds",
action = function(trait,playerUnit,abil)
abil.areaMult = abil.areaMult + 0.2
abil.rangeMult = abil.rangeMult + 0.1
end,
level_max = 10,
x2 = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_false_true,
})
abiltrait_spiritwarrior_dashing = Trait.new({
name = "Dashing",
description = "|cff00ff00+6%% Base Range(Ability)|r",
icon = "AbilitySpiritWarrior.dds",
action = function(trait,playerUnit,abil)
abil.attackrange = abil.attackrange + 15
for summon in abil.summon_set:elements() do
summon.attack.range = abil.attackrange
end
end,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_three_six_ten,
})
abiltrait_spiritwarrior_swift = Trait.new({
name = "Swift",
description = "|cff00ff00+0.15 Base Attack Speed (Ability)|r",
icon = "AbilitySpiritWarrior.dds",
action = function(trait,playerUnit,abil)
abil.attackcd = abil.attackcd - 0.15
for summon in abil.summon_set:elements() do
summon.attack.cooldown = abil.attackcd
end
end,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_three_six_ten,
})
ability_spiritwarrior = Ability.new({
name = "Spirit Warrior",
description = "Summons a spirit warrior that remains stationary and fights nearby enemies with melee magical attacks. Performs a dash attack in your direction when you move further away.",
icon = "AbilitySpiritWarrior.dds",
action = function(abil,playerUnit)
local attack = Attack.new(spiritwarrior_attack,playerUnit,ability_spiritwarrior)
abiltrait_spiritwarrior_strength:add_to_pool(attack)
abiltrait_spiritwarrior_cunning:add_to_pool(attack)
abiltrait_spiritwarrior_ruthless:add_to_pool(attack)
abiltrait_spiritwarrior_coverage:add_to_pool(attack)
abiltrait_spiritwarrior_dashing:add_to_pool(attack)
abiltrait_spiritwarrior_swift:add_to_pool(attack)
end,
})
abilityupgrade_dashimpact = Ability.new({
upgradeAbility = ability_spiritwarrior,
name = "Dash Impact",
description = "At the end of his dash the spirit warrior casts a physical shockwave that damages and slows down enemies.",
action = function(abil,playerUnit,attack)
attack.dashImpact = true
end,
})
abilityupgrade_spiritorbs = Ability.new({
upgradeAbility = ability_spiritwarrior,
name = "Spirit Orbs",
description = "Spirit Warriors get Orbs that travel around them and damaging enemies they hit. They do not scale with Astronomer's Orb upgrades.",
action = function(abil,playerUnit,attack)
attack.spiritOrbs = true
attack:update_func()
end,
})
abilityupgrade_spiritneedles = Ability.new({
upgradeAbility = ability_spiritwarrior,
name = "Spirit Needles",
description = " Adds an emitter than shoots needle projectiles at random targets. Does not scale with Phantom Needle traits.",
action = function(abil,playerUnit,attack)
attack.spiritNeedles = true
end,
})
local function destroy_warrior(self)
DestroyLightning(self.light)
self.chargeDur = nil
self.chargeDurMax = nil
self.light = nil
self.speedBuiltUp = nil
self.targetX = nil
self.targetY = nil
self.sourceX = nil
self.sourceY = nil
self.destroy = nil
self.needleCD = nil
ReleaseKeyTable(self.hit_list)
self.hit_list = nil
self.ability.summon_set:remove(self)
for i, list in ipairs(self.hit_lists) do
ReleaseKeyTable(list)
end
ReleaseTable(self.hit_lists)
self.hit_lists = nil
if self.orbs then
self.ability:update_summoned(-#self.orbs)
for i = 1, #self.orbs, 1 do
DestroyEffect(self.orbs[i])
end
ReleaseTable(self.orbs)
self.orbs = nil
end
self.rotation = nil
Summon.destroy(self)
end
local function create_warrior(self)
local x,y = self.owner.position[1],self.owner.position[2]
local x2,y2 = x + GetRandomReal(-64,64), y + GetRandomReal(-64,64)
local summon = Summon.new(summon_spirit_warrior,x2,y2,self.owner,self)
BlzSetSpecialEffectColor(summon.sfx,255,75,50)
BlzSetSpecialEffectAlpha(summon.sfx,150)
summon.light = AddLightning('AROW',false,x2,y2,x,y)
SetLightningColor(summon.light,0,0,0,1)
self.summon_set:add(summon)
summon.attack.cooldown = self.attackcd
summon.attack.range = self.attackrange
summon.targetX = 0
summon.targetY = 0
summon.sourceX = 0
summon.sourceY = 0
summon.needleCD = 0
summon.hit_list = NewKeyTable()
summon.hit_lists = NewTable()
summon.destroy = destroy_warrior
return summon
end
local function update_warrior(self)
local attack = self.ability
if attack.spiritOrbs then
self.orbs = self.orbs or NewTable()
self.hit_lists = self.hit_lists or NewTable()
self.rotation = self.rotation or GetRandomReal(0,PI)
local count = MathRound(attack:get_summon_count())
while count ~= #self.orbs do
if count > #self.orbs then
local sfx = AddSpecialEffect(attack.model2,0,0)
BlzSetSpecialEffectColor(sfx,255,75,50)
BlzSetSpecialEffectAlpha(sfx,150)
table.insert(self.orbs,sfx)
table.insert(self.hit_lists,NewKeyTable())
self:update_summoned(1)
else
local pos = #self.orbs
DestroyEffect(self.orbs[pos])
ReleaseKeyTable(self.hit_lists[pos])
self.hit_lists[pos] = nil
self.orbs[pos] = nil
self:update_summoned(-1)
end
end
local scale = attack:get_scale()
for i = 1, #self.orbs, 1 do
BlzSetSpecialEffectPosition(self.orbs[i],0,0,-500)
BlzSetSpecialEffectScale(self.orbs[i],scale*0.35)
end
end
end
local function hit_iter(u,attack,list)
if (list[u] == nil or list[u] < GAME_TIME) and Types.is(u,Types.Damageable) then
list[u] = GAME_TIME + 1.0
attack:deal_damage(u,10)
end
end
local chargeAnimDur = 1.134
local durMult = 0.5
local function move_projectiles(self)
local spiritOrbs = self.spiritOrbs
local spiritNeedles = self.spiritNeedles
local range = self:get_range()
local speed = self:get_speed()
local area = self:get_area()
local half = area * 0.5
for spirit in self.summon_set:elements() do
local x,y = spirit.position[1], spirit.position[2]
local owner = self.owner
local x2,y2 = owner.position[1], owner.position[2]
if not spirit.chargeDur then
local dRatio = math.min(1,DistanceSquaredTo(x,y,x2,y2)/(range*range))
if dRatio == 1 then
spirit.canMove = false
spirit.canAnimate = false
spirit.attack:reset()
spirit.chargeDur = 0
spirit.chargeDurMax = chargeAnimDur * durMult
SOUND_CHARGE(GetRandomReal(0.7,0.9),true,x,y)
BlzSetSpecialEffectTimeScale(spirit.sfx,durMult)
spirit.sourceX = x
spirit.sourceY = y
spirit.targetX = x2 + GetRandomReal(-64,64)
spirit.targetY = y2 + GetRandomReal(-64,64)
spirit.yaw = math.atan(y2-y,x2-x)
BlzSetSpecialEffectYaw(spirit.sfx,spirit.yaw)
BlzPlaySpecialEffect(spirit.sfx,ANIM_TYPE_SPELL)
SetLightningColor(spirit.light,0,0,0,0)
else
SetLightningColor(spirit.light,1,.3,.25,dRatio)
MoveLightning(spirit.light,false,x2,y2,x,y)
end
else
spirit.chargeDur = math.min(spirit.chargeDur + DEFAULT_TIMEOUT,spirit.chargeDurMax)
local t = easeInX(spirit.chargeDur / spirit.chargeDurMax,1.5)
x = lerp(spirit.sourceX,spirit.targetX,t)
y = lerp(spirit.sourceY,spirit.targetY,t)
spirit:move(x,y)
if t == 1 then
spirit.canMove = true
spirit.canAnimate = true
if self.dashImpact then
unit_collision_hash:each(x-half,y-half,area,area,hit_iter,self,spirit.hit_list)
local sfx = AddSpecialEffect(self.model,x,y)
BlzSetSpecialEffectScale(sfx,self:get_scale())
DestroyEffect(sfx)
BlzSetSpecialEffectTimeScale(spirit.sfx,1.0)
end
spirit.chargeDur = nil
else
unit_collision_hash:each(spirit,hit_iter,self,spirit.hit_list)
end
end
if spiritOrbs then
spirit.rotation = spirit.rotation + speed
if spirit.rotation > PI then
spirit.rotation = spirit.rotation - TAU
for i = 1, #spirit.hit_lists, 1 do
ClearKeyTable(spirit.hit_lists[i])
end
end
local offset_angle = TAU / #spirit.orbs
local orb_area = area * 0.32
local orb_half = orb_area * 0.5
local orb_range = range * 0.35
for i = 1, #spirit.orbs, 1 do
local r = offset_angle * i + spirit.rotation
local x1,y1 = x + Cos(r) * orb_range,y + Sin(r) * orb_range
BlzSetSpecialEffectPosition(spirit.orbs[i],x1-SFX_OFFSET,y1-SFX_OFFSET,50)
unit_collision_hash:each(x1 - orb_half, y1 - orb_half, orb_area, orb_area, orb_iter,self,x1,y1,spirit.hit_lists[i],orb_half)
end
end
if spiritNeedles and spirit.needleCD < GAME_TIME then
local big_area = range * 2
local results = unit_collision_hash:get_of_type(x - range, y - range, big_area, big_area,Types.Damageable)
if #results > 0 then
spirit.needleCD = GAME_TIME + self:get_attack_speed()/self:get_attack_count(self)
local target = results[GetRandomInt(1,#results)]
local smol = area*0.16
Missile.new(self.model3,x,y,angleBetween(x,y,target.position[1],target.position[2]),Attack.get_speed(self)*3,self.owner,self,range + target.size + smol + smol,self:get_force() * 5, smol)
end
ReleaseTable(results)
end
end
self.timer:callDelayed(DEFAULT_TIMEOUT, move_projectiles,self)
end
spiritwarrior_attack = {
name = "Spirit Warrior",
model = "Abilities\\Spells\\Orc\\WarStomp\\WarStompCaster",
cooldown = 0.4,
area = 300.0,
damage = 40.0,
base_scale = 1.0,
range = 400.,
attack_count = 1.0,
summon_count = 2.0,
critChanceBonus = 0.1,
critDamageBonus = 0.6,
cone_angle = PI*0.375,
speed = 10.0,
force = 1.0,
damageType = DamageTypes.Physical,
properties = AttackProperties.Melee + AttackProperties.Summon,
type = AttackTypes.Unique,
gain_action = function(self,data)
self.attackcd = 1.1
self.model2 = "AstronomerOrb.mdx"
self.model3 = "NeedleOrange.MDX"
self.attackrange = 225
self.summon_set = Set.create()
self.timer:callDelayed(DEFAULT_TIMEOUT, move_projectiles,self)
self.get_speed = orb_get_speed
end,
update_func = function(self)
local count = MathRound(self:get_summon_count())
local size = self.summon_set:size()
for summon in self.summon_set:elements() do
if count > size then
size = size - 1
destroy_warrior(summon)
end
end
while size < count do
size = size + 1
create_warrior(self)
end
for summon in self.summon_set:elements() do
update_warrior(summon)
end
end,
destroy_func = function(self)
for spirit in self.summon_set:elements() do
spirit:destroy()
end
self.model2 = nil
self.model3 = nil
self.attackcd = nil
self.attackrange = nil
self.summon_set:destroy()
self.summon_set = nil
self.spiritOrbs = nil
self.dashImpact = nil
self.spiritNeedles = nil
self.get_speed = nil
end,
move = function(self,x,y)
for spirit in self.summon_set:elements() do
spirit.targetX = spirit.targetX + x
spirit.targetY = spirit.targetY + y
spirit.sourceX = spirit.sourceX + x
spirit.sourceY = spirit.sourceY + y
end
end
}
end
function ArcaneRiftInit()
abiltrait_arcanerift_blast = Trait.new({
name = "Blast",
description = "|cff00ff00+10%% Damage (Ability)|n+10%% Area (Ability)|r",
icon = "AbilityArcaneRift.dds",
action = function(trait,playerUnit,abil)
abil.damageMult = abil.damageMult + 0.1
abil.areaMult = abil.areaMult + 0.1
end,
level_max = 10,
x2 = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_true_true_false,
})
abiltrait_arcanerift_concentrate = Trait.new({
name = "Concentrate",
description = "|cff00ff00+20%% Damage (Ability)|n+5%% Crit Chance (Ability)|r|n|cffff0000-10%% Area (Ability)|r",
icon = "AbilityArcaneRift.dds",
action = function(trait,playerUnit,abil)
abil.damageMult = abil.damageMult + 0.2
abil.areaMult = abil.areaMult - 0.1
abil.critChanceBonus = abil.critChanceBonus + 0.05
end,
x2 = 9,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_false_true_true,
})
abiltrait_arcanerift_frequency = Trait.new({
name = "Frequency",
description = "|cff00ff00+20%% Multistrike (Ability)|r|n|cffff0000-10%% Damage (Ability)|r",
icon = "AbilityArcaneRift.dds",
action = function(trait,playerUnit,abil)
abil.multistrikeMult = abil.multistrikeMult + 0.2
abil.damageMult = abil.damageMult - 0.1
end,
level_max = 10,
x2 = 9,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_true_false,
})
abiltrait_arcanerift_occurance = Trait.new({
name = "Occurance",
description = "|cff00ff00+10%% Multistrike (Ability)|n+10%% Attack Speed (Ability)|r",
icon = "AbilityArcaneRift.dds",
action = function(trait,playerUnit,abil)
abil.multistrikeMult = abil.multistrikeMult + 0.1
abil.attackSpeedMult = abil.attackSpeedMult + 0.1
end,
level_max = 10,
x2 = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_false_true,
})
abiltrait_arcanerift_bloat = Trait.new({
name = "Bloat",
description = "|cff00ff00+30%% Area (Ability)|n+20%% Duration (Ability)|r|n|cffff0000-15%% Damage (Ability)|r",
icon = "AbilityArcaneRift.dds",
action = function(trait,playerUnit,abil)
abil.damageMult = abil.damageMult - 0.15
abil.areaMult = abil.areaMult + 0.3
abil.forceMult = abil.forceMult + 0.2
end,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_three_six_ten,
})
abiltrait_arcanerift_scatter = Trait.new({
name = "Scatter",
description = "|cff00ff00+1 Base Rift (Ability)|r|n|cffff0000-30%% Area (Ability)|r",
icon = "AbilityArcaneRift.dds",
action = function(trait,playerUnit,abil)
abil.attack_count = abil.attack_count + 1
abil.areaMult = abil.areaMult - 0.3
end,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_three_six_ten,
})
ability_arcanerift = Ability.new({
name = "Arcane Rift",
description = "Randomly spawns arcane rifts around you. Touching an arcane rift triggers a magic explosion damaging nearby enemies. Force increases rift lifetime.",
icon = "AbilityArcaneRift.dds",
action = function(abil,playerUnit)
local attack = Attack.new(arcane_rift_attack,playerUnit,ability_arcanerift)
abiltrait_arcanerift_blast:add_to_pool(attack)
abiltrait_arcanerift_concentrate:add_to_pool(attack)
abiltrait_arcanerift_frequency:add_to_pool(attack)
abiltrait_arcanerift_occurance:add_to_pool(attack)
abiltrait_arcanerift_bloat:add_to_pool(attack)
abiltrait_arcanerift_scatter:add_to_pool(attack)
end,
})
abilityupgrade_riftsplinters = Ability.new({
upgradeAbility = ability_arcanerift,
name = "Rift Splinters",
description = "When arcane rifts explode, they emit magic projecties dealing damage to enemies that touch them.",
action = function(abil,playerUnit,attack)
if not attack.riftSplinters then
attack.timer:callDelayed(DEFAULT_TIMEOUT, move_arcane_splinters,attack)
end
attack.riftSplinters = true
end,
})
abilityupgrade_wanderingrifts = Ability.new({
upgradeAbility = ability_arcanerift,
name = "Wandering Rifts",
description = "Arcane rifts slowly move towards you and explode when touching enemies. Their speed scales with your movement speed.",
action = function(abil,playerUnit,attack)
attack.wanderingRifts = true
end,
})
abilityupgrade_elementalrift = Ability.new({
upgradeAbility = ability_arcanerift,
name = {
"Elemental Rift: Fire",
"Elemental Rift: Ice",
"Elemental Rift: Lightning",
},
description = {
"Rifts spawns with an element based on element types. Adds Fire damage type to the Rift source and Splinter upgrade.",
"Rifts spawns with an element based on element types. Adds Ice damage type to the Rift source and Splinter upgrade.",
"Rifts spawns with an element based on element types. Adds Lightning damage type to the Rift source and Splinter upgrade."
},
action = {
function(abil,playerUnit,attack)
attack.elementalRift = 1
attack.r = 212
attack.g = 94
attack.b = 25
attack.damageType = DamageTypes.Fire
attack.effect_function = attack_effect_burn
attack.model2 = "Objects\\Spawnmodels\\Human\\HCancelDeath\\HCancelDeath.mdl"
end,
function(abil,playerUnit,attack)
attack.elementalRift = 2
attack.r = 128
attack.g = 128
attack.b = 255
attack.damageType = DamageTypes.Ice
attack.effect_function = attack_effect_frost
attack.model2 = "Abilities\\Spells\\Undead\\FreezingBreath\\FreezingBreathMissile.mdl"
end,
function(abil,playerUnit,attack)
attack.elementalRift = 3
attack.r = 255
attack.g = 255
attack.b = 0
attack.damageType = DamageTypes.Lightning
attack.effect_function = attack_effect_electrify
attack.model2 = "Abilities\\Weapons\\FarseerMissile\\FarseerMissile.mdl"
end,
},
})
local function set_ready(rift)
rift.ready = true
BlzSetSpecialEffectTimeScale(rift.sfx,GetRandomReal(0.1,0.3))
end
arcanerift = {}
arcanerift.__index = arcanerift
function arcanerift.new(x,y,attack,count)
local self = setmetatable(NewTable(),arcanerift)
self.sfx = AddSpecialEffect("ArcaneRift2.mdx",x-SFX_OFFSET,y-SFX_OFFSET)
BlzSetSpecialEffectScale(self.sfx,attack:get_scale())
BlzSetSpecialEffectColor(self.sfx,attack.r,attack.g,attack.b)
BlzSetSpecialEffectYaw(self.sfx,GetRandomReal(0,TAU))
BlzSetSpecialEffectZ(self.sfx,2)
self.sfx2 = AddSpecialEffect("AreaDamageIndicator.mdx",x-SFX_OFFSET,y-SFX_OFFSET)
BlzSetSpecialEffectTimeScale(self.sfx2,0.0)
BlzSetSpecialEffectTime(self.sfx2,0.5)
self.area = attack:get_area()
BlzSetSpecialEffectScale(self.sfx2,self.area/128)
attack.timer:callDelayed(1.0, set_ready, self)
self.x = x
self.y = y
self.dur = GAME_TIME + attack:get_force() + 1
self.attack = attack
self.count = count
attack.rift_set:add(self)
return self
end
function arcanerift.destroy(self)
self.attack.rift_set:remove(self)
BlzSetSpecialEffectTimeScale(self.sfx,2.0)
BlzSetSpecialEffectZ(self.sfx2,-99999)
DestroyEffect(self.sfx2)
DestroyEffect(self.sfx)
self.sfx2 = nil
self.x = nil
self.y = nil
self.sfx = nil
self.ready = nil
self.dur = nil
self.area = nil
self.count = nil
self.attack = nil
ReleaseTable(self)
end
local function hit_iter(u,attack)
attack:deal_damage(u)
end
function arcanerift.trigger(rift,self)
local area = rift.area
local half = area * 0.5
local x,y = rift.x,rift.y
local scale = self:get_scale()
SOUND_RIFT_IMPACT(GetRandomReal(0.9,1.2),true,x,y)
unit_collision_hash:each(x - half, y - half, area, area, hit_iter,self)
local sfx = AddSpecialEffect(self.model2,x,y)
BlzSetSpecialEffectScale(sfx,scale*0.65)
DestroyEffectEx(sfx)
if self.elementalRift == 2 then
BlzSetSpecialEffectTime(sfx,0.1)
end
if self.riftSplinters then
area = area * 0.13
half = area * 0.5
local dur = self:get_force()
local range = self:get_range()
for i = 1, rift.count * 3, 1 do
local dir = GetRandomReal(0,TAU)
local range_dif = range * GetRandomReal(0.6,1.0)
local x2,y2 = x + math.cos(dir) * range_dif, y + math.sin(dir) * range_dif
local splint = splinter.new(scale*0.28,x,y,x2,y2,1.5,dur,self,area,half,0.1)
BlzSetSpecialEffectColor(splint.sfx,self.r,self.g,self.b)
end
end
rift:destroy()
end
local function check(self)
local wanderingRifts = self.wanderingRifts
local x1,y1 = self.owner.position[1], self.owner.position[2]
for rift in self.rift_set:elements() do
local x,y = rift.x,rift.y
if wanderingRifts then
local x2,y2 = VectorAngle(x, y, x1, y1)
rift.x = rift.x - x2 * 1.5
rift.y = rift.y - y2 * 1.5
BlzSetSpecialEffectPosition(rift.sfx2,rift.x - SFX_OFFSET,rift.y - SFX_OFFSET,0)
BlzSetSpecialEffectPosition(rift.sfx,rift.x - SFX_OFFSET,rift.y - SFX_OFFSET,2)
end
if rift.ready then
local area = rift.area * 0.25
local half = area * 0.5
if area_has_player(x,y,area,self.owner) or (wanderingRifts and unit_collision_hash:cell_occupied(x-half,y-half,area,area,Types.Damageable)) then
rift:trigger(self)
elseif rift.dur < GAME_TIME then
rift:destroy()
end
end
end
self.timer:callDelayed(DEFAULT_TIMEOUT, check,self)
end
local function strike_period(self)
local x,y = self.owner.position[1], self.owner.position[2]
local range = self:get_range()
local count = self:increment_count(self)
for i = 1, count do
local x2,y2
while x2 == nil or unit_collision_hash:cell_occupied(x2- 32, y2 - 32, 64, 64,Types.Collidable) do
local r = GetRandomReal(range*0.5,range)
local a = GetRandomReal(0,TAU)
x2,y2 = x + math.cos(a) * r, y + math.sin(a) * r
end
arcanerift.new(x2,y2,self,count)
end
self.timer:callDelayed(self:start_cooldown(), strike_period,self)
end
arcane_rift_attack = {
name = "Arcane Rift",
model = "RiftSplinter.mdx",
range = 450.,
cooldown = 5.5,
area = 400.0,
damage = 320.0,
base_scale = 2.5,
critDamageBonus = 0.65,
critChanceBonus = 0.2,
effectChance = 0.1,
force = 6,
attack_count = 2.0,
damageType = DamageTypes.Magical,
properties = AttackProperties.Area,
type = AttackTypes.Unique,
gain_action = function(self)
self.r = 140
self.g = 100
self.b = 255
self.dmg_cd = 0.0
self.rift_set = Set.create()
self.splinter_set = Set.create()
self.model2 = "ArcaneNova.mdx"
self.timer:callDelayed(self:start_cooldown(), strike_period,self)
self.timer:callDelayed(DEFAULT_TIMEOUT, check,self)
end,
destroy_func = function(self)
for rift in self.rift_set:elements() do
rift:destroy()
end
self.r = nil
self.g = nil
self.b = nil
self.rift_set:destroy()
self.rift_set = nil
self.model2 = nil
self.dmg_cd = nil
self.riftSplinters = nil
self.elementalRift = nil
self.wanderingRifts = nil
for splint in self.splinter_set:elements() do
splint:destroy()
end
self.splinter_set:destroy()
self.splinter_set = nil
end,
move = function(self,x,y)
for rift in self.rift_set:elements() do
rift.x = rift.x + x
rift.y = rift.y + y
BlzSetSpecialEffectPosition(rift.sfx,rift.x-SFX_OFFSET,rift.y-SFX_OFFSET,2)
BlzSetSpecialEffectPosition(rift.sfx2,rift.x-SFX_OFFSET,rift.y-SFX_OFFSET,0)
end
for splint in self.splinter_set:elements() do
splint.x1 = splint.x1 + x
splint.y1 = splint.y1 + y
splint.x2 = splint.x2 + x
splint.y2 = splint.y2 + y
end
end
}
end
function InfestationInit()
abiltrait_infestation_hunger = Trait.new({
name = "Hunger",
description = "|cff00ff00+10%% Damage Tick Rate (Ability)|r",
icon = "AbilityInfestation.dds",
action = function(trait,playerUnit,abil)
abil.damageSpeed = abil.damageSpeed - 0.1
end,
level_max = 10,
x2 = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_true_true_false,
})
abiltrait_infestation_ferocity = Trait.new({
name = "Ferocity",
description = "|cff00ff00+15%% Attack Speed (Ability)|n+10%% Movement Speed (Ability)|r",
icon = "AbilityInfestation.dds",
action = function(trait,playerUnit,abil)
abil.attackSpeedMult = abil.attackSpeedMult + 0.15
abil.moveSpeedMult = abil.moveSpeedMult + 0.1
for u in abil.summons:elements() do
u.moveSpeedMult = abil.moveSpeedMult
end
end,
x2 = 9,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_false_true_true,
})
abiltrait_infestation_contagion = Trait.new({
name = "Contagion",
description = "|cff00ff00+25%% Range (Ability)|r",
icon = "AbilityInfestation.dds",
action = function(trait,playerUnit,abil)
abil.rangeMult = abil.rangeMult + 0.25
end,
level_max = 10,
x2 = 9,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_true_false,
})
abiltrait_infestation_spread = Trait.new({
name = "Spread",
description = "|cff00ff00+25%% Spawn Count (Ability)|r",
icon = "AbilityInfestation.dds",
action = function(trait,playerUnit,abil)
abil.multistrikeMult = abil.multistrikeMult + 0.25
end,
level_max = 10,
x2 = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_false_true,
})
abiltrait_infestation_outburst = Trait.new({
name = "Outburst",
description = "|cff00ff00+20%% Damage (Ability)|n+5%% Crit Chance (Ability)|r",
icon = "AbilityInfestation.dds",
action = function(trait,playerUnit,abil)
abil.damageMult = abil.damageMult + 0.2
abil.critChanceBonus = abil.critChanceBonus + 0.05
end,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_three_six_ten,
})
abiltrait_infestation_lethality = Trait.new({
name = "Lethality",
description = "|cff00ff00+7 Base Damage (Ability)|n+15%% Crit Damage (Ability)|r",
icon = "AbilityInfestation.dds",
action = function(trait,playerUnit,abil)
abil.damage = abil.damage + 7
abil.critDamageBonus = abil.critDamageBonus + 0.15
end,
level_max = 10,
level_func = TraitLevelFunction_Eight,
ability_level_func = AbilityLevelFunction_three_six_ten,
})
ability_infestation = Ability.new({
name = "Infestation",
description = "Periodically send out a parasitic creature that latches onto a target enemy. The parasite deals damage over time to the host. If the host dies due to the parasite, an additional parasite is created with a shorter lifespan.",
icon = "AbilityInfestation.dds",
action = function(abil,playerUnit)
local attack = Attack.new(infestation_attack,playerUnit,ability_infestation)
abiltrait_infestation_hunger:add_to_pool(attack)
abiltrait_infestation_ferocity:add_to_pool(attack)
abiltrait_infestation_contagion:add_to_pool(attack)
abiltrait_infestation_spread:add_to_pool(attack)
abiltrait_infestation_outburst:add_to_pool(attack)
abiltrait_infestation_lethality:add_to_pool(attack)
end,
})
local ANIM_TIME = 0.7
local function animation2(self,summon,pos1,pos2,dur)
dur = math.min(dur + DEFAULT_TIMEOUT,ANIM_TIME)
local t = dur / ANIM_TIME
local tc = easeInCubic(t)
local x2,y2 = lerp(pos1.x,pos2.x,tc), lerp(pos1.y,pos2.y,tc)
if dur < ANIM_TIME and summon.type ~= nil then
self.timer:callDelayed(DEFAULT_TIMEOUT, animation2, self, summon, pos1,pos2,dur)
else
ReleaseUpdatingVector(pos1)
ReleaseUpdatingVector(pos2)
if summon.type == nil then return end
summon.canMove = true
end
summon:move(x2,y2)
BlzSetSpecialEffectScale(summon.sfx,tc)
end
local function create_summon(self,x,y,dur,playAnim)
local summon
if playAnim then
summon = Summon.new(summon_parasite,x,y,self.owner,self)
summon.canMove = false
summon.timedLife = dur + ANIM_TIME
local x2,y2 = GetRandomLocationStrictRange(x,y,self:get_range(),summon_parasite.size,Types.Collidable)
local pos2 = GetUpdatingVector(x2,y2)
local pos1 = GetUpdatingVector(x,y)
local yaw = math.atan(y2-y, x2-x)
BlzSetSpecialEffectScale(summon.sfx,0.0)
summon.yaw = yaw
BlzSetSpecialEffectYaw(summon.sfx,yaw)
self.timer:callDelayed(DEFAULT_TIMEOUT, animation2, self, summon, pos1,pos2,0)
SOUND_PARASITE_SPAWN:random()(GetRandomReal(0.9,1.1),true,x,y)
else
summon = Summon.new(summon_parasite,x+GetRandomReal(-16,16),y+GetRandomReal(-16,16),self.owner,self)
summon.timedLife = dur
end
self.summons:add(summon)
summon.getTargetFlag = Types.Uninfested
summon.moveSpeedMult = self.moveSpeedMult
return summon
end
local function animation(abil,sfx,target,time,pos,dur)
time = math.min(time + DEFAULT_TIMEOUT,ANIM_TIME)
local t = time / ANIM_TIME
if target.position == nil then
local summon = create_summon(abil,BlzGetLocalSpecialEffectX(sfx),BlzGetLocalSpecialEffectY(sfx),dur,true)
BlzSetSpecialEffectZ(sfx,-9999)
DestroyEffect(sfx)
abil.effects:remove(sfx)
ReleaseUpdatingVector(pos)
elseif t == 1.0 then
BlzSetSpecialEffectZ(sfx,-9999)
DestroyEffect(sfx)
abil.effects:remove(sfx)
ReleaseUpdatingVector(pos)
local tbl = NewTable()
tbl.u = target
tbl.dur = dur
table.insert(abil.damageList,tbl)
abil:update_summoned(1)
else
local x = lerp(pos.x,target.position[1],t)
local y = lerp(pos.y,target.position[2],t)
local z = ParabolicArc(target.position[3] or 0, 200., t)
BlzSetSpecialEffectPosition(sfx,x,y,z)
BlzSetSpecialEffectScale(sfx,lerp(1.0,0.0,easeInX(t,5)))
BlzSetSpecialEffectPitch(sfx,lerp(0.0,HALF_PI,easeOutX(t,5)))
abil.timer:callDelayed(DEFAULT_TIMEOUT, animation, abil, sfx, target,time,pos,dur)
end
end
local function dmg_iter(abil,target,dur,max_dur)
if target.type == nil then
return
end
if dur == 0 then
target.type = target.type | Types.Uninfested
end
local x,y = target.position[1],target.position[2]
if abil.swarmEvolution then
abil:deal_damage(target,1.0 + abil.summoned_count * 0.02)
else
abil:deal_damage(target)
end
if target.type == nil then
if abil.swarming then
create_summon(abil,x,y,abil:get_force()+GAME_TIME,true)
else
create_summon(abil,x,y,abil:get_force()*0.3+GAME_TIME,true)
end
return
end
if dur >= max_dur then
return
end
abil.timer:callDelayed(abil.damageSpeed, dmg_iter, abil,target,dur+1,max_dur)
end
summon_attack_infestation = {
range = 128.0,
in_range_function = function(attack,count)
local summon = attack.owner
local abil = summon.ability
local sfx = summon.sfx
if not Types.is(summon.target,Types.Uninfested) then
summon.target = abil.owner
local range = abil:get_range()
local half = range * 0.5
local closest = unit_collision_hash:get_first(summon.position[1] - half, summon.position[2] - half, range, range,summon.getTargetFlag)
if closest then
summon.target = closest
else
return
end
end
summon.sfx = nil
abil.effects:add(sfx)
local pos = GetUpdatingVector(summon.position[1],summon.position[2])
abil.timer:callDelayed(DEFAULT_TIMEOUT, animation, abil, sfx, summon.target,0.0,pos,summon.timedLife+ANIM_TIME)
summon.target.marked = GAME_TIME + ANIM_TIME
summon.target.type = summon.target.type & ~Types.Uninfested
local t = abil.damageSpeed
local f = abil:get_force()
local max_dur = MathRound(f/t)
abil.timer:callDelayed(t, dmg_iter, abil,summon.target,0,max_dur)
summon:destroy()
end,
cooldown = 1.0,
attack_time = 1.0,
delay = 0.01,
}
summon_parasite.attack = summon_attack_infestation
local function strike_period(self)
local count = floorWithProbabilisticRemainder(self:get_summon_count())
local dur = self:get_force() + GAME_TIME
for i = 1, count do
create_summon(self,self.owner.position[1],self.owner.position[2],dur,false)
end
self.timer:callDelayed(self:start_cooldown(), strike_period,self)
end
abilityupgrade_swarmevolution = Ability.new({
upgradeAbility = ability_infestation,
name = "Parasitic Connection",
description = "Parasites deal +2%% damage per parasite alive.",
icon = "AbilityInfestation.dds",
action = function(abil,playerUnit,attack)
attack.swarmEvolution = true
end,
})
abilityupgrade_swarming = Ability.new({
upgradeAbility = ability_infestation,
name = "Swarming",
description = "Parasites that are created from killing units spawn with the full duration.",
icon = "AbilityInfestation.dds",
action = function(abil,playerUnit,attack)
attack.swarming = true
end,
})
abilityupgrade_swarminfection = Ability.new({
upgradeAbility = ability_infestation,
name = "Infection",
description = "Parasites have a 50%% chance to inflict fragile per hit and increases the base jump range by 50%%.",
icon = "AbilityInfestation.dds",
action = function(abil,playerUnit,attack)
attack.effect_function = attack_effect_fragile
attack.effectChance = 0.5
attack.range = attack.range *1.5
end,
})
local function clearTargetTbl(tbl,self)
tbl.u = nil
tbl.dur = nil
ReleaseTable(tbl)
self:update_summoned(-1)
end
local function unit_death(self,target)
local i = 1
local damageList = self.damageList
while i <= #damageList do
local tbl = damageList[i]
if tbl.u == target then
damageList[i] = damageList[#damageList]
damageList[#damageList] = nil
create_summon(self,target.position[1],target.position[2],tbl.dur,true)
clearTargetTbl(tbl,self)
else
i = i + 1
end
end
end
local function summon_death(self,summon)
if self == summon.ability then
summon.ability.summons:remove(summon)
end
end
infestation_attack = {
name = "Infestation",
cooldown = 4.0,
damage = 50.0,
base_scale = 1.0,
range = 128.,
force = 5.0,
critChanceBonus = 0.2,
effectChance = 0.5,
summon_count = 1.0,
properties = AttackProperties.Melee + AttackProperties.Summon,
type = AttackTypes.Unique,
gain_action = function(self,data)
self.summons = Set.create()
self.effects = Set.create()
self.damageList = NewTable()
self.timer:callDelayed(self:start_cooldown(), strike_period,self)
local event = Unit.kill_event:add_action(unit_death,self)
table.insert(self.event_actions,event)
self.damageSpeed = 1.0
self.moveSpeedMult = 1.0
event = Summon.kill_event:add_action(summon_death,self)
table.insert(self.event_actions,event)
end,
destroy_func = function(self)
self.damageSpeed = nil
self.moveSpeedMult = nil
for u in self.summons:elements() do
u:destroy()
end
for sfx in self.effects:elements() do
DestroyEffect(sfx)
end
for i, tbl in ipairs(self.damageList) do
clearTargetTbl(tbl,self)
end
ReleaseTable(self.damageList)
self.damageList = nil
self.summons:destroy()
self.effects:destroy()
self.effects = nil
self.summons = nil
self.swarmEvolution = nil
self.swarming = nil
end,
}
end
do
local MAX_BONUS_REGEN = 0.01
local MAX_BONUS_ATTACK_SPEED = 0.2
local MAX_BONUS_MOVE_SPEED = 0.2
local ACCUMULATE_TIME = 5.0
local function check(self)
local owner = self.owner
owner.moveSpeedMult = tonumber(string.format("%%0.3f",owner.moveSpeedMult - self.moveSpeedBonus))
self.moveSpeedBonus = 0.0
owner.attackSpeedMult = tonumber(string.format("%%0.3f",owner.attackSpeedMult - self.attackSpeedBonus))
self.attackSpeedBonus = 0.0
owner.baseHealthRegen = tonumber(string.format("%%0.3f",owner.baseHealthRegen - self.regenBonus))
self.regenBonus = 0.0
if owner.moving then
self.t = math.max(self.t - 0.1,0.0)
else
self.t = math.min(self.t + 0.1,ACCUMULATE_TIME)
end
local t = self.t/ACCUMULATE_TIME
self.regenBonus = owner:get_max_health() * MAX_BONUS_REGEN * t
owner.baseHealthRegen = owner.baseHealthRegen + self.regenBonus
if owner.health >= owner.max_health then
self.attackSpeedBonus = MAX_BONUS_ATTACK_SPEED * t
owner.attackSpeedMult = owner.attackSpeedMult + self.attackSpeedBonus
else
self.moveSpeedBonus = MAX_BONUS_MOVE_SPEED * t
owner.moveSpeedMult = owner.moveSpeedMult + self.moveSpeedBonus
end
self.timer:callDelayed(0.1, check,self)
end
pace_setters_attack = {
gain_action = function(self)
self.t = 0.0
self.regenBonus = 0.0
self.attackSpeedBonus = 0.0
self.moveSpeedBonus = 0.0
self.timer:callDelayed(0.1, check,self)
end,
destroy_func = function(self)
local owner = self.owner
owner.moveSpeedMult = tonumber(string.format("%%0.3f",owner.moveSpeedMult - self.moveSpeedBonus))
owner.attackSpeedMult = tonumber(string.format("%%0.3f",owner.attackSpeedMult - self.attackSpeedBonus))
owner.baseHealthRegen = tonumber(string.format("%%0.3f",owner.baseHealthRegen - self.regenBonus))
self.t = nil
self.regenBonus = nil
self.attackSpeedBonus = nil
self.moveSpeedBonus = nil
end,
}
end
do
local BONUS_BLOCK = 0.2
local BONUS_DEFEFNSE = 0.2
plated_boots_attack = {
gain_action = function(self)
local owner = self.owner
owner.blockMult = owner.blockMult + BONUS_BLOCK
owner.defenseMult = owner.defenseMult + BONUS_DEFEFNSE
end,
destroy_func = function(self)
local owner = self.owner
owner.blockMult = owner.blockMult - BONUS_BLOCK
owner.defenseMult = owner.defenseMult - BONUS_DEFEFNSE
end,
}
end
do
local function spawn_fire(self)
local owner = self.owner
if owner.moving then
meteorfire.new(self:get_scale(),owner.position[1],owner.position[2],self,self:get_force(),0.4)
end
self.timer:callDelayed(1.1/owner:get_movespeed(), spawn_fire,self)
end
firewalker_boots_attack = {
base_scale = 0.75,
force = 4.0,
gain_action = function(self)
local owner = self.owner
owner.speed = owner.speed + 0.25
self.fire_set = Set.create()
self.timer:callDelayed(0.1, meteorfire_check_fire,self)
self.timer:callDelayed(1.1/owner:get_movespeed(), spawn_fire,self)
end,
destroy_func = function(self)
local owner = self.owner
owner.speed = owner.speed - 0.25
for projectile in self.fire_set:elements() do
projectile:destroy()
end
self.fire_set:destroy()
self.fire_set = nil
end,
move = function(self,x,y)
for projectile in self.fire_set:elements() do
projectile.x = projectile.x + x
projectile.y = projectile.y + y
BlzSetSpecialEffectPosition(projectile.sfx,projectile.x-SFX_OFFSET,projectile.y-SFX_OFFSET,0)
end
end,
}
end
do
local MAX_BONUS_ATTACK_SPEED = 0.5
local MOVE_SPEED_PERCENT_MULT = 0.6
local function check(self)
local owner = self.owner
local atk = owner.attacks[1]
atk.attackSpeedMult = tonumber(string.format("%%0.3f",atk.attackSpeedMult - self.attackSpeedBonus))
self.attackSpeedBonus = 0.0
if owner.moving then
self.attackSpeedBonus = math.min(MAX_BONUS_ATTACK_SPEED,(owner.moveSpeedMult-1) * MOVE_SPEED_PERCENT_MULT)
atk.attackSpeedMult = atk.attackSpeedMult + self.attackSpeedBonus
end
self.timer:callDelayed(0.1, check,self)
end
beserker_boots_attack = {
gain_action = function(self)
self.attackSpeedBonus = 0.0
self.timer:callDelayed(0.1, check,self)
end,
destroy_func = function(self)
local owner = self.owner
local atk = owner.attacks[1]
atk.attackSpeedMult = tonumber(string.format("%%0.3f",atk.attackSpeedMult - self.attackSpeedBonus))
self.attackSpeedBonus = nil
end,
}
end
do
local ACCUMULATE_TIME = 5.0
local MAX_BLOCK = 100.0
local function check(self)
local owner = self.owner
owner.block = tonumber(string.format("%%0.3f",owner.block - self.blockBonus))
self.blockBonus = 0.0
if owner.moving then
self.t = math.min(self.t + 0.1,ACCUMULATE_TIME)
else
self.t = math.max(self.t - 0.1,0.0)
end
local t = self.t/ACCUMULATE_TIME
self.blockBonus = owner.moveSpeedMult * MAX_BLOCK * t
owner.block = owner.block + self.blockBonus
self.timer:callDelayed(0.1, check,self)
end
elven_slippers_attack = {
gain_action = function(self)
self.t = 0.0
self.blockBonus = 0.0
self.timer:callDelayed(0.1, check,self)
end,
destroy_func = function(self)
local owner = self.owner
owner.block = tonumber(string.format("%%0.3f",owner.block - self.blockBonus))
self.t = nil
self.blockBonus = nil
end,
}
end
do
local function spawn_goo(self)
local owner = self.owner
if owner.moving then
local f = self:get_force()
local goo = frostfloor.new("goo_puddle2.mdx",self:get_scale(),owner.position[1],owner.position[2],self,f,0.4,f*0.5)
BlzSetSpecialEffectColor(goo.sfx,200,255,0)
BlzSetSpecialEffectAlpha(goo.sfx,GetRandomInt(100,150))
end
self.timer:callDelayed(2.1/owner:get_movespeed(), spawn_goo,self)
end
bogged_boots_attack = {
base_scale = 0.75,
force = 10.0,
gain_action = function(self)
local owner = self.owner
self.icefloor_set = Set.create()
self.timer:callDelayed(0.1, frostavalanche_check_ice,self)
self.timer:callDelayed(2.1/owner:get_movespeed(), spawn_goo,self)
end,
destroy_func = function(self)
local owner = self.owner
for projectile in self.icefloor_set:elements() do
projectile:destroy()
end
self.icefloor_set:destroy()
self.icefloor_set = nil
end,
move = function(self,x,y)
for projectile in self.icefloor_set:elements() do
projectile.x = projectile.x + x
projectile.y = projectile.y + y
BlzSetSpecialEffectPosition(projectile.sfx,projectile.x-SFX_OFFSET,projectile.y-SFX_OFFSET,0)
end
end,
}
end
do
spiketrap = {}
spiketrap.__index = spiketrap
function spiketrap.new(model,scale,area,x,y,attack,dur)
local self = setmetatable(NewTable(),spiketrap)
self.x = x
self.y = y
self.sfx = AddSpecialEffect(model,x-SFX_OFFSET,y-SFX_OFFSET)
self.area = area
self.half = area * 0.5
self.attack = attack
self.dur = dur + GAME_TIME
attack.spike_traps_set:add(self)
BlzSetSpecialEffectYaw(self.sfx,GetRandomReal(0,TAU))
BlzSetSpecialEffectScale(self.sfx,scale)
return self
end
function spiketrap:destroy()
self.attack.spike_traps_set:remove(self)
DestroyEffect(self.sfx)
self.x = nil
self.y = nil
self.dur = nil
self.attack = nil
setmetatable(self,nil)
self.area = nil
self.half = nil
self.sfx = nil
ReleaseTable(self)
end
local function spawn_traps(self,player)
local owner = self.owner
if player ~= owner or self:is_on_cooldown() then return end
self:start_cooldown()
local range = self:get_range()
local x,y = owner.position[1], owner.position[2]
local area = self:get_area()
local half = area * 0.5
local count = self:increment_count(self)
local scale = self:get_scale()
local dur = self:get_force()
local a = TAU/count
for i = 1, count do
local x2,y2 = math.cos(a*i) * range + x, math.sin(a*i) * range + y
spiketrap.new(self.model,scale,area,x2,y2,self,dur)
end
end
local hit = false
local function trap_iter(u,projectile,attack)
attack:deal_damage(u)
hit = true
end
function check_spike_traps(self)
for projectile in self.spike_traps_set:elements() do
hit = false
unit_collision_hash:each(projectile.x - projectile.half, projectile.y - projectile.half, projectile.area, projectile.area, trap_iter,projectile,self)
if projectile.dur <= GAME_TIME or hit then
projectile:destroy()
end
end
self.timer:callDelayed(0.1, check_spike_traps,self)
end
spiked_boots_attack = {
damage = 100.0,
base_scale = 0.75,
force = 10.0,
cooldown = 4.0,
attack_count = 10.0,
area = 64.0,
range = 150.0,
model = "BearTrap.mdx",
gain_action = function(self)
local owner = self.owner
self.unblockable = true
self.spike_traps_set = Set.create()
self.timer:callDelayed(0.1, check_spike_traps,self)
local event = PlayerUnit.hit_event:add_action(spawn_traps,self)
table.insert(self.event_actions,event)
end,
destroy_func = function(self)
self.unblockable = nil
local owner = self.owner
for projectile in self.spike_traps_set:elements() do
projectile:destroy()
end
self.spike_traps_set:destroy()
self.spike_traps_set = nil
end,
move = function(self,x,y)
for projectile in self.spike_traps_set:elements() do
projectile.x = projectile.x + x
projectile.y = projectile.y + y
BlzSetSpecialEffectPosition(projectile.sfx,projectile.x-SFX_OFFSET,projectile.y-SFX_OFFSET,0)
end
end,
on_hit_function = function(target,attack)
target:stun(attack.owner,1.0)
end,
}
end
do
local MOVE_SPEED_BONUS = 0.25
running_shoes_attack = {
gain_action = function(self)
local owner = self.owner
owner.moveSpeedMult = owner.moveSpeedMult + MOVE_SPEED_BONUS
end,
destroy_func = function(self)
local owner = self.owner
owner.moveSpeedMult = owner.moveSpeedMult - MOVE_SPEED_BONUS
end,
}
end
do
local ACCUMULATE_TIME = 2.5
local function check(self)
local owner = self.owner
local max = ACCUMULATE_TIME/owner.moveSpeedMult
if owner.moving then
self.t = self.t + 0.1
if self.t > max then
self.t = 0.0
local range = self:get_range()
local half = range * 0.5
local x,y = owner.position[1],owner.position[2]
local sfx = AddSpecialEffect("ElectricSpark.mdx",x-SFX_OFFSET,y-SFX_OFFSET)
BlzSetSpecialEffectScale(sfx,range*0.0025)
BlzSetSpecialEffectAlpha(sfx,100)
DestroyEffect(sfx)
unit_collision_hash:each(x - half, y - half, range, range, electric_shockwave_iter,owner)
end
end
self.timer:callDelayed(0.1, check,self)
end
electrostatic_treads_attack = {
range = 256.,
gain_action = function(self)
self.t = 0.0
self.timer:callDelayed(0.1, check,self)
end,
destroy_func = function(self)
self.t = nil
end,
}
end
do
local ACCUMULATE_TIME = 5.0
local ACCUMULATE_MULT = 0.06
local function check(self)
local owner = self.owner
owner.damageMult = tonumber(string.format("%%0.3f",owner.damageMult - self.damageBonus))
self.damageBonus = 0.0
if owner.moving then
self.t = 0.0
else
self.t = math.min(self.t + 0.1,ACCUMULATE_TIME)
self.damageBonus = ACCUMULATE_MULT * self.t
owner.damageMult = owner.damageMult + self.damageBonus
end
self.timer:callDelayed(0.1, check,self)
end
hunters_garb_attack = {
gain_action = function(self)
self.t = 0.0
self.damageBonus = 0.0
self.timer:callDelayed(0.1, check,self)
end,
destroy_func = function(self)
local owner = self.owner
owner.damageMult = tonumber(string.format("%%0.3f",owner.damageMult - self.damageBonus))
self.t = nil
self.damageBonus = nil
end,
}
end
do
local DEFAULT_CHANCE = 0.1
local HIT_CHANCE = 2.0
local function burn_iter(u,owner,chance)
if Types.is(u,Types.Effectable) then
u:burn(owner,chance)
end
end
local function check(self)
local owner = self.owner
local range = owner:get_pickup_range()
local half = range * 0.5
unit_collision_hash:each(owner.position[1] - half, owner.position[2] - half, range, range, burn_iter,owner,self.nextChance)
self.timer:callDelayed(0.5, check,self)
self.nextChance = DEFAULT_CHANCE
end
local function trigger_hit_chance(self)
self.nextChance = HIT_CHANCE
end
blazing_shell_attack = {
gain_action = function(self)
self.timer:callDelayed(0.5, check,self)
self.nextChance = DEFAULT_CHANCE
local event = PlayerUnit.hit_event:add_action(trigger_hit_chance,self)
table.insert(self.event_actions,event)
end,
destroy_func = function(self)
self.nextChance = nil
end,
}
end
do
local ON_KILL_HEAL_CHANCE = 0.1
local ON_KILL_DAMAGE_INCREASE_CHANCE = 0.04
local ON_KILL_HEAL = 1.0
local ON_KILL_DAMAGE_INCREASE = 0.01
local MAX_BONUS = 0.5
local function unit_dies(owner,wearableData)
local r = GetRandomReal(0.0,1.0)
if owner.health >= owner.max_health then
if r >= ON_KILL_DAMAGE_INCREASE_CHANCE then
owner.damageMult = tonumber(string.format("%%0.3f",owner.damageMult - wearableData.damageBonus))
wearableData.damageBonus = math.min(MAX_BONUS,wearableData.damageBonus + ON_KILL_DAMAGE_INCREASE)
owner.damageMult = owner.damageMult + wearableData.damageBonus
end
elseif r >= ON_KILL_HEAL_CHANCE then
owner.regenAccum = owner.regenAccum + ON_KILL_HEAL
end
end
bloodsoaked_shirt_attack = {
gain_action = function(self,data,wearable)
local owner = self.owner
self.wearableData = wearable.data
wearable.data.damageBonus = wearable.data.damageBonus or 0.0
owner.damageMult = owner.damageMult + wearable.data.damageBonus
local event = Unit.kill_event:add_action(unit_dies,self.owner,wearable.data)
table.insert(self.event_actions,event)
end,
destroy_func = function(self)
local wearableData = self.wearableData
local owner = self.owner
owner.damageMult = tonumber(string.format("%%0.3f",owner.damageMult - wearableData.damageBonus))
self.wearableData = nil
end,
}
end
do
local REMOVE_DEFENSE_TIME = 10.0
local GAIN_FORCE_TIME = 6.0
local DEFENSE_GAIN_PER_HIT = 3.0
local MAX_DEFENSE_BONUS = 30.0
local MAX_REGEN_BONUS = 5.0
local REGEN_GAIN_PER_HIT = 0.5
local MAX_FORCE_BONUS = 0.3
local FORCE_GAIN_PER_SECOND = 0.03
local function check(self)
local owner = self.owner
owner.defense = tonumber(string.format("%%0.3f",owner.defense - self.bonusDefense))
owner.baseHealthRegen = tonumber(string.format("%%0.3f",owner.baseHealthRegen - self.bonusRegen))
owner.forceMult = tonumber(string.format("%%0.3f",owner.forceMult - self.bonusForce))
if self.hitLast + REMOVE_DEFENSE_TIME < GAME_TIME then
self.bonusDefense = 0.0
self.bonusRegen = 0.0
end
if self.hitLast + GAIN_FORCE_TIME < GAME_TIME then
self.bonusForce = math.min(self.bonusForce + FORCE_GAIN_PER_SECOND,MAX_FORCE_BONUS)
end
owner.defense = owner.defense + self.bonusDefense
owner.baseHealthRegen = owner.baseHealthRegen + self.bonusRegen
owner.forceMult = owner.forceMult + self.bonusForce
self.timer:callDelayed(1.0, check,self)
end
local function trigger_hit_chance(self,owner)
self.hitLast = GAME_TIME
owner.defense = tonumber(string.format("%%0.3f",owner.defense - self.bonusDefense))
owner.baseHealthRegen = tonumber(string.format("%%0.3f",owner.baseHealthRegen - self.bonusRegen))
owner.forceMult = tonumber(string.format("%%0.3f",owner.forceMult - self.bonusForce))
self.bonusDefense = math.min(self.bonusDefense + DEFENSE_GAIN_PER_HIT,MAX_DEFENSE_BONUS)
self.bonusRegen = math.min(self.bonusRegen + REGEN_GAIN_PER_HIT,MAX_REGEN_BONUS)
self.bonusForce = 0.0
owner.defense = owner.defense + self.bonusDefense
owner.baseHealthRegen = owner.baseHealthRegen + self.bonusRegen
owner.forceMult = owner.forceMult + self.bonusForce
end
defiant_plate_attack = {
gain_action = function(self)
self.bonusDefense = 0.0
self.bonusRegen = 0.0
self.bonusForce = 0.0
self.hitLast = 0.0
self.timer:callDelayed(1.0, check,self)
local event = PlayerUnit.damage_event:add_action(trigger_hit_chance,self)
table.insert(self.event_actions,event)
end,
destroy_func = function(self)
local owner = self.owner
owner.defense = tonumber(string.format("%%0.3f",owner.defense - self.bonusDefense))
owner.baseHealthRegen = tonumber(string.format("%%0.3f",owner.baseHealthRegen - self.bonusRegen))
owner.forceMult = tonumber(string.format("%%0.3f",owner.forceMult - self.bonusForce))
self.bonusDefense = nil
self.bonusRegen = nil
self.bonusForce = nil
self.hitLast = nil
end,
}
end
do
local BLOCK_BONUS = 15.0
cloakshadow = {}
cloakshadow.__index = cloakshadow
function cloakshadow.new(scale,x,y,attack,dur)
local self = setmetatable(NewTable(),cloakshadow)
self.sfx = AddSpecialEffect("BlackCloudOfFog7.mdx",x-SFX_OFFSET,y-SFX_OFFSET)
self.x = x
self.y = y
self.dur = GAME_TIME+dur
self.attack = attack
self.area = attack:get_area()
attack.shadow_set:add(self)
return self
end
function cloakshadow:destroy()
self.attack.shadow_set:remove(self)
self.attack = nil
self.dur = nil
setmetatable(self,nil)
DestroyEffectEx(self.sfx)
self.sfx = nil
self.x = nil
self.area = nil
self.y = nil
ReleaseTable(self)
end
local function hit_iter(u,self)
self:deal_damage(u)
end
local function player_iter(ply,self,inside_list)
if not inside_list:contains(ply) then
inside_list:add(ply)
ply.block = ply.block + BLOCK_BONUS
SetUnitVertexColor(ply.sfx,255,255,255,100)
end
end
local function remove_inside(self)
for ply in self.inside_list:elements() do
ply.block = ply.block - BLOCK_BONUS
SetUnitVertexColor(ply.sfx,255,255,255,255)
self.inside_list:remove(ply)
end
end
function cloakshadow_check(self)
remove_inside(self)
for shadow in self.shadow_set:elements() do
local area = shadow.area
local half = area * 0.5
unit_collision_hash:each(shadow.x - half, shadow.y - half, area, area, hit_iter,self)
player_collision_hash:each(shadow.x - half, shadow.y - half, area, area, player_iter,self,self.inside_list)
if shadow.dur < GAME_TIME then
shadow:destroy()
end
end
self.timer:callDelayed(0.2, cloakshadow_check,self)
end
local function spawn_shadow(self)
local owner = self.owner
local x,y = GetRandomLocationStrictRange(owner.position[1],owner.position[2],self:get_range(),128.,Types.Collidable)
cloakshadow.new(self:get_scale(),x,y,self,self:get_force())
self.timer:callDelayed(self:start_cooldown(), spawn_shadow,self)
end
shadow_cloak_attack = {
base_scale = 0.5,
range = 256.,
area = 256.,
damage = 7.5,
force = 8.0,
cooldown = 4.0,
gain_action = function(self)
local owner = self.owner
self.shadow_set = Set.create()
self.inside_list = Set.create()
self.timer:callDelayed(0.2, cloakshadow_check,self)
self.timer:callDelayed(self:start_cooldown(), spawn_shadow,self)
end,
destroy_func = function(self)
local owner = self.owner
for projectile in self.shadow_set:elements() do
projectile:destroy()
end
remove_inside(self)
self.shadow_set:destroy()
self.inside_list:destroy()
self.inside_list = nil
self.shadow_set = nil
end,
move = function(self,x,y)
for projectile in self.shadow_set:elements() do
projectile.x = projectile.x + x
projectile.y = projectile.y + y
BlzSetSpecialEffectPosition(projectile.sfx,projectile.x-SFX_OFFSET,projectile.y-SFX_OFFSET,0)
end
end,
}
end
do
local DEBUFF_CHANCE = 0.5
function hit_iter(u,owner)
if Types.is(u,Types.Effectable) and GetRandomReal(0.0,1.0) > DEBUFF_CHANCE then
local which = GetRandomInt(1,3)
if which == 1 then
u:frail(owner,1.0)
elseif which == 2 then
u:slow(owner,1.0)
else
u:afflict(owner,1.0)
end
end
end
local function check(self)
local area = self:get_range()
local half = area * 0.5
local owner = self.owner
unit_collision_hash:each(owner.position[1] - half, owner.position[2] - half, area, area, hit_iter,owner)
self.timer:callDelayed(self:start_cooldown(), check,self)
end
brokers_cape_attack = {
range = 300.,
cooldown = 1.0,
gain_action = function(self)
self.timer:callDelayed(self:start_cooldown(), check,self)
end,
}
end
do
local DEFENSE_BONUS = 0.2
local HEALTH_BONUS = 0.2
chain_mail_attack = {
gain_action = function(self)
local owner = self.owner
owner:set_healthMult(owner.healthMult + HEALTH_BONUS)
owner.defenseMult = owner.defenseMult + DEFENSE_BONUS
end,
destroy_func = function(self)
local owner = self.owner
owner:set_healthMult(owner.healthMult - HEALTH_BONUS)
owner.defenseMult = owner.defenseMult - DEFENSE_BONUS
end,
}
end
do
local BLOCK_BONUS = 0.2
local HEALTH_BONUS = 0.2
plate_armor_attack = {
gain_action = function(self)
local owner = self.owner
owner:set_healthMult(owner.healthMult + HEALTH_BONUS)
owner.blockMult = owner.blockMult + BLOCK_BONUS
end,
destroy_func = function(self)
local owner = self.owner
owner:set_healthMult(owner.healthMult - HEALTH_BONUS)
owner.blockMult = owner.blockMult - BLOCK_BONUS
end,
}
end
do
local HEALTH_BONUS = -0.3
local ATTACK_SPEED_BONUS = 0.3
local MOVE_SPEED_BONUS = 0.3
local DAMAGE_MULT_BONUS = 0.3
crimson_carapace_attack = {
gain_action = function(self)
local owner = self.owner
owner:set_healthMult(owner.healthMult + HEALTH_BONUS)
owner.summonAttackSpeedMult = owner.summonAttackSpeedMult + ATTACK_SPEED_BONUS
owner.summonMoveSpeedMult = owner.summonMoveSpeedMult + MOVE_SPEED_BONUS
owner.summonDamageMult = owner.summonDamageMult + DAMAGE_MULT_BONUS
end,
destroy_func = function(self)
local owner = self.owner
owner:set_healthMult(owner.healthMult - HEALTH_BONUS)
owner.summonAttackSpeedMult = owner.summonAttackSpeedMult - ATTACK_SPEED_BONUS
owner.summonMoveSpeedMult = owner.summonMoveSpeedMult - MOVE_SPEED_BONUS
owner.summonDamageMult = owner.summonDamageMult - DAMAGE_MULT_BONUS
end,
}
end
do
local MULTISTRIKE_BONUS = 0.2
hunting_gloves_attack = {
gain_action = function(self)
local owner = self.owner
owner.multistrike = owner.multistrike + MULTISTRIKE_BONUS
end,
destroy_func = function(self)
local owner = self.owner
owner.multistrike = owner.multistrike - MULTISTRIKE_BONUS
end,
}
end
do
local SUMMON_SPAWN_BONUS = 0.4
local SUMMON_FORCE_BONUS = 0.2
local SUMMON_DAMAGE_BONUS = 0.2
invocators_grasp_attack = {
gain_action = function(self)
local owner = self.owner
owner.summonCountMult = owner.summonCountMult + SUMMON_SPAWN_BONUS
owner.summonForceMult = owner.summonForceMult + SUMMON_FORCE_BONUS
owner.summonDamageMult = owner.summonDamageMult + SUMMON_DAMAGE_BONUS
for attack in all_attacks:elements() do
attack:update_func()
end
end,
destroy_func = function(self)
local owner = self.owner
owner.summonCountMult = owner.summonCountMult - SUMMON_SPAWN_BONUS
owner.summonForceMult = owner.summonForceMult - SUMMON_FORCE_BONUS
owner.summonDamageMult = owner.summonDamageMult - SUMMON_DAMAGE_BONUS
for attack in all_attacks:elements() do
attack:update_func()
end
end,
}
end
do
local AFFLICT_CHANCE = 0.5
local function hit(self,whichAttack,target)
if AttackProperties.is(whichAttack,AttackProperties.Melee) and Types.is(target,Types.Effectable) then
target:afflict(self.owner,AFFLICT_CHANCE)
end
end
unholy_touch_attack = {
gain_action = function(self)
local event = Attack.hit_event:add_action(hit,self)
table.insert(self.event_actions,event)
end,
}
end
do
local BURN_CHANCE_BONUS = 0.5
local function hit_iter(u,self)
self:deal_damage(u)
end
local stacks = 0
local function hit(self,whichAttack,target)
if whichAttack.damageType == DamageTypes.Physical and Types.is(target,Types.Effectable) then
stacks = target:get_effect_stacks(EFFECT_TYPES.Burn)
if stacks and stacks > 0 then
local area = self:get_area()
local half = area * 0.5
local x,y = target.position[1], target.position[2]
unit_collision_hash:each(x - half, y - half, area, area, hit_iter,self)
local sfx = AddSpecialEffect("Abilities\\Weapons\\FlyingMachine\\FlyingMachineImpact.mdl",x-SFX_OFFSET,y-SFX_OFFSET)
BlzSetSpecialEffectScale(sfx,area/128.)
BlzSetSpecialEffectZ(sfx,area/8)
DestroyEffect(sfx)
end
end
end
local function get_damage(self)
return Attack.get_damage(self) * stacks
end
sparking_tips_attack = {
damage = 5.0,
area = 150.,
effect_function = attack_effect_burn,
effectChance = 0.1,
damageType = DamageTypes.Fire,
gain_action = function(self)
self.get_damage = get_damage
local event = Attack.hit_event:add_action(hit,self)
table.insert(self.event_actions,event)
local owner = self.owner
owner.burnBonus = owner.burnBonus + BURN_CHANCE_BONUS
end,
destroy_func = function(self)
self.get_damage = nil
local owner = self.owner
owner.burnBonus = owner.burnBonus - BURN_CHANCE_BONUS
end,
}
end
do
local function get_damage(self)
return Attack.get_damage(self.owner.attacks[1])
end
local function hit(self,player,amount,source)
local owner = self.owner
if player ~= owner or self:is_on_cooldown() then return end
self:start_cooldown()
self:deal_damage(source)
end
thornfists_attack = {
cooldown = 0.5,
critChanceBonus = 3.0,
gain_action = function(self)
local owner = self.owner
self.get_damage = get_damage
self.unblockable = true
local event = PlayerUnit.damage_event:add_action(hit,self)
table.insert(self.event_actions,event)
end,
destroy_func = function(self)
self.unblockable = nil
self.get_damage = nil
end,
}
end
do
local function fire(self,playerUnit)
if self.owner == playerUnit and playerUnit.health <= 0 then
local itemList = GetEquipedItems()
local max = playerUnit:get_max_health()
playerUnit.health = max * 0.3
local wear = itemList[GetRandomInt(1,#itemList)]
DestroyEffect(AddSpecialEffectTarget("Abilities\\Spells\\Human\\ReviveHuman\\ReviveHuman.mdl",playerUnit.sfx,"origin"))
self.timer:callDelayed(0.0,wear.destroy,wear)
ReleaseTable(itemList)
if playerUnit.pdata == PlayerDataLocal then
UI.Healthbar_Update(playerUnit.health,max)
end
end
end
devouring_hands_attack = {
gain_action = function(self)
local event = PlayerUnit.about_to_die:add_action(fire,self)
table.insert(self.event_actions,event)
end,
}
end
do
local BLOCK_BONUS = 10.
local function hit(self,player,amount,source)
if amount == 0.0 and GetRandomReal(0.0,1.0) > 0.5 then
local prevDir = player.yaw
if source.type == nil then source = player end
player.yaw = math.atan(source.position[2] - player.position[2], source.position[1] - player.position[1])
local atk = self.owner.attacks[1]
atk.always_crit = true
atk:finished(player.sfx)
atk.always_crit = nil
player.yaw = prevDir
end
end
fencer_gauntlets_attack = {
gain_action = function(self)
local owner = self.owner
local event = PlayerUnit.hit_event:add_action(hit,self)
table.insert(self.event_actions,event)
owner.block = owner.block + BLOCK_BONUS
end,
destroy_func = function(self)
local owner = self.owner
owner.block = owner.block - BLOCK_BONUS
end,
}
end
do
local ATTACK_SPEED_BONUS = 0.3
quickhand_gloves_attack = {
gain_action = function(self)
local owner = self.owner
owner.attackSpeedMult = owner.attackSpeedMult + ATTACK_SPEED_BONUS
end,
destroy_func = function(self)
local owner = self.owner
owner.attackSpeedMult = owner.attackSpeedMult - ATTACK_SPEED_BONUS
end,
}
end
do
local PICKUP_RANGE_BONUS = 0.8
longfinder_gloves_attack = {
gain_action = function(self)
local owner = self.owner
owner.pickupRangeMult = owner.pickupRangeMult + PICKUP_RANGE_BONUS
end,
destroy_func = function(self)
local owner = self.owner
owner.pickupRangeMult = owner.pickupRangeMult - PICKUP_RANGE_BONUS
end,
}
end
do
local EXP_BONUS = 0.25
gauntlets_of_accumulation_attack = {
gain_action = function(self)
local owner = self.owner
owner.expMult = owner.expMult + EXP_BONUS
end,
destroy_func = function(self)
local owner = self.owner
owner.expMult = owner.expMult - EXP_BONUS
end,
}
end
do
local MAX_BONUS_DAMAGE = 0.666
local MIN_BUILD_START_TIME = 2.0
local ACCUMULATE_TIME = 8.0
local function check(self)
local owner = self.owner
owner.damageMult = tonumber(string.format("%%0.3f",owner.damageMult - self.damageBonus))
self.damageBonus = 0.0
self.t = math.min(self.t + 0.1,ACCUMULATE_TIME)
local t = math.max(0.0,(self.t-MIN_BUILD_START_TIME)/(ACCUMULATE_TIME-MIN_BUILD_START_TIME))
self.damageBonus = t * MAX_BONUS_DAMAGE
owner.damageMult = owner.damageMult + self.damageBonus
self.timer:callDelayed(0.1, check,self)
end
local function hit(self)
self.t = 0.0
end
spellcaster_gloves_attack = {
gain_action = function(self)
self.t = 0.0
self.damageBonus = 0.0
local event = Attack.main_attack_fire:add_action(hit,self)
table.insert(self.event_actions,event)
self.timer:callDelayed(0.1, check,self)
end,
destroy_func = function(self)
local owner = self.owner
owner.damageMult = tonumber(string.format("%%0.3f",owner.damageMult - self.damageBonus))
self.t = nil
self.damageBonus = nil
end,
}
end
do
local function hit(self,whichAttack,target)
if whichAttack.damageType == DamageTypes.Magical and Types.is(target,Types.Effectable) and target:get_effect_stacks(EFFECT_TYPES.Electrify) > 0 then
chain_lightning(self,self.owner.position[1],self.owner.position[2],self:get_force(),NewKeyTable())
end
end
thunder_crown_attack = {
damage = 100.0,
area = 150.,
range = 425.,
effect_function = attack_effect_electrify,
effectChance = 0.25,
force = 4.0,
damageType = DamageTypes.Lightning,
gain_action = function(self)
local event = Attack.hit_event:add_action(hit,self)
table.insert(self.event_actions,event)
end,
}
end
do
local function hit_iter(u,owner,self)
if Types.is(u,Types.Effectable) then
u:slow(owner,1.0)
self:deal_damage(u)
end
end
local function check(self)
local area = self:get_area()
local half = area * 0.5
local owner = self.owner
local x,y = owner.position[1], owner.position[2]
x = x + half * math.cos(owner.yaw)
y = y + half * math.sin(owner.yaw)
self.timer:callDelayed(DEFAULT_TIMEOUT, check,self)
BlzSetSpecialEffectPosition(self.sfx,x,y,0)
if self:is_on_cooldown() then return end
self:start_cooldown()
unit_collision_hash:each(x - half, y - half, area, area, hit_iter,owner,self)
end
local function get_damage(self,target)
return Attack.get_damage(self) * target:get_effect_stacks(EFFECT_TYPES.Slow)
end
gorgon_mask_attack = {
damage = 10.,
area = 300.,
cooldown = 1.0,
gain_action = function(self)
self.get_damage = get_damage
self.sfx = AddSpecialEffect("IlluminaRing_green.mdx",0,0)
BlzSetSpecialEffectZ(self.sfx,99999)
self.timer:callDelayed(DEFAULT_TIMEOUT, check,self)
end,
update_func = function(self)
BlzSetSpecialEffectScale(self.sfx,self:get_area()/120)
end,
destroy_func = function(self)
DestroyEffect(self.sfx)
self.sfx = nil
self.get_damage = nil
end,
}
end
do
local DEFENSE_BONUS = 0.1
local HEALTH_BONUS = 0.2
hood_attack = {
gain_action = function(self)
local owner = self.owner
owner:set_healthMult(owner.healthMult + HEALTH_BONUS)
owner.defenseMult = owner.defenseMult + DEFENSE_BONUS
end,
destroy_func = function(self)
local owner = self.owner
owner:set_healthMult(owner.healthMult - HEALTH_BONUS)
owner.defenseMult = owner.defenseMult - DEFENSE_BONUS
end,
}
end
do
local DAMAGE = 30.0
local function hit(self,owner,wearableData)
local which = GetRandomInt(1,3)
local bonus = GetRandomReal(0.01,0.02)
if which == 1 then
owner.damageMult = tonumber(string.format("%%0.3f",owner.damageMult - wearableData.damageBonus))
wearableData.damageBonus = wearableData.damageBonus + bonus
owner.damageMult = owner.damageMult + wearableData.damageBonus
elseif which == 2 then
owner.attackSpeedMult = tonumber(string.format("%%0.3f",owner.attackSpeedMult - wearableData.attackspeedBonus))
wearableData.attackspeedBonus = wearableData.attackspeedBonus + bonus
owner.attackSpeedMult = owner.attackSpeedMult + wearableData.attackspeedBonus
else
owner.multistrike = tonumber(string.format("%%0.3f",owner.multistrike - wearableData.multistrikeBonus))
wearableData.multistrikeBonus = wearableData.multistrikeBonus + bonus
owner.multistrike = owner.multistrike + wearableData.multistrikeBonus
end
self.timer:callDelayed(self:start_cooldown(), hit,self,owner,wearableData)
SOUND_MASK_OF_MADNESS(GetRandomReal(0.9,1.1))
DestroyEffectEx(AddSpecialEffect("Objects\\Spawnmodels\\Human\\HumanLargeDeathExplode\\HumanLargeDeathExplode.mdl",owner.position[1],owner.position[2]))
owner:damage_self(DAMAGE)
end
mask_of_madness_attack = {
cooldown = 30.,
gain_action = function(self,data,wearable)
local owner = self.owner
local wearableData = wearable.data
self.wearableData = wearableData
wearableData.damageBonus = wearableData.damageBonus or 0.0
wearableData.attackspeedBonus = wearableData.attackspeedBonus or 0.0
wearableData.multistrikeBonus = wearableData.multistrikeBonus or 0.0
owner.damageMult = owner.damageMult + wearableData.damageBonus
owner.multistrike = owner.multistrike + wearableData.multistrikeBonus
owner.damageMult = owner.damageMult + wearableData.damageBonus
self.timer:callDelayed(self:start_cooldown(), hit,self,owner,wearableData)
end,
destroy_func = function(self)
local wearableData = self.wearableData
local owner = self.owner
owner.damageMult = tonumber(string.format("%%0.3f",owner.damageMult - wearableData.damageBonus))
owner.attackSpeedMult = tonumber(string.format("%%0.3f",owner.attackSpeedMult - wearableData.attackspeedBonus))
owner.multistrike = tonumber(string.format("%%0.3f",owner.multistrike - wearableData.multistrikeBonus))
self.wearableData = nil
end,
}
end
do
local function hit_iter(u,owner)
if Types.is(u,Types.Effectable) then
u:frail(owner,4.0)
end
end
local function check(self)
local area = self:get_area()
local half = area * 0.5
local owner = self.owner
local x,y = owner.position[1],owner.position[2]
unit_collision_hash:each(x - half, y - half, area, area, hit_iter,owner)
local sfx = AddSpecialEffect("Abilities\\Spells\\Other\\HowlOfTerror\\HowlCaster.mdl",x,y)
BlzSetSpecialEffectScale(sfx,area/512)
DestroyEffectEx(sfx)
self.timer:callDelayed(self:start_cooldown(), check,self)
end
war_horns_attack = {
area = 400.,
cooldown = 3.0,
attack_count = 1.0,
gain_action = function(self)
self.timer:callDelayed(self:start_cooldown(), check,self)
end,
}
end
do
local MAX_DAMAGE_BONUS = 0.6
local GAIN_PER_BURN = 0.04
local function hit(self)
local owner = self.owner
owner.damageMult = tonumber(string.format("%%0.3f",owner.damageMult - self.damageBonus))
self.damageBonus = math.min(GAIN_PER_BURN * GetEffectCountTotal(EFFECT_TYPES.Burn),MAX_DAMAGE_BONUS)
owner.damageMult = owner.damageMult + self.damageBonus
self.timer:callDelayed(0.1, hit,self)
end
ruby_circlet_attack = {
gain_action = function(self,data,wearable)
self.damageBonus = 0.0
self.timer:callDelayed(0.1, hit,self)
end,
destroy_func = function(self)
local owner = self.owner
owner.damageMult = tonumber(string.format("%%0.3f",owner.damageMult - self.damageBonus))
self.damageBonus = nil
end,
}
end
do
local function spawn(self,u)
if u.miniboss or u.boss then
local owner = self.owner
local x,y = owner.position[1],owner.position[2]
local dir = GetRandomReal(-PI,PI)
local range = GetRandomReal(200,400)
local x2,y2 = GetRandomLocationInRange(x+range*math.cos(dir),y+range*math.sin(dir),1024,64,Types.Collidable)
local maxHp = owner:get_max_health()
Item.new(item_food_potion,x2,y2)
if math.floor(owner.health) == math.floor(maxHp) then
dir = GetRandomReal(-PI,PI)
range = GetRandomReal(200,400)
x2,y2 = GetRandomLocationInRange(x+range*math.cos(dir),y+range*math.sin(dir),1024,64,Types.Collidable)
Item.new(item_powerups:random(),x2,y2)
end
owner:buff(
"baseHealthRegen",
"fighterHeadband",
"",
"chest",
maxHp*.01,
20.,
false,
true
)
end
end
fighters_headband_attack = {
gain_action = function(self)
local event = Unit.spawn_event:add_action(spawn,self)
table.insert(self.event_actions,event)
end,
}
end
do
local DURATION = 10.0
local BONUS_PER_KILL = 0.003
local MAX_BONUS = 0.3
local function kill(self,u)
self.lastKill = GAME_TIME + DURATION
local owner = self.owner
owner.attackSpeedMult = tonumber(string.format("%%0.3f",owner.attackSpeedMult - self.speedBonus))
self.speedBonus = math.min(MAX_BONUS,self.speedBonus + BONUS_PER_KILL)
owner.attackSpeedMult = owner.attackSpeedMult + self.speedBonus
end
local function hit(self)
if self.lastKill < GAME_TIME then
local owner = self.owner
owner.attackSpeedMult = tonumber(string.format("%%0.3f",owner.attackSpeedMult - self.speedBonus))
self.speedBonus = 0.0
end
self.timer:callDelayed(1.0, hit,self)
end
wind_crown_attack = {
gain_action = function(self)
self.timer:callDelayed(1.0, hit,self)
self.lastKill = 0.0
self.speedBonus = 0.0
local event = Unit.kill_event:add_action(kill,self)
table.insert(self.event_actions,event)
end,
destroy_func = function(self)
local owner = self.owner
owner.attackSpeedMult = tonumber(string.format("%%0.3f",owner.attackSpeedMult - self.speedBonus))
self.lastKill = nil
self.speedBonus = nil
end,
}
end
do
local BLOCK_BONUS = 0.15
local HEALTH_BONUS = 0.15
helmet_attack = {
gain_action = function(self)
local owner = self.owner
owner:set_healthMult(owner.healthMult + HEALTH_BONUS)
owner.blockMult = owner.blockMult + BLOCK_BONUS
end,
destroy_func = function(self)
local owner = self.owner
owner:set_healthMult(owner.healthMult - HEALTH_BONUS)
owner.blockMult = owner.blockMult - BLOCK_BONUS
end,
}
end
do
local MOVE_BONUS = 0.1
gatherers_charm_attack = {
gain_action = function(self)
local owner = self.owner
owner.moveSpeedMult = owner.moveSpeedMult + MOVE_BONUS
end,
destroy_func = function(self)
local owner = self.owner
owner.moveSpeedMult = owner.moveSpeedMult - MOVE_BONUS
end,
}
end
do
local DURATION = 14.0
local DAMAGE_BONUS = 1.0
local ATTACK_SPEED_BONUS = 1.0
local function check_end(self)
print(self.lastGain,GAME_TIME)
if self.hasBonus then
local owner = self.owner
self.hasBonus = false
owner.attackSpeedMult = tonumber(string.format("%%0.3f",owner.attackSpeedMult - ATTACK_SPEED_BONUS))
owner.damageMult = tonumber(string.format("%%0.3f",owner.damageMult - DAMAGE_BONUS))
end
end
local function healed(self)
if not self.hasBonus then
local dur = self:get_force()
self.lastGain = GAME_TIME + dur
local owner = self.owner
self.hasBonus = true
owner.attackSpeedMult = owner.attackSpeedMult + ATTACK_SPEED_BONUS
owner.damageMult = owner.damageMult + DAMAGE_BONUS
self.timer:callDelayed(dur+1.0, check_end,self)
end
end
local function gain_item(self,playerUnit,it)
if it.itemType == item_food_potion then
healed(self)
end
end
warriors_fervour_attack = {
force = 14.0,
gain_action = function(self)
self.lastGain = 0.0
self.hasBonus = false
local event = Item.gain_event:add_action(gain_item,self)
table.insert(self.event_actions,event)
event = PlayerUnit.heal_event:add_action(healed,self)
table.insert(self.event_actions,event)
end,
destroy_func = function(self)
local owner = self.owner
if self.hasBonus then
owner.attackSpeedMult = tonumber(string.format("%%0.3f",owner.attackSpeedMult - ATTACK_SPEED_BONUS))
owner.damageMult = tonumber(string.format("%%0.3f",owner.damageMult - DAMAGE_BONUS))
end
self.hasBonus = nil
self.lastGain = nil
end,
}
end
do
local DAMAGE_BONUS = 0.3
local FORCE_BONUS = 0.3
local function check(self)
if not self.charged then
local owner = self.owner
owner.forceMult = owner.forceMult + FORCE_BONUS
owner.damageMult = owner.damageMult + DAMAGE_BONUS
self.charged = true
end
self.timer:callDelayed(self:get_attack_speed()/self:get_attack_count(), check,self)
end
local function hit(self,playerUnit,amount,source)
if self.charged then
self.charged = false
playerUnit.health = playerUnit.health + amount
playerUnit:update_health()
playerUnit.forceMult = tonumber(string.format("%%0.3f",playerUnit.forceMult - FORCE_BONUS))
playerUnit.damageMult = tonumber(string.format("%%0.3f",playerUnit.damageMult - DAMAGE_BONUS))
self.timer:reset()
self.timer:callDelayed(self:start_cooldown(), check,self)
end
end
maidens_tear_attack = {
cooldown = 30.,
attack_count = 1.0,
gain_action = function(self)
self.charged = false
local event = PlayerUnit.damage_event:add_action(hit,self)
table.insert(self.event_actions,event)
self.timer:callDelayed(self:start_cooldown(), check,self)
end,
destroy_func = function(self)
local owner = self.owner
if self.charged then
owner.forceMult = tonumber(string.format("%%0.3f",owner.forceMult - FORCE_BONUS))
owner.damageMult = tonumber(string.format("%%0.3f",owner.damageMult - DAMAGE_BONUS))
end
self.charged = nil
end,
}
end
do
local COOLDOWN_GAIN = 6.0
local DAMAGE_GAIN_PER_TICK = 0.05
local REGEN_PER_SUMMON = 0.1
local function check(self)
local owner = self.owner
owner.baseHealthRegen = tonumber(string.format("%%0.3f",owner.baseHealthRegen - self.bonusRegen))
self.bonusRegen = owner.summon_count * REGEN_PER_SUMMON
owner.baseHealthRegen = owner.baseHealthRegen + self.bonusRegen
self.timer:callDelayed(0.1, check,self)
end
local function tick(self,wearableData)
local owner = self.owner
if math.floor(owner.health) == math.floor(owner:get_max_health()) then
self.cooldown = self.cooldown + COOLDOWN_GAIN
owner.summonDamageMult = tonumber(string.format("%%0.3f",owner.summonDamageMult - wearableData.damageBonus))
wearableData.damageBonus = wearableData.damageBonus + DAMAGE_GAIN_PER_TICK
owner.summonDamageMult = owner.summonDamageMult + wearableData.damageBonus
end
self.timer:callDelayed(self:start_cooldown(), tick,self,wearableData)
end
sheperds_boon_attack = {
attack_count = 1.0,
cooldown = 30.0,
gain_action = function(self,data,wearable)
local owner = self.owner
local wearableData = wearable.data
self.wearableData = wearableData
self.bonusRegen = 0.0
wearableData.damageBonus = wearableData.damageBonus or 0.0
wearableData.cooldown = wearableData.cooldown or 30.0
self.cooldown = wearableData.cooldown
owner.summonDamageMult = owner.summonDamageMult + wearableData.damageBonus
self.timer:callDelayed(0.1, check,self)
self.timer:callDelayed(self:start_cooldown(), tick,self,wearableData)
end,
destroy_func = function(self)
local owner = self.owner
local wearableData = self.wearableData
owner.summonDamageMult = tonumber(string.format("%%0.3f",owner.summonDamageMult - wearableData.damageBonus))
owner.baseHealthRegen = tonumber(string.format("%%0.3f",owner.baseHealthRegen - self.bonusRegen))
wearableData.cooldown = self.cooldown
self.wearableData = nil
self.bonusRegen = nil
end,
}
end
do
local LIFE_PER_BONUS = 1.0
local DAMAGE_PER_BONUS = 0.001
local function check(self)
local owner = self.owner
owner.damageMult = tonumber(string.format("%%0.3f",owner.damageMult - self.bonusDamage))
local missing = owner:get_max_health() - owner.health
self.bonusDamage = (missing / LIFE_PER_BONUS) * DAMAGE_PER_BONUS
owner.damageMult = owner.damageMult + self.bonusDamage
self.timer:callDelayed(0.1, check,self)
end
scars_of_toil_attack = {
gain_action = function(self)
local owner = self.owner
self.bonusDamage = 0.0
self.timer:callDelayed(0.1, check,self)
end,
destroy_func = function(self)
local owner = self.owner
owner.damageMult = tonumber(string.format("%%0.3f",owner.damageMult - self.bonusDamage))
self.bonusDamage = nil
end,
}
end
do
local MAX_LIFE_TRIGGER = 200.0
local TRIGGER_GAIN_PER = 1.0
local DAMAGE_GAIN_PER = 0.005
local HEAL_AMOUNT = 10.0
local MAX_DAMAGE_BONUS = 0.5
local function check(self,wearableData,atk,target,dmg)
local owner = self.owner
if atk.owner == owner then
wearableData.dmgDealt = wearableData.dmgDealt + dmg
local max = owner:get_max_health()
local threshold = wearableData.lifeTriggerPercent * max
while wearableData.dmgDealt > threshold do
wearableData.dmgDealt = wearableData.dmgDealt - threshold
wearableData.lifeTriggerPercent = wearableData.lifeTriggerPercent + TRIGGER_GAIN_PER
threshold = wearableData.lifeTriggerPercent * max
if max <= owner.health then
owner.damageMult = tonumber(string.format("%%0.3f",owner.damageMult - wearableData.bonusDmg))
wearableData.bonusDmg = math.min(wearableData.bonusDmg + DAMAGE_GAIN_PER,MAX_DAMAGE_BONUS)
owner.damageMult = owner.damageMult + wearableData.bonusDmg
else
owner:heal(HEAL_AMOUNT)
end
end
end
end
blood_catcher_attack = {
gain_action = function(self,data,wearable)
local owner = self.owner
local wearableData = wearable.data
self.wearableData = wearableData
wearableData.dmgDealt = wearableData.dmgDealt or 0.0
wearableData.bonusDmg = wearableData.bonusDmg or 0.0
wearableData.lifeTriggerPercent = wearableData.lifeTriggerPercent or MAX_LIFE_TRIGGER
owner.damageMult = owner.damageMult + wearableData.bonusDmg
local event = Attack.hit_event:add_action(check,self,wearableData)
table.insert(self.event_actions,event)
end,
destroy_func = function(self)
local owner = self.owner
local wearableData = self.wearableData
owner.damageMult = tonumber(string.format("%%0.3f",owner.damageMult - wearableData.bonusDmg))
self.wearableData = nil
end,
}
end
do
local PROC_CHANCE = 0.02
local function check(self,atk,target,dmg)
local owner = self.owner
if atk.owner == owner and owner.attacks[1] == atk and Types.is(target,Types.Effectable) and GetRandomReal(0.0,1.0) <= PROC_CHANCE then
if target.effects_sfx[EFFECT_TYPES.Frost] then
target:frost(owner,1.0)
end
if target.effects_sfx[EFFECT_TYPES.Burn] then
target:burn(owner,1.0)
end
if target.effects_sfx[EFFECT_TYPES.Electrify] then
target:electrify(owner,1.0)
end
end
end
elemental_resonator_attack = {
gain_action = function(self,data,wearable)
local owner = self.owner
local event = Attack.hit_event:add_action(check,self)
table.insert(self.event_actions,event)
end,
}
end
do
local MAX_DAMAGE_BONUS = 0.75
local DAMAGE_PER_ENEMY = 0.03
local function check(self)
local owner = self.owner
owner.damageMult = tonumber(string.format("%%0.3f",owner.damageMult - self.dmgBonus))
local range = owner:get_pickup_range()
local half = range * 0.5
local count = unit_collision_hash:count_of_type(owner.position[1]-half,owner.position[2]-half,range,range,Types.Effectable)
self.dmgBonus = math.min(count * DAMAGE_PER_ENEMY,MAX_DAMAGE_BONUS)
owner.damageMult = owner.damageMult + self.dmgBonus
self.timer:callDelayed(0.1, check,self)
end
collar_of_confidence_attack = {
gain_action = function(self)
self.dmgBonus = 0.0
self.timer:callDelayed(0.1, check,self)
end,
destroy_func = function(self)
local owner = self.owner
owner.damageMult = tonumber(string.format("%%0.3f",owner.damageMult - self.dmgBonus))
self.dmgBonus = nil
end,
}
end
do
local MIN_XP_BONUS = 0.0
local MAX_XP_BONUS = 0.3
local LOSS_PER_LEVEL = 0.005
local function check(self)
local owner = self.owner
owner.expMult = tonumber(string.format("%%0.3f",owner.expMult - self.expBonus))
self.expBonus = math.max(MAX_XP_BONUS-owner.level * LOSS_PER_LEVEL,MIN_XP_BONUS)
owner.expMult = owner.expMult + self.expBonus
self.timer:callDelayed(1.0, check,self)
end
jade_amulet_attack = {
gain_action = function(self)
self.expBonus = 0.0
self.timer:callDelayed(1.0, check,self)
end,
destroy_func = function(self)
local owner = self.owner
owner.expMult = tonumber(string.format("%%0.3f",owner.expMult - self.expBonus))
self.expBonus = nil
end,
}
end
do
local MAX_DAMAGE_BONUS = 0.5
local DAMAGE_DEC_PER_ENEMY = 0.05
local function check(self)
local owner = self.owner
owner.damageMult = tonumber(string.format("%%0.3f",owner.damageMult - self.dmgBonus))
local range = owner:get_pickup_range()
local half = range * 0.5
local count = unit_collision_hash:count_of_type(owner.position[1]-half,owner.position[2]-half,range,range,Types.Effectable)
self.dmgBonus = math.max(MAX_DAMAGE_BONUS - count * DAMAGE_DEC_PER_ENEMY,0.0)
owner.damageMult = owner.damageMult + self.dmgBonus
self.timer:callDelayed(0.1, check,self)
end
duellists_spark_attack = {
gain_action = function(self)
self.dmgBonus = 0.0
self.timer:callDelayed(0.1, check,self)
end,
destroy_func = function(self)
local owner = self.owner
owner.damageMult = tonumber(string.format("%%0.3f",owner.damageMult - self.dmgBonus))
self.dmgBonus = nil
end,
}
end
do
local CHANCE_BONUS = 0.15
local function check(self,atk,target,dmg)
if Types.is(target,Types.Effectable) then
target:frost(self.owner,CHANCE_BONUS)
end
end
ring_of_frost_attack = {
gain_action = function(self)
local event = Attack.hit_event:add_action(check,self)
table.insert(self.event_actions,event)
end,
}
end
do
local CRIT_BONUS = 0.45
copper_ring_attack = {
gain_action = function(self)
local owner = self.owner
owner.critBonus = owner.critBonus + CRIT_BONUS
end,
destroy_func = function(self)
local owner = self.owner
owner.critBonus = owner.critBonus - CRIT_BONUS
end,
}
end
do
local CHANCE_BONUS = 0.15
local function check(self,atk,target,dmg)
if Types.is(target,Types.Effectable) then
target:electrify(self.owner,CHANCE_BONUS)
end
end
ring_of_thunder_attack = {
gain_action = function(self)
local event = Attack.hit_event:add_action(check,self)
table.insert(self.event_actions,event)
end,
}
end
do
local MAX_BONUS_REGEN = 0.01
local MAX_BONUS_ATTACK_SPEED = 0.3
local MIN_BONUS_ATTACK_SPEED = -0.2
local MAX_BONUS_MOVE_SPEED = 0.3
local MIN_BONUS_MOVE_SPEED = -0.2
local function check(self)
local owner = self.owner
owner.moveSpeedMult = tonumber(string.format("%%0.3f",owner.moveSpeedMult - self.moveSpeedBonus))
owner.attackSpeedMult = tonumber(string.format("%%0.3f",owner.attackSpeedMult - self.attackSpeedBonus))
self.attackSpeedBonus = 0
self.moveSpeedBonus = 0
if owner.attacking > GAME_TIME then
self.attackSpeedBonus = self.attackSpeedBonus + MAX_BONUS_ATTACK_SPEED
self.moveSpeedBonus = self.moveSpeedBonus + MIN_BONUS_MOVE_SPEED
end
if owner.moving then
self.attackSpeedBonus = self.attackSpeedBonus + MIN_BONUS_ATTACK_SPEED
self.moveSpeedBonus = self.moveSpeedBonus + MAX_BONUS_MOVE_SPEED
end
owner.attackSpeedMult = owner.attackSpeedMult + self.attackSpeedBonus
owner.moveSpeedMult = owner.moveSpeedMult + self.moveSpeedBonus
self.timer:callDelayed(0.1, check,self)
end
guiding_star_attack = {
gain_action = function(self)
self.attackSpeedBonus = 0.0
self.moveSpeedBonus = 0.0
self.timer:callDelayed(0.1, check,self)
end,
destroy_func = function(self)
local owner = self.owner
owner.moveSpeedMult = tonumber(string.format("%%0.3f",owner.moveSpeedMult - self.moveSpeedBonus))
owner.attackSpeedMult = tonumber(string.format("%%0.3f",owner.attackSpeedMult - self.attackSpeedBonus))
self.attackSpeedBonus = nil
self.moveSpeedBonus = nil
end,
}
end
do
local function fire(self,wearable,playerUnit)
if self.owner == playerUnit and playerUnit.health <= 0 then
local max = playerUnit:get_max_health()
playerUnit:heal(max * 0.5)
DestroyEffect(AddSpecialEffectTarget("Abilities\\Spells\\Human\\ReviveHuman\\ReviveHuman.mdl",playerUnit.sfx,"origin"))
self.timer:callDelayed(0.0,wearable.destroy,wearable)
end
end
seal_of_rebirth_attack = {
gain_action = function(self,data,wearable)
local event = PlayerUnit.about_to_die:add_action(fire,self,wearable)
table.insert(self.event_actions,event)
self.wearable = wearable
end,
destroy_func = function(self)
self.wearable = nil
end,
}
end
do
local function dmg_iter(u,attack,dmg)
attack:basic_damage(u,dmg)
end
local function hit(self,whichAttack,target,actual_dmg,x,y)
if whichAttack.damageType == DamageTypes.Physical and GetRandomReal(0.0,1.0) <= 0.2 then
local area = self:get_area()
local half = area * 0.5
local dmg = actual_dmg * 0.4
local sfx = AddSpecialEffect("NovaModel.mdx",x-SFX_OFFSET,y-SFX_OFFSET)
BlzSetSpecialEffectScale(sfx,area/128)
BlzSetSpecialEffectTimeScale(sfx,3.0)
DestroyEffect(sfx)
unit_collision_hash:each(x - half, y - half, area, area, dmg_iter,self,dmg)
end
end
echoing_band_attack = {
area = 250.,
damageType = DamageTypes.Magical,
properties = AttackProperties.Area,
gain_action = function(self)
local event = Attack.hit_event:add_action(hit,self)
table.insert(self.event_actions,event)
end,
}
end
do
local BONUS_DAMAGE = 10.0
iron_ring_attack = {
gain_action = function(self)
local owner = self.owner
owner.baseDamageBonus = owner.baseDamageBonus + BONUS_DAMAGE
end,
destroy_func = function(self)
local owner = self.owner
owner.baseDamageBonus = owner.baseDamageBonus - BONUS_DAMAGE
end,
}
end
do
local CHANCE_BONUS = 0.15
local function check(self,atk,target,dmg)
if Types.is(target,Types.Effectable) then
target:burn(self.owner,CHANCE_BONUS)
end
end
ring_of_fire_attack = {
gain_action = function(self)
local event = Attack.hit_event:add_action(check,self)
table.insert(self.event_actions,event)
end,
}
end
do
local function spawn(self)
local owner = self.owner
if self.rats:size() < self:get_summon_count() then
local s = Summon.new(summon_rat,owner.position[1],owner.position[2],owner,self)
BlzSetSpecialEffectColor(s.sfx,100,255,100)
BlzSetSpecialEffectAlpha(s.sfx,150)
s.attacks_before_death = MathRound(self:get_force())
self.rats:add(s)
end
self.timer:callDelayed(self:start_cooldown(), spawn,self)
end
local function died(self,summon)
if self.rats then
self.rats:remove(summon)
end
end
pest_ring_attack = {
summon_count = 5.0,
cooldown = 1.0,
force = 5.0,
damage = 20.0,
attack_count = 1.0,
effectChance = 2.0,
gain_action = function(self)
self.effect_function = attack_effect_pest
self.rats = Set.create()
self.timer:callDelayed(self:start_cooldown(), spawn,self)
local event = Summon.kill_event:add_action(died,self)
table.insert(self.event_actions,event)
end,
destroy_func = function(self)
for s in self.rats:elements() do
s:destroy()
end
self.rats:destroy()
self.rats = nil
end,
}
end
do
local function spawn(self)
local owner = self.owner
if self.skeles:size() < self:get_summon_count() then
local s
if GetRandomInt(1,3) == 1 then
s = Summon.new(summon_skeleton_mage,owner.position[1],owner.position[2],owner,self)
else
s = Summon.new(summon_skeleton_warrior,owner.position[1],owner.position[2],owner,self)
end
s.timedLife = self:get_force() + GAME_TIME
BlzSetSpecialEffectColor(s.sfx,100,255,100)
BlzSetSpecialEffectAlpha(s.sfx,150)
self.skeles:add(s)
end
self.timer:callDelayed(self:start_cooldown(), spawn,self)
end
local function died(self,summon)
if self.skeles then
self.skeles:remove(summon)
end
end
necromancers_clutch_attack = {
summon_count = 5.0,
cooldown = 15.0,
force = 50.0,
damage = 40.0,
attack_count = 1.0,
properties = AttackProperties.Summon,
gain_action = function(self)
self.skeles = Set.create()
self.timer:callDelayed(self:start_cooldown(), spawn,self)
local event = Summon.kill_event:add_action(died,self)
table.insert(self.event_actions,event)
end,
destroy_func = function(self)
for s in self.skeles:elements() do
s:destroy()
end
self.skeles:destroy()
self.skeles = nil
end,
}
end
do
local function spawn(self)
local owner = self.owner
local x,y = owner.position[1],owner.position[2]
local maxHp = owner:get_max_health()
if math.floor(owner.health) == math.floor(maxHp) then
local dir = GetRandomReal(-PI,PI)
local range = GetRandomReal(200,400)
local x2,y2 = GetRandomLocationInRange(x+range*math.cos(dir),y+range*math.sin(dir),1024,64,Types.Collidable)
Item.new(item_powerups:random(),x2,y2)
else
local dir = GetRandomReal(-PI,PI)
local range = GetRandomReal(200,400)
local x2,y2 = GetRandomLocationInRange(x+range*math.cos(dir),y+range*math.sin(dir),1024,64,Types.Collidable)
Item.new(item_health_ray,x2,y2)
end
self.timer:callDelayed(self:start_cooldown(), spawn,self)
end
holy_relic_attack = {
cooldown = 30.0,
gain_action = function(self)
self.timer:callDelayed(self:start_cooldown(), spawn,self)
end,
}
end
do
local function spawn(self)
local owner = self.owner
if self.imps:size() < self:get_summon_count() then
local s = Summon.new(summon_imp,owner.position[1],owner.position[2],owner,self)
s.timedLife = self:get_force() + GAME_TIME
BlzSetSpecialEffectColor(s.sfx,100,255,100)
BlzSetSpecialEffectAlpha(s.sfx,150)
self.imps:add(s)
end
self.timer:callDelayed(self:start_cooldown(), spawn,self)
end
local function died(self,summon)
if self.imps then
self.imps:remove(summon)
end
end
demonic_bond_attack = {
summon_count = 5.0,
cooldown = 5.0,
force = 50.0,
damage = 40.0,
attack_count = 1.0,
cone_angle = PI*0.6,
properties = AttackProperties.Summon,
gain_action = function(self)
self.imps = Set.create()
self.timer:callDelayed(self:start_cooldown(), spawn,self)
local event = Summon.kill_event:add_action(died,self)
table.insert(self.event_actions,event)
end,
destroy_func = function(self)
for s in self.imps:elements() do
s:destroy()
end
self.imps:destroy()
self.imps = nil
end,
}
end
do
local CRIT_BONUS = 0.15
wooden_ring_attack = {
gain_action = function(self)
local owner = self.owner
owner.baseCritChance = owner.baseCritChance + CRIT_BONUS
end,
destroy_func = function(self)
local owner = self.owner
owner.baseCritChance = owner.baseCritChance - CRIT_BONUS
end,
}
end
do
local function generatePerpendicularLinePoints(x, y, direction, distanceFromOrigin, count, radius)
local perpDirection = direction + HALF_PI
local baseX = x + distanceFromOrigin * math.cos(direction)
local baseY = y + distanceFromOrigin * math.sin(direction)
local c = math.cos(perpDirection)
local s = math.sin(perpDirection)
local lineLength = (count - 1) * radius
local points = NewTable()
local i = 0
local half = radius * 0.5
while i < count do
local offset = (i - (count - 1) / 2) * radius
local x2 = baseX + offset * c
local y2 = baseY + offset * s
i = i + 1
if unit_collision_hash:cell_occupied(x2 - half, y2 - half, radius, radius,Types.Collidable) then
count = count + 1
else
table.insert(points, Vector(x2,y2))
end
end
return points
end
local function spawn(self)
local owner = self.owner
local mouse_pos = owner.pdata.mouse_pos
local dist = math.min(DistanceTo(owner.position[1],owner.position[2],mouse_pos[1],mouse_pos[2]),self:get_range())
local points = generatePerpendicularLinePoints(owner.position[1],owner.position[2],owner.yaw,dist,self:get_summon_count(),summon_zombie_wall.size)
local life = self:get_force() + GAME_TIME
local half = summon_zombie_wall.size * 0.5
local health = (self.owner:get_max_health() / crown_of_decay_attack.force) * self:get_force()
for i, v in ipairs(points) do
local s = Summon.new(summon_zombie_wall,v.x,v.y,owner,self)
s.health = health
s.timedLife = life
BlzSetSpecialEffectColor(s.sfx,100,255,100)
BlzSetSpecialEffectAlpha(s.sfx,150)
self.imps:add(s)
ReleaseVector(v)
points[i] = nil
end
ReleaseTable(points)
self.timer:callDelayed(self:start_cooldown(), spawn,self)
end
local function died(self,summon)
if self.imps then
self.imps:remove(summon)
end
end
crown_of_decay_attack = {
summon_count = 6.0,
cooldown = 10.0,
force = 7.0,
damage = 10.0,
range = 256.0,
attack_count = 1.0,
cone_angle = PI*0.6,
properties = AttackProperties.Summon,
gain_action = function(self)
self.imps = Set.create()
self.timer:callDelayed(self:start_cooldown(), spawn,self)
local event = Summon.kill_event:add_action(died,self)
table.insert(self.event_actions,event)
end,
destroy_func = function(self)
for s in self.imps:elements() do
s:destroy()
end
self.imps:destroy()
self.imps = nil
end,
}
end
function InitAbilties()
AstronomerOrbInit()
LightningStrikesInit()
PhantomNeedlesInit()
ArcaneSplintersInit()
RadiantAuraInit()
RingBladesInit()
DragonsBreathInit()
FrostAvalancheInit()
MeteorStrikeInit()
DancingDaggersInit()
FlameStrikeInit()
SummonGolemInit()
TransfixionInit()
KugelblitzInit()
HailstormInit()
MorningStarInit()
SpiritWarriorInit()
ArcaneRiftInit()
InfestationInit()
end
function AddAbilitiesToPool()
AbilityPool:clear()
AbilityUpgradePool:clear()
ability_astronomers_orbs:add_to_pool()
ability_lightningstrikes:add_to_pool()
ability_phantomneedles:add_to_pool()
ability_arcanesplinters:add_to_pool()
ability_radiantaura:add_to_pool()
ability_ringblades:add_to_pool()
ability_dragonsbreath:add_to_pool()
ability_frostavalanche:add_to_pool()
ability_meteorstrike:add_to_pool()
ability_dancingdaggers:add_to_pool()
ability_flamestrike:add_to_pool()
ability_summongolem:add_to_pool()
ability_transfixion:add_to_pool()
ability_kugelblitz:add_to_pool()
ability_hailstorm:add_to_pool()
ability_morningstar:add_to_pool()
ability_spiritwarrior:add_to_pool()
ability_arcanerift:add_to_pool()
ability_infestation:add_to_pool()
end
do
function attack_click_event(attack)
local event = Mouse.press_event:add_action(Attack.start,attack)
table.insert(attack.event_actions,event)
end
function attack_update_blank()
end
function attack_effect_blank()
end
function attack_effect_frost_electrify(self,attack,chance)
local val = self:electrify(attack.owner,chance)
if self.type then
val = val + self:frost(attack.owner,chance)
end
return val
end
function attack_effect_frost_burn(self,attack,chance)
local val = self:burn(attack.owner,chance)
if self.type then
val = val + self:frost(attack.owner,chance)
end
return val
end
function attack_effect_electrify(self,attack,chance)
return self:electrify(attack.owner,chance)
end
function attack_effect_burn(self,attack,chance)
return self:burn(attack.owner,chance)
end
function attack_effect_frost(self,attack,chance)
return self:frost(attack.owner,chance)
end
function attack_effect_slow(self,attack,chance)
return self:slow(attack.owner,chance)
end
function attack_effect_fragile(self,attack,chance)
return self:frail(attack.owner,chance)
end
function attack_effect_fragile_afflict(self,attack,chance)
return self:frail(attack.owner,chance) + self:afflict(attack.owner,chance)
end
function attack_effect_pest(self,attack,chance)
local which = GetRandomInt(1,3)
if which == 1 then
self:frail(attack.owner,chance)
elseif which == 2 then
self:slow(attack.owner,chance)
else
self:afflict(attack.owner,chance)
end
end
default_attack = {
cooldown = 0.75,
area = 396.0,
damage = 80.0,
range = 396.0,
delay = 0.35,
cone_angle = PI*0.25,
main_attack = true,
type = AttackTypes.Cone,
gain_action = attack_click_event,
}
end
function InitTraits()
local function elemental_surge_update_abil(damageType,effect_func_primary,effect_func_default,abil)
if abil.summon_count and abil.horde_summon == nil then
local prev = abil.effect_function
abil.damageType = damageType
if prev then
abil.effect_function = function(self,attack,chance)
prev(self,attack,chance)
self:effect_func_primary(attack.owner,0.05)
end
else
abil.effect_function = effect_func_default
end
end
end
trait_elemental_surge = Trait.new({
multi_trait = 3,
name = "Elemental Surge",
subname1 = "Fire",
subname2 = "Lightning",
subname3 = "Ice",
descriptions1 = "|cff00ff00Transforms damage of summons to fire.|r|n|cffff0000Gives a 5%% chance to burn on hit.|r|nHorde Summons are excluded.",
descriptions2 = "|cff00ff00Transforms damage of summons to lightning.|r|n|cffff0000Gives a 5%% chance to electrify on hit.|r|nHorde Summons are excluded.",
descriptions3 = "|cff00ff00Transforms damage of summons to ice.|r|n|cffff0000Gives a 5%% chance to frost on hit.|r|nHorde Summons are excluded.",
icon = {"TraitElementalSurgeFire.dds","TraitElementalSurgeLightning.dds","TraitElementalSurgeIce.dds"},
actions1 = function(trait,playerUnit)
local event = Attack.gain_event:add_action(elemental_surge_update_abil,DamageTypes.Fire,Unit.burn,attack_effect_burn)
table.insert(playerUnit.event_actions,event)
for i, abil in ipairs(playerUnit.attacks) do
elemental_surge_update_abil(DamageTypes.Fire,Unit.burn,attack_effect_burn,abil)
end
end,
actions2 = function(trait,playerUnit)
local event = Attack.gain_event:add_action(elemental_surge_update_abil,DamageTypes.Lightning,Unit.electrify,attack_effect_electrify)
table.insert(playerUnit.event_actions,event)
for i, abil in ipairs(playerUnit.attacks) do
elemental_surge_update_abil(DamageTypes.Lightning,Unit.electrify,attack_effect_electrify,abil)
end
end,
actions3 = function(trait,playerUnit)
local event = Attack.gain_event:add_action(elemental_surge_update_abil,DamageTypes.Ice,Unit.frost,attack_effect_frost)
table.insert(playerUnit.event_actions,event)
for i, abil in ipairs(playerUnit.attacks) do
elemental_surge_update_abil(DamageTypes.Ice,Unit.frost,attack_effect_frost,abil)
end
end,
level_max = 1,
level_func = TraitLevelFunction_Thirty,
})
trait_demonic_exchange = Trait.new({
multi_trait = 3,
name = "Demonic Exchange",
subname1 = "Damage",
subname2 = "Force",
subname3 = "Speed",
descriptions1 = "|cff00ff00+30%% Damage (Summon)|r|n|cffff0000-3 Base Defense|r",
descriptions2 = "|cff00ff00+15%% Force (Summon)|n+15%% Spawns (Summon)|r|n|cffff0000-10%% Health|r",
descriptions3 = "|cff00ff00+14%% Attack Speed (Summon)|n+18%% Movement Speed (Summon)|r|n|cffff0000-6%% Movement Speed|r",
icon = "TraitDemonicExchange.dds",
actions1 = function(trait,playerUnit)
playerUnit.defense = playerUnit.defense - 3
playerUnit.summonDamageMult = playerUnit.summonDamageMult + 0.3
end,
actions2 = function(trait,playerUnit)
playerUnit:set_healthMult(playerUnit.healthMult - 0.1)
playerUnit.summonForceMult = playerUnit.summonForceMult + 0.15
playerUnit.summonCountMult = playerUnit.summonCountMult + 0.15
end,
actions3 = function(trait,playerUnit)
playerUnit.moveSpeedMult = playerUnit.moveSpeedMult - 0.06
playerUnit.summonAttackSpeedMult = playerUnit.summonAttackSpeedMult + 0.14
playerUnit.summonMoveSpeedMult = playerUnit.summonMoveSpeedMult + 0.18
end,
level_max = 3,
level_func = TraitLevelFunction_Ten_Twenty_Thirtyfive,
})
trait_crippling_strikes = Trait.new({
name = "Crippling Strikes",
description = "|cff00ff00+15%% Slow Chance|r",
icon = "TraitCripplingStrikes.dds",
action = function(trait,playerUnit)
playerUnit.slowBonus = playerUnit.slowBonus + 0.15
end,
level_max = 3,
level_func = TraitLevelFunction_Ten_Twenty_Thirtyfive,
})
trait_false_blessing = Trait.new({
name = "False Blessing",
description = "|cff00ff00+5%% Fragile Chance|n+5%% Affliction Chance|r",
icon = "TraitFalseBlessing.dds",
action = function(trait,playerUnit)
playerUnit.afflictBonus = playerUnit.afflictBonus + 0.5
playerUnit.frailBonus = playerUnit.frailBonus + 0.5
end,
level_max = 3,
level_func = TraitLevelFunction_Ten_Twenty_Thirtyfive,
})
trait_lightning_affinity = Trait.new({
name = "Lightning Affinity",
description = "|cff00ff00+5%% Crit Chance (Lightning)|n+5%% Electrify Damage|n+2%% Electrify Chance|r",
icon = "TraitLightningAffinity.dds",
action = function(trait,playerUnit)
playerUnit.critMultTable[DamageTypes.Lightning] = playerUnit.critMultTable[DamageTypes.Lightning] + 0.05
playerUnit.damageMultTable[DamageTypes.Electrify] = playerUnit.damageMultTable[DamageTypes.Electrify] + 0.05
playerUnit.electricBonus = playerUnit.electricBonus + 0.02
end,
level_max = 3,
level_func = TraitLevelFunction_Ten_Twenty_Thirtyfive,
})
trait_frost_affinity = Trait.new({
name = "Frost Affinity",
description = "|cff00ff00+5%% Damage (Ice)|n+5%% Frost Damage|n+2%% Frost Chance|r",
icon = "TraitFrostAffinity.dds",
action = function(trait,playerUnit)
playerUnit.damageMultTable[DamageTypes.Ice] = playerUnit.damageMultTable[DamageTypes.Ice] + 0.05
playerUnit.damageMultTable[DamageTypes.Frost] = playerUnit.damageMultTable[DamageTypes.Frost] + 0.05
playerUnit.frostBonus = playerUnit.frostBonus + 0.02
end,
level_max = 3,
level_func = TraitLevelFunction_Ten_Twenty_Thirtyfive,
})
trait_fire_affinity = Trait.new({
name = "Fire Affinity",
description = "|cff00ff00+5%% Area (Fire)|n+5%% Burn Damage|n+2%% Burn Chance|r",
icon = "TraitFireAffinity.dds",
action = function(trait,playerUnit)
playerUnit.damageMultTable[DamageTypes.Burn] = playerUnit.damageMultTable[DamageTypes.Burn] + 0.05
playerUnit.areaMultTable[DamageTypes.Fire] = playerUnit.areaMultTable[DamageTypes.Fire] + 0.05
playerUnit.burnBonus = playerUnit.burnBonus + 0.02
end,
level_max = 3,
level_func = TraitLevelFunction_Ten_Twenty_Thirtyfive,
})
trait_bulwark = Trait.new({
name = "Bulwark",
description = "|cff00ff00+10%% Defense|n+10%% Block Strength|r",
icon = "TraitBulwark.dds",
action = function(trait,playerUnit)
playerUnit.defenseMult = playerUnit.defenseMult + 0.1
playerUnit.blockMult = playerUnit.blockMult + 0.1
end,
level_max = 3,
level_func = TraitLevelFunction_Five_Ten_Twenty,
})
trait_thick_hide = Trait.new({
name = "Thick Hide",
description = "|cff00ff00+3 Base Defense|r",
icon = "TraitThickHide.dds",
action = function(trait,playerUnit)
playerUnit.defense = playerUnit.defense + 3.0
end,
level_max = 5,
level_func = TraitLevelFunction_Default,
})
trait_force = Trait.new({
name = "Channeling",
description = "|cff00ff00+12%% Force|r",
icon = "TraitForce.dds",
action = function(trait,playerUnit)
playerUnit.forceMult = playerUnit.forceMult + 0.12
end,
level_max = 5,
level_func = TraitLevelFunction_Default,
})
trait_parry = Trait.new({
name = "Parry",
description = "|cff00ff00+3 Base Block Strength|r",
icon = "TraitParry.dds",
action = function(trait,playerUnit)
playerUnit.block = playerUnit.block + 3.0
end,
level_max = 5,
level_func = TraitLevelFunction_Default,
})
trait_ruthlessness = Trait.new({
name = "Ruthlessness",
description = "|cff00ff00+15%% Crit Damage|r",
icon = "TraitRuthlessness.dds",
action = function(trait,playerUnit)
playerUnit.critMult = playerUnit.critMult + 0.15
end,
level_max = 5,
level_func = TraitLevelFunction_Default,
})
trait_metabolism = Trait.new({
name = "Metabolism",
description = "|cff00ff00+0.15/s Base Regeneration|r",
icon = "TraitMetabolism.dds",
action = function(trait,playerUnit)
playerUnit.baseHealthRegen = playerUnit.baseHealthRegen + 0.15
end,
level_max = 5,
level_func = TraitLevelFunction_Default,
})
trait_multistrike = Trait.new({
name = "Multistrike",
description = "|cff00ff00+10%% Multistrike|r",
icon = "TraitMultistrike.dds",
action = function(trait,playerUnit)
playerUnit.multistrike = playerUnit.multistrike + 0.1
end,
level_max = 3,
level_func = TraitLevelFunction_Fifteen,
})
trait_range = Trait.new({
name = "Reach",
description = "|cff00ff00+10%% Range|r",
icon = "TraitRange.dds",
action = function(trait,playerUnit)
playerUnit.rangeMult = playerUnit.rangeMult + 0.1
end,
level_max = 5,
level_func = TraitLevelFunction_Default,
})
trait_long_fingers = Trait.new({
name = "Long Fingers",
description = "|cff00ff00+30%% Pickup Range|r",
icon = "TraitLongFingers.dds",
action = function(trait,playerUnit)
playerUnit.pickupRangeMult = playerUnit.pickupRangeMult + 0.3
end,
level_max = 5,
level_func = TraitLevelFunction_Default,
})
trait_area = Trait.new({
name = "Collateral Damage",
description = "|cff00ff00+10%% Area of Effect|r",
icon = "TraitArea.dds",
action = function(trait,playerUnit)
playerUnit.areaMult = playerUnit.areaMult + 0.1
end,
level_max = 5,
level_func = TraitLevelFunction_Default,
})
trait_quick_hands = Trait.new({
name = "Quick Hands",
description = "|cff00ff00+15%% Attack Speed|r",
icon = "TraitQuickHands.dds",
action = function(trait,playerUnit)
playerUnit.attackSpeedMult = playerUnit.attackSpeedMult + 0.15
end,
level_max = 5,
level_func = TraitLevelFunction_Default,
})
trait_reflexes = Trait.new({
name = "Swiftness",
description = "|cff00ff00+6%% Movement Speed|r",
icon = "TraitSwiftness.dds",
action = function(trait,playerUnit)
playerUnit.moveSpeedMult = playerUnit.moveSpeedMult + 0.06
end,
level_max = 5,
level_func = TraitLevelFunction_Default,
})
trait_strength = Trait.new({
name = "Strength",
description = "|cff00ff00+10%% Damage|r",
icon = "TraitStrength.dds",
action = function(trait,playerUnit)
playerUnit.damageMult = playerUnit.damageMult + 0.1
end,
level_max = 5,
level_func = TraitLevelFunction_Default,
})
trait_vitality = Trait.new({
name = "Vitality",
description = "|cff00ff00+50 Base Health|r",
icon = "TraitVitality.dds",
action = function(trait,playerUnit)
playerUnit:add_health(50)
end,
level_max = 5,
level_func = TraitLevelFunction_Default,
})
trait_cunning_technique = Trait.new({
name = "Cunning Technique",
description = "|cff00ff00+5%% Base Crit Chance|r",
icon = "TraitCriticalChance.dds",
action = function(trait,playerUnit)
playerUnit.baseCritChance = playerUnit.baseCritChance + 0.05
end,
level_max = 5,
level_func = TraitLevelFunction_Default,
})
end
function AddDefaultTraitsToPool()
TraitPool:clear()
MemoryTraitPool:clear()
trait_cunning_technique:add_to_pool()
trait_vitality:add_to_pool()
trait_strength:add_to_pool()
trait_reflexes:add_to_pool()
trait_quick_hands:add_to_pool()
trait_area:add_to_pool()
trait_long_fingers:add_to_pool()
trait_range:add_to_pool()
trait_multistrike:add_to_pool()
trait_metabolism:add_to_pool()
trait_ruthlessness:add_to_pool()
trait_parry:add_to_pool()
trait_force:add_to_pool()
trait_thick_hide:add_to_pool()
trait_bulwark:add_to_pool()
end
do
SOUND_ORB_COLLECT = Sound:new({path = "Sounds\\OrbCollect.mp3",duration = 0.261,volume = 60})
SOUND_UI_HOVER = Sound:new({path = "SoundUIHover.mp3",duration = 0.626,volume = 127})
SOUND_UI_PRESS = Sound:new({path = "SoundUIPress.mp3",duration = 2.22,volume = 127})
SOUND_UI_START_GAME = Sound:new({path = "SoundUIStart.mp3",duration = 2.194,volume = 127})
SOUND_UI_GAIN_SCROLL = Sound:new({path = "SoundUIGainScroll.mp3",duration = 2.507,volume = 127})
SOUND_UI_BOSS_UNIQUE = Sound:new({path = "SoundBossSpawn.mp3",duration = 9.012,volume = 127})
SOUND_UI_BOSS_RANDOM = Sound:new({path = "SoundBossSpawn1.mp3",duration = 3.996,volume = 127})
SOUND_DEATH = Sound:new({path = "SoundDeath.mp3",duration = 7.941,volume = 127})
SOUND_CHARGE = Sound:new({path = "Abilities\\Spells\\Human\\Defend\\DefendCaster",duration = 1.166,volume = 60,is3D = true})
SOUND_BURN_LOOP = Sound:new({path = "BurnLoop.flac", duration = 4.835, volume = 20, is3D = true, looping = true})
SOUND_SUMMON_GHOST = Sound:new({path = "Abilities\\Spells\\Human\\Slow\\SorceressCastAttack1", duration=0.834, volume=100, is3D=true})
SOUND_BARREL_DEATH = Sound:new({path = "WoodBoxBreak.mp3", duration=1.848, volume=40, is3D=true})
SOUND_POTION = Sound:new({path = "SoundPotion.mp3",duration = 1.128,volume = 100})
SOUND_FORCE_FIELD_LOOP =Sound:new({path = "Abilities\\Spells\\Human\\ManaFlare\\ManaFlareLoop", duration = 2.770, volume = 127, is3D = true, looping = true})
SOUND_CHAIN_LIGHTNING = Sound:new({path = "Abilities\\Spells\\Orc\\LightningShield\\LightningShieldTarget", duration = 3.877, volume = 127, is3D = true})
SOUND_MACE_SWING = Sound:new({path = "SoundMaceSwing.mp3", duration = 0.731, volume = 127, is3D = true, cooldown = 0.1})
SOUND_RIFT_IMPACT = Sound:new({path = "RiftImpact.mp3", duration = 0.888, volume = 127, is3D = true})
SOUND_DRAGON_BREATH = Sound:new({path = "DragonBreath.mp3", duration = 3.082, volume = 127, is3D = true})
SOUND_MAGNET = Sound:new({path = "Magnet.mp3", duration = 2.586, volume = 127, is3D = true})
SOUND_RADIANT_AURA = Sound:new({path = "RadiantAura1.mp3", duration = 1.332, volume = 127, is3D = true})
SOUND_NONE = Sound:new({path = "Units\\Critters\\DuneWorm\\DuneWormDeath1.flac",duration = 0.0, volume = 0, is3D = true})
SOUND_SPLINTER_SPAWN = Set.create(
Sound:new({path = "SoundArcaneSplinter1.mp3",duration = 0.731,volume = 127,is3D=true}),
Sound:new({path = "SoundArcaneSplinter2.mp3",duration = 0.992,volume = 127,is3D=true})
)
SOUND_PARASITE_SPAWN = Set.create(
Sound:new({path = "SoundInfestation1.mp3",duration = 3.813,volume = 20,is3D=true,cooldown = 0.1}),
Sound:new({path = "SoundInfestation2.mp3",duration = 3.082,volume = 20,is3D=true,cooldown = 0.1})
)
SOUND_TRANSFIXION = Set.create(
Sound:new({path = "Transfixion1.mp3",duration = 1.104,volume = 100,is3D=true}),
Sound:new({path = "Transfixion2.mp3",duration = 1.608,volume = 100,is3D=true}),
Sound:new({path = "Transfixion3.mp3",duration = 1.8,volume = 100,is3D=true}),
Sound:new({path = "Transfixion4.mp3",duration = 1.32,volume = 100,is3D=true})
)
SOUND_FLAME_THROWER = Set.create(
Sound:new({path = "FlameThrow1.mp3",duration = 1.645,volume = 100,is3D=true}),
Sound:new({path = "FlameThrow2.mp3",duration = 1.828,volume = 100,is3D=true}),
Sound:new({path = "FlameThrow3.mp3",duration = 1.645,volume = 100,is3D=true}),
Sound:new({path = "FlameThrow4.mp3",duration = 2.115,volume = 100,is3D=true}),
Sound:new({path = "FlameThrow5.mp3",duration = 2.011,volume = 100,is3D=true}),
Sound:new({path = "FlameThrow6.mp3",duration = 2.037,volume = 100,is3D=true}),
Sound:new({path = "FlameThrow7.mp3",duration = 2.612,volume = 100,is3D=true})
)
SOUND_EAT = Set.create(
Sound:new({path = "SoundEat1.mp3",duration = 0.522,volume = 127}),
Sound:new({path = "SoundEat2.mp3",duration = 0.391,volume = 127})
)
SOUND_ZAP = Set.create(
Sound:new({path = "ElectricZap1.mp3",duration = 0.548,volume = 127,is3D=true}),
Sound:new({path = "ElectricZap1.mp3",duration = 0.522,volume = 127,is3D=true}),
Sound:new({path = "ElectricZap1.mp3",duration = 0.653,volume = 127,is3D=true})
)
SOUND_CLERIC_ATTACK = Set.create(
Sound:new({path = "ClericAttack1.mp3",duration = 1.097,volume = 127,is3D=true}),
Sound:new({path = "ClericAttack2.mp3",duration = 1.724,volume = 127,is3D=true}),
Sound:new({path = "ClericAttack3.mp3",duration = 1.880,volume = 127,is3D=true})
)
SOUND_HIT = Set.create(
Sound:new({path = "Sound\\Units\\Combat\\MetalLightSliceFlesh1",duration = 0.597,volume = 60}),
Sound:new({path = "Sound\\Units\\Combat\\MetalLightSliceFlesh2",duration = 0.563,volume = 60}),
Sound:new({path = "Sound\\Units\\Combat\\MetalLightSliceFlesh3",duration = 0.489,volume = 60})
)
SOUND_SPEAR_SWING = Sound:new({path = "Abilities\\Weapons\\huntermissile\\HeadHunterMissileLaunch",duration = 0.292,volume = 60, is3D = true})
SOUND_BLOCK = Set.create(
Sound:new({path = "Sound\\Units\\Combat\\WoodLightBashMetal1",duration = 0.378,volume = 60,is3D = true}),
Sound:new({path = "Sound\\Units\\Combat\\WoodLightBashMetal2",duration = 0.754,volume = 60,is3D = true}),
Sound:new({path = "Sound\\Units\\Combat\\WoodLightBashMetal3",duration = 0.531,volume = 60,is3D = true})
)
SOUND_METAL_SLICE_WOOD = Set.create(
Sound:new({path = "Sound\\Units\\Combat\\MetalLightSliceWood1",duration = 0.555,volume = 60,is3D = true}),
Sound:new({path = "Sound\\Units\\Combat\\MetalLightSliceWood2",duration = 0.506,volume = 60,is3D = true}),
Sound:new({path = "Sound\\Units\\Combat\\MetalLightSliceWood3",duration = 0.438,volume = 60,is3D = true})
)
SOUND_WOOD_BASH_WOOD = Set.create(
Sound:new({path = "Sound\\Units\\Combat\\WoodLightBashWood1",duration = 0.606,volume = 60,is3D = true}),
Sound:new({path = "Sound\\Units\\Combat\\WoodLightBashWood2",duration = 0.628,volume = 60,is3D = true}),
Sound:new({path = "Sound\\Units\\Combat\\WoodLightBashWood3",duration = 0.819,volume = 60,is3D = true})
)
SOUND_CHEST = Set.create(
Sound:new({path = "ChestOpen1.mp3",duration = 2.847,volume = 60}),
Sound:new({path = "ChestOpen2.mp3",duration = 2.847,volume = 60}),
Sound:new({path = "ChestOpen3.mp3",duration = 3.787,volume = 60})
)
SOUND_MASK_OF_MADNESS = Sound:new({path = "abilities\\spells\\nightelf\\shadowstrike\\shadowstrikebirth1.flac",duration = 2.194,volume = 60})
end
do
item_powerups = Set.create()
item_ability_scroll = {
model = "ScrollRed.mdx",
tracked = "AbilityScrollIcon.dds",
pickup_func = function(playerUnit)
UI.ShowAbilitySelect(playerUnit)
SOUND_UI_GAIN_SCROLL()
end,
}
item_equipment_chest = {
model = "Objects\\InventoryItems\\TreasureChest\\treasurechest.mdl",
tracked = "ChestIcon.dds",
delayDestroyEffect = 0.0,
rotation = PI,
scale = 1.2,
pickup_func = function(playerUnit,it)
BlzSetSpecialEffectTimeScale(it.sfx,1.0)
if it.potion then
UI.ChooseItem(1,true)
else
UI.ChooseItem(GetRandomInt(2,3),false)
end
it.potion = nil
end,
spawn_func = function(it)
BlzSetSpecialEffectTimeScale(it.sfx,0.0)
it.potion = GetRandomInt(1,4) == 4
if it.potion then
BlzSetSpecialEffectColor(it.sfx,0,150,255)
end
end,
}
item_food_meat = {
model = "ItemHaunch.mdx",
pickup_func = function(playerUnit)
playerUnit.regenAccum = playerUnit.regenAccum + 5.0
SOUND_EAT:random()(GetRandomReal(0.9,1.1))
end,
}
item_food_bread = {
model = "RPGBread.mdx",
pickup_func = function(playerUnit)
playerUnit.regenAccum = playerUnit.regenAccum + 5.0
SOUND_EAT:random()(GetRandomReal(0.9,1.1))
end,
}
item_food_potion = {
model = "Item_Potion_BigRed.mdx",
tracked = "HealthPotionIcon.dds",
pickup_func = function(playerUnit)
local bonus = playerUnit:get_max_health() * 0.05
playerUnit.regenAccum = playerUnit.regenAccum + 25.0 + bonus
SOUND_POTION()
end,
}
item_health_ray = {
model = "CeilingRays.mdl",
tracked = "ItemGodRayIcon.dds",
scale = 0.3,
pickup_func = function(playerUnit)
playerUnit:heal(50.)
playerUnit:buff(
"baseHealthRegen",
"healthRay",
"",
"chest",
0.5,
100.,
false,
true
)
end,
}
item_magnet = {
model = "MagnetItem2.mdx",
tracked = "magnetTexture.dds",
scale = 0.5,
pickup_func = function(playerUnit)
SOUND_MAGNET(GetRandomReal(0.9,1.1),true,playerUnit.position[1],playerUnit.position[2])
for orb in all_exp:elements() do
orb:mark_captured(playerUnit)
end
end,
}
item_ankh = {
model = "AnkhItem.mdx",
tracked = "ItemIconAnkh.dds",
scale = 0.5,
pickup_func = function(playerUnit)
playerUnit:buff(
"moveSpeedMult",
"ankh1",
"GlowShield1.mdx",
"origin",
0.25,
10.,
false,
true
)
playerUnit.invulnerable = 10.0 + GAME_TIME
end,
}
item_burning_oil_potion = {
model = "BurningOilPotionItem.mdx",
tracked = "ItemIconBurningOilPotion.dds",
scale = 0.5,
pickup_func = function(playerUnit)
playerUnit:buff(
"attackSpeedMult",
"burningOil",
"Abilities\\Spells\\Orc\\LiquidFire\\Liquidfire.mdl",
"origin",
1.0,
15.,
false,
true
)
end,
}
item_damage_boost = {
model = "DamageBoostItem.mdx",
tracked = "ItemDamageBoostIcon.dds",
scale = 0.5,
pickup_func = function(playerUnit)
playerUnit:buff(
"damageMult",
"itemdmgBoost",
"Abilities\\Spells\\Orc\\Voodoo\\VoodooAuraTarget.mdl",
"chest",
0.5,
10.,
false,
true
)
end,
}
item_powerups:add(item_ankh,item_burning_oil_potion,item_damage_boost)
end
do
barrel_drop_list = WeightedTable.new()
barrel_drop_list:add(item_magnet,3)
barrel_drop_list:add(item_food_meat,25)
barrel_drop_list:add(item_food_bread,25)
barrel_drop_list:add(item_food_potion,10)
barrel_drop_list:add(item_ankh,1.5)
barrel_drop_list:add(false,58.6)
barrel_drop_list:add(item_burning_oil_potion,1.5)
barrel_drop_list:add(item_damage_boost,1.5)
end
do
destructable_pillar = {
model = "Doodads\\Icecrown\\Props\\IceCrownPillar\\IceCrownPillar0",
size = 128.0,
scale = 1.6,
square = false,
type = Types.ProjectileStopper,
}
destructable_barrel = {
name = "Barrel",
model = "Buildings\\Other\\BarrelsUnit0\\BarrelsUnit0",
size = 64.0,
square = false,
health = 200,
drops = barrel_drop_list,
deathSound = SOUND_BARREL_DEATH,
}
destructable_crater = {
model = "Doodads\\Dungeon\\Rocks\\Cave_FieryCrator\\Cave_FieryCrator",
size = 128.,
scale = 0.704,
square = false,
height = -8
}
destructable_dungeon_wall = {
model = "DungeonWallCave.mdx",
size = 128.0,
scale = 1.0,
square = true,
type = Types.ProjectileStopper,
}
destructable_dungeon_corner_inner = {
model = "DungeonWallCaveInner",
size = 128.0,
scale = 1.0,
square = true,
type = Types.ProjectileStopper,
}
destructable_dungeon_corner_outer = {
model = "DungeonWallCaveOuter",
size = 128.0,
scale = 1.0,
square = true,
type = Types.ProjectileStopper,
}
destructable_blackness = {
model = "DungeonBlackness",
size = 128.0,
scale = 1.0,
square = true,
type = Types.ProjectileStopper,
}
destructable_torch = {
model = "Doodads\\LordaeronSummer\\Props\\TorchHuman\\TorchHuman",
scale = 1.0
}
end
do
Preload("GravityStorm.mdx")
local function dmg_player_iter(u,source,damage)
if u.type ~= nil then
u:damage(source,damage)
end
end
unit_attack_skulls = {
range = 400.0,
in_range_function = function(attack)
Missile.newUnitMissile("VoidboltMinor.mdx",attack.owner.position[1],attack.owner.position[2],
attack.owner.yaw,
15.0,
64.0,
attack.owner,
attack.owner.damage,
2048.0,
1.5
)
end,
cooldown = 5.0,
attack_time = 1.5,
delay = 0.5,
}
local function increase_movement_speed(u,sfx)
if u.sfx == sfx then
u.speed = u.speed + 0.5
DEFAULT_TIMER:callDelayed(0.25,increase_movement_speed,u,sfx)
end
end
unit_attack_homing_skulls = {
range = 1024.0,
cooldown = 2.5,
attack_time = 0.5,
delay = 0.5,
in_range_function = function(attack)
local owner = attack.owner
local x,y = owner.position[1] + math.cos(owner.yaw) * 50, owner.position[2] + math.sin(owner.yaw) * 50
local u = Unit.new(unit_skull_tracker,x,y,owner.yaw)
DestroyEffectEx(AddSpecialEffect("Abilities\\Spells\\Undead\\AnimateDead\\AnimateDeadTarget.mdl",x,y))
u.health = owner.health_max^0.7
u.health_max = u.health
u.collisionEnabled = false
u.damage = owner.damage
u.target = owner.target
DEFAULT_TIMER:callDelayed(0.25,increase_movement_speed,u,u.sfx)
end,
}
unit_suicide_attack = {
range = 64.0,
cooldown = 10.0,
attack_time = 0.5,
delay = 0.5,
in_range_function = function(attack)
attack.owner.target:damage(attack.owner,attack.owner.damage)
attack.owner:destroy()
end
}
unit_attack_tri_skulls = {
range = 500.0,
in_range_function = function(attack)
Missile.newUnitMissile("VoidboltMedium.mdx",attack.owner.position[1],attack.owner.position[2],
attack.owner.yaw,
10.0,
96.0,
attack.owner,
attack.owner.damage,
2048.0,
1.1
)
Missile.newUnitMissile("VoidboltMedium.mdx",attack.owner.position[1],attack.owner.position[2],
attack.owner.yaw-0.785398,
10.0,
96.0,
attack.owner,
attack.owner.damage,
2048.0,
1.1
)
Missile.newUnitMissile("VoidboltMedium.mdx",attack.owner.position[1],attack.owner.position[2],
attack.owner.yaw+0.785398,
10.0,
96.0,
attack.owner,
attack.owner.damage,
2048.0,
1.1
)
end,
cooldown = 6.0,
attack_time = 2.5,
delay = 0.5,
}
unit_attack_arrows = {
range = 600.0,
in_range_function = function(attack)
local m = Missile.newUnitMissile("Abilities\\Spells\\Other\\BlackArrow\\BlackArrowMissile",attack.owner.position[1],attack.owner.position[2],
attack.owner.yaw,
25.0,
64.0,
attack.owner,
attack.owner.damage,
2048.0
)
BlzSetSpecialEffectScale(m.sfx,1.5)
end,
cooldown = 5.0,
attack_time = 2.25,
delay = 0.875,
}
local function drop_corpse(startV,endV,sfx,curTime,totalTime)
local t = math.min(curTime/totalTime,1.0)
local x = lerp(startV.x,endV.x,t)
local y = lerp(startV.y,endV.y,t)
local z = ParabolicArc(0, 300, t)
BlzSetSpecialEffectPosition(sfx,x,y,z)
if t == 1.0 then
ReleaseUpdatingVector(startV)
ReleaseUpdatingVector(endV)
return
end
DEFAULT_TIMER:callDelayed(DEFAULT_TIMEOUT,drop_corpse,startV,endV,sfx,curTime+DEFAULT_TIMEOUT,totalTime)
end
local function jump_period(u,startV,endV,curTime,totalTime,area,startA,endA,sfx,ind)
local t = math.min(curTime/totalTime,1.0)
if getmetatable(u) ~= Unit then
DEFAULT_TIMER:callDelayed(0.0,drop_corpse,startV,endV,sfx,curTime,totalTime)
BlzSetSpecialEffectZ(ind,-9999)
return
end
local x = lerp(startV.x,endV.x,t)
local y = lerp(startV.y,endV.y,t)
local z = ParabolicArc(0, 300, t)
u:set_facing(lerp_angle(startA,endA,easeOutCubic(t)))
u.position[3] = z
u:move(x,y)
if t == 1.0 then
ReleaseUpdatingVector(startV)
ReleaseUpdatingVector(endV)
u.disabled_movement = nil
BlzSetSpecialEffectZ(ind,-9999)
local sfx = AddSpecialEffect("GravityStorm.mdx",x-SFX_OFFSET,y-SFX_OFFSET)
BlzSetSpecialEffectScale(sfx,area/1024.0)
DestroyEffectEx(sfx)
local half = area * 0.5
player_collision_hash:each(x - half, y - half, area, area, dmg_player_iter,u,u.damage * 2)
return
end
DEFAULT_TIMER:callDelayed(DEFAULT_TIMEOUT,jump_period,u,startV,endV,curTime+DEFAULT_TIMEOUT,totalTime,area,startA,endA,sfx,ind)
end
unit_attack_jump = {
range = 750.0,
cooldown = 6.0,
attack_time = 0.75,
delay = 0.5,
in_range_function = function(attack)
local u = attack.owner
local area = u.size * 4
local half = area * 0.5
local time = 1.25
local x,y = u.target:project_position(time)
x = x + GetRandomReal(-half,half)
y = y + GetRandomReal(-half,half)
if unit_collision_hash:cell_occupied(x - u.size*0.5, y - u.size*0.5,u.size,u.size,Types.Collidable) then return end
local startV = GetUpdatingVector(u.position[1],u.position[2])
local endV = GetUpdatingVector(x,y)
local startA = u.yaw
local endA = math.atan(endV.y - startV.y,endV.x-startV.x)
local ind = AddSpecialEffect("AreaDamageIndicator.mdx",x-SFX_OFFSET,y-SFX_OFFSET)
BlzSetSpecialEffectScale(ind,area/128)
BlzSetSpecialEffectTimeScale(ind,1/time)
BlzSetSpecialEffectColor(ind,255,0,255)
DestroyEffectEx(ind)
u.disabled_movement = true
u.anim = nil
BlzPlaySpecialEffectWithTimeScale(u.sfx,ANIM_TYPE_ATTACK,1.0)
DEFAULT_TIMER:callDelayed(DEFAULT_TIMEOUT,jump_period,u,startV,endV,0.0,time,area,startA,endA,u.sfx,ind)
end,
}
local function adjacent_waves(missile,sfx,count)
if missile.sfx == sfx and count > 0 then
local yaw = math.atan(missile.dir_vec[2],missile.dir_vec[1])
local m = Missile.newUnitMissile("PurpleWave.mdx",missile.position[1],missile.position[2],
yaw + HALF_PI,
15.0,
128.0,
missile.owner,
missile.damage,
800.0
)
DEFAULT_TIMER:callDelayed(0.5,adjacent_waves,m,m.sfx,count-1)
m = Missile.newUnitMissile("PurpleWave.mdx",missile.position[1],missile.position[2],
yaw - HALF_PI,
15.0,
128.0,
missile.owner,
missile.damage,
800.0
)
DEFAULT_TIMER:callDelayed(1.0,adjacent_waves,m,m.sfx,count-1)
end
end
unit_attack_shockwave = {
range = 200.0,
in_range_function = function(attack)
local m = Missile.newUnitMissile("PurpleWave.mdx",attack.owner.position[1],attack.owner.position[2],
attack.owner.yaw,
15.0,
128.0,
attack.owner,
attack.damage,
800.0
)
DEFAULT_TIMER:callDelayed(1.0,adjacent_waves,m,m.sfx,2)
end,
cooldown = 10.0,
attack_time = 1.75,
delay = 0.5,
damage = 50.0,
}
unit_attack_smash = {
range = 256.0,
in_range_function = function(attack)
local m = Missile.newUnitMissile("Abilities\\Spells\\Orc\\WarStomp\\WarStompCaster",attack.owner.position[1],attack.owner.position[2],
attack.owner.yaw,
64,
256,
attack.owner,
attack.damage,
attack.range
)
end,
cooldown = 2.5,
attack_time = 1.25,
delay = 0.5,
damage = 50.0,
}
unit_attack_slash = {
range = 196.0,
in_range_function = function(attack)
local owner = attack.owner
local x,y = owner.position[1],owner.position[2]
local yaw = owner.yaw
local range = attack.range * 0.5
local x2,y2 = x + range * math.cos(yaw), y + range * math.sin(yaw)
local sfx = AddSpecialEffect("CullingCleavePurple.mdx",x,y)
local half = range * 0.5
BlzSetSpecialEffectScale(sfx,0.75)
BlzSetSpecialEffectYaw(sfx,yaw)
DestroyEffect(sfx)
player_collision_hash:each(x2 - half, y2 - half, range, range, dmg_player_iter,owner,owner.damage)
end,
cooldown = 5.5,
attack_time = 1.5,
delay = 0.5,
}
end
do
function unit_angle_move_normal(u,moveAngle,targetAngle)
return moveAngle
end
function unit_angle_move_sludge(u,moveAngle,targetAngle)
if u.colliding == 0 then
return MathRound(moveAngle / THIRD_TAU) * THIRD_TAU
else
return moveAngle
end
end
function unit_speed_move_not_facing(u,speed)
end
function unit_angle_move_four(u,moveAngle,targetAngle)
if u.colliding == 0 then
return MathRound(targetAngle / HALF_PI) * HALF_PI
else
return moveAngle
end
end
function unit_speed_move_normal(u,speed)
return speed
end
function unit_speed_move_none(u,speed)
return 0
end
function unit_speed_move_bonus(u,speed)
return speed + u.speedBonus
end
function unit_angle_move_not_facing(u,moveAngle,targetAngle)
if u.colliding > 0 then
u.speedBonus = u.speedBonus * 0.75
end
if math.abs(angle_difference(u.target.yaw, targetAngle)) < PI * 0.85 then
u.speed_move_func = unit_speed_move_bonus
u.speedBonus = u.speedBonus + math.min(DEFAULT_TIMEOUT * u.speed,u.speed*3)
return moveAngle
else
u.speed_move_func = unit_speed_move_none
u.speedBonus = 0
u.turn_speed = u.baseTurnSpeed
return u.yaw
end
end
function unit_speed_move_sludge(u,speed)
return speed * easeOutCubic((math.sin(GAME_TIME * 7.0 + u.spawn_time) + 1.0) * 0.5 )
end
function unit_speed_move_slime(u,speed)
local t = ParabolicArc(1,0,(math.sin(GAME_TIME * 2.5 + u.spawn_time) + 1.0) * 0.5)
u.position[3] = t * 125
return speed * t
end
function unit_angle_move_slime(u,moveAngle,targetAngle)
local t = ParabolicArc(1,0,(math.sin(GAME_TIME * 2.5 + u.spawn_time) + 1.0) * 0.5)
if t < 0.3 then
return moveAngle
else
return u.yaw
end
end
local near_angle_x = 0.0
local near_angle_y = 0.0
local near_count = 0
local function near_angle_iter(u,angle_move_func,x,y)
if u.angle_move_func == angle_move_func then
near_angle_x = near_angle_x + math.cos(u.yaw)
near_angle_y = near_angle_y + math.sin(u.yaw)
end
end
function unit_angle_move_flock(u,moveAngle,targetAngle)
if u.colliding == 0 then
near_angle_x = math.cos(moveAngle)
near_angle_y = math.sin(moveAngle)
unit_collision_hash:each(u.position[1] - 128, u.position[2] - 128, 256, 256, near_angle_iter,u.angle_move_func)
return math.atan(near_angle_y,near_angle_x)
else
return moveAngle
end
end
local function near_speed_iter(u,speed_move_func)
if u.speed_move_func == speed_move_func then
near_count = near_count + 1
end
end
function unit_speed_move_group_speed(u,speed)
near_count = 0
unit_collision_hash:each(u.position[1] - 128, u.position[2] - 128, 256, 256, near_speed_iter,u.speed_move_func)
return speed + near_count
end
function unit_angle_move_gargoyle(u,moveAngle,targetAngle)
local t = easeOutSextic((math.sin(GAME_TIME *0.75) + 1.0) * 0.5 )
if u.colliding == 0 then
return wrap(targetAngle + t * HALF_PI,-PI,PI)
else
return moveAngle
end
end
function unit_angle_flee(u,moveAngle,targetAngle)
local t = wrap(GAME_TIME*0.05,0.0,1.0)
if t > 0.6 then
return moveAngle + PI
else
return moveAngle
end
end
function unit_angle_circle_flee(u,moveAngle,targetAngle)
local t = wrap(GAME_TIME*0.05,0.0,1.0)
if t > 0.6 and u.colliding == 0 then
return targetAngle + HALF_PI
else
return moveAngle
end
end
function unit_speed_move_fast(u,speed)
return speed * 3.0
end
function unit_speed_invisible(u,speed)
local t = wrap((u.spawn_time + GAME_TIME)*0.1,0.0,1.0)
if (t > 0.6 or u.spawn_time > GAME_TIME) and u.health >= u.health_max then
if u.spawn_time < GAME_TIME then
u.spawn_time = GAME_TIME + 2.0
DestroyEffect(AddSpecialEffect("Abilities\\Spells\\Human\\Polymorph\\PolyMorphDoneGround.mdl",u.position[1],u.position[2]))
BlzSetSpecialEffectAlpha(u.sfx,50)
u.collision_enabled = false
end
return speed * 3
else
BlzSetSpecialEffectAlpha(u.sfx,255)
u.collision_enabled = true
return speed
end
end
function unit_speed_blink(u,speed)
if u.spawn_time < GAME_TIME and u.target.position then
local d = DistanceSquaredTo(u.position[1],u.position[2],u.target.position[1],u.target.position[2])
local range = 400
if u.attack ~= nil then
range = u.attack.range
end
if d > range * range then
u.spawn_time = GAME_TIME + 3.0
DestroyEffect(AddSpecialEffect("Abilities\\Spells\\NightElf\\Blink\\BlinkCaster.mdl",u.position[1],u.position[2]))
speed = speed * 48
end
end
return speed
end
function unit_angle_gas_flee(u,moveAngle,targetAngle)
local a = unit_angle_circle_flee(u,moveAngle,targetAngle)
if a ~= moveAngle then
u.speed_move_func = unit_speed_move_fast
if u.spawn_time < GAME_TIME then
u.spawn_time = GAME_TIME + 0.333
Missile.newUnitMissile("units\\undead\\PlagueCloud\\PlagueCloud",u.position[1],u.position[2],
targetAngle,
0.05,
128.0,
u,
50.0,
64.0
)
end
else
u.speed_move_func = unit_speed_move_normal
end
return a
end
end
do
unit_assasin_skeleton = {
name = "Skeletal Assasin",
model = 'SkeletonAssassin',
size = 48.0,
speed = 3.0,
turn_speed = 0.08,
scale = 1.0,
exp = 5.0,
health = 450.0,
damage = 40.0,
speed_move_func = unit_speed_invisible
}
unit_angel = {
name = "Weeping Angel",
model = 'AngelStatue2.mdx',
size = 48.0,
speed = 3.0,
turn_speed = 0.04,
scale = 0.5,
exp = 5.0,
health = 1200.0,
damage = 40.0,
angle_move_func = unit_angle_move_not_facing,
on_create_func = function(self)
self.speedBonus = 0.0
self.baseTurnSpeed = self.turn_speed
self.remove_func = unit_angel.remove_func
end,
remove_func = function(self)
self.speedBonus = nil
self.baseTurnSpeed = nil
end,
damage_func = function(self,amount,unblockable)
if self.speedBonus > 0.0 then
return 0
else
return amount
end
end,
}
unit_vanguard_skeleton = {
name = "Skeletal Vanguard",
model = 'SkeletonVanguard',
size = 64.0,
speed = 2.5,
turn_speed = 0.04,
scale = 1.1,
exp = 22.5,
health = 2000.0,
damage = 30.0,
attack = unit_attack_slash,
damage_func = function(self,amount,unblockable)
if (self.health - amount)/self.health_max < 0.5 and self.speed_move_func ~= unit_speed_move_fast then
self:subanimation(SUBANIM_TYPE_FAST,true)
self.speed_move_func = unit_speed_move_fast
end
return amount
end,
}
unit_strong_mage_skeleton = {
name = "Undead Mage",
model = 'units\\undead\\Necromancer\\Necromancer',
size = 60.0,
speed = 3.0,
turn_speed = 0.08,
scale = 1.3,
exp = 12.0,
health = 450.0,
damage = 40.0,
speed_move_func = unit_speed_blink,
attack = unit_attack_skulls,
}
unit_meat_boy = {
name = "Meat Boy",
model = 'units\\undead\\Abomination\\Abomination',
size = 96.,
speed = 4.0,
turn_speed = 0.08,
scale = 1.25,
exp = 100.5,
health = 50000.0,
damage = 60.0,
bossType = BossTypes.Unique,
angle_move_func = unit_angle_gas_flee,
attack = unit_attack_smash,
}
unit_skull_tracker = {
name = "Skull Tracker",
model = 'WinglessCursedSkull.mdx',
size = 16.,
speed = 2.0,
turn_speed = 0.2,
scale = 1.0,
exp = 1.0,
health = 100.0,
damage = 10.0,
attack = unit_suicide_attack,
on_create_func = function(self)
self.remove_func = unit_skull_tracker.remove_func
self.position[3] = 64.0
end,
remove_func = function(self)
self.collisionEnabled = nil
end
}
unit_basic_skeleton = {
name = "Skeleton",
model = 'RPGSkeleton',
size = 48.,
speed = 2.0,
turn_speed = 0.02,
scale = 1.0,
exp = 1.0,
health = 100.0,
damage = 10.0,
}
local FORCE_FIELD_AREA = 488.0
local function move_forcefield(data,self)
if not self.shield then
return
end
local x,y = self.position[1], self.position[2]
BlzSetSpecialEffectPosition(self.shield,x,y-SFX_OFFSET,0)
data.lastMoved = data.lastMoved + DEFAULT_TIMEOUT
if data.lastMoved > 0.25 then
SetSoundPosition(data.snd,x,y,0)
data.lastMoved = 0.0
unit_collision_hash:update(data,x - FORCE_FIELD_AREA, y - FORCE_FIELD_AREA, FORCE_FIELD_AREA*2, FORCE_FIELD_AREA*2)
end
DEFAULT_TIMER:callDelayed(DEFAULT_TIMEOUT,move_forcefield,data,self)
end
unit_forcefield_skeleton = {
name = "Wight",
model = 'WightUnit3.mdx',
size = 96.,
speed = 2.0,
turn_speed = 0.02,
scale = 1.0,
exp = 25.0,
health = 3000.0,
damage = 25.0,
on_create_func = function(self)
local x,y = self.position[1], self.position[2]
self.shield = AddSpecialEffect("ForceField2.mdx",x-SFX_OFFSET,y-SFX_OFFSET)
BlzSetSpecialEffectScale(self.shield,FORCE_FIELD_AREA/448.)
local data = NewTable()
data.position = self.position
data.half_size = FORCE_FIELD_AREA
data.type = Types.PushesPlayer + Types.Summon
data.ignoreCollision = true
data.pushStr = 0.5
data.lastMoved = 0.0
data.snd = SOUND_FORCE_FIELD_LOOP(1.0,true,x,y)
BlzSetSpecialEffectColor(self.shield,0,255,0)
self.sheilddata = data
unit_collision_hash:add(data, x - FORCE_FIELD_AREA, y - FORCE_FIELD_AREA, FORCE_FIELD_AREA*2, FORCE_FIELD_AREA*2)
DEFAULT_TIMER:callDelayed(DEFAULT_TIMEOUT,move_forcefield,data,self)
self.remove_func = unit_forcefield_skeleton.remove_func
end,
remove_func = function(self)
DestroyEffect(self.shield)
self.shield = nil
local data = self.sheilddata
unit_collision_hash:remove(data)
data.ignoreCollision = nil
data.pushStr = nil
data.type = nil
data.position = nil
data.half_size = nil
data.lastMoved = nil
SOUND_FORCE_FIELD_LOOP:stop(data.snd,false)
data.snd = nil
ReleaseTable(data)
self.sheilddata = nil
end,
}
function AreaDamagePlayer(playerUnit,dmg)
playerUnit:damage(nil,dmg)
end
local function damageArea(x,y,dmg,area,remainTime)
local e = AddSpecialEffect("FireExplosion.mdx",x-SFX_OFFSET,y-SFX_OFFSET)
BlzSetSpecialEffectScale(e,area/256)
DestroyEffect(e)
local half = area * 0.5
DEFAULT_TIMER:callDelayed(remainTime,shash.each,player_collision_hash,x - half, y - half, area, area, AreaDamagePlayer,dmg)
end
unit_lava_slime = {
name = "Lava Slime",
model = 'LavaSlime.mdx',
size = 48.,
speed = 5.0,
turn_speed = 0.1,
scale = 1.0,
exp = 15.0,
health = 1000.0,
damage = 10.0,
speed_move_func = unit_speed_move_slime,
angle_move_func = unit_angle_move_slime,
death_func = function(self)
local area = 256
local time = 1.5
local dmg = 25
local x,y = self.position[1], self.position[2]
local ind = AddSpecialEffect("AreaDamageIndicator.mdx",x-SFX_OFFSET,y-SFX_OFFSET)
BlzSetSpecialEffectScale(ind,area/128)
BlzSetSpecialEffectTimeScale(ind,1/time)
BlzSetSpecialEffectColor(ind,255,0,255)
DestroyEffect(ind)
local effect_time = time * 0.75
DEFAULT_TIMER:callDelayed(effect_time,damageArea,x,y,dmg,area,time-effect_time)
end,
}
unit_beetle = {
name = "Beetle",
model = 'units\\undead\\scarab\\scarab',
size = 32.,
speed = 6.0,
turn_speed = 0.24,
scale = 0.9,
exp = 7.0,
health = 600.0,
damage = 20.0,
angle_move_func = unit_angle_move_gargoyle,
speed_move_func = unit_speed_move_sludge,
}
unit_mage_skeleton = {
name = "Skeletal Mage",
model = 'units\\undead\\SkeletonMage\\SkeletonMage',
size = 48.0,
speed = 4.0,
turn_speed = 0.04,
scale = 1.1,
exp = 4.0,
health = 125.0,
damage = 5.0,
attack = unit_attack_skulls,
}
unit_archer_skeleton = {
name = "Skeletal Archer",
model = 'units\\creeps\\SkeletonArcher\\SkeletonArcher',
size = 48.0,
speed = 4.0,
turn_speed = 0.02,
scale = 1.1,
exp = 16.0,
health = 625.0,
damage = 45.0,
attack = unit_attack_arrows,
}
unit_warrior_skeleton = {
name = "Skeletal Warrior",
model = 'units\\undead\\Skeleton\\Skeleton',
size = 48.0,
speed = 4.0,
turn_speed = 0.04,
scale = 1.0,
exp = 2.5,
health = 155.0,
damage = 12.0,
}
unit_shield_skeleton = {
name = "Skeletal Defender",
model = 'ShieldSkeleton',
size = 48.0,
speed = 2.0,
turn_speed = 0.04,
scale = 1.0,
exp = 3.5,
health = 245.0,
damage = 15.0,
damage_func = function(self,amount,unblockable)
if self.health/self.health_max < 0.5 or unblockable then return amount end
amount = BlockDamage(amount,80)
if amount == 0.0 then
CreateBlockEffect(self.position[1],self.position[2])
elseif (self.health - amount)/self.health_max < 0.5 and self.angle_move_func ~= unit_angle_move_normal then
self:subanimation(SUBANIM_TYPE_DEFEND,false)
self.speed = self.speed + 2.0
self.angle_move_func = unit_angle_move_normal
end
return amount
end,
on_create_func = function(self)
self:subanimation(SUBANIM_TYPE_DEFEND,true)
end,
angle_move_func = unit_angle_move_four
}
unit_viking_skeleton = {
name = "Skeletal Juggernaut",
model = 'VikingSkeleton',
size = 96.0,
speed = 4.0,
turn_speed = 0.04,
scale = 2.0,
exp = 32.5,
health = 9350.0,
damage = 30.0,
bossType = BossTypes.Unique,
attack = unit_attack_shockwave,
outline = true,
outline_r = 255,
outline_g = 75,
outline_b = 50,
outline_alpha = 200,
outline_offset = 8,
}
unit_sludge = {
name = "Sludge",
model = 'units\\creeps\\SludgeMonster\\SludgeMonster',
size = 48.0,
speed = 6.0,
turn_speed = 0.04,
scale = 1.0,
exp = 7.5,
health = 255.0,
damage = 30.0,
angle_move_func = unit_angle_move_sludge,
speed_move_func = unit_speed_move_sludge,
}
local function create4(whichType,target,yaw,x,y)
local u = Unit.new(unit_sludge,x-48,y+48,yaw)
u:set_target(target)
u = Unit.new(unit_sludge,x-48,y-48,yaw)
u:set_target(target)
u = Unit.new(unit_sludge,x+48,y+48,yaw)
u:set_target(target)
u = Unit.new(unit_sludge,x+48,y-48,yaw)
u:set_target(target)
end
unit_green_sludge = {
name = "Vile Sludge",
model = 'units\\other\\DalaranReject\\DalaranReject',
size = 64.0,
speed = 6.0,
turn_speed = 0.04,
scale = 1.25,
exp = 30,
health = 2000.0,
damage = 45.0,
angle_move_func = unit_angle_move_sludge,
speed_move_func = unit_speed_move_sludge,
death_func = function(self)
local scale = self:get_scale() * 0.75
local x,y = self.position[1],self.position[2]
local m = Missile.newUnitMissile("goo_puddle2.mdx",x,y,
GetRandomReal(-PI,PI),
0.001,
scale * 128.,
self,
25.0,
1.0,
scale,
0.0,
0.0
)
DEFAULT_TIMER:callDelayed(0.1,create4,unit_sludge,self.target,self.yaw,x,y)
BlzSetSpecialEffectColor(m.sfx,50,150,0)
end,
}
unit_gargoyle = {
name = "Gargoyle",
model = 'units\\undead\\Gargoyle\\Gargoyle',
size = 48.0,
speed = 6.0,
turn_speed = 0.07,
scale = 1.0,
exp = 6.5,
health = 415.0,
damage = 25.0,
angle_move_func = unit_angle_move_gargoyle,
}
unit_banshee = {
name = "Banshee",
model = 'units\\undead\\Banshee\\Banshee',
size = 48.0,
speed = 1.0,
turn_speed = 0.04,
scale = 1.0,
exp = 9.5,
health = 615.0,
damage = 35.0,
angle_move_func = unit_angle_move_flock,
speed_move_func = unit_speed_move_group_speed,
}
end
do
local function cone_iter(u,attack,x,y,angle,cone_angle,range,dmgType,effect_func,sound_list)
if u.type == nil then return end
local x1 = u.position[1]
local y1 = u.position[2]
if isPointInsideCone(x, y, angle, cone_angle, x1, y1) and IsInRange(x1, y1,x,y,range)then
attack:deal_damage(u,nil,nil,dmgType,effect_func)
if sound_list then
sound_list:random()(GetRandomReal(0.9,1.1),true,x1,y1)
end
end
end
function summon_cone_attack(attack,count,dmgType,area_mult,effect_func,sound_list)
local summon = attack.owner
local abil = summon.ability
local x,y = summon.position[1],summon.position[2]
local half = attack:get_range()
local area = half * 2.0
local dir = summon.yaw
local cone_angle = abil:get_cone_angle() * (area_mult or 1.0)
unit_collision_hash:each(x - half, y - half, area, area, cone_iter,abil,x,y,dir,cone_angle,half,dmgType,effect_func,sound_list)
AreaIndicator.new(AreaIndicatorTypes.Cone,x,y,half,dir,cone_angle):destroy(0.1)
if count > 1 then
abil.timer:callDelayed(0.15, summon_cone_attack,attack,count-1,dmgType,area_mult,effect_func)
end
end
summon_attack_cone = {
range = 200.0,
in_range_function = summon_cone_attack,
cooldown = 1.25,
attack_time = 0.75,
delay = 0.5,
}
summon_attack_spear = {
range = 200.0,
in_range_function = function(attack,count,dmgType,area_mult,effect_func)
local summon = attack.owner
local x,y = summon.position[1],summon.position[2]
summon_cone_attack(attack,count,dmgType,area_mult,effect_func,SOUND_WOOD_BASH_WOOD)
end,
cooldown = 1.25,
attack_time = 1.25,
delay = 0.5,
begin_function = function(attack,owner)
local x,y = owner.position[1],owner.position[2]
SOUND_SPEAR_SWING(GetRandomReal(0.9,1.1),true,x,y)
end,
}
summon_attack_skele_mage = {
range = 400.0,
in_range_function = function(attack)
local abil = attack.owner.ability
local m = Missile.new(
"Abilities\\Weapons\\SkeletalMageMissile\\SkeletalMageMissile.mdl",
attack.owner.position[1],
attack.owner.position[2],
attack.owner.yaw,
15.0,
abil.owner,
abil,
1024.0,
math.huge,
64.0
)
BlzSetSpecialEffectScale(m.sfx,1.5)
end,
cooldown = 2.0,
attack_time = 1.5,
delay = 0.5,
}
summon_attack_imp = {
range = 125.0,
in_range_function = summon_cone_attack,
cooldown = 3.25,
attack_time = 0.5,
delay = 0.5,
}
end
do
summon_golem = {
model = 'units\\creeps\\RockGolem\\RockGolem',
size = 64.0,
speed = 6.0,
turn_speed = 0.18,
scale = 1.0,
health = math.huge,
attack = summon_attack_cone,
}
summon_parasite = {
model = 'units\\critters\\DuneWorm\\DuneWorm',
size = 32.0,
speed = 6.0,
turn_speed = 0.3,
scale = 1.0,
health = math.huge,
}
summon_spirit_warrior = {
model = 'SpiritWarrior.mdx',
size = 96.0,
speed = 0,
turn_speed = 0.18,
scale = 1.25,
health = math.huge,
attack = summon_attack_spear,
ignoreCollision = true,
}
summon_zombie_wall = {
model = 'units\\creeps\\Zombie\\Zombie',
size = 64.0,
speed = 0,
turn_speed = 0.18,
scale = 1.25,
health = 250.,
attack = summon_attack_imp,
wall = true,
}
summon_rat = {
model = 'Rat.mdx',
size = 32.0,
turn_speed = 0.3,
scale = 0.5,
health = math.huge,
speed = 6.0,
attack_on_touch = true,
ignoreCollision = true,
}
summon_skeleton_warrior = {
model = 'units\\undead\\Skeleton\\Skeleton',
size = 32.0,
speed = 9.0,
turn_speed = 0.18,
scale = 1.1,
health = 1000.,
attack_on_touch = true,
ignoreCollision = true,
}
summon_skeleton_mage = {
model = 'units\\undead\\SkeletonMage\\SkeletonMage',
size = 32.0,
speed = 5.0,
turn_speed = 0.18,
scale = 1.1,
health = 250.,
attack = summon_attack_skele_mage,
attack_on_touch = true,
ignoreCollision = true,
}
summon_imp = {
model = 'DemonTaskmaster.mdx',
size = 32.0,
speed = 7.5,
turn_speed = 0.22,
scale = 1.1,
health = 750.,
attack = summon_attack_imp,
attack_on_touch = true,
ignoreCollision = true,
}
end
do
function UndergroundSpawnStart()
AddWaveType(unit_basic_skeleton,0.6,160)
SPAWN_TIMER:callDelayed(60, AddWaveType,unit_basic_skeleton,0.2,120)
SPAWN_TIMER:callDelayed(120, AddWaveType,unit_viking_skeleton,0.5,nil,1)
SPAWN_TIMER:callDelayed(160, AddWaveType,unit_warrior_skeleton,0.35,300)
SPAWN_TIMER:callDelayed(180, AddWaveType,unit_mage_skeleton,3.0,300)
SPAWN_TIMER:callDelayed(240, AddWaveType,unit_shield_skeleton,0.35,300)
SPAWN_TIMER:callDelayed(320, AddWaveType,unit_sludge,0.35,500)
SPAWN_TIMER:callDelayed(410, AddWaveType,unit_sludge,0.35,500)
SPAWN_TIMER:callDelayed(500, AddWaveType,unit_meat_boy,0.5,nil,1)
SPAWN_TIMER:callDelayed(550, AddWaveType,unit_forcefield_skeleton,5,700)
SPAWN_TIMER:callDelayed(520, AddWaveType,unit_gargoyle,0.2,620)
SPAWN_TIMER:callDelayed(640, AddWaveType,unit_banshee,0.1,750)
SPAWN_TIMER:callDelayed(760, AddWaveType,unit_vanguard_skeleton,1.5,850)
SPAWN_TIMER:callDelayed(760, AddWaveType,unit_assasin_skeleton,0.5,850)
SPAWN_TIMER:callDelayed(760, AddWaveType,unit_strong_mage_skeleton,1,850)
SPAWN_TIMER:callDelayed(850, AddWaveType,unit_archer_skeleton,2,925)
SPAWN_TIMER:callDelayed(850, AddWaveType,unit_green_sludge,0.5,925)
SPAWN_TIMER:callDelayed(900, AddWaveType,unit_angel,3,1000)
end
end
do
potion_drop_list = WeightedTable.new()
potion_drop_list:add(Wearable.new({
name = "Strong Wine",
description = "Allows you to reroll the complete selection of traits when leveling up.",
icon = "IconPotion5.dds",
type = Wearable.Type.POTION,
action = function(self,pdata)
Potions.increment(pdata,StrongWine_PotionButton)
end
}),8)
potion_drop_list:add(Wearable.new({
name = "Potion of Oblivion",
description = "Allows you to banish a trait and all its follow up traits from the trait selection when leveling up.",
icon = "IconPotion1.dds",
type = Wearable.Type.POTION,
action = function(self,pdata)
Potions.increment(pdata,Oblivion_PotionButton)
end
}),8)
potion_drop_list:add(Wearable.new({
name = "Potion of Memories",
description = "Allows you to memorize a trait so its guaranteed to appear next time you level up.",
icon = "IconPotion6.dds",
type = Wearable.Type.POTION,
action = function(self,pdata)
Potions.increment(pdata,Memory_PotionButton)
end
}),8)
potion_drop_list:add(Wearable.new({
name = "Reverberant Tinkture",
description = "Allows you to apply the effects of a trait twice when leveling up.",
icon = "IconPotion4.dds",
type = Wearable.Type.POTION,
action = function(self,pdata)
Potions.increment(pdata,Reverberant_PotionButton)
end
}),3)
potion_drop_list:add(Wearable.new({
name = "Potion of Renewal",
description = "Allows you to reroll the complete selection of Abilities.",
icon = "IconPotion3.dds",
type = Wearable.Type.POTION,
action = function(self,pdata)
Potions.increment(pdata,Renewal_PotionButton)
end
}),3)
potion_drop_list:add(Wearable.new({
name = "Hallucinogenic Elixir",
description = "Allows you to reroll the selection of Items in a Chest.",
icon = "IconPotion2.dds",
type = Wearable.Type.POTION,
action = function(self,pdata)
Potions.increment(pdata,Hallucinogenic_PotionButton)
end
}),3)
Wearable.new({
name = "Pace Setter",
description = "Accumulate 5s while not moving: Heal by 1%%|nFull Health: +20%% Attack Speed|nWounded: +20%% Movement Speed",
icon = "ItemPaceSetter.dds",
type = Wearable.Type.BOOTS,
attack_type = pace_setters_attack,
})
Wearable.new({
name = "Plated Boots",
description = "+20%% Defense|n+20%% Block Strength",
icon = "ItemPlatedBoots.dds",
type = Wearable.Type.BOOTS,
attack_type = plated_boots_attack,
})
Wearable.new({
name = "Firewalker Boots",
description = "Leaves a trail of fire, burning enemies.|n+0.5m/s base movement speed.",
icon = "ItemFirewalkerBoots.dds",
type = Wearable.Type.BOOTS,
attack_type = firewalker_boots_attack,
})
Wearable.new({
name = "Berserker Boots",
description = "While moving gain attack speed bonus on your Main Weapon based on 60%% of your movement speed bonus.|nMaximum of 50%%.",
icon = "ItemBeserkerBoots.dds",
type = Wearable.Type.BOOTS,
attack_type = beserker_boots_attack,
})
Wearable.new({
name = "Elven Slippers",
description = "Increases block strength while moving. Builds up by moved distance. Block strength is limited to 10, movement speed buffs increase the cap. Standing still removes the bonus.",
icon = "ItemElvenSlippers.dds",
type = Wearable.Type.BOOTS,
attack_type = elven_slippers_attack,
})
Wearable.new({
name = "Bogged Boots",
description = "Leave a trail of goo, Enemies that touch the goo slow down. Puddles disappear after 10 seconds or when several enemies moved over it.",
icon = "ItemBoggedBoots.dds",
type = Wearable.Type.BOOTS,
attack_type = bogged_boots_attack,
})
Wearable.new({
name = "Spiked Boots",
description = "When hit, drop 10 spikes to the ground that deal 100 damage and stun the enemy on contact. Takes 4 seconds to recharge. Can't be blocked.",
icon = "ItemSpikedBoots.dds",
type = Wearable.Type.BOOTS,
attack_type = spiked_boots_attack,
})
Wearable.new({
name = "Running Shoes",
description = "+25%% Movement Speed",
icon = "ItemRunnerShoes.dds",
type = Wearable.Type.BOOTS,
attack_type = running_shoes_attack,
})
Wearable.new({
name = "Electrostatic Treads",
description = "Charges up when you move. When fully charged emits an electric shock wave that damages and electrifies enemies.",
icon = "ItemElectrostaticTreads.dds",
type = Wearable.Type.BOOTS,
attack_type = electrostatic_treads_attack,
})
Wearable.new({
name = "Hunter's Garb",
description = "If you stand still your damage is increases by 6%% per second (up to 30%%). The bonus is reset as soon as you move.",
icon = "ItemHuntersGarb.dds",
type = Wearable.Type.CHEST,
attack_type = hunters_garb_attack,
})
Wearable.new({
name = "Blazing Shell",
description = "Every 0.5s: 10%% chance to Burn enemies.|nBeing hit: 200%% chance to Burn enemies.|n3m range, increases with Pickup Range.",
icon = "ItemBlazingShell.dds",
type = Wearable.Type.CHEST,
attack_type = blazing_shell_attack,
})
Wearable.new({
name = "Blood-Soaked Shirt",
description = "On kill with 10%% chance: +1 HP.|nOn full health when killing a foe:|n0.4%% chance for +1%% Damage (Max. 50%%)",
icon = "ItemBloodSoakedShirt.dds",
type = Wearable.Type.CHEST,
attack_type = bloodsoaked_shirt_attack,
})
Wearable.new({
name = "Defiant Plate",
description = "When damaged: +3 Defense for 10s (max. 30)|nWhen damaged: +0.5/s Regen. for 10s (max. 5/s)|nWhen undamaged for 6s: +3%% Force/s (max. 30%%)",
icon = "ItemDefiantPlate.dds",
type = Wearable.Type.CHEST,
attack_type = defiant_plate_attack,
})
Wearable.new({
name = "Shadow Cloak",
description = "Spawns shadows near you that damage enemies and increase your block strength by 15.",
icon = "ItemShadowCloak.dds",
type = Wearable.Type.CHEST,
attack_type = shadow_cloak_attack,
})
Wearable.new({
name = "Broker's Cape",
description = "Every 1.0s: Spreads a random debuff in 3m range.|nDebuffs: Fragile, Affliction, Slow|nChance: 50%% on each enemy in range.",
icon = "ItemBrokersCape.dds",
type = Wearable.Type.CHEST,
attack_type = brokers_cape_attack,
})
Wearable.new({
name = "Chain Mail",
description = "+20%% Defense|n+20%% Health",
icon = "ItemChainMail.dds",
type = Wearable.Type.CHEST,
attack_type = chain_mail_attack,
})
Wearable.new({
name = "Plate Armor",
description = "+20%% Block Strength|n+20%% Health",
icon = "ItemPlateArmor.dds",
type = Wearable.Type.CHEST,
attack_type = plate_armor_attack,
})
Wearable.new({
name = "Crimson Carapace",
description = "-30%% Health|n+30%% Summon Attack Speed|n+30%% Summon Movement Speed|n+30%% Summon Damage",
icon = "ItemCrimsonCarapace.dds",
type = Wearable.Type.CHEST,
attack_type = crimson_carapace_attack,
})
Wearable.new({
name = "Hunting Gloves",
description = "+20%% Multistrike",
icon = "ItemHuntingGloves.dds",
type = Wearable.Type.GLOVES,
attack_type = hunting_gloves_attack,
})
Wearable.new({
name = "Invocator's Grasp",
description = "+40%% Summon Spawns|n+20%% Summon Force|n+20%% Summon Damage",
icon = "ItemInvocatorsGrasp.dds",
type = Wearable.Type.GLOVES,
attack_type = invocators_grasp_attack,
})
Wearable.new({
name = "Unholy Touch",
description = "Adds a 50%% chance for all Melee attacks to apply Affliction.",
icon = "ItemUnholyTouch.dds",
type = Wearable.Type.GLOVES,
attack_type = unholy_touch_attack,
})
Wearable.new({
name = "Sparking Tips",
description = "Creates sparks when you hit burning enemy with physical attacks. Sparks inflict burn and damage enemies based on how many burn stacks they have.|n+50%% Burn Chance",
icon = "ItemSparklingTips.dds",
type = Wearable.Type.GLOVES,
attack_type = sparking_tips_attack,
})
Wearable.new({
name = "Thornfists",
description = "Deals a 3 times over-crit hit to each enemy that hurts you.",
icon = "ItemThornfists.dds",
type = Wearable.Type.GLOVES,
attack_type = thornfists_attack,
})
Wearable.new({
name = "Devouring Hands",
description = "When you die, Devouring Hands will consume a random equipped item to revive you with 30%% of your max health",
icon = "ItemDevouringHands.dds",
type = Wearable.Type.GLOVES,
attack_type = devouring_hands_attack,
})
Wearable.new({
name = "Fencer Gauntlets",
description = "Each time you block you have a 50%% chance for an additional attack that always crits.|n+10 Block Strength",
icon = "ItemFencerGauntlets.dds",
type = Wearable.Type.GLOVES,
attack_type = fencer_gauntlets_attack,
})
Wearable.new({
name = "Quickhand Gloves",
description = "+30%% Attack Speed",
icon = "ItemQuickhandGloves.dds",
type = Wearable.Type.GLOVES,
attack_type = quickhand_gloves_attack,
})
Wearable.new({
name = "Longfinger Gloves",
description = "+80%% Pickup Range",
icon = "ItemLongfinerGloves.dds",
type = Wearable.Type.GLOVES,
attack_type = longfinder_gloves_attack,
})
Wearable.new({
name = "Gauntlets of Accumulation",
description = "+25%% XP Gain",
icon = "ItemGauntletsOfAccumulation.dds",
type = Wearable.Type.GLOVES,
attack_type = gauntlets_of_accumulation_attack,
})
Wearable.new({
name = "Spellcaster Gloves",
description = "When you do not use your main attack for 2 seconds, your abilities become stronger over time, up to 66.6%%.",
icon = "ItemSpellcasterGloves.dds",
type = Wearable.Type.GLOVES,
attack_type = spellcaster_gloves_attack,
})
Wearable.new({
name = "Thunder Crown",
description = "Hitting electrified enemies with magic attacks triggers a chain lightning. The lightning deals 100 damage and has a chance to electrify the target.",
icon = "ItemThunderCrown.dds",
type = Wearable.Type.HEADWEAR,
attack_type = thunder_crown_attack,
})
Wearable.new({
name = "Gorgon Mask",
description = "Every 1.0s: Has a 100%% chance to apply Slow.|nDeal 10 Magic damage for every Slow effect.|nCovers area with 3m radius in front of you.",
icon = "ItemGorgonMask.dds",
type = Wearable.Type.HEADWEAR,
attack_type = gorgon_mask_attack,
})
Wearable.new({
name = "Hood",
description = "+20%% Health|n+10%% Defense",
icon = "ItemHood.dds",
type = Wearable.Type.HEADWEAR,
attack_type = hood_attack,
})
Wearable.new({
name = "Mask of Madness",
description = "Every 30s: Take 30 damage and gain permanent player buff of +1%% - 2%% to Damage, Attack Speed or Multistrike.",
icon = "ItemMaskOfMadness.dds",
type = Wearable.Type.HEADWEAR,
attack_type = mask_of_madness_attack,
})
Wearable.new({
name = "War Horns",
description = "Perform a warcry every 3 seconds.|nAdds 4 stacks of Fragile on all enemies in a range of 6 meters.",
icon = "ItemWarHorns.dds",
type = Wearable.Type.HEADWEAR,
attack_type = war_horns_attack,
})
Wearable.new({
name = "Ruby Circlet",
description = "Deals 4%% additional damage for each burning enemy. Limited to 60%%.",
icon = "ItemRubyCirclet.dds",
type = Wearable.Type.HEADWEAR,
attack_type = ruby_circlet_attack,
})
Wearable.new({
name = "Fighter's Headband",
description = "Effect triggers when a Elite or Boss appears.|nFor 20s: Regenerate 1%%/s|nAlways Spawn: Health Potion|nSpawn on Full Health: Power Up",
icon = "ItemFightersHeadband.dds",
type = Wearable.Type.HEADWEAR,
attack_type = fighters_headband_attack,
})
Wearable.new({
name = "Wind Crown",
description = "0.3%% attack speed for 10s for each killed enemy (up to +30%% attack speed).",
icon = "ItemWindCrown.dds",
type = Wearable.Type.HEADWEAR,
attack_type = wind_crown_attack,
})
Wearable.new({
name = "Helmet",
description = "+15%% Block Strength|n+15%% Health",
icon = "ItemHelmet.dds",
type = Wearable.Type.HEADWEAR,
attack_type = helmet_attack,
})
Wearable.new({
name = "Crown Of Decay",
description = "Summons a zombie wall which attacks and blocks enemies from passing every 15 seconds.",
icon = "ItemCrownOfDecay.dds",
type = Wearable.Type.HEADWEAR,
attack_type = crown_of_decay_attack,
})
Wearable.new({
name = "Gatherer's Charm",
description = "Helps you locating ingredients for new potions by showing the direction to the closest herbs.|n+10%% Movement Speed",
icon = "ItemGathersCharm.dds",
type = Wearable.Type.NECKLACE,
attack_type = gatherers_charm_attack,
})
Wearable.new({
name = "Warrior's Fervour",
description = "Activates on picking up Potions or Heal Magic.|nFor 15s: +100%% Damage & Attack Speed",
icon = "ItemWarriorFervour.dds",
type = Wearable.Type.NECKLACE,
attack_type = warriors_fervour_attack,
})
Wearable.new({
name = "Maiden's Tear",
description = "Requires to be charged. (30s)|nOn Hit: Negate damage, charge is used.|nWhile charged: +30%% Force & +30%% Damage",
icon = "ItemMaidenTear.dds",
type = Wearable.Type.NECKLACE,
attack_type = maidens_tear_attack,
})
Wearable.new({
name = "Shepherd's Boon",
description = "Per active summon: +0.10/s Regeneration|nOn full health, apply buff every tick.|nTick Speed: 30s + 6s for every buff.|nBuff: +5%% Damage (Summon)",
icon = "ItemShepardsBoon.dds",
type = Wearable.Type.NECKLACE,
attack_type = sheperds_boon_attack,
})
Wearable.new({
name = "Scars of Toil",
description = "Per 1 HP missing:|n+0.1%% Damage",
icon = "ItemScarsOfToil.dds",
type = Wearable.Type.NECKLACE,
attack_type = scars_of_toil_attack,
})
Wearable.new({
name = "Blood Catcher",
description = "On 200x max. HP damage dealt: +10 HP|nEach time triggered: +1%% to damage threshold.|nOn full health: +0.5%% Damage (max. 50%%)",
icon = "ItemBloodCatcher.dds",
type = Wearable.Type.NECKLACE,
attack_type = blood_catcher_attack,
})
Wearable.new({
name = "Elemental Resonator",
description = "On hit with a Main Weapon:|nBurn, Electrify and Frost have a 2%% chance for each of their own stacks to be applied again.",
icon = "ItemElementalResonator.dds",
type = Wearable.Type.NECKLACE,
attack_type = elemental_resonator_attack,
})
Wearable.new({
name = "Collar of Confidence",
description = "+3%% damage for each enemy in your pickup range.|nThe maximum is 75%%",
icon = "ItemCollarOfConfidence.dds",
type = Wearable.Type.NECKLACE,
attack_type = collar_of_confidence_attack,
})
Wearable.new({
name = "Jade Amulet",
description = "+30%% XP Gain|n-0.5%% XP Gain every level|nXP Gain cannot get below 0%%",
icon = "ItemJadeAmulet.dds",
type = Wearable.Type.NECKLACE,
attack_type = jade_amulet_attack,
})
Wearable.new({
name = "Duellist's Spark",
description = "+50%% damage|n-5%% damage for each enemy in your pickup range",
icon = "ItemDuelistsSpark.dds",
type = Wearable.Type.NECKLACE,
attack_type = duellists_spark_attack,
})
Wearable.new({
name = "Ring of Frost",
description = "Changes the damage type of your main weapon into ice damage.|nSet 15%% Frost Chance",
icon = "ItemRingOfFrost.dds",
type = Wearable.Type.RING,
attack_type = ring_of_frost_attack,
})
Wearable.new({
name = "Copper Ring",
description = "+45%% Crit Damage",
icon = "ItemCopperRing.dds",
type = Wearable.Type.RING,
attack_type = copper_ring_attack,
})
Wearable.new({
name = "Ring of Thunder",
description = "Changes the damage type of your main weapon into lightning damage.|nSet 15%% Electrify Chance",
icon = "ItemRingOfThunder.dds",
type = Wearable.Type.RING,
attack_type = ring_of_thunder_attack,
})
Wearable.new({
name = "Guiding Star",
description = "While attacking: +30%% attack speed, -20%% movement speed|nWhile moving: -20%% attack speed, +30%% movement speed",
icon = "ItemGuidingStar.dds",
type = Wearable.Type.RING,
attack_type = guiding_star_attack,
})
Wearable.new({
name = "Seal of Rebirth",
description = "When you die, the seal breaks and revives you with 50%% of your max health",
icon = "ItemSealOfRebirth.dds",
type = Wearable.Type.RING,
attack_type = seal_of_rebirth_attack,
})
Wearable.new({
name = "Echoing Band",
description = "On hit with physical damage:|n20%% Chance to spawn a shockwave dealing 40%% of the original damage in an area with a radius 3m.",
icon = "ItemEchoingBand.dds",
type = Wearable.Type.RING,
attack_type = echoing_band_attack,
})
Wearable.new({
name = "Iron Ring",
description = "+10 Base Damage",
icon = "ItemIronRing.dds",
type = Wearable.Type.RING,
attack_type = iron_ring_attack,
})
Wearable.new({
name = "Ring of Fire",
description = "Changes the damage type of your main weapon into fire damage.|nSet 15%% Burn Chance",
icon = "ItemRingOfFire.dds",
type = Wearable.Type.RING,
attack_type = ring_of_fire_attack,
})
Wearable.new({
name = "Pest Ring",
description = "Every 1.0s: Summon 1 rat, with 5 Pest Bites.|nPest Bites apply 2: Fragile, Affliction, Slow|nRats deal 20 damage to enemies they touch.",
icon = "ItemPestRing.dds",
type = Wearable.Type.RING,
attack_type = pest_ring_attack,
})
Wearable.new({
name = "Necromancers Clutch",
description = "Every 15 seconds, you summon a skeleton to fight for you.|nSkeletons die after a given time (can be prolonged).|nCan handle 5 summons at a time.",
icon = "ItemNecromancersClutch.dds",
type = Wearable.Type.RING,
attack_type = necromancers_clutch_attack,
})
Wearable.new({
name = "Holy Relic",
description = "Every 30s when damaged: Place Holy Light|n> Heals 50 HP and regenerates 0.5/s for 100s|nEvery 30s when full health: Drop Power Up",
icon = "ItemHolyRelic.dds",
type = Wearable.Type.RING,
attack_type = holy_relic_attack,
})
Wearable.new({
name = "Demonic Bond",
description = "Every 5 seconds, you summons an imp to fight by your side.|nImps die after a given time (can be prolonged).|nCan handle 5 summons at a time.",
icon = "ItemDemonicBond.dds",
type = Wearable.Type.RING,
attack_type = demonic_bond_attack,
})
Wearable.new({
name = "Wooden Ring",
description = "+15%% Base Crit Chance",
icon = "ItemWoodenRing.dds",
type = Wearable.Type.RING,
attack_type = wooden_ring_attack,
})
end
function SwordsmanInit()
PlayerClasses = Set.create()
trait_swordsman_dedication = Trait.new({
multi_trait = 2,
name = "Dedication",
subname1 = "Health",
subname2 = "Regeneration",
descriptions1 = {
"|cff00ff00+70 Base Health|n+6%% Movement Speed|r",
"|cff00ff00+70 Base Health|n+3 Base Defense|r",
"|cff00ff00+70 Base Health|n+3 Base Block Strength|r",
"|cff00ff00+140 Base Health|r",
},
descriptions2 = {
"|cff00ff00+0.25/s Base Regeneration|n+6%% Movement Speed|r",
"|cff00ff00+0.25/s Base Regeneration|n+3 Base Defense|r",
"|cff00ff00+0.25/s Base Regeneration|n+3 Base Block Strength|r",
"|cff00ff00+0.25/s Base Regeneration|n+70 Base Health|r",
},
icon = "TraitSwordsmanDedication.dds",
actions1 = {
function(trait,playerUnit)
playerUnit:add_health(70)
playerUnit.moveSpeedMult = playerUnit.moveSpeedMult + 0.06
end,
function(trait,playerUnit)
playerUnit:add_health(70)
playerUnit.defense = playerUnit.defense + 3
end,
function(trait,playerUnit)
playerUnit:add_health(70)
playerUnit.block = playerUnit.block + 3
end,
function(trait,playerUnit)
playerUnit:add_health(140)
end,
},
actions2 = {
function(trait,playerUnit)
playerUnit.baseHealthRegen = playerUnit.baseHealthRegen + 0.25
playerUnit.moveSpeedMult = playerUnit.moveSpeedMult + 0.06
end,
function(trait,playerUnit)
playerUnit.baseHealthRegen = playerUnit.baseHealthRegen + 0.25
playerUnit.defense = playerUnit.defense + 3
end,
function(trait,playerUnit)
playerUnit.baseHealthRegen = playerUnit.baseHealthRegen + 0.25
playerUnit.block = playerUnit.block + 3
end,
function(trait,playerUnit)
playerUnit.baseHealthRegen = playerUnit.baseHealthRegen + 0.25
playerUnit:add_health(70)
end,
},
level_max = 4,
level_func = TraitLevelFunction_Eight,
})
trait_swordsman_prof_stance = Trait.new({
multi_trait = 2,
name = "Proficient Stance",
subname1 = "Defensive",
subname2 = "Offensive",
descriptions1 = {
"|cff00ff00+3 Base Defense|n+3 Base Block Strength|r",
"|cff00ff00+10%% Range (Main Weapon)|n+3 Base Defense|r",
"|cff00ff00+10%% Area (Main Weapon)|n+3 Base Block Strength|r",
"|cff00ff00+6 Base Defense|n+6 Base Block Strength|r",
},
descriptions2 = {
"|cff00ff00+10%% Area (Main Weapon)|n+10%% Range (Main Weapon)|r",
"|cff00ff00+10%% Range (Main Weapon)|n+10%% Damage (Main Weapon)|r",
"|cff00ff00+10%% Area (Main Weapon)|n+10%% Attack Speed (Main Weapon)|r",
"|cff00ff00+10%% Area (Main Weapon)|n+10%% Range (Main Weapon)|r",
},
icon = "TraitSwordsmanProfStance.dds",
actions1 = {
function(trait,playerUnit)
playerUnit.defense = playerUnit.defense + 3
playerUnit.block = playerUnit.block + 3
end,
function(trait,playerUnit)
local atk = playerUnit.attacks[1]
atk.rangeMult = atk.rangeMult + 0.1
playerUnit.defense = playerUnit.defense + 3
end,
function(trait,playerUnit)
local atk = playerUnit.attacks[1]
atk.areaMult = atk.areaMult + 0.1
playerUnit.block = playerUnit.block + 3
end,
function(trait,playerUnit)
local atk = playerUnit.attacks[1]
playerUnit.defense = playerUnit.defense + 6
playerUnit.block = playerUnit.block + 6
end,
},
actions2 = {
function(trait,playerUnit)
local atk = playerUnit.attacks[1]
atk.areaMult = atk.areaMult + 0.1
atk.rangeMult = atk.rangeMult + 0.1
end,
function(trait,playerUnit)
local atk = playerUnit.attacks[1]
atk.damageMult = atk.damageMult + 0.1
atk.rangeMult = atk.rangeMult + 0.1
end,
function(trait,playerUnit)
local atk = playerUnit.attacks[1]
atk.attackSpeedMult = atk.attackSpeedMult + 0.1
atk.areaMult = atk.areaMult + 0.1
end,
function(trait,playerUnit)
local atk = playerUnit.attacks[1]
atk.areaMult = atk.areaMult + 0.1
atk.rangeMult = atk.rangeMult + 0.1
end,
},
level_max = 4,
level_func = TraitLevelFunction_Eight,
})
trait_swordsman_weapon_proficiency = Trait.new({
multi_trait = 2,
name = "Weapon Proficiency",
subname1 = "Dexterity",
subname2 = "Power",
descriptions1 = {
"|cff00ff00Attack Speed +10%% (Main Weapon)|nMultistrike +10%% (Main Weapon)|r",
"|cff00ff00Attack Speed +10%% (Main Weapon)|nArea +10%% (Main Weapon)|r",
"|cff00ff00Range +10%% (Main Weapon)|nMultistrike +10%% (Main Weapon)|r",
"|cff00ff00Attack Speed +15%% (Main Weapon)|nMultistrike +15%% (Main Weapon)|r",
},
descriptions2 = {
"|cff00ff00Damage +20%% (Main Weapon)|r",
"|cff00ff00Damage +10%% (Main Weapon)|nRange +10%% (Main Weapon)|r",
"|cff00ff00Area +10%% (Main Weapon)|nDamage +10%% (Main Weapon)|r",
"|cff00ff00Damage +10%%|nForce +10%%|r",
},
icon = "TraitSwordsmanWeaponProf.dds",
actions1 = {
function(trait,playerUnit)
local atk = playerUnit.attacks[1]
atk.attackSpeedMult = atk.attackSpeedMult + 0.1
atk.multistrikeMult = atk.multistrikeMult + 0.1
end,
function(trait,playerUnit)
local atk = playerUnit.attacks[1]
atk.attackSpeedMult = atk.attackSpeedMult + 0.1
atk.areaMult = atk.areaMult + 0.1
end,
function(trait,playerUnit)
local atk = playerUnit.attacks[1]
atk.rangeMult = atk.rangeMult + 0.1
atk.multistrikeMult = atk.multistrikeMult + 0.1
end,
function(trait,playerUnit)
local atk = playerUnit.attacks[1]
atk.attackSpeedMult = atk.attackSpeedMult + 0.15
atk.multistrikeMult = atk.multistrikeMult + 0.15
end,
},
actions2 = {
function(trait,playerUnit)
local atk = playerUnit.attacks[1]
atk.damageMult = atk.damageMult + 0.2
end,
function(trait,playerUnit)
local atk = playerUnit.attacks[1]
atk.damageMult = atk.damageMult + 0.1
atk.rangeMult = atk.rangeMult + 0.1
end,
function(trait,playerUnit)
local atk = playerUnit.attacks[1]
atk.areaMult = atk.areaMult + 0.1
atk.damageMult = atk.damageMult + 0.1
end,
function(trait,playerUnit)
local atk = playerUnit.attacks[1]
playerUnit.damageMult = playerUnit.damageMult + 0.1
playerUnit.forceMult = playerUnit.forceMult + 0.1
end,
},
level_max = 4,
level_func = TraitLevelFunction_Eight,
})
swordsman_player = {
name = "Swordsman",
description = function(self)
s = "|cffa0a0a0Focusing on survivability. Scales well with area and range.|n|nDamages enemies in a medium sized cone in the direction aimed.|r|n|n"..
"Stats:"..
"|nHealth: "..tostring(math.floor(self.max_health))..
"|nHealth Regen: "..tostring(self.baseHealthRegen).."/sec"..
"|nDefense: "..tostring(math.floor(self.defense))..
"|nBlock: "..tostring(math.floor(self.block))..
"|n|nWeapon: Sword"..
"|nDamage: "..tostring(math.floor(self.attack.damage))..
"|nRange: "..tostring(math.floor(self.attack.range))..
"|nAttack Speed: "..string.format("%%0.1f",1./self.attack.cooldown).."/sec"..
"|nCone Size: "..tostring(math.floor(self.attack.cone_angle*bj_RADTODEG)).." degrees"
return s
end,
model = FourCC('hfoo'),
size = 32.0,
speed = 2.5,
scale = 1.0,
max_health = 500.0,
baseHealthRegen = 1.0,
defense = 10,
block = 10,
bone_height = 0.0,
anims = {
walk = {6,0.8},
stand = {0,1.5},
attack = {4,1.0},
},
level_function = function(self)
local atk = self.attacks[1]
atk.damage = atk.damage + 0.5
self:add_health(2.5)
if self.level %% 10.0 == 0.0 then
self.damageMult = self.damageMult + 0.05
end
end,
attack = {
cooldown = 1.0,
damage = 100.0,
range = 270.0,
delay = 0.35,
critDamageBonus = 0.65,
critChanceBonus = 0.2,
cone_angle = PI*0.25,
main_attack = true,
properties = AttackProperties.Melee,
type = AttackTypes.Cone,
gain_action = attack_click_event,
hit_sound = SOUND_METAL_SLICE_WOOD,
on_begin_function = function(attack)
local x,y = attack.owner.position[1],attack.owner.position[2]
SOUND_SPEAR_SWING(GetRandomReal(0.9,1.1),true,x,y)
end,
on_hit_function = function(target,attack)
target:stun(attack.owner,0.2)
end,
force = 50,
},
class_traits = {
trait_swordsman_weapon_proficiency,
trait_swordsman_dedication,
trait_swordsman_prof_stance
}
}
PlayerClasses:add(swordsman_player)
end
function ArcherInit()
trait_archer_weapon_prof = Trait.new({
multi_trait = 2,
name = "Weapon Proficiency",
subname1 = "Pierce",
subname2 = "Quickdraw",
descriptions1 = {
"|cff00ff00+1 Base Force (Main Weapon)|r",
"|cff00ff00+0.5 Base Force (Main Weapon)|n+10%% Damage (Main Weapon)|r",
"|cff00ff00+0.5 Base Force (Main Weapon)|n+10%% Damage (Main Weapon)|r",
"|cff00ff00+15%% Crit Damage|r",
},
descriptions2 = {
"|cff00ff00+10%% Attack Speed (Main Weapon)|r",
"|cff00ff00+10%% Attack Speed (Main Weapon)|r",
"|cff00ff00+10%% Attack Speed (Main Weapon)|r",
"|cff00ff00+0.15 Base Attacks (Main Weapon)|r",
},
icon = "TraitArcherWeaponProf.dds",
actions1 = {
function(trait,playerUnit)
local atk = playerUnit.attacks[1]
atk.force = atk.force + 1
end,
function(trait,playerUnit)
local atk = playerUnit.attacks[1]
atk.force = atk.force + 0.5
atk.damageMult = atk.damageMult + 0.1
end,
function(trait,playerUnit)
local atk = playerUnit.attacks[1]
atk.force = atk.force + 0.5
atk.damageMult = atk.damageMult + 0.1
end,
function(trait,playerUnit)
playerUnit.critBonus = playerUnit.critBonus + 0.15
end,
},
actions2 = {
function(trait,playerUnit)
local atk = playerUnit.attacks[1]
atk.attackSpeedMult = atk.attackSpeedMult + 0.1
end,
function(trait,playerUnit)
local atk = playerUnit.attacks[1]
atk.attackSpeedMult = atk.attackSpeedMult + 0.1
end,
function(trait,playerUnit)
local atk = playerUnit.attacks[1]
atk.attackSpeedMult = atk.attackSpeedMult + 0.1
end,
function(trait,playerUnit)
local atk = playerUnit.attacks[1]
atk.attack_count = atk.attack_count + 0.15
end,
},
level_max = 4,
level_func = TraitLevelFunction_Eight,
})
trait_archer_prof_stance = Trait.new({
multi_trait = 2,
name = "Proficient Stance",
subname1 = "Barrage",
subname2 = "Pinpoint",
descriptions1 = {
"|cff00ff00+15%% Multistrike (Main Weapon)|r",
"|cff00ff00+15%% Multistrike (Main Weapon)|r",
"|cff00ff00+15%% Multistrike (Main Weapon)|r",
"|cff00ff00+30%% Multistrike (Main Weapon)|r",
},
descriptions2 = {
"|cff00ff00+5%% Base Crit Chance (Main Weapon)|n+15%% Crit Damage (Main Weapon)|r",
"|cff00ff00+5%% Base Crit Chance (Main Weapon)|n+15%% Crit Damage (Main Weapon)|r",
"|cff00ff00+5%% Base Crit Chance (Main Weapon)|n+15%% Crit Damage (Main Weapon)|r",
"|cff00ff00+10%% Crit Chance|r",
},
icon = "TraitArcherProfStance.dds",
actions1 = {
function(trait,playerUnit)
local atk = playerUnit.attacks[1]
atk.multistrikeMult = atk.multistrikeMult + 0.15
end,
function(trait,playerUnit)
local atk = playerUnit.attacks[1]
atk.multistrikeMult = atk.multistrikeMult + 0.15
end,
function(trait,playerUnit)
local atk = playerUnit.attacks[1]
atk.multistrikeMult = atk.multistrikeMult + 0.15
end,
function(trait,playerUnit)
local atk = playerUnit.attacks[1]
atk.multistrikeMult = atk.multistrikeMult + 0.3
end,
},
actions2 = {
function(trait,playerUnit)
local atk = playerUnit.attacks[1]
atk.critDamageBonus = atk.critDamageBonus + 0.15
atk.critChanceBonus = atk.critChanceBonus + 0.05
end,
function(trait,playerUnit)
local atk = playerUnit.attacks[1]
atk.critDamageBonus = atk.critDamageBonus + 0.15
atk.critChanceBonus = atk.critChanceBonus + 0.05
end,
function(trait,playerUnit)
local atk = playerUnit.attacks[1]
atk.critDamageBonus = atk.critDamageBonus + 0.15
atk.critChanceBonus = atk.critChanceBonus + 0.05
end,
function(trait,playerUnit)
playerUnit.baseCritChance = playerUnit.baseCritChance + .1
end,
},
level_max = 4,
level_func = TraitLevelFunction_Eight,
})
trait_archer_dedication = Trait.new({
multi_trait = 2,
name = "Dedication",
subname1 = "Agility",
subname2 = "Regeneration",
descriptions1 = {
"|cff00ff00+50 Base Health|n+9%% Movement Speed|r",
"|cff00ff00+70 Base Health|n+3 Base Defense|r",
"|cff00ff00+70 Base Health|n+3 Base Block Strength|r",
"|cff00ff00+140 Base Health|r",
},
descriptions2 = {
"|cff00ff00+0.2/s Base Regeneration|n+50 Base Health|r",
"|cff00ff00+0.2/s Base Regeneration|n+50 Base Health|r",
"|cff00ff00+0.2/s Base Regeneration|n+3 Base Defense|r",
"|cff00ff00+0.2/s Base Regeneration|n+6%% Movement Speed|r",
},
icon = "TraitArcherDedication.dds",
actions1 = {
function(trait,playerUnit)
playerUnit:add_health(50)
playerUnit.moveSpeedMult = playerUnit.moveSpeedMult + 0.09
end,
function(trait,playerUnit)
playerUnit.moveSpeedMult = playerUnit.moveSpeedMult + 0.09
playerUnit.defense = playerUnit.defense + 3
end,
function(trait,playerUnit)
playerUnit:add_health(50)
playerUnit.moveSpeedMult = playerUnit.moveSpeedMult + 0.09
end,
function(trait,playerUnit)
playerUnit.moveSpeedMult = playerUnit.moveSpeedMult + 0.18
end,
},
actions2 = {
function(trait,playerUnit)
playerUnit.baseHealthRegen = playerUnit.baseHealthRegen + 0.2
playerUnit:add_health(50)
end,
function(trait,playerUnit)
playerUnit.baseHealthRegen = playerUnit.baseHealthRegen + 0.2
playerUnit:add_health(50)
end,
function(trait,playerUnit)
playerUnit.baseHealthRegen = playerUnit.baseHealthRegen + 0.2
playerUnit.defense = playerUnit.defense + 3
end,
function(trait,playerUnit)
playerUnit.baseHealthRegen = playerUnit.baseHealthRegen + 0.2
playerUnit.moveSpeedMult = playerUnit.moveSpeedMult + 0.09
end,
},
level_max = 4,
level_func = TraitLevelFunction_Eight,
})
archer_player = {
name = "Archer",
description = function(self)
s = "|cffa0a0a0Fast hero with a high critical chance and mutlistrike values.|n|nFires three arrows in a fan in the direction aimed.|r|n|n"..
"Stats:"..
"|nHealth: "..tostring(math.floor(self.max_health))..
"|nHealth Regen: "..tostring(self.baseHealthRegen).."/sec"..
"|nDefense: "..tostring(math.floor(self.defense))..
"|nBlock: "..tostring(math.floor(self.block))..
"|n|nWeapon: Bow"..
"|nDamage: "..tostring(math.floor(self.attack.damage))..
"|nRange: "..tostring(math.floor(self.attack.range))..
"|nAttack Speed: "..string.format("%%0.1f",1./self.attack.cooldown).."/sec"..
"|nForce/Pierce: "..tostring(self.attack.force)..
"|nProjectile Count: "..tostring(math.floor(self.attack.attack_count))
return s
end,
model = FourCC('earc'),
size = 32.0,
speed = 2.75,
scale = 1.0,
max_health = 300.0,
baseHealthRegen = 1.0,
defense = 5,
block = 5,
bone_angle_offset = -HALF_PI*0.5,
anims = {
walk = {7,0.833},
stand = {0,1.6},
attack = {5,1.0},
},
level_function = function(self)
local atk = self.attacks[1]
atk.damage = atk.damage + 0.3
self.moveSpeedMult = self.moveSpeedMult + 0.003
if self.level %% 10.0 == 0.0 then
self.baseCritChance = self.baseCritChance + 0.025
end
end,
attack = {
model = "Abilities\\Weapons\\Arrow\\ArrowMissile.mdl",
speed = 15.0,
cooldown = 1.052631,
area = 32.0,
damage = 50.0,
range = 1024.0,
critDamageBonus = 1.0,
critChanceBonus = 0.33,
delay = 0.5,
force = 3,
attack_count = 3,
main_attack = true,
type = AttackTypes.Missile,
gain_action = attack_click_event,
properties = AttackProperties.Projectile,
on_hit_function = function(target,attack)
target:stun(attack.owner,0.1)
end,
},
class_traits = {
trait_archer_dedication,
trait_archer_prof_stance,
trait_archer_weapon_prof,
}
}
PlayerClasses:add(archer_player)
end
function ExterminatorInit()
trait_exterminator_weapon_prof = Trait.new({
multi_trait = 2,
name = "Weapon Proficiency",
subname1 = "Concentration",
subname2 = "Stream",
descriptions1 = "|cff00ff00+20%% Damage (Main Weapon)|n+10%% Force (Main Weapon)|r",
descriptions2 = "|cff00ff00+15%% Attack Speed (Main Weapon)|r",
icon = "TraitExterminatorWeaponProf.dds",
actions1 = function(trait,playerUnit)
local atk = playerUnit.attacks[1]
atk.damageMult = atk.damageMult + 0.2
atk.forceMult = atk.forceMult + 0.1
end,
actions2 = function(trait,playerUnit)
local atk = playerUnit.attacks[1]
atk.attackSpeedMult = atk.attackSpeedMult + 0.15
end,
level_max = 4,
level_func = TraitLevelFunction_Eight,
})
trait_exterminator_prof_stance = Trait.new({
multi_trait = 2,
name = "Proficient Stance",
subname1 = "Conflagration",
subname2 = "Hellfire",
descriptions1 = "|cff00ff00+6%% Multistrike (Fire)|n +6%% Area (Fire)|r",
descriptions2 = "|cff00ff00+2%% Burn Chance|n+10%% Burn Damage|r",
icon = "TraitExterminatorProfStance.dds",
actions1 = function(trait,playerUnit)
playerUnit.multistrikeMultTable[DamageTypes.Fire] = playerUnit.multistrikeMultTable[DamageTypes.Fire] + 0.06
playerUnit.areaMultTable[DamageTypes.Fire] = playerUnit.areaMultTable[DamageTypes.Fire] + 0.06
end,
actions2 = function(trait,playerUnit)
playerUnit.damageMultTable[DamageTypes.Burn] = playerUnit.damageMultTable[DamageTypes.Burn] + 0.06
playerUnit.burnBonus = playerUnit.burnBonus + 0.1
end,
level_max = 4,
level_func = TraitLevelFunction_Eight,
})
trait_exterminator_dedication = Trait.new({
multi_trait = 2,
name = "Dedication",
subname1 = "Heat",
subname2 = "Steeled",
descriptions1 = "|cff00ff00+10%% Regeneration|n+5%% Movement Speed|r",
descriptions2 = "|cff00ff00+10%% Health|n+10%% Defense|r",
icon = "TraitExterminatorDedication.dds",
actions1 = function(trait,playerUnit)
playerUnit.regenMult = playerUnit.regenMult + 0.1
playerUnit.moveSpeedMult = playerUnit.moveSpeedMult + 0.05
end,
actions2 = function(trait,playerUnit)
playerUnit:set_healthMult(playerUnit.healthMult + 0.1)
playerUnit.defenseMult = playerUnit.defenseMult + 0.1
end,
level_max = 4,
level_func = TraitLevelFunction_Eight,
})
flamecasterMissile = {}
flamecasterMissile.__index = flamecasterMissile
function flamecasterMissile.new(scale,x1,y1,x2,y2,attack,area,half,dur)
local self = setmetatable(NewTable(),flamecasterMissile)
self.sfx = AddSpecialEffect(attack.model,x1-SFX_OFFSET,y1-SFX_OFFSET)
BlzSetSpecialEffectZ(self.sfx,64)
BlzSetSpecialEffectYaw(self.sfx,GetRandomReal(-PI,PI))
BlzSetSpecialEffectScale(self.sfx,scale)
SOUND_FLAME_THROWER:random()(GetRandomReal(0.9,1.1),true,x1,y1)
self.x1 = x1
self.y1 = y1
self.x2 = x2
self.y2 = y2
self.max_dur = dur
self.dur = 0.0
self.attack = attack
self.area = area
self.half = half
self.hits = attack:get_force()
self.reduc = 1.0
self.reducMult = 0.8 * (self.hits/10)
self.hit_list = NewKeyTable()
attack.projectile_set:add(self)
return self
end
function flamecasterMissile:destroy()
self.attack.projectile_set:remove(self)
DestroyEffect(self.sfx)
self.x1 = nil
self.y1 = nil
self.x2 = nil
self.y2 = nil
self.max_dur = nil
self.dur = nil
self.attack = nil
setmetatable(self,nil)
self.area = nil
self.half = nil
self.sfx = nil
self.hits = nil
self.reduc = nil
self.reducMult = nil
ReleaseKeyTable(self.hit_list)
self.hit_list = nil
ReleaseTable(self)
end
local destroyMe = false
local function hit_iter(u,projectile,list,attack)
if list[u] == nil then
list[u] = true
local _, e, dmg = attack:deal_damage(u,projectile.reduc)
projectile.reduc = projectile.reduc * projectile.reducMult
projectile.hits = projectile.hits - 1
if (dmg and dmg <= 0) or projectile.hits <= 0 then destroyMe = true end
end
end
local function move_projectile(self)
for projectile in self.projectile_set:elements() do
projectile.dur = math.min(projectile.dur + DEFAULT_TIMEOUT,projectile.max_dur)
local t = easeOutX(projectile.dur/projectile.max_dur,2)
local half = projectile.half
local x = lerp(projectile.x1,projectile.x2,t)
local y = lerp(projectile.y1,projectile.y2,t)
destroyMe = false
unit_collision_hash:each(x-half,y-half,projectile.area,projectile.area,hit_iter,projectile,projectile.hit_list,self)
BlzSetSpecialEffectPosition(projectile.sfx,x-SFX_OFFSET,y-SFX_OFFSET,64)
if projectile.dur == projectile.max_dur or destroyMe or unit_collision_hash:cell_occupied(x, y, 0, 0,Types.ProjectileStopper) then
projectile:destroy()
end
end
self.timer:callDelayed(DEFAULT_TIMEOUT, move_projectile,self)
end
local function checkSound(self)
if not self.attacking and self.snd then
SOUND_BURN_LOOP:stop(self.snd)
self.snd = nil
end
end
local function start(self,pdata,state)
self:start(pdata,state)
if state then
if not self.snd then
self.snd = SOUND_BURN_LOOP(1.0,true,nil,nil,self.owner.sfx)
end
else
self.timer:callDelayed(1.5, checkSound,self)
end
end
exterminator_player = {
name = "Exterminator",
description = function(self)
s = "|cffa0a0a0Sets enemies on fire and focuses on burn chance and damage.|n|nRapidly emit flame that set enemies on fire.|r|n|n"..
"Stats:"..
"|nHealth: "..tostring(math.floor(self.max_health))..
"|nHealth Regen: "..tostring(self.baseHealthRegen).."/sec"..
"|nDefense: "..tostring(math.floor(self.defense))..
"|nBlock: "..tostring(math.floor(self.block))..
"|n|nWeapon: Flamecaster"..
"|nDamage: "..tostring(math.floor(self.attack.damage))..
"|nRange: "..tostring(math.floor(self.attack.range))..
"|nAttack Speed: "..string.format("%%0.1f",1./self.attack.cooldown).."/sec"..
"|nForce/Pierce: "..tostring(self.attack.force)..
"|nProjectile Count: "..tostring(math.floor(self.attack.attack_count))..
"|nBurn Chance: "..tostring(self.attack.effectChance*100).."%%"
return s
end,
model = FourCC('hrif'),
size = 32.0,
speed = 2.5,
scale = 1.0,
max_health = 300.0,
baseHealthRegen = 1.0,
defense = 5,
block = 5,
bone_angle_offset = -HALF_PI*.9,
bone_height = 0.0,
anims = {
walk = {5,0.833},
stand = {0,1.333},
attack = {3,0.867},
},
level_function = function(self)
local atk = self.attacks[1]
atk.damage = atk.damage + 0.25
atk.effectChance = atk.effectChance + 0.001
self.damageMultTable[DamageTypes.Burn] = self.damageMultTable[DamageTypes.Burn] + 0.005
self.defenseMult = self.defenseMult + 0.005
end,
attack = {
model = "Flamethrower.mdx",
cooldown = 0.333,
area = 64.0,
damage = 50.0,
range = 350.0,
critChanceBonus = 0.05,
critDamageBonus = 1.0,
delay = 0.05,
force = 10,
attack_count = 1.0,
main_attack = true,
damageType = DamageTypes.Fire,
type = AttackTypes.Missile,
gain_action = attack_click_event,
properties = AttackProperties.Projectile,
effectChance = 0.15,
effect_function = attack_effect_burn,
finished = function(self)
local range = self:get_range()
local x,y = self.owner.position[1], self.owner.position[2]
local area = self:get_area()
local half = area * 0.5
local count = self:increment_count(self)
local dur = 1.5 * (self:get_force() / 10 + range/350) * 0.5
local yaw = self.owner.yaw
local scale = self:get_scale()
local angleScale = 0.1 * scale
local a = math.min(angleScale * count,TAU)
local angleBetweenProjectiles = a / (count - 1)
local startingAngle = yaw - (a / 2)
local j = 0
for i = 0, count - 1 do
a = startingAngle + (angleBetweenProjectiles * i)
local x2,y2 = x + math.cos(a) * range, y + math.sin(a) * range
self.timer:callDelayed(j,flamecasterMissile.new,scale,x,y,x2,y2,self,area,half,dur)
j = j + 0.015
end
end,
gain_action = function(self)
self.projectile_set = Set.create()
self.timer:callDelayed(DEFAULT_TIMEOUT, move_projectile,self)
local event = Mouse.press_event:add_action(start,self)
table.insert(self.event_actions,event)
end,
destroy_func = function(self)
for projectile in self.projectile_set:elements() do
projectile:destroy()
end
if self.snd then
SOUND_BURN_LOOP:stop(self.snd,true)
self.snd = nil
end
self.projectile_set:destroy()
self.projectile_set = nil
end,
move = function(self,x,y)
for projectile in self.projectile_set:elements() do
projectile.x1 = projectile.x1 + x
projectile.y1 = projectile.y1 + y
projectile.x2 = projectile.x2 + x
projectile.y2 = projectile.y2 + y
end
end,
},
class_traits = {
trait_exterminator_weapon_prof,
trait_exterminator_prof_stance,
trait_exterminator_dedication,
}
}
PlayerClasses:add(exterminator_player)
end
function WarlockInit()
trait_warlock_weapon_prof = Trait.new({
multi_trait = 2,
name = "Weapon Proficiency",
subname1 = "Amplify",
subname2 = "Channeling",
descriptions1 = "|cff00ff00+10%% Damage (Main Weapon)|n+10%% Multistrike (Main Weapon)|r",
descriptions2 = "|cff00ff00+10%% Attack Speed (Main Weapon)|n+10%% Damage (Main Weapon)|r",
icon = "TraitWarlockWeaponProf.dds",
actions1 = function(trait,playerUnit)
local atk = playerUnit.attacks[1]
atk.damageMult = atk.damageMult + 0.1
atk.multistrikeMult = atk.multistrikeMult + 0.1
end,
actions2 = function(trait,playerUnit)
local atk = playerUnit.attacks[1]
atk.damageMult = atk.damageMult + 0.1
atk.attackSpeedMult = atk.attackSpeedMult + 0.1
end,
level_max = 4,
level_func = TraitLevelFunction_Eight,
})
trait_warlock_prof_stance = Trait.new({
multi_trait = 2,
name = "Proficient Stance",
subname1 = "Sacrifice",
subname2 = "Summon Vigor",
descriptions1 = "|cff00ff00+20%% Summon Count|r|n|cffff0000-20%% Damage (Main Weapon)|r",
descriptions2 = "|cff00ff00+20%% Damage (Summons)|r",
icon = "TraitWarlockProfStance.dds",
actions1 = function(trait,playerUnit)
local atk = playerUnit.attacks[1]
atk.damageMult = atk.damageMult - 0.2
playerUnit.summonCountMult = playerUnit.summonCountMult + 0.2
end,
actions2 = function(trait,playerUnit)
playerUnit.summonDamageMult = playerUnit.summonDamageMult + 0.2
end,
level_max = 4,
level_func = TraitLevelFunction_Eight,
})
trait_warlock_dedication = Trait.new({
multi_trait = 2,
name = "Dedication",
subname1 = "Blood Letting",
subname2 = "Defensive",
descriptions1 = "|cff00ff00+20%% Force|r|n|cffff0000-10%% Health|r",
descriptions2 = "|cff00ff00+5%% Health|n+5%% Defense|n+5%% Movement Speed|r",
icon = "TraitWarlockDedication.dds",
actions1 = function(trait,playerUnit)
playerUnit:set_healthMult(playerUnit.healthMult - 0.1)
playerUnit.forceMult = playerUnit.forceMult + 0.2
end,
actions2 = function(trait,playerUnit)
playerUnit:set_healthMult(playerUnit.healthMult + 0.05)
playerUnit.defenseMult = playerUnit.defenseMult + 0.05
playerUnit.moveSpeedMult = playerUnit.moveSpeedMult + 0.05
end,
level_max = 4,
level_func = TraitLevelFunction_Eight,
})
ravagingSpirit = {}
ravagingSpirit.__index = ravagingSpirit
function ravagingSpirit.new(scale,x,y,speed,yaw,attack,area,half,dur,hits)
local self = setmetatable(NewTable(),ravagingSpirit)
self.sfx = AddSpecialEffect(attack.model,x-SFX_OFFSET,y-SFX_OFFSET)
BlzSetSpecialEffectZ(self.sfx,32)
BlzSetSpecialEffectYaw(self.sfx,yaw)
BlzSetSpecialEffectScale(self.sfx,scale)
self.x = x
self.y = y
self.dur = dur
self.yaw = yaw
self.attack = attack
self.area = area
self.half = half
self.hits = hits
self.speed = speed
self.targetSwitch = 0
self.hit_list = NewKeyTable()
self.target = attack.owner
attack.projectile_set:add(self)
attack:update_summoned(1,true)
return self
end
function ravagingSpirit:destroy()
self.attack:update_summoned(-1,true)
self.attack.projectile_set:remove(self)
DestroyEffect(self.sfx)
self.x = nil
self.y = nil
self.dur = nil
self.attack = nil
self.area = nil
self.yaw = nil
self.half = nil
self.speed = nil
self.targetSwitch = nil
self.sfx = nil
self.hits = nil
self.target = nil
ReleaseKeyTable(self.hit_list)
self.hit_list = nil
setmetatable(self,nil)
ReleaseTable(self)
end
local destroyMe = false
local function hit_iter(u,projectile,list,attack)
if not list[u] or list[u] < GAME_TIME then
list[u] = GAME_TIME + 1.0
attack:deal_damage(u)
projectile.hits = projectile.hits - 1
end
end
local function move_projectile(self)
for projectile in self.projectile_set:elements() do
projectile.dur = projectile.dur - DEFAULT_TIMEOUT
local x,y = projectile.x, projectile.y
if projectile.targetSwitch < GAME_TIME or not projectile.target.position then
projectile.targetSwitch = projectile.targetSwitch + 0.75
projectile.target = unit_collision_hash:get_first(x - 512, y - 512, 1024, 1024,Types.Damageable) or self.owner
end
local yaw = change_angle_bounded(projectile.yaw,math.atan(projectile.target.position[2]-y,projectile.target.position[1]-x),0.05)
BlzSetSpecialEffectYaw(projectile.sfx,yaw)
x = x + math.cos(yaw) * projectile.speed
y = y + math.sin(yaw) * projectile.speed
projectile.x = x
projectile.y = y
projectile.yaw = yaw
unit_collision_hash:each(x - projectile.half, y - projectile.half, projectile.area, projectile.area, hit_iter,projectile,projectile.hit_list,self)
BlzSetSpecialEffectPosition(projectile.sfx,x-SFX_OFFSET,y-SFX_OFFSET,32)
if projectile.dur <= 0 or projectile.hits <= 0 then
projectile:destroy()
end
end
self.timer:callDelayed(DEFAULT_TIMEOUT, move_projectile,self)
end
local function summon(self,j,scale,x,y,speed,yaw,area,half,dur,force)
self.timer:callDelayed(j,ravagingSpirit.new,scale,x,y,speed,yaw,self,area,half,dur,force)
SOUND_SUMMON_GHOST(GetRandomReal(0.9,1.1),true,x,y)
end
warlock_player = {
name = "Warlock",
description = function(self)
s = "|cffa0a0a0Vulnerable, but has additional damage options. Focused on summons.|n|nSummons ghostly projectiles that home in on nearby enemies.|r|n|n"..
"Stats:"..
"|nHealth: "..tostring(math.floor(self.max_health))..
"|nHealth Regen: "..tostring(self.baseHealthRegen).."/sec"..
"|nDefense: "..tostring(math.floor(self.defense))..
"|nBlock: "..tostring(math.floor(self.block))..
"|n|nWeapon: Ravaging Spectres"..
"|nDamage: "..tostring(math.floor(self.attack.damage))..
"|nRange: "..tostring(math.floor(self.attack.range))..
"|nAttack Speed: "..string.format("%%0.1f",1./self.attack.cooldown).."/sec"..
"|nForce/Pierce: "..tostring(self.attack.force)..
"|nSummon Count: "..tostring(math.floor(self.attack.summon_count))
return s
end,
model = FourCC('nmed'),
size = 32.0,
speed = 2.5,
scale = 1.0,
max_health = 300.0,
baseHealthRegen = 0.0,
defense = 5,
block = 5,
bone_angle_offset = 0,
bone_height = 0.0,
anims = {
walk = {1,0.833},
stand = {0,1.333},
attack = {5,0.867},
},
level_function = function(self)
local atk = self.attacks[1]
atk.damage = atk.damage + 0.25
atk.effectChance = atk.effectChance + 0.1
self.damageMultTable[DamageTypes.Burn] = self.damageMultTable[DamageTypes.Burn] + 0.05
self.defense = self.defense + 0.05
end,
attack = {
model = "GreenGhostSummons.mdx",
speed = 25.0,
cooldown = 1.666,
area = 96.0,
damage = 50.0,
range = 350.0,
base_scale = 0.6,
critChanceBonus = 0.1,
critDamageBonus = 1.0,
delay = 0.25,
force = 100,
summon_count = 2.0,
main_attack = true,
damageType = DamageTypes.Magical,
type = AttackTypes.Missile,
gain_action = attack_click_event,
properties = AttackProperties.Projectile + AttackProperties.Summon,
finished = function(self)
local range = self:get_range()
local x,y = self.owner.position[1], self.owner.position[2]
local area = self:get_area()
local half = area * 0.5
local count = self:increment_count(self)
local force = self:get_force()
local dur = 2.5 * (self:get_force() / 100)
local speed = self:get_speed()
local yaw = self.owner.yaw
local scale = self:get_scale()
local j = 0
for i = 0, count - 1 do
summon(self,j,scale,x,y,speed,yaw,area,half,dur,force)
j = j + 0.1
end
end,
gain_action = function(self)
self.projectile_set = Set.create()
self.summon_is_attack = true
self.timer:callDelayed(DEFAULT_TIMEOUT, move_projectile,self)
local event = Mouse.press_event:add_action(self.start,self)
table.insert(self.event_actions,event)
end,
destroy_func = function(self)
for projectile in self.projectile_set:elements() do
projectile:destroy()
end
self.projectile_set:destroy()
self.projectile_set = nil
self.summon_is_attack = nil
end,
move = function(self,x,y)
for projectile in self.projectile_set:elements() do
projectile.x = projectile.x + x
projectile.y = projectile.y + y
end
end,
},
class_traits = {
trait_warlock_weapon_prof,
trait_warlock_prof_stance,
trait_warlock_dedication,
}
}
PlayerClasses:add(warlock_player)
end
function SorceressInit()
trait_sorceress_weapon_prof = Trait.new({
multi_trait = 2,
name = "Weapon Proficiency",
subname1 = "Strength",
subname2 = "Strikes",
descriptions1 = "|cff00ff00+30%% Damage (Main Weapon)|r",
descriptions2 = "|cff00ff00+6%% Attack Speed (Main Weapon)|n+10%% Force (Main Weapon)|n+6%% Multistrike (Main Weapon)|r",
icon = "TraitSorceressWeaponProf.dds",
actions1 = function(trait,playerUnit)
local atk = playerUnit.attacks[1]
atk.damageMult = atk.damageMult + 0.3
end,
actions2 = function(trait,playerUnit)
local atk = playerUnit.attacks[1]
atk.forceMult = atk.forceMult + 0.1
atk.multistrikeMult = atk.multistrikeMult + 0.06
atk.attackSpeedMult = atk.attackSpeedMult + 0.06
end,
level_max = 4,
level_func = TraitLevelFunction_Eight,
})
trait_sorceress_prof_stance = Trait.new({
multi_trait = 2,
name = "Proficient Stance",
subname1 = "Stability",
subname2 = "Voltage",
descriptions1 = "|cff00ff00+20%% Crit Chance (Lightning)|r",
descriptions2 = "|cff00ff00+2%% Electrify Chance|n+10%% Electrify Damage|r",
icon = "TraitSorceressProfStance.dds",
actions1 = function(trait,playerUnit)
playerUnit.critMultTable[DamageTypes.Electrify] = playerUnit.critMultTable[DamageTypes.Electrify] + 0.2
end,
actions2 = function(trait,playerUnit)
playerUnit.damageMultTable[DamageTypes.Electrify] = playerUnit.damageMultTable[DamageTypes.Electrify] + 0.1
playerUnit.electricBonus = playerUnit.electricBonus + 0.1
end,
level_max = 4,
level_func = TraitLevelFunction_Eight,
})
trait_sorceress_dedication = Trait.new({
multi_trait = 2,
name = "Dedication",
subname1 = "Nimble",
subname2 = "Protection",
descriptions1 = "|cff00ff00+10%% Movement Speed|r",
descriptions2 = "|cff00ff00+10%% Health|n+10%% Block Strength|r",
icon = "TraitSorceressDedication.dds",
actions1 = function(trait,playerUnit)
playerUnit.moveSpeedMult = playerUnit.moveSpeedMult + 0.1
end,
actions2 = function(trait,playerUnit)
playerUnit:set_healthMult(playerUnit.healthMult + 0.1)
playerUnit.blockMult = playerUnit.blockMult + 0.1
end,
level_max = 4,
level_func = TraitLevelFunction_Eight,
})
local function timedLightning(duration,source,updatingVec,z)
if not source.yaw then
return
end
local x = source.position[1] or 0
local y = source.position[2] or 0
x = x + math.cos(source.yaw) * 50
y = y + math.sin(source.yaw) * 50
l = AddLightningEx('CLSB',true,x,y,z,updatingVec.x,updatingVec.y,0)
local sfx = AddSpecialEffect("BlueBloodLustTarget2.mdx",x,y)
BlzSetSpecialEffectZ(sfx,32)
DestroyEffectEx(sfx)
sfx = AddSpecialEffect("BlueBloodLustTarget2.mdx",updatingVec.x,updatingVec.y)
BlzSetSpecialEffectZ(sfx,32)
DestroyEffectEx(sfx)
ReleaseUpdatingVector(updatingVec)
DEFAULT_TIMER:callDelayed(duration, DestroyLightning,l)
end
sorceress_player = {
name = "Sorceress",
description = function(self)
s = "|cffa0a0a0Mistress of Lightning, electrifying enemies like no other. Nimble and good at blocking.|n|nCasts multiple lightning bolts that jump from one target to another.|r|n|n"..
"Stats:"..
"|nHealth: "..tostring(math.floor(self.max_health))..
"|nHealth Regen: "..tostring(self.baseHealthRegen).."/sec"..
"|nDefense: "..tostring(math.floor(self.defense))..
"|nBlock: "..tostring(math.floor(self.block))..
"|n|nWeapon: Chain Lightning"..
"|nDamage: "..tostring(math.floor(self.attack.damage))..
"|nRange: "..tostring(math.floor(self.attack.range*2))..
"|nAttack Speed: "..string.format("%%0.1f",1./self.attack.cooldown).."/sec"..
"|nLightning Jumps: "..tostring(self.attack.force)..
"|nAttack Count: "..tostring(math.floor(self.attack.attack_count))..
"|nElectrify Chance: "..tostring(self.attack.effectChance*100).."%%"
return s
end,
model = FourCC('hsor'),
size = 32.0,
speed = 2.75,
scale = 1.0,
max_health = 300.0,
baseHealthRegen = 0.5,
defense = 0,
block = 20,
bone_angle_offset = 0,
bone_height = -100,
anims = {
walk = {5,2.0},
stand = {0,1.5},
attack = {3,1.4},
},
level_function = function(self)
local atk = self.attacks[1]
atk.damage = atk.damage + 0.5
atk.critChanceBonus = atk.critChanceBonus + 0.003
self.blockMult = self.blockMult + 0.005
self.damageMultTable[DamageTypes.Electrify] = self.damageMultTable[DamageTypes.Electrify] + 0.005
end,
attack = {
cooldown = 1.0,
area = 96.0,
damage = 100.0,
range = 150.0,
base_scale = 1.0,
critChanceBonus = 0.33,
critDamageBonus = .25,
delay = 0.75,
force = 3,
attack_count = 2.0,
main_attack = true,
damageType = DamageTypes.Lightning,
effectChance = 0.15,
effect_function = attack_effect_electrify,
type = AttackTypes.Missile,
gain_action = attack_click_event,
properties = AttackProperties.Elemental,
finished = function(self)
local pdata = self.owner.pdata
local x1, y1 = pdata.mouse_pos[1], pdata.mouse_pos[2]
local x,y = self.owner.position[1], self.owner.position[2]
local range = math.min(self:get_range()*2,DistanceTo(x1,y1,x,y))
local count = self:increment_count(self)
local force = self:get_force()
local yaw = self.owner.yaw
local j = 0
SOUND_CHAIN_LIGHTNING(GetRandomReal(1.0,1.2),true,x1,y1)
for i = 0, count - 1 do
x1 = x + math.cos(yaw+GetRandomReal(-0.436332,0.436332)) * range
y1 = y + math.sin(yaw+GetRandomReal(-0.436332,0.436332)) * range
DEFAULT_TIMER:callDelayed(j,timedLightning,0.2,self.owner,GetUpdatingVector(x1,y1),32)
self.timer:callDelayed(j,chain_lightning,self,GetUpdatingVector(x1,y1),force,NewKeyTable())
j = j + 0.1
end
end,
gain_action = function(self)
local event = Mouse.press_event:add_action(self.start,self)
table.insert(self.event_actions,event)
end
},
class_traits = {
trait_sorceress_weapon_prof,
trait_sorceress_prof_stance,
trait_sorceress_dedication,
}
}
PlayerClasses:add(sorceress_player)
end
function ClericInit()
trait_cleric_dedication = Trait.new({
multi_trait = 2,
name = "Dedication",
subname1 = "Passible",
subname2 = "Penitent",
descriptions1 = "|cff00ff00+10%% Health|n+10%% Defense|r",
descriptions2 = "|cff00ff00+20%% Health Regeneration|r",
icon = "TraitClericDedication.dds",
actions1 = function(trait,playerUnit)
playerUnit:set_healthMult(playerUnit.healthMult + 0.1)
playerUnit.defenseMult = playerUnit.defenseMult + 0.1
end,
actions2 = function(trait,playerUnit)
playerUnit.regenMult = playerUnit.regenMult + 0.2
end,
level_max = 4,
level_func = TraitLevelFunction_Eight,
})
trait_cleric_prof_stance = Trait.new({
multi_trait = 2,
name = "Proficient Stance",
subname1 = "Purgation",
subname2 = "Purification",
descriptions1 = "|cff00ff00+20%% Damage (Magic)|r",
descriptions2 = "|cff00ff00+10%% Attack Speed (Magic)|r",
icon = "TraitClericProfStance.dds",
actions1 = function(trait,playerUnit)
playerUnit.damageMultTable[DamageTypes.Magical] = playerUnit.damageMultTable[DamageTypes.Magical] + 0.2
end,
actions2 = function(trait,playerUnit)
playerUnit.attackSpeedTable[DamageTypes.Magical] = playerUnit.attackSpeedTable[DamageTypes.Magical] + 0.1
end,
level_max = 4,
level_func = TraitLevelFunction_Eight,
})
trait_cleric_weapon_proficiency = Trait.new({
multi_trait = 2,
name = "Weapon Proficiency",
subname1 = "Propagation",
subname2 = "Punishment",
descriptions1 = "|cff00ff00+30%% Debuff Chance (Main Weapon)|r",
descriptions2 = "|cff00ff00+30%% Damage (Main Weapon)|r",
icon = "TraitClericWeaponProf.dds",
actions1 = function(trait,playerUnit)
local atk = playerUnit.attacks[1]
atk.effectChance = atk.effectChance + 0.3
end,
actions2 = function(trait,playerUnit)
local atk = playerUnit.attacks[1]
atk.damageMult = atk.damageMult + 0.3
end,
level_max = 4,
level_func = TraitLevelFunction_Eight,
})
local function cone_iter(u,attack,x,y,angle,cone_angle,range,list)
if not Types.is(u,Types.Damageable) then return end
local x1 = u.position[1]
local y1 = u.position[2]
if isPointInsideCone(x, y, angle, cone_angle, x1, y1) and IsInRange(x1, y1,x,y,range) then
table.insert(list,u)
end
end
local function cone_attack(attack,half,count)
local owner = attack.owner
local x,y = owner.position[1],owner.position[2]
local area = half * 2.0
local dir = owner.yaw
local cone_angle = attack:get_cone_angle()
local list = NewTable()
unit_collision_hash:each(x - half, y - half, area, area, cone_iter,attack,x,y,dir,cone_angle,half,list)
local ind = AreaIndicator.new(AreaIndicatorTypes.Cone,x,y,half,dir,cone_angle):destroy(0.1)
BlzSetSpecialEffectColor(ind.sfx,255,255,0)
SOUND_CLERIC_ATTACK:random()(GetRandomReal(1.0,1.2),true,x,y)
local dmg = attack:get_damage()/#list
for i, u in ipairs(list) do
local x,y,z = u.position[1],u.position[2],u.position[3] or 0
local scale = u:get_scale()
attack:deal_damage(u,nil,dmg)
if getmetatable(u) == nil then
local sfx = AddSpecialEffect("Abilities\\Spells\\Undead\\VampiricAura\\VampiricAuraTarget.mdl",x,y)
BlzSetSpecialEffectZ(sfx,z)
BlzSetSpecialEffectScale(sfx,scale)
DestroyEffect(sfx)
end
end
if count > 1 then
attack.timer:callDelayed(0.15, cone_attack,attack,half,count-1)
end
end
cleric_player = {
name = "Cleric",
description = function(self)
s = "|cffa0a0a0Weakens enemies and has high regeneration values.|n|nDeals damage in a cone, equally split among all hit foes.|r|n|n"..
"Stats:"..
"|nHealth: "..tostring(math.floor(self.max_health))..
"|nHealth Regen: "..tostring(self.baseHealthRegen).."/sec"..
"|nDefense: "..tostring(math.floor(self.defense))..
"|nBlock: "..tostring(math.floor(self.block))..
"|n|nWeapon: Sword"..
"|nDamage: "..tostring(math.floor(self.attack.damage))..
"|nRange: "..tostring(math.floor(self.attack.range))..
"|nAttack Speed: "..string.format("%%0.1f",1./self.attack.cooldown).."/sec"..
"|nCone Size: "..tostring(math.floor(self.attack.cone_angle*bj_RADTODEG)).." degrees"
return s
end,
model = FourCC('hmpr'),
size = 32.0,
speed = 2.25,
scale = 1.0,
max_health = 400.0,
baseHealthRegen = 1.0,
defense = 0,
block = 0,
bone_height = 0.0,
anims = {
walk = {1,0.933},
stand = {0,1.5},
attack = {5,1.167},
},
level_function = function(self)
local atk = self.attacks[1]
atk.damage = atk.damage + 5
atk.attack_count = atk.attack_count + 0.02
self.damageMult = self.damageMult + 0.005
self.regenMult = self.regenMult + 0.005
end,
attack = {
cooldown = 1.5,
damage = 1000.0,
range = 330.0,
delay = 0.35,
critDamageBonus = 0.5,
critChanceBonus = 0.2,
cone_angle = 1.0472,
main_attack = true,
effectChance = 0.5,
damageType = DamageTypes.Magical,
effect_function = attack_effect_fragile_afflict,
properties = AttackProperties.Melee,
type = AttackTypes.Unique,
gain_action = attack_click_event,
finished = function(self)
cone_attack(self,self:get_range(),self:increment_count(self))
end,
force = 20,
},
class_traits = {
trait_cleric_weapon_proficiency,
trait_cleric_dedication,
trait_cleric_prof_stance
}
}
PlayerClasses:add(cleric_player)
end
local last
local permarooms = {}
local permaroomSize = {}
DUNGEON_CELL_OPEN = 1
DUNGEON_CELL_CLOSED = 2
DUNGEON_CELL_BLACKLIST =3
function DungeonResetPermaRooms()
permarooms = {}
end
function DungeonAddPermaRoom(x,y,size)
size = size or 2
permarooms[x + y*1e3] = size
end
function GenerateDungeon(x,y,ROOM_MIN,ROOM_MAX,roomChance,CHUNK_SIZE_X,CHUNK_SIZE_Y)
local map = NewTable()
local room_centerX = NewTable()
local room_centerY = NewTable()
local rooms = 0
local maxGrowth = math.max(ROOM_MAX * 2,9) * 2
local minDist = ROOM_MIN * ROOM_MIN
local doubleMax = maxGrowth * 2
for i = -maxGrowth, CHUNK_SIZE_X + doubleMax do
if last then
for j = -maxGrowth, CHUNK_SIZE_Y + doubleMax do
last[i][j] = nil
end
ReleaseTable(last[i])
last[i] = nil
end
map[i] = NewTable()
end
if last then
ReleaseTable(last)
end
last = map
for x1 = -maxGrowth, CHUNK_SIZE_X + maxGrowth do
for y1 = -maxGrowth, CHUNK_SIZE_Y + maxGrowth do
local r = math.floor(x1 + x + (y + y1) * 1e3)
SetRandomSeed(r)
if GetRandomReal(0,1) > roomChance or permarooms[r] then
local roomX, roomY
local which = DUNGEON_CELL_OPEN
if permarooms[r] then
roomX = permarooms[r]
roomY = permarooms[r]
which = DUNGEON_CELL_BLACKLIST
else
which = DUNGEON_CELL_OPEN
roomX = GetRandomInt(ROOM_MIN,ROOM_MAX)-1
roomY = GetRandomInt(ROOM_MIN,ROOM_MAX)-1
end
rooms = rooms + 1
room_centerX[rooms] = x1 + roomX // 2
room_centerY[rooms] = y1 + roomY // 2
for x2 = 0, roomX do
local x3 = x2+x1
for y2 = 0, roomY do
local y3 = y2+y1
map[x3][y3]= which
end
end
elseif not map[x1][y1] then
map[x1][y1]= DUNGEON_CELL_CLOSED
end
end
end
while rooms > 1 do
local dist = math.huge
local room
local curRoom = rooms
rooms = rooms -1
for i = 1, curRoom -1 do
local curDist = DistanceSquaredTo(room_centerX[i],room_centerY[i],room_centerX[curRoom],room_centerY[curRoom])
if curDist <= minDist then
goto continue
end
if curDist <= dist then
dist = curDist
room = i
end
end
if room then
local x1,y1 = room_centerX[room],room_centerY[room]
local x2,y2 = room_centerX[curRoom],room_centerY[curRoom]
dirX = x1 > x2 and -1 or 1
for x3 = x1, x2, dirX do
map[x3][y1]= true
end
dirY = y1 > y2 and -1 or 1
for y3 = y1, y2, dirY do
map[x2][y3]= true
end
else
return map
end
::continue::
end
ReleaseTable(room_centerX)
ReleaseTable(room_centerY)
return map
end
Layout = {}
Layout.TOP_LEFT = 1
Layout.TOP_MIDDLE = 2
Layout.TOP_RIGHT = 4
Layout.MIDDLE_RIGHT = 8
Layout.BOTTOM_RIGHT = 16
Layout.BOTTOM_MIDDLE = 32
Layout.BOTTOM_LEFT = 64
Layout.MIDDLE_LEFT = 128
Layout[1] = "TOP_LEFT"
Layout[2] = "TOP_MIDDLE"
Layout[3] = "TOP_RIGHT"
Layout[4] = "MIDDLE_RIGHT"
Layout[5] = "BOTTOM_RIGHT"
Layout[6] = "BOTTOM_MIDDLE"
Layout[7] = "BOTTOM_LEFT"
Layout[8] = "MIDDLE_LEFT"
local rule = {}
function CheckLayout(map,x,y,truX,truY,make)
local thisLayout =
(map[x][y+1] == DUNGEON_CELL_CLOSED and Layout.TOP_MIDDLE or 0) +
(map[x][y-1] == DUNGEON_CELL_CLOSED and Layout.BOTTOM_MIDDLE or 0) +
(map[x+1][y] == DUNGEON_CELL_CLOSED and Layout.MIDDLE_RIGHT or 0) +
(map[x-1][y] == DUNGEON_CELL_CLOSED and Layout.MIDDLE_LEFT or 0) +
(map[x-1][y+1] == DUNGEON_CELL_CLOSED and Layout.TOP_LEFT or 0) +
(map[x-1][y-1] == DUNGEON_CELL_CLOSED and Layout.BOTTOM_LEFT or 0) +
(map[x+1][y+1] == DUNGEON_CELL_CLOSED and Layout.TOP_RIGHT or 0) +
(map[x+1][y-1] == DUNGEON_CELL_CLOSED and Layout.BOTTOM_RIGHT or 0)
local d
if rule[thisLayout] ~= nil then
if not make then return true end
d = rule[thisLayout](truX,truY,map,x,y)
end
return d, thisLayout
end
function GetCellNeighbors(map,x,y)
return
(map[x][y+1] == DUNGEON_CELL_CLOSED and 1 or 0) +
(map[x][y-1] == DUNGEON_CELL_CLOSED and 1 or 0) +
(map[x+1][y] == DUNGEON_CELL_CLOSED and 1 or 0) +
(map[x-1][y] == DUNGEON_CELL_CLOSED and 1 or 0) +
(map[x-1][y+1] == DUNGEON_CELL_CLOSED and 1 or 0) +
(map[x-1][y-1] == DUNGEON_CELL_CLOSED and 1 or 0) +
(map[x+1][y+1] == DUNGEON_CELL_CLOSED and 1 or 0) +
(map[x+1][y-1] == DUNGEON_CELL_CLOSED and 1 or 0)
end
function RotateRule(flags,dest_type, initial_rotation)
for i = 1, 4 do
local rot = initial_rotation - HALF_PI * (i-1)
local flag = 0
local inc = (i-1) * 2
for j, v in ipairs(flags) do
flag = flag + Layout[Layout[wrap(v+inc,1,8)]]
end
rule[flag] = function(x,y,map,x2,y2)
return Destructable.new(dest_type,x,y,rot)
end
end
end
RotateRule({1,2,3,4,8},destructable_dungeon_wall,4.71239)
RotateRule({1,2,3,4,8,5},destructable_dungeon_wall,4.71239)
RotateRule({1,2,3,4,8,7},destructable_dungeon_wall,4.71239)
RotateRule({1,2,3,4,8,7,5},destructable_dungeon_wall,4.71239)
RotateRule({2,3,4},destructable_dungeon_corner_outer,PI)
RotateRule({2,3,4,1},destructable_dungeon_corner_outer,PI)
RotateRule({2,3,4,5},destructable_dungeon_corner_outer,PI)
RotateRule({1,2,3,4,5,6,8},destructable_dungeon_corner_inner,4.71239)
RotateRule({1,2,3,4,5,6,7,8},destructable_blackness,4.71239)
local function decimate_check(map,x,y)
if map[x][y] == DUNGEON_CELL_CLOSED and not CheckLayout(map,x,y)then
map[x][y] = DUNGEON_CELL_OPEN
decimate_check(map,x+1,y)
decimate_check(map,x-1,y)
decimate_check(map,x,y+1)
decimate_check(map,x,y-1)
decimate_check(map,x-1,y+1)
decimate_check(map,x+1,y-1)
decimate_check(map,x-1,y-1)
decimate_check(map,x+1,y+1)
end
end
local function delay1(map,CHUNK_SIZE_X,CHUNK_SIZE_Y,world_x,world_y,length,width,length_half,width_half,offset_x,offset_y)
WorldGeneration.destroyOld(offset_x,offset_y)
for x = 1, CHUNK_SIZE_X do
local truX = x * 128 - length_half
local x2 = to_tile(truX + length * world_x)
for y = 1, CHUNK_SIZE_Y do
local truY = y * 128 - width_half
local y2 = to_tile(truY + width * world_y)
local d, layout
local val = map[x][y]
if val == DUNGEON_CELL_CLOSED then
d, layout = CheckLayout(map,x,y,truX,truY,true)
elseif val ~= DUNGEON_CELL_BLACKLIST then
local n = GetCellNeighbors(map,x,y)
local ind = math.floor(wrap(x2,0,9999) + wrap(y2,0,9999) * 1e5)
SetRandomSeed(ind)
local r = GetRandomReal(0,1)
if r < 0.01 then
d = Destructable.new(destructable_torch,truX,truY,GetRandomReal(0,TAU))
BlzSetSpecialEffectPitch(d.sfx,GetRandomReal(-0.2,0.2))
elseif r < 0.02 and n == 0 then
d = Destructable.new(destructable_crater,truX,truY,GetRandomReal(0,TAU))
elseif r < 0.03 and n == 0 then
for j = 1, GetRandomInt(1,2) do
for k = 1, GetRandomInt(1,2) do
Destructable.new(destructable_barrel,truX + j * 64-64,truY + k * 64-64,0)
end
end
end
end
local n = NormalizeNoise(Noise.openSimplex2D(x2, y2,0.01))
SetTerrainType(truX,truY,FourCC('Ddrt'),MathRound(lerp(0,17,n)),1,1)
end
end
end
function Decimate(map,min_x,min_y,CHUNK_SIZE_X,CHUNK_SIZE_Y,world_x,world_y,length,width,length_half,width_half,offset_x,offset_y)
local steps = 0
for x = min_x, CHUNK_SIZE_X do
for y = min_y, CHUNK_SIZE_Y do
steps = steps + 1
decimate_check(map,x,y)
if steps > 500 then
UNPAUSEABLE_TIMER:callDelayed(0.1,Decimate,map,x,0,CHUNK_SIZE_X,CHUNK_SIZE_Y,world_x,world_y,length,width,length_half,width_half,offset_x,offset_y)
return
end
end
end
UNPAUSEABLE_TIMER:callDelayed(0.1,delay1,map,CHUNK_SIZE_X-1,CHUNK_SIZE_Y-1,world_x,world_y,length,width,length_half,width_half,offset_x,offset_y)
end
function GenerateUnderground(world_x,world_y,length,width,length_half,width_half,offset_x,offset_y)
WorldGeneration.spawnFunction = UndergroundSpawnStart
WorldGeneration.ROOM_MIN = 3
WorldGeneration.ROOM_MAX = 12
WorldGeneration.ROOM_CHANCE = 0.96
local CHUNK_SIZE_X = length//128
local CHUNK_SIZE_Y = width//128
local wx,wy = world_x * CHUNK_SIZE_X , world_y * CHUNK_SIZE_Y
local map = GenerateDungeon(wx,wy,WorldGeneration.ROOM_MIN,WorldGeneration.ROOM_MAX,WorldGeneration.ROOM_CHANCE,CHUNK_SIZE_X,CHUNK_SIZE_Y)
UNPAUSEABLE_TIMER:callDelayed(0.1,Decimate,map,0,0,CHUNK_SIZE_X+1,CHUNK_SIZE_Y+1,world_x,world_y,length,width,length_half,width_half,offset_x,offset_y)
end
do
WorldGeneration = {}
local world_x = 0
local world_y = 0
local noise_min, noise_max
local noise_range
local rng = RNG.new({mult = 48271})
local effect_destroy_queue = Set.create()
local effect_time = {}
local loading = false
local newWorld = false
local oldAddLighting = AddLightning
local oldAddLightingEx = AddLightningEx
local oldMoveLightning = MoveLightning
local oldMoveLightningEx = MoveLightningEx
local oldDestroyLightning = DestroyLightning
local light2list = {}
local lightning_list = Set.create()
local vec_list = {}
function GetUpdatingVector(x,y)
v = NewTable()
v.x = x
v.y = y
table.insert(vec_list,v)
return v
end
function ReleaseUpdatingVector(v)
table.removeobject(vec_list,v)
v.x = nil
v.y = nil
ReleaseTable(v)
end
function AddLightning(whichType,checkVisibility,x1,y1,x2,y2)
local ldata = NewTable()
ldata.z1 = 0
ldata.z2 = 0
ldata.x1=x1
ldata.y1=y1
ldata.x2=x2
ldata.y2=y2
ldata.checkVisibility = checkVisibility
ldata.l = oldAddLightingEx(whichType,checkVisibility,x1,y1,ldata.z1,x2,y2,ldata.z2)
lightning_list:add(ldata)
light2list[ldata.l] = ldata
return ldata.l
end
function MoveLightning(l,checkVisibility,x1,y1,x2,y2)
local ldata = light2list[l]
ldata.x1=x1
ldata.y1=y1
ldata.x2=x2
ldata.y2=y2
ldata.checkVisibility = checkVisibility
oldMoveLightningEx(l,checkVisibility,x1,y1,ldata.z1,x2,y2,ldata.z2)
end
function MoveLightningEx(l,checkVisibility,x1,y1,z1,x2,y2,z2)
local ldata = light2list[l]
ldata.x1=x1
ldata.y1=y1
ldata.x2=x2
ldata.y2=y2
ldata.z1 = z1
ldata.z2 = z2
oldMoveLightningEx(l,checkVisibility,x1,y1,z1,x2,y2,z2)
end
function AddLightningEx(whichType,checkVisibility,x1,y1,z1,x2,y2,z2)
local ldata = NewTable()
ldata.z1 = z1
ldata.z2 = z2
ldata.x1=x1
ldata.y1=y1
ldata.x2=x2
ldata.y2=y2
ldata.checkVisibility = checkVisibility
ldata.l = oldAddLightingEx(whichType,checkVisibility,x1,y1,z1,x2,y2,z2)
lightning_list:add(ldata)
light2list[ldata.l] = ldata
return ldata.l
end
function DestroyLightning(whichBolt)
local ldata = light2list[whichBolt]
light2list[whichBolt] = nil
ldata.x1=nil
ldata.y1=nil
ldata.x2=nil
ldata.y2=nil
ldata.z1=nil
ldata.z2=nil
ldata.checkVisibility = nil
oldDestroyLightning(ldata.l)
ldata.l = nil
ReleaseTable(ldata)
lightning_list:remove(ldata)
end
WorldGeneration.doneLoading = Event.new()
function DestroyEffectEx(sfx)
if sfx == nil then return end
effect_time[sfx] = GAME_TIME + 5.0
effect_destroy_queue:add(sfx)
BlzPlaySpecialEffect(sfx,ANIM_TYPE_DEATH)
end
local function check_effects()
for sfx in effect_destroy_queue:elements() do
if effect_time[sfx] <= GAME_TIME then
effect_destroy_queue:remove(sfx)
BlzSetSpecialEffectZ(sfx,-9999)
DestroyEffect(sfx)
effect_time[sfx] = nil
end
end
end
function WorldGeneration.cordsToWorld(x,y)
return x + PlayableBounds.length * world_x, y + PlayableBounds.width * world_y
end
function WorldGeneration.cordsToWorldCell(x,y)
return x + PlayableBounds.cell_length * world_x, y + PlayableBounds.cell_width * world_y
end
function to_tile(x)
return x//128 * 128
end
function NormalizeNoise(x)
return (x - noise_min)/noise_range
end
function GetNoiseRange(cell_size,scale)
local min = 1.0
local max = -1.0
for x = 1, 100, 1 do
for y = 1, 100, 1 do
local n = Noise.openSimplex2D(x*cell_size, y*cell_size,scale)
if n > max then
max = n
elseif n < min then
min = n
end
end
end
return min,max
end
function WorldGeneration.push(x,y,offset_x,offset_y)
if loading then return end
loading = true
world_x = world_x + x
world_y = world_y + y
local length = PlayableBounds.length
local width = PlayableBounds.width
local length_half = length * TeleportBounds.point
local width_half = width * TeleportBounds.point
WorldGeneration.biome(world_x,world_y,length,width,length_half,width_half,offset_x,offset_y)
end
function WorldGeneration.moveWorldObjects(offset_x,offset_y)
Camera.offset(offset_x,offset_y)
Sound.move_all(offset_x,offset_y)
for i, vec in ipairs(vec_list) do
vec.x = vec.x + offset_x
vec.y = vec.y + offset_y
end
for ldata in lightning_list:elements() do
ldata.x1 = ldata.x1 + offset_x
ldata.y1 = ldata.y1 + offset_y
ldata.x2 = ldata.x2 + offset_x
ldata.y2 = ldata.y2 + offset_y
MoveLightning(ldata.l,ldata.checkVisibility,ldata.x1,ldata.y1,ldata.x2,ldata.y2,ldata.z1,ldata.z2)
end
for sfx in effect_destroy_queue:elements() do
BlzSetSpecialEffectX(sfx,BlzGetLocalSpecialEffectX(sfx)+offset_x)
BlzSetSpecialEffectY(sfx,BlzGetLocalSpecialEffectY(sfx)+offset_y)
end
for u in units:elements() do
u:move(u.position[1] + offset_x,u.position[2] + offset_y)
end
for orb in all_exp:elements() do
orb:move(orb.position[1] + offset_x,orb.position[2] + offset_y)
end
for it in all_items:elements() do
it:move(it.position[1] + offset_x,it.position[2] + offset_y)
end
for missile in all_missiles:elements() do
missile:move(missile.position[1] + offset_x,missile.position[2] + offset_y)
end
for attack in all_attacks:elements() do
attack:move(offset_x,offset_y)
end
for orb in captured_orbs:elements() do
orb.start[1] = orb.start[1] + offset_x
orb.start[2] = orb.start[2] + offset_y
end
for u in summons:elements() do
u:move(u.position[1] + offset_x,u.position[2] + offset_y)
if u.path then
for i, v in ipairs(u.path.Path) do
v.x = v.x + offset_x
v.y = v.y + offset_y
end
end
end
for u in playerunits:elements() do
u:move(u.position[1] + offset_x,u.position[2] + offset_y)
end
loading = false
end
function WorldGeneration.destroyOld(offset_x,offset_y)
for dest in destructables:elements() do
dest:destroy(true)
end
WorldGeneration.moveWorldObjects(offset_x,offset_y)
WorldGeneration.doneLoading:fire(newWorld)
newWorld = false
end
function WorldGeneration.Init()
noise_min, noise_max = GetNoiseRange(128,0.01)
noise_range = noise_max - noise_min
DEFAULT_TIMER:callPeriodically(2.5, nil, check_effects)
end
function WorldGeneration.add_scrolls(count)
for i = 1, count do
local distX = GetRandomInt(50,100)
if GetRandomReal(0,1) > 0.5 then
distX = -distX
end
local distY = GetRandomInt(50,100)
if GetRandomReal(0,1) > 0.5 then
distY = -distY
end
distX = distX + PlayableBounds.cell_length//2
distY = distY + PlayableBounds.cell_width//2
local x,y = WorldGeneration.cordsToWorldCell(distX-1,distY-1)
DungeonAddPermaRoom(x,y,2)
distX = distX * 128 - PlayableBounds.half_length
distY = distY * 128 - PlayableBounds.half_width
Item.new(item_ability_scroll,distX,distY)
end
end
function WorldGeneration.start_fresh(whichBiome)
newWorld = true
DungeonResetPermaRooms()
WorldGeneration.biome = whichBiome
world_x = GetRandomInt(-999,999)
world_y = GetRandomInt(-999,999)
local x,y = WorldGeneration.cordsToWorldCell(PlayableBounds.cell_length//2-1,PlayableBounds.cell_width//2-1)
DungeonAddPermaRoom(x,y,2)
WorldGeneration.push(0,0,0,0)
end
function WorldGeneration.destroy()
for sfx in effect_destroy_queue:elements() do
effect_destroy_queue:remove(sfx)
BlzSetSpecialEffectZ(sfx,-9999)
DestroyEffect(sfx)
effect_time[sfx] = nil
end
for u in units:elements() do
u:destroy()
end
for orb in all_exp:elements() do
orb:destroy()
end
for it in all_items:elements() do
it:destroy()
end
for u in playerunits:elements() do
u:destroy()
end
for dest in destructables:elements() do
dest:destroy()
end
for m in all_missiles:elements() do
m:destroy()
end
destructables.reset()
ResetWaveTypes()
end
end