local ____lualib = require("lualib_bundle") local __TS__NumberToFixed = ____lualib.__TS__NumberToFixed local __TS__ArrayFind = ____lualib.__TS__ArrayFind local ____exports = {} local getPredictedGroundPosition, getTargetHorizontalForward, getAimInFrontOfPredictedTarget, getBossForwardAimPoint, addCoilWaveAimJitter, getNevermoreCoilWavePhase, canCoilWaveHitAnyEnemy, canCoilBeamHitAnyEnemy, canHubCrossburstHitAnyEnemy, debugLog, NevermoreBossThink, tryIdleTripleCoilForward, purgeDebugPrint, removeAllModifiersFromUnit, purgeAndPositionNevermoreForRequiem, tryRequiemFromCenterPattern, tryCastPointAbility, tryComputeTimeWalkJump, tryTimeWalkIntoCoilPattern, trySpellSeries, tryExecuteQueuedCoil, abilities, cachedTarget, nextTargetSearchAt, comboLockUntil, queuedCoilAt, queuedCoilCount, queuedCoilInitial, queuedCoilSkipStreak, lastOrderAt, spellSeries, seriesCooldownUntil, SERIES_ROTATION, seriesRotationIndex, SERIES_TRIPLE_MIN, SERIES_TRIPLE_MAX, SERIES_WAVE_MIN, SERIES_WAVE_MAX, SERIES_BEAM_MIN, SERIES_BEAM_MAX, SERIES_CROSS_MIN, SERIES_CROSS_MAX, BETWEEN_SERIES_PAUSE, BETWEEN_CAST_GAP, WITHIN_SERIES_GAP, TIME_WALK_MIN_DISTANCE, PREDICT_LEAD_SPELLS, PREDICT_LEAD_TIME_WALK, PREDICT_MAX_LEAD_UNITS, HERO_ACQUIRE_RADIUS, COIL_JITTER_DIST_MIN, COIL_JITTER_DIST_MAX, COIL_JITTER_FOLLOW_MIN, COIL_JITTER_FOLLOW_MAX, AIM_FORWARD_FIRST_CAST, AIM_FORWARD_FOLLOW_CAST, MAX_QUEUED_COIL_SKIP_STREAK, TIME_WALK_STANDOFF_MIN, TIME_WALK_STANDOFF_MAX, aiNextCastAt, nextIdleTripleAt, requiemCommitPending, DEBUG_AI, nextDebugStateAt, debugTagNextAt, REQUIEM_RARE_ROLL_MAX, DEBUG_NEVERMORE_PURGE, PURGE_LOG_MAX_PER_CALL, NEVERMORE_PURGE_SKIP_MODIFIER_NAMES local ____modifier_boss_nevermore_debuff_immune = require("abilities.creep.modifier_boss_nevermore_debuff_immune") local modifier_boss_nevermore_debuff_immune = ____modifier_boss_nevermore_debuff_immune.modifier_boss_nevermore_debuff_immune local ____modifier_boss_nevermore_phase_terror_wave = require("abilities.creep.modifier_boss_nevermore_phase_terror_wave") local applyNevermorePhaseTerrorWave = ____modifier_boss_nevermore_phase_terror_wave.applyNevermorePhaseTerrorWave local modifier_boss_nevermore_phase_terror_wave = ____modifier_boss_nevermore_phase_terror_wave.modifier_boss_nevermore_phase_terror_wave local ____modifier_boss_nevermore_requiem_gate = require("abilities.creep.modifier_boss_nevermore_requiem_gate") local applyNevermoreRequiemGate = ____modifier_boss_nevermore_requiem_gate.applyNevermoreRequiemGate local modifier_boss_nevermore_requiem_gate = ____modifier_boss_nevermore_requiem_gate.modifier_boss_nevermore_requiem_gate local ____modifier_boss_hud_health_bar = require("abilities.modifiers.modifier_boss_hud_health_bar") local BOSS_NEVERMORE_NAME_TOKEN = ____modifier_boss_hud_health_bar.BOSS_NEVERMORE_NAME_TOKEN local applyBossHudHealthBar = ____modifier_boss_hud_health_bar.applyBossHudHealthBar local ____nevermore_boss_requiem_bridge = require("ai.nevermore_boss_requiem_bridge") local nevermoreBumpRequiemAiCooldown = ____nevermore_boss_requiem_bridge.nevermoreBumpRequiemAiCooldown local nevermoreGetRequiemNextAiTime = ____nevermore_boss_requiem_bridge.nevermoreGetRequiemNextAiTime local nevermoreNeedsMandatoryRequiem = ____nevermore_boss_requiem_bridge.nevermoreNeedsMandatoryRequiem local nevermoreRegisterPhaseRequiemHook = ____nevermore_boss_requiem_bridge.nevermoreRegisterPhaseRequiemHook local ____dota_ts_adapter = require("lib.dota_ts_adapter") local registerEntityFunction = ____dota_ts_adapter.registerEntityFunction function getPredictedGroundPosition(self, target, leadSeconds) local cur = GetGroundPosition( target:GetAbsOrigin(), nil ) local vx = target:GetVelocity().x local vy = target:GetVelocity().y local speed2d = math.sqrt(vx * vx + vy * vy) if speed2d < 25 then local spd = math.max( 1, target:GetIdealSpeed() ) local fwd = target:GetForwardVector() vx = fwd.x * spd vy = fwd.y * spd end local dx = vx * leadSeconds local dy = vy * leadSeconds local leadLen = math.sqrt(dx * dx + dy * dy) if leadLen > PREDICT_MAX_LEAD_UNITS and leadLen > 0 then local scale = PREDICT_MAX_LEAD_UNITS / leadLen dx = dx * scale dy = dy * scale end return GetGroundPosition( cur + Vector(dx, dy, 0), nil ) end function getTargetHorizontalForward(self, target) local f = target:GetForwardVector() local len2d = math.sqrt(f.x * f.x + f.y * f.y) if len2d < 0.01 then return Vector(1, 0, 0) end return Vector(f.x / len2d, f.y / len2d, 0) end function getAimInFrontOfPredictedTarget(self, target, leadSeconds, forwardUnits) local pred = getPredictedGroundPosition(nil, target, leadSeconds) local fwd = getTargetHorizontalForward(nil, target) return GetGroundPosition(pred + fwd * forwardUnits, nil) end function getBossForwardAimPoint(self, boss, distance) local o = GetGroundPosition( boss:GetAbsOrigin(), nil ) local f = boss:GetForwardVector() local len2d = math.sqrt(f.x * f.x + f.y * f.y) local dir = len2d < 0.01 and Vector(1, 0, 0) or Vector(f.x / len2d, f.y / len2d, 0) return GetGroundPosition(o + dir * distance, nil) end function addCoilWaveAimJitter(self, anchor, castIndexInSeries) local sector = castIndexInSeries * 149 % 360 local yaw = sector + RandomInt(0, 89) local distMin = castIndexInSeries <= 0 and COIL_JITTER_DIST_MIN or COIL_JITTER_FOLLOW_MIN local distMax = castIndexInSeries <= 0 and COIL_JITTER_DIST_MAX or COIL_JITTER_FOLLOW_MAX local dist = RandomInt(distMin, distMax) local offset = RotatePosition( Vector(0, 0, 0), QAngle(0, yaw, 0), Vector(dist, 0, 0) ) return GetGroundPosition(anchor + offset, nil) end function getNevermoreCoilWavePhase(self, boss) local hp = boss: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 canCoilWaveHitAnyEnemy(self, boss, aimPoint) local ab = abilities.coilWave if not ab or ab:IsNull() then return false end local phase = getNevermoreCoilWavePhase(nil, boss) local origin = boss:GetAbsOrigin() local dir = aimPoint - origin dir.z = 0 if dir:Length2D() < 1 then local f = boss:GetForwardVector() dir = Vector(f.x, f.y, 0) end dir = dir:Normalized() local radius = ab:GetSpecialValueFor("radius") if radius <= 0 then return false end local baseSlotsKv = ab:GetSpecialValueFor("lane_slot_count") local baseSlots = baseSlotsKv > 0 and math.floor(baseSlotsKv) or 5 local slotCount = baseSlots + (phase - 1) local forwardKv = ab:GetSpecialValueFor("lane_forward_dist") local forwardDist = (forwardKv > 0 and forwardKv or 360) + (phase - 1) * 35 local spacingKv = ab:GetSpecialValueFor("lane_slot_spacing") local spacingFactor = spacingKv > 0 and spacingKv or 1.32 local spacing = math.max(radius * spacingFactor, 185) local right = Vector(-dir.y, dir.x, 0):Normalized() local centerRow = GetGroundPosition(origin + dir * forwardDist, nil) local mid = (slotCount - 1) / 2 local searchR = forwardDist + radius + spacing * slotCount + 450 local enemies = FindUnitsInRadius( boss:GetTeamNumber(), origin, nil, searchR, DOTA_UNIT_TARGET_TEAM_ENEMY, bit.bor(DOTA_UNIT_TARGET_HERO, DOTA_UNIT_TARGET_BASIC), DOTA_UNIT_TARGET_FLAG_NONE, FIND_ANY_ORDER, false ) local hullFudge = 28 for ____, u in ipairs(enemies) do do if not u or u:IsNull() or not u:IsAlive() then goto __continue18 end local ep = GetGroundPosition( u:GetAbsOrigin(), nil ) do local i = 0 while i < slotCount do local lateral = (i - mid) * spacing local slotPos = GetGroundPosition(centerRow + right * lateral, nil) if (ep - slotPos):Length2D() <= radius + hullFudge then return true end i = i + 1 end end end ::__continue18:: end return false end function canCoilBeamHitAnyEnemy(self, boss, aimPoint) local ab = abilities.coilBeam if not ab or ab:IsNull() then return false end local phase = getNevermoreCoilWavePhase(nil, boss) local origin = boss:GetAbsOrigin() local dir = aimPoint - origin dir.z = 0 if dir:Length2D() < 1 then local f = boss:GetForwardVector() dir = Vector(f.x, f.y, 0) end dir = dir:Normalized() local radius = ab:GetSpecialValueFor("radius") if radius <= 0 then return false end local baseSlotsKv = ab:GetSpecialValueFor("lane_slot_count") local baseSlots = baseSlotsKv > 0 and math.floor(baseSlotsKv) or 12 local bonusKv = ab:GetSpecialValueFor("lane_slot_phase_bonus") local perPhase = bonusKv > 0 and math.floor(bonusKv) or 2 local slotCount = baseSlots + (phase - 1) * perPhase local startKv = ab:GetSpecialValueFor("beam_start_dist") local startDist = startKv > 0 and startKv or 140 local stepKv = ab:GetSpecialValueFor("beam_step") local step = stepKv > 0 and stepKv or math.max(radius * 1.22, 195) local endAlong = startDist + (slotCount - 1) * step local searchR = endAlong + radius + 200 local enemies = FindUnitsInRadius( boss:GetTeamNumber(), origin, nil, searchR, DOTA_UNIT_TARGET_TEAM_ENEMY, bit.bor(DOTA_UNIT_TARGET_HERO, DOTA_UNIT_TARGET_BASIC), DOTA_UNIT_TARGET_FLAG_NONE, FIND_ANY_ORDER, false ) local hullFudge = 28 for ____, u in ipairs(enemies) do do if not u or u:IsNull() or not u:IsAlive() then goto __continue27 end local ep = GetGroundPosition( u:GetAbsOrigin(), nil ) do local i = 0 while i < slotCount do local along = startDist + i * step local slotPos = GetGroundPosition(origin + dir * along, nil) if (ep - slotPos):Length2D() <= radius + hullFudge then return true end i = i + 1 end end end ::__continue27:: end return false end function canHubCrossburstHitAnyEnemy(self, boss) local ab = abilities.hubCrossburst if not ab or ab:IsNull() then return false end local pickKv = ab:GetSpecialValueFor("spawn_pick_radius") local pick = pickKv > 0 and pickKv or 1500 local startKv = ab:GetSpecialValueFor("ring_start_dist") local stepKv = ab:GetSpecialValueFor("ring_step") local countKv = ab:GetSpecialValueFor("ring_count") local countBonusKv = ab:GetSpecialValueFor("ring_count_phase_bonus") local radius = ab:GetSpecialValueFor("radius") local start = startKv > 0 and startKv or 90 local step = stepKv > 0 and stepKv or 190 local baseCnt = countKv > 0 and math.floor(countKv) or 7 local perPh = countBonusKv > 0 and math.floor(countBonusKv) or 1 local phase = getNevermoreCoilWavePhase(nil, boss) local cnt = math.max(1, baseCnt + (phase - 1) * perPh) local r = radius > 0 and radius or 165 local armReach = start + math.max(0, cnt - 1) * step + r local searchR = pick + armReach + 250 local enemies = FindUnitsInRadius( boss:GetTeamNumber(), boss:GetAbsOrigin(), nil, searchR, DOTA_UNIT_TARGET_TEAM_ENEMY, bit.bor(DOTA_UNIT_TARGET_HERO, DOTA_UNIT_TARGET_BASIC), DOTA_UNIT_TARGET_FLAG_NONE, FIND_ANY_ORDER, false ) for ____, u in ipairs(enemies) do if u and not u:IsNull() and u:IsAlive() then return true end end return false end function debugLog(self, tag, message, throttle) if throttle == nil then throttle = 0.35 end if not DEBUG_AI then return end local now = GameRules:GetGameTime() local nextAt = debugTagNextAt[tag] or 0 if now < nextAt then return end debugTagNextAt[tag] = now + throttle print((("[NevermoreAI][" .. tag) .. "] ") .. message) end function NevermoreBossThink(self) if not IsServer() or not thisEntity or thisEntity:IsNull() then return 0.5 end if not thisEntity:IsAlive() then return 0.5 end if thisEntity:IsChanneling() then return 0.5 end if thisEntity:GetOwner() ~= nil and thisEntity:GetOwner():IsRealHero() then return 0.5 end local now = GameRules:GetGameTime() if now >= nextDebugStateAt then nextDebugStateAt = now + 1 local ser = spellSeries and (spellSeries.kind .. "x") .. tostring(spellSeries.remaining) or "idle" debugLog( nil, "state", (((((("lock=" .. __TS__NumberToFixed(comboLockUntil - now, 2)) .. " target=") .. (cachedTarget and cachedTarget:GetUnitName() or "none")) .. " q=") .. tostring(queuedCoilCount)) .. " series=") .. ser, 0.95 ) end if tryExecuteQueuedCoil(nil, now) then return 0.14 end if now < comboLockUntil then return 0.14 end local shouldRefreshTarget = now >= nextTargetSearchAt or not cachedTarget or cachedTarget:IsNull() or not cachedTarget:IsAlive() or (cachedTarget:GetAbsOrigin() - thisEntity:GetAbsOrigin()):Length2D() > HERO_ACQUIRE_RADIUS if shouldRefreshTarget then local enemies = FindUnitsInRadius( thisEntity:GetTeamNumber(), thisEntity:GetAbsOrigin(), nil, HERO_ACQUIRE_RADIUS, DOTA_UNIT_TARGET_TEAM_ENEMY, DOTA_UNIT_TARGET_HERO, DOTA_UNIT_TARGET_FLAG_NO_INVIS, FIND_CLOSEST, false ) if #enemies == 0 then enemies = FindUnitsInRadius( thisEntity:GetTeamNumber(), thisEntity:GetAbsOrigin(), nil, HERO_ACQUIRE_RADIUS, DOTA_UNIT_TARGET_TEAM_ENEMY, DOTA_UNIT_TARGET_BASIC, DOTA_UNIT_TARGET_FLAG_NO_INVIS, FIND_CLOSEST, false ) end local enemyHero = __TS__ArrayFind( enemies, function(____, unit) return unit:IsRealHero() and unit:IsAlive() end ) cachedTarget = enemyHero or enemies[1] nextTargetSearchAt = now + (cachedTarget ~= nil and 0.55 or 1) debugLog( nil, "target", (("refresh enemies=" .. tostring(#enemies)) .. " chosen=") .. (cachedTarget ~= nil and cachedTarget:GetUnitName() or "none"), 0.35 ) end local primaryTarget = cachedTarget if not primaryTarget then requiemCommitPending = false spellSeries = nil queuedCoilCount = 0 queuedCoilInitial = 0 queuedCoilSkipStreak = 0 if tryIdleTripleCoilForward(nil, now) then return 0.14 end return 0.25 end if tryTimeWalkIntoCoilPattern(nil, primaryTarget, now) then return 0.14 end if trySpellSeries(nil, primaryTarget, now) then return 0.14 end if tryRequiemFromCenterPattern(nil, primaryTarget, now) then return 0.14 end comboLockUntil = now + 0.18 return 0.18 end function tryIdleTripleCoilForward(self, now) if now < nextIdleTripleAt then return false end if not abilities.tripleCoil or not abilities.tripleCoil:IsFullyCastable() then return false end if thisEntity:HasModifier("modifier_boss_nevermore_time_walk") then return false end local pos = getBossForwardAimPoint( nil, thisEntity, RandomInt(280, 520) ) if not tryCastPointAbility(nil, abilities.tripleCoil, pos, now) then return false end nextIdleTripleAt = now + 3.2 comboLockUntil = now + 1 debugLog(nil, "idle_triple", "no enemies — triple forward", 0.4) return true end function purgeDebugPrint(self, message) if DEBUG_NEVERMORE_PURGE then print("[NevermorePurge] " .. message) end end function removeAllModifiersFromUnit(self, unit) if not unit or unit:IsNull() then purgeDebugPrint(nil, "removeAllModifiersFromUnit: unit пустой") return end local unitLabel = "" do pcall(function() unitLabel = unit:GetUnitName() end) end local startCount = unit:GetModifierCount() purgeDebugPrint( nil, (((("старт: " .. unitLabel) .. " ent=") .. tostring(unit:entindex())) .. " modifiers=") .. tostring(startCount) ) local guard = 0 local maxIterations = 400 local logged = 0 while unit:GetModifierCount() > 0 and guard < maxIterations do do guard = guard + 1 local beforeCount = unit:GetModifierCount() local lastIdx = beforeCount - 1 local name = unit:GetModifierNameByIndex(lastIdx) if name == nil or name == "" then purgeDebugPrint( nil, (("прервали: пустое имя по индексу " .. tostring(lastIdx)) .. ", count=") .. tostring(beforeCount) ) break end if NEVERMORE_PURGE_SKIP_MODIFIER_NAMES[name] then local removedSkipped = false do local i = lastIdx - 1 while i >= 0 do do local altName = unit:GetModifierNameByIndex(i) if not altName or NEVERMORE_PURGE_SKIP_MODIFIER_NAMES[altName] then goto __continue72 end unit:RemoveModifierByName(altName) removedSkipped = true break end ::__continue72:: i = i - 1 end end if not removedSkipped then purgeDebugPrint( nil, "стоп whitelist: остались только защищённые модификаторы, count=" .. tostring(beforeCount) ) break end goto __continue69 end if logged < PURGE_LOG_MAX_PER_CALL then purgeDebugPrint( nil, ((((((" #" .. tostring(guard)) .. " снятие: idx=") .. tostring(lastIdx)) .. " name=\"") .. name) .. "\" count до=") .. tostring(beforeCount) ) logged = logged + 1 elseif logged == PURGE_LOG_MAX_PER_CALL then purgeDebugPrint( nil, (" … дальше без логов каждого шага (лимит " .. tostring(PURGE_LOG_MAX_PER_CALL)) .. ")" ) logged = logged + 1 end unit:RemoveModifierByName(name) local afterCount = unit:GetModifierCount() if afterCount >= beforeCount then purgeDebugPrint( nil, (((("СТОП: RemoveModifierByName не уменьшил список (движок не снял?) name=\"" .. name) .. "\" до=") .. tostring(beforeCount)) .. " после=") .. tostring(afterCount) ) break end end ::__continue69:: end local endCount = unit:GetModifierCount() if guard >= maxIterations then purgeDebugPrint( nil, (("ВНИМАНИЕ: достигнут лимит итераций " .. tostring(maxIterations)) .. ", осталось модификаторов: ") .. tostring(endCount) ) end purgeDebugPrint( nil, ((((("конец цикла: снятий≈" .. tostring(guard)) .. ", осталось modifiers=") .. tostring(endCount)) .. " (") .. unitLabel) .. ")" ) end function purgeAndPositionNevermoreForRequiem(self) local t0 = IsServer() and GameRules:GetGameTime() or 0 local n0 = thisEntity:GetModifierCount() purgeDebugPrint( nil, (((("purgeAndPosition: до снятия time=" .. __TS__NumberToFixed(t0, 2)) .. " modifiers=") .. tostring(n0)) .. " requiemPending=") .. tostring(requiemCommitPending) ) removeAllModifiersFromUnit(nil, thisEntity) local n1 = thisEntity:GetModifierCount() purgeDebugPrint( nil, "purgeAndPosition: после removeAll modifiers=" .. tostring(n1) ) thisEntity:Purge( true, true, false, true, true ) local n2 = thisEntity:GetModifierCount() purgeDebugPrint( nil, "purgeAndPosition: после Purge modifiers=" .. tostring(n2) ) applyNevermoreRequiemGate(nil, thisEntity) local center = Entities:FindByName(nil, "nevermore_center_point") if center then debugLog(nil, "requiem", "teleport to nevermore_center_point", 0.2) FindClearSpaceForUnit( thisEntity, center:GetAbsOrigin(), true ) local cp = center:GetAbsOrigin() purgeDebugPrint( nil, ((((("телепорт: nevermore_center_point ok pos=(" .. __TS__NumberToFixed(cp.x, 0)) .. ",") .. __TS__NumberToFixed(cp.y, 0)) .. ",") .. __TS__NumberToFixed(cp.z, 0)) .. ")" ) else purgeDebugPrint(nil, "телепорт: сущность nevermore_center_point НЕ НАЙДЕНА на карте") debugLog(nil, "requiem", "center point not found", 1) end local req = abilities.requiem if req and not req:IsNull() then purgeDebugPrint( nil, (((((((("requiem ability: castable=" .. tostring(req:IsFullyCastable())) .. " channel=") .. tostring(req:GetChannelTime())) .. " cd=") .. __TS__NumberToFixed( req:GetCooldownTimeRemaining(), 2 )) .. " mana=") .. __TS__NumberToFixed( thisEntity:GetMana(), 0 )) .. "/") .. __TS__NumberToFixed( thisEntity:GetMaxMana(), 0 ) ) else purgeDebugPrint(nil, "requiem ability: отсутствует") end end function tryRequiemFromCenterPattern(self, target, now) if not abilities.requiem then return false end if not target or target:IsNull() or not target:IsAlive() then requiemCommitPending = false return false end local mandatoryRequiem = nevermoreNeedsMandatoryRequiem(nil, thisEntity) if not requiemCommitPending then if not mandatoryRequiem and now < nevermoreGetRequiemNextAiTime(nil) then return false end if not abilities.requiem:IsFullyCastable() then return false end local hpPct = thisEntity:GetHealthPercent() local allowByHp = hpPct <= 38 local allowByRareRoll = RandomInt(1, 100) <= REQUIEM_RARE_ROLL_MAX if not mandatoryRequiem and not allowByHp and not allowByRareRoll then debugLog( nil, "requiem", ("skip rare ult (hp=" .. __TS__NumberToFixed(hpPct, 0)) .. "%)", 2 ) return false end requiemCommitPending = true if mandatoryRequiem then debugLog( nil, "requiem", ("mandatory ult hp=" .. __TS__NumberToFixed(hpPct, 0)) .. "%", 0.5 ) end end purgeAndPositionNevermoreForRequiem(nil) local castPosition = getAimInFrontOfPredictedTarget(nil, target, PREDICT_LEAD_SPELLS + 0.15, AIM_FORWARD_FIRST_CAST) if not tryCastPointAbility(nil, abilities.requiem, castPosition, now) then comboLockUntil = now + 0.12 return true end requiemCommitPending = false debugLog( nil, "requiem", (("cast at " .. __TS__NumberToFixed(castPosition.x, 0)) .. ",") .. __TS__NumberToFixed(castPosition.y, 0), 0.2 ) nevermoreBumpRequiemAiCooldown(nil) comboLockUntil = now + 1 queuedCoilCount = 0 queuedCoilInitial = 0 spellSeries = nil seriesCooldownUntil = now + 2 return true end function tryCastPointAbility(self, ability, position, now) if not ability or ability:IsNull() then debugLog(nil, "cast_fail", "ability missing", 0.5) return false end if not ability:IsFullyCastable() then debugLog( nil, "cast_fail", ability:GetAbilityName() .. " not castable", 0.25 ) return false end if now < lastOrderAt + 0.12 then debugLog( nil, "cast_fail", ability:GetAbilityName() .. " blocked by order throttle", 0.25 ) return false end ExecuteOrderFromTable({ UnitIndex = thisEntity:entindex(), OrderType = DOTA_UNIT_ORDER_CAST_POSITION, AbilityIndex = ability:entindex(), Position = position }) lastOrderAt = now debugLog( nil, "cast_ok", (((ability:GetAbilityName() .. " -> ") .. __TS__NumberToFixed(position.x, 0)) .. ",") .. __TS__NumberToFixed(position.y, 0), 0.12 ) return true end function tryComputeTimeWalkJump(self, target) if not abilities.timeWalk then return nil end local bossPos = thisEntity:GetAbsOrigin() local predicted = getPredictedGroundPosition(nil, target, PREDICT_LEAD_TIME_WALK) local toPred = predicted - bossPos local distanceToPredicted = toPred:Length2D() if distanceToPredicted <= TIME_WALK_MIN_DISTANCE then return nil end local dir = distanceToPredicted < 1 and thisEntity:GetForwardVector() or toPred:Normalized() local standoff = RandomInt(TIME_WALK_STANDOFF_MIN, TIME_WALK_STANDOFF_MAX) local jumpPos = GetGroundPosition(predicted - dir * standoff, nil) local maxRange = abilities.timeWalk:GetCastRange(bossPos, nil) local toJump = jumpPos - bossPos local jumpLen = toJump:Length2D() if jumpLen > maxRange - 30 then jumpPos = GetGroundPosition( bossPos + toJump:Normalized() * math.max(50, maxRange - 40), nil ) end return jumpPos end function tryTimeWalkIntoCoilPattern(self, target, now) if not abilities.timeWalk or not abilities.coilWave then return false end if now < aiNextCastAt.timeWalk or now < aiNextCastAt.coilWave then return false end if not abilities.timeWalk:IsFullyCastable() or not abilities.coilWave:IsFullyCastable() then return false end if thisEntity:HasModifier("modifier_boss_nevermore_time_walk") then return false end local jumpPos = tryComputeTimeWalkJump(nil, target) if not jumpPos then return false end local predictedForLog = getPredictedGroundPosition(nil, target, PREDICT_LEAD_TIME_WALK) local distBossToPred = (predictedForLog - thisEntity:GetAbsOrigin()):Length2D() if not tryCastPointAbility(nil, abilities.timeWalk, jumpPos, now) then return false end debugLog( nil, "time_walk", (("distToPred=" .. __TS__NumberToFixed(distBossToPred, 0)) .. " queuedCoils=") .. tostring(queuedCoilCount), 0.2 ) aiNextCastAt.timeWalk = now + 3 aiNextCastAt.coilWave = now + BETWEEN_CAST_GAP comboLockUntil = now + 0.7 spellSeries = nil seriesCooldownUntil = now + 4 queuedCoilCount = RandomInt(2, 3) queuedCoilInitial = queuedCoilCount queuedCoilAt = now + 0.55 queuedCoilSkipStreak = 0 return true end function trySpellSeries(self, target, now) if thisEntity:HasModifier("modifier_boss_nevermore_time_walk") then return false end if spellSeries == nil then if now < seriesCooldownUntil then return false end local kind = SERIES_ROTATION[seriesRotationIndex % #SERIES_ROTATION + 1] seriesRotationIndex = seriesRotationIndex + 1 local count = kind == "triple" and RandomInt(SERIES_TRIPLE_MIN, SERIES_TRIPLE_MAX) or (kind == "wave" and RandomInt(SERIES_WAVE_MIN, SERIES_WAVE_MAX) or (kind == "beam" and RandomInt(SERIES_BEAM_MIN, SERIES_BEAM_MAX) or RandomInt(SERIES_CROSS_MIN, SERIES_CROSS_MAX))) local ____temp_2 if kind == "triple" then ____temp_2 = abilities.tripleCoil else local ____temp_1 if kind == "wave" then ____temp_1 = abilities.coilWave else local ____temp_0 if kind == "beam" then ____temp_0 = abilities.coilBeam else ____temp_0 = abilities.hubCrossburst end ____temp_1 = ____temp_0 end ____temp_2 = ____temp_1 end local ab = ____temp_2 if not ab then return false end spellSeries = {kind = kind, remaining = count, totalInSeries = count} debugLog( nil, "series", (("start " .. kind) .. " x") .. tostring(count), 0.2 ) end local series = spellSeries if not series then return false end local ____temp_5 if series.kind == "triple" then ____temp_5 = abilities.tripleCoil else local ____temp_4 if series.kind == "wave" then ____temp_4 = abilities.coilWave else local ____temp_3 if series.kind == "beam" then ____temp_3 = abilities.coilBeam else ____temp_3 = abilities.hubCrossburst end ____temp_4 = ____temp_3 end ____temp_5 = ____temp_4 end local ability = ____temp_5 if not ability or not ability:IsFullyCastable() then debugLog(nil, "series", series.kind .. " not castable, wait", 0.3) comboLockUntil = now + 0.25 return true end local castIndex = series.totalInSeries - series.remaining local forwardUnits = castIndex == 0 and AIM_FORWARD_FIRST_CAST or AIM_FORWARD_FOLLOW_CAST local anchor = getAimInFrontOfPredictedTarget(nil, target, PREDICT_LEAD_SPELLS, forwardUnits) local castPos = addCoilWaveAimJitter(nil, anchor, castIndex) if series.kind == "wave" and not canCoilWaveHitAnyEnemy(nil, thisEntity, castPos) then debugLog(nil, "series", "wave abort — никого в полосе, сбрасываем серию", 0.28) spellSeries = nil seriesCooldownUntil = now + 0.45 comboLockUntil = now + 0.22 return true end if series.kind == "beam" and not canCoilBeamHitAnyEnemy(nil, thisEntity, castPos) then debugLog(nil, "series", "beam abort — никого на луче, сбрасываем серию", 0.28) spellSeries = nil seriesCooldownUntil = now + 0.45 comboLockUntil = now + 0.22 return true end if series.kind == "cross" and not canHubCrossburstHitAnyEnemy(nil, thisEntity) then debugLog(nil, "series", "crossburst abort — врагов слишком далеко для зоны хаба", 0.28) spellSeries = nil seriesCooldownUntil = now + 0.45 comboLockUntil = now + 0.22 return true end if not tryCastPointAbility(nil, ability, castPos, now) then return false end series.remaining = series.remaining - 1 debugLog( nil, "series", (series.kind .. " cast, left=") .. tostring(series.remaining), 0.15 ) if series.remaining <= 0 then spellSeries = nil seriesCooldownUntil = now + BETWEEN_SERIES_PAUSE comboLockUntil = now + WITHIN_SERIES_GAP debugLog( nil, "series", ("end -> pause " .. tostring(BETWEEN_SERIES_PAUSE)) .. "s", 0.2 ) else comboLockUntil = now + WITHIN_SERIES_GAP end return true end function tryExecuteQueuedCoil(self, now) if queuedCoilCount <= 0 or now < queuedCoilAt then return false end if not cachedTarget or cachedTarget:IsNull() or not cachedTarget:IsAlive() then debugLog(nil, "queued", "target invalid -> clear queue", 0.5) queuedCoilCount = 0 queuedCoilInitial = 0 queuedCoilSkipStreak = 0 return false end if not abilities.coilWave or not abilities.coilWave:IsFullyCastable() then debugLog(nil, "queued", "coil not castable -> postpone", 0.35) queuedCoilAt = now + 0.2 return false end local castIndex = queuedCoilInitial - queuedCoilCount local forwardUnits = castIndex == 0 and AIM_FORWARD_FIRST_CAST or AIM_FORWARD_FOLLOW_CAST local anchor = getAimInFrontOfPredictedTarget(nil, cachedTarget, PREDICT_LEAD_SPELLS, forwardUnits) local pos = addCoilWaveAimJitter(nil, anchor, castIndex) if not canCoilWaveHitAnyEnemy(nil, thisEntity, pos) then queuedCoilSkipStreak = queuedCoilSkipStreak + 1 if queuedCoilSkipStreak >= MAX_QUEUED_COIL_SKIP_STREAK then debugLog(nil, "queued", "coil abort queue — нет целей в полосе слишком долго", 0.35) queuedCoilCount = 0 queuedCoilInitial = 0 queuedCoilSkipStreak = 0 return false end debugLog(nil, "queued", "coil skip — никого в полосе, откладываем", 0.3) queuedCoilAt = now + 0.35 return true end if not tryCastPointAbility(nil, abilities.coilWave, pos, now) then return false end queuedCoilSkipStreak = 0 queuedCoilCount = queuedCoilCount - 1 queuedCoilAt = now + BETWEEN_CAST_GAP aiNextCastAt.coilWave = math.max(aiNextCastAt.coilWave, now + BETWEEN_CAST_GAP) comboLockUntil = now + BETWEEN_CAST_GAP debugLog( nil, "queued", "coil fired, left=" .. tostring(queuedCoilCount), 0.2 ) return true end abilities = {} nextTargetSearchAt = 0 comboLockUntil = 0 queuedCoilAt = 0 queuedCoilCount = 0 queuedCoilInitial = 0 queuedCoilSkipStreak = 0 lastOrderAt = 0 seriesCooldownUntil = 0 SERIES_ROTATION = {"triple", "wave", "beam", "cross"} seriesRotationIndex = 0 SERIES_TRIPLE_MIN = 2 SERIES_TRIPLE_MAX = 4 SERIES_WAVE_MIN = 2 SERIES_WAVE_MAX = 4 SERIES_BEAM_MIN = 2 SERIES_BEAM_MAX = 4 SERIES_CROSS_MIN = 1 SERIES_CROSS_MAX = 2 BETWEEN_SERIES_PAUSE = 1.6 BETWEEN_CAST_GAP = 1 WITHIN_SERIES_GAP = BETWEEN_CAST_GAP TIME_WALK_MIN_DISTANCE = 2000 PREDICT_LEAD_SPELLS = 0.42 PREDICT_LEAD_TIME_WALK = 0.55 PREDICT_MAX_LEAD_UNITS = 480 HERO_ACQUIRE_RADIUS = 12000 COIL_JITTER_DIST_MIN = 60 COIL_JITTER_DIST_MAX = 200 COIL_JITTER_FOLLOW_MIN = 90 COIL_JITTER_FOLLOW_MAX = 260 AIM_FORWARD_FIRST_CAST = 100 AIM_FORWARD_FOLLOW_CAST = 50 MAX_QUEUED_COIL_SKIP_STREAK = 5 TIME_WALK_STANDOFF_MIN = 380 TIME_WALK_STANDOFF_MAX = 560 aiNextCastAt = {timeWalk = 0, tripleCoil = 0, coilWave = 0} nextIdleTripleAt = 0 requiemCommitPending = false DEBUG_AI = true nextDebugStateAt = 0 debugTagNextAt = {} registerEntityFunction( nil, "Spawn", function() if not IsServer() or not thisEntity then return end applyBossHudHealthBar(nil, thisEntity, BOSS_NEVERMORE_NAME_TOKEN) applyNevermorePhaseTerrorWave(nil, thisEntity) applyNevermoreRequiemGate(nil, thisEntity) thisEntity:AddNewModifier( thisEntity, getModifierSourceAbility(nil, thisEntity), modifier_boss_nevermore_debuff_immune.name, {} ) nevermoreRegisterPhaseRequiemHook( nil, function() requiemCommitPending = false spellSeries = nil queuedCoilCount = 0 queuedCoilInitial = 0 queuedCoilSkipStreak = 0 seriesCooldownUntil = GameRules:GetGameTime() + 2 end ) abilities.timeWalk = thisEntity:FindAbilityByName("boss_nevermore_time_walk") or nil abilities.requiem = thisEntity:FindAbilityByName("boss_nevermore_requiem_barrage") or nil abilities.tripleCoil = thisEntity:FindAbilityByName("boss_nevermore_triple_coil_aoe") or nil abilities.coilWave = thisEntity:FindAbilityByName("boss_nevermore_coil_wave") or nil abilities.coilBeam = thisEntity:FindAbilityByName("boss_nevermore_coil_beam") or nil abilities.hubCrossburst = thisEntity:FindAbilityByName("boss_nevermore_hub_crossburst") or nil thisEntity:SetContextThink("NevermoreBossThink", NevermoreBossThink, 0.25) end ) REQUIEM_RARE_ROLL_MAX = 4 DEBUG_NEVERMORE_PURGE = true PURGE_LOG_MAX_PER_CALL = 40 NEVERMORE_PURGE_SKIP_MODIFIER_NAMES = {[modifier_boss_nevermore_debuff_immune.name] = true, [modifier_boss_nevermore_phase_terror_wave.name] = true, [modifier_boss_nevermore_requiem_gate.name] = true, modifier_no_healthbar = true} return ____exports