local ____lualib = require("lualib_bundle") local __TS__Class = ____lualib.__TS__Class local Map = ____lualib.Map local __TS__New = ____lualib.__TS__New local __TS__ClassExtends = ____lualib.__TS__ClassExtends local __TS__Decorate = ____lualib.__TS__Decorate local ____exports = {} local modifier_hero_gravestone_charger local ____dota_ts_adapter = require("lib.dota_ts_adapter") local BaseModifier = ____dota_ts_adapter.BaseModifier local registerModifier = ____dota_ts_adapter.registerModifier local ____invul_box = require("abilities.invul_box") local invul_box = ____invul_box.invul_box local ____game_stats_tracker = require("game_stats_tracker") local GameStatsTracker = ____game_stats_tracker.GameStatsTracker local ____player_connection_state = require("utils.player_connection_state") local DOTA_CONNECTION_STATE = ____player_connection_state.DOTA_CONNECTION_STATE --- Базовый юнит из npc_units_custom (хитбокс/инвул); визуал задаём моделью надгробия. local GRAVESTONE_UNIT_NAME = "npc_grave" local GRAVESTONE_MODEL = "models/items/wraith_king/arcana/wk_arcana_tombstone.vmdl" --- Как в CutsceneFunctions — targetname живого босса на арене. local NEVERMORE_BOSS_TARGETNAME = "npc_boss_nevermore" --- infinity_levels: дроп item_tombstone_respawn + PICKUP_ITEM → BeginChannel(5). -- Здесь то же время ожидания, но без предмета и без поднятия — только стояние в радиусе (как шрайн/defension). local GRAVESTONE_RADIUS = 300 local GRAVESTONE_CAPTURE_TIME = 5 local INTERVAL = 0.03 --- Маркер на земле как в infinity_levels (GameMode precache Muerta). local MUERTA_GRAVE_MARKER_PARTICLE = "particles/econ/items/muerta/muerta_gravemarker_lvl1.vpcf" --- Последовательность модели надгробия при захвате (тряска). Должна быть в .vmdl (часто у tombstone/Muerta). local GRAVESTONE_CAPTURE_SEQUENCE = "sk_tombstone_reincarnation_no_reform" --- FindByName — только targetname в карте / SetEntityName; босс из KV — npc_dota_creature, -- поэтому дублируем поиск по GetUnitName у живых юнитов в большом радиусе от точки арены. local function findLiveNevermoreBoss(self) local byTargetName = Entities:FindByName(nil, NEVERMORE_BOSS_TARGETNAME) if byTargetName ~= nil and IsValidEntity(byTargetName) and byTargetName:IsAlive() then return byTargetName end local searchOrigin = Vector(0, 0, 0) local spawnPt = Entities:FindByName(nil, "point_boss_spawn_point") if spawnPt ~= nil then searchOrigin = spawnPt:GetAbsOrigin() end local units = FindUnitsInRadius( DOTA_TEAM_NEUTRALS, searchOrigin, nil, 25000, DOTA_UNIT_TARGET_TEAM_BOTH, DOTA_UNIT_TARGET_ALL, DOTA_UNIT_TARGET_FLAG_NONE, FIND_ANY_ORDER, false ) do local i = 0 while i < #units do local u = units[i + 1] if u ~= nil and IsValidEntity(u) and u:IsAlive() and u:GetUnitName() == NEVERMORE_BOSS_TARGETNAME then return u end i = i + 1 end end return nil end local function isNevermoreBossFightActive(self) return findLiveNevermoreBoss(nil) ~= nil end --- Radiant, сейчас реально в матче (CONNECTED). Ливнувших/дропнутых не считаем. local function isGoodGuysPlayerCountedForGravestoneRules(self, playerId) if not PlayerResource:IsValidPlayerID(playerId) or not PlayerResource:IsValidPlayer(playerId) then return false end if PlayerResource:GetTeam(playerId) ~= DOTA_TEAM_GOODGUYS then return false end return PlayerResource:GetConnectionState(playerId) == DOTA_CONNECTION_STATE.CONNECTED end local function countGoodGuysPlayersInMatch(self) local n = 0 do local i = 0 while i < DOTA_MAX_PLAYERS do local pid = i if isGoodGuysPlayerCountedForGravestoneRules(nil, pid) then n = n + 1 end i = i + 1 end end return n end --- Сколько героев Radiant из учитываемых слотов реально живы (истина для проверки вайпа). local function countAliveGoodGuysHeroesInMatch(self) local n = 0 do local i = 0 while i < DOTA_MAX_PLAYERS do do local pid = i if not isGoodGuysPlayerCountedForGravestoneRules(nil, pid) then goto __continue15 end local hero = PlayerResource:GetSelectedHeroEntity(pid) if not hero or not IsValidEntity(hero) then goto __continue15 end if not hero:IsHero() or not hero:IsRealHero() or hero:IsIllusion() then goto __continue15 end if hero:GetTeamNumber() ~= DOTA_TEAM_GOODGUYS then goto __continue15 end if hero:IsAlive() then n = n + 1 end end ::__continue15:: i = i + 1 end end return n end --- Можно ли вообще поднимать героя респавном (надгробие не ставим и не завершаем заряд, если нет). local function isHeroEligibleForGravestoneRespawn(self, hero) if hero:WillReincarnate() then return false end if hero:IsReincarnating() then return false end return true end function ____exports.precacheHeroGravestoneParticles(self, context) PrecacheResource("model", GRAVESTONE_MODEL, context) PrecacheResource("particle", MUERTA_GRAVE_MARKER_PARTICLE, context) PrecacheResource("particle", "particles/shrine/capture_point_ring_overthrow.vpcf", context) PrecacheResource("particle", "particles/shrine/capture_point_ring_clock_overthrow.vpcf", context) PrecacheResource("particle", "particles/units/heroes/hero_skeletonking/wraith_king_reincarnate.vpcf", context) end --- Надгробие: живой союзник стоит в зоне — копится прогресс (не клик по предмету, как в infinity_levels). ____exports.HeroGravestoneRespawn = __TS__Class() local HeroGravestoneRespawn = ____exports.HeroGravestoneRespawn HeroGravestoneRespawn.name = "HeroGravestoneRespawn" HeroGravestoneRespawn.____file_path = "scripts/vscripts/gameplay/hero_gravestone_respawn.lua" function HeroGravestoneRespawn.prototype.____constructor(self) end function HeroGravestoneRespawn.initialize(self) ListenToGameEvent( "entity_killed", function(event) return self:onEntityKilled(event) end, nil ) ListenToGameEvent( "dota_player_spawned", function(event) return self:onPlayerSpawned(event) end, nil ) end function HeroGravestoneRespawn.registerGravestone(self, playerId, entIndex) local prev = self.graveEntIndexByPlayer:get(playerId) if prev ~= nil and prev ~= entIndex then local old = EntIndexToHScript(prev) if old and IsValidEntity(old) then UTIL_Remove(old) end end self.graveEntIndexByPlayer:set(playerId, entIndex) end function HeroGravestoneRespawn.unregisterGravestone(self, playerId) self.graveEntIndexByPlayer:delete(playerId) end function HeroGravestoneRespawn.hasGravestoneForPlayer(self, playerId) return self.graveEntIndexByPlayer:has(playerId) end function HeroGravestoneRespawn.removeAllGravestones(self) self.graveEntIndexByPlayer:forEach(function(____, entIndex) local u = EntIndexToHScript(entIndex) if u and IsValidEntity(u) then UTIL_Remove(u) end end) self.graveEntIndexByPlayer:clear() end function HeroGravestoneRespawn.pruneGravestonesForPlayersOutOfMatch(self) local toRemove = {} self.graveEntIndexByPlayer:forEach(function(____, _entIndex, playerId) if not isGoodGuysPlayerCountedForGravestoneRules(nil, playerId) then toRemove[#toRemove + 1] = playerId end end) for ____, pid in ipairs(toRemove) do local entIndex = self.graveEntIndexByPlayer:get(pid) self:unregisterGravestone(pid) if entIndex ~= nil then local u = EntIndexToHScript(entIndex) if u and IsValidEntity(u) then UTIL_Remove(u) end end end end function HeroGravestoneRespawn.maybePruneGravestonesForPlayersOutOfMatch(self) local t = GameRules:GetGameTime() if t - self.lastOutOfMatchPruneGameTime < 1 then return end self.lastOutOfMatchPruneGameTime = t self:pruneGravestonesForPlayersOutOfMatch() end function HeroGravestoneRespawn.onPlayerSpawned(self, event) local entIndex = self.graveEntIndexByPlayer:get(event.PlayerID) if entIndex == nil then return end self.graveEntIndexByPlayer:delete(event.PlayerID) local unit = EntIndexToHScript(entIndex) if unit and IsValidEntity(unit) then UTIL_Remove(unit) end end function HeroGravestoneRespawn.onEntityKilled(self, event) if not IsServer() then return end if GameRules:State_Get() ~= DOTA_GAMERULES_STATE_GAME_IN_PROGRESS then return end local killed = EntIndexToHScript(event.entindex_killed) if not killed or not IsValidEntity(killed) then return end if killed:GetUnitName() == NEVERMORE_BOSS_TARGETNAME then self:removeAllGravestones() return end if not killed:IsHero() then return end if not killed:IsRealHero() then return end if killed:IsIllusion() then return end local playerId = killed:GetPlayerOwnerID() if playerId < 0 or not PlayerResource:IsValidPlayerID(playerId) then return end if killed:GetTeamNumber() ~= DOTA_TEAM_GOODGUYS then return end local heroDead = killed local cutscene = GameRules.CutsceneManager if cutscene ~= nil and cutscene:isCutsceneActive() then return end if not isNevermoreBossFightActive(nil) then return end local origin = killed:GetAbsOrigin() local ground = GetGroundPosition(origin, killed) local grave = CreateUnitByName( GRAVESTONE_UNIT_NAME, ground, true, nil, nil, DOTA_TEAM_NEUTRALS ) if not grave or not IsValidEntity(grave) then return end FindClearSpaceForUnit(grave, ground, true) grave:AddAbility(invul_box.name) grave:SetModel(GRAVESTONE_MODEL) grave:SetUnitName(GRAVESTONE_UNIT_NAME) grave:SetEntityName(GRAVESTONE_UNIT_NAME) grave:AddNewModifier( grave, getModifierSourceAbility(nil, grave), modifier_hero_gravestone_charger.name, {ownerPlayerId = playerId} ) self:registerGravestone( playerId, grave:entindex() ) self:pruneGravestonesForPlayersOutOfMatch() self:pruneGravestonesForAliveHeroes() local players = countGoodGuysPlayersInMatch(nil) if players > 0 and self.graveEntIndexByPlayer.size == players and countAliveGoodGuysHeroesInMatch(nil) == 0 then GameStatsTracker:getInstance():onDefeat(function() GameRules:SetGameWinner(DOTA_TEAM_BADGUYS) end) end end function HeroGravestoneRespawn.pruneGravestonesForAliveHeroes(self) local toRemove = {} self.graveEntIndexByPlayer:forEach(function(____, _entIndex, playerId) local hero = PlayerResource:GetSelectedHeroEntity(playerId) if hero and IsValidEntity(hero) and hero:IsHero() and hero:IsRealHero() and not hero:IsIllusion() and hero:IsAlive() then toRemove[#toRemove + 1] = playerId end end) for ____, pid in ipairs(toRemove) do local entIndex = self.graveEntIndexByPlayer:get(pid) self:unregisterGravestone(pid) if entIndex ~= nil then local u = EntIndexToHScript(entIndex) if u and IsValidEntity(u) then UTIL_Remove(u) end end end end HeroGravestoneRespawn.graveEntIndexByPlayer = __TS__New(Map) HeroGravestoneRespawn.lastOutOfMatchPruneGameTime = -1000000000 modifier_hero_gravestone_charger = __TS__Class() modifier_hero_gravestone_charger.name = "modifier_hero_gravestone_charger" modifier_hero_gravestone_charger.____file_path = "scripts/vscripts/gameplay/hero_gravestone_respawn.lua" __TS__ClassExtends(modifier_hero_gravestone_charger, BaseModifier) function modifier_hero_gravestone_charger.prototype.____constructor(self, ...) BaseModifier.prototype.____constructor(self, ...) self.vPosition = Vector(0, 0, 0) self.progress = 0 self.timer = 0 self.captureSequenceActive = false end function modifier_hero_gravestone_charger.prototype.IsHidden(self) return true end function modifier_hero_gravestone_charger.prototype.IsPurgable(self) return false end function modifier_hero_gravestone_charger.prototype.OnCreated(self, params) if not IsServer() then return end self.ownerPlayerId = params and params.ownerPlayerId or -1 self.vPosition = self:GetParent():GetAbsOrigin() self:StartIntervalThink(INTERVAL) self.vision = AddFOWViewer( DOTA_TEAM_GOODGUYS, self.vPosition, GRAVESTONE_RADIUS, 99999, true ) local parent = self:GetParent() self.muertaAmbient = ParticleManager:CreateParticle(MUERTA_GRAVE_MARKER_PARTICLE, PATTACH_ABSORIGIN_FOLLOW, parent) ParticleManager:SetParticleControl( self.muertaAmbient, 0, parent:GetAbsOrigin() ) end function modifier_hero_gravestone_charger.prototype.OnDestroy(self) if not IsServer() then return end if self.vision ~= nil then RemoveFOWViewer(DOTA_TEAM_GOODGUYS, self.vision) end if self.muertaAmbient ~= nil then ParticleManager:DestroyParticle(self.muertaAmbient, false) ParticleManager:ReleaseParticleIndex(self.muertaAmbient) end if self.baseParticle ~= nil then ParticleManager:DestroyParticle(self.baseParticle, false) ParticleManager:ReleaseParticleIndex(self.baseParticle) end if self.clockParticle ~= nil then ParticleManager:DestroyParticle(self.clockParticle, true) ParticleManager:ReleaseParticleIndex(self.clockParticle) end end function modifier_hero_gravestone_charger.prototype.checkCapturePointParticles(self) local parent = self:GetParent() if not self.baseParticle then self.baseParticle = ParticleManager:CreateParticle("particles/shrine/capture_point_ring_overthrow.vpcf", PATTACH_WORLDORIGIN, parent) ParticleManager:SetParticleControl( self.baseParticle, 0, parent:GetAbsOrigin() ) ParticleManager:SetParticleControl( self.baseParticle, 3, Vector(150, 150, 150) ) ParticleManager:SetParticleControl( self.baseParticle, 9, Vector(GRAVESTONE_RADIUS, 0, 0) ) else ParticleManager:SetParticleControl( self.baseParticle, 0, parent:GetAbsOrigin() ) end if self.progress <= 0 or self.progress >= 1 then if self.clockParticle ~= nil then ParticleManager:DestroyParticle(self.clockParticle, true) ParticleManager:ReleaseParticleIndex(self.clockParticle) self.clockParticle = nil end return end local team = DOTA_TEAM_GOODGUYS local origin = parent:GetAbsOrigin() local z = origin.z + 75 if not self.clockParticle then self.clockParticle = ParticleManager:CreateParticleForTeam("particles/shrine/capture_point_ring_clock_overthrow.vpcf", PATTACH_WORLDORIGIN, parent, team) ParticleManager:SetParticleControl( self.clockParticle, 0, Vector(origin.x, origin.y, z) ) ParticleManager:SetParticleControl( self.clockParticle, 11, Vector(0, 0, 1) ) end ParticleManager:SetParticleControl( self.clockParticle, 3, Vector(220, 220, 220) ) ParticleManager:SetParticleControl( self.clockParticle, 9, Vector(GRAVESTONE_RADIUS, 0, 0) ) ParticleManager:SetParticleControl( self.clockParticle, 17, Vector(self.progress, 0, 0) ) ParticleManager:SetParticleControl( self.clockParticle, 0, Vector(origin.x, origin.y, z) ) end function modifier_hero_gravestone_charger.prototype.updateCaptureModelAnimation(self, parent, isCapturing) if isCapturing then if not self.captureSequenceActive then parent:SetSequence(GRAVESTONE_CAPTURE_SEQUENCE) self.captureSequenceActive = true elseif parent:IsSequenceFinished() then parent:ResetSequence(GRAVESTONE_CAPTURE_SEQUENCE) end else if self.captureSequenceActive then parent:StopAnimation() self.captureSequenceActive = false end end end function modifier_hero_gravestone_charger.prototype.OnIntervalThink(self) if not IsServer() then return end ____exports.HeroGravestoneRespawn:maybePruneGravestonesForPlayersOutOfMatch() local parent = self:GetParent() if not parent or not IsValidEntity(parent) then return end if not isNevermoreBossFightActive(nil) then ____exports.HeroGravestoneRespawn:unregisterGravestone(self.ownerPlayerId) UTIL_Remove(parent) return end local boundHero = PlayerResource:GetSelectedHeroEntity(self.ownerPlayerId) if boundHero and IsValidEntity(boundHero) and not boundHero:IsAlive() and not isHeroEligibleForGravestoneRespawn(nil, boundHero) then ____exports.HeroGravestoneRespawn:unregisterGravestone(self.ownerPlayerId) UTIL_Remove(parent) return end self.vPosition = parent:GetAbsOrigin() local heroesInRadius = FindUnitsInRadius( DOTA_TEAM_NEUTRALS, self.vPosition, nil, GRAVESTONE_RADIUS, DOTA_UNIT_TARGET_TEAM_ENEMY, DOTA_UNIT_TARGET_HERO, bit.bor( bit.bor( bit.bor( bit.bor(DOTA_UNIT_TARGET_FLAG_NOT_CREEP_HERO, DOTA_UNIT_TARGET_FLAG_MAGIC_IMMUNE_ENEMIES), DOTA_UNIT_TARGET_FLAG_INVULNERABLE ), DOTA_UNIT_TARGET_FLAG_NOT_ILLUSIONS ), DOTA_UNIT_TARGET_FLAG_OUT_OF_WORLD ), FIND_ANY_ORDER, false ) local heroes = {} local teamRegister = {} for ____, unit in ipairs(heroesInRadius) do if unit:IsHero() and unit:IsAlive() and unit:IsRealHero() then local team = unit:GetTeamNumber() if not teamRegister[team] then teamRegister[team] = true heroes[#heroes + 1] = unit end end end if #heroes == 1 then self.timer = self.timer + INTERVAL self.progress = self.timer / GRAVESTONE_CAPTURE_TIME if self.progress >= 1 then self:completeResurrection() return end else self.timer = math.max(0, self.timer - INTERVAL) self.progress = self.timer / GRAVESTONE_CAPTURE_TIME end local isCapturing = #heroes == 1 and self.progress > 0 and self.progress < 1 self:updateCaptureModelAnimation(parent, isCapturing) self:checkCapturePointParticles() end function modifier_hero_gravestone_charger.prototype.completeResurrection(self) local parent = self:GetParent() if not parent or not IsValidEntity(parent) then return end if not isNevermoreBossFightActive(nil) then ____exports.HeroGravestoneRespawn:unregisterGravestone(self.ownerPlayerId) UTIL_Remove(parent) return end local hero = PlayerResource:GetSelectedHeroEntity(self.ownerPlayerId) if not hero or not IsValidEntity(hero) then ____exports.HeroGravestoneRespawn:unregisterGravestone(self.ownerPlayerId) UTIL_Remove(parent) return end if hero:IsAlive() then ____exports.HeroGravestoneRespawn:unregisterGravestone(self.ownerPlayerId) UTIL_Remove(parent) return end if not isHeroEligibleForGravestoneRespawn(nil, hero) then ____exports.HeroGravestoneRespawn:unregisterGravestone(self.ownerPlayerId) UTIL_Remove(parent) return end local pos = parent:GetAbsOrigin() hero:SetRespawnPosition(pos) do local function ____catch() ____exports.HeroGravestoneRespawn:unregisterGravestone(self.ownerPlayerId) UTIL_Remove(parent) return true end local ____try, ____hasReturned, ____returnValue = pcall(function() hero:RespawnHero(false, false) end) if not ____try then ____hasReturned, ____returnValue = ____catch() end if ____hasReturned then return ____returnValue end end FindClearSpaceForUnit(hero, pos, true) local pfx = ParticleManager:CreateParticle("particles/units/heroes/hero_skeletonking/wraith_king_reincarnate.vpcf", PATTACH_ABSORIGIN_FOLLOW, hero) ParticleManager:SetParticleControl( pfx, 0, hero:GetAbsOrigin() ) ParticleManager:ReleaseParticleIndex(pfx) parent:EmitSound("Outpost.Captured") ____exports.HeroGravestoneRespawn:unregisterGravestone(self.ownerPlayerId) UTIL_Remove(parent) end modifier_hero_gravestone_charger = __TS__Decorate( modifier_hero_gravestone_charger, modifier_hero_gravestone_charger, {registerModifier(nil)}, {kind = "class", name = "modifier_hero_gravestone_charger"} ) return ____exports