Remove advanced_npc mod

master
Solebull 2019-04-09 22:45:05 +02:00
parent 44d5be2c11
commit 4afe6b20f9
44 changed files with 1 additions and 13582 deletions

View File

@ -3,3 +3,4 @@ their name for reference :
villagers: not needed and suspected to slow down chests;
darkage: seems to add game-time map generation;
advanced_npc: seems to slow down gameplay;

View File

@ -1,4 +0,0 @@
.idea/
backup/
debug.txt
advanced_npc.iml

View File

@ -1,46 +0,0 @@
advanced_npc
============
Introduction
------------
Advanced NPC is a mod for Minetest using mobs_redo API.
The goal of this mod is to be able to have live villages in Minetest. These NPCs are highly inspired by the typical NPCs of _Harvest Moon_ games. The general idea is that on almost all buildings of a village there are NPCs that are kind of intelligent: they have daily tasks they perform, can speak to players, can trade with the player, can use their own items (chests and furnaces for example), know where to go around their house and village, can be lumbers, miners or any other Minetest-suitable profession and can ultimately engage into relationships with the player. And while basically only players are mentioned here, the ultimate goal is that they can do all of this also among themselves, so that villages are alive and evolving by themselves, without player intervention.
Installation
------------
__NOTE__: Advanced NPC is still under development. While the mod is largely stable, it lacks one of the most important pieces: spawning. Currently, NPCs can be spawned using eggs (found in creative inventory as 'NPC') and by themselves on villages of the [mg_villages mod](https://forum.minetest.net/viewtopic.php?t=13589). NPCs will spawn automatically on mg_villages villages and over time will populate the entire village. If something goes wrong, you can reset the village by:
- Clearing all objects (in chat, type /clearobjects quick)
- Restore original plotmarkers (in chat, type /restore_plotmarkers radius)
- The radius can be any number, but it is recommended you use a not so large number. 200 is suitable. So stand in the middle of the village and then run that command.
This will actually restore the village and will slowly make NPCs spawn again. Currently there's no way to disable NPCs spawning on village, except by going to `spawner.lua` and commenting out all of `minetest.register_abm()` code.
__Download__ the mod [here](https://github.com/hkzorman/advanced_npc/archive/master.zip) (link always pointing to latest version)
For this mod to work correctly, you also need to install the [mobs_redo](https://github.com/tenplus1/mobs_redo) mod. After installation, make sure you enable it in your world.
License
-------
__advanced_npc__ is Copyright (C) 2016-2017 Hector Franqui (zorman2000), licensed under the GPLv3 license. See `license.txt` for details.
The `pathfinder.lua` file contains code slighlty modified from the [pathfinder mod](https://github.com/MarkuBu/pathfinder) by MarkBu, which is licensed as WTFPL. See `actions/pathfinder.lua` for details.
Current NPC textures are from mobs_redo mod.
The following textures are by Zorman2000:
- marriage_ring.png - CC BY-SA
Documentation and API
---------------------
This mod requires a good user manual, and also is planned to have an extensive API, properly documented. Unfortunately, these still aren't ready. A very very very WIP manual can be found in the [wiki](https://github.com/hkzorman/advanced_npc/wiki/Concept%3A-Dialogues)
Roadmap
-------
See it on the [wiki](https://github.com/hkzorman/advanced_npc/wiki).

View File

@ -1,4 +0,0 @@
default
mobs
mg_villages?
intllib?

View File

@ -1 +0,0 @@
Adds NPCs which are smart, have homes, can talk, trade and even establish friendships and more with you!

View File

@ -1,695 +0,0 @@
-------------------------------------------------------------------------------------
-- NPC dialogue code by Zorman2000
-------------------------------------------------------------------------------------
npc.dialogue = {}
npc.dialogue.POSITIVE_GIFT_ANSWER_PREFIX = "Yes, give "
npc.dialogue.NEGATIVE_ANSWER_LABEL = "Nevermind"
npc.dialogue.MIN_DIALOGUES = 2
npc.dialogue.MAX_DIALOGUES = 4
npc.dialogue.dialogue_type = {
married = 1,
casual_trade = 2,
dedicated_trade = 3,
custom_trade = 4
}
-- This table contains the answers of dialogue boxes
npc.dialogue.dialogue_results = {
options_dialogue = {},
yes_no_dialogue = {}
}
npc.dialogue.tags = {
UNISEX = "unisex",
MALE = "male",
FEMALE = "female",
-- Relationship based tags - these are one-to-one with the
-- phase names.
DEFAULT_MARRIED_DIALOGUE = "default_married_dialogue",
PHASE_1 = "phase1",
PHASE_2 = "phase2",
PHASE_3 = "phase3",
PHASE_4 = "phase4",
PHASE_5 = "phase5",
GIFT_ITEM_HINT = "gift_item_hint",
GIFT_ITEM_RESPONSE = "gift_item_response",
GIFT_ITEM_LIKED = "gift_item_liked",
GIFT_ITEM_UNLIKED = "gift_item_unliked",
-- Trade-related tags
DEFAULT_CASUAL_TRADE = "default_casual_trade_dialogue",
DEFAULT_DEDICATED_TRADE = "default_dedicated_trade_dialogue",
DEFAULT_BUY_OFFER = "buy_offer",
DEFAULT_SELL_OFFER = "sell_offer",
-- Occupation-based tags - these are one-to-one with the
-- default occupation names
BASIC = "basic", -- Dialogues related to the basic occupation should
-- use this. As basic occupation is generic, any occupation
-- should be able to use these dialogues.
DEFAULT_FARMER = "default_farmer",
DEFAULT_COOKER = "default_cooker"
}
-- This table will contain all the registered dialogues for NPCs
npc.dialogue.registered_dialogues = {}
npc.dialogue.cache_keys = {
CASUAL_BUY_DIALOGUE = {key="CASUAL_BUY_DIALOGUE", tags={npc.dialogue.tags.DEFAULT_CASUAL_TRADE, npc.dialogue.tags.DEFAULT_BUY_OFFER}},
CASUAL_SELL_DIALOGUE = {key="CASUAL_SELL_DIALOGUE", tags={npc.dialogue.tags.DEFAULT_CASUAL_TRADE, npc.dialogue.tags.DEFAULT_SELL_OFFER}},
DEDICATED_TRADER_DIALOGUE = {key="DEDICATED_TRADER_DIALOGUE", tags={npc.dialogue.tags.DEFAULT_DEDICATED_TRADE}},
MARRIED_DIALOGUE = {key="MARRIED_DIALOGUE", tags={npc.dialogue.tags.DEFAULT_MARRIED_DIALOGUE}},
}
npc.dialogue.cache = {}
--------------------------------------------------------------------------------------
-- Dialogue registration functions
-- All dialogues will be registered by providing a definition.
-- A unique key will be assigned to them. The dialogue definition is the following:
-- {
-- text: "",
-- ^ The "spoken" dialogue line
-- flag:
-- ^ If the flag with the specified name has the specified value
-- then this dialogue is valid
-- {
-- name: ""
-- ^ Name of the flag
-- value:
-- ^ Expected value of the flag. A flag can be a function. In such a case, it is
-- expected the function will return this value.
-- },
-- tags = {
-- -- Tags are an array of string that allow to classify dialogues
-- -- A dialogue can have as many tags as desired and can take any form.
-- -- However, for consistency, some predefined tags can be found at
-- -- npc.dialogue.tags.
-- -- Example:
-- "phase1",
-- "any"
-- }
-- responses = {
-- -- Array of responses the player can choose. A response can be of
-- -- two types: as [1] or as [2] (see example below)
-- [1] = {
-- text = "Yes",
-- -- Text displayed to the player
-- action_type = "dialogue",
-- -- Type of action that happens when the player chooses this response.
-- -- can be "dialogue" or "function". This example shows "dialogue"
-- action = {
-- text = "It's so beautiful, and big, and large, and infinite, and..."
-- },
-- },
-- -- A table containing a dialogue. This means you can include not only
-- -- text but also flag and responses as well. Dialogues are recursive.
-- [2] = {
-- text = "No",
-- action_type = "function",
-- action = function(self, player)
-- -- A function will have access to self, which is the NPC
-- -- and the player, which is the player ObjectRef. You can
-- -- pretty much do anything here. The example here is very simple,
-- -- just sending a chat message. But you can add items to players
-- -- or to NPCs and so on.
-- minetest.chat_send_player(player:get_player_name(), "Oh, ok...")
-- end,
-- },
-- }
-- }
--------------------------------------------------------------------------------------
-- This function sets a unique response ID (made of <depth>:<response index>) to
-- each response that features a function. This is to be able to locate the
-- function easily later
local function set_response_ids_recursively(dialogue, depth, dialogue_id)
-- Base case: dialogue object with no responses and no responses below it
if dialogue.responses == nil
and (dialogue.action_type == "dialogue" and dialogue.action.responses == nil) then
return
elseif dialogue.responses ~= nil then
-- Assign a response ID to each response
local response_id_prefix = tostring(depth)..":"
for key,value in ipairs(dialogue.responses) do
if value.action_type == "function" then
value.response_id = response_id_prefix..key
value.dialogue_id = dialogue_id
else
-- We have a dialogue action type. Need to check if dialogue has further responses
if value.action.responses ~= nil then
set_response_ids_recursively(value.action, depth + 1, dialogue_id)
end
end
end
end
end
-- The register dialogue function will just receive the definition as
-- explained above. The unique key will be the index it gets into the
-- array when inserted.
function npc.dialogue.register_dialogue(def)
-- If def has not tags then apply the default ones
if not def.tags then
def.tags = {npc.dialogue.tags.UNISEX, npc.dialogue.tags.PHASE_1}
end
local dialogue_id = table.getn(npc.dialogue.registered_dialogues) + 1
-- Set the response IDs - required for dialogue objects that
-- form trees of dialogues
set_response_ids_recursively(def, 0, dialogue_id)
def.key = dialogue_id
-- Insert dialogue into table
table.insert(npc.dialogue.registered_dialogues, def)
return dialogue_id
end
-- This function returns a table of dialogues that meet the given
-- tags array. The keys in the table are the keys in
-- npc.dialogue.registered_dialogues, therefore you can use them to
--retrieve specific dialogues. However, it should be stored by the NPC.
function npc.dialogue.search_dialogue_by_tags(tags, find_all)
--minetest.log("Tags being searched: "..dump(tags))
local result = {}
for key, def in pairs(npc.dialogue.registered_dialogues) do
-- Check if def.tags have any of the provided tags
local tags_found = 0
--minetest.log("Tags on dialogue def: "..dump(def.tags))
for i = 1, #tags do
if npc.utils.array_contains(def.tags, tags[i]) then
tags_found = tags_found + 1
end
end
--minetest.log("Tags found: "..dump(tags_found))
-- Check if we found all tags
if find_all then
if tags_found == #tags then
-- Add result
result[key] = def
end
elseif not find_all then
if tags_found == #tags or tags_found == #def.tags then
-- Add result
result[key] = def
end
end
end
return result
end
function npc.dialogue.get_cached_dialogue_key(_cache_key, tags)
local cache_key = _cache_key
if type(_cache_key) == "table" then
cache_key = _cache_key.key
tags = _cache_key.tags
end
local key = npc.dialogue.cache[cache_key]
-- Check if key isn't cached
if not key then
-- Search for the dialogue
local dialogues = npc.dialogue.search_dialogue_by_tags(tags, true)
key = npc.utils.get_map_keys(dialogues)[1]
-- Populate cache
npc.dialogue.cache[cache_key] = key
-- Return key
return key
else
-- Return the cached key
return key
end
end
--------------------------------------------------------------------------------------
-- Dialogue box definitions
-- The dialogue boxes are used for the player to interact with the
-- NPC in dialogues.
--------------------------------------------------------------------------------------
-- Creates and shows a multi-option dialogue based on the number of responses
-- that the dialogue object contains
function npc.dialogue.show_options_dialogue(self,
dialogue_key,
dialogue,
dismiss_option_label,
player_name)
local responses = dialogue.responses
local options_length = table.getn(responses) + 1
local formspec_height = (options_length * 0.7) + 0.4
local formspec = "size[7,"..tostring(formspec_height).."]"
for i = 1, #responses do
local y = 0.8;
if i > 1 then
y = (0.75 * i)
end
formspec = formspec.."button_exit[0.5,"
..(y - 0.5)..";6,0.5;opt"..tostring(i)..";"..responses[i].text.."]"
end
formspec = formspec.."button_exit[0.5,"
..(formspec_height - 0.7)..";6,0.5;exit;"..dismiss_option_label.."]"
-- Create entry on options_dialogue table
npc.dialogue.dialogue_results.options_dialogue[player_name] = {
npc = self,
dialogue = dialogue,
dialogue_key = dialogue_key,
is_married_dialogue =
(dialogue.dialogue_type == npc.dialogue.dialogue_type.married),
is_custom_trade_dialogue =
(dialogue.dialogue_type == npc.dialogue.dialogue_type.custom_trade),
casual_trade_type = dialogue.casual_trade_type,
options = responses
}
minetest.show_formspec(player_name, "advanced_npc:options", formspec)
end
-- This function is used for showing a yes/no dialogue formspec
function npc.dialogue.show_yes_no_dialogue(self,
prompt,
positive_answer_label,
positive_callback,
negative_answer_label,
negative_callback,
player_name)
npc.exec.set_input_wait_state(self)
local formspec = "size[7,3]"..
"label[0.5,0.1;"..prompt.."]"..
"button_exit[0.5,1.15;6,0.5;yes_option;"..positive_answer_label.."]"..
"button_exit[0.5,1.95;6,0.5;no_option;"..negative_answer_label.."]"
-- Create entry into responses table
npc.dialogue.dialogue_results.yes_no_dialogue[player_name] = {
npc = self,
yes_callback = positive_callback,
no_callback = negative_callback
}
minetest.show_formspec(player_name, "advanced_npc:yes_no", formspec)
end
--------------------------------------------------------------------------------------
-- Dialogue methods
--------------------------------------------------------------------------------------
-- Select random dialogue objects for an NPC based on gender
-- and the relationship phase with player
function npc.dialogue.select_random_dialogues_for_npc(self, phase)
local result = {
normal = {},
hints = {}
}
local phase_tag = "phase1"
if phase then
phase_tag = phase
end
local search_tags = {
"unisex",
self.gender,
phase_tag,
self.occupation
}
local dialogues = npc.dialogue.search_dialogue_by_tags(search_tags)
if dialogues and next(dialogues) ~= nil then
local keys = npc.utils.get_map_keys(dialogues)
-- Determine how many dialogue lines the NPC will have
local number_of_dialogues = math.random(npc.dialogue.MIN_DIALOGUES, npc.dialogue.MAX_DIALOGUES)
for i = 1, number_of_dialogues do
local key_id = math.random(1, #keys)
result.normal[i] = keys[key_id]
npc.log("DEBUG", "Adding dialogue: "..dump(dialogues[keys[key_id]]))
end
-- Add item hints.
for i = 1, 2 do
local hints = npc.relationships.get_dialogues_for_gift_item(
self.gift_data.favorite_items["fav"..tostring(i)],
npc.dialogue.tags.GIFT_ITEM_HINT,
npc.dialogue.tags.GIFT_ITEM_LIKED,
self.gender,
phase_tag)
for key, value in pairs(hints) do
result.hints[i] = key
end
end
for i = 3, 4 do
local hints = npc.relationships.get_dialogues_for_gift_item(
self.gift_data.disliked_items["dis"..tostring(i-2)],
npc.dialogue.tags.GIFT_ITEM_HINT,
npc.dialogue.tags.GIFT_ITEM_UNLIKED,
self.gender)
for key, value in pairs(hints) do
result.hints[i] = key
end
end
end
npc.log("DEBUG", "Dialogue results:"..dump(result))
return result
end
-- This function creates a multi-option dialogue from the custom trades that the
-- NPC have.
function npc.dialogue.create_custom_trade_options(self, player)
-- Create the action for each option
local actions = {}
for i = 1, #self.trader_data.custom_trades do
table.insert(actions,
function()
npc.trade.show_custom_trade_offer(self, player, self.trader_data.custom_trades[i])
end)
end
-- Default text to be shown for dialogue prompt
local text = npc.trade.CUSTOM_TRADES_PROMPT_TEXT
-- Get the options from each custom trade entry
local options = {}
if #self.trader_data.custom_trades == 1 then
table.insert(options, self.trader_data.custom_trades[1].button_prompt)
text = self.trader_data.custom_trades[1].option_prompt
else
for i = 1, #self.trader_data.custom_trades do
table.insert(options, self.trader_data.custom_trades[i].button_prompt)
end
end
-- Create dialogue object
local dialogue = npc.dialogue.create_option_dialogue(text, options, actions)
dialogue.dialogue_type = npc.dialogue.dialogue_type.custom_trade
return dialogue
end
-- This function will choose randomly a dialogue from the NPC data
-- and process it.
function npc.dialogue.start_dialogue(self, player, show_married_dialogue)
-- Choose a dialogue randomly
local dialogue = {}
-- Construct dialogue for marriage
if npc.relationships.get_relationship_phase(self, player:get_player_name()) == "phase6"
and show_married_dialogue == true then
dialogue = npc.relationships.MARRIED_NPC_DIALOGUE
npc.dialogue.process_dialogue(self, dialogue, player:get_player_name())
return
end
-- Show options dialogue for dedicated trader
if self.trader_data.trader_status == npc.trade.TRADER then
dialogue = npc.dialogue.get_cached_dialogue_key(npc.dialogue.cache_keys.DEDICATED_TRADER_DIALOGUE)
npc.dialogue.process_dialogue(self, dialogue, player:get_player_name())
return
end
local chance = math.random(1, 100)
--minetest.log("Chance: "..dump(chance))
if chance < 30 then
-- Show trading options for casual traders
-- If NPC has custom trading options, these will be
-- shown as well with equal chance as the casual
-- buy/sell options
if self.trader_data.trader_status == npc.trade.NONE then
-- Show custom trade options if available
if table.getn(self.trader_data.custom_trades) > 0 then
-- Show custom trade options
dialogue = npc.dialogue.create_custom_trade_options(self, player)
else
-- If not available, choose normal dialogue
dialogue = self.dialogues.normal[math.random(1, #self.dialogues.normal)]
end
elseif self.trader_data.trader_status == npc.trade.CASUAL then
local max_trade_chance = 2
if table.getn(self.trader_data.custom_trades) > 0 then
max_trade_chance = 3
end
-- Show buy/sell with 50% chance each
local trade_chance = math.random(1, max_trade_chance)
if trade_chance == 1 then
-- Show casual buy dialogue
dialogue = npc.dialogue.get_cached_dialogue_key(npc.dialogue.cache_keys.CASUAL_BUY_DIALOGUE)
elseif trade_chance == 2 then
-- Show casual sell dialogue
dialogue = npc.dialogue.get_cached_dialogue_key(npc.dialogue.cache_keys.CASUAL_SELL_DIALOGUE)
elseif trade_chance == 3 then
-- Show custom trade options
dialogue = npc.dialogue.create_custom_trade_options(self, player)
end
end
elseif chance >= 30 and chance < 90 then
-- Choose a random dialogue from the common ones
dialogue = self.dialogues.normal[math.random(1, #self.dialogues.normal)]
elseif chance >= 90 then
-- Check if gift items hints are enabled
minetest.log("Self gift data enable: "..dump(self.gift_data.enable_gift_items_hints))
if self.gift_data.enable_gift_items_hints then
-- Choose a random dialogue line from the favorite/disliked item hints
dialogue = self.dialogues.hints[math.random(1, 4)]
else
-- Choose a random dialogue from the common ones
dialogue = self.dialogues.normal[math.random(1, #self.dialogues.normal)]
end
end
local dialogue_result = npc.dialogue.process_dialogue(self, dialogue, player:get_player_name())
if dialogue_result == false then
-- Try to find another dialogue line
npc.dialogue.start_dialogue(self, player, show_married_dialogue)
end
end
-- This function processes a dialogue object and performs
-- actions depending on what is defined in the object
function npc.dialogue.process_dialogue(self, dialogue, player_name)
-- Freeze NPC actions
npc.exec.set_input_wait_state(self)
--npc.lock_actions(self)
local dialogue_key = -1
if type(dialogue) ~= "table" then
dialogue_key = dialogue
dialogue = npc.dialogue.registered_dialogues[dialogue]
--minetest.log("Found dialogue: "..dump(dialogue))
end
-- Check if this dialogue has a flag definition
if dialogue.flag then
-- Check if the NPC has this flag
local flag_value = npc.get_flag(self, dialogue.flag.name)
if flag_value ~= nil then
-- Check if value of the flag is equal to the expected value
if flag_value ~= dialogue.flag.value then
-- Do not process this dialogue
return false
end
else
if (type(dialogue.flag.value) == "boolean" and dialogue.flag.value ~= false)
or (type(dialogue.flag.value) == "number" and dialogue.flag.value > 0) then
-- Do not process this dialogue
return false
end
end
end
-- Send dialogue line
if dialogue.text then
npc.chat(self.npc_name, player_name, dialogue.text)
end
-- Check if dialogue has responses. If it doesn't, unlock the actions
-- queue and reset actions timer.'
if not dialogue.responses then
npc.exec.set_ready_state(self)
end
-- Check if there are responses, then show multi-option dialogue if there are
if dialogue.responses then
npc.dialogue.show_options_dialogue(
self,
dialogue_key,
dialogue,
npc.dialogue.NEGATIVE_ANSWER_LABEL,
player_name
)
end
-- Dialogue object processed successfully
return true
end
function npc.dialogue.create_option_dialogue(prompt, options, actions)
local result = {}
result.text = prompt
result.responses = {}
for i = 1, #options do
table.insert(result.responses, {text = options[i], action_type="function", action=actions[i]})
end
return result
end
-----------------------------------------------------------------------------
-- Functions for rotating NPC to look at player
-- (taken from the mobs_redo API)
-----------------------------------------------------------------------------
local atan = function(x)
if x ~= x then
return 0
else
return math.atan(x)
end
end
function npc.dialogue.rotate_npc_to_player(self)
local s = self.object:getpos()
local objs = minetest.get_objects_inside_radius(s, 4)
local lp = nil
local yaw = 0
for n = 1, #objs do
if objs[n]:is_player() then
lp = objs[n]:getpos()
break
end
end
if lp then
local vec = {
x = lp.x - s.x,
y = lp.y - s.y,
z = lp.z - s.z
}
yaw = (atan(vec.z / vec.x) + math.pi / 2) - self.rotate
if lp.x > s.x then
yaw = yaw + math.pi
end
end
self.object:setyaw(yaw)
end
---------------------------------------------------------------------------------------
-- Answer processing functions
---------------------------------------------------------------------------------------
-- This function locates a response object that has function on the dialogue tree.
local function get_response_object_by_id_recursive(dialogue, current_depth, response_id)
if dialogue.responses == nil
and (dialogue.action_type == "dialogue" and dialoge.action.responses == nil) then
return nil
elseif dialogue.responses ~= nil then
-- Get current depth and response ID
local d_i1, d_i2 = string.find(response_id, ":")
--minetest.log("N1: "..dump(string.sub(response_id, 0, d_i1))..", N2: "..dump(string.sub(response_id, 1, d_i1-1)))
local depth = tonumber(string.sub(response_id, 0, d_i1-1))
local id = tonumber(string.sub(response_id, d_i2 + 1))
--minetest.log("Depth: "..dump(depth)..", id: "..dump(id))
-- Check each response
for key,value in ipairs(dialogue.responses) do
--minetest.log("Key: "..dump(key)..", value: "..dump(value)..", comp1: "..dump(current_depth == depth))
if value.action_type == "function" then
-- Check if we are on correct response and correct depth
if current_depth == depth then
if key == id then
return value
end
end
else
--minetest.log("Entering again...")
-- We have a dialogue action type. Need to check if dialogue has further responses
if value.action.responses ~= nil then
local response = get_response_object_by_id_recursive(value.action, current_depth + 1, response_id)
if response ~= nil then
return response
end
end
end
end
end
end
-- Handler for dialogue formspec
minetest.register_on_player_receive_fields(function (player, formname, fields)
-- Additional checks for other forms should be handled here
-- Handle yes/no dialogue
if formname == "advanced_npc:yes_no" then
local player_name = player:get_player_name()
if fields then
local player_response = npc.dialogue.dialogue_results.yes_no_dialogue[player_name]
-- Unlock queue, reset action timer and unfreeze NPC.
npc.exec.set_ready_state(player_response.npc)
if fields.yes_option then
player_response.yes_callback()
elseif fields.no_option then
player_response.no_callback()
end
end
end
-- Manage options dialogue
if formname == "advanced_npc:options" then
local player_name = player:get_player_name()
if fields then
-- Get player response
local player_response = npc.dialogue.dialogue_results.options_dialogue[player_name]
-- Check if the player hit the negative option or esc button
if fields["exit"] or fields["quit"] == "true" then
-- Unlock queue, reset action timer and unfreeze NPC.
npc.exec.set_ready_state(player_response.npc)
end
for i = 1, #player_response.options do
local button_label = "opt"..tostring(i)
if fields[button_label] then
if player_response.options[i].action_type == "dialogue" then
-- Process dialogue object
npc.dialogue.process_dialogue(player_response.npc,
player_response.options[i].action,
player_name)
elseif player_response.options[i].action_type == "function" then
-- Execute function - get it directly from definition
-- Find NPC relationship phase with player
local phase =
npc.relationships.get_relationship_phase(player_response.npc, player_name)
-- Check if NPC is married and the married NPC dialogue should be shown
if phase == "phase6" and player_response.is_married_dialogue == true then
-- Get the function definitions from the married dialogue
npc.relationships.MARRIED_NPC_DIALOGUE
.responses[player_response.options[i].response_id]
.action(player_response.npc, player)
elseif player_response.is_custom_trade_dialogue == true then
-- Functions for a custom trade should be available from the same dialogue
-- object as they are created on demand
minetest.log("Player response: "..dump(player_response.options[i]))
player_response.options[i].action(player_response.npc, player)
else
-- Get dialogue from registered dialogues
local dialogue = npc.dialogue.registered_dialogues[player_response.options[i].dialogue_id]
local response = get_response_object_by_id_recursive(dialogue, 0, player_response.options[i].response_id)
-- Execute function
response.action(player_response.npc, player)
-- Unlock queue, reset action timer and unfreeze NPC.
npc.exec.set_ready_state(player_response.npc)
end
end
return
end
end
end
end
end)

View File

@ -1,598 +0,0 @@
Advanced_NPC API Reference Alpha-2 (DEV)
=========================================
* More information at <https://github.com/hkzorman/advanced_npc/wiki>
IMPORTANT: This WIP & unfinished file contains the definitions of current advanced_npc functions
(Some documentation is lacking, so please bear in mind that this WIP file is just to enhance it)
Summary
-------
* Introduction
* Initialize NPC
* NPC Steps
* Programs
* Schedules
* Occupations
* Locations
* Dialogues
* Definition tables
Introduction
------------
You can consult this document for help on API of behaviors for the NPCs.
The goal is to be able to have NPCs that have the same functionality as normal players.
The NPCs make Sokomine's mg_villages in Minetest alive although they can
be manually spawned outside the village and work as good as new.
Here is some information about the API methods and systems.
* npc.lua also uses methods and functions from the dependency: mobs_redo <https://github.com/tenplus1/mobs_redo>
Initialize NPC
--------------
The API works with some variables into Lua Entity that represent a NPC,
then you should initialize the Lua Entity before that it really assume
a controled behavior.
### Methods
* `npc.initialize(entity, pos, is_lua_entity, npc_stats, occupation_name)` : Initialize a NPC
The simplest way to start a mob (of mobs_redo API) is by using the `on_spawn` function
Note: currently this call is unduly repeated (mobs_redo problem), so you should check if npc has already been initialized.
on_spawn = function(self)
if self.initialized == nil then
npc.initialize(self, self.object:getpos(), true)
self.tamed = false
end
end
Or after add in the world
local obj = minetest.add_entity({x=0, y=10, z=0}, "mobs:sheep", {naked = true})
local luaentity = get_luaentity(obj)
npc.initialize(luaentity, luaentity.object:getpos(), true)
luaentity.tamed = false
NPC Steps
---------
The API works with NPC steps, then `on_step` callback need run the
`npc.on_step(luaentity)`. This function process the NPC actions
and return the freeze state, which is used for stop mobs_redo behavior.
Example:
on_step = function(self, dtime)
npc.step(self, dtime)
end
Mobs of Mobs_Redo API uses `do_custom` function instead of `on_step` callback
and it needs return the freeze state to stop mobs_redo behavior.
Here is a recommended code.
do_custom = function(self, dtime)
-- Here is my "do_custom" code
-- Process the NPC action and return freeze state
return npc.step(self, dtime)
end
Execution API
-------------
The API follows a simple OS-based model where tasks performed by NPCs are encapsulated
in the concepts of `instructions` and `programs`. `Instructions` are "small", "atomic"
actions performed by a NPC (like rotating, standing, etc.) and `programs` are a
"collection" of instructions with logic on what to execute and what not (for example,
walking to a specific position). The NPC executes different programs in order to be
able to perform tasks (e.g. going to sleep on a bed).
The execution environment of Advanced NPC is based on processes, which are instances
of a program. Processes have an internal instruction queue and execution context for
storing variables; they can be interrupted and their state upon interruption is
stored for later restoration. Processes can also be enqueued into a process queue
which is managed by a process scheduler (which runs roughly each second). The process
scheduler has the responsibility of determining what is the next process to be executed.
### State processes
A very important concept introduced by the execution environment are `state processes`.
A state process is used to determine the actions of a NPC on a given state. The usual
examples for states are:
- idle
- wandering
- following an object
- attacking
All of the above `states` are actions that have similar properties:
- Triggered by a particular action, e.g. NPC is punched (attack state) or NPC is sleeping (idle state)
- Executed constantly until a particular goal is reached or more important action takes place
Therefore, a `state process` is a special type of process that is executed constantly while
the process queue is empty.
### Operation principle
Note: The information in this subtopic should not be considered for external development,
only for knowledge about the principle of internal operation.
A process is an instance of a program, with the following attributes:
* A state, which can be any of:
* inactive: process that was just enqueued
* executing: process' Lua function is being executed and not finished yet
* running: process finished execution, and may or may not have instructions on its queue
* paused: interrupted process
* ready: process was interrupted, and then restored, it is ready to run again
* waiting_user_input: happens when on_rightclick interaction occurs
* An instruction_queue, where instructions are enqueued and executed over time.
In terms of OS, think of this as some kind of program counter
* An execution_context, which is the data space of the process.
The execution context is a map of key-value pairs,
supporting read-only values (can't be updated again).
* An interrupted_process, in case that this process interrupted a previous one,
so that it can restored exactly as it was
* An instruction state, where the current instruction being executed is stored as well as its state
(so it can be re-executed in case process is interrupted)
The process definition is in private `_exec.create_process_entry()` function. This is like this so a process
is always complete and ensured to have all its attributes. The proper way to create a process entry and enqueue
it into the NPC's process queue is by using the `npc.exec.enqueue_program()`.
The process definition (as Lua table) is the following:
{
id = process_id,
program_name = program_name,
arguments = arguments,
state = npc.exec.proc.state.INACTIVE,
execution_context = {
data = {},
instr_interval = 1,
instr_timer = 0
},
instruction_queue = {},
current_instruction = {
entry = {},
state = npc.exec.proc.instr.state.INACTIVE,
pos = {}
},
interrupt_options = npc.exec.create_interrupt_options(interrupt_options),
interrupted_process = {},
is_state_process = is_state_program
}
The state process have an additional attribute and is_state_process is set to true:
state_process_id = os.time()
### Writing and registering programs
Programs are just a Lua function.
Many examples of programs can be found on the code, but the following
are some general tips to keep in mind while writing programs
* If you are doing anything that needs to be done in the future
(example, walking and then checking a node), run the initial instruction and enqueue
the rest.
* The correct way to run a program from a program is to use `advanced_npc:interrupt`
* If you need to evaluate any value in the future (example, after movement), store
it in a process variable (see the `npc.exec.var*` functions)
* You can use instruction recursion to do loops.
* If you are writing any state program, do not make it loop. It will loop for free
(scheduler will execute again and again, so your variables are not lost)
* And finally, if your process is simple, don't enqueue any instruction unless you
want to have a certain pause between instruction execution for visual reasons
(e.g NPC sitting to laying, everythig executed quickly will not look nice)
### Permanent storage functionality
Permanent storage functionality - create, read, update and delete variables
in the NPC's permnanent storage.
IMPORTANT: These variables are *NOT* deleted. Be careful what you store on it or
the NPC object can grow in size very quickly.
For temporary storage, use `npc.exec.var.*` functions.
#### Methods
* `npc.data.put(luaentity, key_name, value, readonly)`: This function adds a value to the permanent data storage in the Lua entity
* Readonly defaults to false.
* Returns false if failed due to key_name conflict, or returns true if successful.
* `npc.data.get(luaentity, key_name)`: Returns the value of a given key. If not found returns nil
* `npc.data.set(luaentity, key_name, new_value)`: This function updates a value in the permanent data storage
* Returns false if the value is read-only or if key isn't found.
* Returns true if able to update value.
* `npc.data.remove(luaentity, key_name)`: This function removes a value in the permanent data storage in the Lua entity
* If the key doesn't exist, returns nil, otherwise, returns the value removed.
### Variable functionality
Variable functionality - create, read, update and delete variables in the
current process.
IMPORTANT: These variables are deleted when the process is finished execution.
For permanent storage, use `npc.data.*` functions.
#### Methods
* `npc.exec.var.put(luaentity, key_name, value, readonly)`: Put a value to the execution context of the current process
* Readonly defaults to false
* Returns false if failed due to key_name conflict, or returns true if successful
* `npc.exec.var.get(luaentity, key_name)`: Returns the value of a given key
* If not found returns nil
* `npc.exec.var.set(luaentity, key_name, new_value)`: Update a value in the execution context
* Returns false if the value is read-only or if key isn't found
* Returns true if able to update value
* `npc.exec.var.remove(luaentity, key_name)`: Remove a variable from the execution context
* If the key doesn't exist, returns nil, otherwise, returns the value removed
### Methods
* `npc.programs.register(program_name, func)`: Register a program
* `npc.programs.is_registered(program_name)`: Check if a program exists
* `npc.programs.execute(luaentity, program_name, {program arguments})`: Execute a program for a NPC
* `npc.programs.instr.register(name, func)`: Register a instruction
* `npc.programs.instr.execute(self, name, args)`: Execute a instruction for a NPC
* `npc.exec.enqueue_program(luaentity, program_name, {program arguments}, interrupt_options, is_state_program)`: Add program to schedule queue
* `npc.exec.proc.enqueue(luaentity, instruction_name, {instruction arguments})`: Add instruction to process queue
* `npc.exec.var.put(luaentity, key_name, value, readonly)`: Put a value to the execution context of the current process
* `npc.exec.var.get(luaentity, key_name)`: Returns the value of a given key_name
* `npc.exec.var.set(luaentity, key_name, new_value)`: Update a value in the execution context
* `npc.exec.var.remove(luaentity, key_name)`: Remove a variable from the execution context
* `npc.data.put(luaentity, key_name, value, readonly)`: This function adds a value to the permanent data storage in the Lua entity
* `npc.data.get(luaentity, key_name)`: Returns the value of a given key
* `npc.data.set(luaentity, key_name, new_value)`: This function updates a value in the permanent data storage in the Lua entity
* `npc.data.remove(luaentity, key_name)`: This function removes a value from the permanent data storage in the Lua entity
Example 1
npc.programs.execute(self, "advanced_npc:walk_to_pos", {
end_pos = {x=0,y=0,z=0},
walkable = {}
})
See more about different programs and his arguments in [programs.md](programs.md) documentation.
Example 2
-- 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_instruction_interval", {interval = wait_time - 1})
npc.exec.proc.enqueue(self, "advanced_npc:set_instruction_interval", {interval = 1})
end)
See more about different instructions and his arguments in [instructions.md](instructions.md) documentation.
### Monitoring API
To complete the OS/microprocessor analogy, the Execution API has a sub-API for registering
timers and callbacks of certain events. This API is called "monitor" API because its main
purpose is to be able to keep track of actions that the NPC performs and act according to this
data. The key concept behind the Monitoring API is to be able to introduce some concepts of
artificial intelligence into the Advanced NPC programs.
#### Timers
Timers can be registered (globally on the `npc.*` namespace) and then added to a NPC for
execution. To register a timer, use:
`npc.monitor.timer.register(name, interval, callback)`
where:
- `name` is a unique name for the timer. Recommended naming convention to use: `<modname>:<related_program_name>:<timer_name>`
- `interval`: the default interval, this can be overriden
- `callback`: a Lua function that is called with `self` (the NPC Lua entity) and a Lua table `args` for arguments
To run a timer, a new instance is created for the particular NPC that will use the timer
and then it is executed internally. The following function is used to start a timer:
`npc.monitor.timer.start(self, name, interval, args)`
where:
- `name` is the unique name of the timer
- `interval` optional, interval for the timer (if nil, uses the default interval)
- `args` a Lua table of arguments for the timer callback
To stop a timer, simply use:
`npc.monitor.timer.stop(self, name)`
##### A word of caution with timers:
While timers can be very useful, they can also be very disruptive, specially if they are
changing state process. Therefore, every timer `callback` function *should* have a condition
check at the very beginning before anything else runs. This way, if the condition for the timer
is no longer valid, it stops and doesn't interferes with other processes running.
#### Callbacks
Callbacks are functions executed whenever another action is executed. All callbacks
execute *after* the actual action. Currently, there are three types of callbacks supported:
- Program callback: executed whenever a program is executed
- Instruction callback: executed whenever a instruction is executed
- Interaction callback: executed whenever a interaction occurred, which are:
- on punch,
- on right-click
- on schedule
Callbacks are categorized in terms of `type` (mentioned above) and `subtype`. For programs and
instruction callbacks, the `subtype` is the program or instruction name.
For interaction callbacks, the subtypes are predetermined (as shown above).
To register a callback, use:
`npc.monitor.callback.register(name, type, subtype, callback)`
where:
- `name` is a unique name for the callback
- `type` is one of the three callback types (defined in `npc.monitor.callback.type`),
- `subtype` is an arbitrary string that denotes the program or instruction name for `program` and `instruction` callbacks respectively, or one of the three subtypes (defined in `npc.monitor.callback.subtype`) as mentioned above for `interaction` callbacks
- `callback` is the function to be executed. The only argument of this function is `self` (the NPC Lua entity)
To execute a callback, use:
`npc.monitor.callback.enqueue(self, type, subtype, name)`
Or to enqueue all callbacks for a specific `type` and `subtype`, do:
`npc.monitor.callback.enqueue_all(self, type, subtype)`
Schedules
---------
The interesting part of Advanced NPC is its ability to simulate realistic
behavior in NPCs. Realistic behavior is defined simply as being able to
perform tasks/programs at a certain time of the day, like usually people do.
This allow the NPC to go to bed, sleep, get up from it, sit in benches, etc.
All of this is simulated through a structured code using programs for action
and tasks.
The implementation resembles a rough OS process scheduling algorithm where
only one process is allowed at a time. The processes or tasks are held in
a queue, where they are executed one at a time in queue fashion.
Interruptions are allowed, and the interrupted action is re-started once
the interruption is finished.
### Schedule time
Only integer value 0 until 23
* 0: 0/24000 - 999
* 1: 1000 - 1999
* 2: 2000 - 2999
* ...
* 22: 22000 - 22999
* 23: 23000 - 23999
### Schedule Type
* "generic" : Returns nil if there are already seven schedules, one for each
day of the week or if the schedule attempting to add already exists.
The date parameter is the day of the week it represents as follows:
Note: Currently only one schedule is supported, for day 0
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
### Methods
* `npc.schedule.create(luaentity, schedule_type, day)` : Create a schedule for a NPC
* `npc.schedule.delete(luaentity, schedule_type, date)` : Delete a schedule for a NPC
* `npc.schedule.entry.put(luaentity, schedule_type, date, time, check, commands)` : Add a schedule entry for a time
* `npc.schedule.entry.get(luaentity, schedule_type, date, time)` : Get a schedule entry
* `npc.schedule.entry.set(luaentity, schedule_type, date, time, check, commands)` : Update a schedule entry
### Examples
-- Schedule entry for 7 in the morning
npc.schedule.entry.put(self, "generic", 0, 7, nil, {
-- Get out of bed
[1] = {
program_name = "schedules:default:wake_up",
arguments = {},
interrupt_options = {}
},
-- Walk to home inside
[2] = {
program_name = "advanced_npc:walk_to_pos",
arguments = {
end_pos = npc.locations.PLACE_TYPE.OTHER.HOME_INSIDE,
walkable = {}
},
interrupt_options = {},
},
})
Occupations
-----------
NPCs need an occupation or job in order to simulate being alive.
This functionality is built on top of the schedules functionality.
Occupations are essentially specific schedules, that can have slight
random variations to provide diversity and make specific occupations
less predictable. Occupations are associated with textures, dialogues,
specific initial items, type of building (and surroundings) where NPC
lives, etc.
### Methods
* `npc.occupations.register_occupation(occupation_name, {occupation definition})` : Register an occupation
* `npc.occupations.initialize_occupation_values(luaentity, occupation_name)` : Initialize an occupation for a NPC
Locations
----------
Locations define which NPCs can access which places and are separated into different types.
### Locations types
Current location types
* `bed_primary` : the bed of a NPC
* `sit_primary`
* `sit_shared`
* `furnace_primary`
* `furnace_shared`
* `storage_primary`
* `storage_shared`
* `home_entrance_door`
* `schedule_target_pos` : used in the schedule actions
* `calculated_target_pos`
* `workplace_primary`
* `workplace_tool`
* `home_plotmarker`
* `home_inside`
* `home_outside`
### Methods
* `npc.locations.add_owned(luaentity, place_name, place_type, pos, access_pos)` : Add owned place.
`luaentity` npc owner.
`place_name` a specific place name.
`place_type` place typing.
`pos` is a position of a node to be owned.
`access_pos` is the coordinate where npc must be to initiate the access.
Location is added for the NPC.
* `npc.locations.add_shared(luaentity, place_name, place_type, pos, access_node)` : Add shared place
Dialogues
---------
Dialogs can be registered to be spoken by NPCs.
### Tags
The flags or marks of the dialogue text. Tags can be used for ....
* "unisex" : Both male and female NPCs can say the defined text.
* "phase1" : NPCs in phase 1 of a relationship can say the defined text.
### Methods
* `set_response_ids_recursively()` : A local function that assigns unique
key IDs to dialogue responses.
* `npc.dialogue.register_dialogue({dialogue definition})` : Defines and
registers dialogues.
* `npc.dialogue.search_dialogue_by_tags({search_tags})` : A method returning
a Lua table of dialogues if called.
Definition tables
-----------------
### Program definition (Programs)
{
program_name = "modname:program1", -- Programs name
arguments = {program arguments}, -- Lua table of arguments for the program
is_state_program = true, --[[
^ [OPTIONAL]
^ If this is true, then this program will be
repeated while there is no next program]]
interrupt_options = {} --[[
^ [OPTIONAL]
^ Is a Lua table that defines what kind of interaction can interrupt the process
when it is running. The "interruption" is not a literal process pause.
It means that the defined interactions can happe while the process is running.
In that fashion, for example, if the NPC is sleeping, talking (right click interaction)
to the NPC can be disabled.
^ The three supported interaction types are defined below.
They are all optional and accept values of true or false
* allow_punch: if enabled, the entity's on_punch() function is executed.
* allow_rightclick: if enabled, when the rightclick of the entitiy is called,
the process is put on waiting_user_input state and entity's on_rightclick() executed
* allow_schedule: enables or disables schedule entries. If disabled, schedule will not run.]]
depends = {}, --[[
^ [OPTIONAL]
^ is an array of numbers, where each number represents an index in the array
of schedule entries for that time.
^ Is a schedule entry concept. For a certain time, an array of programs
is enqueued when the scheduled time arrives. The programs are enqueued
in the order they are given in the array. If a program have a chance argument,
it means that it could or couldn't happen. Therefore, some programs may or
may not run, hence the depends.
chance = <number>, --[[
^ [OPTIONAL]
^ chance x in 100 of this program be executed
}
### Occupation definition (`register_occupation`)
{
dialogues = {
enable_gift_item_dialogues = true, --[[
^ This flag enables/disables gift item dialogues.
^ If not set, it defaults to true. ]]
type = "", -- The type can be "given", "mix" or "tags"
data = {}, --[[
^ Array of dialogue definitions. This will have dialogue
if the type is either "mix" or "given" ]]
tags = {}, --[[
^ Array of tags to search for. This will have tags
if the type is either "mix" or "tags" ]]
},
textures = {}, --[[
^ Textures are an array of textures, as usually given on
an entity definition. If given, the NPC will be guaranteed
to have one of the given textures. Also, ensure they have sex
as well in the filename so they can be chosen appropriately.
^ If left empty, it can spawn with any texture. ]]
walkable_nodes = {}, -- Walkable nodes
building_types = {}, --[[
^ An array of string where each string is the type of building
where the NPC can spawn with this occupation.
^ Example: building_type = {"farm", "house"}
^ If left empty or nil, NPC can spawn in any building ]]
surrounding_building_types = {}, --[[
^ An array of string where each string is the type of building
that is an immediate neighbor of the NPC's home which can also
be suitable for this occupation. Example, if NPC is farmer and
spawns on house, then it has to be because there is a field
nearby.
^ If left empty or nil, surrounding buildings doesn't matter. ]]
workplace_nodes = {}, --[[
^ An array of string where each string is a node the NPC works with.
^ These are useful for assigning workplaces and work work nodes. ]]
initial_inventory = {}, --[[
^ An array of entries like the following:
{name="", count=1} -- or
{name="", random=true, min=1, max=10}
^ This will initialize the inventory for the NPC with the given
items and the specified count, or, a count between min and max
when the entry contains random=true
^ If left empty, it will initialize with random items. ]]
initial_trader_status = "", --[[
^ String that specifies initial trader value.
^ Valid values are: "casual", "trader", "none" ]]
schedules_entries = {},
^ This is a Lua table of schedules where the index is a schedule time:
{
[<schedule time>] = {
[1] = {program definition},
[2] = {program definition},
...
},
[<schedule time>] = {
[1] = {program definition},
[2] = {program definition},
...
},
...
}
}
### Dialogue definition (`register_dialogue`)
{
text = "Hello.", --[[
^ The dialogue text itself.
^ It must be included in the method.]]
tags = {"tag1", "tag2"} --[[
^ The flags or marks of the dialogue text.
^ The object can be excluded. ]]
}
Examples:
Syntax example 1:
npc.dialogue.register_dialogue({
text = "Hello.", -- "Hello." will be said by the NPC upon rightclick and displayed in the messages section.
tags = {"unisex", "phase1"} -- The flags that define the conditions of who and what can say the text.
})
Syntax example 2:
npc.dialogue.register_dialogue({
text = "Hello again."
-- The tags object is excluded, meaning that any NPC can say "Hello again." upon rightclick under no condition.
})

View File

@ -1,19 +0,0 @@
Instructions for Programs
Advanced_NPC Alpha-2 (DEV)
==========================
IMPORTANT: In this documentation is only the explanation of the particular
operation of each predefined instructions. Read reference documentation
for details about API operation at [api.md](api.md).
### Default Instructions
These instructions are already registered in the API.
This section describes these instructions and their respective arguments.
#### `WAIT` (advanced_npc:wait)
This instruction causes the object to wait stopped for a time.
In other words, syntacic sugar to make a process wait for a specific interval.
{
time = <number>, -- Time number in seconds
}

View File

@ -1,90 +0,0 @@
Advanced NPC 1.0 proposal
-------------------------
While Advanced NPC provides functionality and a level of intelligence that no other mob mod can, it is still limited in some features and to its ultimate purpose of creating functional towns and/or simulated communities. The following are the areas that has been identified as lacking:
- Idle/wandering
- When NPCs aren't executing actions, their movement is very dumb. They wander aimlessly, constantly and usually bump into obstacles and keep walking nevertheless. They get stuck at places they shouldn't.
- Relationships
- Relationships are very hardcoded, and there's no flexibility on them.
- Unable to add more functionality
- All actions are hardcoded. While the essentials are in, making a NPC operate another node that is not a furnace/chest/door is almost impossible. If a mod adds a node and wants NPC to be able to operate it, it is certainly very hard.
- More randomness in schedules
- While schedules are all about making NPCs do actions at certain times, it is not flexible enough to make it look more realistic. One morning a NPC can get up and make breakfast or not, put some music on a music player or not, go outside their home and wander around, etc.
- Unable to react to certain triggers
- When NPCs are punched, `mobs_redo` takes over and controls the NPC. Also, NPCs are unable to scan an area for certain things and perform actions continually based on it.
The above are all playability issues and deficiencies. Some technical issues has to be addressed as well regarding the API. Given all these, the following is a proposal to move the mod towards the correct direction.
##Proposed changes:
- Unify the actions/tasks/schedule property change/schedule query API into a `commands` API
- Add new commands to bring the NPC interaction level closer to that of a player
- Rename `flags` to `properties`
- Allow registering scripts, or collections of commands for external mods to provide extra functionality
Unified Commands API
--------------------
The goal of this API is to provide consistency and extensibility to the actions a NPC can perform. First of all, rename actions/tasks/property change/query to `commands`. Each command will have the following properties that determines how it is to be executed and what it does:
- Type: specifies the type of command. The following are valid types:
- `instruction`: Used for fundamental, atomic operations. This type maps directly to what are called now `actions`, which are for example, walk one step, dig, place, etc.
- `control`: Used for specific commands that are flow control statements. Example: If-else, for loops. The conditional statements is a Lua boolean expression.
- `script`: Used for collections of commands, executed on a sequential structure. This type maps directly to `tasks`.
- Execution: specifies how the command is to be executed. The following are valid valuesf for this parameter:
- `immediate`: Will execute this command immediately, without any enqueing. Very little commands should be able to do this. The `control` commands should be executed immediately as they need to enqueue certain commands depending on their conditions.
- `default`: Command will be enqueued and executed on the global command timer call.
- Interruptable: specifies whether the global command timer and/or the scheduler can interrupt the command. Boolean value, can be set to false or true.
- _Important_: Non-interruptable commands should be able to finish by themselves. The API will execute the default command once a non-interruptable command is done and if it doesn't executes another command.
- Parameters: a Lua table with all the parameters that the command requires. Depending on the type, some parameters are required. Below is a list of required parameters per type:
- `instruction`: Requires just the parameters required by the instruction to execute.
- `control`: Requires different parameters depending on the type of control.
- Required:
- `condition`: The condition to be evaluated. This is a Lua boolean expression.
- `match_commands`: A Lua array with the commands to be executed if condition evaluates to `true`.
- Dependent on type:
- `operation`: Only required in `for-loop` command. Operation to execute on the loop variable (e.g. increase/decrease)
- `repetition`: Optional for `for-loop` command. Can't be used together with `max` and `min`.
- `max`: Optional for `for-loop` command. Can't be used together with `repetition`. Requires `min`. Randomizes a loop execution and sets the upper bound of how many times the loop will execute.
- `min`: Optional for `for-loop` command. Can't be used together with `repetition`. Requires `max`. Randomizes a loop execution and sets the lower bound of how many times the loop will execute.
- `else_commands`: Only required in `if-else` command. A Lua array with the commands to be executed if condition evaluates to `false`.
- `script`: A Lua array of commands to execute, in order
The following `instruction` commands will be added to the default set:
- `do_punch`: Executes the `on_punch` function of a node, object or player
- `do_rightclick`: Executes the `on_rightclick` function of a node, object or player
- `set_property`: Sets the value of a variable in the `self.properties` object. If the variable doesn't exists, it is created. This command is executed immediately and is not enqueued.
- Parameters:
- `key`: The property key-name. This is a variable in the `self.properties` object
- `value`: The property value.
- `get_property`: Returns the value of a given property. This command is executed immediately and is not enqueued.
- Parameters:
- `key`: The property key-name.
- `set_internal_property`: Sets the value of a limited set of internal properties related to the NPC trading and personality variables.
- `get_internal_property`: Gets the value of a limited set of internal properties related to the NPC trading and personality variables.
- `add_item_to_npc`: Adds an item to the NPC inventory, without any specific source.
- `remove_item_from_npc`: Removes a specific item from the NPC inventory.
- `query`: Executes a query for nodes or objects. Returns a Lua table with none, single or many positions.
The following `control` commands will be added to the default set:
- `if-else`: An if-else control statement that will execute immediately. It will evaluate the given `condition` parameter and execute commands depending on the evaluation of the `condition`.
- Parameters:
- `condition`: A Lua boolean expression to be evaluated.
- `true-commands`: A Lua array of commands to be executed if `condition` evaluates to `true`.
- `else-commands`: A Lua array of commands to be executed if `condition` evaluates to `false`.
- `loop`: A flexible loop command. Supports for-loop and while-loops. The amount of loops done will be available in `npc.commands.current_loop_count`. Executes immediately, it is not enqueued.
- Parameters:
##Extensibility
Once the above commands has been added, it is possible to safely build scripts which don't touch directly many of the internal NPC mechanisms. An API will be provided for external mods to register scripts that let NPCs perform actions related to those mods, e.g. operating a node provided by the mod. The API for this will be:
`npc.commands.register_script(name, script)`
All registered scripts have the following properties:
- They are interruptable by the command queue/scheduler
- They are not immediately executed
The `script` parameter is a Lua array of commands that will be executed when the script is executed.

View File

@ -1,148 +0,0 @@
Programs for Actions and Tasks
Advanced_NPC Alpha-2 (DEV)
==========================
IMPORTANT: In this documentation is only the explanation of the particular
operation of each predefined action and task programs. Read reference documentation
for details about API operation at [api.md](api.md).
### Default Programs
These programs are already registered in the API.
This section describes these programs and their respective arguments.
#### `IDLE` (advanced_npc:idle)
This program meant to be run when NPC are doing nothing and standing idle.
Idle program doesn't loops, it is meant to be executed as a state program
(which is scheduled continously as long as the process queue is empty)
It has two main features (as-of the moment, more planned):
{
acknowledge_nearby_objs = true, --[[
^ Acknowledge nearby objects by looking at them,
with configurable object search interval and radius]]
wander_chance = 0, --[[
^ Trigger wandering with configurable chance (1-100 chance of wander/0 for never)
and radius (how many nodes to wander from starting point)]]
}
#### `USE BED` (advanced_npc:use_bed)
Sequence of actions that allows the NPC to use a bed.
{
pos = {x=0,y=0,z=0}, --[[
^ Position of bed to be used.
^ Can be a coordinate x,y,z.
^ Can be a place name of the NPC place map.
Example: "bed_primary" ]]
action = action, --[[
^ Whether to get up or lay on bed
^ Defined in npc.commands.const.beds.action
^ Available options:
* npc.commands.const.beds.LAY : lay
* npc.commands.const.beds.GET_UP : get up
}
#### `WALK TO POS` (advanced_npc:walk_to_pos)
NPC will walk to the given position. This task uses the pathfinder to calculate the nodes
in the path that the NPC will walk through, then enqueues walk_step actions, combined with
correct directional rotations and opening/closing of doors on the path.
{
end_pos = {x=0,y=0,z=0}, --[[
^ Destination position to reach.
^ Can be a coordinate x,y,z.
^ Can be a place name of the NPC place map.
The position must be walkable for the npc to stop in,
or in the access position of the place.
Example: "home_inside" ]]
walkable = {}, --[[
^ An array of node names to consider as walkable nodes
for finding the path to the destination. ]]
use_access_node = true, --[[
^ Boolean, if true, when using places, it will find path
to the "accessible" node (empty or walkable node around
the target node) instead of to the target node.
^ Default is true. ]]
enforce_move = true, --[[
^ Boolean, if true and no path is found from the NPC's
position to the end_pos, the NPC will be teleported
to the destination (or, if use_access_node == true it will
teleport to the access position)
^ Default is true. ]]
}
#### `INTERNAL PROPERTY CHANGE` (advanced_npc:internal_property_change)
Changes the value of an internal property of a NPC Lua entity.
{
property = <string>, --[[
^ Property type
^ Property types:
"flag" for flags save in `flags` Lua table for in Lua entity]]
args = {
action = <string>, --[[
^ Type change action
^ Change types:
"set" for set the value
"reset" for reset the value 0 for number, false for boolean and "" for strings]]
flag_name = <string>, -- Flag name
flag_value = <value>, -- New flag value
}
}
#### `NODE QUERY` (advanced_npc:node_query)
Check and run a program with nodes found near.
{
range = 2, -- Range of checked area in blocks.
count = 20, -- How many checks will be performed.
random_execution_times = true, --[[
^ Randomizes the number of checks that will be performed.
^ min_count and max_count is required ]]
min_count = 20, -- minimum of checks
max_count = 25, -- maximum of checks
nodes = {"itemstring1", "itemstring2"}, --[[
^ Nodes to be found for the actions.
^ When a node is found, it is add in the npc place map
with the place name "schedule_target_pos"
prefer_last_acted_upon_node = true, -- If prefer to act on nodes already acted upon
walkable_nodes = {"itemstring1", "itemstring2"}, -- Walkable nodes
on_found_executables = { --[[
^ Table where index is a itemstring of the node to be found,
and value is an array of programs to be performed
when found the node. ]]
["itemstring1"] = {
[1] = <program>,
[2] = <program>,
[3] = <program>
},
["itemstring2"] = {
[1] = <program>,
[2] = <program>
}
},
on_not_found_executables = { --[[
^ An array of programs to be performed when not found any node.
[1] = <program>,
[2] = <program>
},
}

File diff suppressed because it is too large Load Diff

View File

@ -1,340 +0,0 @@
--
-- User: hfranqui
-- Date: 3/8/18
-- Time: 2:41 PM
--
npc.programs.const = {
dir_data = {
-- North
[0] = {
yaw = 0,
vel = {x=0, y=0, z=1}
},
-- East
[1] = {
yaw = (3 * math.pi) / 2,
vel = {x=1, y=0, z=0}
},
-- South
[2] = {
yaw = math.pi,
vel = {x=0, y=0, z=-1}
},
-- West
[3] = {
yaw = math.pi / 2,
vel = {x=-1, y=0, z=0}
},
-- North east
[4] = {
yaw = (7 * math.pi) / 4,
vel = {x=1, y=0, z=1}
},
-- North west
[5] = {
yaw = math.pi / 4,
vel = {x=-1, y=0, z=1}
},
-- South east
[6] = {
yaw = (5 * math.pi) / 4,
vel = {x=1, y=0, z=-1}
},
-- South west
[7] = {
yaw = (3 * math.pi) / 4,
vel = {x=-1, y=0, z=-1}
}
},
node_ops = {
doors = {
command = {
OPEN = 1,
CLOSE = 2
},
state = {
OPEN = 1,
CLOSED = 2
}
},
beds = {
LAY = 1,
GET_UP = 2
},
sittable = {
SIT = 1,
GET_UP = 2
}
},
speeds = {
one_nps_speed = 1,
one_half_nps_speed = 1.5,
two_nps_speed = 2
},
place_src = {
take_from_inventory = "take_from_inventory",
take_from_inventory_forced = "take_from_inventory_forced",
force_place = "force_place"
},
craft_src = {
take_from_inventory = "take_from_inventory",
take_from_inventory_forced = "take_from_inventory_forced",
force_craft = "force_craft"
}
}
npc.programs.helper = {}
-- Helper functions
-- This function returns the direction enum
-- for the moving from v1 to v2
function npc.programs.helper.get_direction(v1, v2)
local vector_dir = vector.direction(v1, v2)
local dir = vector.round(vector_dir)
if dir.x ~= 0 and dir.z ~= 0 then
if dir.x > 0 and dir.z > 0 then
return npc.direction.north_east
elseif dir.x > 0 and dir.z < 0 then
return npc.direction.south_east
elseif dir.x < 0 and dir.z > 0 then
return npc.direction.north_west
elseif dir.x < 0 and dir.z < 0 then
return npc.direction.south_west
end
elseif dir.x ~= 0 and dir.z == 0 then
if dir.x > 0 then
return npc.direction.east
else
return npc.direction.west
end
elseif dir.z ~= 0 and dir.x == 0 then
if dir.z > 0 then
return npc.direction.north
else
return npc.direction.south
end
end
end
-- This function allows to move into directions that are walkable. It
-- avoids fences and allows to move on plants.
-- This will make for nice wanderings, making the NPC move smartly instead
-- of just *oftenly* getting stuck at places... note that this will *NOT*
-- completely avoid the NPC being stuck
function npc.programs.helper.random_dir(start_pos, speed, dir_start, dir_end)
--
local bad_dirs = {}
--minetest.log("Args: "..dump(start_pos)..", "..dump(speed)..","..dump(dir_start)..", "..dump(dir_end))
-- Limit the number of tries - otherwise it could become an infinite loop
for i = 1, 8 do
local dir = math.random(dir_start, dir_end)
if (bad_dirs[dir] == false) then
-- Found dir that was known as bad, try dir + 1 until not
-- found or greater than dir_end
local good_found = false
for j = dir_start, dir_end do
dir = dir + 1
if bad_dirs[dir] == nil then
break
end
end
if good_found == false then
return -1
end
end
-- Find out if there are walkable nodes in the path ahead
local vel = vector.multiply(npc.programs.const.dir_data[dir].vel, speed)
local pos = vector.add(start_pos, vel)
local node_below = minetest.get_node(vector.round(pos))
local node_above = minetest.get_node(vector.round({x=pos.x,y=pos.y+1,z=pos.z}))
if node_below and node_above then
if npc.locations.is_walkable(node_below.name) and npc.locations.is_walkable(node_above.name) then
return dir
else
bad_dirs[dir] = false
end
end
end
-- Return -1 signaling that no good direction could be found
return -1
end
-- TODO: Refactor this function so that it uses a table to check
-- for doors instead of having separate logic for each door type
function npc.programs.helper.get_openable_node_state(node, pos, npc_dir)
--minetest.log("Node name: "..dump(node.name))
local state = npc.programs.const.node_ops.doors.state.CLOSED
-- Check for MTG doors and gates
local mtg_door_closed = false
if minetest.get_item_group(node.name, "door") > 0 then
local back_pos = vector.add(pos, minetest.facedir_to_dir(node.param2))
local back_node = minetest.get_node(back_pos)
if back_node.name == "air" or minetest.registered_nodes[back_node.name].walkable == false then
mtg_door_closed = true
end
end
-- Check for cottages gates
local open_i1, open_i2 = string.find(node.name, "_close")
-- Check for cottages half door
local half_door_is_closed = false
if node.name == "cottages:half_door" then
half_door_is_closed = (node.param2 + 2) % 4 == npc_dir
end
if mtg_door_closed == false and open_i1 == nil and half_door_is_closed == false then
state = npc.programs.const.node_ops.doors.state.OPEN
end
--minetest.log("Door state: "..dump(state))
return state
end
-- Can receive a position argument in different formats
-- TODO: Document formats
function npc.programs.helper.get_pos_argument(self, pos, use_access_node)
-- minetest.log("Type of pos: "..dump(type(pos)))
-- minetest.log("Pos: "..dump(pos))
-- Check which type of position argument we received
if type(pos) == "table" then
--minetest.log("Received table pos: "..dump(pos))
-- Check if table is position
if pos.x ~= nil and pos.y ~= nil and pos.z ~= nil then
-- Position received, return position
return pos
elseif pos.place_type ~= nil then
-- Received table in the following format:
-- {
-- place_category = "",
-- place_type = "",
-- index = 1,
-- use_access_node = false|true,
-- try_alternative_if_used = true|false
-- }
local index = pos.index or 1
local use_access_node = pos.use_access_node or false
local try_alternative_if_used = pos.try_alternative_if_used or false
local places = npc.locations.get_by_type(self, pos.place_type)
minetest.log("Place type: "..dump(pos.place_type))
minetest.log("Places: "..dump(places))
-- Check index is valid on the places map
if #places >= index then
local place = places[index]
-- Check if place is used, and if it is, find alternative if required
if try_alternative_if_used == true then
minetest.log("Self places map: "..dump(self.places_map))
minetest.log("Place category: "..dump(pos.place_category))
minetest.log("Place type: "..dump(pos.place_type))
minetest.log("Original Place: "..dump(place))
place = npc.locations.find_unused_place(self, pos.place_category, pos.place_type, place)
minetest.log("New place: "..dump(place))
if next(place) ~= nil then
--minetest.log("Mark as used? "..dump(pos.mark_target_as_used))
if pos.mark_target_as_used == true then
--minetest.log("Marking as used: "..minetest.pos_to_string(place.pos))
npc.locations.mark_place_used(place.pos, npc.locations.USE_STATE.USED)
end
npc.locations.add_shared_accessible_place(
self, {owner="", node_pos=place.pos}, npc.locations.data.calculated.target, true, {})
else
return nil
end
end
-- Check if access node is desired
if use_access_node == true then
-- Return actual node pos
return place.access_node, place.pos
else
-- Return node pos that allows access to node
return place.pos
end
end
end
elseif type(pos) == "string" then
--npc.log("INFO", "Places map: "..dump(self.places_map))
-- Received name of place, so we are going to look for the actual pos
local places_pos = npc.locations.get_by_type(self, pos, false)
--npc.log("INFO", "FOUND: "..dump(places_pos))
-- Return nil if no position found
if places_pos == nil or #places_pos == 0 then
return nil
end
-- Check if received more than one position
if #places_pos > 1 then
-- Check all places, return owned if existent, else return the first one
for i = 1, #places_pos do
if places_pos[i].status == "owned" then
if use_access_node == true then
return places_pos[i].access_node, places_pos[i].pos
else
return places_pos[i].pos
end
end
end
end
-- Return the first position only if it couldn't find an owned
-- place, or if it there is only one
if use_access_node == true then
return places_pos[1].access_node, places_pos[1].pos
else
return places_pos[1].pos
end
end
end
-- Helper function to determine if a NPC is moving
function npc.programs.helper.is_moving(self)
return math.abs(vector.length(self.object:getvelocity())) > 0
end
-- Leave this for now as it might be useful
--npc.commands.register_script("advanced_npc:optimized_walk_to_pos", function(self, args)
-- local start_pos = self.object:getpos()
-- local end_pos = args.end_pos
-- local walkable_nodes = args.walkable_nodes or {}
-- -- Optimized walking -- since distances can be really short,
-- -- a simple walk_step() action can do most of the times. For
-- -- this, however, we need to calculate direction
-- -- First of all, check distance
-- local distance = vector.distance(start_pos, end_pos)
-- if distance < 3 then
-- -- Will do walk_step based instead
-- if distance > 1 then
-- args = {
-- dir = npc.commands.get_direction(start_pos, end_pos),
-- speed = npc.programs.const.speeds.one_nps_speed
-- }
-- -- Enqueue walk step
-- npc.enqueue_command(self, npc.commands.cmd.WALK_STEP, args)
-- end
-- -- Add standing action to look at end_pos
-- npc.enqueue_command(self, npc.commands.cmd.STAND,
-- {dir = npc.commands.get_direction(self.object:getpos(), end_pos)}
-- )
-- else
-- -- Set proper use_access_node param
-- -- local use_access_node = true
-- -- if args.use_access_node ~= nil then
-- -- use_access_node = args.use_access_node
-- -- end
-- -- local new_args = {
-- -- end_pos = end_pos,
-- -- walkable = walkable_nodes,
-- -- use_access_node = use_access_node
-- -- }
-- -- -- Enqueue
-- -- npc.enqueue_script(self, "advanced_npc:walk_to_pos", new_args)
-- local walk_args = {
-- dir = npc.commands.get_direction(start_pos, end_pos),
-- speed = npc.commands.one_nps_speed
-- }
-- -- Enqueue walk step
-- npc.enqueue_command(self, npc.commands.cmd.WALK_STEP, walk_args)
-- end
--end)

View File

@ -1,39 +0,0 @@
--
-- Created by IntelliJ IDEA.
-- Date: 3/8/18
-- Time: 2:06 PM
--
-- Global namespace
npc.programs.instr = {
helper = {}
}
-- Private namespace
local _programs = {
instr = {
registered_instructions = {}
}
}
-- Registration function
function npc.programs.instr.register(name, func)
if _programs.instr.registered_instructions[name] ~= nil then
npc.log("ERROR", "Attempted to register instrcution with name: "..dump(name)..".\nInstruction already exists.")
return
end
_programs.instr.registered_instructions[name] = {func = func}
end
-- Execution function
function npc.programs.instr.execute(self, name, args)
if _programs.instr.registered_instructions[name] == nil then
npc.log("ERROR", "Attempted to execute instruction with name "..dump(name)..".\nInstruction doesn't exists.")
return
end
-- Enqueue callbacks if any
if npc.monitor.callback.exists(npc.monitor.callback.type.instruction, name) then
-- Enqueue all callbacks for this instruction
npc.monitor.callback.enqueue_all(self, npc.monitor.callback.type.instruction, name)
end
--npc.log("INFO", "Executing instruction '"..dump(name).."' with args:\n"..dump(args))
return _programs.instr.registered_instructions[name].func(self, args)
end

View File

@ -1,730 +0,0 @@
--
-- 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)

View File

@ -1,819 +0,0 @@
-- Places code for Advanced NPC by Zorman2000
---------------------------------------------------------------------------------------
-- Places functionality
---------------------------------------------------------------------------------------
-- In addition, the NPCs need to know where some places are, and know
-- where there are nodes they can use. For example, they need to know where the
-- chest they use is located, both to walk to it and to use it. They also need
-- to know where the farm they work is located, or where the bed they sleep is.
-- Other mods have to be supported for this to work correctly, as there are
-- many sitting nodes, many beds, many tables, chests, etc. For now, by default,
-- support for default MTG games and cottages mod is going to be provided.
-- Public API
npc.locations = {}
-- Function to register nodes
-- Categories are utilized to categorize nodes by type, example beds,
-- furnaces, etc. Sub-categories give more particular categorization to
-- beds in terms of how the NPC uses them
function npc.locations.register_node(node_name, category, sub_category)
-- Check category, if it doesn't exists, create it
if npc.locations.data.categories[category] == nil then
-- Create category name
npc.locations.data.categories[category] = category
npc.locations.data[category] = {}
end
-- Check sub-category and create it if necessary
if npc.locations.data[category][sub_category] == nil then
-- Create sub category
npc.locations.data[category][sub_category] = sub_category
end
-- Add node
npc.locations.nodes[category][#npc.locations.nodes[category] + 1] = node_name
end
-- Default set of registered nodes
npc.locations.nodes = {
bed = {
"beds:bed_bottom",
"beds:fancy_bed_bottom",
"cottages:bed_foot",
"cottages:straw_mat",
"cottages:sleeping_mat"
},
sittable = {
"cottages:bench",
-- currently commented out since some npcs
-- were sitting at stairs that are actually staircases
-- TODO: register other stair types
--"stairs:stair_wood"
},
storage = {
"default:chest",
"default:chest_locked",
"cottages:shelf"
},
furnace = {
"default:furnace",
"default:furnace_active"
},
openable = {
-- TODO: register fences
"doors:door_glass_a",
"doors:door_glass_b",
"doors:door_obsidian_a",
"doors:door_obsidian_b",
"doors:door_steel_a",
"doors:door_steel_b",
"doors:door_wood_a",
"doors:door_wood_b",
"cottages:gate_open",
"cottages:gate_closed",
"cottages:half_door"
},
plotmarker = {
"mg_villages:plotmarker",
"advanced_npc:plotmarker"
},
workplace = {
-- TODO: do we have an advanced_npc workplace?
"mg_villages:mob_workplace_marker",
"advanced_npc:workplace_marker"
}
}
-- Spawner function assign_places can also take a generic categories, and specially
-- process the hardcoded ones.
npc.locations.data = {
categories = {
bed = "bed",
sittable = "sittable",
furnace = "furnace",
storage = "storage",
openable = "openable",
schedule = "schedule",
calculated = "calculated",
workplace = "workplace",
other = "other"
},
bed = {
primary = "bed_primary"
},
sittable = {
primary = "sit_primary",
shared = "sit_shared"
},
furnace = {
primary = "furnace_primary",
shared = "furnace_shared"
},
storage = {
primary = "storage_primary",
shared = "storage_shared"
},
openable = {
home_entrance_door = "home_entrance_door",
room_entrance_door = "room_entrance_door"
},
schedule = {
target = "schedule_target_pos"
},
calculated = {
target = "calculated_target_pos"
},
workplace = {
primary = "workplace_primary",
tool = "workplace_tool"
},
other = {
home_plotmarker = "home_plotmarker",
home_inside = "home_inside",
home_outside = "home_outside",
room_inside = "room_inside",
room_outside = "room_outside"
},
is_primary = function(place_type)
local p1,p2 = string.find(place_type, "primary")
return p1 ~= nil
end,
-- Only works for place types where there is a "primary" and a "shared"
get_alternative = function(place_category, place_type)
local result = {}
local place_types = npc.locations.data[place_category]
-- Determine search type
local search_shared = false
if npc.locations.data.is_primary(place_type) then
search_shared = true
end
for key,place_type in pairs(place_types) do
if search_shared == true then
if npc.locations.data.is_primary(place_type) == false then
return place_type
end
else
if npc.locations.data.is_primary(place_type) == true then
return place_type
end
end
end
end
}
npc.locations.USE_STATE = {
USED = "true",
NOT_USED = "false"
}
function npc.locations.add_shared(self, place_name, place_type, pos, access_node)
-- Set metadata of node
local meta = minetest.get_meta(pos)
if not meta:get_string("advanced_npc:used") then
meta:set_string("advanced_npc:used", npc.locations.USE_STATE.NOT_USED)
end
meta:set_string("advanced_npc:owner", "")
-- This *should* avoid lags
meta:mark_as_private({"advanced_npc:used", "advanced_npc:owner"})
self.places_map[place_name] = {type=place_type, pos=pos, access_node=access_node or pos, status="shared"}
end
function npc.locations.add_owned(self, place_name, place_type, pos, access_node)
-- Set metadata of node
local meta = minetest.get_meta(pos)
if not meta:get_string("advanced_npc:used") then
meta:set_string("advanced_npc:used", npc.locations.USE_STATE.NOT_USED)
end
meta:set_string("advanced_npc:owner", self.npc_id)
-- This *should* avoid lags
meta:mark_as_private({"advanced_npc:used", "advanced_npc:owner"})
self.places_map[place_name] = {type=place_type, pos=pos, access_node=access_node or pos, status="owned"}
end
function npc.locations.add_owned_accessible_place(self, nodes, place_type, walkables)
for i = 1, #nodes do
-- Check if node has owner
local owner = minetest.get_meta(nodes[i].node_pos):get_string("advanced_npc:owner")
--minetest.log("Condition: "..dump(owner == ""))
if owner == "" then
-- If node has no owner, check if it is accessible
local empty_nodes = npc.locations.find_orthogonal_accessible_node(
nodes[i].node_pos, nil, 0, true, walkables)
-- Check if node is accessible
if #empty_nodes > 0 then
-- Set owner to this NPC
nodes[i].owner = self.npc_id
-- Assign node to NPC
npc.locations.add_owned(self, place_type, place_type,
nodes[i].node_pos, empty_nodes[1].pos)
npc.log("DEBUG", "Added node at "..minetest.pos_to_string(nodes[i].node_pos)
.." to NPC "..dump(self.npc_name))
-- Return node
return nodes[i]
end
end
end
end
-- Override flag allows to overwrite a place in the places_map.
-- The only valid use right now is for schedules - don't use this
-- anywhere else unless you have a position that changes over time.
function npc.locations.add_shared_accessible_place(self, nodes, place_type, override, walkables)
if not override or (override and override == false) then
for i = 1, #nodes do
-- Check if not adding same owned place
if nodes[i].owner ~= self.npc_id then
-- Check if it is accessible
local empty_nodes = npc.locations.find_orthogonal_accessible_node(
nodes[i].node_pos, nil, 0, true, walkables)
-- Check if node is accessible
if #empty_nodes > 0 then
-- Assign node to NPC
npc.locations.add_shared(self, place_type..dump(i),
place_type, nodes[i].node_pos, empty_nodes[1].pos)
else
npc.log("WARNING", "Found non-accessible place at pos: "..minetest.pos_to_string(nodes[i].node_pos))
end
end
end
elseif override == true then
-- Note: Nodes is only *one* node in case override = true
-- Check if it is accessible
local empty_nodes = npc.locations.find_orthogonal_accessible_node(
nodes.node_pos, nil, 0, true, walkables)
-- Check if node is accessible
if #empty_nodes > 0 then
-- Nodes is only one node
npc.locations.add_shared(self, place_type, place_type, nodes.node_pos, empty_nodes[1].pos)
end
end
end
function npc.locations.mark_place_used(pos, value)
local meta = minetest.get_meta(pos)
local used = meta:get_string("advanced_npc:used")
if value == used then
npc.log("WARNING", "Attempted to set 'used' property of node at "
..minetest.pos_to_string(pos).." to the same value: '"..dump(value).."'")
return false
else
meta:set_string("advanced_npc:used", value)
npc.log("DEBUG", "'Used' value at pos "..minetest.pos_to_string(pos)..": "..dump(meta:get_string("advanced_npc:used")))
return true
end
end
-- This function is to find an alternative place if the original is
-- not usable. If the original place is a "primary" place, it will
-- try to find a "shared" place. If it is a "shared" place, it will try
-- to find a "primary" place. If none is found, it retuns the given type.
function npc.locations.find_unused_place(self, place_category, place_type, original_place)
local result = {}
-- Check if node is being used
local meta = minetest.get_meta(original_place.pos)
local used = meta:get_string("advanced_npc:used")
if used == npc.locations.USE_STATE.USED then
-- Node is being used, try to find alternative
local alternative_place_type = npc.locations.data.get_alternative(place_category, place_type)
--minetest.log("Alternative place type: "..dump(alternative_place_type))
local alternative_places = npc.locations.get_by_type(self, alternative_place_type)
--minetest.log("Alternatives: "..dump(alternative_places))
for i = 1, #alternative_places do
meta = minetest.get_meta(alternative_places[i].pos)
local used = meta:get_string("advanced_npc:used")
if used == npc.locations.USE_STATE.NOT_USED then
return alternative_places[i]
end
end
else
result = original_place
end
return result
end
function npc.locations.get_by_type(self, place_type, exact_match)
local result = {}
for _, place_entry in pairs(self.places_map) do
--minetest.log("Looking for: "..dump(place_type)..", in: "..dump(place_entry.type))
local s, _ = string.find(place_entry.type, place_type)
if s ~= nil then
--minetest.log("Found: "..dump(place_entry))
result[#result + 1] = place_entry
end
end
--minetest.log("Returning: "..dump(result))
return result
end
---------------------------------------------------------------------------------------
-- Utility functions
---------------------------------------------------------------------------------------
-- The following are utility functions that are used to operate on nodes for
-- specific conditions
-- This func
function npc.locations.is_walkable(node_name)
return node_name == "air"
-- Any walkable node except fences
or (minetest.registered_nodes[node_name].walkable == false
and minetest.registered_nodes[node_name].groups.fence ~= 1)
-- Farming plants
or minetest.registered_nodes[node_name].groups.plant == 1
end
-- This function searches on a squared are of the given radius
-- for nodes of the given type. The type should be npc.locations.nodes
function npc.locations.find_node_nearby(pos, type, radius, vertical_range_limit)
local y_offset = radius
if vertical_range_limit then
y_offset = vertical_range_limit
end
-- Determine area points
local start_pos = {x=pos.x - radius, y=pos.y - y_offset, z=pos.z - radius}
local end_pos = {x=pos.x + radius, y=pos.y + y_offset, z=pos.z + radius}
-- Get nodes
local nodes = minetest.find_nodes_in_area(start_pos, end_pos, type)
return nodes
end
-- TODO: This function can be improved to support a radius greater than 1.
function npc.locations.find_node_orthogonally(pos, nodes, y_adjustment)
-- Call the more generic function with appropriate params
return npc.locations.find_orthogonal_accessible_node(pos, nodes, y_adjustment, nil, nil)
end
-- TODO: This function can be improved to support a radius greater than 1.
function npc.locations.find_orthogonal_accessible_node(pos, nodes, y_adjustment, include_walkables, extra_walkables)
-- Calculate orthogonal points
local points = {}
points[#points + 1] = {x=pos.x+1,y=pos.y+y_adjustment,z=pos.z}
points[#points + 1] = {x=pos.x-1,y=pos.y+y_adjustment,z=pos.z}
points[#points + 1] = {x=pos.x,y=pos.y+y_adjustment,z=pos.z+1}
points[#points + 1] = {x=pos.x,y=pos.y+y_adjustment,z=pos.z-1}
local result = {}
for _,point in pairs(points) do
local node = minetest.get_node(point)
-- Search for specific node names
if nodes then
for _,node_name in pairs(nodes) do
if node.name == node_name then
table.insert(result, {name=node.name, pos=point, param2=node.param2})
end
end
else
-- Search for air, walkable nodes, or any node availble in the extra_walkables array
if node.name == "air"
or (include_walkables == true
and minetest.registered_nodes[node.name].walkable == false
and minetest.registered_nodes[node.name].groups.fence ~= 1)
or (extra_walkables and npc.utils.array_contains(extra_walkables, node.name)) then
result[#result + 1] = {name=node.name, pos=point, param2=node.param2}
end
end
end
return result
end
-- Wrapper around minetest.find_nodes_in_area()
-- TODO: Verify if this wrapper is actually needed
function npc.locations.find_node_in_area(start_pos, end_pos, type)
local nodes = minetest.find_nodes_in_area(start_pos, end_pos, type)
return nodes
end
-- Function used to filter all nodes in the first floor of a building
-- If floor height isn't given, it will assume 2
-- Notice that nodes is an array of entries {node_pos={}, type={}}
function npc.locations.filter_first_floor_nodes(nodes, ground_pos, floor_height)
local height = floor_height or 2
local result = {}
for _,node in pairs(nodes) do
if node.node_pos.y <= ground_pos.y + height then
result[#result + 1] = node
end
end
return result
end
-- Creates an array of {pos=<node_pos>, owner=''} for managing
-- which NPC owns what
function npc.locations.get_nodes_by_type(start_pos, end_pos, type)
local result = {}
local nodes = npc.locations.find_node_in_area(start_pos, end_pos, type)
--minetest.log("Found "..dump(#nodes).." nodes of type: "..dump(type))
for _,node_pos in pairs(nodes) do
local entry = {}
entry["node_pos"] = node_pos
entry["owner"] = ''
result[#result + 1] = entry
end
return result
end
-- Function to get mg_villages building data
if minetest.get_modpath("mg_villages") ~= nil then
function npc.locations.get_mg_villages_building_data(pos)
local result = {
village_id = "",
plot_nr = -1,
building_data = {},
building_type = "",
}
local meta = minetest.get_meta(pos)
result.plot_nr = meta:get_int("plot_nr")
result.village_id = meta:get_string("village_id")
-- Get building data
if mg_villages.get_plot_and_building_data then
local all_data = mg_villages.get_plot_and_building_data(result.village_id, result.plot_nr)
if all_data then
result.building_data = all_data.building_data
result.building_type = result.building_data.typ
result["building_pos_data"] = all_data.bpos
end
else
-- Following line from mg_villages mod, protection.lua
local btype = mg_villages.all_villages[result.village_id].to_add_data.bpos[result.plot_nr].btype
result.building_data = mg_villages.BUILDINGS[btype]
result.building_type = result.building_data.typ
end
return result
end
-- Pre-requisite: only run this function on mg_villages:plotmarker that has been adapted
-- by using spawner.adapt_mg_villages_plotmarker
function npc.locations.get_all_workplaces_from_plotmarker(pos)
local result = {}
local meta = minetest.get_meta(pos)
local pos_data = minetest.deserialize(meta:get_string("building_pos_data"))
if pos_data then
local workplaces = pos_data.workplaces
if workplaces then
-- Insert all workplaces in this plotmarker
for i = 1, #workplaces do
table.insert(result,
{
workplace=workplaces[i],
building_type=meta:get_string("building_type"),
surrounding_workplace = false,
node_pos= {
x=workplaces[i].x,
y=workplaces[i].y,
z=workplaces[i].z
}
})
end
end
end
-- Check the other plotmarkers as well
local nearby_plotmarkers = minetest.deserialize(meta:get_string("nearby_plotmarkers"))
npc.log("DEBUG", "Nearby plotmarkers: "..dump(nearby_plotmarkers))
if nearby_plotmarkers then
for i = 1, #nearby_plotmarkers do
if nearby_plotmarkers[i].workplaces then
-- Insert all workplaces in this plotmarker
for j = 1, #nearby_plotmarkers[i].workplaces do
--minetest.log("Nearby plotmarker workplace #"..dump(j)..": "..dump(nearby_plotmarkers[i].workplaces[j]))
table.insert(result, {
workplace=nearby_plotmarkers[i].workplaces[j],
building_type = nearby_plotmarkers[i].building_type,
surrounding_workplace = true,
node_pos = {
x=nearby_plotmarkers[i].workplaces[j].x,
y=nearby_plotmarkers[i].workplaces[j].y,
z=nearby_plotmarkers[i].workplaces[j].z
}
})
end
end
end
end
return result
end
end
-- This function will search for nodes of type plotmarker and,
-- in case of being an mg_villages plotmarker, it will fetch building
-- information and include in result.
function npc.locations.find_plotmarkers(pos, radius, exclude_current_pos)
local result = {}
local start_pos = {x=pos.x - radius, y=pos.y - 1, z=pos.z - radius}
local end_pos = {x=pos.x + radius, y=pos.y + 1, z=pos.z + radius}
local nodes = minetest.find_nodes_in_area(start_pos, end_pos,
npc.locations.nodes.plotmarker)
npc.log("INFO", "Found "..dump(#nodes).." plotmarkers")
-- Scan nodes
for i = 1, #nodes do
-- Check if current plotmarker is to be excluded from the list
local exclude = false
if exclude_current_pos then
if pos.x == nodes[i].x and pos.y == nodes[i].y and pos.z == nodes[i].z then
exclude = true
end
end
-- Analyze and include node if not excluded
if not exclude then
local node = minetest.get_node(nodes[i])
local def = {}
def["pos"] = nodes[i]
def["name"] = node.name
if node.name == "mg_villages:plotmarker" and npc.locations.get_mg_villages_building_data then
local data = npc.locations.get_mg_villages_building_data(nodes[i])
def["plot_nr"] = data.plot_nr
def["village_id"] = data.village_id
--def["building_data"] = data.building_data
def["building_type"] = data.building_type
npc.log("INFO", "["..dump(data.building_type).."]")
if data.building_pos_data then
def["building_pos_data"] = data.building_pos_data
if next(data.building_pos_data.workplaces) ~= nil then
def["workplaces"] = data.building_pos_data.workplaces
end
end
if data.workplaces and next(data.workplaces) ~= nil then
def["workplaces"] = data.workplaces
end
end
-- Add building
--minetest.log("Adding building: "..dump(def))
table.insert(result, def)
end
end
return result
end
-- Scans an area for the supported nodes: beds, benches,
-- furnaces, storage (e.g. chests) and openable (e.g. doors).
-- Returns a table with these classifications
function npc.locations.scan_area_for_usable_nodes(pos1, pos2)
minetest.log("Bed Nodes: "..dump(npc.locations.nodes.bed))
local result = {
bed_type = {},
sittable_type = {},
furnace_type = {},
storage_type = {},
openable_type = {},
workplace_type = {}
}
local start_pos, end_pos = vector.sort(pos1, pos2)
result.bed_type = npc.locations.get_nodes_by_type(start_pos, end_pos, npc.locations.nodes.bed)
result.sittable_type = npc.locations.get_nodes_by_type(start_pos, end_pos, npc.locations.nodes.sittable)
result.furnace_type = npc.locations.get_nodes_by_type(start_pos, end_pos, npc.locations.nodes.furnace)
result.storage_type = npc.locations.get_nodes_by_type(start_pos, end_pos, npc.locations.nodes.storage)
result.openable_type = npc.locations.get_nodes_by_type(start_pos, end_pos, npc.locations.nodes.openable)
-- Find workplace nodes: if mg_villages:plotmarker is given as start pos, take it from there.
-- If not, search for them.
local node = minetest.get_node(pos1)
if node.name == "mg_villages:plotmarker" then
if npc.locations.get_all_workplaces_from_plotmarker then
result.workplace_type = npc.locations.get_all_workplaces_from_plotmarker(pos1)
end
else
-- Just search for workplace nodes
-- The search radius is increased by 2
result.workplace_type = npc.locations.get_nodes_by_type(
{x=start_pos.x-20, y=start_pos.y, z=start_pos.z-20},
{x=end_pos.x+20, y=end_pos.y, z=end_pos.z+20},
npc.locations.nodes.workplace)
-- Find out building type and add it to the result
for i = 1, #result.workplace_type do
local meta = minetest.get_meta(result.workplace_type[i].node_pos)
local building_type = meta:get_string("building_type") or "none"
local surrounding_workplace = meta:get_string("surrounding_workplace") or "false"
result.workplace_type[i]["surrounding_workplace"] = minetest.is_yes(surrounding_workplace)
result.workplace_type[i]["building_type"] = building_type
end
end
return result
end
-- Helper function to clear metadata in an array of nodes
-- Metadata that will be cleared is:
-- advanced_npc:used
-- advanced_npc:owner
local function clear_metadata(nodes)
local c = 0
for i = 1, #nodes do
local meta = minetest.get_meta(nodes[i].node_pos)
meta:set_string("advanced_npc:owner", "")
meta:set_string("advanced_npc:used", npc.locations.USE_STATE.NOT_USED)
c = c + 1
end
return c
end
function npc.locations.clear_metadata_usable_nodes_in_area(node_data)
local count = 0
if node_data then
count = count + clear_metadata(node_data.bed_type)
count = count + clear_metadata(node_data.sittable_type)
count = count + clear_metadata(node_data.furnace_type)
count = count + clear_metadata(node_data.storage_type)
count = count + clear_metadata(node_data.openable_type)
-- Clear workplace nodes
if node_data.workplace_type then
for i = 1, #node_data.workplace_type do
local meta = minetest.get_meta(node_data.workplace_type[i].node_pos)
meta:set_string("work_data", nil)
count = count + 1
end
end
end
return count
end
local function get_decorated_path(start_pos, end_pos)
local entity = {}
entity.collisionbox = {-0.20,-1.0,-0.20, 0.20,0.8,0.20}
local path = npc.pathfinder.find_path(start_pos, end_pos, entity, true)
return path
end
function npc.locations.find_building_entrance(bed_nodes, marker_pos)
-- Exit if no bed nodes given
if bed_nodes == nil or (bed_nodes and #bed_nodes == 0) then
return
end
-- Iterate through bed nodes to try and find an entrance
for i = 1, #bed_nodes do
-- Initialize positions
local start_pos = bed_nodes[i].node_pos
local end_pos = {x=marker_pos.x, y=marker_pos.y, z=marker_pos.z}
npc.log("INFO", "Trying to find path from "..minetest.pos_to_string(start_pos).." to "..minetest.pos_to_string(end_pos))
-- Find path from the bed node to the plotmarker
local decorated_path = get_decorated_path(start_pos, end_pos)
-- Find building entrance, traverse path backwards and return first node that is openable
if decorated_path then
for j = #decorated_path, 1, -1 do
if decorated_path[j].type == npc.pathfinder.node_types.openable then
local result = {
door = vector.round(decorated_path[j].pos),
inside = vector.round(decorated_path[j-1].pos),
outside = vector.round(decorated_path[j+1].pos)
}
return result
end
end
end
npc.log("INFO", "Attempt "..dump(i).." of "..dump(#bed_nodes).." of finding entrance from bed failed.")
end
end
function npc.locations.find_bedroom_entrance(bed_node, marker_pos)
local start_pos = bed_node.node_pos
local end_pos = {x=marker_pos.x, y=marker_pos.y, z=marker_pos.z }
-- Find path from the bed node to the plotmarker
local decorated_path = get_decorated_path(start_pos, end_pos)
--minetest.log("Decorated path: "..dump(decorated_path))
-- Find building entrance, traverse path forward and return first node that is openable
for i = 1, #decorated_path do
if decorated_path[i].type == npc.pathfinder.node_types.openable then
return {
door = vector.floor(decorated_path[i].pos),
inside = vector.floor(decorated_path[i-1].pos),
outside = vector.floor(decorated_path[i+1].pos)
}
end
end
end
--------------------------------------------------------------------
-- WARNING! Code below here DOESN'T WORKS correctly... don't use! --
--------------------------------------------------------------------
-- Specialized function to find all sittable nodes supported by the
-- mod, namely default stairs and cottages' benches. Since not all
-- stairs nodes placed aren't meant to simulate benches, this function
-- is necessary in order to find stairs that are meant to be benches.
function npc.locations.find_sittable_nodes_nearby(pos, radius)
local result = {}
-- Try to find sittable nodes
local nodes = npc.locations.find_node_nearby(pos, npc.locations.nodes.sittable, radius)
-- Highly unorthodox check for emptinnes
if nodes[1] ~= nil then
for i = 1, #nodes do
-- Get node name, try to avoid using the staircase check if not a stair node
local node = minetest.get_node(nodes[i])
local i1, _ = string.find(node.name, "stairs:")
if i1 ~= nil then
if npc.locations.is_in_staircase(nodes[i]) < 1 then
table.insert(result, nodes[i])
end
else
-- Add node as it is sittable
table.insert(result, nodes[i])
end
end
end
-- Return sittable nodes
return result
end
-- Specialized function to find sittable stairs: stairs that don't
-- have any other stair above them. Only stairs using the default
-- stairs mod are supported for now.
-- Receives a position of a stair node.
npc.locations.staircase = {
none = 0,
bottom = 1,
middle = 2,
top = 3
}
function npc.locations.is_in_staircase(pos)
local node = minetest.get_node(pos)
-- Verify node is actually from default stairs mod
local p1, _ = string.find(node.name, "stairs:")
if p1 ~= nil then
-- Calculate the logical position to the lower and upper stairs node location
local up_x_adj, up_z_adj = 0, 0
local lo_x_adj, lo_z_adj = 0, 0
if node.param2 == 1 then
up_z_adj = -1
lo_z_adj = 1
elseif node.param2 == 2 then
up_z_adj = 1
lo_z_adj = -1
elseif node.param2 == 3 then
up_x_adj = -1
lo_x_adj = 1
elseif node.param2 == 4 then
up_x_adj = 1
lo_x_adj = -1
else
-- This is not a staircase
return false
end
-- Calculate upper and lower position
local upper_pos = {x=pos.x + up_x_adj, y=pos.y + 1, z=pos.z + up_z_adj}
local lower_pos = {x=pos.x + lo_x_adj, y=pos.y - 1, z=pos.z + lo_z_adj}
-- Get next node
local upper_node = minetest.get_node(upper_pos)
local lower_node = minetest.get_node(lower_pos)
--minetest.log("Next node: "..dump(upper_pos))
-- Check if next node is also a stairs node
local up_p1, _ = string.find(upper_node.name, "stairs:")
local lo_p1, _ = string.find(lower_node.name, "stairs:")
if up_p1 ~= nil then
-- By default, think this is bottom of staircase.
local result = npc.locations.staircase.bottom
-- Try downwards now
if lo_p1 ~= nil then
result = npc.locations.staircase.middle
end
return result
else
-- Check if there is a staircase downwards
if lo_p1 ~= nil then
return npc.locations.staircase.top
else
return npc.locations.staircase.none
end
end
end
-- This is not a stairs node
return nil
end
-- WARNING: DEPRECATED
-- Specialized function to find the node position right behind
-- a door. Used to make NPCs enter buildings.
function npc.locations.find_node_in_front_and_behind_door(door_pos)
local door = minetest.get_node(door_pos)
local scan_pos = {
{x=door_pos.x + 1, y=door_pos.y, z=door_pos.z},
{x=door_pos.x - 1, y=door_pos.y, z=door_pos.z},
{x=door_pos.x, y=door_pos.y, z=door_pos.z + 1},
{x=door_pos.x, y=door_pos.y, z=door_pos.z - 1}
}
local facedir_vector = minetest.facedir_to_dir(door.param2)
local back_pos = vector.add(door_pos, facedir_vector)
local back_node = minetest.get_node(back_pos)
local front_pos
if back_node.name == "air" or minetest.registered_nodes[back_node.name].walkable == false then
-- Door is closed, so back_pos is the actual behind position.
-- Calculate front node
front_pos = vector.add(door_pos, vector.multiply(facedir_vector, -1))
else
-- Door is open, need to find the front and back pos
facedir_vector = minetest.facedir_to_dir((door.param2 + 2) % 4)
back_pos = vector.add(door_pos, facedir_vector)
front_pos = vector.add(door_pos, vector.multiply(facedir_vector, -1))
end
return back_pos, front_pos
end

View File

@ -1,99 +0,0 @@
-- Node functionality registry for NPC actions by Zorman2000
---------------------------------------------------------------------------------------
-- In this script, some functionality and information required for nodes
-- to be used correctly by an NPC is described.
-- To avoid as many definitions as possible, the names of the nodes
-- can actually be prefixes.
-- This table will contain the registered nodes
npc.programs.instr.nodes = {
doors = {},
beds = {},
sittable = {}
}
---------------------------------------------------------------------------------------
-- Beds functionality supported by default
---------------------------------------------------------------------------------------
-- Functionality for default beds.
-- Since other mods may be used in the same way as the default beds,
-- this one is a global registration
npc.programs.instr.nodes.default_bed_registration = {
get_lay_pos = function(pos, dir)
return {x = pos.x + dir.x / 2, y = pos.y + 1, z = pos.z + dir.z / 2}
end,
type = "bed"
}
-- The code used in get_lay_pos is from cottages mod and slightly modified.
local cottages_bed_registration = {
get_lay_pos = function(pos, dir)
return {x = pos.x + dir.x / 2, y = pos.y + 1.4, z = pos.z + dir.z / 2}
end,
type = "bed"
}
local cottages_mat_registration = {
get_lay_pos = function(pos, dir)
return {x = pos.x + dir.x / 2, y = pos.y + 1, z = pos.z + dir.z / 2}
end,
type = "mat"
}
---------------------------------------------------------------------------------------
-- Sitting functionality supported by default
---------------------------------------------------------------------------------------
-- Functionality for allowing the NPC to sit on default stairs and cottages' bench
local sittable_stair_registration = {
get_sit_pos = function(pos, param2)
local result = {x=pos.x, y=pos.y+1, z=pos.z};
if param2 == 0 then
result.z = result.z-0.2;
elseif param2 == 1 then
result.x = result.x-0.2;
elseif param2 == 2 then
result.z = result.z+0.2;
elseif param2 == 3 then
result.x = result.x+0.2;
end
return result
end
}
local cottages_bench_registration = {
get_sit_pos = function(pos, param2)
local result = {x=pos.x, y=pos.y+1, z=pos.z};
if param2 == 0 then
result.z = result.z+0.3;
elseif param2 == 1 then
result.x = result.x+0.3;
elseif param2 == 2 then
result.z = result.z-0.3;
elseif param2 == 3 then
result.x = result.x-0.3;
end
return result
end
}
---------------------------------------------------------------------------------------
-- Registry of bed nodes
---------------------------------------------------------------------------------------
-- Default beds.
npc.programs.instr.nodes.beds["beds:bed_bottom"] = npc.programs.instr.nodes.default_bed_registration
npc.programs.instr.nodes.beds["beds:fancy_bed_bottom"] = npc.programs.instr.nodes.default_bed_registration
-- Cottages beds
npc.programs.instr.nodes.beds["cottages:bed_foot"] = cottages_bed_registration
npc.programs.instr.nodes.beds["cottages:sleeping_mat"] = cottages_mat_registration
npc.programs.instr.nodes.beds["cottages:straw_mat"] = cottages_mat_registration
---------------------------------------------------------------------------------------
-- Registry of sittable nodes
---------------------------------------------------------------------------------------
-- Normal wooden stairs
npc.programs.instr.nodes.sittable["stairs:stair_wood"] = sittable_stair_registration
-- Cottages bench
npc.programs.instr.nodes.sittable["cottages:bench"] = cottages_bench_registration

View File

@ -1,395 +0,0 @@
-- Pathfinding code by MarkBu, original can be found here:
-- https://github.com/MarkuBu/pathfinder
--
-- Modifications by Zorman2000
-- This version is slightly modified to use another "walkable" function,
-- plus add a "decorating" path function which allows to know the type
-- of nodes in the path.
---------------------------------------------------------------------------------------
-- Pathfinding functionality
---------------------------------------------------------------------------------------
npc.pathfinder = {}
local pathfinder = {}
npc.pathfinder.node_types = {
start = 0,
goal = 1,
walkable = 2,
openable = 3,
non_walkable = 4
}
npc.pathfinder.nodes = {
openable_prefix = {
"doors:",
"cottages:gate",
"cottages:half_door"
}
}
-- This function is used to determine if a node is walkable
-- or openable, in which case is good to use when finding a path
function pathfinder.is_good_node(node, exceptions)
--local function is_good_node(node, exceptions)
-- Is openable is to support doors, fence gates and other
-- doors from other mods. Currently, default doors, gates
-- and cottages doors are supported.
local is_openable = false
for _,node_prefix in pairs(npc.pathfinder.nodes.openable_prefix) do
local start_i,end_i = string.find(node.name, node_prefix)
if start_i ~= nil then
is_openable = true
break
end
end
if node ~= nil and node.name ~= nil and not minetest.registered_nodes[node.name].walkable then
return npc.pathfinder.node_types.walkable
elseif is_openable then
return npc.pathfinder.node_types.openable
else
for i = 1, #exceptions do
if node.name == exceptions[i] then
return npc.pathfinder.node_types.walkable
end
end
return npc.pathfinder.node_types.non_walkable
end
end
function pathfinder.get_decorated_path(path)
-- Get details from path nodes
local path_detail = {}
for i = 1, #path do
local node = minetest.get_node(path[i])
table.insert(path_detail, {pos={x=path[i].x, y=path[i].y-0.5, z=path[i].z},
type=pathfinder.is_good_node(node, {})})
end
npc.log("DEBUG", "Detailed path: "..dump(path_detail))
return path_detail
end
function npc.pathfinder.find_path(start_pos, end_pos, entity, decorate_path)
local path = pathfinder.find_path(start_pos, end_pos, entity)
if path then
if decorate_path then
path = pathfinder.get_decorated_path(path)
end
else
npc.log("ERROR", "Couldn't find path from "..minetest.pos_to_string(start_pos)
.." to "..minetest.pos_to_string(end_pos))
end
return path
end
-- From this point onwards is MarkBu's original pathfinder code,
-- except for the "walkable" function, which is modified by Zorman2000
-- to include doors and other "walkable" nodes.
-- The version here is exactly this:
-- https://github.com/MarkuBu/pathfinder/commit/ca0b433bf5efde5da545b11b2691fa7f7e53dc30
--[[
minetest.get_content_id(name)
minetest.registered_nodes
minetest.get_name_from_content_id(id)
local ivm = a:index(pos.x, pos.y, pos.z)
local ivm = a:indexp(pos)
minetest.hash_node_position({x=,y=,z=})
minetest.get_position_from_hash(hash)
start_index, target_index, current_index
^ Hash of position
current_value
^ {int:hCost, int:gCost, int:fCost, hash:parent, vect:pos}
]]--
local openSet = {}
local closedSet = {}
local function get_distance(start_pos, end_pos)
local distX = math.abs(start_pos.x - end_pos.x)
local distZ = math.abs(start_pos.z - end_pos.z)
if distX > distZ then
return 14 * distZ + 10 * (distX - distZ)
else
return 14 * distX + 10 * (distZ - distX)
end
end
local function get_distance_to_neighbor(start_pos, end_pos)
local distX = math.abs(start_pos.x - end_pos.x)
local distY = math.abs(start_pos.y - end_pos.y)
local distZ = math.abs(start_pos.z - end_pos.z)
if distX > distZ then
return (14 * distZ + 10 * (distX - distZ)) * (distY + 1)
else
return (14 * distX + 10 * (distZ - distX)) * (distY + 1)
end
end
-- This function is used to determine if a node is walkable
-- or openable, in which case is good to use when finding a path
local function walkable(node, exceptions)
local exceptions = exceptions or {}
-- Is openable is to support doors, fence gates and other
-- doors from other mods. Currently, default doors, gates
-- and cottages doors are supported.
--minetest.log("Is good node: "..dump(node))
local is_openable = false
for _,node_prefix in pairs(npc.pathfinder.nodes.openable_prefix) do
local start_i,end_i = string.find(node.name, node_prefix)
if start_i ~= nil then
is_openable = true
break
end
end
-- Detect mg_villages ceilings usage of thin wood nodeboxes
-- TODO: Improve
local is_mg_villages_ceiling = false
if node.name == "cottages:wood_flat" then
is_mg_villages_ceiling = true
end
if node ~= nil
and node.name ~= nil
and node.name ~= "ignore"
and minetest.registered_nodes[node.name]
and not minetest.registered_nodes[node.name].walkable then
return false
elseif is_openable then
return false
elseif is_mg_villages_ceiling then
return false
else
for i = 1, #exceptions do
if node.name == exceptions[i] then
return false
end
end
return true
end
end
local function check_clearance(cpos, x, z, height)
for i = 1, height do
local n_name = minetest.get_node({x = cpos.x + x, y = cpos.y + i, z = cpos.z + z}).name
local c_name = minetest.get_node({x = cpos.x, y = cpos.y + i, z = cpos.z}).name
--~ print(i, n_name, c_name)
if walkable(n_name) or walkable(c_name) then
return false
end
end
return true
end
local function get_neighbor_ground_level(pos, jump_height, fall_height)
local node = minetest.get_node(pos)
local height = 0
if walkable(node) then
repeat
height = height + 1
if height > jump_height then
return nil
end
pos.y = pos.y + 1
node = minetest.get_node(pos)
until not walkable(node)
return pos
else
repeat
height = height + 1
if height > fall_height then
return nil
end
pos.y = pos.y - 1
node = minetest.get_node(pos)
until walkable(node)
return {x = pos.x, y = pos.y + 1, z = pos.z}
end
end
function pathfinder.find_path(pos, endpos, entity)
local start_index = minetest.hash_node_position(pos)
local target_index = minetest.hash_node_position(endpos)
local count = 1
openSet = {}
closedSet = {}
local h_start = get_distance(pos, endpos)
openSet[start_index] = {hCost = h_start, gCost = 0, fCost = h_start, parent = nil, pos = pos}
-- Entity values
local entity_height = math.ceil(entity.collisionbox[5] - entity.collisionbox[2])
local entity_fear_height = entity.fear_height or 2
local entity_jump_height = entity.jump_height or 1
repeat
local current_index
local current_values
-- Get one index as reference from openSet
for i, v in pairs(openSet) do
current_index = i
current_values = v
break
end
-- Search for lowest fCost
for i, v in pairs(openSet) do
if v.fCost < openSet[current_index].fCost or v.fCost == current_values.fCost and v.hCost < current_values.hCost then
current_index = i
current_values = v
end
end
openSet[current_index] = nil
closedSet[current_index] = current_values
count = count - 1
if current_index == target_index then
-- print("Success")
local path = {}
local reverse_path = {}
repeat
if not closedSet[current_index] then
return
end
table.insert(path, closedSet[current_index].pos)
current_index = closedSet[current_index].parent
if #path > 100 then
-- print("path to long")
return
end
until start_index == current_index
repeat
table.insert(reverse_path, table.remove(path))
until #path == 0
-- print("path lenght: "..#reverse_path)
return reverse_path
end
local current_pos = current_values.pos
local neighbors = {}
local neighbors_index = 1
for z = -1, 1 do
for x = -1, 1 do
local neighbor_pos = {x = current_pos.x + x, y = current_pos.y, z = current_pos.z + z}
local neighbor = minetest.get_node(neighbor_pos)
local neighbor_ground_level = get_neighbor_ground_level(neighbor_pos, entity_jump_height, entity_fear_height)
local neighbor_clearance = false
if neighbor_ground_level then
-- print(neighbor_ground_level.y - current_pos.y)
--minetest.set_node(neighbor_ground_level, {name = "default:dry_shrub"})
local node_above_head = minetest.get_node(
{x = current_pos.x, y = current_pos.y + entity_height, z = current_pos.z})
if neighbor_ground_level.y - current_pos.y > 0 and not walkable(node_above_head) then
local height = -1
repeat
height = height + 1
local node = minetest.get_node(
{x = neighbor_ground_level.x,
y = neighbor_ground_level.y + height,
z = neighbor_ground_level.z})
until walkable(node) or height > entity_height
if height >= entity_height then
neighbor_clearance = true
end
elseif neighbor_ground_level.y - current_pos.y > 0 and walkable(node_above_head) then
neighbors[neighbors_index] = {
hash = nil,
pos = nil,
clear = nil,
walkable = nil,
}
else
local height = -1
repeat
height = height + 1
local node = minetest.get_node(
{x = neighbor_ground_level.x,
y = current_pos.y + height,
z = neighbor_ground_level.z})
until walkable(node) or height > entity_height
if height >= entity_height then
neighbor_clearance = true
end
end
neighbors[neighbors_index] = {
hash = minetest.hash_node_position(neighbor_ground_level),
pos = neighbor_ground_level,
clear = neighbor_clearance,
walkable = walkable(neighbor),
}
else
neighbors[neighbors_index] = {
hash = nil,
pos = nil,
clear = nil,
walkable = nil,
}
end
neighbors_index = neighbors_index + 1
end
end
for id, neighbor in pairs(neighbors) do
-- don't cut corners
local cut_corner = false
if id == 1 then
if not neighbors[id + 1].clear or not neighbors[id + 3].clear
or neighbors[id + 1].walkable or neighbors[id + 3].walkable then
cut_corner = true
end
elseif id == 3 then
if not neighbors[id - 1].clear or not neighbors[id + 3].clear
or neighbors[id - 1].walkable or neighbors[id + 3].walkable then
cut_corner = true
end
elseif id == 7 then
if not neighbors[id + 1].clear or not neighbors[id - 3].clear
or neighbors[id + 1].walkable or neighbors[id - 3].walkable then
cut_corner = true
end
elseif id == 9 then
if not neighbors[id - 1].clear or not neighbors[id - 3].clear
or neighbors[id - 1].walkable or neighbors[id - 3].walkable then
cut_corner = true
end
end
if neighbor.hash ~= current_index and not closedSet[neighbor.hash] and neighbor.clear and not cut_corner then
local move_cost_to_neighbor = current_values.gCost + get_distance_to_neighbor(current_values.pos, neighbor.pos)
local gCost = 0
if openSet[neighbor.hash] then
gCost = openSet[neighbor.hash].gCost
end
if move_cost_to_neighbor < gCost or not openSet[neighbor.hash] then
if not openSet[neighbor.hash] then
count = count + 1
end
local hCost = get_distance(neighbor.pos, endpos)
openSet[neighbor.hash] = {
gCost = move_cost_to_neighbor,
hCost = hCost,
fCost = move_cost_to_neighbor + hCost,
parent = current_index,
pos = neighbor.pos
}
end
end
end
if count > 100 then
-- print("fail")
return
end
until count < 1
-- print("count < 1")
return {pos}
end

View File

@ -1,41 +0,0 @@
--
-- User: hfranqui
-- Date: 3/8/18
-- Time: 2:06 PM
--
-- Global namespace
npc.programs = {}
-- Private namespace
local _programs = {
registered_programs = {}
}
-- Registration function
function npc.programs.register(name, func)
if _programs.registered_programs[name] ~= nil then
npc.log("ERROR", "Attempted to register program with name: "..dump(name)..".\nProgram already exists.")
return
end
_programs.registered_programs[name] = {func = func }
npc.log("INFO", "Successfully registered program '"..dump(name).."'")
end
function npc.programs.is_registered(name)
return _programs.registered_programs[name] ~= nil
end
-- Execution function
function npc.programs.execute(self, name, args)
if _programs.registered_programs[name] == nil then
npc.log("ERROR", "Attempted to execute program with name "..dump(name)..".\nProgram doesn't exists.")
return
end
-- Enqueue callbacks if any
if npc.monitor.callback.exists(npc.monitor.callback.type.program, name) then
-- Enqueue all callbacks for this instruction
npc.monitor.callback.enqueue_all(self, npc.monitor.callback.type.program, name)
end
--npc.log("INFO", "Executing program '"..dump(name).."' with args:\n"..dump(args))
return _programs.registered_programs[name].func(self, args)
end

View File

@ -1,154 +0,0 @@
--
-- Created by IntelliJ IDEA.
-- Date: 3/8/18
-- Time: 2:42 PM
--
-- Follow program. This is a looping program that will try to follow an
-- entity or player until either of the following conditions are met:
-- - A certain flag is set to false
-- - The object is reached and a callback executed.
-- Arguments:
-- - `radius`: integer, initial search radius. Default is 3
-- - `max_radius`: integer, maximum search radius. If target isn't found within initial radius,
-- radius will increase up to this value. Default is 20
-- - `speed`: number, walking speed for the NPC while following. Default is 3
-- - `target`: string, can be "player" or "entity".
-- - `player_name`: string, name of player to follow
-- - `entity_type`: string, type of entity to follow. NOT IMPLEMENTED.
-- - `on_reach`: function, if given, on reaching the target, this function will
-- be called and executed. On execution, the script will finish. DO NOT use with
-- `follow_flag`.
-- - `follow_flag`: string, flag name. If given, the script will keep running until the
-- value of this flag is false. DO NOT use with `on_reach`.
npc.programs.register("advanced_npc:follow", function(self, args)
-- Set default arguments if not present
args.radius = args.radius or 3
args.max_radius = args.max_radius or 20
args.speed = args.speed or 3
args.results_key = "advanced_npc:follow:player_follow"
-- Run this 1/speed times in a second
npc.programs.instr.execute(self, npc.programs.instr.default.SET_INTERVAL, {interval=1/args.speed, freeze=true})
-- Make NPC climb one-block heights. Makes following easier
self.stepheight = 1.1
self.object:set_properties(self)
-- Execution
-- Follow, results to be stored on execution context with key "results_key"
npc.exec.proc.enqueue(self, "advanced_npc:follow:follow_player", args, args.results_key)
-- Check if follow is complete
npc.exec.proc.enqueue(self, "advanced_npc:follow:check_if_complete", args)
end)
-- Follow script functions
-- Function used to reset NPC values once following is complete
npc.programs.instr.register("advanced_npc:follow:reset", function(self)
self.stepheight = 0.6
self.object:set_properties(self)
npc.programs.instr.execute(self, npc.programs.instr.default.SET_INTERVAL, {interval=1, freeze=false})
end)
-- Follow the player
npc.programs.instr.register("advanced_npc:follow:follow_player", function(self, args)
if args.target == "player" then
local player_name = args.player_name
local objs = minetest.get_objects_inside_radius(self.object:getpos(), args.radius)
-- Check if objects were found
minetest.log("Objects found: "..dump(objs))
if #objs > 0 then
for _,obj in pairs(objs) do
if obj then
-- Check if this is the player we are looking for
if obj:is_player() and obj:get_player_name() == player_name then
local target_pos = vector.round(obj:getpos())
-- Calculate distance - if less than 3, avoid walking any further
if vector.distance(self.object:getpos(), target_pos) < 3 then
npc.log("PROCESS", "[follow] Destination reached")
-- Destination reached
-- Add standing action if NPC is still moving
if math.abs(vector.length(self.object:getvelocity())) > 0 then
npc.programs.instr.execute(self, npc.programs.instr.default.STAND,
{dir = minetest.dir_to_yaw(vector.direction(self.object:getpos(), target_pos))}
)
end
-- Rotate NPC towards player
npc.programs.instr.execute(self, npc.programs.instr.default.ROTATE,
{yaw = minetest.dir_to_yaw(vector.direction(self.object:getpos(), target_pos))})
-- Execute `on_reach` function if present
if args.on_reach then
npc.log("PROCESS", "[follow] Executing on_reach callback...")
args.on_reach(self, obj)
return {reached_target = true, target_pos = target_pos, end_execution = true}
end
return {reached_target = true, target_pos = target_pos}
else
npc.log("PROCESS", "[follow] Walking towards player...")
local walk_args = {
yaw = minetest.dir_to_yaw(vector.direction(self.object:getpos(), target_pos)),
speed = args.speed
}
-- Enqueue walk step
npc.programs.instr.execute(self, npc.programs.instr.default.WALK_STEP, walk_args)
return {reached_target = false, target_pos = target_pos}
end
end
end
end
-- Player not found, stop
npc.programs.instr.execute(self, npc.programs.instr.default.STAND, {})
return {reached_target = false, target_pos = nil}
end
end
return {reached_target = false, target_pos = nil}
end)
npc.programs.instr.register("advanced_npc:follow:check_if_complete", function(self, args)
-- Check if follow is still needed
if npc.get_flag(self, args.follow_flag) == false then
-- Stop, follow no more
npc.programs.instr.execute(self, npc.programs.instr.default.STAND, {})
-- Clear flag
npc.update_flag(self, args.follow_flag, nil)
-- Reset actions interval and NPC stepheight
npc.programs.instr.execute(self, "advanced_npc:follow:reset", {})
return
end
-- Get results from following
local follow_result = npc.exec.var.get(self, args.results_key)
-- Check results
if follow_result == nil then
npc.log("WARNING", "Unable to find result in execution context for 'follow_player' function using key: "..
dump(args.results_key))
return
end
-- Clean execution context
npc.exec.var.remove(self, args.results_key)
-- Check if target reached and on_reach function executed
if follow_result.reached_target == true and follow_result.end_execution == true then
return
end
-- on_reach is not set, keep executing until follow flag is off.
if follow_result.target_pos ~= nil then
-- Keep walking or waiting for player to keep moving
npc.exec.proc.enqueue(self, "advanced_npc:follow:follow_player", args, args.results_key)
-- Check if follow is complete
npc.exec.proc.enqueue(self, "advanced_npc:follow:check_if_complete", args)
--npc.enqueue_function(self, detect_more_movement, {player_pos = follow_result.target_pos})
else
-- Cannot find
npc.log("PROCESS", "[follow] Walking towards player")
-- Modify args to increase radius
args.radius = args.radius + 1
npc.exec.proc.enqueue(self, "advanced_npc:follow:follow_player", args, args.results_key)
-- Check if follow is complete
npc.exec.proc.enqueue(self, "advanced_npc:follow:check_if_complete", args)
end
end)

View File

@ -1,166 +0,0 @@
--
-- Created by IntelliJ IDEA.
-- Date: 3/8/18
-- Time: 6:34 PM
--
-- Register callback - count number of rightclick interactions
npc.monitor.callback.register("interactions_since_ack", "interaction", "on_rightclick", function(self)
local interaction_count_since_ack =
npc.data.get_or_put_if_nil(self, "interaction_count_since_ack", 0)
npc.data.set(self, "interaction_count_since_ack", interaction_count_since_ack + 1)
end)
-- Timer for stopping acknowledge of nearby objects if no interactions
npc.monitor.timer.register("advanced_npc:idle:acknowledge_burnout", 5, function(self, args)
-- Check if timer should run
if self.execution.state_process.program_name ~= "advanced_npc:idle"
and self.execution.state_process.program_name ~= "advanced_npc:wander"
or self.execution.state_process.arguments.acknowledge_nearby_objs == false then
-- Stop current timer
npc.monitor.timer.stop(self, "advanced_npc:idle:acknowledge_burnout")
return
end
-- Get number of interactions
local interaction_count_since_ack = npc.data.get(self, "interaction_count_since_ack")
-- Check if there has been any interaction
if interaction_count_since_ack == 0 or interaction_count_since_ack == nil then
-- Stop current timer
npc.monitor.timer.stop(self, "advanced_npc:idle:acknowledge_burnout")
-- Activate burnout reversal timer
npc.monitor.timer.start(self, "advanced_npc:idle:burnout_reversal", args.reversal_timeout or 5, args)
-- Change to wander state
npc.exec.set_state_program(self, "advanced_npc:wander", {
idle_chance = 0,
acknowledge_nearby_objs = false
}, {})
else
-- Reset interaction count
npc.data.set(self, "interaction_count_since_ack", 0)
end
end)
-- Timer to start acknowledging again
npc.monitor.timer.register("advanced_npc:idle:burnout_reversal", 5, function(self, args)
-- Check if timer should run
if self.execution.state_process.program_name ~= "advanced_npc:idle"
and self.execution.state_process.program_name ~= "advanced_npc:wander"
or self.execution.state_process.arguments.acknowledge_nearby_objs == false then
-- Stop current timer
npc.monitor.timer.stop(self, "advanced_npc:idle:burnout_reversal")
return
end
-- Stop burnot timer and burnout reversal
npc.monitor.timer.stop(self, "advanced_npc:idle:burnout_reversal")
-- Signal instruction to restart acknowldge burnout
npc.exec.var.set(self, "start_acknowledge_burnout", true)
-- Change to wander state with acknowledge true
npc.exec.set_state_program(self, "advanced_npc:idle", {
acknowledge_nearby_objs = true
}, {})
end)
-- TODO: Implement whitelist
npc.programs.instr.register("advanced_npc:idle:acknowledge_objects", function(self, args)
local obj_search_radius = args.obj_search_radius or 4
local acknowledge_burnout = args.acknowledge_burnout or 0
local obj_whitelist = args.whitelist
local objs = minetest.get_objects_inside_radius(self.object:getpos(), obj_search_radius)
if #objs > 1 then
for _,obj in pairs(objs) do
if obj:is_player() or
(obj:get_luaentity() and obj:get_luaentity().npc_id and obj:get_luaentity().npc_id ~= self.npc_id) or
(obj:get_luaentity() and obj:get_luaentity().type == "animal") then
-- Rotate NPC towards object
local yaw = minetest.dir_to_yaw(vector.direction(self.object:getpos(), obj:getpos()))
npc.programs.instr.execute(self, npc.programs.instr.default.ROTATE, {yaw=yaw})
-- Check if we have to activate timer to stop acknowledging
if acknowledge_burnout > 0 then
local start_timer = npc.exec.var.get_or_put_if_nil(self, "start_acknowledge_burnout", true)
if start_timer == true then
npc.exec.var.set(self, "start_acknowledge_burnout", false)
-- Activate burnout timer
npc.monitor.timer.start(self,
"advanced_npc:idle:acknowledge_burnout",
acknowledge_burnout,
{reversal_timeout = acknowledge_burnout})
end
end
-- Object found
return true
end
end
end
-- Object not found
return false
end)
-- Idle state script. NPC stays still on this state.
-- It is possible for it to acknowledge other NPCs or players if
-- configured as arguments.
-- Arguments:
-- - `acknowledge_nearby_objs`: boolean. If true, will look for objects and
-- rotate towards them when close by.
-- - `obj_search_interval`: integer, interval in seconds to search for objects.
-- Default is 5
-- - `obj_search_radius`: integer, radius in nodes to search for objects.
-- Default is 5
npc.programs.register("advanced_npc:idle", function(self, args)
local search_nearby_objs = args.acknowledge_nearby_objs
local obj_search_interval = args.obj_search_interval or 5
local obj_search_radius = args.obj_search_radius or 4
local wander_chance = args.wander_chance or 30
local max_wandering_radius = args.max_wandering_radius or 10
local max_acknowledge_time = args.max_acknowledge_time
-- Check if NPC is moving, if it is, stop.
if npc.programs.helper.is_moving(self) then
npc.programs.instr.execute(self, npc.programs.instr.default.STAND, {})
end
local objs_found = false
if search_nearby_objs == true then
-- Search nearby objects and acknowledge them
objs_found = npc.programs.instr.execute(self, "advanced_npc:idle:acknowledge_objects", {
obj_search_radius = obj_search_radius,
acknowledge_burnout = max_acknowledge_time
})
-- if objs_found == true then
-- -- Shorten interval to rotate accurately towards object
-- --npc.programs.instr.execute(self, npc.programs.instr.default.SET_PROCESS_INTERVAL, {interval=0.5})
-- else
-- end
else
-- Stop all acknowledging timers
npc.monitor.timer.stop(self, "advanced_npc:idle:acknowledge_burnout")
npc.monitor.timer.stop(self, "advanced_npc:idle:burnout_reversal")
end
-- Calculate wandering chance
if objs_found == false then
local calculated_wander_chance = math.random(0, 100)
if calculated_wander_chance < wander_chance then
npc.log("INFO", "Switching to wander state")
-- Change to wander state process with mostly default args
npc.exec.set_state_program(self, "advanced_npc:wander", {
max_radius = max_wandering_radius,
idle_chance = 0,
acknowledge_nearby_objs = search_nearby_objs
}, {})
return
else
-- Set interval
npc.programs.instr.execute(self, "advanced_npc:wait", {time=5})
--npc.programs.instr.execute(self, npc.programs.instr.default.SET_PROCESS_INTERVAL, {interval=obj_search_interval})
minetest.log("No obj found")
end
end
end)

View File

@ -1,66 +0,0 @@
--
-- User: zorman2000
-- Date: 3/27/18
-- Time: 6:54 PM
--
npc.programs.internal_properties = {
put_item = "put_item",
put_multiple_items = "put_multiple_items",
take_item = "take_item",
change_trader_status = "trader_status",
can_receive_gifts = "can_receive_gifts",
flag = "flag",
enable_gift_items_hints = "enable_gift_items_hints",
set_trade_list = "set_trade_list"
}
npc.programs.register("advanced_npc:internal_property_change", function(self, args)
local properties = {}
-- Check if this is a just a single property
if args.property and args.args then
properties[#properties + 1] = {property = args.property, args = args.args }
else
-- Args is an array of property objects as above
properties = args
end
-- Process each property
for i = 1, #properties do
local property = properties[i].property
local args = properties[i].args
if property == npc.programs.internal_properties.change_trader_status then
npc.programs.instr.execute(self, "advanced_npc:trade:change_trader_status", args)
elseif property == npc.programs.internal_properties.set_trade_list then
npc.programs.instr.execute(self, "advanced_npc:trade:set_trade_list", args)
elseif property == npc.programs.internal_properties.put_item then
npc.programs.instr.execute(self, "advanced_npc:inventory_put", args)
elseif property == npc.programs.internal_properties.put_multiple_items then
npc.programs.instr.execute(self, "advanced_npc:inventory_put_multiple", args)
elseif property == npc.programs.internal_properties.take_item then
npc.programs.instr.execute(self, "advanced_npc:inventory_take", args)
elseif property == npc.programs.internal_properties.can_receive_gifts then
local value = args.can_receive_gifts
-- Set status
self.can_receive_gifts = value
elseif property == npc.programs.internal_properties.flag then
local action = args.action
if action == "set" then
-- Adds or overwrites an existing flag and sets it to the given value
self.flags[args.flag_name] = args.flag_value
elseif action == "reset" then
-- Sets value of flag to false or to 0
local flag_type = type(self.flags[args.flag_name])
if flag_type == "number" then
self.flags[args.flag_name] = 0
elseif flag_type == "boolean" then
self.flags[args.flag_name] = false
end
end
elseif property == npc.schedule_properties.enable_gift_item_hints then
self.gift_data.enable_gift_items_hints = args.value
end
end
end)

View File

@ -1,244 +0,0 @@
--
-- User: hfranqui
-- Date: 4/20/18
-- Time: 9:16 AM
-- Description: Node query program to replace scheduler check functionality
--
npc.programs.register("advanced_npc:node_query", function(self, args)
local times_to_execute = args.count or 0
local randomize_execution_count = args.randomize_execution_count
local max_count = args.max_count
local min_count = args.min_count
local state_program_on_finished = args.state_program_on_finished
local range = args.range
local vertical_range_limit = args.vertical_range_limit or args.range
local walkable_nodes = args.walkable_nodes
local nodes = args.nodes
local prefer_last_acted_upon_node = args.prefer_last_acted_upon_node
local on_found_executables = args.on_found_executables
local on_not_found_executables = args.on_not_found_executables
-- Set random execution count to false if argument not provided
if randomize_execution_count == nil then
randomize_execution_count = false
elseif randomize_execution_count == true then
-- Calculate count if random
times_to_execute = math.random(min_count, max_count)
end
-- Get NPC position
local start_pos = self.object:getpos()
-- Search nodes
local found_nodes = npc.locations.find_node_nearby(start_pos, nodes, range, vertical_range_limit)
-- Check if any node was found
npc.log("DEBUG_SCHEDULE", "Found nodes using radius: "..dump(found_nodes))
if found_nodes and #found_nodes > 0 then
local node_pos
local node
local last_node_acted_upon = npc.exec.var.get(self, "last_node_acted_upon")
-- Check if there is preference to act on nodes already acted upon
if prefer_last_acted_upon_node == true and last_node_acted_upon then
-- Find a node other than the acted upon - try 3 times
for i = 1, #found_nodes do
node_pos = found_nodes[i]
-- Get node info
node = minetest.get_node(node_pos)
if node.name == last_node_acted_upon then
break
end
end
else
-- Create variable
npc.exec.var.put(self, "last_node_acted_upon", "")
-- Pick a random node to act upon
node_pos = found_nodes[math.random(1, #found_nodes)]
-- Get node info
node = minetest.get_node(node_pos)
end
-- Save this node as the last acted upon
npc.exec.var.set(self, "last_node_acted_upon", node.name)
-- Set node as a place
-- Note: Code below isn't *adding* a node, but overwriting the
-- place with "schedule_target_pos" place type
npc.log("DEBUG_SCHEDULE", "Found "..dump(node.name).." at pos: "..minetest.pos_to_string(node_pos))
npc.locations.add_shared_accessible_place(
self, {owner="", node_pos=node_pos}, npc.locations.data.calculated.target, true, walkable_nodes)
-- Get actions related to node and enqueue them
for i = 1, #on_found_executables[node.name] do
-- local args = {}
-- local action
-- -- Calculate arguments for the following supported actions:
-- -- - Dig
-- -- - Place
-- -- - Walk step
-- -- - Walk to position
-- -- - Use furnace
-- if actions[node.name][i].action == npc.commands.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 = actions[node.name][i].args.add_to_inventory or true,
-- bypass_protection = actions[node.name][i].args.bypass_protection or false
-- }
-- npc.add_action(self, actions[node.name][i].action, args)
-- elseif actions[node.name][i].action == npc.commands.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.
-- -- Defaults: items will be taken from inventory if existing,
-- -- if not will be force-placed (item comes from thin air)
-- -- Protection will be respected
-- args = {
-- pos = actions[node.name][i].args.pos or node_pos,
-- source = actions[node.name][i].args.source or npc.commands.take_from_inventory_forced,
-- node = actions[node.name][i].args.node,
-- bypass_protection = actions[node.name][i].args.bypass_protection or false
-- }
-- --minetest.log("Enqueue dig action with args: "..dump(args))
-- npc.add_action(self, actions[node.name][i].action, args)
-- elseif actions[node.name][i].action == npc.commands.cmd.ROTATE then
-- -- Set arguments
-- args = {
-- dir = actions[node.name][i].dir,
-- start_pos = actions[node.name][i].start_pos
-- or {x=start_pos.x, y=node_pos.y, z=start_pos.z},
-- end_pos = actions[node.name][i].end_pos or node_pos
-- }
-- -- Enqueue action
-- npc.add_action(self, actions[node.name][i].action, args)
-- elseif actions[node.name][i].action == npc.commands.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.commands.get_direction(start_pos, node_pos)
-- minetest.log("actions: "..dump(actions[node.name][i]))
-- 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.commands.one_nps_speed,
-- target_pos = actions[node.name][i].args.target_pos or node_pos
-- }
-- npc.add_action(self, actions[node.name][i].action, args)
-- elseif actions[node.name][i].task == npc.commands.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
-- -- this, however, we need to calculate direction
-- -- First of all, check distance
-- local distance = vector.distance(start_pos, node_pos)
-- if distance < 3 then
-- -- Will do walk_step based instead
-- if distance > 1 then
-- args = {
-- dir = npc.commands.get_direction(start_pos, node_pos),
-- speed = npc.commands.one_nps_speed
-- }
-- -- Enqueue walk step
-- npc.add_action(self, npc.commands.cmd.WALK_STEP, args)
-- end
-- -- Add standing action to look at node
-- npc.add_action(self, npc.commands.cmd.STAND,
-- {dir = npc.commands.get_direction(self.object:getpos(), 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 walkable_nodes or {}
-- }
-- -- Enqueue
-- npc.enqueue_script(self, actions[node.name][i].task, args)
-- end
-- elseif actions[node.name][i].task == npc.commands.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
-- }
-- npc.enqueue_script(self, actions[node.name][i].task, args)
-- else
-- -- Action or task that is not supported for value calculation
-- npc.enqueue_schedule_action(self, actions[node.name][i])
-- end
local executable = on_found_executables[node.name][i]
--minetest.log("Executable["..dump(i).."]: "..dump(executable))
if executable then
if executable.is_state_program then
-- Set state program
npc.exec.set_state_program(self,
executable.program_name,
executable.arguments,
executable.interrupt_option)
end
-- Enqueue entry
npc.exec.proc.enqueue(self, "advanced_npc:interrupt", {
new_program = executable.program_name,
new_args = executable.arguments,
interrupt_options = executable.interrupt_options}
)
end
end
--npc.log("DEBUG_SCHEDULE", "Actions queue: "..dump(self.actions.queue))
else
-- No nodes found, enqueue none_actions
for i = 1, #on_not_found_executables do
-- Add start_pos to none_actions
--on_not_found_executables[i].args["start_pos"] = start_pos
-- Enqueue actions
--npc.add_action(self, none_actions[i].action, none_actions[i].args)
npc.exec.enqueue_program(self,
on_not_found_executables[i].program_name,
on_not_found_executables[i].arguments,
on_not_found_executables[i].interrupt_options)
end
-- No nodes found
--npc.log("DEBUG_SCHEDULE", "Actions queue: "..dump(self.actions.queue))
end
if times_to_execute or (randomize_execution_count and max_count and min_count) then
-- Increase execution count
local execution_count = npc.exec.var.get(self, "execution_count")
if execution_count == nil then
execution_count = 0
npc.exec.var.put(self, "execution_count", execution_count)
end
execution_count = execution_count + 1
npc.exec.var.set(self, "execution_count", execution_count)
-- Check if max number of executions was reached
if execution_count > times_to_execute then
if state_program_on_finished then
npc.exec.set_state_program(self,
state_program_on_finished.program_name,
state_program_on_finished.arguments,
state_program_on_finished.interrupt_option)
end
end
end
end)
---- Range: integer, radius in which nodes will be searched. Recommended radius is
---- between 1-3
---- Nodes: array of node names
---- Actions: map of node names to entries {action=<action_enum>, args={}}.
---- Arguments can be empty - the check function will try to determine most
---- 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)
-- npc.log("DEBUG_SCHEDULE", "Prev Actions queue: "..dump(self.actions.queue))
--end

View File

@ -1,87 +0,0 @@
--
-- User: hfranqui
-- Date: 3/12/18
-- Time: 9:00 AM
--
-- This function makes the NPC lay or stand up from a bed. The
-- pos is the location of the bed, command can be lay or get up
npc.programs.register("advanced_npc:use_bed", function(self, args)
local pos = npc.programs.helper.get_pos_argument(self, args.pos)
if pos == nil then
npc.log("WARNING", "Got nil position in 'use_bed' using args.pos: "..dump(args.pos))
return
end
local action = args.action
local enable_usage_marking = args.enable_usage_marking or true
local node = minetest.get_node(pos)
--minetest.log(dump(node))
local dir = minetest.facedir_to_dir(node.param2)
if action == npc.programs.const.node_ops.beds.LAY then
-- Get position
-- Error here due to ignore. Need to come up with better solution
if node.name == "ignore" then
return
end
local bed_pos = npc.programs.instr.nodes.beds[node.name].get_lay_pos(pos, dir)
-- Sit down on bed, rotate to correct direction
npc.programs.instr.execute(self, npc.programs.instr.default.SIT, {pos=bed_pos, dir=(node.param2 + 2) % 4})
-- Lay down
npc.exec.proc.enqueue(self, npc.programs.instr.default.LAY, {})
if enable_usage_marking then
-- Set place as used
npc.locations.mark_place_used(pos, npc.locations.USE_STATE.USED)
end
else
-- Calculate position to get up
-- Error here due to ignore. Need to come up with better solution
if node.name == "ignore" then
return
end
local bed_pos_y = npc.programs.instr.nodes.beds[node.name].get_lay_pos(pos, dir).y
local bed_pos = {x = pos.x, y = bed_pos_y, z = pos.z}
-- Sit up
npc.programs.instr.execute(self, npc.programs.instr.default.SIT, {pos=bed_pos})
-- Initialize direction: Default is front of bottom of bed
local dir = (node.param2 + 2) % 4
-- Find empty node around node
-- Take into account that mats are close to the floor, so y adjustmen is zero
local y_adjustment = -1
if npc.programs.instr.nodes.beds[node.name].type == "mat" then
y_adjustment = 0
end
local pos_out_of_bed = pos
local empty_nodes = npc.locations.find_node_orthogonally(bed_pos, {"air", "cottages:bench"}, y_adjustment)
if empty_nodes ~= nil and #empty_nodes > 0 then
-- Get direction to the empty node
dir = npc.programs.helper.get_direction(bed_pos, empty_nodes[1].pos)
-- Calculate position to get out of bed
pos_out_of_bed =
{x=empty_nodes[1].pos.x, y=empty_nodes[1].pos.y + 1, z=empty_nodes[1].pos.z}
-- Account for benches if they are present to avoid standing over them
if empty_nodes[1].name == "cottages:bench" then
pos_out_of_bed = {x=empty_nodes[1].pos.x, y=empty_nodes[1].pos.y + 1, z=empty_nodes[1].pos.z}
if empty_nodes[1].param2 == 0 then
pos_out_of_bed.z = pos_out_of_bed.z - 0.3
elseif empty_nodes[1].param2 == 1 then
pos_out_of_bed.x = pos_out_of_bed.x - 0.3
elseif empty_nodes[1].param2 == 2 then
pos_out_of_bed.z = pos_out_of_bed.z + 0.3
elseif empty_nodes[1].param2 == 3 then
pos_out_of_bed.x = pos_out_of_bed.x + 0.3
end
end
end
-- Stand out of bed
npc.exec.proc.enqueue(self, npc.programs.instr.default.STAND, {pos=pos_out_of_bed, dir=dir})
if enable_usage_marking then
-- Set place as unused
npc.locations.mark_place_used(pos, npc.locations.USE_STATE.NOT_USED)
end
end
end)

View File

@ -1,153 +0,0 @@
--
-- User: hfranqui
-- Date: 3/12/18
-- Time: 9:00 AM
--
-- This function allows a NPC to use a furnace using only items from
-- its own inventory. Fuel is not provided. Once the furnace is finished
-- with the fuel items the NPC will take whatever was cooked and whatever
-- remained to cook. The function received the position of the furnace
-- to use, and the item to cook in furnace. Item is an itemstring
npc.programs.register("advanced_npc:use_furnace", function(self, args)
local pos = npc.programs.helper.get_pos_argument(self, args.pos)
if pos == nil then
npc.log("WARNING", "Got nil position in 'use_furnace' using args.pos: "..dump(args.pos))
return
end
local enable_usage_marking = args.enable_usage_marking or true
local item = args.item
local freeze = args.freeze
-- Define which items are usable as fuels. The NPC
-- will mainly use this as fuels to avoid getting useful
-- items (such as coal lumps) for burning
local fuels = {"default:leaves",
"default:pine_needles",
"default:tree",
"default:acacia_tree",
"default:aspen_tree",
"default:jungletree",
"default:pine_tree",
"default:coalblock",
"farming:straw"}
-- Check if NPC has item to cook
local src_item = npc.inventory_contains(self, npc.get_item_name(item))
if src_item == nil then
-- Unable to cook item that is not in inventory
return false
end
-- Check if NPC has a fuel item
for i = 1,9 do
local fuel_item = npc.inventory_contains(self, fuels[i])
if fuel_item ~= nil then
-- Get fuel item's burn time
local fuel_time =
minetest.get_craft_result({method="fuel", width=1, items={ItemStack(fuel_item.item_string)}}).time
local total_fuel_time = fuel_time * npc.get_item_count(fuel_item.item_string)
npc.log("DEBUG", "Fuel time: "..dump(fuel_time))
-- Get item to cook's cooking time
local cook_result =
minetest.get_craft_result({method="cooking", width=1, items={ItemStack(src_item.item_string)}})
local total_cook_time = cook_result.time * npc.get_item_count(item)
npc.log("DEBUG", "Cook: "..dump(cook_result))
npc.log("DEBUG", "Total cook time: "..total_cook_time
..", total fuel burn time: "..dump(total_fuel_time))
-- Check if there is enough fuel to cook all items
if total_cook_time > total_fuel_time then
-- Don't have enough fuel to cook item. Return the difference
-- so it may help on trying to acquire the fuel later.
-- NOTE: Yes, returning here means that NPC could probably have other
-- items usable as fuels and ignore them. This should be ok for now,
-- considering that fuel items are ordered in a way where cheaper, less
-- useless items come first, saving possible valuable items.
return cook_result.time - fuel_time
end
-- Set furnace as used if flag is enabled
if enable_usage_marking then
-- Set place as used
npc.locations.mark_place_used(pos, npc.locations.USE_STATE.USED)
end
-- Calculate how much fuel is needed
local fuel_amount = total_cook_time / fuel_time
if fuel_amount < 1 then
fuel_amount = 1
end
npc.log("DEBUG", "Amount of fuel needed: "..fuel_amount)
-- Put this item on the fuel inventory list of the furnace
local args = {
player = nil,
pos = pos,
inv_list = "fuel",
item_name = npc.get_item_name(fuel_item.item_string),
count = fuel_amount
}
npc.programs.instr.execute(self, npc.programs.instr.default.PUT_ITEM, args)
-- Put the item that we want to cook on the furnace
args = {
player = nil,
pos = pos,
inv_list = "src",
item_name = npc.get_item_name(src_item.item_string),
count = npc.get_item_count(item),
is_furnace = true
}
npc.exec.proc.enqueue(self, npc.programs.instr.default.PUT_ITEM, args)
-- Now, set NPC to wait until furnace is done.
npc.log("DEBUG", "Setting wait command for "..dump(total_cook_time))
npc.exec.proc.enqueue(self, npc.programs.instr.default.SET_INTERVAL, {interval=total_cook_time, freeze=freeze})
-- Reset timer
npc.exec.proc.enqueue(self, npc.programs.instr.default.SET_INTERVAL, {interval=1, freeze=true})
-- If freeze is false, then we will have to find the way back to the furnace
-- once cooking is done.
if freeze == false then
npc.log("DEBUG", "Adding walk to position to wandering: "..dump(pos))
npc.exec.proc.enqueue(self, npc.programs.instr.default.INTERRUPT, {
new_program = "advanced_npc:walk_to_pos",
new_args = {end_pos=pos, walkable={}},
{}
})
--npc.enqueue_script(self, npc.programs.instr.default.WALK_TO_POS, {end_pos=pos, walkable={}})
end
-- Take cooked items back
args = {
player = nil,
pos = pos,
inv_list = "dst",
item_name = cook_result.item:get_name(),
count = npc.get_item_count(item),
is_furnace = false
}
npc.log("DEBUG", "Taking item back: "..minetest.pos_to_string(pos))
npc.exec.proc.enqueue(self, npc.programs.instr.default.TAKE_ITEM, args)
npc.log("DEBUG", "Inventory: "..dump(self.inventory))
-- Set furnace as unused if flag is enabled
if enable_usage_marking then
-- Set place as used
npc.locations.mark_place_used(pos, npc.locations.USE_STATE.NOT_USED)
end
return true
end
end
-- Couldn't use the furnace due to lack of items
return false
end)

View File

@ -1,65 +0,0 @@
--
-- User: hfranqui
-- Date: 3/12/18
-- Time: 9:00 AM
--
-- This function makes the NPC lay or stand up from a sittable node. The
-- pos is the location of the sittable node, command can be lay or get up
npc.programs.register("advanced_npc:use_sittable", function(self, args)
local pos = npc.programs.helper.get_pos_argument(self, args.pos)
if pos == nil then
npc.log("WARNING", "Got nil position in 'use_sittable' using args.pos: "..dump(args.pos))
return
end
local action = args.action
local enable_usage_marking = args.enable_usage_marking or true
local node = minetest.get_node(pos)
if action == npc.programs.const.node_ops.sittable.SIT then
minetest.log("Sitting...")
-- Calculate position depending on bench
-- Error here due to ignore. Need to come up with better solution
if node.name == "ignore" then
return
end
if npc.programs.instr.nodes.sittable[node.name] == nil then
npc.log("WARNING", "Couldn't find node def for sittable node for node: "..dump(node.name))
return
end
local sit_pos = npc.programs.instr.nodes.sittable[node.name].get_sit_pos(pos, node.param2)
-- Sit down on bench/chair/stairs
npc.programs.instr.execute(self, npc.programs.instr.default.SIT, {pos=sit_pos, dir=(node.param2 + 2) % 4})
if enable_usage_marking then
-- Set place as used
npc.locations.mark_place_used(pos, npc.locations.USE_STATE.USED)
end
else
if self.npc_state.movement.is_sitting == false then
npc.log("DEBUG_ACTION", "NPC "..self.npc_name.." attempted to get up from sit when it is not sitting.")
return
end
-- Find empty areas around chair
local dir = node.param2 + 2 % 4
-- Default it to the current position in case it can't find empty
-- position around sittable node. Weird
local pos_out_of_sittable = pos
local empty_nodes = npc.locations.find_node_orthogonally(pos, {"air"}, 0)
if empty_nodes ~= nil and #empty_nodes > 0 then
-- Get direction to the empty node
dir = npc.programs.helper.get_direction(pos, empty_nodes[1].pos)
-- Calculate position to get out of sittable node
pos_out_of_sittable =
{x=empty_nodes[1].pos.x, y=empty_nodes[1].pos.y + 1, z=empty_nodes[1].pos.z}
end
-- Stand
npc.programs.instr.execute(self, npc.programs.instr.default.STAND, {pos=pos_out_of_sittable, dir=dir})
minetest.log("Setting sittable at "..minetest.pos_to_string(pos).." as not used")
if enable_usage_marking then
-- Set place as unused
npc.locations.mark_place_used(pos, npc.locations.USE_STATE.NOT_USED)
end
end
end)

View File

@ -1,185 +0,0 @@
--
-- User: hfranqui
-- Date: 3/12/18
-- Time: 9:00 AM
--
-- This program can be used to make the NPC walk from one
-- position to another. If the optional parameter walkable_nodes
-- is included, which is a table of node names, these nodes are
-- going to be considered walkable for the algorithm to find a
-- path.
npc.programs.register("advanced_npc:walk_to_pos", function(self, args)
-- Get arguments for this task
local use_access_node = true
if args.use_access_node ~= nil then
use_access_node = args.use_access_node
end
local end_pos, node_pos = npc.programs.helper.get_pos_argument(self, args.end_pos, use_access_node)
if end_pos == nil then
npc.log("WARNING", "Got nil position in 'walk_to_pos' using args.pos: "..dump(args.end_pos))
return
end
local enforce_move = args.enforce_move or true
local optimize_one_node_distance = args.optimize_one_node_distance or true
local walkable_nodes = args.walkable
self.stepheight = 1.1
self.object:set_properties(self)
-- Round start_pos to make sure it can find start and end
local start_pos = vector.round(self.object:getpos())
-- Check if start_pos and end_pos are the same
local distance = vector.distance(start_pos, end_pos)
if distance < 0.75 then
-- Check if it was using access node, if it was, rotate NPC into that direction
if use_access_node == true and node_pos then
local yaw = minetest.dir_to_yaw(vector.direction(end_pos, node_pos))
npc.programs.instr.execute(self, npc.programs.instr.default.ROTATE, {yaw = yaw})
end
npc.log("WARNING", "walk_to_pos Found start_pos == end_pos")
return
elseif distance >= 0.75 and distance < 2 then
local yaw = minetest.dir_to_yaw(vector.direction(start_pos, end_pos))
local target_pos = {x=end_pos.x, y=self.object:getpos().y, z=end_pos.z}
-- Check if it is using access node
if use_access_node == true and node_pos then
-- Walk to end_pos, rotate to node_pos
local final_yaw = minetest.dir_to_yaw(vector.direction(end_pos, node_pos))
npc.programs.instr.execute(self, npc.programs.instr.default.WALK_STEP,
{yaw = yaw, target_pos=target_pos})
npc.exec.proc.enqueue(self, npc.programs.instr.default.STAND, {yaw=final_yaw})
else
-- Walk to end_pos
npc.programs.instr.execute(self, npc.programs.instr.default.WALK_STEP,
{yaw = yaw, target_pos=target_pos})
npc.exec.proc.enqueue(self, npc.programs.instr.default.STAND, {})
end
return
else
-- Set walkable nodes to empty if the parameter hasn't been used
if walkable_nodes == nil then
walkable_nodes = {}
end
-- Find path
local path = npc.pathfinder.find_path(start_pos, end_pos, self, walkable_nodes)
if path ~= nil and #path >= 1 then
npc.log("INFO", "walk_to_pos Found path ("..dump(#path).." nodes) from "
..minetest.pos_to_string(start_pos).." to: "..minetest.pos_to_string(end_pos))
-- Add start pos to path
table.insert(path, 1, {pos=start_pos, type=2})
-- Store path
self.npc_state.movement.walking.path = path
-- Local variables
local door_opened = false
local steps_since_door_opened = 0
local speed = npc.programs.const.speeds.two_nps_speed
-- Set the command timer interval to half second. This is to account for
-- the increased speed when walking.
npc.programs.instr.execute(self, npc.programs.instr.default.SET_INTERVAL, {interval=0.5, freeze=true})
-- Set the initial last and target positions
--self.npc_state.movement.walking.target_pos = path[2].pos
-- Add steps to path
for i = 1, #path do
-- Do not add an extra step if reached the goal node
if (i+1) == #path then
-- Add direction to last node
local dir = vector.direction(path[i].pos, end_pos)
local yaw = minetest.dir_to_yaw(dir)
-- Add the last step
npc.exec.proc.enqueue(self, npc.programs.instr.default.WALK_STEP,
{yaw = minetest.dir_to_yaw(dir), speed = speed, target_pos = path[i+1].pos})
-- Add stand animation at end
-- This is not the proper fix (and node_pos), but for now
-- it will avoid crashes
if use_access_node == true and node_pos then
--dir = npc.programs.helper.get_direction(end_pos, node_pos)
--minetest.log("end pos: "..dump(end_pos))
--minetest.log("Node pos: "..dump(node_pos))
yaw = minetest.dir_to_yaw(vector.direction(end_pos, node_pos))
end
-- If door is opened, close it
if door_opened then
npc.exec.proc.enqueue(self, npc.programs.instr.default.STAND, {yaw=minetest.dir_to_yaw(vector.direction(path[i+1].pos, path[i].pos))})
-- Close door
npc.exec.proc.enqueue(self, npc.programs.instr.default.USE_OPENABLE, {
pos=path[i].pos, command=npc.programs.const.node_ops.doors.command.CLOSE})
door_opened = false
end
-- Change dir if using access_node
npc.exec.proc.enqueue(self, npc.programs.instr.default.STAND, {yaw = yaw})
break
end
-- Get direction to move from path[i] to path[i+1]
local dir = vector.direction(path[i].pos, path[i+1].pos)
-- If a diagonal, increase speed by sqrt(2)
if dir.x ~= 0 and dir.z ~=0 then
speed = speed * math.sqrt(2)
end
-- Check if next node is a door, if it is, open it, then walk
if path[i+1].type == npc.pathfinder.node_types.openable then
-- Check if door is already open
local node = minetest.get_node(path[i+1].pos)
if npc.programs.helper.get_openable_node_state(node, path[i+1].pos, dir)
== npc.programs.const.node_ops.doors.state.CLOSED then
--minetest.log("Opening command to open door")
-- Stop to open door, this avoids misplaced movements later on
npc.exec.proc.enqueue(self, npc.programs.instr.default.STAND, {yaw=minetest.dir_to_yaw(dir)})
-- Open door
npc.exec.proc.enqueue(self, npc.programs.instr.default.USE_OPENABLE,
{pos=path[i+1].pos, dir=dir, command=npc.programs.const.node_ops.doors.command.OPEN})
door_opened = true
end
end
-- Add walk command to command queue
npc.exec.proc.enqueue(self, npc.programs.instr.default.WALK_STEP,
{yaw = minetest.dir_to_yaw(dir), speed = speed, target_pos = path[i+1].pos})
-- Restore speed to default
speed = npc.programs.const.speeds.two_nps_speed
-- Count the number of steps taken after opening a door
if door_opened then
steps_since_door_opened = steps_since_door_opened + 1
end
if door_opened then
if steps_since_door_opened == 2 then
-- Stop to close door, this avoids misplaced movements later on
npc.exec.proc.enqueue(self, npc.programs.instr.default.STAND, {yaw=minetest.dir_to_yaw(vector.direction(path[i+1].pos, path[i].pos))})
-- Close door
npc.exec.proc.enqueue(self, npc.programs.instr.default.USE_OPENABLE, {
pos=path[i].pos, command=npc.programs.const.node_ops.doors.command.CLOSE})
-- Reset values
steps_since_door_opened = 0
door_opened = false
end
end
end
-- Return the command interval to default interval of 1 second
-- By default, always freeze.
npc.exec.proc.enqueue(self, npc.programs.instr.default.SET_INTERVAL, {interval=1})
else
-- Unable to find path
npc.log("WARNING", "walk_to_pos Unable to find path. Teleporting to: "..minetest.pos_to_string(end_pos))
-- Check if movement is enforced
if enforce_move then
-- Move to end pos
self.object:moveto({x=end_pos.x, y=end_pos.y+1, z=end_pos.z})
end
end
end
end)

View File

@ -1,77 +0,0 @@
--
-- User: hfranqui
-- Date: 4/6/18
-- Time: 9:18 AM
-- Description: Wander program for Advanced NPC
--
-- Chance is a number from 0 to 100, indicates the chance
-- a NPC will have of walking one step on a random direction
npc.programs.register("advanced_npc:wander", function(self, args)
local acknowledge_nearby_objs = args.acknowledge_nearby_objs
local max_acknowledge_time = args.max_acknowledge_time
local obj_search_radius = args.obj_search_radius or 3
local chance = args.chance or 60
local max_radius = args.max_radius or 10
local speed = args.speed or npc.programs.const.speeds.one_nps_speed
local idle_chance = args.idle_chance or 10
-- First check if there's any object to acknowledge
local objs_found = false
if acknowledge_nearby_objs == true then
objs_found = npc.programs.instr.execute(self, "advanced_npc:idle:acknowledge_objects", {
obj_search_radius = obj_search_radius,
acknowledge_burnout = max_acknowledge_time
})
end
-- Check if there was any object found
if objs_found == false then
-- No object found, proceed to wander
-- Calculate chance of walking
local calculated_chance = math.random(0, 100)
if calculated_chance < chance then
-- Store initial position
local init_pos = npc.exec.var.get(self, "init_pos")
if init_pos == nil then
init_pos = vector.round(self.object:getpos())
npc.exec.var.put(self, "init_pos", init_pos)
end
-- Check if NPC has reached its maximum wandering radius
if vector.distance(init_pos, self.object:getpos()) >= max_radius then
--minetest.log("Walking back")
-- Walk back to the initial position
npc.exec.proc.enqueue(self, npc.programs.instr.default.WALK_STEP, {
yaw = minetest.dir_to_yaw(vector.direction(self.object:getpos(), init_pos)),
speed = speed
})
npc.exec.proc.enqueue(self, npc.programs.instr.default.STAND, {})
else
minetest.log("Walking randomly")
-- Walk in a random direction
local npc_pos = self.object:getpos()
npc.exec.proc.enqueue(self, npc.programs.instr.default.WALK_STEP, {
dir = "random_orthogonal",
start_pos = npc_pos,
speed = speed
})
npc.exec.proc.enqueue(self, npc.programs.instr.default.STAND, {})
end
end
else
-- Object found, switch to idle
npc.exec.set_state_program(self, "advanced_npc:idle", {acknowledge_nearby_objs = true}, {})
return
end
-- Calculate idle chance
local calculated_idle_chance = math.random(0, 100)
if calculated_idle_chance < idle_chance then
npc.log("INFO", "Switching BACK to idle state")
-- Change to idle state process
npc.exec.set_state_program(self, "advanced_npc:idle", {acknowledge_nearby_objs = acknowledge_nearby_objs}, {})
end
end)

View File

@ -1,100 +0,0 @@
--
-- User: hfranqui
-- Date: 5/3/18
-- Time: 9:30 PM
-- Description:
--
npc.info = {
names = {},
textures = {},
gift_items = {}
}
npc.info.search_criteria = {
any_match = "any_match",
all_match = "all_match",
exact_match = "exact_match"
}
function npc.info.register_name(name, tags)
if npc.info.names[name] ~= nil then
npc.log("WARNING", "Attempt to register an existing name: "..dump(name))
return
end
npc.info.names[name] = tags
end
local function search_using_tags(map, tags_to_search, search_criteria)
local result = {}
-- Do a very inefficient search - need to see how to organize this better
-- Traverse all tags for each name, one by one
-- minetest.log("Search: "..dump(tags_to_search))
--minetest.log("Map: "..dump(map))
for name, tags_for_name in pairs(map) do
-- minetest.log("Name: "..dump(name)..", "..dump(tags_for_name))
local tags_found = 0
-- For every tags array for a name, compare with tags_to_search
-- and count how many tags match
for i = 1, #tags_to_search do
for j = 1, #tags_for_name do
-- minetest.log("Tag[i]: "..tags_to_search[i])
-- minetest.log("Tag[j]: "..tags_for_name[j])
if tags_to_search[i] == tags_for_name[j] then
tags_found = tags_found + 1
end
end
end
-- minetest.log("Found: "..dump(tags_found))
-- Check if exact match true is true. If it is, tags_for_name and
-- tags_to_search need to have same number of tags and all match
if tags_found > 0 then
if search_criteria == npc.info.search_criteria.exact_match then
if tags_found == #tags_to_search and tags_found == #tags_for_name then
result[#result + 1] = name
end
elseif search_criteria == npc.info.search_criteria.all_match then
if tags_found == #tags_to_search then
result[#result + 1] = name
end
-- minetest.log("Result: "..dump(result))
elseif search_criteria == npc.info.search_criteria.any_match then
result[#result + 1] = name
end
end
end
-- minetest.log("Result: "..dump(result))
return result
end
function npc.info.get_names(tags_to_search, search_criteria)
return search_using_tags(npc.info.names, tags_to_search, search_criteria)
end
function npc.info.register_texture(filename, tags)
if npc.info.textures[filename] ~= nil then
-- Compare tags, ignore same, add new
local existing_tags = npc.info.textures[filename]
for i = 1, #tags do
local unmatched_count = 0
for j = 1, #existing_tags do
if tags[i] ~= existing_tags[j] then
unmatched_count = unmatched_count + 1
end
end
if unmatched_count == #existing_tags then
-- Tag was not found, add it
npc.info.textures[filename][#existing_tags + 1] = tags[i]
end
end
npc.log("WARNING", "Attempt to register an existing texture with filename: "..dump(filename))
return
end
npc.info.textures[filename] = tags
end
function npc.info.get_textures(tags_to_search, search_criteria)
--minetest.log("Textures: "..dump(npc.info.textures))
return search_using_tags(npc.info.textures, tags_to_search, search_criteria)
end

View File

@ -1,55 +0,0 @@
-- Advanced NPC mod by Zorman2000
local path = minetest.get_modpath("advanced_npc")
-- Intllib
local S
if minetest.get_modpath("intllib") then
S = intllib.Getter()
else
S = function(s, a, ...)
if a == nil then
return s
end
a = {a, ...}
return s:gsub("(@?)@(%(?)(%d+)(%)?)",
function(e, o, n, c)
if e == ""then
return a[tonumber(n)] .. (o == "" and c or "")
else
return "@" .. o .. n .. c
end
end)
end
end
mobs.intllib = S
dofile(path .. "/npc.lua")
dofile(path .. "/utils.lua")
dofile(path .. "/spawner.lua")
dofile(path .. "/relationships.lua")
dofile(path .. "/dialogue.lua")
dofile(path .. "/trade/trade.lua")
dofile(path .. "/trade/prices.lua")
--dofile(path .. "/actions/actions.lua")
-- New program/instructions API
dofile(path .. "/executable/programs/api.lua")
dofile(path .. "/executable/helper.lua")
dofile(path .. "/executable/instructions/api.lua")
dofile(path .. "/executable/instructions/builtin_instructions.lua")
-- Builtin programs
dofile(path .. "/executable/programs/builtin/follow.lua")
dofile(path .. "/executable/programs/builtin/idle.lua")
dofile(path .. "/executable/programs/builtin/wander.lua")
dofile(path .. "/executable/programs/builtin/walk_to_pos.lua")
dofile(path .. "/executable/programs/builtin/use_bed.lua")
dofile(path .. "/executable/programs/builtin/use_sittable.lua")
dofile(path .. "/executable/programs/builtin/internal_property_change.lua")
dofile(path .. "/executable/programs/builtin/node_query.lua")
dofile(path .. "/executable/locations.lua")
dofile(path .. "/executable/pathfinder.lua")
dofile(path .. "/executable/node_registry.lua")
dofile(path .. "/occupations/occupations.lua")
-- Load random data definitions
dofile(path .. "/info/info.lua")
print (S("[Mod] Advanced NPC loaded"))

View File

@ -1,678 +0,0 @@
Copyright (C) 2016-2017 Hector Franqui (zorman2000)
Full GNU GPL v3:
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
{one line to give the program's name and a brief idea of what it does.}
Copyright (C) {year} {name of author}
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
{project} Copyright (C) {year} {fullname}
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.

View File

@ -1,23 +0,0 @@
# German Translation for mobs_npc mod
# Deutsche Übersetzung der mobs_npc Mod
# last update: 2016/June/10
# Author: Xanthin
#init.lua
[MOD] Mobs Redo 'NPCs' loaded = [MOD] Mobs Redo 'NPCs' geladen
#npc.lua
NPC dropped you an item for gold! = NSC ließ dir für Gold einen Gegenstand fallen!
NPC stands still. = NSC bleibt stehen.
NPC will follow you. = NSC wird dir folgen.
Npc = Nsc
#trader.lua
Trader @1 = Händler @1
[NPC] <Trader @1 > Hello, @2, have a look at my wares. = [NSC] <Händler @1 > Hallo, @2, wirf einen Blick auf meine Waren.
Trader @1's stock: = Händler @1s Warenlager
Selection = Auswahl
Price = Preis
Payment = Bezahlung
Bought items = Ware
Trader = Händler

View File

@ -1,21 +0,0 @@
# Template for translations of mobs_npc mod
# last update: 2016/June/10
#init.lua
[MOD] Mobs Redo 'NPCs' loaded =
#npc.lua
NPC dropped you an item for gold! =
NPC stands still. =
NPC will follow you. =
Npc =
#trader.lua
Trader @1 =
[NPC] <Trader @1 > Hello, @2, have a look at my wares. =
Trader @1's stock: =
Selection =
Price =
Payment =
Bought items =
Trader =

View File

@ -1 +0,0 @@
name = advanced_npc

File diff suppressed because it is too large Load Diff

View File

@ -1,501 +0,0 @@
-- Occupations/jobs functionality by Zorman2000
-----------------------------------------------
-- Occupations functionality
-- NPCs need an occupation or job in order to simulate being alive.
-- This functionality is built on top of the schedules functionality.
-- Occupations are essentially specific schedules, that can have slight
-- random variations to provide diversity and make specific occupations
-- less predictable. Occupations are associated with textures, dialogues,
-- specific initial items, type of building (and surroundings) where NPC
-- lives, etc.
-- Example of an occupation: farmer
-- The farmer will have to live in a farm, or just beside a field.
-- It will have the following schedule:
-- 6AM - get out of bed, walk to home inside, goes to chest, retrieves
-- seeds and wander
-- 7AM - goes out to the field and randomly start harvesting and planting
-- crops that are already fully grown
-- 12PM - gets a random but moderate (5-15) amount of seeds and harvested
-- - crops. Goes into the house, stores 1/4 of the amount in a chest,
-- - gets all currency items it has, and sits into a bench
-- 1PM - goes outside the house and becomes trader, sells the remaining
-- - seeds and crops
-- 6PM - goes inside the house. Stores all currency items it has, all
-- - remainin seeds and crops, and sits on a bench
-- 8PM - gets out of the bench, wanders inside home
-- 10PM - goes to bed
-- Implementation:
-- A function, npc.register_occupation(), will be provided to register an
-- occupation that can be used to initialize NPCs. The format is the following:
-- {
-- dialogues = {
-- enable_gift_item_dialogues = true,
-- -- This flag enables/disables gift item dialogues.
-- -- If not set, it defaults to true.
-- type = "",
-- -- The type can be "given", "mix" or "tags"
-- data = {},
-- -- Array of dialogue definitions. This will have dialogue
-- -- if the type is either "mix" or "given"
-- tags = {},
-- -- Array of tags to search for. This will have tags
-- -- if the type is either "mix" or "tags"
--
-- },
-- textures = {},
-- -- Textures are an array of textures, as usually given on
-- -- an entity definition. If given, the NPC will be guaranteed
-- -- to have one of the given textures. Also, ensure they have gender
-- -- as well in the filename so they can be chosen appropriately.
-- -- If left empty, it can spawn with any texture.
-- building_types = {},
-- -- An array of string where each string is the type of building
-- -- where the NPC can spawn with this occupation.
-- -- Example: building_type = {"farm", "house"}
-- -- If left empty or nil, NPC can spawn in any building
-- surrounding_building_types = {},
-- -- An array of string where each string is the type of building
-- -- that is an immediate neighbor of the NPC's home which can also
-- -- be suitable for this occupation. Example, if NPC is farmer and
-- -- spawns on house, then it has to be because there is a field
-- -- nearby. If left empty or nil, surrounding buildings doesn't
-- -- matter
-- workplace_nodes = {},
-- -- An array of string where each string is a node the NPC
-- -- works with. These are useful for assigning workplaces and work
-- -- work nodes.
-- initial_inventory = {},
-- -- An array of entries like the following:
-- -- {name="", count=1} -- or
-- -- {name="", random=true, min=1, max=10}
-- -- This will initialize the inventory for the NPC with the given
-- -- items and the specified count, or, a count between min and max
-- -- when the entry contains random=true
-- -- If left empty, it will initialize with random items.
-- initial_trader_status = "",
-- -- String that specifies initial trader value. Valid values are:
-- -- "casual", "trader", "none"
-- schedules_entries = {},
-- -- This is a table of tables in the following format:
-- -- {
-- [1] = {[1] = action = npc.action.cmd.freeze, args={freeze=true}},
-- [13] = {[1] = action = npc.action.cmd.freeze, args={freeze=false},
-- [2] = action = npc.action.cmd.freeze, args={freeze=true}
-- },
-- [23] = {[1] = action=npc.action.cmd.freeze, args={freeze=false}}
-- -- }
-- -- The numbers, [1], [13] and [23] are the times when the entries
-- -- corresponding to each are supposed to happen. The tables with
-- -- [1], [1],[2] and [1] actions respectively are the entries that
-- -- will happen at time 1, 13 and 23.
-- }
-- Public API
npc.occupations = {}
-- Private API
local occupations = {}
-- This array contains all the registered occupations.
-- The key is the name of the occupation.
npc.occupations.registered_occupations = {}
-- Basic occupation name
npc.occupations.basic_name = "default_basic"
-- This is the basic occupation definition, this is for all NPCs that
-- don't have a specific occupation. It serves as an example.
npc.occupations.basic_def = {
-- Use random textures
textures = {},
-- Use random dialogues
dialogues = {},
-- Initialize inventory with random items
initial_inventory = {},
-- Initialize schedule
schedules_entries = {
-- Schedule entry for 7 in the morning
-- [7] = {
-- -- Get out of bed
-- [1] = {task = npc.commands.cmd.USE_BED, args = {
-- pos = npc.locations.data.bed.primary,
-- action = npc.commands.const.beds.GET_UP
-- }
-- },
-- -- Walk to home inside
-- [2] = {task = npc.commands.cmd.WALK_TO_POS, args = {
-- end_pos = npc.locations.data.OTHER.HOME_INSIDE,
-- walkable = {}
-- },
-- chance = 75
-- },
-- -- Allow mobs_redo wandering
-- [3] = {action = npc.commands.cmd.FREEZE, args = {freeze = false}}
-- },
-- -- Schedule entry for 7 in the morning
-- [8] = {
-- -- Walk to outside of home
-- [1] = {task = npc.commands.cmd.WALK_TO_POS, args = {
-- end_pos = npc.locations.data.OTHER.HOME_OUTSIDE,
-- walkable = {}
-- },
-- chance = 75
-- },
-- -- Allow mobs_redo wandering
-- [2] = {action = npc.commands.cmd.FREEZE, args = {freeze = false}}
-- },
-- -- Schedule entry for 12 midday
-- [12] = {
-- -- Walk to a sittable node
-- [1] = {task = npc.commands.cmd.WALK_TO_POS, args = {
-- end_pos = {place_type=npc.locations.data.SITTABLE.PRIMARY, use_access_node=true},
-- walkable = {"cottages:bench"}
-- },
-- chance = 75
-- },
-- -- Sit on the node
-- [2] = {task = npc.commands.cmd.USE_SITTABLE, args = {
-- pos = npc.locations.data.SITTABLE.PRIMARY,
-- action = npc.commands.const.sittable.SIT
-- },
-- depends = {1}
-- },
-- -- Stay put into place
-- [3] = {action = npc.commands.cmd.SET_INTERVAL, args = {
-- freeze = true,
-- interval = 35
-- },
-- depends = {2}
-- },
-- [4] = {action = npc.commands.cmd.SET_INTERVAL, args = {
-- freeze = true,
-- interval = npc.commands.default_interval
-- },
-- depends = {3}
-- },
-- -- Get up from sit
-- [5] = {action = npc.commands.cmd.USE_SITTABLE, args = {
-- pos = npc.locations.data.SITTABLE.PRIMARY,
-- action = npc.commands.const.sittable.GET_UP
-- },
-- depends = {4}
-- }
-- },
-- -- Schedule entry for 1 in the afternoon
-- [13] = {
-- -- Give NPC money to buy from player
-- [1] = {property = npc.schedule_properties.put_multiple_items, args = {
-- itemlist = {
-- {name="default:iron_lump", random=true, min=2, max=4}
-- }
-- },
-- chance = 75
-- },
-- -- Change trader status to "trader"
-- [2] = {property = npc.schedule_properties.trader_status, args = {
-- status = npc.trade.TRADER
-- },
-- chance = 75
-- },
-- [3] = {property = npc.schedule_properties.can_receive_gifts, args = {
-- can_receive_gifts = false
-- },
-- depends = {1}
-- },
-- -- Allow mobs_redo wandering
-- [4] = {action = npc.commands.cmd.FREEZE, args = {freeze = false}}
-- },
-- -- Schedule entry for 6 in the evening
-- [18] = {
-- -- Change trader status to "none"
-- [1] = {property = npc.schedule_properties.trader_status, args = {
-- status = npc.trade.NONE
-- }
-- },
-- -- Enable gift receiving again
-- [2] = {property = npc.schedule_properties.can_receive_gifts, args = {
-- can_receive_gifts = true
-- }
-- },
-- -- Get inside home
-- [3] = {task = npc.commands.cmd.WALK_TO_POS, args = {
-- end_pos = npc.locations.data.OTHER.HOME_INSIDE,
-- walkable = {}
-- }
-- },
-- -- Allow mobs_redo wandering
-- [4] = {action = npc.commands.cmd.FREEZE, args = {freeze = false}}
-- },
-- -- Schedule entry for 10 in the evening
-- [22] = {
-- [1] = {task = npc.commands.cmd.WALK_TO_POS, args = {
-- end_pos = {place_type=npc.locations.data.bed.primary, use_access_node=true},
-- walkable = {}
-- }
-- },
-- -- Use bed
-- [2] = {task = npc.commands.cmd.USE_BED, args = {
-- pos = npc.locations.data.bed.primary,
-- action = npc.commands.const.beds.LAY
-- }
-- },
-- -- Stay put on bed
-- [3] = {action = npc.commands.cmd.FREEZE, args = {freeze = true}}
-- }
}
}
-- This function registers an occupation
function npc.occupations.register_occupation(name, def)
-- Register all textures per definition
if def.textures and next(def.textures) ~= nil then
-- These are in the format: {name="", tags={"tag1","tag2", ...}}
for i = 1, #def.textures do
npc.info.register_texture(def.textures[i].name, def.textures[i].tags)
end
end
-- Register all dialogues per definition
local dialogue_keys = {}
if def.dialogues then
-- Check which type of dialogues we have
if def.dialogues.type == "given" then
-- We have been given the dialogues, so def.dialogues.data contains
-- an array of dialogues
for _, dialogue in pairs(def.dialogues.data) do
-- Add to the dialogue tags the "occupation name"
table.insert(dialogue.tags, name)
-- Register dialogue
npc.log("INFO", "Registering dialogue for occupation "..dump(name)..": "..dump(dialogue))
local key = npc.dialogue.register_dialogue(dialogue)
-- Add key to set of dialogue keys
table.insert(dialogue_keys, key)
end
elseif def.dialogues.type == "mix" then
-- We have been given the dialogues, so def.dialogues.data contains
-- an array of dialogues and def.dialogues.tags contains an array of
-- tags. Currently only registering will be performed.
-- Register dialogues
for _, dialogue in pairs(def.dialogues.data) do
-- Add to the dialogue tags the "occupation name"
table.insert(dialogue.tags, name)
-- Register dialogue
local key = npc.dialogue.register_dialogue(dialogue)
-- Add key to set of dialogue keys
table.insert(dialogue_keys, key)
end
end
end
-- Save into the definition the dialogue keys
def.dialogues["keys"] = dialogue_keys
-- Validate state program
if def.state_program then
if npc.programs.is_registered(def.state_program.name) == false then
npc.log("ERROR", "Unable to find program with name: "..dump(def.state_program.name))
return
end
end
-- Save the definition
npc.occupations.registered_occupations[name] = def
npc.log("INFO", "Successfully registered occupation with name: "..dump(name))
end
-- This function scans all registered occupations and filter them by
-- building type and surrounding building type, returning an array
-- of occupation names (strings)
-- BEWARE! Below this lines lies ugly, incomprehensible code!
function npc.occupations.get_for_building(building_type, surrounding_building_types)
local result = {}
for name,def in pairs(npc.occupations.registered_occupations) do
-- Check for empty or nil building types, in that case, any building
if def.building_types == nil or def.building_types == {}
and def.surrounding_building_types == nil or def.surrounding_building_types == {} then
-- Empty building types, add to result
table.insert(result, name)
elseif def.building_types ~= nil and #def.building_types > 0 then
-- Check if building type is contained in the def's building types
if npc.utils.array_contains(def.building_types, building_type) then
table.insert(result, name)
end
end
-- Check for empty or nil surrounding building types
if def.surrounding_building_types ~= nil
and #def.surrounding_building_types > 0 then
-- -- Add this occupation
-- --table.insert(result, name)
-- else
-- Surrounding buildings is not empty, loop though them and compare
-- to the given ones
for i = 1, #surrounding_building_types do
for j = 1, #def.surrounding_building_types do
-- Check if the definition's surrounding building type is the same
-- as the given one
if def.surrounding_building_types[j].type
== surrounding_building_types[i].type then
-- Check if the origin buildings contain the expected type
if npc.utils.array_contains(def.surrounding_building_types[j].origin_building_types,
surrounding_building_types[i].origin_building_type) then
-- Add this occupation
table.insert(result, name)
end
end
end
end
end
end
return result
end
-- This function will initialize entities values related to
-- the occupation: textures, dialogues, inventory items and
-- will set schedules accordingly.
function npc.occupations.initialize_occupation_values(self, occupation_name)
-- Get occupation definition
local def = npc.occupations.registered_occupations[occupation_name]
if not def then
npc.log("WARNING", "No definition found for occupation name: "..dump(occupation_name))
return
end
npc.log("INFO", "Overriding NPC values using occupation '"..dump(occupation_name).."' values")
-- Initialize textures, else it will leave the current textures
-- Pick them from tags
if def.textures and table.getn(def.textures) > 0 then
-- Select a texture
local available_textures = npc.info.get_textures({self.gender, self.age, occupation_name}, "all_match")
-- Set texture if it found for gender and age
-- If an array was returned, select a random texture from it
if next(available_textures) ~= nil then
self.selected_texture = available_textures[math.random(1, #available_textures)]
end
else
-- Try to choose a random texture - if exists
if next(npc.info.textures) ~= nil then
local available_textures = npc.info.get_textures({self.gender, self.age}, "all_match")
self.selected_texture = available_textures[math.random(1, #available_textures)]
else
-- Return a default texture
self.selected_texture = "default_"..self.gender..".png"
end
end
minetest.log("Result: "..dump(self.selected_texture))
-- Set texture and base texture
self.textures = {self.selected_texture}
self.base_texture = {self.selected_texture}
-- Refresh entity
self.object:set_properties(self)
-- Initialize inventory
if def.initial_inventory and table.getn(def.initial_inventory) > 0 then
for i = 1, #def.initial_inventory do
local item = def.initial_inventory[i]
-- Check if item count is randomized
if item.random and item.min and item.max then
npc.add_item_to_inventory(self, item.name, math.random(item.min, item.max))
else
-- Add item with the given count
npc.add_item_to_inventory(self, item.name, item.count)
end
end
end
-- Initialize dialogues
if def.dialogues then
-- Check for gift item dialogues enable
if def.dialogues.disable_gift_item_dialogues then
self.dialogues.hints = {}
end
local dialogue_keys = {}
-- Check which type of dialogues we have
if def.dialogues.type == "given" and def.dialogues.keys then
-- We have been given the dialogues, so def.dialogues.data contains
-- an array of dialogues. These dialogues were registered, therefore we need
-- just the keys
for i = 1, #def.dialogues.keys do
table.insert(dialogue_keys, def.dialogues.keys[i])
end
elseif def.dialogues.type == "mix" then
-- We have been given the dialogues, so def.dialogues.data contains
-- an array of dialogues and def.dialogues.tags contains an array of
-- tags that we will use to search
if def.dialogues.keys then
-- Add the registered dialogues
for i = 1, #def.dialogues.keys do
table.insert(dialogue_keys, def.dialogues.keys[i])
end
end
-- Find dialogues using tags
local dialogues = npc.dialogue.search_dialogue_by_tags(def.dialogues.tags, true)
-- Add keys to set of dialogue keys
for _, key in pairs(npc.utils.get_map_keys(dialogues)) do
table.insert(dialogue_keys, key)
end
elseif def.dialogues.type == "tags" then
-- We need to find the dialogues from tags. def.dialogues.tags contains
-- an array of tags that we will use to search.
local dialogues = npc.dialogue.search_dialogue_by_tags(def.dialogues.tags, true)
-- Add keys to set of dialogue keys
dialogue_keys = npc.utils.get_map_keys(dialogues)
end
-- Add dialogues to NPC
-- Check if there is a max of dialogues to be added
local max_dialogue_count = npc.dialogue.MAX_DIALOGUES
if def.dialogues.max_count and def.dialogues.max_count > 0 then
max_dialogue_count = def.dialogues.max_count
end
-- Add dialogues to the normal dialogues for NPC
if #dialogue_keys > 0 then
self.dialogues.normal = {}
for i = 1, math.min(max_dialogue_count, #dialogue_keys) do
self.dialogues.normal[i] = dialogue_keys[i]
end
end
end
-- Initialize properties
minetest.log("def.properties: "..dump(def.properties))
if def.properties then
-- Initialize trader status
if def.properties.initial_trader_status then
self.trader_data.trader_status = def.properties.initial_trader_status
end
-- Enable/disable gift items hints
if def.properties.enable_gift_items_hints ~= nil then
self.gift_data.enable_gift_items_hints = def.properties.enable_gift_items_hints
end
end
-- Initialize state program
if def.state_program then
npc.exec.set_state_program(self,
def.state_program.name,
def.state_program.args,
def.state_program.interrupt_options)
npc.log("INFO", "Successfully set state program "..dump(def.state_program.name))
end
-- Initialize schedule entries
if def.schedules_entries and table.getn(npc.utils.get_map_keys(def.schedules_entries)) > 0 then
-- Create schedule in NPC
npc.schedule.create(self, npc.schedule.const.types.generic, 0)
-- Traverse schedules
for time, entries in pairs(def.schedules_entries) do
-- Add schedule entry for each time
npc.schedule.entry.put(self, npc.schedule.const.types.generic, 0, time, nil, entries)
end
end
npc.log("INFO", "Successfully initialized NPC with occupation values")
end

View File

@ -1,680 +0,0 @@
-- Relationships code for Advanced NPC by Zorman2000
---------------------------------------------------------------------------------------
-- Gift and relationship system
---------------------------------------------------------------------------------------
-- Each NPCs has 2 favorite and 2 disliked items. These items are chosen at spawn
-- time and will be re-chosen when the age changes (from child to adult, for example).
-- The items are chosen from the npc.FAVORITE_ITEMS table, and depends on gender and age.
-- A player, via right-click, or another NPC, can gift an item to a NPC. In the case
-- of the player, the player will give one of the currently wielded item. Gifts can be
-- given only once per some time period, the NPC will reject the given item if still
-- the period isn't over.
-- If the NPC is neutral on the item (meanining it's neither favorite or disliked), it
-- is possible it will not accept it, and the relationship the giver has with the NPC
-- will be unchanged.
-- In the other hand, if the item given its a favorite, the relationship points the NPC
-- has with giver will increase by a given amount, depending on favoriteness. Favorite 1
-- will increase the relationship by 2 * npc.ITEM_GIFT_EFFECT, and favorite 2 only by
-- npc.ITEM_GIFT_EFFECT. Similarly, if the item given is a disliked item, the NPC will
-- not take it, and its relationship points with the giver will decrease by 2 or 1 times
-- npc.ITEM_GIFT_EFFECT.
local S = mobs.intllib
npc.relationships = {}
-- Constants
npc.relationships.ITEM_GIFT_EFFECT = 2.5
-- Expected values for these are 720 each respectively
npc.relationships.GIFT_TIMER_INTERVAL = 360
npc.relationships.RELATIONSHIP_DECREASE_TIMER_INTERVAL = 720
npc.relationships.RELATIONSHIP_PHASE = {}
-- Define phases
npc.relationships.RELATIONSHIP_PHASE["phase1"] = {limit = 10}
npc.relationships.RELATIONSHIP_PHASE["phase2"] = {limit = 25}
npc.relationships.RELATIONSHIP_PHASE["phase3"] = {limit = 45}
npc.relationships.RELATIONSHIP_PHASE["phase4"] = {limit = 70}
npc.relationships.RELATIONSHIP_PHASE["phase5"] = {limit = 100}
npc.relationships.GIFT_ITEM_LIKED = "liked"
npc.relationships.GIFT_ITEM_DISLIKED = "disliked"
npc.relationships.GIFT_ITEM_HINT = "hint"
npc.relationships.GIFT_ITEM_RESPONSE = "response"
-- Favorite and disliked items tables
npc.relationships.gift_items = {
liked = {
female = {
["phase1"] = {},
["phase2"] = {},
["phase3"] = {},
["phase4"] = {},
["phase5"] = {},
["phase6"] = {}
},
male = {
["phase1"] = {},
["phase2"] = {},
["phase3"] = {},
["phase4"] = {},
["phase5"] = {},
["phase6"] = {}
}
},
disliked = {
female = {},
male = {}
}
}
npc.relationships.DEFAULT_RESPONSE_NO_GIFT_RECEIVE =
"Thank you, but I don't need anything for now."
-- Married NPC dialogue definition
npc.relationships.MARRIED_NPC_DIALOGUE = {
text = "Hi darling!",
is_married_dialogue = true,
responses = {
[1] = {
text = "Let's talk!",
action_type = "function",
response_id = 1,
action = function(self, player)
npc.start_dialogue(self, player, false)
end
},
[2] = {
text = "Honey, can you wait for me here?",
action_type = "function",
response_id = 2,
action = function(self, player)
self.order = "stand"
npc.chat(self.npc_name, player:get_player_name(),
S("Ok dear, I will wait here for you."))
end
},
[3] = {
text = "Please, come with me!",
action_type = "function",
response_id = 3,
action = function(self, player)
self.order = "follow"
npc.chat(self.npc_name, player:get_player_name(), S("Ok, let's go!"))
end
}
}
}
-- Function to get relationship phase
function npc.relationships.get_relationship_phase_by_points(points)
if points > npc.relationships.RELATIONSHIP_PHASE["phase5"].limit then
return "phase6"
elseif points > npc.relationships.RELATIONSHIP_PHASE["phase4"].limit then
return "phase5"
elseif points > npc.relationships.RELATIONSHIP_PHASE["phase3"].limit then
return "phase4"
elseif points > npc.relationships.RELATIONSHIP_PHASE["phase2"].limit then
return "phase3"
elseif points > npc.relationships.RELATIONSHIP_PHASE["phase1"].limit then
return "phase2"
else
return "phase1"
end
end
-- Registration functions
-----------------------------------------------------------------------------
-- Items can be registered to be part of the gift system using the
-- below function. The def is the following:
-- {
-- dialogues = {
-- liked = {
-- -- ^ This is an array of the following table:
-- [1] = {dialogue_type="", gender="", text=""}
-- -- ^ dialogue_type: defines is this is a hint or a response.
-- -- valid values are: "hint", "response"
-- -- gender: valid values are: "male", female"
-- -- text: the dialogue text
-- },
-- disliked = {
-- -- ^ This is an array with the same type of tables as above
-- }
-- }
-- }
function npc.relationships.register_favorite_item(item_name, phase, gender, def)
local dialogues = {}
-- Register dialogues based on the hints and responses
-- Liked
for i = 1, #def.hints do
table.insert(dialogues, {
text = def.hints[i],
tags = {phase, item_name, gender,
npc.dialogue.tags.GIFT_ITEM_HINT, npc.dialogue.tags.GIFT_ITEM_LIKED}
})
end
for i = 1, #def.responses do
table.insert(dialogues, {
text = def.responses[i],
tags = {phase, item_name, gender,
npc.dialogue.tags.GIFT_ITEM_RESPONSE, npc.dialogue.tags.GIFT_ITEM_LIKED}
})
end
-- Register all dialogues
for i = 1, #dialogues do
npc.dialogue.register_dialogue(dialogues[i])
end
-- Insert item into table
table.insert(npc.relationships.gift_items.liked[gender][phase], item_name)
end
function npc.relationships.register_disliked_item(item_name, gender, def)
local dialogues = {}
-- Register dialogues based on the hints and responses
-- Liked
for i = 1, #def.hints do
table.insert(dialogues, {
text = def.hints[i],
tags = {item_name, gender,
npc.dialogue.tags.GIFT_ITEM_HINT, npc.dialogue.tags.GIFT_ITEM_UNLIKED}
})
end
for i = 1, #def.responses do
table.insert(dialogues, {
text = def.responses[i],
tags = {item_name, gender,
npc.dialogue.tags.GIFT_ITEM_RESPONSE, npc.dialogue.tags.GIFT_ITEM_UNLIKED}
})
end
-- Register all dialogues
for i = 1, #dialogues do
npc.dialogue.register_dialogue(dialogues[i])
end
-- Insert item into table
table.insert(npc.relationships.gift_items.disliked[gender], item_name)
end
function npc.relationships.get_dialogues_for_gift_item(item_name, dialogue_type, item_type, gender, phase)
local tags = {
[1] = item_name,
[2] = dialogue_type,
[3] = item_type,
[4] = gender
}
if phase ~= nil then
tags[5] = phase
end
npc.log("DEBUG","Searching with tags: "..dump(tags))
return npc.dialogue.search_dialogue_by_tags(tags, true)
end
-- Returns the response message for a given item
function npc.relationships.get_response_for_favorite_item(item_name, gender, phase)
local items = npc.FAVORITE_ITEMS.female
if gender == npc.MALE then
items = npc.FAVORITE_ITEMS.male
end
for i = 1, #items[phase] do
if items[phase][i].item == item_name then
return items[phase][i].response
end
end
return nil
end
-- Returns the response message for a disliked item
function npc.relationships.get_response_for_disliked_item(item_name, gender)
local items = npc.DISLIKED_ITEMS.female
if gender == npc.MALE then
items = npc.DISLIKED_ITEMS.male
end
for i = 1, #items do
minetest.log(dump(items[i]))
if items[i].item == item_name then
--minetest.log("Returning: "..dump(items[i].response))
return items[i].response
end
end
return nil
end
-- Gets the item hint for a favorite item
function npc.relationships.get_hint_for_favorite_item(item_name, gender, phase)
for i = 1, #npc.FAVORITE_ITEMS[gender][phase] do
if npc.FAVORITE_ITEMS[gender][phase][i].item == item_name then
return npc.FAVORITE_ITEMS[gender][phase][i].hint
end
end
return nil
end
-- Gets the item hint for a disliked item
function npc.relationships.get_hint_for_disliked_item(item_name, gender)
for i = 1, #npc.DISLIKED_ITEMS[gender] do
if npc.DISLIKED_ITEMS[gender][i].item == item_name then
return npc.DISLIKED_ITEMS[gender][i].hint
end
end
return nil
end
-- Relationship functions
-----------------------------------------------------------------------------
-- This function selects two random items from the npc.favorite_items table
-- It checks for gender and phase for choosing the items
function npc.relationships.select_random_favorite_items(gender, phase)
local result = {}
local items = {}
-- -- Filter gender
-- if gender == npc.FEMALE then
-- items = npc.FAVORITE_ITEMS.female
-- else
-- items = npc.FAVORITE_ITEMS.male
-- end
-- Select the phase
-- items = items[phase]
items = npc.relationships.gift_items.liked[gender][phase]
if items and next(items) ~= nil then
result.fav1 = items[math.random(1, #items)]
result.fav2 = items[math.random(1, #items)]
end
return result
end
-- This function selects two random items from the npc.disliked_items table
-- It checks for gender for choosing the items. They stay the same for all
-- phases
function npc.relationships.select_random_disliked_items(gender)
local result = {}
local items = {}
-- -- Filter gender
-- if gender == npc.FEMALE then
-- items = npc.DISLIKED_ITEMS.female
-- else
-- items = npc.DISLIKED_ITEMS.male
-- end
items = npc.relationships.gift_items.disliked[gender]
if items and next(items) ~= nil then
result.dis1 = items[math.random(1, #items)]
result.dis2 = items[math.random(1, #items)]
end
return result
end
-- Creates a relationship with a given player or NPC
local function create_relationship(self, clicker_name)
local count = #self.relationships
self.relationships[count + 1] = {
-- Player or NPC name with whom the relationship is with
name = clicker_name,
-- Relationship points
points = 0,
-- Relationship phase, used for items and for phrases
phase = "phase1",
-- How frequent can the NPC receive a gift
gift_interval = npc.relationships.GIFT_TIMER_INTERVAL,
-- Current timer count since last gift
gift_timer_value = 0,
-- The amount of time without providing gift or talking that will decrease relationship points
relationship_decrease_interval = npc.relationships.RELATIONSHIP_DECREASE_TIMER_INTERVAL,
-- Current timer count for relationship decrease
relationship_decrease_timer_value = 0,
-- Current timer count since last time player talked to NPC
talk_timer_value = 0
}
end
-- Returns a relationship points
local function get_relationship_points(self, clicker_name)
for i = 1, #self.relationships do
if self.relationships[i].name == clicker_name then
return self.relationships[i].points
end
end
return nil
end
-- Updates relationship with given points
local function update_relationship(self, clicker_name, modifier)
for i = 1, #self.relationships do
if self.relationships[i].name == clicker_name then
self.relationships[i].points = self.relationships[i].points + modifier
local current_phase = self.relationships[i].phase
self.relationships[i].phase =
npc.relationships.get_relationship_phase_by_points(self.relationships[i].points)
if current_phase ~= self.relationships[i].phase then
-- Re-select favorite items per new phase
self.gift_data.favorite_items =
npc.relationships.select_random_favorite_items(self.gender, self.relationships[i].phase)
-- Re-select dialogues per new
self.dialogues =
npc.dialogue.select_random_dialogues_for_npc(self,
self.relationships[i].phase)
return true
end
return false
end
end
-- Relationship not found, huge error
return nil
end
-- Checks if a relationship with given player or NPC exists
local function check_relationship_exists(self, clicker_name)
for i = 1, #self.relationships do
if self.relationships[i].name == clicker_name then
return true
end
end
return false
end
-- Returns the relationship phase given the name of the player
function npc.relationships.get_relationship_phase(self, clicker_name)
for i = 1, #self.relationships do
if clicker_name == self.relationships[i].name then
return self.relationships[i].phase
end
end
return nil
end
-- Checks if NPC can receive gifts
local function check_npc_can_receive_gift(self, clicker_name)
for i = 1, #self.relationships do
if self.relationships[i].name == clicker_name then
-- Checks avoid married NPC to receive from others
if self.is_married_to == nil
or (self.is_married ~= nil and self.is_married_to == clicker_name) then
return self.relationships[i].gift_timer_value >= self.relationships[i].gift_interval
else
return false
end
end
end
-- Not found
return nil
end
-- Checks if relationship can be updated by talking
local function check_relationship_by_talk_timer_ready(self, clicker_name)
for i = 1, #self.relationships do
if self.relationships[i].name == clicker_name then
return self.relationships[i].talk_timer_value >= self.relationships[i].gift_interval
end
end
-- Not found
return nil
end
-- Resets the gift timer
local function reset_gift_timer(self, clicker_name)
for i = 1, #self.relationships do
if self.relationships[i].name == clicker_name then
self.relationships[i].gift_timer_value = 0
self.relationships[i].relationship_decrease_timer_value = 0
return
end
end
end
-- Resets the talk timer
local function reset_talk_timer(self, clicker_name)
for i = 1, #self.relationships do
if self.relationships[i].name == clicker_name then
self.relationships[i].talk_timer_value = 0
return
end
end
end
-- Resets the relationshop decrease timer
local function reset_relationship_decrease_timer(self, clicker_name)
for i = 1, #self.relationships do
if self.relationships[i].name == clicker_name then
self.relationships[i].relationship_decrease_timer_value = 0
return
end
end
end
-- Gifts functions
---------------------------------------------------------------------------------------
-- Displays message and hearts depending on relationship level
local function show_receive_gift_reaction(self, item_name, modifier, clicker_name, phase_change)
local points = get_relationship_points(self, clicker_name)
local pos = self.object:getpos()
-- Positive modifier (favorite items) reactions
if modifier >= 0 then
local phase = npc.relationships.get_relationship_phase_by_points(points)
if phase == "phase3" then
npc.effect({x = pos.x, y = pos.y + 1, z = pos.z}, 2, "heart.png")
elseif phase == "phase4" then
npc.effect({x = pos.x, y = pos.y + 1, z = pos.z}, 4, "heart.png")
elseif phase == "phase5" then
npc.effect({x = pos.x, y = pos.y + 1, z = pos.z}, 6, "heart.png")
elseif phase == "phase6" then
npc.effect({x = pos.x, y = pos.y + 1, z = pos.z}, 8, "heart.png")
end
if phase_change then
local number_code = phase:byte(phase:len()) - 1
phase = "phase"..string.char(number_code)
end
-- Send message
-- TODO: There might be an error with getting the message...
--minetest.log("Item_name: "..dump(item_name)..", gender: "..dump(self.gender)..", phase: "..dump(phase))
local dialogues_found = npc.relationships.get_dialogues_for_gift_item(
item_name,
npc.dialogue.tags.GIFT_ITEM_RESPONSE,
npc.dialogue.tags.GIFT_ITEM_LIKED,
self.gender,
phase)
for _, item_dialogue in pairs(dialogues_found) do
npc.chat(self.npc_name, clicker_name, item_dialogue.text)
end
-- Disliked items reactions
elseif modifier < 0 then
npc.effect({x = pos.x, y = pos.y + 1, z = pos.z}, 8, "default_item_smoke.png")
--minetest.log("Item name: "..item_name..", gender: "..self.gender)
-- There should be only one dialogue, however, it returns a key-value
-- result where we will have to do one loop
local dialogues_found = npc.relationships.get_dialogues_for_gift_item(
item_name,
npc.dialogue.tags.GIFT_ITEM_RESPONSE,
npc.dialogue.tags.GIFT_ITEM_UNLIKED,
self.gender)
-- minetest.log("Message: "..dump(message_to_send))
for _, item_dialogue in pairs(dialogues_found) do
npc.chat(self.npc_name, clicker_name, item_dialogue.text)
end
end
end
-- Receive gift function; applies relationship points as explained above
-- Also, creates a relationship object if not present
function npc.relationships.receive_gift(self, clicker)
-- Return if clicker is not offering an item
local item = npc.get_entity_wielded_item(clicker)
if item:get_name() == "" then return false end
-- Get clicker name
local clicker_name = npc.get_entity_name(clicker)
-- Create relationship if it doesn't exists
if check_relationship_exists(self, clicker_name) == false then
create_relationship(self, clicker_name)
end
-- If NPC received a gift from this person, then reject any more gifts for now
if check_npc_can_receive_gift(self, clicker_name) == false then
npc.chat(self.npc_name, clicker_name, "Thanks, but I don't need anything for now")
return false
end
-- If NPC is ready for marriage, do no accept anything else but the ring,
-- and that with only a certain chance. The self.owner is to whom is married
-- this NPC... he he.
if get_relationship_points(self, clicker_name) >=
npc.relationships.RELATIONSHIP_PHASE["phase5"].limit
and self.owner ~= clicker_name
and item:get_name() ~= "advanced_npc:marriage_ring" then
npc.chat(self.npc_name, clicker_name,
"Thank you my love, but I think that you have given me")
npc.chat(self.npc_name, clicker_name,
"enough gifts for now. Maybe we should go a step further")
-- Reset gift timer
reset_gift_timer(self, clicker_name)
return true
elseif get_relationship_points(self, clicker_name) >=
npc.relationships.RELATIONSHIP_PHASE["phase5"].limit
and item:get_name() == "advanced_npc:marriage_ring" then
-- If the player/entity is offering a marriage ring, then NPC will accept with a 50%
-- chance to marry the clicker
local receive_chance = math.random(1, 10)
-- Receive ring and get married
if receive_chance < 6 then
npc.chat(self.npc_name, clicker_name,
"Oh, oh you make me so happy! Yes! I will marry you!")
-- Get ring
item:take_item()
clicker:set_wielded_item(item)
-- TODO: Implement marriage event
-- Show marriage reaction
local pos = self.object:getpos()
effect({x = pos.x, y = pos.y + 1, z = pos.z}, 20, "heart.png", 4)
-- Give 100 points, so NPC is really happy on marriage
update_relationship(self, clicker_name, 100)
-- This sets the married state, for now. Hehe
self.owner = clicker_name
-- Reject ring for now
else
npc.chat(self.npc_name, clicker_name,
"Dear, I feel the same as you. But maybe not yet...")
end
-- Reset gift timer
reset_gift_timer(self, clicker_name)
return true
end
-- Marriage gifts: except for disliked items, all product a 0.5 * npc.ITEM_GIFT_EFFECT
-- Disliked items cause only a -0.5 point effect
if get_relationship_points(self, clicker_name) >=
npc.relationships.RELATIONSHIP_PHASE["phase5"].limit then
local modifier = 0.5 * npc.ITEM_GIFT_EFFECT
-- Check for disliked items
if item:get_name() == self.gift_data.disliked_items.dis1
or item:get_name() == self.gift_data.disliked_items.dis2 then
modifier = -0.5
show_receive_gift_reaction(self, item:get_name(), modifier, clicker_name, false)
elseif item:get_name() == self.gift_data.favorite_items.fav1
or item:get_name() == self.gift_data.favorite_items.fav2 then
-- Favorite item reaction
show_receive_gift_reaction(self, item:get_name(), modifier, clicker_name, false)
else
-- Neutral item reaction
npc.chat(self.npc_name, clicker_name, "Thank you honey!")
end
-- Take item
item:take_item()
clicker:set_wielded_item(item)
-- Update relationship
update_relationship(self, clicker_name, modifier)
-- Reset gift timer
reset_gift_timer(self, clicker_name)
return true
end
-- Modifies relationship depending on given item
local modifier = 0
local take = true
local show_reaction = false
if item:get_name() == self.gift_data.favorite_items.fav1 then
modifier = 2 * npc.relationships.ITEM_GIFT_EFFECT
show_reaction = true
elseif item:get_name() == self.gift_data.favorite_items.fav2 then
modifier = npc.relationships.ITEM_GIFT_EFFECT
show_reaction = true
elseif item:get_name() == self.gift_data.disliked_items.dis1 then
modifier = (-2) * npc.relationships.ITEM_GIFT_EFFECT
show_reaction = true
elseif item:get_name() == self.gift_data.disliked_items.dis2 then
modifier = (-1) * npc.relationships.ITEM_GIFT_EFFECT
show_reaction = true
else
-- If item is not a favorite or a dislike, then receive chance
-- if 70%
local receive_chance = math.random(1,10)
if receive_chance < 7 then
npc.chat(self.npc_name, clicker_name, "Thanks. I will find some use for this.")
else
npc.chat(self.npc_name, clicker_name, "Thank you, but no, I have no use for this.")
take = false
end
show_reaction = false
end
-- Update relationship status
local is_phase_changed = update_relationship(self, clicker_name, modifier)
-- Show NPC reaction to gift
if show_reaction == true then
show_receive_gift_reaction(self, item:get_name(), modifier, clicker_name, is_phase_changed)
end
-- Take item if NPC accepted it
if take == true then
item:take_item()
clicker:set_wielded_item(item)
end
npc.log("DEBUG", "NPC: "..dump(self))
-- Reset gift timer
reset_gift_timer(self, clicker_name)
return true
end
-- Relationships are slowly increased by talking, increases by +0.2.
-- Talking to married NPC increases relationship by +1
-- TODO: This needs a timer as the gift timer. NPC will talk anyways
-- but relationship will not increase.
function npc.relationships.dialogue_relationship_update(self, clicker)
-- Get clicker name
local clicker_name = npc.get_entity_name(clicker)
-- Check if relationship can be updated via talk
if check_relationship_by_talk_timer_ready(self, clicker_name) == false then
return
end
-- Create relationship if it doesn't exists
if check_relationship_exists(self, clicker_name) == false then
create_relationship(self, clicker_name)
end
local modifier = 0.2
if self.is_married_to ~= nil and clicker_name == self.is_married_to then
modifier = 1
end
-- Update relationship
update_relationship(self, clicker_name, modifier)
-- Resert timers
reset_talk_timer(self, clicker_name)
reset_relationship_decrease_timer(self, clicker_name)
end

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +0,0 @@
-- Spawner markers
-- Specialized functionality to allow players do NPC spawning
-- on their own custom buildings.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1018 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 901 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 903 B

View File

@ -1,159 +0,0 @@
-- Price table for items bought/sold by NPC traders by Zorman2000
-- This table should be globally accessible so that other mods can set
-- prices as they see fit.
npc.trade.prices = {
currency = {
tier1 = "tier1",
tier2 = "tier2",
tier3 = "tier3"
}
}
-- Table that contains the prices
local price_table = {}
-- Currency table
-- Define default currency (based on lumps from default)
local currency_table = {
tier1 = {string = "default:gold_lump", name = "Gold lump"},
tier2 = {string = "default:copper_lump", name = "Copper lump"},
tier3 = {string = "default:iron_lump", name = "Iron lump"}
}
-- Functions
function npc.trade.prices.update(item_name, tier, count)
for key,value in pairs(price_table) do
if key == item_name then
value = {tier=currency_table[tier].string, count=count}
return
end
end
return nil
end
function npc.trade.prices.get(item_name)
local price_entry = price_table[item_name]
if price_entry then
return price_entry
end
return nil
end
function npc.trade.prices.add(item_name, tier, count)
if npc.trade.prices.get(item_name) == nil then
price_table[item_name] = {tier=currency_table[tier].string, count=count}
else
npc.trade.prices.update(item_name, tier, count)
end
end
function npc.trade.prices.remove(item_name)
price_table[item_name] = nil
end
-- Gets all the item for a specified budget
function npc.trade.prices.get_items_for_currency_count(tier, count, price_factor)
local result = {}
--minetest.log("Currency quantity: "..dump(count))
for item_name, price in pairs(price_table) do
-- Check price currency is of the same tier
if price.tier == tier and price.count <= count then
result[item_name] = {price = price}
--minetest.log("Item name: "..dump(item_name)..", Price: "..dump(price))
local min_buying_item_count = 1
-- Calculate price NPC is going to buy for
local buying_price_count = price.count * price_factor
-- Check if the buying price is not an integer
if buying_price_count % 1 ~= 0 then
-- If not, increase the buying item count until we get an integer
local adjust = 1 / price_factor
if price.count < 1 then
adjust = 1 / (price.count * price_factor)
end
min_buying_item_count = min_buying_item_count * adjust
end
--minetest.log("Minimum item buy quantity: "..dump(min_buying_item_count))
--minetest.log("Minimum item price quantity: "..dump(buying_price_count))
-- Calculate maximum buyable quantity
local max_buying_item_count = min_buying_item_count
while ((max_buying_item_count + min_buying_item_count) * buying_price_count <= count) do
max_buying_item_count = max_buying_item_count + min_buying_item_count
end
--minetest.log("Maximum item buy quantity: "..dump(max_buying_item_count))
result[item_name].min_buyable_item_count = min_buying_item_count
result[item_name].min_buyable_item_price = buying_price_count
result[item_name].max_buyable_item_count = max_buying_item_count
end
end
--minetest.log("Final result: "..dump(result))
return result
end
-- Accepts table in format :
-- {string = "itemstring", name = "Currency Item Name"}
function npc.trade.prices.set_currency(tier1, tier2, tier3)
currency_table = {
tier1 = tier1,
tier2 = tier2,
tier3 = tier3
}
end
function npc.trade.prices.get_currency_name(tier)
return currency_table[tier].name
end
function npc.trade.prices.get_currency_itemstring(tier)
return currency_table[tier].string
end
-- This method will compare the given item string to the
-- currencies set in the currencies table. Returns true if
-- itemstring is a currency.
function npc.trade.prices.is_item_currency(itemstring)
if npc.get_item_name(itemstring) == currency_table.tier3.string
or npc.get_item_name(itemstring) == currency_table.tier2.string
or npc.get_item_name(itemstring) == currency_table.tier1.string then
return true
end
return false
end
-- Default definitions for in-game items
-- Tier 3 items: cheap items
price_table["default:cobble"] = {tier = currency_table["tier3"].string, count = 0.1}
price_table["flowers:geranium"] = {tier = currency_table["tier3"].string, count = 0.5}
price_table["default:apple"] = {tier = currency_table["tier3"].string, count = 1}
price_table["vessels:drinking_glass"] = {tier = currency_table["tier3"].string, count = 1}
price_table["default:tree"] = {tier = currency_table["tier3"].string, count = 2}
price_table["flowers:rose"] = {tier = currency_table["tier3"].string, count = 2}
price_table["flowers:dandelion_yellow"]= {tier = currency_table["tier3"].string, count = 2}
price_table["flowers:dandelion_white"] = {tier = currency_table["tier3"].string, count = 2}
price_table["default:stone"] = {tier = currency_table["tier3"].string, count = 2}
price_table["farming:seed_cotton"] = {tier = currency_table["tier3"].string, count = 3}
price_table["farming:seed_wheat"] = {tier = currency_table["tier3"].string, count = 3}
price_table["default:clay_lump"] = {tier = currency_table["tier3"].string, count = 3}
price_table["default:wood"] = {tier = currency_table["tier3"].string, count = 3}
price_table["mobs:meat_raw"] = {tier = currency_table["tier3"].string, count = 4}
price_table["flowers:chrysanthemum_green"] = {tier = currency_table["tier3"].string, count = 4}
price_table["default:sapling"] = {tier = currency_table["tier3"].string, count = 5}
price_table["mobs:meat"] = {tier = currency_table["tier3"].string, count = 5}
price_table["mobs:leather"] = {tier = currency_table["tier3"].string, count = 6}
price_table["default:sword_stone"] = {tier = currency_table["tier3"].string, count = 6}
price_table["default:shovel_stone"] = {tier = currency_table["tier3"].string, count = 6}
price_table["default:axe_stone"] = {tier = currency_table["tier3"].string, count = 6}
price_table["farming:hoe_stone"] = {tier = currency_table["tier3"].string, count = 6}
price_table["default:pick_stone"] = {tier = currency_table["tier3"].string, count = 7}
price_table["bucket:bucket_empty"] = {tier = currency_table["tier3"].string, count = 10}
price_table["farming:cotton"] = {tier = currency_table["tier3"].string, count = 15}
price_table["farming:bread"] = {tier = currency_table["tier3"].string, count = 20}
-- Tier 2 items: medium priced items
-- Tier 1 items: expensive items
price_table["default:mese_crystal"] = {tier = currency_table["tier1"].string, count = 45}
price_table["default:diamond"] = {tier = currency_table["tier1"].string, count = 90}
price_table["advanced_npc:marriage_ring"] = {tier = currency_table["tier1"].string, count = 100}

View File

@ -1,771 +0,0 @@
-- Trading code for Advanced NPC by Zorman2000
npc.trade = {}
npc.trade.CASUAL = "casual"
npc.trade.TRADER = "trader"
npc.trade.NONE = "none"
npc.trade.OFFER_BUY = "buy"
npc.trade.OFFER_SELL = "sell"
-- This variable establishes how much items a dedicated
-- trader will buy until retiring the offer
npc.trade.DEDICATED_MAX_BUY_AMOUNT = 5
-- This table holds all responses for trades
npc.trade.results = {
single_trade_offer = {},
trade_offers = {},
custom_trade_offer = {}
}
-- This is the text to be shown each time the NPC has more
-- than one custom trade options to choose from
npc.trade.CUSTOM_TRADES_PROMPT_TEXT = "Hi there, how can I help you today?"
-- Casual trader NPC dialogues definition
-- Casual buyer
npc.dialogue.register_dialogue({
text = "I'm looking to buy some items, are you interested?",
--casual_trade_type = npc.trade.OFFER_BUY,
tags = {"default_casual_trade_dialogue", "buy_offer"},
--dialogue_type = npc.dialogue.dialogue_type.casual_trade,
responses = {
[1] = {
text = "Sell",
action_type = "function",
response_id = 1,
action = function(self, player)
npc.trade.show_trade_offer_formspec(self, player, npc.trade.OFFER_BUY)
end
}
}
})
-- npc.trade.CASUAL_TRADE_BUY_DIALOGUE = {
-- text = "I'm looking to buy some items, are you interested?",
-- casual_trade_type = npc.trade.OFFER_BUY,
-- dialogue_type = npc.dialogue.dialogue_type.casual_trade,
-- responses = {
-- [1] = {
-- text = "Sell",
-- action_type = "function",
-- response_id = 1,
-- action = function(self, player)
-- npc.trade.show_trade_offer_formspec(self, player, npc.trade.OFFER_BUY)
-- end
-- }
-- }
-- }
-- Casual seller
npc.dialogue.register_dialogue({
text = "I have some items to sell, are you interested?",
--dialogue_type = npc.dialogue.dialogue_type.casual_trade,
tags = {"default_casual_trade_dialogue", "sell_offer"},
--casual_trade_type = npc.trade.OFFER_SELL,
responses = {
[1] = {
text = "Buy",
action_type = "function",
response_id = 1,
action = function(self, player)
npc.trade.show_trade_offer_formspec(self, player, npc.trade.OFFER_SELL)
end
}
}
})
-- npc.trade.CASUAL_TRADE_SELL_DIALOGUE = {
-- text = "I have some items to sell, are you interested?",
-- dialogue_type = npc.dialogue.dialogue_type.casual_trade,
-- casual_trade_type = npc.trade.OFFER_SELL,
-- responses = {
-- [1] = {
-- text = "Buy",
-- action_type = "function",
-- response_id = 1,
-- action = function(self, player)
-- npc.trade.show_trade_offer_formspec(self, player, npc.trade.OFFER_SELL)
-- end
-- }
-- }
-- }
-- Dedicated trade dialogue prompt
npc.dialogue.register_dialogue({
text = "Hello there, would you like to trade?",
tags = {npc.dialogue.tags.DEFAULT_DEDICATED_TRADE},
dialogue_type = npc.dialogue.dialogue_type.dedicated_trade,
responses = {
[1] = {
text = "Buy",
action_type = "function",
response_id = 1,
action = function(self, player)
npc.trade.show_dedicated_trade_formspec(self, player, npc.trade.OFFER_SELL)
end
},
[2] = {
text = "Sell",
action_type = "function",
response_id = 2,
action = function(self, player)
npc.trade.show_dedicated_trade_formspec(self, player, npc.trade.OFFER_BUY)
end
},
[3] = {
text = "Other",
action_type = "function",
response_id = 3,
action = function(self, player)
local dialogue = npc.dialogue.create_custom_trade_options(self, player)
npc.dialogue.process_dialogue(self, dialogue, player:get_player_name())
end
}
}
})
-- npc.trade.DEDICATED_TRADER_PROMPT = {
-- text = "Hello there, would you like to trade?",
-- dialogue_type = npc.dialogue.dialogue_type.dedicated_trade,
-- responses = {
-- [1] = {
-- text = "Buy",
-- action_type = "function",
-- response_id = 1,
-- action = function(self, player)
-- npc.trade.show_dedicated_trade_formspec(self, player, npc.trade.OFFER_SELL)
-- end
-- },
-- [2] = {
-- text = "Sell",
-- action_type = "function",
-- response_id = 2,
-- action = function(self, player)
-- npc.trade.show_dedicated_trade_formspec(self, player, npc.trade.OFFER_BUY)
-- end
-- },
-- [3] = {
-- text = "Other",
-- action_type = "function",
-- response_id = 3,
-- action = function(self, player)
-- local dialogue = npc.dialogue.create_custom_trade_options(self, player)
-- npc.dialogue.process_dialogue(self, dialogue, player:get_player_name())
-- end
-- }
-- }
-- }
function npc.trade.show_trade_offer_formspec(self, player, offer_type)
-- Strings for formspec, to include international support later
local prompt_string = " offers to buy from you"
local for_string = "for"
local buy_sell_string = "Sell"
-- Get offer. As this is casual trading, NPCs will only have
-- one trade offer
local trade_offer = self.trader_data.buy_offers[1]
if offer_type == npc.trade.OFFER_SELL then
trade_offer = self.trader_data.sell_offers[1]
prompt_string = " wants to sell to you"
buy_sell_string = "Buy"
end
local formspec = "size[8,4]"..
default.gui_bg..
default.gui_bg_img..
default.gui_slots..
"label[2,0.1;"..self.npc_name..prompt_string.."]"..
"item_image_button[2,1.3;1.2,1.2;"..trade_offer.item..";item;]"..
"label[3.75,1.75;"..for_string.."]"..
"item_image_button[4.8,1.3;1.2,1.2;"..trade_offer.price[1]..";price;]"..
"button_exit[1,3.3;2.9,0.5;yes_option;"..buy_sell_string.."]"..
"button_exit[4.1,3.3;2.9,0.5;no_option;"..npc.dialogue.NEGATIVE_ANSWER_LABEL.."]"
-- Create entry into results table
npc.trade.results.single_trade_offer[player:get_player_name()] = {
trade_offer = trade_offer,
npc = self
}
-- Show formspec to player
minetest.show_formspec(player:get_player_name(), "advanced_npc:trade_offer", formspec)
end
function npc.trade.show_dedicated_trade_formspec(self, player, offers_type)
-- Choose the correct offers
local offers = self.trader_data.buy_offers
local menu_offer_type = "sell"
if offers_type == npc.trade.OFFER_SELL then
offers = self.trader_data.sell_offers
menu_offer_type = "buy"
end
-- Create a grid with the items for trade offer
local max_columns = 4
local current_x = 0.2
local current_y = 0.5
local current_col = 1
local current_row = 1
local formspec = "size[8.9,8.2]"..
default.gui_bg..
default.gui_bg_img..
default.gui_slots..
"label[0.2,0.05;Click on the price button to "..menu_offer_type.." item]"
for i = 1, #offers do
local price_item_name = minetest.registered_items[npc.get_item_name(offers[i].price[1])].description
local count_label = ""
if npc.get_item_count(offers[i].item) > 1 then
count_label = "label["..(current_x + 1.35)..","..(current_y + 1)..";"..npc.get_item_count(offers[i].item).."]"
end
formspec = formspec..
"box["..current_x..","..current_y..";2,2.3;#212121]"..
"item_image_button["..(current_x + 0.45)..","..(current_y + 0.15)..";1.3,1.3;"..npc.get_item_name(offers[i].item)..";item"..i..";]"..
count_label..
"item_image_button["..(current_x + 1.15)..","..(current_y + 1.4)..";1,1;"..offers[i].price[1]..";price"..i..";]"..
"label["..(current_x + 0.15)..","..(current_y + 1.7)..";Price]"
current_x = current_x + 2.1
current_col = current_col + 1
if current_col > 4 then
current_col = 1
current_x = 0.2
current_y = current_y + 2.4
end
end
formspec = formspec .. "button_exit[2.5,7.9;3.9,0.5;exit;Nevermind]"
-- Create entry into results table
npc.trade.results.trade_offers[player:get_player_name()] = {
offers_type = offers_type,
offers = offers,
npc = self
}
minetest.show_formspec(player:get_player_name(), "advanced_npc:dedicated_trading_offers", formspec)
end
-- For the moment, the trade offer for custom trade is always of sell type
function npc.trade.show_custom_trade_offer(self, player, offer)
local for_string = "for"
-- Create payments grid. Try to center it. When there are 4
-- payment options, a grid is to be displayed.
local price_count = #offer.price
local start_x = 2
local margin_x = 0
local start_y = 1.45
if price_count == 2 then
start_x = 1.5
margin_x = 0.3
elseif price_count == 3 then
start_x = 1.15
margin_x = 0.85
elseif price_count == 4 then
start_x = 1.5
start_y = 0.8
margin_x = 0.3
end
-- Create payment grid
local price_grid = ""
for i = 1, #offer.price do
price_grid = price_grid.."item_image_button["..start_x..","..start_y..";1,1;"..offer.price[i]..";price"..i..";]"
if #offer.price == 4 and i == 2 then
start_x = 1.5
start_y = start_y + 1
else
start_x = start_x + 1
end
end
local formspec = "size[8,4]"..
default.gui_bg..
default.gui_bg_img..
default.gui_slots..
"label[2,0.1;"..self.npc_name..": "..offer.dialogue_prompt.."]"..
price_grid..
"label["..(margin_x + 3.75)..",1.75;"..for_string.."]"..
"item_image_button["..(margin_x + 4.8)..",1.3;1.2,1.2;"..offer.item..";item;]"..
"button_exit[1,3.3;2.9,0.5;yes_option;"..offer.button_prompt.."]"..
"button_exit[4.1,3.3;2.9,0.5;no_option;"..npc.dialogue.NEGATIVE_ANSWER_LABEL.."]"
-- Create entry into results table
npc.trade.results.custom_trade_offer[player:get_player_name()] = {
trade_offer = offer,
npc = self
}
-- Show formspec to player
minetest.show_formspec(player:get_player_name(), "advanced_npc:custom_trade_offer", formspec)
end
function npc.trade.get_random_trade_status()
local chance = math.random(1,10)
if chance < 3 then
-- Non-trader
return npc.trade.NONE
elseif 3 <= chance and chance <= 7 then
-- Casual trader
return npc.trade.CASUAL
elseif chance > 7 then
-- Trader by profession
return npc.trade.TRADER
end
end
-- This function generates and stores on the NPC data trade
-- offers depending on the trader status.
function npc.trade.generate_trade_offers_by_status(self)
-- Get trader status
local status = self.trader_data.trader_status
-- Check what is the trader status
if status == npc.trade.NONE then
-- For none, clear all offers
self.trader_data.buy_offers = {}
self.trader_data.sell_offers = {}
elseif status == npc.trade.CASUAL then
-- For casual, generate one buy and one sell offer
self.trader_data.buy_offers = {
[1] = npc.trade.get_casual_trade_offer(self, npc.trade.OFFER_BUY)
}
self.trader_data.sell_offers = {
[1] = npc.trade.get_casual_trade_offer(self, npc.trade.OFFER_SELL)
}
elseif status == npc.trade.TRADER then
-- Clear current offers
self.trader_data.buy_offers = {}
self.trader_data.sell_offers = {}
-- Get trade offers for a dedicated trader
local offers = npc.trade.get_dedicated_trade_offers(self)
-- Store buy offers
for i = 1, #offers.buy do
table.insert(self.trader_data.buy_offers, offers.buy[i])
end
-- Store sell offers
for i = 1, #offers.sell do
table.insert(self.trader_data.sell_offers, offers.sell[i])
end
end
end
-- Convenience method that retrieves all the currency
-- items that a NPC has on his/her inventory
function npc.trade.get_currencies_in_inventory(self)
local result = {}
local tier3 = npc.inventory_contains(self, npc.trade.prices.get_currency_itemstring("tier3"))
local tier2 = npc.inventory_contains(self, npc.trade.prices.get_currency_itemstring("tier2"))
local tier1 = npc.inventory_contains(self, npc.trade.prices.get_currency_itemstring("tier1"))
if tier3 ~= nil then
table.insert(result, {name = npc.get_item_name(tier3.item_string),
count = npc.get_item_count(tier3.item_string)} )
end
if tier2 ~= nil then
table.insert(result, {name = npc.get_item_name(tier2.item_string),
count = npc.get_item_count(tier2.item_string)} )
end
if tier1 ~= nil then
table.insert(result, {name = npc.get_item_name(tier1.item_string),
count = npc.get_item_count(tier1.item_string)} )
end
--minetest.log("Found currency in inventory: "..dump(result))
return result
end
-- This function will return an offer object, based
-- on the items the NPC has.
function npc.trade.get_casual_trade_offer(self, offer_type)
local result = {}
-- Check offer type
if offer_type == npc.trade.OFFER_BUY then
-- Create buy offer based on what the NPC can actually buy
local currencies = npc.trade.get_currencies_in_inventory(self)
-- Choose a random currency
local chosen_tier = currencies[math.random(#currencies)]
-- Get items for this currency
local buyable_items =
npc.trade.prices.get_items_for_currency_count(chosen_tier.name, chosen_tier.count, 0.5)
-- Select a random item from the buyable items
local item_set = {}
for item,price_info in pairs(buyable_items) do
table.insert(item_set, item)
end
local item = item_set[math.random(#item_set)]
-- Choose buying quantity. Since this is a buy offer, NPC will buy items
-- at half the price. Therefore, NPC will always ask for even quantities
-- so that the price count is always an integer number
local amount_to_buy = math.random(buyable_items[item].min_buyable_item_count, buyable_items[item].max_buyable_item_count)
-- Create trade offer
--minetest.log("Buyable item: "..dump(buyable_items[item]))
result = npc.trade.create_offer(npc.trade.OFFER_BUY, item, buyable_items[item].price, buyable_items[item].min_buyable_item_price, amount_to_buy)
else
-- Make sell offer, NPC will sell items to player at regular price
-- NPC will also offer items from their inventory
local sellable_items = {}
for i = 1, #self.inventory do
if self.inventory[i] ~= "" then
if npc.trade.prices.is_item_currency(self.inventory[i]) == false then
table.insert(sellable_items, self.inventory[i])
end
end
end
-- Check if there are no sellable items to avoid crash
if #sellable_items > 0 then
-- Choose a random item from the sellable items
local item = sellable_items[math.random(#sellable_items)]
-- Choose how many of this item will be sold to player
local count = math.random(npc.get_item_count(item))
-- Create trade offer
result = npc.trade.create_offer(npc.trade.OFFER_SELL, npc.get_item_name(item), nil, nil, count)
end
end
return result
end
-- The following function create buy and sell offers for dedicated traders,
-- based on the trader list and the source of items. Initially, it will only
-- be NPC inventories. In the future, it should support both NPC and chest
-- inventories,
function npc.trade.get_dedicated_trade_offers(self)
local offers = {
sell = {},
buy = {}
}
local trade_list = self.trader_data.trade_list
npc.log("INFO", "NPC Inventory: "..dump(self.inventory))
for item_name, trade_info in pairs(trade_list) do
-- Abort if more than 12 buy or sell offers are made
if table.getn(offers.sell) >= 12 or table.getn(offers.buy) >= 12 then
break
end
-- For each item on the trader list, check if it is in the NPC inventory.
-- If it is, create a sell offer, else create a buy offer if possible.
-- Also, avoid creating sell offers immediately if the item was just bought
local item = npc.inventory_contains(self, item_name)
minetest.log("Searched item: "..dump(item_name))
minetest.log("Found: "..dump(item))
if item ~= nil and trade_info.last_offer_type ~= npc.trade.OFFER_BUY then
-- Check if item can be sold
if trade_info.item_sold_count == nil
or (trade_info.item_sold_count ~= nil
and (trade_info.max_item_sell_count
and trade_info.item_sold_count < trade_info.max_item_sell_count)) then
-- This check makes sure that the NPC will keep max_item_sell_count at any time.
if trade_info.amount_to_keep == nil or (trade_info.amount_to_keep ~= nil
and trade_info.amount_to_keep < ItemStack(item.item_string):get_count()) then
-- Create sell offer for this item. Currently, traders will offer to sell only
-- one of their items to allow the fine control for players to buy what they want.
-- This requires, however, that the trade offers are re-generated everytime a
-- sell is made.
table.insert(offers.sell, npc.trade.create_offer(
npc.trade.OFFER_SELL,
item_name,
nil,
nil,
1)
)
-- Set last offer type
trade_info.last_offer_type = npc.trade.OFFER_SELL
end
else
-- Clear the trade info for this item
trade_info.item_sold_count = 0
end
else
-- Avoid flipping an item to the buy side if the stock was just depleted
if trade_info.last_offer_type ~= npc.trade.OFFER_SELL then
-- Create buy offer for this item
-- Only do if the NPC can actually afford the items.
local currencies = npc.trade.get_currencies_in_inventory(self)
-- Check if currency isn't empty
if #currencies > 0 then
-- Choose a random currency
local chosen_tier = currencies[math.random(#currencies)]
-- Get items for this currency
local buyable_items =
npc.trade.prices.get_items_for_currency_count(chosen_tier.name, chosen_tier.count, 0.5)
-- Check if the item from trader list is present in the buyable items list
for buyable_item, price_info in pairs(buyable_items) do
if buyable_item == item_name then
-- If item found, create a buy offer for this item
-- Again, offers are created for one item only. Buy offers should be removed
-- after the NPC has bought a certain quantity, say, 5 items.
if trade_info.item_bought_count == nil
or (trade_info.item_bought_count ~= nil
and (trade_info.max_item_buy_count and trade_info.item_bought_count <= trade_info.max_item_buy_count
or trade_info.item_bought_count <= npc.trade.DEDICATED_MAX_BUY_AMOUNT)) then
-- Create trade offer for this item
table.insert(offers.buy, npc.trade.create_offer(
npc.trade.OFFER_BUY,
item_name,
price_info.price,
price_info.min_buyable_item_price,
price_info.min_buyable_item_count)
)
-- Set last offer type
trade_info.last_offer_type = npc.trade.OFFER_BUY
else
-- Clear the trade info for this item
trade_info.item_bought_count = 0
end
end
end
end
end
end
end
return offers
end
-- Creates a trade offer based on the offer type, given item and count. If
-- the offer is a "buy" offer, it is required to provide the price item and
-- the minimum price item count.
function npc.trade.create_offer(offer_type, item, price, min_price_item_count, count)
local result = {}
-- Check offer type
if offer_type == npc.trade.OFFER_BUY then
-- Get price for the given item
-- Create price itemstring
local price_string = price.tier.." "
..tostring( min_price_item_count * count )
-- Build the return object
-- Price is always an array, in this case of size 1
result = {
offer_type = offer_type,
item = item.." "..count,
price = {[1] = price_string}
}
else
-- Make sell offer, NPC will sell items to player at regular price
-- Get and calculate price for this object
local price_object = npc.trade.prices.get(item)
if price_object == nil then
npc.log("WARNING", "Found nil price for item: "..dump(item))
return nil
end
-- Check price object, if price < 1 then offer to sell for 1
if price_object.count < 1 then
price_object.count = 1
end
local price_string = price_object.tier.." "..tostring(price_object.count * count)
-- Build return object
-- Price is always an array, in this case of size 1
result = {
offer_type = offer_type,
item = npc.get_item_name(item).." "..count,
price = {[1] = price_string}
}
end
return result
end
-- A custom sell trade offer is a special type of trading the NPC can
-- have where a different prompt and multiple payment objects are
-- required from the player. A good example is offering to repair a sword,
-- where the player has to give an amount of currency and the sword to
-- repair in exchange to get a fully repaired sword.
-- For the moment being, only sell is supported.
function npc.trade.create_custom_sell_trade_offer(option_prompt, dialogue_prompt, button_prompt, item, payments)
return {
offer_type = npc.OFFER_SELL,
option_prompt = option_prompt,
dialogue_prompt = dialogue_prompt,
button_prompt = button_prompt,
item = item,
price = payments
}
end
-- TODO: This method needs to be refactored to be able to manage
-- both NPC inventories and chest inventories.
-- Returns true if trade was possible, else returns false.
function npc.trade.perform_trade(self, player_name, offer)
local item_stack = ItemStack(offer.item)
-- Create item stacks for each price item
local price_stacks = {}
for i = 1, #offer.price do
table.insert(price_stacks, ItemStack(offer.price[i]))
end
local inv = minetest.get_inventory({type = "player", name = player_name})
-- Check if offer is a buy or sell
if offer.offer_type == npc.trade.OFFER_BUY then
-- If NPC is buying from player, then player loses item, gets price
-- Check player has the item being buyed
if inv:contains_item("main", item_stack) then
-- Check if there is enough room to add the price item to player
for i = 1, #price_stacks do
if inv:room_for_item("main", price_stacks[i]) then
-- Remove item from player
inv:remove_item("main", item_stack)
-- Remove price item(s) from NPC
for j = 1, #price_stacks do
npc.take_item_from_inventory_itemstring(self, price_stacks[j])
end
-- Add item to NPC's inventory
npc.add_item_to_inventory_itemstring(self, offer.item)
-- Add price items to player
for j = 1, #price_stacks do
inv:add_item("main", price_stacks[j])
end
-- Send message to player
npc.chat(self.npc_name, player_name, "Thank you!")
return true
else
npc.chat(self.npc_name, player_name,
"Looks like you can't get what I'm giving you for payment!")
return false
end
end
else
npc.chat(self.npc_name, player_name,
"Looks like you don't have what I want to buy...")
return false
end
else
-- If NPC is selling to the player, then player gives price and gets
-- item, NPC loses item and gets price.
for i = 1, #price_stacks do
-- Check NPC has the required item to pay
if inv:contains_item("main", price_stacks[i]) then
-- Check if there is enough room to add the item to player
if inv:room_for_item("main", item_stack) then
-- Remove price item from player
for j = 1, #price_stacks do
inv:remove_item("main", price_stacks[j])
end
-- Remove sell item from NPC
npc.take_item_from_inventory_itemstring(self, offer.item)
-- Add price to NPC's inventory
for i = 1, #offer.price do
npc.add_item_to_inventory_itemstring(self, offer.price[i])
end
-- Add item items to player
inv:add_item("main", item_stack)
-- Send message to player
npc.chat(self.npc_name, player_name, "Thank you!")
return true
else
npc.chat(self.npc_name, player_name,
"Looks like you can't carry anything else...")
return false
end
else
npc.chat(self.npc_name, player_name,
"Looks like you don't have what I'm asking for!")
return false
end
end
end
end
-- Handler for chat formspec
minetest.register_on_player_receive_fields(function (player, formname, fields)
-- Additional checks for other forms should be handled here
-- Handle casual trade dialogue
if formname == "advanced_npc:trade_offer" then
local player_name = player:get_player_name()
if fields then
local player_response = npc.trade.results.single_trade_offer[player_name]
-- Unlock the action timer
npc.exec.set_ready_state(player_response.npc)
if fields.yes_option then
npc.trade.perform_trade(player_response.npc, player_name, player_response.trade_offer)
elseif fields.no_option then
minetest.chat_send_player(player_name, "Talk to me if you change your mind!")
end
end
elseif formname == "advanced_npc:dedicated_trading_offers" then
local player_name = player:get_player_name()
if fields then
local player_response = npc.trade.results.trade_offers[player_name]
-- Unlock the action timer
npc.exec.set_ready_state(player_response.npc)
local trade_offers = npc.trade.results.trade_offers[player_name].offers
-- Check which price was clicked
for i = 1, #trade_offers do
local price_button = "price"..tostring(i)
if fields[price_button] then
local trade_result = npc.trade.perform_trade(player_response.npc, player_name, trade_offers[i])
if trade_result == true then
-- Lock actions
npc.exec.set_input_wait_state(player_response.npc)
-- Account for buyed items
if player_response.offers_type == npc.trade.OFFER_BUY then
-- Increase the item bought count
local offer_item_name = npc.get_item_name(trade_offers[i].item)
--minetest.log("Bought item name: "..dump(offer_item_name))
--minetest.log(dump(player_response.npc.trader_data.trade_list[offer_item_name]))
-- Check if this item has been bought before
if player_response.npc.trader_data.trade_list[offer_item_name].item_bought_count == nil then
-- Set first count to 1
player_response.npc.trader_data.trade_list[offer_item_name].item_bought_count = 1
else
-- Increase count
player_response.npc.trader_data.trade_list[offer_item_name].item_bought_count
= player_response.npc.trader_data.trade_list[offer_item_name].item_bought_count + 1
end
else
-- Also count how many items are sold
local offer_item_name = npc.get_item_name(trade_offers[i].item)
-- Check if this item has been sold before
if player_response.npc.trader_data.trade_list[offer_item_name].item_sold_count == nil then
-- Set first count to 1
player_response.npc.trader_data.trade_list[offer_item_name].item_sold_count = 1
else
-- Increase count
player_response.npc.trader_data.trade_list[offer_item_name].item_sold_count
= player_response.npc.trader_data.trade_list[offer_item_name].item_sold_count + 1
end
end
-- Re-generate trade offers
npc.trade.generate_trade_offers_by_status(player_response.npc)
-- Show refreshed formspec again to player
npc.trade.show_dedicated_trade_formspec(player_response.npc, player, player_response.offers_type)
return true
else
minetest.close_formspec(player_name, "advanced_npc:dedicated_trading_offers")
return false
end
--minetest.log("Player selected: "..dump(trade_offers[i]))
end
end
end
elseif formname == "advanced_npc:custom_trade_offer" then
-- Handle custom trade formspec
local player_name = player:get_player_name()
if fields then
local player_response = npc.trade.results.custom_trade_offer[player_name]
-- Unlock the action timer
npc.exec.set_ready_state(player_response.npc)
if fields.yes_option then
npc.trade.perform_trade(player_response.npc, player_name, player_response.trade_offer)
elseif fields.no_option then
minetest.chat_send_player(player_name, "Talk to me if you change your mind!")
end
end
end
end)

View File

@ -1,97 +0,0 @@
-- Basic utilities to work with table operations in Lua, and specific querying
-- By Zorman2000
npc.utils = {}
function npc.utils.split(inputstr, sep)
if sep == nil then
sep = "%s"
end
local t={}
local i=1
for str in string.gmatch(inputstr, "([^"..sep.."]+)") do
t[i] = str
i = i + 1
end
return t
end
function npc.utils.array_contains(array, item)
--minetest.log("Array: "..dump(array))
--minetest.log("Item being searched: "..dump(item))
for i = 1, #array do
--minetest.log("Equals? "..dump(array[i] == item))
if array[i] == item then
return true
end
end
return false
end
function npc.utils.array_is_subset_of_array(set, subset)
local match_count = 0
for j = 1, #subset do
for k = 1, #set do
if subset[j] == set[k] then
match_count = match_count + 1
end
end
end
-- Check match count
return match_count == #subset
end
function npc.utils.get_map_keys(map)
local result = {}
for key, _ in pairs(map) do
table.insert(result, key)
end
return result
end
function npc.utils.get_map_values(map)
local result = {}
for _, value in pairs(map) do
table.insert(result, value)
end
return result
end
-- This function searches for a node given the conditions specified in the
-- query object, starting from the given start_pos and up to a certain, specified
-- range.
-- Query object:
-- search_type: determines the direction to search nodes.
-- Valid values are: orthogonal, cross, cube
-- - orthogonal search means only nodes which are parallel to the search node's faces
-- will be considered. This limits the search to only 6 nodes.
-- - cross search will look at the same nodes as orthogonal, plus will also
-- check nodes diagonal to the node four horizontal nodes. This search looks at 14 nodes
-- - cube search means to look every node surrounding the node, including all diagonals.
-- This search looks at 26 nodes.
-- search_nodes: array of nodes to search for
-- surrounding_nodes: object specifying which neighbor nodes are to be expected and
-- at which locations. Valid keys are:
-- - North (+Z dir)
-- - East (+x dir)
-- - South (-Z dir)
-- - West (-X dir)
-- - Top (+Y dir)
-- - Bottom (-Y dir)
-- Example: ["bottom"] = {nodes={"default:dirt"}, criteria="all"}
-- Each object will contain nodes, and criteria for acceptance.
-- Criteria values are:
-- - any: true as long as one of the nodes on this side is one of the specified
-- in "nodes"
-- - all: true when the set of neighboring nodes on this side contain one or many of
-- the specified "nodes"
-- - all-exact: true when the set of neighboring nodes on this side contain all nodes
-- specified in "nodes"
-- - shape: true when the set of neighboring nodes on this side contains nodes in
-- the exact given shape. If so, nodes will not be an array, but a 2d array
-- of three rows and three columns, with the specific shape. Notice that
-- the nodes on the side can vary depending on the search type (orthogonal,
-- cross, cube)
function npc.utils.search_node(query, start_pos, range)
end