Files
Dota-Zombie-Invasion/scripts/vscripts/gameplay/hero_gravestone_respawn.lua
T
2026-05-29 15:11:31 +07:00

595 lines
22 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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