6d95836d11
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>
1778 lines
77 KiB
Lua
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
|