495 lines
20 KiB
Lua
495 lines
20 KiB
Lua
local ____lualib = require("lualib_bundle")
|
||
local __TS__Class = ____lualib.__TS__Class
|
||
local __TS__ClassExtends = ____lualib.__TS__ClassExtends
|
||
local __TS__ArrayMap = ____lualib.__TS__ArrayMap
|
||
local __TS__ArrayPush = ____lualib.__TS__ArrayPush
|
||
local __TS__SparseArrayNew = ____lualib.__TS__SparseArrayNew
|
||
local __TS__SparseArrayPush = ____lualib.__TS__SparseArrayPush
|
||
local __TS__SparseArraySpread = ____lualib.__TS__SparseArraySpread
|
||
local __TS__Decorate = ____lualib.__TS__Decorate
|
||
local ____exports = {}
|
||
local modifier_boss_nevermore_requiem_barrage_casting
|
||
local ____nevermore_boss_requiem_bridge = require("ai.nevermore_boss_requiem_bridge")
|
||
local nevermoreIncrementRequiemCastCount = ____nevermore_boss_requiem_bridge.nevermoreIncrementRequiemCastCount
|
||
local ____dota_ts_adapter = require("lib.dota_ts_adapter")
|
||
local BaseAbility = ____dota_ts_adapter.BaseAbility
|
||
local BaseModifier = ____dota_ts_adapter.BaseModifier
|
||
local registerAbility = ____dota_ts_adapter.registerAbility
|
||
local registerModifier = ____dota_ts_adapter.registerModifier
|
||
local ____modifier_boss_nevermore_requiem_magic_resist_debuff = require("abilities.creep.modifier_boss_nevermore_requiem_magic_resist_debuff")
|
||
local modifier_boss_nevermore_requiem_magic_resist_debuff = ____modifier_boss_nevermore_requiem_magic_resist_debuff.modifier_boss_nevermore_requiem_magic_resist_debuff
|
||
--- Глобальный множитель урона залпа (подогнан под KV-бафф ~+60%).
|
||
local REQUIEM_BARRAGE_DAMAGE_MULT = 1.6
|
||
--- Доп. дальность волны за стадию HP босса (согласовано с KV wave_distance ×1.6).
|
||
local REQUIEM_PHASE_DISTANCE_BONUS = 192
|
||
____exports.boss_nevermore_requiem_barrage = __TS__Class()
|
||
local boss_nevermore_requiem_barrage = ____exports.boss_nevermore_requiem_barrage
|
||
boss_nevermore_requiem_barrage.name = "boss_nevermore_requiem_barrage"
|
||
boss_nevermore_requiem_barrage.____file_path = "scripts/vscripts/abilities/creep/boss_nevermore_requiem_barrage.lua"
|
||
__TS__ClassExtends(boss_nevermore_requiem_barrage, BaseAbility)
|
||
function boss_nevermore_requiem_barrage.prototype.____constructor(self, ...)
|
||
BaseAbility.prototype.____constructor(self, ...)
|
||
self.castSerial = 0
|
||
self.phasePreviewParticles = {}
|
||
self.phasePreviewRotationOffset = 0
|
||
self.activePreviewParticles = {}
|
||
self.gestureLoopSerial = 0
|
||
end
|
||
function boss_nevermore_requiem_barrage.prototype.Precache(self, context)
|
||
PrecacheResource("particle", "particles/units/heroes/hero_nevermore/nevermore_requiemofsouls_line.vpcf", context)
|
||
PrecacheResource("particle", "particles/boss_tinker_laser_preview_vector.vpcf", context)
|
||
PrecacheResource("soundfile", "soundevents/game_sounds_heroes/game_sounds_nevermore.vsndevts", context)
|
||
end
|
||
function boss_nevermore_requiem_barrage.prototype.GetChannelTime(self)
|
||
local channel = self:GetSpecialValueFor("channel_time")
|
||
return channel > 0 and channel or 3
|
||
end
|
||
function boss_nevermore_requiem_barrage.prototype.getBossPhase(self)
|
||
local caster = self:GetCaster()
|
||
if not caster or caster:IsNull() then
|
||
return 1
|
||
end
|
||
local hp = caster:GetHealthPercent()
|
||
if hp <= 25 then
|
||
return 4
|
||
end
|
||
if hp <= 50 then
|
||
return 3
|
||
end
|
||
if hp <= 75 then
|
||
return 2
|
||
end
|
||
return 1
|
||
end
|
||
function boss_nevermore_requiem_barrage.prototype.OnAbilityPhaseStart(self)
|
||
if not IsServer() then
|
||
return true
|
||
end
|
||
local caster = self:GetCaster()
|
||
local point = self:GetCursorPosition()
|
||
local toPoint = point - caster:GetAbsOrigin()
|
||
local baseDirection = toPoint:Length2D() < 1 and caster:GetForwardVector() or toPoint:Normalized()
|
||
self:clearPhasePreview()
|
||
self.phasePreviewRotationOffset = self:getWaveRotationOffset(1)
|
||
local previewDirections = self:buildBurstDirections(
|
||
baseDirection,
|
||
self.phasePreviewRotationOffset,
|
||
self:getBossPhase()
|
||
)
|
||
local previewDuration = self:GetCastPoint() > 0 and self:GetCastPoint() or 0.3
|
||
self.phasePreviewParticles = self:createWavePreview(previewDirections, previewDuration)
|
||
return true
|
||
end
|
||
function boss_nevermore_requiem_barrage.prototype.OnAbilityPhaseInterrupted(self)
|
||
if not IsServer() then
|
||
return
|
||
end
|
||
self:clearPhasePreview()
|
||
end
|
||
function boss_nevermore_requiem_barrage.prototype.OnSpellStart(self)
|
||
if not IsServer() then
|
||
return
|
||
end
|
||
local caster = self:GetCaster()
|
||
nevermoreIncrementRequiemCastCount(nil, caster)
|
||
local point = self:GetCursorPosition()
|
||
local toPoint = point - caster:GetAbsOrigin()
|
||
local baseDirection = toPoint:Length2D() < 1 and caster:GetForwardVector() or toPoint:Normalized()
|
||
self.castSerial = self.castSerial + 1
|
||
local currentCast = self.castSerial
|
||
local channelTime = self:GetChannelTime()
|
||
local phase = self:getBossPhase()
|
||
local baseWaveInterval = self:GetSpecialValueFor("wave_interval") > 0 and self:GetSpecialValueFor("wave_interval") or 1
|
||
local waveInterval = math.max(0.35, baseWaveInterval - (phase - 1) * 0.12)
|
||
local wavesCount = math.max(
|
||
1,
|
||
math.floor(channelTime / waveInterval)
|
||
)
|
||
local previewTime = self:GetSpecialValueFor("wave_preview_time") > 0 and self:GetSpecialValueFor("wave_preview_time") or 0.35
|
||
local waveDistanceKv = self:GetSpecialValueFor("wave_distance") > 0 and self:GetSpecialValueFor("wave_distance") or 850
|
||
local travelDist = waveDistanceKv + (phase - 1) * REQUIEM_PHASE_DISTANCE_BONUS
|
||
local waveTravelTimeMax = self:getMaxWaveTravelTime(travelDist)
|
||
self:clearPhasePreview()
|
||
caster:AddNewModifier(caster, self, modifier_boss_nevermore_requiem_barrage_casting.name, {duration = channelTime + 0.1})
|
||
EmitSoundOn("Hero_Nevermore.RequiemOfSoulsCast", caster)
|
||
self:startGestureLoop(caster)
|
||
local firstRotationOffset = self.phasePreviewRotationOffset
|
||
self:fireSoulWaveBurst(baseDirection, firstRotationOffset)
|
||
Timers:CreateTimer(
|
||
waveTravelTimeMax,
|
||
function()
|
||
self:destroyPreviewParticles(self.phasePreviewParticles)
|
||
self.phasePreviewParticles = {}
|
||
return nil
|
||
end
|
||
)
|
||
do
|
||
local i = 1
|
||
while i <= wavesCount do
|
||
local fireTime = i * waveInterval
|
||
local waveRotationOffset = self:getWaveRotationOffset(i + 1)
|
||
local wavePreviewParticles = {}
|
||
local wasPreviewShown = false
|
||
local speedsForWave
|
||
Timers:CreateTimer(
|
||
math.max(0, fireTime - previewTime),
|
||
function()
|
||
if self.castSerial ~= currentCast then
|
||
return nil
|
||
end
|
||
if not caster or caster:IsNull() or not caster:IsAlive() then
|
||
return nil
|
||
end
|
||
local previewDirections = self:buildBurstDirections(
|
||
baseDirection,
|
||
waveRotationOffset,
|
||
self:getBossPhase()
|
||
)
|
||
speedsForWave = __TS__ArrayMap(
|
||
previewDirections,
|
||
function() return self:sampleWaveSpeed() end
|
||
)
|
||
wavePreviewParticles = self:createWavePreview(previewDirections, previewTime, speedsForWave)
|
||
wasPreviewShown = true
|
||
return nil
|
||
end
|
||
)
|
||
Timers:CreateTimer(
|
||
fireTime,
|
||
function()
|
||
if not wasPreviewShown then
|
||
return nil
|
||
end
|
||
if not caster or caster:IsNull() or not caster:IsAlive() then
|
||
return nil
|
||
end
|
||
self:fireSoulWaveBurst(baseDirection, waveRotationOffset, speedsForWave)
|
||
Timers:CreateTimer(
|
||
waveTravelTimeMax,
|
||
function()
|
||
self:destroyPreviewParticles(wavePreviewParticles)
|
||
return nil
|
||
end
|
||
)
|
||
return nil
|
||
end
|
||
)
|
||
i = i + 1
|
||
end
|
||
end
|
||
end
|
||
function boss_nevermore_requiem_barrage.prototype.OnChannelFinish(self, _bInterrupted)
|
||
if not IsServer() then
|
||
return
|
||
end
|
||
self.castSerial = self.castSerial + 1
|
||
local caster = self:GetCaster()
|
||
self:clearPhasePreview()
|
||
self:clearAllActivePreviews()
|
||
self:stopGestureLoop(caster)
|
||
caster:RemoveModifierByName(modifier_boss_nevermore_requiem_barrage_casting.name)
|
||
StopSoundOn("Hero_Nevermore.RequiemOfSoulsCast", caster)
|
||
end
|
||
function boss_nevermore_requiem_barrage.prototype.startGestureLoop(self, caster)
|
||
self.gestureLoopSerial = self.gestureLoopSerial + 1
|
||
local serial = self.gestureLoopSerial
|
||
local interval = 0.45
|
||
local tick
|
||
tick = function()
|
||
if self.gestureLoopSerial ~= serial then
|
||
return
|
||
end
|
||
if not caster or caster:IsNull() or not caster:IsAlive() then
|
||
return
|
||
end
|
||
if not caster:HasModifier(modifier_boss_nevermore_requiem_barrage_casting.name) then
|
||
return
|
||
end
|
||
caster:StartGestureWithPlaybackRate(ACT_DOTA_RAZE_1, 1)
|
||
Timers:CreateTimer(
|
||
interval,
|
||
function()
|
||
tick(nil)
|
||
return nil
|
||
end
|
||
)
|
||
end
|
||
tick(nil)
|
||
end
|
||
function boss_nevermore_requiem_barrage.prototype.stopGestureLoop(self, caster)
|
||
self.gestureLoopSerial = self.gestureLoopSerial + 1
|
||
if not caster or caster:IsNull() then
|
||
return
|
||
end
|
||
caster:FadeGesture(ACT_DOTA_RAZE_1)
|
||
end
|
||
function boss_nevermore_requiem_barrage.prototype.getWaveSpeedBounds(self)
|
||
local base = self:GetSpecialValueFor("wave_speed") > 0 and self:GetSpecialValueFor("wave_speed") or 800
|
||
local minKv = self:GetSpecialValueFor("wave_speed_min")
|
||
local maxKv = self:GetSpecialValueFor("wave_speed_max")
|
||
if minKv > 0 and maxKv > 0 and maxKv >= minKv then
|
||
return {minKv, maxKv}
|
||
end
|
||
local lo = math.max(120, base * 0.72)
|
||
local hi = base * 1.32
|
||
return {lo, hi}
|
||
end
|
||
function boss_nevermore_requiem_barrage.prototype.sampleWaveSpeed(self)
|
||
local mn, mx = unpack(self:getWaveSpeedBounds())
|
||
return RandomFloat(mn, mx)
|
||
end
|
||
function boss_nevermore_requiem_barrage.prototype.getMaxWaveTravelTime(self, distance)
|
||
local mn, _ = unpack(self:getWaveSpeedBounds())
|
||
return distance / mn
|
||
end
|
||
function boss_nevermore_requiem_barrage.prototype.fireSoulWaveBurst(self, baseDirection, rotationOffset, precomputedSpeeds)
|
||
local phase = self:getBossPhase()
|
||
local directions = self:buildBurstDirections(baseDirection, rotationOffset, phase)
|
||
local waveDistanceBase = self:GetSpecialValueFor("wave_distance") > 0 and self:GetSpecialValueFor("wave_distance") or 850
|
||
local startRadius = self:GetSpecialValueFor("wave_width_start") > 0 and self:GetSpecialValueFor("wave_width_start") or 45
|
||
local endRadius = self:GetSpecialValueFor("wave_width_end") > 0 and self:GetSpecialValueFor("wave_width_end") or 45
|
||
local distance = waveDistanceBase + (phase - 1) * REQUIEM_PHASE_DISTANCE_BONUS
|
||
do
|
||
local i = 0
|
||
while i < #directions do
|
||
local direction = directions[i + 1]
|
||
local spd = precomputedSpeeds ~= nil and precomputedSpeeds[i + 1] ~= nil and precomputedSpeeds[i + 1] or self:sampleWaveSpeed()
|
||
self:fireSoulWave(
|
||
direction,
|
||
distance,
|
||
spd,
|
||
startRadius,
|
||
endRadius
|
||
)
|
||
i = i + 1
|
||
end
|
||
end
|
||
EmitSoundOn(
|
||
"Hero_Nevermore.RequiemOfSouls",
|
||
self:GetCaster()
|
||
)
|
||
end
|
||
function boss_nevermore_requiem_barrage.prototype.getWaveRotationOffset(self, waveIndex)
|
||
local randomOffset = RandomInt(0, 359)
|
||
local waveOffset = waveIndex * 17
|
||
return (randomOffset + waveOffset) % 360
|
||
end
|
||
function boss_nevermore_requiem_barrage.prototype.buildBurstDirections(self, baseDirection, rotationOffset, phase)
|
||
local dirs = {}
|
||
local baseAngles = {
|
||
0,
|
||
90,
|
||
180,
|
||
270,
|
||
45,
|
||
135,
|
||
225,
|
||
315
|
||
}
|
||
local phaseAngles = {}
|
||
if phase >= 1 then
|
||
__TS__ArrayPush(
|
||
phaseAngles,
|
||
22.5,
|
||
112.5,
|
||
202.5,
|
||
292.5
|
||
)
|
||
end
|
||
if phase >= 2 then
|
||
__TS__ArrayPush(
|
||
phaseAngles,
|
||
30,
|
||
120,
|
||
210,
|
||
300
|
||
)
|
||
end
|
||
if phase >= 3 then
|
||
__TS__ArrayPush(
|
||
phaseAngles,
|
||
67.5,
|
||
157.5,
|
||
247.5,
|
||
337.5
|
||
)
|
||
end
|
||
if phase >= 4 then
|
||
__TS__ArrayPush(
|
||
phaseAngles,
|
||
15,
|
||
105,
|
||
195,
|
||
285
|
||
)
|
||
end
|
||
local ____array_0 = __TS__SparseArrayNew(unpack(baseAngles))
|
||
__TS__SparseArrayPush(
|
||
____array_0,
|
||
unpack(phaseAngles)
|
||
)
|
||
local angles = {__TS__SparseArraySpread(____array_0)}
|
||
for ____, angle in ipairs(angles) do
|
||
local q = QAngle(0, angle + rotationOffset, 0)
|
||
dirs[#dirs + 1] = RotatePosition(
|
||
Vector(0, 0, 0),
|
||
q,
|
||
baseDirection
|
||
)
|
||
end
|
||
return dirs
|
||
end
|
||
function boss_nevermore_requiem_barrage.prototype.createWavePreview(self, directions, previewDuration, speeds)
|
||
local caster = self:GetCaster()
|
||
local phase = self:getBossPhase()
|
||
local distance = (self:GetSpecialValueFor("wave_distance") > 0 and self:GetSpecialValueFor("wave_distance") or 850) + (phase - 1) * REQUIEM_PHASE_DISTANCE_BONUS
|
||
local defaultSpeed = self:GetSpecialValueFor("wave_speed") > 0 and self:GetSpecialValueFor("wave_speed") or 800
|
||
local casterPos = caster:GetAbsOrigin()
|
||
local casterZ = casterPos.z
|
||
local particles = {}
|
||
do
|
||
local i = 0
|
||
while i < #directions do
|
||
local direction = directions[i + 1]
|
||
local spd = speeds ~= nil and speeds[i + 1] ~= nil and speeds[i + 1] or defaultSpeed
|
||
local travelTime = distance / spd
|
||
local startPos = casterPos + direction * 110
|
||
startPos.z = casterZ
|
||
local endPos = startPos + direction * distance
|
||
endPos.z = casterZ
|
||
local previewFx = ParticleManager:CreateParticle("particles/boss_tinker_laser_preview_vector.vpcf", PATTACH_WORLDORIGIN, nil)
|
||
ParticleManager:SetParticleControl(previewFx, 0, startPos)
|
||
ParticleManager:SetParticleControl(previewFx, 1, endPos)
|
||
ParticleManager:SetParticleControl(
|
||
previewFx,
|
||
2,
|
||
Vector(
|
||
math.max(previewDuration, travelTime),
|
||
0,
|
||
0
|
||
)
|
||
)
|
||
particles[#particles + 1] = previewFx
|
||
local ____self_activePreviewParticles_1 = self.activePreviewParticles
|
||
____self_activePreviewParticles_1[#____self_activePreviewParticles_1 + 1] = previewFx
|
||
i = i + 1
|
||
end
|
||
end
|
||
return particles
|
||
end
|
||
function boss_nevermore_requiem_barrage.prototype.destroyPreviewParticles(self, particles)
|
||
if #particles == 0 then
|
||
return
|
||
end
|
||
local toRemove = {}
|
||
for ____, p in ipairs(particles) do
|
||
toRemove[p] = true
|
||
end
|
||
for ____, particle in ipairs(particles) do
|
||
ParticleManager:DestroyParticle(particle, false)
|
||
ParticleManager:ReleaseParticleIndex(particle)
|
||
end
|
||
local nextActive = {}
|
||
for ____, p in ipairs(self.activePreviewParticles) do
|
||
if not (toRemove[p] ~= nil) then
|
||
nextActive[#nextActive + 1] = p
|
||
end
|
||
end
|
||
self.activePreviewParticles = nextActive
|
||
end
|
||
function boss_nevermore_requiem_barrage.prototype.clearPhasePreview(self)
|
||
self:destroyPreviewParticles(self.phasePreviewParticles)
|
||
self.phasePreviewParticles = {}
|
||
end
|
||
function boss_nevermore_requiem_barrage.prototype.clearAllActivePreviews(self)
|
||
self:destroyPreviewParticles(self.activePreviewParticles)
|
||
self.activePreviewParticles = {}
|
||
end
|
||
function boss_nevermore_requiem_barrage.prototype.fireSoulWave(self, direction, distance, speed, startRadius, endRadius)
|
||
local caster = self:GetCaster()
|
||
local casterPos = caster:GetAbsOrigin()
|
||
local startPos = casterPos + direction * 110
|
||
startPos.z = casterPos.z
|
||
local velocity = direction * speed
|
||
velocity.z = 0
|
||
ProjectileManager:CreateLinearProjectile({
|
||
Ability = self,
|
||
EffectName = "",
|
||
vSpawnOrigin = startPos,
|
||
fDistance = distance,
|
||
fStartRadius = startRadius,
|
||
fEndRadius = endRadius,
|
||
Source = caster,
|
||
bHasFrontalCone = false,
|
||
iUnitTargetTeam = DOTA_UNIT_TARGET_TEAM_ENEMY,
|
||
iUnitTargetFlags = DOTA_UNIT_TARGET_FLAG_NONE,
|
||
iUnitTargetType = bit.bor(DOTA_UNIT_TARGET_HERO, DOTA_UNIT_TARGET_BASIC),
|
||
vVelocity = velocity,
|
||
bProvidesVision = false
|
||
})
|
||
local travelTime = distance / speed
|
||
local lineParticle = ParticleManager:CreateParticle("particles/units/heroes/hero_nevermore/nevermore_requiemofsouls_line.vpcf", PATTACH_WORLDORIGIN, nil)
|
||
ParticleManager:SetParticleControl(lineParticle, 0, startPos)
|
||
ParticleManager:SetParticleControl(lineParticle, 1, velocity)
|
||
ParticleManager:SetParticleControl(
|
||
lineParticle,
|
||
2,
|
||
Vector(0, travelTime, 0)
|
||
)
|
||
ParticleManager:ReleaseParticleIndex(lineParticle)
|
||
end
|
||
function boss_nevermore_requiem_barrage.prototype.OnProjectileHit(self, target, _location)
|
||
if not target then
|
||
return false
|
||
end
|
||
local caster = self:GetCaster()
|
||
local phase = self:getBossPhase()
|
||
local baseDamage = caster:GetAverageTrueAttackDamage(target)
|
||
local damage = baseDamage * (1 + (phase - 1) * 0.28) * REQUIEM_BARRAGE_DAMAGE_MULT
|
||
ApplyDamage({
|
||
victim = target,
|
||
attacker = caster,
|
||
damage = damage,
|
||
damage_type = DAMAGE_TYPE_MAGICAL,
|
||
ability = self
|
||
})
|
||
local debuff = target:AddNewModifier(caster, self, modifier_boss_nevermore_requiem_magic_resist_debuff.name, {duration = -1})
|
||
if debuff ~= nil then
|
||
debuff:SetStackCount(debuff:GetStackCount() + 1)
|
||
end
|
||
return false
|
||
end
|
||
boss_nevermore_requiem_barrage = __TS__Decorate(
|
||
boss_nevermore_requiem_barrage,
|
||
boss_nevermore_requiem_barrage,
|
||
{registerAbility(nil)},
|
||
{kind = "class", name = "boss_nevermore_requiem_barrage"}
|
||
)
|
||
____exports.boss_nevermore_requiem_barrage = boss_nevermore_requiem_barrage
|
||
modifier_boss_nevermore_requiem_barrage_casting = __TS__Class()
|
||
modifier_boss_nevermore_requiem_barrage_casting.name = "modifier_boss_nevermore_requiem_barrage_casting"
|
||
modifier_boss_nevermore_requiem_barrage_casting.____file_path = "scripts/vscripts/abilities/creep/boss_nevermore_requiem_barrage.lua"
|
||
__TS__ClassExtends(modifier_boss_nevermore_requiem_barrage_casting, BaseModifier)
|
||
function modifier_boss_nevermore_requiem_barrage_casting.prototype.IsHidden(self)
|
||
return true
|
||
end
|
||
function modifier_boss_nevermore_requiem_barrage_casting.prototype.IsPurgable(self)
|
||
return false
|
||
end
|
||
function modifier_boss_nevermore_requiem_barrage_casting.prototype.CheckState(self)
|
||
return {
|
||
[MODIFIER_STATE_STUNNED] = false,
|
||
[MODIFIER_STATE_DISARMED] = true,
|
||
[MODIFIER_STATE_SILENCED] = false,
|
||
[MODIFIER_STATE_ROOTED] = true,
|
||
[MODIFIER_STATE_COMMAND_RESTRICTED] = true
|
||
}
|
||
end
|
||
modifier_boss_nevermore_requiem_barrage_casting = __TS__Decorate(
|
||
modifier_boss_nevermore_requiem_barrage_casting,
|
||
modifier_boss_nevermore_requiem_barrage_casting,
|
||
{registerModifier(nil)},
|
||
{kind = "class", name = "modifier_boss_nevermore_requiem_barrage_casting"}
|
||
)
|
||
return ____exports
|