15 Commits

Author SHA1 Message Date
4c0e2b574a Temporary fix for male textures issue. 2017-08-11 10:44:57 -04:00
bf935fd091 Dialogues: Fix bug with NPCs being frozen if 'Esc' button was pressed to exit dialogue 2017-06-29 19:01:03 -04:00
6141af11aa Relationships: Increase gift timer and relationship decrease timer to more usual values.
Fixed small bug on giving unliked items.
2017-06-27 18:39:13 -04:00
e70888c3e5 Fix bug with 'ignore' nodes when some map areas are not fully loaded but NPC try to perform actions on them. 2017-06-27 18:38:28 -04:00
cc56446206 NPC: (WIP) Schedule properties support adding one or multiple items to NPC inventory, and support taking one item from NPC inventory.
Attempt to fix child textures issue (2).
2017-06-21 21:07:36 -04:00
7110c49b42 Spawner: Avoid multiple spawning by not replacing the mg_villages:plotmarker, but just override its definition.
NPC: (WIP) Support change properties on schedules.
2017-06-21 07:16:11 -04:00
fd4cec0d63 Spawner: Assign sits, furnaces and storage nodes to spawned NPCs.
Small basic schedule change: NPCs now sit from 12-1 on whatever sit they 'own'.
Fixed /restore_plotmarkers not clearing all metadata.
NPC: Attempted to add a fix for the children growing on their own (due to mobs_redo).
2017-06-19 21:03:24 -04:00
3df43ab580 Spawner: Assign sits, furnaces and storage nodes to spawned NPCs.
Small basic schedule change: NPCs now sit from 12-1 on whatever sit they 'own'.
Fixed /restore_plotmarkers not clearing all metadata.
NPC: Attempted to add a fix for the children growing on their own (due to mobs_redo).
2017-06-19 20:54:26 -04:00
c19ea70242 Spawner: Add clearing of 'replaced' string to /restore_plotmarkers 2017-06-18 11:19:33 -04:00
a3b428fe14 Fix README.txt and restore correct spawning frequency. 2017-06-17 13:21:24 -04:00
4814c16ba0 Remove debug.txt 2017-06-17 13:00:54 -04:00
2530918fe9 Fix various bugs 2017-06-17 12:55:50 -04:00
fb549e7f93 Pathfinder: Drop jumper.lua's pathfinder in favor of MarkBu's pathfinder with slight modifications, as the later one supports 3D paths.
Clean up old pathfinder code.
Add path decoration to MarkBu pathfinder's generated paths.
Change actions.lua and places.lua to use the new pathfinder.
Properly modify README.md.
2017-06-17 11:47:51 -04:00
e1bf931064 Relationships: change texture of smoke to use default's mod texture 2017-06-17 11:45:57 -04:00
3e006ac828 Add proper description.txt, depends.txt 2017-06-17 11:15:40 -04:00
20 changed files with 754 additions and 412966 deletions

View File

@ -4,25 +4,40 @@ advanced_npc
Introduction
------------
Advanced NPC is a mod for Minetest, based on mobs_redo.
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.
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 will spawn on stone (default:stone) and the mg_villages' plotmarkers (mg_villages:plotmarker). The spawning is not controlled, so you will have several of them walking around. This is not how it is planned and is just for testing purposes. In the future, only a handful of NPCs should spawn at village house's plotmarker and they will know their way around the house and have specific jobs.
__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)
__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 `jumper.lua` file contains code based on the [Jumper library](https://github.com/Yonaba/Jumper), which is Copyright (c) 2012-2013 Roland Yonaba, licensed under MIT license. See `actions/jumper.lua` 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

View File

@ -599,11 +599,15 @@ function npc.actions.use_bed(self, args)
end
local action = args.action
local node = minetest.get_node(pos)
minetest.log(dump(node))
--minetest.log(dump(node))
local dir = minetest.facedir_to_dir(node.param2)
if action == npc.actions.const.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.actions.nodes.beds[node.name].get_lay_pos(pos, dir)
-- Sit down on bed, rotate to correct direction
npc.add_action(self, npc.actions.cmd.SIT, {pos=bed_pos, dir=(node.param2 + 2) % 4})
@ -611,6 +615,10 @@ function npc.actions.use_bed(self, args)
npc.add_action(self, npc.actions.cmd.LAY, {})
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.actions.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
@ -662,6 +670,10 @@ function npc.actions.use_sittable(self, args)
if action == npc.actions.const.sittable.SIT then
-- Calculate position depending on bench
-- Error here due to ignore. Need to come up with better solution
if node.name == "ignore" then
return
end
local sit_pos = npc.actions.nodes.sittable[node.name].get_sit_pos(pos, node.param2)
-- Sit down on bench/chair/stairs
npc.add_action(self, npc.actions.cmd.SIT, {pos=sit_pos, dir=(node.param2 + 2) % 4})
@ -746,20 +758,9 @@ function npc.actions.walk_to_pos(self, args)
end
-- Find path
--local path = pathfinder.find_path(start_pos, end_pos, 20, walkable_nodes)
local path = pathfinder.find_path(start_pos, end_pos, self)
local path = npc.pathfinder.find_path(start_pos, end_pos, self, true)
if path ~= nil and #path > 1 then
-- Get details from path nodes
-- This might get moved to proper place, pathfinder.lua code
local path_detail = {}
for i = 1, #path do
local node = minetest.get_node(path[i])
table.insert(path_detail, {pos=path[i], type=npc.pathfinder.is_good_node(node, {})})
end
path = path_detail
npc.log("DEBUG", "Detailed path: "..dump(path))
npc.log("INFO", "walk_to_pos Found path to node: "..minetest.pos_to_string(end_pos))
-- Store path
self.actions.walking.path = path

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,13 @@
-- Pathfinding code by Zorman2000
-- 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
---------------------------------------------------------------------------------------
-- This class contains functions that allows to map the 3D map of Minetest into
-- a 2D array (basically by ignoring the y coordinate for the moment being) in order
-- to use the A* pathfinding algorithm to find the shortest path from one node to
-- another. The A* algorithm implementation is in the 'jumper.lua' file which a
-- reduced and slightly modified version of the Jumper library, by Roland Yonaba
-- (https://github.com/Yonaba/Jumper).
-- Mapping algorithm: transforms a Minetest map surface to a 2d grid.
npc.pathfinder = {}
@ -30,57 +29,15 @@ npc.pathfinder.nodes = {
}
}
-- This function uses the mapping functions and the A* algorithm implementation
-- of the Jumper library to find a path from start_pos to end_pos. The range is
-- an extra amount of nodes to search in both the x and z coordinates.
function npc.pathfinder.find_path(start_pos, end_pos, range, walkable_nodes)
-- Check that start and end position are not the same
if start_pos.x == end_pos.x and start_pos.z == end_pos.z then
return nil
end
-- Set walkable nodes to empty if parameter wasn't used
if walkable_nodes == nil then
walkable_nodes = {}
end
-- Map the Minetest area to a 2D array
local map = pathfinder.create_map(start_pos, end_pos, range, walkable_nodes)
-- Find start and end positions
local pos = pathfinder.find_start_and_end_pos(map)
-- Normalize the map
local normalized_map = pathfinder.normalize_map(map)
-- Create pathfinder object
local grid_object = Grid(normalized_map)
-- Define what is a walkable node
local walkable = 0
-- Pathfinder object using A* algorithm
local finder = Pathfinder(grid_object, "ASTAR", walkable)
-- Set orthogonal mode meaning it will not move in diagonal directions
finder:setMode("ORTHOGONAL")
-- Calculates the path, and its length
local path = finder:getPath(pos.start_pos.x, pos.start_pos.z, pos.end_pos.x, pos.end_pos.z)
--minetest.log("Found path: "..dump(path))
-- Pretty-printing the results
if path then
return pathfinder.get_path(map, path:nodes())
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
function npc.pathfinder.is_good_node(node, exceptions)
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.
--minetest.log("Is good node: "..dump(node))
local is_openable = false
for _,node_prefix in pairs(npc.pathfinder.nodes.openable_prefix) do
--for _,node_prefix in pairs(pathfinder.nodes.openable_prefix) do
local start_i,end_i = string.find(node.name, node_prefix)
if start_i ~= nil then
is_openable = true
@ -88,159 +45,350 @@ function npc.pathfinder.is_good_node(node, exceptions)
end
end
if node ~= nil and node.name ~= nil and not minetest.registered_nodes[node.name].walkable then
--return pathfinder.node_types.walkable
return npc.pathfinder.node_types.walkable
elseif is_openable then
return npc.pathfinder.node_types.openable
--return pathfinder.node_types.openable
else
for i = 1, #exceptions do
if node.name == exceptions[i] then
return npc.pathfinder.node_types.walkable
--return pathfinder.node_types.walkable
end
end
return npc.pathfinder.node_types.non_walkable
--return pathfinder.node_types.non_walkable
end
end
function pathfinder.create_map(start_pos, end_pos, extra_range, walkables)
minetest.log("Start pos: "..minetest.pos_to_string(start_pos))
minetest.log("End pos: "..minetest.pos_to_string(end_pos))
-- Calculate all signs to ensure:
-- 1. Correct area calculation
-- 2. Iterate in the correct direction
local start_x_sign = (start_pos.x - end_pos.x) / math.abs(start_pos.x - end_pos.x)
local start_z_sign = (start_pos.z - end_pos.z) / math.abs(start_pos.z - end_pos.z)
local end_x_sign = (end_pos.x - start_pos.x) / math.abs(end_pos.x - start_pos.x)
local end_z_sign = (end_pos.z - start_pos.z) / math.abs(end_pos.z - start_pos.z)
-- Correct the signs if they are nan
if math.abs(start_pos.x - end_pos.x) == 0 then
start_x_sign = -1
end_x_sign = 1
end
if math.abs(start_pos.z - end_pos.z) == 0 then
start_z_sign = -1
end_z_sign = 1
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=path[i], type=pathfinder.is_good_node(node, {})})
end
-- Get starting and ending positions, adding the extra nodes to the area
local pos1 = {x=start_pos.x + (extra_range * start_x_sign), y = start_pos.y - 1, z=start_pos.z + (extra_range * start_z_sign)}
local pos2 = {x=end_pos.x + (extra_range * end_x_sign), y = end_pos.y, z=end_pos.z + (extra_range * end_z_sign)}
npc.log("DEBUG", "Detailed path: "..dump(path_detail))
return path_detail
end
local grid = {}
-- Loop through the area and classify nodes
for z = 1, math.abs(pos1.z - pos2.z) do
local current_row = {}
for x = 1, math.abs(pos1.x - pos2.x) do
-- Calculate current position
local current_pos = {x=pos1.x + (x*end_x_sign), y=pos1.y, z=pos1.z + (z*end_z_sign)}
-- Check if this is the starting position
if current_pos.x == start_pos.x and current_pos.z == start_pos.z then
-- Is start position
table.insert(current_row, {pos=current_pos, type=pathfinder.node_types.start})
elseif current_pos.x == end_pos.x and current_pos.z == end_pos.z then
-- Is ending position or goal position
table.insert(current_row, {pos=current_pos, type=pathfinder.node_types.goal})
else
-- Check if node is walkable
local node = minetest.get_node(current_pos)
-- Check node has air above it
local node_above = minetest.get_node({x=current_pos.x, y=current_pos.y+1, z=current_pos.z})
if node.name == "air" then
-- Check if node above is air
if node.name == "air" then
-- If air do no more checks
table.insert(current_row, {pos=current_pos, type=pathfinder.node_types.walkable})
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
-- Check if it is of a walkable or openable type
table.insert(current_row, {pos=current_pos, type=is_good_node(node, walkables)})
npc.log("ERROR", "Couldn't find path from "..minetest.pos_to_string(start_pos)
.." to "..minetest.pos_to_string(end_pos))
end
end
end
-- Insert the converted row into the grid
table.insert(grid, current_row)
return path
end
return grid
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
-- Utility function to print the created map to the console.
-- Used for debug.
local function print_map(map)
for z,row in pairs(map) do
local row_string = "["
for x,node in pairs(row) do
if node.type == 2 then
row_string = row_string.."- "
--[[
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
row_string = row_string..node.type.." "
end
-- Use the following if the coordinates are also needed
--row_string = row_string..node.type..": {"..node.pos.x..", "..node.pos.y..", "..node.pos.z.."}, "
end
row_string = row_string.."]"
print(row_string)
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)
-- This function find the starting and ending points in the
-- map representation, and returns the coordinates in the map
-- for the pathfinding algorithm to use
function pathfinder.find_start_and_end_pos(map)
-- This is for debug
--print_map(map)
local result = {}
for z,row in pairs(map) do
for x,node in pairs(row) do
if node.type == pathfinder.node_types.start then
--minetest.log("Start node: "..dump(node))
result["start_pos"] = {x=x, z=z}
elseif node.type == pathfinder.node_types.goal then
--minetest.log("End node: "..dump(node))
result["end_pos"] = {x=x, z=z}
end
end
end
--minetest.log("Found start and end positions: ("..result.start_pos.)..", "..minetest.pos_to_string(result.end_pos))
return result
end
-- This function transforms the grid into binary values
-- (0 walkable, 1 non-walkable) for the pathfinding algorithm.
function pathfinder.normalize_map(map)
local result = {}
for _,row in pairs(map) do
local result_row = {}
for _,node in pairs(row) do
if node.type ~= pathfinder.node_types.non_walkable then
table.insert(result_row, 0)
if distX > distZ then
return (14 * distZ + 10 * (distX - distZ)) * (distY + 1)
else
table.insert(result_row, 1)
return (14 * distX + 10 * (distZ - distX)) * (distY + 1)
end
end
table.insert(result, result_row)
end
return result
end
-- This function returns an array of tables with two parameters: type and pos.
-- The position parameter is the actual coordinate on the Minetest map. The
-- type is the type of the node at the coordinate defined as pathfinder.node_types.
function pathfinder.get_path(map, path_nodes)
local result = {}
for node, count in path_nodes do
table.insert(result, map[node:getY()][node:getX()])
-- For debug
--minetest.log("Node: "..dump(map[node:getY()][node:getX()]))
--print(('Step: %d - x: %d - y: %d'):format(count, node:getX(), node:getY()))
-- 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
return result
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

@ -22,8 +22,10 @@ npc.places.nodes = {
},
SITTABLE_TYPE = {
"cottages:bench",
-- Currently commented out since some NPCs
-- were sitting at stairs that are actually staircases
-- TODO: Register other stair types
"stairs:stair_wood"
--"stairs:stair_wood"
},
STORAGE_TYPE = {
"default:chest",
@ -56,7 +58,16 @@ npc.places.PLACE_TYPE = {
PRIMARY = "bed_primary"
},
SITTABLE = {
PRIMARY = "sit_primary"
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"
@ -68,27 +79,51 @@ npc.places.PLACE_TYPE = {
}
}
function npc.places.add_public(self, place_name, place_type, pos, access_node)
--minetest.log("Place name: "..dump(place_name)..", type: "..dump(place_type))
function npc.places.add_shared(self, place_name, place_type, pos, access_node)
self.places_map[place_name] = {type=place_type, pos=pos, access_node=access_node or pos, status="shared"}
end
-- Adds a specific node to the NPC places, and modifies the
-- node metadata to identify the NPC as the owner. This allows
-- other NPCs to avoid to take this as their own.
function npc.places.add_owned(self, place_name, place_type, pos, access_node)
-- Get node metadata
--local meta = minetest.get_meta(pos)
-- Check if it is owned by an NPC?
--if meta:get_string("npc_owner") == "" then
-- Set owned by NPC
--meta:set_string("npc_owner", self.npc_id)
-- Add place to list
self.places_map[place_name] = {type=place_type, pos=pos, access_node=access_node or pos, status="owned"}
--npc.places.add_public(self, place_name, place_type, pos)
return true
--end
--return false
end
function npc.places.add_unowned_accessible_place(self, nodes, place_type)
for i = 1, #nodes do
-- Check if node has owner
if nodes[i].owner == "" then
-- If node has no owner, check if it is accessible
local empty_nodes = npc.places.find_node_orthogonally(
nodes[i].node_pos, {"air"}, 0)
-- 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.places.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))
break
end
end
end
end
function npc.places.add_shared_accessible_place(self, nodes, place_type)
for i = 1, #nodes do
-- Check of not adding same owned sit
if nodes[i].owner ~= self.npc_id then
-- Check if it is accessible
local empty_nodes = npc.places.find_node_orthogonally(
nodes[i].node_pos, {"air"}, 0)
-- Check if bed is accessible
if #empty_nodes > 0 then
-- Assign node to NPC
npc.places.add_shared(self, place_type..dump(i),
place_type, nodes[i].node_pos, empty_nodes[1].pos)
end
end
end
end
function npc.places.get_by_type(self, place_type)
@ -196,7 +231,7 @@ function npc.places.find_entrance_from_openable_nodes(all_openable_nodes, marker
entity.collisionbox = {-0.20,-1.0,-0.20, 0.20,0.8,0.20}
--minetest.log("Start pos: "..minetest.pos_to_string(start_pos))
--minetest.log("End pos: "..minetest.pos_to_string(end_pos))
local path = pathfinder.find_path(start_pos, end_pos, entity)
local path = npc.pathfinder.find_path(start_pos, end_pos, entity, false)
--minetest.log("Found path: "..dump(path))
if path ~= nil then
--minetest.log("Path distance: "..dump(#path))

411240
debug.txt

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -437,8 +437,8 @@ minetest.register_on_player_receive_fields(function (player, formname, fields)
-- Get player response
local player_response = npc.dialogue.dialogue_results.options_dialogue[player_name]
-- Check if the player hit the negative option
if fields["exit"] then
-- 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.unlock_actions(player_response.npc)
end

View File

@ -32,7 +32,6 @@ dofile(path .. "/trade/prices.lua")
dofile(path .. "/actions/actions.lua")
dofile(path .. "/actions/places.lua")
dofile(path .. "/actions/pathfinder.lua")
dofile(path .. "/actions/jumper.lua")
dofile(path .. "/actions/node_registry.lua")
dofile(path .. "/random_data.lua")

109
npc.lua
View File

@ -44,14 +44,14 @@ npc.action_state = {
npc.log_level = {
INFO = true,
WARNING = false,
WARNING = true,
ERROR = true,
DEBUG = false
}
npc.texture_check = {
timer = 0,
interval = 0
interval = 2
}
---------------------------------------------------------------------------------------
@ -149,9 +149,9 @@ local function get_random_texture(sex, age)
end
-- Choose whether NPC can have relationships. Only 30% of NPCs cannot have relationships
local function can_have_relationships(age)
local function can_have_relationships(is_child)
-- Children can't have relationships
if not age then
if is_child then
return false
end
local chance = math.random(1,10)
@ -264,8 +264,8 @@ function npc.initialize(entity, pos, is_lua_entity, npc_stats)
elseif child_s <= age_chance and age_chance <= child_e then
selected_age = npc.age.child
ent.visual_size = {
x = 0.5,
y = 0.5
x = 0.75,
y = 0.75
}
ent.collisionbox = {-0.10,-0.50,-0.10, 0.10,0.40,0.10}
ent.is_child = true
@ -310,6 +310,11 @@ function npc.initialize(entity, pos, is_lua_entity, npc_stats)
-- Flag that determines if NPC can have a relationship
ent.can_have_relationship = can_have_relationships(ent.is_child)
ent.infotext = "Interested in relationships: "..dump(ent.can_have_relationship)
-- Flag to determine if NPC can receive gifts
ent.can_receive_gifts = ent.can_have_relationship
-- Initialize relationships object
ent.relationships = {}
@ -455,6 +460,24 @@ function npc.initialize(entity, pos, is_lua_entity, npc_stats)
ent.object:set_properties(ent)
end
---------------------------------------------------------------------------------------
-- Trading functions
---------------------------------------------------------------------------------------
function npc.generate_trade_list_from_inventory(self)
local list = {}
for i = 1, #self.inventory do
list[npc.get_item_name(self.inventory[i])] = {}
end
self.trader_data.trade_list.both = list
end
function npc.set_trading_status(self, status)
-- Set status
self.trader_data.trader_status = status
-- Re-generate trade offers
npc.trade.generate_trade_offers_by_status(self)
end
---------------------------------------------------------------------------------------
-- Inventory functions
---------------------------------------------------------------------------------------
@ -743,6 +766,14 @@ npc.schedule_types = {
["date_based"] = "date_based"
}
npc.schedule_properties = {
put_item = "put_item",
put_multiple_items = "put_multiple_items",
take_item = "take_item",
trader_status = "trader_status",
can_receive_gifts = "can_receive_gifts"
}
local function get_time_in_hours()
return minetest.get_timeofday() * 24
end
@ -861,6 +892,41 @@ function npc.delete_schedule_entry(self, schedule_type, date, time)
end
end
function npc.schedule_change_property(self, property, args)
if property == npc.schedule_properties.trader_status then
-- Get status from args
local status = args.status
-- Set status to NPC
npc.set_trading_status(self, status)
elseif property == npc.schedule_properties.put_item then
local itemstring = args.itemstring
-- Add item
npc.add_item_to_inventory_itemstring(self, itemstring)
elseif property == npc.schedule_properties.put_multiple_items then
local itemlist = args.itemlist
for i = 1, #itemlist do
local itemlist_entry = itemlist[i]
local current_itemstring = itemlist[i].name
if itemlist_entry.random == true then
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
elseif property == npc.schedule_properties.take_item then
local itemstring = args.itemstring
-- Add item
npc.take_item_from_inventory_itemstring(self, itemstring)
elseif property == npc.schedule_properties.can_receive_gifts then
local value = args.can_receive_gifts
-- Set status
self.can_receive_gifts = value
end
end
---------------------------------------------------------------------------------------
-- NPC Definition
---------------------------------------------------------------------------------------
@ -884,6 +950,11 @@ mobs:register_mob("advanced_npc:npc", {
drawtype = "front",
textures = {
{"npc_male1.png"},
--{"npc_male2.png"},
--{"npc_male3.png"},
--{"npc_male4.png"},
--{"npc_male5.png"},
--{"npc_male6.png"},
{"npc_female1.png"}, -- female by nuttmeg20
},
child_texture = {
@ -898,7 +969,7 @@ mobs:register_mob("advanced_npc:npc", {
stepheight = 0.6,
walk_velocity = 1,
run_velocity = 3,
jump = true,
jump = false,
drops = {
{name = "default:wood", chance = 1, min = 1, max = 3},
{name = "default:apple", chance = 2, min = 1, max = 2},
@ -934,16 +1005,14 @@ mobs:register_mob("advanced_npc:npc", {
local item = clicker:get_wielded_item()
local name = clicker:get_player_name()
--self.child = true
--self.textures = {"mobs_npc_child_male1.png"}
--self.base_texture = "mobs_npc_child_male1.png"
--self.object:set_properties(self)
npc.log("DEBUG", "Right-clicked NPC: "..dump(self))
-- Receive gift or start chat. If player has no item in hand
-- then it is going to start chat directly
if self.can_have_relationship and item:to_table() ~= nil then
minetest.log("self.can_have_relationship: "..dump(self.can_have_relationship)..", self.can_receive_gifts: "..dump(self.can_receive_gifts)..", table: "..dump(item:to_table()))
if self.can_have_relationship
and self.can_receive_gifts
and item:to_table() ~= nil then
-- Get item name
local item = minetest.registered_items[item:get_name()]
local item_name = item.description
@ -980,13 +1049,19 @@ mobs:register_mob("advanced_npc:npc", {
-- NPC is initialized, check other variables
-- Check child texture issues
if self.is_child then
-- Check texture
npc.texture_check.timer = npc.texture_check.timer + dtime
if npc.texture_check.timer > npc.texture_check.interval then
-- Reset timer
npc.texture_check.timer = 0
-- Set hornytimer to zero every 60 seconds so that children
-- don't grow automatically
self.hornytimer = 0
-- Set correct textures
self.texture = {self.selected_texture}
self.base_texture = {self.selected_texture}
self.object:set_properties(self)
npc.log("WARNING", "Corrected textures on NPC child "..dump(self.npc_name))
-- Set interval to large interval so this code isn't called frequently
npc.texture_check.interval = 60
end
@ -1086,12 +1161,16 @@ mobs:register_mob("advanced_npc:npc", {
npc.log("DEBUG", "Adding actions to action queue")
-- Add to action queue all actions on schedule
for i = 1, #schedule[time] do
if schedule[time][i].action == nil then
--minetest.log("schedule[time]: "..dump(schedule[time]))
if schedule[time][i].task ~= nil then
-- Add task
npc.add_task(self, schedule[time][i].task, schedule[time][i].args)
else
elseif schedule[time][i].action ~= nil then
-- Add action
npc.add_action(self, schedule[time][i].action, schedule[time][i].args)
elseif schedule[time][i].property ~= nil then
-- Change NPC property
npc.schedule_change_property(self, schedule[time][i].property, schedule[time][i].args)
end
end
npc.log("DEBUG", "New action queue: "..dump(self.actions))

View File

@ -27,8 +27,8 @@ npc.relationships = {}
npc.relationships.ITEM_GIFT_EFFECT = 2.5
-- Expected values for these are 720 each respectively
npc.relationships.GIFT_TIMER_INTERVAL = 2
npc.relationships.RELATIONSHIP_DECREASE_TIMER_INTERVAL = 60
npc.relationships.GIFT_TIMER_INTERVAL = 360
npc.relationships.RELATIONSHIP_DECREASE_TIMER_INTERVAL = 720
npc.relationships.RELATIONSHIP_PHASE = {}
-- Define phases
@ -57,7 +57,8 @@ npc.relationships.MARRIED_NPC_DIALOGUE = {
response_id = 2,
action = function(self, player)
self.order = "stand"
minetest.chat_send_player(player:get_player_name(), S("Ok dear, I will wait here for you."))
npc.chat(self.npc_name, player:get_player_name(),
S("Ok dear, I will wait here for you."))
end
},
[3] = {
@ -66,7 +67,7 @@ npc.relationships.MARRIED_NPC_DIALOGUE = {
response_id = 3,
action = function(self, player)
self.order = "follow"
minetest.chat_send_player(player:get_player_name(), S("Ok, let's go!"))
npc.chat(self.npc_name, player:get_player_name(), S("Ok, let's go!"))
end
}
}
@ -114,7 +115,7 @@ function npc.relationships.get_response_for_disliked_item(item_name, sex)
for i = 1, #items do
minetest.log(dump(items[i]))
if items[i].item == item_name then
minetest.log("Returning: "..dump(items[i].response))
--minetest.log("Returning: "..dump(items[i].response))
return items[i].response
end
end
@ -350,16 +351,16 @@ local function show_receive_gift_reaction(self, item_name, modifier, clicker_nam
end
-- Send message
-- TODO: There might be an error with getting the message...
minetest.log("Item_name: "..dump(item_name)..", sex: "..dump(self.sex)..", phase: "..dump(phase))
--minetest.log("Item_name: "..dump(item_name)..", sex: "..dump(self.sex)..", phase: "..dump(phase))
local message_to_send =
npc.relationships.get_response_for_favorite_item(item_name, self.sex, phase)
minetest.chat_send_player(clicker_name, message_to_send)
npc.chat(self.npc_name, clicker_name, message_to_send)
-- Disliked items reactions
elseif modifier < 0 then
effect({x = pos.x, y = pos.y + 1, z = pos.z}, 8, "smoke.png")
minetest.log("Item name: "..item_name..", sex: "..self.sex)
effect({x = pos.x, y = pos.y + 1, z = pos.z}, 8, "default_item_smoke.png")
--minetest.log("Item name: "..item_name..", sex: "..self.sex)
local message_to_send = npc.relationships.get_response_for_disliked_item(item_name, self.sex)
minetest.chat_send_player(clicker_name, message_to_send)
npc.chat(self.npc_name, clicker_name, message_to_send)
end
end
@ -381,7 +382,7 @@ function npc.relationships.receive_gift(self, clicker)
-- 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
minetest.chat_send_player(clicker_name, "Thanks, but I don't need anything for now")
npc.chat(self.npc_name, clicker_name, "Thanks, but I don't need anything for now")
return false
end
@ -392,9 +393,9 @@ function npc.relationships.receive_gift(self, clicker)
npc.relationships.RELATIONSHIP_PHASE["phase5"].limit
and self.owner ~= clicker_name
and item:get_name() ~= "advanced_npc:marriage_ring" then
minetest.chat_send_player(clicker_name,
npc.chat(self.npc_name, clicker_name,
"Thank you my love, but I think that you have given me")
minetest.chat_send_player(clicker_name,
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)
@ -407,7 +408,7 @@ function npc.relationships.receive_gift(self, clicker)
local receive_chance = math.random(1, 10)
-- Receive ring and get married
if receive_chance < 6 then
minetest.chat_send_player(clicker_name,
npc.chat(self.npc_name, clicker_name,
"Oh, oh you make me so happy! Yes! I will marry you!")
-- Get ring
item:take_item()
@ -422,7 +423,7 @@ function npc.relationships.receive_gift(self, clicker)
self.owner = clicker_name
-- Reject ring for now
else
minetest.chat_send_player(clicker_name,
npc.chat(self.npc_name, clicker_name,
"Dear, I feel the same as you. But maybe not yet...")
end
@ -446,7 +447,7 @@ function npc.relationships.receive_gift(self, clicker)
show_receive_gift_reaction(self, item:get_name(), modifier, clicker_name, false)
else
-- Neutral item reaction
minetest.chat_send_player(clicker_name, "Thank you honey!")
npc.chat(self.npc_name, clicker_name, "Thank you honey!")
end
-- Take item
item:take_item()
@ -480,9 +481,9 @@ function npc.relationships.receive_gift(self, clicker)
-- if 70%
local receive_chance = math.random(1,10)
if receive_chance < 7 then
minetest.chat_send_player(clicker_name, "Thanks. I will find some use for this.")
npc.chat(self.npc_name, clicker_name, "Thanks. I will find some use for this.")
else
minetest.chat_send_player(clicker_name, "Thank you, but no, I have no use for this.")
npc.chat(self.npc_name, clicker_name, "Thank you, but no, I have no use for this.")
take = false
end
show_reaction = false
@ -502,7 +503,7 @@ function npc.relationships.receive_gift(self, clicker)
clicker:set_wielded_item(item)
end
minetest.log(dump(self))
npc.log("DEBUG", "NPC: "..dump(self))
-- Reset gift timer
reset_gift_timer(self, clicker_name)
return true

View File

@ -88,16 +88,72 @@ local function get_basic_schedule()
-- Allow mobs_redo wandering
[3] = {action = npc.actions.cmd.FREEZE, args = {freeze = false}}
},
-- Noon actions: go inside the house
-- This will be executed around 12 PM MTG time
noon_actions = {
-- Walk to a sittable node
[1] = {task = npc.actions.cmd.WALK_TO_POS, args = {
end_pos = {place_type=npc.places.PLACE_TYPE.SITTABLE.PRIMARY, use_access_node=true},
walkable = {"cottages:bench"}
}
},
-- Sit on the node
[2] = {task = npc.actions.cmd.USE_SITTABLE, args = {
pos = npc.places.PLACE_TYPE.SITTABLE.PRIMARY,
action = npc.actions.const.sittable.SIT
}
},
-- Stay put into place
[3] = {action = npc.actions.cmd.FREEZE, args = {freeze = true}}
},
-- Afternoon actions: go inside the house
-- This will be executed around 1 PM MTG time
afternoon_actions = {
-- Get up of the sit
[1] = {task = npc.actions.cmd.USE_SITTABLE, args = {
pos = npc.places.PLACE_TYPE.SITTABLE.PRIMARY,
action = npc.actions.const.sittable.GET_UP
}
},
-- Give NPC money to buy from player
[2] = {property = npc.schedule_properties.put_multiple_items, args = {
itemlist = {
{name="default:iron_lump", random=true, min=2, max=4}
}
}
},
-- Change trader status to "trader"
[3] = {property = npc.schedule_properties.trader_status, args = {
status = npc.trade.TRADER
}
},
[4] = {property = npc.schedule_properties.can_receive_gifts, args = {
value = true
}
},
-- Allow mobs_redo wandering
[5] = {action = npc.actions.cmd.FREEZE, args = {freeze = false}}
},
-- Afternoon actions: go inside the house
-- This will be executed around 6 PM MTG time
afternoon_actions = {
late_afternoon_actions = {
-- 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
[1] = {task = npc.actions.cmd.WALK_TO_POS, args = {
[3] = {task = npc.actions.cmd.WALK_TO_POS, args = {
end_pos = npc.places.PLACE_TYPE.OTHER.HOME_INSIDE,
walkable = {}}
},
-- Allow mobs_redo wandering
[2] = {action = npc.actions.cmd.FREEZE, args = {freeze = false}}
[4] = {action = npc.actions.cmd.FREEZE, args = {freeze = false}}
},
-- Evening actions: walk to bed and use it.
-- This will be executed around 10 PM MTG time
@ -164,6 +220,7 @@ function spawner.scan_area(pos1, pos2)
result.bed_type = spawner.get_nodes_by_type(start_pos, end_pos, npc.places.nodes.BED_TYPE)
result.sittable_type = spawner.get_nodes_by_type(start_pos, end_pos, npc.places.nodes.SITTABLE_TYPE)
-- Filter out
result.furnace_type = spawner.get_nodes_by_type(start_pos, end_pos, npc.places.nodes.FURNACE_TYPE)
result.storage_type = spawner.get_nodes_by_type(start_pos, end_pos, npc.places.nodes.STORAGE_TYPE)
result.openable_type = spawner.get_nodes_by_type(start_pos, end_pos, npc.places.nodes.OPENABLE_TYPE)
@ -190,49 +247,77 @@ function spawner.assign_places(self, pos)
local node_data = minetest.deserialize(meta:get_string("node_data"))
-- Assign plotmarker
npc.places.add_public(self, npc.places.PLACE_TYPE.OTHER.HOME_PLOTMARKER,
npc.places.add_shared(self, npc.places.PLACE_TYPE.OTHER.HOME_PLOTMARKER,
npc.places.PLACE_TYPE.OTHER.HOME_PLOTMARKER, pos)
-- Assign entrance door and related locations
if entrance ~= nil and entrance.node_pos ~= nil then
npc.places.add_public(self, npc.places.PLACE_TYPE.OPENABLE.HOME_ENTRANCE_DOOR, npc.places.PLACE_TYPE.OPENABLE.HOME_ENTRANCE_DOOR, entrance.node_pos)
npc.places.add_shared(self, npc.places.PLACE_TYPE.OPENABLE.HOME_ENTRANCE_DOOR, npc.places.PLACE_TYPE.OPENABLE.HOME_ENTRANCE_DOOR, entrance.node_pos)
-- Find the position inside and outside the door
local entrance_inside = npc.places.find_node_behind_door(entrance.node_pos)
local entrance_outside = npc.places.find_node_in_front_of_door(entrance.node_pos)
-- Assign these places to NPC
npc.places.add_public(self, npc.places.PLACE_TYPE.OTHER.HOME_INSIDE, npc.places.PLACE_TYPE.OTHER.HOME_INSIDE, entrance_inside)
npc.places.add_public(self, npc.places.PLACE_TYPE.OTHER.HOME_OUTSIDE, npc.places.PLACE_TYPE.OTHER.HOME_OUTSIDE, entrance_outside)
npc.places.add_shared(self, npc.places.PLACE_TYPE.OTHER.HOME_INSIDE, npc.places.PLACE_TYPE.OTHER.HOME_INSIDE, entrance_inside)
npc.places.add_shared(self, npc.places.PLACE_TYPE.OTHER.HOME_OUTSIDE, npc.places.PLACE_TYPE.OTHER.HOME_OUTSIDE, entrance_outside)
end
-- Assign beds
if #node_data.bed_type > 0 then
-- Find unowned bed
for i = 1, #node_data.bed_type do
-- Check if bed has owner
--minetest.log("Node: "..dump(node_data.bed_type[i]))
if node_data.bed_type[i].owner == "" then
-- If bed has no owner, check if it is accessible
local empty_nodes = npc.places.find_node_orthogonally(
node_data.bed_type[i].node_pos, {"air"}, 0)
-- Check if bed is accessible
if #empty_nodes > 0 then
-- Set owner to this NPC
node_data.bed_type[i].owner = self.npc_id
-- Assign node to NPC
npc.places.add_owned(self, npc.places.PLACE_TYPE.BED.PRIMARY,
npc.places.PLACE_TYPE.BED.PRIMARY, node_data.bed_type[i].node_pos, empty_nodes[1].pos)
-- Assign a specific sittable node to a NPC.
npc.places.add_unowned_accessible_place(self, node_data.bed_type,
npc.places.PLACE_TYPE.BED.PRIMARY)
-- Store changes to node_data
meta:set_string("node_data", minetest.serialize(node_data))
npc.log("DEBUG", "Added bed at "..minetest.pos_to_string(node_data.bed_type[i].node_pos)
.." to NPC "..dump(self.npc_name))
break
end
end
end
end
--local plot_info = minetest.deserialize(meta:get_string("plot_info"))
--minetest.log("Plot info:"..dump(plot_info))
-- Assign sits
if #node_data.sittable_type > 0 then
-- Check if there are same or more amount of sits as beds
if #node_data.sittable_type >= #node_data.bed_type then
-- Assign a specific sittable node to a NPC.
npc.places.add_unowned_accessible_place(self, node_data.sittable_type,
npc.places.PLACE_TYPE.SITTABLE.PRIMARY)
-- Store changes to node_data
meta:set_string("node_data", minetest.serialize(node_data))
end
-- Add all sits to places as shared since NPC should be able to sit
-- at any accessible sit
npc.places.add_shared_accessible_place(self, node_data.sittable_type,
npc.places.PLACE_TYPE.SITTABLE.SHARED)
end
-- Assign furnaces
if #node_data.furnace_type > 0 then
-- Check if there are same or more amount of furnace as beds
if #node_data.furnace_type >= #node_data.bed_type then
-- Assign a specific furnace node to a NPC.
npc.places.add_unowned_accessible_place(self, node_data.furnace_type,
npc.places.PLACE_TYPE.FURNACE.PRIMARY)
-- Store changes to node_data
meta:set_string("node_data", minetest.serialize(node_data))
end
-- Add all furnaces to places as shared since NPC should be able to use
-- any accessible furnace
npc.places.add_shared_accessible_place(self, node_data.furnace_type,
npc.places.PLACE_TYPE.FURNACE.SHARED)
end
-- Assign storage nodes
if #node_data.storage_type > 0 then
-- Check if there are same or more amount of storage as beds
if #node_data.storage_type >= #node_data.bed_type then
-- Assign a specific storage node to a NPC.
npc.places.add_unowned_accessible_place(self, node_data.storage_type,
npc.places.PLACE_TYPE.STORAGE.PRIMARY)
-- Store changes to node_data
meta:set_string("node_data", minetest.serialize(node_data))
end
-- Add all storage-types to places as shared since NPC should be able
-- to use other storage nodes as well.
npc.places.add_shared_accessible_place(self, node_data.storage_type,
npc.places.PLACE_TYPE.STORAGE.SHARED)
end
npc.log("DEBUG", "Places for NPC "..self.npc_name..": "..dump(self.places_map))
-- Make NPC go into their house
@ -253,8 +338,14 @@ function spawner.assign_schedules(self, pos)
-- Add schedule entry for morning actions
npc.add_schedule_entry(self, npc.schedule_types.generic, 0, 8, nil, basic_schedule.morning_actions)
-- Add schedule entry for noon actions
npc.add_schedule_entry(self, npc.schedule_types.generic, 0, 12, nil, basic_schedule.noon_actions)
-- Add schedule entry for afternoon actions
npc.add_schedule_entry(self, npc.schedule_types.generic, 0, 18, nil, basic_schedule.afternoon_actions)
npc.add_schedule_entry(self, npc.schedule_types.generic, 0, 13, nil, basic_schedule.afternoon_actions)
-- Add schedule entry for late afternoon actions
npc.add_schedule_entry(self, npc.schedule_types.generic, 0, 18, nil, basic_schedule.late_afternoon_actions)
-- Add schedule entry for evening actions
npc.add_schedule_entry(self, npc.schedule_types.generic, 0, 22, nil, basic_schedule.evening_actions)
@ -348,17 +439,15 @@ end
-- This function takes care of calculating how many NPCs will be spawn
function spawner.calculate_npc_spawning(pos)
-- Check node
local node = minetest.get_node(pos)
if node.name ~= "advanced_npc:plotmarker_auto_spawner" then
return
end
-- Check node metadata
local meta = minetest.get_meta(pos)
if meta:get_string("replaced") ~= "true" then
return
end
-- Get nodes for this building
local node_data = minetest.deserialize(meta:get_string("node_data"))
if node_data == nil then
npc.log("ERROR", "Mis-configured advanced_npc:plotmarker_auto_spawner at position: "..minetest.pos_to_string(pos))
npc.log("ERROR", "Mis-configured mg_villages:plotmarker at position: "..minetest.pos_to_string(pos))
return
end
-- Check number of beds
@ -468,6 +557,11 @@ function spawner.replace_mg_villages_plotmarker(pos)
local village_id = meta:get_string("village_id")
local plot_nr = meta:get_int("plot_nr")
local infotext = meta:get_string("infotext")
-- Check for nil values above
if (not village_id or (village and village == ""))
or (not plot_nr or (plot_nr and plot_nr == 0)) then
return
end
-- Following line from mg_villages mod, protection.lua
local btype = mg_villages.all_villages[village_id].to_add_data.bpos[plot_nr].btype
local building_data = mg_villages.BUILDINGS[btype]
@ -479,7 +573,7 @@ function spawner.replace_mg_villages_plotmarker(pos)
npc.log("INFO", "Replacing mg_villages:plotmarker at "..minetest.pos_to_string(pos))
-- Replace the plotmarker for auto-spawner
minetest.set_node(pos, {name="advanced_npc:plotmarker_auto_spawner"})
--minetest.set_node(pos, {name="advanced_npc:plotmarker_auto_spawner"})
-- Store old plotmarker metadata again
meta:set_string("village_id", village_id)
meta:set_int("plot_nr", plot_nr)
@ -525,9 +619,12 @@ function spawner.replace_mg_villages_plotmarker(pos)
child_total = 0
}
meta:set_string("npc_stats", minetest.serialize(npc_stats))
-- Set replaced
meta:set_string("replaced", "true")
-- Calculate how many NPCs will spawn
spawner.calculate_npc_spawning(pos)
-- Stop searching for building type
break
end
end
end
@ -539,19 +636,19 @@ if minetest.get_modpath("mg_villages") ~= nil then
-- Node registration
-- This node is currently a slightly modified mg_villages:plotmarker
-- TODO: Change formspec to a more detailed one.
minetest.register_node("advanced_npc:plotmarker_auto_spawner", {
description = "Automatic NPC Spawner",
drawtype = "nodebox",
tiles = {"default_stone.png"},
paramtype = "light",
paramtype2 = "facedir",
node_box = {
type = "fixed",
fixed = {
{-0.5+2/16, -0.5, -0.5+2/16, 0.5-2/16, -0.5+2/16, 0.5-2/16},
--{-0.5+0/16, -0.5, -0.5+0/16, 0.5-0/16, -0.5+0/16, 0.5-0/16},
}
},
minetest.override_item("mg_villages:plotmarker", {
-- description = "Automatic NPC Spawner",
-- drawtype = "nodebox",
-- tiles = {"default_stone.png"},
-- paramtype = "light",
-- paramtype2 = "facedir",
-- node_box = {
-- type = "fixed",
-- fixed = {
-- {-0.5+2/16, -0.5, -0.5+2/16, 0.5-2/16, -0.5+2/16, 0.5-2/16},
-- --{-0.5+0/16, -0.5, -0.5+0/16, 0.5-0/16, -0.5+0/16, 0.5-0/16},
-- }
-- },
walkable = false,
groups = {cracky=3,stone=2},
@ -563,7 +660,7 @@ if minetest.get_modpath("mg_villages") ~= nil then
--minetest.log("First-floor beds: "..dump(spawner.filter_first_floor_nodes(nodedata.bed_type, pos)))
--local entrance = npc.places.find_entrance_from_openable_nodes(nodedata.openable_type, pos)
--minetest.log("Found entrance: "..dump(entrance))
minetest.log("Replaced: "..dump(minetest.get_meta(pos):get_string("replaced")))
-- for i = 1, #nodedata.bed_type do
-- nodedata.bed_type[i].owner = ""
-- end
@ -574,22 +671,22 @@ if minetest.get_modpath("mg_villages") ~= nil then
return mg_villages.plotmarker_formspec( pos, nil, {}, clicker )
end,
on_receive_fields = function(pos, formname, fields, sender)
return mg_villages.plotmarker_formspec( pos, formname, fields, sender );
end,
-- on_receive_fields = function(pos, formname, fields, sender)
-- return mg_villages.plotmarker_formspec( pos, formname, fields, sender );
-- end,
on_timer = function(pos, elapsed)
npc.spawner.spawn_npc(pos)
end,
-- protect against digging
can_dig = function(pos, player)
local meta = minetest.get_meta(pos);
if (meta and meta:get_string("village_id") ~= "" and meta:get_int("plot_nr") and meta:get_int("plot_nr") > 0 ) then
return false;
end
return true;
end
-- can_dig = function(pos, player)
-- local meta = minetest.get_meta(pos);
-- if (meta and meta:get_string("village_id") ~= "" and meta:get_int("plot_nr") and meta:get_int("plot_nr") > 0 ) then
-- return false;
-- end
-- return true;
-- end
})
-- LBM Registration
@ -618,18 +715,22 @@ if minetest.get_modpath("mg_villages") ~= nil then
chance = 1,--5,
catch_up = true,
action = function(pos, node, active_object_count, active_object_count_wider)
-- Check if replacement is needed
if minetest.get_meta(pos):get_string("replaced") == "true" then
return
end
-- Check if replacement is activated
if npc.spawner.replace_activated then
-- Replace mg_villages:plotmarker
spawner.replace_mg_villages_plotmarker(pos)
-- Set NPCs to spawn
spawner.calculate_npc_spawning(pos)
end
end
})
end
--minetest.register_alias_force("mg_villages:plotmarker", )
-- Chat commands to manage spawners
minetest.register_chatcommand("restore_plotmarkers", {
description = "Replaces all advanced_npc:plotmarker_auto_spawner with mg_villages:plotmarker in the specified radius.",
@ -652,7 +753,7 @@ minetest.register_chatcommand("restore_plotmarkers", {
local start_pos = {x=pos.x - radius, y=pos.y - radius, z=pos.z - radius}
local end_pos = {x=pos.x + radius, y=pos.y + radius, z=pos.z + radius}
local nodes = minetest.find_nodes_in_area_under_air(start_pos, end_pos,
{"advanced_npc:plotmarker_auto_spawner"})
{"mg_villages:plotmarker"})
-- Check if we have nodes to replace
minetest.chat_send_player(name, "Found "..dump(#nodes).." nodes to replace...")
if #nodes == 0 then
@ -660,13 +761,10 @@ minetest.register_chatcommand("restore_plotmarkers", {
end
-- Replace all nodes
for i = 1, #nodes do
--minetest.log(dump(nodes[i]))
local meta = minetest.get_meta(nodes[i])
local village_id = meta:get_string("village_id")
local plot_nr = meta:get_int("plot_nr")
local infotext = meta:get_string("infotext")
-- Replace node
minetest.set_node(nodes[i], {name="mg_villages:plotmarker"})
-- Set metadata
meta = minetest.get_meta(nodes[i])
meta:set_string("village_id", village_id)
@ -676,6 +774,7 @@ minetest.register_chatcommand("restore_plotmarkers", {
meta:set_string("node_data", nil)
meta:set_string("npcs", nil)
meta:set_string("npc_stats", nil)
meta:set_string("replaced", "false")
end
minetest.chat_send_player(name, "Finished replacement of "..dump(#nodes).." auto-spawners successfully")
end

Binary file not shown.

Before

Width:  |  Height:  |  Size: 323 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 267 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 783 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 783 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 779 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 901 B

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 B