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: ")
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 "NeatTextMessages" end
do
--[[
===========================================================================================
Neat Text Messages
by Antares
Recreation of the default text messages with more customizability.
How to import:
Copy this library into your map. To get better looking messages with text shadow and an optional
tooltip-like box around the text, copy the "NeatTextMessage.fdf" and "NeatMessageTemplates.toc"
files from the test map into your map without a subpath.
Edit the parameters in the config section to your liking.
Replace all DisplayTextToForce calls etc. with the appropriate function from this library.
Default text formatting can be overwritten by setting up NeatFormats. Examples are given in the
test section. All formatting parameters that aren't set for a NeatFormat will use the default
values instead.
NeatMessage creator functions return an integer. This integer is a pointer to the created message
that can be used to edit, extend, or remove the message. The returned integer is asynchronous
and will be 0 for all players for whom the message isn't displayed.
You can set up additional text windows with NeatWindow.create. If you create a neat message
without specifying the window in which it should be created, it will always be created in the
default window specified in the config.
===========================================================================================
API
===========================================================================================
NeatMessage(whichMessage)
NeatMessageToPlayer(whichPlayer, whichMessage)
NeatMessageToForce(whichForce, whichMessage)
NeatMessageTimed(duration, whichMessage)
NeatMessageToPlayerTimed(whichPlayer, duration, whichMessage)
NeatMessageToForceTimed(whichForce, duration, whichMessage)
NeatMessageFormatted(whichMessage, whichFormat)
NeatMessageToPlayerFormatted(whichPlayer, whichMessage, whichFormat)
NeatMessageToForceFormatted(whichForce, whichMessage, whichFormat)
NeatMessageTimedFormatted(duration, whichMessage, whichFormat)
NeatMessageToPlayerTimedFormatted(whichPlayer, duration, whichMessage, whichFormat)
NeatMessageToForceTimedFormatted(whichForce, duration, whichMessage, whichFormat)
NeatMessageInWindow(whichMessage, whichWindow)
NeatMessageToPlayerInWindow(whichPlayer, whichMessage, whichWindow)
NeatMessageToForceInWindow(whichForce, whichMessage, whichWindow)
NeatMessageTimedInWindow(duration, whichMessage, whichWindow)
NeatMessageToPlayerTimedInWindow(whichPlayer, duration, whichMessage, whichWindow)
NeatMessageToForceTimedInWindow(whichForce, duration, whichMessage, whichWindow)
NeatMessageFormattedInWindow(whichMessage, whichFormat, whichWindow)
NeatMessageToPlayerFormattedInWindow(whichPlayer, whichMessage, whichFormat, whichWindow)
NeatMessageToForceFormattedInWindow(whichForce, whichMessage, whichFormat, whichWindow)
NeatMessageTimedFormattedInWindow(duration, whichMessage, whichFormat, whichWindow)
NeatMessageToPlayerTimedFormattedInWindow(whichPlayer, duration, whichMessage, whichFormat, whichWindow)
NeatMessageToForceTimedFormattedInWindow(whichForce, duration, whichMessage, whichFormat, whichWindow)
===========================================================================================
EditNeatMessage(messagePointer, newText)
AddNeatMessageTimeRemaining(messagePointer, additionalTime)
SetNeatMessageTimeRemaining(messagePointer, newTime)
RemoveNeatMessage(messagePointer)
AutoSetNeatMessageTimeRemaining(messagePointer, accountForTimeElapsed)
IsMessageDisplayed(messagePointer)
NeatMessageAddIcon(messagePointer, width, height, orientation, texture)
(valid arguments for orientation are "topleft", "topright", "bottomleft", "bottomright")
NeatMessageHideIcon(messagePointer)
ClearNeatMessages()
ClearNeatMessagesForPlayer(whichPlayer)
ClearNeatMessagesForForce(whichForce)
ClearNeatMessagesInWindow(whichWindow)
ClearNeatMessagesForPlayerInWindow(whichPlayer, whichWindow)
ClearNeatMessagesForForceInWindow(whichForce, whichWindow)
set myFormat = NeatFormat.create()
set myFormat.spacing =
set myFormat.fadeOutTime =
set myFormat.fadeInTime =
set myFormat.fontSize =
set myFormat.minDuration =
set myFormat.durationIncrease =
set myFormat.verticalAlignment =
set myFormat.horizontalAlignment =
set myFormat.isBoxed =
call myFormat:copy(copiedFormat)
set myWindow = NeatWindow.create(xPosition, yPosition, width, height, maxMessages, topToBottom)
===========================================================================================
]]
--=========================================================================================
--Config
--=========================================================================================
--Default text formatting. Can be overwritten by setting up neatFormats.
local MESSAGE_MINIMUM_DURATION = 2.5 ---@type number --Display duration of a message with zero characters.
local MESSAGE_DURATION_INCREASE_PER_CHARACTER = 0.12 ---@type number
local TEXT_MESSAGE_FONT_SIZE = 14 ---@type number
local SPACING_BETWEEN_MESSAGES = 0.0 ---@type number
local FADE_IN_TIME = 0.0 ---@type number
local FADE_OUT_TIME = 1.8 ---@type number
local VERTICAL_ALIGNMENT = TEXT_JUSTIFY_MIDDLE ---@type textaligntype --TEXT_JUSTIFY_BOTTOM, TEXT_JUSTIFY_MIDDLE, or TEXT_JUSTIFY_TOP
local HORIZONTAL_ALIGNMENT = TEXT_JUSTIFY_CENTER ---@type textaligntype --TEXT_JUSTIFY_LEFT, TEXT_JUSTIFY_CENTER, or TEXT_JUSTIFY_RIGHT
local BOXED_MESSAGES = false ---@type boolean --Create tooltip box around text messages? Requires .fdf file and INCLUDE_FDF enabled.
--Default text window parameters.
TEXT_MESSAGE_X_POSITION = 0.2 ---@type number --0 = left, 1 = right (bottom-left corner)
TEXT_MESSAGE_Y_POSITION = 0.05 ---@type number --0 = bottom, 0.6 = top (bottom-left corner)
TEXT_MESSAGE_BLOCK_MAX_HEIGHT = 0.2 ---@type number --Maximum height of the entire text message block. Messages pushed out of that area will be removed.
TEXT_MESSAGE_BLOCK_WIDTH = 0.4 ---@type number
MAX_TEXT_MESSAGES = 5 ---@type integer --Maximum number of messages on the screen at the same time. If you want a non-scrolling window, simply set this number to 1.
MESSAGE_ORDER_TOP_TO_BOTTOM = false ---@type boolean --Set true if new messages should appear above old messages.
--Config
local INCLUDE_FDF = true ---@type boolean --NeatMessage.fdf has been imported?
local COPY_TO_MESSAGE_LOG = true ---@type boolean --(Only singleplayer) Copies messages to message log by printing out the message with DisplayTextToPlayer, then clearing all text. Will interfere with other default text messages.
local REPLACE_BLIZZARD_FUNCTION_CALLS = false ---@type boolean --Replaces Display(Timed)TextToForce, ClearTextMessages, and ClearTextMessagesBJ.
local TOOLTIP_ABILITY = 'Amls' ---@type string --For REPLACE_BLIZZARD_FUNCTION_CALLS only. Any unused ability for which the library can change the tooltip to extract the TRIGSTR from.
--=========================================================================================
local isSinglePlayer = nil ---@type boolean
local masterTimer = nil ---@type timer
local numMessagesOnScreen = 0 ---@type integer
local messageCounter = 0 ---@type integer
local frameOfMessage = {} ---@type integer[]
local windowOfMessage = {} ---@type NeatWindow[]
local TIME_STEP = 0.05 ---@type number
DEFAULT_NEAT_FORMAT = nil ---@type NeatFormat
DEFAULT_NEAT_WINDOW = nil ---@type NeatWindow
local neatWindow = {} ---@type NeatWindow[]
local numNeatWindows = 0 ---@type integer
local ChangeTextFormatting = nil ---@type function
local ChangeText = nil ---@type function
---@class NeatFormat
NeatFormat = {
spacing = SPACING_BETWEEN_MESSAGES, ---@type number
fadeOutTime = FADE_OUT_TIME, ---@type number
fadeInTime = FADE_IN_TIME, ---@type number
fontSize = TEXT_MESSAGE_FONT_SIZE, ---@type number
minDuration = MESSAGE_MINIMUM_DURATION, ---@type number
durationIncrease = MESSAGE_DURATION_INCREASE_PER_CHARACTER, ---@type number
verticalAlignment = VERTICAL_ALIGNMENT, ---@type textaligntype
horizontalAlignment = HORIZONTAL_ALIGNMENT, ---@type textaligntype
isBoxed = BOXED_MESSAGES ---@type boolean
}
local NeatFormatMt = {__index = NeatFormat}
---@return NeatFormat
function NeatFormat.create()
local new = {}
setmetatable(new, NeatFormatMt)
return new
end
---@param copiedFormat NeatFormat
function NeatFormat:copy(copiedFormat)
self.spacing = copiedFormat.spacing
self.fadeOutTime = copiedFormat.fadeOutTime
self.fadeInTime = copiedFormat.fadeInTime
self.fontSize = copiedFormat.fontSize
self.minDuration = copiedFormat.minDuration
self.durationIncrease = copiedFormat.durationIncrease
self.verticalAlignment = copiedFormat.verticalAlignment
self.horizontalAlignment = copiedFormat.horizontalAlignment
self.isBoxed = copiedFormat.isBoxed
end
---@class NeatWindow
NeatWindow = {
xPosition = nil,
yPosition = nil,
maxHeight = nil,
width = nil,
maxTextMessages = nil,
isTopToBottom = nil,
textMessageFrame = nil,
textMessageText = nil,
textMessageBox = nil,
textCarryingFrame = nil,
textMessageIcon = nil,
messageFormat = nil,
messageTimeRemaining = nil,
messageTimeElapsed = nil,
textHeight = nil,
textMessageIconOrientation = nil,
messageOfFrame = 0,
}
local neatWindowMt = {__index = NeatWindow}
---@param xPosition number
---@param yPosition number
---@param width number
---@param maxHeight number
---@param maxTextMessages integer
---@param isTopToBottom boolean
---@return NeatWindow
function NeatWindow.create(xPosition, yPosition, width, maxHeight, maxTextMessages, isTopToBottom)
local new = {}
setmetatable(new, neatWindowMt)
new.xPosition = xPosition
new.yPosition = yPosition
new.maxHeight = maxHeight
new.width = width
new.maxTextMessages = maxTextMessages
new.isTopToBottom = isTopToBottom
if isTopToBottom then
new.yPosition = new.yPosition + maxHeight
end
numNeatWindows = numNeatWindows + 1
neatWindow[numNeatWindows] = new
new.textMessageFrame = {}
new.textMessageText = {}
new.textMessageBox = {}
new.textCarryingFrame = {}
new.textMessageIcon = {}
new.messageFormat = {}
new.textMessageIconOrientation = {}
new.textHeight = {}
new.messageTimeRemaining = {}
new.messageTimeElapsed = {}
new.messageOfFrame = {}
for i = 1, maxTextMessages do
if INCLUDE_FDF then
new.textMessageFrame[i] = BlzCreateFrame("TextMessage", BlzGetOriginFrame(ORIGIN_FRAME_GAME_UI, 0), 0, 0)
new.textMessageText[i] = BlzFrameGetChild( new.textMessageFrame[i],0)
new.textMessageBox[i] = BlzFrameGetChild( new.textMessageFrame[i],1)
BlzFrameSetSize(new.textMessageText[i], width, 0)
BlzFrameSetScale(new.textMessageText[i], TEXT_MESSAGE_FONT_SIZE/10.)
BlzFrameSetAbsPoint(new.textMessageFrame[i], FRAMEPOINT_BOTTOMLEFT, xPosition, yPosition)
BlzFrameSetTextAlignment(new.textMessageText[i] , VERTICAL_ALIGNMENT , HORIZONTAL_ALIGNMENT)
BlzFrameSetVisible( new.textMessageFrame[i] , false )
BlzFrameSetEnable(new.textMessageFrame[i],false)
BlzFrameSetEnable(new.textMessageText[i],false)
BlzFrameSetLevel(new.textMessageText[i],1)
BlzFrameSetLevel(new.textMessageBox[i],0)
new.textCarryingFrame[i] = new.textMessageText[i]
new.textMessageIcon[i] = BlzCreateFrameByType("BACKDROP", "textMessageIcon" .. i , BlzGetOriginFrame(ORIGIN_FRAME_GAME_UI, 0), "", 0)
BlzFrameSetEnable(new.textMessageIcon[i],false)
BlzFrameSetVisible(new.textMessageIcon[i],false)
new.messageFormat[i] = DEFAULT_NEAT_FORMAT
new.messageOfFrame[i] = 0
new.textHeight[i] = 0
new.messageTimeRemaining[i] = 0
new.messageTimeElapsed[i] = 0
new.messageOfFrame[i] = 0
ChangeTextFormatting(new, i, DEFAULT_NEAT_FORMAT)
ChangeText(new, i, "")
else
new.textMessageFrame[i] = BlzCreateFrameByType("TEXT", "textMessageFrame" , BlzGetOriginFrame(ORIGIN_FRAME_GAME_UI, 0), "", 0)
BlzFrameSetScale(new.textMessageFrame[i], TEXT_MESSAGE_FONT_SIZE/10.)
BlzFrameSetTextAlignment(new.textMessageFrame[i] , VERTICAL_ALIGNMENT , HORIZONTAL_ALIGNMENT)
BlzFrameSetAbsPoint(new.textMessageFrame[i], FRAMEPOINT_BOTTOMLEFT, xPosition, yPosition)
BlzFrameSetVisible( new.textMessageFrame[i] , false )
BlzFrameSetEnable(new.textMessageFrame[i],false)
new.textCarryingFrame[i] = new.textMessageFrame[i]
new.textMessageIcon[i] = BlzCreateFrameByType("BACKDROP", "textMessageIcon" .. i , BlzGetOriginFrame(ORIGIN_FRAME_GAME_UI, 0), "", 0)
BlzFrameSetEnable(new.textMessageIcon[i],false)
BlzFrameSetVisible(new.textMessageIcon[i],false)
new.messageFormat[i] = DEFAULT_NEAT_FORMAT
new.messageOfFrame[i] = 0
new.textHeight[i] = 0
new.messageTimeRemaining[i] = 0
new.messageTimeElapsed[i] = 0
new.messageOfFrame[i] = 0
ChangeTextFormatting(new, i, DEFAULT_NEAT_FORMAT)
ChangeText(new, i, "")
end
end
return new
end
--=========================================================================================
function ClearText()
doNotClear = true
ClearTextMessages()
doNotClear = false
end
---@param whichString string
---@return integer
local function GetAdjustedStringLength(whichString)
local rawLength = string.len(whichString) ---@type integer
local adjustedLength = rawLength ---@type integer
local j = 1 ---@type integer
local secondCharacter ---@type string
while j <= rawLength - 10 do
if string.sub(whichString, j, j) == "|" then
secondCharacter = string.lower(string.sub(whichString, j+1, j+1))
if secondCharacter == "c" then
adjustedLength = adjustedLength - 10
j = j + 10
elseif secondCharacter == "r" then
adjustedLength = adjustedLength - 2
j = j + 2
end
else
j = j + 1
end
end
return adjustedLength
end
---@param w NeatWindow
---@param whichFrame integer
---@param whichFormat NeatFormat
ChangeTextFormatting = function(w, whichFrame, whichFormat)
if whichFormat == 0 then
return
end
w.messageFormat[whichFrame] = whichFormat
BlzFrameSetScale(w.textCarryingFrame[whichFrame], whichFormat.fontSize/10.)
BlzFrameSetTextAlignment( w.textCarryingFrame[whichFrame], whichFormat.verticalAlignment, whichFormat.horizontalAlignment )
end
---@param w NeatWindow
---@param whichFrame integer
---@param whichText string
ChangeText = function(w, whichFrame, whichText)
BlzFrameSetText( w.textCarryingFrame[whichFrame] , whichText )
if w.maxTextMessages == 1 then
BlzFrameSetSize( w.textCarryingFrame[whichFrame] , w.width / (w.messageFormat[whichFrame].fontSize/10.) , w.maxHeight / (w.messageFormat[whichFrame].fontSize/10.) )
else
BlzFrameSetSize( w.textCarryingFrame[whichFrame] , w.width / (w.messageFormat[whichFrame].fontSize/10.) , 0 )
end
if INCLUDE_FDF then
BlzFrameSetSize( w.textMessageFrame[whichFrame] , BlzFrameGetWidth(w.textMessageText[whichFrame]) + 0.008 , BlzFrameGetHeight(w.textMessageText[whichFrame]) + 0.009 )
BlzFrameSetPoint( w.textMessageBox[whichFrame] , FRAMEPOINT_BOTTOMLEFT , w.textMessageFrame[whichFrame] , FRAMEPOINT_BOTTOMLEFT , 0 , -0.0007*(w.messageFormat[whichFrame].fontSize-13) )
BlzFrameSetPoint( w.textMessageBox[whichFrame] , FRAMEPOINT_TOPRIGHT , w.textMessageFrame[whichFrame] , FRAMEPOINT_TOPRIGHT , 0 , 0 )
end
if whichText == "" then
BlzFrameSetVisible( w.textMessageFrame[whichFrame] , false )
BlzFrameSetAlpha( w.textMessageFrame[whichFrame] , 255 )
else
BlzFrameSetVisible( w.textMessageFrame[whichFrame] , true )
end
end
---@param w NeatWindow
---@param whichFrame integer
---@param collapseFrame boolean
local function HideTextMessage(w, whichFrame, collapseFrame)
if BlzFrameGetText(w.textCarryingFrame[whichFrame]) ~= "" then
numMessagesOnScreen = numMessagesOnScreen - 1
end
ChangeText(w,whichFrame,"")
w.messageTimeRemaining[whichFrame] = 0
frameOfMessage[w.messageOfFrame[whichFrame]] = nil
w.messageOfFrame[whichFrame] = 0
if collapseFrame then
w.textHeight[whichFrame] = 0
end
if w.textMessageIconOrientation[whichFrame] ~= nil then
BlzFrameSetVisible( w.textMessageIcon[whichFrame] , false )
BlzFrameSetAlpha( w.textMessageIcon[whichFrame] , 255 )
end
end
local function FadeoutLoop()
local w ---@type NeatWindow
if numMessagesOnScreen == 0 then
return
end
for j = 1, numNeatWindows do
w = neatWindow[j]
for i = 1, w.maxTextMessages do
if w.messageTimeRemaining[i] > 0 then
w.messageTimeRemaining[i] = w.messageTimeRemaining[i] - TIME_STEP
w.messageTimeElapsed[i] = w.messageTimeElapsed[i] + TIME_STEP
if w.messageTimeRemaining[i] < w.messageFormat[i].fadeOutTime then
if w.messageTimeRemaining[i] < 0 then
HideTextMessage(w,i,false)
else
BlzFrameSetAlpha( w.textMessageFrame[i] , math.floor(255*w.messageTimeRemaining[i]/w.messageFormat[i].fadeOutTime) )
end
elseif w.messageTimeElapsed[i] < w.messageFormat[i].fadeInTime then
BlzFrameSetAlpha( w.textMessageFrame[i] , math.floor(255*w.messageTimeElapsed[i]/w.messageFormat[i].fadeInTime) )
end
if w.textMessageIconOrientation[i] ~= nil then
BlzFrameSetAlpha( w.textMessageIcon[i] , BlzFrameGetAlpha(w.textMessageFrame[i]) )
end
end
end
end
end
---@param w NeatWindow
local function RepositionAllMessages(w)
local yOffset = {}
--=========================================================================================
--Get message heights
--=========================================================================================
yOffset[1] = 0
for i = 2, w.maxTextMessages do
yOffset[i] = yOffset[i-1] + w.textHeight[i-1]
end
--=========================================================================================
--Reposition messages
--=========================================================================================
for i = 1, w.maxTextMessages do
if yOffset[i] + w.textHeight[i] > w.maxHeight then
HideTextMessage(w,i,true)
elseif w.isTopToBottom then
BlzFrameSetAbsPoint( w.textMessageFrame[i] , FRAMEPOINT_BOTTOMLEFT , w.xPosition , w.yPosition - w.textHeight[i] - yOffset[i] )
else
BlzFrameSetAbsPoint( w.textMessageFrame[i] , FRAMEPOINT_BOTTOMLEFT , w.xPosition , w.yPosition + yOffset[i] )
end
end
end
---@param whichText string
---@param forcedDuration number
---@param whichFormat NeatFormat
---@param w NeatWindow
---@return integer
local function AddTextMessage(whichText, forcedDuration, whichFormat, w)
local tempFrame ---@type framehandle
if whichText == "" then
return 0
end
if COPY_TO_MESSAGE_LOG and isSinglePlayer then
DisplayTextToPlayer( GetLocalPlayer() , 0 , 0 , whichText .. [[
]] )
ClearTextMessages()
end
if BlzFrameGetText(w.textCarryingFrame[w.maxTextMessages]) == "" then
numMessagesOnScreen = numMessagesOnScreen + 1
end
--=========================================================================================
--Transfer messages to next frame
--=========================================================================================
tempFrame = w.textMessageIcon[w.maxTextMessages]
for i = w.maxTextMessages - 1, 1, -1 do
w.messageTimeRemaining[i+1] = w.messageTimeRemaining[i]
w.messageTimeElapsed[i+1] = w.messageTimeElapsed[i]
ChangeTextFormatting(w, i+1, w.messageFormat[i])
ChangeText(w, i+1, BlzFrameGetText(w.textCarryingFrame[i]))
if w.messageOfFrame[i] ~= 0 then
w.messageOfFrame[i+1] = w.messageOfFrame[i]
frameOfMessage[w.messageOfFrame[i+1]] = i + 1
end
if w.messageTimeRemaining[i+1] < w.messageFormat[i+1].fadeOutTime then
BlzFrameSetAlpha( w.textMessageFrame[i+1] , math.floor(255*w.messageTimeRemaining[i+1]/w.messageFormat[i+1].fadeOutTime) )
else
BlzFrameSetAlpha( w.textMessageFrame[i+1] , 255 )
end
BlzFrameSetVisible( w.textMessageFrame[i+1] , true )
if INCLUDE_FDF then
BlzFrameSetVisible( w.textMessageBox[i+1] , BlzFrameIsVisible(w.textMessageBox[i]) )
end
w.textHeight[i+1] = w.textHeight[i]
w.textMessageIcon[i+1] = w.textMessageIcon[i]
w.textMessageIconOrientation[i+1] = w.textMessageIconOrientation[i]
if w.textMessageIconOrientation[i] ~= nil then
if w.textMessageIconOrientation[i+1] == "topleft" then
BlzFrameSetPoint( w.textMessageIcon[i+1] , FRAMEPOINT_TOPRIGHT , w.textMessageFrame[i+1] , FRAMEPOINT_TOPLEFT , 0 , 0 )
elseif w.textMessageIconOrientation[i+1] == "topright" then
BlzFrameSetPoint( w.textMessageIcon[i+1] , FRAMEPOINT_TOPLEFT , w.textMessageFrame[i+1] , FRAMEPOINT_TOPRIGHT , 0 , 0 )
elseif w.textMessageIconOrientation[i+1] == "bottomleft" then
BlzFrameSetPoint( w.textMessageIcon[i+1] , FRAMEPOINT_BOTTOMRIGHT , w.textMessageFrame[i+1] , FRAMEPOINT_BOTTOMLEFT , 0 , 0 )
elseif w.textMessageIconOrientation[i+1] == "bottomright" then
BlzFrameSetPoint( w.textMessageIcon[i+1] , FRAMEPOINT_BOTTOMLEFT , w.textMessageFrame[i+1] , FRAMEPOINT_BOTTOMRIGHT , 0 , 0 )
end
end
end
w.textMessageIcon[1] = tempFrame
--=========================================================================================
--Setup new message
--=========================================================================================
ChangeTextFormatting(w, 1, whichFormat)
ChangeText(w, 1, whichText)
w.textHeight[1] = BlzFrameGetHeight(w.textMessageFrame[1]) + math.max(whichFormat.spacing , w.messageFormat[1].spacing)
if INCLUDE_FDF then
BlzFrameSetVisible( w.textMessageBox[1], whichFormat.isBoxed )
end
if forcedDuration ~= 0 then
w.messageTimeRemaining[1] = forcedDuration + whichFormat.fadeOutTime
else
w.messageTimeRemaining[1] = whichFormat.minDuration + whichFormat.durationIncrease*GetAdjustedStringLength(whichText) + whichFormat.fadeOutTime
end
w.messageTimeElapsed[1] = 0
if whichFormat.fadeInTime > 0 then
BlzFrameSetAlpha(w.textMessageFrame[1] , 0)
else
BlzFrameSetAlpha(w.textMessageFrame[1] , 255)
end
BlzFrameSetVisible( w.textMessageFrame[1] , true )
w.textMessageIconOrientation[1] = nil
messageCounter = messageCounter + 1
w.messageOfFrame[1] = messageCounter
frameOfMessage[messageCounter] = 1
windowOfMessage[messageCounter] = w
if w.maxTextMessages > 1 then
RepositionAllMessages(w)
end
return messageCounter
end
local function Init()
local p ---@type integer
local numPlayers ---@type integer
if INCLUDE_FDF then
BlzLoadTOCFile("NeatMessageTemplates.toc")
end
masterTimer = CreateTimer()
TimerStart( masterTimer , TIME_STEP , true , FadeoutLoop )
DEFAULT_NEAT_FORMAT = NeatFormat.create()
DEFAULT_NEAT_WINDOW = NeatWindow.create(TEXT_MESSAGE_X_POSITION, TEXT_MESSAGE_Y_POSITION, TEXT_MESSAGE_BLOCK_WIDTH, TEXT_MESSAGE_BLOCK_MAX_HEIGHT, MAX_TEXT_MESSAGES, MESSAGE_ORDER_TOP_TO_BOTTOM)
if COPY_TO_MESSAGE_LOG then
p = 0
numPlayers = 1
while p <= 23 do
if GetPlayerSlotState(Player(p)) == PLAYER_SLOT_STATE_PLAYING and GetPlayerController(Player(p)) == MAP_CONTROL_USER then
numPlayers = numPlayers + 1
end
p = p + 1
end
isSinglePlayer = numPlayers == 1
end
if REPLACE_BLIZZARD_FUNCTION_CALLS then
local hook = Require.optionally "Hook"
if hook then
clearTextTimer = CreateTimer()
function Hook:DisplayTextToForce(whichForce, message)
local extractedString
BlzSetAbilityTooltip( FourCC(TOOLTIP_ABILITY), message, 0)
extractedString = BlzGetAbilityTooltip(FourCC(TOOLTIP_ABILITY), 0)
NeatMessageToForce(whichForce, extractedString)
TimerStart(clearTextTimer, 0.0, false, ClearText)
end
function Hook:DisplayTimedTextToForce(whichForce, duration, message)
local extractedString
BlzSetAbilityTooltip( FourCC(TOOLTIP_ABILITY), message, 0)
extractedString = BlzGetAbilityTooltip(FourCC(TOOLTIP_ABILITY), 0)
NeatMessageToForceTimed( whichForce , duration , extractedString )
TimerStart(clearTextTimer, 0.0, false, ClearText)
end
function Hook:ClearTextMessagesBJ(whichForce)
self.old(whichForce)
ClearNeatMessagesForForce(whichForce)
end
function Hook:ClearTextMessages()
self.old()
ClearNeatMessages()
end
else
error("REPLACE_BLIZZARD_FUNCTION_CALLS requires Hook library.")
end
end
end
--===========================================================================================
--API
--===========================================================================================
--Constructors
--===========================================================================================
---@param message string
---@return integer
function NeatMessage(message)
return AddTextMessage(message, 0, DEFAULT_NEAT_FORMAT, DEFAULT_NEAT_WINDOW)
end
---@param whichPlayer player
---@param message string
---@return integer
function NeatMessageToPlayer(whichPlayer, message)
if GetLocalPlayer() == whichPlayer then
return AddTextMessage(message , 0, DEFAULT_NEAT_FORMAT, DEFAULT_NEAT_WINDOW)
end
return 0
end
---@param whichForce force
---@param message string
---@return integer
function NeatMessageToForce(whichForce, message)
if IsPlayerInForce( GetLocalPlayer() , whichForce ) then
return AddTextMessage(message, 0, DEFAULT_NEAT_FORMAT, DEFAULT_NEAT_WINDOW)
end
return 0
end
---@param duration number
---@param message string
---@return integer
function NeatMessageTimed(duration, message)
return AddTextMessage(message, duration, DEFAULT_NEAT_FORMAT, DEFAULT_NEAT_WINDOW)
end
---@param whichPlayer player
---@param duration number
---@param message string
---@return integer
function NeatMessageToPlayerTimed(whichPlayer, duration, message)
if GetLocalPlayer() == whichPlayer then
return AddTextMessage(message, duration, DEFAULT_NEAT_FORMAT, DEFAULT_NEAT_WINDOW)
end
return 0
end
---@param whichForce force
---@param duration number
---@param message string
---@return integer
function NeatMessageToForceTimed(whichForce, duration, message)
if IsPlayerInForce( GetLocalPlayer() , whichForce ) then
return AddTextMessage(message, duration, DEFAULT_NEAT_FORMAT, DEFAULT_NEAT_WINDOW)
end
return 0
end
---@param message string
---@param whichFormat NeatFormat
---@return integer
function NeatMessageFormatted(message, whichFormat)
return AddTextMessage(message, 0, whichFormat, DEFAULT_NEAT_WINDOW)
end
---@param whichPlayer player
---@param message string
---@param whichFormat NeatFormat
---@return integer
function NeatMessageToPlayerFormatted(whichPlayer, message, whichFormat)
if GetLocalPlayer() == whichPlayer then
return AddTextMessage(message , 0, whichFormat, DEFAULT_NEAT_WINDOW)
end
return 0
end
---@param whichForce force
---@param message string
---@param whichFormat NeatFormat
---@return integer
function NeatMessageToForceFormatted(whichForce, message, whichFormat)
if IsPlayerInForce( GetLocalPlayer() , whichForce ) then
return AddTextMessage(message, 0, whichFormat, DEFAULT_NEAT_WINDOW)
end
return 0
end
---@param whichPlayer player
---@param duration number
---@param message string
---@param whichFormat NeatFormat
---@return integer
function NeatMessageToPlayerTimedFormatted(whichPlayer, duration, message, whichFormat)
if GetLocalPlayer() == whichPlayer then
return AddTextMessage(message, duration, whichFormat, DEFAULT_NEAT_WINDOW)
end
return 0
end
---@param whichForce force
---@param duration number
---@param message string
---@param whichFormat NeatFormat
---@return integer
function NeatMessageToForceTimedFormatted(whichForce, duration, message, whichFormat)
if IsPlayerInForce( GetLocalPlayer() , whichForce ) then
return AddTextMessage(message, duration, whichFormat, DEFAULT_NEAT_WINDOW)
end
return 0
end
---@param duration number
---@param message string
---@param whichFormat NeatFormat
---@return integer
function NeatMessageTimedFormatted(duration, message, whichFormat)
return AddTextMessage(message, duration, whichFormat, DEFAULT_NEAT_WINDOW)
end
---@param message string
---@param whichWindow NeatWindow
---@return integer
function NeatMessageInWindow(message, whichWindow)
return AddTextMessage(message, 0, DEFAULT_NEAT_FORMAT, whichWindow)
end
---@param whichPlayer player
---@param message string
---@param whichWindow NeatWindow
---@return integer
function NeatMessageToPlayerInWindow(whichPlayer, message, whichWindow)
if GetLocalPlayer() == whichPlayer then
return AddTextMessage(message , 0, DEFAULT_NEAT_FORMAT, whichWindow)
end
return 0
end
---@param whichForce force
---@param message string
---@param whichWindow NeatWindow
---@return integer
function NeatMessageToForceInWindow(whichForce, message, whichWindow)
if IsPlayerInForce( GetLocalPlayer() , whichForce ) then
return AddTextMessage(message, 0, DEFAULT_NEAT_FORMAT, whichWindow)
end
return 0
end
---@param duration number
---@param message string
---@param whichWindow NeatWindow
---@return integer
function NeatMessageTimedInWindow(duration, message, whichWindow)
return AddTextMessage(message, duration, DEFAULT_NEAT_FORMAT, whichWindow)
end
---@param whichPlayer player
---@param duration number
---@param message string
---@param whichWindow NeatWindow
---@return integer
function NeatMessageToPlayerTimedInWindow(whichPlayer, duration, message, whichWindow)
if GetLocalPlayer() == whichPlayer then
return AddTextMessage(message, duration, DEFAULT_NEAT_FORMAT, whichWindow)
end
return 0
end
---@param whichForce force
---@param duration number
---@param message string
---@param whichWindow NeatWindow
---@return integer
function NeatMessageToForceTimedInWindow(whichForce, duration, message, whichWindow)
if IsPlayerInForce( GetLocalPlayer() , whichForce ) then
return AddTextMessage(message, duration, DEFAULT_NEAT_FORMAT, whichWindow)
end
return 0
end
---@param message string
---@param whichFormat NeatFormat
---@param whichWindow NeatWindow
---@return integer
function NeatMessageFormattedInWindow(message, whichFormat, whichWindow)
return AddTextMessage(message, 0, whichFormat, whichWindow)
end
---@param whichPlayer player
---@param message string
---@param whichFormat NeatFormat
---@param whichWindow NeatWindow
---@return integer
function NeatMessageToPlayerFormattedInWindow(whichPlayer, message, whichFormat, whichWindow)
if GetLocalPlayer() == whichPlayer then
return AddTextMessage(message , 0, whichFormat, whichWindow)
end
return 0
end
---@param whichForce force
---@param message string
---@param whichFormat NeatFormat
---@param whichWindow NeatWindow
---@return integer
function NeatMessageToForceFormattedInWindow(whichForce, message, whichFormat, whichWindow)
if IsPlayerInForce( GetLocalPlayer() , whichForce ) then
return AddTextMessage(message, 0, whichFormat, whichWindow)
end
return 0
end
---@param whichPlayer player
---@param duration number
---@param message string
---@param whichFormat NeatFormat
---@param whichWindow NeatWindow
---@return integer
function NeatMessageToPlayerTimedFormattedInWindow(whichPlayer, duration, message, whichFormat, whichWindow)
if GetLocalPlayer() == whichPlayer then
return AddTextMessage(message, duration, whichFormat, whichWindow)
end
return 0
end
---@param whichForce force
---@param duration number
---@param message string
---@param whichFormat NeatFormat
---@param whichWindow NeatWindow
---@return integer
function NeatMessageToForceTimedFormattedInWindow(whichForce, duration, message, whichFormat, whichWindow)
if IsPlayerInForce( GetLocalPlayer() , whichForce ) then
return AddTextMessage(message, duration, whichFormat, whichWindow)
end
return 0
end
---@param duration number
---@param message string
---@param whichFormat NeatFormat
---@param whichWindow NeatWindow
---@return integer
function NeatMessageTimedFormattedInWindow(duration, message, whichFormat, whichWindow)
return AddTextMessage(message, duration, whichFormat, whichWindow)
end
--Utility
--===========================================================================================
---@param messagePointer integer
---@param newText string
function EditNeatMessage(messagePointer, newText)
local whichFrame = frameOfMessage[messagePointer] ---@type integer
local whichWindow = windowOfMessage[messagePointer] ---@type NeatWindow
if messagePointer == 0 or whichFrame == nil then
return
end
ChangeText(whichWindow, whichFrame, newText)
whichWindow.textHeight[whichFrame] = BlzFrameGetHeight(whichWindow.textMessageFrame[whichFrame]) + math.max(whichWindow.messageFormat[whichFrame].spacing , whichWindow.messageFormat[whichFrame+1].spacing)
RepositionAllMessages(whichWindow)
end
---@param messagePointer integer
---@param additionalTime number
function AddNeatMessageTimeRemaining(messagePointer, additionalTime)
local whichFrame = frameOfMessage[messagePointer] ---@type integer
local whichWindow = windowOfMessage[messagePointer] ---@type NeatWindow
if messagePointer == 0 or whichFrame == nil then
return
end
whichWindow.messageTimeRemaining[whichFrame] = math.max(whichWindow.messageTimeRemaining[whichFrame] + additionalTime, whichWindow.messageFormat[whichFrame].fadeOutTime)
end
---@param messagePointer integer
---@param newTime number
function SetNeatMessageTimeRemaining(messagePointer, newTime)
local whichFrame = frameOfMessage[messagePointer] ---@type integer
local whichWindow = windowOfMessage[messagePointer] ---@type NeatWindow
if messagePointer == 0 or whichFrame == nil then
return
end
whichWindow.messageTimeRemaining[whichFrame] = math.max(newTime, whichWindow.messageFormat[whichFrame].fadeOutTime)
end
---@param messagePointer integer
---@param accountForTimeElapsed boolean
function AutoSetNeatMessageTimeRemaining(messagePointer, accountForTimeElapsed)
local whichFrame = frameOfMessage[messagePointer] ---@type integer
local whichWindow = windowOfMessage[messagePointer] ---@type NeatWindow
local whichFormat = whichWindow.messageFormat[whichFrame] ---@type NeatFormat
if messagePointer == 0 or whichFrame == nil then
return
end
whichWindow.messageTimeRemaining[whichFrame] = whichFormat.minDuration + whichFormat.durationIncrease*GetAdjustedStringLength(BlzFrameGetText(whichWindow.textCarryingFrame[whichFrame])) + whichFormat.fadeOutTime
if accountForTimeElapsed then
whichWindow.messageTimeRemaining[whichFrame] = math.max(whichWindow.messageTimeRemaining[whichFrame] - whichWindow.messageTimeElapsed[whichFrame], whichFormat.fadeOutTime)
end
end
---@param messagePointer integer
function RemoveNeatMessage(messagePointer)
local whichFrame = frameOfMessage[messagePointer] ---@type integer
local whichWindow = windowOfMessage[messagePointer] ---@type NeatWindow
if messagePointer == 0 or whichFrame == nil then
return
end
whichWindow.messageTimeRemaining[whichFrame] = whichWindow.messageFormat[whichFrame].fadeOutTime
end
---@param messagePointer integer
---@return boolean
function IsNeatMessageDisplayed(messagePointer)
return frameOfMessage[messagePointer] ~= nil
end
---@param messagePointer integer
---@param width number
---@param height number
---@param orientation string
---@param texture string
function NeatMessageAddIcon(messagePointer, width, height, orientation, texture)
local whichFrame = frameOfMessage[messagePointer] ---@type integer
local whichWindow = windowOfMessage[messagePointer] ---@type NeatWindow
BlzFrameSetVisible( whichWindow.textMessageIcon[whichFrame] , true )
BlzFrameSetAlpha( whichWindow.textMessageIcon[whichFrame] , 255 )
BlzFrameSetSize( whichWindow.textMessageIcon[whichFrame] , width , height )
if orientation == "topleft" then
BlzFrameSetPoint( whichWindow.textMessageIcon[whichFrame] , FRAMEPOINT_TOPRIGHT , whichWindow.textMessageFrame[whichFrame] , FRAMEPOINT_TOPLEFT , 0 , 0 )
elseif orientation == "topright" then
BlzFrameSetPoint( whichWindow.textMessageIcon[whichFrame] , FRAMEPOINT_TOPLEFT , whichWindow.textMessageFrame[whichFrame] , FRAMEPOINT_TOPRIGHT , 0 , 0 )
elseif orientation == "bottomleft" then
BlzFrameSetPoint( whichWindow.textMessageIcon[whichFrame] , FRAMEPOINT_BOTTOMRIGHT , whichWindow.textMessageFrame[whichFrame] , FRAMEPOINT_BOTTOMLEFT , 0 , 0 )
elseif orientation == "bottomright" then
BlzFrameSetPoint( whichWindow.textMessageIcon[whichFrame] , FRAMEPOINT_BOTTOMLEFT , whichWindow.textMessageFrame[whichFrame] , FRAMEPOINT_BOTTOMRIGHT , 0 , 0 )
else
print("Invalid icon orientation...")
return
end
BlzFrameSetTexture( whichWindow.textMessageIcon[whichFrame] , texture , 0 , true )
whichWindow.textMessageIconOrientation[whichFrame] = orientation
if BlzFrameGetHeight( whichWindow.textMessageFrame[whichFrame] ) < height then
BlzFrameSetSize( whichWindow.textCarryingFrame[whichFrame] , whichWindow.width / (whichWindow.messageFormat[whichFrame].fontSize/10.) , height - 0.018 )
if INCLUDE_FDF then
BlzFrameSetSize( whichWindow.textMessageFrame[whichFrame] , BlzFrameGetWidth(whichWindow.textMessageText[whichFrame]) + 0.008 , BlzFrameGetHeight(whichWindow.textMessageText[whichFrame]) + 0.009 )
BlzFrameSetPoint( whichWindow.textMessageBox[whichFrame] , FRAMEPOINT_BOTTOMLEFT , whichWindow.textMessageFrame[whichFrame] , FRAMEPOINT_BOTTOMLEFT , 0 , -0.0007*(whichWindow.messageFormat[whichFrame].fontSize-13) )
BlzFrameSetPoint( whichWindow.textMessageBox[whichFrame] , FRAMEPOINT_TOPRIGHT , whichWindow.textMessageFrame[whichFrame] , FRAMEPOINT_TOPRIGHT , 0 , 0 )
end
whichWindow.textHeight[whichFrame] = BlzFrameGetHeight(whichWindow.textMessageFrame[whichFrame]) + math.max(whichWindow.messageFormat[whichFrame].spacing , whichWindow.messageFormat[whichFrame+1].spacing)
RepositionAllMessages(whichWindow)
end
end
---@param messagePointer integer
function NeatMessageHideIcon(messagePointer)
local whichFrame = frameOfMessage[messagePointer] ---@type integer
local whichWindow = windowOfMessage[messagePointer] ---@type NeatWindow
BlzFrameSetVisible( whichWindow.textMessageIcon[whichFrame] , false )
whichWindow.textMessageIconOrientation[whichFrame] = nil
end
---@param whichPlayer player
function ClearNeatMessagesForPlayer(whichPlayer)
if GetLocalPlayer() == whichPlayer then
for j = 1, numNeatWindows do
for i = 1, neatWindow[j].maxTextMessages do
HideTextMessage(neatWindow[j], i, true)
end
end
end
end
function ClearNeatMessages()
if doNotClear then
return
end
for j = 1, numNeatWindows do
for i = 1, neatWindow[j].maxTextMessages do
HideTextMessage(neatWindow[j], i, true)
end
end
end
---@param whichForce force
function ClearNeatMessagesForForce(whichForce)
if IsPlayerInForce(GetLocalPlayer() , whichForce) then
for j = 1, numNeatWindows do
for i = 1, neatWindow[j].maxTextMessages do
HideTextMessage(neatWindow[j], i, true)
end
end
end
end
---@param whichPlayer player
---@param whichWindow NeatWindow
function ClearNeatMessagesForPlayerInWindow(whichPlayer, whichWindow)
if GetLocalPlayer() == whichPlayer then
for i = 1, whichWindow.maxTextMessages do
HideTextMessage(whichWindow, i, true)
end
end
end
---@param whichWindow NeatWindow
function ClearNeatMessagesInWindow(whichWindow)
for i = 1, whichWindow.maxTextMessages do
HideTextMessage(whichWindow, i, true)
end
end
---@param whichForce force
---@param whichWindow NeatWindow
function ClearNeatMessagesForForceInWindow(whichForce, whichWindow)
if IsPlayerInForce(GetLocalPlayer() , whichForce) then
for i = 1, whichWindow.maxTextMessages do
HideTextMessage(whichWindow, i, true)
end
end
end
OnInit.global("NeatTextMessages", Init)
end
--[[
==========================================================================================================================================================
Antares' Hero Selection
==========================================================================================================================================================
A P I
==========================================================================================================================================================
function InitHeroSelection() //Creates hero list, player list, and frames.
function BeginHeroSelection() //Enforces camera and/or shows menu, based on config.
function EnableHeroSelection() //Allows heroes to be picked.
function EnablePlayerHeroSelection(whichPlayer, enable)
function EnablePlayerHeroPreselection(whichPlayer, enable)
function PlayerEscapeHeroSelection(whichPlayer) //Hides frames and unlocks camera for player.
function PlayerReturnToHeroSelection(whichPlayer) //Allows player to repick his or her hero.
function HeroSelectionAllRandom()
function HeroSelectionPlayerForceSelect(whichPlayer, whichHero)
function HeroSelectionPlayerForceRandom(whichPlayer)
function HeroSelectionForceEnd() //Will give each player who hasn't picked a random hero.
function BanHeroFromSelection(whichHero, disable) //Hero will remain in the menu.
function BanHeroFromSelectionForPlayer(whichHero, whichPlayer, disable)
function HeroSelectionSetTimeRemaining(time)
function HeroSelectionAddTimeRemaining(time)
function RestartHeroSelection() //For game restarts. Resets all variables and calls BeginHeroSelection. No one can be in hero selection at the time.
function StartHeroBanPhase(timeLimit) //Starts a ban phase with specified time limit, after which EnableHeroSelection is called. Requires MENU_INCLUDE_BAN_BUTTON.
function StartHeroSelectionTimer(timeLimit, callback) //Starts the timer that is displayed during hero selection with a custom callback function.
function NoOneInHeroSelection()
function EveryoneHasHero()
==========================================================================================================================================================
How to import:
Copy into empty triggers in your map:
HeroSelectionDocumentation
HeroSelectionConfig
HeroSelectionCallbacks
HeroDeclaration
HeroSelection
Import:
HeroSelectionMenu.fdf
CustomTooltip.fdf
HeroSelectionTemplates.toc
GeneralHeroGlow.mdx (optional)
Copy Destructables:
Hero No Shadow (optional)
Hero Shadow (optional)
===========================================================================================
Getting started:
HeroSelection has a long config that you should go through from top to bottom, with the
customization options becoming more and more specific as you progress. The fine-tuning
section has a vast number of options, which may look overwhelming, but you do not need
to edit any of them to get a working system.
To get a working system, here is the To-Do List:
1. Set up the terrain and the hero selection camera.
2. Import your heroes into HeroDeclaration and set the fields.
3. Set up tooltips for your heroes.
4. Create a trigger in your map that initializes the hero selection.
5. Set up the interface functions OnEscape and OnFinal.
===========================================================================================
1. Hero Selection Camera
To set up the hero selection camera, edit
FOREGROUND_HERO_X
FOREGROUND_HERO_Y
HERO_SELECTION_ANGLE
===========================================================================================
2. Importing Heroes
To import your heroes, go into HeroDeclaration and create a repository for your heroes:
MyHero1 = Hero.create()
MyHero2 = Hero.create()
Then, set the fields for your heroes as specified. Some of the variables may already be
set elsewhere in your map. In that case, I recommend writing a function to automatically
copy the variables into those fields.
===========================================================================================
3. Tooltips
To set up a tooltip, you can either write it directly during hero declaration using the
tooltip field. You can also fetch the tooltip from the unit itself or from an ability by
setting the tooltipAbility field. To fetch it from an ability, create a new non-hero ability
with one level. Set the tooltipAbility field to the id of whichever object you want to use.
===========================================================================================
4. Initializing Hero Selection
This system has no init function that runs automatically, since it requires the hero and
player lists to initialize and you may want to set them at a later point. To initialize, do:
InitHeroSelection()
This will create frames, create the hero list, and initialize players. To start hero selection, do:
BeginHeroSelection()
This will enforce the hero selection camera and/or show the menu to players, depending on
your config. However, players will not be able to select heroes until you do:
EnableHeroSelection()
===========================================================================================
5. Creating the Interface
Now that we have a trigger to begin hero selection, we have to be able to exit it and
create the heroes that were selected. To do this, edit the OnEscape and OnFinal function
in the callbacks section. The variables you can use in those functions are "HeroIdOfPlayer",
which stores the unit id of the hero selected, and "HeroIconPathOfPlayer", which can be useful
for updating a multiboard icon, for example.
===========================================================================================
After the hero selection system is successfully integrated into your map, go to the config
section and adjust the parameters controlling the system's behavior. Then, when everything
is working as intended, you can go into the fine-tuning section and go nuts customizing
every part of the system.
You may want to change some parameters based on the game state. Note, however, that changing
some constants after hero selection has been initialized might have no effect and/or lead to
bugs.
===========================================================================================
You can customize the appearance of the hero selection menu background by editing the
HeroSelectionMenu.fdf. By default, the background uses the escape menu textures of whatever
race the player is playing, but this can be changed by editing BackdropBackground and
BackdropEdgeFile. Remove DecorateFileNames when using imported assets!
One important parameter is BackdropCornerSize, which controls the thickness of the border
texture. Make sure to update MENU_BORDER_TILE_SIZE accordingly, so that the textures remain
seamless.
Read more here on how to edit an .fdf file:
https://www.hiveworkshop.com/threads/ui-reading-a-fdf.315850/
===========================================================================================
Neat Messages:
The showcase map includes NeatMessages, a library designed to give more control over text
messages. With default text messages, you will most likely run into the problem that they
cannot be placed where you want them to and they might hide a UI element or the heroes as a
result. Therefore, if you want to add text messages to the hero selection, I strongly
recommend using NeatMessages.
https://www.hiveworkshop.com/threads/neat-text-messages.350336/
===========================================================================================
How to setup multiple pages:
Some maps might have more heroes than fit on one page. To setup multiple pages, divide your
heroes into multiple categories in HeroDeclaration. To hide the category captions, set the
CATEGORY_NAMES to nil. Now, set the PAGE_OF_CATEGORY to the page at which the heroes of that
category should be appear.
===========================================================================================
How to setup a draft:
To setup a draft, do BeginHeroSelection, but then, instead of using EnableHeroSelection,
do EnablePlayerHeroSelection for the player that should pick the first hero. Set a time
limit for that player picking his or her hero with StartHeroSelectionTimer(timeLimit, yourCallback).
In your callback, do HeroSelectionPlayerForceRandom. Finally, in the OnPick custom code function,
write logic to enable hero selection for the next player.
===========================================================================================
Screen coordinates:
All UI element positions are given in screen coordinates. x-coordinates go from 0 (left) to
0.8, excluding the widescreen area (no frame can be put there). y-coordinates go from 0 (bottom)
to 0.6 (top). On a 1080p monitor, 0.00056 corresponds to 1 pixel. 0.01 corresponds to ~18 pixel.
You can place the hero selection menu as well as the mouse-over tooltips into the widescreen
area by setting the x-position to a negative number / a number greater than 0.8. The width of
the widescreen area is 0.6*(16/9 - 4/3)/2 = 0.1333, so the lowest possible x-coordinate is
-0.1333 and the highest 0.9333. The position will automatically be adjusted for players who
are not using a widescreen monitor.
Read more on screen coordinates here:
https://www.hiveworkshop.com/threads/ui-positionate-frames.315860/
===========================================================================================
Page cycle button styles (for PAGE_CYCLE_BUTTON_TEXT_STYLE):
"EnvelopRandomButton" Icon-style buttons directly left and right of the random buttons.
"LeftRightMenuButton" Icon-style buttons left and right of the menu in the same row as the random buttons.
"BelowRandomButton" Icon-style buttons below the random buttons.
"LeftRightMiddleButton" Icon-style buttons in the middle of the left and right edges of the menu.
"LeftVerticalButton" Icon-style buttons in the middle of the left edge of the menu, arranged vertically.
"RightVerticalButton" Icon-style buttons in the middle of the right edge of the menu, arranged vertically.
"EnvelopAcceptButton" Text-style buttons directly left and right of the accept button.
"LeftRightBottomButton" Text-style buttons on the bottom edge in the left and right corners.
"TangentTopButton" Text-style buttons in the center of the top edge right next to each other.
"LeftRightTopButton" Text-style buttons on the top edge in the left and right corners.
]]
if Debug then Debug.beginFile 'HeroDeclaration' end
---@diagnostic disable: undefined-global
do
MountainKing = nil ---@type Hero
Paladin = nil ---@type Hero
Archmage = nil ---@type Hero
BloodMage = nil ---@type Hero
PriestessOfTheMoon = nil ---@type Hero
DemonHunter = nil ---@type Hero
KeeperOfTheGrove = nil ---@type Hero
Warden = nil ---@type Hero
function HeroDeclaration()
--========================================================================================
--Here you declare all heroes in your map. The order in which you create them determine the order in which they appear in the hero selection menu.
--The fields that should be set are:
--[[
integer array abilities The abilities of this hero. Starts at index 1.
boolean array isNonHeroAbility Set this flag if the ability at the index is a non-hero ability. This will make the tooltip instead of the research tooltip appear on mouse-over.
integer unitId Unit id of the hero.
integer tooltipAbility Set up any non-hero ability with one level for each hero and write the hero's description in its tooltip.
string selectEmote Sound path of the emote the hero should play when selected.
animtype selectAnim animType of select animation. For example, "spell" is ANIMTYPE_SPELL.
subanimtype selectSubAnim subanimtype of select animation. For example "spell slam" is ANIMTYPE_SPELL + SUBANIMTYPE_SLAM.
number selectAnimLength Length of the select animation. If set incorrectly, animation will be interrupted or freeze.
integer category In which category in the menu should this hero be put?
boolean needsHeroGlow A hero glow will be added to heroes with models that don't have a glow. Requires "GeneralHeroGlow.mdx" to be imported.
boolean unavailable Heroes with this flag will appear in the menu but cannot be picked.
boolean array unavailableToTeam Hero will not appear in the menu for players in a team for which this flag was set. Can be used to create completely different hero rosters for different teams.
]]
--========================================================================================
MountainKing = Hero.create()
Paladin = Hero.create()
Archmage = Hero.create()
BloodMage = Hero.create()
PriestessOfTheMoon = Hero.create()
DemonHunter = Hero.create()
KeeperOfTheGrove = Hero.create()
Warden = Hero.create()
--========================================================================================
Archmage.abilities[1] = FourCC('AHbz')
Archmage.abilities[2] = FourCC('AHwe')
Archmage.abilities[3] = FourCC('AHab')
Archmage.abilities[4] = FourCC('AHmt')
Archmage.selectEmote = "Units\\Human\\HeroArchMage\\HeroArchMageWarcry1.flac"
Archmage.unitId = FourCC('Hamg')
Archmage.tooltip = "dudenstein"
Archmage.needsHeroGlow = false
Archmage.category = 3
Archmage.selectAnim = ANIM_TYPE_SPELL
Archmage.selectSubAnim = nil
Archmage.selectAnimLength = 2.7
--========================================================================================
Paladin.abilities[1] = FourCC('AHhb')
Paladin.abilities[2] = FourCC('AHds')
Paladin.abilities[3] = FourCC('AHad')
Paladin.abilities[4] = FourCC('AHre')
Paladin.selectEmote = "Units\\Human\\HeroPaladin\\HeroPaladinYesAttack1.flac"
Paladin.unitId = FourCC('Hpal')
Paladin.tooltipAbility = FourCC('Tpal')
Paladin.needsHeroGlow = false
Paladin.category = 1
Paladin.selectAnim = ANIM_TYPE_SPELL
Paladin.selectSubAnim = nil
Paladin.selectAnimLength = 2.167
--========================================================================================
MountainKing.abilities[1] = FourCC('AHtb')
MountainKing.abilities[2] = FourCC('AHtc')
MountainKing.abilities[3] = FourCC('AHbh')
MountainKing.abilities[4] = FourCC('AHav')
MountainKing.selectEmote = "Units\\Human\\HeroMountainKing\\HeroMountainKingYesAttack1.flac"
MountainKing.unitId = FourCC('Hmkg')
MountainKing.tooltipAbility = FourCC('Tmkg')
MountainKing.needsHeroGlow = false
MountainKing.category = 1
MountainKing.selectAnim = ANIM_TYPE_SPELL
MountainKing.selectSubAnim = SUBANIM_TYPE_SLAM
MountainKing.selectAnimLength = 1.0
--========================================================================================
BloodMage.abilities[1] = FourCC('AHfs')
BloodMage.abilities[2] = FourCC('AHbn')
BloodMage.abilities[3] = FourCC('AHdr')
BloodMage.abilities[4] = FourCC('AHpx')
BloodMage.selectEmote = "Units\\Human\\HeroBloodElf\\BloodElfMageYesAttack2.flac"
BloodMage.unitId = FourCC('Hblm')
BloodMage.tooltipAbility = FourCC('Tblm')
BloodMage.needsHeroGlow = false
BloodMage.category = 3
BloodMage.selectAnim = ANIM_TYPE_SPELL
BloodMage.selectSubAnim = SUBANIM_TYPE_CHANNEL
BloodMage.selectAnimLength = 2.0
--========================================================================================
PriestessOfTheMoon.abilities[1] = FourCC('AEst')
PriestessOfTheMoon.abilities[2] = FourCC('AHfa')
PriestessOfTheMoon.abilities[3] = FourCC('AEar')
PriestessOfTheMoon.abilities[4] = FourCC('AEsf')
PriestessOfTheMoon.selectEmote = "Units\\nightElf\\HeroMoonPriestess\\HeroMoonPriestessYesAttack2.flac"
PriestessOfTheMoon.unitId = FourCC('Emoo')
PriestessOfTheMoon.tooltipAbility = FourCC('Tmoo')
PriestessOfTheMoon.needsHeroGlow = false
PriestessOfTheMoon.category = 2
PriestessOfTheMoon.selectAnim = ANIM_TYPE_SPELL
PriestessOfTheMoon.selectSubAnim = nil
PriestessOfTheMoon.selectAnimLength = 1.333
--========================================================================================
DemonHunter.abilities[1] = FourCC('AEmb')
DemonHunter.abilities[2] = FourCC('AEim')
DemonHunter.abilities[3] = FourCC('AEev')
DemonHunter.abilities[4] = FourCC('AEme')
DemonHunter.selectEmote = "Units\\NightElf\\HeroDemonHunter\\HeroDemonHunterYesAttack1.flac"
DemonHunter.unitId = FourCC('Edem')
DemonHunter.tooltipAbility = FourCC('Tdem')
DemonHunter.needsHeroGlow = false
DemonHunter.category = 2
DemonHunter.selectAnim = ANIM_TYPE_SPELL
DemonHunter.selectSubAnim = SUBANIM_TYPE_THROW
DemonHunter.selectAnimLength = 0.9
--========================================================================================
KeeperOfTheGrove.abilities[1] = FourCC('AEer')
KeeperOfTheGrove.abilities[2] = FourCC('AEfn')
KeeperOfTheGrove.abilities[3] = FourCC('AEah')
KeeperOfTheGrove.abilities[4] = FourCC('AEtq')
KeeperOfTheGrove.selectEmote = "Units\\NightElf\\HeroKeeperOfTheGrove\\KeeperOfTheGroveYesAttack2.flac"
KeeperOfTheGrove.unitId = FourCC('Ekee')
KeeperOfTheGrove.tooltipAbility = FourCC('Tkee')
KeeperOfTheGrove.needsHeroGlow = false
KeeperOfTheGrove.category = 3
KeeperOfTheGrove.selectAnim = ANIM_TYPE_STAND
KeeperOfTheGrove.selectSubAnim = SUBANIM_TYPE_VICTORY
KeeperOfTheGrove.selectAnimLength = 3.333
--========================================================================================
Warden.abilities[1] = FourCC('AEfk')
Warden.abilities[2] = FourCC('AEbl')
Warden.abilities[3] = FourCC('AEsh')
Warden.abilities[4] = FourCC('AEsv')
Warden.selectEmote = "Units\\nightElf\\HeroWarden\\HeroWardenYesAttack3.flac"
Warden.unitId = FourCC('Ewar')
Warden.tooltipAbility = FourCC('Twar')
Warden.needsHeroGlow = false
Warden.category = 2
Warden.selectAnim = ANIM_TYPE_SPELL
Warden.selectSubAnim = nil
Warden.selectAnimLength = 1.2
--========================================================================================
end
end
if Debug then Debug.endFile() end
if Debug then Debug.beginFile 'HeroSelectionCallbacks' end
do
--=================================================================================================================================
--Globals for interfacing with rest of map.
--=================================================================================================================================
HeroIdOfPlayer = {} --Stores the unit id of the hero each player picked.
HeroIconPathOfPlayer = {} --Stores the icon path of the hero each player picked.
--=================================================================================================================================
--Can also be used in custom code. Read-Only!
SelectedHeroEffect = {} ---@type effect[]
PlayerHasHero = {} ---@type boolean[] --Player has picked a hero (does not mean hero was created).
IsInHeroSelection = {} ---@type boolean[] --Player is currently viewing the hero selection screen.
InBanPhase = false ---@type boolean
PlayerIsHuman = {} ---@type boolean[]
ColoredPlayerName = {} ---@type string[]
HeroSelectionDisabledForPlayer = {} ---@type boolean[]
HeroPreselectionDisabledForPlayer = {} ---@type boolean[]
PlayerHasBan = {} ---@type boolean[]
IsRepicking = {} ---@type boolean[]
SelectingPlayers = {} ---@type player[]
NumPlayersWithHero = 0 ---@type integer
NumPlayersInSelection = 0 ---@type integer
NumSelectingPlayers = 0 ---@type integer
NumSelectingHumans = 0 ---@type integer
PlayerNumberOfSlot = {} ---@type integer[]
BACKGROUND_HERO_X = {} ---@type number[]
BACKGROUND_HERO_Y = {} ---@type number[]
NUMBER_OF_HEROES = 0 ---@type integer
--=================================================================================================================================
--These functions are the main interface with the rest of your map.
--=================================================================================================================================
---@param whichPlayer player
function HeroSelectionOnEscape(whichPlayer)
--Here you can insert code that is executed when a player is kicked out of hero selection. This should include creating
--the hero that player selected as well as setting that player's camera location. Will be executed before OnFinal.
--Example code:
PlayerHero[whichPlayer] = CreateUnit(whichPlayer, HeroIdOfPlayer[whichPlayer], HERO_SPAWN_X, HERO_SPAWN_Y, 90)
if GetLocalPlayer() == whichPlayer then
SetCameraPosition(HERO_SPAWN_X, HERO_SPAWN_Y)
end
if ClearNeatMessages then
ClearNeatMessages()
end
end
function HeroSelectionOnFinal()
--Here you can insert the code that is executed when hero selection is concluded. This will most likely involve calling the main
--function that progresses your map after hero selection ends. It is recommended to execute player-specific actions in OnEscape
--instead.
--Example code:
if BeginGame then
BeginGame()
end
end
--=================================================================================================================================
--These functions allow you to add additional visual effects, sounds, texts etc. to the hero selection that can't be achieved with
--the default options.
--=================================================================================================================================
function HeroSelectionOnRestart()
--Here you can insert additional code that is executed when hero selection is restarted.
--Example code:
for __, P in ipairs(SelectingPlayers) do
DestroyEffect(Circles[P])
end
end
function HeroSelectionOnLast()
--Here you can insert additional code that is executed when the last player picks a hero.
--Example code:
if MusicSlowFadeOut then
MusicSlowFadeOut()
end
end
---@param whichPlayer player
---@param whichHero Hero
---@param wasRandomSelect boolean
---@param wasRepick boolean
function HeroSelectionOnPick(whichPlayer, whichHero, wasRandomSelect, wasRepick)
--Here you can insert additional code that is executed when a player picks a hero.
end
---@param whichPlayer player
---@param oldHero Hero
---@param newHero Hero
function HeroSelectionOnPreselect(whichPlayer, oldHero, newHero)
--Here you can insert additional code that is executed when a player switches to a new hero during pre-selection.
end
---@param whichPlayer player
function HeroSelectionOnReturn(whichPlayer)
--Here you can insert additional code that is executed when a player returns to hero selection.
end
function HeroSelectionOnAllRandom()
--Here you can insert additional code that is executed when all random mode is selected.
end
function HeroSelectionOnBegin()
--Here you can insert additional code that is executed when players are locked into hero selection.
--Example code:
if NeatMessageTimedInWindow then
NeatMessageTimedInWindow(5, "Heroes from all across Azeroth have gathered to fight the darkness. Only you can save this land...", CenterWindow)
else
DisplayTimedTextToForce(GetPlayersAll(), 5, "Heroes from all across Azeroth have gathered to fight the darkness. Only you can save this land...")
end
end
function HeroSelectionOnEnable()
--Here you can insert additional code that is executed when hero selection is enabled.
--Example code:
local tempLoc
Circles = Circles or {} ---@type effect[]
for __, P in ipairs(SelectingPlayers) do
Circles[P] = AddSpecialEffect("buildings\\other\\CircleOfPower\\CircleOfPower.mdl", BACKGROUND_HERO_X[P], BACKGROUND_HERO_Y[P])
BlzSetSpecialEffectColorByPlayer(Circles[P], Player(PLAYER_NEUTRAL_AGGRESSIVE))
tempLoc = Location(BACKGROUND_HERO_X[P], BACKGROUND_HERO_Y[P])
BlzSetSpecialEffectZ(Circles[P], GetLocationZ(tempLoc) + 10)
RemoveLocation(tempLoc)
end
ClearMapMusic()
PlayMusic("Nightsong.mp3")
end
function HeroSelectionOnExpire()
--Here you can insert the code that is executed when the timer for the hero selection duration expires.
--Example code:
if NeatMessageTimedInWindow then
NeatMessageTimedInWindow(4, "The time has expired.", CenterWindow)
end
end
---@param whichPlayer player
function HeroSelectionOnLeave(whichPlayer)
--Here you can insert additional code that is executed when a player who is in hero selection leaves the game.
end
end
if Debug then Debug.endFile() end
if Debug then Debug.beginFile 'HeroSelectionConfig' end
do
AHS = setmetatable({
--=================================================================================================================================
--These constants determine important aspects of the hero selection. You can fine-tune visuals, sounds etc. further down below.
--=================================================================================================================================
HERO_SELECTION_ENABLE_DEBUG_MODE = true ---@type boolean --Printout errors and check for function crashes.
--Overall behavior.
,HERO_CAN_BE_PICKED_MULTIPLE_TIMES = false ---@type boolean --Set to true if the same hero should be able to be picked multiple times.
,AUTO_SET_SELECTING_PLAYERS = false ---@type boolean --Set to true if hero selection should be enabled for all human players. Set to false if you want to set the player list manually (which can include computer players).
,COMPUTER_AUTO_PICK_RANDOM_HERO = false ---@type boolean --Set to true if computer players that are in hero selection should automatically pick a random hero after all human players have picked.
,ESCAPE_PLAYER_AFTER_SELECTING = false ---@type boolean --Set to true if a player picking a hero should kick him or her out of the hero selection menu and camera.
,CONCEAL_HERO_PICKS_FROM_ENEMIES = false ---@type boolean --Set to true if hero picks should be concealed from enemies (players in another team or all players if there are no teams), including emote, effect, and text messages.
,MENU_INCLUDE_RANDOM_PICK = true ---@type boolean --Include button for picking a random hero at the bottom of the menu.
,MENU_INCLUDE_SUGGEST_RANDOM = true ---@type boolean --Include button to pre-select a random hero next to the random pick button.
,TIME_LIMIT = 60 ---@type number --Set a time limit after which players who haven't chosen a hero will be assigned one at random. Set to 0 for no time limit.
--Camera and foreground hero positions (background hero locations are set in GetPlayerBackgroundHeroLocation). Set to a table to create separate locations for different teams (all three must be tables or none).
,FOREGROUND_HERO_X = -128 ---@type number | number[] --x-Position of foreground hero.
,FOREGROUND_HERO_Y = 256 ---@type number | number[] --y-Position of foreground hero.
,HERO_SELECTION_ANGLE = 0 ---@type number | number[] --Hero selection viewing direction of camera (0 = facing east, 90 = facing north etc.).
--Visuals.
,ENFORCE_CAMERA = true ---@type boolean --Set to false if players' camera should not be changed during hero selection.
,HIDE_GAME_UI = true ---@type boolean --Set to true if all UI elements other than the hero selection menu should be hidden.
,CREATE_FOREGROUND_HERO = true ---@type boolean --Set to false if no foreground hero should appear on preselection.
,CREATE_BACKGROUND_HEROES = true ---@type boolean --Set to true if players should be able to see other players' picks in the background.
--Categories.
,CATEGORY_NAMES = {"|cffffcc00Strength|r", ---@type string[] --Names of the hero categories that appear in the hero selection menu.
"|cffffcc00Agility|r",
"|cffffcc00Intelligence|r"}
,PAGE_OF_CATEGORY = {1, 1, 1} ---@type integer[] --Set the page at which that category appears within the menu. When there is more than one page, page cycle buttons appear in the menu.
--Players.
,PLAYER_SELECTS_HERO = { ---@type boolean[] --Set the list of players for which hero selection is enabled (only if AUTO_SET_SELECTING_PLAYERS = false). It is disabled here because computer players must be added to the hero selection.
true,
true,
true,
true,
true,
true
}
,TEAM_OF_PLAYER = { ---@type integer[] --Set the teams of players. Players with team 0 are enemies to all players.
1,
1,
1,
2,
2,
2
}
--=================================================================================================================================
---@param playerIndex integer
---@param whichSlot integer
---@param numberOfPlayers integer
---@param whichTeam integer
---@param slotInTeam integer
---@param teamSize integer
---@return number, number
,GetPlayerBackgroundHeroLocation = function(playerIndex, whichSlot, numberOfPlayers, whichTeam, slotInTeam, teamSize)
local deltaHorizontal ---@type number
local deltaVertical ---@type number
--=================================================================================================================================
--[[
CREATE_BACKGROUND_HEROES only.
Here you can customize the positions of the background heroes. Init loops through and calls this function for each player.
Input:
playerIndex
whichSlot The position of the player when enumerating all players for which hero selection is enabled.
numberOfPlayers The number of players for which hero selection is enabled.
whichTeam The team the player is assigned to. Discard if there are no teams.
slotInTeam The position of that player within his or her team. Discard if there are no teams.
teamSize The size of the player's team. Discard if there are no teams.
Output:
deltaVertical The distance between the background hero and the camera eye position along the camera viewing direction.
deltaHorizontal The offset between the background hero and the camera eye position perpendicular to the camera viewing direction.
]]
--==================================================================================================================================
--Example code:
local angle ---@type number
local dist ---@type number
local BACKGROUND_HERO_OFFSET_BACKROW = 1425 ---@type number
local BACKGROUND_HERO_OFFSET_FRONTROW = 1250 ---@type number
if teamSize == 1 then
angle = 9
dist = BACKGROUND_HERO_OFFSET_BACKROW
elseif slotInTeam == 1 then
angle = 5
dist = BACKGROUND_HERO_OFFSET_BACKROW
elseif slotInTeam == 2 then
angle = 13
dist = BACKGROUND_HERO_OFFSET_BACKROW
else
angle = 9
dist = BACKGROUND_HERO_OFFSET_FRONTROW
end
if whichTeam == 2 then
angle = -angle
end
deltaHorizontal = Sin(Deg2Rad(angle))*dist
deltaVertical = Cos(Deg2Rad(angle))*dist
--==================================================================================================================================
return deltaHorizontal, deltaVertical
end
--==================================================================================================================================
-- F I N E - T U N I N G
--==================================================================================================================================
--How hero selection looks like before it is enabled (you can ignore this if you plan on enabling hero selection right away).
,SHOW_MENU_BEFORE_ENABLED = false ---@type boolean --Set to true if players should be able to see the hero selection menu before hero selection is enabled.
,PRE_SELECT_BEFORE_ENABLED = false ---@type boolean --Set to true if players should be able to pre-select heroes before hero selection is enabled.
--Camera setup (ENFORCE_CAMERA).
,CAMERA_PITCH = 12 ---@type number --Hero selection camera angle of attack in degrees. 0 = facing horizon, 90 = facing ground.
,CAMERA_DISTANCE = 1300 ---@type number --Hero selection camera distance from camera target.
,CAMERA_TARGET_OFFSET = 500 ---@type number --Distance between foreground hero and camera target. Positive = Camera target is behind the hero.
,CAMERA_PERPENDICULAR_OFFSET = 0 ---@type number --Shifts the camera left (negative) or right (positive) with respect to the foreground hero.
,CAMERA_Z_OFFSET = 100 ---@type number --Hero selection camera z-offset.
,CAMERA_FIELD_OF_VIEW = 70 ---@type number --Hero selection camera field of view.
--Sounds and visuals of hero pre-selection (CREATE_FOREGROUND_HERO).
,PRESELECT_EFFECT = nil ---@type string --Special effect played on the hero's position for the triggering player when switching to a new hero during pre-selection. Set to nil for no effect.
,PLAY_EMOTE_ON_PRESELECT = true ---@type boolean --Set to true if the hero should play its selection emote when the player switches to that hero during pre-selection.
,PLAY_ANIMATION_ON_PRESELECT = true ---@type boolean --Set to true if the hero should play the selection animation when the player switches to that hero during pre-selection.
,PHANTOM_HERO_WHEN_CANNOT_BE_PICKED = true ---@type boolean --Set to true if a hero should be black and transparent when it is pre-selected but cannot be picked.
,FOREGROUND_HERO_Z = 0 ---@type number --z-Position of foreground hero.
--Sounds and visuals on hero pick.
,PICK_EFFECT = "HolyLight.mdx" ---@type string --Special effect played on the hero's position for the triggering player when picking a hero. Set to nil for no effect.
,PICK_SOUND = "Sound\\Interface\\ItemReceived.flac" ---@type string --Sound effect played for the triggering player when selecting a hero. Set to nil for no sound.
,PLAY_EMOTE_ON_PICK = false ---@type boolean --Set to true if a hero should play its selection emote when a player chooses that hero.
,PLAY_ANIMATION_ON_PICK = false ---@type boolean --Set to true if a hero should play the selection animation when a player chooses that hero.
,PLAYER_PICK_ESCAPE_DELAY = 4.0 ---@type number --Delay between selecting a hero and being kicked out of hero selection (ESCAPE_PLAYER_AFTER_SELECTING only).
,FOREGROUND_HERO_FADEOUT_DELAY = 1.0 ---@type number --The time it takes for the foreground hero to start fading out after being selected.
,FOREGROUND_HERO_FADEOUT_TIME = 1.5 ---@type number --The time it takes for the foreground hero to fade out after being selected.
,OTHER_PLAYER_HERO_PICK_SOUND = "Sound\\Interface\\InGameChatWhat1.flac" ---@type string --Sound played when another player picks a hero. Set to nil for no sound.
--Text messages on hero pick.
,CREATE_TEXT_MESSAGE_ON_PICK = true ---@type boolean --Set to true if a text message should be sent to all other players when a hero is picked (except to enemies when concealed).
,TEXT_MESSAGE_X_OFFSET = 0.35 ---@type number --x-Offset of text messages from default. Text messages will still suck. Recommend using NeatMessages.
,TEXT_MESSAGE_Y_OFFSET = 0 ---@type number --y-Offset of text messages from default.
,USE_HERO_PROPER_NAME = false ---@type boolean --Instead of the hero's name, its proper name will be displayed in the text message. For example, "Uther" instead of "Paladin". Will temporarily create a hero to get the proper name.
,MESSAGE_EVEN_WHEN_CONCEALED = true ---@type boolean --Set to true if players should still get a message notifying that a player has picked a hero even when it is concealed which hero was picked (CONCEAL_HERO_PICKS_FROM_ENEMIES).
,INCLUDE_PROGRESSION_IN_MESSAGE = false ---@type boolean --Set to true if the displayed text message should include how many players have selected their hero.
--Sounds and visuals of background heroes (CREATE_BACKGROUND_HEROES).
,PLAYER_TEXT_TAGS = true ---@type boolean --Create text tags that show the players' names over the background heroes.
,BACKGROUND_HERO_FADEIN_TIME = 1.0 ---@type number --The time it takes for the background hero to fade in after being selected.
,PLAY_EMOTE_ON_BACKGROUND_HERO = true ---@type boolean --Set to true if a hero should play its selection emote for all other players as it fades in.
,PLAY_ANIMATION_ON_BACKGROUND_HERO = true ---@type boolean --Set to true if the background hero should play its selection animation as it fades in.
,BACKGROUND_HERO_FADEIN_EFFECT = "HolyLightRoyal.mdx" ---@type string --Special effect played on the background hero as it fades in.
,BACKGROUND_HERO_SELF_HIGHLIGHT = "RadianceHoly.mdx" ---@type string --Special effect added at the location of a player's own background hero to highlight it. Set to nil for no highlight.
,BACKGROUND_HERO_HIGHLIGHT_Z = 70 ---@type number --z-Position of background hero self highlight.
,BACKGROUND_HERO_FACING_POINT_OFFSET = -500 ---@type number --Adjusts where the background heroes face. 0 = Background heroes will face the foreground hero. Negative value = Face a point closer to the camera. Positive value = Face a point further from the camera.
,CONCEALED_HERO_EFFECT = "Objects\\InventoryItems\\QuestionMark\\QuestionMark.mdl" ---@type string --Model for the background hero seen by a player for which the hero pick was concealed.
--Last player picks a hero.
,LAST_PLAYER_SELECT_END_DELAY = 6.0 ---@type number --The amount of time after the last player selects a hero and the hero selection ends (ignore if ESCAPE_PLAYER_AFTER_SELECTING)
,DELETE_BACKGROUND_HEROES_AFTER_END = false ---@type boolean --Set to false if background heroes should not get removed when hero selection ends. For example, when a player repicks, they are still there.
--Layout of hero selection menu.
,MENU_NUMBER_OF_COLUMNS = 2 ---@type integer --Number of hero buttons per row.
,MENU_X_LEFT = -0.05 ---@type number --x-Position of left edge of hero selection menu. A negative number enters the widescreen area.
,MENU_Y_TOP = 0.55 ---@type number --y-Position of top edge of hero selection menu.
,MENU_BUTTON_SIZE = 0.039 ---@type number --Size of individual hero buttons.
,MENU_LEFT_RIGHT_EDGE_GAP = 0.02 ---@type number --Gap between left and right edges of menu and first and last buttons.
,MENU_TOP_EDGE_GAP = 0.015 ---@type number --Gap between top edge of menu and first button/first category title.
,MENU_BOTTOM_EDGE_GAP = 0.02 ---@type number --Gap between bottom edge of menu and last button.
,MENU_BUTTON_BUTTON_GAP = 0.005 ---@type number --Gap between two individual hero buttons.
,MENU_CATEGORY_FONT_SIZE = 16 ---@type number --Font size of category titles.
,MENU_CATEGORY_GAP = 0.028 ---@type number --Gap between buttons of two different categories.
,MENU_CATEGORY_TITLE_Y = -0.001 ---@type number --y-Position shift between category title and center of gap between categories.
,MENU_BORDER_TILE_SIZE = 0.03 ---@type number --This rounds up the width and height of the menu to an integer multiple of the specified number. This is useful if you're using a tiled border texture, so that there's no discontinuity in the texture.
--Value should be equal to BackdropCornerSize in the HeroSelectionMenu.fdf. Set to 0 to disable.
,MENU_HEROES_RANDOM_GAP = 0.02 ---@type number --Additional gap between last hero button and random pick button.
--Select button.
,SELECT_BUTTON_TEXT = "Accept" ---@type string --The text in the select hero button at the bottom of the menu.
,SELECT_BUTTON_SCALE = 1.0 ---@type number --The scale of the select hero button at the bottom of the menu.
,SELECT_BUTTON_WIDTH = 0.092 ---@type number --The width of the select hero button at the bottom of the menu.
--Display of random options.
,RANDOM_HERO_TOOLTIP = "Choose a random hero." ---@type string
,SUGGEST_RANDOM_TOOLTIP = "Suggest a random hero, but don't select it just yet." ---@type string
,RANDOM_HERO_ICON = "ReplaceableTextures\\CommandButtons\\BTNRandomIncredibleIcon.blp" ---@type string
,SUGGEST_RANDOM_ICON = "ReplaceableTextures\\CommandButtons\\BTNSelectHeroOn.blp" ---@type string
,RANDOM_SELECT_CYCLE_STYLE = true ---@type boolean --Set to true if the foreground hero should cycle randomly between different heroes while random hero is pre-selected. Set to false if a question mark should be shown.
,RANDOM_SELECT_CYCLE_INTERVAL = 0.2 ---@type number --How fast the heroes are cycled through when random hero is pre-selected (RANDOM_SELECT_CYCLE_STYLE).
--Layout of ability preview buttons.
,HERO_ABILITY_PREVIEW_BUTTON_X = 0.0 ---@type number --x-Position of topmost ability preview button relative to topright corner of menu.
,HERO_ABILITY_PREVIEW_BUTTON_Y = -0.016 ---@type number --y-Position of topmost ability preview button relative to topright corner of menu.
,HERO_ABILITY_PREVIEW_BUTTON_SIZE = 0.025 ---@type number --The size of the hero ability preview buttons that appear on the right side of the menu when pre-selecting a hero.
,ABILITY_BUTTON_HORIZONTAL_LAYOUT = false ---@type boolean --Set to true if ability preview buttons should be arranged horizontally instead of vertically.
,HERO_ABILITY_LEVEL_DELIMITER = " - [" ---@type string --To get the name of a hero ability without the level-text from the tooltip (such as "Stormbolt - [Level 1]" -> "Stormbolt"). This string is used to detect where the name of the ability ends. Set to nil if not necessary.
--Page cycle buttons (when multiple pages are set).
,PAGE_CYCLE_BUTTON_STYLE = "EnvelopRandomButton" ---@type string --Set the page cycle button style. Styles are found in documentation.
,MENU_PAGE_CYCLE_X_OFFSET = 0.0 ---@type number --x-Position of the page cycle buttons relative to the auto-position. Positive = moves inward. Negative = moves outward.
,MENU_PAGE_CYCLE_Y_OFFSET = 0.0 ---@type number --y-Position of the page cycle up button relative to the auto-position.
,MENU_PAGE_CYCLE_SCALE = 1.0 ---@type number --Sets the size of the page cycle buttons relative to hero buttons (for icon style buttons) or the accept button (for text style buttons).
,MENU_PAGE_DOWN_ICON = "ReplaceableTextures\\CommandButtons\\BTNNagaBurrow.blp" ---@type string --(for icon style buttons)
,MENU_PAGE_UP_ICON = "ReplaceableTextures\\CommandButtons\\BTNNagaUnBurrow.blp" ---@type string --(for icon style buttons)
--Display of big caption.
,HERO_SELECTION_CAPTION = "Choose your Hero!" ---@type string --The text displayed on the screen during hero selection. Set nil to omit text.
,CAPTION_FONT_SIZE = 30 ---@type number --Font size of hero selection caption.
,CAPTION_X = 0.4 ---@type number --x-Position of caption center.
,CAPTION_Y = 0.41 ---@type number --y-Position of caption center.
,CAPTION_COLOR_1 = "|cffffcc00" ---@type string --Caption will cycle between color 1 and color 2. Set the same for no color cycling.
,CAPTION_COLOR_2 = "|cffffffff" ---@type string --Caption will cycle between color 1 and color 2. Set the same for no color cycling.
,CAPTION_ALPHA_1 = 255 ---@type integer --Caption will cycle between alpha 1 and alpha 2. Set the same for no alpha cycling.
,CAPTION_ALPHA_2 = 255 ---@type integer --Caption will cycle between alpha 1 and alpha 2. Set the same for no alpha cycling.
,CAPTION_CYCLE_TIME = 4.0 ---@type number --The time it takes for the caption to cycle between color 1/2 and alpha 1/2.
,CAPTION_FADEOUT_TIME = 2.0 ---@type number --The time it takes for the caption to fade out after a player has picked a hero. Set to -1 for no fade out.
--Position of hero button and ability preview button mouse-over tooltips.
,TOOLTIP_LEFT_X = 0.51 ---@type number
,TOOLTIP_Y = 0.13 ---@type number
,TOOLTIP_LOCK_TOP = false ---@type boolean --Set to true if TOOLTIP_Y should refer to top instead of bottom edge.
,TOOLTIP_WIDTH = 0.29 ---@type number
--Display of countdown timer.
,TIMER_TEXT = "Time Remaining: |cffffcc00" ---@type string --Text before the time remaining display
,TIMER_BAN_PHASE_TEXT = "Ban Phase: |cffffcc00" ---@type string --Text before the time remaining display during the ban phase.
,TIMER_FONT_SIZE = 11 ---@type number --Font size of the time remaining display.
,TIMER_X = 0.4 ---@type number --x-Position of center of the time remaining display.
,TIMER_Y = 0.59 ---@type number --y-Position of the center of the time remaining display.
--Shadows of heroes.
,CREATE_SHADOWS = false ---@type boolean --Create shadows with destructables for heroes (since they are special effects and don't have shadows).
,SHADOW_DESTRUCTABLE_ID = FourCC('Dsha') ---@type integer --Destructable id of the shadow that's created for heroes.
,NO_SHADOW_DESTRUCTABLE_ID = FourCC('Dnsh') ---@type integer --Dummy destructable without a shadow.
--Player Colors
,PLAYER_COLOR = {
[1] = "|cffff0402",
[2] = "|cff1052ff",
[3] = "|cff1BE6BA",
[4] = "|cff8530b1",
[5] = "|cfffffc00",
[6] = "|cffff8a0d",
[7] = "|cff20bf00",
[8] = "|cffE35BAF",
[9] = "|cff949697",
[10] = "|cff7EBFF1",
[11] = "|cff106247",
[12] = "|cff4F2B05",
[13] = "|cff9C0000",
[14] = "|cff0000C2",
[15] = "|cff00EBEB",
[16] = "|cffBE00FF",
[17] = "|cffECCC86",
[18] = "|cffF7A48B",
[19] = "|cffBFFF80",
[20] = "|cffDBB8EC",
[21] = "|cff4F4F55",
[22] = "|cffECF0FF",
[23] = "|cff00781E",
[24] = "|cffA46F34"
}
}, {__index = _G})
--=================================================================================================================================
end
if Debug then Debug.endFile() end
if Debug then Debug.beginFile 'HeroSelection' end
do
--================================================================
local _ENV = AHS
local selectedHeroGlow = {} ---@type effect[]
local backgroundHero = {} ---@type effect[]
local backgroundHeroGlow = {} ---@type effect[]
local backgroundHeroHighlight = {} ---@type effect[]
local backgroundHeroShadow = {} ---@type destructable[]
local foregroundHeroShadow = nil ---@type destructable
local backgroundHeroTextTag = {} ---@type texttag[]
local preselectedHeroIndex = {} ---@type integer[]
local pickedHeroIndex = {} ---@type integer[]
local localPlayerId = nil ---@type integer --asynchronous
local heroIndexWasPicked = {} ---@type boolean[]
local heroIndexWasBanned = {} ---@type boolean[][]
local localForegroundHeroX = nil ---@type number --asynchronous
local localForegroundHeroY = nil ---@type number --asynchronous
local localHeroSelectionAngle = nil ---@type number --asynchronous
local currentPage = 1 ---@type integer --asynchronous
local captionFrame = nil ---@type framehandle
local timerFrame = nil ---@type framehandle
local heroSelectionMenu = nil ---@type framehandle
local heroSelectionButton = {} ---@type framehandle[]
local heroSelectionButtonIcon = {} ---@type framehandle[]
local heroSelectionButtonHighlight = nil ---@type framehandle
local heroSelectionButtonIconClicked = {} ---@type framehandle[]
local heroSelectionButtonTooltip = {} ---@type framehandle[]
local heroSelectionButtonTooltipTitle = {} ---@type framehandle[]
local heroSelectionButtonTooltipText = {} ---@type framehandle[]
local heroSelectionAbility = {} ---@type framehandle[]
local heroSelectionAbilityHover = {} ---@type framehandle[]
local heroSelectionAbilityTooltip = {} ---@type framehandle[]
local heroSelectionAbilityTooltipTitle = {} ---@type framehandle[]
local heroSelectionAbilityTooltipText = {} ---@type framehandle[]
local heroSelectionCategory = {} ---@type framehandle[]
local heroAcceptButton = nil ---@type framehandle
local heroBanButton = nil ---@type framehandle
local fullScreenFrame = nil ---@type framehandle
local fullScreenParent = nil ---@type framehandle
local heroSelectionCamera = nil ---@type camerasetup
local lockCameraTimer = nil ---@type timer
local captionTimer = nil ---@type timer
local countdownTimer = nil ---@type timer
local countdownUpdateTimer = nil ---@type timer
local heroSelectionButtonTrigger = nil ---@type trigger
local pageCycleTrigger = nil ---@type trigger
local isForcedSelect = false ---@type boolean
local captionAlphaMultiplier = 1 ---@type number
local TOOLTIP_BASE_HEIGHT = 0.032 ---@type number
local tooltipLeftXLocal = nil ---@type number
local r1 ---@type integer
local r2 ---@type integer
local g1 ---@type integer
local g2 ---@type integer
local b1 ---@type integer
local b2 ---@type integer
local data = {} ---@type table[]
local storePlayerIndex ---@type integer
local storeHeroIndex ---@type integer
local moveableLoc = nil ---@type location
local GARBAGE_DUMP_X ---@type number
local GARBAGE_DUMP_Y ---@type number
local NUMBER_OF_CATEGORIES = 0 ---@type integer
local NUMBER_OF_PAGES = 1 ---@type integer
local NUMBER_OF_ABILITY_FRAMES = 0 ---@type integer
local NUMBER_OF_TEAMS = 0 ---@type integer
local RANDOM_HERO = 0 ---@type integer
local SUGGEST_RANDOM = 0 ---@type integer
local PAGE_DOWN = 0 ---@type integer
local PAGE_UP = 0 ---@type integer
local numHeroes = 0
do
_G.HeroList = {}
---@class Hero
_G.Hero = {
abilities = nil,
isNonHeroAbility = nil,
unitId = 0,
tooltip = "",
tooltipAbility = 0,
selectEmote = nil,
selectAnim = nil,
selectSubAnim = nil,
selectAnimLength = 0,
category = 0,
needsHeroGlow = nil,
unavailable = nil,
unavailableToTeam = nil,
index = 0,
red = 255,
green = 255,
blue = 255,
name = nil,
modelPath = nil, ---@type string
iconPath = nil,
scalingValue = 1,
}
local mt = { __index = Hero }
function Hero.create()
local new = {}
setmetatable(new, mt)
numHeroes = numHeroes + 1
new.abilities = {}
new.isNonHeroAbility = {}
new.unavailableToTeam = {}
new.index = numHeroes
HeroList[numHeroes] = new
return new
end
function Hero:GetValues()
local tempUnit ---@type unit
local tempItem ---@type item
tempUnit = CreateUnit( Player(PLAYER_NEUTRAL_PASSIVE) , self.unitId , GARBAGE_DUMP_X , GARBAGE_DUMP_Y , 0 )
self.name = GetUnitName(tempUnit)
self.scalingValue = BlzGetUnitRealField( tempUnit , UNIT_RF_SCALING_VALUE )
self.red = BlzGetUnitIntegerField(tempUnit, UNIT_IF_TINTING_COLOR_RED)
self.green = BlzGetUnitIntegerField(tempUnit, UNIT_IF_TINTING_COLOR_GREEN)
self.blue = BlzGetUnitIntegerField(tempUnit, UNIT_IF_TINTING_COLOR_BLUE)
self.iconPath = BlzGetAbilityIcon(GetUnitTypeId(tempUnit))
RemoveUnit(tempUnit)
tempItem = CreateItem(FourCC('afac'), GARBAGE_DUMP_X, GARBAGE_DUMP_Y)
BlzSetItemSkin(tempItem, self.unitId)
self.modelPath = BlzGetItemStringField(tempItem, ITEM_SF_MODEL_USED)
RemoveItem(tempItem)
end
end
--==========================================================================================================================================================
--Utility
--==========================================================================================================================================================
local function InitFullScreenParents()
CreateLeaderboardBJ(bj_FORCE_ALL_PLAYERS, "title")
local leaderboard = BlzGetFrameByName("Leaderboard", 0)
BlzFrameSetSize(leaderboard, 0, 0)
BlzFrameSetVisible(BlzGetFrameByName("LeaderboardBackdrop", 0), false)
BlzFrameSetVisible(BlzGetFrameByName("LeaderboardTitle", 0), false)
fullScreenParent = BlzCreateFrameByType("FRAME", "fullScreenParent", leaderboard, "", 0)
fullScreenFrame = BlzCreateFrameByType("FRAME", "fullscreen", fullScreenParent, "", 0)
BlzFrameSetVisible(fullScreenFrame, false)
local localClientHeight = BlzGetLocalClientHeight()
if localClientHeight > 0 then
BlzFrameSetSize(fullScreenFrame, BlzGetLocalClientWidth()/localClientHeight*0.6, 0.6)
else
BlzFrameSetSize(fullScreenFrame, 16/9*0.6, 0.6)
end
BlzFrameSetAbsPoint(fullScreenFrame, FRAMEPOINT_BOTTOM, 0.4, 0)
end
---@param t timer
---@return integer
local function TimerCounterPlus(t)
if data[t] == nil then
data[t] = {}
end
data[t].counter = (data[t].counter or 0) + 1
return data[t].counter
end
---@param rawSeconds number
---@return string
local function GetClockString(rawSeconds)
local seconds ---@type integer
local minutes = math.floor(rawSeconds) // 60 ---@type integer
local clock ---@type string
seconds = ModuloInteger(math.floor(rawSeconds), 60)
clock = minutes .. ":"
if seconds >= 10 then
clock = clock .. math.floor(seconds)
else
clock = clock .. "0" .. math.floor(seconds)
end
return clock
end
local function TimeExpires()
HeroSelectionForceEnd()
PauseTimer(countdownUpdateTimer)
BlzFrameSetText(timerFrame, TIMER_TEXT .. GetClockString(0))
HeroSelectionOnExpire()
end
---@param x number
---@param y number
---@return number
local function GetLocZ(x, y)
MoveLocation(moveableLoc, x, y)
return GetLocationZ(moveableLoc)
end
---@param soundPath string
---@param localPlayerCanHearSound boolean
local 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
---@param heroIndex integer
---@param capitalize boolean
---@return string
local function GetPickedHeroDisplayedName(heroIndex, capitalize)
local tempUnit ---@type unit
local name ---@type string
if USE_HERO_PROPER_NAME then
tempUnit = CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), HeroList[heroIndex].unitId, GARBAGE_DUMP_X, GARBAGE_DUMP_Y, 0)
name = "|cffffcc00" .. GetHeroProperName(tempUnit) .. "|r"
RemoveUnit(tempUnit)
return name
else
if capitalize then
return "The |cffffcc00" .. HeroList[heroIndex].name .. "|r"
else
return "the |cffffcc00" .. HeroList[heroIndex].name .. "|r"
end
end
end
--==========================================================================================================================================================
--Hero Menu
--==========================================================================================================================================================
---@param heroIndex integer
---@param P integer
---@return boolean
local function HeroCanBePicked(heroIndex, P)
return heroIndex == RANDOM_HERO or (heroIndex ~= 0 and not ((heroIndexWasPicked[heroIndex] and not HERO_CAN_BE_PICKED_MULTIPLE_TIMES) or HeroList[heroIndex].unavailable or heroIndexWasBanned[P][heroIndex]))
end
---@param id integer
---@return string
local function GetHeroAbilityName(id)
local tooltip = BlzGetAbilityTooltip(id, 0)
local length = StringLength(tooltip)
local delimLength = StringLength(HERO_ABILITY_LEVEL_DELIMITER)
local j
if HERO_ABILITY_LEVEL_DELIMITER == nil or HERO_ABILITY_LEVEL_DELIMITER == "" then
return tooltip
end
for i = 1, length - delimLength do
j = 0
while not (SubString(tooltip, i+j, i+j+1) ~= SubString(HERO_ABILITY_LEVEL_DELIMITER, j, j+1)) do
if j == delimLength - 1 then
return SubString(tooltip, 0, i)
end
j = j + 1
end
end
return tooltip
end
---@param currentSelection integer
---@param P integer
---@return integer
local function GetRandomHero(currentSelection, P)
local heroesAvailable = {} ---@type integer[]
local numChars = 0 ---@type integer
local currentSelectionAvailable = false ---@type boolean
local currentSelectionChar ---@type integer
for i = 1, NUMBER_OF_HEROES do
if HeroCanBePicked(i, P) and not HeroList[i].unavailableToTeam[TEAM_OF_PLAYER[P]] then
numChars = numChars + 1
heroesAvailable[numChars] = i
if currentSelection == i then
currentSelectionAvailable = true
currentSelectionChar = numChars
end
end
end
if currentSelectionAvailable and currentSelection ~= 0 then
--Return random hero not currently selected.
return heroesAvailable[ModuloInteger( currentSelectionChar + GetRandomInt(0,numChars-2) , numChars ) + 1]
else
return heroesAvailable[GetRandomInt(1,numChars)]
end
end
---@param whichButton integer
local function SetButtonFrames(whichButton)
heroSelectionButtonIcon[whichButton] = BlzFrameGetChild(heroSelectionButton[whichButton],0)
heroSelectionButtonIconClicked[whichButton] = BlzFrameGetChild(heroSelectionButton[whichButton],1)
BlzFrameClearAllPoints(heroSelectionButtonIconClicked[whichButton])
BlzFrameSetPoint(heroSelectionButtonIconClicked[whichButton], FRAMEPOINT_BOTTOMLEFT, heroSelectionButton[whichButton], FRAMEPOINT_BOTTOMLEFT, 0.001, 0.001)
BlzFrameSetPoint(heroSelectionButtonIconClicked[whichButton], FRAMEPOINT_TOPRIGHT, heroSelectionButton[whichButton], FRAMEPOINT_TOPRIGHT, -0.001, -0.001)
end
---@param whichButton integer
---@param whichTitle string
---@param whichTooltip string
local function SetButtonTooltip(whichButton, whichTitle, whichTooltip)
heroSelectionButtonTooltip[whichButton] = BlzCreateFrame("CustomTooltip", heroSelectionButton[whichButton], 0, 0)
if TOOLTIP_LOCK_TOP then
BlzFrameSetPoint( heroSelectionButtonTooltip[whichButton] , FRAMEPOINT_TOPLEFT , fullScreenFrame , FRAMEPOINT_BOTTOMLEFT , tooltipLeftXLocal , TOOLTIP_Y )
else
BlzFrameSetPoint( heroSelectionButtonTooltip[whichButton] , FRAMEPOINT_BOTTOMLEFT , fullScreenFrame , FRAMEPOINT_BOTTOMLEFT , tooltipLeftXLocal , TOOLTIP_Y )
end
BlzFrameSetTooltip( heroSelectionButton[whichButton] , heroSelectionButtonTooltip[whichButton] )
heroSelectionButtonTooltipTitle[whichButton] = BlzFrameGetChild(heroSelectionButtonTooltip[whichButton],0)
heroSelectionButtonTooltipText[whichButton] = BlzFrameGetChild(heroSelectionButtonTooltip[whichButton],1)
BlzFrameSetText( heroSelectionButtonTooltipTitle[whichButton] , whichTitle )
BlzFrameSetText( heroSelectionButtonTooltipText[whichButton] , whichTooltip )
BlzFrameSetSize(heroSelectionButtonTooltipText[whichButton] , TOOLTIP_WIDTH - 0.01 , 0.0 )
BlzFrameSetSize(heroSelectionButtonTooltip[whichButton] , TOOLTIP_WIDTH , BlzFrameGetHeight(heroSelectionButtonTooltipText[whichButton]) + TOOLTIP_BASE_HEIGHT)
end
---@param whichButton integer
---@param disabledTexture boolean
local function SetButtonTextures(whichButton, disabledTexture)
local stringLength ---@type integer
local disabledPath ---@type string
local path ---@type string
if whichButton <= NUMBER_OF_HEROES then
path = HeroList[whichButton].iconPath
elseif whichButton == RANDOM_HERO then
path = RANDOM_HERO_ICON
elseif whichButton == SUGGEST_RANDOM then
path = SUGGEST_RANDOM_ICON
elseif whichButton == PAGE_DOWN then
path = MENU_PAGE_DOWN_ICON
else
path = MENU_PAGE_UP_ICON
end
if not disabledTexture then
BlzFrameSetTexture(heroSelectionButtonIcon[whichButton] , path , 0 , true)
BlzFrameSetTexture(heroSelectionButtonIconClicked[whichButton] , path , 0 , true)
else
stringLength = StringLength(path)
for i = 0, stringLength - 3 do
if SubString(path, i, i+1) == "B" and SubString(path, i+1, i+2) == "T" and SubString(path, i+2, i+3) == "N" then
disabledPath = "ReplaceableTextures\\CommandButtonsDisabled\\DISBTN" .. SubString(path, i+3, stringLength)
BlzFrameSetTexture(heroSelectionButtonIcon[whichButton] , disabledPath, 0 , true)
BlzFrameSetTexture(heroSelectionButtonIconClicked[whichButton] , disabledPath, 0 , true)
return
end
end
end
end
--==========================================================================================================================================================
--Caption
--==========================================================================================================================================================
local function TextColor()
local counter = TimerCounterPlus(GetExpiredTimer())
local colorState = (1 + math.cos(2*math.pi*counter/(CAPTION_CYCLE_TIME/0.02)))/2
local r = math.floor((1 - colorState)*r1 + colorState*r2)
local g = math.floor((1 - colorState)*g1 + colorState*g2)
local b = math.floor((1 - colorState)*b1 + colorState*b2)
local colorString = "|cff" .. string.format("\x2502x", r) .. string.format("\x2502x", g) .. string.format("\x2502x", b)
if CAPTION_FADEOUT_TIME ~= -1 then
if PlayerHasHero[localPlayerId] then
if CAPTION_FADEOUT_TIME > 0 then
captionAlphaMultiplier = math.max(0, captionAlphaMultiplier - 1/(50*CAPTION_FADEOUT_TIME))
else
captionAlphaMultiplier = 0
end
end
end
BlzFrameSetText(captionFrame, colorString .. HERO_SELECTION_CAPTION .. "|r")
BlzFrameSetAlpha(captionFrame, math.floor(captionAlphaMultiplier*((1 - colorState)*CAPTION_ALPHA_1 + colorState*CAPTION_ALPHA_2)))
end
---@param hexString string
---@return integer
local function HexToInt(hexString)
return tonumber(hexString, 16)
end
local function AnimateCaption()
--Get RGB values of CAPTION_COLOR_1 and CAPTION_COLO_2.
r1 = HexToInt(string.sub(CAPTION_COLOR_1, 5, 6))
g1 = HexToInt(string.sub(CAPTION_COLOR_1, 7, 8))
b1 = HexToInt(string.sub(CAPTION_COLOR_1, 9, 10))
r2 = HexToInt(string.sub(CAPTION_COLOR_2, 5, 6))
g2 = HexToInt(string.sub(CAPTION_COLOR_2, 7, 8))
b2 = HexToInt(string.sub(CAPTION_COLOR_2, 9, 10))
TimerStart( captionTimer , 0.02 , true , TextColor )
end
--==========================================================================================================================================================
--Hero Effects
--==========================================================================================================================================================
local function FadeInBackgroundHero()
local t = GetExpiredTimer() ---@type timer
local counter = TimerCounterPlus(t)
local P, concealed = table.unpack(data[t])
local whichHero = HeroList[pickedHeroIndex[P]] ---@type Hero
--Hero for owner fades in later than for other players since it's still in the front.
local ownerDelay = math.floor(50*(FOREGROUND_HERO_FADEOUT_DELAY + FOREGROUND_HERO_FADEOUT_TIME)//BACKGROUND_HERO_FADEIN_TIME) ---@type integer
local modelPath ---@type string
if localPlayerId ~= P and counter <= 51 then
if concealed then
BlzSetSpecialEffectAlpha( backgroundHero[P] , math.floor(2.5*counter) )
if whichHero.needsHeroGlow then
BlzSetSpecialEffectAlpha( backgroundHeroGlow[P], math.floor(2.5*counter) )
end
else
BlzSetSpecialEffectAlpha( backgroundHero[P] , 5*counter )
if whichHero.needsHeroGlow then
BlzSetSpecialEffectAlpha( backgroundHeroGlow[P], 5*counter )
end
end
elseif localPlayerId == P and counter > ownerDelay then
BlzSetSpecialEffectAlpha( backgroundHero[P] , 5*(counter - ownerDelay))
if whichHero.needsHeroGlow then
BlzSetSpecialEffectAlpha( backgroundHeroGlow[P], 5*(counter - ownerDelay))
end
end
if CREATE_SHADOWS then
if counter == ownerDelay then
RemoveDestructable(backgroundHeroShadow[P])
backgroundHeroShadow[P] = CreateDestructable(SHADOW_DESTRUCTABLE_ID, BACKGROUND_HERO_X[P], BACKGROUND_HERO_Y[P], 0, 1, 0)
end
end
if counter == 51 + ownerDelay then
if BACKGROUND_HERO_SELF_HIGHLIGHT ~= nil then
if localPlayerId == P then
modelPath = BACKGROUND_HERO_SELF_HIGHLIGHT
else
modelPath = ""
end
backgroundHeroHighlight[P] = AddSpecialEffect(modelPath, BACKGROUND_HERO_X[P], BACKGROUND_HERO_Y[P])
BlzSetSpecialEffectZ(backgroundHeroHighlight[P], GetLocZ(BACKGROUND_HERO_X[P], BACKGROUND_HERO_Y[P]) + BACKGROUND_HERO_HIGHLIGHT_Z)
end
DestroyTimer(t)
end
end
local function FadeoutForegroundHero()
local t = GetExpiredTimer() ---@type timer
local counter = TimerCounterPlus(t) ---@type integer
local P = data[t][1] ---@type integer
if counter < 51 and FOREGROUND_HERO_FADEOUT_TIME > 0 then
BlzSetSpecialEffectAlpha( SelectedHeroEffect[P] , 255 - 5*counter )
BlzSetSpecialEffectAlpha( selectedHeroGlow[P] , 255 - 5*counter )
TimerStart( t , FOREGROUND_HERO_FADEOUT_TIME/50.0 , false , FadeoutForegroundHero )
else
BlzSetSpecialEffectPosition( SelectedHeroEffect[P] , GARBAGE_DUMP_X , GARBAGE_DUMP_Y , 0 )
DestroyEffect( SelectedHeroEffect[P] )
DestroyEffect( selectedHeroGlow[P] )
DestroyTimer(t)
end
end
local function ResetAnimation()
local t = GetExpiredTimer() ---@type timer
local P, heroIndex, animateBackgroundHero = table.unpack(data[t])
if animateBackgroundHero then
if localPlayerId ~= P then
BlzSpecialEffectClearSubAnimations(backgroundHero[P])
BlzPlaySpecialEffect( backgroundHero[P] , ANIM_TYPE_STAND )
end
else
if preselectedHeroIndex[P] == heroIndex then
BlzSpecialEffectClearSubAnimations(SelectedHeroEffect[P])
BlzPlaySpecialEffect( SelectedHeroEffect[P] , ANIM_TYPE_STAND )
end
end
DestroyTimer(t)
end
---@param P integer
---@param heroIndex integer
---@param animateBackgroundHero boolean
local function PlayHeroAnimation(P, heroIndex, animateBackgroundHero)
local t = CreateTimer() ---@type timer
if animateBackgroundHero then
if localPlayerId ~= P and (not CONCEAL_HERO_PICKS_FROM_ENEMIES or (TEAM_OF_PLAYER[localPlayerId] == TEAM_OF_PLAYER[P] and TEAM_OF_PLAYER[P] ~= 0)) then
BlzSpecialEffectAddSubAnimation( backgroundHero[P] , HeroList[heroIndex].selectSubAnim )
BlzPlaySpecialEffect( backgroundHero[P] , HeroList[heroIndex].selectAnim )
end
else
BlzSpecialEffectAddSubAnimation( SelectedHeroEffect[P] , HeroList[heroIndex].selectSubAnim )
BlzPlaySpecialEffect( SelectedHeroEffect[P] , HeroList[heroIndex].selectAnim )
end
TimerStart( t , HeroList[heroIndex].selectAnimLength , false , ResetAnimation )
data[t] = {P, heroIndex, animateBackgroundHero}
end
---@param P integer
---@param heroIndex integer
local function CreateNewForegroundHero(P, heroIndex)
local modelPath = "" ---@type string
local glowPath = "" ---@type string
local locZ ---@type number
local whichHero = HeroList[heroIndex] ---@type Hero
if localPlayerId == P then
if heroIndex == RANDOM_HERO then
modelPath = "Objects\\InventoryItems\\QuestionMark\\QuestionMark.mdl"
else
modelPath = whichHero.modelPath
end
if whichHero.needsHeroGlow then
glowPath = "GeneralHeroGlow.mdx"
end
end
BlzSetSpecialEffectPosition( SelectedHeroEffect[P] , GARBAGE_DUMP_X , GARBAGE_DUMP_Y , 0 )
BlzSetSpecialEffectTimeScale( SelectedHeroEffect[P] , 9999)
BlzSetSpecialEffectScale(SelectedHeroEffect[P], 0)
DestroyEffect(SelectedHeroEffect[P])
DestroyEffect(selectedHeroGlow[P])
SelectedHeroEffect[P] = AddSpecialEffect( modelPath , localForegroundHeroX , localForegroundHeroY )
selectedHeroGlow[P] = AddSpecialEffect( glowPath , localForegroundHeroX , localForegroundHeroY )
locZ = GetLocZ(localForegroundHeroX, localForegroundHeroY)
BlzSetSpecialEffectZ( SelectedHeroEffect[P], locZ + FOREGROUND_HERO_Z )
BlzSetSpecialEffectZ( selectedHeroGlow[P], locZ + FOREGROUND_HERO_Z )
BlzSetSpecialEffectYaw( SelectedHeroEffect[P] , Deg2Rad(localHeroSelectionAngle) + math.pi )
BlzSetSpecialEffectColorByPlayer( SelectedHeroEffect[P] , Player(P - 1) )
BlzSetSpecialEffectColorByPlayer( selectedHeroGlow[P] , Player(P - 1) )
if heroIndex ~= RANDOM_HERO then
BlzSetSpecialEffectScale( SelectedHeroEffect[P] , whichHero.scalingValue )
end
if PHANTOM_HERO_WHEN_CANNOT_BE_PICKED and (HeroSelectionDisabledForPlayer[P] or not HeroCanBePicked(heroIndex, P)) then
BlzSetSpecialEffectColor( SelectedHeroEffect[P], 0, 0, 0 )
BlzSetSpecialEffectAlpha( SelectedHeroEffect[P], 128 )
else
BlzSetSpecialEffectColor( SelectedHeroEffect[P] , whichHero.red , whichHero.green , whichHero.blue)
end
end
---@param P integer
local function DeleteBackgroundHero(P)
BlzSetSpecialEffectPosition(backgroundHero[P], GARBAGE_DUMP_X, GARBAGE_DUMP_Y, 0)
DestroyEffect(backgroundHero[P])
DestroyEffect(backgroundHeroGlow[P])
DestroyEffect(backgroundHeroHighlight[P])
if CREATE_SHADOWS then
RemoveDestructable(backgroundHeroShadow[P])
end
if PLAYER_TEXT_TAGS then
DestroyTextTag(backgroundHeroTextTag[P])
end
end
--==========================================================================================================================================================
--Miscellaneous.
--==========================================================================================================================================================
local function UpdateTimerFrame()
if InBanPhase then
BlzFrameSetText(timerFrame, TIMER_BAN_PHASE_TEXT .. GetClockString(TimerGetRemaining(countdownTimer)))
else
BlzFrameSetText(timerFrame, TIMER_TEXT .. GetClockString(TimerGetRemaining(countdownTimer)))
end
end
local function LockSelecterCamera()
for i = 1, NumSelectingPlayers do
if IsInHeroSelection[PlayerNumberOfSlot[i]] then
CameraSetupApplyForPlayer( true, heroSelectionCamera, Player(PlayerNumberOfSlot[i] - 1), 0 )
end
end
end
--==========================================================================================================================================================
--End hero selection.
--==========================================================================================================================================================
---@param whichPlayer player
local function EscapePlayer(whichPlayer)
local P = GetPlayerId(whichPlayer) + 1 ---@type integer
if not IsInHeroSelection[P] then
if HERO_SELECTION_ENABLE_DEBUG_MODE then
print("|cffff0000Warning:|r Attempted to escape player who is not in hero selection...")
end
return
end
IsInHeroSelection[P] = false
preselectedHeroIndex[P] = 0
IsRepicking[P] = false
IsRepicking[Player(P-1)] = false
PlayerHasBan[P] = false
PlayerHasBan[Player(P-1)] = false
NumPlayersInSelection = NumPlayersInSelection - 1
DestroyEffect(SelectedHeroEffect[P])
ResetToGameCameraForPlayer(whichPlayer, 0)
local consoleUIBackdrop = BlzGetFrameByName("ConsoleUIBackdrop",0)
local bottomUI = BlzGetFrameByName("ConsoleBottomBar", 0)
local topUI = BlzGetFrameByName("ConsoleTopBar", 0)
if localPlayerId == P then
BlzFrameSetVisible(heroSelectionMenu, false)
BlzFrameSetVisible(captionFrame, false)
BlzFrameSetVisible(timerFrame, false)
if HIDE_GAME_UI then
BlzHideOriginFrames(false)
BlzFrameSetVisible(consoleUIBackdrop, true)
BlzFrameSetVisible(bottomUI, true)
BlzFrameSetVisible(topUI, true)
end
end
HeroSelectionOnEscape(whichPlayer)
end
local function EndHeroSelection()
local P ---@type integer
PauseTimer(lockCameraTimer)
PauseTimer(countdownTimer)
PauseTimer(countdownUpdateTimer)
if not ESCAPE_PLAYER_AFTER_SELECTING then
for i = 1, NumSelectingPlayers do
P = PlayerNumberOfSlot[i]
EscapePlayer(Player(P-1))
end
end
for i = 1, NumSelectingPlayers do
P = PlayerNumberOfSlot[i]
BlzSetSpecialEffectPosition(SelectedHeroEffect[P], GARBAGE_DUMP_X, GARBAGE_DUMP_Y, 0)
DestroyEffect(SelectedHeroEffect[P])
DestroyEffect(selectedHeroGlow[P])
if DELETE_BACKGROUND_HEROES_AFTER_END then
DeleteBackgroundHero(P)
end
end
if CREATE_SHADOWS then
RemoveDestructable(foregroundHeroShadow)
end
HeroSelectionOnFinal()
end
local function EscapePlayerCaller()
local t = GetExpiredTimer() ---@type timer
EscapePlayer(Player(data[t][1]-1))
DestroyTimer(t)
if ESCAPE_PLAYER_AFTER_SELECTING then
if NumPlayersInSelection == 0 then
EndHeroSelection()
end
end
end
local function EndHeroSelectionCaller()
local t = GetExpiredTimer() ---@type timer
EndHeroSelection()
DestroyTimer(t)
end
local function OnPlayerLeave()
local whichPlayer = GetTriggerPlayer() ---@type player
local P = GetPlayerId(whichPlayer) + 1 ---@type integer
for i = 1, NumSelectingPlayers do
if P == PlayerNumberOfSlot[i] then
for j = i, NumSelectingPlayers - 1 do
PlayerNumberOfSlot[j] = PlayerNumberOfSlot[j+1]
end
break
end
end
NumSelectingPlayers = NumSelectingPlayers - 1
if IsInHeroSelection[P] then
HeroSelectionOnLeave(whichPlayer)
NumPlayersInSelection = NumPlayersInSelection - 1
IsInHeroSelection[P] = false
end
if PlayerHasHero[P] then
NumPlayersWithHero = NumPlayersWithHero - 1
PlayerHasHero[P] = false
BlzSetSpecialEffectPosition(SelectedHeroEffect[P], GARBAGE_DUMP_X, GARBAGE_DUMP_Y, 0)
DestroyEffect(SelectedHeroEffect[P])
DestroyEffect(selectedHeroGlow[P])
DestroyEffect(selectedHeroGlow[P])
DeleteBackgroundHero(P)
elseif NumPlayersWithHero == NumSelectingPlayers and not IsRepicking[P] then
TimerStart( CreateTimer() , LAST_PLAYER_SELECT_END_DELAY , false , EndHeroSelectionCaller )
end
end
--==========================================================================================================================================================
--Cycle Page.
--==========================================================================================================================================================
local function DisplayNewPage(P)
if preselectedHeroIndex[P] ~= 0 and preselectedHeroIndex[P] <= NUMBER_OF_HEROES then
BlzFrameSetVisible(heroSelectionButtonHighlight, PAGE_OF_CATEGORY[HeroList[preselectedHeroIndex[P]].category] == currentPage)
end
for i = 1, NUMBER_OF_CATEGORIES do
BlzFrameSetVisible(heroSelectionCategory[i], PAGE_OF_CATEGORY[i] == currentPage)
end
for i = 1, NUMBER_OF_HEROES do
BlzFrameSetVisible(heroSelectionButton[i], PAGE_OF_CATEGORY[Hero.list[i].category] == currentPage and not Hero.list[i].unavailableToTeam[TEAM_OF_PLAYER[localPlayerId]])
end
end
local function CyclePage()
local whichPlayer = GetTriggerPlayer() ---@type player
local P = GetPlayerId(whichPlayer) + 1 ---@type integer
PlaySoundLocal( "Sound\\Interface\\BigButtonClick.flac" , localPlayerId == P )
if localPlayerId == P then
if BlzGetTriggerFrame() == heroSelectionButton[PAGE_DOWN] then
currentPage = ModuloInteger(currentPage - 2, NUMBER_OF_PAGES) + 1
else
currentPage = ModuloInteger(currentPage, NUMBER_OF_PAGES) + 1
end
DisplayNewPage(P)
end
end
--==========================================================================================================================================================
--Ban Hero.
--==========================================================================================================================================================
---@param whichHero Hero
---@param disable boolean
---@param playerIndex? integer | nil
local function ExecuteBan(whichHero, disable, playerIndex)
local P ---@type integer
if disable then
BlzFrameSetText(heroSelectionButtonTooltipText[whichHero.index] , (whichHero.tooltipAbility ~= 0 and BlzGetAbilityExtendedTooltip(whichHero.tooltipAbility, 0) or whichHero.tooltip) .. "|n|n|cffff0000This hero was banned.|r")
else
BlzFrameSetText(heroSelectionButtonTooltipText[whichHero.index] , whichHero.tooltipAbility ~= 0 and BlzGetAbilityExtendedTooltip(whichHero.tooltipAbility, 0) or whichHero.tooltip)
end
BlzFrameSetSize(heroSelectionButtonTooltipText[whichHero.index] , TOOLTIP_WIDTH - 0.01 , 0.0 )
BlzFrameSetSize(heroSelectionButtonTooltip[whichHero.index] , TOOLTIP_WIDTH , BlzFrameGetHeight(heroSelectionButtonTooltipText[whichHero.index]) + TOOLTIP_BASE_HEIGHT)
for i = 1, NumSelectingPlayers do
P = PlayerNumberOfSlot[i]
if playerIndex == nil or playerIndex == P then
heroIndexWasBanned[P][whichHero.index] = true
if localPlayerId == P then
SetButtonTextures(whichHero.index, not HeroCanBePicked(whichHero.index, P))
if preselectedHeroIndex[P] == whichHero then
BlzFrameSetEnable( heroAcceptButton , false )
BlzFrameSetEnable( heroBanButton , false )
if PHANTOM_HERO_WHEN_CANNOT_BE_PICKED then
BlzSetSpecialEffectAlpha(SelectedHeroEffect[P], 128)
BlzSetSpecialEffectColor(SelectedHeroEffect[P], 0, 0, 0)
end
end
end
end
end
end
local function BanHero()
local message ---@type string
local whichPlayer = GetTriggerPlayer() ---@type player
local P = GetPlayerId(whichPlayer) + 1 ---@type integer
local heroIndex = preselectedHeroIndex[P] ---@type integer
if localPlayerId == P then
BlzFrameSetEnable(heroBanButton, false)
end
PlayerHasBan[P] = false
PlayerHasBan[Player(P-1)] = false
message = GetPickedHeroDisplayedName(heroIndex, true) .. " was banned."
if NeatMessage then
NeatMessage(message)
else
DisplayTextToPlayer(GetLocalPlayer(), TEXT_MESSAGE_X_OFFSET, TEXT_MESSAGE_Y_OFFSET, message)
end
if OTHER_PLAYER_HERO_PICK_SOUND ~= nil then
PlaySoundLocal(OTHER_PLAYER_HERO_PICK_SOUND, true)
end
ExecuteBan(HeroList[heroIndex], true)
end
--==========================================================================================================================================================
--Pick Hero.
--==========================================================================================================================================================
local function PickHero()
local P ---@type integer
local Q ---@type integer
local message ---@type string
local whichPlayer ---@type player
local heroIndex ---@type integer
local t ---@type timer
local modelPath ---@type string
local wasRandomSelect ---@type boolean
local concealed ---@type boolean
local id ---@type integer
local allHumanPlayersHaveHeroes ---@type boolean
local pickEffect ---@type effect
if not isForcedSelect then
whichPlayer = GetTriggerPlayer()
P = GetPlayerId(whichPlayer) + 1
heroIndex = preselectedHeroIndex[P]
else
P = storePlayerIndex
whichPlayer = Player(P-1)
heroIndex = storeHeroIndex
end
concealed = CONCEAL_HERO_PICKS_FROM_ENEMIES and localPlayerId ~= P and (TEAM_OF_PLAYER[localPlayerId] ~= TEAM_OF_PLAYER[P] or TEAM_OF_PLAYER[P] == 0)
--Random
if heroIndex == RANDOM_HERO then
heroIndex = GetRandomHero(0, P)
if PICK_SOUND ~= nil then
PlaySoundLocal(PICK_SOUND , localPlayerId == P )
end
wasRandomSelect = true
if CREATE_FOREGROUND_HERO then
if localPlayerId == P then
modelPath = HeroList[heroIndex].modelPath
else
modelPath = ""
end
DestroyEffect(SelectedHeroEffect[P])
BlzSetSpecialEffectPosition( SelectedHeroEffect[P] , GARBAGE_DUMP_X , GARBAGE_DUMP_Y , 0 )
SelectedHeroEffect[P] = AddSpecialEffect( modelPath , localForegroundHeroX , localForegroundHeroY )
BlzSetSpecialEffectYaw( SelectedHeroEffect[P] , Deg2Rad(localHeroSelectionAngle) + math.pi )
BlzSetSpecialEffectScale( SelectedHeroEffect[P] , HeroList[heroIndex].scalingValue )
end
else
PlaySoundLocal( "Sound\\Interface\\BigButtonClick.flac" , localPlayerId == P )
if PICK_SOUND ~= nil then
PlaySoundLocal(PICK_SOUND , localPlayerId == P )
end
wasRandomSelect = false
end
--Disable Buttons for selecting player.
for i = 1, SUGGEST_RANDOM do
if localPlayerId == P then
SetButtonTextures(i, true)
end
end
if localPlayerId == P then
BlzFrameSetEnable( heroAcceptButton , false )
BlzFrameSetEnable( heroBanButton , false )
end
--Disable button for other players that have pre-selected that hero.
if CREATE_FOREGROUND_HERO and not HERO_CAN_BE_PICKED_MULTIPLE_TIMES then
SetButtonTextures(heroIndex, true)
for i = 1, NumSelectingPlayers do
if localPlayerId == PlayerNumberOfSlot[i] and preselectedHeroIndex[PlayerNumberOfSlot[i]] == heroIndex then
BlzFrameSetEnable( heroAcceptButton , false )
BlzFrameSetEnable( heroBanButton , false )
if PHANTOM_HERO_WHEN_CANNOT_BE_PICKED and PlayerNumberOfSlot[i] ~= P then
BlzSetSpecialEffectAlpha(SelectedHeroEffect[PlayerNumberOfSlot[i]], 128)
BlzSetSpecialEffectColor(SelectedHeroEffect[PlayerNumberOfSlot[i]], 0, 0, 0)
end
end
end
end
--Set variables
heroIndexWasPicked[heroIndex] = true
pickedHeroIndex[P] = heroIndex
HeroIdOfPlayer[P] = HeroList[heroIndex].unitId
HeroIdOfPlayer[Player(P-1)] = HeroIdOfPlayer[P]
HeroIconPathOfPlayer[P] = "ReplaceableTextures\\CommandButtons\\" .. HeroList[heroIndex].iconPath
HeroIconPathOfPlayer[Player(P-1)] = HeroIconPathOfPlayer[P]
PlayerHasHero[P] = true
PlayerHasHero[Player(P-1)] = true
PlayerHasBan[P] = false
PlayerHasBan[Player(P-1)] = false
NumPlayersWithHero = NumPlayersWithHero + 1
--Text messages
if OTHER_PLAYER_HERO_PICK_SOUND ~= nil then
PlaySoundLocal(OTHER_PLAYER_HERO_PICK_SOUND, localPlayerId ~= P and (MESSAGE_EVEN_WHEN_CONCEALED or not concealed))
end
for i = 1, NumSelectingPlayers do
Q = PlayerNumberOfSlot[i]
if P ~= Q then
if CREATE_TEXT_MESSAGE_ON_PICK then
message = ""
if (CONCEAL_HERO_PICKS_FROM_ENEMIES and (TEAM_OF_PLAYER[P] ~= TEAM_OF_PLAYER[Q] or TEAM_OF_PLAYER[P] == 0)) then
if MESSAGE_EVEN_WHEN_CONCEALED then
message = message .. ColoredPlayerName[P] .. " has selected a hero."
end
else
if wasRandomSelect then
message = message .. ColoredPlayerName[P] .. " has randomly selected " .. GetPickedHeroDisplayedName(heroIndex, false) .. "."
else
message = message .. ColoredPlayerName[P] .. " has selected " .. GetPickedHeroDisplayedName(heroIndex, false) .. "."
end
end
if INCLUDE_PROGRESSION_IN_MESSAGE then
message = message .. " " .. NumPlayersWithHero .. "/" .. NumSelectingPlayers .. " players have selected."
end
if message ~= "" then
if NeatMessageToPlayer then
NeatMessageToPlayer(Player(Q-1), message)
else
DisplayTextToPlayer(Player(Q-1), TEXT_MESSAGE_X_OFFSET, TEXT_MESSAGE_Y_OFFSET, message)
end
end
end
else
if wasRandomSelect then
message = "You randomly selected " .. GetPickedHeroDisplayedName(heroIndex, false) .. "."
else
message = "You selected " .. GetPickedHeroDisplayedName(heroIndex, false) .. "."
end
if INCLUDE_PROGRESSION_IN_MESSAGE then
message = message .. " " .. NumPlayersWithHero .. "/" .. NumSelectingPlayers .. " players have selected."
end
if NeatMessageToPlayer then
NeatMessageToPlayer(Player(P-1), message)
else
DisplayTextToPlayer(Player(P-1), TEXT_MESSAGE_X_OFFSET, TEXT_MESSAGE_Y_OFFSET, message)
end
end
end
--Foreground hero
if CREATE_FOREGROUND_HERO then
if PICK_EFFECT ~= nil then
if localPlayerId == P then
modelPath = PICK_EFFECT
else
modelPath = ""
end
end
pickEffect = AddSpecialEffect( modelPath , localForegroundHeroX , localForegroundHeroY )
BlzSetSpecialEffectZ(pickEffect, GetLocZ(localForegroundHeroX, localForegroundHeroY) + FOREGROUND_HERO_Z)
DestroyEffect(pickEffect)
if PLAY_ANIMATION_ON_PICK then
PlayHeroAnimation(P, heroIndex, false)
end
if PLAY_EMOTE_ON_PICK then
PlaySoundLocal( HeroList[heroIndex].selectEmote , localPlayerId == P )
end
if CREATE_BACKGROUND_HEROES or ESCAPE_PLAYER_AFTER_SELECTING or IsRepicking[P] then
t = CreateTimer()
TimerStart( t , FOREGROUND_HERO_FADEOUT_DELAY , false , FadeoutForegroundHero )
data[t] = {P}
end
end
--Create Background Hero
if CREATE_BACKGROUND_HEROES then
if concealed then
modelPath = CONCEALED_HERO_EFFECT
else
modelPath = HeroList[heroIndex].modelPath
end
backgroundHero[P] = AddSpecialEffect( modelPath , BACKGROUND_HERO_X[P] , BACKGROUND_HERO_Y[P] )
BlzSetSpecialEffectScale( backgroundHero[P] , HeroList[heroIndex].scalingValue )
BlzSetSpecialEffectAlpha( backgroundHero[P] , 0 )
BlzSetSpecialEffectYaw( backgroundHero[P] , math.atan(localForegroundHeroY - BACKGROUND_HERO_Y[P] + math.sin(Deg2Rad(localHeroSelectionAngle))*BACKGROUND_HERO_FACING_POINT_OFFSET , localForegroundHeroX - BACKGROUND_HERO_X[P] + math.cos(Deg2Rad(localHeroSelectionAngle))*BACKGROUND_HERO_FACING_POINT_OFFSET) )
BlzSetSpecialEffectColorByPlayer( backgroundHero[P] , whichPlayer )
BlzSetSpecialEffectColor( backgroundHero[P] , HeroList[heroIndex].red , HeroList[heroIndex].green , HeroList[heroIndex].blue)
if HeroList[heroIndex].needsHeroGlow then
backgroundHeroGlow[P] = AddSpecialEffect( "GeneralHeroGlow.mdx" , BACKGROUND_HERO_X[P] , BACKGROUND_HERO_Y[P] )
BlzSetSpecialEffectAlpha( backgroundHeroGlow[P], 0 )
BlzSetSpecialEffectColorByPlayer( backgroundHeroGlow[P] , whichPlayer )
end
if concealed then
BlzSetSpecialEffectColor(backgroundHero[P], 0, 0, 0)
end
if PLAY_ANIMATION_ON_BACKGROUND_HERO then
PlayHeroAnimation(P, heroIndex, true)
end
if BACKGROUND_HERO_FADEIN_EFFECT ~= nil then
if localPlayerId ~= P then
modelPath = BACKGROUND_HERO_FADEIN_EFFECT
else
modelPath = ""
end
DestroyEffect(AddSpecialEffect(modelPath, BACKGROUND_HERO_X[P], BACKGROUND_HERO_Y[P] ))
end
if PLAY_EMOTE_ON_BACKGROUND_HERO then
PlaySoundLocal(HeroList[heroIndex].selectEmote, localPlayerId ~= P and IsInHeroSelection[localPlayerId] and not concealed)
end
if CREATE_SHADOWS then
if localPlayerId == P then
id = NO_SHADOW_DESTRUCTABLE_ID
else
id = SHADOW_DESTRUCTABLE_ID
end
backgroundHeroShadow[P] = CreateDestructable(id, BACKGROUND_HERO_X[P], BACKGROUND_HERO_Y[P], 0, 1, 0)
end
if BACKGROUND_HERO_FADEIN_TIME > 0 then
t = CreateTimer()
TimerStart( t , BACKGROUND_HERO_FADEIN_TIME/50.0 , true , FadeInBackgroundHero )
data[t] = {P, concealed}
else
if concealed then
BlzSetSpecialEffectAlpha( backgroundHero[P] , 128 )
else
BlzSetSpecialEffectAlpha( backgroundHero[P] , 255 )
end
end
end
if ESCAPE_PLAYER_AFTER_SELECTING or IsRepicking[P] then
t = CreateTimer()
TimerStart( t , PLAYER_PICK_ESCAPE_DELAY , false , EscapePlayerCaller)
data[t] = {P}
end
--End hero selection.
if not IsRepicking[P] and NumPlayersWithHero == NumSelectingPlayers then
HeroSelectionOnLast()
if TIME_LIMIT > 0 then
PauseTimer(countdownTimer)
end
if not ESCAPE_PLAYER_AFTER_SELECTING then
TimerStart( CreateTimer() , LAST_PLAYER_SELECT_END_DELAY , false , EndHeroSelectionCaller )
end
end
if COMPUTER_AUTO_PICK_RANDOM_HERO then
if PlayerIsHuman[P] and not IsRepicking[P] then
allHumanPlayersHaveHeroes = true
for i = 1, NumSelectingPlayers do
P = PlayerNumberOfSlot[i]
if PlayerIsHuman[P] and not PlayerHasHero[P] then
allHumanPlayersHaveHeroes = false
break
end
end
if allHumanPlayersHaveHeroes then
for i = 1, NumSelectingPlayers do
P = PlayerNumberOfSlot[i]
if not PlayerIsHuman[P] and not PlayerHasHero[P] and IsInHeroSelection[P] then
isForcedSelect = true
storePlayerIndex = P
storeHeroIndex = RANDOM_HERO
PickHero()
isForcedSelect = false
end
end
end
end
end
HeroSelectionOnPick(whichPlayer, HeroList[heroIndex], wasRandomSelect, IsRepicking[P])
end
--==========================================================================================================================================================
--Preselect Hero.
--==========================================================================================================================================================
local function PreselectRandomCycle()
local t = GetExpiredTimer() ---@type timer
local P, heroIndexShown = table.unpack(data[t])
local newHeroShown ---@type integer
if preselectedHeroIndex[P] == RANDOM_HERO and not PlayerHasHero[P] then
newHeroShown = GetRandomHero(heroIndexShown, P)
CreateNewForegroundHero(P, newHeroShown)
data[t][2] = newHeroShown
else
DestroyTimer(t)
end
end
local function PreselectHero()
local whichPlayer = GetTriggerPlayer() ---@type player
local P = GetPlayerId(whichPlayer) + 1 ---@type integer
local whichFrame = BlzGetTriggerFrame() ---@type framehandle
local isSuggest ---@type boolean
local isRandom ---@type boolean
local heroIndex ---@type integer
local modelPath = "" ---@type string
local t ---@type timer
local oldHero = preselectedHeroIndex[P] ---@type integer
local id ---@type integer
local preselectEffect ---@type effect
local firstRandomHero ---@type integer
if localPlayerId == P then
BlzFrameSetEnable(whichFrame, false)
BlzFrameSetEnable(whichFrame, true)
end
if HeroPreselectionDisabledForPlayer[P] then
return
end
PlaySoundLocal( "Sound\\Interface\\BigButtonClick.flac" , localPlayerId == P )
if PRESELECT_EFFECT ~= nil then
if localPlayerId == P then
modelPath = PRESELECT_EFFECT
end
preselectEffect = AddSpecialEffect( modelPath , localForegroundHeroX , localForegroundHeroY )
BlzSetSpecialEffectZ(preselectEffect, GetLocZ(localForegroundHeroX, localForegroundHeroY) + FOREGROUND_HERO_Z)
DestroyEffect(preselectEffect)
end
for i = 1, SUGGEST_RANDOM do
if whichFrame == heroSelectionButton[i] then
heroIndex = i
if heroIndex == RANDOM_HERO then
isSuggest = false
isRandom = true
elseif heroIndex == SUGGEST_RANDOM then
isSuggest = true
isRandom = false
else
isSuggest = false
isRandom = false
end
break
end
end
if isSuggest then
heroIndex = GetRandomHero(preselectedHeroIndex[P], P)
if localPlayerId == P and PAGE_OF_CATEGORY[HeroList[heroIndex].category] ~= currentPage then
currentPage = PAGE_OF_CATEGORY[HeroList[heroIndex].category]
DisplayNewPage(P)
end
end
if localPlayerId == P then
if not isRandom then
for i = 1, NUMBER_OF_ABILITY_FRAMES do
if HeroList[heroIndex].abilities[i] then
BlzFrameSetTexture( heroSelectionAbility[i] , BlzGetAbilityIcon( HeroList[heroIndex].abilities[i] ) , 0, true )
BlzFrameSetText( heroSelectionAbilityTooltipTitle[i] , GetHeroAbilityName(HeroList[heroIndex].abilities[i]) )
if HeroList[heroIndex].isNonHeroAbility[i] then
BlzFrameSetText( heroSelectionAbilityTooltipText[i] , BlzGetAbilityExtendedTooltip( HeroList[heroIndex].abilities[i] , 0 ) )
else
BlzFrameSetText( heroSelectionAbilityTooltipText[i] , BlzGetAbilityResearchExtendedTooltip( HeroList[heroIndex].abilities[i] , 0 ) )
end
BlzFrameSetSize(heroSelectionAbilityTooltipText[i] , TOOLTIP_WIDTH - 0.01 , 0.0 )
BlzFrameSetSize(heroSelectionAbilityTooltip[i] , TOOLTIP_WIDTH , BlzFrameGetHeight(heroSelectionAbilityTooltipText[i]) + TOOLTIP_BASE_HEIGHT)
BlzFrameSetVisible( heroSelectionAbility[i] , true )
else
BlzFrameSetVisible( heroSelectionAbility[i] , false )
end
end
else
for i = 1, NUMBER_OF_ABILITY_FRAMES do
BlzFrameSetVisible( heroSelectionAbility[i] , false )
end
end
BlzFrameSetEnable( heroAcceptButton , HeroCanBePicked(heroIndex, P) and not PlayerHasHero[P] and not HeroSelectionDisabledForPlayer[P] )
BlzFrameSetEnable( heroBanButton , HeroCanBePicked(heroIndex, P) and heroIndex ~= RANDOM_HERO and PlayerHasBan[P] )
end
if CREATE_FOREGROUND_HERO then
if not PlayerHasHero[P] and preselectedHeroIndex[P] ~= heroIndex then
if not isRandom then
CreateNewForegroundHero(P, heroIndex)
if HeroCanBePicked(heroIndex, P) and not HeroSelectionDisabledForPlayer[P] then
if PLAY_EMOTE_ON_PRESELECT then
PlaySoundLocal( HeroList[heroIndex].selectEmote , localPlayerId == P )
end
if PLAY_ANIMATION_ON_PRESELECT then
PlayHeroAnimation(P, heroIndex, false)
end
end
elseif preselectedHeroIndex[P] ~= RANDOM_HERO then
if RANDOM_SELECT_CYCLE_STYLE then
firstRandomHero = GetRandomHero(0,P)
CreateNewForegroundHero(P, firstRandomHero)
t = CreateTimer()
TimerStart( t , RANDOM_SELECT_CYCLE_INTERVAL , true , PreselectRandomCycle )
data[t] = {P, firstRandomHero}
else
CreateNewForegroundHero(P, RANDOM_HERO)
end
end
end
if CREATE_SHADOWS then
RemoveDestructable(foregroundHeroShadow)
if preselectedHeroIndex[localPlayerId] == 0 or PlayerHasHero[localPlayerId] then
id = NO_SHADOW_DESTRUCTABLE_ID
else
id = SHADOW_DESTRUCTABLE_ID
end
foregroundHeroShadow = CreateDestructable(id, localForegroundHeroX, localForegroundHeroY, 0, 1, 0)
end
end
if localPlayerId == P then
if not isRandom then
BlzFrameSetPoint( heroSelectionButtonHighlight , FRAMEPOINT_TOPLEFT , heroSelectionButton[heroIndex] , FRAMEPOINT_TOPLEFT , 0.005*0.039/MENU_BUTTON_SIZE , -0.005*0.039/MENU_BUTTON_SIZE )
BlzFrameSetPoint( heroSelectionButtonHighlight , FRAMEPOINT_BOTTOMRIGHT , heroSelectionButton[heroIndex] , FRAMEPOINT_BOTTOMRIGHT , -0.005*0.039/MENU_BUTTON_SIZE , 0.005*0.039/MENU_BUTTON_SIZE )
else
BlzFrameSetPoint( heroSelectionButtonHighlight , FRAMEPOINT_TOPLEFT , heroSelectionButton[RANDOM_HERO] , FRAMEPOINT_TOPLEFT , 0.005*0.039/MENU_BUTTON_SIZE , -0.005*0.039/MENU_BUTTON_SIZE )
BlzFrameSetPoint( heroSelectionButtonHighlight , FRAMEPOINT_BOTTOMRIGHT , heroSelectionButton[RANDOM_HERO] , FRAMEPOINT_BOTTOMRIGHT , -0.005*0.039/MENU_BUTTON_SIZE , 0.005*0.039/MENU_BUTTON_SIZE )
end
BlzFrameSetVisible( heroSelectionButtonHighlight , true )
end
preselectedHeroIndex[P] = heroIndex
HeroSelectionOnPreselect(whichPlayer, HeroList[oldHero], HeroList[heroIndex])
end
--==========================================================================================================================================================
--Init.
--==========================================================================================================================================================
local function InitMenu()
local jLocal ---@type integer
local h ---@type integer
local trig ---@type trigger
local buttonSpacing = MENU_BUTTON_SIZE + MENU_BUTTON_BUTTON_GAP ---@type number
local menuWidth ---@type number
local menuHeight ---@type number
local Ystart ---@type number
local currentY ---@type number
local currentYLowest ---@type number
local widthDiff ---@type number
local column ---@type integer
local heroesThisCategory ---@type integer
local whichHeroes = {} ---@type integer[]
local heroesThisCategoryLocal ---@type integer
local newRow ---@type boolean
local xOffset ---@type number
local glueTextOffset = 0.005 ---@type number
local hasNoCategoryHeroes = false ---@type boolean
local firstCategoryThisPage ---@type boolean
BlzLoadTOCFile("HeroSelectionTemplates.toc")
menuWidth = 2*MENU_LEFT_RIGHT_EDGE_GAP + MENU_NUMBER_OF_COLUMNS*MENU_BUTTON_SIZE + (MENU_NUMBER_OF_COLUMNS - 1)*MENU_BUTTON_BUTTON_GAP
if MENU_BORDER_TILE_SIZE > 0 then
widthDiff = menuWidth
menuWidth = math.floor(menuWidth/MENU_BORDER_TILE_SIZE + 0.99)*MENU_BORDER_TILE_SIZE
widthDiff = menuWidth - widthDiff
else
widthDiff = 0
end
local localClientHeight = BlzGetLocalClientHeight()
local wideScreenAreaWidth
if localClientHeight > 0 then
wideScreenAreaWidth = 0.6*(BlzGetLocalClientWidth()/localClientHeight - 4/3)/2
else
wideScreenAreaWidth = 0.6*(16/9 - 4/3)/2
end
local menuXLeftLocal = math.max(0, MENU_X_LEFT + wideScreenAreaWidth)
tooltipLeftXLocal = math.min(TOOLTIP_LEFT_X + wideScreenAreaWidth, 0.8 + wideScreenAreaWidth - TOOLTIP_WIDTH)
heroSelectionMenu = BlzCreateFrame("HeroSelectionMenu", fullScreenParent, 0, 0)
BlzFrameSetPoint(heroSelectionMenu, FRAMEPOINT_TOPLEFT , fullScreenFrame , FRAMEPOINT_BOTTOMLEFT , menuXLeftLocal , MENU_Y_TOP )
BlzFrameSetPoint(heroSelectionMenu, FRAMEPOINT_BOTTOMRIGHT , fullScreenFrame , FRAMEPOINT_BOTTOMLEFT, menuXLeftLocal + menuWidth , 0 )
heroSelectionButtonTrigger = CreateTrigger()
TriggerAddAction( heroSelectionButtonTrigger , PreselectHero )
Ystart = MENU_Y_TOP - MENU_TOP_EDGE_GAP
currentYLowest = Ystart
--Hero buttons
for k = 1, NUMBER_OF_PAGES do
currentY = Ystart
firstCategoryThisPage = true
for i = 0, NUMBER_OF_CATEGORIES do
if PAGE_OF_CATEGORY[i] == k or (PAGE_OF_CATEGORY[i] == nil and k == 1) then
if i > 0 then
if not firstCategoryThisPage or (CATEGORY_NAMES[i] ~= nil and CATEGORY_NAMES[i] ~= "") then
currentY = currentY - MENU_CATEGORY_GAP
end
if not firstCategoryThisPage then
currentY = currentY - buttonSpacing
end
if CATEGORY_NAMES[i] ~= nil and CATEGORY_NAMES[i] ~= "" then
local scale = MENU_CATEGORY_FONT_SIZE/10.
heroSelectionCategory[i] = BlzCreateFrameByType("TEXT", CATEGORY_NAMES[i], heroSelectionMenu, "", 0)
BlzFrameSetPoint(heroSelectionCategory[i] , FRAMEPOINT_TOPLEFT , fullScreenFrame , FRAMEPOINT_BOTTOMLEFT , menuXLeftLocal/scale , (currentY + MENU_CATEGORY_GAP/2 + MENU_CATEGORY_TITLE_Y + 0.02)/scale)
BlzFrameSetPoint(heroSelectionCategory[i] , FRAMEPOINT_BOTTOMRIGHT , fullScreenFrame , FRAMEPOINT_BOTTOMLEFT , (menuXLeftLocal + menuWidth)/scale , (currentY + MENU_CATEGORY_GAP/2 + MENU_CATEGORY_TITLE_Y - 0.02)/scale)
BlzFrameSetTextAlignment(heroSelectionCategory[i] , TEXT_JUSTIFY_MIDDLE , TEXT_JUSTIFY_CENTER)
BlzFrameSetScale(heroSelectionCategory[i], MENU_CATEGORY_FONT_SIZE/10.)
BlzFrameSetText(heroSelectionCategory[i] , CATEGORY_NAMES[i])
if k ~= 1 then
BlzFrameSetVisible(heroSelectionCategory[i], false)
end
end
end
heroesThisCategory = 0
heroesThisCategoryLocal = 0
for j = 1, NUMBER_OF_HEROES do
if HeroList[j].category == i then
heroesThisCategory = heroesThisCategory + 1
whichHeroes[heroesThisCategory] = j
if not HeroList[j].unavailableToTeam[TEAM_OF_PLAYER[localPlayerId]] then
heroesThisCategoryLocal = heroesThisCategoryLocal + 1
end
end
end
if i == 0 and heroesThisCategoryLocal > 0 then
hasNoCategoryHeroes = true
end
column = 0
newRow = true
jLocal = 1
for j = 1, heroesThisCategory do
if newRow then
if heroesThisCategoryLocal - (jLocal-1) < MENU_NUMBER_OF_COLUMNS then
xOffset = buttonSpacing/2 * (MENU_NUMBER_OF_COLUMNS - (heroesThisCategoryLocal - (jLocal-1))) + widthDiff/2
else
xOffset = widthDiff/2
end
if jLocal ~= 1 then
currentY = currentY - buttonSpacing
end
end
h = whichHeroes[j]
heroSelectionButton[h] = BlzCreateFrameByType("GLUETEXTBUTTON", "heroSelectionButton_" .. h, heroSelectionMenu, "ScriptDialogButton", 0)
BlzFrameSetPoint( heroSelectionButton[h] , FRAMEPOINT_TOPLEFT , fullScreenFrame , FRAMEPOINT_BOTTOMLEFT , menuXLeftLocal + MENU_LEFT_RIGHT_EDGE_GAP + xOffset + column*buttonSpacing - glueTextOffset, currentY + glueTextOffset )
BlzFrameSetPoint( heroSelectionButton[h] , FRAMEPOINT_BOTTOMRIGHT , fullScreenFrame , FRAMEPOINT_BOTTOMLEFT , menuXLeftLocal + MENU_LEFT_RIGHT_EDGE_GAP + xOffset + column*buttonSpacing + MENU_BUTTON_SIZE + glueTextOffset, currentY - MENU_BUTTON_SIZE - glueTextOffset )
SetButtonFrames(h)
SetButtonTextures(h, not PRE_SELECT_BEFORE_ENABLED or HeroList[h].unavailable)
if not HeroList[h].unavailable then
SetButtonTooltip(h, HeroList[h].name, HeroList[h].tooltipAbility ~= 0 and BlzGetAbilityExtendedTooltip(HeroList[h].tooltipAbility, 0) or HeroList[h].tooltip)
else
SetButtonTooltip(h, HeroList[h].name, (HeroList[h].tooltipAbility ~= 0 and BlzGetAbilityExtendedTooltip(HeroList[h].tooltipAbility, 0) or HeroList[h].tooltip) .. "|n|n|cffff0000Not available.|r")
end
BlzFrameSetVisible(heroSelectionButton[h], not HeroList[h].unavailableToTeam[TEAM_OF_PLAYER[localPlayerId]] and k == 1)
BlzTriggerRegisterFrameEvent( heroSelectionButtonTrigger, heroSelectionButton[h] , FRAMEEVENT_CONTROL_CLICK )
if not HeroList[h].unavailableToTeam[TEAM_OF_PLAYER[localPlayerId]] then
column = column + 1
if column == MENU_NUMBER_OF_COLUMNS and jLocal < heroesThisCategoryLocal then
newRow = true
column = 0
else
newRow = false
end
jLocal = jLocal + 1
end
end
if i > 0 or hasNoCategoryHeroes then
firstCategoryThisPage = false
end
end
end
if currentY < currentYLowest then
currentYLowest = currentY
end
end
currentY = currentYLowest
--Random
local buttonsInRandomRow = 0
if MENU_INCLUDE_RANDOM_PICK or MENU_INCLUDE_SUGGEST_RANDOM or (NUMBER_OF_PAGES > 1 and PAGE_CYCLE_BUTTON_STYLE == "EnvelopRandomButton") then
currentY = currentY - buttonSpacing - MENU_HEROES_RANDOM_GAP
buttonsInRandomRow = 0
if MENU_INCLUDE_RANDOM_PICK then
buttonsInRandomRow = buttonsInRandomRow + 1
end
if MENU_INCLUDE_SUGGEST_RANDOM then
buttonsInRandomRow = buttonsInRandomRow + 1
end
if (NUMBER_OF_PAGES > 1 and PAGE_CYCLE_BUTTON_STYLE == "EnvelopRandomButton") then
buttonsInRandomRow = buttonsInRandomRow + 2
end
if HERO_SELECTION_ENABLE_DEBUG_MODE and MENU_NUMBER_OF_COLUMNS < buttonsInRandomRow then
print("|cffff0000Warning:|r Not enough columns to accomodate all buttons in the random row...")
end
end
if MENU_INCLUDE_RANDOM_PICK then
if MENU_INCLUDE_SUGGEST_RANDOM then
xOffset = buttonSpacing/2 * (MENU_NUMBER_OF_COLUMNS - 2) + widthDiff/2
else
xOffset = buttonSpacing/2 * (MENU_NUMBER_OF_COLUMNS - 1) + widthDiff/2
end
heroSelectionButton[RANDOM_HERO] = BlzCreateFrameByType("GLUETEXTBUTTON", "heroRandomButton" , heroSelectionMenu, "ScriptDialogButton", 0)
BlzFrameSetPoint( heroSelectionButton[RANDOM_HERO] , FRAMEPOINT_TOPLEFT , fullScreenFrame , FRAMEPOINT_BOTTOMLEFT , menuXLeftLocal + MENU_LEFT_RIGHT_EDGE_GAP + xOffset - glueTextOffset, currentY + glueTextOffset )
BlzFrameSetPoint( heroSelectionButton[RANDOM_HERO] , FRAMEPOINT_BOTTOMRIGHT , fullScreenFrame , FRAMEPOINT_BOTTOMLEFT , menuXLeftLocal + MENU_LEFT_RIGHT_EDGE_GAP + xOffset + MENU_BUTTON_SIZE + glueTextOffset, currentY - MENU_BUTTON_SIZE - glueTextOffset )
SetButtonFrames(RANDOM_HERO)
SetButtonTextures(RANDOM_HERO, not PRE_SELECT_BEFORE_ENABLED)
SetButtonTooltip(RANDOM_HERO, "Select Random", RANDOM_HERO_TOOLTIP)
BlzTriggerRegisterFrameEvent( heroSelectionButtonTrigger, heroSelectionButton[RANDOM_HERO] , FRAMEEVENT_CONTROL_CLICK )
end
--Suggest
if MENU_INCLUDE_SUGGEST_RANDOM then
if MENU_INCLUDE_RANDOM_PICK then
xOffset = buttonSpacing/2 * MENU_NUMBER_OF_COLUMNS + widthDiff/2
else
xOffset = buttonSpacing/2 * (MENU_NUMBER_OF_COLUMNS - 1) + widthDiff/2
end
heroSelectionButton[SUGGEST_RANDOM] = BlzCreateFrameByType("GLUETEXTBUTTON", "heroRandomButton" , heroSelectionMenu, "ScriptDialogButton", 0)
BlzFrameSetPoint( heroSelectionButton[SUGGEST_RANDOM] , FRAMEPOINT_TOPLEFT , fullScreenFrame , FRAMEPOINT_BOTTOMLEFT , menuXLeftLocal + MENU_LEFT_RIGHT_EDGE_GAP + xOffset - glueTextOffset, currentY + glueTextOffset )
BlzFrameSetPoint( heroSelectionButton[SUGGEST_RANDOM] , FRAMEPOINT_BOTTOMRIGHT , fullScreenFrame , FRAMEPOINT_BOTTOMLEFT , menuXLeftLocal + MENU_LEFT_RIGHT_EDGE_GAP + xOffset + MENU_BUTTON_SIZE + glueTextOffset, currentY - MENU_BUTTON_SIZE - glueTextOffset )
SetButtonFrames(SUGGEST_RANDOM)
SetButtonTextures(SUGGEST_RANDOM, not PRE_SELECT_BEFORE_ENABLED)
SetButtonTooltip(SUGGEST_RANDOM, "Suggest Random", SUGGEST_RANDOM_TOOLTIP)
BlzTriggerRegisterFrameEvent( heroSelectionButtonTrigger, heroSelectionButton[SUGGEST_RANDOM] , FRAMEEVENT_CONTROL_CLICK )
end
--Set Bottom Corners
menuHeight = MENU_Y_TOP - (currentY - buttonSpacing - MENU_BOTTOM_EDGE_GAP)
if MENU_BORDER_TILE_SIZE > 0 then
menuHeight = math.floor(menuHeight/MENU_BORDER_TILE_SIZE + 0.99)*MENU_BORDER_TILE_SIZE
end
BlzFrameSetPoint(heroSelectionMenu, FRAMEPOINT_BOTTOMRIGHT , fullScreenFrame , FRAMEPOINT_BOTTOMLEFT , menuXLeftLocal + menuWidth , MENU_Y_TOP - menuHeight )
--Page Cycle Buttons
if NUMBER_OF_PAGES > 1 then
pageCycleTrigger = CreateTrigger()
TriggerAddAction(pageCycleTrigger, CyclePage)
local pageCycleButtonSpacing = buttonSpacing * MENU_PAGE_CYCLE_SCALE
local pageCycleButtonSize = MENU_BUTTON_SIZE * MENU_PAGE_CYCLE_SCALE
local pageCycleScaleOffset = (buttonSpacing - pageCycleButtonSpacing)/2
local pageCycleDownX
local pageCycleUpX
local pageCycleDownY
local pageCycleUpY
local pageCycleTextType
if PAGE_CYCLE_BUTTON_STYLE == "EnvelopRandomButton" then
pageCycleDownX = menuXLeftLocal + MENU_LEFT_RIGHT_EDGE_GAP + buttonSpacing/2*(MENU_NUMBER_OF_COLUMNS - buttonsInRandomRow) + widthDiff/2
pageCycleUpX = menuXLeftLocal + MENU_LEFT_RIGHT_EDGE_GAP + buttonSpacing/2*(MENU_NUMBER_OF_COLUMNS + buttonsInRandomRow - 2) + widthDiff/2
pageCycleDownY = currentY
pageCycleUpY = currentY
pageCycleTextType = false
elseif PAGE_CYCLE_BUTTON_STYLE == "LeftRightMenuButton" then
pageCycleDownX = menuXLeftLocal + MENU_LEFT_RIGHT_EDGE_GAP + widthDiff/2
pageCycleUpX = menuXLeftLocal + MENU_LEFT_RIGHT_EDGE_GAP + pageCycleButtonSpacing*(MENU_NUMBER_OF_COLUMNS - 1) + widthDiff/2
pageCycleDownY = currentY
pageCycleUpY = currentY
pageCycleTextType = false
elseif PAGE_CYCLE_BUTTON_STYLE == "BelowRandomButton" then
pageCycleDownX = MENU_LEFT_RIGHT_EDGE_GAP + buttonSpacing/2*(MENU_NUMBER_OF_COLUMNS - 2) + widthDiff/2
pageCycleUpX = MENU_LEFT_RIGHT_EDGE_GAP + buttonSpacing/2*MENU_NUMBER_OF_COLUMNS + widthDiff/2
currentY = currentY - pageCycleButtonSpacing - MENU_HEROES_RANDOM_GAP
pageCycleDownY = currentY
pageCycleUpY = currentY
pageCycleTextType = false
menuHeight = MENU_Y_TOP - (currentY - buttonSpacing - MENU_BOTTOM_EDGE_GAP)
if MENU_BORDER_TILE_SIZE > 0 then
menuHeight = math.floor(menuHeight/MENU_BORDER_TILE_SIZE + 0.99)*MENU_BORDER_TILE_SIZE
end
BlzFrameSetPoint(heroSelectionMenu, FRAMEPOINT_BOTTOMRIGHT , fullScreenFrame , FRAMEPOINT_BOTTOMLEFT , menuXLeftLocal + menuWidth , MENU_Y_TOP - menuHeight )
elseif PAGE_CYCLE_BUTTON_STYLE == "LeftRightMiddleButton" then
pageCycleDownX = menuXLeftLocal - pageCycleButtonSize/2
pageCycleUpX = menuXLeftLocal + menuWidth - pageCycleButtonSize/2
pageCycleDownY = MENU_Y_TOP - (menuHeight - pageCycleButtonSize)/2
pageCycleUpY = pageCycleDownY
pageCycleTextType = false
elseif PAGE_CYCLE_BUTTON_STYLE == "LeftVerticalButton" then
pageCycleDownX = menuXLeftLocal - pageCycleButtonSize/2
pageCycleUpX = menuXLeftLocal - pageCycleButtonSize/2
pageCycleDownY = MENU_Y_TOP - menuHeight/2
pageCycleUpY = MENU_Y_TOP - menuHeight/2 + pageCycleButtonSpacing
pageCycleTextType = false
elseif PAGE_CYCLE_BUTTON_STYLE == "RightVerticalButton" then
pageCycleDownX = menuXLeftLocal + menuWidth - pageCycleButtonSize/2
pageCycleUpX = menuXLeftLocal + menuWidth - pageCycleButtonSize/2
pageCycleDownY = MENU_Y_TOP - menuHeight/2
pageCycleUpY = MENU_Y_TOP - menuHeight/2 + pageCycleButtonSpacing
pageCycleTextType = false
elseif PAGE_CYCLE_BUTTON_STYLE == "EnvelopAcceptButton" then
pageCycleDownX = menuXLeftLocal + menuWidth/2 - MENU_PAGE_CYCLE_SCALE*SELECT_BUTTON_WIDTH - SELECT_BUTTON_WIDTH/2
pageCycleUpX = menuXLeftLocal + menuWidth/2 + SELECT_BUTTON_WIDTH/2
pageCycleDownY = MENU_Y_TOP - menuHeight
pageCycleUpY = pageCycleDownY
pageCycleTextType = true
elseif PAGE_CYCLE_BUTTON_STYLE == "LeftRightBottomButton" then
pageCycleDownX = menuXLeftLocal
pageCycleUpX = menuXLeftLocal + menuWidth - SELECT_BUTTON_WIDTH*MENU_PAGE_CYCLE_SCALE
pageCycleDownY = MENU_Y_TOP - menuHeight
pageCycleUpY = pageCycleDownY
pageCycleTextType = true
elseif PAGE_CYCLE_BUTTON_STYLE == "TangentTopButton" then
pageCycleDownX = menuXLeftLocal + menuWidth/2 - SELECT_BUTTON_WIDTH*MENU_PAGE_CYCLE_SCALE
pageCycleUpX = menuXLeftLocal + menuWidth/2
pageCycleDownY = MENU_Y_TOP - 0.0175
pageCycleUpY = pageCycleDownY
pageCycleTextType = true
elseif PAGE_CYCLE_BUTTON_STYLE == "LeftRightTopButton" then
pageCycleDownX = menuXLeftLocal
pageCycleUpX = menuXLeftLocal + menuWidth - SELECT_BUTTON_WIDTH*MENU_PAGE_CYCLE_SCALE
pageCycleDownY = MENU_Y_TOP - 0.0175
pageCycleUpY = pageCycleDownY
pageCycleTextType = true
elseif HERO_SELECTION_ENABLE_DEBUG_MODE then
BJDebugMsg("|cffff0000Warning:|r Unrecognized page cycle button style (" .. PAGE_CYCLE_BUTTON_STYLE .. ").")
end
if pageCycleTextType then
heroSelectionButton[PAGE_DOWN] = BlzCreateFrameByType("GLUETEXTBUTTON", "pageDownButton" , heroSelectionMenu, "ScriptDialogButton", 0)
BlzFrameSetPoint( heroSelectionButton[PAGE_DOWN] , FRAMEPOINT_TOPLEFT , fullScreenFrame , FRAMEPOINT_BOTTOMLEFT , pageCycleDownX + MENU_PAGE_CYCLE_X_OFFSET , pageCycleDownY + 0.012 + 0.012*SELECT_BUTTON_SCALE*MENU_PAGE_CYCLE_SCALE + MENU_PAGE_CYCLE_Y_OFFSET )
BlzFrameSetPoint( heroSelectionButton[PAGE_DOWN] , FRAMEPOINT_BOTTOMRIGHT , fullScreenFrame , FRAMEPOINT_BOTTOMLEFT , pageCycleDownX + MENU_PAGE_CYCLE_X_OFFSET + SELECT_BUTTON_WIDTH*MENU_PAGE_CYCLE_SCALE , pageCycleDownY - 0.003 - 0.003*SELECT_BUTTON_SCALE*MENU_PAGE_CYCLE_SCALE + MENU_PAGE_CYCLE_Y_OFFSET )
BlzFrameSetText( heroSelectionButton[PAGE_DOWN] , "Prev" )
BlzFrameSetScale( heroSelectionButton[PAGE_DOWN] , SELECT_BUTTON_SCALE*MENU_PAGE_CYCLE_SCALE )
else
heroSelectionButton[PAGE_DOWN] = BlzCreateFrameByType("GLUETEXTBUTTON", "pageDownButton" , heroSelectionMenu, "ScriptDialogButton", 0)
BlzFrameSetPoint( heroSelectionButton[PAGE_DOWN] , FRAMEPOINT_TOPLEFT , fullScreenFrame , FRAMEPOINT_BOTTOMLEFT , pageCycleDownX + MENU_PAGE_CYCLE_X_OFFSET - glueTextOffset + pageCycleScaleOffset, pageCycleDownY + glueTextOffset + MENU_PAGE_CYCLE_Y_OFFSET - pageCycleScaleOffset )
BlzFrameSetPoint( heroSelectionButton[PAGE_DOWN] , FRAMEPOINT_BOTTOMRIGHT , fullScreenFrame , FRAMEPOINT_BOTTOMLEFT , pageCycleDownX + MENU_PAGE_CYCLE_X_OFFSET + pageCycleButtonSize + glueTextOffset + pageCycleScaleOffset, pageCycleDownY - pageCycleButtonSize - glueTextOffset + MENU_PAGE_CYCLE_Y_OFFSET - pageCycleScaleOffset )
SetButtonFrames(PAGE_DOWN)
SetButtonTextures(PAGE_DOWN, not PRE_SELECT_BEFORE_ENABLED)
SetButtonTooltip(PAGE_DOWN, "Page Down", "Go to the previous page.")
end
BlzTriggerRegisterFrameEvent( pageCycleTrigger, heroSelectionButton[PAGE_DOWN] , FRAMEEVENT_CONTROL_CLICK )
if pageCycleTextType then
heroSelectionButton[PAGE_UP] = BlzCreateFrameByType("GLUETEXTBUTTON", "pageUpButton" , heroSelectionMenu, "ScriptDialogButton", 0)
BlzFrameSetPoint( heroSelectionButton[PAGE_UP] , FRAMEPOINT_TOPLEFT , fullScreenFrame , FRAMEPOINT_BOTTOMLEFT , pageCycleUpX - MENU_PAGE_CYCLE_X_OFFSET , pageCycleUpY + 0.012 + 0.012*SELECT_BUTTON_SCALE*MENU_PAGE_CYCLE_SCALE + MENU_PAGE_CYCLE_Y_OFFSET )
BlzFrameSetPoint( heroSelectionButton[PAGE_UP] , FRAMEPOINT_BOTTOMRIGHT , fullScreenFrame , FRAMEPOINT_BOTTOMLEFT , pageCycleUpX - MENU_PAGE_CYCLE_X_OFFSET + SELECT_BUTTON_WIDTH*MENU_PAGE_CYCLE_SCALE , pageCycleUpY - 0.003 - 0.003*SELECT_BUTTON_SCALE*MENU_PAGE_CYCLE_SCALE + MENU_PAGE_CYCLE_Y_OFFSET )
BlzFrameSetText( heroSelectionButton[PAGE_UP] , "Next" )
BlzFrameSetScale( heroSelectionButton[PAGE_UP] , SELECT_BUTTON_SCALE*MENU_PAGE_CYCLE_SCALE )
else
heroSelectionButton[PAGE_UP] = BlzCreateFrameByType("GLUETEXTBUTTON", "pageUpButton" , heroSelectionMenu, "ScriptDialogButton", 0)
BlzFrameSetPoint( heroSelectionButton[PAGE_UP] , FRAMEPOINT_TOPLEFT , fullScreenFrame , FRAMEPOINT_BOTTOMLEFT , pageCycleUpX - MENU_PAGE_CYCLE_X_OFFSET - glueTextOffset + pageCycleScaleOffset , pageCycleUpY + glueTextOffset + MENU_PAGE_CYCLE_Y_OFFSET - pageCycleScaleOffset )
BlzFrameSetPoint( heroSelectionButton[PAGE_UP] , FRAMEPOINT_BOTTOMRIGHT , fullScreenFrame , FRAMEPOINT_BOTTOMLEFT , pageCycleUpX - MENU_PAGE_CYCLE_X_OFFSET + pageCycleButtonSize + glueTextOffset + pageCycleScaleOffset , pageCycleUpY - pageCycleButtonSize - glueTextOffset + MENU_PAGE_CYCLE_Y_OFFSET - pageCycleScaleOffset )
SetButtonFrames(PAGE_UP)
SetButtonTextures(PAGE_UP, not PRE_SELECT_BEFORE_ENABLED)
SetButtonTooltip(PAGE_UP, "Page Up", "Go to the next page.")
end
BlzTriggerRegisterFrameEvent( pageCycleTrigger, heroSelectionButton[PAGE_UP] , FRAMEEVENT_CONTROL_CLICK )
end
--Highlight
heroSelectionButtonHighlight = BlzCreateFrameByType("SPRITE", "SpriteName", heroSelectionMenu, "", 0)
BlzFrameSetModel(heroSelectionButtonHighlight, "UI\\Feedback\\Autocast\\UI-ModalButtonOn.mdl", 0)
BlzFrameSetScale(heroSelectionButtonHighlight, MENU_BUTTON_SIZE/0.039)
BlzFrameSetVisible( heroSelectionButtonHighlight , false )
--Accept
heroAcceptButton = BlzCreateFrameByType("GLUETEXTBUTTON", "heroAcceptButton" , heroSelectionMenu, "ScriptDialogButton", 0)
BlzFrameSetPoint( heroAcceptButton , FRAMEPOINT_TOPLEFT , fullScreenFrame , FRAMEPOINT_BOTTOMLEFT , menuXLeftLocal + menuWidth/2 - SELECT_BUTTON_WIDTH/2 , MENU_Y_TOP - menuHeight + 0.012 + 0.012*SELECT_BUTTON_SCALE )
BlzFrameSetPoint( heroAcceptButton , FRAMEPOINT_BOTTOMRIGHT , fullScreenFrame , FRAMEPOINT_BOTTOMLEFT , menuXLeftLocal + menuWidth/2 + SELECT_BUTTON_WIDTH/2 , MENU_Y_TOP - menuHeight - 0.003 - 0.003*SELECT_BUTTON_SCALE )
BlzFrameSetText( heroAcceptButton , SELECT_BUTTON_TEXT )
BlzFrameSetScale( heroAcceptButton , SELECT_BUTTON_SCALE )
BlzFrameSetEnable( heroAcceptButton , false )
trig = CreateTrigger()
BlzTriggerRegisterFrameEvent( trig , heroAcceptButton , FRAMEEVENT_CONTROL_CLICK )
TriggerAddAction( trig, PickHero )
--Ban
heroBanButton = BlzCreateFrameByType("GLUETEXTBUTTON", "heroBanButton" , heroSelectionMenu, "ScriptDialogButton", 0)
BlzFrameSetPoint( heroBanButton , FRAMEPOINT_TOPLEFT , fullScreenFrame , FRAMEPOINT_BOTTOMLEFT , menuXLeftLocal + menuWidth/2 - SELECT_BUTTON_WIDTH/2 , MENU_Y_TOP - menuHeight + 0.012 + 0.012*SELECT_BUTTON_SCALE )
BlzFrameSetPoint( heroBanButton , FRAMEPOINT_BOTTOMRIGHT , fullScreenFrame , FRAMEPOINT_BOTTOMLEFT , menuXLeftLocal + menuWidth/2 + SELECT_BUTTON_WIDTH/2 , MENU_Y_TOP - menuHeight - 0.003 - 0.003*SELECT_BUTTON_SCALE )
BlzFrameSetText( heroBanButton , SELECT_BUTTON_TEXT )
BlzFrameSetText( heroBanButton , "Ban" )
BlzFrameSetEnable( heroBanButton , false )
BlzFrameSetVisible( heroBanButton , false)
trig = CreateTrigger()
BlzTriggerRegisterFrameEvent( trig , heroBanButton , FRAMEEVENT_CONTROL_CLICK )
TriggerAddAction( trig, BanHero )
--Ability Buttons
for i = 1, NUMBER_OF_ABILITY_FRAMES do
heroSelectionAbility[i] = BlzCreateFrameByType("BACKDROP", "heroSelectionAbility_" .. i, heroSelectionMenu, "", 0)
if ABILITY_BUTTON_HORIZONTAL_LAYOUT then
BlzFrameSetPoint(heroSelectionAbility[i], FRAMEPOINT_TOPLEFT, heroSelectionMenu , FRAMEPOINT_TOPRIGHT, HERO_ABILITY_PREVIEW_BUTTON_X + HERO_ABILITY_PREVIEW_BUTTON_SIZE*(i-1), HERO_ABILITY_PREVIEW_BUTTON_Y)
else
BlzFrameSetPoint(heroSelectionAbility[i], FRAMEPOINT_TOPLEFT, heroSelectionMenu , FRAMEPOINT_TOPRIGHT, HERO_ABILITY_PREVIEW_BUTTON_X, HERO_ABILITY_PREVIEW_BUTTON_Y - HERO_ABILITY_PREVIEW_BUTTON_SIZE*(i-1))
end
BlzFrameSetSize(heroSelectionAbility[i], HERO_ABILITY_PREVIEW_BUTTON_SIZE, HERO_ABILITY_PREVIEW_BUTTON_SIZE)
BlzFrameSetVisible( heroSelectionAbility[i] , false )
heroSelectionAbilityHover[i] = BlzCreateFrameByType("FRAME" , "heroIconFrameHover" , heroSelectionAbility[i] , "" , 0)
BlzFrameSetAllPoints( heroSelectionAbilityHover[i] , heroSelectionAbility[i] )
heroSelectionAbilityTooltip[i] = BlzCreateFrame("CustomTooltip", heroSelectionAbilityHover[i], 0, 0)
if TOOLTIP_LOCK_TOP then
BlzFrameSetPoint( heroSelectionAbilityTooltip[i] , FRAMEPOINT_TOPLEFT , fullScreenFrame , FRAMEPOINT_BOTTOMLEFT , tooltipLeftXLocal , TOOLTIP_Y )
else
BlzFrameSetPoint( heroSelectionAbilityTooltip[i] , FRAMEPOINT_BOTTOMLEFT , fullScreenFrame , FRAMEPOINT_BOTTOMLEFT , tooltipLeftXLocal , TOOLTIP_Y )
end
BlzFrameSetTooltip( heroSelectionAbilityHover[i] , heroSelectionAbilityTooltip[i] )
BlzFrameSetSize( heroSelectionAbilityTooltip[i] , TOOLTIP_WIDTH , 0.0 )
heroSelectionAbilityTooltipTitle[i] = BlzFrameGetChild( heroSelectionAbilityTooltip[i],0)
heroSelectionAbilityTooltipText[i] = BlzFrameGetChild( heroSelectionAbilityTooltip[i],1)
end
BlzFrameSetVisible( heroSelectionMenu, false)
end
--==========================================================================================================================================================
--API
--==========================================================================================================================================================
---@return boolean
_G.EveryoneHasHero = function()
return NumPlayersWithHero == NumSelectingPlayers
end
---@return boolean
_G.NoOneInHeroSelection = function()
return NumPlayersInSelection == 0
end
---@param whichPlayer player
---@param enable boolean
_G.EnablePlayerHeroSelection = function(whichPlayer, enable)
local P = GetPlayerId(whichPlayer) + 1 ---@type integer
if not IsInHeroSelection[P] then
if HERO_SELECTION_ENABLE_DEBUG_MODE then
print("|cffff0000Warning:|r Attempted to enable hero selection for player who is not in hero selection...")
end
return
end
if enable then
if localPlayerId == P then
BlzFrameSetVisible(heroSelectionMenu, true)
for i = 1, PAGE_UP do
SetButtonTextures(i, false)
i = i + 1
end
end
if PHANTOM_HERO_WHEN_CANNOT_BE_PICKED then
BlzSetSpecialEffectColor( SelectedHeroEffect[localPlayerId] , 255 , 255 , 255 )
BlzSetSpecialEffectAlpha( SelectedHeroEffect[localPlayerId] , 255 )
end
else
if localPlayerId == P then
for i = 1, NUMBER_OF_ABILITY_FRAMES do
BlzFrameSetVisible( heroSelectionAbility[i] , false )
i = i + 1
end
for i = 1, PAGE_UP do
SetButtonTextures(i, true)
end
BlzFrameSetEnable(heroAcceptButton, false)
end
if PHANTOM_HERO_WHEN_CANNOT_BE_PICKED then
BlzSetSpecialEffectColor( SelectedHeroEffect[localPlayerId] , 0 , 0 , 0 )
BlzSetSpecialEffectAlpha( SelectedHeroEffect[localPlayerId] , 128 )
end
preselectedHeroIndex[P] = 0
end
HeroSelectionDisabledForPlayer[P] = not enable
HeroSelectionDisabledForPlayer[Player(P-1)] = not enable
end
---@param whichPlayer player
---@param enable boolean
_G.EnablePlayerHeroPreselection = function(whichPlayer, enable)
local P = GetPlayerId(whichPlayer) + 1 ---@type integer
if not IsInHeroSelection[P] then
if HERO_SELECTION_ENABLE_DEBUG_MODE then
print("|cffff0000Warning:|r Attempted to enable hero preselection for player who is not in hero selection...")
end
return
end
if enable then
if GetLocalPlayer() == whichPlayer then
for i = 1, PAGE_UP do
SetButtonTextures(i, false)
end
end
else
if GetLocalPlayer() == whichPlayer then
for i = 1, NUMBER_OF_ABILITY_FRAMES do
BlzFrameSetVisible( heroSelectionAbility[i] , false )
end
for i = 1, PAGE_UP do
SetButtonTextures(i, true)
end
BlzFrameSetVisible( heroSelectionButtonHighlight, false)
BlzFrameSetEnable(heroAcceptButton, false)
end
BlzSetSpecialEffectPosition(SelectedHeroEffect[P], GARBAGE_DUMP_X, GARBAGE_DUMP_Y, 0)
DestroyEffect(SelectedHeroEffect[P])
DestroyEffect(selectedHeroGlow[P])
preselectedHeroIndex[P] = 0
end
HeroPreselectionDisabledForPlayer[P] = not enable
HeroPreselectionDisabledForPlayer[Player(P-1)] = not enable
end
_G.HeroSelectionAllRandom = function()
local P ---@type integer
if NumPlayersInSelection == 0 then
if HERO_SELECTION_ENABLE_DEBUG_MODE then
print("|cffff0000Warning:|r Attempted to start hero ban phase, but no one is in hero selection right now...")
end
return
end
storeHeroIndex = RANDOM_HERO
isForcedSelect = true
for i = 1, NumSelectingPlayers do
P = PlayerNumberOfSlot[i]
if not PlayerHasHero[P] then
storePlayerIndex = P
PickHero()
end
end
HeroSelectionOnAllRandom()
isForcedSelect = false
end
---@param whichPlayer player
---@param whichHero Hero
_G.HeroSelectionPlayerForceSelect = function(whichPlayer, whichHero)
if not IsInHeroSelection[GetPlayerId(whichPlayer) + 1] then
if HERO_SELECTION_ENABLE_DEBUG_MODE then
print("|cffff0000Warning:|r Attempted to force select on a player who is not in hero selection...")
end
return
end
if PlayerHasHero[GetPlayerId(whichPlayer) + 1] then
if HERO_SELECTION_ENABLE_DEBUG_MODE then
print("|cffff0000Warning:|r Attempted to force select on a player who already selected a hero...")
end
return
end
isForcedSelect = true
storePlayerIndex = GetPlayerId(whichPlayer) + 1
storeHeroIndex = whichHero.index
PickHero()
isForcedSelect = false
end
---@param whichPlayer player
_G.HeroSelectionPlayerForceRandom = function(whichPlayer)
if not IsInHeroSelection[GetPlayerId(whichPlayer) + 1] then
if HERO_SELECTION_ENABLE_DEBUG_MODE then
print("|cffff0000Warning:|r Attempted to force random on a player who is not in hero selection...")
end
return
end
if PlayerHasHero[GetPlayerId(whichPlayer) + 1] then
if HERO_SELECTION_ENABLE_DEBUG_MODE then
print("|cffff0000Warning:|r Attempted to force random on a player who already selected a hero...")
end
return
end
isForcedSelect = true
storePlayerIndex = GetPlayerId(whichPlayer) + 1
storeHeroIndex = RANDOM_HERO
PickHero()
isForcedSelect = false
end
_G.HeroSelectionForceEnd = function()
local P ---@type integer
for i = 1, NumSelectingPlayers do
P = PlayerNumberOfSlot[i]
if not PlayerHasHero[P] then
HeroSelectionPlayerForceRandom(Player(P-1))
end
end
end
---@param whichPlayer player
_G.PlayerEscapeHeroSelection = function(whichPlayer)
if not IsInHeroSelection[GetPlayerId(whichPlayer) + 1] then
return
end
EscapePlayer(whichPlayer)
end
---@param whichHero Hero
---@param disable boolean
_G.BanHeroFromSelection = function(whichHero, disable)
ExecuteBan(whichHero, disable, nil)
end
---@param whichHero Hero
---@param whichPlayer player
---@param disable boolean
_G.BanHeroFromSelectionForPlayer = function(whichHero, whichPlayer, disable)
ExecuteBan(whichHero, disable, GetPlayerId(whichPlayer) + 1)
end
---@param time number
_G.HeroSelectionSetTimeRemaining = function(time)
TimerStart(countdownTimer, time, false, TimeExpires)
BlzFrameSetText(timerFrame, TIMER_TEXT + GetClockString(TimerGetRemaining(countdownTimer)))
BlzFrameSetVisible(timerFrame, IsInHeroSelection[localPlayerId])
end
---@param time number
_G.HeroSelectionAddTimeRemaining = function(time)
TimerStart(countdownTimer, TimerGetRemaining(countdownTimer) + time, false, TimeExpires)
BlzFrameSetText(timerFrame, TIMER_TEXT + GetClockString(TimerGetRemaining(countdownTimer)))
BlzFrameSetVisible(timerFrame, IsInHeroSelection[localPlayerId])
end
_G.EnableHeroSelection = function()
local P ---@type integer
if NumPlayersInSelection == 0 then
if HERO_SELECTION_ENABLE_DEBUG_MODE then
print("|cffff0000Warning:|r Attempted to enable hero selection, but no one is in hero selection right now...")
end
return
end
if TIME_LIMIT ~= 0 then
BlzFrameSetVisible(timerFrame, IsInHeroSelection[localPlayerId])
TimerStart(countdownTimer, TIME_LIMIT, false, TimeExpires)
TimerStart(countdownUpdateTimer, 1.0, true, UpdateTimerFrame)
BlzFrameSetText(timerFrame, TIMER_TEXT .. GetClockString(TimerGetRemaining(countdownTimer)))
end
for i = 1, NumSelectingPlayers do
P = PlayerNumberOfSlot[i]
if IsInHeroSelection[P] then
HeroSelectionDisabledForPlayer[P] = false
HeroSelectionDisabledForPlayer[Player(P-1)] = false
HeroPreselectionDisabledForPlayer[P] = false
HeroPreselectionDisabledForPlayer[Player(P-1)] = false
PlayerHasBan[P] = false
PlayerHasBan[Player(P-1)] = false
if localPlayerId == P then
BlzFrameSetVisible(heroSelectionMenu, true)
end
if PLAYER_TEXT_TAGS then
backgroundHeroTextTag[P] = CreateTextTag()
SetTextTagText(backgroundHeroTextTag[P], ColoredPlayerName[P] , 0.023)
SetTextTagPos(backgroundHeroTextTag[P], BACKGROUND_HERO_X[P], BACKGROUND_HERO_Y[P], 25)
SetTextTagVisibility(backgroundHeroTextTag[P], true)
end
end
end
BlzFrameSetVisible(heroAcceptButton, true )
BlzFrameSetVisible(heroBanButton, false )
BlzFrameSetEnable( heroBanButton, false )
captionAlphaMultiplier = 1
if HERO_SELECTION_CAPTION ~= nil then
BlzFrameSetVisible(captionFrame, IsInHeroSelection[localPlayerId])
if CAPTION_COLOR_1 ~= CAPTION_COLOR_2 or CAPTION_ALPHA_1 ~= CAPTION_ALPHA_2 then
AnimateCaption()
end
end
if HeroCanBePicked(preselectedHeroIndex[localPlayerId], localPlayerId) then
BlzFrameSetEnable( heroAcceptButton, true )
if PHANTOM_HERO_WHEN_CANNOT_BE_PICKED then
BlzSetSpecialEffectColor( SelectedHeroEffect[localPlayerId] , 255 , 255 , 255 )
BlzSetSpecialEffectAlpha( SelectedHeroEffect[localPlayerId] , 255 )
end
end
InBanPhase = false
for i = 1, NUMBER_OF_HEROES do
SetButtonTextures(i, not HeroCanBePicked(i, localPlayerId))
end
SetButtonTextures(RANDOM_HERO, false)
SetButtonTextures(SUGGEST_RANDOM, false)
SetButtonTextures(PAGE_DOWN, false)
SetButtonTextures(PAGE_UP, false)
HeroSelectionOnEnable()
end
---@param timeout number
---@param callback function
_G.StartHeroSelectionTimer = function(timeout, callback)
TimerStart( countdownTimer, timeout, false, callback)
BlzFrameSetVisible(timerFrame, IsInHeroSelection[localPlayerId])
TimerStart(countdownUpdateTimer, 1.0, true, UpdateTimerFrame)
BlzFrameSetText(timerFrame, TIMER_TEXT + GetClockString(TimerGetRemaining(countdownTimer)))
end
---@param timeLimit number
_G.StartHeroBanPhase = function(timeLimit)
local P ---@type integer
if NumPlayersInSelection == 0 then
if HERO_SELECTION_ENABLE_DEBUG_MODE then
print("|cffff0000Warning:|r Attempted to start hero ban phase, but no one is in hero selection right now...")
end
return
end
for i = 1, NumSelectingPlayers do
P = PlayerNumberOfSlot[i]
PlayerHasBan[P] = true
PlayerHasBan[Player(P-1)] = true
HeroPreselectionDisabledForPlayer[P] = false
HeroPreselectionDisabledForPlayer[Player(P-1)] = false
if localPlayerId == P then
BlzFrameSetVisible(heroSelectionMenu, true)
end
end
InBanPhase = true
for i = 1, NUMBER_OF_HEROES do
SetButtonTextures(i, not HeroCanBePicked(i, localPlayerId))
end
SetButtonTextures(SUGGEST_RANDOM, false)
SetButtonTextures(PAGE_DOWN, false)
SetButtonTextures(PAGE_UP, false)
BlzFrameSetVisible(heroAcceptButton, false)
BlzFrameSetVisible(heroBanButton, true)
BlzFrameSetEnable( heroBanButton , HeroCanBePicked(preselectedHeroIndex[localPlayerId], localPlayerId) and preselectedHeroIndex[P] ~= RANDOM_HERO )
StartHeroSelectionTimer(timeLimit, EnableHeroSelection)
end
---@param whichPlayer player
_G.PlayerReturnToHeroSelection = function(whichPlayer)
local P = GetPlayerId(whichPlayer) + 1 ---@type integer
local id ---@type integer
if heroSelectionMenu == nil then
if HERO_SELECTION_ENABLE_DEBUG_MODE then
print("|cffff0000Warning:|r Attempted to return player to hero selection, but hero selection was not initialized...")
end
return
end
PlayerHasHero[P] = false
PlayerHasHero[Player(P-1)] = false
IsInHeroSelection[P] = true
IsInHeroSelection[Player(P-1)] = true
IsRepicking[P] = true
IsRepicking[Player(P-1)] = true
NumPlayersWithHero = NumPlayersWithHero - 1
NumPlayersInSelection = NumPlayersInSelection + 1
heroIndexWasPicked[pickedHeroIndex[P]] = false
pickedHeroIndex[P] = 0
if not DELETE_BACKGROUND_HEROES_AFTER_END then
BlzSetSpecialEffectPosition(backgroundHero[P], GARBAGE_DUMP_X, GARBAGE_DUMP_Y, 0)
DeleteBackgroundHero(P)
end
for i = 1, NUMBER_OF_HEROES do
SetButtonTextures(i, not HeroCanBePicked(i, localPlayerId))
end
SetButtonTextures(RANDOM_HERO, false)
SetButtonTextures(SUGGEST_RANDOM, false)
SetButtonTextures(PAGE_DOWN, false)
SetButtonTextures(PAGE_UP, false)
local consoleUIBackdrop = BlzGetFrameByName("ConsoleUIBackdrop",0)
local bottomUI = BlzGetFrameByName("ConsoleBottomBar", 0)
local topUI = BlzGetFrameByName("ConsoleTopBar", 0)
if localPlayerId == P then
ClearSelection()
BlzFrameSetVisible(heroSelectionMenu, true)
BlzFrameSetVisible( heroSelectionButtonHighlight , false )
if HIDE_GAME_UI then
BlzFrameSetVisible(consoleUIBackdrop, false)
BlzFrameSetVisible(bottomUI, false)
BlzFrameSetVisible(topUI, false)
print(GetHandleId(bottomUI))
print(GetHandleId(topUI))
BlzHideOriginFrames(true)
BlzFrameSetAllPoints(BlzGetOriginFrame(ORIGIN_FRAME_WORLD_FRAME, 0), BlzGetOriginFrame(ORIGIN_FRAME_GAME_UI, 0))
end
end
if CREATE_SHADOWS then
RemoveDestructable(foregroundHeroShadow)
if preselectedHeroIndex[localPlayerId] == 0 or PlayerHasHero[localPlayerId] then
id = NO_SHADOW_DESTRUCTABLE_ID
else
id = SHADOW_DESTRUCTABLE_ID
end
foregroundHeroShadow = CreateDestructable(id, localForegroundHeroX, localForegroundHeroY, 0, 1, 0)
end
if ENFORCE_CAMERA then
TimerStart(lockCameraTimer, 0.01, true, LockSelecterCamera)
end
HeroSelectionOnReturn(whichPlayer)
end
_G.BeginHeroSelection = function()
local P ---@type integer
if heroSelectionMenu == nil then
if HERO_SELECTION_ENABLE_DEBUG_MODE then
print("|cffff0000Warning:|r Attempted to begin hero selection, but hero selection was not initialized...")
end
return
end
if HIDE_GAME_UI then
BlzHideOriginFrames(true)
BlzFrameSetAllPoints(BlzGetOriginFrame(ORIGIN_FRAME_WORLD_FRAME, 0), BlzGetOriginFrame(ORIGIN_FRAME_GAME_UI, 0))
BlzFrameSetVisible(BlzGetFrameByName("ConsoleUIBackdrop",0), false)
BlzFrameSetVisible(BlzGetFrameByName("ConsoleBottomBar", 0), false)
BlzFrameSetVisible(BlzGetFrameByName("ConsoleTopBar", 0), false)
end
for i = 1, NumSelectingPlayers do
P = PlayerNumberOfSlot[i]
IsInHeroSelection[P] = true
NumPlayersInSelection = NumPlayersInSelection + 1
if localPlayerId == P then
ClearSelection()
end
end
if CREATE_SHADOWS then
foregroundHeroShadow = CreateDestructable(NO_SHADOW_DESTRUCTABLE_ID, localForegroundHeroX, localForegroundHeroY, 0, 1, 0)
end
if SHOW_MENU_BEFORE_ENABLED then
BlzFrameSetVisible(heroSelectionMenu, true)
end
BlzFrameSetVisible( heroSelectionButtonHighlight , false )
if ENFORCE_CAMERA then
TimerStart(lockCameraTimer, 0.01, true, LockSelecterCamera)
end
HeroSelectionOnBegin()
end
_G.RestartHeroSelection = function()
local P ---@type integer
if heroSelectionMenu == nil then
if HERO_SELECTION_ENABLE_DEBUG_MODE then
print("|cffff0000Warning:|r Attempted to restart hero selection, but hero selection was not initialized...")
end
return
end
if NumPlayersInSelection > 0 then
if HERO_SELECTION_ENABLE_DEBUG_MODE then
print("|cffff0000Warning:|r Attempted to restart hero selection while there are players in hero selection...")
end
return
end
HeroSelectionOnRestart()
for i = 1, NumSelectingPlayers do
P = PlayerNumberOfSlot[i]
PlayerHasHero[P] = false
PlayerHasHero[Player(P-1)] = false
PlayerHasBan[P] = false
PlayerHasBan[Player(P-1)] = false
IsInHeroSelection[P] = false
IsInHeroSelection[Player(P-1)] = false
HeroSelectionDisabledForPlayer[P] = true
HeroSelectionDisabledForPlayer[Player(P-1)] = true
pickedHeroIndex[P] = 0
if CREATE_BACKGROUND_HEROES and not DELETE_BACKGROUND_HEROES_AFTER_END then
BlzSetSpecialEffectPosition(backgroundHero[P], GARBAGE_DUMP_X, GARBAGE_DUMP_Y, 0)
DeleteBackgroundHero(P)
end
for j = 1, NUMBER_OF_HEROES do
heroIndexWasBanned[i][j] = false
end
end
for i = 1, NUMBER_OF_HEROES do
heroIndexWasPicked[i] = false
end
NumPlayersWithHero = 0
NumPlayersInSelection = 0
BeginHeroSelection()
end
_G.InitHeroSelection = function()
local P ---@type integer
local j ---@type integer
local leaveTrigger = CreateTrigger() ---@type trigger
local teamSize = {} ---@type integer[]
local slotInTeam = {} ---@type integer[]
local cameraTargetPositionX = {} ---@type number[]
local cameraTargetPositionY = {} ---@type number[]
local cameraEyePositionX ---@type number
local cameraEyePositionY ---@type number
moveableLoc = Location(0,0)
InitFullScreenParents()
lockCameraTimer = CreateTimer()
captionTimer = CreateTimer()
countdownTimer = CreateTimer()
countdownUpdateTimer = CreateTimer()
GARBAGE_DUMP_X = GetRectMaxX(GetPlayableMapRect())
GARBAGE_DUMP_Y = GetRectMaxY(GetPlayableMapRect())
localPlayerId = GetPlayerId(GetLocalPlayer()) + 1
--Player List
for i = 1, 24 do
if not AUTO_SET_SELECTING_PLAYERS then
if PLAYER_SELECTS_HERO[i] and GetPlayerSlotState(Player(i-1)) == PLAYER_SLOT_STATE_PLAYING then
local whichPlayer = Player(i-1)
table.insert(SelectingPlayers, whichPlayer)
NumSelectingPlayers = NumSelectingPlayers + 1
PlayerNumberOfSlot[NumSelectingPlayers] = i
ColoredPlayerName[i] = PLAYER_COLOR[i] .. GetPlayerName(Player(i-1)) .. "|r"
HeroPreselectionDisabledForPlayer[i] = not PRE_SELECT_BEFORE_ENABLED
HeroSelectionDisabledForPlayer[i] = true
preselectedHeroIndex[i] = 0
pickedHeroIndex[i] = 0
if GetPlayerController(whichPlayer) == MAP_CONTROL_USER then
PlayerIsHuman[i] = true
PlayerIsHuman[whichPlayer] = true
NumSelectingHumans = NumSelectingHumans + 1
TriggerRegisterPlayerEvent(leaveTrigger, whichPlayer, EVENT_PLAYER_LEAVE)
else
PlayerIsHuman[i] = false
PlayerIsHuman[whichPlayer] = false
end
ColoredPlayerName[whichPlayer] = ColoredPlayerName[i]
HeroPreselectionDisabledForPlayer[whichPlayer] = HeroPreselectionDisabledForPlayer[i]
HeroSelectionDisabledForPlayer[whichPlayer] = HeroSelectionDisabledForPlayer[i]
heroIndexWasBanned[i] = {}
end
else
if GetPlayerSlotState(Player(i-1)) == PLAYER_SLOT_STATE_PLAYING and GetPlayerController(Player(i-1)) == MAP_CONTROL_USER then
local whichPlayer = Player(i-1)
table.insert(SelectingPlayers, whichPlayer)
NumSelectingPlayers = NumSelectingPlayers + 1
PlayerNumberOfSlot[NumSelectingPlayers] = i
ColoredPlayerName[i] = PLAYER_COLOR[i] .. GetPlayerName(Player(i-1)) .. "|r"
HeroPreselectionDisabledForPlayer[i] = not PRE_SELECT_BEFORE_ENABLED
HeroSelectionDisabledForPlayer[i] = true
preselectedHeroIndex[i] = 0
pickedHeroIndex[i] = 0
PlayerIsHuman[i] = true
NumSelectingHumans = NumSelectingHumans + 1
TriggerRegisterPlayerEvent(leaveTrigger, whichPlayer, EVENT_PLAYER_LEAVE)
ColoredPlayerName[whichPlayer] = ColoredPlayerName[i]
HeroPreselectionDisabledForPlayer[whichPlayer] = HeroPreselectionDisabledForPlayer[i]
HeroSelectionDisabledForPlayer[whichPlayer] = HeroSelectionDisabledForPlayer[i]
PlayerIsHuman[whichPlayer] = true
heroIndexWasBanned[i] = {}
end
end
end
NUMBER_OF_TEAMS = 0
for i = 1, NumSelectingPlayers do
NUMBER_OF_TEAMS = math.max(NUMBER_OF_TEAMS, TEAM_OF_PLAYER[PlayerNumberOfSlot[i]])
end
--Foreground hero locations are asynchronous. Background hero locations are not.
if type(FOREGROUND_HERO_X) == "table" and type(FOREGROUND_HERO_Y) == "table" and type(HERO_SELECTION_ANGLE) == "table" and #FOREGROUND_HERO_X == #FOREGROUND_HERO_Y and #FOREGROUND_HERO_X == #HERO_SELECTION_ANGLE then
localForegroundHeroX = FOREGROUND_HERO_X[TEAM_OF_PLAYER[localPlayerId]]
localForegroundHeroY = FOREGROUND_HERO_Y[TEAM_OF_PLAYER[localPlayerId]]
localHeroSelectionAngle = HERO_SELECTION_ANGLE[TEAM_OF_PLAYER[localPlayerId]]
for i = 1, NUMBER_OF_TEAMS do
cameraTargetPositionX[i] = FOREGROUND_HERO_X[i] + math.cos(Deg2Rad(HERO_SELECTION_ANGLE[i]))*CAMERA_TARGET_OFFSET + math.sin(Deg2Rad(HERO_SELECTION_ANGLE[i]))*CAMERA_PERPENDICULAR_OFFSET
cameraTargetPositionY[i] = FOREGROUND_HERO_Y[i] + math.sin(Deg2Rad(HERO_SELECTION_ANGLE[i]))*CAMERA_TARGET_OFFSET - math.cos(Deg2Rad(HERO_SELECTION_ANGLE[i]))*CAMERA_PERPENDICULAR_OFFSET
end
elseif type(FOREGROUND_HERO_X) == "number" and type(FOREGROUND_HERO_Y) == "number" and type(HERO_SELECTION_ANGLE) == "number" then
localForegroundHeroX = FOREGROUND_HERO_X
---@diagnostic disable-next-line: cast-local-type
localForegroundHeroY = FOREGROUND_HERO_Y
---@diagnostic disable-next-line: cast-local-type
localHeroSelectionAngle = HERO_SELECTION_ANGLE
for i = 1, NUMBER_OF_TEAMS do
---@diagnostic disable-next-line: param-type-mismatch
cameraTargetPositionX[i] = FOREGROUND_HERO_X + math.cos(Deg2Rad(HERO_SELECTION_ANGLE))*CAMERA_TARGET_OFFSET + math.sin(Deg2Rad(HERO_SELECTION_ANGLE))*CAMERA_PERPENDICULAR_OFFSET
---@diagnostic disable-next-line: param-type-mismatch
cameraTargetPositionY[i] = FOREGROUND_HERO_Y + math.sin(Deg2Rad(HERO_SELECTION_ANGLE))*CAMERA_TARGET_OFFSET - math.cos(Deg2Rad(HERO_SELECTION_ANGLE))*CAMERA_PERPENDICULAR_OFFSET
end
else
print("|cffff0000Warning:|r Hero selection coordinates table size mismatch...")
return
end
TriggerAddAction(leaveTrigger, OnPlayerLeave)
HeroDeclaration()
NUMBER_OF_HEROES = numHeroes
RANDOM_HERO = NUMBER_OF_HEROES + 1
SUGGEST_RANDOM = NUMBER_OF_HEROES + 2
PAGE_DOWN = NUMBER_OF_HEROES + 3
PAGE_UP = NUMBER_OF_HEROES + 4
for i = 1, NUMBER_OF_HEROES do
j = 0
repeat
j = j + 1
until HeroList[i].abilities[j] == nil
NUMBER_OF_ABILITY_FRAMES = math.max(NUMBER_OF_ABILITY_FRAMES, j - 1)
if HeroList[i].category > NUMBER_OF_CATEGORIES then
NUMBER_OF_CATEGORIES = HeroList[i].category
end
HeroList[i]:GetValues()
end
for i = 1, NUMBER_OF_CATEGORIES do
NUMBER_OF_PAGES = math.max(NUMBER_OF_PAGES, PAGE_OF_CATEGORY[i])
end
--Camera
heroSelectionCamera = CreateCameraSetup()
CameraSetupSetField(heroSelectionCamera , CAMERA_FIELD_ZOFFSET , CAMERA_Z_OFFSET , 0.0)
CameraSetupSetField(heroSelectionCamera , CAMERA_FIELD_ANGLE_OF_ATTACK , 360 - CAMERA_PITCH , 0.0)
CameraSetupSetField(heroSelectionCamera , CAMERA_FIELD_TARGET_DISTANCE , CAMERA_DISTANCE , 0.0)
CameraSetupSetField(heroSelectionCamera , CAMERA_FIELD_ROLL , 0.0 , 0.0)
CameraSetupSetField(heroSelectionCamera , CAMERA_FIELD_FIELD_OF_VIEW , CAMERA_FIELD_OF_VIEW , 0.0)
CameraSetupSetField(heroSelectionCamera , CAMERA_FIELD_FARZ , 7500.0 , 0.0)
---@diagnostic disable-next-line: param-type-mismatch
CameraSetupSetField(heroSelectionCamera , CAMERA_FIELD_ROTATION , localHeroSelectionAngle , 0.0)
CameraSetupSetDestPosition(heroSelectionCamera , cameraTargetPositionX[TEAM_OF_PLAYER[localPlayerId]] , cameraTargetPositionY[TEAM_OF_PLAYER[localPlayerId]] , 0.0)
--Background Heroes
for i = 1, NumSelectingPlayers do
P = PlayerNumberOfSlot[i]
teamSize[TEAM_OF_PLAYER[P]] = (teamSize[TEAM_OF_PLAYER[P]] or 0) + 1
slotInTeam[P] = teamSize[TEAM_OF_PLAYER[P]]
end
for i = 1, NumSelectingPlayers do
P = PlayerNumberOfSlot[i]
local x
local y
x, y = GetPlayerBackgroundHeroLocation(P, i, NumSelectingPlayers, TEAM_OF_PLAYER[P], slotInTeam[P], teamSize[TEAM_OF_PLAYER[P]])
if type(FOREGROUND_HERO_X) == "table" then
cameraEyePositionX = cameraTargetPositionX[TEAM_OF_PLAYER[P]] - math.cos(Deg2Rad(CAMERA_PITCH))*math.cos(Deg2Rad(HERO_SELECTION_ANGLE[TEAM_OF_PLAYER[P]]))*CAMERA_DISTANCE
cameraEyePositionY = cameraTargetPositionY[TEAM_OF_PLAYER[P]] - math.cos(Deg2Rad(CAMERA_PITCH))*math.sin(Deg2Rad(HERO_SELECTION_ANGLE[TEAM_OF_PLAYER[P]]))*CAMERA_DISTANCE
BACKGROUND_HERO_X[P] = cameraEyePositionX - math.sin(Deg2Rad(HERO_SELECTION_ANGLE[TEAM_OF_PLAYER[P]]))*x + math.cos(Deg2Rad(HERO_SELECTION_ANGLE[TEAM_OF_PLAYER[P]]))*y
BACKGROUND_HERO_Y[P] = cameraEyePositionY + math.cos(Deg2Rad(HERO_SELECTION_ANGLE[TEAM_OF_PLAYER[P]]))*x + math.sin(Deg2Rad(HERO_SELECTION_ANGLE[TEAM_OF_PLAYER[P]]))*y
else
---@diagnostic disable-next-line: param-type-mismatch
cameraEyePositionX = cameraTargetPositionX[TEAM_OF_PLAYER[P]] - math.cos(Deg2Rad(CAMERA_PITCH))*math.cos(Deg2Rad(HERO_SELECTION_ANGLE))*CAMERA_DISTANCE
---@diagnostic disable-next-line: param-type-mismatch
cameraEyePositionY = cameraTargetPositionY[TEAM_OF_PLAYER[P]] - math.cos(Deg2Rad(CAMERA_PITCH))*math.sin(Deg2Rad(HERO_SELECTION_ANGLE))*CAMERA_DISTANCE
---@diagnostic disable-next-line: param-type-mismatch
BACKGROUND_HERO_X[P] = cameraEyePositionX - math.sin(Deg2Rad(HERO_SELECTION_ANGLE))*x + math.cos(Deg2Rad(HERO_SELECTION_ANGLE))*y
---@diagnostic disable-next-line: param-type-mismatch
BACKGROUND_HERO_Y[P] = cameraEyePositionY + math.cos(Deg2Rad(HERO_SELECTION_ANGLE))*x + math.sin(Deg2Rad(HERO_SELECTION_ANGLE))*y
end
BACKGROUND_HERO_X[Player(P-1)] = BACKGROUND_HERO_X[P]
BACKGROUND_HERO_Y[Player(P-1)] = BACKGROUND_HERO_Y[P]
end
--Caption
captionFrame = BlzCreateFrameByType("TEXT", "captionFrame", BlzGetOriginFrame(ORIGIN_FRAME_WORLD_FRAME, 0), "", 0)
BlzFrameSetVisible(captionFrame , false)
BlzFrameSetEnable(captionFrame , false)
BlzFrameSetSize(captionFrame, 0.4, 0.05)
BlzFrameSetAbsPoint(captionFrame , FRAMEPOINT_CENTER , CAPTION_X , CAPTION_Y)
BlzFrameSetText(captionFrame, CAPTION_COLOR_1 .. HERO_SELECTION_CAPTION .. "|r")
BlzFrameSetTextAlignment(captionFrame , TEXT_JUSTIFY_MIDDLE , TEXT_JUSTIFY_CENTER)
BlzFrameSetScale(captionFrame, CAPTION_FONT_SIZE/10.)
BlzFrameSetAlpha(captionFrame , CAPTION_ALPHA_1)
--Timer
timerFrame = BlzCreateFrameByType("TEXT", "timerFrame", BlzGetOriginFrame(ORIGIN_FRAME_WORLD_FRAME, 0), "", 0)
BlzFrameSetVisible(timerFrame , false)
BlzFrameSetEnable(timerFrame , false)
BlzFrameSetSize(timerFrame, 0.15, 0.05)
BlzFrameSetAbsPoint(timerFrame , FRAMEPOINT_CENTER , TIMER_X , TIMER_Y)
BlzFrameSetTextAlignment(timerFrame , TEXT_JUSTIFY_MIDDLE , TEXT_JUSTIFY_CENTER)
BlzFrameSetScale(timerFrame, TIMER_FONT_SIZE/10.)
InitMenu()
end
end
if Debug then Debug.endFile() end
do LIBRARY_TestHeroSelection = true
local SCOPE_PREFIX = "TestHeroSelection_" ---@type string requires HeroSelection
local loopInt = 0 ---@type integer
HERO_SPAWN_X = -3000 ---@type number
HERO_SPAWN_Y = 1300 ---@type number
PlayerHero ={} ---@type unit[]
CenterWindow = nil ---@type NeatWindow
local function MusicFadeOutLoop()
loopInt = loopInt + 1
if loopInt < 100 then
SetMusicVolumeBJ(100 - loopInt)
else
DestroyTimer(GetExpiredTimer())
ClearMapMusic()
StopMusic(false)
SetMusicVolumeBJ(100)
end
end
function MusicSlowFadeOut()
TimerStart(CreateTimer(), 0.05, true , MusicFadeOutLoop)
loopInt = 0
end
function BeginGame()
PlayMusic("Sound\\Music\\mp3Music\\NightElf3.mp3")
DisplayTextToForce(GetPlayersAll(), "Let the battle begin!")
end
local function PickBots()
loopInt = loopInt + 1
if loopInt < 6 then
HeroSelectionPlayerForceRandom(Player(loopInt))
else
DestroyTimer(GetExpiredTimer())
end
end
local function Enable()
EnableHeroSelection()
TimerStart(GetExpiredTimer(), 2.0, true , PickBots)
end
local function Start()
InitHeroSelection()
BeginHeroSelection()
TimerStart(GetExpiredTimer(), 7.0, false , Enable)
end
local function Vendor()
if GetItemTypeId(GetManipulatedItem()) == FourCC('stwp') then
PlayerReturnToHeroSelection(GetOwningPlayer(GetTriggerUnit()))
RemoveUnit(GetTriggerUnit())
else
if NoOneInHeroSelection() then
for i = 1, 6 do
RemoveUnit(PlayerHero[i])
end
RestartHeroSelection()
TimerStart(CreateTimer(), 7.0, false , Enable)
else
BJDebugMsg("Someone is in hero selection right now...")
end
end
end
local function Init()
local trig = CreateTrigger() ---@type trigger
CenterWindow = NeatWindow.create(TEXT_MESSAGE_X_POSITION, 0.22, TEXT_MESSAGE_BLOCK_WIDTH, 0.2, 1, false)
TimerStart( CreateTimer(), 0.01, false , Start )
FogEnableOff()
FogMaskEnableOff()
SetTimeOfDay(0)
SetTimeOfDayScale(0)
SetSkyModel("Starsphere.mdx")
SetDoodadAnimationRect(GetPlayableMapRect(), FourCC('D00O'), "stand lumber", true)
TriggerAddAction(trig, Vendor)
TriggerRegisterAnyUnitEventBJ(trig, EVENT_PLAYER_UNIT_PICKUP_ITEM)
end
OnInit.final(Init)
end