Name | Type | is_array | initial_value |
---@diagnostic disable: undefined-global
if Debug then Debug.beginFile 'TotalInitialization' end
--[[ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Total Initialization version 5.3.1
Created by: Bribe
Contributors: Eikonium, HerlySQR, Tasyen, Luashine, Forsakn
Inspiration: Almia, ScorpioT1000, Troll-Brain
Hosted at: https://github.com/BribeFromTheHive/Lua/blob/master/TotalInitialization.lua
Debug library hosted at: https://www.hiveworkshop.com/threads/debug-utils-ingame-console-etc.330758/
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ]]
---Calls the user's initialization function during the map's loading process. The first argument should either be the init function,
---or it should be the string to give the initializer a name (works similarly to a module name/identically to a vJass library name).
---
---To use requirements, call `Require.strict 'LibraryName'` or `Require.optional 'LibraryName'`. Alternatively, the OnInit callback
---function can take the `Require` table as a single parameter: `OnInit(function(import) import.strict 'ThisIsTheSameAsRequire' end)`.
---
-- - `OnInit.global` or just `OnInit` is called after InitGlobals and is the standard point to initialize.
-- - `OnInit.trig` is called after InitCustomTriggers, and is useful for removing hooks that should only apply to GUI events.
-- - `OnInit.map` is the last point in initialization before the loading screen is completed.
-- - `OnInit.final` occurs immediately after the loading screen has disappeared, and the game has started.
---@class OnInit
--
--Simple Initialization without declaring a library name:
---@overload async fun(initCallback: Initializer.Callback)
--
--Advanced initialization with a library name and an optional third argument to signal to Eikonium's DebugUtils that the file has ended.
---@overload async fun(libraryName: string, initCallback: Initializer.Callback, debugLineNum?: integer)
--
--A way to yield your library to allow other libraries in the same initialization sequence to load, then resume once they have loaded.
---@overload async fun(customInitializerName: string)
OnInit = {}
---@alias Initializer.Callback fun(require?: Requirement | {[string]: Requirement}):...?
---@alias Requirement async fun(reqName: string, source?: table): unknown
-- `Require` will yield the calling `OnInit` initialization function until the requirement (referenced as a string) exists. It will check the
-- global API (for example, does 'GlobalRemap' exist) and then check for any named OnInit resources which might use that same string as its name.
--
-- Due to the way Sumneko's syntax highlighter works, the return value will only be linted for defined @class objects (and doesn't work for regular
-- globals like `TimerStart`). I tried to request the functionality here: https://github.com/sumneko/lua-language-server/issues/1792 , however it
-- was closed. Presumably, there are other requests asking for it, but I wouldn't count on it.
--
-- To declare a requirement, use: `Require.strict 'SomeLibrary'` or (if you don't care about the missing linting functionality) `Require 'SomeLibrary'`
--
-- To optionally require something, use any other suffix (such as `.optionally` or `.nonstrict`): `Require.optional 'SomeLibrary'`
--
---@class Require: { [string]: Requirement }
---@overload async fun(reqName: string, source?: table): string
Require = {}
do
local library = {} --You can change this to false if you don't use `Require` nor the `OnInit.library` API.
--CONFIGURABLE LEGACY API FUNCTION:
---@param _ENV table
---@param OnInit any
local function assignLegacyAPI(_ENV, OnInit)
OnGlobalInit = OnInit; OnTrigInit = OnInit.trig; OnMapInit = OnInit.map; OnGameStart = OnInit.final --Global Initialization Lite API
--OnMainInit = OnInit.main; OnLibraryInit = OnInit.library; OnGameInit = OnInit.final --short-lived experimental API
--onGlobalInit = OnInit; onTriggerInit = OnInit.trig; onInitialization = OnInit.map; onGameStart = OnInit.final --original Global Initialization API
--OnTriggerInit = OnInit.trig; OnInitialization = OnInit.map --Forsakn's Ordered Indices API
end
--END CONFIGURABLES
local _G, rawget, insert =
_G, rawget, table.insert
local initFuncQueue = {}
---@param name string
---@param continue? function
local function runInitializers(name, continue)
--print('running:', name, tostring(initFuncQueue[name]))
if initFuncQueue[name] then
for _,func in ipairs(initFuncQueue[name]) do
coroutine.wrap(func)(Require)
end
initFuncQueue[name] = nil
end
if library then
library:resume()
end
if continue then
continue()
end
end
local function initEverything()
---@param hookName string
---@param continue? function
local function hook(hookName, continue)
local hookedFunc = rawget(_G, hookName)
if hookedFunc then
rawset(_G, hookName,
function()
hookedFunc()
runInitializers(hookName, continue)
end
)
else
runInitializers(hookName, continue)
end
end
hook(
'InitGlobals',
function()
hook(
'InitCustomTriggers',
function()
hook('RunInitializationTriggers')
end
)
end
)
hook(
'MarkGameStarted',
function()
if library then
for _,func in ipairs(library.queuedInitializerList) do
func(nil, true) --run errors for missing requirements.
end
for _,func in pairs(library.yieldedModuleMatrix) do
func(true) --run errors for modules that aren't required.
end
end
OnInit = nil
Require = nil
end
)
end
---@param initName string
---@param libraryName string | Initializer.Callback
---@param func? Initializer.Callback
---@param debugLineNum? integer
---@param incDebugLevel? boolean
local function addUserFunc(initName, libraryName, func, debugLineNum, incDebugLevel)
if not func then
---@cast libraryName Initializer.Callback
func = libraryName
else
assert(type(libraryName) == 'string')
if debugLineNum and Debug then
Debug.beginFile(libraryName, incDebugLevel and 3 or 2)
Debug.data.sourceMap[#Debug.data.sourceMap].lastLine = debugLineNum
end
if library then
func = library:create(libraryName, func)
end
end
assert(type(func) == 'function')
--print('adding user func: ' , initName , libraryName, debugLineNum, incDebugLevel)
initFuncQueue[initName] = initFuncQueue[initName] or {}
insert(initFuncQueue[initName], func)
if initName == 'root' or initName == 'module' then
runInitializers(initName)
end
end
---@param name string
local function createInit(name)
---@async
---@param libraryName string --Assign your callback a unique name, allowing other OnInit callbacks can use it as a requirement.
---@param userInitFunc Initializer.Callback --Define a function to be called at the chosen point in the initialization process. It can optionally take the `Require` object as a parameter. Its optional return value(s) are passed to a requiring library via the `Require` object (defaults to `true`).
---@param debugLineNum? integer --If the Debug library is present, you can call Debug.getLine() for this parameter (which should coincide with the last line of your script file). This will neatly tie-in with OnInit's built-in Debug library functionality to define a starting line and an ending line for your module.
---@overload async fun(userInitFunc: Initializer.Callback)
return function(libraryName, userInitFunc, debugLineNum)
addUserFunc(name, libraryName, userInitFunc, debugLineNum)
end
end
OnInit.global = createInit 'InitGlobals' -- Called after InitGlobals, and is the standard point to initialize.
OnInit.trig = createInit 'InitCustomTriggers' -- Called after InitCustomTriggers, and is useful for removing hooks that should only apply to GUI events.
OnInit.map = createInit 'RunInitializationTriggers' -- Called last in the script's loading screen sequence. Runs after the GUI "Map Initialization" events have run.
OnInit.final = createInit 'MarkGameStarted' -- Called immediately after the loading screen has disappeared, and the game has started.
do
---@param self table
---@param libraryNameOrInitFunc function | string
---@param userInitFunc function
---@param debugLineNum number
local function __call(
self,
libraryNameOrInitFunc,
userInitFunc,
debugLineNum
)
if userInitFunc or type(libraryNameOrInitFunc) == 'function' then
addUserFunc(
'InitGlobals', --Calling OnInit directly defaults to OnInit.global (AKA OnGlobalInit)
libraryNameOrInitFunc,
userInitFunc,
debugLineNum,
true
)
elseif library then
library:declare(libraryNameOrInitFunc) --API handler for OnInit "Custom initializer"
else
error(
"Bad OnInit args: "..
tostring(libraryNameOrInitFunc) .. ", " ..
tostring(userInitFunc)
)
end
end
setmetatable(OnInit --[[@as table]], { __call = __call })
end
do --if you don't need the initializers for 'root', 'config' and 'main', you can delete this do...end block.
local gmt = getmetatable(_G) or
getmetatable(setmetatable(_G, {}))
local rawIndex = gmt.__newindex or rawset
local hookMainAndConfig
---@param _G table
---@param key string
---@param fnOrDiscard unknown
function hookMainAndConfig(_G, key, fnOrDiscard)
if key == 'main' or key == 'config' then
---@cast fnOrDiscard function
if key == 'main' then
runInitializers 'root'
end
rawIndex(_G, key, function()
if key == 'config' then
fnOrDiscard()
elseif gmt.__newindex == hookMainAndConfig then
gmt.__newindex = rawIndex --restore the original __newindex if no further hooks on __newindex exist.
end
runInitializers(key)
if key == 'main' then
fnOrDiscard()
end
end)
else
rawIndex(_G, key, fnOrDiscard)
end
end
gmt.__newindex = hookMainAndConfig
OnInit.root = createInit 'root' -- Runs immediately during the Lua root, but is yieldable (allowing requirements) and pcalled.
OnInit.config = createInit 'config' -- Runs when `config` is called. Credit to @Luashine: https://www.hiveworkshop.com/threads/inject-main-config-from-we-trigger-code-like-jasshelper.338201/
OnInit.main = createInit 'main' -- Runs when `main` is called. Idea from @Tasyen: https://www.hiveworkshop.com/threads/global-initialization.317099/post-3374063
end
if library then
library.queuedInitializerList = {}
library.customDeclarationList = {}
library.yieldedModuleMatrix = {}
library.moduleValueMatrix = {}
function library:pack(name, ...)
self.moduleValueMatrix[name] = table.pack(...)
end
function library:resume()
if self.queuedInitializerList[1] then
local continue, tempQueue, forceOptional
::initLibraries::
repeat
continue=false
self.queuedInitializerList, tempQueue =
{}, self.queuedInitializerList
for _,func in ipairs(tempQueue) do
if func(forceOptional) then
continue=true --Something was initialized; therefore further systems might be able to initialize.
else
insert(self.queuedInitializerList, func) --If the queued initializer returns false, that means its requirement wasn't met, so we re-queue it.
end
end
until not continue or not self.queuedInitializerList[1]
if self.customDeclarationList[1] then
self.customDeclarationList, tempQueue =
{}, self.customDeclarationList
for _,func in ipairs(tempQueue) do
func() --unfreeze any custom initializers.
end
elseif not forceOptional then
forceOptional = true
else
return
end
goto initLibraries
end
end
local function declareName(name, initialValue)
assert(type(name) == 'string')
assert(library.moduleValueMatrix[name] == nil)
library.moduleValueMatrix[name] =
initialValue and { true, n = 1 }
end
function library:create(name, userFunc)
assert(type(userFunc) == 'function')
declareName(name, false) --declare itself as a non-loaded library.
return function()
self:pack(name, userFunc(Require)) --pack return values to allow multiple values to be communicated.
if self.moduleValueMatrix[name].n == 0 then
self:pack(name, true) --No values were returned; therefore simply package the value as `true`
end
end
end
---@async
function library:declare(name)
declareName(name, true) --declare itself as a loaded library.
local co = coroutine.running()
insert(
self.customDeclarationList,
function()
coroutine.resume(co)
end
)
coroutine.yield() --yields the calling function until after all currently-queued initializers have run.
end
local processRequirement
---@async
function processRequirement(
optional,
requirement,
explicitSource
)
if type(optional) == 'string' then
optional, requirement, explicitSource =
true, optional, requirement --optional requirement (processed by the __index method)
else
optional = false --strict requirement (processed by the __call method)
end
local source = explicitSource or _G
assert(type(source)=='table')
assert(type(requirement)=='string')
::reindex::
local subSource, subReq =
requirement:match("([\x25w_]+)\x25.(.+)") --Check if user is requiring using "table.property" syntax
if subSource and subReq then
source,
requirement =
processRequirement(subSource, source), --If the container is nil, yield until it is not.
subReq
if type(source)=='table' then
explicitSource = source
goto reindex --check for further nested properties ("table.property.subProperty.anyOthers").
else
return --The source table for the requirement wasn't found, so disregard the rest (this only happens with optional requirements).
end
end
local function loadRequirement(unpack)
local package = rawget(source, requirement) --check if the requirement exists in the host table.
if not package and not explicitSource then
if library.yieldedModuleMatrix[requirement] then
library.yieldedModuleMatrix[requirement]() --load module if it exists
end
package = library.moduleValueMatrix[requirement] --retrieve the return value from the module.
if unpack and type(package)=='table' then
return table.unpack(package, 1, package.n) --using unpack allows any number of values to be returned by the required library.
end
end
return package
end
local co, loaded
local function checkReqs(forceOptional, printErrors)
if not loaded then
loaded = loadRequirement()
loaded = loaded or optional and
(loaded==nil or forceOptional)
if loaded then
if co then coroutine.resume(co) end --resume only if it was yielded in the first place.
return loaded
elseif printErrors then
coroutine.resume(co, true)
end
end
end
if not checkReqs() then --only yield if the requirement doesn't already exist.
co = coroutine.running()
insert(library.queuedInitializerList, checkReqs)
if coroutine.yield() then
error("Missing Requirement: "..requirement) --handle the error within the user's function to get an accurate stack trace via the `try` function.
end
end
return loadRequirement(true)
end
---@type Requirement
function Require.strict(name, explicitSource)
return processRequirement(nil, name, explicitSource)
end
setmetatable(Require --[[@as table]], {
__call = processRequirement,
__index = function()
return processRequirement
end
})
local module = createInit 'module'
--- `OnInit.module` will only call the OnInit function if the module is required by another resource, rather than being called at a pre-
--- specified point in the loading process. It works similarly to Go, in that including modules in your map that are not actually being
--- required will throw an error message.
---@param name string
---@param func fun(require?: Initializer.Callback):any
---@param debugLineNum? integer
OnInit.module = function(name, func, debugLineNum)
if func then
local userFunc = func
func = function(require)
local co = coroutine.running()
library.yieldedModuleMatrix[name] =
function(failure)
library.yieldedModuleMatrix[name] = nil
coroutine.resume(co, failure)
end
if coroutine.yield() then
error("Module declared but not required: "..name)
end
return userFunc(require)
end
end
module(name, func, debugLineNum)
end
end
if assignLegacyAPI then --This block handles legacy code.
---Allows packaging multiple requirements into one table and queues the initialization for later.
---@deprecated
---@param initList string | table
---@param userFunc function
function OnInit.library(initList, userFunc)
local typeOf = type(initList)
assert(typeOf=='table' or typeOf=='string')
assert(type(userFunc) == 'function')
local function caller(use)
if typeOf=='string' then
use(initList)
else
for _,initName in ipairs(initList) do
use(initName)
end
if initList.optional then
for _,initName in ipairs(initList.optional) do
use.lazily(initName)
end
end
end
end
if initList.name then
OnInit(initList.name, caller)
else
OnInit(caller)
end
end
local legacyTable = {}
assignLegacyAPI(legacyTable, OnInit)
for key,func in pairs(legacyTable) do
rawset(_G, key, func)
end
OnInit.final(function()
for key in pairs(legacyTable) do
rawset(_G, key, nil)
end
end)
end
initEverything()
end
if Debug then Debug.endFile() end
do; local _, codeLoc = pcall(error, "", 2) --get line number where DebugUtils begins.
--[[
--------------------------
-- | Debug Utils 2.0a | --
--------------------------
--> https://www.hiveworkshop.com/threads/debug-utils-ingame-console-etc.330758/
- by Eikonium, with special thanks to:
- @Bribe, for pretty table print, showing that xpcall's message handler executes before the stack unwinds and useful suggestions like name caching and stack trace improvements.
- @Jampion, for useful suggestions like print caching and applying Debug.try to all code entry points
- @Luashine, for useful feedback and building "WC3 Debug Console Paste Helperβ" (https://github.com/Luashine/wc3-debug-console-paste-helper#readme)
- @HerlySQR, for showing a way to get a stack trace in Wc3 (https://www.hiveworkshop.com/threads/lua-getstacktrace.340841/)
- @Macadamia, for showing a way to print warnings upon accessing undeclared globals, where this all started with (https://www.hiveworkshop.com/threads/lua-very-simply-trick-to-help-lua-users-track-syntax-errors.326266/)
-----------------------------------------------------------------------------------------------------------------------------
| Provides debugging utility for Wc3-maps using Lua. |
| |
| Including: |
| 1. Automatic ingame error messages upon running erroneous code from triggers or timers. |
| 2. Ingame Console that allows you to execute code via Wc3 ingame chat. |
| 3. Automatic warnings upon reading undeclared globals (which also triggers after misspelling globals) |
| 4. Debug-Library functions for manual error handling. |
| 5. Caching of loading screen print messages until game start (which simplifies error handling during loading screen) |
| 6. Overwritten tostring/print-functions to show the actual string-name of an object instead of the memory position. |
| 7. Conversion of war3map.lua-error messages to local file error messages. |
| 8. Other useful debug utility (table.print and Debug.wc3Type) |
-----------------------------------------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| Installation: |
| |
| 1. Copy the code (DebugUtils.lua, StringWidth.lua and IngameConsole.lua) into your map. Use script files (Ctrl+U) in your trigger editor, not text-based triggers! |
| 2. Order the files: DebugUtils above StringWidth above IngameConsole. Make sure they are above ALL other scripts (crucial for local line number feature). |
| 3. Adjust the settings in the settings-section further below to receive the debug environment that fits your needs. |
| |
| Deinstallation: |
| |
| - Debug Utils is meant to provide debugging utility and as such, shall be removed or invalidated from the map closely before release. |
| - Optimally delete the whole Debug library. If that isn't suitable (because you have used library functions at too many places), you can instead replace Debug Utils |
| by the following line of code that will invalidate all Debug functionality (without breaking your code): |
| Debug = setmetatable({try = function(...) return select(2,pcall(...)) end}, {__index = function(t,k) return DoNothing end}); try = Debug.try |
| - If that is also not suitable for you (because your systems rely on the Debug functionality to some degree), at least set ALLOW_INGAME_CODE_EXECUTION to false. |
| - Be sure to test your map thoroughly after removing Debug Utils. |
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
* Documentation and API-Functions:
*
* - All automatic functionality provided by Debug Utils can be deactivated using the settings directly below the documentation.
*
* -------------------------
* | Ingame Code Execution |
* -------------------------
* - Debug Utils provides the ability to run code via chat command from within Wc3, if you have conducted step 3 from the installation section.
* - You can either open the ingame console by typing "-console" into the chat, or directly execute code by typing "-exec <code>".
* - See IngameConsole script for further documentation.
*
* ------------------
* | Error Handling |
* ------------------
* - Debug Utils automatically applies error handling (i.e. Debug.try) to code executed by your triggers and timers (error handling means that error messages are printed on screen, if anything doesn't run properly).
* - You can still use the below library functions for manual debugging.
*
* Debug.try(funcToExecute, ...) / try(funcToExecute, ...) -> ...
* - Calls the specified function with the specified parameters in protected mode (i.e. code after Debug.try will continue to run even if the function fails to execute).
* - If the call is successful, returns the specified function's original return values (so p1 = Debug.try(Player, 0) will work fine).
* - If the call is unsuccessful, prints an error message on screen (including stack trace and parameters you have potentially logged before the error occured)
* - By default, the error message consists of a line-reference to war3map.lua (which you can look into by forcing a syntax error in WE or by exporting it from your map via File -> Export Script).
* You can get more helpful references to local script files instead, see section about "Local script references".
* - Example: Assume you have a code line like "func(param1,param2)", which doesn't work and you want to know why.
* Option 1: Change it to "Debug.try(func, param1, param2)", i.e. separate the function from the parameters.
* Option 2: Change it to "Debug.try(function() return func(param1, param2) end)", i.e. pack it into an anonymous function (optionally skip the return statement).
* Debug.log(...)
* - Logs the specified parameters to the Debug-log. The Debug-log will be printed upon the next error being catched by Debug.try, Debug.assert or Debug.throwError.
* - The Debug-log will only hold one set of parameters per code-location. That means, if you call Debug.log() inside any function, only the params saved within the latest call of that function will be kept.
* Debug.throwError(...)
* - Prints an error message including document, line number, stack trace, previously logged parameters and all specified parameters on screen. Parameters can have any type.
* - In contrast to Lua's native error function, this can be called outside of protected mode and doesn't halt code execution.
* Debug.assert(condition:boolean, errorMsg:string, ...) -> ...
* - Prints the specified error message including document, line number, stack trace and previously logged parameters on screen, IF the specified condition fails (i.e. resolves to false/nil).
* - Returns ..., IF the specified condition holds.
* - This works exactly like Lua's native assert, except that it also works outside of protected mode and does not halt code execution.
* Debug.traceback() -> string
* - Returns the stack trace at the position where this is called. You need to manually print it.
* Debug.getLine([depth: integer]) -> integer?
* - Returns the line in war3map.lua, where this function is executed.
* - You can specify a depth d >= 1 to instead return the line, where the d-th function in the stack trace was called. I.e. depth = 2 will return the line of execution of the function that calls Debug.getLine.
* - Due to Wc3's limited stack trace ability, this might sometimes return nil for depth >= 3, so better apply nil-checks on the result.
* Debug.getLocalErrorMsg(errorMsg:string) -> string
* - Takes an error message containing a file and a linenumber and converts war3map.lua-lines to local document lines as defined by uses of Debug.beginFile() and Debug.endFile().
* - Error Msg must be formatted like "<document>:<linenumber><Rest>".
*
* -----------------------------------
* | Warnings for undeclared globals |
* -----------------------------------
* - DebugUtils will print warnings on screen, if you read an undeclared global variable.
* - This is technically the case, when you misspelled on a function name, like calling CraeteUnit instead of CreateUnit.
* - Keep in mind though that the same warning will pop up after reading a global that was intentionally nilled. If you don't like this, turn of this feature in the settings.
*
* -----------------
* | Print Caching |
* -----------------
* - DebugUtils caches print()-calls occuring during loading screen and delays them to after game start.
* - This also applies to loading screen error messages, so you can wrap erroneous parts of your Lua root in Debug.try-blocks and see the message after game start.
*
* -------------------------
* | Local File Stacktrace |
* -------------------------
* - By default, error messages and stack traces printed by the error handling functionality of Debug Utils contain references to war3map.lua (a big file just appending all your local scripts).
* - The Debug-library provides the two functions below to index your local scripts, activating local file names and line numbers (matching those in your IDE) instead of the war3map.lua ones.
* - This allows you to inspect errors within your IDE (VSCode) instead of the World Editor.
*
* Debug.beginFile(fileName: string [, depth: integer])
* - Tells the Debug library that the specified file begins exactly here (i.e. in the line, where this is called).
* - Using this improves stack traces of error messages. "war3map.lua"-references between <here> and the next Debug.endFile() will be converted to file-specific references.
* - All war3map.lua-lines located between the call of Debug.beginFile(fileName) and the next call of Debug.beginFile OR Debug.endFile are treated to be part of "fileName".
* - !!! To be called in the Lua root in Line 1 of every document you wish to track. Line 1 means exactly line 1, before any comment! This way, the line shown in the trace will exactly match your IDE.
* - Depth can be ignored, except if you want to use a custom wrapper around Debug.beginFile(), in which case you need to set the depth parameter to 1 to record the line of the wrapper instead of the line of Debug.beginFile().
* Debug.endFile([depth: integer])
* - Ends the current file that was previously begun by using Debug.beginFile(). War3map.lua-lines after this will not be converted until the next instance of Debug.beginFile().
* - The next call of Debug.beginFile() will also end the previous one, so using Debug.endFile() is optional. Mainly recommended to use, if you prefer to have war3map.lua-references in a certain part of your script (such as within GUI triggers).
* - Depth can be ignored, except if you want to use a custom wrapper around Debug.endFile(), you need to increase the depth parameter to 1 to record the line of the wrapper instead of the line of Debug.endFile().
*
* ----------------
* | Name Caching |
* ----------------
* - DebugUtils overwrites the tostring-function so that it prints the name of a non-primitive object (if available) instead of its memory position. The same applies to print().
* - For instance, print(CreateUnit) will show "function: CreateUnit" on screen instead of "function: 0063A698".
* - The table holding all those names is referred to as "Name Cache".
* - All names of objects in global scope will automatically be added to the Name Cache both within Lua root and again at game start (to get names for overwritten natives and your own objects).
* - New names entering global scope will also automatically be added, even after game start. The same applies to subtables of _G up to a depth of Debug.settings.NAME_CACHE_DEPTH.
* - Objects within subtables will be named after their parent tables and keys. For instance, the name of the function within T = {{bla = function() end}} is "T[1].bla".
* - The automatic adding doesn't work for objects saved into existing variables/keys after game start (because it's based on __newindex metamethod which simply doesn't trigger)
* - You can manually add names to the name cache by using the following API-functions:
*
* Debug.registerName(whichObject:any, name:string)
* - Adds the specified object under the specified name to the name cache, letting tostring and print output "<type>: <name>" going foward.
* - The object must be non-primitive, i.e. this won't work on strings, numbers and booleans.
* - This will overwrite existing names for the specified object with the specified name.
* Debug.registerNamesFrom(parentTable:table [, parentTableName:string] [, depth])
* - Adds names for all values from within the specified parentTable to the name cache.
* - Names for entries will be like "<parentTableName>.<key>" or "<parentTableName>[<key>]" (depending on the key type), using the existing name of the parentTable from the name cache.
* - You can optionally specify a parentTableName to use that for the entry naming instead of the existing name. Doing so will also register that name for the parentTable, if it doesn't already has one.
* - Specifying the empty string as parentTableName will suppress it in the naming and just register all values as "<key>". Note that only string keys will be considered this way.
* - In contrast to Debug.registerName(), this function will NOT overwrite existing names, but just add names for new objects.
* Debug.oldTostring(object:any) -> string
* - The old tostring-function in case you still need outputs like "function: 0063A698".
*
* -----------------
* | Other Utility |
* -----------------
*
* Debug.wc3Type(object:any) -> string
* - Returns the Warcraft3-type of the input object. E.g. Debug.wc3Type(Player(0)) will return "player".
* - Returns type(object), if used on Lua-objects.
* table.tostring(whichTable [, depth:integer] [, pretty_yn:boolean])
* - Creates a list of all (key,value)-pairs from the specified table. Also lists subtable entries up to the specified depth (unlimited, if not specified).
* - E.g. for T = {"a", 5, {7}}, table.tostring(T) would output '{(1, "a"), (2, 5), (3, {(1, 7)})}' (if using concise style, i.e. pretty_yn being nil or false).
* - Not specifying a depth can potentially lead to a stack overflow for self-referential tables (e.g X = {}; X[1] = X). Choose a sensible depth to prevent this (in doubt start with 1 and test upwards).
* - Supports pretty style by setting pretty_yn to true. Pretty style is linebreak-separated, uses indentations and has other visual improvements. Use it on small tables only, because Wc3 can't show that many linebreaks at once.
* - All of the following is valid syntax: 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.
* table.print(whichTable [, depth:integer] [, pretty_yn:boolean])
* - Prints table.tostring(...).
*
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------]]
----------------
--| Settings |--
----------------
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 = false ---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
---Tells the Debug library that the specified file begins exactly here (i.e. in the line, where this is called).
---
---Using this improves stack traces of error messages. Stack trace will have "war3map.lua"-references between this and the next Debug.endFile() converted to file-specific references.
---
---To be called in the Lua root in Line 1 of every file you wish to track! Line 1 means exactly line 1, before any comment! This way, the line shown in the trace will exactly match your IDE.
---
---If you want to use a custom wrapper around Debug.beginFile(), you need to increase the depth parameter to 1 to record the line of the wrapper instead of the line of Debug.beginFile().
---@param fileName string
---@param depth? integer default: 0. Set to 1, if you call this from a wrapper (and use the wrapper in line 1 of every document).
---@param lastLine? integer Ignore this. For compatibility with Total Initialization.
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
---Tells the Debug library that the file previously started with Debug.beginFile() ends here.
---This is in theory optional to use, as the next call of Debug.beginFile will also end the previous. Still good practice to always use this in the last line of every file.
---If you want to use a custom wrapper around Debug.endFile(), you need to increase the depth parameter to 1 to record the line of the wrapper instead of the line of Debug.endFile().
---@param depth? integer
function Debug.endFile(depth)
depth = depth or 0
local line = Debug.getLine(depth + 1)
sourceMap[#sourceMap].lastLine = line
end
---Takes an error message containing a file and a linenumber and converts both to local file and line as saved to Debug.sourceMap.
---@param errorMsg string must be formatted like "<document>:<linenumber><RestOfMsg>".
---@return string convertedMsg a string of the form "<localDocument>:<localLinenumber><RestOfMsg>"
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
----------------------
--| Error Handling |--
----------------------
local concat
---Applies tostring() on all input params and concatenates them 4-space-separated.
---@param firstParam any
---@param ... any
---@return string
concat = function(firstParam, ...)
if select('#', ...) == 0 then
return tostring(firstParam)
end
return tostring(firstParam) .. ' ' .. concat(...)
end
---Returns the stack trace between the specified startDepth and endDepth.
---The trace lists file names and line numbers. File name is only listed, if it has changed from the previous traced line.
---The previous file can also be specified as an input parameter to suppress the first file name in case it's identical.
---@param startDepth integer
---@param endDepth integer
---@return string trace
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
---Message Handler to be used by the try-function below.
---Adds stack trace plus formatting to the message and prints it.
---@param errorMsg string
---@param startDepth? integer default: 4 for use in xpcall
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
---Tries to execute the specified function with the specified parameters in protected mode and prints an error message (including stack trace), if unsuccessful.
---
---Example use: Assume you have a code line like "CreateUnit(0,1,2)", which doesn't work and you want to know why.
---* Option 1: Change it to "Debug.try(CreateUnit, 0, 1, 2)", i.e. separate the function from the parameters.
---* Option 2: Change it to "Debug.try(function() return CreateUnit(0,1,2) end)", i.e. pack it into an anonymous function. You can skip the "return", if you don't need the return values.
---When no error occured, the try-function will return all values returned by the input function.
---When an error occurs, try will print the resulting error and stack trace.
---@param funcToExecute function the function to call in protected mode
---@param ... any params for the input-function
---@return ... any
function Debug.try(funcToExecute, ...)
return select(2, xpcall(funcToExecute, errorHandler,...))
end
try = Debug.try
---Prints "ERROR:" and the specified error objects on the Screen. Also prints the stack trace leading to the error. You can specify as many arguments as you wish.
---
---In contrast to Lua's native error function, this can be called outside of protected mode and doesn't halt code execution.
---@param ... any objects/errormessages to be printed (doesn't have to be strings)
function Debug.throwError(...)
errorHandler(getStackTrace(4,4) .. ": " .. concat(...), 5)
end
---Prints the specified error message, if the specified condition fails (i.e. if it resolves to false or nil).
---
---Returns all specified arguments after the errorMsg, if the condition holds.
---
---In contrast to Lua's native assert function, this can be called outside of protected mode and doesn't halt code execution (even in case of condition failure).
---@param condition any actually a boolean, but you can use any object as a boolean.
---@param errorMsg string the message to be printed, if the condition fails
---@param ... any will be returned, if the condition holds
function Debug.assert(condition, errorMsg, ...)
if condition then
return ...
else
errorHandler(getStackTrace(4,4) .. ": " .. errorMsg, 5)
end
end
---Returns the stack trace at the code position where this function is called.
---The returned string includes war3map.lua/blizzard.j.lua code positions of all functions from the stack trace in the order of execution (most recent call last). It does NOT include function names.
---@return string
function Debug.traceback()
return getStackTrace(3,200)
end
---Saves the specified parameters to the debug log at the location where this function is called. The Debug-log will be printed for all affected locations upon the try-function catching an error.
---The log is unique per code location: Parameters logged at code line x will overwrite the previous ones logged at x. Parameters logged at different locations will all persist and be printed.
---@param ... any save any information, for instance the parameters of the function call that you are logging.
function Debug.log(...)
local _, location = pcall(error, "", 3) ---@diagnostic disable-next-line: need-check-nil
paramLog[location or ''] = concat(...)
end
------------------------------------
--| Name Caching (API-functions) |--
------------------------------------
--Help-table. The registerName-functions below shall not work on call-by-value-types, i.e. booleans, strings and numbers (renaming a value of any primitive type doesn't make sense).
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'})
---Removes the name from the name cache, if already used for any object (freeing it for the new object). This makes sure that a name is always unique.
---This doesn't solve the
---@param name string
local function removeNameIfNecessary(name)
if nameCacheMirror[name] then
nameCache[nameCacheMirror[name]] = nil
nameCacheMirror[name] = nil
end
end
---Registers a name for the specified object, which will be the future output for tostring(whichObject).
---You can overwrite existing names for whichObject by using this.
---@param whichObject any
---@param name string
function Debug.registerName(whichObject, name)
if not skipType[type(whichObject)] then
removeNameIfNecessary(name)
nameCache[whichObject] = name
nameCacheMirror[name] = whichObject
nameDepths[name] = 0
end
end
---Registers a new name to the nameCache as either just <key> (if parentTableName is the empty string), <table>.<key> (if parentTableName is given and string key doesn't contain whitespace) or <name>[<key>] notation (for other keys in existing tables).
---Only string keys without whitespace support <key>- and <table>.<key>-notation. All other keys require a parentTableName.
---@param parentTableName string | '""' empty string suppresses <table>-affix.
---@param key any
---@param object any only call-be-ref types allowed
---@param parentTableDepth? integer
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
---Registers all call-by-reference objects in the given parentTable to the nameCache.
---Automatically filters out primitive objects and already registed Objects.
---@param parentTable table
---@param parentTableName? string
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
---Adds names for all values of the specified parentTable to the name cache. Names will be "<parentTableName>.<key>" or "<parentTableName>[<key>]", depending on the key type.
---
---Example: Given a table T = {f = function() end, [1] = {}}, tostring(T.f) and tostring(T[1]) will output "function: T.f" and "table: T[1]" respectively after running Debug.registerNamesFrom(T).
---The name of T itself must either be specified as an input parameter OR have previously been registered. It can also be suppressed by inputting the empty string (so objects will just display by their own names).
---The names of objects in global scope are automatically registered during loading screen.
---@param parentTable table base table of which all entries shall be registered (in the Form parentTableName.objectName).
---@param parentTableName? string|'""' Nil: takes <parentTableName> as previously registered. Empty String: Skips <parentTableName> completely. String <s>: Objects will show up as "<s>.<objectName>".
---@param depth? integer objects within sub-tables up to the specified depth will also be added. Default: 1 (only elements of whichTable). Must be >= 1.
---@overload fun(parentTable:table, depth:integer)
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 = true ---@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()
if Debug then Debug.beginFile "Hook" end
--ββββββββββββββββββββββββββββββββββββββ
-- Hook version 7.1.0.1
-- Created by: Bribe
-- Contributors: Eikonium, Jampion, MyPad, Wrda
--βββββββββββββββββββββββββββββββββββββββββββββ
---@class Hook.property
---@field next function|Hook.property --Call the next/native function. Also works with any given name (old/native/original/etc.). The args and return values align with the original function.
---@field remove fun(all?: boolean) --Remove the hook. Pass the boolean "true" to remove all hooks.
---@field package tree HookTree --Reference to the tree storing each hook on that particular key in that particular host.
---@field package priority number
---@field package index integer
---@field package hookAsBasicFn? function
----@field package debugId? string
----@field package debugNext? string
---@class Hook: {[integer]: Hook.property, [string]: function}
Hook = {}
do
local looseValuesMT = { __mode = "v" }
local hostKeyTreeMatrix = ---@type table<table, table<any, HookTree>>
setmetatable({
--Already add a hook matrix for _G right away.
[_G] = setmetatable({}, looseValuesMT)
}, looseValuesMT)
---@class HookTree: { [number]: Hook.property }
---@field host table
---@field key unknown --What the function was indexed to (_G items are typically indexed via strings)
---@field hasHookAsBasicFn boolean
---Reindexes a HookTree, inserting or removing a hook and updating the properties of each hook.
---@param tree HookTree
---@param index integer
---@param newHook? table
local function reindexTree(tree, index, newHook)
if newHook then
table.insert(tree, index, newHook)
else
table.remove(tree, index)
end
local top = #tree
local prevHook = tree[index - 1]
-- `table.insert` and `table.remove` shift the elements upwards or downwards,
-- so this loop manually aligns the tree elements with this shift.
for i = index, top do
local currentHook = tree[i]
currentHook.index = i
currentHook.next = (i > 1) and
rawget(prevHook, 'hookAsBasicFn') or
prevHook
--currentHook.debugNext = tostring(currentHook.next)
prevHook = currentHook
end
local topHookBasicFn = rawget(tree[top], 'hookAsBasicFn')
if topHookBasicFn then
if not tree.hasHookAsBasicFn or rawget(tree.host, tree.key) ~= topHookBasicFn then
tree.hasHookAsBasicFn = true
--a different basic function should be called for this hook
--instead of the one that was previously there.
tree.host[tree.key] = topHookBasicFn
end
else
--The below comparison rules out 'nil' and 'true'.
--Q: Why rule out nil?
--A: There is no need to reassign a host hook handler if there is already one in place.
if tree.hasHookAsBasicFn ~= false then
tree.host[tree.key] = function(...)
return tree[#tree](...)
end
end
tree.hasHookAsBasicFn = false
end
end
---@param hookProperty Hook.property
---@param deleteAllHooks? boolean
function Hook.delete(hookProperty, deleteAllHooks)
local tree = hookProperty.tree
hookProperty.tree = nil
if deleteAllHooks or #tree == 1 then
--Reset the host table's native behavior for the hooked key.
tree.host[tree.key] =
(tree[0] ~= DoNothing) and
tree[0] or
nil
hostKeyTreeMatrix[tree.host][tree.key] = nil
else
reindexTree(tree, hookProperty.index)
end
end
---@param hostTableToHook? table
---@param defaultNativeBehavior? function
---@param hookedTableIsMetaTable? boolean
local function setupHostTable(hostTableToHook, defaultNativeBehavior, hookedTableIsMetaTable)
hostTableToHook = hostTableToHook or _G
if hookedTableIsMetaTable or
(defaultNativeBehavior and hookedTableIsMetaTable == nil)
then
hostTableToHook = getmetatable(hostTableToHook) or
getmetatable(setmetatable(hostTableToHook, {}))
end
return hostTableToHook
end
---@param tree HookTree
---@param priority number
local function huntThroughPriorityList(tree, priority)
local index = 1
local topIndex = #tree
repeat
if priority <= tree[index].priority then
break
end
index = index + 1
until index > topIndex
return index
end
---@param hostTableToHook table
---@param key unknown
---@param defaultNativeBehavior? function
---@return HookTree | nil
local function createHookTree(hostTableToHook, key, defaultNativeBehavior)
local nativeFn = rawget(hostTableToHook, key) or
defaultNativeBehavior or
((hostTableToHook ~= _G or type(key) ~= "string") and
DoNothing)
if not nativeFn then
--Logging is used here instead of directly throwing an error, because
--no one can be sure that we're running within a debug-friendly thread.
(Debug and Debug.throwError or print)("Hook Error: No value found for key: " .. tostring(key))
return
end
---@class HookTree
local tree = {
host = hostTableToHook,
key = key,
[0] = nativeFn,
--debugNativeId = tostring(nativeFn)
}
hostKeyTreeMatrix[hostTableToHook][key] = tree
return tree
end
---@param self Hook.property
local function __index(self)
return self.next
end
---@param key unknown Usually `string` (the name of the native you wish to hook)
---@param callbackFn fun(Hook, ...):any The function you want to run when the native is called. The first parameter is type "Hook", and the remaining parameters (and return value(s)) align with the original function.
---@param priority? number Defaults to 0. Hooks are called in order of highest priority down to lowest priority. The native itself has the lowest priority.
---@param hostTableToHook? table Defaults to _G (the table that stores all global variables).
---@param defaultNativeBehavior? function If the native does not exist in the host table, use this default instead.
---@param hookedTableIsMetaTable? boolean Whether to store into the host's metatable instead. Defaults to true if the "default" parameter is given.
---@param hookAsBasicFn? boolean When adding a hook instance, the default behavior is to use the __call metamethod in metatables to govern callbacks. If this is `true`, it will instead use normal function callbacks.
---@return Hook.property
function Hook.add(
key,
callbackFn,
priority,
hostTableToHook,
defaultNativeBehavior,
hookedTableIsMetaTable,
hookAsBasicFn
)
priority = priority or 0
hostTableToHook = setupHostTable(hostTableToHook, defaultNativeBehavior, hookedTableIsMetaTable)
hostKeyTreeMatrix[hostTableToHook] =
hostKeyTreeMatrix[hostTableToHook] or
setmetatable({}, looseValuesMT)
local index = 1
local tree = hostKeyTreeMatrix[hostTableToHook][key]
if tree then
index = huntThroughPriorityList(tree, priority)
else
---@diagnostic disable-next-line: cast-local-type
tree = createHookTree(hostTableToHook, key, defaultNativeBehavior)
if not tree then
return ---@diagnostic disable-line: missing-return-value
end
end
local new = {
priority = priority,
tree = tree
}
function new.remove(deleteAllHooks)
Hook.delete(new, deleteAllHooks)
end
--new.debugId = tostring(callbackFn) .. ' and ' .. tostring(new)
if hookAsBasicFn then
new.hookAsBasicFn = callbackFn
else
setmetatable(new, {
__call = callbackFn,
__index = __index
})
end
reindexTree(tree, index, new)
return new
end
end
---Hook.basic avoids creating a metatable for the hook.
---This is necessary for adding hooks to metatable methods such as __index.
---The main difference versus Hook.add is in the parameters passed to callbackFn;
---Hook.add has a 'self' argument which points to the hook, whereas Hook.basic does not.
---@param key unknown
---@param callbackFn fun(Hook, ...):any
---@param priority? number
---@param hostTableToHook? table
---@param defaultNativeBehavior? function
---@param hookedTableIsMetaTable? boolean
function Hook.basic(key, callbackFn, priority, hostTableToHook, defaultNativeBehavior, hookedTableIsMetaTable)
return Hook.add(key, callbackFn, priority, hostTableToHook, defaultNativeBehavior, hookedTableIsMetaTable, true)
end
---@deprecated
---@see Hook.add for args
function AddHook(...)
local new = Hook.basic(...)
return function(...)
return new.next(...)
end, new.remove
end
setmetatable(Hook, {
__newindex = function(_, key, callback)
Hook.add(key, callback)
end
})
if Debug then Debug.endFile() end
if Debug then Debug.beginFile "PrecomputedHeightMap" end ---@diagnostic disable: param-type-mismatch
do LIBRARY_PrecomputedHeightMap = true
--[[
===============================================================================================================================================================
Precomputed Height Map
by Antares
===============================================================================================================================================================
GetTerrainZ(x, y) Replaces GetLocZ(x, y).
GetUnitZ(whichUnit) Replaces BlzGetUnitZ(whichUnit).
GetUnitCoordinates(whichUnit) Returns x, y, and z-coordinates of a unit.
GetCliffAdjustedZ(x, y) Returns a value in tiles neighboring cliffs that is more aligned with the visual effect. Requires STORE_CLIFF_DATA.
===============================================================================================================================================================
Computes the terrain height of your map on map initialization for later use. To get the terrain height at any location, use GetTerrainZ, replacing GetLocZ:
function GetLocZ(x, y)
MoveLocation(moveableLoc, x, y)
return GetLocationZ(moveableLoc)
end
GetTerrainZ is less prone to cause desyncs and is approximately twice as fast.
You have the option to save the height map to a file on map initialization. You can then reimport the data into the map to load the height map from that data.
This will make the use of Z-coordinates completely safe, as all clients are guaranteed to use exactly the same data. It is recommended to do this once for the
release version of your map.
To do this, set the flag for WRITE_HEIGHT_MAP and launch your map. The terrain height map will be generated on map initialization and saved to a file in your
Warcraft III\CustomMapData\ folder. Open that file in a text editor, then remove all occurances of
call Preload( "
" )
with find and replace (including the quotation marks and tab space). Then, remove
function PreloadFiles takes nothing returns nothing
call PreloadStart()
at the beginning of the file and
call PreloadEnd( 0.0 )
endfunction
at the end of the file. Finally, remove all line breaks by removing \n and \r. The result should be something like
HeightMapCode = "|pk44mM-b+b1-dr|krjdhWcy1aa1|eWcyaa"
except much longer.
Copy the entire string and paste it anywhere into the Lua root in your map, for example into the Config section of this library. Now, every time your map is
launched, the height map will be read from the string instead of being generated, making it guaranteed to be synced.
To check if the code has been generated correctly, launch your map one more time in single-player. The height map generated from the code will be checked against
one generated in the traditional way.
--=============================================================================================================================================================
Config
--=============================================================================================================================================================
]]
local SUBFOLDER = "PrecomputedHeightMap"
local STORE_CLIFF_DATA = true
local WRITE_HEIGHT_MAP = false
local VALIDATE_HEIGHT_MAP = true
local VISUALIZE_HEIGHT_MAP = false
--=============================================================================================================================================================
local heightMap = {} ---@type table[]
local terrainHasCliffs = {} ---@type table[]
local moveableLoc = nil ---@type location
local MINIMUM_Z = -256 ---@type number
local worldMinX
local worldMinY
local worldMaxX
local worldMaxY
local iMax
local jMax
local chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
local NUMBER_OF_CHARS = 52
local function GetLocZ(x, y)
MoveLocation(moveableLoc, x, y)
return GetLocationZ(moveableLoc)
end
GetTerrainZ = GetLocZ
---@param whichUnit unit
---@return number
function GetUnitZ(whichUnit)
return GetTerrainZ(GetUnitX(whichUnit), GetUnitY(whichUnit)) + GetUnitFlyHeight(whichUnit)
end
---@param whichUnit unit
---@return number, number, number
function GetUnitCoordinates(whichUnit)
local x = GetUnitX(whichUnit)
local y = GetUnitY(whichUnit)
return x, y, GetTerrainZ(x, y) + GetUnitFlyHeight(whichUnit)
end
local function CreateHeightMap()
local xMin = (worldMinX // 128)*128
local yMin = (worldMinY // 128)*128
local xMax = (worldMaxX // 128)*128 + 1
local yMax = (worldMaxY // 128)*128 + 1
local x = xMin
local y
local i = 1
local j
while x <= xMax do
heightMap[i] = {}
terrainHasCliffs[i] = {}
y = yMin
j = 1
while y <= yMax do
heightMap[i][j] = GetLocZ(x,y)
if VISUALIZE_HEIGHT_MAP then
BlzSetSpecialEffectZ(AddSpecialEffect("Doodads\\Cinematic\\GlowingRunes\\GlowingRunes0", x, y), heightMap[i][j] - 40)
end
if STORE_CLIFF_DATA then
local level1 = GetTerrainCliffLevel(x, y)
local level2 = GetTerrainCliffLevel(x, y + 128)
local level3 = GetTerrainCliffLevel(x + 128, y)
local level4 = GetTerrainCliffLevel(x, y + 128)
if level1 ~= level2 or level1 ~= level3 or level1 ~= level4 then
terrainHasCliffs[i][j] = true
end
end
j = j + 1
y = y + 128
end
i = i + 1
x = x + 128
end
iMax = i - 2
jMax = j - 2
---@param x number
---@param y number
---@return number
GetTerrainZ = function(x, y)
local rx = (x - worldMinX)/128 + 1
local ry = (y - worldMinY)/128 + 1
local i = rx // 1
local j = ry // 1
rx = rx - i
ry = ry - j
if i < 1 then
i = 1
rx = 0
elseif i > iMax then
i = iMax
rx = 1
end
if j < 1 then
j = 1
ry = 0
elseif j > jMax then
j = jMax
ry = 1
end
if rx + ry > 1 then --In top-right triangle
return (rx + ry - 1)*heightMap[i+1][j+1] + ((1 - rx)*heightMap[i][j+1] + (1 - ry)*heightMap[i+1][j])
else
return (1 - rx - ry)*heightMap[i][j] + (rx*heightMap[i+1][j] + ry*heightMap[i][j+1])
end
end
if not STORE_CLIFF_DATA then
GetCliffAdjustedZ = GetTerrainZ
end
end
---@param x number
---@param y number
---@return number
function GetCliffAdjustedZ(x, y)
local rx = (x - worldMinX)/128 + 1
local ry = (y - worldMinY)/128 + 1
local i = rx // 1
local j = ry // 1
rx = rx - i
ry = ry - j
if i < 1 then
i = 1
rx = 0
elseif i > iMax then
i = iMax
rx = 1
end
if j < 1 then
j = 1
ry = 0
elseif j > jMax then
j = jMax
ry = 1
end
if terrainHasCliffs[i][j] then
if rx < 0.5 then
if ry < 0.5 then
return heightMap[i][j]
else
return heightMap[i][j+1]
end
elseif ry < 0.5 then
return heightMap[i+1][j]
else
return heightMap[i+1][j+1]
end
else
if rx + ry > 1 then --In top-right triangle
return (rx + ry - 1)*heightMap[i+1][j+1] + ((1 - rx)*heightMap[i][j+1] + (1 - ry)*heightMap[i+1][j])
else
return (1 - rx - ry)*heightMap[i][j] + (rx*heightMap[i+1][j] + ry*heightMap[i][j+1])
end
end
end
local function ValidateHeightMap()
local xMin = (worldMinX // 128)*128
local yMin = (worldMinY // 128)*128
local xMax = (worldMaxX // 128)*128 + 1
local yMax = (worldMaxY // 128)*128 + 1
local numOutdated = 0
local x = xMin
local y
local i = 1
local j
while x <= xMax do
y = yMin
j = 1
while y <= yMax do
if heightMap[i][j] then
if VISUALIZE_HEIGHT_MAP then
BlzSetSpecialEffectZ(AddSpecialEffect("Doodads\\Cinematic\\GlowingRunes\\GlowingRunes0", x, y), heightMap[i][j] - 40)
end
if bj_isSinglePlayer and math.abs(heightMap[i][j] - GetLocZ(x, y)) > 1 then
numOutdated = numOutdated + 1
end
else
print("Height Map nil at x = " .. x .. ", y = " .. y)
end
j = j + 1
y = y + 128
end
i = i + 1
x = x + 128
end
if numOutdated > 0 then
print("|cffff0000Warning:|r Height Map is outdated at " .. numOutdated .. " locations...")
end
end
local function ReadHeightMap()
local charPos = 0
local numRepetitions = 0
local charValues = {}
for i = 1, string.len(chars) do
charValues[string.sub(chars, i, i)] = i - 1
end
local firstChar = nil
local PLUS = 0
local MINUS = 1
local ABS = 2
local segmentType = ABS
for i = 1, #heightMap do
for j = 1, #heightMap[i] do
if numRepetitions > 0 then
heightMap[i][j] = heightMap[i][j-1]
numRepetitions = numRepetitions - 1
else
local valueDetermined = false
while not valueDetermined do
charPos = charPos + 1
local char = string.sub(HeightMapCode, charPos, charPos)
if char == "+" then
segmentType = PLUS
charPos = charPos + 1
char = string.sub(HeightMapCode, charPos, charPos)
elseif char == "-" then
segmentType = MINUS
charPos = charPos + 1
char = string.sub(HeightMapCode, charPos, charPos)
elseif char == "|" then
segmentType = ABS
charPos = charPos + 1
char = string.sub(HeightMapCode, charPos, charPos)
end
if tonumber(char) then
local k = 0
while tonumber(string.sub(HeightMapCode, charPos + k + 1, charPos + k + 1)) do
k = k + 1
end
numRepetitions = tonumber(string.sub(HeightMapCode, charPos, charPos + k)) - 1
charPos = charPos + k
valueDetermined = true
heightMap[i][j] = heightMap[i][j-1]
else
if segmentType == PLUS then
heightMap[i][j] = heightMap[i][j-1] + charValues[char]
valueDetermined = true
elseif segmentType == MINUS then
heightMap[i][j] = heightMap[i][j-1] - charValues[char]
valueDetermined = true
elseif firstChar then
if charValues[firstChar] and charValues[char] then
heightMap[i][j] = charValues[firstChar]*NUMBER_OF_CHARS + charValues[char] + MINIMUM_Z
else
heightMap[i][j] = 0
end
firstChar = nil
valueDetermined = true
else
firstChar = char
end
end
end
end
end
end
HeightMapCode = nil
end
local function WriteHeightMap(subfolder)
PreloadGenClear()
PreloadGenStart()
local numRepetitions = 0
local firstChar
local secondChar
local stringLength = 0
local lastValue = 0
local PLUS = 0
local MINUS = 1
local ABS = 2
local segmentType = ABS
local preloadString = {'HeightMapCode = "'}
for i = 1, #heightMap do
for j = 1, #heightMap[i] do
if j > 1 then
local diff = (heightMap[i][j] - lastValue)//1
if diff == 0 then
numRepetitions = numRepetitions + 1
else
if numRepetitions > 0 then
table.insert(preloadString, numRepetitions)
end
numRepetitions = 0
if diff > 0 and diff < NUMBER_OF_CHARS then
if segmentType ~= PLUS then
segmentType = PLUS
table.insert(preloadString, "+")
end
elseif diff < 0 and diff > -NUMBER_OF_CHARS then
if segmentType ~= MINUS then
segmentType = MINUS
table.insert(preloadString, "-")
end
else
if segmentType ~= ABS then
segmentType = ABS
table.insert(preloadString, "|")
end
end
if segmentType == ABS then
firstChar = (heightMap[i][j] - MINIMUM_Z) // NUMBER_OF_CHARS + 1
secondChar = heightMap[i][j]//1 - MINIMUM_Z - (heightMap[i][j]//1 - MINIMUM_Z)//NUMBER_OF_CHARS*NUMBER_OF_CHARS + 1
table.insert(preloadString, string.sub(chars, firstChar, firstChar) .. string.sub(chars, secondChar, secondChar))
elseif segmentType == PLUS then
firstChar = diff//1 + 1
table.insert(preloadString, string.sub(chars, firstChar, firstChar))
elseif segmentType == MINUS then
firstChar = -diff//1 + 1
table.insert(preloadString, string.sub(chars, firstChar, firstChar))
end
end
else
if numRepetitions > 0 then
table.insert(preloadString, numRepetitions)
end
segmentType = ABS
table.insert(preloadString, "|")
numRepetitions = 0
firstChar = (heightMap[i][j] - MINIMUM_Z) // NUMBER_OF_CHARS + 1
secondChar = heightMap[i][j]//1 - MINIMUM_Z - (heightMap[i][j]//1 - MINIMUM_Z)//NUMBER_OF_CHARS*NUMBER_OF_CHARS + 1
table.insert(preloadString, string.sub(chars, firstChar, firstChar) .. string.sub(chars, secondChar, secondChar))
end
lastValue = heightMap[i][j]//1
stringLength = stringLength + 1
if stringLength == 100 then
Preload(table.concat(preloadString))
stringLength = 0
for k, __ in ipairs(preloadString) do
preloadString[k] = nil
end
end
end
end
if numRepetitions > 0 then
table.insert(preloadString, numRepetitions)
end
table.insert(preloadString, '"')
Preload(table.concat(preloadString))
PreloadGenEnd(subfolder .. "\\heightMap.txt")
print("Written Height Map to CustomMapData\\" .. subfolder .. "\\heightMap.txt")
end
local function InitHeightMap()
local xMin = (worldMinX // 128)*128
local yMin = (worldMinY // 128)*128
local xMax = (worldMaxX // 128)*128 + 1
local yMax = (worldMaxY // 128)*128 + 1
local x = xMin
local y
local i = 1
local j
while x <= xMax do
heightMap[i] = {}
terrainHasCliffs[i] = {}
y = yMin
j = 1
while y <= yMax do
heightMap[i][j] = 0
if STORE_CLIFF_DATA then
local level1 = GetTerrainCliffLevel(x, y)
local level2 = GetTerrainCliffLevel(x, y + 128)
local level3 = GetTerrainCliffLevel(x + 128, y)
local level4 = GetTerrainCliffLevel(x, y + 128)
if level1 ~= level2 or level1 ~= level3 or level1 ~= level4 then
terrainHasCliffs[i][j] = true
end
end
j = j + 1
y = y + 128
end
i = i + 1
x = x + 128
end
end
OnInit.global("PrecomputedHeightMap", function()
local worldBounds = GetWorldBounds()
worldMinX = GetRectMinX(worldBounds)
worldMinY = GetRectMinY(worldBounds)
worldMaxX = GetRectMaxX(worldBounds)
worldMaxY = GetRectMaxY(worldBounds)
moveableLoc = Location(0, 0)
if HeightMapCode then
InitHeightMap()
ReadHeightMap()
if VALIDATE_HEIGHT_MAP then
ValidateHeightMap()
end
else
CreateHeightMap()
if WRITE_HEIGHT_MAP then
WriteHeightMap(SUBFOLDER)
end
end
end)
end
if Debug then Debug.beginFile("WidgetType") end
do
local hash = nil ---@type hashtable
local isUnit = {}
local isDestructable = {}
local isItem = {}
local mt = {__mode = "k"}
setmetatable(isUnit, mt)
setmetatable(isDestructable, mt)
setmetatable(isItem, mt)
---@param agent agent
---@return boolean
function AgentIsWidget(agent)
SaveAgentHandle(hash, 0, 0, agent)
return LoadWidgetHandle(hash, 0, 0) ~= nil
end
---@param widget widget
---@return boolean
function WidgetIsUnit(widget)
if isUnit[widget] == nil then
SaveWidgetHandle(hash, 0, 0, widget)
isUnit[widget] = LoadUnitHandle(hash, 0, 0) ~= nil
end
return isUnit[widget]
end
---@param widget widget
---@return boolean
function WidgetIsDestructable(widget)
if isDestructable[widget] == nil then
SaveWidgetHandle(hash, 0, 0, widget)
isDestructable[widget] = LoadDestructableHandle(hash, 0, 0) ~= nil
end
return isDestructable[widget]
end
---@param widget widget
---@return boolean
function WidgetIsItem(widget)
if isItem[widget] == nil then
SaveWidgetHandle(hash, 0, 0, widget)
isItem[widget] = LoadItemHandle(hash, 0, 0) ~= nil
end
return isItem[widget]
end
---@param widget widget
---@return string
function GetWidgetType(widget)
if isUnit[widget] then
return "unit"
elseif isDestructable[widget] then
return "destructable"
elseif isItem[widget] then
return "item"
end
SaveWidgetHandle(hash, 0, 0, widget)
if LoadUnitHandle(hash, 0, 0) then
isUnit[widget] = true
return "unit"
elseif LoadDestructableHandle(hash, 0, 0) then
isDestructable[widget] = true
return "destructable"
else
isItem[widget] = true
return "item"
end
end
OnInit.global("WidgetType", function()
hash = InitHashtable()
end)
end
--[[
=============================================================================================================================================================
A Limitless Interaction Caller Engine
A Lua system to easily create highly performant checks and interactions, between any type of objects.
Requires:
TotalInitialization https://www.hiveworkshop.com/threads/total-initialization.317099/
=============================================================================================================================================================
Overview
=============================================================================================================================================================
To install this library, copy it into an empty script file in your map.
To enable debug mode, import "CustomTooltip.toc" and "CustomTooltip.fdf".
There are only few values to be set in the config. First, change the MAP_CREATORS field to your name.
The main work is creating actors and their interaction functions. Let's say we want to create a new spell. It should create multiple orbs at different locations
that each have an aura around them. In normal GUI/JASS/Lua, we would need a periodic timer, loop over our orbs, GroupEnumUnitsInRange to find units affected,
filter out the units that aren't eligible, and then run our code. ALICE takes care of the entire control structure of our code with a network of its main
internal object, the actor. The task of writing the correct control structure then transforms into assigning the actors the correct properties on creation.
=============================================================================================================================================================
What is an actor?
=============================================================================================================================================================
An actor is a class that is attached to any type of object and interacts with other actors that were added to the system. Two actors form a pair. Actors carry
identifiers and function interfaces determining their interactions and with which other actors they form a pair.
When two actors in a pair interact, an interaction function that you can specify will be called. After the pair's interaction is calculated, the next
interaction is placed in the queue. It is up to you to set the time interval between the two interactions. This is done via the return value of the
interactionFunc. The interactionFunc uses two input arguments which specify the "hosts" of the two actors; the objects the actors are attached to. Hosts can
be any type of object - a unit, an item, a class, a player - whatever thing you want the actor to represent. However, while you may pass any type of object as
the host, many features only support widgets and Lua tables.
Actors can both receive and initiate pairs. When a new actor is created, it checks if it should initiate a pair with each already existing actor and all
existing actors will check if they should initiate a pair themselves. Two-way pairs are not possible.
ALICE will automatically retrieve the position of an actor, if it's applicable. If it can't, it will create it as a global actor.
=============================================================================================================================================================
Schematic
=============================================================================================================================================================
PairingCheck β Identifier
Identifier β PairingCheck
β β β
Object A β Actor A Pair Actor B β Object B
β
*InteractionFunc β β
β β β
Object A β Object B β
β β
Time until next call β
*inherited from the actor initiating the pair.
=============================================================================================================================================================
How to create an actor
=============================================================================================================================================================
To create an actor, do ALICE_Create(host, identifier, interactionFunc). There are additional values you can set, but they're not important for getting started.
-----host-----
Any type of object which the actor is supposed to represent. You can create multiple actors for one host.
-----identifier-----
A string or string sequence that can be used in a filter to identify different types of actors. Examples: "hero", "missile", or {"unit", "hero", "human", "Hpal"}.
-----interactionFunc-----
A function or table that specifies the function that is called when this object interacts with another object. If a function is passed, the actor will pair with
everything (not recommended!). With a table, you can specify different interaction functions for different types of objects. For example: {milk = Drink, apple = Eat},
will use the Drink function for all actors with the "milk" identifier and the Eat function for all actors with the "apple" identifier.
Actors will only pair with other actors for which an interactionFunc exists, but you can use the "other" keyword to declare an interactionFunc that is used for
all actors not included in your list.
-----flags-----
Flags are an optional fourth input argument. They are discussed in the advanced tutorial.
-----ALICE_CreateFromClass-----
ALICE_CreateFromClass(class) creates an actor with a table as the host and retrieves all input arguments and all flags directly from the table fields.
=============================================================================================================================================================
How to create an interaction function
=============================================================================================================================================================
An interactionFunc must have two input arguments of any type, specifying the objects (not the actors), and one return value of type number, specifying the time
until the next interaction (in seconds). The object that is the source of the interaction func will always be passed as the first argument.
Within the interaction func, you have access to the Pair API.
Example:
function Explode(whichMine, whichUnit)
local dist = ALICE_PairGetDistance()
if dist < 100 then
ALICE_Kill(whichMine)
ALICE_Kill(whichUnit)
end
return (dist - 100)/300
end
If you want the interactions to be executed every step, you can omit the return value. This reduces the overhead required and improves performance. Note, however,
that the function type is determined only on first evaluation. Therefore, all branches inside the interactionFunc require a return value or none.
]]
--[[
=============================================================================================================================================================
Self-Interaction Functions
=============================================================================================================================================================
Self-interaction is a convenient way to execute code that affects only one actor. By default, actors will never pair with themselves or another actor with the
same host, but you can use the "self" keyword in an interactionFunc table to declare a self-interaction function. In the self-interaction function, the actor's
host is passed as both arguments.
Example:
interactionFunc = {missile = DestroyMissile, self = Move}
function Move(blast, __)
blast.x = blast.x + blast.vx
blast.y = blast.y + blast.vy
BlzSetSpecialEffectPosition(blast.specialEffect, blast.x, blast.y, 0)
end
This creates a projectile that moves itself every step and presumably destroys missiles on contact.
You can pass a table to define multiple self-interaction functions. Example:
interactionFunc = {missile = DestroyMissile, self = {Move, Accelerate}}
=============================================================================================================================================================
Flags
=============================================================================================================================================================
Flags are a table that contains additional parameters that allow you to control the behavior of actors much more than what is possible with just the standard
input arguments.
The possible flags are (ordered by increasing niche-ness):
radius
anchor
destroyOnDeath
isStationary
bindToBuff
bindToOrder
onActorDestroy
zOffset
cellCheckInterval
pairsWith
priority
hasInfiniteRange
Example: {isStationary = true, radius = 200}
-----radius-----
Overwrite the default object radius. The radius determines which cells the object is in. Objects can only interact with other objects with which they share
at least one cell, so the radius should be at least the object's interaction range. Some additional leeway is recommended, since cells are not updated on
every iteration.
-----anchor-----
Overwrites the host as the source of the actor coordinates with the object provided in anchor. Also reroutes the destroyOnDeath trigger to the anchor and
prevents actors anchored to the same object from pairing. Useful if you want to pass as the host a data table attached to a unit. Then you can anchor the actor
to the unit directly and omit coordinate fields in the table.
-----destroyOnDeath-----
Destroy the actor automatically when its host or anchor is destroyed. Works only for widgets.
-----isStationary-----
Disables cell checks for this object if it is stationary and cannot move to improve performance. Automatically enabled for destructables.
-----bindToBuff / bindToOrder-----
Automatically destroy the actor when the host or anchor unit no longer has the buff with the bindToBuff string or when its current order is no longer the
string bindToOrder.
-----onActorDestroy------
A function that is executed when the actor is destroyed. Passes the host as the argument.
-----zOffset-----
Moves the z-position of the object relative to its host or anchor. This modifies the behavior of ALICE_PairGetDistance3D() and similar functions.
-----cellCheckInterval-----
Overwrite the default cell check interval and set how often it should be checked if the object has left a cell it was previously in. A fast-moving object
should be checked more often than a slow-moving object.
-----pairsWith-----
A string, string sequence, or function that specifies which other actors the newly created actor has interactions with. For example: "unit" will pair with all
actors that have the "unit" identifier. By default, an actor will pair with all other actors for which an interaction function exists, but the pairsWith flag
adds another restriction. Using tables for interactionFuncs or using the pairsWith flag can achieve similar results, but the level of control with pairsWith
is much higher. Example:
ALICE_Create(host, "killer", {unit = KillUnit})
ALICE_Create(host, "killer", KillUnit, {pairsWith = "unit"})
are identical.
How to create a pairsWith flag is explained under actor filters.
-----priority-----
Determines which actor takes priority over the other when deciding which interactionFunc is called in a case where it would be ambiguous otherwise. If both
actors pair with the other, the interactionFunc of the one with the higher priority is called. The default priority is 0.
-----hasInfiniteRange-----
If this flag is set to true, the actor will interact over any distance like a global actor, except it still uses coordinates. More performant than setting the
radius to a very high value.
=============================================================================================================================================================
How to create Actor Filters
=============================================================================================================================================================
Actor filters are used in enum functions, in ALICE_HasIdentifier and in the pairsWith flag. Actor filters can be strings, tables, or functions.
A string will filter all actors that possess the identifier.
In a table, every entry has to be a string except for the optional last entry, which is a parameter to specifiy how the list should be interpreted.
{"A", "B", ..., MATCHING_TYPE_ANY} Filter all actors that have at least one of the listed identifiers. Default if not specified.
{"A", "B", ..., MATCHING_TYPE_ALL} Filter all actors that have all the listed identifiers.
{"A", "B", ..., MATCHING_TYPE_EXACT} Filter all actors that have exactly the listed identifiers.
{"A", "B", ..., MATCHING_TYPE_EXCLUDE} Filter all actors that have none of the listed identifiers.
A function must have one input argument that references the filtered actor's host and a boolean return value. Example: A newly created actor should only
pair with nonhero, undead units. The filter function could be:
function FilterNonheroUndead(filterHost)
--Prevent calling natives on a non-unit host.
if not ALICE_HasIdentifier(filterHost, "unit") then
return false
end
if IsUnitType(filterHost, UNIT_TYPE_HERO) then
return false
end
return IsUnitType(filterHost, UNIT_TYPE_UNDEAD)
end
This could of course also be achieved by setting all the necessary flags for each actor and using the table {"unit", "nonhero", "undead", MATCHING_TYPE_ALL}.
-----Owner filter-----
An actor created for a host that has an owner (a unit-type host or a class-type host with a CLASS_FIELD_OWNER field) can be filtered by table-type filters
based on its owner. This is done by inserting an additional value at the end of the table. Valid arguments are:
PLAYER_FILTER_TYPE_ALLY Pairs with all objects owned by an ally of the object's owner.
PLAYER_FILTER_TYPE_ENEMY Pairs with all objects owned by an enemy of the object's owner.
PLAYER_FILTER_TYPE_OWNER Pairs with all objects owned by the object's owner.
PLAYER_FILTER_TYPE_NOT_OWNER Pairs with all objects not owned by the object's owner.
Player Pairs with all objects owned by the specified player.
Example: {"unit", "ground", MATCHING_TYPE_ALL, PLAYER_FILTER_TYPE_ENEMY}
will pair with all ground units that are owned by an enemy of the new object's owner.
=============================================================================================================================================================
]]
--[[
====================================================================================================================================================================
A P I
====================================================================================================================================================================
β’ All object functions use keyword as an optional parameter. This parameter is only needed if
you create multiple actors for one host. It searches for the actor that has that keyword in
its identifier.
β’ All functions starting with ALICE_Pair can only be used within user-defined functions called
by ALICE.
-----CORE API------
ALICE_Create(host, identifier, interactionFunc, flags) Create an actor for the object host and add it to the cycle.
ALICE_CreateFromClass(whichClass) Create an actor for a table host and retrieve all input arguments from that table.
ALICE_Destroy(whichObject) Destroy the actor of the object and remove it from the cycle.
ALICE_Kill(whichObject) Calls the appropriate function to destroy the object, then destroys all actors attached
to it. If the object is a table, the whichObject:destroy() method will be called. If no
destroy method exists, it will try to destroy the class's special effect.
-----MATH API-----
ALICE_PairGetDistance2D() Returns the distance between the objects of the pair currently being evaluated in two
dimensions.
ALICE_PairGetDistance3D() The same, but takes z into account.
ALICE_PairGetAngle2D() Returns the angle from object A to object B of the pair currently being evaluated.
ALICE_PairGetAngle3D() Returns the horizontal and vertical angles from object A to object B.
ALICE_PairGetCoordinates2D() Returns the coordinates of the objects in the pair currently being evaluated in the order
x1, y1, x2, y2.
ALICE_PairGetCoordinates3D() Returns the coordinates of the objects in the pair currently being evaluated in the order
x1, y1, z1, x2, y2, z2.
ALICE_GetCoordinates2D(whichObject) Returns the coordinates x, y of an object.
ALICE_GetCoordinates3D(whichObject) Returns the coordinates x, y, z of an object.
-----PAIR UTILITY API-----
ALICE_PairIsFriend() Returns true if the owners of the objects in the current pair are allies.
ALICE_PairIsEnemy() Returns true if the owners of the objects in the current pair are enemies.
ALICE_PairSetInteractionFunc(whichFunc) Changes the interactionFunc of the pair currently being evaluated. You cannot replace a
function with a return value with one that has no return value and vice versa.
ALICE_PairDisable() Disables interactions between the actors of the current pair after this one.
ALICE_PairLoadData() Returns a table unique to the pair currently being evaluated, which can be used to
read and write data. Optimal argument to set a metatable for the data table.
ALICE_PairOnDestroy(callback) Executes the function callback(objectA, objectB, pairData) when either of the actors in
the current pair are destroyed. Note: Only one callback. Avoid inlining callback function
to avoid garbage build-up.
ALICE_PairIsFirstContact() Returns true if this is the first time this function was invoked for the current pair,
otherwise false.
ALICE_PairIsUnoccupied() Returns false if this function was invoked for another pair that has the same
interactionFunc and the same receiving actor. Otherwise, returns true. In other words,
only one pair can execute the code within an ALICE_PairIsUnoccupied() block. Useful for
creating non-stacking effects.
ALICE_PairCooldown(duration) Returns true if this function isn't on cooldown for the pair currently being evaluated,
then invokes a cooldown of the specified duration.
ALICE_PairForget() Purge pair data, call onDestroy function and reset ALICE_PairIsFirstContact and
ALICE_IsUnoccupied functions.
β’ Type "downtherabbithole" in-game to enable debug mode. In debug mode, you can click near an
object to visualize its actor, its interactions, and see all attributes of that actor.
-----DEBUG API------
ALICE_ListGlobals() List all global actors.
ALICE_GetFromUnique(number) Returns the actor with the specified unique number.
ALICE_Select(whichActor) Select the specified actor. Pass integer to select by unique number. Requires debug
mode.
ALICE_PairVisualize(duration) Create a lightning effect between the objects of the current pair. Useful to check
if objects are interacting as intended. Optional lightning type argument.
ALICE_HighlightFunc(whichFunc) Changes the lightning type for the visualization lightnings of the interactionFunc
to stand out in debug mode. If the function is global, you can refer to it by name.
ALICE_VisualizeCells(whichObject, enable) Enable/disable visualization of the cells the object is currently in.
ALICE_PairsWith(objectA, objectB) Checks if the objectA would pair with the objectB.
ALICE_Halt() Halt the entire cycle.
ALICE_SlowMotion(factor) Slow down the ALICE cycle by the specified factor. Use ALICE_TimeScale to refer to
the slow motion factor in external functions.
ALICE_Resume() Resume the entire cycle.
ALICE_Statistics() Prints out statistics showing which actors are occupying which percentage of the
calculations.
ALICE_Benchmark() Continuously prints the cycle evaluation time and the number of actors, pair interactions,
and cell checks until disabled.
ALICE_VisualizeAllCells() Create lightning effects around all cells.
ALICE_VisualizeAllActors() Creates arrows above all non-global actors.
====================================================================================================================================================================
A D V A N C E D A P I
====================================================================================================================================================================
β’ Enum functions return a table with all objects that are in included in filter. If the filter
specifies a player filter, you need to specify the whoFilters player. Example:
ALICE_EnumObjects({"unit", PLAYER_FILTER_TYPE_ALLY}, Player(0)) will return all units that are
owned by an ally of red.
-----ENUM API-----
ALICE_EnumObjects(filter, whoFilters)
ALICE_EnumObjectsInRange(x, y, range, filter, whoFilters)
ALICE_EnumObjectsInRect(minx, miny, maxx, maxy, filter, whoFilters)
ALICE_ForAllObjectsDo(filter, whoFilters, action)
ALICE_ForAllObjectsInRangeDo(x, y, range, filter, whoFilters, action)
ALICE_ForAllObjectsInRectDo(minx, miny, maxx, maxy, filter, whoFilters, action)
-----OBJECT UTILITY API------
ALICE_AddSelfInteraction(whichObject, whichFunc) Adds a self-interaction with the specified function to the object. If a self-interaction
with that function already exists, nothing happens.
ALICE_RemoveSelfInteraction(whichObject, whichFunc) Removes the self-interaction with the specified function from the object.
ALICE_SetClassOwner(whichObject) Change the owner of a class-type object.
-----IDENTIFIER API------
ALICE_AddIdentifier(whichObject, whichIdentifier) Add identifier(s) to an object and pair it with all other objects it is now eligible
to be paired with. Slow function!
ALICE_RemoveIdentifier(whichObject, whichIdentifier) Remove identifier(s) from an object and remove all pairings with objects it is no
longer eligible to be paired with. Slow function!
ALICE_SwapIdentifier(whichObject, oldIdentifier, newIdentifier) Exchanges one of the object's identifier with another. If the old identifier is not
found, the new one won't be added. Slow function!
ALICE_SetIdentifier(whichObject, newIdentifier) Sets the object's identifier to a string or string sequence. Slow function!
ALICE_HasIdentifier(whichObject, identifier) Checks if the object toCheck contains the identifier toMatch. ToMatch can be a string
or a string sequence with optional matching type entry (see under Pairing Logic).
ALICE_GetIdentifier(whichObject) Compiles the identifiers of an object into a table.
-----ACTOR API-----
ALICE_PairGetActors() Returns the actors of the current pair. All actor fields should be considered read-only!
ALICE_HasActor(whichObject) Checks if an actor already exists with the object as its host.
ALICE_GetActor(whichObject) Returns the actor stored to a host.
ALICE_GetAllActors(whichObject) Returns a table containing all actors attached to an object.
-----MISC API-----
ALICE_FuncSetInit(whichFunc, initFunc) Calls the initFunc with the hosts as arguments whenever a pair is created with the
specified interactionFunc.
ALICE_FuncSetPersistOnOutOfRange(whichFunc) Changes the behavior of pairs using the specified function so that the interactions
continue to be evaluated when the two objects leave their interaction range.
ALICE_TimeElapsed The time elapsed since the start of the game. Useful to store the time of the last
interaction between two objects.
ALICE_Periodic(timeOut, callback) Starts a periodic timer that invokes the callback function. The timer is affected by
ALICE_SlowMotion. The callback function has a boolean return value that specifies if the
loop should be terminated.
-----OPTIMIZATION API-----
ALICE_PairPause() Pauses interactions of the current pair after this one. This function is unreliable and
should only be used to improve performance. Resume with ALICE_Unpause.
ALICE_Unpause(whichObject) Unpauses all paused interactions of the object.
ALICE_FuncSetMaxRange(whichFunc, maxRange) All pairs using this function will get automatically disabled if both objects are
stationary and their distance is greater than the max range.
ALICE_FuncSetRandomDelay(whichFunc, randomDelay) The first interaction of all pairs using this function will be delayed by a random time
from 0 to the specified delay. Useful to spread out interactions and avoid lag spikes
when creating many objects at the same time.
ALICE_SetRadius(whichObject, newRadius) Changes the radius of the object.
ALICE_SetStationary(whichObject, enable) Sets an object to stationary/not stationary.
ALICE_SetCellCheckInterval(whichObject, newInterval) Changes how often ALICE checks if the object has entered a new cell.
====================================================================================================================================================================
S U P E R A D V A N C E D A P I
====================================================================================================================================================================
β’ The Matrix API allows external access to pair data and share that data between pairs.
-----MATRIX API-----
ALICE_MatrixCreate(whichFunc) Creates and returns a 2D-table storing data for all objects that are paired using the
specified function or list of functions. Whenever a pair is created using one of the
functions, new entries are created automatically. Matrix entries are duplicated and
stored both as a sequence and as a dictionary with the objects as keys.
ALICE_WriteMatrix(objectA, objectB, A, B, key) Writes the value A into the data matrix at [objectA][objectB][key] and the value B at
[objectB][objectA][key]. Pass KEEP_VALUE to not overwrite the current value and only
store one. You can store multiple values by passing a string sequence as key.
ALICE_PairReadMatrix() Returns two tables with the data stored at [objectA][objectB] and at [objectB][objectA]
in the matrix associated with the current pair's interactionFunc.
ALICE_LoadMatrixRow(object, whichFunc) Load the matrix row for the specified object and function.
====================================================================================================================================================================
]]
--[[
=============================================================================================================================================================
Troubleshooting
=============================================================================================================================================================
-----downtherabbithole-----
In-game, you can type "downtherabbithole" to enable debug mode. While in debug mode, you can left-click on objects to select their actor and receive information
about their attributes. In addition, their current cells and interactions will be visualized. You can combine debug mode with Eikonium's IngameConsole to execute
ALICE debug functions. Requires "CustomTooltip.toc" and "CustomTooltip.fdf". Set your interactionFuncs to global so that their names can get displayed.
When a thread crashes, ALICE will print out the types and the unique numbers of the actors responsible. You can type ALICE_Select(uniqueNumber) to select one
of those actors.
Global actors cannot be selected by clicking on them, but you can select them with ALICE_ListGlobals() and then ALICE_Select(uniqueNumber).
-----Actors not interacting-----
This can be due to actors not having the intended pairing behavior, because of interaction misses, or because of one of the actor's radii being too small.
To check if the pairing behavior is correct, check the actor tooltip in debug mode or do ALICE_PairsWith.
An interaction miss occurs when two objects get into proximity with each other and should interact, but do not because the interval calculated during their
last interaction was too long and they're still in the queue. To avoid interaction misses, set the interaction length sufficiently low, especially during
debugging, even to 0 (the interval will be ceiled to MIN_INTERVAL).
If you select an actor, you will see the cells it is currently in. This will incidate its interaction range. If you set the radius of an actor too low or forget
to change it from its default value, the actors will interact, but only at short range. The radius should be set to the maximum interaction distance plus some leeway.
The leeway should be equal to the maximum distance the object can travel during two cell checks (CELL_CHECK_INTERVAL). Select the actor in debug mode to see the
cells the system thinks it's currently in.
You can add lightnings with ALICE_PairVisualize(duration) in the interaction func. This will create a lightning between the hosts of two interacting actors
whenever their interaction function is called. This does not work for global actors.
-----Actors interacting when they shouldn't-----
If you create a new actor, it will not only pair with other actors based on its own pairing logic, but other actors will also check if the new actor is included
in their logic. For example, you place a few units on the map, then create an effect that should affect those units. Because there are no other actors on the
map at that moment, you might not restrict how it pairs in any way. However, that actor will not only pair with those units but will continue to pair with every
new actor that gets created.
-----Attempting to do arithmetic on a nil value-----
This error could mean that you forgot the interactionFunc's return value in one of the possible branches. Note that a function should either always or never have
a return value.
-----Thread crash-----
If your custom code crashes the thread, ALICE will remove the pair that caused the crash and prevent the two actors from pairing again, then on the second crash
caused by the same actor isolate that actor completely.
-----Never edit actor class fields-----
ALICE_GetActor gives you access to the interal actor class, but you should still only edit its class fields with the API functions. Editing class fields can lead
to all kinds of unforeseeable effects.
-----Forgetting to destroy an actor-----
Forgetting to destroy an actor when the associated host is destroyed will keep the pairs attached to that actor inside the cycle indefinitely. The pairs will
continue to be evaluated, clogging up the system. Because the reference to the host is lost, depending on the interactionFunc you wrote, it might crash the
function. Always set the .destroyOnDeath field to true to actors attached to widgets unless they're already bound to a buff or channeling spell.
=============================================================================================================================================================
]]
if Debug then Debug.beginFile "ALICE" end
---@diagnostic disable: need-check-nil
do
--[[
=============================================================================================================================================================
A Limitless Interaction Caller Engine
A Lua system to easily create highly performant checks and interactions, between any type of objects.
Requires:
TotalInitialization https://www.hiveworkshop.com/threads/total-initialization.317099/
WidgetType Included in Test Map.
PrecomputedHeightMap (optional) https://www.hiveworkshop.com/threads/precomputed-synchronized-terrain-height-map.353477/
=============================================================================================================================================================
C O N F I G
=============================================================================================================================================================
]]
local MAP_CREATORS = {"Antares", "WorldEdit"} ---@type string[]
--Print out warnings, errors, and enable the "downtherabbithole" cheat code for the players with these names. #XXXX not required.
ALICE_MIN_INTERVAL = 0.02 ---@type number
--Minimum interval between interactions in seconds. Sets the time step of the timer. All interaction intervals are an integer multiple of this value.
--Class Interface (important if you want to use tables as hosts, tells ALICE which fields it should look out for to retrieve information from your classes).
ALICE_CLASS_FIELD_OWNER = "owner" ---@type string
--The label of the owner of class-type hosts. If the field is not present, the actor will have no owner. Owner can be a player or converted player id.
--To change the owner of a class after creation, do not change the class field, but use ALICE_SetClassOwner.
ALICE_CLASS_FIELD_EFFECT = "visual" ---@type string
--The label of the special effect of class-type hosts (missiles etc).
--Debugging
local MAXIMUM_EVALUATION_TIME = 0.03 ---@type number
--If each cycle evaluation takes longer than this many seconds for a longer period of time, the cycle will terminate, preventing the game from freezing. You
--can then enable debug mode and better gather information about what caused the crash.
local HALT_ON_FIRST_CRASH = true ---@type boolean
--Abort the cycle the first time it crashes. Makes it easier to identify a bug if downstream errors are prevented.
--Optimization
ALICE_MAX_INTERVAL = 10.0 ---@type number
--Maximum interval between interactions in seconds.
local NUM_CELLS_X = 70 ---@type integer
local NUM_CELLS_Y = 70 ---@type integer
--The playable map area is partitioned into this many cells. Objects only interact with other objects that share a cell with them. Larger maps require more
--cells.
ALICE_DEFAULT_CELL_CHECK_INTERVAL = 0.1 ---@type number
--How often the system checks if objects left their current cell. Should be overwritten with the cellCheckInterval flag for fast-moving objects.
ALICE_DEFAULT_OBJECT_RADIUS = 75 ---@type number
--How large an actor is when it comes to determining in which cells it is in. Should be overwritten with the radius flag for objects with a larger interaction
--range.
--===========================================================================================================================================================
--#region Variables
local ceil = math.ceil
local floor = math.floor
local max = math.max
local min = math.min
local sqrt = math.sqrt
local atan = math.atan
local CLASS_FIELD_OWNER = ALICE_CLASS_FIELD_OWNER
local CLASS_FIELD_EFFECT = ALICE_CLASS_FIELD_EFFECT
local MIN_INTERVAL = ALICE_MIN_INTERVAL
local MAX_INTERVAL = ALICE_MAX_INTERVAL
local MASTER_TIMER = nil ---@type timer
local MAX_STEPS = 0 ---@type integer
local CYCLE_LENGTH = 0 ---@type integer
local DO_NOT_EVALUATE = 0 ---@type integer
local USE_CELLS = NUM_CELLS_X > 0 and NUM_CELLS_Y > 0 ---@type boolean
local MAP_MIN_X ---@type number
local MAP_MAX_X ---@type number
local MAP_MIN_Y ---@type number
local MAP_MAX_Y ---@type number
local CELL_MIN_X = {} ---@type number[]
local CELL_MIN_Y = {} ---@type number[]
local CELL_MAX_X = {} ---@type number[]
local CELL_MAX_Y = {} ---@type number[]
local CELL_LIST = {} ---@type table[]
for i = 1, NUM_CELLS_X do
CELL_LIST[i] = {}
end
local counter = 0 ---@type integer
local unboundCounter = 0 ---@type integer
local currentPair = nil ---@type Pair | nil
local totalActors = 0 ---@type integer
local freezeCounter = 0 ---@type number
local isCrash = false ---@type boolean
local numPairs = {} ---@type integer[]
local whichPairs = {} ---@type table[]
local numFixedPairs = 0 ---@type integer
local fixedPairs = {} ---@type Pair[]
local numUncategorizedPairs = 0 ---@type integer
local uncategorizedPairs = {} ---@type Pair[]
local actorList = {} ---@type Actor[]
local celllessActorList = {} ---@type Actor[]
local pairList = {} ---@type Pair[]
local pairingExcluded = {} ---@type table[]
local excludedManually = {} ---@type table[]
local numCellChecks = nil ---@type integer[]
local cellCheckedActors = {} ---@type Actor[]
local unusedPairs = {} ---@type Pair[]
local unusedActors = {} ---@type Actor[]
local unusedTables = {} ---@type table[]
local destroyedActors = {} ---@type Actor[]
local actorOf = {} ---@type Actor[]
local bindChecks = {} ---@type Actor[]
local isEveryStepFunction = {} ---@type boolean[]
local functionMaxRange = {} ---@type number[]
local functionPersistOnBreak = {} ---@type boolean[]
local functionInitializer = {} ---@type function[]
local functionRandomDelay = {} ---@type number[]
local functionMatrix = {} ---@type table[]
local delayedCallbacks = {} ---@type function[]
local selfInteractionActor = nil ---@type Actor
local INV_MIN_INTERVAL = 1/MIN_INTERVAL - 0.001 ---@type number
ALICE_TimeScale = 1.0 ---@type number
ALICE_TimeElapsed = 0.0 ---@type number
MATCHING_TYPE_ANY = 0 ---@type integer
MATCHING_TYPE_ALL = 1 ---@type integer
MATCHING_TYPE_EXACT = 2 ---@type integer
MATCHING_TYPE_EXCLUDE = 3 ---@type integer
HIGHEST_MATCHING_TYPE = 3 ---@type integer
PLAYER_FILTER_TYPE_ALLY = 4 ---@type integer
PLAYER_FILTER_TYPE_ENEMY = 5 ---@type integer
PLAYER_FILTER_TYPE_OWNER = 6 ---@type integer
PLAYER_FILTER_TYPE_NOT_OWNER = 7 ---@type integer
PLAYER_FILTER_TYPE_ANY = 8 ---@type integer
KEEP_VALUE = {} ---@type table
local GLOBAL_HOST = {} ---@type table
--Will warn you if you pass a flag in ALICE_Create that's not in this list.
local RECOGNIZED_FIELDS = { ---@type boolean[]
pairsWith = true,
priority = true,
anchor = true,
zOffset = true,
cellCheckInterval = true,
isStationary = true,
radius = true,
destroyOnDeath = true,
bindToBuff = true,
bindToOrder = true,
onActorDestroy = true,
randomDelay = true,
hasInfiniteRange = true,
}
--Array indices for pair fields. Storing as a sequence reduces memory usage.
local ActorA = 1 ---@type integer
local ActorB = 2 ---@type integer
local IndexA = 3 ---@type integer
local IndexB = 4 ---@type integer
local InteractionFunc = 5 ---@type integer
local CurrentPosition = 6 ---@type integer
local PositionInStep = 7 ---@type integer
local DestructionQueued = 8 ---@type integer
local HostA = 9 ---@type integer
local HostB = 10 ---@type integer
local EveryStep = 11 ---@type integer
local UserData = 12 ---@type integer
local Pause = 13 ---@type integer
local debugModeEnabled = false ---@type boolean
local mouseClickTrigger = nil ---@type trigger
local cycleSelectTrigger = nil ---@type trigger
local selectedActor = nil ---@type Actor | nil
local debugTooltip = nil ---@type framehandle
local debugTooltipText = nil ---@type framehandle
local debugTooltipTitle = nil ---@type framehandle
local highlightedFunc = nil ---@type function | nil
local benchmark = false ---@type boolean
local visualizeAllActors = false ---@type boolean
local visualizeAllCells = false ---@type boolean
local evaluationTime = {} ---@type table
local Create = nil ---@type function
local Destroy = nil ---@type function
local CreateReference = nil ---@type function
local RemoveReference = nil ---@type function
local SetCoordinateFuncs = nil ---@type function
local SetOwner = nil ---@type function
local CreateOnDeathTrigger = nil ---@type function
local InitPairingLogic = nil ---@type function
local InitCells = nil ---@type function
local InitCellChecks = nil ---@type function
local Teleport = nil ---@type function
local SharesCellWith = nil ---@type function
local CreateBinds = nil ---@type function
local ReevaluatePairings = nil ---@type function
local VisualizeCells = nil ---@type function
local RedrawCellVisualizers = nil ---@type function
local Isolate = nil ---@type function
local Deselect = nil ---@type function
local Select = nil ---@type function
local GetDescription = nil ---@type function
local CreateVisualizer = nil ---@type function
local GetTerrainZ = nil ---@type function
local GetActor = nil ---@type function
local moveableLoc = nil ---@type location
local Warning = nil ---@type function
--#endregion
--===========================================================================================================================================================
--Filter Functions
--===========================================================================================================================================================
--#region Filter Functions
---For debug functions.
---@param whichIdentifier string | string[] | nil
local function Identifier2String(whichIdentifier)
if whichIdentifier == nil then
return "()"
elseif type(whichIdentifier) == "string" then
return whichIdentifier
elseif type(whichIdentifier) == "table" then
local toString = "("
local i = 1
for key, __ in pairs(whichIdentifier) do
if i > 1 then
toString = toString .. ", "
end
toString = toString .. key
i = i + 1
end
toString = toString .. ")"
return toString
end
return nil
end
---For pairing check.
---@param toCheck table
---@param toMatch table
---@return boolean
local function CheckIdentifierOverlap(toCheck, toMatch)
local identifier = toCheck.identifier
local pairsWith = toMatch.pairsWith
local matchingType = toMatch.matchingType
if matchingType == MATCHING_TYPE_ANY then
for key, __ in pairs(pairsWith) do
if identifier[key] then
return true
end
end
return false
elseif matchingType == MATCHING_TYPE_ALL then
for key, __ in pairs(pairsWith) do
if not identifier[key] then
return false
end
end
return true
elseif matchingType == MATCHING_TYPE_EXACT then
for key, __ in pairs(pairsWith) do
if not identifier[key] then
return false
end
end
for key, __ in pairs(identifier) do
if not pairsWith[key] then
return false
end
end
return true
elseif matchingType == MATCHING_TYPE_EXCLUDE then
for key, __ in pairs(pairsWith) do
if identifier[key] then
return false
end
end
return true
else
Warning("|cffff0000Warning:|r Invalid matching type specified...")
return false
end
end
---For pair statistics.
---@param toMatch any
---@param toCheck any
---@return boolean
local function HasExactIdentifier(toMatch, toCheck)
for key, __ in pairs(toMatch) do
if not toCheck[key] then
return false
end
end
for key, __ in pairs(toCheck) do
if not toMatch[key] then
return false
end
end
return true
end
---@param whichPlayer player
---@param whichFilter integer
---@return boolean
local function IsInPlayerFilter(whichPlayer, whichFilter, whoFilters)
---PLAYER_FILTER_TYPE_ANY evaluated before function call.
if whichPlayer == nil then
return false
elseif type(whichFilter) == "userdata" then
return whichPlayer == whichFilter
elseif whoFilters == nil then
return true
elseif whichFilter == PLAYER_FILTER_TYPE_ALLY then
return IsPlayerAlly(whichPlayer, whoFilters)
elseif whichFilter == PLAYER_FILTER_TYPE_ENEMY then
return IsPlayerEnemy(whichPlayer, whoFilters)
elseif whichFilter == PLAYER_FILTER_TYPE_OWNER then
return whichPlayer == whoFilters
elseif whichFilter == PLAYER_FILTER_TYPE_NOT_OWNER then
return whichPlayer ~= whoFilters
end
Warning("|cffff0000Warning:|r Unrecognized player filter type...")
return true
end
--#endregion
--===========================================================================================================================================================
--Utility
--===========================================================================================================================================================
--#region Utility
Warning = function(whichWarning)
for __, name in ipairs(MAP_CREATORS) do
if string.find(GetPlayerName(GetLocalPlayer()), name) then
print(whichWarning)
end
end
end
local function GetUnusedTable()
if #unusedTables == 0 then
return {}
else
local returnTable = unusedTables[#unusedTables]
unusedTables[#unusedTables] = nil
return returnTable
end
end
local function ReturnTable(whichTable)
for key, __ in pairs(whichTable) do
whichTable[key] = nil
end
unusedTables[#unusedTables + 1] = whichTable
setmetatable(whichTable, nil)
end
---@param func function
---@return string
local function FunctionToString(func)
local string = string.gsub(tostring(func), "function: ", "")
if string.sub(string,1,1) == "0" then
return string.sub(string, string.len(string) - 3, string.len(string))
else
return string
end
end
---@param whichFunction function
local function DelayCallback(whichFunction)
--Certain functions must not be called from within interactionFuncs. This postpones their call until the loop is over.
delayedCallbacks[#delayedCallbacks + 1] = whichFunction
end
---@param whichPair Pair
---@param duration number
---@param lightningType string
local function VisualizationLightning(whichPair, duration, lightningType)
local A = whichPair[ActorA]
local B = whichPair[ActorB]
if A.isGlobal or B.isGlobal then
return
end
local xa = A.x[A.anchor]
local ya = A.y[A.anchor]
local za = A.z[A]
local xb = B.x[B.anchor]
local yb = B.y[B.anchor]
local zb = B.z[B]
local visLight
if za and zb then
visLight = AddLightningEx(lightningType, true, xa, ya, za, xb, yb, zb)
else
visLight = AddLightning(lightningType, true, xa, ya, xb, yb)
end
TimerStart(CreateTimer(), duration*ALICE_TimeScale, false, function()
DestroyLightning(visLight)
DestroyTimer(GetExpiredTimer())
end)
end
local function OnUnitChangeOwner()
local u = GetTriggerUnit()
if actorOf[u] then
if actorOf[u].isActor then
SetOwner(actorOf[u], actorOf[u].host)
if actorOf[u].reevaluateCaller == nil then
actorOf[u].reevaluateCaller = function() ReevaluatePairings(actorOf[u]) end
end
DelayCallback(actorOf[u].reevaluateCaller)
else
for __, actor in ipairs(actorOf[u]) do
SetOwner(actor, actor.host)
if actor.reevaluateCaller == nil then
actor.reevaluateCaller = function() ReevaluatePairings(actorOf[u]) end
end
DelayCallback(actor.reevaluateCaller)
end
end
end
end
---@param currentPairList table
---@return string
local function GetPairStatistics(currentPairList)
if #currentPairList == 0 then
return "\nThere are no pairs currently being evaluated."
end
local countActivePairs = 0
local pairTypes = {}
for __, pair in ipairs(currentPairList) do
if not pair[DestructionQueued] then
local idA = pair[ActorA].identifier
local idB = pair[ActorB].identifier
local recognized = false
for __, pairType in ipairs(pairTypes) do
if HasExactIdentifier(pairType[1], idA) and HasExactIdentifier(pairType[2], idB) then
recognized = true
pairType[3] = pairType[3] + 1
break
end
end
if not recognized then
table.insert(pairTypes, {idA, idB, 1})
end
countActivePairs = countActivePairs + 1
end
end
local statistic = ""
if countActivePairs == 0 then
return "\nThere are no pairs currently being evaluated."
end
table.sort(pairTypes, function(a, b) return a[3] > b[3] end)
for __, pairType in ipairs(pairTypes) do
if floor(100*pairType[3]/countActivePairs) > 1 then
statistic = statistic .. "\n" .. floor(100*pairType[3]/countActivePairs) .. "\x25 |cffffcc00" .. Identifier2String(pairType[1]) .. "|r, |cffaaaaff" .. Identifier2String(pairType[2]) .. "|r"
end
end
return statistic
end
local function WillAlwaysBeOutOfRange(actorA, actorB, maxRange)
if not actorA.usesCells or not actorB.usesCells then
return false
end
if not actorA.isStationary or not actorB.isStationary then
return false
end
local dx = actorA.x[actorA.anchor] - actorB.x[actorB.anchor]
local dy = actorA.y[actorA.anchor] - actorB.y[actorB.anchor]
return dx*dx + dy*dy > maxRange*maxRange
end
local function DoNothing(__,__)
--Dummy function if correct interactionFunc could not be determined.
return MAX_INTERVAL
end
--#endregion
--===========================================================================================================================================================
--Coordinate funcs
--===========================================================================================================================================================
--#region Coordinate Funcs
--x and y are stored with an anchor key because GetUnitX etc. cannot take an actor as an argument and they are identical for all actors anchored to the same
--object. z is stored with an actor key because it requires zOffset, which is stored on the actor, and the z-values are not guaranteed to be identical for all
--actors anchored to the same object.
---@param anchor table
---@return number
local function GetClassX(anchor)
return anchor.x
end
---@param anchor table
---@return number
local function GetClassY(anchor)
return anchor.y
end
---@param actor Actor
---@return number
local function GetClassZ(actor)
return actor.anchor.z + actor.zOffset
end
---@param actor Actor
---@return number
local function GetUnitZ(actor)
return GetTerrainZ(actor.x[actor.anchor], actor.y[actor.anchor]) + GetUnitFlyHeight(actor.anchor) + actor.zOffset
end
---@param actor Actor
---@return number
local function GetDestructableZ(actor)
return GetTerrainZ(actor.x[actor.anchor], actor.y[actor.anchor]) + actor.zOffset
end
---@param actor Actor
---@return number
local function GetItemZ(actor)
return GetTerrainZ(actor.x[actor.anchor], actor.y[actor.anchor]) + actor.zOffset
end
local classX = {}
local classY = {}
local classZ = {}
local unitX = {}
local unitY = {}
local unitZ = {}
local destructableX = {}
local destructableY = {}
local destructableZ = {}
local itemX = {}
local itemY = {}
local itemZ = {}
local globalXYZ = {} --Empty table, returns nil
setmetatable(classX, {__index = function(self, key) self[key] = GetClassX(key) return self[key] end})
setmetatable(classY, {__index = function(self, key) self[key] = GetClassY(key) return self[key] end})
setmetatable(classZ, {__index = function(self, key) self[key] = GetClassZ(key) return self[key] end})
setmetatable(unitX, {__index = function(self, key) self[key] = GetUnitX(key) return self[key] end})
setmetatable(unitY, {__index = function(self, key) self[key] = GetUnitY(key) return self[key] end})
setmetatable(unitZ, {__index = function(self, key) self[key] = GetUnitZ(key) return self[key] end})
setmetatable(destructableX, {__index = function(self, key) self[key] = GetDestructableX(key) return self[key] end})
setmetatable(destructableY, {__index = function(self, key) self[key] = GetDestructableY(key) return self[key] end})
setmetatable(destructableZ, {__index = function(self, key) self[key] = GetDestructableZ(key) return self[key] end})
setmetatable(itemX, {__index = function(self, key) self[key] = GetItemX(key) return self[key] end})
setmetatable(itemY, {__index = function(self, key) self[key] = GetItemY(key) return self[key] end})
setmetatable(itemZ, {__index = function(self, key) self[key] = GetItemZ(key) return self[key] end})
--#endregion
--===========================================================================================================================================================
--Pair Class
--===========================================================================================================================================================
--#region Pair
---@class Pair
local Pair = {
[ActorA] = nil, ---@type Actor
[ActorB] = nil, ---@type Actor
[IndexA] = 0, ---@type integer
[IndexB] = 0, ---@type integer
[InteractionFunc] = nil, ---@type function
[CurrentPosition] = 0, ---@type integer
[PositionInStep] = 0, ---@type integer
[DestructionQueued] = nil, ---@type boolean
[HostA] = nil, ---@type any
[HostB] = nil, ---@type any
[EveryStep] = nil, ---@type boolean
[UserData] = nil, ---@type table
[Pause] = nil, ---@type function
}
local function PairUserDataOnDestroy(self)
if self[UserData].onDestroy and not isCrash then
self[UserData].onDestroy(self[HostA], self[HostB], self[UserData])
end
ReturnTable(self[UserData])
self[UserData] = nil
end
---@param actorA Actor
---@param actorB Actor
---@param interactionFunc function
---@return Pair | nil
local function CreatePair(actorA, actorB, interactionFunc)
if interactionFunc == nil or actorA.isIsolated or actorB.isIsolated or actorA.host == actorB.host or actorA.anchor == actorB.anchor or
(functionMaxRange[interactionFunc] and WillAlwaysBeOutOfRange(actorA, actorB, functionMaxRange[interactionFunc])) then
pairingExcluded[actorA][actorB] = true
pairingExcluded[actorB][actorA] = true
return nil
end
local self ---@type Pair
if #unusedPairs == 0 then
self = {} ---@type Pair
else
self = unusedPairs[#unusedPairs]
unusedPairs[#unusedPairs] = nil
end
self[ActorA] = actorA
self[ActorB] = actorB
self[HostA] = actorA.host
if actorB == selfInteractionActor then
self[HostB] = actorA.host
else
self[HostB] = actorB.host
end
self[InteractionFunc] = interactionFunc
self[IndexA] = #actorA.pairs + 1
self[IndexB] = #actorB.pairs + 1
actorA.pairs[self[IndexA]] = self
actorB.pairs[self[IndexB]] = self
self[DestructionQueued] = false
self[UserData] = nil
actorA.isPairedWith[actorB] = true
actorB.isPairedWith[actorA] = true
pairList[actorA][actorB] = self
if functionMatrix[interactionFunc] then
local hostA = self[HostA]
local hostB = self[HostB]
local matrix = functionMatrix[interactionFunc]
local rowA = matrix[hostA]
if rowA == nil then
matrix[hostA] = GetUnusedTable()
rowA = matrix[hostA]
end
local entry = GetUnusedTable()
rowA[hostB] = entry
entry.index = #rowA + 1
rowA[entry.index] = entry
entry.partner = hostB
setmetatable(entry, matrix.metatable)
if actorB ~= selfInteractionActor then
local rowB = matrix[hostB]
if rowB == nil then
matrix[hostB] = GetUnusedTable()
rowB = matrix[hostB]
end
entry = GetUnusedTable()
rowB[hostA] = entry
entry.index = #rowB + 1
rowB[entry.index] = entry
entry.partner = hostA
setmetatable(entry, matrix.metatable)
end
end
if functionInitializer[interactionFunc] then
local tempPair = currentPair
currentPair = self
functionInitializer[interactionFunc](self[HostA], self[HostB])
currentPair = tempPair
end
--Check if function is known to be Every Step or Variable Step. If not set yet, use pair to categorize based on if function has return value. If interaction has delay, cannot use it to categorize.
if functionRandomDelay[interactionFunc] == nil then
if isEveryStepFunction[interactionFunc] then
numFixedPairs = numFixedPairs + 1
fixedPairs[numFixedPairs] = self
self[PositionInStep] = numFixedPairs
self[EveryStep] = true
elseif isEveryStepFunction[interactionFunc] == false then
local firstStep = counter + 1
if firstStep > CYCLE_LENGTH then
firstStep = firstStep - CYCLE_LENGTH
end
numPairs[firstStep] = numPairs[firstStep] + 1
whichPairs[firstStep][numPairs[firstStep]] = self
self[CurrentPosition] = firstStep
self[PositionInStep] = numPairs[firstStep]
self[EveryStep] = false
else
numUncategorizedPairs = numUncategorizedPairs + 1
uncategorizedPairs[numUncategorizedPairs] = self
self[PositionInStep] = numUncategorizedPairs
self[EveryStep] = nil
end
else
isEveryStepFunction[interactionFunc] = false
local firstStep = counter + min(MAX_STEPS, ceil(functionRandomDelay[interactionFunc]*math.random()*INV_MIN_INTERVAL))
if firstStep > CYCLE_LENGTH then
firstStep = firstStep - CYCLE_LENGTH
end
numPairs[firstStep] = numPairs[firstStep] + 1
whichPairs[firstStep][numPairs[firstStep]] = self
self[CurrentPosition] = firstStep
self[PositionInStep] = numPairs[firstStep]
self[EveryStep] = false
end
return self
end
local function DestroyPair(self)
if self[EveryStep] then
if self[PositionInStep] ~= 0 then
fixedPairs[numFixedPairs][PositionInStep] = self[PositionInStep]
fixedPairs[self[PositionInStep]] = fixedPairs[numFixedPairs]
numFixedPairs = numFixedPairs - 1
end
elseif self[EveryStep] == false then
local currentPosition = self[CurrentPosition] ---@type integer
local pairAtHighestPosition = whichPairs[currentPosition][numPairs[currentPosition]]
whichPairs[currentPosition][self[PositionInStep]] = pairAtHighestPosition
pairAtHighestPosition[PositionInStep] = self[PositionInStep]
numPairs[currentPosition] = numPairs[currentPosition] - 1
end
self[CurrentPosition] = nil
self[PositionInStep] = nil
local A = self[ActorA] ---@type Actor
local B = self[ActorB] ---@type Actor
if self[UserData] then
PairUserDataOnDestroy(self)
end
if functionMatrix[self[InteractionFunc]] then
local matrix = functionMatrix[self[InteractionFunc]]
local hostA = self[HostA]
local hostB = self[HostB]
local rowA = matrix[hostA]
local rowB = matrix[hostB]
local index = rowA[hostB].index
local entryAtHighestPosition = rowA[#rowA]
rowA[index] = entryAtHighestPosition
rowA[entryAtHighestPosition.partner].index = index
rowA[#rowA] = nil
ReturnTable(rowA[hostB])
rowA[hostB] = nil
if #rowA == 0 then
ReturnTable(rowA)
matrix[hostA] = nil
end
if hostB ~= hostA then
index = rowB[hostA].index
entryAtHighestPosition = rowB[#rowB]
rowB[index] = entryAtHighestPosition
rowB[entryAtHighestPosition.partner].index = index
rowB[#rowB] = nil
ReturnTable(rowB[hostA])
rowB[hostA] = nil
if #rowB == 0 then
ReturnTable(rowB)
matrix[hostB] = nil
end
end
end
--Reset ALICE_PairIsUnoccupied()
if self[ActorB][self[InteractionFunc]] == self then
self[ActorB][self[InteractionFunc]] = nil
end
local pairAtHighestPosition = B.pairs[#B.pairs]
if pairAtHighestPosition[ActorB] == B then
pairAtHighestPosition[IndexB] = self[IndexB]
else
pairAtHighestPosition[IndexA] = self[IndexB]
end
B.pairs[self[IndexB]] = pairAtHighestPosition
B.pairs[#B.pairs] = nil
pairAtHighestPosition = A.pairs[#A.pairs]
if pairAtHighestPosition[ActorA] == A then
pairAtHighestPosition[IndexA] = self[IndexA]
else
pairAtHighestPosition[IndexB] = self[IndexA]
end
A.pairs[self[IndexA]] = pairAtHighestPosition
A.pairs[#A.pairs] = nil
unusedPairs[#unusedPairs + 1] = self
A.isPairedWith[B] = nil
B.isPairedWith[A] = nil
pairList[A][B] = nil
pairList[B][A] = nil
end
---@param actorA Actor
---@param actorB Actor
---@param tryAgain boolean
---@return Actor, Actor, function | nil
local function GetPairParameters(actorA, actorB, tryAgain)
--First tries to find the interactionFunc from the actor with the higher priority,
--then, if unsuccessful, calls itself with reversed argument order.
--Returns interactionFunc and order of actors.
local interactionFunc = actorA.interactionFunc
if type(interactionFunc) == "table" then
local foundFunc = nil
for keyword, func in pairs(interactionFunc) do
if actorB.identifier[keyword] then
if foundFunc == nil then
foundFunc = func
else
error("Interaction func ambiguous for actors with " .. Identifier2String(actorA.identifier) .. " and actors with identifier " .. Identifier2String(actorB.identifier))
end
end
end
if foundFunc then
return actorA, actorB, foundFunc
end
if interactionFunc.other then
return actorA, actorB, interactionFunc.other
elseif tryAgain then
return GetPairParameters(actorB, actorA, false)
else
return actorA, actorB, nil
end
else
---@diagnostic disable-next-line: return-type-mismatch
if interactionFunc then
return actorA, actorB, interactionFunc
elseif tryAgain then
return GetPairParameters(actorB, actorA, false)
else
return actorA, actorB, nil
end
end
end
local pairingEvaluator = {}
pairingEvaluator["boolean"] = function(__, actorB) return actorB.pairsWith end
pairingEvaluator["string"] = function(actorA, actorB) return actorA.identifier[actorB.pairsWith] end
pairingEvaluator["table"] = function(actorA, actorB) return CheckIdentifierOverlap(actorA, actorB) and (actorB.playerFilterType == PLAYER_FILTER_TYPE_ANY or IsInPlayerFilter(actorA.owner, actorB.playerFilterType, actorB.owner)) end
pairingEvaluator["function"] = function(actorA, actorB) return actorB.pairsWith(actorA.host) end
pairingEvaluator["nil"] = function(__, __) return false end
--#endregion
--===========================================================================================================================================================
--Actor Class
--===========================================================================================================================================================
--#region Actor
---@class Actor
Actor = {
--Main:
isActor = nil, ---@type boolean
host = nil, ---@type any
anchor = nil, ---@type any
owner = nil, ---@type player | nil
evaluator = nil, ---@type function
interactionFunc = nil, ---@type function | table | nil
selfInteractions = nil, ---@type table
identifier = nil, ---@type table
pairsWith = nil, ---@type string | table | function | boolean
matchingType = nil, ---@type integer | nil
playerFilterType = nil, ---@type integer | player | nil
pairs = nil, ---@type Pair[]
isPairedWith = nil, ---@type boolean[]
visualizer = nil, ---@type effect
alreadyDestroyed = nil, ---@type boolean
causedCrash = nil, ---@type boolean | nil
isIsolated = nil, ---@type boolean
--Coordinates:
x = nil, ---@type table
y = nil, ---@type table
z = nil, ---@type table
lastX = nil, ---@type number
lastY = nil, ---@type number
zOffset = nil, ---@type number
--Flags:
priority = nil, ---@type integer
index = nil, ---@type integer
isStationary = nil, ---@type boolean | nil
unique = nil, ---@type integer
bindToBuff = nil, ---@type string | nil
unit = nil, ---@type unit | nil
waitingForBuff = nil, ---@type boolean | nil
bindToOrder = nil, ---@type integer | nil
onDestroy = nil, ---@type function | nil
randomDelay = nil, ---@type number | nil
destroyOnDeathTrigger = nil, ---@type trigger | nil
--Cell interaction:
isGlobal = nil, ---@type boolean
usesCells = nil, ---@type boolean
radius = nil, ---@type number
minX = nil, ---@type integer
minY = nil, ---@type integer
maxX = nil, ---@type integer
maxY = nil, ---@type integer
cellCheckInterval = nil, ---@type integer
nextCellCheck = nil, ---@type integer
positionInCellCheck = nil, ---@type integer
cells = nil, ---@type Cell[]
cellIndex = nil, ---@type integer[]
cellsVisualized = nil, ---@type boolean
cellVisualizers = nil, ---@type lightning[]
--Caller functions:
reevaluateCaller = nil, ---@type function
unpauseCaller = nil ---@type function
}
---@param host any
---@param identifier string | string[] | nil
---@param interactionFunc function | table | nil
---@param pairsWith function | string | table | nil
---@param priority number | nil
---@param radius number | nil
---@param cellCheckInterval number | nil
---@param isStationary boolean | nil
---@param destroyOnDeath boolean | nil
---@param anchor any
---@param zOffset number | nil
---@param bindToBuff string | nil
---@param bindToOrder string | nil
---@param onActorDestroy function | nil
---@param hasInfiniteRange boolean | nil
---@return Actor | nil
Create = function(host, identifier, interactionFunc, pairsWith, priority, radius, cellCheckInterval, isStationary, destroyOnDeath, anchor, zOffset, bindToBuff, bindToOrder, onActorDestroy, hasInfiniteRange)
if type(identifier) ~= "string" and type(identifier) ~= "table" then
if identifier == nil then
error("Object identifier is nil.")
else
error("Object identifier must be string or table, but was " .. type(identifier))
end
end
if type(host) == "table" and host.isActor then
error("Actors as hosts are not supported.")
end
local self ---@type Actor
if #unusedActors == 0 then
--Actors have their own table recycling system. These fields do not get nilled on destroy.
self = {}
self.isActor = true
self.identifier = {}
self.isPairedWith = {}
self.pairs = {}
self.cells = {}
self.cellIndex = {}
self.selfInteractions = {}
pairList[self] = {}
pairingExcluded[self] = {}
excludedManually[self] = {}
else
self = unusedActors[#unusedActors]
unusedActors[#unusedActors] = nil
end
totalActors = totalActors + 1
self.unique = totalActors
self.causedCrash = nil
--Transform sequence into hashmap.
if type(identifier) == "string" then
self.identifier[identifier] = true
elseif type(identifier) == "table" then
for i = 1, #identifier do
self.identifier[identifier[i]] = true
end
end
--Transform sequence into hashmap.
if type(interactionFunc) == "table" then
self.interactionFunc = GetUnusedTable()
for keyword, func in pairs(interactionFunc) do
self.interactionFunc[keyword] = func
end
else
self.interactionFunc = interactionFunc
end
InitPairingLogic(self, pairsWith)
self.evaluator = pairingEvaluator[type(self.pairsWith)]
self.host = host or GLOBAL_HOST
self.anchor = anchor or host
if host then
CreateReference(self)
end
self.zOffset = zOffset or 0
SetOwner(self, host)
self.isStationary = isStationary == true --overwritten if host is destructable
SetCoordinateFuncs(self)
self.lastX = self.x[self.anchor]
self.lastY = self.y[self.anchor]
self.isGlobal = self.x == globalXYZ
self.usesCells = not hasInfiniteRange and not self.isGlobal
priority = priority or 0
self.priority = priority
local actorPairs
local selfPairs
local whichActorList
if self.usesCells then
whichActorList = celllessActorList
else
whichActorList = actorList
end
for __, actor in ipairs(whichActorList) do
if self.anchor ~= actor.anchor then
selfPairs = self.evaluator(actor, self)
actorPairs = actor.evaluator(self, actor)
if selfPairs and actorPairs then
if self.priority < actor.priority then
CreatePair(GetPairParameters(actor, self, true))
else
CreatePair(GetPairParameters(self, actor, true))
end
elseif selfPairs then
CreatePair(GetPairParameters(self, actor, false))
elseif actorPairs then
CreatePair(GetPairParameters(actor, self, false))
end
end
end
if type(self.interactionFunc) == "table" and self.interactionFunc.self then
if type(self.interactionFunc.self) == "table" then
for __, func in ipairs(self.interactionFunc.self) do
self.selfInteractions[func] = CreatePair(self, selfInteractionActor, func)
end
else
self.selfInteractions[self.interactionFunc.self] = CreatePair(self, selfInteractionActor, self.interactionFunc.self)
end
end
if self.usesCells then
self.radius = radius or ALICE_DEFAULT_OBJECT_RADIUS
InitCells(self)
if self.isStationary then
self.nextCellCheck = DO_NOT_EVALUATE
else
InitCellChecks(self, cellCheckInterval or ALICE_DEFAULT_CELL_CHECK_INTERVAL)
end
end
if destroyOnDeath then
CreateOnDeathTrigger(self)
end
if bindToBuff then
CreateBinds(self, bindToBuff, nil)
elseif bindToOrder then
CreateBinds(self, nil, bindToOrder)
end
self.onDestroy = onActorDestroy
if visualizeAllActors and not self.isGlobal then
CreateVisualizer(self)
end
self.alreadyDestroyed = false
actorList[#actorList + 1] = self
if not self.usesCells then
celllessActorList[#celllessActorList + 1] = self
end
self.index = #actorList
return self
end
Destroy = function(self)
if self == nil or self.alreadyDestroyed then
return
end
self.alreadyDestroyed = true
destroyedActors[#destroyedActors + 1] = self
if self.onDestroy then
self.onDestroy(self.host)
self.onDestroy = nil
end
actorList[#actorList].index = self.index
actorList[self.index] = actorList[#actorList]
actorList[#actorList] = nil
if not self.usesCells then
for i, actor in ipairs(celllessActorList) do
if self == actor then
celllessActorList[i] = celllessActorList[#celllessActorList]
celllessActorList[#celllessActorList] = nil
break
end
end
end
if type(self.pairsWith) == "table" then
ReturnTable(self.pairsWith)
end
if type(self.interactionFunc) == "table" then
ReturnTable(self.interactionFunc)
end
for key, __ in pairs(self.identifier) do
self.identifier[key] = nil
end
for key, __ in pairs(self.selfInteractions) do
self.selfInteractions[key] = nil
end
for __, pair in ipairs(self.pairs) do
pair[DestructionQueued] = true
end
for key, __ in pairs(pairingExcluded[self]) do
pairingExcluded[self][key] = nil
pairingExcluded[key][self] = nil
end
for key, __ in pairs(excludedManually[self]) do
excludedManually[self][key] = nil
excludedManually[key][self] = nil
end
if self.usesCells then
if not self.isStationary then
local nextCheck = self.nextCellCheck
local actorAtHighestPosition = cellCheckedActors[nextCheck][numCellChecks[nextCheck]]
actorAtHighestPosition.positionInCellCheck = self.positionInCellCheck
cellCheckedActors[nextCheck][self.positionInCellCheck] = actorAtHighestPosition
numCellChecks[nextCheck] = numCellChecks[nextCheck] - 1
end
for i = #self.cells, 1, -1 do
self.cells[i]:remove(self)
end
end
if self.host ~= GLOBAL_HOST then
RemoveReference(self, self.host)
if self.host ~= self.anchor then
RemoveReference(self, self.anchor)
end
self.host = nil
self.x[self.anchor] = nil
self.y[self.anchor] = nil
self.z[self.anchor] = nil
self.anchor = nil
end
if self.destroyOnDeathTrigger then
TriggerClearActions(self.destroyOnDeathTrigger)
DestroyTrigger(self.destroyOnDeathTrigger)
self.destroyOnDeathTrigger = nil
end
if self.cellsVisualized then
for __, bolt in ipairs(self.cellVisualizers) do
DestroyLightning(bolt)
end
self.cellsVisualized = nil
end
if visualizeAllActors and not self.isGlobal then
DestroyEffect(self.visualizer)
end
if self == selectedActor then
DestroyEffect(self.visualizer)
selectedActor = nil
end
self.x = nil
self.y = nil
self.z = nil
self.lastX = nil
self.lastY = nil
self.bindToBuff = nil
self.bindToOrder = nil
self.isIsolated = nil
end
---Create a reference to the actor in both the host and the anchor. If more than one actor, transform into a table and store actors in a sequence.
CreateReference = function(self)
if actorOf[self.host] == nil then
actorOf[self.host] = self
elseif actorOf[self.host].isActor then
actorOf[self.host] = {actorOf[self.host], self}
else
table.insert(actorOf[self.host], self)
end
if self.host ~= self.anchor then
if actorOf[self.anchor] == nil then
actorOf[self.anchor] = self
elseif actorOf[self.anchor].isActor then
actorOf[self.anchor] = {actorOf[self.anchor], self}
else
table.insert(actorOf[self.anchor], self)
end
end
end
RemoveReference = function(self, object)
if actorOf[object].isActor then
actorOf[object] = nil
else
for j, v in ipairs(actorOf[object]) do
if self == v then
table.remove(actorOf[object], j)
end
end
if #actorOf[object] == 1 then
actorOf[object] = actorOf[object][1]
end
end
end
SetCoordinateFuncs = function(self)
if self.anchor == nil then
self.x = globalXYZ
self.y = globalXYZ
self.z = globalXYZ
elseif type(self.anchor) == "userdata" then
if WidgetIsUnit(self.anchor) then
self.x = unitX
self.y = unitY
self.z = unitZ
elseif WidgetIsDestructable(self.anchor) then
self.x = destructableX
self.y = destructableY
self.z = destructableZ
self.isStationary = true
elseif WidgetIsItem(self.anchor) then
self.x = itemX
self.y = itemY
self.z = itemZ
end
elseif self.anchor.x then
self.x = classX
self.y = classY
self.z = classZ
self.anchor.z = self.anchor.z or 0
end
end
---@param source any
---@return player | nil
SetOwner = function(self, source)
if type(source) == "userdata" then
if WidgetIsUnit(source) then
self.owner = GetOwningPlayer(source)
else
self.owner = nil
end
elseif type(source) == "table" then
if type(source[CLASS_FIELD_OWNER]) == "number" then
self.owner = Player(source[CLASS_FIELD_OWNER] - 1)
elseif type(source[CLASS_FIELD_OWNER]) == "userdata" then
self.owner = source[CLASS_FIELD_OWNER]
else
self.owner = nil
end
end
if self.owner == nil and source ~= self.anchor then
SetOwner(self, self.anchor)
end
end
CreateOnDeathTrigger = function(self)
if type(self.host) == "userdata" and AgentIsWidget(self.host) then
self.destroyOnDeathTrigger = CreateTrigger()
TriggerRegisterDeathEvent(self.destroyOnDeathTrigger, self.host)
TriggerAddAction(self.destroyOnDeathTrigger, function() Destroy(self) end)
elseif type(self.anchor) == "userdata" and AgentIsWidget(self.anchor) then
self.destroyOnDeathTrigger = CreateTrigger()
TriggerRegisterDeathEvent(self.destroyOnDeathTrigger, self.anchor)
TriggerAddAction(self.destroyOnDeathTrigger, function() Destroy(self) end)
else
Warning("|cffff0000Warning:|r Cannot use automatic onDeath clean-up with non-widget hosts...")
end
end
---@param pairsWith table
InitPairingLogic = function(self, pairsWith)
--Transforms pairingLogic into form that can be used by internal functions.
--From string sequence to boolean table with string keys.
if type(pairsWith) == "table" then
--Determine pairing logic from pairsWith.
self.pairsWith = GetUnusedTable()
local j = 1
while type(pairsWith[j]) == "string" do
self.pairsWith[pairsWith[j]] = true
j = j + 1
end
for i = j, #pairsWith do
if type(pairsWith[i]) == "number" then
if pairsWith[i] <= HIGHEST_MATCHING_TYPE then
self.matchingType = pairsWith[i]
else
self.playerFilterType = pairsWith[i]
end
elseif type(pairsWith[i]) == "userdata" then
self.playerFilterType = pairsWith[i]
end
end
if self.matchingType == nil then
self.matchingType = MATCHING_TYPE_ANY
end
if self.playerFilterType == nil then
self.playerFilterType = PLAYER_FILTER_TYPE_ANY
end
if self.interactionFunc == nil then
Warning("|cffff0000Warning:|r No interaction func set for actor with identifiers " .. Identifier2String(self.identifier) .. "...")
end
elseif pairsWith == nil then
--Determine pairing logic from interactionFunc.
if type(self.interactionFunc) == "table" then
if self.interactionFunc.other then
self.pairsWith = true
else
self.pairsWith = GetUnusedTable()
for key, __ in pairs(self.interactionFunc) do
self.pairsWith[key] = true
end
self.matchingType = MATCHING_TYPE_ANY
self.playerFilterType = PLAYER_FILTER_TYPE_ANY
end
else
self.pairsWith = self.interactionFunc ~= nil
end
else
self.pairsWith = pairsWith
end
end
InitCells = function(self)
local x = self.x[self.anchor]
local y = self.y[self.anchor]
self.minX = min(NUM_CELLS_X, max(1, ceil(NUM_CELLS_X*(x - self.radius - MAP_MIN_X)/(MAP_MAX_X - MAP_MIN_X))))
self.minY = min(NUM_CELLS_Y, max(1, ceil(NUM_CELLS_Y*(y - self.radius - MAP_MIN_Y)/(MAP_MAX_Y - MAP_MIN_Y))))
self.maxX = min(NUM_CELLS_X, max(1, ceil(NUM_CELLS_X*(x + self.radius - MAP_MIN_X)/(MAP_MAX_X - MAP_MIN_X))))
self.maxY = min(NUM_CELLS_Y, max(1, ceil(NUM_CELLS_Y*(y + self.radius - MAP_MIN_Y)/(MAP_MAX_Y - MAP_MIN_Y))))
for X = self.minX, self.maxX do
for Y = self.minY, self.maxY do
CELL_LIST[X][Y]:enter(self)
end
end
end
---@param interval number
InitCellChecks = function(self, interval)
self.cellCheckInterval = min(MAX_STEPS, max(1, ceil((interval - 0.001)*INV_MIN_INTERVAL)))
local nextStep = counter + self.cellCheckInterval
if nextStep > CYCLE_LENGTH then
nextStep = nextStep - CYCLE_LENGTH
end
numCellChecks[nextStep] = numCellChecks[nextStep] + 1
cellCheckedActors[nextStep][numCellChecks[nextStep]] = self
self.nextCellCheck = nextStep
self.positionInCellCheck = numCellChecks[nextStep]
end
---Actor has left current cell but is not in adjacent cell.
---@param x number
---@param y number
Teleport = function(self, x, y)
self.minX = min(NUM_CELLS_X, max(1, ceil(NUM_CELLS_X*(x - self.radius - MAP_MIN_X)/(MAP_MAX_X - MAP_MIN_X))))
self.minY = min(NUM_CELLS_Y, max(1, ceil(NUM_CELLS_Y*(y - self.radius - MAP_MIN_Y)/(MAP_MAX_Y - MAP_MIN_Y))))
self.maxX = min(NUM_CELLS_X, max(1, ceil(NUM_CELLS_X*(x + self.radius - MAP_MIN_X)/(MAP_MAX_X - MAP_MIN_X))))
self.maxY = min(NUM_CELLS_Y, max(1, ceil(NUM_CELLS_Y*(y + self.radius - MAP_MIN_Y)/(MAP_MAX_Y - MAP_MIN_Y))))
for i = #self.cells, 1, -1 do
self.cells[i]:leave(self)
end
for X = self.minX, self.maxX do
for Y = self.minY, self.maxY do
CELL_LIST[X][Y]:enter(self, true)
end
end
end
---@param actor Actor
---@return boolean
SharesCellWith = function(self, actor)
for __, cellA in ipairs(self.cells) do
for __, cellB in ipairs(actor.cells) do
if cellA == cellB then
return true
end
end
end
return false
end
---@param bindToBuff string | nil
---@param bindToOrder string | nil
CreateBinds = function(self, bindToBuff, bindToOrder)
if bindToBuff then
self.bindToBuff = bindToBuff --Do not convert to id so that string can be displayed in debug mode.
self.waitingForBuff = true
table.insert(bindChecks, self)
elseif bindToOrder then
self.bindToOrder = OrderId(bindToOrder) --Convert because faster and you can convert back for debug mode.
table.insert(bindChecks, self)
end
if type(self.host) == "userdata" and WidgetIsUnit(self.host) then
self.unit = self.host
elseif type(self.anchor) == "userdata" and WidgetIsUnit(self.anchor) then
self.unit = self.anchor
else
Warning("|cffff0000Warning:|r Attempted to bind actor with identifier " .. Identifier2String(self.identifier) .. " to a buff or order, but that actor doesn't have a unit host or anchor...")
end
end
ReevaluatePairings = function(self)
if self.alreadyDestroyed then
return
end
local selfPairs
local actorPairs
for __, actor in ipairs(actorList) do
if pairingExcluded[self][actor] and not excludedManually[self][actor] then
pairingExcluded[self][actor] = nil
pairingExcluded[actor][self] = nil
if (not self.usesCells or not actor.usesCells or SharesCellWith(self, actor)) then
selfPairs = self.evaluator(actor, self)
actorPairs = actor.evaluator(self, actor)
if selfPairs and actorPairs then
if self.priority < actor.priority then
CreatePair(GetPairParameters(actor, self, true))
else
CreatePair(GetPairParameters(self, actor, true))
end
elseif selfPairs then
CreatePair(GetPairParameters(self, actor, false))
elseif actorPairs then
CreatePair(GetPairParameters(actor, self, false))
end
end
end
end
local actor
local pair
for i = #self.pairs, 1, -1 do
pair = self.pairs[i]
if pair[ActorB] ~= selfInteractionActor then
if self == pair[ActorA] then
actor = pair[ActorB]
else
actor = pair[ActorA]
end
if not actor.alreadyDestroyed then
if not self.evaluator(actor, self) and not actor.evaluator(self, actor) then
DestroyPair(pair)
end
end
end
end
end
---For debug mode.
---@param enable boolean
VisualizeCells = function(self, enable)
if enable == self.cellsVisualized then
return
end
if self.cellsVisualized then
self.cellsVisualized = false
DestroyLightning(self.cellVisualizers[1])
DestroyLightning(self.cellVisualizers[2])
DestroyLightning(self.cellVisualizers[3])
DestroyLightning(self.cellVisualizers[4])
elseif self.usesCells then
self.cellVisualizers = {}
self.cellsVisualized = true
local minx = CELL_MIN_X[self.minX]
local miny = CELL_MIN_Y[self.minY]
local maxx = CELL_MAX_X[self.maxX]
local maxy = CELL_MAX_Y[self.maxY]
self.cellVisualizers[1] = AddLightning("LEAS", false, maxx, miny, maxx, maxy)
self.cellVisualizers[2] = AddLightning("LEAS", false, maxx, maxy, minx, maxy)
self.cellVisualizers[3] = AddLightning("LEAS", false, minx, maxy, minx, miny)
self.cellVisualizers[4] = AddLightning("LEAS", false, minx, miny, maxx, miny)
end
end
---For debug mode.
RedrawCellVisualizers = function(self)
local minx = CELL_MIN_X[self.minX]
local miny = CELL_MIN_Y[self.minY]
local maxx = CELL_MAX_X[self.maxX]
local maxy = CELL_MAX_Y[self.maxY]
MoveLightning(self.cellVisualizers[1], false, maxx, miny, maxx, maxy)
MoveLightning(self.cellVisualizers[2], false, maxx, maxy, minx, maxy)
MoveLightning(self.cellVisualizers[3], false, minx, maxy, minx, miny)
MoveLightning(self.cellVisualizers[4], false, minx, miny, maxx, miny)
end
---Called when an actor crashes the thread two times. Cannot pair anymore.
Isolate = function(self)
for __, actor in ipairs(actorList) do
if pairList[actor][self] or pairList[self][actor] then
DestroyPair(pairList[actor][self] or pairList[self][actor])
end
end
self.isIsolated = true
for __, actor in ipairs(actorList) do
pairingExcluded[self][actor] = true
pairingExcluded[actor][self] = true
end
end
---For debug mode.
Deselect = function(self)
selectedActor = nil
if not visualizeAllActors then
DestroyEffect(self.visualizer)
end
ALICE_VisualizeCells(self, false)
BlzFrameSetVisible(debugTooltip, false)
end
---For debug mode.
---@return string, string
GetDescription = function(self)
local description = ""
if self.host ~= GLOBAL_HOST then
description = description .. "|cffffcc00Host:|r " .. tostring(self.host)
end
if self.anchor ~= self.host then
description = description .. "\n|cffffcc00Anchor:|r " .. tostring(self.anchor)
end
description = description .. "\n|cffffcc00InteractionFuncs:|r "
if type(self.interactionFunc) == "table" then
local first = true
for key, func in pairs(self.interactionFunc) do
if key ~= "self" then
if not first then
description = description .. ", "
end
description = description .. key .. " - " .. FunctionToString(func)
first = false
end
end
if self.interactionFunc.self then
description = description .. "\n|cffffcc00Self-Interaction:|r "
if type(self.interactionFunc.self) == "function" then
description = description .. FunctionToString(self.interactionFunc.self)
elseif type(self.interactionFunc.self) == "table" then
first = true
for __, func in ipairs(self.interactionFunc.self) do
if not first then
description = description .. ", "
end
description = description .. FunctionToString(func)
first = false
end
else
description = description .. "|cffff0000Invalid type!|r"
end
end
else
description = description .. tostring(self.interactionFunc)
end
description = description .. "\n|cffffcc00Pairs with:|r "
if type(self.pairsWith) == "table" then
local count = 0
for key, __ in pairs(self.pairsWith) do
if key ~= "self" then
count = count + 1
end
end
if count > 1 then
if self.matchingType == MATCHING_TYPE_ANY then
description = description .. "Any of "
elseif self.matchingType == MATCHING_TYPE_ALL then
description = description .. "All of "
elseif self.matchingType == MATCHING_TYPE_EXACT then
description = description .. "Exactly "
elseif self.matchingType == MATCHING_TYPE_EXCLUDE then
description = description .. "None of "
end
end
local first = true
for key, __ in pairs(self.pairsWith) do
if key ~= "self" then
if not first then
description = description .. ", "
end
description = description .. key
if type(self.interactionFunc) == "table" and self.interactionFunc.other == nil and self.interactionFunc[key] == nil then
description = description .. " |cffaaaaaa(interactionFunc must exist)|r"
end
first = false
end
end
if type(self.playerFilterType) == "userdata" then
description = description .. " (all actors owned by player " .. GetPlayerId(self.playerFilterType) + 1 .. ")"
elseif self.playerFilterType ~= PLAYER_FILTER_TYPE_ANY then
if self.owner == nil then
description = description .. " |cffff0000(player filter requires owner!)|r"
elseif self.playerFilterType == PLAYER_FILTER_TYPE_ENEMY then
description = description .. " (actor must be enemy)"
elseif self.playerFilterType == PLAYER_FILTER_TYPE_OWNER then
description = description .. " (actor must have the same owner)"
elseif self.playerFilterType == PLAYER_FILTER_TYPE_NOT_OWNER then
description = description .. " (actor must have a different owner)"
end
end
elseif self.pairsWith == false or self.pairsWith == nil then
description = description .. "Nothing"
elseif self.pairsWith == true then
if self.playerFilterType == PLAYER_FILTER_TYPE_ANY then
description = description .. "Everything"
elseif self.playerFilterType == PLAYER_FILTER_TYPE_ALLY then
description = description .. "All allied actors."
elseif self.playerFilterType == PLAYER_FILTER_TYPE_ENEMY then
description = description .. "All enemy actors."
elseif self.playerFilterType == PLAYER_FILTER_TYPE_OWNER then
description = description .. "All actors with the same owner."
elseif self.playerFilterType == PLAYER_FILTER_TYPE_NOT_OWNER then
description = description .. "All actors with a different owner."
elseif type(self.playerFilterType) == "userdata" then
description = description .. "All actors owned by player " .. GetPlayerId(self.playerFilterType) + 1
end
elseif type(self.pairsWith) == "function" then
description = description .. tostring(self.pairsWith)
end
if self.priority ~= 0 then
description = description .. "\n|cffffcc00Priority:|r " .. self.priority
end
if self.cellCheckInterval then
if math.abs(self.cellCheckInterval*MIN_INTERVAL - ALICE_DEFAULT_CELL_CHECK_INTERVAL) > 0.001 then
description = description .. "\n|cffffcc00Cell Check Interval:|r " .. self.cellCheckInterval*MIN_INTERVAL
end
elseif not self.isGlobal then
if self.usesCells then
description = description .. "\n|cffffcc00Stationary:|r true"
else
description = description .. "\n|cffffcc00Has infinite range:|r true"
end
end
if self.radius and self.radius ~= ALICE_DEFAULT_OBJECT_RADIUS then
description = description .. "\n|cffffcc00Radius:|r " .. self.radius
end
if self.owner then
description = description .. "\n|cffffcc00Owner:|r Player " .. GetPlayerId(self.owner) + 1
end
if self.bindToBuff then
description = description .. "\n|cffffcc00Bound to buff:|r " .. self.bindToBuff
if self.waitingForBuff then
description = description .. " |cffaaaaaa(waiting for buff to be applied)|r"
end
end
if self.bindToOrder then
description = description .. "\n|cffffcc00Bound to order:|r " .. OrderId2String(self.bindToOrder) .. " |cffaaaaaa(current order = " .. OrderId2String(GetUnitCurrentOrder(self.anchor)) .. ")"
end
if self.onDestroy then
description = description .. "\n|cffffcc00On Destroy:|r " .. FunctionToString(self.onDestroy)
end
description = description .. "\n\n|cffffcc00Unique Number:|r " .. self.unique
local numOutgoing = 0
local numIncoming = 0
local hasError = false
local outgoingFuncs = {}
local incomingFuncs = {}
for __, pair in ipairs(self.pairs) do
if pair[ActorB] ~= selfInteractionActor then
if pair[ActorA] == self then
numOutgoing = numOutgoing + 1
outgoingFuncs[pair[InteractionFunc]] = (outgoingFuncs[pair[InteractionFunc]] or 0) + 1
elseif pair[ActorB] == self then
numIncoming = numIncoming + 1
incomingFuncs[pair[InteractionFunc]] = (incomingFuncs[pair[InteractionFunc]] or 0) + 1
else
hasError = true
end
end
end
description = description .. "\n|cffffcc00Outgoing pairs:|r " .. numOutgoing
if numOutgoing > 0 then
local first = true
description = description .. "|cffaaaaaa ("
for key, number in pairs(outgoingFuncs) do
if not first then
description = description .. ", |r"
end
description = description .. "|cffffcc00" .. number .. "|r |cffaaaaaa" .. FunctionToString(key)
first = false
end
description = description .. ")|r"
end
description = description .. "\n|cffffcc00Incoming pairs:|r " .. numIncoming
if numIncoming > 0 then
local first = true
description = description .. "|cffaaaaaa ("
for key, number in pairs(incomingFuncs) do
if not first then
description = description .. ", |r"
end
description = description .. "|cffffcc00" .. number .. "|r |cffaaaaaa" .. FunctionToString(key)
first = false
end
description = description .. ")|r"
end
if self.usesCells then
local x, y = self.x[self.anchor], self.y[self.anchor]
description = description .. "\n\n|cffffcc00x:|r " .. x
description = description .. "\n|cffffcc00y:|r " .. y
description = description .. "\n|cffffcc00z:|r " .. self.z[self]
end
if hasError then
description = description .. "\n\n|cffff0000DESYNCED PAIR DETECTED!|r"
end
if self.causedCrash then
description = description .. "\n\n|cffff0000CAUSED CRASH!|r"
end
if self.isGlobal then
return description, "|cff00bb00Global Actor:|r " .. Identifier2String(self.identifier)
else
if numOutgoing == 0 and numIncoming == 0 then
return description, "|cffaaaaaaUnpaired Actor:|r " .. Identifier2String(self.identifier)
else
return description, "|cffffff00Actor:|r " .. Identifier2String(self.identifier)
end
end
end
---For debug mode.
Select = function(self)
selectedActor = self
local description, title = GetDescription(self)
BlzFrameSetText(debugTooltipText, description)
BlzFrameSetText(debugTooltipTitle, title )
BlzFrameSetSize(debugTooltipText, 0.28, 0.0)
BlzFrameSetSize(debugTooltip, 0.29, BlzFrameGetHeight(debugTooltipText) + 0.0315)
BlzFrameSetVisible(debugTooltip, true)
if not self.isGlobal then
CreateVisualizer(self)
if self.usesCells and not visualizeAllActors then
ALICE_VisualizeCells(self, true)
end
end
end
---For debug mode.
CreateVisualizer = function(self)
local x = self.x[self.anchor]
local y = self.y[self.anchor]
self.visualizer = AddSpecialEffect("Abilities\\Spells\\Other\\Aneu\\AneuTarget.mdl", x, y)
if self.owner then
BlzSetSpecialEffectColorByPlayer(self.visualizer, self.owner)
else
BlzSetSpecialEffectColorByPlayer(self.visualizer, Player(21))
end
BlzSetSpecialEffectZ(self.visualizer, self.z[self] + 75)
end
--#endregion
--===========================================================================================================================================================
--Cell Class
--===========================================================================================================================================================
--#region Cell
---@class Cell
local Cell = {
actors = nil, ---@type Actor[]
horizontalLightning = nil, ---@type lightning
verticalLightning = nil ---@type lightning
}
local cellMt = {__index = Cell}
---@param X integer
---@param Y integer
function Cell.create(X, Y)
local new = {}
setmetatable(new, cellMt)
new.actors = {}
CELL_LIST[X][Y] = new
return new
end
function Cell:enter(actorA, wasTeleport)
local aPairs
local bPairs
actorA.cells[#actorA.cells + 1] = self
self.actors[#self.actors + 1] = actorA
actorA.cellIndex[self] = #self.actors
for __, actorB in ipairs(self.actors) do
if actorA ~= actorB then
local thisPair = pairList[actorA][actorB] or pairList[actorB][actorA]
if thisPair then
if thisPair[EveryStep] then
if thisPair[PositionInStep] == 0 then
numFixedPairs = numFixedPairs + 1
fixedPairs[numFixedPairs] = thisPair
thisPair[PositionInStep] = numFixedPairs
end
elseif (thisPair[CurrentPosition] == DO_NOT_EVALUATE or wasTeleport) then
--If an actor enters a new cell, it doesn't need to force an immediate interaction with another
--actor if their pair is already enabled. Unless it's a teleport.
local currentPosition = thisPair[CurrentPosition]
local nextStep = counter + 1
if nextStep > CYCLE_LENGTH then
nextStep = nextStep - CYCLE_LENGTH
end
local pairAtHighestPosition = whichPairs[currentPosition][numPairs[currentPosition]]
pairAtHighestPosition[PositionInStep] = thisPair[PositionInStep]
whichPairs[currentPosition][thisPair[PositionInStep]] = pairAtHighestPosition
numPairs[currentPosition] = numPairs[currentPosition] - 1
numPairs[nextStep] = numPairs[nextStep] + 1
whichPairs[nextStep][numPairs[nextStep]] = thisPair
thisPair[CurrentPosition] = nextStep
thisPair[PositionInStep] = numPairs[nextStep]
end
elseif not pairingExcluded[actorA][actorB] then
if actorA.anchor ~= actorB.anchor then
aPairs = actorA.evaluator(actorB, actorA)
bPairs = actorB.evaluator(actorA, actorB)
if aPairs and bPairs then
if actorA.priority < actorB.priority then
CreatePair(GetPairParameters(actorB, actorA, true))
else
CreatePair(GetPairParameters(actorA, actorB, true))
end
elseif aPairs then
CreatePair(GetPairParameters(actorA, actorB, false))
elseif bPairs then
CreatePair(GetPairParameters(actorB, actorA, false))
else
pairingExcluded[actorA][actorB] = true
pairingExcluded[actorB][actorA] = true
end
end
end
end
end
end
function Cell:remove(actorA)
for i, cell in ipairs(actorA.cells) do
if self == cell then
actorA.cells[i] = actorA.cells[#actorA.cells]
actorA.cells[#actorA.cells] = nil
break
end
end
local actors = self.actors
actors[#actors].cellIndex[self] = actorA.cellIndex[self]
actors[actorA.cellIndex[self]] = actors[#actors]
actors[#actors] = nil
end
function Cell:leave(actorA)
self:remove(actorA)
for __, actorB in ipairs(self.actors) do
if actorA ~= actorB then
local thisPair = pairList[actorA][actorB] or pairList[actorB][actorA]
if thisPair then
if thisPair[EveryStep] then
if thisPair[PositionInStep] ~= 0 and (actorA.maxX < actorB.minX or actorA.minX > actorB.maxX or actorA.maxY < actorB.minY or actorA.minY > actorB.maxY) and not functionPersistOnBreak[thisPair[InteractionFunc]] then
fixedPairs[numFixedPairs][PositionInStep] = thisPair[PositionInStep]
fixedPairs[thisPair[PositionInStep]] = fixedPairs[numFixedPairs]
numFixedPairs = numFixedPairs - 1
thisPair[PositionInStep] = 0
end
elseif thisPair[CurrentPosition] ~= DO_NOT_EVALUATE and (actorA.maxX < actorB.minX or actorA.minX > actorB.maxX or actorA.maxY < actorB.minY or actorA.minY > actorB.maxY) and not functionPersistOnBreak[thisPair[InteractionFunc]] then
if thisPair[EveryStep] == false then
local currentPosition = thisPair[CurrentPosition]
local nextStep = DO_NOT_EVALUATE
local pairAtHighestPosition = whichPairs[currentPosition][numPairs[currentPosition]]
pairAtHighestPosition[PositionInStep] = thisPair[PositionInStep]
whichPairs[currentPosition][thisPair[PositionInStep]] = pairAtHighestPosition
numPairs[currentPosition] = numPairs[currentPosition] - 1
numPairs[nextStep] = numPairs[nextStep] + 1
whichPairs[nextStep][numPairs[nextStep]] = thisPair
thisPair[CurrentPosition] = nextStep
thisPair[PositionInStep] = numPairs[nextStep]
end
end
end
end
end
end
--#endregion
--===========================================================================================================================================================
--Repair
--===========================================================================================================================================================
--#region Repair
local function RepairCycle(firstPosition)
local numSteps
local nextStep
for i = firstPosition, numPairs[counter] do
currentPair = whichPairs[counter][i]
if currentPair[DestructionQueued] then
numSteps = MAX_STEPS
else
numSteps = (currentPair[InteractionFunc](currentPair[HostA], currentPair[HostB])*INV_MIN_INTERVAL + 1) // 1
if numSteps < 1 then
numSteps = 1
elseif numSteps > MAX_STEPS then
numSteps = MAX_STEPS
end
end
nextStep = counter + numSteps
if nextStep > CYCLE_LENGTH then
nextStep = nextStep - CYCLE_LENGTH
end
numPairs[nextStep] = numPairs[nextStep] + 1
whichPairs[nextStep][numPairs[nextStep]] = currentPair
currentPair[CurrentPosition] = nextStep
currentPair[PositionInStep] = numPairs[nextStep]
end
numPairs[counter] = 0
for i = 1, numFixedPairs do
currentPair = fixedPairs[i]
currentPair[InteractionFunc](currentPair[HostA], currentPair[HostB])
end
currentPair = nil
end
local function OnCrash()
local crashingPair = currentPair
--Remove pair and continue with cycle after the crashing pair.
local A = crashingPair[ActorA]
local B = crashingPair[ActorB]
pairingExcluded[A][B] = true
pairingExcluded[B][A] = true
if B == selfInteractionActor then
Warning("\n|cffff0000Error:|r ALICE Cycle crashed during last execution. The identifier of the actor responsible is "
.. "|cffffcc00" .. Identifier2String(A.identifier) .. "|r. Unique number: " .. A.unique .. ". The crash occured during self-interaction with function |cffaaaaff" .. FunctionToString(crashingPair[InteractionFunc]) .. "|r. The interaction has been disabled...")
else
Warning("\n|cffff0000Error:|r ALICE Cycle crashed during last execution. The identifiers of the actors responsible are "
.. "|cffffcc00" .. Identifier2String(A.identifier) .. "|r and |cffaaaaff" .. Identifier2String(B.identifier) .. "|r. Unique numbers: " .. A.unique .. ", " .. B.unique .. ". The pair has been removed from the cycle...")
end
if isEveryStepFunction[crashingPair[InteractionFunc]] == nil then
for index, pair in ipairs(uncategorizedPairs) do
if pair == crashingPair then
uncategorizedPairs[index] = uncategorizedPairs[numUncategorizedPairs]
uncategorizedPairs[numUncategorizedPairs] = nil
numUncategorizedPairs = numUncategorizedPairs - 1
break
end
end
isCrash = true
elseif not crashingPair[EveryStep] then
local nextPosition = crashingPair[PositionInStep] + 1
numPairs[DO_NOT_EVALUATE] = numPairs[DO_NOT_EVALUATE] + 1
whichPairs[DO_NOT_EVALUATE][numPairs[DO_NOT_EVALUATE]] = crashingPair
crashingPair[CurrentPosition] = DO_NOT_EVALUATE
crashingPair[PositionInStep] = numPairs[DO_NOT_EVALUATE]
isCrash = true
DestroyPair(crashingPair)
RepairCycle(nextPosition)
else
isCrash = true
DestroyPair(crashingPair)
end
--If this is the second time the same actor caused a crash, isolate it to prevent it from causing further crashes.
if A.causedCrash then
Warning("\nActor with identifier " .. Identifier2String(A.identifier) .. ", unique number: " .. A.unique .. " is repeatedly causing crashes. Isolating...")
Isolate(A)
elseif B.causedCrash and B ~= selfInteractionActor then
Warning("\nActor with identifier " .. Identifier2String(B.identifier) .. ", unique number: " .. B.unique .. " is repeatedly causing crashes. Isolating...")
end
A.causedCrash = true
B.causedCrash = true
isCrash = false
end
--#endregion
--===========================================================================================================================================================
--Main Functions
--===========================================================================================================================================================
--#region Main Functions
local function ResetCoordinateLookupTables()
for key, __ in pairs(classX) do
classX[key] = nil
classY[key] = nil
end
for key, __ in pairs(classZ) do
classZ[key] = nil
end
for key, __ in pairs(unitX) do
unitX[key] = nil
unitY[key] = nil
end
for key, __ in pairs(unitZ) do
unitZ[key] = nil
end
for key, __ in pairs(itemX) do
itemX[key] = nil
itemY[key] = nil
end
for key, __ in pairs(itemZ) do
itemZ[key] = nil
end
end
local function BindChecks()
for i = #bindChecks, 1, -1 do
local actor = bindChecks[i]
if actor.bindToBuff then
if actor.waitingForBuff then
if GetUnitAbilityLevel(actor.unit, FourCC(actor.bindToBuff)) > 0 then
actor.waitingForBuff = nil
end
elseif GetUnitAbilityLevel(actor.unit, FourCC(actor.bindToBuff)) == 0 then
Destroy(actor)
bindChecks[i] = bindChecks[#bindChecks]
bindChecks[#bindChecks] = nil
end
elseif actor.bindToOrder then
if GetUnitCurrentOrder(actor.unit) ~= actor.bindToOrder then
Destroy(actor)
bindChecks[i] = bindChecks[#bindChecks]
bindChecks[#bindChecks] = nil
end
end
end
end
---All destroyed pairs are only flagged and not destroyed until main cycle completes. This function destroys all pairs in one go.
local function RemovePairsFromCycle()
for i = #destroyedActors, 1, -1 do
local actor = destroyedActors[i]
if actor then
local pairs = actor.pairs
for j = #pairs, 1, -1 do
currentPair = pairs[j]
DestroyPair(currentPair)
end
unusedActors[#unusedActors + 1] = actor
end
destroyedActors[i] = nil
end
end
local function CellCheck()
local x
local y
local radius
local minx
local miny
local maxx
local maxy
local actor
local actorsThisStep = cellCheckedActors[counter]
local nextStep
for i = 1, numCellChecks[counter] do
actor = actorsThisStep[i]
x = actor.x[actor.anchor]
y = actor.y[actor.anchor]
radius = actor.radius
minx = x - radius
miny = y - radius
maxx = x + radius
maxy = y + radius
if x > actor.lastX then
if minx > CELL_MAX_X[actor.minX] and actor.minX < NUM_CELLS_X then
actor.minX = actor.minX + 1
if minx > CELL_MAX_X[actor.minX] then
Teleport(actor, x, y)
else
for Y = actor.minY, actor.maxY do
CELL_LIST[actor.minX - 1][Y]:leave(actor)
end
end
if actor.cellsVisualized then
RedrawCellVisualizers(actor)
end
end
if maxx > CELL_MAX_X[actor.maxX] and actor.maxX < NUM_CELLS_X then
actor.maxX = actor.maxX + 1
if maxx > CELL_MAX_X[actor.maxX] then
Teleport(actor, x, y)
else
for Y = actor.minY, actor.maxY do
CELL_LIST[actor.maxX][Y]:enter(actor, false)
end
end
if actor.cellsVisualized then
RedrawCellVisualizers(actor)
end
end
else
if minx < CELL_MIN_X[actor.minX] and actor.minX > 1 then
actor.minX = actor.minX - 1
if minx < CELL_MIN_X[actor.minX] then
Teleport(actor, x, y)
else
for Y = actor.minY, actor.maxY do
CELL_LIST[actor.minX][Y]:enter(actor, false)
end
end
if actor.cellsVisualized then
RedrawCellVisualizers(actor)
end
end
if maxx < CELL_MIN_X[actor.maxX] and actor.maxX > 1 then
actor.maxX = actor.maxX - 1
if maxx < CELL_MIN_X[actor.maxX] then
Teleport(actor, x, y)
else
for Y = actor.minY, actor.maxY do
CELL_LIST[actor.maxX + 1][Y]:leave(actor)
end
end
if actor.cellsVisualized then
RedrawCellVisualizers(actor)
end
end
end
if y > actor.lastY then
if miny > CELL_MAX_Y[actor.minY] and actor.minY < NUM_CELLS_Y then
actor.minY = actor.minY + 1
if miny > CELL_MAX_Y[actor.minY] then
Teleport(actor, x, y)
else
for X = actor.minX, actor.maxX do
CELL_LIST[X][actor.minY - 1]:leave(actor)
end
end
if actor.cellsVisualized then
RedrawCellVisualizers(actor)
end
end
if maxy > CELL_MAX_Y[actor.maxY] and actor.maxY < NUM_CELLS_Y then
actor.maxY = actor.maxY + 1
if maxy > CELL_MAX_Y[actor.maxY] then
Teleport(actor, x, y)
else
for X = actor.minX, actor.maxX do
CELL_LIST[X][actor.maxY]:enter(actor, false)
end
end
if actor.cellsVisualized then
RedrawCellVisualizers(actor)
end
end
else
if miny < CELL_MIN_Y[actor.minY] and actor.minY > 1 then
actor.minY = actor.minY - 1
if miny < CELL_MIN_Y[actor.minY] then
Teleport(actor, x, y)
else
for X = actor.minX, actor.maxX do
CELL_LIST[X][actor.minY]:enter(actor, false)
end
end
if actor.cellsVisualized then
RedrawCellVisualizers(actor)
end
end
if maxy < CELL_MIN_Y[actor.maxY] and actor.maxY > 1 then
actor.maxY = actor.maxY - 1
if maxy < CELL_MIN_Y[actor.maxY] then
Teleport(actor, x, y)
else
for X = actor.minX, actor.maxX do
CELL_LIST[X][actor.maxY + 1]:leave(actor)
end
end
if actor.cellsVisualized then
RedrawCellVisualizers(actor)
end
end
end
actor.lastX = x
actor.lastY = y
nextStep = counter + actor.cellCheckInterval
if nextStep > CYCLE_LENGTH then
nextStep = nextStep - CYCLE_LENGTH
end
numCellChecks[nextStep] = numCellChecks[nextStep] + 1
cellCheckedActors[nextStep][numCellChecks[nextStep]] = actor
actor.nextCellCheck = nextStep
actor.positionInCellCheck = numCellChecks[nextStep]
if visualizeAllActors then
BlzSetSpecialEffectPosition(actor.visualizer, x, y, actor.z[actor] + 75)
end
end
end
---For debug mode.
local function UpdateSelectedActor()
if selectedActor then
if not selectedActor.isGlobal then
local x = selectedActor.x[selectedActor.anchor]
local y = selectedActor.y[selectedActor.anchor]
BlzSetSpecialEffectPosition(selectedActor.visualizer, x, y, selectedActor.z[selectedActor] + 75)
for __, pair in ipairs(selectedActor.pairs) do
if pair[EveryStep] or pair[CurrentPosition] == counter then
if pair[InteractionFunc] == highlightedFunc then
VisualizationLightning(pair, 0.05, "DRAM")
else
VisualizationLightning(pair, 0.05, "DRAL")
end
end
end
SetCameraQuickPosition(x, y)
end
local description, title = GetDescription(selectedActor)
BlzFrameSetText(debugTooltipText, description)
BlzFrameSetText(debugTooltipTitle, title )
BlzFrameSetSize(debugTooltipText, 0.28, 0.0)
BlzFrameSetSize(debugTooltip, 0.29, BlzFrameGetHeight(debugTooltipText) + 0.0315)
end
end
--#endregion
--===========================================================================================================================================================
--Main
--===========================================================================================================================================================
--#region Main
local function Main()
local destructionQueued = DestructionQueued
local interactionFunc = InteractionFunc
local hostA = HostA
local hostB = HostB
local currentPosition = CurrentPosition
local positionInStep = PositionInStep
local invMinInterval = INV_MIN_INTERVAL
local evalCounter = unboundCounter - (unboundCounter // 10)*10 + 1
local startTime = os.clock()
if currentPair then
if HALT_ON_FIRST_CRASH then
local A = currentPair[ActorA]
local B = currentPair[ActorB]
Warning("\n|cffff0000Error:|r ALICE Cycle crashed during last execution. The identifiers of the actors responsible are "
.. "|cffffcc00" .. Identifier2String(A.identifier) .. "|r and |cffaaaaff" .. Identifier2String(B.identifier) .. "|r. Unique numbers: " .. A.unique .. ", " .. B.unique .. ". ALICE Cycle aborted due to HALT_ON_FIRST_CRASH being enabled. To resume, do ALICE_Resume()...")
ALICE_Halt()
return
else
OnCrash()
end
end
ResetCoordinateLookupTables()
--Needs to be first-in first-out because of pause/unpause.
local k = 1
while delayedCallbacks[k] do
delayedCallbacks[k]()
k = k + 1
end
for i = 1, #delayedCallbacks do
delayedCallbacks[i] = nil
end
BindChecks()
RemovePairsFromCycle()
counter = counter + 1
if counter > CYCLE_LENGTH then
counter = 1
end
unboundCounter = unboundCounter + 1
ALICE_TimeElapsed = unboundCounter*MIN_INTERVAL
CellCheck()
if benchmark then
local averageEvalTime = 0
for i = 1, 10 do
averageEvalTime = averageEvalTime + (evaluationTime[i] or 0)/10
end
print("eval time: |cffffcc00" .. string.format("\x25." .. 1 .. "f", 1000*averageEvalTime) .. "ms|r, actors: " .. #actorList .. ", pairs: " .. numPairs[counter] + numFixedPairs .. ", cell checks: " .. numCellChecks[counter])
end
numCellChecks[counter] = 0
if debugModeEnabled then
UpdateSelectedActor()
end
local numSteps
local nextStep
--Every Step Cycle
for i = 1, numFixedPairs do
currentPair = fixedPairs[i]
if not currentPair[destructionQueued] then
currentPair[interactionFunc](currentPair[hostA], currentPair[hostB])
end
end
--Variable Step Cycle
for i = 1, numPairs[counter] do
currentPair = whichPairs[counter][i]
if currentPair[destructionQueued] then
numSteps = MAX_STEPS
else
numSteps = (currentPair[interactionFunc](currentPair[hostA], currentPair[hostB])*invMinInterval + 1) // 1 --convert seconds to steps, then ceil.
if numSteps < 1 then
numSteps = 1
elseif numSteps > MAX_STEPS then
numSteps = MAX_STEPS
end
end
nextStep = counter + numSteps
if nextStep > CYCLE_LENGTH then
nextStep = nextStep - CYCLE_LENGTH
end
numPairs[nextStep] = numPairs[nextStep] + 1
whichPairs[nextStep][numPairs[nextStep]] = currentPair
currentPair[currentPosition] = nextStep
currentPair[positionInStep] = numPairs[nextStep]
end
numPairs[counter] = 0
--Categorize Pairs
for i = 1, numUncategorizedPairs do
currentPair = uncategorizedPairs[i]
if not currentPair[destructionQueued] then
local returnValue = currentPair[interactionFunc](currentPair[hostA], currentPair[hostB])
if returnValue then
numSteps = (returnValue*invMinInterval + 1) // 1
if numSteps < 1 then
numSteps = 1
elseif numSteps > MAX_STEPS then
numSteps = MAX_STEPS
end
nextStep = counter + numSteps
if nextStep > CYCLE_LENGTH then
nextStep = nextStep - CYCLE_LENGTH
end
numPairs[nextStep] = numPairs[nextStep] + 1
whichPairs[nextStep][numPairs[nextStep]] = currentPair
currentPair[currentPosition] = nextStep
currentPair[positionInStep] = numPairs[nextStep]
currentPair[EveryStep] = false
isEveryStepFunction[currentPair[interactionFunc]] = false
else
numFixedPairs = numFixedPairs + 1
fixedPairs[numFixedPairs] = currentPair
currentPair[positionInStep] = numFixedPairs
currentPair[EveryStep] = true
isEveryStepFunction[currentPair[interactionFunc]] = true
end
end
end
numUncategorizedPairs = 0
currentPair = nil
k = 1
while delayedCallbacks[k] do
delayedCallbacks[k]()
k = k + 1
end
for i = 1, #delayedCallbacks do
delayedCallbacks[i] = nil
end
local endTime = os.clock()
evaluationTime[evalCounter] = endTime - startTime
if bj_isSinglePlayer then
freezeCounter = freezeCounter + math.min(2*MAXIMUM_EVALUATION_TIME, (evaluationTime[evalCounter] - MAXIMUM_EVALUATION_TIME))/10
if freezeCounter < 0 then
freezeCounter = 0
elseif freezeCounter >= MAXIMUM_EVALUATION_TIME then
Warning("|cffff0000Error:|r ALICE Cycle terminated due to evaluation time exceeding the specified MAXIMUM_EVALUATION_TIME. The identifiers of the actors in the pairs responsible for the overload are:\n" .. GetPairStatistics(whichPairs[counter]))
PauseTimer(MASTER_TIMER)
end
end
end
--#endregion
--===========================================================================================================================================================
--Debug Mode
--===========================================================================================================================================================
---@param x number
---@param y number
---@param z number
---@return number, number
local function World2Screen(eyeX, eyeY, eyeZ, angleOfAttack, x, y, z)
local cosAngle = math.cos(angleOfAttack)
local sinAngle = math.sin(angleOfAttack)
local dx = x - eyeX
local dy = y - eyeY
local dz = (z or 0) - eyeZ
local yPrime = cosAngle*dy - sinAngle*dz
local zPrime = sinAngle*dy + cosAngle*dz
return 0.4 + 0.7425*dx/yPrime, 0.355 + 0.7425*zPrime/yPrime
end
--#region Debug Mode
local function OnMouseClick()
if BlzGetTriggerPlayerMouseButton() ~= MOUSE_BUTTON_TYPE_LEFT then
return
end
if selectedActor then
Deselect(selectedActor)
end
local mouseX = BlzGetTriggerPlayerMouseX()
local mouseY = BlzGetTriggerPlayerMouseY()
local objects = ALICE_EnumObjectsInRange(mouseX, mouseY, 500, true, nil)
local closestDist = 0.04
local closestObject = nil
local eyeX = GetCameraEyePositionX()
local eyeY = GetCameraEyePositionY()
local eyeZ = GetCameraEyePositionZ()
local angleOfAttack = -GetCameraField(CAMERA_FIELD_ANGLE_OF_ATTACK)
local mouseScreenX, mouseScreenY = World2Screen(eyeX, eyeY, eyeZ, angleOfAttack, mouseX, mouseY, GetTerrainZ(mouseX, mouseY))
local x, y, dx, dy
for __, object in ipairs(objects) do
local actor = GetActor(object)
--Find the actor that is closest to the mouse-cursor.
x, y = World2Screen(eyeX, eyeY, eyeZ, angleOfAttack, ALICE_GetCoordinates3D(actor))
dx, dy = x - mouseScreenX, y - mouseScreenY
local dist = sqrt(dx*dx + dy*dy)
if dist < closestDist then
closestDist = dist
closestObject = actor.host
end
end
if closestObject then
if actorOf[closestObject].isActor then
Select(actorOf[closestObject])
else
print("Multiple actors are anchored to this object. Press |cffffcc00Ctrl + T|r to cycle through.")
Select(actorOf[closestObject][1])
end
end
end
---Cycle through actors anchored to the same object.
local function OnCtrlT()
if selectedActor == nil then
return
end
local selectedObject = selectedActor.anchor
if actorOf[selectedObject].isActor then
return
end
for index, actor in ipairs(actorOf[selectedObject]) do
if selectedActor == actor then
Deselect(selectedActor)
if actorOf[selectedObject][index + 1] then
Select(actorOf[selectedObject][index + 1])
return
else
Select(actorOf[selectedObject][1])
return
end
end
end
end
local function EnableDebugMode()
if not debugModeEnabled then
Warning("\nDebug mode enabled. Left-click near an actor to display attributes and enable visualization. \n\nYou can use the Eikonium's IngameConsole to execute code on the selected actor. You refer to it with ALICE_GetActor().")
debugModeEnabled = true
BlzLoadTOCFile("CustomTooltip.toc")
debugTooltip = BlzCreateFrame("CustomTooltip", BlzGetOriginFrame(ORIGIN_FRAME_WORLD_FRAME, 0), 0, 0)
BlzFrameSetAbsPoint( debugTooltip , FRAMEPOINT_BOTTOMRIGHT , 0.8 , 0.165 )
BlzFrameSetSize( debugTooltip , 0.32 , 0.0 )
debugTooltipTitle = BlzFrameGetChild(debugTooltip,0)
debugTooltipText = BlzFrameGetChild(debugTooltip,1)
mouseClickTrigger = CreateTrigger()
TriggerRegisterPlayerEvent(mouseClickTrigger, GetTriggerPlayer(), EVENT_PLAYER_MOUSE_DOWN)
TriggerAddAction(mouseClickTrigger, OnMouseClick)
cycleSelectTrigger = CreateTrigger()
BlzTriggerRegisterPlayerKeyEvent(cycleSelectTrigger, GetTriggerPlayer(), OSKEY_T, 2, true)
TriggerAddAction(cycleSelectTrigger, OnCtrlT)
else
Warning("\nDebug mode has been disabled.")
if selectedActor then
Deselect(selectedActor)
end
debugModeEnabled = false
DestroyTrigger(mouseClickTrigger)
DestroyTrigger(cycleSelectTrigger)
BlzDestroyFrame(debugTooltip)
end
end
--#endregion
--===========================================================================================================================================================
--Init
--===========================================================================================================================================================
--#region Init
OnInit.global("ALICE", function()
Require "WidgetType"
MASTER_TIMER = CreateTimer()
MAX_STEPS = floor(MAX_INTERVAL/MIN_INTERVAL)
CYCLE_LENGTH = MAX_STEPS + 1
DO_NOT_EVALUATE = CYCLE_LENGTH + 1
for i = 1, DO_NOT_EVALUATE do
numPairs[i] = 0
whichPairs[i] = {}
end
numCellChecks = {}
for i = 1, CYCLE_LENGTH do
numCellChecks[i] = 0
cellCheckedActors[i] = {}
end
if USE_CELLS then
local worldBounds = GetWorldBounds()
MAP_MIN_X = GetRectMinX(worldBounds)
MAP_MAX_X = GetRectMaxX(worldBounds)
MAP_MIN_Y = GetRectMinY(worldBounds)
MAP_MAX_Y = GetRectMaxY(worldBounds)
for x = 1, NUM_CELLS_X do
for y = 1, NUM_CELLS_Y do
Cell.create(x,y)
end
end
for x = 1, NUM_CELLS_X do
CELL_MIN_X[x] = MAP_MIN_X + (x-1)/NUM_CELLS_X*(MAP_MAX_X - MAP_MIN_X)
CELL_MAX_X[x] = MAP_MIN_X + x/NUM_CELLS_X*(MAP_MAX_X - MAP_MIN_X)
end
for y = 1, NUM_CELLS_Y do
CELL_MIN_Y[y] = MAP_MIN_Y + (y-1)/NUM_CELLS_Y*(MAP_MAX_Y - MAP_MIN_Y)
CELL_MAX_Y[y] = MAP_MIN_Y + y/NUM_CELLS_Y*(MAP_MAX_Y - MAP_MIN_Y)
end
end
local trig = CreateTrigger()
for i = 0, 23 do
for __, name in ipairs(MAP_CREATORS) do
if string.find(GetPlayerName(Player(i)), name) then
TriggerRegisterPlayerChatEvent(trig, Player(i), "downtherabbithole", true)
TriggerRegisterPlayerChatEvent(trig, Player(i), "-downtherabbithole", true)
end
end
end
TriggerAddAction(trig, EnableDebugMode)
selfInteractionActor = ALICE_Create(nil, "selfInteraction", nil)
actorList[#actorList] = nil
celllessActorList[#celllessActorList] = nil
totalActors = totalActors - 1
selfInteractionActor.unique = 0
trig = CreateTrigger()
TriggerRegisterAnyUnitEventBJ(trig, EVENT_PLAYER_UNIT_CHANGE_OWNER)
TriggerAddAction(trig, OnUnitChangeOwner)
TimerStart(MASTER_TIMER, MIN_INTERVAL, true, Main)
local precomputedHeightMap = Require.optionally "PrecomputedHeightMap"
if precomputedHeightMap then
GetTerrainZ = _G.GetTerrainZ
else
moveableLoc = Location(0, 0)
GetTerrainZ = function(x, y)
MoveLocation(moveableLoc, x, y)
return GetLocationZ(moveableLoc)
end
end
end)
--#endregion
--===========================================================================================================================================================
--API
--===========================================================================================================================================================
--#region API
---@type Actor
local actorDummy = {
pairsWith = {},
interactionFunc = DoNothing
}
---Create an actor for the object host and add it to the cycle.
---@param host any
---@param identifier string | string[] | nil
---@param interactionFunc function | table | nil
---@param flags? table
---@return Actor | nil
function ALICE_Create(host, identifier, interactionFunc, flags)
if flags == nil then
return Create(host, identifier, interactionFunc)
else
for key, __ in pairs(flags) do
if not RECOGNIZED_FIELDS[key] then
Warning("|cffff0000Warning:|r Unrecognized field |cffffcc00" .. key .. "|r in flags passed to function ALICE_Create...")
end
end
return Create(
host,
identifier,
interactionFunc,
flags.pairsWith,
flags.priority,
flags.radius,
flags.cellCheckInterval,
flags.isStationary,
flags.destroyOnDeath,
flags.anchor,
flags.zOffset,
flags.bindToBuff,
flags.bindToOrder,
flags.onActorDestroy,
flags.hasInfiniteRange
)
end
end
---Create an actor for a table host and retrieve all input arguments from that table.
---@param whichClass table
---@return Actor | nil
function ALICE_CreateFromClass(whichClass)
return Create(
whichClass,
whichClass.identifier,
whichClass.interactionFunc,
whichClass.pairsWith,
whichClass.priority,
whichClass.radius,
whichClass.cellCheckInterval,
whichClass.isStationary,
whichClass.destroyOnDeath,
whichClass.anchor,
whichClass.zOffset,
whichClass.bindToBuff,
whichClass.bindToOrder,
whichClass.onActorDestroy,
whichClass.hasInfiniteRange
)
end
---Destroy the actor of the object and remove it from the cycle. Optional keyword parameter to specify actor with the keyword in its identifier for an object with multiple actors.
---@param object any
---@param keyword? string
function ALICE_Destroy(object, keyword)
object = GetActor(object, keyword)
if object then
Destroy(object)
end
end
--Object API
--===========================================================================================================================================================
---Checks if an actor already exists with the object as its host. Optional keyword parameter to specify actor with the keyword in its identifier for an object with multiple actors.
---@param object any
---@param keyword? string
---@return boolean
function ALICE_HasActor(object, keyword)
return GetActor(object, keyword) ~= nil
end
---Returns the actor stored to a host. Optional keyword parameter to specify actor with the keyword in its identifier for an object with multiple actors.
---@param object any
---@param keyword? string
---@return Actor | nil
function ALICE_GetActor(object, keyword)
if object == nil then
if selectedActor then
return selectedActor
end
return nil
end
if actorOf[object] then
if actorOf[object].isActor then
if keyword == nil or actorOf[object].identifier[keyword] then
return actorOf[object]
else
return nil
end
elseif keyword == nil then
--If called within interactionFunc and keyword is not specified, prioritize returning actor that's in the current pair, then an actor for which the object is the
--host, not the anchor.
if currentPair then
if object == currentPair[HostA] then
return currentPair[ActorA]
elseif object == currentPair[HostB] then
return currentPair[ActorB]
else
for __, actor in ipairs(actorOf[object]) do
if actor.host == object then
return actor
end
end
return actorOf[object][1]
end
else
for __, actor in ipairs(actorOf[object]) do
if actor.host == object then
return actor
end
end
return actorOf[object][1]
end
else
for __, actor in ipairs(actorOf[object]) do
if actor.identifier[keyword] then
return actor
end
end
return nil
end
elseif type(object) == "table" and object.isActor then
return object
else
return nil
end
end
GetActor = ALICE_GetActor
---Returns a table containing all actors attached to an object.
---@param object any
---@return table
function ALICE_GetAllActors(object)
if actorOf[object] then
if actorOf[object].isActor then
return {actorOf[object]}
else
return actorOf[object]
end
end
return {}
end
---Change the owner of a class-type object.
---@param object table
---@param newOwner player | integer
function ALICE_SetClassOwner(object, newOwner)
if type(object) ~= "table" then
Warning("|cffff0000Warning:|r Attempted to set owner of non-class object...")
return
end
if type(newOwner) == "number" then
newOwner = Player(newOwner - 1)
end
object[CLASS_FIELD_OWNER] = newOwner
if actorOf[object] then
if actorOf[object].isActor then
SetOwner(actorOf[object], actorOf[object].host)
if actorOf[object].reevaluateCaller == nil then
actorOf[object].reevaluateCaller = function() ReevaluatePairings(actorOf[object]) end
end
DelayCallback(actorOf[object].reevaluateCaller)
else
for __, actor in ipairs(actorOf[object]) do
SetOwner(actor, actor.host)
if actor.reevaluateCaller == nil then
actor.reevaluateCaller = function() ReevaluatePairings(actorOf[object]) end
end
DelayCallback(actor.reevaluateCaller)
end
end
end
end
---Calls the appropriate function to destroy the object, then destroys all actors attached to it. If the object is a table, the object:destroy() method will be called. If no destroy function exists, it will try to destroy the class's special effect.
---@param object any
function ALICE_Kill(object)
if type(object) == "table" then
if type(object.destroy) == "function" then
object:destroy()
elseif type(object[CLASS_FIELD_EFFECT]) == "userdata" then
DestroyEffect(object[CLASS_FIELD_EFFECT])
end
elseif type(object) == "userdata" then
if WidgetIsUnit(object) then
KillUnit(object)
elseif WidgetIsDestructable(object) then
KillDestructable(object)
elseif WidgetIsItem(object) then
RemoveItem(object)
else
Warning("|cffff0000Warning:|r Attempted to kill host of a type for which no kill function was set...")
end
else
Warning("|cffff0000Warning:|r Attempted to kill a host that is not an object...")
end
if actorOf[object] then
if actorOf[object].isActor then
Destroy(actorOf[object])
else
for __, actor in ipairs(actorOf[object]) do
Destroy(actor)
end
end
end
end
---Unpauses all paused interactions of the object. Optional keyword parameter to specify actor with the keyword in its identifier for an object with multiple actors.
---@param object any
---@param keyword? string
function ALICE_Unpause(object, keyword)
local actor = GetActor(object,keyword)
if actor == nil then
return
end
if actor.unpauseCaller == nil then
actor.unpauseCaller = function()
for __, pair in ipairs(actor.pairs) do
local actorA = pair[ActorA]
local actorB = pair[ActorB]
if pair[EveryStep] then
if pair[PositionInStep] == 0 and (not actorA.usesCells or not actorB.usesCells or SharesCellWith(actorA, actorB)) then
numFixedPairs = numFixedPairs + 1
fixedPairs[numFixedPairs] = pair
pair[PositionInStep] = numFixedPairs
end
elseif pair[EveryStep] == false then
local currentPosition = pair[CurrentPosition]
if currentPosition == DO_NOT_EVALUATE and (not actorA.usesCells or not actorB.usesCells or SharesCellWith(actorA, actorB)) then
local nextStep = counter + 1
if nextStep > CYCLE_LENGTH then
nextStep = nextStep - CYCLE_LENGTH
end
local pairAtHighestPosition = whichPairs[currentPosition][numPairs[currentPosition]]
pairAtHighestPosition[PositionInStep] = pair[PositionInStep]
whichPairs[currentPosition][pair[PositionInStep]] = pairAtHighestPosition
numPairs[currentPosition] = numPairs[currentPosition] - 1
numPairs[nextStep] = numPairs[nextStep] + 1
whichPairs[nextStep][numPairs[nextStep]] = pair
pair[CurrentPosition] = nextStep
pair[PositionInStep] = numPairs[nextStep]
end
end
end
end
end
DelayCallback(actor.unpauseCaller)
end
---Adds a self-interaction with the specified function to the object. If a self-interaction with that function already exists, nothing happens.
---@param object any
---@param whichFunc function
---@param keyword? string
function ALICE_AddSelfInteraction(object, whichFunc, keyword)
local actor = GetActor(object,keyword)
if actor == nil then
return
end
if actor.selfInteractions[whichFunc] ~= nil then
return
end
DelayCallback(function()
if actor.selfInteractions[whichFunc] == nil then
actor.selfInteractions[whichFunc] = CreatePair(actor, selfInteractionActor, whichFunc)
end
end)
end
---Removes the self-interaction with the specified function from the object.
---@param object any
---@param whichFunc function
---@param keyword? string
function ALICE_RemoveSelfInteraction(object, whichFunc, keyword)
local actor = GetActor(object,keyword)
if actor == nil then
return
end
if actor.selfInteractions[whichFunc] == nil then
return
end
local pair = actor.selfInteractions[whichFunc]
DelayCallback(function()
if actor.selfInteractions[whichFunc] ~= nil then
DestroyPair(pair)
actor.selfInteractions[whichFunc] = nil
end
end)
end
---@param object any
---@param whichFunc function
---@param keyword? string
function ALICE_HasSelfInteraction(object, whichFunc, keyword)
local actor = GetActor(object,keyword)
if actor == nil then
return
end
return actor.selfInteractions[whichFunc] ~= nil
end
---Returns the coordinates x, y of an object. This function uses hashed values and may not be accurate if immediately called after changing an object's location.
---@param object any
---@param keyword? string
---@return number, number
function ALICE_GetCoordinates2D(object, keyword)
local actor = GetActor(object,keyword)
if actor == nil then
return 0, 0
end
return actor.x[actor.anchor], actor.y[actor.anchor]
end
---Returns the coordinates x, y, z of an object. This function uses hashed values and may not be accurate if immediately called after changing an object's location.
---@param object any
---@param keyword? string
---@return number, number, number
function ALICE_GetCoordinates3D(object, keyword)
local actor = GetActor(object,keyword)
if actor == nil then
return 0, 0, 0
end
return actor.x[actor.anchor], actor.y[actor.anchor], actor.z[actor]
end
--Identifier API
--===========================================================================================================================================================
---Add identifier(s) to an object and pair it with all other objects it is now eligible to be paired with. Optional keyword parameter to specify actor with the keyword in its identifier for an object with multiple actors. Slow function!
---@param object any
---@param newIdentifier string | string[]
---@param keyword? string
function ALICE_AddIdentifier(object, newIdentifier, keyword)
local actor = GetActor(object,keyword)
if actor == nil then
return
end
if type(newIdentifier) == "string" then
actor.identifier[newIdentifier] = true
else
for __, word in ipairs(newIdentifier) do
actor.identifier[word] = true
end
end
if actor.reevaluateCaller == nil then
actor.reevaluateCaller = function() ReevaluatePairings(actor) end
end
DelayCallback(actor.reevaluateCaller)
end
---Remove identifier(s) from an object and remove all pairings with objects it is no longer eligible to be paired with. Optional keyword parameter to specify actor with the keyword in its identifier for an object with multiple actors. Slow function!
---@param object any
---@param toRemove string | string[]
---@param keyword? string
function ALICE_RemoveIdentifier(object, toRemove, keyword)
local actor = GetActor(object,keyword)
if actor == nil then
return
end
if type(toRemove) == "string" then
if actor.identifier[toRemove] == nil then
return
end
actor.identifier[toRemove] = nil
else
local removedSomething = false
for __, word in ipairs(toRemove) do
if actor.identifier[word] then
removedSomething = true
actor.identifier[word] = nil
end
end
if not removedSomething then
return
end
end
if actor.reevaluateCaller == nil then
actor.reevaluateCaller = function() ReevaluatePairings(actor) end
end
DelayCallback(actor.reevaluateCaller)
end
---Exchanges one of the object's identifier with another. If the old identifier is not found, the new one won't be added. Optional keyword parameter to specify actor with the keyword in its identifier for an object with multiple actors. Slow function!
---@param object any
---@param oldIdentifier string
---@param newIdentifier string
---@param keyword? string
function ALICE_SwapIdentifier(object, oldIdentifier, newIdentifier, keyword)
local actor = GetActor(object,keyword)
if actor == nil then
return
end
if actor.identifier[oldIdentifier] == nil then
return
end
actor.identifier[oldIdentifier] = nil
actor.identifier[newIdentifier] = true
if actor.reevaluateCaller == nil then
actor.reevaluateCaller = function() ReevaluatePairings(actor) end
end
DelayCallback(actor.reevaluateCaller)
end
---Sets the object's identifier to a string or string sequence. Slow function!
---@param object any
---@param newIdentifier string | string[] | nil
---@param keyword? string
function ALICE_SetIdentifier(object, newIdentifier, keyword)
local actor = GetActor(object,keyword)
if actor == nil then
return
end
for word, __ in pairs(actor.identifier) do
actor.identifier[word] = nil
end
for __, word in ipairs(newIdentifier) do
actor.identifier[word] = true
end
if actor.reevaluateCaller == nil then
actor.reevaluateCaller = function() ReevaluatePairings(actor) end
end
DelayCallback(actor.reevaluateCaller)
end
---Checks if the object toCheck contains the identifier toMatch. ToMatch can be a string or a string sequence with optional matching type entry (see under Pairing Logic). Optional keyword parameter to specify actor with the keyword in its identifier for an object with multiple actors.
---@param object any
---@param identifier string | table
---@param keyword? string
---@return boolean
function ALICE_HasIdentifier(object, identifier, keyword)
local actor = GetActor(object, keyword)
if actor == nil then
return false
end
if type(identifier) == "table" then
for key, __ in pairs(actorDummy.pairsWith) do
actorDummy.pairsWith[key] = nil
end
InitPairingLogic(actorDummy, identifier)
return CheckIdentifierOverlap(actor, actorDummy)
else
return actor.identifier[identifier]
end
end
---Compiles the identifiers of an object into a table. Optional keyword parameter to specify actor with the keyword in its identifier for an object with multiple actors.
---@param object any
---@param keyword? string
function ALICE_GetIdentifier(object, keyword)
local actor = GetActor(object,keyword)
if actor == nil then
return
end
local returnTable = {}
for key, __ in pairs(actor.identifier) do
table.insert(returnTable, key)
end
table.sort(returnTable, function(a, b) return tostring(a) < tostring(b) end)
return returnTable
end
--Pair API
--===========================================================================================================================================================
---Changes the interactionFunc of the pair currently being evaluated. You cannot replace a function with a return value with one that has no return value and vice versa.
---@param whichFunc function
function ALICE_PairSetInteractionFunc(whichFunc)
if currentPair == nil then
error("Attempted to call Pair API function from outside of allowed functions.")
end
if currentPair[ActorB] == selfInteractionActor then
currentPair[ActorA].selfInteractions[currentPair[InteractionFunc]] = nil
currentPair[ActorA].selfInteractions[whichFunc] = currentPair
end
currentPair[InteractionFunc] = whichFunc
end
---Disables interactions between the actors of the current pair after this one.
function ALICE_PairDisable()
if currentPair == nil then
error("Attempted to call Pair API function from outside of allowed functions.")
end
pairingExcluded[currentPair[ActorA]][currentPair[ActorB]] = true
pairingExcluded[currentPair[ActorB]][currentPair[ActorA]] = true
excludedManually[currentPair[ActorA]][currentPair[ActorB]] = true
excludedManually[currentPair[ActorB]][currentPair[ActorA]] = true
if currentPair[ActorB] == selfInteractionActor then
currentPair[ActorA].selfInteractions[currentPair[InteractionFunc]] = nil
end
local toRemovePair = currentPair
DelayCallback(function()
DestroyPair(toRemovePair)
end)
end
---Returns a table unique to the pair currently being evaluated, which can be used to read and write data. Optimal argument to set a metatable for the data table.
---@param whichMetatable? table
---@return table
function ALICE_PairLoadData(whichMetatable)
if currentPair == nil then
error("Attempted to call Pair API function from outside of allowed functions.")
end
if currentPair[UserData] == nil then
currentPair[UserData] = GetUnusedTable()
setmetatable(currentPair[UserData], whichMetatable)
end
return currentPair[UserData]
end
---Executes the function callback(objectA, objectB, pairData) when either of the actors in the current pair are destroyed. Only one callback!
function ALICE_PairOnDestroy(callback)
if currentPair == nil then
error("Attempted to call Pair API function from outside of allowed functions.")
end
if currentPair[UserData] == nil then
currentPair[UserData] = GetUnusedTable()
end
currentPair[UserData].onDestroy = callback
end
---Returns the distance between the objects of the pair currently being evaluated in two dimensions. This function uses hashed values and may not be accurate if immediately called after changing an object's location.
---@return number
function ALICE_PairGetDistance2D()
if currentPair == nil then
error("Attempted to call Pair API function from outside of allowed functions.")
end
local actorA = currentPair[ActorA]
local actorB = currentPair[ActorB]
if actorA and actorB == selfInteractionActor then
return 0
end
local dx = actorA.x[actorA.anchor] - actorB.x[actorB.anchor]
local dy = actorA.y[actorA.anchor] - actorB.y[actorB.anchor]
return sqrt(dx*dx + dy*dy)
end
---Returns the distance between the objects of the pair currently being evaluated in three dimensions. This function uses hashed values and may not be accurate if immediately called after changing an object's location.
---@return number
function ALICE_PairGetDistance3D()
if currentPair == nil then
error("Attempted to call Pair API function from outside of allowed functions.")
end
local actorA = currentPair[ActorA]
local actorB = currentPair[ActorB]
if actorA and actorB == selfInteractionActor then
return 0
end
local dx = actorA.x[actorA.anchor] - actorB.x[actorB.anchor]
local dy = actorA.y[actorA.anchor] - actorB.y[actorB.anchor]
local dz = actorA.z[actorA] - actorB.z[actorB]
return sqrt(dx^2 + dy^2 + dz^2)
end
---Returns the angle from object A to object B of the pair currently being evaluated. This function uses hashed values and may not be accurate if immediately called after changing an object's location.
---@return number
function ALICE_PairGetAngle2D()
if currentPair == nil then
error("Attempted to call Pair API function from outside of allowed functions.")
end
local actorA = currentPair[ActorA]
local actorB = currentPair[ActorB]
if actorA and actorB == selfInteractionActor then
return 0
end
local dx = actorB.x[actorB.anchor] - actorA.x[actorA.anchor]
local dy = actorB.y[actorB.anchor] - actorA.y[actorA.anchor]
return atan(dy, dx)
end
---Returns the horizontal and vertical angles from object A to object B of the pair currently being evaluated. This function uses hashed values and may not be accurate if immediately called after changing an object's location.
---@return number, number
function ALICE_PairGetAngle3D()
if currentPair == nil then
error("Attempted to call Pair API function from outside of allowed functions.")
end
local actorA = currentPair[ActorA]
local actorB = currentPair[ActorB]
if actorA and actorB == selfInteractionActor then
return 0, 0
end
local dx = actorB.x[actorB.anchor] - actorA.x[actorA.anchor]
local dy = actorB.y[actorB.anchor] - actorA.y[actorA.anchor]
local dz = actorB.z[actorB] - actorA.z[actorA]
return atan(dy, dx), atan(dz, sqrt(dx*dx + dy*dy))
end
---Returns the coordinates of the objects in the pair currently being evaluated in the order x1, y1, x2, y2. This function uses hashed values and may not be accurate if immediately called after changing an object's location.
---@return number, number, number, number
function ALICE_PairGetCoordinates2D()
if currentPair == nil then
error("Attempted to call Pair API function from outside of allowed functions.")
end
local actorA = currentPair[ActorA]
local actorB = currentPair[ActorB]
return actorA.x[actorA.anchor], actorA.y[actorA.anchor], actorB.x[actorB.anchor], actorB.y[actorB.anchor]
end
---Returns the coordinates of the objects in the pair currently being evaluated in the order x1, y1, z1, x2, y2, z2. This function uses hashed values and may not be accurate if immediately called after changing an object's location.
---@return number, number, number, number, number, number
function ALICE_PairGetCoordinates3D()
if currentPair == nil then
error("Attempted to call Pair API function from outside of allowed functions.")
end
local actorA = currentPair[ActorA]
local actorB = currentPair[ActorB]
return actorA.x[actorA.anchor], actorA.y[actorA.anchor], actorA.z[actorA], actorB.x[actorB.anchor], actorB.y[actorB.anchor], actorB.z[actorB]
end
---Returns true if the owners of the objects in the current pair are allies.
---@return boolean
function ALICE_PairIsFriend()
if currentPair == nil then
error("Attempted to call Pair API function from outside of allowed functions.")
end
local objectA = currentPair[ActorA]
local objectB = currentPair[ActorB]
if objectA.owner == nil or objectB.owner == nil then
return false
end
return IsPlayerAlly(objectA.owner, objectB.owner)
end
---Returns true if the owners of the objects in the current pair are enemies.
---@return boolean
function ALICE_PairIsEnemy()
if currentPair == nil then
error("Attempted to call Pair API function from outside of allowed functions.")
end
local objectA = currentPair[ActorA]
local objectB = currentPair[ActorB]
if objectA.owner == nil or objectB.owner == nil then
return false
end
return IsPlayerEnemy(objectA.owner, objectB.owner)
end
---Returns true if this is the first time this function was invoked for the current pair,otherwise false.
---@return boolean
function ALICE_PairIsFirstContact()
if currentPair == nil then
error("Attempted to call Pair API function from outside of allowed functions.")
end
if currentPair[UserData] == nil then
ALICE_PairLoadData()
currentPair[UserData].hadContact = true
return true
elseif currentPair[UserData].hadContact then
return false
else
currentPair[UserData].hadContact = true
return true
end
end
---Returns true if this function isn't on cooldown for the pair currently being evaluated, then invokes a cooldown of the specified duration.
---@param duration number
---@return boolean
function ALICE_PairCooldown(duration)
if currentPair == nil then
error("Attempted to call Pair API function from outside of allowed functions.")
end
if currentPair[UserData] == nil then
ALICE_PairLoadData()
currentPair[UserData].cooldownExpires = unboundCounter + (duration*INV_MIN_INTERVAL) // 1
return true
elseif currentPair[UserData].cooldownExpires == nil or currentPair[UserData].cooldownExpires <= unboundCounter then
currentPair[UserData].cooldownExpires = unboundCounter + (duration*INV_MIN_INTERVAL) // 1
return true
else
return false
end
end
---Purge pair data, call onDestroy method and reset ALICE_PairIsFirstContact and ALICE_IsUnoccupied functions.
function ALICE_PairForget()
if currentPair == nil then
error("Attempted to call Pair API function from outside of allowed functions.")
end
if currentPair[ActorB][currentPair[InteractionFunc]] == currentPair then
currentPair[ActorB][currentPair[InteractionFunc]] = nil
end
if currentPair[UserData] then
PairUserDataOnDestroy(currentPair)
end
end
---Returns false if this function was invoked for another pair that has the same interactionFunc and the same receiving actor. Otherwise, returns true. In other words, only one pair can execute the code within an ALICE_PairIsUnoccupied() block.
function ALICE_PairIsUnoccupied()
if currentPair == nil then
error("Attempted to call Pair API function from outside of allowed functions.")
end
if currentPair[ActorB][currentPair[InteractionFunc]] and currentPair[ActorB][currentPair[InteractionFunc]] ~= currentPair then
return false
else
--Store for the receiving actor at the key of the interaction func the current pair as occupying that slot, blocking other pairs.
currentPair[ActorB][currentPair[InteractionFunc]] = currentPair
return true
end
end
---Pauses interactions of the current pair after this one. This function is unreliable and should only be used to improve performance. Resume with ALICE_Unpause.
function ALICE_PairPause()
if currentPair == nil then
error("Attempted to call Pair API function from outside of allowed functions.")
end
local pausedPair = currentPair
if isEveryStepFunction[currentPair[InteractionFunc]] == nil then
return
end
if pausedPair[Pause] == nil then
pausedPair[Pause] = function()
if pausedPair[EveryStep] then
if pausedPair[PositionInStep] ~= 0 then
fixedPairs[numFixedPairs][PositionInStep] = pausedPair[PositionInStep]
fixedPairs[pausedPair[PositionInStep]] = fixedPairs[numFixedPairs]
numFixedPairs = numFixedPairs - 1
pausedPair[PositionInStep] = 0
end
elseif pausedPair[EveryStep] == false then
local currentPosition = pausedPair[CurrentPosition]
if currentPosition ~= DO_NOT_EVALUATE then
local nextStep = DO_NOT_EVALUATE
local pairAtHighestPosition = whichPairs[currentPosition][numPairs[currentPosition]]
pairAtHighestPosition[PositionInStep] = pausedPair[PositionInStep]
whichPairs[currentPosition][pausedPair[PositionInStep]] = pairAtHighestPosition
numPairs[currentPosition] = numPairs[currentPosition] - 1
numPairs[nextStep] = numPairs[nextStep] + 1
whichPairs[nextStep][numPairs[nextStep]] = pausedPair
pausedPair[CurrentPosition] = nextStep
pausedPair[PositionInStep] = numPairs[nextStep]
end
end
end
end
DelayCallback(pausedPair[Pause])
end
---Returns the actors of the current pair. All actor fields should be considered read-only!
---@return Actor, Actor
function ALICE_PairGetActors()
return currentPair[ActorA], currentPair[ActorB]
end
--Enum API
--===========================================================================================================================================================
local enumExceptions = {}
---Enum functions return a table with all objects that are in included in filter. If the filter specifies a player filter, you need to specify the whoFilters player.
---@param filter function | boolean | string | table | nil
---@param whoFilters player | nil
---@return table
function ALICE_EnumObjects(filter, whoFilters)
local returnTable = {}
if filter == nil then
filter = true
end
if type(filter) == "table" then
InitPairingLogic(actorDummy, filter)
else
actorDummy.pairsWith = filter
end
actorDummy.owner = whoFilters
local evaluator = pairingEvaluator[type(filter)]
for __, actor in ipairs(actorList) do
if evaluator(actor, actorDummy) and not enumExceptions[actor.host] then
returnTable[#returnTable + 1] = actor.host
enumExceptions[actor.host] = true
end
end
for key, __ in pairs(enumExceptions) do
enumExceptions[key] = nil
end
return returnTable
end
---Enum functions return a table with all objects that are in included in filter. If the filter specifies a player filter, you need to specify the whoFilters player.
---@param filter function | boolean | string | table | nil
---@param whoFilters player | nil
---@param action function
function ALICE_ForAllObjectsDo(filter, whoFilters, action)
local list = ALICE_EnumObjects(filter, whoFilters)
for __, object in ipairs(list) do
action(object)
end
end
---Enum functions return a table with all objects that are in included in filter. If the filter specifies a player filter, you need to specify the whoFilters player.
---@param x number
---@param y number
---@param range number
---@param filter function | boolean | string | table | nil
---@param whoFilters player | nil
---@return table
function ALICE_EnumObjectsInRange(x, y, range, filter, whoFilters)
local returnTable = {}
ResetCoordinateLookupTables()
if filter == nil then
filter = true
end
local minX = min(NUM_CELLS_X, max(1, ceil(NUM_CELLS_X*(x - range - MAP_MIN_X)/(MAP_MAX_X - MAP_MIN_X))))
local minY = min(NUM_CELLS_Y, max(1, ceil(NUM_CELLS_Y*(y - range - MAP_MIN_Y)/(MAP_MAX_Y - MAP_MIN_Y))))
local maxX = min(NUM_CELLS_X, max(1, ceil(NUM_CELLS_X*(x + range - MAP_MIN_X)/(MAP_MAX_X - MAP_MIN_X))))
local maxY = min(NUM_CELLS_Y, max(1, ceil(NUM_CELLS_Y*(y + range - MAP_MIN_Y)/(MAP_MAX_Y - MAP_MIN_Y))))
local dx
local dy
local rangeSquared = range*range
if type(filter) == "table" then
InitPairingLogic(actorDummy, filter)
else
actorDummy.pairsWith = filter
end
actorDummy.owner = whoFilters
local evaluator = pairingEvaluator[type(filter)]
for X = minX, maxX do
for Y = minY, maxY do
for __, actor in ipairs(CELL_LIST[X][Y].actors) do
if not enumExceptions[actor.host] then
if evaluator(actor, actorDummy) then
enumExceptions[actor.host] = true
dx = actor.x[actor.anchor] - x
dy = actor.y[actor.anchor] - y
if dx*dx + dy*dy < rangeSquared then
returnTable[#returnTable + 1] = actor.host
end
end
end
end
end
end
for key, __ in pairs(enumExceptions) do
enumExceptions[key] = nil
end
return returnTable
end
---Enum functions return a table with all objects that are in included in filter. If the filter specifies a player filter, you need to specify the whoFilters player.
---@param x number
---@param y number
---@param range number
---@param filter function | boolean | string | table | nil
---@param whoFilters player | nil
---@param action function
function ALICE_ForAllObjectsInRangeDo(x, y, range, filter, whoFilters, action)
local list = ALICE_EnumObjectsInRange(x, y, range, filter, whoFilters)
for __, object in ipairs(list) do
action(object)
end
end
---Enum functions return a table with all objects that are in included in filter. If the filter specifies a player filter, you need to specify the whoFilters player.
---@param minx number
---@param miny number
---@param maxx number
---@param maxy number
---@param filter function | boolean | string | table | nil
---@param whoFilters? player | nil
---@return table
function ALICE_EnumObjectsInRect(minx, miny, maxx, maxy, filter, whoFilters)
local returnTable = {}
ResetCoordinateLookupTables()
if filter == nil then
filter = true
end
local minX = min(NUM_CELLS_X, max(1, ceil(NUM_CELLS_X*(minx - MAP_MIN_X)/(MAP_MAX_X - MAP_MIN_X))))
local minY = min(NUM_CELLS_Y, max(1, ceil(NUM_CELLS_Y*(miny - MAP_MIN_Y)/(MAP_MAX_Y - MAP_MIN_Y))))
local maxX = min(NUM_CELLS_X, max(1, ceil(NUM_CELLS_X*(maxx - MAP_MIN_X)/(MAP_MAX_X - MAP_MIN_X))))
local maxY = min(NUM_CELLS_Y, max(1, ceil(NUM_CELLS_Y*(maxy - MAP_MIN_Y)/(MAP_MAX_Y - MAP_MIN_Y))))
local x
local y
if type(filter) == "table" then
InitPairingLogic(actorDummy, filter)
else
actorDummy.pairsWith = filter
end
actorDummy.owner = whoFilters
local evaluator = pairingEvaluator[type(filter)]
for X = minX, maxX do
for Y = minY, maxY do
for __, actor in ipairs(CELL_LIST[X][Y].actors) do
if not enumExceptions[actor.host] then
if evaluator(actor, actorDummy) then
enumExceptions[actor.host] = true
x = actor.x[actor.anchor]
y = actor.y[actor.anchor]
if x > minx and x < maxx and y > miny and y < maxy then
returnTable[#returnTable + 1] = actor.host
end
end
end
end
end
end
for key, __ in pairs(enumExceptions) do
enumExceptions[key] = nil
end
return returnTable
end
---Enum functions return a table with all objects that are in included in filter. If the filter specifies a player filter, you need to specify the whoFilters player.
---@param minx number
---@param miny number
---@param maxx number
---@param maxy number
---@param filter function | boolean | string | table | nil
---@param whoFilters player | nil
---@param action function
function ALICE_ForAllObjectsInRectDo(minx, miny, maxx, maxy, filter, whoFilters, action)
local list = ALICE_EnumObjectsInRect(minx, miny, maxx, maxy, filter, whoFilters)
for __, object in ipairs(list) do
action(object)
end
end
--Debug API
--===========================================================================================================================================================
---Pause the entire cycle.
function ALICE_Halt()
ALICE_TimeScale = 1000000
PauseTimer(MASTER_TIMER)
end
---Slow down the ALICE cycle by the specified factor. Use ALICE_TimeScale to refer to the slow motion factor in external functions.
---@param factor number
function ALICE_SlowMotion(factor)
ALICE_TimeScale = factor
TimerStart(MASTER_TIMER, MIN_INTERVAL*factor, true, Main)
end
---Resume the entire cycle.
function ALICE_Resume()
ALICE_TimeScale = 1
TimerStart(MASTER_TIMER, MIN_INTERVAL, true, Main)
end
---Create a lightning effect between the objects of the current pair. Optional lightning type argument.
---@param duration? number
---@param lightningType? string
function ALICE_PairVisualize(duration, lightningType)
if currentPair == nil then
error("Attempted to call Pair API function from outside of allowed functions.")
end
local actorA = currentPair[ActorA]
local actorB = currentPair[ActorB]
local xa = actorA.x[actorA.anchor]
local ya = actorA.y[actorA.anchor]
local xb = actorB.x[actorB.anchor]
local yb = actorB.y[actorB.anchor]
if xa and ya and xb and yb then
local visLight = AddLightning(lightningType or "DRAL", true, xa, ya, xb, yb)
TimerStart(CreateTimer(), duration or 0.05, false, function()
DestroyLightning(visLight)
DestroyTimer(GetExpiredTimer())
end)
end
end
---Changes the lightning type for the visualization lightnings of the interactionFunc to stand out in debug mode. If the function is global, it can be refered to by name.
---@param whichFunc string | function | nil
function ALICE_HighlightFunc(whichFunc)
if type(whichFunc) == "string" then
if _G[whichFunc] then
highlightedFunc = _G[whichFunc]
else
Warning("|cffff0000Warning:|r Function to highlight not found...")
end
elseif type(whichFunc) == "function" then
highlightedFunc = whichFunc
elseif whichFunc == nil then
highlightedFunc = nil
else
Warning("|cffff0000Warning:|r Invalid argument in ALICE_HighlightFunction...")
end
end
---Checks if the objectA would pair with the objectB.
---@param objectA any
---@param objectB any
---@return boolean
function ALICE_PairsWith(objectA, objectB)
objectA = GetActor(objectA)
if objectA == nil then
return false
end
objectB = GetActor(objectB)
if objectB == nil then
return false
end
return objectA.evaluator(objectB, objectA) or objectB.evaluator(objectA, objectB)
end
---Enable/disable visualization of the cells the object is currently in. Optional keyword parameter to specify actor with the keyword in its identifier for an object with multiple actors.
---@param whichObject any
---@param enable? boolean
---@param keyword? string
function ALICE_VisualizeCells(whichObject, enable, keyword)
whichObject = GetActor(whichObject,keyword)
if whichObject == nil then
Warning("|cffff0000Warning:|r Could not find actor of argument in ALICE_VisualizeCells...")
return
end
VisualizeCells(whichObject, (enable == nil) or enable)
end
---Prints out statistics showing which actors are occupying which percentage of the calculations.
function ALICE_Statistics()
Warning("|cffffcc00Statistics:|r The identifiers of the actors in the pairs currently being evaluated are:\n" .. GetPairStatistics(whichPairs[counter+1]))
end
---List all global actors.
function ALICE_ListGlobals()
local message = "List of all global actors:"
for __, actor in ipairs(celllessActorList) do
if actor.isGlobal then
message = message .. "\n" .. Identifier2String(actor.identifier) .. ", Unique: " .. actor.unique
end
end
Warning(message)
end
---Select the specified actor. Pass integer to select by unique number. Requires debug mode.
---@param whichActor Actor | integer
function ALICE_Select(whichActor)
if not debugModeEnabled then
Warning("|cffff0000Error:|r ALICE_Select only available in debug mode...")
return
end
if type(whichActor) == "table" then
Select(whichActor)
else
local actor = ALICE_GetFromUnique(whichActor)
if actor then
Select(actor)
SetCameraPosition(ALICE_GetCoordinates2D(actor))
else
print("\nNo actor exists with the specified unique number...")
end
end
end
---Returns the actor with the specified unique number.
---@param number integer
---@return Actor | nil
function ALICE_GetFromUnique(number)
for __, actor in ipairs(actorList) do
if actor.unique == number then
return actor
end
end
return nil
end
---Continuously prints the number of actors, pair interactions and cell checks until disabled.
function ALICE_Benchmark()
benchmark = not benchmark
end
---Create lightning effects around all cells.
function ALICE_VisualizeAllCells()
visualizeAllCells = not visualizeAllCells
if visualizeAllCells then
for X = 1, NUM_CELLS_X do
for Y = 1, NUM_CELLS_Y do
local minx = MAP_MIN_X + (X-1)/NUM_CELLS_X*(MAP_MAX_X - MAP_MIN_X)
local miny = MAP_MIN_Y + (Y-1)/NUM_CELLS_Y*(MAP_MAX_Y - MAP_MIN_Y)
local maxx = MAP_MIN_X + X/NUM_CELLS_X*(MAP_MAX_X - MAP_MIN_X)
local maxy = MAP_MIN_Y + Y/NUM_CELLS_Y*(MAP_MAX_Y - MAP_MIN_Y)
CELL_LIST[X][Y].horizontalLightning = AddLightning("DRAM", false, minx, miny, maxx, miny)
SetLightningColor(CELL_LIST[X][Y].horizontalLightning, 1, 1, 1, 0.35)
CELL_LIST[X][Y].verticalLightning = AddLightning("DRAM", false, maxx, miny, maxx, maxy)
SetLightningColor(CELL_LIST[X][Y].verticalLightning, 1, 1, 1, 0.35)
end
end
else
for X = 1, NUM_CELLS_X do
for Y = 1, NUM_CELLS_Y do
DestroyLightning(CELL_LIST[X][Y].horizontalLightning)
DestroyLightning(CELL_LIST[X][Y].verticalLightning)
end
end
end
end
---Creates arrows above all non-global actors.
function ALICE_VisualizeAllActors()
visualizeAllActors = not visualizeAllActors
if visualizeAllActors then
for __, actor in ipairs(actorList) do
if not actor.isGlobal and not actor ~= selectedActor then
CreateVisualizer(actor)
end
end
else
for __, actor in ipairs(actorList) do
if not actor.isGlobal and not actor ~= selectedActor then
DestroyEffect(actor.visualizer)
end
end
end
end
--Matrix API
--===========================================================================================================================================================
---Creates and returns a 2D-table storing data for all objects that are paired using the specified function or list of functions.
---@param whichFunc function | table
---@param metatable? table
---@return table
function ALICE_MatrixCreate(whichFunc, metatable)
local newMatrix = {}
if type(whichFunc) == "table" then
for __, func in ipairs(whichFunc) do
functionMatrix[func] = newMatrix
end
else
functionMatrix[whichFunc] = newMatrix
end
newMatrix.metatable = metatable
return newMatrix
end
---Writes the value A into the data matrix at [objectA][objectB][key] and the value B at [objectB][objectA][key]. Pass KEEP_VALUE to not overwrite the current value and only store one. You can store multiple values by passing a string sequence as key.
---@param objectA any
---@param objectB any
---@param A any
---@param B any
---@param key string | table
function ALICE_WriteMatrix(objectA, objectB, A, B, key)
local matrix = functionMatrix[currentPair[InteractionFunc]]
local rowA = matrix[objectA]
local rowB = matrix[objectB]
local entryAB = rowA[objectB]
local entryBA = rowB[objectA]
if type(key) == "table" then
for i, subkey in ipairs(key) do
if A ~= KEEP_VALUE and A[i] ~= KEEP_VALUE then
entryAB[subkey] = A[i]
end
if B ~= KEEP_VALUE and B[i] ~= KEEP_VALUE then
entryBA[subkey] = B[i]
end
end
else
if A ~= KEEP_VALUE then
entryAB[key] = A
end
if B ~= KEEP_VALUE then
entryBA[key] = B
end
end
end
---Returns two tables with the data stored at [objectA][objectB] and at [objectB][objectA].
---@return table, table
function ALICE_PairReadMatrix()
if currentPair == nil then
error("Attempted to call Pair API function from outside of allowed functions.")
end
local matrix = functionMatrix[currentPair[InteractionFunc]]
return matrix[currentPair[HostA]][currentPair[HostB]], matrix[currentPair[HostB]][currentPair[HostA]]
end
---@param object any
---@param whichFunc function
---@return table | nil
function ALICE_LoadMatrixRow(object, whichFunc)
return functionMatrix[whichFunc][object]
end
--Optimization API
--===========================================================================================================================================================
---All pairs using this function will get automatically disabled if both objects are stationary and their distance is greater than the max range.
---@param whichFunc function
---@param maxRange number
function ALICE_FuncSetMaxRange(whichFunc, maxRange)
functionMaxRange[whichFunc] = maxRange
end
---The first interaction of all pairs using this function will be delayed by a random time from 0 to the specified delay.
---@param whichFunc function
---@param randomDelay number
function ALICE_FuncSetRandomDelay(whichFunc, randomDelay)
functionRandomDelay[whichFunc] = randomDelay
end
---Calls the initFunc with the hosts as arguments whenever a pair is created with the specified interactionFunc.
---@param whichFunc function
---@param initFunc function
function ALICE_FuncSetInit(whichFunc, initFunc)
functionInitializer[whichFunc] = initFunc
end
---Changes the radius of the object. Optional keyword parameter to specify actor with the keyword in its identifier for an object with multiple actors.
---@param object any
---@param newRadius number
---@param keyword? string
function ALICE_SetRadius(object, newRadius, keyword)
object = GetActor(object,keyword)
if object == nil then
return
end
object.radius = newRadius
end
---Changes the behavior of pairs using the specified function so that the interactions continue to be evaluated when the two objects leave their interaction range.
---@param whichFunc function
function ALICE_FuncSetPersistOnOutOfRange(whichFunc)
functionPersistOnBreak[whichFunc] = true
end
---Sets an object to stationary/not stationary. Optional keyword parameter to specify actor with the keyword in its identifier for an object with multiple actors.
---@param object any
---@param enable boolean
---@param keyword? string
function ALICE_SetStationary(object, enable, keyword)
local actor = GetActor(object,keyword)
if actor == nil then
return
end
if actor.isStationary == enable then
return
end
actor.isStationary = enable
if enable then
local nextCheck = actor.nextCellCheck
local actorAtHighestPosition = cellCheckedActors[nextCheck][numCellChecks[nextCheck]]
actorAtHighestPosition.positionInCellCheck = actor.positionInCellCheck
cellCheckedActors[nextCheck][actor.positionInCellCheck] = actorAtHighestPosition
numCellChecks[nextCheck] = numCellChecks[nextCheck] - 1
actor.nextCellCheck = DO_NOT_EVALUATE
else
local nextStep = counter + 1
if nextStep > CYCLE_LENGTH then
nextStep = nextStep - CYCLE_LENGTH
end
numCellChecks[nextStep] = numCellChecks[nextStep] + 1
cellCheckedActors[nextStep][numCellChecks[nextStep]] = actor
actor.nextCellCheck = nextStep
actor.positionInCellCheck = numCellChecks[nextStep]
end
end
---Changes how often ALICE checks if the object has entered a new cell. Optional keyword parameter to specify actor with the keyword in its identifier for an object with multiple actors.
---@param object any
---@param newInterval number
---@param keyword? string
function ALICE_SetCellCheckInterval(object, newInterval, keyword)
object = GetActor(object,keyword)
if object == nil then
return
end
object.cellCheckInterval = min(MAX_STEPS, max(1, ceil((newInterval - 0.001)*INV_MIN_INTERVAL)))
end
---Starts a periodic timer that invokes the callback function. The timer is affected by slow motion. The callback function has a boolean return value that specifies if the loop should be terminated.
---@param timeOut number
---@param callback function
---@return timer
function ALICE_Periodic(timeOut, callback)
local t = CreateTimer()
local func = nil ---@type function
func = function()
if not callback() then
TimerStart(t, timeOut*ALICE_TimeScale, false, func)
else
DestroyTimer(t)
end
end
TimerStart(t, timeOut*ALICE_TimeScale, false, func)
return t
end
--#endregion
end
if Debug then Debug.beginFile "CAT Widgets" end
do
--[[
=============================================================================================================================================================
Complementary Actor Template
Functions required for units, items, and destructables CAT
=============================================================================================================================================================
]]
CAT_ExceptionList = {}
CAT_WidgetIdRadius = {}
CAT_WidgetIdIsStationary = {}
CAT_WidgetIdCellCheckInterval = {}
---@param whichString string
---@return string
function CAT_ToCamelCase(whichString)
whichString = whichString:gsub("(\x25s)(\x25a)", function(__, letter) return letter:upper() end)
return string.lower(string.sub(whichString,1,1)) .. string.sub(whichString,2)
end
---@param widgetId integer
---@param whichList table
---@return boolean
function CAT_IsWidgetIdInList(widgetId, whichList)
for __, v in ipairs(whichList) do
if v == widgetId then
return true
end
end
return false
end
---@param widgetId integer
function CAT_AddException(widgetId)
if not CAT_IsWidgetIdInList(widgetId, CAT_ExceptionList) then
table.insert(CAT_ExceptionList, widgetId)
end
end
---@param widgetId integer
---@param radius number
function CAT_SetWidgetIdRadius(widgetId, radius)
CAT_WidgetIdRadius[widgetId] = radius
end
---@param widgetId integer
---@param interval number
function CAT_SetWidgetCellCheckInterval(widgetId, interval)
CAT_WidgetIdCellCheckInterval[widgetId] = interval
end
---@param widgetId integer
function CAT_SetWidgetIdStationary(widgetId)
CAT_WidgetIdIsStationary[widgetId] = true
end
OnInit.global("CAT_Widgets", function() end)
end
if Debug then Debug.beginFile "CAT Units" end
do
--[[
=============================================================================================================================================================
Complementary ALICE Template
by Antares
Requires:
ALICE https://www.hiveworkshop.com/threads/a-l-i-c-e-interaction-engine.353126/
Widgets CAT
TotalInitialization https://www.hiveworkshop.com/threads/total-initialization.317099/
Hook https://www.hiveworkshop.com/threads/hook.339153/
=============================================================================================================================================================
U N I T S
=============================================================================================================================================================
This template automatically creates an actor for each unit that enters the map (units with Locust are excluded). A unit actor will get the identifiers "CAT",
"unit" and "<unitName>" (transformed to camelCase formatting to be consistent with other identifiers), "hero"/"nonhero", and "flying/ground". You can choose to
add unit classifications to the identifiers.
These actors will not initiate any pairs themselves. They will be destroyed on the unit's death.
You can add exceptions with CAT_AddException(unitId). Units with ids that were added to the exception list will not get an actor.
Also contains a function to retrieve a unit's current velocity. This is used in the Collisions CAT.
=============================================================================================================================================================
C O N F I G
=============================================================================================================================================================
]]
local IGNORE_UNIT_UNLESS_IN_LIST = false ---@type boolean
--If set to true, no actor will be created unless it has been enabled for that unit type with CAT_AddUnitException.
local ADD_UNIT_CLASSIFICATIONS = false ---@type boolean
--Add identifiers such as "mechanical"/"nonmechanical" to the actor.
local MONITOR_UNIT_VELOCITY = true ---@type boolean
--Periodically check each unit's position so that the unit's velocity can be retrieved for collisions.
--[[
=============================================================================================================================================================
A P I
=============================================================================================================================================================
CAT_AddUnitActor(whichUnit)
CAT_AddException(unitId)
CAT_SetWidgetIdRadius(unitId, radius)
CAT_SetWidgetCellCheckInterval(unitId, interval)
CAT_SetWidgetIdStationary(unitId)
=============================================================================================================================================================
]]
local actorFlags = {}
local identifiers = {}
local interactionFunc = {}
local OnRevive = nil
local objectsCAT = nil
local monitorUnitX = {}
local monitorUnitY = {}
local monitorUnitZ = {}
CAT_UnitVelocityX = {}
CAT_UnitVelocityY = {}
CAT_UnitVelocityZ = {}
local INTERVAL = (0.1 // ALICE_MIN_INTERVAL)*ALICE_MIN_INTERVAL
local INV_INTERVAL = 1/INTERVAL
local function MonitorUnitVelocityOnDestroy(unit, __, __)
monitorUnitX[unit], monitorUnitY[unit], monitorUnitZ[unit] = nil, nil, nil
CAT_UnitVelocityX[unit], CAT_UnitVelocityY[unit], CAT_UnitVelocityZ[unit] = nil, nil, nil
end
local function InitMonitorUnitVelocity(unit)
monitorUnitX[unit], monitorUnitY[unit], monitorUnitZ[unit] = GetUnitCoordinates(unit)
CAT_UnitVelocityX[unit], CAT_UnitVelocityY[unit], CAT_UnitVelocityZ[unit] = 0, 0, 0
ALICE_PairOnDestroy(MonitorUnitVelocityOnDestroy)
end
function CAT_MonitorUnitVelocity(unit)
local x, y, z = GetUnitCoordinates(unit)
CAT_UnitVelocityX[unit], CAT_UnitVelocityY[unit], CAT_UnitVelocityZ[unit] = (x - monitorUnitX[unit])*INV_INTERVAL, (y - monitorUnitY[unit])*INV_INTERVAL, (z - monitorUnitZ[unit])*INV_INTERVAL
monitorUnitX[unit], monitorUnitY[unit], monitorUnitZ[unit] = x, y, z
return INTERVAL
end
---@param u unit
---@param ignoreExceptions boolean
local function CreateUnitActor(u, ignoreExceptions)
local id = GetUnitTypeId(u)
if not ignoreExceptions then
if GetUnitAbilityLevel(u, FourCC("Aloc")) > 0 then --Locust
return
end
if CAT_IsWidgetIdInList(id, CAT_ExceptionList) ~= IGNORE_UNIT_UNLESS_IN_LIST then
return
end
end
for key, __ in pairs(identifiers) do
identifiers[key] = nil
end
table.insert(identifiers, "CAT")
table.insert(identifiers, "unit")
table.insert(identifiers, CAT_ToCamelCase(GetUnitName(u)))
if IsUnitType(u, UNIT_TYPE_HERO) then
table.insert(identifiers, "hero")
else
table.insert(identifiers, "nonhero")
end
if IsUnitType(u, UNIT_TYPE_FLYING) then
table.insert(identifiers, "flying")
else
table.insert(identifiers, "ground")
end
if ADD_UNIT_CLASSIFICATIONS then
if IsUnitType(u, UNIT_TYPE_UNDEAD) then
table.insert(identifiers, "undead")
else
table.insert(identifiers, "nonundead")
end
if IsUnitType(u, UNIT_TYPE_MECHANICAL) then
table.insert(identifiers, "mechanical")
else
table.insert(identifiers, "nonmechanical")
end
if IsUnitType(u, UNIT_TYPE_STRUCTURE) then
table.insert(identifiers, "structure")
else
table.insert(identifiers, "nonstructure")
end
if IsUnitType(u, UNIT_TYPE_ANCIENT) then
table.insert(identifiers, "ancient")
else
table.insert(identifiers, "nonancient")
end
if IsUnitType(u, UNIT_TYPE_SAPPER) then
table.insert(identifiers, "sapper")
else
table.insert(identifiers, "nonsapper")
end
end
actorFlags.isStationary = CAT_WidgetIdIsStationary[id] or (CAT_WidgetIdIsStationary[id] == nil and IsUnitType(u, UNIT_TYPE_STRUCTURE))
actorFlags.radius = CAT_WidgetIdRadius[id]
actorFlags.cellCheckInterval = CAT_WidgetIdCellCheckInterval[id]
if MONITOR_UNIT_VELOCITY and not actorFlags.isStationary then
interactionFunc.self = CAT_MonitorUnitVelocity
else
interactionFunc.self = nil
end
--Objects CAT
if objectsCAT then
local height = WidgetIdHeight[id] or DEFAULT_UNIT_HEIGHT_FACTOR*BlzGetUnitCollisionSize(u)
if height then
actorFlags.zOffset = height/2
else
actorFlags.zOffset = nil
end
else
actorFlags.zOffset = nil
end
local trig = CreateTrigger()
TriggerAddAction(trig, OnRevive)
TriggerRegisterUnitStateEvent(trig, u, UNIT_STATE_LIFE, GREATER_THAN_OR_EQUAL, 0.405)
ALICE_Create(u, identifiers, interactionFunc, actorFlags)
end
local function CorpseCleanUp(u)
if GetUnitTypeId(u) == 0 then
ALICE_Destroy(u, "corpse")
end
return 1.0
end
OnRevive = function()
local u = GetTriggerUnit()
ALICE_SwapIdentifier(u, "corpse", "unit", "CAT")
ALICE_RemoveSelfInteraction(u, CorpseCleanUp, "CAT")
end
local function OnUnitDeath()
local u = GetTriggerUnit()
if not ALICE_HasActor(u, "CAT") then
return
end
ALICE_SwapIdentifier(u, "unit", "corpse", "CAT")
ALICE_AddSelfInteraction(u, CorpseCleanUp, "CAT")
end
local function OnUnitEnter()
CreateUnitActor(GetTriggerUnit(), false)
end
function Hook:RemoveUnit(whichUnit)
ALICE_Destroy(whichUnit)
self.old(whichUnit)
end
local function InitUnitsCAT()
Require "ALICE"
Require "CAT_Widgets"
objectsCAT = Require.optionally "CAT_Objects"
ALICE_FuncSetInit(CAT_MonitorUnitVelocity, InitMonitorUnitVelocity)
ALICE_FuncSetRandomDelay(CAT_MonitorUnitVelocity, 0.1)
local reg = CreateRegion()
RegionAddRect(reg, GetWorldBounds())
local trig = CreateTrigger()
TriggerRegisterEnterRegion(trig, reg, nil)
TriggerRegisterAnyUnitEventBJ(trig, EVENT_PLAYER_HERO_REVIVE_FINISH)
TriggerAddAction(trig, OnUnitEnter)
trig = CreateTrigger()
TriggerRegisterAnyUnitEventBJ(trig, EVENT_PLAYER_UNIT_DEATH)
TriggerAddAction(trig, OnUnitDeath)
local G = CreateGroup()
GroupEnumUnitsInRect(G, GetPlayableMapRect(), nil)
ForGroup(G, function() CreateUnitActor(GetEnumUnit(), false) end)
DestroyGroup(G)
end
OnInit.global("CAT_Units", InitUnitsCAT)
--===========================================================================================================================================================
---@param unitId integer
function CAT_AddUnitException(unitId)
if not CAT_IsWidgetIdInList(unitId, CAT_ExceptionList) then
table.insert(CAT_ExceptionList, unitId)
end
end
---@param whichUnit unit
function CAT_AddUnitActor(whichUnit)
CreateUnitActor(whichUnit, true)
end
end
if Debug then Debug.beginFile "CAT Destructables" end
do
--[[
=============================================================================================================================================================
Complementary ALICE Template
by Antares
Requires:
ALICE https://www.hiveworkshop.com/threads/a-l-i-c-e-interaction-engine.353126/
Widgets CAT
TotalInitialization https://www.hiveworkshop.com/threads/total-initialization.317099/
Hook https://www.hiveworkshop.com/threads/hook.339153/
=============================================================================================================================================================
D E S T R U C T A B L E S
=============================================================================================================================================================
This template automatically creates an actor for each destructable. A destructable actor will get the identifiers "destructable" and <destructableName>
(transformed to camelCase formatting to be consistent with other identifiers). CAT also searches for "Tree", "Rock", "Gate", "Bridge", and "Blocker" in its
name and adds those identifiers if it finds them.
These actors will not initiate any pairs themselves. They will be destroyed on the destructable's death.
You can add exceptions with CAT_AddException(destructableId). Destructables with ids that were added to the exception list will not get an actor.
Limitations: Revived destructables do not get an actor.
=============================================================================================================================================================
C O N F I G
=============================================================================================================================================================
]]
local IGNORE_DESTRUCTABLE_UNLESS_IN_LIST = false ---@type boolean
--If set to true, no actor will be created unless it has been enabled for that destructable type with CAT_AddException.
--[[
=============================================================================================================================================================
A P I
=============================================================================================================================================================
CAT_AddDestructableActor(whichDestructable)
CAT_AddException(destructableId)
CAT_SetWidgetIdRadius(destructableId, radius)
--===========================================================================================================================================================
]]
local actorFlags = {destroyOnDeath = true}
local identifiers = {}
local objectsCAT = nil
---@param d destructable
---@param ignoreExceptions boolean
local function CreateDestructableActor(d, ignoreExceptions)
local id = GetDestructableTypeId(d)
if not ignoreExceptions then
if CAT_IsWidgetIdInList(id, CAT_ExceptionList) ~= IGNORE_DESTRUCTABLE_UNLESS_IN_LIST then
return
end
end
for key, __ in pairs(identifiers) do
identifiers[key] = nil
end
table.insert(identifiers, "CAT")
table.insert(identifiers, "destructable")
local name = GetDestructableName(d)
table.insert(identifiers, CAT_ToCamelCase(name))
if string.find(name, "Tree") then
table.insert(identifiers, "tree")
elseif string.find(name, "Rock") then
table.insert(identifiers, "rock")
elseif string.find(name, "Blocker") then
table.insert(identifiers, "blocker")
elseif string.find(name, "Gate") then
table.insert(identifiers, "gate")
elseif string.find(name, "Bridge") then
table.insert(identifiers, "bridge")
end
--Objects CAT
if objectsCAT then
local height = WidgetIdHeight[id] or DEFAULT_DESTRUCTABLE_HEIGHT
if height then
actorFlags.zOffset = height/2
else
actorFlags.zOffset = nil
end
else
actorFlags.zOffset = nil
end
actorFlags.radius = CAT_WidgetIdRadius[id]
ALICE_Create(d, identifiers, nil, actorFlags)
end
if Hook then
function Hook:CreateDestructable(...)
local newDestructable
newDestructable = self.old(...)
CreateDestructableActor(newDestructable, false)
return newDestructable
end
function Hook:CreateDestructableZ(...)
local newDestructable
newDestructable = self.old(...)
CreateDestructableActor(newDestructable, false)
return newDestructable
end
function Hook:BlzCreateDestructableWithSkin(...)
local newDestructable
newDestructable = self.old(...)
CreateDestructableActor(newDestructable, false)
return newDestructable
end
function Hook:BlzCreateDestructableZWithSkin(...)
local newDestructable
newDestructable = self.old(...)
CreateDestructableActor(newDestructable, false)
return newDestructable
end
end
local function InitDestructablesCAT()
Require "ALICE"
Require "CAT_Widgets"
objectsCAT = Require.optionally "CAT_Objects"
EnumDestructablesInRect(GetWorldBounds(), nil, function() CreateDestructableActor(GetEnumDestructable(), false) end)
end
OnInit.global("CAT_Destructables", InitDestructablesCAT)
---@param whichDestructable destructable
function CAT_AddDestructableActor(whichDestructable)
CreateDestructableActor(whichDestructable, true)
end
end
if Debug then Debug.beginFile "CAT Items" end
do
--[[
=============================================================================================================================================================
Complementary ALICE Template
by Antares
Requires:
ALICE https://www.hiveworkshop.com/threads/a-l-i-c-e-interaction-engine.353126/
Widgets CAT
TotalInitialization https://www.hiveworkshop.com/threads/total-initialization.317099/
Hook https://www.hiveworkshop.com/threads/hook.339153/
=============================================================================================================================================================
I T E M S
=============================================================================================================================================================
This template automatically creates an actor for each item. An item actor will get the identifiers "item" and <itemName> (transformed to camelCase formatting
to be consistent with other identifiers).
These actors will not initiate any pairs themselves. They will be destroyed on the item's death.
You can add exceptions with CAT_AddException(itemId). Items with ids that were added to the exception list will not get an actor.
=============================================================================================================================================================
C O N F I G
=============================================================================================================================================================
]]
local IGNORE_ITEM_UNLESS_IN_LIST = false ---@type boolean
--If set to true, no actor will be created unless it has been enabled for that item type with CAT_AddException.
--[[
=============================================================================================================================================================
A P I
=============================================================================================================================================================
CAT_AddItemActor(whichItem)
CAT_AddException(itemId)
CAT_SetWidgetIdRadius(itemId, radius)
CAT_SetWidgetCellCheckInterval(itemId, interval)
--===========================================================================================================================================================
]]
local actorFlags = {destroyOnDeath = true}
local identifiers = {}
local objectsCAT = nil
---@param i item
---@param ignoreExceptions boolean
local function CreateItemActor(i, ignoreExceptions)
local id = GetItemTypeId(i)
if not ignoreExceptions then
if CAT_IsWidgetIdInList(id, CAT_ExceptionList) ~= IGNORE_ITEM_UNLESS_IN_LIST then
return
end
end
if id == FourCC("Ikno") then --knockback item from collisions CAT.
return
end
for key, __ in pairs(identifiers) do
identifiers[key] = nil
end
table.insert(identifiers, "CAT")
table.insert(identifiers, "item")
local name = GetItemName(i)
table.insert(identifiers, CAT_ToCamelCase(name))
actorFlags.radius = CAT_WidgetIdRadius[id]
--Objects CAT
if objectsCAT then
local height = WidgetIdHeight[id] or DEFAULT_ITEM_HEIGHT
if height then
actorFlags.zOffset = height/2
else
actorFlags.zOffset = nil
end
else
actorFlags.zOffset = nil
end
ALICE_Create(i, identifiers, nil, actorFlags)
end
if Hook then
function Hook:CreateItem(...)
local newItem
newItem = self.old(...)
CreateItemActor(newItem, false)
return newItem
end
end
local function OnItemPickup()
ALICE_Destroy(GetManipulatedItem(), "CAT")
end
local function OnItemDrop()
CreateItemActor(GetManipulatedItem(), false)
end
function InitItemsCAT()
Require "ALICE"
Require "CAT_Widgets"
objectsCAT = Require.optionally "CAT_Objects"
EnumItemsInRect(GetPlayableMapRect(), nil, function() CreateItemActor(GetEnumItem(), false) end)
local trig = CreateTrigger()
TriggerRegisterAnyUnitEventBJ(trig, EVENT_PLAYER_UNIT_DROP_ITEM)
TriggerAddAction(trig, OnItemDrop)
trig = CreateTrigger()
TriggerRegisterAnyUnitEventBJ(trig, EVENT_PLAYER_UNIT_PICKUP_ITEM)
TriggerAddAction(trig, OnItemPickup)
end
OnInit.global("CAT_Items", InitItemsCAT)
---@param whichItem item
function CAT_AddItemActor(whichItem)
CreateItemActor(whichItem, true)
end
end
if Debug then Debug.beginFile "CAT Objects" end
do
--[[
=============================================================================================================================================================
Complementary ALICE Template
ALICE https://www.hiveworkshop.com/threads/a-l-i-c-e-interaction-engine.353126/
TotalInitialization https://www.hiveworkshop.com/threads/total-initialization.317099/
Knockback Item ('Ikno') Required for unit knockback (optional).
=============================================================================================================================================================
O B J E C T S
=============================================================================================================================================================
This template contains functions and config variables required for other CATs. For many config parameters, CATs other than the one they primarily affect need
access to them, so they are put here as globals.
=============================================================================================================================================================
C O N F I G
=============================================================================================================================================================
]]
-------------------------------------------------------------------------------------------------------------------------------------------------------------
--For Entities
DEFAULT_ENTITY_MAX_SPEED = 1000 ---@type number
--The maximum speed that an entity can reasonably achieve to determine the frequency with which collision checks must be performed. Can be overwritten with the
--.maxSpeed field in an entity's class table.
ENTITY_MAXIMUM_Z = 3000 ---@type number
ENTITY_MINIMUM_Z = -2000 ---@type number
--Sets the vertical bounds for the kill trigger in CAT_OutOfBoundsCheck.
-------------------------------------------------------------------------------------------------------------------------------------------------------------
--For Ballistics
STATIC_FRICTION_FACTOR = 5 ---@type number
--The friction increase for a sliding object when it comes to rest. High value can prevent jittering in tightly packed collisions.
MINIMUM_BOUNCE_VELOCITY = 5 ---@type number
--If an airborne object would bounce off the surface with this much or less speed, it will become ground-bound instead.
GRAVITY = 800 ---@type number
--Strength of gravitational acceleration.
MAX_SLIDING_CURVATURE_RADIUS = 80 ---@type number
--Determines how curved a surface must be before an object can no longer slide across it but is instead reflected as though it hit a wall. Higher value is
--less forgiving.
-------------------------------------------------------------------------------------------------------------------------------------------------------------
--For Collisions
DEFAULT_UNIT_HEIGHT_FACTOR = 4.0 ---@type number
DEFAULT_UNIT_ELASTICITY = 0.5 ---@type number
DEFAULT_UNIT_MASS = 5 ---@type number
DEFAULT_UNIT_FRICTION = 350 ---@type number
DEFAULT_DESTRUCTABLE_RADIUS = 64 ---@type number
DEFAULT_DESTRUCTABLE_HEIGHT = 250 ---@type number
DEFAULT_DESTRUCTABLE_ELASTICITY = 0.2 ---@type number
DEFAULT_ITEM_RADIUS = 40 ---@type number
DEFAULT_ITEM_HEIGHT = 40 ---@type number
DEFAULT_ITEM_ELASTICITY = 0.2 ---@type number
UnitIdMass = {} ---@type number[]
UnitIdFriction = {} ---@type number[]
WidgetIdRadius = {} ---@type number[]
WidgetIdHeight = {} ---@type number[]
WidgetIdElasticity = {} ---@type number[]
-------------------------------------------------------------------------------------------------------------------------------------------------------------
--Terrain Properties
DIFFERENT_SURFACE_FRICTIONS = false ---@type boolean
--You can adjust the friction of different terrain types by editing the TerrainTypeFriction table. Disable if not needed to improve performance.
TerrainTypeFriction = {} ---@type number[]
TerrainTypeElasticity = {} ---@type number[]
--===========================================================================================================================================================
--Overwrite the default values for different widget types and terrain types like this:
--TerrainTypeFriction[FourCC('Ydtr')] = 1.5
--UnitIdMass[FourCC('hfoo')] = 3
--[[
=============================================================================================================================================================
L I S T O F F U N C T I O N S
=============================================================================================================================================================
CAT_Knockback(whichUnit, vx, vy, vz) Applies a knockback to the specified unit. NOTE: 3D knockbacks are not yet implemented!
CAT_GetObjectVelocity3D(whichObject) Returns the x, y, and z-velocity of the specified object. Accepts widgets and entities.
CAT_GetObjectMass(whichObject) Returns the mass of the specified object. Accepts widgets and entities.
=============================================================================================================================================================
]]
local INF = math.huge
local sqrt = math.sqrt
local max = math.max
local cos = math.cos
local sin = math.sin
local atan2 = math.atan
local knockbackItem = nil ---@type item
local knockbackSpeed = {} ---@type table[]
local UNIT_MAX_SPEED = 522
local UNIT_MAX_SPEED_SQUARED = UNIT_MAX_SPEED^2
local WORLD_MIN_X = nil ---@type number
local WORLD_MIN_Y = nil ---@type number
local function ApplyKnockbackOnDestroy(u, __, __)
knockbackSpeed[u] = nil
end
local function InitApplyKnockback(u)
local data = ALICE_PairLoadData()
data.friction = (UnitIdFriction[GetUnitTypeId(u)] or DEFAULT_UNIT_FRICTION)
ALICE_PairOnDestroy(ApplyKnockbackOnDestroy)
end
function CAT_ApplyKnockback(u)
local data = ALICE_PairLoadData()
local xi
local yi
local phi = atan2(knockbackSpeed[u].y, knockbackSpeed[u].x)
local x = GetUnitX(u) + knockbackSpeed[u].x*INTERVAL
local y = GetUnitY(u) + knockbackSpeed[u].y*INTERVAL
if knockbackItem then
SetItemPosition(knockbackItem, x, y)
xi = GetItemX(knockbackItem)
yi = GetItemY(knockbackItem)
if not IsTerrainPathable(x , y , PATHING_TYPE_WALKABILITY) and x - xi < 1 and x - xi > -1 and y - yi < 1 and y - yi > -1 then
SetUnitX(u, x)
SetUnitY(u, y)
else
knockbackSpeed[u].x = 0
knockbackSpeed[u].y = 0
ALICE_PairPause()
return
end
SetItemPosition(knockbackItem, WORLD_MIN_X, WORLD_MIN_Y)
else
SetUnitX(u, x)
SetUnitY(u, y)
end
local velocity = sqrt(knockbackSpeed[u].x^2 + knockbackSpeed[u].y^2)
velocity = max(0, velocity - data.friction*(TerrainTypeFriction[GetTerrainType(x, y)] or 1)*INTERVAL)
knockbackSpeed[u].x = velocity*cos(phi)
knockbackSpeed[u].y = velocity*sin(phi)
if velocity == 0 then
ALICE_PairPause()
end
end
---@param u unit
---@param vx number
---@param vy number
---@param vz number
function CAT_Knockback(u, vx, vy, vz)
if not knockbackSpeed[u] then
knockbackSpeed[u] = {}
ALICE_AddSelfInteraction(u, CAT_ApplyKnockback, "CAT")
knockbackSpeed[u].x = 0
knockbackSpeed[u].y = 0
knockbackSpeed[u].z = 0
else
ALICE_Unpause(u, "CAT")
end
knockbackSpeed[u].x = (knockbackSpeed[u].x) + vx
knockbackSpeed[u].y = (knockbackSpeed[u].y) + vy
end
---@param object unit | destructable | item | table
---@return number
function CAT_GetObjectMass(object)
if type(object) == "table" then
if object.mass then
return object.mass
elseif object.anchor then
if type(object.anchor) == "table" then
return object.anchor.mass
elseif WidgetIsUnit(object.anchor) then
return UnitIdMass[GetUnitTypeId(object.anchor)] or DEFAULT_UNIT_MASS
else
return INF
end
else
return 0
end
elseif WidgetIsUnit(object) then
return UnitIdMass[GetUnitTypeId(object)] or DEFAULT_UNIT_MASS
else
return INF
end
end
---@param unit unit
---@return number, number, number
local function GetUnitVelocity3D(unit)
if CAT_UnitVelocityX[unit] then --Units CAT
local totalSpeedSquared = CAT_UnitVelocityX[unit]^2 + CAT_UnitVelocityY[unit]^2 + CAT_UnitVelocityZ[unit]^2
--If unit could not have traveled that fast naturally, it was most likely a teleport. Then, do not use velocity.
if totalSpeedSquared > UNIT_MAX_SPEED_SQUARED then
local totalSpeed = sqrt(totalSpeedSquared)
if knockbackSpeed[unit] then --Collisions CAT
local kbSpeed = knockbackSpeed[unit].x^2 + knockbackSpeed[unit].y^2 + knockbackSpeed[unit].z^2
if totalSpeed > kbSpeed + UNIT_MAX_SPEED then
return knockbackSpeed[unit].x, knockbackSpeed[unit].y, knockbackSpeed[unit].z
else
return CAT_UnitVelocityX[unit], CAT_UnitVelocityY[unit], CAT_UnitVelocityZ[unit]
end
else
return 0, 0, 0
end
else
return CAT_UnitVelocityX[unit], CAT_UnitVelocityY[unit], CAT_UnitVelocityZ[unit]
end
elseif knockbackSpeed[unit] then
return knockbackSpeed[unit].x, knockbackSpeed[unit].y, knockbackSpeed[unit].z
else
return 0, 0, 0
end
end
---@param object unit | destructable | item | table
---@return number, number, number
function CAT_GetObjectVelocity3D(object)
if type(object) == "table" then
if object.vx then
return object.vx, object.vy, object.vz
elseif object.anchor then
if type(object.anchor) == "table" then
return object.anchor.vx, object.anchor.vy, object.anchor.vz
elseif WidgetIsUnit(object.anchor) then
return GetUnitVelocity3D(object.anchor)
else
return 0, 0, 0
end
else
return 0, 0, 0
end
else
local type = GetWidgetType(object)
if type == "unit" then
return GetUnitVelocity3D(object)
else
return 0, 0, 0
end
end
end
function CAT_LaunchOffset(entity)
if entity.vz then
local vTotal = sqrt(entity.vx^2 + entity.vy^2 + entity.vz^2)
entity.x = entity.x + entity.launchOffset*entity.vx/vTotal
entity.y = entity.y + entity.launchOffset*entity.vy/vTotal
entity.z = entity.z + entity.launchOffset*entity.vz/vTotal
if entity[VISUAL] then
BlzSetSpecialEffectPosition(entity[VISUAL], entity.x, entity.y, entity.z)
end
else
local vTotal = sqrt(entity.vx^2 + entity.vy^2)
entity.x = entity.x + entity.launchOffset*entity.vx/vTotal
entity.y = entity.y + entity.launchOffset*entity.vy/vTotal
if entity[VISUAL] then
BlzSetSpecialEffectX(entity[VISUAL], entity.x)
BlzSetSpecialEffectY(entity[VISUAL], entity.y)
end
end
end
OnInit.global("CAT_Objects", function()
local worldBounds = GetWorldBounds()
WORLD_MIN_X = GetRectMinX(worldBounds)
WORLD_MIN_Y = GetRectMinY(worldBounds)
knockbackItem = CreateItem(FourCC("Ikno"), WORLD_MIN_X, WORLD_MIN_Y)
ALICE_FuncSetInit(CAT_ApplyKnockback, InitApplyKnockback)
end)
end
if Debug then Debug.beginFile "CAT Entities" end
do
--[[
===============================================================================================================================================================================
Complementary ALICE Template
by Antares
Requires:
ALICE https://www.hiveworkshop.com/threads/a-l-i-c-e-interaction-engine.353126/
PrecomputedHeightMap (optional) https://www.hiveworkshop.com/threads/precomputed-synchronized-terrain-height-map.353477/
TotalInitialization https://www.hiveworkshop.com/threads/total-initialization.317099/
===============================================================================================================================================================================
E N T I T I E S
===============================================================================================================================================================================
This template contains several functions that help with creating and managing entities*. It also includes the terrain collision check function.
*Objects represented by a table with coordinate fields .x, .y, .z, velocity fields .vx, .vy, .vz, and a special effect (field name set in ALICE, default .visual).
How to create an entity:
Create a class that contains coordinate and velocity fields. If you want 3D movement, you need to include z-coordinates and z-velocity, otherwise x and y suffice. Also create
the special effect field in your class that represents the visual of your entity.
To interface the entities with the functions in this template, add self-interactions to your entities. Function parameters are customized by editing table fields of your entity.
Some parameters are mutable, others are not. In many cases, you can easily alter the implementation of a function to make a parameter mutable.
Example:
Football = {
x = nil,
y = nil,
z = nil,
vx = nil,
vy = nil,
vz = nil,
visual = nil,
--ALICE
identifier = "football",
interactionFunc = {
football = CAT_CollisionCheck3D,
self = {
CAT_MoveBallistic,
CAT_CheckTerrainCollision
}
},
--CAT
onTerrainCollision = CAT_OnTerrainCollisionBounce,
onTerrainCallback = BounceFootball,
onEntityCollision = CAT_EntityBounce3D,
friction = 100,
elasticity = 0.7,
collisionRadius = 35
}
Football.__index = Football
function Football.create(x, y, z, vx, vy, vz)
local new = {}
setmetatable(new, Football)
new.x, new.y, new.z = x, y, z
new.vx, new.vy, new.vz = vx, vy, vz
new.visual = AddSpecialEffect(effectPath, x, y)
BlzSetSpecialEffectZ(effectPath, z)
ALICE_CreateFromClass(new)
end
===============================================================================================================================================================================
L I S T O F F U N C T I O N S
===============================================================================================================================================================================
CAT_Decay Makes your entity get destroyed after X seconds, where X is set through the .lifetime field of your entity table.
With the optional .onExpire field, you can set a callback function that is invoked when the entity decays.
CAT_CheckTerrainCollision When enabled, checks for terrain collision and invokes a callback function upon collision. The callback function is
retrieved from the .onTerrainCollision field. It is called with the parameters
(entity, normalSpeed, tangentialSpeed, totalSpeed). If the onTerrainCollision field is empty, ALICE_Kill will be
invoked instead. Requires the .collisionRadius field of your table to be set.
CAT_TerrainBounce A function for onTerrainCollision. Uses the .elasticity field to determine the elasticity of the collision. This
value is multiplied by the TerrainTypeElasticity, which can be set in the Objects CAT.
CAT_OutOfBoundsCheck Checks if the entity has left the playable map area and destroys it if it does. You can set the .reflectsOnBounds
field of your entity table to true to make it reflect on the bounds instead of being destroyed. Uses the .maxSpeed
field to determine how often the out-of-bounds check is performed, or DEFAULT_ENTITY_MAX_SPEED if not set.
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
CAT_SetEntityBounds(xMin, yMin, xMax, yMax) Change the entity bounds for CAT_OutOfBoundsCheck. The default bounds are the world bounds. z-bounds are set in the
config.
CAT_GlobalReplace(widgetId, widgetType, constructorFunc) A function that allows you to place widgets using the World Editor and replace them on map initialization with
entities. The constructorFunc is the function creating your entity class. It will be called with the parameters
x, y, z, life, mana, owner, facing. The last three parameters are only available if the replaced widget is a unit.
===============================================================================================================================================================================
]]
local sqrt = math.sqrt
local min = math.min
local atan2 = math.atan
local INTERVAL = nil ---@type number
local entityBoundMinX = nil ---@type number
local entityBoundMinY = nil ---@type number
local entityBoundMaxX = nil ---@type number
local entityBoundMaxY = nil ---@type number
--=============================================================================================================================================================================
function CAT_Decay(entity)
entity.lifetime = entity.lifetime - INTERVAL
if entity.lifetime <= 0 then
if entity.onExpire then
entity:onExpire()
end
ALICE_Kill(entity)
end
end
---@param class table
---@param x number
---@param y number
function CAT_TerrainBounce(class, x, y)
--Get normal vector of surface.
local z_x1 = GetCliffAdjustedZ(x - 16, y)
local z_x2 = GetCliffAdjustedZ(x + 16, y)
local z_y1 = GetCliffAdjustedZ(x, y - 16)
local z_y2 = GetCliffAdjustedZ(x, y + 16)
local dz_x = (z_x2 - z_x1)/32
local dz_y = (z_y2 - z_y1)/32
local vec1_z = dz_x
local vec2_z = dz_y
local vecN_x = -vec1_z
local vecN_y = -vec2_z
local vecN_z = 1
local norm = sqrt(vecN_x^2 + vecN_y^2 + vecN_z^2)
vecN_x = vecN_x/norm
vecN_y = vecN_y/norm
vecN_z = vecN_z/norm
--object is coming from below the ground.
local normalSpeed = -(vecN_x*class.vx + vecN_y*class.vy + vecN_z*class.vz)
if normalSpeed < 0 then
return
end
local e = (1 + class.elasticity*(TerrainTypeElasticity[GetTerrainType(x, y)] or 1))
--Householder transformation.
local H11 = 1 - e*vecN_x^2
local H12 = -e*vecN_x*vecN_y
local H13 = -e*vecN_x*vecN_z
local H21 = H12
local H22 = 1 - e*vecN_y^2
local H23 = -e*vecN_y*vecN_z
local H31 = H13
local H32 = H23
local H33 = 1 - e*vecN_z^2
if class.onTerrainCallback ~= nil then
local totalSpeed = sqrt(class.vx^2 + class.vy^2 + class.vz^2)
local tangentialSpeed = sqrt(totalSpeed^2 - normalSpeed^2)
class:onTerrainCallback(normalSpeed, tangentialSpeed, totalSpeed)
end
local newvx = H11*class.vx + H12*class.vy + H13*class.vz
local newvy = H21*class.vx + H22*class.vy + H23*class.vz
local newvz = H31*class.vx + H32*class.vy + H33*class.vz
class.vx, class.vy, class.vz = newvx, newvy, newvz
class.vzOld = class.vz
class.hasCollided = true
if vecN_x*class.vx + vecN_y*class.vy + vecN_z*class.vz < MINIMUM_BOUNCE_VELOCITY then
class.theta = atan2(class.vz, sqrt(class.vx^2 + class.vy^2))
class.isAirborne = false
end
end
function CAT_CheckTerrainCollision(entity)
if entity.isAirborne == false then
ALICE_PairPause()
end
local height = GetCliffAdjustedZ(entity.x, entity.y)
local dist = entity.z - height - entity.collisionRadius
local vHorizontal = sqrt(entity.vx^2 + entity.vy^2)
if dist < 0 then
if entity.onTerrainCollision then
entity:onTerrainCollision(entity.x, entity.y)
else
ALICE_Kill(entity)
end
return INTERVAL
elseif vHorizontal - entity.vz < 200 then
return dist/600
else
return dist/(3*(vHorizontal - entity.vz))
end
end
---@param entity table
---@param isVertical boolean
---@param coord number
local function ReflectOnBounds(entity, isVertical, coord)
if isVertical then
entity.vy = -entity.vy
entity.y = 2*coord - entity.y
else
entity.vx = -entity.vx
entity.x = 2*coord - entity.x
end
end
function CAT_OutOfBoundsCheck(entity)
local nearestDist
if entity.x < entityBoundMinX then
if entity.reflectOnBounds then
ReflectOnBounds(entity, false, entityBoundMinX)
else
ALICE_Kill(entity)
end
return 0
else
nearestDist = entity.x - entityBoundMinX
end
if entity.y < entityBoundMinY then
if entity.reflectOnBounds then
ReflectOnBounds(entity, true, entityBoundMinY)
else
ALICE_Kill(entity)
end
return 0
else
nearestDist = min(nearestDist, entity.y - entityBoundMinY)
end
if entity.x > entityBoundMaxX then
if entity.reflectOnBounds then
ReflectOnBounds(entity, false, entityBoundMaxX)
else
ALICE_Kill(entity)
end
return 0
else
nearestDist = min(nearestDist, entityBoundMaxX - entity.x)
end
if entity.y > entityBoundMaxY then
if entity.reflectOnBounds then
ReflectOnBounds(entity, true, entityBoundMaxY)
else
ALICE_Kill(entity)
end
return 0
else
nearestDist = min(nearestDist, entityBoundMaxY - entity.y)
end
if entity.z then
if entity.z > ENTITY_MAXIMUM_Z then
ALICE_Kill(entity)
return 0
end
if entity.z < ENTITY_MINIMUM_Z then
ALICE_Kill(entity)
return 0
end
end
if entity.isResting then
ALICE_PairPause()
end
return nearestDist/(entity.maxSpeed or DEFAULT_ENTITY_MAX_SPEED)
end
---Removes all widgets with the specified widgetId and calls the constructorFunc with the parameters func(x, y, z, life, mana, owner, facing). The last three parameters are nil if the widget is not a unit.
---@param widgetId integer
---@param widgetType string
---@param constructorFunc function
function CAT_GlobalReplace(widgetId, widgetType, constructorFunc)
if widgetType == "unit" then
local G = CreateGroup()
GroupEnumUnitsInRect(G, bj_mapInitialPlayableArea, nil)
local i = 0
local u = BlzGroupUnitAt(G, i)
while u do
if GetUnitTypeId(u) == widgetId then
local x, y = GetUnitX(u), GetUnitY(u)
local z = GetTerrainZ(x, y) + GetUnitFlyHeight(u)
local life, mana = GetUnitState(u, UNIT_STATE_LIFE), GetUnitState(u, UNIT_STATE_MANA)
local owner = GetOwningPlayer(u)
local facing = GetUnitFacing(u)
RemoveUnit(u)
constructorFunc(x, y, z, life, mana, owner, facing)
end
i = i + 1
u = BlzGroupUnitAt(G, i)
end
DestroyGroup(G)
elseif widgetType == "destructable" then
EnumDestructablesInRect(bj_mapInitialPlayableArea, nil, function()
local d = GetEnumDestructable()
if GetDestructableTypeId(d) == widgetId then
local x = GetDestructableX(d)
local y = GetDestructableY(d)
local z = GetTerrainZ(x, y)
local life = GetDestructableLife(d)
RemoveDestructable(d)
constructorFunc(x, y, z, life)
end
end)
elseif widgetType == "item" then
EnumItemsInRect(bj_mapInitialPlayableArea, nil, function()
local i = GetEnumItem()
if GetItemTypeId(i) == widgetId then
local x = GetItemX(i)
local y = GetItemY(i)
local z = GetTerrainZ(x, y)
RemoveItem(i)
constructorFunc(x, y, z)
end
end)
end
end
function CAT_SetEntityBounds(minX, minY, maxX, maxY)
entityBoundMinX = minX
entityBoundMinY = minY
entityBoundMaxX = maxX
entityBoundMaxY = maxY
end
local function InitEntitiesCAT()
Require "ALICE"
local worldBounds = GetWorldBounds()
CAT_SetEntityBounds(GetRectMinX(worldBounds), GetRectMinY(worldBounds), GetRectMaxX(worldBounds), GetRectMaxY(worldBounds))
RemoveRect(worldBounds)
INTERVAL = ALICE_MIN_INTERVAL
end
OnInit.global("CAT_Entities", InitEntitiesCAT)
--===========================================================================================================================================================
end
if Debug then Debug.beginFile "CAT Missiles" end
do
--[[
===============================================================================================================================================================================
Complementary ALICE Template
by Antares
Requires:
ALICE https://www.hiveworkshop.com/threads/a-l-i-c-e-interaction-engine.353126/
Objects CAT
PrecomputedHeightMap https://www.hiveworkshop.com/threads/precomputed-synchronized-terrain-height-map.353477/
TotalInitialization https://www.hiveworkshop.com/threads/total-initialization.317099/
===============================================================================================================================================================================
M I S S I L E S
===============================================================================================================================================================================
This template includes a several functions that move or accelerate entities*, allowing for the creation of missiles or projectiles. Only the ballistic move function is found in
the Ballistics CAT. For an example of how to use a movement functions, see Entities CAT.
*Objects represented by a table with coordinate fields .x, .y, .z, velocity fields .vx, .vy, .vz, and a special effect (field name set in ALICE, default .visual).
===============================================================================================================================================================================
L I S T O F F U N C T I O N S
===============================================================================================================================================================================
β’ All movement functions use the optional .visualZ field. You can use this field to shift the position of the
special effect in z-direction.
β’ All movement functions use the optional .launchOffset field. This shifts the position of the entity by that
much in the direction of the movement.
CAT_MoveAutoHeight Linear movement along the terrain surface.
CAT_Move2D Simplest movement function. Linear movement in the x-y plane.
CAT_Move3D Linear movement in any direction.
CAT_MoveArced The missile moves towards the target location specified by the fields .targetX and .targetY with the velocity .speed.
The .arc field determines how curved the missile's path is. The optional .arcAngle field shifts the direction of the
missile's arc. With an arcAngle of 0, the missile arcs vertically, with 90 or -90, it arcs horizontally. A missile
using this movement function will move towards the location until it collides with something, ignoring all external
effects.
CAT_MoveHoming2D The missile follows a unit specified by the .target field with a velocity .speed. You can limit the turn rate with the
optional .turnRate field. The value is expected to be in degrees per second. The optional .disconnectionDistance field
controls the maximum distance the unit can travel between two updates before the homing breaks. Entities using homing
movement will always move with the same speed, ignoring external effects.
CAT_MoveArcedHoming The same as MoveHoming2D, but movement in three dimensions. The additional .arc field determines how high the
missile flies as it travels towards the unit. Attempts to turn towards the unit will still be only in the x-y-plane.
CAT_MoveHoming3D The same as MoveHoming2D, but movement in three dimensions. The missile will use all three dimensions to find the
lowest turn angle towards the unit.
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
CAT_AccelerateHoming2D Accelerates the entity towards the target unit set with the .target field. The acceleration is set with the
.acceleration field. The optional .deceleration field determines the acceleration the entity experiences when it is
currently not moving towards the entity. Will use acceleration if not set. The optional .maxSpeed field limits the
maximum velocity of the entity in direction of the target. This is not a movement function and must be paired with a
compatible movement function; one that reacts to external effects. Can be combined with CAT_OrientPropelled2D function
(Effects CAT).
CAT_AccelerateHoming3D The same as CAT_AccelerateHoming2D, but in three dimensions.
===============================================================================================================================================================================
C O N F I G
===============================================================================================================================================================================
]]
local sqrt = math.sqrt
local atan2 = math.atan
local cos = math.cos
local sin = math.sin
local acos = math.acos
local exp = math.exp
local PI = bj_PI
local TAU = 2*PI
local INTERVAL = nil ---@type number
local VISUAL = nil ---@type string
local function Cosh(x)
return (exp(x) + exp(-x))/2
end
local function Sinh(x)
return (exp(x) - exp(-x))/2
end
local function InitMoveGeneric(missile)
missile.visualZ = missile.visualZ or 0
if missile.launchOffset then
CAT_LaunchOffset(missile)
end
end
function CAT_MoveAutoHeight(missile)
missile.x = missile.x + missile.vx*INTERVAL
missile.y = missile.y + missile.vy*INTERVAL
missile.z = GetTerrainZ(missile.x, missile.y)
BlzSetSpecialEffectPosition(missile[VISUAL], missile.x, missile.y, missile.z + missile.visualZ)
end
local function InitMove2D(missile)
if missile.visualZ then
BlzSetSpecialEffectZ(missile.visual, missile.visualZ + GetTerrainZ(missile.x, missile.y))
end
missile.visualZ = missile.visualZ or 0
if missile.launchOffset then
CAT_LaunchOffset(missile)
end
end
function CAT_Move2D(missile)
missile.x = missile.x + missile.vx*INTERVAL
missile.y = missile.y + missile.vy*INTERVAL
BlzSetSpecialEffectX(missile[VISUAL], missile.x)
BlzSetSpecialEffectY(missile[VISUAL], missile.y)
end
function CAT_Move3D(missile)
missile.x = missile.x + missile.vx*INTERVAL
missile.y = missile.y + missile.vy*INTERVAL
missile.z = missile.z + missile.vz*INTERVAL
BlzSetSpecialEffectPosition(missile[VISUAL], missile.x, missile.y, missile.z + missile.visualZ)
end
local function InitMoveArced(missile)
missile.launchX = missile.x
missile.launchY = missile.y
missile.launchZ = missile.z
missile.targetZ = missile.targetZ or GetTerrainZ(missile.targetX, missile.targetY)
local dx, dy, dz = missile.targetX - missile.launchX, missile.targetY - missile.launchY, missile.targetZ - missile.launchZ
local dist = sqrt(dx^2 + dy^2 + dz^2)
local angle = acos(dx/dist)
local nx = 0
local ny = -dz
local nz = dy
local norm = sqrt(nx^2 + ny^2 + nz^2)
nx, ny, nz = nx*norm, ny*norm, nz*norm
local cosAngle = cos(angle)
local sinAngle = sin(angle)
local oneMinCos = 1 - cosAngle
missile.R11 = nx*nx*oneMinCos + cosAngle
missile.R12 = nx*ny*oneMinCos - nz*sinAngle
missile.R13 = nx*nz*oneMinCos + ny*sinAngle
missile.R21 = ny*nx*oneMinCos + nz*sinAngle
missile.R22 = ny*ny*oneMinCos + cosAngle
missile.R23 = ny*nz*oneMinCos - nx*sinAngle
missile.R31 = nz*nx*oneMinCos - ny*sinAngle
missile.R32 = nz*ny*oneMinCos + nx*sinAngle
missile.R33 = nz*nz*oneMinCos + cosAngle
missile.cosArcAngle = cos((missile.arcAngle*bj_DEGTORAD) or 0)
missile.sinArcAngle = sin((missile.arcAngle*bj_DEGTORAD) or 0)
missile.coshArc = Cosh(missile.arc)
missile.travelDist = dist
missile.param = -1
missile.dparam = missile.speed*INTERVAL/dist
--Not accurate, simply to prevent crashes on collisions before the first movement step (unlikely).
missile.vx = missile.speed*dy/dist
missile.vy = missile.speed*dy/dist
missile.vz = missile.speed*dz/dist
if missile.launchOffset then
local timeDilation = sqrt(1 + (2*missile.arc*Sinh(missile.param))^2)
missile.param = missile.param + missile.launchOffset/(missile.travelDist*timeDilation)
local xPrime = missile.travelDist*(missile.param + 1)/2
local y = missile.travelDist*(Cosh(missile.arc*missile.param) - missile.coshArc)
local yPrime = y*missile.sinArcAngle
local zPrime = y*missile.cosArcAngle
missile.x = missile.launchX + missile.R11*xPrime + missile.R12*yPrime + missile.R13*zPrime
missile.y = missile.launchY + missile.R21*xPrime + missile.R22*yPrime + missile.R23*zPrime
missile.z = missile.launchZ + missile.R31*xPrime + missile.R32*yPrime + missile.R33*zPrime
if missile[VISUAL] then
BlzSetSpecialEffectPosition(missile[VISUAL], missile.x, missile.y, missile.z)
end
end
end
function CAT_MoveArced(missile)
missile.param = missile.param + missile.dparam/sqrt(1 + (2*missile.arc*Sinh(missile.param))^2)
local xLast, yLast, zLast = missile.x, missile.y, missile.z
local xPrime = missile.travelDist*(missile.param + 1)/2
local y = missile.travelDist*(Cosh(missile.arc*missile.param) - missile.coshArc)
local yPrime = y*missile.sinArcAngle
local zPrime = y*missile.cosArcAngle
missile.x = missile.launchX + missile.R11*xPrime + missile.R12*yPrime + missile.R13*zPrime
missile.y = missile.launchY + missile.R21*xPrime + missile.R22*yPrime + missile.R23*zPrime
missile.z = missile.launchZ + missile.R31*xPrime + missile.R32*yPrime + missile.R33*zPrime
missile.vx = (missile.x - xLast)/INTERVAL
missile.vy = (missile.y - yLast)/INTERVAL
missile.vz = (missile.z - zLast)/INTERVAL
BlzSetSpecialEffectPosition(missile.visual, missile.x, missile.y, missile.z)
end
local function InitMoveHoming2D(missile)
if missile.target == nil then
print("|cffff0000Warning:|r No target set for missile using homing movement...")
end
missile.visualZ = missile.visualZ or 0
missile.turnRate = missile.turnRate or math.huge
missile.turnRateNormalized = bj_DEGTORAD*missile.turnRate*0.1
missile.turnRadius = missile.speed/(bj_DEGTORAD*missile.turnRate)
missile.lastKnownTargetX, missile.lastKnownTargetY = GetUnitX(missile.target), GetUnitY(missile.target)
if missile.vx and missile.vy then
missile.currentAngle = atan2(missile.vy, missile.vx)
else
missile.currentAngle = atan2(GetUnitY(missile.target) - missile.y, GetUnitX(missile.target) - missile.x)
end
if missile.launchOffset then
CAT_LaunchOffset(missile)
end
end
function CAT_MoveHoming2D(missile)
if ALICE_PairCooldown(0.1) then
if missile.disconnectionDistance then
local x, y = GetUnitX(missile.target), GetUnitY(missile.target)
local distSquared = (x - missile.lastKnownTargetX)^2 + (y - missile.lastKnownTargetY)^2
if distSquared < missile.disconnectionDistance^2 then
missile.lastKnownTargetX, missile.lastKnownTargetY = x, y
else
missile.target = nil
end
else
missile.lastKnownTargetX, missile.lastKnownTargetY = GetUnitX(missile.target), GetUnitY(missile.target)
end
local angle = atan2(missile.lastKnownTargetY - missile.y, missile.lastKnownTargetX - missile.x)
local currentAngle = missile.currentAngle
local diff = angle - currentAngle
local absDiff
if diff < 0 then
diff = diff + TAU
elseif diff > TAU then
diff = diff - TAU
end
if diff > PI then
diff = diff - TAU
absDiff = -diff
else
absDiff = diff
end
if absDiff < missile.turnRateNormalized then
currentAngle = angle
elseif diff < 0 then
--Check if target is inside the circle that the missile cannot currently reach. If so, stop turning to gain distance.
local turnLocAngle = currentAngle - PI/2
local turnLocX = missile.x + missile.turnRadius*cos(turnLocAngle)
local turnLocY = missile.y + missile.turnRadius*sin(turnLocAngle)
local targetTurnLocDist = sqrt((turnLocX - missile.lastKnownTargetX)^2 + (turnLocY - missile.lastKnownTargetY)^2)
if targetTurnLocDist > missile.turnRadius or (targetTurnLocDist > 0.9*missile.turnRadius and missile.isTurning) then
missile.isTurning = true
currentAngle = currentAngle - missile.turnRateNormalized
if currentAngle < 0 then
currentAngle = currentAngle + TAU
end
else
missile.isTurning = false
end
else
local turnLocAngle = currentAngle + PI/2
local turnLocX = missile.x + missile.turnRadius*cos(turnLocAngle)
local turnLocY = missile.y + missile.turnRadius*sin(turnLocAngle)
local targetTurnLocDist = sqrt((turnLocX - missile.lastKnownTargetX)^2 + (turnLocY - missile.lastKnownTargetY)^2)
if targetTurnLocDist > missile.turnRadius or (targetTurnLocDist > 0.9*missile.turnRadius and missile.isTurning) then
missile.isTurning = true
currentAngle = currentAngle + missile.turnRateNormalized
if currentAngle > TAU then
currentAngle = currentAngle - TAU
end
else
missile.isTurning = false
end
end
missile.vx = missile.speed*cos(currentAngle)
missile.vy = missile.speed*sin(currentAngle)
missile.currentAngle = currentAngle
end
missile.x = missile.x + missile.vx
missile.y = missile.y + missile.vy
BlzSetSpecialEffectPosition(missile[VISUAL], missile.x, missile.y, GetTerrainZ(missile.x, missile.y) + missile.visualZ)
end
local function InitMoveArcedHoming(missile)
if missile.target == nil then
print("|cffff0000Warning:|r No target set for missile using homing movement...")
end
missile.visualZ = missile.visualZ or 0
missile.turnRate = missile.turnRate or math.huge
missile.turnRateNormalized = bj_DEGTORAD*missile.turnRate*0.1
missile.turnRadius = missile.speed/(bj_DEGTORAD*missile.turnRate)
missile.lastKnownTargetX, missile.lastKnownTargetY, missile.lastKnownTargetZ = GetUnitCoordinates(missile.target)
if not missile.vx then
local xu, yu, zu = GetUnitCoordinates(missile.target)
local dx, dy = xu - missile.x, yu - missile.y
local horizontalDist = sqrt(dx^2 + dy^2)
local dz = zu - missile.z + missile.arc*horizontalDist
local dist = sqrt(horizontalDist^2 + dz^2)
missile.vx = dx/dist*missile.speed
missile.vy = dy/dist*missile.speed
missile.vz = dz/dist*missile.speed
else
local norm = missile.speed/sqrt(missile.vx^2 + missile.vy^2 + missile.vz^2)
missile.vx, missile.vy, missile.vz = missile.vx*norm, missile.vy*norm, missile.vz*norm
end
if missile.launchOffset then
CAT_LaunchOffset(missile)
end
end
function CAT_MoveArcedHoming(missile)
if ALICE_PairCooldown(0.1) then
if missile.disconnectionDistance then
local x, y, z = GetUnitCoordinates(missile.target)
local distSquared = (x - missile.lastKnownTargetX)^2 + (y - missile.lastKnownTargetY)^2 + (z - missile.lastKnownTargetZ)^2
if distSquared < missile.disconnectionDistance^2 then
missile.lastKnownTargetX, missile.lastKnownTargetY, missile.lastKnownTargetZ = x, y, z
else
missile.target = nil
end
else
missile.lastKnownTargetX, missile.lastKnownTargetY, missile.lastKnownTargetZ = GetUnitCoordinates(missile.target)
end
local vx, vy, vz = missile.vx, missile.vy, missile.vz
local dx, dy = missile.lastKnownTargetX - missile.x, missile.lastKnownTargetY - missile.y
local horizontalDist = sqrt(dx^2 + dy^2)
local phi = atan2(dy, dx)
local currentPhi = atan2(vy, vx)
local diff = phi - currentPhi
local absDiff
if diff < 0 then
diff = diff + TAU
elseif diff > TAU then
diff = diff - TAU
end
if diff > PI then
diff = diff - TAU
absDiff = -diff
else
absDiff = diff
end
if absDiff < missile.turnRateNormalized then
currentPhi = phi
elseif diff < 0 then
--Check if target is inside the circle that the missile cannot currently reach. If so, stop turning to gain distance.
local turnLocPhi = currentPhi - PI/2
local turnLocX = missile.x + missile.turnRadius*cos(turnLocPhi)
local turnLocY = missile.y + missile.turnRadius*sin(turnLocPhi)
local targetTurnLocHoriDist = sqrt((turnLocX - missile.lastKnownTargetX)^2 + (turnLocY - missile.lastKnownTargetY)^2)
if targetTurnLocHoriDist > missile.turnRadius or (targetTurnLocHoriDist > 0.9*missile.turnRadius and missile.isTurning) then
missile.isTurning = true
currentPhi = currentPhi - missile.turnRateNormalized
if currentPhi < 0 then
currentPhi = currentPhi + TAU
end
else
missile.isTurning = false
end
else
local turnLocPhi = currentPhi + PI/2
local turnLocX = missile.x + missile.turnRadius*cos(turnLocPhi)
local turnLocY = missile.y + missile.turnRadius*sin(turnLocPhi)
local targetTurnLocHoriDist = sqrt((turnLocX - missile.lastKnownTargetX)^2 + (turnLocY - missile.lastKnownTargetY)^2)
missile.isTurning = true
if targetTurnLocHoriDist > missile.turnRadius or (targetTurnLocHoriDist > 0.9*missile.turnRadius and missile.isTurning) then
currentPhi = currentPhi + missile.turnRateNormalized
if currentPhi > TAU then
currentPhi = currentPhi - TAU
end
else
missile.isTurning = false
end
end
local dz = missile.lastKnownTargetZ - missile.z + missile.arc*(horizontalDist + absDiff*missile.turnRadius)
local theta = atan2(dz, horizontalDist)
local currentTheta = atan2(vz, sqrt(vx^2 + vy^2))
diff = theta - currentTheta
if diff < 0 then
diff = diff + TAU
elseif diff > TAU then
diff = diff - TAU
end
if diff > PI then
diff = diff - TAU
absDiff = -diff
else
absDiff = diff
end
if absDiff < missile.turnRateNormalized then
currentTheta = theta
elseif diff < 0 then
currentTheta = currentTheta - missile.turnRateNormalized
if currentTheta < 0 then
currentTheta = currentTheta + TAU
end
else
currentTheta = currentTheta + missile.turnRateNormalized
if currentTheta > TAU then
currentTheta = currentTheta - TAU
end
end
missile.vx = missile.speed*cos(currentPhi)*cos(currentTheta)
missile.vy = missile.speed*sin(currentPhi)*cos(currentTheta)
missile.vz = missile.speed*sin(currentTheta)
end
missile.x = missile.x + missile.vx*INTERVAL
missile.y = missile.y + missile.vy*INTERVAL
missile.z = missile.z + missile.vz*INTERVAL
BlzSetSpecialEffectPosition(missile[VISUAL], missile.x, missile.y, missile.z + missile.visualZ)
end
local function InitMoveHoming3D(missile)
if missile.target == nil then
print("|cffff0000Warning:|r No target set for missile using homing movement...")
end
missile.visualZ = missile.visualZ or 0
missile.turnRate = missile.turnRate or math.huge
missile.turnRateNormalized = bj_DEGTORAD*missile.turnRate*0.1
missile.turnRadius = missile.speed/(bj_DEGTORAD*missile.turnRate)
missile.lastKnownTargetX, missile.lastKnownTargetY, missile.lastKnownTargetZ = GetUnitCoordinates(missile.target)
if not missile.vx then
local xu, yu, zu = GetUnitCoordinates(missile.target)
local dx, dy = xu - missile.x, yu - missile.y
local horizontalDist = sqrt(dx^2 + dy^2)
local dz = zu - missile.z
local dist = sqrt(horizontalDist^2 + dz^2)
missile.vx = dx/dist*missile.speed
missile.vy = dy/dist*missile.speed
missile.vz = dz/dist*missile.speed
else
local norm = missile.speed/sqrt(missile.vx^2 + missile.vy^2 + missile.vz^2)
missile.vx, missile.vy, missile.vz = missile.vx*norm, missile.vy*norm, missile.vz*norm
end
if missile.launchOffset then
CAT_LaunchOffset(missile)
end
end
function CAT_MoveHoming3D(missile)
if ALICE_PairCooldown(0.1) then
if UnitAlive(missile.target) then
if missile.disconnectionDistance then
local x, y, z = GetUnitCoordinates(missile.target)
local distSquared = (x - missile.lastKnownTargetX)^2 + (y - missile.lastKnownTargetY)^2 + (z - missile.lastKnownTargetZ)^2
if distSquared < missile.disconnectionDistance^2 then
missile.lastKnownTargetX, missile.lastKnownTargetY, missile.lastKnownTargetZ = x, y, z
else
missile.target = nil
end
else
missile.lastKnownTargetX, missile.lastKnownTargetY, missile.lastKnownTargetZ = GetUnitCoordinates(missile.target)
end
end
local vx, vy, vz = missile.vx, missile.vy, missile.vz
local dx, dy, dz = missile.lastKnownTargetX - missile.x, missile.lastKnownTargetY - missile.y, missile.lastKnownTargetZ - missile.z
local dist = sqrt(dx^2 + dy^2 + dz^2)
local angleDiff = acos((dx*vx + dy*vy + dz*vz)/(missile.speed*dist))
local turnAngle
if angleDiff >= missile.turnRateNormalized then
turnAngle = -angleDiff
else
turnAngle = -missile.turnRateNormalized
end
local nx = vy*dz - vz*dy
local ny = vz*dx - vx*dz
local nz = vx*dy - vy*dx
local invNorm = 1/sqrt(nx^2 + ny^2 + nz^2)
if invNorm ~= math.huge then
nx, ny, nz = nx*invNorm, ny*invNorm, nz*invNorm
if dist < 2*missile.turnRadius then
--Check if target is inside the circle that the missile cannot currently reach. If so, stop turning to gain distance.
local ntpx = vz*ny - vy*nz
local ntpy = vx*nz - vz*nx
local ntpz = vy*nx - vx*ny
invNorm = missile.turnRadius/sqrt(ntpx^2 + ntpy^2 + ntpz^2)
ntpx, ntpy, ntpz = ntpx*invNorm + missile.x, ntpy*invNorm + missile.y, ntpz*invNorm + missile.z
local targetTurnLocDist = sqrt((missile.lastKnownTargetX - ntpx)^2 + (missile.lastKnownTargetY - ntpy)^2 + (missile.lastKnownTargetZ - ntpz)^2)
if targetTurnLocDist > missile.turnRadius or (targetTurnLocDist > 0.9*missile.turnRadius and missile.isTurning) then
missile.isTurning = true
missile.x = missile.x + missile.vx*INTERVAL
missile.y = missile.y + missile.vy*INTERVAL
missile.z = missile.z + missile.vz*INTERVAL
BlzSetSpecialEffectPosition(missile[VISUAL], missile.x, missile.y, missile.z + missile.visualZ)
return
else
missile.isTurning = false
end
end
local cosAngle = cos(turnAngle)
local sinAngle = sin(turnAngle)
local oneMinCos = 1 - cosAngle
M11 = nx*nx*oneMinCos + cosAngle
M12 = nx*ny*oneMinCos - nz*sinAngle
M13 = nx*nz*oneMinCos + ny*sinAngle
M21 = ny*nx*oneMinCos + nz*sinAngle
M22 = ny*ny*oneMinCos + cosAngle
M23 = ny*nz*oneMinCos - nx*sinAngle
M31 = nz*nx*oneMinCos - ny*sinAngle
M32 = nz*ny*oneMinCos + nx*sinAngle
M33 = nz*nz*oneMinCos + cosAngle
local newvx = vx*M11 + vy*M21 + vz*M31
local newvy = vx*M12 + vy*M22 + vz*M32
local newvz = vx*M13 + vy*M23 + vz*M33
invNorm = missile.speed/sqrt(newvx^2 + newvy^2 + newvz^2)
missile.vx, missile.vy, missile.vz = newvx*invNorm, newvy*invNorm, newvz*invNorm
end
end
missile.x = missile.x + missile.vx*INTERVAL
missile.y = missile.y + missile.vy*INTERVAL
missile.z = missile.z + missile.vz*INTERVAL
BlzSetSpecialEffectPosition(missile[VISUAL], missile.x, missile.y, missile.z + missile.visualZ)
end
local function InitAccelerateHoming(missile)
missile.deceleration = missile.deceleration or missile.acceleration
missile.maxSpeed = missile.maxSpeed or math.huge
end
function CAT_AccelerateHoming2D(missile)
if ALICE_PairCooldown(0.1) then
local decelerationNormed = missile.deceleration*INTERVAL
local dx = GetUnitX(missile.target) - missile.x
local dy = GetUnitY(missile.target) - missile.y
local dist = sqrt(dx*dx + dy*dy)
local cosAngle = dx/dist
local sinAngle = dy/dist
local vxPrime = cosAngle * missile.vx + sinAngle * missile.vy
local vyPrime = -sinAngle * missile.vx + cosAngle * missile.vy
local axPrime
local ayPrime
if vxPrime > missile.maxSpeed then
axPrime = -missile.deceleration
elseif vxPrime < 0 then
axPrime = missile.deceleration
else
axPrime = missile.acceleration
end
if vyPrime > decelerationNormed then
ayPrime = -missile.deceleration
elseif vyPrime > 0 then
ayPrime = -vyPrime/INTERVAL
elseif vyPrime < -decelerationNormed then
ayPrime = missile.deceleration
else
ayPrime = -vyPrime/INTERVAL
end
missile.ax = cosAngle * axPrime - sinAngle * ayPrime
missile.ay = sinAngle * axPrime + cosAngle * ayPrime
end
missile.vx = missile.vx + missile.ax*INTERVAL
missile.vy = missile.vy + missile.ay*INTERVAL
end
local function InitMissilesCAT()
Require "ALICE"
Require "CAT_Objects"
INTERVAL = ALICE_MIN_INTERVAL
VISUAL = ALICE_CLASS_FIELD_EFFECT
ALICE_FuncSetInit(CAT_MoveAutoHeight, InitMoveGeneric)
ALICE_FuncSetInit(CAT_Move2D, InitMove2D)
ALICE_FuncSetInit(CAT_Move3D, InitMoveGeneric)
ALICE_FuncSetInit(CAT_MoveHoming2D, InitMoveHoming2D)
ALICE_FuncSetInit(CAT_MoveHoming3D, InitMoveHoming3D)
ALICE_FuncSetInit(CAT_MoveArcedHoming, InitMoveArcedHoming)
ALICE_FuncSetInit(CAT_AccelerateHoming2D, InitAccelerateHoming)
ALICE_FuncSetInit(CAT_MoveArced, InitMoveArced)
end
OnInit.global("CAT_Missiles", InitMissilesCAT)
end
if Debug then Debug.beginFile "CAT Effects" end
do
--[[
============================================================================================================================================================================
Complementary ALICE Template
by Antares
Requires:
ALICE https://www.hiveworkshop.com/threads/a-l-i-c-e-interaction-engine.353126/
PrecomuptedHeightMap https://www.hiveworkshop.com/threads/precomputed-synchronized-terrain-height-map.353477/
TotalInitialization https://www.hiveworkshop.com/threads/total-initialization.317099/
============================================================================================================================================================================
E F F E C T S
============================================================================================================================================================================
This template contains various auxiliary functions to make your entities* look nice. Functions are added to an entity by adding it to the self-interaction table. Function
parameters are customized by editing table fields of your entity. Some parameters are mutable, others are not. In many cases, you can easily alter the implementation of a
function to make a parameter mutable.
*Objects represented by a table with coordinate fields .x, .y, .z, velocity fields .vx, .vy, .vz, and a special effect (field name set in ALICE, default .visual).
============================================================================================================================================================================
Function List:
============================================================================================================================================================================
CAT_MoveEffect A simple function that moves the entity's visual to its current location. Useful for entities anchored to a unit.
CAT_AnimateShadow Adds a shadow to your entity. Requires "CAT_Shadow.mdl" imported. Your entity table requires two fields to be set,
shadowWidth and shadowHeight.
CAT_Orient2D Orients the special effect to always be aligned with its movement direction in 2 dimensions.
CAT_Orient3D Orients the special effect to always be aligned with its movement direction in 3 dimensions.
CAT_OrientPropelled2D Orients the special effect to always be aligned with its acceleration direction in 2 dimensions. Can be combined with
CAT_AccelerateHoming functions. If you use your own acceleration function, use .ax and .ay for the fields determining
the acceleration
CAT_OrientPropelled3D The same as CAT_OrientPropelled2D, but takes z-acceleration into account.
CAT_OrientRoll Orients the special effect of your entity to look like it is rolling across the ground. Your entity table requires
the .collisionRadius field to be set. Optional field .rollSpeed. Default value 1.
CAT_OrientProjectile Orients the special effect with OrientEffect3D until it collides with something, after which OrientRoll will be used.
CAT_InitRandomOrientation(entity) Initializes the entity's special effect to a random orientation. Not a self-interaction function! Call this function
on your entity during creation after creating the special effect.
CAT_InitDirectedOrientation(entity) Initializes the entity's special effect to an orientation pointing towards its movement direction. Not a
self-interaction function! Call this function on your entity during creation after creating the special effect.
CAT_AttachEffect(whichEntity, whichEffect, zOffset) Attaches an additional effect to the entity. Not a self-interaction function! Call this function at any time, but only
after registering your entity with ALICE.
--==========================================================================================================================================================================
]]
local sqrt = math.sqrt
local atan2 = math.atan
local cos = math.cos
local sin = math.sin
local function ClearShadow(entity, __, __)
DestroyEffect(entity.shadow)
end
---@param entity table
local function InitAnimateShadow(entity)
entity.shadow = AddSpecialEffect("CAT_Shadow.mdl", entity.x, entity.y)
BlzSetSpecialEffectTimeScale(entity.shadow, 0)
if entity.shadowHeight > entity.shadowWidth then
BlzSetSpecialEffectTime(entity.shadow, 1 + 0.25*(entity.shadowHeight/entity.shadowWidth - 1))
BlzSetSpecialEffectScale(entity.shadow, entity.shadowWidth/200)
entity.shadowOffsetY = 0.15*entity.shadowWidth
entity.shadowOffsetX = 0.25*entity.shadowWidth
entity.shadowOffsetZ = 0.1*entity.shadowHeight
entity.shadowAttenuation = 12/entity.shadowWidth
else
BlzSetSpecialEffectTime(entity.shadow, 1 - 0.25*(entity.shadowWidth/entity.shadowHeight - 1))
BlzSetSpecialEffectScale(entity.shadow, entity.shadowHeight/200)
entity.shadowOffsetY = 0.15*entity.shadowHeight
entity.shadowOffsetX = 0.25*entity.shadowHeight
entity.shadowOffsetZ = 0.1*entity.shadowWidth
entity.shadowAttenuation = 12/entity.shadowHeight
end
ALICE_PairOnDestroy(ClearShadow)
end
---@param entity table
function CAT_AnimateShadow(entity)
local x, y = entity.x + entity.shadowOffsetX, entity.y + entity.shadowOffsetY
local terrainZ = GetTerrainZ(x, y)
if entity.z then
local alpha = 255 - (entity.shadowAttenuation*(entity.z - terrainZ)) // 1
if alpha < 0 then
alpha = 0
end
BlzSetSpecialEffectAlpha(entity.shadow, alpha)
end
BlzSetSpecialEffectPosition(entity.shadow, x, y, terrainZ + entity.shadowOffsetZ)
if ALICE_PairCooldown(0.2) then
local gradientX = (GetTerrainZ(x + 16, y) - GetTerrainZ(x - 16, y))*0.0313
local gradientY = (GetTerrainZ(x, y + 16) - GetTerrainZ(x, y - 16))*0.0313
BlzSetSpecialEffectPitch(entity.shadow, -atan2(gradientX, 1))
BlzSetSpecialEffectRoll(entity.shadow, atan2(gradientY, 1))
end
if entity.isResting then
ALICE_PairPause()
end
end
local function ClearAttachedEffect(entity, __, __)
if type(entity.attachedEffect) == "userdata" then
DestroyEffect(entity.attachedEffect)
else
for __, effect in ipairs(entity.attachedEffect) do
DestroyEffect(effect)
end
end
end
local function InitMoveAttachedEffect(entity)
ALICE_PairOnDestroy(ClearAttachedEffect)
end
local function MoveAttachedEffect(entity)
if type(entity.attachedEffect) == "userdata" then
BlzSetSpecialEffectPosition(entity.attachedEffect, entity.x, entity.y, entity.z + entity.attachedEffectZ)
else
for index, effect in ipairs(entity.attachedEffect) do
BlzSetSpecialEffectPosition(effect, entity.x, entity.y, entity.z + entity.attachedEffectZ[index])
end
end
if entity.isResting then
ALICE_PairPause()
end
end
---@param whichEntity table
---@param whichEffect effect
---@param zOffset number
function CAT_AttachEffect(whichEntity, whichEffect, zOffset)
if whichEntity.attachedEffect == nil then
whichEntity.attachedEffect = whichEffect
whichEntity.attachedEffectZ = zOffset or 0
ALICE_AddSelfInteraction(whichEntity, MoveAttachedEffect)
elseif whichEntity.attachedEffect == "userdata" then
whichEntity.attachedEffect = {whichEntity.attachedEffect, whichEffect}
whichEntity.attachedEffectZ = {whichEntity.attachedEffectZ, zOffset or 0}
else
table.insert(whichEntity.attachedEffect, whichEffect)
table.insert(whichEntity.attachedEffectZ, zOffset or 0)
end
end
local function InitMoveEffect(entity)
entity.visualZ = entity.visualZ or 0
end
function CAT_MoveEffect(entity)
local x, y, z, __, __, __ = ALICE_PairGetCoordinates3D()
BlzSetSpecialEffectPosition(entity.visual, x, y, z + entity.visualZ)
end
function CAT_Orient2D(entity)
if entity.vx ~= 0 or entity.vy ~= 0 then
BlzSetSpecialEffectYaw(entity[VISUAL], atan2(entity.vy, entity.vx))
end
if entity.isResting then
ALICE_PairPause()
end
return 0.1
end
function CAT_Orient3D(entity)
if entity.vx ~= 0 or entity.vy ~= 0 or entity.vz ~= 0 then
BlzSetSpecialEffectOrientation(entity[VISUAL], atan2(entity.vy, entity.vx), atan2(-entity.vz, sqrt(entity.vx^2 + entity.vy^2)), 0)
end
if entity.isResting then
ALICE_PairPause()
end
return 0.1
end
function CAT_OrientPropelled2D(entity)
if entity.ax ~= 0 or entity.ay ~= 0 then
BlzSetSpecialEffectYaw(entity[VISUAL], atan2(entity.ay, entity.ax))
end
return 0.1
end
function CAT_OrientPropelled3D(entity)
if entity.ax ~= 0 or entity.ay ~= 0 or entity.az ~= 0 then
BlzSetSpecialEffectOrientation(entity[VISUAL], atan2(entity.ay, entity.ax), atan2(-entity.az, sqrt(entity.ax^2 + entity.ay^2)), 0)
end
return 0.1
end
local function InitOrientation(entity)
if entity.O11 then
return
end
entity.O11 = 1
entity.O12 = 0
entity.O13 = 0
entity.O21 = 0
entity.O22 = 1
entity.O23 = 0
entity.O31 = 0
entity.O32 = 0
entity.O33 = 1
BlzSetSpecialEffectOrientation(entity[VISUAL], atan2(-entity.O12, entity.O11), atan2(entity.O13, sqrt(entity.O12^2 + entity.O11^2)), atan2(entity.O23, entity.O33))
end
function CAT_OrientRoll(entity)
local norm = sqrt(entity.vy^2 + entity.vx^2)
if norm == 0 then
if entity.isResting then
ALICE_PairPause()
end
return
end
local nx, ny = entity.vy/norm, entity.vx/norm
local alpha = INTERVAL*norm/entity.collisionRadius*(entity.rollSpeed or 1)
local cosAngle = cos(alpha)
local sinAngle = sin(alpha)
local oneMinCos = 1 - cosAngle
local M11 = nx*nx*oneMinCos + cosAngle
local M12 = nx*ny*oneMinCos
local M13 = ny*sinAngle
local M21 = ny*nx*oneMinCos
local M22 = ny*ny*oneMinCos + cosAngle
local M23 = -nx*sinAngle
local M31 = -ny*sinAngle
local M32 = nx*sinAngle
local M33 = cosAngle
local O11 = entity.O11*M11 + entity.O12*M21 + entity.O13*M31
local O12 = entity.O11*M12 + entity.O12*M22 + entity.O13*M32
local O13 = entity.O11*M13 + entity.O12*M23 + entity.O13*M33
local O21 = entity.O21*M11 + entity.O22*M21 + entity.O23*M31
local O22 = entity.O21*M12 + entity.O22*M22 + entity.O23*M32
local O23 = entity.O21*M13 + entity.O22*M23 + entity.O23*M33
local O31 = entity.O31*M11 + entity.O32*M21 + entity.O33*M31
local O32 = entity.O31*M12 + entity.O32*M22 + entity.O33*M32
local O33 = entity.O31*M13 + entity.O32*M23 + entity.O33*M33
entity.O11, entity.O12, entity.O13, entity.O21, entity.O22, entity.O23, entity.O31, entity.O32, entity.O33 = O11, O12, O13, O21, O22, O23, O31, O32, O33
BlzSetSpecialEffectOrientation(entity[VISUAL], atan2(-entity.O12, entity.O11), atan2(entity.O13, sqrt(entity.O12^2 + entity.O11^2)), atan2(entity.O23, entity.O33))
end
function CAT_OrientProjectile(entity)
if not entity.hasCollided then
CAT_Orient3D(entity)
else
CAT_InitRandomOrientation(entity)
ALICE_PairSetInteractionFunc(CAT_OrientRoll)
CAT_OrientRoll(entity)
end
end
function CAT_InitRandomOrientation(entity)
local nx = 1- 2*math.random()
local ny = 1- 2*math.random()
local nz = 1- 2*math.random()
local norm = sqrt(nx^2 + ny^2 + nz^2)
if norm == 0 then
return
end
nx, ny, nz = nx/norm, ny/norm, nz/norm
local alpha = 2*bj_PI*math.random()
local cosAngle = cos(alpha)
local sinAngle = sin(alpha)
local oneMinCos = 1 - cosAngle
entity.O11 = nx*nx*oneMinCos + cosAngle
entity.O12 = nx*ny*oneMinCos - nz*sinAngle
entity.O13 = nx*nz*oneMinCos + ny*sinAngle
entity.O21 = ny*nx*oneMinCos + nz*sinAngle
entity.O22 = ny*ny*oneMinCos + cosAngle
entity.O23 = ny*nz*oneMinCos - nx*sinAngle
entity.O31 = nz*nx*oneMinCos - ny*sinAngle
entity.O32 = nz*ny*oneMinCos + nx*sinAngle
entity.O33 = nz*nz*oneMinCos + cosAngle
BlzSetSpecialEffectOrientation(entity[VISUAL], atan2(-entity.O12, entity.O11), atan2(entity.O13, sqrt(entity.O12^2 + entity.O11^2)), atan2(entity.O23, entity.O33))
end
function CAT_InitDirectedOrientation(entity)
local alpha = atan2(entity.vy, entity.vx)
local cosAngle = cos(alpha)
local sinAngle = sin(alpha)
entity.O11 = cosAngle
entity.O12 = -sinAngle
entity.O13 = 0
entity.O21 = sinAngle
entity.O22 = cosAngle
entity.O23 = 0
entity.O31 = 0
entity.O32 = 0
entity.O33 = 1
BlzSetSpecialEffectOrientation(entity[VISUAL], atan2(-entity.O12, entity.O11), atan2(entity.O13, sqrt(entity.O12^2 + entity.O11^2)), atan2(entity.O23, entity.O33))
end
function InitEffectsCAT()
Require "ALICE"
Require "PrecomputedHeightMap"
INTERVAL = ALICE_MIN_INTERVAL
VISUAL = ALICE_CLASS_FIELD_EFFECT
ALICE_FuncSetInit(CAT_AnimateShadow, InitAnimateShadow)
ALICE_FuncSetInit(CAT_MoveEffect, InitMoveEffect)
ALICE_FuncSetInit(MoveAttachedEffect, InitMoveAttachedEffect)
ALICE_FuncSetInit(CAT_OrientRoll, InitOrientation)
end
OnInit.global("CAT_Effects", InitEffectsCAT)
end
if Debug then Debug.beginFile "CAT Collisions" end
do
--[[
=============================================================================================================================================================
Complementary ALICE Template
by Antares
Requires:
ALICE https://www.hiveworkshop.com/threads/a-l-i-c-e-interaction-engine.353126/
Objects CAT
TotalInitialization https://www.hiveworkshop.com/threads/total-initialization.317099/
Knockback Item ('Ikno') (optional)
=============================================================================================================================================================
C O L L I S I O N S 3 D
=============================================================================================================================================================
This template contains various functions to detect and execute collisions between entities* and any type of object. Terrain collision is not part of this
template. The functions are accessed by adding them to your entity class tables (for an example, see Entities CAT).
*Objects represented by a table with coordinate fields .x, .y, .z, velocity fields .vx, .vy, .vz, and a special effect (field name set in ALICE, default .visual).
The collision box of entities is a sphere. The radius is set with the .collisionRadius field in your entity class table. The collision box of widgets is a
cylinder with a rounded top. The parameters for widgets are set in the Objects CAT.
To add a collision check to your entity class, add the appropriate collision check function to the class's interactionFunc. For example:
interactionFunc = {
unit = CAT_UnitCollisionCheck3D,
destructable = CAT_DestructableCollisionCheck3D
}
By default, on collision, the entity will be destroyed with ALICE_Kill unless a different callback function is provided. To do so, add these fields to your
class table:
onEntityCollision
onUnitCollision
onDestructableCollision
onItemCollision
You can use one of the preset functions or your own. The preset functions calculate different parameters about the collision, call an additional callback
function that you can customize, then perform additional actions depending on the specific function used. To add a callback, add these fields to your class
table:
onEntityCallback
onUnitCallback
onDestructableCallback
onItemCallback
The callback function will be called with 11 parameters:
callback(
entity, object,
collisionPointX, collisionPointY, collisionPointZ,
normalSpeed, tangentialSpeed, totalSpeed,
centerOfMassVelocityX, centerOfMassVelocityY, centerOfMassVelocityZ
)
The value for all onXYZCollision and onXYZcallback fields can be either a function or a table. A table must be a function dictionary, where the keys denote
the identifiers of the objects for which the callback function should be used. This is similar to how interactionFuncs are set up.
Example:
onEntityCollision = {
ball = CAT_EntityBounce3D,
spike = CAT_EntityImpact3D,
other = CAT_EntityPassThrough3D
}
-------------------------------------------------------------------------------------------------------------------------------------------------------------
You can have an entity automatically deal damage to a widget on collision without the need to define a callback function by setting these fields:
onUnitDamage
onDestructableDamage
onItemDamage
For units, the .source field must be set to specifiy the owner of the entity and source of damage. The .onUnitAttackType field determines the attack type used.
The default type is ATTACK_TYPE_NORMAL. The same for .onUnitDamageType. The default type is DAMAGE_TYPE_MAGIC.
-------------------------------------------------------------------------------------------------------------------------------------------------------------
Additional parameters:
To calculate the recoil of the collision, the preset functions require the .mass field to be set for your entity. The masses of units is set in the Objects
CAT. If you don't want an object to be recoiled, set its mass to math.huge (infinity). The masses of items and destructables are infinite automatically.
In addition, the bounce functions use the .elasticity parameter ranging from 0 to 1 to calculate the elasticity of the bounce. If not set, the default value of
1 will be used. The elasticity of widgets is set in the Objects CAT. The elasticity of the collision is sqrt(elasticity 1 * elasticity 2).
To estimate the frequency with which the collision check should be performed, the collision check functions use the .maxSpeed parameter. It specifies the
maximum speed that the entity can reasonably reach. The default value is DEFAULT_ENTITY_MAX_SPEED (Objects CAT).
Summary of parameter fields:
collisionRadius Required by all functions.
mass Used by all callback functions except pass through. Default value is 0.
elasticity Used by bounce functions. Default value is 1.
maxSpeed Used by collision check function. Default value is DEFAULT_ENTITY_MAX_SPEED.
-------------------------------------------------------------------------------------------------------------------------------------------------------------
Anchors:
You can anchor a table to a widget. This will reroute the collision recoil to the anchor if it is a unit. You can also enable unit-widget collisions this way,
although there may be some bugs and issues with this. If the table has a mass field, that value will be used to calculate the recoil. Otherwise, the unit's mass
will be used. Other fields cannot be overwritten and must be provided.
=============================================================================================================================================================
L I S T O F F U N C T I O N S
=============================================================================================================================================================
Entity-Entity Collisions:
CAT_EntityCollisionCheck3D
Callbacks:
CAT_EntityBounce3D Reflect the two entities.
CAT_EntityImpact3D Destroy the initiating entity and recoil the other.
CAT_EntityDevour3D Destroy the receiving entity and recoil the other.
CAT_EntityAnnihilate3D Destroy both entities.
CAT_EntityPassThrough3D Execute the callback function once, but do not destroy or recoil either.
Entity-Unit Collisions:
CAT_UnitCollisionCheck3D
Callbacks:
CAT_UnitBounce3D Reflect the entity on the unit and recoil the unit.
CAT_UnitImpact3D Destroy the entity and recoil the unit.
CAT_UnitDevour3D Kill the unit and recoil the entity.
CAT_UnitAnnihilate3D Destroy both the unit and the entity.
CAT_UnitPassThrough3D Execute the callback function once, but do not destroy or recoil either.
Entity-Destructable Collisions:
CAT_DestructableCollisionCheck3D
Callbacks:
CAT_DestructableBounce3D Reflect the entity on the destructable.
CAT_DestructableImpact3D Destroy the entity.
CAT_DestructableDevour3D Destroy the destructable and recoil the entity.
CAT_DestructableAnnihilate3D Destroy both the destructable and the entity.
CAT_DestructablePassThrough3D Execute the callback function once, but do not destroy or recoil either.
Entity-Item Collisions:
CAT_ItemCollisionCheck3D
Callbacks:
CAT_ItemBounce3D Reflect the entity on the item.
CAT_ItemImpact3D Destroy the entity.
CAT_ItemDevour3D Destroy the item and recoil the entity.
CAT_ItemAnnihilate3D Destroy the item and the entity.
CAT_ItemPassThrough3D Execute the callback function once, but do not destroy or recoil either.
--===========================================================================================================================================================
]]
local INTERVAL = nil
local VISUAL = nil
local UNIT_MAX_SPEED = 522
local INF = math.huge
local sqrt = math.sqrt
local cos = math.cos
local sin = math.sin
local atan2 = math.atan
---@param entity any
---@param fieldName string
---@return function | nil
local function FindCallback(entity, fieldName)
if entity[fieldName] then
if type(entity[fieldName]) == "table" then
local whichFunc = nil
local __, actorB = ALICE_PairGetActors()
for key, __ in pairs(entity[fieldName]) do
if actorB.identifier[key] then
if whichFunc == nil then
whichFunc = entity[fieldName][key]
else
error(fieldName .. " function ambiguous.")
end
end
end
return whichFunc or entity[fieldName].other
else
return entity[fieldName]
end
end
return nil
end
local function DamageWidget(entity, widget)
if WidgetIsUnit(widget) then
UnitDamageTarget(entity.source, widget, entity.onUnitDamage, false, false, entity.onUnitAttackType or ATTACK_TYPE_NORMAL, entity.onUnitDamageType or DAMAGE_TYPE_MAGIC, WEAPON_TYPE_WHOKNOWS)
elseif WidgetIsDestructable(widget) then
SetDestructableLife(widget, GetDestructableLife(widget) - entity.onDestructableDamage)
elseif WidgetIsItem(widget) then
SetWidgetLife(widget, GetWidgetLife(widget) - entity.onItemDamage)
else
error("Attempted to damage non-widget object.")
end
end
local function Backtrack(entity, vx, vy, vz, collisionX, collisionY, collisionZ)
local factor = entity.collisionRadius/sqrt(vx^2 + vy^2 + vz^2)
entity.x = collisionX - vx*factor
entity.y = collisionY - vy*factor
entity.z = collisionZ - vz*factor
BlzSetSpecialEffectPosition(entity[VISUAL], entity.x, entity.y, entity.z)
end
--===========================================================================================================================================================
--Entity-Entity Collisions
--==========================================================================================================================================================
---@param A table
---@param B table
local function GetEntityCollisionPoint3D(A, B, ax, ay, az, bx, by, bz)
local collisionDist = A.collisionRadius + B.collisionRadius
local collisionX = (ax*B.collisionRadius + bx*A.collisionRadius)/collisionDist
local collisionY = (ay*B.collisionRadius + by*A.collisionRadius)/collisionDist
local collisionZ = (az*B.collisionRadius + bz*A.collisionRadius)/collisionDist
return collisionX, collisionY, collisionZ, collisionDist
end
local function EntityCollisionMath3D(A, B)
local xa, ya, za, xb, yb, zb = ALICE_PairGetCoordinates3D()
local dx, dy, dz = xa - xb, ya - yb, za - zb
local vxa, vya, vza = CAT_GetObjectVelocity3D(A)
local vxb, vyb, vzb = CAT_GetObjectVelocity3D(B)
local dvx, dvy, dvz = vxa - vxb, vya - vyb, vza - vzb
local dist = sqrt(dx^2 + dy^2 + dz^2)
local normalSpeed = -(dx*dvx + dy*dvy + dz*dvz)/dist
if normalSpeed < 0 then
return false
end
local totalSpeed = sqrt(dvx^2 + dvy^2 + dvz^2)
local tangentialSpeed = sqrt(totalSpeed^2 - normalSpeed^2)
local massA = CAT_GetObjectMass(A)
local massB = CAT_GetObjectMass(B)
local invMassSum = 1/(massA + massB)
local centerOfMassVx, centerOfMassVy, centerOfMassVz
if massA == INF then
if massB == INF then
centerOfMassVx = (vxa*massA + vxb*massB)*invMassSum
centerOfMassVy = (vya*massA + vyb*massB)*invMassSum
centerOfMassVz = (vza*massA + vzb*massB)*invMassSum
else
centerOfMassVx = vxa
centerOfMassVy = vya
centerOfMassVz = vza
end
elseif massB == INF then
centerOfMassVx = vxb
centerOfMassVy = vyb
centerOfMassVz = vzb
else
centerOfMassVx = (vxa*massA + vxb*massB)*invMassSum
centerOfMassVy = (vya*massA + vyb*massB)*invMassSum
centerOfMassVz = (vza*massA + vzb*massB)*invMassSum
end
return true, xa, ya, za, xb, yb, zb, dist,
vxa, vya, vza, vxb, vyb, vzb,
dx, dy, dz, dvx, dvy, dvz,
massA, massB,
normalSpeed, totalSpeed, tangentialSpeed,
centerOfMassVx, centerOfMassVy, centerOfMassVz
end
local function EntityRecoilAndDisplace3D(object, x, y, z, recoilX, recoilY, recoilZ)
if object.anchor then
if type(object.anchor) == "table" then
object.anchor.x,object.anchor.y, object.anchor.z = x, y, z
object.anchor.vx, object.anchor.vy, object.anchor.vz = object.anchor.vx + recoilX, object.anchor.vy + recoilY, object.anchor.vz + recoilZ
else
SetUnitX(object.anchor, x)
SetUnitY(object.anchor, y)
CAT_Knockback(object.anchor, recoilX, recoilY, recoilZ)
end
else
object.x,object.y, object.z = x, y, z
object.vx, object.vy, object.vz = object.vx + recoilX, object.vy + recoilY, object.vz + recoilZ
end
end
local function EntityRecoil3D(object, recoilX, recoilY, recoilZ)
if object.anchor then
if type(object.anchor) == "table" then
object.anchor.vx, object.anchor.vy, object.anchor.vz = object.anchor.vx + recoilX, object.anchor.vy + recoilY, object.anchor.vz + recoilZ
else
CAT_Knockback(object.anchor, recoilX, recoilY, recoilZ)
end
else
object.vx, object.vy, object.vz = object.vx + recoilX, object.vy + recoilY, object.vz + recoilZ
end
end
local function GetMassRatio(massA, massB)
--massA >>> massB -> 1
--massB >>> massA -> 0
if massA == 0 and massB == 0 then
return 0.5
elseif massA == INF then
if massB == INF then
return 0.5
else
return 1
end
else
return massA/(massA + massB)
end
end
-------------
--Callbacks
-------------
function CAT_EntityBounce3D(A, B)
local validCollision, xa, ya, za, xb, yb, zb, dist,
vxa, vya, vza, vxb, vyb, vzb,
dx, dy, dz, dvx, dvy, dvz,
massA, massB,
normalSpeed, totalSpeed, tangentialSpeed,
centerOfMassVx, centerOfMassVy, centerOfMassVz = EntityCollisionMath3D(A, B)
if not validCollision then
return
end
local elasticity = sqrt((A.elasticity or 1)*(B.elasticity or 1))
local e = (1 + elasticity)
local Aunmoved
local Bunmoved
--Avoid unpausing resting entity with only minor bounce.
if A.isResting and normalSpeed*e/2*B.mass/A.mass < A.friction*STATIC_FRICTION_FACTOR*INTERVAL then
Aunmoved = true
end
if B.isResting and normalSpeed*e/2*A.mass/B.mass < B.friction*STATIC_FRICTION_FACTOR*INTERVAL then
Bunmoved = true
end
local nx, ny, nz = dx/dist, dy/dist, dz/dist
local Advx = vxa - centerOfMassVx
local Advy = vya - centerOfMassVy
local Advz = vza - centerOfMassVz
local Bdvx = vxb - centerOfMassVx
local Bdvy = vyb - centerOfMassVy
local Bdvz = vzb - centerOfMassVz
--Householder transformation.
local H11 = 1 - e*nx^2
local H12 = -e*nx*ny
local H13 = -e*nx*nz
local H21 = H12
local H22 = 1 - e*ny^2
local H23 = -e*ny*nz
local H31 = H13
local H32 = H23
local H33 = 1 - e*nz^2
local collisionX, collisionY, collisionZ, collisionDist = GetEntityCollisionPoint3D(A, B, xa, ya, za, xb, yb, zb)
local massRatio
if Aunmoved then
massRatio = 1
elseif Bunmoved then
massRatio = 0
else
massRatio = GetMassRatio(massA, massB)
end
local xNew, yNew, zNew, recoilX, recoilY, recoilZ
local displacement = collisionDist - dist
if massRatio < 1 then
xNew = xa + 1.001*dx/dist*displacement*(1 - massRatio)
yNew = ya + 1.001*dy/dist*displacement*(1 - massRatio)
zNew = za + 1.001*dz/dist*displacement*(1 - massRatio)
recoilX = H11*Advx + H12*Advy + H13*Advz + centerOfMassVx - vxa
recoilY = H21*Advx + H22*Advy + H23*Advz + centerOfMassVy - vya
recoilZ = H31*Advx + H32*Advy + H33*Advz + centerOfMassVz - vza
EntityRecoilAndDisplace3D(A, xNew, yNew, zNew, recoilX, recoilY, recoilZ)
else
A.vx, A.vy, A.vz = 0, 0, 0
end
if massRatio > 0 then
xNew = xb - 1.001*dx/dist*displacement*massRatio
yNew = yb - 1.001*dy/dist*displacement*massRatio
zNew = zb - 1.001*dz/dist*displacement*massRatio
recoilX = H11*Bdvx + H12*Bdvy + H13*Bdvz + centerOfMassVx - vxb
recoilY = H21*Bdvx + H22*Bdvy + H23*Bdvz + centerOfMassVy - vyb
recoilZ = H31*Bdvx + H32*Bdvy + H33*Bdvz + centerOfMassVz - vzb
EntityRecoilAndDisplace3D(B, xNew, yNew, zNew, recoilX, recoilY, recoilZ)
else
B.vx, B.vy, B.vz = 0, 0, 0
end
local callback = FindCallback(A, "onEntityCallback")
if callback then
callback(A, B, collisionX, collisionY, collisionZ, normalSpeed, tangentialSpeed, totalSpeed, centerOfMassVx, centerOfMassVy, centerOfMassVz)
end
end
function CAT_EntityImpact3D(A, B)
local validCollision, xa, ya, za, xb, yb, zb, dist,
vxa, vya, vza, vxb, vyb, vzb,
dx, dy, dz, dvx, dvy, dvz,
massA, massB,
normalSpeed, totalSpeed, tangentialSpeed,
centerOfMassVx, centerOfMassVy, centerOfMassVz = EntityCollisionMath3D(A, B)
local collisionX, collisionY, collisionZ, collisionDist = GetEntityCollisionPoint3D(A, B, xa, ya, za, xb, yb, zb)
if massA > 0 then
local unmoved
--Avoid unpausing resting entity with only minor bounce.
if B.isResting and normalSpeed/2*A.mass/B.mass < B.friction*STATIC_FRICTION_FACTOR*INTERVAL or (massB == INF and massA == INF) then
unmoved = true
end
if not unmoved then
local recoilX, recoilY, recoilZ
local massRatio = GetMassRatio(massA, massB)
if massRatio > 0 then
recoilX = dvx*massRatio
recoilY = dvy*massRatio
recoilZ = dvz*massRatio
EntityRecoil3D(B, recoilX, recoilY, recoilZ)
end
end
end
local callback = FindCallback(A, "onEntityCallback")
if callback then
callback(A, B, collisionX, collisionY, collisionZ, normalSpeed, tangentialSpeed, totalSpeed, centerOfMassVx, centerOfMassVy, centerOfMassVz)
end
Backtrack(A, vxa, vya, vza, collisionX, collisionY, collisionZ)
ALICE_Kill(A)
end
function CAT_EntityDevour3D(A, B)
local validCollision, xa, ya, za, xb, yb, zb, dist,
vxa, vya, vza, vxb, vyb, vzb,
dx, dy, dz, dvx, dvy, dvz,
massA, massB,
normalSpeed, totalSpeed, tangentialSpeed,
centerOfMassVx, centerOfMassVy, centerOfMassVz = EntityCollisionMath3D(A, B)
local collisionX, collisionY, collisionZ, collisionDist = GetEntityCollisionPoint3D(A, B, xa, ya, za, xb, yb, zb)
if massA > 0 then
local unmoved
--Avoid unpausing resting entity with only minor bounce.
if A.isResting and normalSpeed/2*B.mass/A.mass < A.friction*STATIC_FRICTION_FACTOR*INTERVAL or (massA == INF and massB == INF) then
unmoved = true
end
if not unmoved then
local recoilX, recoilY, recoilZ
local massRatio = GetMassRatio(massA, massB)
if massRatio > 0 then
recoilX = -dvx*(1 - massRatio)
recoilY = -dvy*(1 - massRatio)
recoilZ = -dvz*(1 - massRatio)
EntityRecoil3D(A, recoilX, recoilY, recoilZ)
end
end
end
local callback = FindCallback(A, "onEntityCallback")
if callback then
callback(A, B, collisionX, collisionY, collisionZ, normalSpeed, tangentialSpeed, totalSpeed, centerOfMassVx, centerOfMassVy, centerOfMassVz)
end
Backtrack(B, vxb, vyb, vzb, collisionX, collisionY, collisionZ)
ALICE_Kill(B)
end
function CAT_EntityAnnihilate3D(A, B)
local validCollision, xa, ya, za, xb, yb, zb, dist,
vxa, vya, vza, vxb, vyb, vzb,
dx, dy, dz, dvx, dvy, dvz,
massA, massB,
normalSpeed, totalSpeed, tangentialSpeed,
centerOfMassVx, centerOfMassVy, centerOfMassVz = EntityCollisionMath3D(A, B)
local collisionX, collisionY, collisionZ, collisionDist = GetEntityCollisionPoint3D(A, B, xa, ya, za, xb, yb, zb)
local callback = FindCallback(A, "onEntityCallback")
if callback then
callback(A, B, collisionX, collisionY, collisionZ, normalSpeed, tangentialSpeed, totalSpeed, centerOfMassVx, centerOfMassVy, centerOfMassVz)
end
Backtrack(A, vxa, vya, vza, collisionX, collisionY, collisionZ)
ALICE_Kill(A)
Backtrack(B, vxb, vyb, vzb, collisionX, collisionY, collisionZ)
ALICE_Kill(B)
end
function CAT_EntityPassThrough3D(A, B)
local callback = FindCallback(A, "onEntityCallback")
if callback then
local validCollision, xa, ya, za, xb, yb, zb, dist,
vxa, vya, vza, vxb, vyb, vzb,
dx, dy, dz, dvx, dvy, dvz,
massA, massB,
normalSpeed, totalSpeed, tangentialSpeed,
centerOfMassVx, centerOfMassVy, centerOfMassVz = EntityCollisionMath3D(A, B)
local collisionX, collisionY, collisionZ, collisionDist = GetEntityCollisionPoint3D(A, B, xa, ya, za, xb, yb, zb)
callback(A, B, collisionX, collisionY, collisionZ, normalSpeed, tangentialSpeed, totalSpeed, centerOfMassVx, centerOfMassVy, centerOfMassVz)
end
ALICE_PairDisable()
end
--------------------
--Collision Checks
--------------------
function CAT_EntityCollisionCheck3D(A, B)
local dx, dy, dz, dist
if A.anchor or B.anchor then
dist = ALICE_PairGetDistance3D()
else
dx = A.x - B.x
dy = A.y - B.y
dz = A.z - B.z
dist = sqrt(dx*dx + dy*dy + dz*dz)
end
local collisionRange = A.collisionRadius + B.collisionRadius
if dist < collisionRange then
local callback = FindCallback(A, "onEntityCollision")
if callback then
callback(A, B)
end
A.hasCollided = true
B.hasCollided = true
end
if A.isResting and B.isResting then
ALICE_PairPause()
end
return (dist - collisionRange)/((A.maxSpeed or DEFAULT_ENTITY_MAX_SPEED) + (B.maxSpeed or DEFAULT_ENTITY_MAX_SPEED))
end
--===========================================================================================================================================================
--Entity-Widget Collisions
--==========================================================================================================================================================
local function WidgetCollisionMath3D(entity, widget)
local data = ALICE_PairLoadData()
local xa, ya, za, xb, yb, zb = ALICE_PairGetCoordinates3D()
local dx, dy, dz = xa - xb, ya - yb, za - zb
local vxa, vya, vza = CAT_GetObjectVelocity3D(entity)
local vxb, vyb, vzb = CAT_GetObjectVelocity3D(widget)
local dvx, dvy, dvz = vxa - vxb, vya - vyb, vza - vzb
local dist = sqrt(dx^2 + dy^2 + dz^2)
local nx, ny, nz
local collisionX, collisionY, collisionZ
local overlap
local phi = atan2(dy, dx)
local theta
local dtop = data.height/2 - dz
local dbottom = -dz - data.height/2
if dtop < data.radius then
theta = atan2(data.radius - dtop, sqrt(dx^2 + dy^2))
local cosTheta = cos(theta)
nx = cos(phi)*cosTheta
ny = sin(phi)*cosTheta
nz = sin(theta)
collisionZ = zb + data.height/2 - (1 - nz)*data.radius
overlap = (data.radius + entity.collisionRadius) - sqrt(dx^2 + dy^2 + (data.radius - dtop)^2)
elseif WidgetIsUnit(widget) and GetUnitFlyHeight(widget) > 0 and dbottom < data.radius then
theta = atan2(dbottom - data.radius, sqrt(dx^2 + dy^2))
local cosTheta = cos(theta)
nx = cos(phi)*cosTheta
ny = sin(phi)*cosTheta
nz = sin(theta)
collisionZ = zb - data.height/2 + (1 + nz)*data.radius
overlap = (data.radius + entity.collisionRadius) - sqrt(dx^2 + dy^2 + (dbottom - data.radius)^2)
else
local horiDist = sqrt(dx^2 + dy^2)
nx = dx/horiDist
ny = dy/horiDist
nz = 0
collisionZ = za
overlap = data.radius + entity.collisionRadius - horiDist
end
local normalSpeed = -(nx*dvx + ny*dvy + nz*dvz)
if normalSpeed < 0 then
return false
end
collisionX = xb + nx*data.radius
collisionY = yb + ny*data.radius
local totalSpeed = sqrt(dvx^2 + dvy^2 + dvz^2)
local tangentialSpeed = sqrt(totalSpeed^2 - normalSpeed^2)
local massA = CAT_GetObjectMass(entity)
local massB = CAT_GetObjectMass(widget)
local massSum = massA + massB
local centerOfMassVx, centerOfMassVy, centerOfMassVz
if massA == INF then
if massB == INF then
centerOfMassVx = (vxa*massA + vxb*massB)/massSum
centerOfMassVy = (vya*massA + vyb*massB)/massSum
centerOfMassVz = (vza*massA + vzb*massB)/massSum
else
centerOfMassVx = vxa
centerOfMassVy = vya
centerOfMassVz = vza
end
elseif massB == INF then
centerOfMassVx = vxb
centerOfMassVy = vyb
centerOfMassVz = vzb
else
centerOfMassVx = (vxa*massA + vxb*massB)/massSum
centerOfMassVy = (vya*massA + vyb*massB)/massSum
centerOfMassVz = (vza*massA + vzb*massB)/massSum
end
return true, xa, ya, za, xb, yb, zb, dist,
vxa, vya, vza, vxb, vyb, vzb,
dx, dy, dz, dvx, dvy, dvz,
nx, ny, nz,
collisionX, collisionY, collisionZ, overlap,
massA, massB,
normalSpeed, totalSpeed, tangentialSpeed,
centerOfMassVx, centerOfMassVy, centerOfMassVz
end
-------------
--Callbacks
-------------
local function WidgetBounce3D(entity, widget)
local isValidCollision, xa, ya, za, xb, yb, zb, dist,
vxa, vya, vza, vxb, vyb, vzb,
dx, dy, dz, dvx, dvy, dvz,
nx, ny, nz,
collisionX, collisionY, collisionZ, overlap,
massA, massB,
normalSpeed, totalSpeed, tangentialSpeed,
centerOfMassVx, centerOfMassVy, centerOfMassVz = WidgetCollisionMath3D(entity, widget)
if not isValidCollision then
return
end
local elasticity
local callbackName
local damageName
local isUnit = false
if WidgetIsUnit(widget) then
isUnit = true
elasticity = sqrt((entity.elasticity or 1)*(WidgetIdElasticity[GetUnitTypeId(widget)] or DEFAULT_UNIT_ELASTICITY))
callbackName = "onUnitCallback"
damageName = "onUnitDamage"
elseif WidgetIsDestructable(widget) then
elasticity = sqrt((entity.elasticity or 1)*(WidgetIdElasticity[GetDestructableTypeId(widget)] or DEFAULT_DESTRUCTABLE_ELASTICITY))
callbackName = "onDestructableCallback"
damageName = "onDestructableDamage"
else
elasticity = sqrt((entity.elasticity or 1)*(WidgetIdElasticity[GetItemTypeId(widget)] or DEFAULT_ITEM_ELASTICITY))
callbackName = "onItemCallback"
damageName = "onItemDamage"
end
local e = (1 + elasticity)
local Advx = vxa - centerOfMassVx
local Advy = vya - centerOfMassVy
local Advz = vza - centerOfMassVz
local Bdvx = vxb - centerOfMassVx
local Bdvy = vyb - centerOfMassVy
local Bdvz = vzb - centerOfMassVz
--Householder transformation.
local H11 = 1 - e*nx^2
local H12 = -e*nx*ny
local H13 = -e*nx*nz
local H21 = H12
local H22 = 1 - e*ny^2
local H23 = -e*ny*nz
local H31 = H13
local H32 = H23
local H33 = 1 - e*nz^2
local massRatio = GetMassRatio(massA, massB)
local xNew, yNew, zNew, recoilX, recoilY, recoilZ
if massRatio < 1 then
xNew = xa + 1.001*nx*overlap*(1 - massRatio)
yNew = ya + 1.001*ny*overlap*(1 - massRatio)
zNew = za + 1.001*nz*overlap*(1 - massRatio)
recoilX = H11*Advx + H12*Advy + H13*Advz + centerOfMassVx - vxa
recoilY = H21*Advx + H22*Advy + H23*Advz + centerOfMassVy - vya
recoilZ = H31*Advx + H32*Advy + H33*Advz + centerOfMassVz - vza
EntityRecoilAndDisplace3D(entity, xNew, yNew, zNew, recoilX, recoilY, recoilZ)
end
if massRatio > 0 then
xNew = xb - 1.001*nx*overlap*massRatio
yNew = yb - 1.001*ny*overlap*massRatio
zNew = zb - 1.001*nz*overlap*massRatio
recoilX = H11*Bdvx + H12*Bdvy + H13*Bdvz + centerOfMassVx - vxb
recoilY = H21*Bdvx + H22*Bdvy + H23*Bdvz + centerOfMassVy - vyb
recoilZ = H31*Bdvx + H32*Bdvy + H33*Bdvz + centerOfMassVz - vzb
if isUnit then
SetUnitX(widget, xNew)
SetUnitY(widget, yNew)
CAT_Knockback(widget, recoilX, recoilY, recoilZ)
end
end
if entity[damageName] then
DamageWidget(entity, widget)
end
local callback = FindCallback(entity, callbackName)
if callback then
callback(entity, widget, collisionX, collisionY, collisionZ, normalSpeed, tangentialSpeed, totalSpeed, centerOfMassVx, centerOfMassVy, centerOfMassVz)
end
end
local function WidgetImpact3D(entity, widget)
local isValidCollision, xa, ya, za, xb, yb, zb, dist,
vxa, vya, vza, vxb, vyb, vzb,
dx, dy, dz, dvx, dvy, dvz,
nx, ny, nz,
collisionX, collisionY, collisionZ, overlap,
massA, massB,
normalSpeed, totalSpeed, tangentialSpeed,
centerOfMassVx, centerOfMassVy, centerOfMassVz = WidgetCollisionMath3D(entity, widget)
if massA > 0 then
local recoilX, recoilY, recoilZ
local massRatio = GetMassRatio(massA, massB)
if massRatio > 0 then
recoilX = dvx*massRatio
recoilY = dvy*massRatio
recoilZ = dvz*massRatio
if WidgetIsUnit(widget) then
CAT_Knockback(widget, recoilX, recoilY, recoilZ)
end
end
end
local callbackName
local damageName
if WidgetIsUnit(widget) then
callbackName = "onUnitCallback"
damageName = "onUnitDamage"
elseif WidgetIsDestructable(widget) then
callbackName = "onDestructableCallback"
damageName = "onDestructableDamage"
else
callbackName = "onItemCallback"
damageName = "onItemDamage"
end
if entity[damageName] then
DamageWidget(entity, widget)
end
local callback = FindCallback(entity, callbackName)
if callback then
callback(entity, widget, collisionX, collisionY, collisionZ, normalSpeed, tangentialSpeed, totalSpeed, centerOfMassVx, centerOfMassVy, centerOfMassVz)
end
Backtrack(entity, vxa, vya, vza, collisionX, collisionY, collisionZ)
ALICE_Kill(entity)
end
local function WidgetDevour3D(entity, widget)
local isValidCollision, xa, ya, za, xb, yb, zb, dist,
vxa, vya, vza, vxb, vyb, vzb,
dx, dy, dz, dvx, dvy, dvz,
nx, ny, nz,
collisionX, collisionY, collisionZ, overlap,
massA, massB,
normalSpeed, totalSpeed, tangentialSpeed,
centerOfMassVx, centerOfMassVy, centerOfMassVz = WidgetCollisionMath3D(entity, widget)
if massA > 0 then
local recoilX, recoilY, recoilZ
local massRatio = GetMassRatio(massA, massB)
if massRatio > 0 then
recoilX = -dvx*(1 - massRatio)
recoilY = -dvy*(1 - massRatio)
recoilZ = -dvz*(1 - massRatio)
EntityRecoil3D(entity, recoilX, recoilY, recoilZ)
end
end
local callbackName
local damageName
if WidgetIsUnit(widget) then
callbackName = "onUnitCallback"
damageName = "onUnitDamage"
elseif WidgetIsDestructable(widget) then
callbackName = "onDestructableCallback"
damageName = "onDestructableDamage"
else
callbackName = "onItemCallback"
damageName = "onItemDamage"
end
if entity[damageName] then
DamageWidget(entity, widget)
end
local callback = FindCallback(entity, callbackName)
if callback then
callback(entity, widget, collisionX, collisionY, collisionZ, normalSpeed, tangentialSpeed, totalSpeed, centerOfMassVx, centerOfMassVy, centerOfMassVz)
end
ALICE_Kill(widget)
end
local function WidgetAnnihilate3D(entity, widget)
local isValidCollision, xa, ya, za, xb, yb, zb, dist,
vxa, vya, vza, vxb, vyb, vzb,
dx, dy, dz, dvx, dvy, dvz,
nx, ny, nz,
collisionX, collisionY, collisionZ, overlap,
massA, massB,
normalSpeed, totalSpeed, tangentialSpeed,
centerOfMassVx, centerOfMassVy, centerOfMassVz = WidgetCollisionMath3D(entity, widget)
local callbackName
if WidgetIsUnit(widget) then
callbackName = "onUnitCallback"
elseif WidgetIsDestructable(widget) then
callbackName = "onDestructableCallback"
else
callbackName = "onItemCallback"
end
local callback = FindCallback(entity, callbackName)
if callback then
callback(entity, widget, collisionX, collisionY, collisionZ, normalSpeed, tangentialSpeed, totalSpeed, centerOfMassVx, centerOfMassVy, centerOfMassVz)
end
Backtrack(entity, vxa, vya, vza, collisionX, collisionY, collisionZ)
ALICE_Kill(entity)
ALICE_Kill(widget)
end
local function WidgetPassThrough3D(entity, widget)
local callbackName
local damageName
if WidgetIsUnit(widget) then
callbackName = "onUnitCallback"
damageName = "onUnitDamage"
elseif WidgetIsDestructable(widget) then
callbackName = "onDestructableCallback"
damageName = "onDestructableDamage"
else
callbackName = "onItemCallback"
damageName = "onItemDamage"
end
if entity[damageName] then
DamageWidget(entity, widget)
end
local callback = FindCallback(entity, callbackName)
if callback then
local isValidCollision, xa, ya, za, xb, yb, zb, dist,
vxa, vya, vza, vxb, vyb, vzb,
dx, dy, dz, dvx, dvy, dvz,
nx, ny, nz,
collisionX, collisionY, collisionZ, overlap,
massA, massB,
normalSpeed, totalSpeed, tangentialSpeed,
centerOfMassVx, centerOfMassVy, centerOfMassVz = WidgetCollisionMath3D(entity, widget)
callback(entity, widget, collisionX, collisionY, collisionZ, normalSpeed, tangentialSpeed, totalSpeed, centerOfMassVx, centerOfMassVy, centerOfMassVz)
end
ALICE_PairDisable()
end
CAT_UnitBounce3D = WidgetBounce3D
CAT_UnitImpact3D = WidgetImpact3D
CAT_UnitDevour3D = WidgetDevour3D
CAT_UnitAnnihilate3D = WidgetAnnihilate3D
CAT_UnitPassThrough3D = WidgetPassThrough3D
CAT_DestructableBounce3D = WidgetBounce3D
CAT_DestructableImpact3D = WidgetImpact3D
CAT_DestructableDevour3D = WidgetDevour3D
CAT_DestructableAnnihilate3D = WidgetAnnihilate3D
CAT_DestructablePassThrough3D = WidgetPassThrough3D
CAT_ItemBounce3D = WidgetBounce3D
CAT_ItemImpact3D = WidgetImpact3D
CAT_ItemDevour3D = WidgetDevour3D
CAT_ItemAnnihilate3D = WidgetAnnihilate3D
CAT_ItemPassThrough3D = WidgetPassThrough3D
--===========================================================================================================================================================
--Entity-Unit Collisions
--==========================================================================================================================================================
--------------------
--Collision Checks
--------------------
local function InitUnitCollisionCheck3D(entity, unit)
local data = ALICE_PairLoadData()
local id = GetUnitTypeId(unit)
local collisionSize = BlzGetUnitCollisionSize(unit)
data.radius = WidgetIdRadius[id] or collisionSize
data.height = WidgetIdHeight[id] or DEFAULT_UNIT_HEIGHT_FACTOR*collisionSize
data.collisionRange = data.radius + entity.collisionRadius
data.maxdz = data.height/2 + entity.collisionRadius
entity.maxSpeed = entity.maxSpeed or DEFAULT_ENTITY_MAX_SPEED
end
function CAT_UnitCollisionCheck3D(entity, unit)
local data = ALICE_PairLoadData()
local dx, dy, dz
if entity.x then
local x, y, z = ALICE_GetCoordinates3D(unit)
dx, dy, dz = x - entity.x, y - entity.y, z - entity.z
else
local xa, ya, za, xb, yb, zb = ALICE_PairGetCoordinates3D()
dx, dy, dz = xb - xa, yb - ya, zb - za
end
local horiDist = sqrt(dx^2 + dy^2)
local dist
if horiDist < data.collisionRange and dz < data.maxdz and dz > -data.maxdz then
local dtop = data.height/2 - dz
local dbottom = data.height/2 + dz
if dtop < data.radius then
dist = sqrt(horiDist^2 + (dtop - data.radius)^2)
elseif dbottom < data.radius then
if GetUnitFlyHeight(unit) > 0 then
dist = sqrt(horiDist^2 + (dbottom - data.radius)^2)
else
dist = horiDist
end
else
dist = horiDist
end
if dist < data.collisionRange then
local callback = FindCallback(entity, "onUnitCollision")
if callback then
callback(entity, unit)
else
ALICE_Kill(entity)
end
end
else
dist = sqrt(horiDist^2 + dz^2)
end
return (horiDist - data.collisionRange)/(entity.maxSpeed + UNIT_MAX_SPEED)
end
--===========================================================================================================================================================
--Entity-Destructable Collisions
--==========================================================================================================================================================
--------------------
--Collision Checks
--------------------
local function InitDestructableCollisionCheck3D(entity, destructable)
local data = ALICE_PairLoadData()
local id = GetDestructableTypeId(destructable)
data.radius = WidgetIdRadius[id] or DEFAULT_DESTRUCTABLE_RADIUS
data.height = WidgetIdHeight[id] or DEFAULT_DESTRUCTABLE_HEIGHT
data.collisionRange = data.radius + entity.collisionRadius
data.maxdz = data.height/2 + entity.collisionRadius
entity.maxSpeed = entity.maxSpeed or DEFAULT_ENTITY_MAX_SPEED
data.xDest, data.yDest, data.zDest = ALICE_GetCoordinates3D(destructable)
end
function CAT_DestructableCollisionCheck3D(entity, destructable)
local data = ALICE_PairLoadData()
local dx, dy, dz
if entity.x then
dx, dy, dz = data.xDest - entity.x, data.yDest - entity.y, data.zDest - entity.z
else
local x, y, z = ALICE_GetCoordinates3D(entity)
dx, dy, dz = data.xDest - x, data.yDest - y, data.zDest - z
end
local horiDist = sqrt(dx^2 + dy^2)
local dist
if horiDist < data.collisionRange and dz < data.maxdz and dz > -data.maxdz then
local dtop = data.height/2 - dz
if dtop < data.radius then
dist = sqrt(horiDist^2 + (dtop - data.radius)^2)
else
dist = horiDist
end
if dist < data.collisionRange then
local callback = FindCallback(entity, "onDestructableCollision")
if callback then
callback(entity, destructable)
else
ALICE_Kill(entity)
end
end
else
dist = sqrt(horiDist^2 + dz^2)
end
if entity.isResting then
ALICE_PairPause()
end
return (horiDist - data.collisionRange)/entity.maxSpeed
end
--===========================================================================================================================================================
--Entity-Item Collisions
--==========================================================================================================================================================
--------------------
--Collision Checks
--------------------
local function InitItemCollisionCheck3D(entity, item)
local data = ALICE_PairLoadData()
local id = GetItemTypeId(item)
data.radius = WidgetIdRadius[id] or DEFAULT_ITEM_RADIUS
data.height = WidgetIdHeight[id] or DEFAULT_ITEM_HEIGHT
data.collisionRange = data.radius + entity.collisionRadius
data.maxdz = data.height/2 + entity.collisionRadius
entity.maxSpeed = entity.maxSpeed or DEFAULT_ENTITY_MAX_SPEED
end
function CAT_ItemCollisionCheck3D(entity, item)
local data = ALICE_PairLoadData()
local xa, ya, za, xb, yb, zb = ALICE_PairGetCoordinates3D()
local dx, dy, dz = xb - xa, yb - ya, zb - za
local horiDist = sqrt(dx^2 + dy^2)
local dist
if horiDist < data.collisionRange and dz < data.maxdz and dz > -data.maxdz then
local dtop = data.height/2 - dz
if dtop < data.radius then
dist = sqrt(horiDist^2 + (dtop - data.radius)^2)
else
dist = horiDist
end
if dist < data.collisionRange then
local callback = FindCallback(entity, "onItemCollision")
if callback then
callback(entity, item)
else
ALICE_Kill(entity)
end
end
else
dist = sqrt(horiDist^2 + dz^2)
end
if entity.isResting then
ALICE_PairPause()
end
return (horiDist - data.collisionRange)/entity.maxSpeed
end
--===========================================================================================================================================================
--Init
--==========================================================================================================================================================
local function InitCollisionsCAT()
Require "ALICE"
Require "CAT_Objects"
INTERVAL = ALICE_MIN_INTERVAL
VISUAL = ALICE_CLASS_FIELD_EFFECT
ALICE_FuncSetInit(CAT_UnitCollisionCheck3D, InitUnitCollisionCheck3D)
ALICE_FuncSetInit(CAT_DestructableCollisionCheck3D, InitDestructableCollisionCheck3D)
ALICE_FuncSetInit(CAT_ItemCollisionCheck3D, InitItemCollisionCheck3D)
end
OnInit.global("CAT_Collisions", InitCollisionsCAT)
end
if Debug then Debug.beginFile "CAT Ballistics" end
do
--[[
=============================================================================================================================================================
Complementary ALICE Template
by Antares
Requires:
ALICE https://www.hiveworkshop.com/threads/a-l-i-c-e-interaction-engine.353126/
Objects CAT
PrecomuptedHeightMap https://www.hiveworkshop.com/threads/precomputed-synchronized-terrain-height-map.353477/
TotalInitialization https://www.hiveworkshop.com/threads/total-initialization.317099/
=============================================================================================================================================================
B A L L I S T I C S
=============================================================================================================================================================
This template includes a function for realistic, ballistic and sliding movement for entities* as well as some helper functions for ballistic movement. To add
ballistic movement to an entity, add the CAT_MoveBallistic function to their self-interaction table (for an example, see Entities CAT).
*Objects represented by a table with coordinate fields .x, .y, .z, velocity fields .vx, .vy, .vz, and a special effect (field name set in ALICE, default .visual).
CAT_MoveBallistic requires the .collisionRadius field for ground-bound sliding movement. To become ground-bound, an entity requires CAT_TerrainCollisionCheck
to be added to its self-iteraction table, and CAT_TerrainBounce set for its .onTerrainCollision field (Entities CAT).
An entity becomes ground-bound when it bounces off the surface with less than the MINIMUM_BOUNCE_VELOCITY (Objects CAT). While ground-bound, an object will slide
realistically across the surface with a friction determined by the .friction field. The value is interpreted as the deceleration experienced per second. It is
multiplied with TerrainTypeFriction, which can also be set in the Objects CAT.
Once a ground-bound entity has come to rest, its friction increases by the STATIC_FRICTION_FACTOR (Objects CAT) and a greater force is required to displace it
again. The ballistic movement function writes values into the entity table to determine its current state. These values are used by other functions. The
.isResting field is used to detect if certain interactions can be paused to save computation resources. This means, that, even large numbers of resting entities
do not affect performance in a significant way.
-------------------------------------------------------------------------------------------------------------------------------------------------------------
Two helper functions for ballistic projectiles are included in this template.
CAT_GetBallisticLaunchSpeedFromVelocity takes a velocity and returns the x, y, and z-velocities that would cause a ballistic projectile launched with the
specified velocity from the launch coordinates to impact at the specified target coordinates. There are usually two solutions. The highArc flag specifies if
the function should search for the solution with the higher angle first. It is possible that there is no solution for a given velocity. In that case, the
function will return nil.
CAT_GetBallisticLaunchSpeedFromAngle takes an angle and returns the x, y, and z-velocities that would cause the proctile launched at that angle from the launch
coordinates to impact at the specified target coordinates. Unlike the from-velocity version, this function always has a solution as long as the vertical angle
between the launch and target coordinates is not greater than the specified launch angle. In that case, the function will return nil.
=============================================================================================================================================================
L I S T O F F U N C T I O N S
=============================================================================================================================================================
CAT_MoveBallistic
CAT_GetBallisticLaunchSpeedFromVelocity(xLaunch, yLaunch, zLaunch, xTarget, yTarget, zTarget, velocity, highArc) -> vx, vy, vz
CAT_GetBallisticLaunchSpeedFromAngle(xLaunch, yLaunch, zLaunch, xTarget, yTarget, zTarget, angle) -> vx, vy, vz
=============================================================================================================================================================
]]
local sqrt = math.sqrt
local atan2 = math.atan
local cos = math.cos
local sin = math.sin
local tan = math.tan
local abs = math.abs
local PI = bj_PI
local function InitMoveBallistic(entity)
entity.visualZ = entity.visualZ or 0
entity.isAirborne = entity.z ~= nil
entity.z = entity.z or GetCliffAdjustedZ(entity.x, entity.y) + entity.collisionRadius
entity.vz = entity.vz or 0
entity.vzOld = entity.vz
entity.theta = atan2(entity.vz, sqrt(entity.vx^2 + entity.vy^2))
if entity.launchOffset then
CAT_LaunchOffset(entity)
end
end
---@param entity table
function CAT_MoveBallistic(entity)
if entity.isResting and entity.vx == 0 and entity.vy == 0 and entity.vz == 0 then
return
elseif entity.isAirborne then
--Ballistic movement
entity.x = entity.x + entity.vx*INTERVAL
entity.y = entity.y + entity.vy*INTERVAL
entity.z = entity.z + entity.vz*INTERVAL - 0.5*GRAVITY*INTERVAL^2
entity.vz = entity.vz - GRAVITY*INTERVAL
entity.vzOld = entity.vz
BlzSetSpecialEffectPosition(entity[VISUAL], entity.x, entity.y, entity.z + entity.visualZ)
if entity.isResting then
ALICE_Unpause(entity)
entity.isResting = false
end
else
--Check if object has received external force that could catapult it off the ground.
if entity.vz > entity.vzOld + MINIMUM_BOUNCE_VELOCITY or entity.vz < entity.vzOld - MINIMUM_BOUNCE_VELOCITY/entity.elasticity then
entity.isAirborne = true
ALICE_Unpause(entity)
ALICE_SetStationary(entity, false)
entity.vzOld = entity.vz
CAT_MoveBallistic(entity)
return
end
--Ground movement
local vHorizontal = sqrt(entity.vx^2 + entity.vy^2)
if vHorizontal > 0 then
--Determine if object is too fast to remain in contact with downwards curved surface.
local freeFallCurvature = -GRAVITY/(vHorizontal^2)
local deltaScanX = 2*entity.vx*INTERVAL
local deltaScanY = 2*entity.vy*INTERVAL
local deltaScanDist = sqrt(deltaScanX^2 + deltaScanY^2)
local z1 = GetCliffAdjustedZ(entity.x - deltaScanX, entity.y - deltaScanY)
local z2 = GetCliffAdjustedZ(entity.x + deltaScanX, entity.y + deltaScanY)
local z = GetCliffAdjustedZ(entity.x, entity.y)
local curvature = (z2 + z1 - 2*z)*1/(128*deltaScanDist)
if freeFallCurvature > curvature then
entity.isAirborne = true
ALICE_Unpause(entity)
entity.x = entity.x + entity.vx*INTERVAL
entity.y = entity.y + entity.vy*INTERVAL
entity.z = entity.z + entity.vz*INTERVAL
BlzSetSpecialEffectPosition(entity[VISUAL], entity.x, entity.y, entity.z + entity.visualZ)
else
local frictionMultiplier = (freeFallCurvature - curvature)/freeFallCurvature --If the object is pressed against the surface due to its curvature, the friction increases.
local curvatureAngle
--Check if the curvature of the surface is so high that an object of its size would get reflected like it hit a wall.
if curvature > 0.0015 then --Don't run this calculation unless curvature is high enough (dz > ~50)
curvatureAngle = atan2(z2 - z, 32) - atan2(z - z1, 32) --Gradient angle difference between two neighboring tiles.
if MAX_SLIDING_CURVATURE_RADIUS*(PI/2 - curvatureAngle)/curvatureAngle < entity.collisionRadius then
local xCollision, yCollision, zCollision = entity.x, entity.y, entity.z
local invVTotal = 1/sqrt(entity.vx^2 + entity.vy^2 + entity.vz^2)
local i = 1
repeat
xCollision = xCollision + 10*entity.vx*invVTotal
yCollision = yCollision + 10*entity.vy*invVTotal
zCollision = zCollision + 10*entity.vz*invVTotal
i = i + 1
until zCollision < GetCliffAdjustedZ(xCollision, yCollision) or i > 50
if i <= 50 then
CAT_TerrainBounce(entity, xCollision, yCollision)
ALICE_Unpause(entity)
entity.isAirborne = true
return
end
end
end
local x, y = entity.x, entity.y
--Get surface orientation.
local gradientX = (GetCliffAdjustedZ(x + 16, y) - GetCliffAdjustedZ(x - 16, y))*0.0313
local gradientY = (GetCliffAdjustedZ(x, y + 16) - GetCliffAdjustedZ(x, y - 16))*0.0313
local gradientSquared = gradientX^2 + gradientY^2
local oldvx = entity.vx
local oldvy = entity.vy
local oldTheta = entity.theta
--Gravitational force
local factor = sqrt(gradientSquared + 1)*GRAVITY
local gx = gradientX/factor
local gy = gradientY/factor
local newvx = oldvx - gx*INTERVAL
local newvy = oldvy - gy*INTERVAL
local phi = atan2(newvy, newvx)
vHorizontal = sqrt(newvx^2 + newvy^2)
--Surface friction
local textureFactor
if DIFFERENT_SURFACE_FRICTIONS then
textureFactor = TerrainTypeFriction[GetTerrainType(x, y)] or 1
else
textureFactor = 1
end
local totalFriction = (entity.friction*textureFactor*frictionMultiplier)*INTERVAL
if vHorizontal < STATIC_FRICTION_FACTOR*totalFriction then
vHorizontal = 0
else
vHorizontal = vHorizontal - totalFriction
end
entity.x = x + (oldvx + newvx)/2*INTERVAL
entity.y = y + (oldvy + newvy)/2*INTERVAL
entity.z = GetCliffAdjustedZ(entity.x, entity.y) + entity.collisionRadius
entity.vx = vHorizontal*cos(phi)
entity.vy = vHorizontal*sin(phi)
entity.vz = gradientX*entity.vx + gradientY*entity.vy
entity.theta = atan2(entity.vz, vHorizontal)
--Conversion of momentum on upward-curved surface.
if oldTheta < entity.theta - 0.001 then
local conversion = cos(entity.theta)/cos(oldTheta)
entity.vx = entity.vx*conversion
entity.vy = entity.vy*conversion
entity.vz = entity.vz*conversion
end
entity.vzOld = entity.vz
BlzSetSpecialEffectPosition(entity[VISUAL], entity.x, entity.y, entity.z + entity.visualZ)
end
if entity.isResting then
ALICE_Unpause(entity)
ALICE_SetStationary(entity, false)
entity.isResting = false
end
elseif not entity.isResting then
--If object has come to a full stop, check if it is stable, and if so, set it to resting.
local x, y = entity.x, entity.y
local gradientX = (GetCliffAdjustedZ(x + 16, y) - GetCliffAdjustedZ(x - 16, y))*0.0313
local gradientY = (GetCliffAdjustedZ(x, y + 16) - GetCliffAdjustedZ(x, y - 16))*0.0313
local gradientSquared = gradientX^2 + gradientY^2
local gx = gradientX/sqrt(gradientSquared + 1)*GRAVITY
local gy = gradientY/sqrt(gradientSquared + 1)*GRAVITY
local g = sqrt(gx^2 + gy^2)
local textureFactor = 1
if DIFFERENT_SURFACE_FRICTIONS then
textureFactor = TerrainTypeFriction[GetTerrainType(x, y)] or 1
end
if g > STATIC_FRICTION_FACTOR*entity.friction*textureFactor then
local phi = atan2(gy, gx)
entity.vx = (g - entity.friction)*cos(phi)*INTERVAL
entity.vy = (g - entity.friction)*sin(phi)*INTERVAL
entity.vz = gradientX*entity.vx + gradientY*entity.vy
entity.vzOld = entity.vz
entity.x = x + entity.vx*INTERVAL/2
entity.y = y + entity.vy*INTERVAL/2
entity.z = GetCliffAdjustedZ(entity.x, entity.y) + entity.collisionRadius
else
ALICE_SetStationary(entity, true)
entity.isResting = true
end
BlzSetSpecialEffectPosition(entity[VISUAL], entity.x, entity.y, entity.z + entity.visualZ)
end
end
end
---@param xLaunch number
---@param yLaunch number
---@param zLaunch number
---@param xTarget number
---@param yTarget number
---@param zTarget number
---@param velocity number
---@param highArc? boolean
---@return number | nil, number | nil, number | nil
function CAT_GetBallisticLaunchSpeedFromVelocity(xLaunch, yLaunch, zLaunch, xTarget, yTarget, zTarget, velocity, highArc)
local dx = xTarget - xLaunch
local dy = yTarget - yLaunch
local dist = sqrt(dx^2 + dy^2)
local stepSize = PI/8
local epsilon = 0.01
local goUp = not highArc
local guess
if highArc then
guess = PI/2.001
else
guess = -PI/2.001
end
local zGuess = zLaunch + dist*tan(guess) - 0.5*GRAVITY*(dist/(cos(guess)*velocity))^2
local delta
local deltaPrevious
for __ = 1, 100 do
if goUp then
guess = guess + stepSize
else
guess = guess - stepSize
end
zGuess = zLaunch + dist*tan(guess) - 0.5*GRAVITY*(dist/(cos(guess)*velocity))^2
deltaPrevious = delta
delta = zGuess - zTarget
if abs(delta) < epsilon then
break
end
if deltaPrevious and (delta > 0) ~= (deltaPrevious > 0) then
goUp = not goUp
stepSize = stepSize*(1 - abs(deltaPrevious)/(abs(deltaPrevious) + abs(delta)))
end
end
if delta > epsilon or delta < -epsilon then
return nil, nil, nil
end
return velocity*cos(guess)*dx/dist, velocity*cos(guess)*dy/dist, velocity*sin(guess)
end
---@param xLaunch number
---@param yLaunch number
---@param zLaunch number
---@param xTarget number
---@param yTarget number
---@param zTarget number
---@param angle number
---@return number | nil, number | nil, number | nil
function CAT_GetBallisticLaunchSpeedFromAngle(xLaunch, yLaunch, zLaunch, xTarget, yTarget, zTarget, angle)
local dx = xTarget - xLaunch
local dy = yTarget - yLaunch
local dz = zTarget - zLaunch
local dist = sqrt(dx^2 + dy^2)
local vSquared = (GRAVITY*dist^2/(2*cos(angle)^2))/(dist*tan(angle) - dz)
if vSquared < 0 then
return nil, nil, nil
end
local velocity = sqrt(vSquared)
return velocity*cos(angle)*dx/dist, velocity*sin(angle)*dy/dist, velocity*sin(angle)
end
local function InitMoveablesCAT()
Require "ALICE"
Require "CAT_Objects"
Require "PrecomputedHeightMap"
INTERVAL = ALICE_MIN_INTERVAL
VISUAL = ALICE_CLASS_FIELD_EFFECT
ALICE_FuncSetInit(CAT_MoveBallistic, InitMoveBallistic)
end
OnInit.global("CAT_Ballistics", InitMoveablesCAT)
end
if Debug then Debug.beginFile "CAT Forces" end ---@diagnostic disable: need-check-nil
do
--[[
=============================================================================================================================================================
Complementary ALICE Template
by Antares
Requires:
ALICE https://www.hiveworkshop.com/threads/a-l-i-c-e-interaction-engine.353126/
Objects CAT
TotalInitialization https://www.hiveworkshop.com/threads/total-initialization.317099/
=============================================================================================================================================================
F O R C E S
=============================================================================================================================================================
The forces library allows you to create interactions where objects are pushing or pulling other objects in the vicinity. A force can affect units and entities,
but the source cannot be a unit. To enable a unit as the source of a force, create a class as the source of the force and anchor it to that unit. There are
preset force functions packaged with this CAT that you can use, but you can also create your own force functions.
To add a force to a class, use the corresponding AddForce function on your class on map initialization. The function will add the necessary entries to your
class table to make the class instances a source of the force field.
To define your own force, you need to define three functions:
InitFunc This function is called when a pair is created for this force interaction. It can be used to precompute data and write it into the pair matrix.
ForceFunc This function determines the strength and direction of the force. It receives 10 input arguments:
(source, target, matrixData, distance, deltaX, deltaY, deltaZ, deltaVelocityX, deltaVelocityY, deltaVelocityZ). It returns three numbers:
(forceX, forceY, forceZ).
IntervalFunc This function determines the interaction interval of your force (the same as the ALICE interactionFunc interval). A lower interval makes the
force more accurate. It receives the same 10 input arguments and returns a number.
The preset force functions can serve as a blueprint for your custom functions.
=============================================================================================================================================================
L I S T O F F U N C T I O N S
=============================================================================================================================================================
The pairsWith parameter is a string or a string sequence listing identifiers of objects that the force should affect.
CAT_AddGravity3D(entity, pairsWith) A long-range attraction that gets stronger as objects move closer together. Uses the
mass field to determine the force strength.
CAT_AddPressure3D(entity, pairsWith) Pushes nearby objects away. The strength is determined by the .pressureStrength field.
The coupling strength with objects is based on the .collisionRadius in the case of
entities, and the collision radius and height in the case of units. The field
.pressureMaxRange determines the maximum range of the force. The force will drop
gradually until it hits zero at that range.
CAT_AddWindField3D(entity, pairsWith) A force that pushes objects into a certain direction. The .windFieldDensity field
determines the strength of the wind field. The .windFieldSpeed field determines the
speed of the wind, the field .windFieldAnglePhi the horizontal direction of the wind,
and the field .windFieldAngleTheta the vertical direction. The .windFieldMaxRange
field determines the radius of the field. The wind density will drop gradually until
it hits zero at that range.
CAT_AddForce3D(entity, pairsWith, initFunc, forceFunc, intervalFunc) Add a custom force to the entity.
=============================================================================================================================================================
C O N F I G
=============================================================================================================================================================
]]
local FORCE_UNIT_MULTIPLIER = 0.3 ---@type number
--Multiplies the forces on a unit (compared to an entity) by this factor.
local GRAVITY_STRENGTH = 50000 ---@type number
--Determines the strength of gravity.
local GRAVITY_ACCURACY = 10 ---@type number
--Determines how often the force fields are updated for gravity. A greater accuracy requires more calculations. Use debug mode to gauge the appropriate accuracy.
local DEFAULT_GRAVITY_MIN_DISTANCE = 128 ---@type number
--Avoids infinities by making the gravity strength not increase further when objects move closer to each other than this value. Can be overwritten with the
--.minDistance field.
local PRESSURE_ACCURACY = 0.05 ---@type number
--Determines how often the force fields are updated for the pressure force. A greater accuracy requires more calculations. Use debug mode to gauge the
--appropriate accuracy.
local WIND_FIELD_ACCURACY = 0.1 ---@type number
--Determines how often the force fields are updated for the wind force. A greater accuracy requires more calculations. Use debug mode to gauge the appropriate
--accuracy.
--===========================================================================================================================================================
local sqrt = math.sqrt
local cos = math.cos
local sin = math.sin
local INTERVAL = nil
local unitForce = {}
local SPHERE_CROSS_SECTION = 4 - 8/bj_PI
local CYLINDER_CROSS_SECTION = 2/bj_PI
local function GetObjectCrossSection(object)
if type(object) == "table" then
if object.collisionRadius then
return SPHERE_CROSS_SECTION*object.collisionRadius
elseif object.anchor then
if type(object.anchor) == "table" then
return SPHERE_CROSS_SECTION*object.anchor.collisionRadius
elseif WidgetIsUnit(object.anchor) then
local collisionSize = BlzGetUnitCollisionSize(object.anchor)
return CYLINDER_CROSS_SECTION*collisionSize*(WidgetIdHeight[GetUnitTypeId(object.anchor)] or DEFAULT_UNIT_HEIGHT_FACTOR*collisionSize)
else
return 0
end
else
return 0
end
elseif WidgetIsUnit(object) then
local collisionSize = BlzGetUnitCollisionSize(object)
return CYLINDER_CROSS_SECTION*collisionSize*(WidgetIdHeight[GetUnitTypeId(object)] or DEFAULT_UNIT_HEIGHT_FACTOR*collisionSize)
else
return 0
end
end
--===========================================================================================================================================================
--3D Force Functions
--===========================================================================================================================================================
local function InitAccelerateEntity3D(entity)
entity.multiplier = INTERVAL/CAT_GetObjectMass(entity)
if entity.multiplier == math.huge then
error("Attempting to apply force to object for which no mass was assigned.")
end
end
local function AccelerateEntity3D(entity)
if entity.Fx == 0 and entity.Fy == 0 and entity.Fz == 0 then
ALICE_PairPause()
return
end
if entity.anchor then
if type(entity.anchor) == "table" then
entity.anchor.vx = entity.anchor.vx + entity.Fx*entity.multiplier
entity.anchor.vy = entity.anchor.vy + entity.Fy*entity.multiplier
entity.anchor.vz = entity.anchor.vz + entity.Fz*entity.multiplier
else
CAT_Knockback(entity.anchor, entity.Fx*entity.multiplier, entity.Fy*entity.multiplier, entity.Fz*entity.multiplier)
end
else
entity.vx = entity.vx + entity.Fx*entity.multiplier
entity.vy = entity.vy + entity.Fy*entity.multiplier
entity.vz = entity.vz + entity.Fz*entity.multiplier
end
entity.Fx = entity.Fx + entity.Fxdt*INTERVAL
entity.Fy = entity.Fy + entity.Fydt*INTERVAL
entity.Fz = entity.Fz + entity.Fzdt*INTERVAL
end
local function ResumForceEntity3D(entity)
entity.Fx, entity.Fy, entity.Fz = 0, 0, 0
entity.Fxdt, entity.Fydt, entity.Fzdt = 0, 0, 0
local row = ALICE_LoadMatrixRow(entity, ResumForceEntity3D)
for __, table in ipairs(row) do
entity.Fx = entity.Fx + table.Fx
entity.Fy = entity.Fy + table.Fy
entity.Fz = entity.Fz + table.Fz
entity.Fxdt = entity.Fxdt + table.Fxdt
entity.Fydt = entity.Fydt + table.Fydt
entity.Fzdt = entity.Fzdt + table.Fzdt
end
if entity.Fx == 0 and entity.Fy == 0 and entity.Fz == 0 then
ALICE_PairPause()
end
return 5.0
end
local function ClearUnitForce(unit, __, __)
unitForce[unit] = nil
end
local function InitAccelerateUnit3D(unit)
local data = ALICE_PairLoadData()
ALICE_PairOnDestroy(ClearUnitForce)
data.multiplier = INTERVAL/CAT_GetObjectMass(unit)
if data.multiplier == math.huge then
error("Attempting to apply force to unit with no mass.")
end
end
local function AccelerateUnit3D(unit)
local data = ALICE_PairLoadData()
local forceTable = unitForce[unit]
if not forceTable or (forceTable.Fx == 0 and forceTable.Fy == 0 and forceTable.Fz == 0) then
ALICE_PairPause()
return
end
CAT_Knockback(unit, forceTable.Fx*data.multiplier, forceTable.Fy*data.multiplier, forceTable.Fz*data.multiplier)
forceTable.Fx = forceTable.Fx + forceTable.Fxdt*INTERVAL
forceTable.Fy = forceTable.Fy + forceTable.Fydt*INTERVAL
forceTable.Fz = forceTable.Fz + forceTable.Fzdt*INTERVAL
end
local function ResumForceUnit3D(unit)
local forceTable = unitForce[unit]
if forceTable == nil then
ALICE_PairPause()
return 5.0
end
forceTable.Fx, forceTable.Fy, forceTable.Fz = 0, 0, 0
forceTable.Fxdt, forceTable.Fydt, forceTable.Fzdt = 0, 0, 0
local row = ALICE_LoadMatrixRow(unit, ResumForceUnit3D)
for __, table in ipairs(row) do
forceTable.Fx = forceTable.Fx + table.Fx
forceTable.Fy = forceTable.Fy + table.Fy
forceTable.Fz = forceTable.Fz + table.Fz
forceTable.Fxdt = forceTable.Fxdt + table.Fxdt
forceTable.Fydt = forceTable.Fydt + table.Fydt
forceTable.Fzdt = forceTable.Fzdt + table.Fzdt
end
if forceTable.Fx == 0 and forceTable.Fy == 0 and forceTable.Fz == 0 then
ALICE_PairPause()
end
return 5.0
end
--===========================================================================================================================================================
local function ClearForce3D(source, target)
local data, force = ALICE_PairReadMatrix()
local FxOld = force.Fx + force.Fxdt*(ALICE_TimeElapsed - force.lastUpdate)
local FyOld = force.Fy + force.Fydt*(ALICE_TimeElapsed - force.lastUpdate)
local FzOld = force.Fz + force.Fzdt*(ALICE_TimeElapsed - force.lastUpdate)
local FxdtOld = force.Fxdt
local FydtOld = force.Fydt
local FzdtOld = force.Fzdt
local forceTable
if data.targetIsEntity then
forceTable = target
else
forceTable = unitForce[target]
end
if forceTable then
forceTable.Fx = forceTable.Fx - FxOld
forceTable.Fy = forceTable.Fy - FyOld
forceTable.Fz = forceTable.Fz - FzOld
forceTable.Fxdt = forceTable.Fxdt - FxdtOld
forceTable.Fydt = forceTable.Fydt - FydtOld
forceTable.Fzdt = forceTable.Fzdt - FzdtOld
end
end
local function InitForce3D(source, target)
local data, force = ALICE_PairReadMatrix()
force.lastUpdate = ALICE_TimeElapsed
data.targetIsEntity = type(target) == "table"
if data.targetIsEntity then
data.typeMultiplier = 1
if target.Fx == nil then
ALICE_AddSelfInteraction(target, AccelerateEntity3D)
ALICE_AddSelfInteraction(target, ResumForceEntity3D)
target.Fx, target.Fy, target.Fz = 0, 0, 0
target.Fxdt, target.Fydt, target.Fzdt = 0, 0, 0
end
elseif WidgetIsUnit(target) then
data.typeMultiplier = FORCE_UNIT_MULTIPLIER
if unitForce[target] == nil then
unitForce[target] = {}
local forceTable = unitForce[target]
ALICE_AddSelfInteraction(target, AccelerateUnit3D)
ALICE_AddSelfInteraction(target, ResumForceUnit3D)
forceTable.Fx, forceTable.Fy, forceTable.Fz = 0, 0, 0
forceTable.Fxdt, forceTable.Fydt, forceTable.Fzdt = 0, 0, 0
end
end
ALICE_PairOnDestroy(ClearForce3D)
end
local function Force3D(source, target)
local data, force = ALICE_PairReadMatrix()
if data.doUserInit then
if source.initForceFunc then
source.initForceFunc(source, target)
end
data.doUserInit = false
end
local xf, yf, zf, xo, yo, zo = ALICE_PairGetCoordinates3D()
local vxf, vyf, vzf = CAT_GetObjectVelocity3D(source)
local vxo, vyo, vzo = CAT_GetObjectVelocity3D(target)
local dx, dy, dz = xf - xo, yf - yo, zf - zo
local dvx, dvy, dvz = vxf - vxo, vyf - vyo, vzf - vzo
local dist = sqrt(dx^2 + dy^2 + dz^2)
local forceX, forceY, forceZ = source.forceFunc(source, target, data, dist, dx, dy, dz, dvx, dvy, dvz)
local interval = (source.intervalFunc(source, target, data, dist, dx, dy, dz, dvx, dvy, dvz) / INTERVAL + 1)*INTERVAL
if interval > ALICE_MAX_INTERVAL then
interval = ALICE_MAX_INTERVAL
end
--Force at next step.
local dxNS = dx + 0.5*dvx*interval
local dyNS = dy + 0.5*dvy*interval
local dzNS = dz + 0.5*dvz*interval
local distNS = sqrt(dxNS^2 + dyNS^2 + dzNS^2)
local forceXNS, forceYNS, forceZNS = source.forceFunc(source, target, data, distNS, dxNS, dyNS, dzNS, dvx, dvy, dvz)
local forceXdt = 2*(forceXNS - forceX)/interval
local forceYdt = 2*(forceYNS - forceY)/interval
local forceZdt = 2*(forceZNS - forceZ)/interval
local lastInterval = (ALICE_TimeElapsed - force.lastUpdate)
local forceTable
if data.targetIsEntity then
forceTable = target
else
forceTable = unitForce[target]
end
local FxOld = force.Fx + force.Fxdt*lastInterval
local FyOld = force.Fy + force.Fydt*lastInterval
local FzOld = force.Fz + force.Fzdt*lastInterval
if forceTable.Fx == 0 and forceTable.Fy == 0 and forceTable.Fz == 0 and (forceX ~= 0 or forceY ~= 0 or forceZ ~= 0) then
ALICE_Unpause(target)
end
forceTable.Fxdt = forceTable.Fxdt + forceXdt - force.Fxdt
forceTable.Fydt = forceTable.Fydt + forceYdt - force.Fydt
forceTable.Fzdt = forceTable.Fzdt + forceZdt - force.Fzdt
forceTable.Fx = forceTable.Fx + forceX - FxOld
forceTable.Fy = forceTable.Fy + forceY - FyOld
forceTable.Fz = forceTable.Fz + forceZ - FzOld
force.Fx, force.Fy, force.Fz = forceX, forceY, forceZ
force.Fxdt, force.Fydt, force.Fzdt = forceXdt, forceYdt, forceZdt
force.lastUpdate = ALICE_TimeElapsed
return interval
end
--===========================================================================================================================================================
--Gravity
--===========================================================================================================================================================
local function InitGravity(source, target)
local data, __ = ALICE_PairReadMatrix()
local massSource = CAT_GetObjectMass(source)
local massTarget = CAT_GetObjectMass(target)
data.strength = massSource*massTarget*(GRAVITY_STRENGTH*1.0001)*data.typeMultiplier --1.0001 to convert to float to avoid integer overflow
data.minDistance = source.minDistance or DEFAULT_GRAVITY_MIN_DISTANCE
data.intervalFactor = 1/(GRAVITY_ACCURACY*data.strength/massTarget)
if data.strength == 0 then
ALICE_PairDisable()
end
end
local function GravityForce3D(source, target, data, dist, dx, dy, dz, dvx, dvy, dvz)
if data.minDistance > dist then
dist = data.minDistance
end
local factor = data.strength/dist^3
return dx*factor, dy*factor, dz*factor
end
local function GravityInterval3D(source, target, data, dist, dx, dy, dz, dvx, dvy, dvz)
--Increased update frequency when objects are moving towards each other.
local vdotd = -(dvx*dx + dvy*dy + dvz*dz)
if vdotd < 0 then
vdotd = 0
end
return dist^2*data.intervalFactor/(1 + vdotd*data.intervalFactor)
end
--===========================================================================================================================================================
--Pressure Force
--===========================================================================================================================================================
local function InitPressureForce(source, target)
local data, __ = ALICE_PairReadMatrix()
data.strengthFactor = GetObjectCrossSection(target)*data.typeMultiplier
data.intervalFactor = CAT_GetObjectMass(target)/(PRESSURE_ACCURACY*data.strengthFactor)
if data.strengthFactor*source.pressureStrength == 0 then
ALICE_PairDisable()
end
end
local function PressureForce3D(source, target, data, dist, dx, dy, dz, dvx, dvy, dvz)
if dist > source.pressureMaxRange then
return 0, 0, 0
end
local factor = -source.pressureStrength*data.strengthFactor*(source.pressureMaxRange - dist)/(source.pressureMaxRange*dist)
return dx*factor, dy*factor, dz*factor
end
local function PressureInterval3D(source, target, data, dist, dx, dy, dz, dvx, dvy, dvz)
--Increased update frequency when objects are moving towards each other.
local vdivd = -(dvx*dx + dvy*dy + dvz*dz)/dist^2
if vdivd < 0 then
vdivd = 0
end
local interval = (0.15 + 0.85*(dist/source.pressureMaxRange))*data.intervalFactor/source.pressureStrength
return interval/(1 + vdivd*interval)
end
--===========================================================================================================================================================
--Windfield Force
--===========================================================================================================================================================
local function InitWindField(source, target)
local data, __ = ALICE_PairReadMatrix()
data.strengthFactor = GetObjectCrossSection(target)*data.typeMultiplier
data.intervalFactor = CAT_GetObjectMass(target)/(WIND_FIELD_ACCURACY*data.strengthFactor)
data.vx = source.windFieldSpeed*cos(source.windFieldAnglePhi)*cos(source.windFieldAngleTheta or 0)
data.vy = source.windFieldSpeed*sin(source.windFieldAnglePhi)*cos(source.windFieldAngleTheta or 0)
data.vz = source.windFieldSpeed*sin(source.windFieldAngleTheta or 0)
if data.strengthFactor*source.windStrength == 0 then
ALICE_PairDisable()
end
end
local function WindForce3D(source, target, data, dist, dx, dy, dz, dvx, dvy, dvz)
if dist > source.windFieldMaxRange then
return 0, 0, 0
end
local dvxWind = dvx + data.vx
local dvyWind = dvy + data.vy
local dvzWind = dvz + data.vz
local dvWindTotal = sqrt(dvxWind^2 + dvyWind^2 + dvzWind^2)
local factor = dvWindTotal*source.windFieldDensity*data.strengthFactor*(source.windFieldMaxRange - dist)/(source.windFieldMaxRange*dist)
return dvxWind*factor, dvyWind*factor, dvzWind*factor
end
local function WindFieldInterval3D(source, target, data, dist, dx, dy, dz, dvx, dvy, dvz)
--Increased update frequency when objects are moving towards each other.
local vdivd = -(dvx*dx + dvy*dy + dvz*dz)/dist^2
if vdivd < 0 then
vdivd = 0
end
local interval = (0.15 + 0.85*(dist/source.windFieldMaxRange))*data.intervalFactor/source.windFieldDensity
return interval/(1 + vdivd*interval)
end
--===========================================================================================================================================================
--API
--===========================================================================================================================================================
---@param class table
---@param interactsWith table | string
---@param initFunc function
---@param forceFunc function
---@param intervalFunc function
function CAT_AddForce3D(class, interactsWith, initFunc, forceFunc, intervalFunc)
class.initForceFunc = initFunc
class.forceFunc = forceFunc
class.intervalFunc = intervalFunc
if class.interactionFunc == nil then
class.interactionFunc = {}
end
if type(interactsWith) == "table" then
for __, id in pairs(interactsWith) do
class.interactionFunc[id] = Force3D
end
else
class.interactionFunc[interactsWith] = Force3D
end
end
---@param class table
---@param interactsWith table | string
function CAT_AddGravity3D(class, interactsWith)
CAT_AddForce3D(class, interactsWith, InitGravity, GravityForce3D, GravityInterval3D)
class.hasInfiniteRange = true
end
---@param class table
---@param interactsWith table | string
function CAT_AddPressure3D(class, interactsWith)
CAT_AddForce3D(class, interactsWith, InitPressureForce, PressureForce3D, PressureInterval3D)
class.radius = math.max(class.radius or ALICE_DEFAULT_OBJECT_RADIUS, class.pressureMaxRange)
end
---@param class table
---@param interactsWith table | string
function CAT_AddWindField3D(class, interactsWith)
CAT_AddForce3D(class, interactsWith, InitWindField, WindForce3D, WindFieldInterval3D)
class.radius = math.max(class.radius or ALICE_DEFAULT_OBJECT_RADIUS, class.windFieldMaxRange)
end
--===========================================================================================================================================================
---@entity ForceMatrix
local ForceMatrix = {
Fx = 0,
Fy = 0,
Fz = 0,
Fxdt = 0,
Fydt = 0,
Fzdt = 0,
lastUpdate = nil,
doUserInit = true
}
ForceMatrix.__index = ForceMatrix
function InitForcesCAT()
Require "ALICE"
Require "CAT_Objects"
INTERVAL = ALICE_MIN_INTERVAL
ALICE_MatrixCreate({ResumForceEntity3D, ResumForceUnit3D, Force3D}, ForceMatrix)
ALICE_FuncSetInit(AccelerateEntity3D, InitAccelerateEntity3D)
ALICE_FuncSetInit(AccelerateUnit3D, InitAccelerateUnit3D)
ALICE_FuncSetInit(Force3D, InitForce3D)
ALICE_FuncSetRandomDelay(ResumForceEntity3D, 5.0)
ALICE_FuncSetRandomDelay(ResumForceUnit3D, 5.0)
end
OnInit.global("CAT_Forces", InitForcesCAT)
end
function Play3DSound(soundPath, x, y)
local s = CreateSound(soundPath , false, true, true, 10, 10, "DefaultEAXON")
SetSoundDistances( s , 600 , 4000 )
SetSoundPosition( s, x, y, 50)
StartSound(s)
KillSoundWhenDone(s)
end
---@param soundPath string
---@param localPlayerCanHearSound boolean
function PlaySoundLocal(soundPath, localPlayerCanHearSound)
local volume ---@type number
local s = CreateSound( soundPath , FALSE, FALSE, FALSE, 10, 10, "DefaultEAXON") ---@type sound
SetSoundChannel(s, 0)
if localPlayerCanHearSound then
volume = 100
else
volume = 0
end
SetSoundVolumeBJ( s , volume )
StartSound(s)
KillSoundWhenDone(s)
end
do
local function ExplodeDynamite(dynamite)
local x, y, z = dynamite.x, dynamite.y, dynamite.z
local explosion = AddSpecialEffect("Objects\\Spawnmodels\\Other\\NeutralBuildingExplosion\\NeutralBuildingExplosion.mdl", x, y)
BlzSetSpecialEffectZ(explosion, dynamite.z)
DestroyEffect(explosion)
BlzSetSpecialEffectZ(dynamite.visual, -2000)
ALICE_ForAllObjectsInRangeDo(dynamite.x, dynamite.y, 475, "unit", nil, function(unit)
local xu, yu = ALICE_GetCoordinates2D(unit)
local dist = math.sqrt((xu - x)^2 + (yu - y)^2)
local strength = 1 - dist/475
local angle = math.atan(yu - y, xu - x)
UnitDamageTarget(dynamite.thrower, unit, 1500*strength, false, false, ATTACK_TYPE_HERO, DAMAGE_TYPE_MAGIC, WEAPON_TYPE_WHOKNOWS)
CAT_Knockback(unit, 1200*strength*math.cos(angle), 1200*strength*math.sin(angle), 0)
end)
end
---@class Dynamite
local Dynamite = {
x = nil,
y = nil,
z = nil,
vx = nil,
vy = nil,
vz = nil,
visual = nil,
thrower = nil,
--ALICE
identifier = {"projectile", "dynamite"},
interactionFunc = {
destructable = CAT_DestructableCollisionCheck3D,
self = {
CAT_MoveBallistic,
CAT_CheckTerrainCollision,
CAT_OrientRoll,
CAT_AnimateShadow,
CAT_Decay
}
},
--CAT
onEntityCollision = CAT_EntityBounce3D,
onDestructableCollision = CAT_DestructableBounce3D,
onTerrainCollision = CAT_TerrainBounce,
onExpire = ExplodeDynamite,
collisionRadius = 20,
visualZ = -20,
rollSpeed = 0.89,
lifetime = 8,
friction = 600,
mass = 1,
shadowWidth = 30,
shadowHeight = 30,
elasticity = 0.35,
}
Dynamite.__index = Dynamite
function Dynamite.create(x, y, z, vx, vy, vz, thrower)
local new = {}
setmetatable(new, Dynamite)
new.x, new.y, new.z = x, y, z
new.vx, new.vy, new.vz = vx, vy, vz
new.visual = AddSpecialEffect("Missile_Dynamite.mdx", x, y)
new.thrower = thrower
BlzSetSpecialEffectZ(new.visual, z)
BlzSetSpecialEffectScale(new.visual, 0.6)
CAT_InitDirectedOrientation(new)
ALICE_CreateFromClass(new)
end
local function ThrowDynamite()
if GetSpellAbilityId() ~= FourCC("Adyn") then
return
end
local u = GetSpellAbilityUnit()
DestroyEffect(Dynamites[u])
local xo, yo = GetUnitX(u), GetUnitY(u)
local xt, yt = GetSpellTargetX(), GetSpellTargetY()
local angle = math.atan(yt - yo, xt - xo)
xo = xo + 50*math.cos(angle)
yo = yo + 50*math.sin(angle)
local zo = GetTerrainZ(xo, yo) + 50
local zt = GetTerrainZ(xt, yt)
local vx, vy, vz = CAT_GetBallisticLaunchSpeedFromVelocity(xo, yo, zo, xt, yt, zt, 700, true)
Dynamite.create(xo, yo, zo, vx, vy, vz, u)
end
OnInit.global(function()
local trig = CreateTrigger()
TriggerAddAction(trig, ThrowDynamite)
TriggerRegisterAnyUnitEventBJ(trig, EVENT_PLAYER_UNIT_SPELL_EFFECT)
end)
end
do
---@class ExplosionForce
local ExplosionForce = {
visual = nil,
--ALICE
identifier = "explosionForce",
interactionFunc = {
self = {
CAT_MoveEffect,
CAT_Decay
}
},
lifetime = 3,
zOffset = -75,
anchor = nil,
destroyOnDeath = true,
--CAT
visualZ = 115,
pressureStrength = 50,
pressureMaxRange = 650
}
ExplosionForce.__index = ExplosionForce
local function ArcaneExplosion()
if GetSpellAbilityId() ~= FourCC("Aaex") then
return
end
local u = GetSpellAbilityUnit()
PlaySoundLocal( "Abilities\\Spells\\Orc\\Voodoo\\BigBadVoodooSpellBirth1.wav", true)
local explosion = {}
explosion.visual = AddSpecialEffect( "CyclonExplosion.mdx" , GetUnitX(u), GetUnitY(u) )
BlzSetSpecialEffectScale(explosion.visual, 1.5)
setmetatable(explosion, ExplosionForce)
explosion.anchor = u
ALICE_CreateFromClass(explosion)
end
OnInit.global(function()
local trig = CreateTrigger()
TriggerAddAction(trig, ArcaneExplosion)
TriggerRegisterAnyUnitEventBJ(trig, EVENT_PLAYER_UNIT_SPELL_EFFECT)
CAT_AddPressure3D(ExplosionForce, {"projectile", "unit"})
end)
end
do
---@class Shell
local Shell = {
x = nil,
y = nil,
z = nil,
vx = nil,
vy = nil,
vz = nil,
visual = nil,
--ALICE
identifier = {"projectile", "shell"},
interactionFunc = {
destructable = CAT_DestructableCollisionCheck3D,
self = {
CAT_MoveBallistic,
CAT_CheckTerrainCollision,
CAT_OrientProjectile,
CAT_AnimateShadow,
}
},
--CAT
onDestructableCollision = CAT_DestructableImpact3D,
collisionRadius = 15,
rollSpeed = 3,
mass = 1,
shadowWidth = 25,
shadowHeight = 25,
elasticity = 0.2,
}
Shell.__index = Shell
function Shell.create(x, y, z, vx, vy, vz)
local new = {}
setmetatable(new, Shell)
new.x, new.y, new.z = x, y, z
new.vx, new.vy, new.vz = vx, vy, vz
new.visual = AddSpecialEffect("Abilities\\Weapons\\CannonTowerMissile\\CannonTowerMissile.mdl", x, y)
BlzSetSpecialEffectZ(new.visual, z)
BlzSetSpecialEffectScale(new.visual, 0.7)
ALICE_CreateFromClass(new)
end
local function OnAttack()
if not BlzGetEventIsAttack() then
return
end
local source = GetEventDamageSource()
if GetUnitTypeId(source) ~= FourCC("hrif") then
return
end
BlzSetEventDamage(0)
local target = BlzGetEventDamageTarget()
local xo, yo = GetUnitX(source), GetUnitY(source)
local zo = GetTerrainZ(xo, yo) + 50
local xt, yt = GetUnitX(target), GetUnitY(target)
local zt = GetTerrainZ(xt, yt) + 75
xo = xo + 40*math.cos(math.atan(yt - yo, xt - xo))
yo = yo + 40*math.sin(math.atan(yt - yo, xt - xo))
local vx, vy, vz = CAT_GetBallisticLaunchSpeedFromVelocity(xo, yo, zo, xt, yt, zt, 900, false)
if vx then
vx = vx * (1 + 0.05*(1 - 2*math.random()))
vy = vy * (1 + 0.05*(1 - 2*math.random()))
vz = vz * (1 + 0.05*(1 - 2*math.random()))
Shell.create(xo, yo, zo, vx, vy, vz)
end
end
OnInit.global(function()
local trig = CreateTrigger()
TriggerAddAction(trig, OnAttack)
TriggerRegisterAnyUnitEventBJ(trig, EVENT_PLAYER_UNIT_DAMAGED)
end)
end
do
local function ReflectProjectile(barrier, shell, x, y, z, normalSpeed, __, __)
local phi, theta = ALICE_PairGetAngle3D()
local shieldEffect = AddSpecialEffect("Abilities\\Spells\\Undead\\ReplenishMana\\ReplenishManaCasterOverhead.mdl", x, y)
BlzSetSpecialEffectZ(shieldEffect, z)
BlzSetSpecialEffectYaw(shieldEffect, phi)
BlzSetSpecialEffectPitch(shieldEffect, -theta + bj_PI/2)
BlzSetSpecialEffectAlpha(shieldEffect, math.min(128, normalSpeed // 2))
BlzSetSpecialEffectScale(shieldEffect, 1.2)
DestroyEffect(shieldEffect)
end
---@class ReflectiveBarrier
ReflectiveBarrier = {
--ALICE
radius = 200,
identifier = "reflectiveBarrier",
interactionFunc = {
projectile = CAT_EntityCollisionCheck3D
},
bindToOrder = "starfall",
anchor = nil,
--CAT
onEntityCollision = {
shell = CAT_EntityBounce3D,
other = CAT_EntityDevour3D
},
onEntityCallback = ReflectProjectile,
collisionRadius = 160,
elasticity = 0.2,
mass = 1100,
zOffset = 50
}
ReflectiveBarrier.__index = ReflectiveBarrier
local function CreateReflectiveBarrier()
if GetSpellAbilityId() ~= FourCC("Ashi") then
return
end
local barrier = {}
setmetatable(barrier, ReflectiveBarrier)
local u = GetSpellAbilityUnit()
barrier.anchor = u
PlaySoundLocal("Abilities\\Spells\\Human\\ManaShield\\ManaShieldCaster1.flac", true)
ALICE_CreateFromClass(barrier)
end
OnInit.global(function()
local trig = CreateTrigger()
TriggerAddAction(trig, CreateReflectiveBarrier)
TriggerRegisterAnyUnitEventBJ(trig, EVENT_PLAYER_UNIT_SPELL_EFFECT)
end)
end
do
---@class Missile
local Missile = {
x = nil,
y = nil,
z = nil,
vx = nil,
vy = nil,
vz = nil,
visual = nil,
target = nil,
source = nil,
--ALICE
identifier = {"projectile", "missile"},
interactionFunc = {
destructable = CAT_DestructableCollisionCheck3D,
unit = CAT_UnitCollisionCheck3D,
self = {
CAT_MoveArcedHoming,
CAT_CheckTerrainCollision,
CAT_OrientProjectile,
CAT_AnimateShadow,
}
},
--CAT
collisionRadius = 25,
arc = 0.24,
turnRate = 50,
speed = 440,
mass = 1,
shadowWidth = 25,
shadowHeight = 25,
}
Missile.__index = Missile
function Missile.create(x, y, z, vx, vy, vz, source, target)
local new = {}
setmetatable(new, Missile)
new.x, new.y, new.z = x, y, z
new.vx, new.vy, new.vz = vx, vy, vz
new.visual = AddSpecialEffect("DO_RocketMissile.mdx", x, y)
new.target = target
new.source = source
BlzSetSpecialEffectZ(new.visual, z)
ALICE_CreateFromClass(new)
end
function Missile:destroy()
BlzSetSpecialEffectZ(self.visual, -2000)
DestroyEffect(self.visual)
local explosion = AddSpecialEffect("Objects\\Spawnmodels\\Other\\NeutralBuildingExplosion\\NeutralBuildingExplosion.mdl", self.x, self.y)
BlzSetSpecialEffectZ(explosion, self.z)
DestroyEffect(explosion)
ALICE_ForAllObjectsInRangeDo(self.x, self.y, 350, {"unit", "destructable"}, nil, function(object)
local x, y, z = ALICE_GetCoordinates3D(object, "CAT")
local dist = math.sqrt((x - self.x)^2 + (y - self.y)^2 + (z - self.z)^2)
if dist < 350 then
if WidgetIsUnit(object) then
UnitDamageTarget(self.source, object, 100*(350 - dist)/350, false, false, ATTACK_TYPE_HERO, DAMAGE_TYPE_MAGIC, WEAPON_TYPE_WHOKNOWS)
else
SetDestructableLife(object, GetDestructableLife(object) - 35*(350 - dist)/350)
end
end
end)
end
local function OnAttack()
if not BlzGetEventIsAttack() then
return
end
local source = GetEventDamageSource()
if GetUnitTypeId(source) ~= FourCC("hfly") then
return
end
BlzSetEventDamage(0)
local target = BlzGetEventDamageTarget()
local xo, yo, zo = GetUnitCoordinates(source)
local xt, yt, zt = GetUnitCoordinates(target)
local dx, dy, dz = xt - xo, yt - yo, zt - zo
local phi = math.atan(dy, dx) + 0.98*(1 - 2*math.random())
local theta = math.atan(dz, math.sqrt(dx^2 + dy^2)) + 0.9*(1 - 2*math.random())
xo = xo + 75*math.cos(phi)*math.cos(theta)
yo = yo + 75*math.sin(phi)*math.cos(theta)
zo = zo + 75*math.sin(theta)
local vx = math.cos(phi)*math.cos(theta)
local vy = math.sin(phi)*math.cos(theta)
local vz = math.sin(theta)
Missile.create(xo, yo, zo, vx, vy, vz, source, target)
end
OnInit.main(function()
local trig = CreateTrigger()
TriggerAddAction(trig, OnAttack)
TriggerRegisterAnyUnitEventBJ(trig, EVENT_PLAYER_UNIT_DAMAGED)
end)
end
do
---@class Fireball
local Fireball = {
visual = nil,
--ALICE
identifier = "fireball",
interactionFunc = {
projectile = CAT_EntityCollisionCheck3D,
self = {
CAT_Move3D,
CAT_Decay,
CAT_OutOfBoundsCheck,
CAT_OrientEffect3D,
CAT_CheckTerrainCollision
}
},
lifetime = 4,
--CAT
collisionRadius = 35,
onEntityCollision = CAT_EntityAnnihilate3D,
}
Fireball.__index = Fireball
function Fireball.create(x, y, z, vx, vy, vz)
local new = {}
setmetatable(new, Fireball)
new.x, new.y, new.z = x, y, z
new.vx, new.vy, new.vz = vx, vy, vz
new.visual = AddSpecialEffect("Abilities\\Weapons\\FireBallMissile\\FireBallMissile.mdl", x, y)
BlzSetSpecialEffectZ(new.visual, z)
ALICE_CreateFromClass(new)
end
local function FireballVolley()
if GetSpellAbilityId() == FourCC("Ahot") then
local u = GetSpellAbilityUnit()
local burn = AddSpecialEffectTarget("Abilities\\Spells\\Other\\ImmolationRed\\ImmolationRedTarget.mdl", u, "chest")
TimerStart(CreateTimer(), 0.1, true, function()
if GetUnitCurrentOrder(u) ~= OrderId("tranquility") then
DestroyEffect(burn)
DestroyTimer(GetExpiredTimer())
end
end)
end
if GetSpellAbilityId() ~= FourCC("Afvl") then
return
end
local u = GetSpellAbilityUnit()
local xu, yu, zu = GetUnitCoordinates(u)
ALICE_ForAllObjectsInRangeDo(xu, yu, 750, "projectile", nil, function(object)
local x, y, z = ALICE_GetCoordinates3D(object)
local dx, dy, dz = x - xu, y - yu, z - zu
local phi, theta = math.atan(dy, dx), math.atan(dz, math.sqrt(dx*dx + dy*dy))
local xf = xu + 75*math.cos(phi)*math.cos(theta)
local yf = yu + 75*math.sin(phi)*math.cos(theta)
local zf = zu + 75*math.sin(theta)
local vx = 600*math.cos(phi)*math.cos(theta)
local vy = 600*math.sin(phi)*math.cos(theta)
local vz = 600*math.sin(theta)
Fireball.create(xf, yf, zf, vx, vy, vz)
end)
end
OnInit.global(function()
local trig = CreateTrigger()
TriggerAddAction(trig, FireballVolley)
TriggerRegisterAnyUnitEventBJ(trig, EVENT_PLAYER_UNIT_SPELL_EFFECT)
end)
end
local function CreateDust(rock, normalSpeed, __, __)
if normalSpeed > 300/rock.size then
DestroyEffect(AddSpecialEffect("Objects\\Spawnmodels\\Undead\\ImpaleTargetDust\\ImpaleTargetDust.mdl", rock.x, rock.y))
Play3DSound("Abilities\\Weapons\\AncientProtectorMissile\\AncientProtectorMissileHit" .. math.random(1,3) .. ".flac", rock.x, rock.y)
end
end
local function CreateSparksRock(rockA, rockB, x, y, z, normalSpeed, __, __)
if normalSpeed > 500/(math.sqrt(rockA.size*rockB.size)) then
local sparks = AddSpecialEffect("Abilities\\Weapons\\FlyingMachine\\FlyingMachineImpact.mdl", x, y)
BlzSetSpecialEffectZ(sparks, z)
DestroyEffect(sparks)
Play3DSound("Abilities\\Weapons\\AncientProtectorMissile\\AncientProtectorMissileHit" .. math.random(1,3) .. ".flac", x, y)
end
end
local function CreateSparksUnit(rock, unit, x, y, z, normalSpeed, __, __)
if normalSpeed > 600 then
local sparks = DestroyEffect(AddSpecialEffect("Abilities\\Spells\\Orc\\WarStomp\\WarStompCaster.mdl", x, y))
elseif normalSpeed > 200 then
local sparks = AddSpecialEffect("Abilities\\Weapons\\FlyingMachine\\FlyingMachineImpact.mdl", x, y)
BlzSetSpecialEffectZ(sparks, z)
DestroyEffect(sparks)
Play3DSound("Abilities\\Weapons\\AncientProtectorMissile\\AncientProtectorMissileHit" .. math.random(1,3) .. ".flac", x, y)
end
end
---@class Rock
Rock ={
x = nil,
y = nil,
vx = nil,
vy = nil,
vz = nil,
visual = nil,
size = nil,
--ALICE
identifier = "rock",
interactionFunc = {
rock = CAT_EntityCollisionCheck3D,
unit = CAT_UnitCollisionCheck3D,
tree = CAT_DestructableCollisionCheck3D,
self = {
CAT_MoveBallistic,
CAT_CheckTerrainCollision,
CAT_OutOfBoundsCheck,
CAT_OrientRoll,
CAT_AnimateShadow
}
},
cellCheckInterval = 0.06,
--CAT
onTerrainCollision = CAT_TerrainBounce,
onTerrainCallback = CreateDust,
onEntityCollision = CAT_EntityBounce3D,
onEntityCallback = CreateSparksRock,
onDestructableCollision = CAT_DestructableBounce3D,
onUnitCollision = CAT_UnitBounce3D,
onUnitCallback = CreateSparksUnit,
collisionRadius = nil,
visualZ = 0,
friction = 150,
maxSpeed = 1000,
mass = nil,
shadowWidth = nil,
shadowHeight = nil,
elasticity = 0.3,
}
local rockMt = {__index = Rock}
---@return Rock
function Rock.create(x, y, vx, vy)
local new = {}
setmetatable(new, rockMt)
new.x = x
new.y = y
new.vx = vx
new.vy = vy
new.size = 0.5 + 2*math.random()
new.visual = AddSpecialEffect("Rock.mdx", new.x, new.y)
BlzSetSpecialEffectScale(new.visual, new.size)
BlzSetSpecialEffectTimeScale(new.visual, 0)
CAT_InitRandomOrientation(new)
new.collisionRadius = 25*new.size
new.radius = new.collisionRadius + 50
new.mass = 0.5*new.size^3
new.shadowWidth = 60*new.size
new.shadowHeight = 80*new.size
ALICE_CreateFromClass(new)
return new
end
function ReplaceRock(x, y, __, __)
Rock.create(x, y, 0, 0)
end
do
local function JainaCrit(__, jaina)
Play3DSound("Jaina_Crit.mp3", GetUnitX(jaina), GetUnitY(jaina))
end
---@class Hammer
local Hammer = {
x = nil,
y = nil,
z = nil,
vx = nil,
vy = nil,
vz = nil,
visual = nil,
--ALICE
identifier = {"projectile", "hammer"},
interactionFunc = {
destructable = CAT_DestructableCollisionCheck3D,
unit = CAT_UnitCollisionCheck3D,
self = {
CAT_Move3D,
CAT_CheckTerrainCollision,
CAT_OrientEffect3D,
CAT_AnimateShadow,
}
},
cellCheckInterval = 0.02,
--CAT
onUnitCollision = CAT_UnitImpact3D,
onUnitCallback = JainaCrit,
collisionRadius = 40,
mass = 100,
maxSpeed = 2000,
shadowWidth = 40,
shadowHeight = 40,
}
Hammer.__index = Hammer
function Hammer.create(x, y, z, vx, vy, vz)
local new = {}
setmetatable(new, Hammer)
new.x, new.y, new.z = x, y, z
new.vx, new.vy, new.vz = vx, vy, vz
new.visual = AddSpecialEffect("Abilities\\Spells\\Human\\StormBolt\\StormBoltMissile.mdl", x, y)
BlzSetSpecialEffectZ(new.visual, z)
BlzSetSpecialEffectColorByPlayer(new.visual, Player(PLAYER_NEUTRAL_PASSIVE))
ALICE_CreateFromClass(new)
end
local function ThrowHammer()
if GetSpellAbilityId() ~= FourCC("Aham") then
return
end
local caster = GetSpellAbilityUnit()
local target = GetSpellTargetUnit()
local xo, yo, zo = ALICE_GetCoordinates3D(caster)
local xt, yt, zt = ALICE_GetCoordinates3D(target)
local dx, dy, dz = xt - xo, yt - yo, zt - zo
local phi = math.atan(dy, dx)
local theta = math.atan(dz, math.sqrt(dx^2 + dy^2))
xo = xo + 75*math.cos(phi)*math.cos(theta)
yo = yo + 75*math.sin(phi)*math.cos(theta)
zo = zo + 75*math.sin(theta)
local vx = 2000*math.cos(phi)*math.cos(theta)
local vy = 2000*math.sin(phi)*math.cos(theta)
local vz = 2000*math.sin(theta)
Hammer.create(xo, yo, zo, vx, vy, vz)
end
OnInit.global(function()
local trig = CreateTrigger()
TriggerAddAction(trig, ThrowHammer)
TriggerRegisterAnyUnitEventBJ(trig, EVENT_PLAYER_UNIT_SPELL_EFFECT)
end)
end
if Debug then Debug.beginFile "Machine Gun" end
do
---@class Bullet
local Bullet = {
x = nil,
y = nil,
z = nil,
vx = nil,
vy = nil,
vz = nil,
visual = nil,
source = nil,
--ALICE
identifier = {"projectile", "shell"},
interactionFunc = {
unit = CAT_UnitCollisionCheck3D,
destructable = CAT_DestructableCollisionCheck3D,
self = {
CAT_Move3D,
CAT_CheckTerrainCollision,
CAT_Orient3D,
CAT_OutOfBoundsCheck,
}
},
--CAT
onUnitCollision = CAT_UnitImpact3D,
onUnitDamage = 1000,
onDestructableCollision = CAT_DestructableImpact3D,
onDestructableDamage = 25,
maxSpeed = 2500,
cellCheckInterval = 0.02,
collisionRadius = 15,
}
Bullet.__index = Bullet
function Bullet.create(x, y, z, phi, source)
local new = {}
setmetatable(new, Bullet)
new.x, new.y, new.z = x, y, z
phi = phi + 0.05*(1 - 2*math.random())
local theta = 0.05*(1 - 2*math.random())
new.vx, new.vy, new.vz = 2500*math.cos(phi + 0.17)*math.cos(theta), 2500*math.sin(phi + 0.17)*math.cos(theta), 2500*math.sin(theta)
new.visual = AddSpecialEffect("SMGPellet.mdl", x, y)
new.source = source
local muzzleFlash = AddSpecialEffect("MuzzleFlash.mdl", x, y)
BlzSetSpecialEffectYaw(muzzleFlash, phi)
BlzSetSpecialEffectZ(muzzleFlash, z)
BlzSetSpecialEffectTimeScale(muzzleFlash, 4.0)
DestroyEffect(muzzleFlash)
BlzSetSpecialEffectZ(new.visual, z)
Play3DSound("Units\\Human\\Rifleman\\RiflemanAttack1.flac", x, y)
ALICE_CreateFromClass(new)
end
local data = {}
local function Shoot()
local u = data[GetExpiredTimer()]
if GetUnitTypeId(u) ~= FourCC("Hakj") then
DestroyTimer(GetExpiredTimer())
return
end
local angle = GetUnitFacing(u)*bj_DEGTORAD
local x, y = GetUnitX(u) + 75*math.cos(angle), GetUnitY(u) + 75*math.sin(angle)
Bullet.create(x, y, GetUnitZ(u) + 70, angle, u)
end
local function InitShoot()
if GetSpellAbilityId() ~= FourCC("Amag") then
return
end
local u = GetSpellAbilityUnit()
TimerStart(CreateTimer(), 1.334, false, function()
local t = ALICE_Periodic(0.1, Shoot)
data[t] = u
end)
end
OnInit.global(function()
local trig = CreateTrigger()
TriggerAddAction(trig, InitShoot)
TriggerRegisterAnyUnitEventBJ(trig, EVENT_PLAYER_UNIT_SPELL_EFFECT)
end)
end
---@diagnostic disable: undefined-global
do
function CameraStuff()
local t = CreateTimer()
CinematicFadeBJ( bj_CINEFADETYPE_FADEIN, 2.00, "ReplaceableTextures\\CameraMasks\\Black_mask.blp", 0, 0, 0, 0 )
SetUserControlForceOff( GetPlayersAll() )
CameraSetupApplyForPlayer( true, gg_cam_Camera_1, Player(0), 0 )
CameraSetupApplyForPlayer( true, gg_cam_Camera_2, Player(0), 5.50 )
TimerStart(t, 4.70, false, function()
CinematicFadeBJ( bj_CINEFADETYPE_FADEOUT, 0.50, "ReplaceableTextures\\CameraMasks\\Black_mask.blp", 0, 0, 0, 0 )
TimerStart(t, 0.50, false, function()
CameraSetupApplyForPlayer( true, gg_cam_Camera_3, Player(0), 0 )
CinematicFadeBJ( bj_CINEFADETYPE_FADEIN, 1.00, "ReplaceableTextures\\CameraMasks\\Black_mask.blp", 0, 0, 0, 0 )
TimerStart(t, 3.70, false, function()
CinematicFadeBJ( bj_CINEFADETYPE_FADEOUT, 0.50, "ReplaceableTextures\\CameraMasks\\Black_mask.blp", 0, 0, 0, 0 )
TimerStart(t, 0.50, false, function()
CameraSetupApplyForPlayer( true, gg_cam_Camera_4, Player(0), 0 )
CinematicFadeBJ( bj_CINEFADETYPE_FADEIN, 1.00, "ReplaceableTextures\\CameraMasks\\Black_mask.blp", 0, 0, 0, 0 )
TimerStart(t, 3.20, false, function()
RotateCameraAroundLocBJ( 330.00, GetRectCenter(GetPlayableMapRect()), Player(0), 13.00 )
TimerStart(t, 17.10, false, function()
CinematicFadeBJ( bj_CINEFADETYPE_FADEOUT, 0.50, "ReplaceableTextures\\CameraMasks\\Black_mask.blp", 0, 0, 0, 0 )
TimerStart(t, 0.50, false, function()
CameraSetupApplyForPlayer( true, gg_cam_Camera_5, Player(0), 0 )
CameraSetupApplyForPlayer( true, gg_cam_Camera_6, Player(0), 6.00 )
CinematicFadeBJ( bj_CINEFADETYPE_FADEIN, 0.50, "ReplaceableTextures\\CameraMasks\\Black_mask.blp", 0, 0, 0, 0 )
TimerStart(t, 7.20, false, function()
CameraSetupApplyForPlayer( true, gg_cam_Camera_7, Player(0), 2.50 )
TimerStart(t, 6.70, false, function()
CinematicFadeBJ( bj_CINEFADETYPE_FADEOUT, 0.50, "ReplaceableTextures\\CameraMasks\\Black_mask.blp", 0, 0, 0, 0 )
TimerStart(t, 0.50, false, function()
CameraSetupApplyForPlayer( true, gg_cam_Camera_4, Player(0), 0 )
CinematicFadeBJ( bj_CINEFADETYPE_FADEIN, 0.50, "ReplaceableTextures\\CameraMasks\\Black_mask.blp", 0, 0, 0, 0 )
CameraSetupApplyForPlayer( true, gg_cam_Camera_8, Player(0), 3.5 )
TimerStart(t, 6.95, false, function()
CinematicFadeBJ( bj_CINEFADETYPE_FADEOUT, 0.50, "ReplaceableTextures\\CameraMasks\\Black_mask.blp", 0, 0, 0, 0 )
TimerStart(t, 0.50, false, function()
CameraSetupApplyForPlayer( true, gg_cam_Camera_9, Player(0), 0 )
CinematicFadeBJ( bj_CINEFADETYPE_FADEIN, 1.00, "ReplaceableTextures\\CameraMasks\\Black_mask.blp", 0, 0, 0, 0 )
CameraSetupApplyForPlayer( true, gg_cam_Camera_10, Player(0), 5.0 )
TimerStart(t, 7.4, false, function()
CinematicFadeBJ( bj_CINEFADETYPE_FADEOUT, 0.50, "ReplaceableTextures\\CameraMasks\\Black_mask.blp", 0, 0, 0, 0 )
TimerStart(t, 0.50, false, function()
CameraSetupApplyForPlayer( true, gg_cam_Camera_11, Player(0), 0 )
CinematicFadeBJ( bj_CINEFADETYPE_FADEIN, 0.50, "ReplaceableTextures\\CameraMasks\\Black_mask.blp", 0, 0, 0, 0 )
TimerStart(t, 14.75, false, function()
CinematicFadeBJ( bj_CINEFADETYPE_FADEOUT, 0.50, "ReplaceableTextures\\CameraMasks\\Black_mask.blp", 0, 0, 0, 0 )
TimerStart(t, 0.50, false, function()
CameraSetupApplyForPlayer( true, gg_cam_Camera_12, Player(0), 0 )
CinematicFadeBJ( bj_CINEFADETYPE_FADEIN, 0.50, "ReplaceableTextures\\CameraMasks\\Black_mask.blp", 0, 0, 0, 0 )
TimerStart(t, 5.0, false, function()
CameraSetupApplyForPlayer( true, gg_cam_Camera_13, Player(0), 1.15 )
TimerStart(t, 2.20, false, function()
CameraSetupApplyForPlayer( true, gg_cam_Camera_14, Player(0), 0 )
CinematicFadeBJ( bj_CINEFADETYPE_FADEIN, 0.50, "ReplaceableTextures\\CameraMasks\\Black_mask.blp", 0, 0, 0, 0 )
TimerStart(t, 4.20, false, function()
CameraSetupApplyForPlayer( true, gg_cam_Camera_15, Player(0), 3.0 )
TimerStart(t, 7.0, false, function()
CameraSetupApplyForPlayer( true, gg_cam_Camera_16, Player(0), 4.0 )
end) end) end) end) end) end) end) end) end) end) end) end) end) end) end) end) end) end) end) end)
end
OnInit.final(CameraStuff)
end
if Debug then Debug.beginFile("Video") end
do
local dwarves = nil
local jaina = nil
Dynamites = {}
function Init()
BlzFrameSetVisible(BlzGetFrameByName("ConsoleUIBackdrop",0), false)
BlzHideOriginFrames(true)
CreateFogModifierRectBJ( true, Player(0), FOG_OF_WAR_VISIBLE, GetPlayableMapRect() )
SetTimeOfDayScale(0)
CAT_GlobalReplace(FourCC("Droc"), "destructable", ReplaceRock)
jaina = CreateUnit(Player(21), FourCC("Hjai"), 0, 0, 90)
dwarves = {}
local posX = {}
local posY = {}
posX[1] = -420
posX[2] = -230
posX[3] = 230
posX[4] = 420
posX[5] = -420
posX[6] = -230
posX[7] = 230
posX[8] = 420
posY[1] = -1150 - 1024
posY[2] = -1340 - 1024
posY[3] = -1340 - 1024
posY[4] = -1150 - 1024
posY[5] = 1150 + 1024
posY[6] = 1340 + 1024
posY[7] = 1340 + 1024
posY[8] = 1150 + 1024
for i = 1, 8 do
dwarves[i] = CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), FourCC("hrif"), posX[i], posY[i], 0)
end
local t = CreateTimer()
TimerStart(t, 2.0, false, function()
for i = 1, 4 do
IssuePointOrder(dwarves[i], "smart", GetUnitX(dwarves[i]), GetUnitY(dwarves[i]) + 1536)
end
for i = 5, 8 do
IssuePointOrder(dwarves[i], "smart", GetUnitX(dwarves[i]), GetUnitY(dwarves[i]) - 1536)
end
TimerStart(t, 8.0, false, function()
IssueImmediateOrder(jaina, "starfall")
TimerStart(t, 1.5, false, function()
PlaySoundLocal("Units\\Human\\Rifleman\\RiflemanYesAttack1.flac", true)
TimerStart(t, 0.5, false, function()
for i = 1, 8 do
TimerStart(CreateTimer(), math.random(), false, function()
IssueTargetOrder(dwarves[i], "attack", jaina)
end)
end
TimerStart(t, 10, false, function()
CreateFogModifierRectBJ( true, Player(0), FOG_OF_WAR_VISIBLE, GetPlayableMapRect() )
posX[1] = -325
posX[2] = 0
posX[3] = 325
posX[4] = -325
posX[5] = 0
posX[6] = 325
posY[1] = -1245 - 1024
posY[2] = -1340 - 1024
posY[3] = -1245 - 1024
posY[4] = 1245 + 1024
posY[5] = 1340 + 1024
posY[6] = 1245 + 1024
local grenadiers = {}
for i = 1, 6 do
grenadiers[i] = CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), FourCC("hgre"), posX[i], posY[i], 0)
Dynamites[grenadiers[i]] = AddSpecialEffectTarget("Attachment_Dynamite.mdx", grenadiers[i], "hand")
end
TimerStart(t, 2, false, function()
for i = 1, 8 do
IssueImmediateOrder(dwarves[i], "stop")
end
for i = 1, 3 do
IssuePointOrder(grenadiers[i], "smart", GetUnitX(grenadiers[i]), GetUnitY(grenadiers[i]) + 1280)
end
for i = 4, 6 do
IssuePointOrder(grenadiers[i], "smart", GetUnitX(grenadiers[i]), GetUnitY(grenadiers[i]) - 1280)
end
TimerStart(t, 7.5, false ,function()
PlaySoundLocal("Units\\Human\\Rifleman\\RiflemanYesAttack4.flac", true)
TimerStart(t, 0.5, false ,function()
for i = 1, 6 do
TimerStart(CreateTimer(), math.random(), false, function()
local angle = math.atan(GetUnitY(jaina) - GetUnitY(grenadiers[i]), GetUnitX(jaina) - GetUnitX(grenadiers[i]))
local x = GetUnitX(grenadiers[i]) + 625*math.cos(angle)
local y = GetUnitY(grenadiers[i]) + 625*math.sin(angle)
IssuePointOrder(grenadiers[i], "stampede", x, y)
end)
end
SetUnitFacingTimed(jaina, 270, 0.5)
TimerStart(t, 3.5, false ,function()
SetUnitFacingTimed(jaina, 90, 0.5)
TimerStart(t, 1.5, false ,function()
IssueImmediateOrder(jaina, "roar")
TimerStart(t, 1.6, false, function()
for i = 1,8 do
local angle = math.atan(GetUnitY(dwarves[i]), GetUnitX(dwarves[i]))
local x, y = GetUnitX(dwarves[i]) + 3000*math.cos(angle), GetUnitY(dwarves[i]) + 3000*math.sin(angle)
IssuePointOrder(dwarves[i], "smart", x, y)
end
for i = 1,6 do
local angle = math.atan(GetUnitY(grenadiers[i]), GetUnitX(grenadiers[i]))
local x, y = GetUnitX(grenadiers[i]) + 3000*math.cos(angle), GetUnitY(grenadiers[i]) + 3000*math.sin(angle)
IssuePointOrder(grenadiers[i], "smart", x, y)
end
TimerStart(t, 5.0, false , function()
local gyros = {}
gyros[1] = CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), FourCC("hfly"), -800, -3000, 0)
gyros[2] = CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), FourCC("hfly"), 800, -3000, 0)
IssuePointOrder(gyros[1], "smart", -384, -1860)
IssuePointOrder(gyros[2], "smart", 384, -1860)
TimerStart(t, 2.0, false, function()
SetUnitFacingTimed(jaina, 270, 0.5)
for i = 1,4 do
if UnitAlive(dwarves[i]) then
IssueImmediateOrder(dwarves[i], "stop")
SetUnitFacingToFaceUnitTimed(dwarves[i], jaina, 0.5)
end
end
for i = 1,3 do
if UnitAlive(grenadiers[i]) then
IssueImmediateOrder(dwarves[i], "stop")
SetUnitFacingToFaceUnitTimed(grenadiers[i], jaina, 0.5)
end
end
TimerStart(t, 3.0, false, function()
PlaySoundLocal("Units\\Human\\Gyrocopter\\GyrocopterYesAttack1.flac", true)
TimerStart(t, 1.0, false, function()
for i = 1,8 do
if UnitAlive(dwarves[i]) then
local angle = math.atan(GetUnitY(dwarves[i]), GetUnitX(dwarves[i]))
local x, y = GetUnitX(dwarves[i]) - 500*math.cos(angle), GetUnitY(dwarves[i]) - 500*math.sin(angle)
IssuePointOrder(dwarves[i], "smart", x, y)
end
end
for i = 1,6 do
if UnitAlive(grenadiers[i]) then
local angle = math.atan(GetUnitY(grenadiers[i]), GetUnitX(grenadiers[i]))
local x, y = GetUnitX(grenadiers[i]) - 500*math.cos(angle), GetUnitY(grenadiers[i]) - 500*math.sin(angle)
IssuePointOrder(grenadiers[i], "smart", x, y)
end
end
IssueTargetOrder(gyros[1], "attack", jaina)
IssueTargetOrder(gyros[2], "attack", jaina)
TimerStart(t, 1.25, false, function()
IssuePointOrder(jaina, "smart", 0, 1550)
TimerStart(t, 0.55, false, function()
IssueImmediateOrder(gyros[1], "stop")
IssueImmediateOrder(gyros[2], "stop")
TimerStart(t, 4.6, false, function()
SetUnitFacingTimed(jaina, 270, 0.5)
TimerStart(t, 1.0, false, function()
IssuePointOrder(jaina, "blink", 0, 0)
TimerStart(t, 0.5, false, function()
SetUnitFacingTimed(jaina, 90, 0.5)
TimerStart(t, 0.6, false, function()
IssuePointOrder(jaina, "smart", -750, -225)
TimerStart(t, 2.5, false, function()
SetUnitFacingTimed(jaina, 90, 0.5)
TimerStart(t, 1.1, false, function()
IssuePointOrder(jaina, "blink", 250, -450)
TimerStart(t, 0.5, false, function()
IssuePointOrder(jaina, "smart", 600, -380)
TimerStart(t, 1.3, false, function()
SetUnitFacingTimed(jaina, 160, 0.5)
TimerStart(t, 0.7, false, function()
IssuePointOrder(jaina, "blink", 0, 0)
TimerStart(t, 0.5, false, function()
IssuePointOrder(jaina, "smart", -800, 0)
TimerStart(t, 2.5, false, function()
SetUnitFacingTimed(jaina, 330, 0.5)
TimerStart(t , 0.5, false, function()
IssueImmediateOrder(jaina, "tranquility")
TimerStart(t, 1.75, false, function()
IssueImmediateOrder(jaina, "stomp")
TimerStart(t, 4.35, false, function()
IssuePointOrder(jaina, "smart", 0, 0)
TimerStart(t, 0.5, false, function()
local mountainKing = CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), FourCC("Hmkg"), 0, 2300, 270)
RemoveUnit(gyros[1])
RemoveUnit(gyros[2])
for i = 1,8 do
if UnitAlive(dwarves[i]) then
RemoveUnit(dwarves[i])
end
end
for i = 1,6 do
if UnitAlive(grenadiers[i]) then
RemoveUnit(grenadiers[i])
end
end
TimerStart(t, 2.25, false, function()
SetUnitFacingTimed(jaina, 270, 0.5)
TimerStart(t, 1.85, false, function()
PlaySoundLocal("Units\\Human\\Muradin\\MuradinYesAttack3.flac", true)
TimerStart(t, 1.5, false, function()
IssueTargetOrder(mountainKing, "deathcoil", jaina)
TimerStart(t, 1.0, false, function()
SetUnitFacingTimed(jaina, 90, 0.5)
TimerStart(t, 1.5, false, function()
SetUnitY(mountainKing, 0)
IssuePointOrder(mountainKing, "smart", 0, -1450)
posX[1] = -1850
posX[2] = -2100
posX[3] = -2350
posX[4] = -2600
posX[5] = 1850
posX[6] = 2100
posX[7] = 2350
posX[8] = 2600
posY[1] = -1900
posY[2] = -2000
posY[3] = -2100
posY[4] = -2200
posY[5] = -1900
posY[6] = -2000
posY[7] = -2100
posY[8] = -2200
for i = 1, 8 do
dwarves[i] = CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), FourCC("hrif"), posX[i], posY[i], 0)
end
for i = 1, 4 do
IssuePointOrder(dwarves[i], "smart", GetUnitX(dwarves[i]) + 1600, GetUnitY(dwarves[i]) + 350)
end
for i = 5, 8 do
IssuePointOrder(dwarves[i], "smart", GetUnitX(dwarves[i]) - 1600, GetUnitY(dwarves[i]) + 350)
end
TimerStart(CreateTimer(), 6.5, false, function()
for i = 1, 8 do
SetUnitFacingToFaceUnitTimed(dwarves[i], jaina, 0.5)
end
end)
TimerStart(t, 0.1, false ,function()
UnitRemoveAbility(jaina, FourCC("Aaex"))
UnitAddAbility(jaina, FourCC("Adiv"))
IssueImmediateOrder(jaina, "roar")
TimerStart(t, 4.4, false, function()
IssuePointOrder(jaina, "smart", 0, -2625)
TimerStart(t, 2.4, false, function()
SetUnitFacingTimed(jaina, 90, 0.5)
TimerStart(t, 0.5, false, function()
IssueTargetOrder(mountainKing, "deathcoil", jaina)
IssueImmediateOrder(jaina, "starfall")
TimerStart(t, 1.25, false, function()
IssueTargetOrder(mountainKing, "deathcoil", jaina)
TimerStart(t, 1.25, false, function()
IssueTargetOrder(mountainKing, "deathcoil", jaina)
TimerStart(t, 1.0, false, function()
IssueImmediateOrder(jaina, "stop")
TimerStart(t, 1.0, false, function()
SetUnitMoveSpeed(jaina, 150)
IssuePointOrder(jaina, "smart", -10, -2425)
TimerStart(t, 2.9, false, function()
SetUnitFacingTimed(jaina, 20, 0.5)
TimerStart(t, 1.25, false, function()
IssueImmediateOrder(jaina, "robogoblin")
PlaySoundLocal("JainaYesAttack2.mp3", true)
TimerStart(t, 1.5, false, function()
SetUnitFacingTimed(jaina, 100, 6.0)
for i = 1, 8 do
IssuePointOrder(dwarves[i], "smart", 0, 2000)
end
IssuePointOrder(mountainKing, "smart", 0, 2000)
TimerStart(t, 6.0, false, function()
SetUnitFacingTimed(jaina, 55, 5.0)
TimerStart(t, 5.0, false, function()
SetUnitFacingTimed(jaina, 100, 5.0)
end) end) end) end) end) end) end) end) end) end) end) end) end) end) end) end) end) end) end) end) end) end) end) end) end) end) end) end) end) end) end) end) end) end) end) end) end) end) end) end) end) end) end) end) end) end) end) end) end) end)
end
OnInit.final(Init)
end