mirror of
https://github.com/ComfyFactory/ComfyFactorio.git
synced 2024-12-30 23:17:53 +02:00
532 lines
15 KiB
Lua
532 lines
15 KiB
Lua
--- 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
|