2016-12-12 02:52:57 +01:00
-- Advanced NPC by Zorman2000
2017-08-25 15:31:45 +02:00
-- Based on original NPC by Tenplus1
2016-11-12 13:06:09 +01:00
local S = mobs.intllib
2016-11-14 12:28:37 +01:00
npc = { }
-- Constants
npc.FEMALE = " female "
npc.MALE = " male "
2016-12-04 21:00:55 +01:00
2017-06-17 15:08:29 +02:00
npc.age = {
adult = " adult " ,
child = " child "
}
2016-12-06 02:22:33 +01:00
npc.INVENTORY_ITEM_MAX_STACK = 99
2017-01-09 17:34:38 +01:00
npc.ANIMATION_STAND_START = 0
npc.ANIMATION_STAND_END = 79
2016-12-18 18:32:39 +01:00
npc.ANIMATION_SIT_START = 81
npc.ANIMATION_SIT_END = 160
npc.ANIMATION_LAY_START = 162
npc.ANIMATION_LAY_END = 166
2017-01-09 17:34:38 +01:00
npc.ANIMATION_WALK_START = 168
npc.ANIMATION_WALK_END = 187
2017-08-11 18:03:45 +02:00
npc.ANIMATION_MINE_START = 189
npc.ANIMATION_MINE_END = 198
2016-12-18 18:32:39 +01:00
npc.direction = {
2016-12-30 03:34:15 +01:00
north = 0 ,
east = 1 ,
south = 2 ,
2017-06-15 00:42:36 +02:00
west = 3 ,
north_east = 4 ,
north_west = 5 ,
south_east = 6 ,
south_west = 7
2016-12-18 18:32:39 +01:00
}
2017-01-19 01:34:02 +01:00
npc.action_state = {
none = 0 ,
executing = 1 ,
interrupted = 2
}
2017-06-17 15:44:25 +02:00
npc.log_level = {
INFO = true ,
2017-06-20 02:54:26 +02:00
WARNING = true ,
2017-06-17 15:44:25 +02:00
ERROR = true ,
DEBUG = false
}
2017-06-17 16:22:00 +02:00
npc.texture_check = {
timer = 0 ,
2017-06-22 03:07:36 +02:00
interval = 2
2017-06-17 16:22:00 +02:00
}
2016-12-12 02:52:57 +01:00
---------------------------------------------------------------------------------------
2016-11-14 12:28:37 +01:00
-- General functions
2016-12-12 02:52:57 +01:00
---------------------------------------------------------------------------------------
2017-06-17 15:44:25 +02:00
-- Logging
function npc . log ( level , message )
if npc.log_level [ level ] then
2017-07-14 01:01:28 +02:00
minetest.log ( " [advanced_npc] " .. level .. " : " .. message )
2017-06-17 15:44:25 +02:00
end
end
-- NPC chat
function npc . chat ( npc_name , player_name , message )
minetest.chat_send_player ( player_name , npc_name .. " : " .. message )
2017-06-17 16:22:00 +02:00
end
2017-06-17 15:44:25 +02:00
2016-11-14 12:28:37 +01:00
-- Gets name of player or NPC
2016-11-29 20:05:09 +01:00
function npc . get_entity_name ( entity )
2016-11-14 12:28:37 +01:00
if entity : is_player ( ) then
2017-07-14 01:01:28 +02:00
return entity : get_player_name ( )
2016-11-14 12:28:37 +01:00
else
2017-07-14 01:01:28 +02:00
return entity : get_luaentity ( ) . name
2016-11-14 12:28:37 +01:00
end
2016-11-12 13:06:09 +01:00
end
2016-11-14 12:28:37 +01:00
-- Returns the item "wielded" by player or NPC
-- TODO: Implement NPC
2016-12-12 02:52:57 +01:00
function npc . get_entity_wielded_item ( entity )
2016-11-14 12:28:37 +01:00
if entity : is_player ( ) then
2017-07-14 01:01:28 +02:00
return entity : get_wielded_item ( )
2016-11-14 12:28:37 +01:00
end
2016-11-12 13:06:09 +01:00
end
2017-03-27 17:08:18 +02:00
---------------------------------------------------------------------------------------
-- Spawning functions
---------------------------------------------------------------------------------------
-- These functions are used at spawn time to determine several
-- random attributes for the NPC in case they are not already
-- defined. On a later phase, pre-defining many of the NPC values
-- will be allowed.
2017-03-27 20:47:31 +02:00
local function get_random_name ( sex )
local i = math.random ( # npc.data . FIRST_NAMES [ sex ] )
return npc.data . FIRST_NAMES [ sex ] [ i ]
end
2017-03-27 17:08:18 +02:00
local function initialize_inventory ( )
return {
2017-07-14 01:01:28 +02:00
[ 1 ] = " " , [ 2 ] = " " , [ 3 ] = " " , [ 4 ] = " " ,
[ 5 ] = " " , [ 6 ] = " " , [ 7 ] = " " , [ 8 ] = " " ,
[ 9 ] = " " , [ 10 ] = " " , [ 11 ] = " " , [ 12 ] = " " ,
[ 13 ] = " " , [ 14 ] = " " , [ 15 ] = " " , [ 16 ] = " " ,
2017-03-27 17:08:18 +02:00
}
end
-- This function checks for "female" text on the texture name
local function is_female_texture ( textures )
for i = 1 , # textures do
2017-07-14 01:01:28 +02:00
if string.find ( textures [ i ] , " female " ) ~= nil then
return true
end
2017-03-27 17:08:18 +02:00
end
return false
end
2017-06-17 15:08:29 +02:00
local function get_random_texture ( sex , age )
local textures = { }
local filtered_textures = { }
2017-07-14 01:01:28 +02:00
-- Find textures by sex and age
2017-06-17 15:08:29 +02:00
if age == npc.age . adult then
2017-07-14 01:01:28 +02:00
--minetest.log("Registered: "..dump(minetest.registered_entities["advanced_npc:npc"]))
textures = minetest.registered_entities [ " advanced_npc:npc " ] . texture_list
2017-06-17 15:08:29 +02:00
elseif age == npc.age . child then
2017-07-14 01:01:28 +02:00
textures = minetest.registered_entities [ " advanced_npc:npc " ] . child_texture
2017-06-17 15:08:29 +02:00
end
for i = 1 , # textures do
2017-07-14 01:01:28 +02:00
local current_texture = textures [ i ] [ 1 ]
2017-08-25 15:31:45 +02:00
if ( sex == npc.MALE
and string.find ( current_texture , sex )
2017-07-14 01:01:28 +02:00
and not string.find ( current_texture , npc.FEMALE ) )
2017-08-25 15:31:45 +02:00
or ( sex == npc.FEMALE
2017-07-14 01:01:28 +02:00
and string.find ( current_texture , sex ) ) then
table.insert ( filtered_textures , current_texture )
end
2017-06-17 15:08:29 +02:00
end
-- Check if filtered textures is empty
if filtered_textures == { } then
2017-07-14 01:01:28 +02:00
return textures [ 1 ] [ 1 ]
2017-06-17 15:08:29 +02:00
end
return filtered_textures [ math.random ( 1 , # filtered_textures ) ]
end
2017-07-14 01:01:28 +02:00
function npc . get_random_texture_from_array ( age , sex , textures )
local filtered_textures = { }
for i = 1 , # textures do
local current_texture = textures [ i ]
-- Filter by age
2017-08-25 15:31:45 +02:00
if ( sex == npc.MALE
and string.find ( current_texture , sex )
2017-07-14 01:01:28 +02:00
and not string.find ( current_texture , npc.FEMALE )
2017-08-25 15:31:45 +02:00
and ( ( age == npc.age . adult
2017-07-14 01:01:28 +02:00
and not string.find ( current_texture , npc.age . child ) )
or ( age == npc.age . child
and string.find ( current_texture , npc.age . child ) )
)
)
2017-08-25 15:31:45 +02:00
or ( sex == npc.FEMALE
2017-07-14 01:01:28 +02:00
and string.find ( current_texture , sex )
2017-08-25 15:31:45 +02:00
and ( ( age == npc.age . adult
2017-07-14 01:01:28 +02:00
and not string.find ( current_texture , npc.age . child ) )
or ( age == npc.age . child
and string.find ( current_texture , npc.age . child ) )
)
) then
2017-08-11 18:03:45 +02:00
table.insert ( filtered_textures , current_texture )
2017-07-14 01:01:28 +02:00
end
end
2017-08-11 18:03:45 +02:00
-- Check if there are no textures
if # filtered_textures == 0 then
return nil
end
2017-07-14 01:01:28 +02:00
return filtered_textures [ math.random ( 1 , # filtered_textures ) ]
end
2017-08-25 15:31:45 +02:00
-- Choose whether NPC can have relationships. Only 30% of NPCs
2017-07-14 01:01:28 +02:00
-- cannot have relationships
2017-06-22 03:07:36 +02:00
local function can_have_relationships ( is_child )
2017-06-17 15:08:29 +02:00
-- Children can't have relationships
2017-06-22 03:07:36 +02:00
if is_child then
2017-07-14 01:01:28 +02:00
return false
2017-06-17 15:08:29 +02:00
end
2017-03-27 17:08:18 +02:00
local chance = math.random ( 1 , 10 )
return chance > 3
end
-- Choose a maximum of two items that the NPC will have at spawn time
-- These items are chosen from the favorite items list.
local function choose_spawn_items ( self )
local number_of_items_to_add = math.random ( 1 , 2 )
local number_of_items = # npc.FAVORITE_ITEMS [ self.sex ] . phase1
2017-08-25 15:31:45 +02:00
2017-03-27 17:08:18 +02:00
for i = 1 , number_of_items_to_add do
2017-07-14 01:01:28 +02:00
npc.add_item_to_inventory (
self ,
2017-08-25 15:31:45 +02:00
npc.FAVORITE_ITEMS [ self.sex ] . phase1 [ math.random ( 1 , number_of_items ) ] . item ,
2017-07-14 01:01:28 +02:00
math.random ( 1 , 5 )
)
2017-03-27 17:08:18 +02:00
end
-- Add currency to the items spawned with. Will add 5-10 tier 3
-- currency items
local currency_item_count = math.random ( 5 , 10 )
npc.add_item_to_inventory ( self , npc.trade . prices.currency . tier3.string , currency_item_count )
-- For test
2017-07-14 01:01:28 +02:00
--npc.add_item_to_inventory(self, "default:tree", 10)
--npc.add_item_to_inventory(self, "default:cobble", 10)
--npc.add_item_to_inventory(self, "default:diamond", 2)
--npc.add_item_to_inventory(self, "default:mese_crystal", 2)
--npc.add_item_to_inventory(self, "flowers:rose", 2)
--npc.add_item_to_inventory(self, "advanced_npc:marriage_ring", 2)
--npc.add_item_to_inventory(self, "flowers:geranium", 2)
--npc.add_item_to_inventory(self, "mobs:meat", 2)
--npc.add_item_to_inventory(self, "mobs:leather", 2)
--npc.add_item_to_inventory(self, "default:sword_stone", 2)
--npc.add_item_to_inventory(self, "default:shovel_stone", 2)
--npc.add_item_to_inventory(self, "default:axe_stone", 2)
2017-03-27 17:08:18 +02:00
--minetest.log("Initial inventory: "..dump(self.inventory))
end
-- Spawn function. Initializes all variables that the
-- NPC will have and choose random, starting values
2017-08-11 18:03:45 +02:00
function npc . initialize ( entity , pos , is_lua_entity , npc_stats , occupation_name )
npc.log ( " INFO " , " Initializing NPC at " .. minetest.pos_to_string ( pos ) )
2017-03-27 17:08:18 +02:00
2017-08-11 18:03:45 +02:00
-- Get variables
local ent = entity
if not is_lua_entity then
ent = entity : get_luaentity ( )
end
2017-03-27 17:08:18 +02:00
2017-08-11 18:03:45 +02:00
-- Avoid NPC to be removed by mobs_redo API
ent.remove_ok = false
2017-08-25 15:31:45 +02:00
2017-08-11 18:03:45 +02:00
-- Determine sex and age
-- If there's no previous NPC data, sex and age will be randomly chosen.
-- - Sex: Female or male will have each 50% of spawning
-- - Age: 90% chance of spawning adults, 10% chance of spawning children.
-- If there is previous data then:
-- - Sex: The unbalanced sex will get a 75% chance of spawning
-- - Example: If there's one male, then female will have 75% spawn chance.
-- - If there's male and female, then each have 50% spawn chance.
-- - Age: For each two adults, the chance of spawning a child next will be 50%
-- If there's a child for two adults, the chance of spawning a child goes to
-- 40% and keeps decreasing unless two adults have no child.
2017-08-25 15:31:45 +02:00
-- Use NPC stats if provided
2017-08-11 18:03:45 +02:00
if npc_stats then
-- Default chances
local male_s , male_e = 0 , 50
local female_s , female_e = 51 , 100
local adult_s , adult_e = 0 , 85
local child_s , child_e = 86 , 100
-- Determine sex probabilities
if npc_stats [ npc.FEMALE ] . total > npc_stats [ npc.MALE ] . total then
male_e = 75
female_s , female_e = 76 , 100
elseif npc_stats [ npc.FEMALE ] . total < npc_stats [ npc.MALE ] . total then
male_e = 25
female_s , female_e = 26 , 100
end
-- Determine age probabilities
if npc_stats [ " adult_total " ] >= 2 then
2017-08-25 15:31:45 +02:00
if npc_stats [ " adult_total " ] % 2 == 0
2017-08-11 18:03:45 +02:00
and ( npc_stats [ " adult_total " ] / 2 > npc_stats [ " child_total " ] ) then
child_s , child_e = 26 , 100
adult_e = 25
else
child_s , child_e = 61 , 100
adult_e = 60
end
end
-- Get sex and age based on the probabilities
local sex_chance = math.random ( 1 , 100 )
local age_chance = math.random ( 1 , 100 )
local selected_sex = " "
local selected_age = " "
-- Select sex
if male_s <= sex_chance and sex_chance <= male_e then
selected_sex = npc.MALE
elseif female_s <= sex_chance and sex_chance <= female_e then
selected_sex = npc.FEMALE
end
-- Set sex for NPC
ent.sex = selected_sex
-- Select age
if adult_s <= age_chance and age_chance <= adult_e then
selected_age = npc.age . adult
elseif child_s <= age_chance and age_chance <= child_e then
selected_age = npc.age . child
ent.visual_size = {
x = 0.65 ,
y = 0.65
}
ent.collisionbox = { - 0.10 , - 0.50 , - 0.10 , 0.10 , 0.40 , 0.10 }
ent.is_child = true
-- For mobs_redo
2017-08-25 15:31:45 +02:00
ent.child = true
2017-08-11 18:03:45 +02:00
end
-- Store the selected age
ent.age = selected_age
-- Set texture accordingly
local selected_texture = get_random_texture ( selected_sex , selected_age )
--minetest.log("Selected texture: "..dump(selected_texture))
-- Store selected texture due to the need to restore it later
ent.selected_texture = selected_texture
-- Set texture and base texture
ent.textures = { selected_texture }
ent.base_texture = { selected_texture }
2017-07-14 01:01:28 +02:00
else
2017-08-11 18:03:45 +02:00
-- Get sex based on texture. This is a 50% chance for
-- each sex as there's same amount of textures for male and female.
-- Do not spawn child as first NPC
if ( is_female_texture ( ent.base_texture ) ) then
ent.sex = npc.FEMALE
else
ent.sex = npc.MALE
end
ent.age = npc.age . adult
2017-07-14 01:01:28 +02:00
end
2017-03-27 20:47:31 +02:00
2017-08-11 18:03:45 +02:00
-- Nametag is initialized to blank
ent.nametag = " "
2017-06-15 00:42:36 +02:00
2017-08-11 18:03:45 +02:00
-- Set name
ent.npc_name = get_random_name ( ent.sex )
2017-03-27 20:47:31 +02:00
2017-08-11 18:03:45 +02:00
-- Set ID
ent.npc_id = tostring ( math.random ( 1000 , 9999 ) ) .. " : " .. ent.npc_name
2017-08-25 15:31:45 +02:00
2017-08-11 18:03:45 +02:00
-- Initialize all gift data
ent.gift_data = {
2017-07-14 01:01:28 +02:00
-- Choose favorite items. Choose phase1 per default
favorite_items = npc.relationships . select_random_favorite_items ( ent.sex , " phase1 " ) ,
-- Choose disliked items. Choose phase1 per default
disliked_items = npc.relationships . select_random_disliked_items ( ent.sex ) ,
2017-03-27 17:08:18 +02:00
}
2017-08-25 15:31:45 +02:00
2017-03-27 17:08:18 +02:00
-- Flag that determines if NPC can have a relationship
2017-06-17 15:08:29 +02:00
ent.can_have_relationship = can_have_relationships ( ent.is_child )
2017-03-27 17:08:18 +02:00
2017-07-14 01:01:28 +02:00
--ent.infotext = "Interested in relationships: "..dump(ent.can_have_relationship)
2017-06-22 03:07:36 +02:00
-- Flag to determine if NPC can receive gifts
ent.can_receive_gifts = ent.can_have_relationship
2017-03-27 17:08:18 +02:00
-- Initialize relationships object
ent.relationships = { }
-- Determines if NPC is married or not
ent.is_married_to = nil
-- Initialize dialogues
2017-07-14 01:01:28 +02:00
ent.dialogues = npc.dialogue . select_random_dialogues_for_npc ( ent , " phase1 " )
2017-08-25 15:31:45 +02:00
2017-03-27 17:08:18 +02:00
-- Declare NPC inventory
ent.inventory = initialize_inventory ( )
-- Choose items to spawn with
choose_spawn_items ( ent )
-- Flags: generic booleans or functions that help drive functionality
ent.flags = { }
-- Declare trade data
ent.trader_data = {
2017-07-14 01:01:28 +02:00
-- Type of trader
trader_status = npc.trade . get_random_trade_status ( ) ,
-- Current buy offers
buy_offers = { } ,
-- Current sell offers
sell_offers = { } ,
-- Items to buy change timer
change_offers_timer = 0 ,
-- Items to buy change timer interval
change_offers_timer_interval = 60 ,
-- Trading list: a list of item names the trader is expected to trade in.
-- It is mostly related to its occupation.
-- If empty, the NPC will revert to casual trading
-- If not, it will try to sell those that it have, and buy the ones it not.
trade_list = {
sell = { } ,
buy = { } ,
both = { }
} ,
-- Custom trade allows to specify more than one payment
-- and a custom prompt (instead of the usual buy or sell prompts)
custom_trades = { }
2017-03-27 17:08:18 +02:00
}
-- Initialize trading offers for NPC
--npc.trade.generate_trade_offers_by_status(ent)
-- if ent.trader_data.trader_status == npc.trade.CASUAL then
-- select_casual_trade_offers(ent)
-- end
-- Actions data
ent.actions = {
2017-07-14 01:01:28 +02:00
-- The queue is a queue of actions to be performed on each interval
queue = { } ,
-- Current value of the action timer
action_timer = 0 ,
-- Determines the interval for each action in the action queue
-- Default is 1. This can be changed via actions
2017-08-11 18:03:45 +02:00
action_interval = npc.actions . default_interval ,
2017-07-14 01:01:28 +02:00
-- Avoid the execution of the action timer
action_timer_lock = false ,
-- Defines the state of the current action
current_action_state = npc.action_state . none ,
-- Store information about action on state before lock
state_before_lock = {
-- State of the mobs_redo API
freeze = false ,
-- State of execution
action_state = npc.action_state . none ,
-- Action executed while on lock
interrupted_action = { }
} ,
2017-05-05 15:13:30 +02:00
-- Walking variables -- required for implementing accurate movement code
walking = {
-- Defines whether NPC is walking to specific position or not
is_walking = false ,
-- Path that the NPC is following
path = { } ,
2017-08-25 15:31:45 +02:00
-- Target position the NPC is supposed to walk to in this step. NOTE:
2017-05-05 15:13:30 +02:00
-- This is NOT the end of the path, but the next position in the path
-- relative to the last position
target_pos = { }
}
2017-03-27 17:08:18 +02:00
}
2017-08-25 15:31:45 +02:00
-- This flag is checked on every step. If it is true, the rest of
2017-03-27 17:08:18 +02:00
-- Mobs Redo API is not executed
ent.freeze = nil
-- This map will hold all the places for the NPC
-- Map entries should be like: "bed" = {x=1, y=1, z=1}
ent.places_map = { }
2017-08-11 18:03:45 +02:00
-- Schedule data
ent.schedules = {
-- Flag to enable or disable the schedules functionality
2017-08-25 15:31:45 +02:00
enabled = true ,
2017-08-11 18:03:45 +02:00
-- Lock for when executing a schedule
lock = false ,
-- Queue of schedules executed
-- Used to calculate dependencies
temp_executed_queue = { } ,
-- An array of schedules, meant to be one per day at some point
--- when calendars are implemented. Allows for only 7 schedules,
-- one for each day of the week
generic = { } ,
2017-08-25 15:31:45 +02:00
-- An array of schedules, meant to be for specific dates in the
2017-08-11 18:03:45 +02:00
-- year. Can contain as many as possible. The keys will be strings
-- in the format MM:DD
date_based = { } ,
-- The following holds the check parameters provided by the
-- current schedule
current_check_params = { }
}
-- If occupation name given, override properties with
-- occupation values and initialize schedules
2017-08-25 15:31:45 +02:00
--minetest.log("Entity age: "..dump(ent.age)..", afult? "..dump(ent.age==npc.age.adult))
npc.log ( " INFO " , " Overriding NPC values with occupation: " .. dump ( occupation_name ) )
2017-08-11 18:03:45 +02:00
if occupation_name and occupation_name ~= " " and ent.age == npc.age . adult then
npc.occupations . initialize_occupation_values ( ent , occupation_name )
end
-- TODO: Remove this - do inside occupation
-- Dedicated trade test
ent.trader_data . trade_list.both = {
[ " default:tree " ] = { } ,
[ " default:cobble " ] = { } ,
[ " default:wood " ] = { } ,
[ " default:diamond " ] = { } ,
[ " default:mese_crystal " ] = { } ,
[ " flowers:rose " ] = { } ,
[ " advanced_npc:marriage_ring " ] = { } ,
[ " flowers:geranium " ] = { } ,
[ " mobs:meat " ] = { } ,
[ " mobs:leather " ] = { } ,
[ " default:sword_stone " ] = { } ,
[ " default:shovel_stone " ] = { } ,
[ " default:axe_stone " ] = { }
}
-- Generate trade offers
npc.trade . generate_trade_offers_by_status ( ent )
-- Add a custom trade offer
-- local offer1 = npc.trade.create_custom_sell_trade_offer("Do you want me to fix your steel sword?", "Fix steel sword", "Fix steel sword", "default:sword_steel", {"default:sword_steel", "default:iron_lump 5"})
-- table.insert(ent.trader_data.custom_trades, offer1)
--local offer2 = npc.trade.create_custom_sell_trade_offer("Do you want me to fix your mese sword?", "Fix mese sword", "Fix mese sword", "default:sword_mese", {"default:sword_mese", "default:copper_lump 10"})
--table.insert(ent.trader_data.custom_trades, offer2)
-- Set initialized flag on
2017-07-14 01:01:28 +02:00
ent.initialized = true
2017-08-25 15:31:45 +02:00
--npc.log("WARNING", "Spawned entity: "..dump(ent))
2017-08-11 18:03:45 +02:00
npc.log ( " INFO " , " Successfully initialized NPC with name " .. dump ( ent.npc_name )
.. " , sex: " .. ent.sex .. " , is child: " .. dump ( ent.is_child )
.. " , texture: " .. dump ( ent.textures ) )
-- Refreshes entity
ent.object : set_properties ( ent )
2017-03-27 17:08:18 +02:00
end
2017-06-21 13:16:11 +02:00
---------------------------------------------------------------------------------------
-- Trading functions
---------------------------------------------------------------------------------------
function npc . generate_trade_list_from_inventory ( self )
local list = { }
for i = 1 , # self.inventory do
2017-07-14 01:01:28 +02:00
list [ npc.get_item_name ( self.inventory [ i ] ) ] = { }
2017-06-21 13:16:11 +02:00
end
self.trader_data . trade_list.both = list
end
function npc . set_trading_status ( self , status )
-- Set status
self.trader_data . trader_status = status
-- Re-generate trade offers
npc.trade . generate_trade_offers_by_status ( self )
end
2016-12-12 02:52:57 +01:00
---------------------------------------------------------------------------------------
2016-12-06 02:22:33 +01:00
-- Inventory functions
2016-12-12 02:52:57 +01:00
---------------------------------------------------------------------------------------
2016-12-06 02:22:33 +01:00
-- NPCs inventories are restrained to 16 slots.
-- Each slot can hold one item up to 99 count.
-- Utility function to get item name from a string
2016-12-17 18:01:35 +01:00
function npc . get_item_name ( item_string )
return ItemStack ( item_string ) : get_name ( )
2016-12-06 02:22:33 +01:00
end
-- Utility function to get item count from a string
2016-12-17 18:01:35 +01:00
function npc . get_item_count ( item_string )
return ItemStack ( item_string ) : get_count ( )
2016-12-06 02:22:33 +01:00
end
-- Add an item to inventory. Returns true if add successful
2016-12-12 02:52:57 +01:00
-- These function can be used to give items to other NPCs
-- given that the "self" variable can be any NPC
2016-12-06 02:22:33 +01:00
function npc . add_item_to_inventory ( self , item_name , count )
-- Check if NPC already has item
local existing_item = npc.inventory_contains ( self , item_name )
2016-12-15 02:03:51 +01:00
if existing_item ~= nil and existing_item.item_string ~= nil then
2017-07-14 01:01:28 +02:00
-- NPC already has item. Get count and see
local existing_count = npc.get_item_count ( existing_item.item_string )
if ( existing_count + count ) < npc.INVENTORY_ITEM_MAX_STACK then
-- Set item here
2017-08-25 15:31:45 +02:00
self.inventory [ existing_item.slot ] =
2017-07-14 01:01:28 +02:00
npc.get_item_name ( existing_item.item_string ) .. " " .. tostring ( existing_count + count )
return true
else
--Find next free slot
for i = 1 , # self.inventory do
if self.inventory [ i ] == " " then
-- Found slot, set item
2017-08-25 15:31:45 +02:00
self.inventory [ i ] =
2017-07-14 01:01:28 +02:00
item_name .. " " .. tostring ( ( existing_count + count ) - npc.INVENTORY_ITEM_MAX_STACK )
return true
end
end
-- No free slot found
return false
end
2016-12-06 02:22:33 +01:00
else
2017-07-14 01:01:28 +02:00
-- Find a free slot
for i = 1 , # self.inventory do
if self.inventory [ i ] == " " then
-- Found slot, set item
self.inventory [ i ] = item_name .. " " .. tostring ( count )
return true
end
end
-- No empty slot found
return false
2016-12-06 02:22:33 +01:00
end
end
2016-12-16 02:51:06 +01:00
-- Same add method but with itemstring for convenience
function npc . add_item_to_inventory_itemstring ( self , item_string )
2016-12-17 18:01:35 +01:00
local item_name = npc.get_item_name ( item_string )
local item_count = npc.get_item_count ( item_string )
2016-12-16 02:51:06 +01:00
npc.add_item_to_inventory ( self , item_name , item_count )
end
2016-12-06 02:22:33 +01:00
-- Checks if an item is contained in the inventory. Returns
-- the item string or nil if not found
function npc . inventory_contains ( self , item_name )
for key , value in pairs ( self.inventory ) do
2017-07-14 01:01:28 +02:00
if value ~= " " and string.find ( value , item_name ) then
return { slot = key , item_string = value }
end
2016-12-06 02:22:33 +01:00
end
-- Item not found
return nil
end
-- Removes the item from an NPC inventory and returns the item
-- with its count (as a string, e.g. "default:apple 2"). Returns
-- nil if unable to get the item.
function npc . take_item_from_inventory ( self , item_name , count )
local existing_item = npc.inventory_contains ( self , item_name )
if existing_item ~= nil then
2017-07-14 01:01:28 +02:00
-- Found item
local existing_count = npc.get_item_count ( existing_item.item_string )
local new_count = existing_count
if existing_count - count < 0 then
-- Remove item first
self.inventory [ existing_item.slot ] = " "
-- TODO: Support for retrieving from next stack. Too complicated
-- and honestly might be unecessary.
return item_name .. " " .. tostring ( new_count )
else
new_count = existing_count - count
if new_count == 0 then
self.inventory [ existing_item.slot ] = " "
else
self.inventory [ existing_item.slot ] = item_name .. " " .. new_count
end
return item_name .. " " .. tostring ( count )
end
2016-12-06 02:22:33 +01:00
else
2017-07-14 01:01:28 +02:00
-- Not able to take item because not found
return nil
2016-12-06 02:22:33 +01:00
end
end
2016-12-17 20:27:23 +01:00
-- Same take method but with itemstring for convenience
function npc . take_item_from_inventory_itemstring ( self , item_string )
local item_name = npc.get_item_name ( item_string )
local item_count = npc.get_item_count ( item_string )
npc.take_item_from_inventory ( self , item_name , item_count )
2016-12-04 21:00:55 +01:00
end
2017-01-26 18:55:04 +01:00
---------------------------------------------------------------------------------------
-- Flag functionality
---------------------------------------------------------------------------------------
-- TODO: Consider removing them as they are pretty simple and straight forward.
-- Generic variables or function that help drive some functionality for the NPC.
function npc . add_flag ( self , flag_name , value )
self.flags [ flag_name ] = value
end
function npc . update_flag ( self , flag_name , value )
self.flags [ flag_name ] = value
end
function npc . get_flag ( self , flag_name )
return self.flags [ flag_name ]
end
---------------------------------------------------------------------------------------
-- Dialogue functionality
---------------------------------------------------------------------------------------
2016-12-04 21:00:55 +01:00
function npc . start_dialogue ( self , clicker , show_married_dialogue )
2016-12-03 00:39:06 +01:00
2016-12-17 20:27:23 +01:00
-- Call dialogue function as normal
2016-12-04 21:00:55 +01:00
npc.dialogue . start_dialogue ( self , clicker , show_married_dialogue )
2016-12-03 00:39:06 +01:00
-- Check and update relationship if needed
2016-12-12 02:52:57 +01:00
npc.relationships . dialogue_relationship_update ( self , clicker )
2016-12-03 00:39:06 +01:00
2016-11-14 12:28:37 +01:00
end
2016-11-12 13:06:09 +01:00
2016-12-18 18:32:39 +01:00
---------------------------------------------------------------------------------------
-- Action functionality
---------------------------------------------------------------------------------------
-- This function adds a function to the action queue.
-- Actions should be added in strict order for tasks to work as expected.
function npc . add_action ( self , action , arguments )
2017-01-14 04:54:27 +01:00
local action_entry = { action = action , args = arguments , is_task = false }
2016-12-18 18:32:39 +01:00
table.insert ( self.actions . queue , action_entry )
end
2017-01-14 04:54:27 +01:00
-- This function adds task actions in-place, as opposed to
-- at the end of the queue. This allows for continued order
function npc . add_task ( self , task , args )
local action_entry = { action = task , args = args , is_task = true }
table.insert ( self.actions . queue , action_entry )
end
2016-12-18 18:32:39 +01:00
-- This function removes the first action in the action queue
2017-01-14 04:54:27 +01:00
-- and then executes it
2016-12-18 18:32:39 +01:00
function npc . execute_action ( self )
2017-08-11 18:03:45 +02:00
-- Check if an action was interrupted
if self.actions . current_action_state == npc.action_state . interrupted then
npc.log ( " DEBUG " , " Re-inserting interrupted action for NPC: ' " .. dump ( self.npc_name ) .. " ': " .. dump ( self.actions . state_before_lock.interrupted_action ) )
-- Insert into queue the interrupted action
table.insert ( self.actions . queue , 1 , self.actions . state_before_lock.interrupted_action )
-- Clear the action
self.actions . state_before_lock.interrupted_action = { }
-- Clear the position
self.actions . state_before_lock.pos = { }
end
local result = nil
if table.getn ( self.actions . queue ) == 0 then
-- Set state to none
self.actions . current_action_state = npc.action_state . none
-- Keep state the same if there are no more actions in actions queue
return self.freeze
end
local action_obj = self.actions . queue [ 1 ]
-- Check if action is null
if action_obj.action == nil then
return
end
-- Check if action is an schedule check
if action_obj.action == " schedule_check " then
-- Execute schedule check
npc.schedule_check ( self )
-- Remove table entry
table.remove ( self.actions . queue , 1 )
-- Return
return false
end
-- If the entry is a task, then push all this new operations in
-- stack fashion
if action_obj.is_task == true then
npc.log ( " DEBUG " , " Executing task for NPC ' " .. dump ( self.npc_name ) .. " ': " .. dump ( action_obj ) )
-- Backup current queue
local backup_queue = self.actions . queue
-- Remove this "task" action from queue
table.remove ( self.actions . queue , 1 )
-- Clear queue
self.actions . queue = { }
-- Now, execute the task with its arguments
result = npc.actions . execute ( self , action_obj.action , action_obj.args )
--result = action_obj.action(self, action_obj.args)
-- After all new actions has been added by task, add the previously
-- queued actions back
for i = 1 , # backup_queue do
table.insert ( self.actions . queue , backup_queue [ i ] )
end
else
npc.log ( " DEBUG " , " Executing action for NPC ' " .. dump ( self.npc_name ) .. " ': " .. dump ( action_obj ) )
-- Store the action that is being executed
self.actions . state_before_lock.interrupted_action = action_obj
-- Store current position
self.actions . state_before_lock.pos = self.object : getpos ( )
-- Execute action as normal
result = npc.actions . execute ( self , action_obj.action , action_obj.args )
-- Remove task
table.remove ( self.actions . queue , 1 )
-- Set state
self.actions . current_action_state = npc.action_state . executing
end
return result
2016-12-18 18:32:39 +01:00
end
2017-01-19 01:34:02 +01:00
function npc . lock_actions ( self )
-- Avoid re-locking if already locked
if self.actions . action_timer_lock == true then
2017-07-14 01:01:28 +02:00
return
2017-01-19 01:34:02 +01:00
end
local pos = self.object : getpos ( )
if self.freeze == false then
2017-07-14 01:01:28 +02:00
-- Round current pos to avoid the NPC being stopped on positions
-- where later on can't walk to the correct positions
-- Choose which position is to be taken as start position
if self.actions . state_before_lock.pos ~= { } then
pos = vector.round ( self.actions . state_before_lock.pos )
else
pos = vector.round ( self.object : getpos ( ) )
end
pos.y = self.object : getpos ( ) . y
2017-01-19 01:34:02 +01:00
end
-- Stop NPC
2017-05-13 19:17:45 +02:00
npc.actions . execute ( self , npc.actions . cmd.STAND , { pos = pos } )
2017-01-19 01:34:02 +01:00
-- Avoid all timer execution
self.actions . action_timer_lock = true
-- Reset timer so that it has some time after interaction is done
self.actions . action_timer = 0
-- Check if there are is an action executing
2017-08-25 15:31:45 +02:00
if self.actions . current_action_state == npc.action_state . executing
2017-07-14 01:01:28 +02:00
and self.freeze == false then
-- Store the current action state
self.actions . state_before_lock.action_state = self.actions . current_action_state
-- Set current action state to interrupted
self.actions . current_action_state = npc.action_state . interrupted
2017-01-19 01:34:02 +01:00
end
-- Store the current freeze variable
self.actions . state_before_lock.freeze = self.freeze
-- Freeze mobs_redo API
self.freeze = false
2017-06-17 15:44:25 +02:00
npc.log ( " DEBUG " , " Locking NPC " .. dump ( self.npc_id ) .. " actions " )
2017-01-19 01:34:02 +01:00
end
function npc . unlock_actions ( self )
-- Allow timers to execute
self.actions . action_timer_lock = false
-- Restore the value of self.freeze
self.freeze = self.actions . state_before_lock.freeze
2017-08-25 15:31:45 +02:00
2017-01-19 01:34:02 +01:00
if table.getn ( self.actions . queue ) == 0 then
2017-07-14 01:01:28 +02:00
-- Allow mobs_redo API to execute since action queue is empty
self.freeze = true
2017-01-19 01:34:02 +01:00
end
2017-06-17 15:44:25 +02:00
npc.log ( " DEBUG " , " Unlocked NPC " .. dump ( self.npc_id ) .. " actions " )
2017-01-19 01:34:02 +01:00
end
2016-12-17 20:27:23 +01:00
2017-03-08 15:36:53 +01:00
---------------------------------------------------------------------------------------
-- Schedule functionality
---------------------------------------------------------------------------------------
-- Schedules allow the NPC to do different things depending on the time of the day.
2017-08-25 15:31:45 +02:00
-- The time of the day is in 24 hours and is consistent with the Minetest Game
2017-03-08 15:36:53 +01:00
-- /time command. Hours will be written as numbers: 1 for 1:00, 13 for 13:00 or 1:00 PM
-- The API is as following: a schedule can be created for a specific date or for a
-- day of the week. A date is a string in the format MM:DD
npc.schedule_types = {
[ " generic " ] = " generic " ,
[ " date_based " ] = " date_based "
}
2017-06-21 13:16:11 +02:00
npc.schedule_properties = {
put_item = " put_item " ,
2017-06-22 03:07:36 +02:00
put_multiple_items = " put_multiple_items " ,
2017-06-21 13:16:11 +02:00
take_item = " take_item " ,
2017-06-22 03:07:36 +02:00
trader_status = " trader_status " ,
can_receive_gifts = " can_receive_gifts "
2017-06-21 13:16:11 +02:00
}
2017-08-25 15:31:45 +02:00
local function get_time_in_hours ( )
2017-03-08 15:36:53 +01:00
return minetest.get_timeofday ( ) * 24
end
-- Create a schedule on a NPC.
-- Schedule types:
-- - Generic: Returns nil if there are already
-- seven schedules, one for each day of the
-- week or if the schedule attempting to add
2017-08-25 15:31:45 +02:00
-- already exists. The date parameter is the
2017-03-08 15:36:53 +01:00
-- day of the week it represents as follows:
-- - 1: Monday
-- - 2: Tuesday
-- - 3: Wednesday
-- - 4: Thursday
-- - 5: Friday
-- - 6: Saturday
-- - 7: Sunday
-- - Date-based: The date parameter should be a
-- string of the format "MM:DD". If it already
-- exists, function retuns nil
function npc . create_schedule ( self , schedule_type , date )
if schedule_type == npc.schedule_types . generic then
2017-07-14 01:01:28 +02:00
-- Check that there are no more than 7 schedules
if # self.schedules . generic == 7 then
-- Unable to add schedule
return nil
elseif # self.schedules . generic < 7 then
-- Check schedule doesn't exists already
if self.schedules . generic [ date ] == nil then
-- Add schedule
self.schedules . generic [ date ] = { }
else
-- Schedule already present
return nil
end
end
2017-03-08 15:36:53 +01:00
elseif schedule_type == npc.schedule_types . date then
2017-07-14 01:01:28 +02:00
-- Check schedule doesn't exists already
if self.schedules . date_based [ date ] == nil then
2017-08-25 15:31:45 +02:00
-- Add schedule
2017-07-14 01:01:28 +02:00
self.schedules . date_based [ date ] = { }
else
-- Schedule already present
return nil
end
2017-03-08 15:36:53 +01:00
end
end
function npc . delete_schedule ( self , schedule_type , date )
2017-08-11 18:03:45 +02:00
-- Delete schedule by setting entry to nil
self.schedules [ schedule_type ] [ date ] = nil
2017-03-08 15:36:53 +01:00
end
-- Schedule entries API
-- Allows to add, get, update and delete entries from each
2017-08-25 15:31:45 +02:00
-- schedule. Attempts to be as safe-fail as possible to avoid crashes.
2017-03-08 15:36:53 +01:00
-- Actions is an array of actions and tasks that the NPC
-- will perform at the scheduled time on the scheduled date
2017-03-09 19:54:37 +01:00
function npc . add_schedule_entry ( self , schedule_type , date , time , check , actions )
2017-08-11 18:03:45 +02:00
-- Check that schedule for date exists
if self.schedules [ schedule_type ] [ date ] ~= nil then
-- Add schedule entry
if check == nil then
self.schedules [ schedule_type ] [ date ] [ time ] = actions
else
self.schedules [ schedule_type ] [ date ] [ time ] . check = check
end
2017-07-14 01:01:28 +02:00
else
2017-08-11 18:03:45 +02:00
-- No schedule found, need to be created for date
return nil
2017-07-14 01:01:28 +02:00
end
2017-03-08 15:36:53 +01:00
end
function npc . get_schedule_entry ( self , schedule_type , date , time )
2017-08-11 18:03:45 +02:00
-- Check if schedule for date exists
if self.schedules [ schedule_type ] [ date ] ~= nil then
-- Return schedule
return self.schedules [ schedule_type ] [ date ] [ time ]
else
-- Schedule for date not found
return nil
end
2017-03-08 15:36:53 +01:00
end
2017-03-09 19:54:37 +01:00
function npc . update_schedule_entry ( self , schedule_type , date , time , check , actions )
2017-08-11 18:03:45 +02:00
-- Check schedule for date exists
if self.schedules [ schedule_type ] [ date ] ~= nil then
-- Check that a schedule entry for that time exists
if self.schedules [ schedule_type ] [ date ] [ time ] ~= nil then
-- Set the new actions
if check == nil then
self.schedules [ schedule_type ] [ date ] [ time ] = actions
else
self.schedules [ schedule_type ] [ date ] [ time ] . check = check
end
else
-- Schedule not found for specified time
return nil
end
2017-07-14 01:01:28 +02:00
else
2017-08-11 18:03:45 +02:00
-- Schedule not found for date
return nil
2017-07-14 01:01:28 +02:00
end
2017-03-08 15:36:53 +01:00
end
function npc . delete_schedule_entry ( self , schedule_type , date , time )
2017-08-11 18:03:45 +02:00
-- Check schedule for date exists
if self.schedules [ schedule_type ] [ date ] ~= nil then
-- Remove schedule entry by setting to nil
self.schedules [ schedule_type ] [ date ] [ time ] = nil
else
-- Schedule not found for date
return nil
end
2017-03-08 15:36:53 +01:00
end
2017-06-21 13:16:11 +02:00
function npc . schedule_change_property ( self , property , args )
2017-08-11 18:03:45 +02:00
if property == npc.schedule_properties . trader_status then
-- Get status from args
local status = args.status
-- Set status to NPC
npc.set_trading_status ( self , status )
elseif property == npc.schedule_properties . put_item then
local itemstring = args.itemstring
-- Add item
npc.add_item_to_inventory_itemstring ( self , itemstring )
elseif property == npc.schedule_properties . put_multiple_items then
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
2017-07-14 01:01:28 +02:00
current_itemstring = current_itemstring
2017-08-11 18:03:45 +02:00
.. " " .. dump ( math.random ( itemlist_entry.min , itemlist_entry.max ) )
2017-07-14 01:01:28 +02:00
else
current_itemstring = current_itemstring .. " " .. tostring ( itemlist_entry.count )
2017-08-11 18:03:45 +02:00
end
-- Add item to inventory
npc.add_item_to_inventory_itemstring ( self , current_itemstring )
2017-07-14 01:01:28 +02:00
end
2017-08-11 18:03:45 +02:00
elseif property == npc.schedule_properties . take_item then
2017-07-14 01:01:28 +02:00
local itemstring = args.itemstring
2017-08-11 18:03:45 +02:00
-- Add item
npc.take_item_from_inventory_itemstring ( self , itemstring )
elseif property == npc.schedule_properties . can_receive_gifts then
local value = args.can_receive_gifts
-- Set status
self.can_receive_gifts = value
end
end
function npc . add_schedule_check ( self )
table.insert ( self.actions . queue , { action = " schedule_check " , args = { } , is_task = false } )
end
2017-08-25 15:31:45 +02:00
-- Range: integer, radius in which nodes will be searched. Recommended radius is
-- between 1-3
2017-08-11 18:03:45 +02:00
-- Nodes: array of node names
2017-08-25 15:31:45 +02:00
-- Actions: map of node names to entries {action=<action_enum>, args={}}.
-- Arguments can be empty - the check function will try to determine most
2017-08-11 18:03:45 +02:00
-- arguments anyways (like pos and dir).
-- Special node "any" will execute those actions on any node except the
-- already specified ones.
-- None-action: array of entries {action=<action_enum>, args={}}.
-- Will be executed when no node is found.
function npc . schedule_check ( self )
local range = self.schedules . current_check_params.range
local nodes = self.schedules . current_check_params.nodes
local actions = self.schedules . current_check_params.actions
local none_actions = self.schedules . current_check_params.none_actions
-- Get NPC position
local start_pos = self.object : getpos ( )
-- Search nodes
local found_nodes = npc.places . find_node_nearby ( start_pos , nodes , range )
-- Check if any node was found
if found_nodes then
-- Pick a random node to act upon
local node_pos = found_nodes [ math.random ( 1 , # found_nodes ) ]
local node = minetest.get_node ( node_pos )
-- Set node as a place
-- Note: Code below isn't *adding* a node, but overwriting the
-- place with "schedule_target_pos" place type
npc.places . add_shared_accessible_place (
self , node , npc.places . PLACE_TYPE.SCHEDULE . TARGET , true )
-- Get actions related to node and enqueue them
for i = 1 , # actions [ node.name ] do
local args = { }
local action = nil
-- Calculate arguments for the following supported actions:
-- - Dig
-- - Place
-- - Walk step
-- - Walk to position
-- - Use furnace
if actions [ node.name ] [ i ] . action == npc.actions . cmd.DIG then
-- Defaults: items will be added to inventory if not specified
-- otherwise, and protection will be respected, if not specified
-- otherwise
args = {
pos = node_pos ,
add_to_inventory = action [ node.name ] [ i ] . args.add_to_inventory or true ,
bypass_protection = action [ node.name ] [ i ] . args.bypass_protection or false
}
elseif actions [ node.name ] [ i ] . action == npc.actions . cmd.PLACE then
-- Position: providing node_pos is because the currently planned
-- behavior for placing nodes is replacing digged nodes. A NPC farmer,
-- for instance, might dig a plant node and plant another one on the
-- same position.
2017-08-25 15:31:45 +02:00
-- Defaults: items will be taken from inventory if existing,
2017-08-11 18:03:45 +02:00
-- if not will be force-placed (item comes from thin air)
-- Protection will be respected
args = {
pos = action [ node.name ] [ i ] . args.pos or node_pos ,
source = action [ node.name ] [ i ] . args.source or npc.actions . take_from_inventory_forced ,
node = action [ node.name ] [ i ] . args.node ,
bypass_protection = action [ node.name ] [ i ] . args.bypass_protection or false
}
elseif actions [ node.name ] [ i ] . action == npc.actions . cmd.WALK_STEP then
-- Defaults: direction is calculated from start node to node_pos.
-- Speed is default wandering speed. Target pos is node_pos
-- Calculate dir if dir is random
local dir = npc.actions . get_direction ( start_pos , node_pos )
if actions [ node.name ] [ i ] . args.dir == " random " then
dir = math.random ( 0 , 7 )
elseif type ( actions [ node.name ] [ i ] . args.dir ) == " number " then
dir = actions [ node.name ] [ i ] . args.dir
end
args = {
dir = dir ,
speed = actions [ node.name ] [ i ] . args.speed or npc.actions . one_nps_speed ,
target_pos = actions [ node.name ] [ i ] . args.target_pos or node_pos
}
elseif actions [ node.name ] [ i ] . action == npc.actions . cmd.WALK_TO_POS then
-- Optimize walking -- since distances can be really short,
-- a simple walk_step() action can do most of the times. For
2017-08-25 15:31:45 +02:00
-- this, however, we need to calculate direction
2017-08-11 18:03:45 +02:00
-- First of all, check distance
if vector.distance ( start_pos , node_pos ) < 3 then
-- Will do walk_step based instead
action = npc.actions . cmd.WALK_STEP
args = {
dir = npc.actions . get_direction ( start_pos , node_pos ) ,
speed = npc.actions . one_nps_speed ,
target_pos = node_pos
}
else
-- Set end pos to be node_pos
args = {
end_pos = actions [ node.name ] [ i ] . args.end_pos or node_pos ,
walkable = actions [ node.name ] [ i ] . args.walkable or { }
}
end
elseif actions [ node.name ] [ i ] . action == npc.actions . cmd.USE_FURNACE then
-- Defaults: pos is node_pos. Freeze is true
args = {
pos = actions [ node.name ] [ i ] . args.pos or node_pos ,
item = actions [ node.name ] [ i ] . args.item ,
freeze = actions [ node.name ] [ i ] . args.freeze or true
}
end
-- Enqueue actions
npc.add_action ( self , action or actions [ node.name ] [ i ] . action , args or actions [ node.name ] [ i ] . args )
end
-- Enqueue next schedule check
2017-08-25 15:31:45 +02:00
if self.schedules . current_check_params.execution_count
2017-08-11 18:03:45 +02:00
< self.schedules . current_check_params.execution_times then
npc.add_schedule_check ( )
end
-- Nodes found
return true
else
-- No nodes found, enqueue none_actions
for i = 1 , # none_actions do
-- Enqueue actions
npc.add_action ( self , none_actions [ i ] . action , none_actions [ i ] . args )
end
-- Enqueue next schedule check
2017-08-25 15:31:45 +02:00
if self.schedules . current_check_params.execution_count
2017-08-11 18:03:45 +02:00
< self.schedules . current_check_params.execution_times then
npc.add_schedule_check ( )
end
-- No nodes found
return false
end
2017-06-21 13:16:11 +02:00
end
2016-12-12 02:52:57 +01:00
---------------------------------------------------------------------------------------
2016-12-17 20:27:23 +01:00
-- NPC Definition
2016-12-12 02:52:57 +01:00
---------------------------------------------------------------------------------------
2016-11-14 12:28:37 +01:00
mobs : register_mob ( " advanced_npc:npc " , {
2016-11-12 13:06:09 +01:00
type = " npc " ,
passive = false ,
damage = 3 ,
attack_type = " dogfight " ,
attacks_monsters = true ,
-- Added group attack
group_attack = true ,
--pathfinding = true,
pathfinding = 1 ,
hp_min = 10 ,
hp_max = 20 ,
armor = 100 ,
2017-08-11 18:03:45 +02:00
collisionbox = { - 0.20 , 0 , - 0.20 , 0.20 , 1.8 , 0.20 } ,
--collisionbox = {-0.20,-1.0,-0.20, 0.20,0.8,0.20},
--collisionbox = {-0.35,-1.0,-0.35, 0.35,0.8,0.35},
2016-11-12 13:06:09 +01:00
visual = " mesh " ,
mesh = " character.b3d " ,
drawtype = " front " ,
textures = {
2017-06-17 15:08:29 +02:00
{ " npc_male1.png " } ,
2017-08-11 18:01:44 +02:00
{ " npc_male2.png " } ,
{ " npc_male3.png " } ,
{ " npc_male4.png " } ,
{ " npc_male5.png " } ,
{ " npc_male6.png " } ,
2017-08-25 15:31:45 +02:00
{ " npc_male7.png " } ,
{ " npc_male8.png " } ,
{ " npc_male9.png " } ,
{ " npc_male10.png " } ,
{ " npc_male11.png " } ,
{ " npc_male12.png " } ,
{ " npc_male13.png " } ,
{ " npc_male14.png " } ,
{ " npc_male15.png " } ,
2017-06-17 15:08:29 +02:00
{ " npc_female1.png " } , -- female by nuttmeg20
2017-08-25 15:31:45 +02:00
{ " npc_female2.png " } ,
{ " npc_female3.png " } ,
{ " npc_female4.png " } ,
{ " npc_female5.png " } ,
{ " npc_female6.png " } ,
{ " npc_female7.png " } ,
{ " npc_female8.png " } ,
{ " npc_female9.png " } ,
{ " npc_female10.png " } ,
{ " npc_female11.png " } ,
2016-11-12 13:06:09 +01:00
} ,
child_texture = {
2017-07-14 01:01:28 +02:00
{ " npc_child_male1.png " } ,
2017-08-11 18:01:44 +02:00
{ " npc_child_female1.png " } ,
2017-08-11 16:44:57 +02:00
} ,
2016-11-12 13:06:09 +01:00
makes_footstep_sound = true ,
sounds = { } ,
-- Added walk chance
walk_chance = 30 ,
-- Added stepheight
2017-04-08 16:21:51 +02:00
stepheight = 0.6 ,
2017-01-16 18:16:42 +01:00
walk_velocity = 1 ,
2016-11-12 13:06:09 +01:00
run_velocity = 3 ,
2017-06-17 18:55:50 +02:00
jump = false ,
2016-11-12 13:06:09 +01:00
drops = {
{ name = " default:wood " , chance = 1 , min = 1 , max = 3 } ,
{ name = " default:apple " , chance = 2 , min = 1 , max = 2 } ,
{ name = " default:axe_stone " , chance = 5 , min = 1 , max = 1 } ,
} ,
water_damage = 0 ,
lava_damage = 2 ,
light_damage = 0 ,
2016-11-14 12:28:37 +01:00
--follow = {"farming:bread", "mobs:meat", "default:diamond"},
2016-11-12 13:06:09 +01:00
view_range = 15 ,
owner = " " ,
order = " follow " ,
--order = "stand",
fear_height = 3 ,
animation = {
speed_normal = 30 ,
speed_run = 30 ,
stand_start = 0 ,
stand_end = 79 ,
walk_start = 168 ,
walk_end = 187 ,
run_start = 168 ,
run_end = 187 ,
punch_start = 200 ,
punch_end = 219 ,
} ,
on_rightclick = function ( self , clicker )
2017-07-14 01:01:28 +02:00
-- Rotate NPC toward its clicker
npc.dialogue . rotate_npc_to_player ( self )
2017-01-19 01:34:02 +01:00
2017-07-14 01:01:28 +02:00
-- Get information from clicker
2016-11-12 13:06:09 +01:00
local item = clicker : get_wielded_item ( )
local name = clicker : get_player_name ( )
2017-06-17 15:08:29 +02:00
2017-08-25 15:33:25 +02:00
npc.log ( " DEBUG " , " Right-clicked NPC: " .. dump ( self ) )
2017-07-14 01:01:28 +02:00
-- Receive gift or start chat. If player has no item in hand
-- then it is going to start chat directly
2017-08-11 18:03:45 +02:00
--minetest.log("self.can_have_relationship: "..dump(self.can_have_relationship)..", self.can_receive_gifts: "..dump(self.can_receive_gifts)..", table: "..dump(item:to_table()))
2017-08-25 15:31:45 +02:00
if self.can_have_relationship
and self.can_receive_gifts
2017-08-11 18:03:45 +02:00
and item : to_table ( ) ~= nil then
-- Get item name
local item = minetest.registered_items [ item : get_name ( ) ]
local item_name = item.description
-- Show dialogue to confirm that player is giving item as gift
npc.dialogue . show_yes_no_dialogue (
self ,
" Do you want to give " .. item_name .. " to " .. self.npc_name .. " ? " ,
npc.dialogue . POSITIVE_GIFT_ANSWER_PREFIX .. item_name ,
function ( )
npc.relationships . receive_gift ( self , clicker )
end ,
npc.dialogue . NEGATIVE_ANSWER_LABEL ,
function ( )
npc.start_dialogue ( self , clicker , true )
end ,
name
)
2017-08-25 15:31:45 +02:00
else
npc.start_dialogue ( self , clicker , true )
end
end ,
2016-11-15 00:39:41 +01:00
do_custom = function ( self , dtime )
2017-08-11 18:03:45 +02:00
if self.initialized == nil then
-- Initialize NPC if spawned using the spawn egg built in from
-- mobs_redo. This functionality will be removed in the future in
2017-08-25 15:31:45 +02:00
-- favor of a better manual spawning method with customization
2017-08-11 18:03:45 +02:00
npc.log ( " WARNING " , " Initializing NPC from entity step. This message should only be appearing if an NPC is being spawned from inventory with egg! " )
npc.initialize ( self , self.object : getpos ( ) , true )
self.tamed = false
self.owner = nil
else
-- NPC is initialized, check other variables
-- Check child texture issues
if self.is_child then
2017-08-25 15:31:45 +02:00
-- Check texture
2017-08-11 18:03:45 +02:00
npc.texture_check . timer = npc.texture_check . timer + dtime
if npc.texture_check . timer > npc.texture_check . interval then
-- Reset timer
npc.texture_check . timer = 0
-- Set hornytimer to zero every 60 seconds so that children
-- don't grow automatically
self.hornytimer = 0
-- Set correct textures
self.texture = { self.selected_texture }
self.base_texture = { self.selected_texture }
self.object : set_properties ( self )
npc.log ( " WARNING " , " Corrected textures on NPC child " .. dump ( self.npc_name ) )
-- Set interval to large interval so this code isn't called frequently
npc.texture_check . interval = 60
end
2017-08-25 15:31:45 +02:00
end
end
2017-07-14 01:01:28 +02:00
2017-08-25 15:31:45 +02:00
-- Timer function for casual traders to reset their trade offers
2017-08-11 18:03:45 +02:00
self.trader_data . change_offers_timer = self.trader_data . change_offers_timer + dtime
-- Check if time has come to change offers
2017-08-25 15:31:45 +02:00
if self.trader_data . trader_status == npc.trade . CASUAL and
2017-08-11 18:03:45 +02:00
self.trader_data . change_offers_timer >= self.trader_data . change_offers_timer_interval then
-- Reset timer
self.trader_data . change_offers_timer = 0
-- Re-select casual trade offers
npc.trade . generate_trade_offers_by_status ( self )
end
2017-08-25 15:31:45 +02:00
2017-07-14 01:01:28 +02:00
-- Timer function for gifts
2017-08-11 18:03:45 +02:00
for i = 1 , # self.relationships do
local relationship = self.relationships [ i ]
-- Gift timer check
if relationship.gift_timer_value < relationship.gift_interval then
relationship.gift_timer_value = relationship.gift_timer_value + dtime
elseif relationship.talk_timer_value < relationship.gift_interval then
-- Relationship talk timer - only allows players to increase relationship
-- by talking on the same intervals as gifts
relationship.talk_timer_value = relationship.talk_timer_value + dtime
2017-07-14 01:01:28 +02:00
else
2017-08-11 18:03:45 +02:00
-- Relationship decrease timer
2017-08-25 15:31:45 +02:00
if relationship.relationship_decrease_timer_value
2017-08-11 18:03:45 +02:00
< relationship.relationship_decrease_interval then
2017-08-25 15:31:45 +02:00
relationship.relationship_decrease_timer_value =
2017-08-11 18:03:45 +02:00
relationship.relationship_decrease_timer_value + dtime
else
-- Check if married to decrease half
if relationship.phase == " phase6 " then
-- Avoid going below the marriage phase limit
2017-08-25 15:31:45 +02:00
if ( relationship.points - 0.5 ) >=
2017-08-11 18:03:45 +02:00
npc.relationships . RELATIONSHIP_PHASE [ " phase5 " ] . limit then
relationship.points = relationship.points - 0.5
end
else
relationship.points = relationship.points - 1
end
relationship.relationship_decrease_timer_value = 0
--minetest.log(dump(self))
end
2017-07-14 01:01:28 +02:00
end
end
2017-08-11 18:03:45 +02:00
-- Action queue timer
-- Check if actions and timers aren't locked
if self.actions . action_timer_lock == false then
-- Increment action timer
self.actions . action_timer = self.actions . action_timer + dtime
if self.actions . action_timer >= self.actions . action_interval then
-- Reset action timer
self.actions . action_timer = 0
-- Check if NPC is walking
if self.actions . walking.is_walking == true then
-- Move NPC to expected position to ensure not getting lost
local pos = self.actions . walking.target_pos
self.object : moveto ( { x = pos.x , y = pos.y - 0.5 , z = pos.z } )
end
-- Execute action
self.freeze = npc.execute_action ( self )
-- Check if there are still remaining actions in the queue
if self.freeze == nil and table.getn ( self.actions . queue ) > 0 then
self.freeze = false
end
end
2017-07-14 01:01:28 +02:00
end
2017-08-25 15:31:45 +02:00
-- Schedule timer
-- Check if schedules are enabled
if self.schedules . enabled == true then
-- Get time of day
local time = get_time_in_hours ( )
-- Check if time is an hour
if ( ( time % 1 ) < dtime ) and self.schedules . lock == false then
-- Activate lock to avoid more than one entry to this code
self.schedules . lock = true
-- Get integer part of time
time = ( time ) - ( time % 1 )
-- Check if there is a schedule entry for this time
-- Note: Currently only one schedule is supported, for day 0
minetest.log ( " Time: " .. dump ( time ) )
local schedule = self.schedules . generic [ 0 ]
if schedule ~= nil then
-- Check if schedule for this time exists
if schedule [ time ] ~= nil then
npc.log ( " DEBUG " , " Adding actions to action queue " )
-- Add to action queue all actions on schedule
for i = 1 , # schedule [ time ] do
-- Check if schedule has a check function
if schedule [ time ] [ i ] . check then
-- Add parameters for check function and run for first time
2017-08-25 15:33:25 +02:00
npc.log ( " DEBUG " , " NPC " .. dump ( self.npc_name ) .. " is starting check on " .. minetest.pos_to_string ( self.object : getpos ( ) ) )
2017-08-25 15:31:45 +02:00
local check_params = schedule [ time ] [ i ]
-- Calculates how many times check will be executed
local execution_times = check_params.count
if check_params.random_execution_times then
execution_times = math.random ( check_params.min_count , check_params.max_count )
end
-- Set current parameters
self.schedules . current_check_params = {
range = check_params.range ,
nodes = check_params.nodes ,
actions = check_params.actions ,
none_actions = check_params.none_actions ,
execution_count = 0 ,
execution_times = execution_times
}
-- Execute check for the first time
npc.schedule_check ( self )
else
2017-08-25 15:33:25 +02:00
npc.log ( " DEBUG " , " Executing schedule entry: " .. dump ( schedule [ time ] [ i ] ) )
2017-08-25 15:31:45 +02:00
-- Run usual schedule entry
-- Check chance
local execution_chance = math.random ( 1 , 100 )
if not schedule [ time ] [ i ] . chance or
( schedule [ time ] [ i ] . chance and execution_chance <= schedule [ time ] [ i ] . chance ) then
-- Check if entry has dependency on other entry
local dependencies_met = nil
if schedule [ time ] [ i ] . depends then
dependencies_met = npc.utils . array_is_subset_of_array (
self.schedules . temp_executed_queue ,
schedule [ time ] [ i ] . depends )
end
-- Check for dependencies being met
if dependencies_met == nil or dependencies_met == true then
-- Add tasks
if schedule [ time ] [ i ] . task ~= nil then
-- Add task
npc.add_task ( self , schedule [ time ] [ i ] . task , schedule [ time ] [ i ] . args )
elseif schedule [ time ] [ i ] . action ~= nil then
-- Add action
npc.add_action ( self , schedule [ time ] [ i ] . action , schedule [ time ] [ i ] . args )
elseif schedule [ time ] [ i ] . property ~= nil then
-- Change NPC property
npc.schedule_change_property ( self , schedule [ time ] [ i ] . property , schedule [ time ] [ i ] . args )
2017-08-11 18:03:45 +02:00
end
2017-08-25 15:31:45 +02:00
-- Backward compatibility check
if self.schedules . temp_executed_queue then
-- Add into execution queue to meet dependency
table.insert ( self.schedules . temp_executed_queue , i )
2017-08-11 18:03:45 +02:00
end
end
2017-08-25 15:31:45 +02:00
else
-- TODO: Change to debug
2017-08-25 15:33:25 +02:00
npc.log ( " DEBUG " , " Skipping schedule entry for time " .. dump ( time ) .. " : " .. dump ( schedule [ time ] [ i ] ) )
2017-08-25 15:31:45 +02:00
end
end
end
-- Clear execution queue
self.schedules . temp_executed_queue = { }
2017-08-25 15:33:25 +02:00
npc.log ( " DEBUG " , " New action queue: " .. dump ( self.actions ) )
2017-08-25 15:31:45 +02:00
end
end
else
-- Check if lock can be released
if ( time % 1 ) > dtime then
-- Release lock
self.schedules . lock = false
end
end
end
2017-08-11 18:03:45 +02:00
return self.freeze
2016-11-15 00:39:41 +01:00
end
2016-11-12 13:06:09 +01:00
} )
2016-11-14 12:28:37 +01:00
-- Spawn
2017-03-23 03:08:41 +01:00
-- mobs:spawn({
2017-03-27 17:08:18 +02:00
-- name = "advanced_npc:npc",
-- nodes = {"advanced_npc:plotmarker_auto_spawner", "mg_villages:plotmarker"},
-- min_light = 3,
-- active_object_count = 1,
2017-03-23 03:08:41 +01:00
-- interval = 5,
-- chance = 1,
2017-03-27 17:08:18 +02:00
-- --max_height = 0,
-- on_spawn = npc.initialize
2017-03-23 03:08:41 +01:00
-- })
2016-11-12 13:06:09 +01:00
2016-12-17 20:27:23 +01:00
-------------------------------------------------------------------------
-- Item definitions
-------------------------------------------------------------------------
2016-12-18 18:32:39 +01:00
mobs : register_egg ( " advanced_npc:npc " , S ( " NPC " ) , " default_brick.png " , 1 )
2016-11-12 13:06:09 +01:00
-- compatibility
2016-11-14 12:28:37 +01:00
mobs : alias_mob ( " mobs:npc " , " advanced_npc:npc " )
-- Marriage ring
minetest.register_craftitem ( " advanced_npc:marriage_ring " , {
description = S ( " Marriage Ring " ) ,
2016-12-16 03:33:57 +01:00
inventory_image = " marriage_ring.png " ,
2016-11-14 12:28:37 +01:00
} )
-- Marriage ring craft recipe
minetest.register_craft ( {
output = " advanced_npc:marriage_ring " ,
recipe = { { " " , " " , " " } ,
2017-07-14 01:01:28 +02:00
{ " " , " default:diamond " , " " } ,
{ " " , " default:gold_ingot " , " " } } ,
2016-11-14 12:28:37 +01:00
} )