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("Boss spawned!", 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