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 Raffle = require "maps.chronosphere.raffles" local Public = {} local random = math.random local floor = math.floor local normal_area = {left_top = {-480, -480}, right_bottom = {480, 480}} local fish_area = {left_top = {-1100, -400}, right_bottom = {1100, 400}} -----------commands----------- local function move_to(position) local command = { type = defines.command.go_to_location, destination = position, distraction = defines.distraction.by_anything } return command end local function attack_target(target) if not target.valid then return end local command = { type = defines.command.attack, target = target, distraction = defines.distraction.by_anything, } return command end local function attack_area(position, radius) local command = { type = defines.command.attack_area, destination = position, radius = radius or 25, distraction = defines.distraction.by_anything } return command end local function attack_obstacles(surface, position) local commands = {} local obstacles = surface.find_entities_filtered{position = position, radius = 25, type = {"simple-entity", "tree", "simple-entity-with-owner"}, limit = 100} if obstacles then Rand.shuffle(obstacles) Rand.shuffle_distance(obstacles, position) 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_anything } end end end commands[#commands + 1] = move_to(position) local command = { type = defines.command.compound, structure_type = defines.compound_command.return_last, commands = commands } return command end local function multicommand(group, commands) if #commands > 0 then local command = { type = defines.command.compound, structure_type = defines.compound_command.return_last, commands = commands } group.set_command(command) end end local function multi_attack(surface, target) surface.set_multi_command({ command = attack_target(target), unit_count = 16 + random(1, floor(1 + game.forces["enemy"].evolution_factor * 100)) * Difficulty.get().difficulty_vote_value, force = "enemy", unit_search_distance = 1024 }) end ------------------------misc functions---------------------- local function generate_side_attack_target(surface, position) local targets = { "character", "pumpjack", "radar", "burner-mining-drill", "electric-mining-drill", "nuclear-reactor", "boiler", "assembling-machine-1", "assembling-machine-2", "assembling-machine-3", "oil-refinery", "centrifuge", "burner-inserter" } local entities = surface.find_entities_filtered{name = targets} if #entities < 1 then return false end entities = Rand.shuffle(entities) entities = Rand.shuffle_distance(entities, position) local weights = {} for index, _ in pairs(entities) do weights[#weights + 1] = 1 + floor((#entities - index) / 2) end return Rand.raffle(entities, weights) end local function generate_main_attack_target() local objective = Chrono_table.get_table() local targets = {objective.locomotive, objective.locomotive, objective.locomotive_cargo[1], objective.locomotive_cargo[2], objective.locomotive_cargo[3]} return targets[random(1, #targets)] end local function generate_expansion_position(start_pos) local objective = Chrono_table.get_table() local target_pos = objective.locomotive.position return {x = (start_pos.x * 0.90 + target_pos.x * 0.10) , y = (start_pos.y * 0.90 + target_pos.y * 0.10)} end local function get_random_close_spawner(surface) local objective = Chrono_table.get_table() local area = normal_area if objective.world.id == 7 then area = fish_area end local spawners = surface.find_entities_filtered({type = "unit-spawner", force = "enemy", area = area}) if not spawners[1] then return false end spawners = Rand.shuffle(spawners) spawners = Rand.shuffle_distance(spawners, objective.locomotive.position) local weights = {} for index, _ in pairs(spawners) do weights[#weights + 1] = 1 + floor((#spawners - index) / 2) end return Rand.raffle(spawners, weights) end local function is_biter_inactive(biter) if not biter.entity then return true end if not biter.entity.valid then return true end if not biter.entity.unit_group then return true end if not biter.entity.unit_group.valid then return true end if game.tick - biter.active_since > 162000 then biter.entity.destroy() return true end return false end local function get_active_biter_count() local bitertable = Chrono_table.get_biter_table() local count = 0 for k, biter in pairs(bitertable.active_biters) do if biter.entity.valid then count = count + 1 else bitertable[k] = nil end end return count end local function select_units_around_spawner(spawner) local bitertable = Chrono_table.get_biter_table() local difficulty = Difficulty.get().difficulty_vote_value local biters = spawner.surface.find_enemy_units(spawner.position, 50, "player") if not biters[1] then return nil 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 bitertable.active_biters[biter.unit_number] == nil then valid_biters[#valid_biters + 1] = biter bitertable.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 = #bitertable.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 = bitertable.biter_raffle[random(1, size_of_biter_raffle)] local position = spawner.surface.find_non_colliding_position(biter_name, spawner.position, 50, 2) if not position then break end local biter = spawner.surface.create_entity({name = biter_name, force = "enemy", position = position}) if bitertable.free_biters > 0 then bitertable.free_biters = bitertable.free_biters - 1 else local local_pollution = math.min(spawner.surface.get_pollution(spawner.position), 400 * game.map_settings.pollution.enemy_attack_pollution_consumption_modifier * game.forces.enemy.evolution_factor) spawner.surface.pollute(spawner.position, -local_pollution) game.pollution_statistics.on_flow("biter-spawner", -local_pollution) if local_pollution < 1 then break end end valid_biters[#valid_biters + 1] = biter bitertable.active_biters[biter.unit_number] = {entity = biter, active_since = game.tick} end end return valid_biters end local function pollution_requirement(surface, position, main) local objective = Chrono_table.get_table() if not position then position = objective.locomotive.position main = true end if objective.world.id == 7 then return true end local pollution = surface.get_pollution(position) local pollution_to_eat = Balance.pollution_spent_per_attack(Difficulty.get().difficulty_vote_value) local multiplier = 0.5 if main then multiplier = 4 end if pollution > multiplier * pollution_to_eat then surface.pollute(position, -pollution_to_eat) game.pollution_statistics.on_flow("small-biter", -pollution_to_eat) return true end return false end local function set_biter_raffle_table(surface) local objective = Chrono_table.get_table() local bitertable = Chrono_table.get_biter_table() local area = normal_area if objective.world.id == 7 then area = fish_area end local biters = surface.find_entities_filtered({type = "unit", force = "enemy", area = area, limit = 100}) if not biters[1] then return end local i = 1 for key, e in pairs(biters) do if key % 5 == 0 then bitertable.biter_raffle[i] = e.name i = i + 1 end end end local function create_attack_group(surface) local objective = Chrono_table.get_table() local bitertable = Chrono_table.get_biter_table() if get_active_biter_count() > 512 * Difficulty.get().difficulty_vote_value then return nil end local spawner = get_random_close_spawner(surface) if not spawner then return nil end local position = surface.find_non_colliding_position("rocket-silo", spawner.position, 256, 1) local units = select_units_around_spawner(spawner) if not units then return nil end local unit_group = surface.create_unit_group({position = position, force = "enemy"}) for _, unit in pairs(units) do unit_group.add_member(unit) end bitertable.unit_groups[unit_group.group_number] = unit_group return unit_group end --------------------------command functions------------------------------- local function colonize(group) --if _DEBUG then game.print(game.tick ..": colonizing") end local surface = group.surface local evo = floor(game.forces['enemy'].evolution_factor * 20) local nests = random(1 + evo, 2 + evo * 2) local commands = {} local biters = surface.find_entities_filtered {position = group.position, radius = 30, name = Raffle.biters, force = 'enemy'} local goodbiters = {} if #biters > 1 then for i = 1, #biters, 1 do if biters[i].unit_group == group then goodbiters[#goodbiters + 1] = biters[i] end end end local eligible_spawns if #goodbiters < 10 then if #group.members < 10 then group.destroy() end return else eligible_spawns = 1 + 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('biter-spawner', group.position, 20, 1, true) if pos then --game.print("[gps=" .. pos.x .. "," .. pos.y .."," .. surface.name .. "]") success = true if random(1, 5) == 1 then local evo = group.force.evolution_factor surface.create_entity({name = Raffle.worms[random(1 + floor(evo * 8), floor(1 + evo * 16))], position = pos, force = group.force}) else surface.create_entity({name = Raffle.spawners[random(1, #Raffle.spawners)], position = pos, force = group.force}) end else commands = { attack_obstacles(surface, group.position) } end end if success then for i = 1, #goodbiters, 1 do if goodbiters[i].valid then goodbiters[i].destroy() end end return end if #commands > 0 then --game.print("Attacking [gps=" .. commands[1].target.position.x .. "," .. commands[1].target.position.y .. "]") multicommand(group, commands) end end local function send_near_biters_to_objective() local objective = Chrono_table.get_table() if objective.game_lost then game.print("lost game") return end local target = generate_main_attack_target() if not target or not target.valid then game.print("no target") return end if pollution_requirement(target.surface, target.position, true) or random(1, math.max(1, 40 - objective.chronojumps)) == 1 then if _DEBUG then game.print(game.tick .. ": sending objective wave") end multi_attack(target.surface, target) end end local function attack_check(group, target, main) local commands = {} if pollution_requirement(group.surface, target.position, main) then commands = { attack_target(target), attack_area(target.position, 32) } else local position = generate_expansion_position(group.position) commands = { attack_obstacles(group.surface, position) } end multicommand(group, commands) end local function give_new_orders(group) local target = generate_side_attack_target(group.surface, group.position) if not target or not target.valid or not pollution_requirement(group.surface, target.position, false) then colonize(group) return end if not group or not group.valid or not target or not target.valid then return end local commands = { attack_target(target), attack_area(target.position, 32) } multicommand(group, commands) end ------------------------- tick minute functions-------------------- local function destroy_inactive_biters() local bitertable = Chrono_table.get_biter_table() for unit_number, biter in pairs(bitertable.active_biters) do if is_biter_inactive(biter, unit_number) then bitertable.active_biters[unit_number] = nil bitertable.free_biters = bitertable.free_biters + 1 end end end local function prepare_biters() local objective = Chrono_table.get_table() local surface = game.surfaces[objective.active_surface_index] set_biter_raffle_table(surface) --if _DEBUG then game.print(game.tick .. ": biters prepared") end end function Public.perform_rogue_attack() local objective = Chrono_table.get_table() local surface = game.surfaces[objective.active_surface_index] local group = create_attack_group(surface) if not group or not group.valid then return end local target = generate_side_attack_target(surface, group.position) if not target or not target.valid then return end attack_check(group, target, false) --if _DEBUG then game.print(game.tick ..": sending rogue attack") end end function Public.perform_main_attack() local objective = Chrono_table.get_table() local surface = game.surfaces[objective.active_surface_index] local group = create_attack_group(surface) local target = generate_main_attack_target() if not group or not group.valid or not target or not target.valid then return end attack_check(group, target, true) --if _DEBUG then game.print(game.tick ..": sending main attack") end end local function check_groups() local objective = Chrono_table.get_table() local bitertable = Chrono_table.get_biter_table() for index, group in pairs(bitertable.unit_groups) do if not group.valid or group.surface.index ~= objective.active_surface_index or #group.members < 1 then bitertable.unit_groups[index] = nil else if group.state == defines.group_state.finished then give_new_orders(group) elseif group.state == defines.group_state.gathering then group.start_moving() end end end end function Public.Tick_actions(tick) local objective = Chrono_table.get_table() if objective.chronojumps == 0 then return end if objective.passivetimer < 60 then return end local tick_minute_functions = { [100] = destroy_inactive_biters, [200] = prepare_biters, -- setup for main_attack [400] = Public.perform_rogue_attack, [700] = Public.perform_main_attack, [1000] = Public.perform_main_attack, [1300] = Public.perform_main_attack, [1500] = Public.perform_main_attack, -- call perform_main_attack 7 times on different ticks [3200] = send_near_biters_to_objective, [3500] = check_groups, } local key = tick % 3600 if tick_minute_functions[key] then tick_minute_functions[key]() end end return Public