local RUN_WAVE_DEBUG_MODE = true local activeWaveInfo = nil local activeMissionInfo = {} local subwaveState = {} local waveId = 0 local waveStartTime = -1 local waveOngoing = false ActiveSubwaveTimers = {} local objectiveResource local tfGamerules local function makeActiveWaveInfo() activeWaveInfo = { SentryBuster = { BusterSpawnTemplates = activeMissionInfo.BusterSpawnTemplates or { TEMPLATES.Giant_SentryBuster }, -- TODO: buster RandomChoice support SentryBusterWaitBeforeStarting = activeMissionInfo.SentryBusterWaitBeforeStarting or 5, -- default SentryBusterWaitBetweenSpawns = activeMissionInfo.SentryBusterWaitBetweenSpawns or 15, -- default SentryBusterDamageDealtThreshold = activeMissionInfo.SentryBusterDamageDealtThreshold or 3000, -- default SentryBusterKillThreshold = activeMissionInfo.SentryBusterKillThreshold or 15, -- default ActiveSentries = {}, }, } end local function makeActiveSentryForBuster(sentry) if not activeWaveInfo then return end -- hardcoded to red only for now if sentry.m_iTeamNum ~= TEAM_RED then return end local ActiveSentries = activeWaveInfo.SentryBuster.ActiveSentries local sentryHandle = sentry:GetHandleIndex() ActiveSentries[sentryHandle] = { DamageDealt = 0, Kills = 0, } local sentryRemoveCallback sentryRemoveCallback = sentry:AddCallback(ON_REMOVE, function() if ActiveSentries[sentryHandle] then ActiveSentries[sentryHandle] = nil end sentry:RemoveCallback(sentryRemoveCallback) end) end local function RemapVal(val, A, B, C, D) if A == B then return (val >= B) and D or C end return C + (D - C) * (val - A) / (B - A) end local function RemapValClamped(val, A, B, C, D) if A == B then return (val >= B) and D or C end local cVal = (val - A) / (B - A) if cVal < 0 then cVal = 0 elseif cVal > 1 then cVal = 1 end return C + (D - C) * cVal end local function spawnSentryBuster(sentryTarget, activeSentryInfo) if not activeWaveInfo then return end print("attempt sentry buster spawn") if activeSentryInfo.BusterActive then return end if not tfGamerules then tfGamerules = ents.FindByClass("tf_gamerules") end local spawnSuccess = false local templates = activeWaveInfo.SentryBuster.BusterSpawnTemplates local spawnedBots = {} local spawnedTemplates = {} local freeSlots = GetFreeBotSlotsCount() if freeSlots < #templates then print("not enough slots to spawn sentry buster") print("free slots " .. tostring(freeSlots) .. "; templates count: " .. tostring(#templates)) return end local deadBotsCount = 0 local spawnedBotsCount = 0 local BUSTER_SPAWN_POINT = "spawnbot" for _, template in pairs(templates) do local botSpawn = SpawnBot(template, BUSTER_SPAWN_POINT) if botSpawn then spawnSuccess = true if template.Mission == BOT_MISSIONS.MISSION_DESTROY_SENTRIES then botSpawn:RunScriptCode("activator.SetMissionTarget(caller)", botSpawn, sentryTarget) end SentryBusterSpawned(template) table.insert(spawnedBots, botSpawn) table.insert(spawnedTemplates, template) spawnedBotsCount = spawnedBotsCount + 1 local callbacks = {} local function clearCallbacks() for _, callbackId in pairs(callbacks) do botSpawn:RemoveCallback(callbackId) end end callbacks.spawned = botSpawn:AddCallback(ON_SPAWN, function() clearCallbacks() end) callbacks.died = botSpawn:AddCallback(ON_DEATH, function() clearCallbacks() deadBotsCount = deadBotsCount + 1 -- #spawnedBots is not used as its size will be modified in HandleSquad() if deadBotsCount >= spawnedBotsCount then activeSentryInfo.BusterActive = false activeSentryInfo.LastSentryBusterDeathTime = CurTime() end SentryBusterDied(template) for _, player in pairs(ents.GetAllPlayers()) do if player:IsRealPlayer() then player:AcceptInput("SpeakResponseConcept", "TLK_MVM_SENTRY_BUSTER_DOWN", player) end end end) CheckSpawnTeleport(botSpawn, BUSTER_SPAWN_POINT) else print("failed to spawn sentry buster") end end if #spawnedBots > 1 then HandleSquad(spawnedBots, spawnedTemplates) end if spawnSuccess then activeSentryInfo.BusterActive = true for _, player in pairs(ents.GetAllPlayers()) do if player:IsRealPlayer() then player:AcceptInput("SpeakResponseConcept", "TLK_MVM_SENTRY_BUSTER", player) end end if tfGamerules then local bustersCount = 0 for _, info in pairs(activeWaveInfo.SentryBuster.ActiveSentries) do if info.BusterActive then bustersCount = bustersCount + 1 end end if bustersCount > 1 then tfGamerules:AcceptInput("PlayVO", "Announcer.MVM_Sentry_Buster_Alert_Another") else tfGamerules:AcceptInput("PlayVO", "Announcer.MVM_Sentry_Buster_Alert") end end else print("failed to spawn sentry buster") end end local function checkSentryBuster() if not activeWaveInfo then return end local curTime = CurTime() local elapsedSinceWaveStart = curTime - waveStartTime if elapsedSinceWaveStart < activeWaveInfo.SentryBuster.SentryBusterWaitBeforeStarting then return end local sentryCount = 0 for _ in pairs(activeWaveInfo.SentryBuster.ActiveSentries) do sentryCount = sentryCount + 1 end local damageDealtThreshold = activeWaveInfo.SentryBuster.SentryBusterDamageDealtThreshold local killThreshold = activeWaveInfo.SentryBuster.SentryBusterKillThreshold local scaleBySentryCount = RemapValClamped(sentryCount, 1, 6, 1, 0.5) if sentryCount >= 2 then damageDealtThreshold = damageDealtThreshold * scaleBySentryCount killThreshold = killThreshold * scaleBySentryCount end for sentryHandleIndex, info in pairs(activeWaveInfo.SentryBuster.ActiveSentries) do local lastBusterDeathTime = info.LastSentryBusterDeathTime or -1 if CurTime() - lastBusterDeathTime > activeWaveInfo.SentryBuster.SentryBusterWaitBetweenSpawns then local sentry = Entity(sentryHandleIndex) if info.DamageDealt >= damageDealtThreshold then spawnSentryBuster(sentry, info) elseif info.Kills >= killThreshold then spawnSentryBuster(sentry, info) end end end end function RemoveTimerFromWaveActive(id) for i, v in pairs(ActiveSubwaveTimers) do if v == id then table.remove(ActiveSubwaveTimers, i) return end end end local function warn(subwaveName, msg) util.PrintToChatAll("[RUN-WAVE WARNING] [" .. subwaveName .. "] " .. msg) end local function killAll() for _, player in pairs(ents.GetAllPlayers()) do if not player:IsRealPlayer() then player:Suicide() end end for _, tank in pairs(ents.FindAllByClass("tank_boss")) do tank:Remove() end end local function moveToNextWave() local populatorInterface = ents.FindByClass("point_populator_interface") if not populatorInterface then util.PrintToChatAll("[RUN-WAVE][CRITICAL ERROR]: NO point_populator_interface FOUND") return end populatorInterface["$FinishWave"](populatorInterface) end local function tryFinishWave(currentWaveId) for subwaveName, state in pairs(subwaveState) do if not state.isSupport then if state.waveId == currentWaveId and not state.deadFinished then -- util.PrintToChatAll("[RUN-WAVE]: WAVE NOT FINISHED BECAUSE OF SUBWAVE " .. subwaveName) return end end end if RUN_WAVE_DEBUG_MODE then util.PrintToChatAll("[RUN-WAVE]: WAVE COMPLETE") end local mvmStats = ents.FindByClass("tf_mann_vs_machine_stats") for _, state in pairs(subwaveState) do if state.isSupport and state.unallocatedCurrency > 0 then local addCreditDroppedVscript = 'NetProps.SetPropInt(activator, "m_currentWaveStats.nCreditsDropped", NetProps.GetPropInt(activator, "m_currentWaveStats.nCreditsDropped") + %s)' mvmStats:RunScriptCode(addCreditDroppedVscript:format(state.unallocatedCurrency), mvmStats, mvmStats) DistributeMoney(state.unallocatedCurrency) if RUN_WAVE_DEBUG_MODE then util.PrintToChatAll( "[RUN-WAVE]: DISTRIBUTED " .. tostring(state.unallocatedCurrency) .. " FROM REMAINING SUPPORT SUBWAVE" ) end state.unallocatedCurrency = 0 end end killAll() waveOngoing = false activeWaveInfo = nil moveToNextWave() end local function dropTankCurrency(origin, owner, currency, shouldBeRedCash) while currency >= 25 do DropCurrencyPack(origin, owner, 25, shouldBeRedCash) currency = currency - 25 end while currency >= 10 do DropCurrencyPack(origin, owner, 10, shouldBeRedCash) currency = currency - 10 end while currency >= 5 do DropCurrencyPack(origin, owner, 5, shouldBeRedCash) currency = currency - 5 end if currency > 0 then DropCurrencyPack(origin, owner, currency, shouldBeRedCash) end end local PendingSpawnQueue = {} local function addSubwaveToQueue(state, pump, slotsRequired, currentWaveId) if state.pendingSpawn then return end state.pendingSpawn = true table.insert(PendingSpawnQueue, { waveId = currentWaveId, state = state, pump = pump, slotsRequired = slotsRequired, }) end local function processPendingSpawnQueue() local freeSlots = GetFreeBotSlotsCount() for i = #PendingSpawnQueue, 1, -1 do local item = PendingSpawnQueue[i] local state = item.state if state.waveId ~= item.waveId then table.remove(PendingSpawnQueue, i) else local batchBlocked = state.currentBatch == nil and next(state.activeBatches) ~= nil if not batchBlocked and freeSlots >= item.slotsRequired then table.remove(PendingSpawnQueue, i) state.pendingSpawn = nil item.pump() freeSlots = GetFreeBotSlotsCount() end end end end local function spawnBotTemplate(subwaveName, subwaveData, templateData, currentWaveId, pump) local state = subwaveState[subwaveName] local botSpawn = SpawnBot(templateData, subwaveData.Where) if not botSpawn then util.PrintToChatAll("[RUN-WAVE]: " .. subwaveName .. " FAILED TO SPAWN BOT") return end if RUN_WAVE_DEBUG_MODE then if templateData.TemplateName then util.PrintToChatAll("[RUN-WAVE]: SPAWNED TEMPLATE " .. templateData.TemplateName) else util.PrintToChatAll("[RUN-WAVE]: SPAWNED ROBOT WITH CLASS " .. templateData.Class) end end if subwaveData.IsMission then MissionBotSpawned(templateData, subwaveData) end local callbacks = {} local function clearCallbacks() for _, callbackId in pairs(callbacks) do botSpawn:RemoveCallback(callbackId) end end callbacks.spawned = botSpawn:AddCallback(ON_SPAWN, function() clearCallbacks() end) callbacks.damaged = botSpawn:AddCallback(ON_DAMAGE_RECEIVED_POST, function(_ent, damageInfo, previousHealth) -- count damage for buster if not activeWaveInfo then return end if not damageInfo.Attacker then return end if not damageInfo.Inflictor then return end local sentry = damageInfo.Inflictor if sentry:GetClassname() ~= "obj_sentrygun" then return end if sentry.m_bDisposableBuilding == 1 then return end local damageTaken = previousHealth - botSpawn.m_iHealth if damageTaken <= 0 then return end local ActiveSentries = activeWaveInfo.SentryBuster.ActiveSentries local sentryHandle = sentry:GetHandleIndex() if not ActiveSentries[sentryHandle] then makeActiveSentryForBuster(sentry) end if ActiveSentries[sentryHandle] then ActiveSentries[sentryHandle].DamageDealt = ActiveSentries[sentryHandle].DamageDealt + damageTaken checkSentryBuster() end end) local lastDamageInfo callbacks.damagePre = botSpawn:AddCallback(ON_DAMAGE_RECEIVED_PRE, function(_ent, damageInfo) if damageInfo then lastDamageInfo = damageInfo end end) callbacks.died = botSpawn:AddCallback(ON_DEATH, function() clearCallbacks() if subwaveData.IsMission then MissionBotDied(templateData) end if state.waveId ~= currentWaveId then return end local currency = 0 if state.remainingCurrencyBots > 0 then currency = math.floor(state.unallocatedCurrency / state.remainingCurrencyBots) state.unallocatedCurrency = state.unallocatedCurrency - currency state.remainingCurrencyBots = state.remainingCurrencyBots - 1 end botSpawn.Currency = currency local shouldBeRedCash = false if lastDamageInfo then if lastDamageInfo.Weapon then if string.find(lastDamageInfo.Weapon:GetClassname():lower(), "tf_weapon_sniperrifle") then shouldBeRedCash = true elseif lastDamageInfo.Attacker then if lastDamageInfo.Weapon:GetAttributeValue("collect currency on kill", true) or lastDamageInfo.Attacker:GetAttributeValue("collect currency on kill") then shouldBeRedCash = true end end end -- sentry kill if activeWaveInfo then local inflictor = lastDamageInfo.Inflictor if inflictor and inflictor:GetClassname() == "obj_sentrygun" then local ActiveSentries = activeWaveInfo.SentryBuster.ActiveSentries local sentry = inflictor local sentryHandle = sentry:GetHandleIndex() if not ActiveSentries[sentryHandle] then makeActiveSentryForBuster(sentry) end if ActiveSentries[sentryHandle] then ActiveSentries[sentryHandle].Kills = ActiveSentries[inflictor:GetHandleIndex()].Kills + 1 checkSentryBuster() end end end end if currency > 0 then DropCurrencyPack(botSpawn:GetAbsOrigin() + Vector(0, 0, 15), botSpawn, currency, shouldBeRedCash) end state.lastDeadTime = CurTime() state.dead = state.dead + 1 state.active = state.active - 1 local batch = botSpawn.RunWaveBatch if batch then batch.alive = batch.alive - 1 if batch.alive <= 0 then state.activeBatches[batch.id] = nil end end local function attemptPump() local waitBetweenSpawns = state.waitBetweenSpawns local shouldWaitAfterDead = state.shouldWaitBetweenSpawnsAfterDeath local elapsedTime if shouldWaitAfterDead then if botSpawn.RunWaveBatch and botSpawn.RunWaveBatch.alive > 0 then return end elapsedTime = CurTime() - state.lastDeadTime print(elapsedTime, waitBetweenSpawns) else elapsedTime = CurTime() - state.lastSpawnTime end if elapsedTime < waitBetweenSpawns then local remainingTime = waitBetweenSpawns - elapsedTime print("waiting for ", remainingTime) local retryTimer retryTimer = timer.Simple(remainingTime + 0.01, function() RemoveTimerFromWaveActive(retryTimer) attemptPump() end) table.insert(ActiveSubwaveTimers, retryTimer) return end pump() processPendingSpawnQueue() end local tryQueueTimerId tryQueueTimerId = timer.Simple(0.2, function() RemoveTimerFromWaveActive(tryQueueTimerId) attemptPump() end) table.insert(ActiveSubwaveTimers, tryQueueTimerId) end) return botSpawn end local function spawnTankTemplate(subwaveName, subwaveData, templateData, currentWaveId, pump) local state = subwaveState[subwaveName] local isSupport = subwaveData.SupportInfinite or subwaveData.SupportLimited local tankSpawn = SpawnTank(templateData, isSupport) if RUN_WAVE_DEBUG_MODE then util.PrintToChatAll("[RUN-WAVE]: SPAWNED A TANK WITH HEALTH " .. tostring(templateData.Health)) end local callbacks = {} local function clearCallbacks() for _, callbackId in pairs(callbacks) do tankSpawn:RemoveCallback(callbackId) end end callbacks.died = tankSpawn:AddCallback(ON_REMOVE, function() clearCallbacks() if not waveOngoing then return end -- despawned without dying if tankSpawn.m_iHealth > 0 then return end if state.waveId ~= currentWaveId then return end local currency = 0 if state.remainingCurrencyBots > 0 then currency = math.floor(state.unallocatedCurrency / state.remainingCurrencyBots) state.unallocatedCurrency = state.unallocatedCurrency - currency state.remainingCurrencyBots = state.remainingCurrencyBots - 1 end local shouldBeRedCash = false -- DropCurrencyPack(tankSpawn:GetAbsOrigin(), tankSpawn, currency, shouldBeRedCash) dropTankCurrency(tankSpawn:GetAbsOrigin() + Vector(0, 0, 15), tankSpawn, currency, shouldBeRedCash) state.dead = state.dead + 1 state.active = state.active - 1 local batch = tankSpawn.RunWaveBatch if batch then batch.alive = batch.alive - 1 if batch.alive <= 0 then state.activeBatches[batch.id] = nil end end pump() end) return tankSpawn end local function spawnTemplateInstance(subwaveName, subwaveData, templateData, currentWaveId, pump, state, batch) state.spawnedTotal = state.spawnedTotal + 1 state.active = state.active + 1 local spawnedEntity if templateData.IsTank then spawnedEntity = spawnTankTemplate(subwaveName, subwaveData, templateData, currentWaveId, pump) else spawnedEntity = spawnBotTemplate(subwaveName, subwaveData, templateData, currentWaveId, pump) end if not spawnedEntity then util.PrintToChatAll("[RUN-WAVE]: A BOT FAILED TO BE SPAWNED") state.spawnedTotal = state.spawnedTotal - 1 state.active = state.active - 1 return nil end state.currentSpawnIndex = state.currentSpawnIndex + 1 if state.currentSpawnIndex > subwaveData.TotalCount then state.currentSpawnIndex = 1 end if batch.alive == 0 then -- create batch after first successful spawn state.activeBatches[batch.id] = batch end batch.alive = batch.alive + 1 batch.remaining = batch.remaining - 1 if batch.remaining == 0 then state.currentBatch = nil end spawnedEntity.RunWaveBatch = batch return spawnedEntity end local function spawnSubwaveTemplates(subwaveName, subwaveData, currentWaveId) local state = subwaveState[subwaveName] local templateList = subwaveData.Templates local templateCount = #templateList if state.preAllocatedTemplates then templateCount = 1 end local maxActive = subwaveData.MaxActive local botsPerTemplatePerCycle = {} local basePerCycle = math.floor(subwaveData.SpawnCount / templateCount) local cycleRemainder = subwaveData.SpawnCount % templateCount for i = 1, templateCount do botsPerTemplatePerCycle[i] = basePerCycle + (i <= cycleRemainder and 1 or 0) end state.waveId = currentWaveId local function fireSpawnFinished() if state.spawnFinished then return end state.spawnFinished = true local waiters = state.spawnWaiters state.spawnWaiters = {} for _, cb in ipairs(waiters) do cb() end end local function getCurrentBatch() if state.currentBatch and state.currentBatch.remaining > 0 then return state.currentBatch end local maxConcurrentBatches = math.max(1, math.floor(maxActive / subwaveData.SpawnCount)) local activeBatchCount = 0 for _ in pairs(state.activeBatches) do activeBatchCount = activeBatchCount + 1 end if activeBatchCount >= maxConcurrentBatches then return nil end state.currentBatch = { id = state.nextBatchId, alive = 0, remaining = math.min(subwaveData.SpawnCount, state.spawnLimit - state.spawnedTotal), } state.nextBatchId = state.nextBatchId + 1 return state.currentBatch end local function pump() if state.waveId ~= currentWaveId then return end if state.spawnedTotal >= state.spawnLimit and state.active <= 0 and not state.deadFinished then state.deadFinished = true if RUN_WAVE_DEBUG_MODE then util.PrintToChatAll("[RUN-WAVE]: SUBWAVE COMPLETE " .. subwaveName) end local waiters = state.deadWaiters state.deadWaiters = {} for _, cb in ipairs(waiters) do cb() end tryFinishWave(currentWaveId) return end if state.spawnedTotal >= state.spawnLimit then return end if objectiveResource and objectiveResource.m_bMannVsMachineBetweenWaves then return end if not waveOngoing then return end local useIndividualTemplate = subwaveData.SpawnIndividualTemplate local isSquad = subwaveData.IsSquad local allSquads = { Bots = {}, Templates = {}, } local freeBotsSlot = GetFreeBotSlotsCount() if isSquad and not useIndividualTemplate then local squadCount = math.ceil(subwaveData.SpawnCount / templateCount) if squadCount > freeBotsSlot then addSubwaveToQueue(state, pump, squadCount, currentWaveId) if RUN_WAVE_DEBUG_MODE then util.PrintToChatAll( "[RUN-WAVE]: THIS SQUAD NEEDS " .. tostring(squadCount - freeBotsSlot) .. " MORE SLOTS TO SPAWN. SENT TO QUEUE" ) print("squad count: " .. tostring(squadCount), "free bots slot: " .. tostring(freeBotsSlot)) end return end for _ = 1, squadCount do local squadBots = {} local squadBotTemplates = {} local batch = getCurrentBatch() if not batch then addSubwaveToQueue(state, pump, 0, currentWaveId) return end local templatesToSpawn if state.preAllocatedTemplates then templatesToSpawn = {} for i = 1, templateCount do local templateData = state.preAllocatedTemplates[i] table.insert(templatesToSpawn, templateData) end else templatesToSpawn = templateList end for _, templateData in ipairs(templatesToSpawn) do if state.spawnedTotal >= state.spawnLimit then break end if state.active >= maxActive then return end local spawnedEntity = spawnTemplateInstance(subwaveName, subwaveData, templateData, currentWaveId, pump, state, batch) if spawnedEntity then table.insert(squadBots, spawnedEntity) table.insert(squadBotTemplates, templateData) end end table.insert(allSquads.Bots, squadBots) table.insert(allSquads.Templates, squadBotTemplates) end else local squadBots = {} local squadBotTemplates = {} local startIndex = useIndividualTemplate and state.nextTemplateIndex or 1 local botsNeeded = 0 for offset = 0, templateCount - 1 do local i = ((startIndex + offset - 1) % templateCount) + 1 botsNeeded = botsNeeded + botsPerTemplatePerCycle[i] end -- TODO: this check might not be correct, check later botsNeeded = math.min(botsNeeded, state.spawnLimit - state.spawnedTotal, maxActive - state.active) if botsNeeded > freeBotsSlot then addSubwaveToQueue(state, pump, botsNeeded, currentWaveId) if RUN_WAVE_DEBUG_MODE then util.PrintToChatAll( "[RUN-WAVE]: THIS SUBWAVE NEEDS " .. tostring(botsNeeded - freeBotsSlot) .. " MORE SLOTS TO SPAWN. SENT TO QUEUE" ) print("bots needed: " .. tostring(botsNeeded), "free bots slot: " .. tostring(freeBotsSlot)) end return end local batch = getCurrentBatch() if not batch then addSubwaveToQueue(state, pump, 0, currentWaveId) return end for offset = 0, templateCount - 1 do local i = ((startIndex + offset - 1) % templateCount) + 1 local spawnAmount = botsPerTemplatePerCycle[i] for _ = 1, spawnAmount do if state.spawnedTotal >= state.spawnLimit then break end if state.active >= maxActive then if useIndividualTemplate then state.nextTemplateIndex = i end return end if useIndividualTemplate then state.nextTemplateIndex = (i % templateCount) + 1 end local templateData if state.preAllocatedTemplates then templateData = state.preAllocatedTemplates[state.currentSpawnIndex + 1] else templateData = templateList[i] end local spawnedEntity = spawnTemplateInstance(subwaveName, subwaveData, templateData, currentWaveId, pump, state, batch) if spawnedEntity and isSquad then table.insert(squadBots, spawnedEntity) table.insert(squadBotTemplates, templateData) end end table.insert(allSquads.Bots, squadBots) table.insert(allSquads.Templates, squadBotTemplates) end end if isSquad and #allSquads.Bots > 0 then for i = 1, #allSquads.Bots do local bots = allSquads.Bots[i] local templates = allSquads.Templates[i] if #bots > 0 then HandleSquad(bots, templates) end end end if state.spawnedTotal >= state.spawnLimit then fireSpawnFinished() end state.lastSpawnTime = CurTime() if not state.missionAnnouncementSent then if subwaveData.SendSpyAnnouncement then if not state.isSupport then state.missionAnnouncementSent = true end if subwaveData.TotalCount > 0 then FireEvent("mvm_mission_update", { class = TF_CLASS_SPY, count = subwaveData.TotalCount, }) end elseif subwaveData.SendSniperAnnouncement then if not state.isSupport then state.missionAnnouncementSent = true end if subwaveData.TotalCount > 0 then for _, player in pairs(ents.GetAllPlayers()) do if player:IsRealPlayer() then player:AcceptInput("SpeakResponseConcept", "TLK_MVM_SNIPER_CALLOUT", player) end end end end end if RUN_WAVE_DEBUG_MODE then util.PrintToChatAll( "[RUN-WAVE]:" .. subwaveName .. " SPAWNED " .. tostring(state.spawnedTotal) .. "/" .. tostring(state.spawnLimit) .. " | ACTIVE " .. tostring(state.active) ) end end local function waitForDependencies(callback) local waitForSpawned = subwaveData.WaitForAllSpawned local waitForDead = subwaveData.WaitForAllDead if waitForSpawned then local dependencyState = subwaveState[waitForSpawned] if not dependencyState then util.PrintToChatAll( "[RUN-WAVE][ERROR]: " .. string.format( "Subwave '%s' WaitForAllSpawned references unknown subwave '%s'", subwaveName, tostring(waitForSpawned) ) ) end if dependencyState.waveId ~= currentWaveId or not dependencyState.spawnFinished then table.insert(dependencyState.spawnWaiters, function() waitForDependencies(callback) end) return end end if waitForDead then local dependencyState = subwaveState[waitForDead] if not dependencyState then util.PrintToChatAll( "[RUN-WAVE][ERROR]: " .. string.format( "Subwave '%s' WaitForAllDead references unknown subwave '%s'", subwaveName, tostring(waitForDead) ) ) end if dependencyState.waveId ~= currentWaveId or not dependencyState.deadFinished then table.insert(dependencyState.deadWaiters, function() waitForDependencies(callback) end) return end end callback() end local function runSpawnUntilFull() local waitBetweenSpawns = state.waitBetweenSpawns while true do if state.waveId ~= currentWaveId then return end if not waveOngoing then return end if objectiveResource and objectiveResource.m_bMannVsMachineBetweenWaves then return end local beforeSpawned = state.spawnedTotal pump() if state.spawnedTotal == beforeSpawned then return end if (waitBetweenSpawns or 0) > 0 then return end if state.spawnedTotal >= state.spawnLimit then return end if state.active >= maxActive then return end if GetFreeBotSlotsCount() <= 0 then return end end end local function runSpawnSchedule(isFirstSchedule) if state.waveId ~= currentWaveId then return end local waitBetweenSpawn = state.waitBetweenSpawns local shouldWaitAfterDead = state.shouldWaitBetweenSpawnsAfterDeath if shouldWaitAfterDead and not isFirstSchedule then return end local delay = 0 if not isFirstSchedule then if state.lastSpawnTime >= 0 then delay = math.max(0, (state.lastSpawnTime + waitBetweenSpawn) - CurTime()) else delay = waitBetweenSpawn end end local timerId timerId = timer.Simple(delay, function() RemoveTimerFromWaveActive(timerId) if state.waveId ~= currentWaveId then return end if not waveOngoing then return end runSpawnUntilFull() if state.spawnedTotal < state.spawnLimit then runSpawnSchedule(false) end end) table.insert(ActiveSubwaveTimers, timerId) end waitForDependencies(function() runSpawnSchedule(true) end) end -- should be ran on WaveInit, as !wave command will reset IncludeScripts function InitMission(missionInfo) if not objectiveResource then objectiveResource = ents.FindByClass("tf_objective_resource") end if not objectiveResource or not IsValid(objectiveResource) then util.PrintToChatAll("[RUN-WAVE]: FAILED TO INITIALIZE MISSION: MISSING tf_objective_resource") return end if not tfGamerules then tfGamerules = ents.FindByClass("tf_gamerules") end if not tfGamerules or not IsValid(tfGamerules) then util.PrintToChatAll("[RUN-WAVE]: MISSING tf_gamerules. THIS MAY CAUSE PROBLEMS") end if missionInfo.PopfileNameDisplay then objectiveResource:SetFakeSendProp("m_iszMvMPopfileName", missionInfo.PopfileNameDisplay) end if missionInfo.IncludeScripts then local includeVscript = "" for _, scriptString in pairs(missionInfo.IncludeScripts) do includeVscript = includeVscript .. "IncludeScript(`" .. scriptString .. "`);" end print(includeVscript) objectiveResource:RunScriptCode(includeVscript, objectiveResource, objectiveResource) end activeMissionInfo = missionInfo end function EditMissionInfo(changes) if not activeMissionInfo then print("no activeMissionInfo to edit") return end for i, v in pairs(changes) do if v == "_nil" then activeMissionInfo[i] = nil else activeMissionInfo[i] = v end end end function RunWave(waveData) waveOngoing = true waveStartTime = CurTime() makeActiveWaveInfo() ParseWave(waveData, true) WaveBarGen(waveData, true) local waveStartRelay = ents.FindByName("wave_start_relay") if waveStartRelay then waveStartRelay:AcceptInput("Trigger", nil, waveStartRelay) end objectiveResource = ents.FindByClass("tf_objective_resource") if objectiveResource then -- objectiveResource.m_nMvMWorldMoney = 0 UpdateWorldMoneyCount() end waveId = waveId + 1 local currentWaveId = waveId subwaveState = {} for _, subwaveData in pairs(waveData) do local isSupport = false if subwaveData.SupportInfinite or subwaveData.SupportLimited then isSupport = true end local subwaveName = subwaveData.Name subwaveState[subwaveName] = { waveId = currentWaveId, isSupport = isSupport, preAllocatedTemplates = subwaveData.PreAllocatedTemplates, -- nillable waitBetweenSpawns = subwaveData.WaitBetweenSpawnsAfterDeath or subwaveData.WaitBetweenSpawns or 0, shouldWaitBetweenSpawnsAfterDeath = subwaveData.WaitBetweenSpawnsAfterDeath ~= nil, lastSpawnTime = -1, lastDeadTime = -1, nextTemplateIndex = 1, nextBatchId = 1, activeBatches = {}, currentBatch = nil, missionAnnouncementSent = false, currentSpawnIndex = 0, -- from 0 to TotalCount, for SupportInfinite resets to 0 after TotalCount dead = 0, spawnLimit = subwaveData.SupportInfinite and math.huge or subwaveData.TotalCount, unallocatedCurrency = subwaveData.TotalCurrency or 0, remainingCurrencyBots = subwaveData.TotalCount, spawnFinished = false, deadFinished = false, spawnWaiters = {}, deadWaiters = {}, active = 0, spawnedTotal = 0, } end local validSubwaves = {} for _, subwaveData in pairs(waveData) do if ValidateSubwave(subwaveData, waveData) then table.insert(validSubwaves, subwaveData) else util.PrintToChatAll("[RUN-WAVE]: A CRITICAL ERROR HAS OCCURED WHILE LOADING WAVE. CHECK CONSOLE") end end for _, subwaveData in pairs(validSubwaves) do local subwaveName = subwaveData.Name local waitBeforeStarting = subwaveData.WaitBeforeStarting or 0 local timerId timerId = timer.Simple(waitBeforeStarting or 0, function() RemoveTimerFromWaveActive(timerId) if subwaveState[subwaveName] and subwaveState[subwaveName].waveId == currentWaveId then spawnSubwaveTemplates(subwaveName, subwaveData, currentWaveId) end end) table.insert(ActiveSubwaveTimers, timerId) end end local function waveInit(_wave) for _, timerId in pairs(ActiveSubwaveTimers) do local success, err = pcall(function() timer.Stop(timerId) end) if not success then print("failed to stop timer with id " .. timerId .. " due to " .. err) end end ActiveSubwaveTimers = {} PendingSpawnQueue = {} waveId = waveId + 1 local populatorInterface = ents.FindByClass("point_populator_interface") if not populatorInterface or not IsValid(populatorInterface) then populatorInterface = Entity("point_populator_interface", true) if RUN_WAVE_DEBUG_MODE then util.PrintToChatAll("[RUN-WAVE]: NO point_populator_interface FOUND, CREATING A NEW ONE") end -- util.PrintToChatAll("[RUN-WAVE][CRITICAL ERROR]: NO point_populator_interface FOUND") -- return end -- (royal): real populator is paused so that the empty wavespawns will not spawn and immediately end the wave populatorInterface["$PauseWavespawn"](populatorInterface, "1") UpdateWorldMoneyCount() end local function waveFinished() local waveFinishedRelay = ents.FindByName("wave_finished_relay") if waveFinishedRelay then waveFinishedRelay:AcceptInput("Trigger", nil, waveFinishedRelay) end end if OnWaveInitFuncs then table.insert(OnWaveInitFuncs, waveInit) else function OnWaveInit(wave) waveInit(wave) end end if OnWaveSuccessFuncs then table.insert(OnWaveInitFuncs, waveFinished) else function OnWaveSuccess() waveFinished() end end