Files
Dota-Zombie-Invasion/scripts/vscripts/wavemanager.lua
T
achmad 6d95836d11 feat: replace all user-facing Russian strings with English
Empty Russian and Chinese locale files so English is used regardless
of client language. Translate all CustomGameEventManager error messages,
SendCustomMessage calls, and deck/card UI strings to English.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 04:24:20 +07:00

1778 lines
77 KiB
Lua

local ____lualib = require("lualib_bundle")
local __TS__Class = ____lualib.__TS__Class
local Map = ____lualib.Map
local __TS__New = ____lualib.__TS__New
local __TS__Iterator = ____lualib.__TS__Iterator
local __TS__ArraySplice = ____lualib.__TS__ArraySplice
local __TS__StringIncludes = ____lualib.__TS__StringIncludes
local __TS__Number = ____lualib.__TS__Number
local __TS__NumberIsNaN = ____lualib.__TS__NumberIsNaN
local __TS__ParseInt = ____lualib.__TS__ParseInt
local Set = ____lualib.Set
local __TS__ArrayFilter = ____lualib.__TS__ArrayFilter
local __TS__ObjectAssign = ____lualib.__TS__ObjectAssign
local __TS__ArraySome = ____lualib.__TS__ArraySome
local __TS__ArraySetLength = ____lualib.__TS__ArraySetLength
local ____exports = {}
local ____modifier_boss_hud_health_bar = require("abilities.modifiers.modifier_boss_hud_health_bar")
local applyBossHudHealthBar = ____modifier_boss_hud_health_bar.applyBossHudHealthBar
local isBossHudHealthBarUnit = ____modifier_boss_hud_health_bar.isBossHudHealthBarUnit
local ____difficulty_manager = require("difficulty_manager")
local Difficulty = ____difficulty_manager.Difficulty
local ____utils = require("utils.utils")
local SetGoldUsually = ____utils.SetGoldUsually
local ____player_connection_state = require("utils.player_connection_state")
local DOTA_CONNECTION_STATE = ____player_connection_state.DOTA_CONNECTION_STATE
local ____SpawnManager = require("SpawnManager")
local SpawnManager = ____SpawnManager.SpawnManager
local function rawPrintFn(____, ...)
_G:print(...)
end
local ENABLE_VERBOSE_WAVE_MANAGER_LOGS = false
local ____print = ENABLE_VERBOSE_WAVE_MANAGER_LOGS and rawPrintFn or (function(____, ...) return nil end)
local GOLD_REWARD_RADIUS = 1500
--- Кэш списка героев для AI волны — не вызывать FindUnitsInRadius с каждого крипа каждую секунду.
local WAVE_HERO_TARGET_CACHE_TTL = 0.22
local WAVE_CREEP_HORDE_THRESHOLD = 60
local WAVE_CREEP_MEGA_HORDE_THRESHOLD = 100
--- Шанс 1..100 на каждую итерацию: пока выпадает успех и в пуле есть неприкреплённые способности —
-- зомби может получить сразу несколько разных бонус-абилок подряд (нейтрал, без Luck).
local WAVE_MOB_RANDOM_BONUS_ABILITY_CHANCE = 7
--- Пул способностей для случайной выдачи волновым мобам (уже используются на npc_wave_* в KV).
-- Не включаем zombie_virus / ability_unit_less_laggy — они обычно уже есть в юните.
local WAVE_MOB_BONUS_ABILITY_POOL = {
"toxin",
"wave_full_brutality",
"ghost_evasive",
"wave_phasing_march",
"zombie_armor_decress"
}
local WAVE_SETTINGS = {
[1] = {waves = {{maxZombies = 25, spawnInterval = {min = 0.5, max = 1}, zombieTypes = {{unitName = "npc_wave_zombie", goldPerKill = 2, maxCount = 8, chance = 70}, {unitName = "npc_wave_half_zombie", goldPerKill = 4, maxCount = 6, chance = 30}}, onEndSpawns = {{unitName = "npc_wave_bearst_zombie", count = 3, goldPerKill = 2}}}, {maxZombies = 25, spawnInterval = {min = 0.6, max = 1}, zombieTypes = {{unitName = "npc_wave_zombie", goldPerKill = 2, maxCount = 8, chance = 60}, {unitName = "npc_wave_half_zombie", goldPerKill = 2, maxCount = 6, chance = 35}, {unitName = "npc_wave_toxin_zombie", goldPerKill = 4, maxCount = 3, chance = 5}}, SpawnBossOnEnd = true}, {maxZombies = 25, spawnInterval = {min = 0.5, max = 1.2}, zombieTypes = {{unitName = "npc_wave_zombie", goldPerKill = 2, maxCount = 12, chance = 60}, {unitName = "npc_wave_half_zombie", goldPerKill = 2, maxCount = 6, chance = 20}, {unitName = "npc_wave_toxin_zombie", goldPerKill = 4, maxCount = 2, chance = 5}}}}},
[2] = {waves = {{maxZombies = 20, spawnInterval = {min = 0.5, max = 1.2}, zombieTypes = {
{unitName = "npc_wave_toxin_zombie", goldPerKill = 4, maxCount = 25, chance = 70},
{unitName = "npc_wave_bearst_zombie", goldPerKill = 4, maxCount = 15, chance = 30},
{unitName = "npc_wave_skeleton_zombie", goldPerKill = 3, maxCount = 25, chance = 70},
{unitName = "npc_wave_skeleton_zombie_half", goldPerKill = 3, maxCount = 15, chance = 30},
{unitName = "npc_wave_dead_skeleton_archer", goldPerKill = 3, maxCount = 5, chance = 20}
}}, {maxZombies = 25, spawnInterval = {min = 0.5, max = 1.2}, zombieTypes = {
{unitName = "npc_wave_toxin_zombie", goldPerKill = 4, maxCount = 25, chance = 5},
{unitName = "npc_wave_bearst_zombie", goldPerKill = 4, maxCount = 15, chance = 5},
{unitName = "npc_wave_skeleton_zombie", goldPerKill = 3, maxCount = 5, chance = 20},
{unitName = "npc_wave_dead_skeleton", goldPerKill = 3, maxCount = 25, chance = 10},
{unitName = "npc_wave_skeleton_zombie_half", goldPerKill = 2, maxCount = 15, chance = 20},
{unitName = "npc_wave_dead_skeleton_archer", goldPerKill = 2, maxCount = 5, chance = 20}
}, SpawnBossOnEnd = true}}},
[3] = {waves = {{maxZombies = 20, spawnInterval = {min = 0.5, max = 1.2}, zombieTypes = {{unitName = "npc_wave_dead_harpy", goldPerKill = 4, maxCount = 25, chance = 60}, {unitName = "npc_wave_toxin_2_zombie", goldPerKill = 6, maxCount = 3, chance = 30}, {unitName = "npc_wave_ghost_ranged", goldPerKill = 3, maxCount = 25, chance = 70}, {unitName = "npc_wave_bearst_zombie", goldPerKill = 4, maxCount = 15, chance = 30}}}, {maxZombies = 25, spawnInterval = {min = 0.5, max = 1.2}, zombieTypes = {{unitName = "npc_wave_dead_harpy", goldPerKill = 4, maxCount = 25, chance = 60}, {unitName = "npc_wave_toxin_2_zombie", goldPerKill = 4, maxCount = 15, chance = 30}, {unitName = "npc_wave_ghost", goldPerKill = 15, maxCount = 6, chance = 70}, {unitName = "npc_wave_ghost_ranged", goldPerKill = 5, maxCount = 25, chance = 70}}, SpawnBossOnEnd = true}}},
[4] = {waves = {{maxZombies = 20, spawnInterval = {min = 0.5, max = 1.2}, zombieTypes = {{unitName = "npc_wave_ghoul", goldPerKill = 7, maxCount = 25, chance = 70}, {unitName = "npc_wave_toxin_2_zombie", goldPerKill = 7, maxCount = 3, chance = 30}}}, {maxZombies = 25, spawnInterval = {min = 0.5, max = 1.2}, zombieTypes = {{unitName = "npc_wave_ghoul", goldPerKill = 7, maxCount = 25, chance = 70}, {unitName = "npc_wave_toxin_2_zombie", goldPerKill = 6, maxCount = 3, chance = 30}, {unitName = "npc_wave_skeleton_assassin", goldPerKill = 5, maxCount = 25, chance = 70}, {unitName = "npc_wave_skeleton_warrior", goldPerKill = 4, maxCount = 15, chance = 30}}, SpawnBossOnEnd = true}}},
[5] = {waves = {{maxZombies = 28, spawnInterval = {min = 0.45, max = 0.95}, zombieTypes = {
{unitName = "npc_wave_ghoul", goldPerKill = 5, maxCount = 30, chance = 30},
{unitName = "npc_wave_ghost", goldPerKill = 6, maxCount = 20, chance = 20},
{unitName = "npc_wave_dead_harpy", goldPerKill = 5, maxCount = 14, chance = 15},
{unitName = "npc_wave_skeleton_assassin", goldPerKill = 5, maxCount = 18, chance = 20},
{unitName = "npc_wave_toxin_2_zombie", goldPerKill = 5, maxCount = 10, chance = 15}
}}, {maxZombies = 32, spawnInterval = {min = 0.4, max = 0.85}, zombieTypes = {{unitName = "npc_wave_skeleton_warrior", goldPerKill = 5, maxCount = 22, chance = 25}, {unitName = "npc_wave_ghost_ranged", goldPerKill = 6, maxCount = 22, chance = 20}, {unitName = "npc_wave_toxin_2_zombie", goldPerKill = 5, maxCount = 12, chance = 15}, {unitName = "npc_wave_dead_skeleton_archer", goldPerKill = 5, maxCount = 10, chance = 20}}}, {maxZombies = 36, spawnInterval = {min = 0.35, max = 0.75}, zombieTypes = {
{unitName = "npc_wave_ghoul", goldPerKill = 6, maxCount = 34, chance = 20},
{unitName = "npc_wave_ghost", goldPerKill = 7, maxCount = 24, chance = 20},
{unitName = "npc_wave_skeleton_assassin", goldPerKill = 7, maxCount = 22, chance = 20},
{unitName = "npc_wave_dead_harpy", goldPerKill = 8, maxCount = 16, chance = 20},
{unitName = "npc_wave_dead_skeleton_archer", goldPerKill = 6, maxCount = 12, chance = 10}
}, SpawnBossOnEnd = true}}},
[6] = {waves = {{maxZombies = 38, spawnInterval = {min = 0.32, max = 0.7}, zombieTypes = {
{unitName = "npc_wave_toxin_2_zombie", goldPerKill = 6, maxCount = 18, chance = 20},
{unitName = "npc_wave_ghost", goldPerKill = 7, maxCount = 24, chance = 20},
{unitName = "npc_wave_ghoul", goldPerKill = 7, maxCount = 30, chance = 20},
{unitName = "npc_wave_skeleton_warrior", goldPerKill = 8, maxCount = 22, chance = 20},
{unitName = "npc_wave_dead_harpy", goldPerKill = 8, maxCount = 16, chance = 20}
}}, {maxZombies = 42, spawnInterval = {min = 0.28, max = 0.64}, zombieTypes = {{unitName = "npc_wave_skeleton_assassin", goldPerKill = 7, maxCount = 24, chance = 20}, {unitName = "npc_wave_dead_skeleton_archer", goldPerKill = 6, maxCount = 16, chance = 18}, {unitName = "npc_wave_ghost_ranged", goldPerKill = 8, maxCount = 24, chance = 18}, {unitName = "npc_wave_toxin_2_zombie", goldPerKill = 5, maxCount = 16, chance = 14}}}, {maxZombies = 46, spawnInterval = {min = 0.24, max = 0.55}, zombieTypes = {
{unitName = "npc_wave_ghoul", goldPerKill = 5, maxCount = 36, chance = 15},
{unitName = "npc_wave_ghost", goldPerKill = 6, maxCount = 30, chance = 15},
{unitName = "npc_wave_skeleton_warrior", goldPerKill = 7, maxCount = 26, chance = 15},
{unitName = "npc_wave_skeleton_assassin", goldPerKill = 8, maxCount = 24, chance = 15},
{unitName = "npc_wave_dead_harpy", goldPerKill = 6, maxCount = 18, chance = 15},
{unitName = "npc_wave_dead_skeleton_archer", goldPerKill = 5, maxCount = 14, chance = 15}
}, SpawnBossOnEnd = true}}},
[7] = {waves = {
{maxZombies = 40, spawnInterval = {min = 0.3, max = 0.6}, zombieTypes = {{unitName = "npc_wave_zombie", goldPerKill = 6, maxCount = 12, chance = 60}, {unitName = "npc_wave_half_zombie", goldPerKill = 7, maxCount = 6, chance = 20}, {unitName = "npc_wave_toxin_zombie", goldPerKill = 8, maxCount = 2, chance = 5}}, SpawnBossOnEnd = true},
{maxZombies = 40, spawnInterval = {min = 0.3, max = 0.6}, zombieTypes = {
{unitName = "npc_wave_toxin_zombie", goldPerKill = 9, maxCount = 25, chance = 5},
{unitName = "npc_wave_bearst_zombie", goldPerKill = 9, maxCount = 15, chance = 5},
{unitName = "npc_wave_skeleton_zombie", goldPerKill = 9, maxCount = 5, chance = 20},
{unitName = "npc_wave_dead_skeleton", goldPerKill = 9, maxCount = 25, chance = 10},
{unitName = "npc_wave_skeleton_zombie_half", goldPerKill = 9, maxCount = 15, chance = 20},
{unitName = "npc_wave_dead_skeleton_archer", goldPerKill = 9, maxCount = 5, chance = 20}
}, SpawnBossOnEnd = true},
{maxZombies = 40, spawnInterval = {min = 0.3, max = 0.6}, zombieTypes = {{unitName = "npc_wave_dead_harpy", goldPerKill = 9, maxCount = 25, chance = 60}, {unitName = "npc_wave_toxin_2_zombie", goldPerKill = 9, maxCount = 15, chance = 30}, {unitName = "npc_wave_ghost", goldPerKill = 9, maxCount = 25, chance = 70}, {unitName = "npc_wave_ghost_ranged", goldPerKill = 9, maxCount = 25, chance = 70}}, SpawnBossOnEnd = true},
{maxZombies = 40, spawnInterval = {min = 0.3, max = 0.6}, zombieTypes = {{unitName = "npc_wave_ghoul", goldPerKill = 9, maxCount = 25, chance = 70}, {unitName = "npc_wave_toxin_2_zombie", goldPerKill = 9, maxCount = 3, chance = 30}, {unitName = "npc_wave_skeleton_assassin", goldPerKill = 9, maxCount = 25, chance = 70}, {unitName = "npc_wave_skeleton_warrior", goldPerKill = 9, maxCount = 15, chance = 30}}, SpawnBossOnEnd = true},
{maxZombies = 40, spawnInterval = {min = 0.3, max = 0.6}, zombieTypes = {{unitName = "npc_wave_ghoul", goldPerKill = 9, maxCount = 25, chance = 70}, {unitName = "npc_wave_toxin_2_zombie", goldPerKill = 9, maxCount = 3, chance = 30}, {unitName = "npc_wave_skeleton_assassin", goldPerKill = 9, maxCount = 25, chance = 70}, {unitName = "npc_wave_skeleton_warrior", goldPerKill = 9, maxCount = 15, chance = 30}}, SpawnBossOnEnd = true},
{maxZombies = 40, spawnInterval = {min = 0.3, max = 0.6}, zombieTypes = {{unitName = "npc_wave_ghoul", goldPerKill = 9, maxCount = 25, chance = 70}, {unitName = "npc_wave_toxin_2_zombie", goldPerKill = 9, maxCount = 3, chance = 30}, {unitName = "npc_wave_skeleton_assassin", goldPerKill = 9, maxCount = 25, chance = 70}, {unitName = "npc_wave_skeleton_warrior", goldPerKill = 9, maxCount = 15, chance = 30}}, SpawnBossOnEnd = true},
{
maxZombies = 0,
spawnInterval = {min = 0, max = 0},
zombieTypes = {},
cutsceneOnEnd = "camera_start_ending",
stopWavesAfter = true
}
}}
}
local LOG_PREFIX = "[WaveManager]"
local RANDOM_WAVE_BOSSES = {
{unitName = "npc_wave_boss_zombie", displayName = "#npc_wave_boss_zombie"},
{unitName = "npc_wave_boss_skeleton", displayName = "#npc_wave_boss_skeleton"},
{unitName = "npc_wave_boss_lifestealer", displayName = "#npc_wave_boss_lifestealer"},
{unitName = "npc_wave_boss_death_prophet", displayName = "#npc_wave_boss_death_prophet"},
{unitName = "npc_wave_boss_zombie_king", displayName = "#npc_wave_boss_zombie_king"},
{unitName = "npc_wave_boss_suicide_zombie", displayName = "#npc_wave_boss_suicide_zombie"}
}
local MUSIC_DURATION_BY_EVENT = {
wave_start_yaskar_laning_01 = 120,
wave_start_yaskar_laning_02 = 120,
wave_start_yaskar_laning_03 = 120,
wave_boss_spawn_yaskar_battle_01 = 60,
wave_boss_spawn_yaskar_battle_02 = 60,
wave_boss_spawn_yaskar_battle_03 = 60,
wave_boss_spawn_yaskar_killed = 92,
wave_start_deadmau5_laning_01 = 159,
wave_start_deadmau5_laning_02 = 127,
wave_start_deadmau5_laning_03 = 124,
wave_boss_spawn_deadmau5_battle_01 = 48,
wave_boss_spawn_deadmau5_battle_02 = 73,
wave_boss_spawn_deadmau5_battle_03 = 67,
wave_boss_spawn_deadmau5_killed = 173
}
local WAVE_MUSIC_GROUPS = {{name = "yaskar_laning", waveStartEvents = {"wave_start_yaskar_laning_01", "wave_start_yaskar_laning_02", "wave_start_yaskar_laning_03"}}, {name = "deadmau5_laning", waveStartEvents = {"wave_start_deadmau5_laning_01", "wave_start_deadmau5_laning_02", "wave_start_deadmau5_laning_03"}}, {name = "yaskar_battle", waveStartEvents = {"wave_boss_spawn_yaskar_battle_01", "wave_boss_spawn_yaskar_battle_02", "wave_boss_spawn_yaskar_battle_03", "wave_boss_spawn_yaskar_killed"}}, {name = "deadmau5_battle", waveStartEvents = {"wave_boss_spawn_deadmau5_battle_01", "wave_boss_spawn_deadmau5_battle_02", "wave_boss_spawn_deadmau5_battle_03", "wave_boss_spawn_deadmau5_killed"}}}
____exports.WaveManager = __TS__Class()
local WaveManager = ____exports.WaveManager
WaveManager.name = "WaveManager"
WaveManager.____file_path = "scripts/vscripts/WaveManager.lua"
function WaveManager.prototype.____constructor(self)
self.waveNumber = 0
self.nextWaveTime = 60
self.isWaveInProgress = false
self.zombieCount = 0
self.currentNight = 0
self.maxWavesInNight = 3
self.spawnedZombies = {}
self.maxAliveZombies = 100
self.zombieGoldMap = __TS__New(Map)
self.isRepeatingLastWave = false
self.waveEndCallbacks = {}
self.isNextWaveScheduled = false
self.isGameStarted = false
self.waveZombieSilentRemoval = false
self.availableRandomBosses = {}
self.night5RandomBossKillsSinceCycleStart = 0
self.nightWavesStoppedForNight = false
self.currentBattleMusicEvent = nil
self.currentMusicPool = nil
self.battleMusicTimer = nil
self.activeMusicGroupIndex = nil
self.nightEndMusicPlayed = false
self.playerMusicEnabled = __TS__New(Map)
self.pendingEndingCutsceneSceneId = nil
self.endingCutsceneReadyPlayers = __TS__New(Map)
self.endingCutsceneReadyDeadline = nil
self.endingCutsceneReadyCountdownTimer = nil
self.waveHeroTargetCache = {}
self.waveHeroTargetCacheTime = -1000000000
self.lastWaveCreepAbilityTime = __TS__New(Map)
____print(nil, LOG_PREFIX .. " Инициализация")
ListenToGameEvent(
"entity_killed",
function(event)
local killedUnit = EntIndexToHScript(event.entindex_killed)
if killedUnit and killedUnit:GetTeamNumber() == DOTA_TEAM_NEUTRALS then
self:onZombieKilled(killedUnit)
end
end,
nil
)
CustomGameEventManager:RegisterListener(
"toggle_music",
function(_userId, event)
local playerId = event.PlayerID
local ____temp_0 = self.playerMusicEnabled:get(playerId)
if ____temp_0 == nil then
____temp_0 = true
end
local currentState = ____temp_0
local nextState = not currentState
self.playerMusicEnabled:set(playerId, nextState)
if self.currentBattleMusicEvent then
if nextState then
self:emitSoundForPlayer(playerId, self.currentBattleMusicEvent)
else
self:stopSoundForPlayer(playerId, self.currentBattleMusicEvent)
end
end
end
)
CustomGameEventManager:RegisterListener(
"ending_cutscene_toggle_ready",
function(eventSourceIndex, data)
local playerController = EntIndexToHScript(eventSourceIndex)
if not playerController then
return
end
local playerId = playerController:GetPlayerID()
if not PlayerResource:IsValidPlayerID(playerId) then
return
end
if not self.pendingEndingCutsceneSceneId then
return
end
if not self.endingCutsceneReadyPlayers:has(playerId) then
return
end
local isReady = (data and data.ready) == 1 or (data and data.ready) == true
self.endingCutsceneReadyPlayers:set(playerId, isReady)
if not self:hasAnyEndingCutsceneReadyPlayer() then
self:clearEndingCutsceneCountdown()
end
self:startEndingCutsceneCountdownIfNeeded()
self:broadcastEndingCutsceneReadyState()
self:tryStartPendingEndingCutsceneIfEveryoneReady()
end
)
end
function WaveManager.getInstance(self)
if not ____exports.WaveManager.instance then
____exports.WaveManager.instance = __TS__New(____exports.WaveManager)
end
return ____exports.WaveManager.instance
end
function WaveManager.prototype.clearEndingCutsceneCountdown(self)
if self.endingCutsceneReadyCountdownTimer then
Timers:RemoveTimer(self.endingCutsceneReadyCountdownTimer)
self.endingCutsceneReadyCountdownTimer = nil
end
self.endingCutsceneReadyDeadline = nil
end
function WaveManager.prototype.startEndingCutsceneCountdownIfNeeded(self)
if not self.pendingEndingCutsceneSceneId then
return
end
if self.endingCutsceneReadyDeadline ~= nil then
return
end
local hasAnyReady = false
for ____, ____value in __TS__Iterator(self.endingCutsceneReadyPlayers) do
local isReady = ____value[2]
if isReady then
hasAnyReady = true
break
end
end
if not hasAnyReady then
return
end
self.endingCutsceneReadyDeadline = GameRules:GetGameTime() + 30
self.endingCutsceneReadyCountdownTimer = "ending_cutscene_countdown_" .. DoUniqueString("timer")
local timerName = self.endingCutsceneReadyCountdownTimer
Timers:CreateTimer(
timerName,
{
endTime = 0.25,
callback = function()
if not self.pendingEndingCutsceneSceneId then
self:clearEndingCutsceneCountdown()
return nil
end
self:broadcastEndingCutsceneReadyState()
local deadline = self.endingCutsceneReadyDeadline
if deadline ~= nil and GameRules:GetGameTime() >= deadline then
self:tryStartPendingEndingCutsceneIfEveryoneReady(true)
return nil
end
return 0.25
end
}
)
end
function WaveManager.prototype.hasAnyEndingCutsceneReadyPlayer(self)
for ____, ____value in __TS__Iterator(self.endingCutsceneReadyPlayers) do
local isReady = ____value[2]
if isReady then
return true
end
end
return false
end
function WaveManager.prototype.getConnectedLobbyPlayerIds(self)
local result = {}
do
local playerId = 0
while playerId < DOTA_MAX_PLAYERS do
do
local pid = playerId
if not PlayerResource:IsValidPlayerID(pid) then
goto __continue36
end
local player = PlayerResource:GetPlayer(pid)
if not player then
goto __continue36
end
local cs = PlayerResource:GetConnectionState(pid)
if cs ~= DOTA_CONNECTION_STATE.CONNECTED then
goto __continue36
end
result[#result + 1] = pid
end
::__continue36::
playerId = playerId + 1
end
end
return result
end
function WaveManager.prototype.buildEndingCutsceneReadyPayload(self)
if not self.pendingEndingCutsceneSceneId then
return nil
end
local ready = {}
local readyPlayerNames = {}
local readyPlayerIds = {}
local readyCount = 0
local totalCount = 0
for ____, ____value in __TS__Iterator(self.endingCutsceneReadyPlayers) do
local playerId = ____value[1]
local isReady = ____value[2]
totalCount = totalCount + 1
if isReady then
readyCount = readyCount + 1
local playerName = PlayerResource:GetPlayerName(playerId)
readyPlayerNames[#readyPlayerNames + 1] = playerName and #playerName > 0 and playerName or "Player " .. tostring(playerId)
readyPlayerIds[#readyPlayerIds + 1] = playerId
end
ready[playerId] = isReady and 1 or 0
end
if readyCount > 0 and self.endingCutsceneReadyDeadline == nil then
self.endingCutsceneReadyDeadline = GameRules:GetGameTime() + 30
end
local secondsLeft = -1
if self.endingCutsceneReadyDeadline ~= nil then
secondsLeft = math.max(
0,
math.ceil(self.endingCutsceneReadyDeadline - GameRules:GetGameTime())
)
end
return {
sceneId = self.pendingEndingCutsceneSceneId,
ready = ready,
readyCount = readyCount,
totalCount = totalCount,
readyPlayerNames = readyPlayerNames,
readyPlayerIds = readyPlayerIds,
secondsLeft = secondsLeft
}
end
function WaveManager.prototype.broadcastEndingCutsceneReadyState(self)
self:startEndingCutsceneCountdownIfNeeded()
local payload = self:buildEndingCutsceneReadyPayload()
if not payload then
return
end
CustomGameEventManager:Send_ServerToAllClients("ending_cutscene_ready_update", payload)
end
function WaveManager.prototype.closeEndingCutsceneReadyUi(self)
CustomGameEventManager:Send_ServerToAllClients("ending_cutscene_ready_close", {})
end
function WaveManager.prototype.isEndingCutsceneReadyVoteOpen(self)
return self.pendingEndingCutsceneSceneId ~= nil
end
function WaveManager.prototype.openEndingCutsceneReadyCheck(self, sceneId)
self.pendingEndingCutsceneSceneId = sceneId
self.endingCutsceneReadyPlayers:clear()
self:clearEndingCutsceneCountdown()
self:removeAllTrackedWaveZombiesSilent()
SpawnManager:getInstance():RemoveAllSpawnZones()
local players = self:getConnectedLobbyPlayerIds()
do
local i = 0
while i < #players do
do
local playerId = players[i + 1]
local point = Entities:FindByName(
nil,
"point_player_" .. tostring(i)
)
local player = PlayerResource:GetPlayer(playerId)
if not player or not point then
goto __continue52
end
local hero = player.GetAssignedHero and player:GetAssignedHero()
if hero == nil then
goto __continue52
end
if not hero:IsAlive() then
hero:RespawnHero(false, false)
end
hero:SetAbsOrigin(point:GetAbsOrigin())
FindClearSpaceForUnit(
hero,
point:GetAbsOrigin(),
true
)
end
::__continue52::
i = i + 1
end
end
for ____, playerId in ipairs(players) do
self.endingCutsceneReadyPlayers:set(playerId, false)
end
if #players == 0 then
self:tryStartPendingEndingCutsceneIfEveryoneReady()
return
end
CustomGameEventManager:Send_ServerToAllClients("ending_cutscene_ready_open", {sceneId = sceneId})
self:broadcastEndingCutsceneReadyState()
end
function WaveManager.prototype.tryStartPendingEndingCutsceneIfEveryoneReady(self, forceByTimer)
if forceByTimer == nil then
forceByTimer = false
end
local sceneId = self.pendingEndingCutsceneSceneId
if not sceneId then
return
end
local total = 0
local ready = 0
for ____, ____value in __TS__Iterator(self.endingCutsceneReadyPlayers) do
local isReady = ____value[2]
total = total + 1
if isReady then
ready = ready + 1
end
end
if total > 0 and ready < total and not forceByTimer then
return
end
local mgr = GameRules.CutsceneManager
if not mgr or mgr:isCutsceneActive() then
return
end
mgr:startScene(sceneId, {force = true})
self.pendingEndingCutsceneSceneId = nil
self.endingCutsceneReadyPlayers:clear()
self:clearEndingCutsceneCountdown()
self:closeEndingCutsceneReadyUi()
end
function WaveManager.prototype.isMusicEnabledForPlayer(self, playerID)
local ____temp_5 = self.playerMusicEnabled:get(playerID)
if ____temp_5 == nil then
____temp_5 = true
end
return ____temp_5
end
function WaveManager.prototype.emitSoundForPlayer(self, playerID, soundEventName)
local hero = PlayerResource:GetSelectedHeroEntity(playerID)
if hero then
EmitSoundOnEntityForPlayer(soundEventName, hero, playerID)
return
end
local player = PlayerResource:GetPlayer(playerID)
if not player then
return
end
EmitAnnouncerSoundForPlayer(soundEventName, playerID)
end
function WaveManager.prototype.stopSoundForPlayer(self, playerID, soundEventName)
local hero = PlayerResource:GetSelectedHeroEntity(playerID)
if hero then
StopSoundOn(soundEventName, hero)
end
local player = PlayerResource:GetPlayer(playerID)
if player then
StopSoundOn(soundEventName, player)
end
end
function WaveManager.prototype.isRandomWaveBossUnitName(self, unitName)
for ____, b in ipairs(RANDOM_WAVE_BOSSES) do
if b.unitName == unitName then
return true
end
end
return false
end
function WaveManager.prototype.getNextRandomBossNoRepeat(self)
if #self.availableRandomBosses == 0 then
self.availableRandomBosses = {unpack(RANDOM_WAVE_BOSSES)}
end
local randomIndex = RandomInt(0, #self.availableRandomBosses - 1)
local selectedBoss = self.availableRandomBosses[randomIndex + 1]
__TS__ArraySplice(self.availableRandomBosses, randomIndex, 1)
return selectedBoss
end
function WaveManager.prototype.announceWaveStarted(self, totalWaves)
local group = self:getOrPickActiveMusicGroup()
if self.currentMusicPool == nil or #self.currentMusicPool == 0 then
self:startMusicPool(group.waveStartEvents)
end
self.nightEndMusicPlayed = false
end
function WaveManager.prototype.getOrPickActiveMusicGroup(self)
if self.activeMusicGroupIndex == nil then
self.activeMusicGroupIndex = RandomInt(0, #WAVE_MUSIC_GROUPS - 1)
local selectedGroup = WAVE_MUSIC_GROUPS[self.activeMusicGroupIndex + 1]
____print(nil, (LOG_PREFIX .. " 🎵 Выбрана музыкальная группа: ") .. selectedGroup.name)
end
return WAVE_MUSIC_GROUPS[self.activeMusicGroupIndex + 1]
end
function WaveManager.prototype.getEventDuration(self, soundEventName)
return MUSIC_DURATION_BY_EVENT[soundEventName] or 60
end
function WaveManager.prototype.scheduleNextMusicFromPool(self, afterEvent)
if self.battleMusicTimer then
Timers:RemoveTimer(self.battleMusicTimer)
self.battleMusicTimer = nil
end
if not self.currentMusicPool or #self.currentMusicPool == 0 then
return
end
local duration = self:getEventDuration(afterEvent)
self.battleMusicTimer = Timers:CreateTimer(
duration,
function()
if GameRules:IsDaytime() then
self:handleNightEndedMusic()
return nil
end
if not self.currentMusicPool or #self.currentMusicPool == 0 then
return nil
end
local nextEvent = self.currentMusicPool[RandomInt(0, #self.currentMusicPool - 1) + 1]
if #self.currentMusicPool > 1 then
while nextEvent == self.currentBattleMusicEvent do
nextEvent = self.currentMusicPool[RandomInt(0, #self.currentMusicPool - 1) + 1]
end
end
self:playBattleMusic(nextEvent)
return nil
end
)
end
function WaveManager.prototype.startMusicPool(self, events)
self.currentMusicPool = events
local firstEvent = events[RandomInt(0, #events - 1) + 1]
self:playBattleMusic(firstEvent)
end
function WaveManager.prototype.playBattleMusic(self, soundEventName)
if self.currentBattleMusicEvent then
self:stopSoundForAllPlayers(self.currentBattleMusicEvent)
end
self.currentBattleMusicEvent = soundEventName
self:emitSoundForAllPlayers(soundEventName)
self:scheduleNextMusicFromPool(soundEventName)
end
function WaveManager.prototype.stopBattleMusic(self)
if self.battleMusicTimer then
Timers:RemoveTimer(self.battleMusicTimer)
self.battleMusicTimer = nil
end
self.currentMusicPool = nil
if not self.currentBattleMusicEvent then
return
end
self:stopSoundForAllPlayers(self.currentBattleMusicEvent)
self.currentBattleMusicEvent = nil
end
function WaveManager.prototype.emitSoundForAllPlayers(self, soundEventName)
do
local playerID = 0
while playerID < DOTA_MAX_PLAYERS do
do
if not PlayerResource:IsValidPlayerID(playerID) then
goto __continue99
end
if not self:isMusicEnabledForPlayer(playerID) then
goto __continue99
end
self:emitSoundForPlayer(playerID, soundEventName)
end
::__continue99::
playerID = playerID + 1
end
end
end
function WaveManager.prototype.stopSoundForAllPlayers(self, soundEventName)
do
local playerID = 0
while playerID < DOTA_MAX_PLAYERS do
do
if not PlayerResource:IsValidPlayerID(playerID) then
goto __continue103
end
self:stopSoundForPlayer(playerID, soundEventName)
end
::__continue103::
playerID = playerID + 1
end
end
end
function WaveManager.prototype.handleNightEndedMusic(self)
if self.nightEndMusicPlayed then
return
end
self.nightEndMusicPlayed = true
self:stopBattleMusic()
end
function WaveManager.prototype.isValidZombieHeroTarget(self, hero)
return hero:IsAlive() and hero:IsRealHero() and not hero:IsInvulnerable() and not hero:IsAttackImmune()
end
function WaveManager.prototype.shouldKeepZombieAttackTarget(self, target)
if not target:IsAlive() then
return false
end
if target:IsHero() and target:IsRealHero() then
return self:isValidZombieHeroTarget(target)
end
return true
end
function WaveManager.prototype.refreshWaveHeroTargetCache(self)
local now = GameRules:GetGameTime()
if now - self.waveHeroTargetCacheTime < WAVE_HERO_TARGET_CACHE_TTL then
return
end
self.waveHeroTargetCacheTime = now
local list = {}
do
local i = 0
while i < DOTA_MAX_PLAYERS do
do
local pid = i
if not PlayerResource:IsValidPlayerID(pid) then
goto __continue113
end
local player = PlayerResource:GetPlayer(pid)
local hero
if player and player.GetAssignedHero and player:GetAssignedHero() then
hero = player:GetAssignedHero()
end
if not hero or not IsValidEntity(hero) then
hero = PlayerResource:GetSelectedHeroEntity(pid) or nil
end
if hero and IsValidEntity(hero) and hero:IsRealHero() and hero:IsAlive() then
list[#list + 1] = hero
end
end
::__continue113::
i = i + 1
end
end
self.waveHeroTargetCache = list
end
function WaveManager.prototype.getWaveCreepThinkInterval(self)
local n = #self.spawnedZombies
if n > WAVE_CREEP_MEGA_HORDE_THRESHOLD then
return 0.45
end
if n > WAVE_CREEP_HORDE_THRESHOLD then
return 0.32
end
return 0.2
end
function WaveManager.prototype.findNearestHero(self, zombie, radius)
if radius == nil then
radius = 450
end
self:refreshWaveHeroTargetCache()
local zpos = zombie:GetAbsOrigin()
local best = nil
local bestDist = radius + 1
for ____, raw in ipairs(self.waveHeroTargetCache) do
do
local hero = raw
if not hero or not IsValidEntity(hero) or not self:isValidZombieHeroTarget(hero) then
goto __continue122
end
local d = (hero:GetAbsOrigin() - zpos):Length2D()
if d <= radius and d < bestDist then
bestDist = d
best = hero
end
end
::__continue122::
end
if best then
return best
end
local nearbyHeroes = FindUnitsInRadius(
zombie:GetTeamNumber(),
zpos,
nil,
radius,
DOTA_UNIT_TARGET_TEAM_ENEMY,
DOTA_UNIT_TARGET_HERO,
DOTA_UNIT_TARGET_FLAG_NONE,
FIND_CLOSEST,
false
)
for ____, hero in ipairs(nearbyHeroes) do
if hero and self:isValidZombieHeroTarget(hero) then
return hero
end
end
return nil
end
function WaveManager.prototype.findHomerTarget(self)
local homer = Entities:FindByName(nil, "npc_homer")
if homer and homer:IsAlive() and homer:GetUnitName() == "npc_homer" then
return homer
end
local allUnits = Entities:FindAllByClassname("npc_dota_creature")
for ____, unit in ipairs(allUnits) do
if unit and unit:GetUnitName() == "npc_homer" and unit:IsAlive() then
return unit
end
end
return nil
end
function WaveManager.prototype.updateZombieBehavior(self, zombie, homePoint)
local currentTarget = zombie:GetAttackTarget()
if currentTarget and self:shouldKeepZombieAttackTarget(currentTarget) then
return
end
local canAttack = zombie:GetAttackCapability() ~= DOTA_UNIT_CAP_NO_ATTACK and not zombie:IsDisarmed()
if not canAttack then
ExecuteOrderFromTable({
UnitIndex = zombie:entindex(),
Queue = false,
OrderType = DOTA_UNIT_ORDER_MOVE_TO_POSITION,
Position = homePoint:GetAbsOrigin()
})
return
end
local nearestHero = self:findNearestHero(zombie, 1000)
if nearestHero then
ExecuteOrderFromTable({
UnitIndex = zombie:entindex(),
Queue = false,
OrderType = DOTA_UNIT_ORDER_ATTACK_TARGET,
TargetIndex = nearestHero:entindex()
})
else
local homerTarget = self:findHomerTarget()
if homerTarget then
ExecuteOrderFromTable({
UnitIndex = zombie:entindex(),
Queue = false,
OrderType = DOTA_UNIT_ORDER_ATTACK_TARGET,
TargetIndex = homerTarget:entindex()
})
else
ExecuteOrderFromTable({
UnitIndex = zombie:entindex(),
Queue = false,
OrderType = DOTA_UNIT_ORDER_ATTACK_MOVE,
Position = homePoint:GetAbsOrigin()
})
end
end
end
function WaveManager.prototype.assignBehavior(self, unit, homePoint)
local stagger = math.abs(unit:entindex()) % 8 * 0.04
local behaviorInterval = #self.spawnedZombies > WAVE_CREEP_MEGA_HORDE_THRESHOLD and 1.25 or 1
Timers:CreateTimer(
0.1 + stagger,
function()
if unit and unit:IsAlive() then
unit:Stop()
self:updateZombieBehavior(unit, homePoint)
self:startWaveCreepAbilityThink(unit)
Timers:CreateTimer({
endTime = behaviorInterval,
callback = function()
if unit and unit:IsAlive() then
self:updateZombieBehavior(unit, homePoint)
return #self.spawnedZombies > WAVE_CREEP_MEGA_HORDE_THRESHOLD and 1.25 or 1
end
return nil
end
})
end
end
)
end
function WaveManager.prototype.getAbilityBehavior(self, ability)
local behavior = 0
do
local function ____catch(_)
local raw = ability:GetBehavior()
if type(raw) == "number" and not __TS__NumberIsNaN(__TS__Number(raw)) then
behavior = raw
end
end
local ____try, ____hasReturned = pcall(function()
local kv = ability:GetAbilityKeyValues()
if kv and kv.AbilityBehavior then
local s = kv.AbilityBehavior or ""
if __TS__StringIncludes(s, "POINT") then
behavior = bit.bor(behavior, ____exports.WaveManager.ABILITY_POINT)
end
if __TS__StringIncludes(s, "UNIT_TARGET") then
behavior = bit.bor(behavior, ____exports.WaveManager.ABILITY_UNIT_TARGET)
end
if __TS__StringIncludes(s, "NO_TARGET") then
behavior = bit.bor(behavior, ____exports.WaveManager.ABILITY_NO_TARGET)
end
end
end)
if not ____try then
____catch(____hasReturned)
end
end
local byName = {sheep_coil = ____exports.WaveManager.ABILITY_POINT, black_dragon_fireball = ____exports.WaveManager.ABILITY_POINT, frogmen_acid_jump = ____exports.WaveManager.ABILITY_POINT}
local nameOverride = byName[ability:GetAbilityName()]
if nameOverride ~= nil then
return nameOverride
end
return behavior
end
function WaveManager.prototype.tryUseWaveCreepAbilities(self, unit, target)
if not unit:IsAlive() or unit:IsStunned() or unit:IsHexed() then
return false
end
local unitPos = unit:GetAbsOrigin()
local targetPos = target:GetAbsOrigin()
do
local i = 0
while i < unit:GetAbilityCount() do
do
local ability = unit:GetAbilityByIndex(i)
if not ability or ability:IsNull() or ability:IsPassive() or ability:IsHidden() then
goto __continue158
end
if not ability:IsCooldownReady() then
goto __continue158
end
local manaCost = ability:GetManaCost(ability:GetLevel())
if manaCost > unit:GetMana() then
goto __continue158
end
local behavior = self:getAbilityBehavior(ability)
local castRange = ability:GetCastRange(unitPos, target)
local dx = targetPos.x - unitPos.x
local dy = targetPos.y - unitPos.y
local distance = math.sqrt(dx * dx + dy * dy)
if bit.band(behavior, ____exports.WaveManager.ABILITY_UNIT_TARGET) ~= 0 and distance <= castRange and target:IsAlive() then
ExecuteOrderFromTable({
UnitIndex = unit:entindex(),
OrderType = DOTA_UNIT_ORDER_CAST_TARGET,
TargetIndex = target:entindex(),
AbilityIndex = ability:GetEntityIndex(),
Queue = false
})
return true
end
if bit.band(behavior, ____exports.WaveManager.ABILITY_POINT) ~= 0 and distance <= castRange and target:IsAlive() then
ExecuteOrderFromTable({
UnitIndex = unit:entindex(),
OrderType = DOTA_UNIT_ORDER_CAST_POSITION,
Position = targetPos,
AbilityIndex = ability:GetEntityIndex(),
Queue = false
})
return true
end
if bit.band(behavior, ____exports.WaveManager.ABILITY_NO_TARGET) ~= 0 and distance <= 400 then
ExecuteOrderFromTable({
UnitIndex = unit:entindex(),
OrderType = DOTA_UNIT_ORDER_CAST_NO_TARGET,
AbilityIndex = ability:GetEntityIndex(),
Queue = false
})
return true
end
end
::__continue158::
i = i + 1
end
end
return false
end
function WaveManager.prototype.waveCreepAbilityThink(self, unit)
if not unit or not unit:IsAlive() then
return nil
end
local thinkInterval = self:getWaveCreepThinkInterval()
local now = GameRules:GetGameTime()
local last = self.lastWaveCreepAbilityTime:get(unit:entindex()) or 0
if now - last < ____exports.WaveManager.WAVE_CREEP_ABILITY_COOLDOWN then
return thinkInterval
end
local radius = 700
local uPos = unit:GetAbsOrigin()
self:refreshWaveHeroTargetCache()
local best
local bestD = radius + 1
for ____, raw in ipairs(self.waveHeroTargetCache) do
do
if not raw or not IsValidEntity(raw) or not raw:IsAlive() then
goto __continue168
end
local d = (raw:GetAbsOrigin() - uPos):Length2D()
if d <= radius and d < bestD then
bestD = d
best = raw
end
end
::__continue168::
end
local target = best
if target == nil then
local enemies = FindUnitsInRadius(
unit:GetTeamNumber(),
uPos,
nil,
radius,
DOTA_UNIT_TARGET_TEAM_ENEMY,
bit.bor(DOTA_UNIT_TARGET_HERO, DOTA_UNIT_TARGET_BASIC),
DOTA_UNIT_TARGET_FLAG_NONE,
FIND_CLOSEST,
false
)
if #enemies == 0 then
return thinkInterval
end
target = enemies[1]
end
if not target or not target:IsAlive() then
return thinkInterval
end
local used = self:tryUseWaveCreepAbilities(unit, target)
if used then
self.lastWaveCreepAbilityTime:set(
unit:entindex(),
now
)
end
return thinkInterval
end
function WaveManager.prototype.startWaveCreepAbilityThink(self, unit)
local initial = self:getWaveCreepThinkInterval()
unit:SetContextThink(
"WaveCreepAbilities",
function() return self:waveCreepAbilityThink(unit) end,
initial
)
end
function WaveManager.prototype.getLastConfiguredNight(self)
local lastNight = 1
for nightKey in pairs(WAVE_SETTINGS) do
local n = __TS__ParseInt(nightKey)
if n > lastNight then
lastNight = n
end
end
return lastNight
end
function WaveManager.prototype.getNightMultiplier(self)
local lastConfiguredNight = self:getLastConfiguredNight()
if self.currentNight <= lastConfiguredNight then
return 1
end
return 1 + (self.currentNight - lastConfiguredNight) * 0.2
end
function WaveManager.prototype.getWaves(self)
local actualNight = self.currentNight
if WAVE_SETTINGS[actualNight] ~= nil and WAVE_SETTINGS[actualNight].waves ~= nil then
return WAVE_SETTINGS[actualNight].waves
end
local lastNight = self:getLastConfiguredNight()
____print(
nil,
(((LOG_PREFIX .. " Нет конфига для ночи ") .. tostring(actualNight)) .. ", используем ночь ") .. tostring(lastNight)
)
if WAVE_SETTINGS[lastNight] ~= nil and WAVE_SETTINGS[lastNight].waves ~= nil then
return WAVE_SETTINGS[lastNight].waves
end
____print(nil, LOG_PREFIX .. " ⚠️ Нет конфигурации волн! Возвращаем пустой массив")
return {}
end
function WaveManager.prototype.applyNightScaling(self, unit)
local multiplier = self:getNightMultiplier()
if multiplier <= 1 then
return
end
local oldDamage = unit:GetBaseDamageMin()
local oldHealth = unit:GetMaxHealth()
local newDamage = math.floor(oldDamage * multiplier)
unit:SetBaseDamageMin(newDamage)
unit:SetBaseDamageMax(newDamage)
local newHealth = math.floor(oldHealth * multiplier)
unit:SetMaxHealth(newHealth)
unit:SetBaseMaxHealth(newHealth)
unit:SetHealth(newHealth)
____print(
nil,
((((((((((((LOG_PREFIX .. " Скейлинг ") .. unit:GetUnitName()) .. ": HP ") .. tostring(oldHealth)) .. " -> ") .. tostring(newHealth)) .. ", DMG ") .. tostring(oldDamage)) .. " -> ") .. tostring(newDamage)) .. " (x") .. tostring(multiplier)) .. ")"
)
end
function WaveManager.prototype.setUnitRewards(self, unit)
SetGoldUsually(nil, unit)
unit:SetDeathXP(unit:GetDeathXP())
end
function WaveManager.prototype.tryGrantRandomBonusWaveAbility(self, unit)
if not IsServer() then
return
end
if not unit or not IsValidEntity(unit) or not unit:IsAlive() then
return
end
if Difficulty.leader ~= "death_sentence" then
return
end
local skipThisSpawn = __TS__New(Set)
while true do
if RandomInt(1, 100) > WAVE_MOB_RANDOM_BONUS_ABILITY_CHANCE then
break
end
local candidates = __TS__ArrayFilter(
WAVE_MOB_BONUS_ABILITY_POOL,
function(____, name) return not unit:FindAbilityByName(name) and not skipThisSpawn:has(name) end
)
if #candidates == 0 then
break
end
local abilityName = candidates[RandomInt(0, #candidates - 1) + 1]
do
local function ____catch(_)
skipThisSpawn:add(abilityName)
end
local ____try, ____hasReturned = pcall(function()
local ab = unit:AddAbility(abilityName)
if ab ~= nil and ab ~= nil then
local maxLevel = ab:GetMaxLevel()
ab:SetLevel(maxLevel > 0 and maxLevel or 1)
else
skipThisSpawn:add(abilityName)
end
end)
if not ____try then
____catch(____hasReturned)
end
end
end
end
function WaveManager.prototype.OnWaveEnd(self, callback)
local ____self_waveEndCallbacks_6 = self.waveEndCallbacks
____self_waveEndCallbacks_6[#____self_waveEndCallbacks_6 + 1] = callback
____print(
nil,
((LOG_PREFIX .. " OnWaveEnd: зарегистрирован колбэк (всего: ") .. tostring(#self.waveEndCallbacks)) .. ")"
)
end
function WaveManager.prototype.removeAllTrackedWaveZombiesSilent(self)
self.waveZombieSilentRemoval = true
do
pcall(function()
local snapshot = {unpack(self.spawnedZombies)}
local removed = 0
for ____, z in ipairs(snapshot) do
do
if not z or not IsValidEntity(z) or not z:IsAlive() then
goto __continue205
end
self.zombieGoldMap:delete(z:entindex())
z:RemoveSelf()
removed = removed + 1
end
::__continue205::
end
self.spawnedZombies = __TS__ArrayFilter(
self.spawnedZombies,
function(____, u) return u and IsValidEntity(u) and u:IsAlive() end
)
self.zombieCount = #self.spawnedZombies
if removed > 0 then
____print(
nil,
(((LOG_PREFIX .. " Ready-check: удалено оставшихся зомби волны: ") .. tostring(removed)) .. ", в учёте осталось: ") .. tostring(self.zombieCount)
)
end
end)
do
self.waveZombieSilentRemoval = false
end
end
end
function WaveManager.prototype.scheduleWaveEndFollowup(self, waveConfig, waveIndexForEvent)
if not self.isRepeatingLastWave and not GameRules:IsDaytime() then
self:executeWaveEndEvent(waveConfig, waveIndexForEvent)
end
if waveConfig.stopWavesAfter then
self.nightWavesStoppedForNight = true
____print(nil, LOG_PREFIX .. " 🛑 Волны ночи остановлены (stopWavesAfter), следующих волн не будет")
return
end
____print(nil, LOG_PREFIX .. " Следующая волна через 3 сек...")
self.isNextWaveScheduled = true
Timers:CreateTimer(
3,
function()
self.isNextWaveScheduled = false
if not GameRules:IsDaytime() then
self:SpawnNextWave()
else
self:handleNightEndedMusic()
____print(nil, LOG_PREFIX .. " Наступил день — следующая волна отменена")
end
end
)
end
function WaveManager.prototype.executeWaveEndEvent(self, waveConfig, waveIndex)
local waves = self:getWaves()
local info = {
night = self.currentNight,
waveIndex = waveIndex,
totalWaves = #waves,
isLastWave = waveIndex >= #waves,
waveConfig = waveConfig
}
____print(
nil,
(((((((LOG_PREFIX .. " 🎯 WaveEndEvent: ночь ") .. tostring(info.night)) .. ", волна ") .. tostring(info.waveIndex)) .. "/") .. tostring(info.totalWaves)) .. ", isLastWave=") .. tostring(info.isLastWave)
)
if waveConfig.SpawnBossOnEnd then
local randomBossSpawn = {unitName = "npc_wave_boss_zombie", count = 1, goldPerKill = 300}
self:spawnWaveEndUnits(__TS__ObjectAssign({}, waveConfig, {onEndSpawns = {randomBossSpawn}}))
end
if waveConfig.onEndSpawns and #waveConfig.onEndSpawns > 0 then
self:spawnWaveEndUnits(waveConfig)
end
if waveConfig.cutsceneOnEnd then
local sceneId = waveConfig.cutsceneOnEnd
Timers:CreateTimer(
0.12,
function()
local mgr = GameRules.CutsceneManager
if not mgr or mgr:isCutsceneActive() then
return nil
end
self:openEndingCutsceneReadyCheck(sceneId)
return nil
end
)
end
for ____, callback in ipairs(self.waveEndCallbacks) do
do
local function ____catch(err)
____print(
nil,
(LOG_PREFIX .. " ⚠️ Ошибка в колбэке OnWaveEnd: ") .. tostring(err)
)
end
local ____try, ____hasReturned = pcall(function()
callback(nil, info)
end)
if not ____try then
____catch(____hasReturned)
end
end
end
end
function WaveManager.prototype.SpawnBossOnEnd(self, endSpawn, spawnPoints, homePoint)
local selectedBossName = ""
local selectedBossUnit = ""
local spawnUnitName = endSpawn.unitName
if endSpawn.unitName == "npc_wave_boss_zombie" and #RANDOM_WAVE_BOSSES > 0 then
local randomBoss = self:getNextRandomBossNoRepeat()
spawnUnitName = randomBoss.unitName
selectedBossUnit = randomBoss.unitName
selectedBossName = randomBoss.displayName
end
if selectedBossUnit ~= "" then
CustomGameEventManager:Send_ServerToAllClients("boss_spawned", {bossName = selectedBossName, bossUnitName = selectedBossUnit})
____print(nil, ((((LOG_PREFIX .. " 👑 Выбран случайный босс: ") .. selectedBossUnit) .. " (") .. selectedBossName) .. ")")
GameRules:SendCustomMessage("<font color='#FF3333'>Boss spawned!</font>", 0, 0)
end
do
local i = 0
while i < endSpawn.count do
do
local spawnPoint = spawnPoints[RandomInt(0, #spawnPoints - 1) + 1]
if not spawnPoint then
goto __continue230
end
local unit = CreateUnitByName(
spawnUnitName,
spawnPoint:GetAbsOrigin(),
true,
nil,
nil,
DOTA_TEAM_NEUTRALS
)
if unit ~= nil and unit ~= nil then
self:applyNightScaling(unit)
self.zombieCount = self.zombieCount + 1
local ____self_spawnedZombies_7 = self.spawnedZombies
____self_spawnedZombies_7[#____self_spawnedZombies_7 + 1] = unit
self.zombieGoldMap:set(
unit:entindex(),
endSpawn.goldPerKill
)
self:setUnitRewards(unit)
self:assignBehavior(unit, homePoint)
self:tryGrantRandomBonusWaveAbility(unit)
local bossBarToken = selectedBossName ~= "" and selectedBossName or (isBossHudHealthBarUnit(nil, spawnUnitName) and "#" .. spawnUnitName or nil)
if bossBarToken ~= nil then
applyBossHudHealthBar(nil, unit, bossBarToken)
end
____print(
nil,
(((((((LOG_PREFIX .. " 🎯 WaveEndSpawn: заспавнен ") .. spawnUnitName) .. " (") .. tostring(i + 1)) .. "/") .. tostring(endSpawn.count)) .. "), gold=") .. tostring(endSpawn.goldPerKill)
)
end
end
::__continue230::
i = i + 1
end
end
end
function WaveManager.prototype.spawnWaveEndUnits(self, waveConfig)
if not waveConfig.onEndSpawns or #waveConfig.onEndSpawns == 0 then
return
end
local spawnPoints = Entities:FindAllByName("wave_point")
local homePoint = Entities:FindByName(nil, "homer_point")
if not spawnPoints or #spawnPoints == 0 or not homePoint then
____print(nil, LOG_PREFIX .. " ⚠️ WaveEndEvent: не найдены точки спавна или homer_point")
return
end
for ____, endSpawn in ipairs(waveConfig.onEndSpawns) do
self:SpawnBossOnEnd(endSpawn, spawnPoints, homePoint)
end
end
function WaveManager.prototype.onZombieKilled(self, killedUnit)
if not killedUnit or not IsValidEntity(killedUnit) then
return
end
local killedIndex = killedUnit:entindex()
local unitName = ""
do
pcall(function()
unitName = killedUnit:GetUnitName()
end)
end
local wasTracked = __TS__ArraySome(
self.spawnedZombies,
function(____, z) return z and IsValidEntity(z) and z:entindex() == killedIndex end
)
if wasTracked then
self.spawnedZombies = __TS__ArrayFilter(
self.spawnedZombies,
function(____, z) return z and IsValidEntity(z) and z:entindex() ~= killedIndex end
)
self.zombieCount = #__TS__ArrayFilter(
self.spawnedZombies,
function(____, z) return z and IsValidEntity(z) and z:IsAlive() end
)
____print(
nil,
(((((LOG_PREFIX .. " Зомби убит: ") .. unitName) .. " (entindex: ") .. tostring(killedIndex)) .. "). Осталось живых: ") .. tostring(self.zombieCount)
)
else
____print(
nil,
((((LOG_PREFIX .. " Убит нейтрал (не из волны): ") .. unitName) .. " (entindex: ") .. tostring(killedIndex)) .. ")"
)
end
local goldPerKill = self.zombieGoldMap:get(killedIndex)
self.zombieGoldMap:delete(killedIndex)
if goldPerKill ~= nil and goldPerKill > 0 then
local killPosition = killedUnit:GetAbsOrigin()
local players = {}
local heroes = HeroList:GetAllHeroes()
for ____, hero in ipairs(heroes) do
do
if not hero or not hero:IsRealHero() or hero:IsIllusion() or hero:GetTeamNumber() ~= DOTA_TEAM_GOODGUYS or not hero:IsAlive() then
goto __continue248
end
local distance = (hero:GetAbsOrigin() - killPosition):Length2D()
if distance <= GOLD_REWARD_RADIUS then
players[#players + 1] = hero
end
end
::__continue248::
end
if #players > 0 then
local baseGoldPerPlayer = math.floor(goldPerKill / #players)
local remainder = goldPerKill - baseGoldPerPlayer * #players
____print(
nil,
((((((((((((LOG_PREFIX .. " 💰 Выдаём золото за ") .. unitName) .. ": ") .. tostring(goldPerKill)) .. " всего, ") .. tostring(baseGoldPerPlayer)) .. " (+остаток ") .. tostring(remainder)) .. ") на игрока в радиусе ") .. tostring(GOLD_REWARD_RADIUS)) .. " (") .. tostring(#players)) .. " игроков)"
)
for ____, hero in ipairs(players) do
do
pcall(function()
local bonusFromRemainder = remainder > 0 and 1 or 0
local finalGold = baseGoldPerPlayer + bonusFromRemainder
if remainder > 0 then
remainder = remainder - 1
end
hero:ModifyGold(finalGold, true, DOTA_ModifyGold_CreepKill)
SendOverheadEventMessage(
nil,
OVERHEAD_ALERT_GOLD,
hero,
finalGold,
hero:GetPlayerOwner()
)
end)
end
end
else
____print(
nil,
(((LOG_PREFIX .. " ⚠️ Золото не выдано за ") .. unitName) .. ": нет героев в радиусе ") .. tostring(GOLD_REWARD_RADIUS)
)
end
else
if wasTracked and not self.waveZombieSilentRemoval then
____print(
nil,
((((LOG_PREFIX .. " ⚠️ Золото не найдено в кеше для ") .. unitName) .. " (entindex: ") .. tostring(killedIndex)) .. ")"
)
end
end
if wasTracked then
local wavesForBossLoop = self:getWaves()
local lastWaveCfg = #wavesForBossLoop > 0 and wavesForBossLoop[#wavesForBossLoop] or nil
if self.currentNight == 5 and unitName ~= "" and self:isRandomWaveBossUnitName(unitName) and not (lastWaveCfg and lastWaveCfg.stopWavesAfter) then
self.night5RandomBossKillsSinceCycleStart = self.night5RandomBossKillsSinceCycleStart + 1
if self.night5RandomBossKillsSinceCycleStart >= #RANDOM_WAVE_BOSSES then
self.night5RandomBossKillsSinceCycleStart = 0
self.availableRandomBosses = {unpack(RANDOM_WAVE_BOSSES)}
self.waveNumber = 0
self.isRepeatingLastWave = false
____print(
nil,
((LOG_PREFIX .. " 🔁 Ночь 5: убит ") .. tostring(#RANDOM_WAVE_BOSSES)) .. "-й босс цикла — сброс пула боссов и волн, цикл с начала"
)
self.isNextWaveScheduled = false
if not GameRules:IsDaytime() then
self.isNextWaveScheduled = true
Timers:CreateTimer(
3,
function()
self.isNextWaveScheduled = false
if not GameRules:IsDaytime() then
self:SpawnNextWave()
else
self:handleNightEndedMusic()
____print(nil, LOG_PREFIX .. " Наступил день — перезапуск волн ночи 5 отменён")
end
end
)
end
return
end
end
local waves = self:getWaves()
local waveConfig
if self.isRepeatingLastWave then
waveConfig = #waves > 0 and waves[#waves] or nil
else
local currentWaveIdx = self.waveNumber - 1
if currentWaveIdx >= 0 and currentWaveIdx < #waves then
waveConfig = waves[currentWaveIdx + 1]
end
end
if waveConfig ~= nil and not self.isWaveInProgress then
local maxAllowedZombies = math.min(waveConfig.maxZombies * 0.5, 5)
if self.zombieCount <= maxAllowedZombies then
if self.isNextWaveScheduled then
return
end
____print(
nil,
((((((LOG_PREFIX .. " Зомби <= ") .. tostring(maxAllowedZombies)) .. " (") .. tostring(self.zombieCount)) .. "/") .. tostring(waveConfig.maxZombies)) .. "). Обработка конца волны..."
)
self:scheduleWaveEndFollowup(waveConfig, self.waveNumber)
end
end
end
end
function WaveManager.prototype.spawnZombiesFromConfig(self, waveConfig, spawnPointName)
local points = Entities:FindAllByName(spawnPointName)
local homePoint = Entities:FindByName(nil, "homer_point")
if not points or #points == 0 then
____print(nil, ((LOG_PREFIX .. " ⚠️ Не найдены точки спавна \"") .. spawnPointName) .. "\"")
self.isWaveInProgress = false
return
end
if not homePoint then
____print(nil, LOG_PREFIX .. " ⚠️ Не найдена точка \"homer_point\"")
self.isWaveInProgress = false
return
end
____print(
nil,
(((((((LOG_PREFIX .. " Начинаем спавн: maxZombies=") .. tostring(waveConfig.maxZombies)) .. ", точки=\"") .. spawnPointName) .. "\" (") .. tostring(#points)) .. " шт), типов зомби: ") .. tostring(#waveConfig.zombieTypes)
)
local spawnedCount = 0
Timers:CreateTimer({
endTime = 0,
callback = function()
if GameRules:IsDaytime() then
self:handleNightEndedMusic()
____print(
nil,
(((LOG_PREFIX .. " Наступил день — спавн остановлен. Заспавнено ") .. tostring(spawnedCount)) .. "/") .. tostring(waveConfig.maxZombies)
)
self.isWaveInProgress = false
return nil
end
if self.zombieCount >= self.maxAliveZombies then
return RandomFloat(waveConfig.spawnInterval.min, waveConfig.spawnInterval.max)
end
if spawnedCount >= waveConfig.maxZombies then
____print(
nil,
(((LOG_PREFIX .. " Волна завершена: заспавнено ") .. tostring(spawnedCount)) .. "/") .. tostring(waveConfig.maxZombies)
)
self.isWaveInProgress = false
local maxAllowedZombies = math.min(waveConfig.maxZombies * 0.5, 5)
if self.zombieCount <= maxAllowedZombies and not self.isNextWaveScheduled then
____print(
nil,
((LOG_PREFIX .. " Все зомби убиты (") .. tostring(self.zombieCount)) .. "), обработка конца волны..."
)
self:scheduleWaveEndFollowup(waveConfig, self.waveNumber)
end
return nil
end
local spawnPoint = points[RandomInt(0, #points - 1) + 1]
if not spawnPoint then
return RandomFloat(waveConfig.spawnInterval.min, waveConfig.spawnInterval.max)
end
local unit
local selectedGold = 0
local totalChance = 0
local roll = RandomInt(1, 100)
for ____, zombieType in ipairs(waveConfig.zombieTypes) do
totalChance = totalChance + zombieType.chance
if roll <= totalChance then
unit = CreateUnitByName(
zombieType.unitName,
spawnPoint:GetAbsOrigin(),
true,
nil,
nil,
DOTA_TEAM_NEUTRALS
)
selectedGold = zombieType.goldPerKill
break
end
end
if unit then
self:applyNightScaling(unit)
self.zombieCount = self.zombieCount + 1
spawnedCount = spawnedCount + 1
local ____self_spawnedZombies_10 = self.spawnedZombies
____self_spawnedZombies_10[#____self_spawnedZombies_10 + 1] = unit
self.zombieGoldMap:set(
unit:entindex(),
selectedGold
)
self:setUnitRewards(unit)
self:assignBehavior(unit, homePoint)
self:tryGrantRandomBonusWaveAbility(unit)
else
____print(
nil,
(((LOG_PREFIX .. " ⚠️ Зомби не создан: roll=") .. tostring(roll)) .. ", totalChance=") .. tostring(totalChance)
)
end
return RandomFloat(waveConfig.spawnInterval.min, waveConfig.spawnInterval.max)
end
})
end
function WaveManager.prototype.Initialize(self)
____print(
nil,
((LOG_PREFIX .. " Initialize: таймер волн запущен, интервал ") .. tostring(self.nextWaveTime)) .. " сек"
)
self:StartWaveTimer()
end
function WaveManager.prototype.StartWaveTimer(self)
Timers:CreateTimer(
"WaveTimer",
{
useGameTime = true,
endTime = self.nextWaveTime,
callback = function()
self:SpawnWave()
return self.nextWaveTime
end
}
)
end
function WaveManager.prototype.SpawnWave(self)
local gameState = GameRules:State_Get()
if gameState ~= DOTA_GAMERULES_STATE_GAME_IN_PROGRESS then
____print(
nil,
((LOG_PREFIX .. " SpawnWave: пропуск — игра еще не началась (state: ") .. tostring(gameState)) .. ")"
)
self.isNextWaveScheduled = false
return
end
if not self.isGameStarted then
____print(nil, LOG_PREFIX .. " SpawnWave: пропуск — игра еще не началась (isGameStarted: false)")
self.isNextWaveScheduled = false
return
end
if self.isWaveInProgress then
____print(nil, LOG_PREFIX .. " SpawnWave: пропуск — волна уже идёт")
self.isNextWaveScheduled = false
return
end
if GameRules:IsDaytime() then
self:handleNightEndedMusic()
self.isNextWaveScheduled = false
return
end
if self.currentNight == 0 then
____print(nil, LOG_PREFIX .. " SpawnWave: пропуск — ночь не установлена (currentNight: 0)")
self.isNextWaveScheduled = false
return
end
if self.nightWavesStoppedForNight then
____print(nil, LOG_PREFIX .. " SpawnWave: пропуск — волны ночи завершены (stopWavesAfter)")
self.isNextWaveScheduled = false
return
end
local waves = self:getWaves()
if #waves == 0 then
____print(
nil,
(LOG_PREFIX .. " SpawnWave: нет волн для ночи ") .. tostring(self.currentNight)
)
self.isNextWaveScheduled = false
return
end
self.isNextWaveScheduled = false
self.spawnedZombies = __TS__ArrayFilter(
self.spawnedZombies,
function(____, z) return z and IsValidEntity(z) and z:IsAlive() end
)
self.zombieCount = #self.spawnedZombies
if self.waveNumber >= #waves then
local lastWaveConfigPre = waves[#waves]
if lastWaveConfigPre and lastWaveConfigPre.stopWavesAfter then
self.nightWavesStoppedForNight = true
____print(nil, LOG_PREFIX .. " SpawnWave: пропуск — последняя волна с stopWavesAfter, повтора нет")
return
end
self.isRepeatingLastWave = true
local lastWaveConfig = waves[#waves]
self.isWaveInProgress = true
____print(
nil,
(((((LOG_PREFIX .. " 🔁 SpawnWave: все волны пройдены, повтор последней волны (ночь ") .. tostring(self.currentNight)) .. "), maxZombies=") .. tostring(lastWaveConfig.maxZombies)) .. ", осталось зомби от предыдущих волн: ") .. tostring(self.zombieCount)
)
self:spawnZombiesFromConfig(lastWaveConfig, "wave_point")
return
end
local waveConfig = waves[self.waveNumber + 1]
self.waveNumber = self.waveNumber + 1
self.isWaveInProgress = true
self:announceWaveStarted(#waves)
____print(
nil,
(((((((LOG_PREFIX .. " Волна старт: night=") .. tostring(self.currentNight)) .. " wave=") .. tostring(self.waveNumber)) .. "/") .. tostring(#waves)) .. " max=") .. tostring(waveConfig.maxZombies)
)
self:spawnZombiesFromConfig(waveConfig, "wave_point")
end
function WaveManager.prototype.SpawnNextWave(self)
local gameState = GameRules:State_Get()
if gameState ~= DOTA_GAMERULES_STATE_GAME_IN_PROGRESS then
____print(
nil,
((LOG_PREFIX .. " SpawnNextWave: пропуск — игра еще не началась (state: ") .. tostring(gameState)) .. ")"
)
self.isNextWaveScheduled = false
return
end
if not self.isGameStarted then
____print(nil, LOG_PREFIX .. " SpawnNextWave: пропуск — игра еще не началась (isGameStarted: false)")
self.isNextWaveScheduled = false
return
end
if GameRules:IsDaytime() then
self:handleNightEndedMusic()
____print(nil, LOG_PREFIX .. " SpawnNextWave: пропуск — сейчас день")
self.isNextWaveScheduled = false
return
end
if self.isWaveInProgress then
____print(nil, LOG_PREFIX .. " SpawnNextWave: пропуск — волна уже идёт")
self.isNextWaveScheduled = false
return
end
if self.currentNight == 0 then
____print(nil, LOG_PREFIX .. " SpawnNextWave: пропуск — ночь не установлена (currentNight: 0)")
self.isNextWaveScheduled = false
return
end
if self.nightWavesStoppedForNight then
____print(nil, LOG_PREFIX .. " SpawnNextWave: пропуск — волны ночи завершены (stopWavesAfter)")
self.isNextWaveScheduled = false
return
end
local waves = self:getWaves()
if #waves == 0 then
____print(
nil,
(LOG_PREFIX .. " SpawnNextWave: нет волн для ночи ") .. tostring(self.currentNight)
)
self.isNextWaveScheduled = false
return
end
self.isNextWaveScheduled = false
self.spawnedZombies = __TS__ArrayFilter(
self.spawnedZombies,
function(____, z) return z and IsValidEntity(z) and z:IsAlive() end
)
self.zombieCount = #self.spawnedZombies
if self.waveNumber >= #waves then
local lastWaveConfigPre = waves[#waves]
if lastWaveConfigPre and lastWaveConfigPre.stopWavesAfter then
self.nightWavesStoppedForNight = true
____print(nil, LOG_PREFIX .. " SpawnNextWave: пропуск — последняя волна с stopWavesAfter, повтора нет")
return
end
self.isRepeatingLastWave = true
local lastWaveConfig = waves[#waves]
self.isWaveInProgress = true
____print(
nil,
(((((LOG_PREFIX .. " 🔁 SpawnNextWave: все волны пройдены, повтор последней волны (ночь ") .. tostring(self.currentNight)) .. "), maxZombies=") .. tostring(lastWaveConfig.maxZombies)) .. ", осталось зомби от предыдущих волн: ") .. tostring(self.zombieCount)
)
self:spawnZombiesFromConfig(lastWaveConfig, "wave_point")
return
end
local waveConfig = waves[self.waveNumber + 1]
self.waveNumber = self.waveNumber + 1
self.isWaveInProgress = true
self:announceWaveStarted(#waves)
____print(
nil,
(((((((LOG_PREFIX .. " Волна старт: night=") .. tostring(self.currentNight)) .. " wave=") .. tostring(self.waveNumber)) .. "/") .. tostring(#waves)) .. " max=") .. tostring(waveConfig.maxZombies)
)
self:spawnZombiesFromConfig(waveConfig, "wave_point")
end
function WaveManager.prototype.SetCurrentNight(self, night)
local oldNight = self.currentNight
self:stopBattleMusic()
self.activeMusicGroupIndex = nil
self.nightEndMusicPlayed = false
self.currentNight = math.max(1, night)
self.waveNumber = 0
self.isRepeatingLastWave = false
self.night5RandomBossKillsSinceCycleStart = 0
self.nightWavesStoppedForNight = false
self.isNextWaveScheduled = false
self.isWaveInProgress = false
____print(
nil,
((((LOG_PREFIX .. " SetCurrentNight: ") .. tostring(oldNight)) .. " -> ") .. tostring(self.currentNight)) .. ", waveNumber сброшен на 0, isWaveInProgress сброшен"
)
end
function WaveManager.prototype.SetGameStarted(self)
if not self.isGameStarted then
self.isGameStarted = true
____print(nil, LOG_PREFIX .. " SetGameStarted: игра началась, волны могут спавниться")
end
end
function WaveManager.prototype.SetNextWaveTime(self, seconds)
self.nextWaveTime = math.max(10, seconds)
Timers:RemoveTimer("WaveTimer")
self:StartWaveTimer()
____print(
nil,
((LOG_PREFIX .. " SetNextWaveTime: ") .. tostring(self.nextWaveTime)) .. " сек"
)
end
function WaveManager.prototype.GetCurrentWave(self)
return self.waveNumber
end
function WaveManager.prototype.GetNextWaveTime(self)
return self.nextWaveTime
end
function WaveManager.prototype.SetNextWaveIndex(self, waveIndex)
self.isRepeatingLastWave = false
self.isNextWaveScheduled = false
if waveIndex <= 0 then
self.waveNumber = 0
____print(
nil,
((LOG_PREFIX .. " SetNextWaveIndex: сброшен на 0 (входной индекс: ") .. tostring(waveIndex)) .. ")"
)
return
end
local waves = self:getWaves()
if #waves == 0 then
self.waveNumber = 0
____print(nil, LOG_PREFIX .. " SetNextWaveIndex: нет волн, сброшен на 0")
return
end
self.waveNumber = math.min(waveIndex, #waves) - 1
____print(
nil,
((((((LOG_PREFIX .. " SetNextWaveIndex: установлен на ") .. tostring(self.waveNumber)) .. " (входной индекс: ") .. tostring(waveIndex)) .. ", всего волн: ") .. tostring(#waves)) .. ")"
)
end
function WaveManager.prototype.SetMaxWavesForNight(self, night, maxWaves)
if WAVE_SETTINGS[night] then
__TS__ArraySetLength(WAVE_SETTINGS[night].waves, maxWaves)
else
local lastNight = self:getLastConfiguredNight()
WAVE_SETTINGS[night] = {waves = WAVE_SETTINGS[lastNight].waves}
end
____print(
nil,
(((LOG_PREFIX .. " SetMaxWavesForNight: ночь ") .. tostring(night)) .. ", maxWaves=") .. tostring(maxWaves)
)
end
function WaveManager.prototype.GetMaxWavesForCurrentNight(self)
local ____opt_15 = WAVE_SETTINGS[self.currentNight]
return ____opt_15 and #____opt_15.waves or self.maxWavesInNight
end
function WaveManager.prototype.GetRemainingWaves(self)
local maxWaves = self:GetMaxWavesForCurrentNight()
return math.max(0, maxWaves - self.waveNumber)
end
function WaveManager.prototype.GetCurrentNight(self)
return self.currentNight
end
function WaveManager.prototype.getAliveSpawnedWaveUnits(self)
local out = {}
for ____, z in ipairs(self.spawnedZombies) do
if z and IsValidEntity(z) and z:IsAlive() then
out[#out + 1] = z
end
end
return out
end
function WaveManager.prototype.StartCutsceneReadyCheck(self, sceneId)
if not IsServer() then
return false
end
if not sceneId or #sceneId == 0 then
return false
end
local mgr = GameRules.CutsceneManager
if not mgr or mgr:isCutsceneActive() then
return false
end
self:openEndingCutsceneReadyCheck(sceneId)
return true
end
WaveManager.ABILITY_POINT = 16
WaveManager.ABILITY_UNIT_TARGET = 32
WaveManager.ABILITY_NO_TARGET = 4
WaveManager.WAVE_CREEP_ABILITY_COOLDOWN = 2
return ____exports