--- created by Gerkiz local Event = require 'utils.event' local Color = require 'utils.color_presets' local Utils = require 'utils.common' local Global = require 'utils.global' local Token = require 'utils.token' local Task = require 'utils.task' local this = { timers = {}, characters = {}, characters_unit_numbers = {} } Global.register( this, function(tbl) this = tbl end ) local Public = {events = {on_entity_mined = Event.generate_event_name('on_entity_mined')}} local max_keepalive = 54000 -- 15 minutes local remove = table.remove local round = math.round local default_radius = 5 Public.command = { noop = 0, seek_and_destroy_cmd = 1, seek_and_mine_cmd = 2 } local clear_corpse_token = Token.register( function(event) local position = event.position local surface = game.get_surface(event.surface_index) local search_info = { type = 'character-corpse', position = position, radius = 1 } local corpses = surface.find_entities_filtered(search_info) if corpses and #corpses > 0 then for _, corpse in pairs(corpses) do if corpse and corpse.valid then if corpse.character_corpse_player_index == 65536 then corpse.destroy() end end end end end ) local function char_callback(callback) local entities = this.characters for i = 1, #entities do local data = entities[i] if data and data.entity and data.entity.valid then callback(data) end end end local function get_near_position(entity) return {x = round(entity.position.x, 0), y = round(entity.position.y, 0)} end local function is_mining_target_taken(selected) if not selected then return false end char_callback( function(data) local entity = data.entity if entity.selected == selected then return true end end ) return false end local function add_character(player_index, entity, render_id, data) local index = #this.characters + 1 if not this.characters[index] then this.characters[index] = { player_index = player_index, index = index, unit_number = entity.unit_number, entity = entity, ttl = game.tick + (data.ttl or max_keepalive), command = data.command, radius = default_radius, max_radius_mine = 20, max_radius_destroy = 150, render_id = render_id, search_local = data.search_local or false, walking_position = {count = 1, position = get_near_position(entity)} } end if not this.characters_unit_numbers[entity.unit_number] then this.characters_unit_numbers[entity.unit_number] = true end end local function exists_character(unit_number) if not next(this.characters_unit_numbers) then return end if this.characters_unit_numbers[unit_number] then return true end return false end local function remove_character(unit_number) if not next(this.characters) then return end for index, data in pairs(this.characters) do if data and data.unit_number == unit_number then if data.entity and data.entity.valid then data.entity.destroy() end if rendering.is_valid(data.render_id) then rendering.destroy(data.render_id) end remove(this.characters, index) end end if this.characters_unit_numbers[unit_number] then this.characters_unit_numbers[unit_number] = nil end end local function get_dir(src, dest) local src_x = Utils.get_axis(src, 'x') local src_y = Utils.get_axis(src, 'y') local dest_x = Utils.get_axis(dest, 'x') local dest_y = Utils.get_axis(dest, 'y') local step = { x = nil, y = nil } local precision = Utils.rand_range(1, 10) if dest_x - precision > src_x then step.x = 1 elseif dest_x < src_x - precision then step.x = -1 else step.x = 0 end if dest_y - precision > src_y then step.y = 1 elseif dest_y < src_y - precision then step.y = -1 else step.y = 0 end return Utils.direction_lookup[step.x][step.y] end local function move_to(entity, target, min_distance) local state = { walking = false } local distance = Utils.get_distance(target.position, entity.position) if min_distance < distance then local dir = get_dir(entity.position, target.position) if dir then state = { walking = true, direction = dir } end end entity.walking_state = state return state.walking end local function refill_ammo(entity) if not entity or not entity.valid then return end local weapon = entity.get_inventory(defines.inventory.character_guns)[entity.selected_gun_index] if weapon and weapon.valid_for_read then local selected_ammo = entity.get_inventory(defines.inventory.character_ammo)[entity.selected_gun_index] if selected_ammo then if not selected_ammo.valid_for_read then if weapon.name == 'shotgun' then entity.insert({name = 'shotgun-shell', count = 5}) end if weapon.name == 'pistol' then entity.insert({name = 'firearm-magazine', count = 5}) end end end end end local function shoot_at(entity, target) entity.selected = target entity.shooting_state = { state = defines.shooting.shooting_enemies, position = target.position } end local function check_progress_and_raise_event(data) if data.entity.selected and data.entity.character_mining_progress >= 0.95 then if not data.raised_event then data.raised_event = true Event.raise( Public.events.on_entity_mined, { player_index = data.player_index, entity = data.entity.selected, surface = data.entity.surface, script_character = data.entity } ) end end end local function mine_entity(data, target) data.entity.selected = target data.entity.mining_state = {mining = true, position = target.position} end local function shoot_stop(entity) entity.shooting_state = { state = defines.shooting.not_shooting, position = {0, 0} } end local function insert_weapons(entity) if not entity or not entity.valid then return end local weapon = entity.get_inventory(defines.inventory.character_guns)[entity.selected_gun_index] if weapon and weapon.valid_for_read then return end if Utils.rand_range(1, 15) == 1 then entity.insert({name = 'shotgun', count = 1}) entity.insert({name = 'shotgun-shell', count = 5}) elseif Utils.rand_range(1, 10) == 1 then entity.insert({name = 'submachine-gun', count = 1}) entity.insert({name = 'firearm-magazine', count = 5}) else entity.insert({name = 'pistol', count = 1}) entity.insert({name = 'firearm-magazine', count = 5}) end end local function seek_and_mine(data) if data.radius >= data.max_radius_mine then if data.overriden_command then data.command = data.overriden_command data.overriden_command = nil return else data.radius = 1 end end local entity = data.entity if not entity or not entity.valid then remove_character(data.unit_number) return end local surface = entity.surface local player_index = data.player_index local player = game.get_player(player_index) if not player or not player.valid or not player.connected then remove_character(data.unit_number) return end local position if data.search_local then position = entity.position else position = player.position end local search_info = { position = position, radius = data.radius, type = { 'simple-entity-with-owner', 'simple-entity', 'tree' }, force = { 'neutral' } } local closest = surface.find_entities_filtered(search_info) if #closest ~= 0 then local target = Utils.get_closest_neighbour_non_player(entity.position, closest) if not target then data.radius = data.radius + 1 return end data.radius = 1 if not move_to(entity, target, 1) then if not is_mining_target_taken(target) then if data.raised_event then data.raised_event = nil end if entity.can_reach_entity(target) then mine_entity(data, target) else move_to(entity, target, 1) end end if data.overriden_command then data.command = data.overriden_command data.overriden_command = nil end end else data.radius = data.radius + 1 end end local function seek_enemy_and_destroy(data) if data.radius >= data.max_radius_destroy then remove_character(data.unit_number) return end local entity = data.entity if not entity or not entity.valid then remove_character(data.unit_number) return end local surface = entity.surface local player_index = data.player_index local player = game.get_player(player_index) if not player or not player.valid or not player.connected then remove_character(data.unit_number) return end local search_info = { type = {'unit', 'unit-spawner', 'turret'}, position = entity.position, radius = data.radius, force = 'enemy' } local closest = surface.find_entities_filtered(search_info) if #closest ~= 0 then local target = Utils.get_closest_neighbour_non_player(entity.position, closest) if not target then data.radius = data.radius + 5 return end data.radius = default_radius insert_weapons(entity) refill_ammo(entity) local inside = ((entity.position.x - data.walking_position.position.x) ^ 2 + (entity.position.y - data.walking_position.position.y) ^ 2) < 1 ^ 2 data.walking_position.position = get_near_position(entity) if inside then data.walking_position.count = data.walking_position.count + 1 end if data.walking_position.count == 3 then data.radius = 1 data.walking_position.count = 1 data.overriden_command = data.command data.command = Public.command.seek_and_mine_cmd seek_and_mine(data) else if not move_to(entity, target, Utils.rand_range(5, 10)) then shoot_at(entity, target) else shoot_stop(entity) end end else data.radius = data.radius + 5 end end --- Creates a new character that seeks and does stuff. ---@param data table ----- @usage local Ai = require 'modules.ai' Ai.create_char({player_index = game.player.index, command = 1}) function Public.create_char(data) if not data or not type(data) == 'table' then return error('No data was provided or the provided data was not a table.', 2) end if not data.player_index or not data.command then return error('No correct data was not provided.', 2) end if data.command ~= Public.command.seek_and_destroy_cmd and data.command ~= Public.command.attack_objects_cmd and data.command ~= Public.command.seek_and_mine_cmd then return error('No correct command was not provided.', 2) end local player = game.get_player(data.player_index) if not player or not player.valid or not player.connected then return error('Provided player was not valid or not connected.', 2) end local surface = player.surface local valid_position = surface.find_non_colliding_position('character', {x = player.position.x, y = player.position.y + 2}, 3, 0.5) if not valid_position then return end local entity = surface.create_entity {name = 'character', position = valid_position, force = player.force} if not entity or not entity.valid then return end entity.associated_player = player if player.character_health_bonus >= 200 then entity.character_health_bonus = player.character_health_bonus / 2 end entity.color = player.color local index = #this.characters + 1 local render_id = rendering.draw_text { text = player.name .. "'s drone #" .. index, surface = player.surface, target = entity, target_offset = {0, -2.25}, color = Color.orange, scale = 1.00, font = 'default-large-semibold', alignment = 'center', scale_with_zoom = false } add_character(player.index, entity, render_id, data) end Event.on_nth_tick( 2, function() char_callback( function(data) check_progress_and_raise_event(data) end ) end ) Event.on_nth_tick( 10, function() local tick = game.tick char_callback( function(data) if data.ttl <= tick then remove_character(data.unit_number) return end local command = data.command if command == Public.command.seek_and_destroy_cmd then seek_enemy_and_destroy(data) elseif command == Public.command.seek_and_mine_cmd then seek_and_mine(data) end end ) end ) Event.add( defines.events.on_entity_died, function(event) local entity = event.entity if not entity or not entity.valid then return end if entity.type ~= 'character' then return end local unit_number = entity.unit_number if not exists_character(unit_number) then return end Task.set_timeout_in_ticks(1, clear_corpse_token, {position = entity.position, surface_index = entity.surface.index}) remove_character(unit_number) end ) return Public