local Chrono_table = require 'maps.chronosphere.table' local Balance = require 'maps.chronosphere.balance' local Difficulty = require 'modules.difficulty_vote' local Rand = require 'maps.chronosphere.random' local Public = {} local math_random = math.random local math_sqrt = math.sqrt local math_floor = math.floor local worm_raffle = {'small-worm-turret', 'medium-worm-turret', 'big-worm-turret', 'behemoth-worm-turret'} local spawner_raffle = {'biter-spawner', 'biter-spawner', 'biter-spawner', 'spitter-spawner'} local biter_raffle = { 'behemoth-biter', 'behemoth-spitter', 'big-biter', 'big-spitter', 'medium-biter', 'medium-spitter', 'small-biter', 'small-spitter' } local vector_radius = 480 local attack_vectors = {} for x = vector_radius * -1, vector_radius, 1 do for y = 0, vector_radius, 1 do local r = math_sqrt(x ^ 2 + y ^ 2) if r < vector_radius and r > vector_radius - 1 then attack_vectors[#attack_vectors + 1] = {x, y} end end end local size_of_vectors = #attack_vectors local function get_active_biter_count() local objective = Chrono_table.get_table() local count = 0 for k, _ in pairs(objective.active_biters) do count = count + 1 end return count end local function set_biter_raffle_table(surface) local objective = Chrono_table.get_table() -- It's fine to only sample the middle local area = {left_top = {-400, -400}, right_bottom = {400, 400}} local biters = surface.find_entities_filtered({type = "unit", force = "enemy", area = area}) if not biters[1] then return end local raffle = objective.biter_raffle local i = 1 for key, e in pairs(biters) do if key % 5 == 0 then raffle[i] = e.name i = i + 1 end end end local function is_biter_inactive(biter) if not biter.entity then --print("AI: active unit " .. unit_number .. " removed, possibly died.") return true end if not biter.entity.valid then --print("AI: active unit " .. unit_number .. " removed, biter invalid.") return true end if not biter.entity.unit_group then --print("AI: active unit " .. unit_number .. " at x" .. biter.entity.position.x .. " y" .. biter.entity.position.y .. " removed, had no unit group.") return true end if not biter.entity.unit_group.valid then --print("AI: active unit " .. unit_number .. " removed, unit group invalid.") return true end if game.tick - biter.active_since > 162000 then --print("AI: " .. "enemy" .. " unit " .. unit_number .. " timed out at tick age " .. game.tick - biter.active_since .. ".") biter.entity.destroy() return true end end local function set_active_biters(group) local objective = Chrono_table.get_table() if not group.valid then return end local active_biters = objective.active_biters for _, unit in pairs(group.members) do if not active_biters[unit.unit_number] then active_biters[unit.unit_number] = {entity = unit, active_since = game.tick} end end end function Public.destroy_inactive_biters() local objective = Chrono_table.get_table() if objective.passivetimer < 60 then return end for _, group in pairs(objective.unit_groups) do set_active_biters(group) end for unit_number, biter in pairs(objective.active_biters) do if is_biter_inactive(biter, unit_number) then objective.active_biters[unit_number] = nil end end end local function colonize(unit_group) local surface = unit_group.surface local evo = math_floor(game.forces['enemy'].evolution_factor * 20) local nests = math_random(1 + evo, 2 + evo * 2) local commands = {} local biters = surface.find_entities_filtered { position = unit_group.position, radius = 30, name = biter_raffle, force = 'enemy' } local goodbiters = {} if #biters > 1 then for i = 1, #biters, 1 do if biters[i].unit_group == unit_group then goodbiters[#goodbiters + 1] = biters[i] end end end local eligible_spawns if #goodbiters < 10 then --game.print("no biters to colonize") if #unit_group.members < 10 then unit_group.destroy() end return else eligible_spawns = 1 + math_floor(#goodbiters / 10) end local success = false for i = 1, nests, 1 do if eligible_spawns < i then break end local pos = surface.find_non_colliding_position('rocket-silo', unit_group.position, 20, 1, true) if not pos then local items = surface.find_entities_filtered { position = unit_group.position, radius = 32, type = 'item-entity', name = 'item-on-ground' } if #items > 0 then for i = 1, #items, 1 do if items[i].stack.name == 'stone' then items[i].destroy() end end pos = surface.find_non_colliding_position('rocket-silo', unit_group.position, 20, 1, true) end end if pos then --game.print("[gps=" .. e.position.x .. "," .. e.position.y .. "]") success = true if math_random(1, 5) == 1 then surface.create_entity( { name = worm_raffle[1 + math_floor((game.forces['enemy'].evolution_factor - 0.000001) * 4)], position = pos, force = unit_group.force } ) else surface.create_entity( {name = spawner_raffle[math_random(1, #spawner_raffle)], position = pos, force = unit_group.force} ) end else local obstacles = surface.find_entities_filtered { position = unit_group.position, radius = 10, type = {'simple-entity', 'tree'}, limit = 50 } if obstacles then for i = 1, #obstacles, 1 do if obstacles[i].valid then commands[#commands + 1] = { type = defines.command.attack, target = obstacles[i], distraction = defines.distraction.by_enemy } end end end end end if success then for i = 1, #goodbiters, 1 do if goodbiters[i].valid then goodbiters[i].destroy() end end end if #commands > 0 then --game.print("Attacking [gps=" .. commands[1].target.position.x .. "," .. commands[1].target.position.y .. "]") unit_group.set_command( { type = defines.command.compound, structure_type = defines.compound_command.return_last, commands = commands } ) end --unit_group.destroy() end function Public.send_near_biters_to_objective() local objective = Chrono_table.get_table() if objective.chronojumps == 0 then return end if objective.passivetimer < 60 then return end if not objective.locomotive then return end if not objective.locomotive_cargo[1] then return end if not objective.locomotive_cargo[2] then return end if not objective.locomotive_cargo[3] then return end local targets = {objective.locomotive, objective.locomotive, objective.locomotive_cargo[1], objective.locomotive_cargo[2], objective.locomotive_cargo[3]} local random_target = targets[math_random(1, #targets)] if objective.game_lost then return end local surface = random_target.surface local pollution = surface.get_pollution(random_target.position) local success = false local difficulty = Difficulty.get().difficulty_vote_value if pollution > 4 * Balance.pollution_spent_per_attack(difficulty) or objective.planet[1].type.id == 17 then local pollution_to_eat = Balance.pollution_spent_per_attack(difficulty) surface.pollute(random_target.position, -pollution_to_eat) game.pollution_statistics.on_flow("biter-spawner", -pollution_to_eat) success = true else if objective.chronojumps < 50 then if math_random(1, 50 - objective.chronojumps) == 1 then success = true end -- game.print("not enough pollution for objective attack") else success = true end end if success then -- game.print("sending objective wave") game.surfaces[objective.active_surface_index].set_multi_command({ command={ type=defines.command.attack, target=random_target, distraction=defines.distraction.none }, unit_count = 16 + math_random(1, math_floor(1 + game.forces["enemy"].evolution_factor * 100)) * Difficulty.get().difficulty_vote_value, force = "enemy", unit_search_distance=128 }) end end local function get_random_close_spawner(surface) local area = {left_top = {-1100, -500}, right_bottom = {1100, 500}} local spawners = surface.find_entities_filtered({type = "unit-spawner", force = "enemy", area = area}) if not spawners[1] then return false end local spawner = spawners[math_random(1,#spawners)] for i = 1, 5, 1 do local spawner_2 = spawners[math_random(1,#spawners)] if spawner_2.position.x ^ 2 + spawner_2.position.y ^ 2 < spawner.position.x ^ 2 + spawner.position.y ^ 2 then spawner = spawner_2 end end return spawner end local function select_units_around_spawner(spawner) local objective = Chrono_table.get_table() local difficulty = Difficulty.get().difficulty_vote_value local biters = spawner.surface.find_enemy_units(spawner.position, 160, "player") if not biters[1] then return false end local valid_biters = {} local unit_count = 0 for _, biter in pairs(biters) do if unit_count >= Balance.max_new_attack_group_size(difficulty) then break end if biter.force.name == "enemy" and objective.active_biters[biter.unit_number] == nil then valid_biters[#valid_biters + 1] = biter objective.active_biters[biter.unit_number] = {entity = biter, active_since = game.tick} unit_count = unit_count + 1 end end --Manual spawning of additional units local size_of_biter_raffle = #objective.biter_raffle if size_of_biter_raffle > 0 then for _ = 1, Balance.max_new_attack_group_size(difficulty) - unit_count, 1 do local biter_name = objective.biter_raffle[math_random(1, size_of_biter_raffle)] local position = spawner.surface.find_non_colliding_position(biter_name, spawner.position, 128, 2) if not position then break end local biter = spawner.surface.create_entity({name = biter_name, force = "enemy", position = position}) valid_biters[#valid_biters + 1] = biter objective.active_biters[biter.unit_number] = {entity = biter, active_since = game.tick} end end return valid_biters end local function generate_attack_target(nearest_player_unit) local objective = Chrono_table.get_table() if objective.game_lost then return nil end local target = Rand.raffle( { nearest_player_unit, objective.locomotive, objective.locomotive_cargo[1], objective.locomotive_cargo[2], objective.locomotive_cargo[3], "pumpjack", "radar" }, {3, 2, 1, 1, 1, 2, 1} ) if target == "pumpjack" or target == "radar" then local entities = game.surfaces[objective.active_surface_index].find_entities_filtered({ name = target }) entities=Rand.shuffle(entities) local entity = false for _, e in pairs(entities) do if e.is_connected_to_electric_network() then entity = e end end if entity and entity.valid then target = entity else target = objective.locomotive end end return target end local function send_group(unit_group, nearest_player_unit) local objective = Chrono_table.get_table() local difficulty = Difficulty.get().difficulty_vote_value local target = generate_attack_target(nearest_player_unit) if not target or not target.valid then colonize(unit_group) return end local surface = target.surface local pollution = surface.get_pollution(target.position) if pollution > 4 * Balance.pollution_spent_per_attack(difficulty) or objective.planet[1].type.id == 17 then local pollution_to_eat = Balance.pollution_spent_per_attack(difficulty) surface.pollute(target.position, -pollution_to_eat) game.pollution_statistics.on_flow("biter-spawner", -pollution_to_eat) if #unit_group.members > 0 then game.pollution_statistics.on_flow(unit_group.members[1].name or "small-biter", - pollution_to_eat) end local commands = {} local vector = attack_vectors[math_random(1, size_of_vectors)] local position = {target.position.x + vector[1], target.position.y + vector[2]} position = unit_group.surface.find_non_colliding_position("stone-furnace", position, 96, 1) if position then commands[#commands + 1] = { type = defines.command.attack_area, destination = position, radius = 24, distraction = defines.distraction.by_enemy } end commands[#commands + 1] = { type = defines.command.attack_area, destination = target.position, radius = 32, distraction = defines.distraction.by_enemy } commands[#commands + 1] = { type = defines.command.attack, target = target, distraction = defines.distraction.by_enemy } unit_group.set_command({ type = defines.command.compound, structure_type = defines.compound_command.return_last, commands = commands }) else colonize(unit_group) end return true end local function is_chunk_empty(surface, area) if surface.count_entities_filtered({type = {"unit-spawner", "unit"}, area = area}) ~= 0 then return false end if surface.count_entities_filtered({force = "player", area = area}) ~= 0 then return false end if surface.count_tiles_filtered({name = {"water", "deepwater"}, area = area}) ~= 0 then return false end return true end local function get_unit_group_position(surface, nearest_player_unit, spawner) if math_random(1,3) ~= 1 then local spawner_chunk_position = {x = math.floor(spawner.position.x / 32), y = math.floor(spawner.position.y / 32)} local valid_chunks = {} for x = -2, 2, 1 do for y = -2, 2, 1 do local chunk = {x = spawner_chunk_position.x + x, y = spawner_chunk_position.y + y} local area = {{chunk.x * 32, chunk.y * 32},{chunk.x * 32 + 32, chunk.y * 32 + 32}} if is_chunk_empty(surface, area) then valid_chunks[#valid_chunks + 1] = chunk end end end if #valid_chunks > 0 then local chunk = valid_chunks[math_random(1, #valid_chunks)] return {x = chunk.x * 32 + 16, y = chunk.y * 32 + 16} end end local unit_group_position = {x = (spawner.position.x + nearest_player_unit.position.x * 0.2) , y = (spawner.position.y + nearest_player_unit.position.y * 0.2)} local pos = surface.find_non_colliding_position("rocket-silo", unit_group_position, 256, 1) if pos then unit_group_position = pos end if not unit_group_position then return false end return unit_group_position end local function create_attack_group(surface) local objective = Chrono_table.get_table() if 256 - get_active_biter_count() < 256 then return false end local spawner = get_random_close_spawner(surface) if not spawner then return false end local nearest_player_unit = surface.find_nearest_enemy({position = spawner.position, max_distance = 1024, force = "enemy"}) if not nearest_player_unit then nearest_player_unit = objective.locomotive end local unit_group_position = get_unit_group_position(surface, nearest_player_unit, spawner) local units = select_units_around_spawner(spawner) if not units then return false end local unit_group = surface.create_unit_group({position = unit_group_position, force = "enemy"}) for _, unit in pairs(units) do unit_group.add_member(unit) end send_group(unit_group, nearest_player_unit) objective.unit_groups[unit_group.group_number] = unit_group end -- function Public.rogue_group() -- local objective = Chrono_table.get_table() -- if objective.passivetimer < 60 then return end -- if not objective.locomotive then return end -- local surface = game.surfaces[objective.active_surface_index] -- local spawner = get_random_close_spawner(surface) -- if not spawner then -- return false -- end -- local position = {x = ((spawner.position.x + objective.locomotive.position.x) * 0.5) , y = ((spawner.position.y + objective.locomotive.position.y) * 0.5)} -- local pos = surface.find_non_colliding_position("rocket-silo", position, 30, 1, true) -- if not pos then return end -- surface.set_multi_command({ -- command={ -- type=defines.command.build_base, -- destination=pos, -- distraction=defines.distraction.none, -- ignore_planner = true -- }, -- unit_count = 16, -- force = "enemy", -- unit_search_distance=128 -- }) -- end function Public.pre_main_attack() local objective = Chrono_table.get_table() if objective.chronojumps == 0 then return end if objective.passivetimer < 60 then return end local surface = game.surfaces[objective.active_surface_index] set_biter_raffle_table(surface) end function Public.perform_main_attack() local objective = Chrono_table.get_table() if objective.chronojumps == 0 then return end if objective.passivetimer < 60 then return end local surface = game.surfaces[objective.active_surface_index] create_attack_group(surface) end function Public.wake_up_sleepy_groups() local objective = Chrono_table.get_table() if objective.chronojumps == 0 then return end if objective.passivetimer < 60 then return end local entity local unit_group for _, biter in pairs(objective.active_biters) do entity = biter.entity if entity then if entity.valid then unit_group = entity.unit_group if unit_group then if unit_group.valid then if unit_group.state == defines.group_state.finished then local nearest_player_unit = entity.surface.find_nearest_enemy({position = entity.position, max_distance = 2048, force = "enemy"}) if not nearest_player_unit then nearest_player_unit = objective.locomotive end local destination = unit_group.surface.find_non_colliding_position("rocket-silo", unit_group.position, 32, 1) if not destination then destination = {x = unit_group.position.x + math_random(-10,10), y = unit_group.position.y + math_random(-10,10)} end unit_group.set_command({ type = defines.command.go_to_location, destination = destination, distraction = defines.distraction.by_enemy }) send_group(unit_group, nearest_player_unit) return elseif unit_group.state == defines.group_state.gathering and not unit_group.surface.find_non_colliding_position("rocket-silo", unit_group.position, 3, 1) then local destination = unit_group.surface.find_non_colliding_position("rocket-silo", unit_group.position, 32, 1) if not destination then destination = {x = unit_group.position.x + math_random(-10,10), y = unit_group.position.y + math_random(-10,10)} end unit_group.set_command({ type = defines.command.go_to_location, destination = destination, distraction = defines.distraction.by_enemy }) -- local nearest_player_unit = entity.surface.find_nearest_enemy({position = entity.position, max_distance = 2048, force = "enemy"}) -- if not nearest_player_unit then nearest_player_unit = objective.locomotive end -- send_group(unit_group, nearest_player_unit) return end end end end end end end return Public