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
--[[
===========================================================================================
Click-And-Hold Movement
by Antares
Enable click-and-hold movement (hold the right mouse-button to move your hero) for hero maps
like aeon of strife, hero arenas, or rpgs.
===========================================================================================
]]
--====================================================
--Config
--====================================================
--Disables the click-and-hold trigger for this many seconds when a unit in the selected group casts a spell. This enables the user to cast a spell without releasing the right mouse-button.
local IGNORE_MOUSE_SPELL_DURATION = 0.5 ---@constant number
--Disables the click-and-hold trigger for this many seconds right after the right mouse-button is pressed. This prevents the trigger from firing on a simple right-click.
local IGNORE_MOUSE_CLICK_DURATION = 0.25 ---@constant number
--Duration of the cooldown on the click-and-hold trigger on giving orders.
local MOUSE_MOVE_COOLDOWN_DURATION = 0.1 ---@constant number
local DISABLE_IF_MULTIPLE_SELECTED = false ---@constant boolean
--The commanded units will stop moving as soon as the right mouse-button is released. They will also ignore pathing blockers between the mouse-cursor and the unit.
local STOP_MOVING_ON_RELEASE = false ---@constant boolean
--Enable if you want to write your own callback function for the click-and-hold trigger. Otherwise, it will be a replaced by a simple smart command.
local CUSTOM_CALLBACK = false ---@constant boolean
--====================================================
local mouseMoveTrigger = {} ---@type trigger[]
local mouseX = {} ---@type number[]
local mouseY = {} ---@type number[]
local rightClickOn = {} ---@type boolean[]
local ignoreMouseTimer = {} ---@type timer []
local data = {}
local selectedUnits = {} ---@type group[]
local setGroup = {} ---@type boolean[]
local noOrders = {} ---@type boolean[]
local fixedCameraLock ---@type boolean
local deltaX = {} ---@type number[]
local deltaY = {} ---@type number[]
--====================================================
---@param whichUnit unit
---@param x number
---@param y number
local function CustomCallback(whichUnit, x, y)
--Define your custom callback code here.
end
--====================================================
---@param P player
---@return boolean
local function SetAndCheckSelection(P)
local invalidSelection ---@type boolean
local first ---@type unit
selectedUnits[P] = CreateGroup()
GroupEnumUnitsSelected(selectedUnits[P] , P , nil)
if DISABLE_IF_MULTIPLE_SELECTED then
invalidSelection = BlzGroupGetSize(selectedUnits[P]) ~= 1
else
invalidSelection = BlzGroupGetSize(selectedUnits[P]) == 0
end
setGroup[P] = false
if invalidSelection then
DestroyGroup(selectedUnits[P])
return false
else
first = FirstOfGroup(selectedUnits[P])
if GetOwningPlayer(first) == P or (IsUnitAlly(first,P) and GetPlayerAlliance(GetOwningPlayer(first),P,ALLIANCE_SHARED_CONTROL)) then
return true
else
DestroyGroup(selectedUnits[P])
return false
end
end
end
---@param P player
local function CommandGroup(P)
local i = BlzGroupGetSize(selectedUnits[P]) - 1 ---@type integer
local u ---@type unit
local angle ---@type number
while i >= 0 do
if CUSTOM_CALLBACK then
CustomCallback( BlzGroupUnitAt(selectedUnits[P] , i) , mouseX[P] , mouseY[P] )
elseif STOP_MOVING_ON_RELEASE then
u = BlzGroupUnitAt(selectedUnits[P] , i)
angle = Atan2( mouseY[P] - GetUnitY(u) , mouseX[P] - GetUnitX(u) )
IssuePointOrderById( BlzGroupUnitAt(selectedUnits[P] , i) , 851971 , GetUnitX(u) + 50*Cos(angle) , GetUnitY(u) + 50*Sin(angle) )
else
IssuePointOrderById( BlzGroupUnitAt(selectedUnits[P] , i) , 851971 , mouseX[P] , mouseY[P] )
end
i = i - 1
end
end
local function Enable()
local P = data[GetExpiredTimer()] ---@type player
noOrders[P] = false
if rightClickOn[P] then
if setGroup[P] then
if SetAndCheckSelection(P) then
CommandGroup(P)
end
else
CommandGroup(P)
end
end
end
local function MouseSpellIgnore()
local whichUnit = GetSpellAbilityUnit() ---@type unit
local P = GetOwningPlayer(whichUnit) ---@type player
if IsUnitInGroup(whichUnit , selectedUnits[P]) then
noOrders[P] = true
TimerStart( ignoreMouseTimer[P] , IGNORE_MOUSE_SPELL_DURATION , false , Enable )
end
end
local function MouseMove()
local P = GetTriggerPlayer() ---@type player
local mX = BlzGetTriggerPlayerMouseX() ---@type number
local mY = BlzGetTriggerPlayerMouseY() ---@type number
if mY ~= 0 or mX ~= 0 then
mouseX[P] = mX
mouseY[P] = mY
local anchor = FCL_Anchor[GetPlayerId(P)]
if anchor then
deltaX[P] = mX - GetUnitX(anchor)
deltaY[P] = mY - GetUnitY(anchor)
end
end
if noOrders[P] then
return
end
if MOUSE_MOVE_COOLDOWN_DURATION > 0 then
noOrders[P] = true
TimerStart( ignoreMouseTimer[P] , MOUSE_MOVE_COOLDOWN_DURATION , false , Enable )
end
CommandGroup(P)
end
local function MouseRightClick()
local P ---@type player
if BlzGetTriggerPlayerMouseButton() == MOUSE_BUTTON_TYPE_RIGHT then
P = GetTriggerPlayer()
rightClickOn[P] = true
noOrders[P] = true
setGroup[P] = true
mouseX[P] = BlzGetTriggerPlayerMouseX()
mouseY[P] = BlzGetTriggerPlayerMouseY()
TimerStart( ignoreMouseTimer[P] , IGNORE_MOUSE_CLICK_DURATION , false , Enable )
EnableTrigger(mouseMoveTrigger[P])
end
end
local function MouseRightRelease()
local P ---@type player
if BlzGetTriggerPlayerMouseButton() == MOUSE_BUTTON_TYPE_RIGHT then
P = GetTriggerPlayer()
DestroyGroup(selectedUnits[P])
rightClickOn[P] = false
DisableTrigger(mouseMoveTrigger[P])
end
end
local function UpdateMousePosition()
local P
for i = 0, 23 do
if FCL_Anchor[i] then
P = Player(i)
if rightClickOn[P] then
mouseX[P] = GetUnitX(FCL_Anchor[i]) + deltaX[P]
mouseY[P] = GetUnitY(FCL_Anchor[i]) + deltaY[P]
CommandGroup(P)
end
end
end
end
local function At0s()
local P ---@type player
local trigDown = CreateTrigger() ---@type trigger
local trigUp = CreateTrigger() ---@type trigger
local trigIgnore = CreateTrigger() ---@type trigger
for p = 0, 23 do
P = Player(p)
if GetPlayerSlotState(P) == PLAYER_SLOT_STATE_PLAYING and GetPlayerController(P) == MAP_CONTROL_USER then
mouseMoveTrigger[P] = CreateTrigger()
TriggerRegisterPlayerEvent( trigDown, P, EVENT_PLAYER_MOUSE_DOWN )
TriggerRegisterPlayerEvent( trigUp, P, EVENT_PLAYER_MOUSE_UP )
TriggerRegisterPlayerEvent( mouseMoveTrigger[P], P, EVENT_PLAYER_MOUSE_MOVE )
TriggerAddAction( mouseMoveTrigger[P] , MouseMove )
DisableTrigger(mouseMoveTrigger[P])
ignoreMouseTimer[P] = CreateTimer()
data[ignoreMouseTimer[P]] = P
end
p = p + 1
end
DestroyTrigger(GetTriggeringTrigger())
TriggerAddAction( trigDown , MouseRightClick )
TriggerAddAction( trigUp , MouseRightRelease )
TriggerRegisterAnyUnitEventBJ( trigIgnore, EVENT_PLAYER_UNIT_SPELL_CAST )
TriggerAddAction( trigIgnore , MouseSpellIgnore )
end
OnInit.final("ClickAndHoldMovement", function()
local at0sTrigger = CreateTrigger() ---@type trigger
TriggerRegisterTimerEvent( at0sTrigger , 0.0 , false )
TriggerAddAction( at0sTrigger , At0s )
fixedCameraLock = Require.optionally "FixedCameraLock"
if fixedCameraLock then
TimerStart(CreateTimer(), MOUSE_MOVE_COOLDOWN_DURATION, true, UpdateMousePosition)
end
end)
end
if Debug then Debug.beginFile "FixedCameraLock" end
do
--[[
=============================================================================================================================================================
Fixed Camera Lock
by Antares
Locks the camera to a unit while dynamically adjusting the z-offset to disable awfulness.
Requires:
TotalInitialization https://www.hiveworkshop.com/threads/total-initialization.317099/
PrecomputedHeightMap (optional) https://www.hiveworkshop.com/threads/precomputed-synchronized-height-map.353477/
=============================================================================================================================================================
A P I
=============================================================================================================================================================
FCL_Lock(whichUnit, whichPlayer) Locks the camera of the specified player to the specified unit or the camera of the owning player of that unit,
if player is not specified.
FCL_Release(whichPlayer) Releases the camera of the specified player or for all players if no player is specified.
=============================================================================================================================================================
C O N F I G
=============================================================================================================================================================
]]
local ADJUSTMENT_INTERVAL = 0.02 ---@constant number
--The adjustment strength when the camera target is below the unit. This type of camera shift is always awful and I recommend a value of 1.
local ADJUSTMENT_STRENGTH_UP = 1.0 ---@constant number
--The adjustment strength when the camera target is above the unit. This camera shift is sometimes actually useful, so you can disable it if you want to.
local ADJUSTMENT_STRENGTH_DOWN = 1.0 ---@constant number
--===========================================================================================================================================================
FCL_Anchor = {} ---@type table<player,unit>
local MASTER_TIMER ---@type timer
local lockEnabled = false ---@type boolean
local moveableLoc ---@type location
local GetLocZ ---@type function
local localPlayer ---@type player
local relockTrigger = {} ---@type table<player,trigger>
---@param whichUnit unit
---@param whichPlayer? player
function FCL_Lock(whichUnit, whichPlayer)
whichPlayer = whichPlayer or GetOwningPlayer(whichUnit)
EnableTrigger(relockTrigger[whichPlayer])
if GetLocalPlayer() == whichPlayer then
SetCameraTargetController(whichUnit, 0, 0, false)
lockEnabled = true
end
FCL_Anchor[whichPlayer] = whichUnit
end
---@param whichPlayer? player
function FCL_Release(whichPlayer)
DisableTrigger(relockTrigger[whichPlayer])
if whichPlayer == nil or GetLocalPlayer() == whichPlayer then
ResetToGameCamera(0)
lockEnabled = false
end
if whichPlayer then
FCL_Anchor[whichPlayer] = nil
else
for i = 0, bj_MAX_PLAYER_SLOTS - 1 do
FCL_Anchor[Player(i)] = nil
end
end
end
local function AdjustCameraHeight()
if lockEnabled then
local dz = GetLocZ(GetUnitX(FCL_Anchor[localPlayer]), GetUnitY(FCL_Anchor[localPlayer])) + GetUnitFlyHeight(FCL_Anchor[localPlayer]) - (GetCameraTargetPositionZ() - GetCameraField(CAMERA_FIELD_ZOFFSET))
if dz > 0 then
SetCameraField(CAMERA_FIELD_ZOFFSET, ADJUSTMENT_STRENGTH_UP*dz, ADJUSTMENT_INTERVAL)
else
SetCameraField(CAMERA_FIELD_ZOFFSET, ADJUSTMENT_STRENGTH_DOWN*dz, ADJUSTMENT_INTERVAL)
end
end
end
local function RelockCamera()
if localPlayer == GetTriggerPlayer() then
SetCameraTargetController(FCL_Anchor[GetTriggerPlayer()], 0, 0, false)
end
end
OnInit.final("FixedCameraLock", function()
local precomputedHeightMap = Require.optionally "PrecomputedHeightMap"
if precomputedHeightMap then
GetLocZ = _G.GetLocZ
else
moveableLoc = Location(0, 0)
GetLocZ = function(x, y)
MoveLocation(moveableLoc, x, y)
return GetLocationZ(moveableLoc)
end
end
localPlayer = GetLocalPlayer()
MASTER_TIMER = CreateTimer()
TimerStart(MASTER_TIMER, ADJUSTMENT_INTERVAL, true, AdjustCameraHeight)
local player
for i = 0, 23 do
player = Player(i)
if GetPlayerSlotState(player) == PLAYER_SLOT_STATE_PLAYING and GetPlayerController(player) == MAP_CONTROL_USER then
relockTrigger[player] = CreateTrigger()
BlzTriggerRegisterPlayerKeyEvent(relockTrigger[player], player, OSKEY_C, 4, true)
TriggerAddAction(relockTrigger[player], RelockCamera)
DisableTrigger(relockTrigger[player])
end
end
end)
end
if Debug then Debug.endFile() end
do
local enabled = true
OnInit.final(function()
Require "FixedCameraLock"
local Tyrande = CreateUnit(Player(0), FourCC "Etyr", -1380, -4100, 0)
FCL_Lock(Tyrande)
print("|cff00ff00Fixed Camera Lock On|r")
TimerStart(CreateTimer(), 5.0, true, function()
if enabled then
enabled = false
FCL_Release(Player(0))
SetCameraTargetController(Tyrande, 0, 0, false)
print("|cffff0000Fixed Camera Lock Off|r")
else
enabled = true
FCL_Lock(Tyrande)
print("|cff00ff00Fixed Camera Lock On|r")
end
end)
local trig = CreateTrigger()
BlzTriggerRegisterPlayerKeyEvent(trig, Player(0), OSKEY_C, 4, true)
TriggerAddAction(trig, function()
FCL_Lock(Tyrande)
end)
end)
end