minetest-pvp/mods/advanced_npc/executable/instructions/builtin_instructions.lua

731 lines
28 KiB
Lua

--
-- Created by IntelliJ IDEA.
-- Date: 3/8/18
-- Time: 2:16 PM
--
---------------------------------------------------------------------------------------
-- Default advanced_npc instructions
---------------------------------------------------------------------------------------
-- Provides a rich set of default instructions to perform most common actions
-- a NPC needs to, like walking, rotating, standing, sitting, inventory
-- interaction, etc.
npc.programs.instr.default = {
SET_INTERVAL = "advanced_npc:set_instruction_interval",
WAIT = "advanced_npc:wait",
SET_PROCESS_INTERVAL = "advanced_npc:set_process_interval",
FREEZE = "advanced_npc:freeze",
INTERRUPT = "advanced_npc:interrupt",
DIG = "advanced_npc:dig",
PLACE = "advanced_npc:place",
ROTATE = "advanced_npc:rotate",
WALK_STEP = "advanced_npc:walk_step",
STAND = "advanced_npc:stand",
SIT = "advanced_npc:sit",
LAY = "advanced_npc:lay",
PUT_ITEM = "advanced_npc:external_inventory_put",
TAKE_ITEM = "advanced_npc:external_inventory_take",
CHECK_ITEM = "advanced_npc:external_inventory_check",
USE_OPENABLE = "advanced_npc:use_openable_node"
}
-- Control instructions --
-- The following instruction alters the instruction timer interval, therefore
-- making waits and pauses possible, or increase timing when some commands want to
-- be performed faster, like walking.
npc.programs.instr.register("advanced_npc:set_instruction_interval", function(self, args)
local new_interval = args.interval
local freeze_mobs_api = args.freeze
self.execution.process_queue[1].execution_context.instr_interval = new_interval
return not freeze_mobs_api
end)
npc.programs.instr.register("advanced_npc:set_process_interval", function(self, args)
local new_interval = args.interval
-- Update interval
self.execution.scheduler_interval = new_interval
end)
-- Syntacic sugar to make a process wait for a specific interval
npc.programs.instr.register("advanced_npc:wait", function(self, args)
local wait_time = args.time
-- npc.programs.instr.execute(self, "advanced_npc:set_process_interval", {interval = wait_time - 1})
-- npc.exec.proc.enqueue(self, "advanced_npc:set_process_interval", {interval = 1})
npc.programs.instr.execute(self, "advanced_npc:set_instruction_interval", {interval = wait_time - 1})
npc.exec.proc.enqueue(self, "advanced_npc:set_instruction_interval", {interval = 1})
end)
-- The following command is for allowing the rest of mobs redo API to be executed
-- after this command ends. This is useful for times when no command is needed
-- and the NPC is allowed to roam freely.
npc.programs.instr.register("advanced_npc:freeze", function(self, args)
local freeze_mobs_api = args.freeze
local disable_rightclick = args.disable_rightclick
if disable_rightclick ~= nil then
npc.log("INFO", "Enabling right-click interrupts for NPC "..self.npc_name..": "..dump(not(disable_rightclick)))
self.enable_rightclick_interaction = not(disable_rightclick)
end
return not(freeze_mobs_api)
end)
-- This instruction allow interrupts to be enqueable, in case some programs
-- needs to be run in the future.
npc.programs.instr.register("advanced_npc:interrupt", function(self, args)
local new_program = args.new_program
local new_args = args.new_args
local interrupt_options = args.interrupt_options
npc.exec.interrupt(self, new_program, new_args, interrupt_options)
end)
npc.programs.instr.register("advanced_npc:set_interrupt_options", function(self, args)
local allow_punch = args.allow_punch
local allow_rightclick = args.allow_rightclick
local allow_schedule = args.allow_schedule
-- Set defaults
if allow_punch == nil then allow_punch = true end
if allow_rightclick == nil then allow_rightclick = true end
if allow_schedule == nil then allow_schedule = true end
-- Set interrupt options
self.execution.process_queue[1].interrupt_options = {
allow_punch = allow_punch,
allow_rightclick = allow_rightclick,
allow_schedule = allow_schedule
}
npc.log("INFO", "New process: "..dump(self.execution.process_queue[1]))
end)
-- This instructions sets the object animation
npc.programs.instr.register("advanced_npc:set_animation", function(self, args)
self.object:set_animation(
{
x = args.start_frame,
y = args.end_frame
},
args.frame_speed,
args.frame_blend or 0,
args.frame_loop or true)
end)
-- Interaction instructions --
-- This command digs the node at the given position
-- If 'add_to_inventory' is true, it will put the digged node in the NPC
-- inventory.
-- Returns true if dig is successful, otherwise false
npc.programs.instr.register("advanced_npc:dig", function(self, args)
local pos = args.pos
local add_to_inventory = args.add_to_inventory
local bypass_protection = args.bypass_protection
local play_sound = args.play_sound or true
local node = minetest.get_node_or_nil(pos)
if node then
-- Set mine animation
self.object:set_animation({
x = npc.ANIMATION_MINE_START,
y = npc.ANIMATION_MINE_END},
self.animation.speed_normal, 0)
-- Play dig sound
if play_sound == true then
if minetest.registered_nodes[node.name].sounds then
minetest.sound_play(
minetest.registered_nodes[node.name].sounds.dug,
{
max_hear_distance = 10,
object = self.object
}
)
end
end
-- Check if protection not enforced
if not bypass_protection then
-- Try to dig node
if minetest.dig_node(pos) then
-- Add to inventory the node drops
if add_to_inventory then
-- Get node drop
local drop = minetest.registered_nodes[node.name].drop
local drop_itemname = node.name
if drop and drop.items then
local random_item = drop.items[math.random(1, #drop.items)]
if random_item then
drop_itemname = random_item.items[1]
end
end
-- Add to NPC inventory
npc.add_item_to_inventory(self, drop_itemname, 1)
end
--return true
return
end
else
-- Add to inventory
if add_to_inventory then
-- Get node drop
local drop = minetest.registered_nodes[node.name].drop
local drop_itemname = node.name
if drop and drop.items then
local random_item = drop.items[math.random(1, #drop.items)]
if random_item then
drop_itemname = random_item.items[1]
end
end
-- Add to NPC inventory
npc.add_item_to_inventory(self, drop_itemname, 1)
end
-- Dig node
minetest.log("Setting air at pos: "..minetest.pos_to_string(pos))
minetest.set_node(pos, {name="air"})
end
end
--return false
end)
-- This command places a given node at the given position
-- There are three ways to source the node:
-- 1. take_from_inventory: takes node from inventory. If not in inventory,
-- node isn't placed.
-- 2. take_from_inventory_forced: takes node from inventory. If not in
-- inventory, node will be placed anyways.
-- 3. force_place: places node regardless of inventory - will not touch
-- the NPCs inventory
npc.programs.instr.register("advanced_npc:place", function(self, args)
local pos = args.pos
local node = args.node
local source = args.source
local bypass_protection = args.bypass_protection
local play_sound = args.play_sound or true
local node_at_pos = minetest.get_node_or_nil(pos)
-- Check if position is empty or has a node that can be built to
if node_at_pos and
(node_at_pos.name == "air" or minetest.registered_nodes[node_at_pos.name].buildable_to == true) then
-- Check protection
if (not bypass_protection and not minetest.is_protected(pos, self.npc_name))
or bypass_protection == true then
-- Take from inventory if necessary
local place_item = false
if source == npc.programs.const.place_src.take_from_inventory then
if npc.take_item_from_inventory(self, node, 1) then
place_item = true
end
elseif source == npc.programs.const.place_src.take_from_inventory_forced then
npc.take_item_from_inventory(self, node, 1)
place_item = true
elseif source == npc.programs.const.place_src.force_place then
place_item = true
end
-- Place node
if place_item == true then
-- Set mine animation
self.object:set_animation({
x = npc.ANIMATION_MINE_START,
y = npc.ANIMATION_MINE_END},
self.animation.speed_normal, 0)
-- Place node
minetest.set_node(pos, {name=node})
-- Play place sound
if play_sound == true then
if minetest.registered_nodes[node].sounds then
minetest.sound_play(
minetest.registered_nodes[node].sounds.place,
{
max_hear_distance = 10,
object = self.object
}
)
end
end
end
end
end
end)
-- The following instruction simulates what a player does when it punches something.
-- In this case, we have two possibilities:
-- - Punch an object (entity or player),
-- - Punch a node
-- If directed against a node, and the node has no special on_punch() callback,
-- the `advanced_npc:dig` instruction will be executed
-- Arguments:
-- - `pointed_thing`, for consistency, this is as explained in the `lua_api.txt`,
-- but without the `{type="nothing"} support. It supports the other two definitions.
-- - `wield_item`, which is an itemstring, that represents the item the NPC
-- is wielding at the time of punching.
npc.programs.instr.register("advanced_npc:punch", function(self, args)
local pointed_thing = self.pointed_thing
local target_type = pointed_thing.type
local target_pos = pointed_thing.above
local target_obj = pointed_thing.ref
local wielded_item = args.wielded_item
local time_from_last_punch = minetest.get_gametime() - self.npc_state.punch.last_punch_time
-- Set time from last punch
self.npc_state.punch.last_punch_time = minetest.get_gametime()
-- If given, enable wielded item
if wielded_item then
self:set_wielded_item(wielded_item)
end
if target_type == "object" and target_obj then
-- Call obj's punch()
target_obj:punch(self, time_from_last_punch, self.object:getyaw())
elseif target_type == "node" and target_pos then
local node = minetest.get_node(target_pos)
local node_def = minetest.registered_nodes[node.name]
if node_def and node_def.on_punch then
-- Call the node's on_punch
node_def.on_punch(target_pos, node, self, {type="node", above=target_pos, below=target_pos})
else
-- Execute the dig instrcution
npc.programs.instr.execute(self, npc.programs.instr.default.DIG, {
pos = target_pos,
bypass_protection = false,
add_to_inventory = true
})
end
end
end)
-- The following instruction simulates what a player does when it rightclicks something.
-- In this case, we have two possibilities:
-- - Right-click an object (entity or player),
-- - Right-click a node
-- Arguments:
-- - `pointed_thing`, for consistency, this is as explained in the `lua_api.txt`,
-- but without the `{type="nothing"} support. It supports the other two definitions.
-- - `wield_item`, which is an itemstring, that represents the item the NPC
-- is wielding at the time of punching.
npc.programs.instr.register("advanced_npc:rightclick", function(self, args)
local pointed_thing = self.pointed_thing
local target_type = pointed_thing.type
local target_pos = pointed_thing.above
local target_obj = pointed_thing.ref
local wielded_item = args.wielded_item
-- If given, enable wielded item
if wielded_item then
self:set_wielded_item(wielded_item)
end
if target_type == "object" and target_obj then
-- Call obj's right-click()
target_obj:right_click(self)
elseif target_type == "node" and target_pos then
local node = minetest.get_node(target_pos)
local node_def = minetest.registered_nodes[node.name]
if node_def and node_def.on_rightclick then
-- Call the node's on_punch
node_def.on_rightclick(target_pos, node, self, self.object:get_wielded_item(), pointed_thing)
end
end
end)
-- This instruction allows the NPC to craft a certain item if it has
-- the required items on its inventory. If the "force_craft" option is
-- used, the NPC will get the item regardless if it has the required items
-- or not
npc.programs.instr.register("advanced_npc:craft", function(self, args)
local item = args.item
local source = args.source
-- Check if source is force-craft, if it is, just add the item to inventory
if source == npc.programs.const.craft_src.force_craft then
-- Add item to inventory
npc.add_item_to_inventory_itemstring(self, item)
return
end
local recipes = minetest.get_all_craftt_recipes(item)
-- Iterate through recipes, only care about those that are "normal",
-- we don't care about cooking or fuel recipes.
-- Check if required items are present
if recipes then
for i = 1, #recipes do
if recipe.method == "normal" then
local missing_items = {}
if recipe.items then
-- Check how many items we have and which we don't
for i = 1, #recipe.items do
if npc.inventory_contains(self, recipe.items[i]) == nil then
missing_items[#missing_items + 1] = recipe.items[i]
end
end
-- Now, check the source for items
local craftable = false
if source == npc.programs.const.craft_src.take_from_inventory then
-- Check if we have all
if next(missing_items) == nil then
craftable = true
end
elseif source == npc.programs.const.craft_src.take_from_inventory_forced then
-- Check if we have missing items
if next(missing_items) ~= nil then
-- Add all missing items
for j = 1, #missing_items do
npc.add_item_to_inventory(self, missing_items[j], 1)
end
craftable = true
end
end
-- Check if item is craftable
if craftable == true then
-- We have all items, craft
-- First, remove all items from NPC inventory
for j = 1, #recipe.items do
npc.take_item_from_inventory(self, recipe.items[j], 1)
end
-- Then add "crafted" element
npc.add_item_to_inventory_itemstring(self, item)
return true
end
return false
end
end
end
else
npc.log("WARNING", "[instr][craft] Found no recipes for item: "..dump(args.item))
end
end)
-- This command is to rotate a mob to a specifc direction. Currently, the code
-- contains also for diagonals, but remaining in the orthogonal domain is preferrable.
npc.programs.instr.register("advanced_npc:rotate", function(self, args)
local dir = args.dir
local yaw = args.yaw or 0
local start_pos = args.start_pos
local end_pos = args.end_pos
-- Calculate dir if positions are given
if start_pos and end_pos and not dir then
dir = npc.programs.helper.get_direction(start_pos, end_pos)
end
-- Only yaw was given
if yaw and not dir and not start_pos and not end_pos then
if (yaw ~= yaw) then yaw = 0 end
--if type(yaw) == "table" then yaw = 0 end
self.object:setyaw(yaw)
return
end
self.rotate = 0
if dir == npc.direction.north then
yaw = 0
elseif dir == npc.direction.north_east then
yaw = (7 * math.pi) / 4
elseif dir == npc.direction.east then
yaw = (3 * math.pi) / 2
elseif dir == npc.direction.south_east then
yaw = (5 * math.pi) / 4
elseif dir == npc.direction.south then
yaw = math.pi
elseif dir == npc.direction.south_west then
yaw = (3 * math.pi) / 4
elseif dir == npc.direction.west then
yaw = math.pi / 2
elseif dir == npc.direction.north_west then
yaw = math.pi / 4
end
if (yaw ~= yaw) then yaw = 0 end
self.object:setyaw(yaw)
end)
-- This function will make the NPC walk one step on a
-- specifc direction. One step means one node. It returns
-- true if it can move on that direction, and false if there is an obstacle
npc.programs.instr.register("advanced_npc:walk_step", function(self, args)
local dir = args.dir
local yaw = args.yaw or 0
local step_into_air_only = args.step_into_air_only
local speed = args.speed
local target_pos = args.target_pos
local start_pos = args.start_pos
local vel = {}
-- Set default node per seconds
if speed == nil then
speed = npc.programs.const.speeds.one_nps_speed
end
-- Only yaw was given, purely rotate and walk in that dir
if yaw and not dir then
vel = vector.multiply(vector.normalize(minetest.yaw_to_dir(yaw)), speed)
else
-- Check if dir should be random
if dir == "random_all" or dir == "random" then
dir = npc.programs.helper.random_dir(start_pos, speed, 0, 7)
end
if dir == "random_orthogonal" then
dir = npc.programs.helper.random_dir(start_pos, speed, 0, 3)
--minetest.log("Returned: "..dump(dir))
end
if dir == npc.direction.north then
vel = {x=0, y=0, z=speed}
elseif dir == npc.direction.north_east then
vel = {x=speed, y=0, z=speed}
elseif dir == npc.direction.east then
vel = {x=speed, y=0, z=0}
elseif dir == npc.direction.south_east then
vel = {x=speed, y=0, z=-speed}
elseif dir == npc.direction.south then
vel = {x=0, y=0, z=-speed}
elseif dir == npc.direction.south_west then
vel = {x=-speed, y=0, z=-speed}
elseif dir == npc.direction.west then
vel = {x=-speed, y=0, z=0}
elseif dir == npc.direction.north_west then
vel = {x=-speed, y=0, z=speed }
else
-- No direction provided or NPC is trapped, center NPC position
-- and return
-- local npc_pos = self.object:getpos()
-- local proper_pos = {x=math.floor(npc_pos.x), y=npc_pos.y, z=math.floor(npc_pos.z)}
-- self.object:moveto(proper_pos)
return
end
end
-- If there is a target position to reach, set it and set walking to true
if target_pos ~= nil then
self.npc_state.movement.walking.target_pos = target_pos
-- Set is_walking = true
npc.set_movement_state(self, {is_walking = true})
end
-- Rotate NPC
npc.programs.instr.execute(self, npc.programs.instr.default.ROTATE, {dir=dir, yaw=yaw})
-- Set velocity so that NPC walks
self.object:setvelocity(vel)
-- Set walk animation
self.object:set_animation({
x = npc.ANIMATION_WALK_START,
y = npc.ANIMATION_WALK_END},
self.animation.speed_normal, 0)
end)
-- This command makes the NPC stand and remain like that
npc.programs.instr.register("advanced_npc:stand", function(self, args)
local pos = args.pos
local dir = args.dir
local yaw = args.yaw
-- Set is_walking = false
npc.set_movement_state(self, {is_idle = true})
-- Stop NPC
self.object:setvelocity({x=0, y=0, z=0})
-- If position given, set to that position
if pos ~= nil then
self.object:moveto(pos)
end
-- If dir given, set to that dir
if dir ~= nil or yaw ~= nil then
npc.programs.instr.execute(self, npc.programs.instr.default.ROTATE, {dir=dir, yaw=yaw})
end
-- Set stand animation
self.object:set_animation({
x = npc.ANIMATION_STAND_START,
y = npc.ANIMATION_STAND_END},
self.animation.speed_normal, 0)
end)
-- This command makes the NPC sit on the node where it is
npc.programs.instr.register("advanced_npc:sit", function(self, args)
local pos = args.pos
local dir = args.dir
-- Set movement state
npc.set_movement_state(self, {is_idle = true, is_sitting = true})
-- Stop NPC
self.object:setvelocity({x=0, y=0, z=0})
-- If position given, set to that position
if pos ~= nil then
self.object:moveto(pos)
end
-- If dir given, set to that dir
if dir ~= nil then
npc.programs.instr.execute(self, npc.programs.instr.default.ROTATE, {dir=dir})
end
-- Set sit animation
self.object:set_animation({
x = npc.ANIMATION_SIT_START,
y = npc.ANIMATION_SIT_END},
self.animation.speed_normal, 0)
end)
-- This command makes the NPC lay on the node where it is
npc.programs.instr.register("advanced_npc:lay", function(self, args)
local pos = args.pos
-- Set movement state
npc.set_movement_state(self, {is_idle = true, is_laying = true})
-- Stop NPC
self.object:setvelocity({x=0, y=0, z=0})
-- If position give, set to that position
if pos ~= nil then
self.object:moveto(pos)
end
-- Set sit animation
self.object:set_animation({
x = npc.ANIMATION_LAY_START,
y = npc.ANIMATION_LAY_END},
self.animation.speed_normal, 0)
end)
-- Inventory interaction instructions --
-- This function is a convenience function to make it easy to put
-- and get items from another inventory (be it a player inv or
-- a node inv)
npc.programs.instr.register("advanced_npc:external_inventory_put", function(self, args)
local player = args.player
local pos = args.pos
local inv_list = args.inv_list
local item_name = args.item_name
local count = args.count
local is_furnace = args.is_furnace
local inv
if player ~= nil then
inv = minetest.get_inventory({type="player", name=player})
else
inv = minetest.get_inventory({type="node", pos=pos})
end
-- Create ItemStack to put on external inventory
local item = ItemStack(item_name.." "..count)
-- Check if there is enough room to add the item on external invenotry
if inv:room_for_item(inv_list, item) then
-- Take item from NPC's inventory
if npc.take_item_from_inventory_itemstring(self, item) then
-- NPC doesn't have item and/or specified quantity
return false
end
-- Add items to external inventory
inv:add_item(inv_list, item)
-- If this is a furnace, start furnace timer
if is_furnace == true then
minetest.get_node_timer(pos):start(1.0)
end
return true
end
-- Not able to put on external inventory
return false
end)
npc.programs.instr.register("advanced_npc:external_inventory_take", function(self, args)
local player = args.player
local pos = args.pos
local inv_list = args.inv_list
local item_name = args.item_name
local count = args.count
local inv
if player ~= nil then
inv = minetest.get_inventory({type="player", name=player})
else
inv = minetest.get_inventory({type="node", pos=pos})
end
-- Create ItemStack to take from external inventory
local item = ItemStack(item_name.." "..count)
-- Check if there is enough of the item to take
if inv:contains_item(inv_list, item) then
-- Add item to NPC's inventory
npc.add_item_to_inventory_itemstring(self, item)
-- Add items to external inventory
inv:remove_item(inv_list, item)
return true
end
-- Not able to put on external inventory
return false
end)
npc.programs.instr.register("advanced_npc:external_inventory_contains", function(self, args)
local player = args.player
local pos = args.pos
local inv_list = args.inv_list
local item_name = args.item_name
local count = args.count
local inv
if player ~= nil then
inv = minetest.get_inventory({type="player", name=player})
else
inv = minetest.get_inventory({type="node", pos=pos})
end
-- Create ItemStack for checking the external inventory
local item = ItemStack(item_name.." "..count)
-- Check if inventory contains item
return inv:contains_item(inv_list, item)
end)
-- This function is used to open or close openable nodes.
-- Currently supported openable nodes are: any doors using the
-- default doors API, and the cottages mod gates and doors.
npc.programs.instr.register("advanced_npc:use_openable_node", function(self, args)
local pos = args.pos
local command = args.command
local dir = args.dir
local node = minetest.get_node(pos)
local state = npc.programs.helper.get_openable_node_state(node, pos, dir)
-- Emulate the NPC being a player
local clicker = self
clicker.is_player = function() return true end
clicker.get_player_name = function(self) return self.npc_id end
if command ~= state then
minetest.registered_nodes[node.name].on_rightclick(pos, node, clicker, nil, nil)
end
end)
-- Internal NPC properties
-- These instructions are mostly syntactic sugar for doing certain operations.
npc.programs.instr.register("advanced_npc:trade:change_trader_status", function(self, args)
-- Get status from args
local status = args.status
-- Set status to NPC
npc.set_trading_status(self, status)
end)
npc.programs.instr.register("advanced_npc:trade:set_trade_list", function(self, args)
-- Insert items
for i = 1, #args.items do
-- Insert entry into trade list
self.trader_data.trade_list[args.items[i].name] = {
max_item_buy_count = args.items[i].buy,
max_item_sell_count = args.items[i].sell,
amount_to_keep = args.items[i].keep
}
end
end)
-- Accepts itemstring
npc.programs.instr.register("advanced_npc:inventory_put", function(self, args)
local itemstring = args.itemstring
-- Add item
npc.add_item_to_inventory_itemstring(self, itemstring)
end)
npc.programs.instr.register("advanced_npc:inventory_put_multiple", function(self, args)
local itemlist = args.itemlist
for i = 1, #itemlist do
local itemlist_entry = itemlist[i]
local current_itemstring = itemlist[i].name
if itemlist_entry.random == true then
current_itemstring = current_itemstring
.." "..dump(math.random(itemlist_entry.min, itemlist_entry.max))
else
current_itemstring = current_itemstring.." "..tostring(itemlist_entry.count)
end
-- Add item to inventory
npc.add_item_to_inventory_itemstring(self, current_itemstring)
end
end)
npc.programs.instr.register("advanced_npc:inventory_take", function(self, args)
local itemstring = args.itemstring
-- Add item
npc.take_item_from_inventory_itemstring(self, itemstring)
end)