744 lines
19 KiB
Lua
744 lines
19 KiB
Lua
local States = require("script/character_interface/states/states")
|
|
local util = require("script/character_script_util")
|
|
|
|
local script_data =
|
|
{
|
|
characters = {}
|
|
}
|
|
|
|
local Character = {}
|
|
|
|
Character.metatable =
|
|
{
|
|
__index = Character,
|
|
}
|
|
|
|
function Character.get_character(unit_number)
|
|
local character = script_data.character[unit_number]
|
|
if character and character.entity.valid then
|
|
return character
|
|
end
|
|
end
|
|
|
|
function Character.new(character_entity)
|
|
|
|
if character_entity.type ~= "character" then
|
|
error("Character interface only works on character entities." .. character_entity.type)
|
|
end
|
|
|
|
script.register_on_entity_destroyed(character_entity)
|
|
|
|
local character =
|
|
{
|
|
entity = character_entity,
|
|
state_stack = {},
|
|
index = character_entity.unit_number,
|
|
}
|
|
|
|
setmetatable(character, Character.metatable)
|
|
|
|
script_data.characters[character.index] = character
|
|
return character
|
|
end
|
|
|
|
function Character:update()
|
|
--error(serpent.block(self.state_stack))
|
|
if not self.entity.valid then
|
|
return
|
|
end
|
|
|
|
local state = self.state_stack[1]
|
|
if state and state.update then
|
|
state:update()
|
|
end
|
|
|
|
end
|
|
|
|
function Character:pop_state(dont_update_state)
|
|
self:print("Popped state "..self.state_stack[1].type)
|
|
table.remove(self.state_stack, 1)
|
|
--maybe?
|
|
if dont_update_state == nil or dont_update_state == false then
|
|
self:update()
|
|
end
|
|
end
|
|
|
|
function Character:push_state(state)
|
|
self:print("Pushed state "..state.type)
|
|
table.insert(self.state_stack, 1, state)
|
|
end
|
|
|
|
function Character:emplace_state(state)
|
|
self:print("Emplaced state "..state.type)
|
|
table.insert(self.state_stack, state)
|
|
end
|
|
|
|
function Character:clear_state()
|
|
for k, state in pairs (self.state_stack) do
|
|
if state.cleanup then
|
|
state:cleanup()
|
|
end
|
|
end
|
|
self.state_stack = {}
|
|
end
|
|
|
|
function Character:follow(target, distance)
|
|
self:push_state(States.follow(self, target, distance))
|
|
end
|
|
|
|
local resource_type =
|
|
{
|
|
["resource"] = true,
|
|
["tree"] = true,
|
|
["simple-entity"] = true
|
|
}
|
|
|
|
function Character:get_mining_distance(type)
|
|
if type and resource_type[type] then
|
|
return self.entity.resource_reach_distance
|
|
else
|
|
return self.entity.reach_distance
|
|
end
|
|
end
|
|
|
|
function Character:get_repair_distance()
|
|
return self.entity.reach_distance
|
|
end
|
|
|
|
function Character:get_reach_distance()
|
|
return self.entity.reach_distance
|
|
end
|
|
|
|
function Character:get_slurp_distance()
|
|
return self.entity.item_pickup_distance
|
|
end
|
|
|
|
function Character:get_build_distance()
|
|
return self.entity.build_distance
|
|
end
|
|
|
|
function Character:can_reach_entity(entity)
|
|
return self.entity.can_reach_entity(entity)
|
|
end
|
|
|
|
function Character:distance(entity_or_position)
|
|
return util.distance(self.entity.position, entity_or_position.position or entity_or_position)
|
|
end
|
|
|
|
function Character:move_to(entity_or_position, distance, timeout)
|
|
local vehicle = self.entity.vehicle
|
|
if vehicle then
|
|
if vehicle.type == "spider-vehicle" then
|
|
self:push_state(States.moving_spider(self, entity_or_position, distance))
|
|
return
|
|
end
|
|
if vehicle.type == "car" then
|
|
self:push_state(States.moving_vehicle(self, entity_or_position, distance or 10))
|
|
return
|
|
end
|
|
end
|
|
self:push_state(States.moving(self, entity_or_position, distance, timeout))
|
|
end
|
|
|
|
function Character:mine(entity, amount)
|
|
self:push_state(States.mining(self, entity, amount))
|
|
end
|
|
|
|
function Character:batch_job(job_type, entities)
|
|
self:push_state(States.batch_job(self, job_type, entities))
|
|
end
|
|
|
|
function Character:find_and_batch(job_type, param)
|
|
self:push_state(States.find_and_batch(self, job_type, param))
|
|
end
|
|
|
|
function Character:repair(entity)
|
|
self:push_state(States.repair(self, entity))
|
|
end
|
|
|
|
function Character:upgrade(entity)
|
|
self:push_state(States.upgrade(self, entity))
|
|
end
|
|
|
|
function Character:attack(entity, timeout)
|
|
self:push_state(States.attack(self, entity, timeout))
|
|
end
|
|
|
|
function Character:attack_area(position, radius)
|
|
self:push_state(States.combat(self, position, radius))
|
|
end
|
|
|
|
function Character:build_ghost(entity)
|
|
self:push_state(States.build_ghost(self, entity))
|
|
end
|
|
|
|
function Character:wait(ticks)
|
|
self:push_state(States.idle(self, ticks))
|
|
end
|
|
|
|
function Character:nearest_enemy(radius, position)
|
|
return self.entity.surface.find_nearest_enemy
|
|
{
|
|
position = position or self.entity.position,
|
|
max_distance = radius or 64,
|
|
force = self.entity.force
|
|
}
|
|
end
|
|
|
|
function Character:defer_job(position, entity)
|
|
self:emplace_state(States.deferred_job(self, position, entity))
|
|
end
|
|
|
|
function Character:is_idle()
|
|
return not self.state_stack[1]
|
|
end
|
|
|
|
function Character:clear_remark()
|
|
if not self.remark_id then return end
|
|
return rendering.is_valid(self.remark_id) and rendering.destroy(self.remark_id)
|
|
end
|
|
|
|
function Character:remark(string, color)
|
|
self:clear_remark()
|
|
self.remark_id = rendering.draw_text
|
|
{
|
|
text = string,
|
|
surface = self.entity.surface,
|
|
target = self.entity,
|
|
target_offset = {0, -3},
|
|
color = color or {1, 1, 1},
|
|
scale = 1,
|
|
time_to_live = 120,
|
|
forces = {self.entity.force},
|
|
players = {self.player},
|
|
visible = true,
|
|
draw_on_ground = false,
|
|
orientation = 0,
|
|
alignment = "center",
|
|
scale_with_zoom = true,
|
|
only_in_alt_mode = false
|
|
}
|
|
end
|
|
|
|
local can_follow =
|
|
{
|
|
["car"] = true,
|
|
["locomotive"] = true,
|
|
["cargo-wagon"] = true,
|
|
["fluid-wagon"] = true,
|
|
["artillery-wagon"] = true,
|
|
["unit"] = true,
|
|
["character"] = true,
|
|
|
|
}
|
|
|
|
function Character:determine_job(entity, position)
|
|
|
|
if not (entity and entity.valid) then
|
|
self:remark{"move-command", string.format("(%.0f, %.0f)", position.x, position.y)}
|
|
self:move_to{position.x, position.y}
|
|
return
|
|
end
|
|
|
|
if self.entity.vehicle then
|
|
-- If we're in a vehicle, we just go to the position
|
|
self:remark{"move-command", string.format("(%.0f, %.0f)", position.x, position.y)}
|
|
self:move_to{position.x, position.y}
|
|
return
|
|
end
|
|
|
|
local force = entity.force
|
|
|
|
if force == self.entity.force then
|
|
|
|
if entity.type == "entity-ghost" then
|
|
self:remark{"build-command"}
|
|
self:find_and_batch("build", {type = "entity-ghost", force = force})
|
|
self:build_ghost(entity, -1)
|
|
return
|
|
end
|
|
|
|
if entity.type == "tile-ghost" then
|
|
self:remark{"build-command"}
|
|
self:find_and_batch("build", {type = "tile-ghost", force = force})
|
|
self:build_ghost(entity, -1)
|
|
return
|
|
end
|
|
|
|
if entity.to_be_deconstructed() then
|
|
self:remark{"deconstruct-command"}
|
|
self:find_and_batch("mine", {to_be_deconstructed = true, force = force})
|
|
self:mine(entity, -1)
|
|
return
|
|
end
|
|
|
|
if entity.get_health_ratio() and entity.get_health_ratio() < 1 then
|
|
self:remark{"repair-command"}
|
|
self:find_and_batch("repair", {radius = 50, force = force})
|
|
self:repair(entity)
|
|
return
|
|
end
|
|
|
|
if entity.to_be_upgraded() then
|
|
self:remark{"upgrade-command"}
|
|
self:find_and_batch("upgrade", {to_be_upgraded = true})
|
|
self:upgrade(entity)
|
|
return
|
|
end
|
|
|
|
local fuel_inventory = entity.get_fuel_inventory()
|
|
if fuel_inventory and fuel_inventory.is_empty() then
|
|
self:remark{"fuel-command"}
|
|
self:find_and_batch("fuel", {radius = 50, force = force})
|
|
self:fuel_entity(entity)
|
|
return
|
|
end
|
|
|
|
end
|
|
|
|
if entity.to_be_deconstructed() and (force.name == "neutral") then
|
|
self:remark{"deconstruct-command"}
|
|
self:find_and_batch("mine", {to_be_deconstructed = true, force = {"neutral", self.entity.force.name}})
|
|
self:mine(entity, -1)
|
|
return
|
|
end
|
|
|
|
if entity.type == "resource" then
|
|
self:remark{"mine-entity-command", entity.localised_name}
|
|
self:find_and_batch("mine", {type = "resource", name = entity.name, amount = -1})
|
|
self:mine(entity, -1)
|
|
return
|
|
end
|
|
|
|
if entity.type == "tree" then
|
|
self:remark{"mine-trees-command"}
|
|
self:find_and_batch("mine", {type = "tree"})
|
|
self:mine(entity, -1)
|
|
return
|
|
end
|
|
|
|
if entity.type == "simple-entity" and force.name == "neutral" then
|
|
self:remark{"mine-entity-command", entity.localised_name}
|
|
self:find_and_batch("mine", {type = "simple-entity"})
|
|
self:mine(entity, -1)
|
|
return
|
|
end
|
|
|
|
if self.entity.force.get_cease_fire(entity.force) == false then
|
|
self:remark{"attack-command"}
|
|
--self:find_and_batch("attack", {radius = 50, force = entity.force})
|
|
self:attack_area(entity.position, 64)
|
|
return
|
|
end
|
|
|
|
if can_follow[entity.type] then
|
|
self:remark{"follow-command", entity.localised_name}
|
|
self:follow(entity)
|
|
else
|
|
self:remark{"move-command", entity.localised_name}
|
|
self:move_to(entity)
|
|
end
|
|
|
|
end
|
|
|
|
function Character:is_moving()
|
|
return self.state_stack[1] and self.state_stack[1].type == States.moving.type
|
|
end
|
|
|
|
function Character:craft_item(name, count)
|
|
self:print("Told to find item "..name.." x"..count)
|
|
self:push_state(States.craft_item(self, name, count))
|
|
end
|
|
|
|
function Character:get_mining_progress()
|
|
return self.entity.character_mining_progress
|
|
end
|
|
|
|
function Character:find_item(name, count)
|
|
self:push_state(States.find_item(self, name, count))
|
|
end
|
|
|
|
function Character:has_item(name, count)
|
|
return self.entity.get_item_count(name) >= ((count or 1))
|
|
end
|
|
|
|
function Character:get_item_count(name)
|
|
return self.entity.get_item_count(name)
|
|
end
|
|
|
|
function Character:find_entities_filtered(param)
|
|
return self.entity.surface.find_entities_filtered(param)
|
|
end
|
|
|
|
function Character:find_entity(param)
|
|
param.limit = 1
|
|
local entities = self.entity.surface.find_entities_filtered(param)
|
|
return entities[1]
|
|
end
|
|
|
|
function Character:is_entity_nearby(radius, param)
|
|
--Slightly optimised, will return true if there is any of the entity matching the params nearby.
|
|
local param = param or {}
|
|
param.position = self:get_position()
|
|
param.radius = radius or 64
|
|
param.limit = 1
|
|
return self.entity.surface.count_entities_filtered(param) > 0
|
|
end
|
|
|
|
function Character:get_forces(type)
|
|
if type == "any" then return nil end
|
|
if type == "enemy" then return self:get_enemy_forces() end
|
|
if type == "friend" then return self:get_friend_forces() end
|
|
if type == "neutral" then return self:get_neutral_forces() end
|
|
end
|
|
|
|
function Character:get_enemy_forces()
|
|
local forces = {}
|
|
local our_force = self.entity.force
|
|
for k, force in pairs (game.forces) do
|
|
if our_force.get_cease_fire(force) == false then
|
|
forces[k] = force.name
|
|
end
|
|
end
|
|
return forces
|
|
end
|
|
|
|
function Character:get_friend_forces()
|
|
local forces = {}
|
|
local our_force = self.entity.force
|
|
for k, force in pairs (game.forces) do
|
|
if our_force.get_friend(force) then
|
|
forces[k] = force.name
|
|
end
|
|
end
|
|
return forces
|
|
end
|
|
|
|
function Character:get_neutral_forces()
|
|
local forces =
|
|
{
|
|
[game.forces.neutral.index] = "neutral"
|
|
}
|
|
local our_force = self.entity.force
|
|
for k, force in pairs (game.forces) do
|
|
if our_force.get_cease_fire(force) and not our_force.get_friend(force) then
|
|
forces[k] = force.name
|
|
end
|
|
end
|
|
return forces
|
|
end
|
|
|
|
function Character:remove_item(name, count)
|
|
--returns how many were removed
|
|
|
|
local to_remove = count
|
|
for k = 1, 10 do
|
|
local inventory = self.entity.get_inventory(k)
|
|
if not inventory then break end
|
|
local stack = inventory.find_item_stack(name)
|
|
|
|
if stack then
|
|
local stack_count = stack.count
|
|
if stack_count >= to_remove then
|
|
stack.count = stack_count - to_remove
|
|
return count
|
|
end
|
|
to_remove = to_remove - stack_count
|
|
stack.clear()
|
|
|
|
if to_remove < 1 then
|
|
return count
|
|
end
|
|
|
|
end
|
|
end
|
|
|
|
if self.entity.player then
|
|
local stack = self.entity.player.cursor_stack
|
|
|
|
if stack then
|
|
local stack_count = stack.count
|
|
if stack_count >= to_remove then
|
|
stack.count = stack_count - to_remove
|
|
return count
|
|
end
|
|
to_remove = to_remove - stack_count
|
|
stack.clear()
|
|
|
|
if to_remove < 1 then
|
|
return count
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
return count - to_remove
|
|
end
|
|
|
|
function Character:can_craft_recipe(recipe)
|
|
if not recipe.enabled then return end
|
|
|
|
if not self.entity.prototype.crafting_categories[recipe.category] then return end
|
|
|
|
for k, ingredient in pairs (recipe.ingredients) do
|
|
if ingredient.type == "fluid" then return end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
function Character:find_nearby_entities(radius, param)
|
|
if not radius then error("Give me a radius dude") end
|
|
local param = param or {}
|
|
param.position = self:get_position()
|
|
param.radius = radius
|
|
return self.entity.surface.find_entities_filtered(param)
|
|
end
|
|
|
|
function Character:update_every_n_ticks(n)
|
|
return (game.tick + self.index) % n == 0
|
|
end
|
|
|
|
function Character:clear_cursor()
|
|
-- Returns true if the cursor is empty.
|
|
local stack = self.entity.cursor_stack
|
|
|
|
if not stack.valid_for_read then
|
|
return true
|
|
end
|
|
|
|
local inventory = self.entity.get_main_inventory()
|
|
|
|
local count = inventory.insert(stack)
|
|
|
|
if count == 0 then
|
|
return false
|
|
end
|
|
|
|
if count == stack.count then
|
|
stack.clear()
|
|
return true
|
|
end
|
|
|
|
stack.count = stack.count - count
|
|
return false
|
|
|
|
end
|
|
|
|
local locale_cache = {}
|
|
local get_localised_item_name = function(name)
|
|
local localised_name = locale_cache[name]
|
|
if localised_name then
|
|
return localised_name
|
|
end
|
|
localised_name = game.item_prototypes[name].localised_name
|
|
locale_cache[name] = localised_name
|
|
return localised_name
|
|
end
|
|
|
|
function Character:take_item_stack(stack, count, source)
|
|
|
|
--game.print(stack.name..": "..stack.count.."; wanted: "..count)
|
|
|
|
local inventory = self.entity.get_main_inventory()
|
|
local original = stack.count
|
|
local name = stack.name
|
|
if count > original then count = original end
|
|
stack.count = count
|
|
local given = inventory.insert(stack)
|
|
if given > original then
|
|
error("WTF"..serpent.block({stack = stack.name, wanted_count = count, given = given, stack_count = stack.count, original = original}))
|
|
end
|
|
stack.count = original - given
|
|
--self:say("Took "..given)
|
|
if source and source.valid then
|
|
source.surface.create_entity
|
|
{
|
|
name = "flying-text",
|
|
text = {"", "+", given, " ", get_localised_item_name(name), " (", inventory.get_item_count(name), ")"},
|
|
position = {source.position.x, source.position.y - 1}
|
|
}
|
|
if self.entity.player then
|
|
self.entity.player.play_sound{path = "utility/inventory_move"}
|
|
end
|
|
end
|
|
|
|
return given
|
|
end
|
|
|
|
function Character:give_item(name, count, target)
|
|
|
|
local given = target.insert{name = name, count = count}
|
|
self:remove_item(name, given)
|
|
|
|
target.surface.create_entity
|
|
{
|
|
name = "flying-text",
|
|
text = {"", "+", given, " ", get_localised_item_name(name), " (", self.entity.get_main_inventory().get_item_count(name), ")"},
|
|
position = {target.position.x, target.position.y - 1}
|
|
}
|
|
|
|
if self.entity.player then
|
|
self.entity.player.play_sound{path = "utility/inventory_move"}
|
|
end
|
|
|
|
return given
|
|
end
|
|
|
|
function Character:take_item(target, name, count)
|
|
self:push_state(States.take_item(self, target, name, count))
|
|
end
|
|
|
|
function Character:take_item_from_belt(target, name, count)
|
|
self:push_state(States.take_item_from_belt(self, target, name, count))
|
|
end
|
|
|
|
function Character:put_item(target, name, count)
|
|
self:push_state(States.put_item(self, target, name, count))
|
|
end
|
|
|
|
function Character:fuel_entity(target)
|
|
self:push_state(States.fuel_entity(self, target))
|
|
end
|
|
|
|
function Character:fuel_entities(entities)
|
|
self:push_state(States.fuel_entities(self, entities))
|
|
end
|
|
|
|
local debug = false
|
|
function Character:print(text)
|
|
if not debug then return end
|
|
game.print(game.tick .. text)
|
|
self:say(text)
|
|
end
|
|
|
|
function Character:say(text)
|
|
self.entity.surface.create_entity{name = "tutorial-flying-text", text = text, position = {self.entity.position.x, self.entity.position.y - 1}}
|
|
end
|
|
|
|
function Character:show_speech_bubble(text)
|
|
if self.speech_bubble and self.speech_bubble.valid then
|
|
self.speech_bubble.start_fading_out()
|
|
self.speech_bubble = nil
|
|
end
|
|
if not text then return end
|
|
self.speech_bubble = self.entity.surface.create_entity{name = "compi-speech-bubble", text = text, position = {self.entity.position.x, self.entity.position.y - 1}, target = self.entity}
|
|
end
|
|
|
|
function Character:on_script_path_request_finished(event)
|
|
local state = self.state_stack[1]
|
|
if state and state.on_script_path_request_finished then
|
|
state:on_script_path_request_finished(event)
|
|
end
|
|
end
|
|
|
|
function Character:get_position()
|
|
return self.entity.position
|
|
end
|
|
|
|
function Character:get_speed()
|
|
return self.entity.character_running_speed
|
|
end
|
|
|
|
function Character:get_closest(entities)
|
|
return self.entity.surface.get_closest(self.entity.position, entities)
|
|
end
|
|
|
|
local direction_size = 2 * defines.direction.south
|
|
function Character:get_direction_to(entity_or_position)
|
|
|
|
local position = entity_or_position.position or entity_or_position
|
|
-- Angle in rads
|
|
local angle = util.angle(position, self:get_position())
|
|
|
|
-- Convert to orientation
|
|
local orientation = (angle / (2 * math.pi)) - 0.25
|
|
if orientation < 0 then orientation = orientation + 1 end
|
|
|
|
--round to nearest 'direction'
|
|
local direction = math.floor((orientation * direction_size) + 0.5)
|
|
if direction == direction_size then direction = 0 end
|
|
|
|
return direction
|
|
end
|
|
|
|
function Character:look_at(entity_or_position)
|
|
local direction = self:get_direction_to(entity_or_position)
|
|
self.entity.direction = direction
|
|
end
|
|
|
|
function Character:on_destroyed()
|
|
--??
|
|
end
|
|
|
|
function Character:load()
|
|
for k, state in pairs (self.state_stack) do
|
|
local state_type = States[state.type]
|
|
assert(state_type, "WTF IS THIS STATE? "..state.type)
|
|
setmetatable(state, state_type.metatable)
|
|
end
|
|
end
|
|
|
|
local on_tick = function(event)
|
|
for k, character in pairs (script_data.characters) do
|
|
character:update()
|
|
end
|
|
end
|
|
|
|
local on_script_path_request_finished = function(event)
|
|
for k, character in pairs (script_data.characters) do
|
|
character:on_script_path_request_finished(event)
|
|
end
|
|
end
|
|
|
|
local on_entity_destroyed = function(event)
|
|
local unit_number = event.unit_number
|
|
if not unit_number then return end
|
|
local character = script_data.characters[unit_number]
|
|
if character then
|
|
character:on_destroyed()
|
|
script_data.characters[unit_number] = nil
|
|
end
|
|
end
|
|
|
|
local lib = {}
|
|
|
|
lib.events =
|
|
{
|
|
[defines.events.on_tick] = on_tick,
|
|
[defines.events.on_script_path_request_finished] = on_script_path_request_finished,
|
|
[defines.events.on_entity_destroyed] = on_entity_destroyed,
|
|
}
|
|
|
|
lib.on_load = function()
|
|
script_data = global.character_interface or script_data
|
|
|
|
for k, character in pairs (script_data.characters) do
|
|
setmetatable(character, Character.metatable)
|
|
character:load()
|
|
end
|
|
|
|
end
|
|
|
|
lib.on_init = function()
|
|
global.character_interface = global.character_interface or script_data
|
|
|
|
game.forces.player.set_cease_fire("neutral", true)
|
|
game.forces.player.set_cease_fire("enemy", false)
|
|
game.forces.player.set_cease_fire("player", true)
|
|
game.forces.player.set_friend("player", true)
|
|
|
|
game.forces.enemy.set_cease_fire("neutral", true)
|
|
game.forces.enemy.set_cease_fire("player", false)
|
|
game.forces.enemy.set_cease_fire("enemy", true)
|
|
|
|
game.forces.neutral.set_cease_fire("neutral", true)
|
|
game.forces.neutral.set_cease_fire("player", true)
|
|
game.forces.neutral.set_cease_fire("enemy", false)
|
|
end
|
|
|
|
lib.new = function(character_entity)
|
|
return Character.new(character_entity)
|
|
end
|
|
|
|
return lib |