Spawner: Large code refactor to remove dependency on plotmarkers.

Most spawner functions can now be called without giving a plotmarker.
Move scanning functions to places.lua.
Places: Cleanup and add more area-scanning functions.
Schedules: Bugfix where schedules weren't being executed due to wrong
"end" order in the do_custom() function.
Data: Moved random data to "data" folder.
Textures: Add 14 male textures and 10 female textures.
Occupations: Small tweaks to "default_basic" occupation.
This commit is contained in:
Hector Franqui 2017-08-25 09:31:45 -04:00
parent 698d247aba
commit 4c102a70a4
28 changed files with 816 additions and 741 deletions

View File

@ -155,6 +155,12 @@ function npc.places.get_by_type(self, place_type)
return result
end
---------------------------------------------------------------------------------------
-- Utility functions
---------------------------------------------------------------------------------------
-- The following are utility functions that are used to operate on nodes for
-- specific conditions
-- This function searches on a squared are of the given radius
-- for nodes of the given type. The type should be npc.places.nodes
function npc.places.find_node_nearby(pos, type, radius)
@ -188,11 +194,64 @@ function npc.places.find_node_orthogonally(pos, nodes, y_adjustment)
return result
end
-- Wrapper around minetest.find_nodes_in_area()
-- TODO: Verify if this wrapper is actually needed
function npc.places.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.places.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
table.insert(result, node)
end
end
return result
end
-- Creates an array of {pos=<node_pos>, owner=''} for managing
-- which NPC owns what
function npc.places.get_nodes_by_type(start_pos, end_pos, type)
local result = {}
local nodes = npc.places.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"] = ''
table.insert(result, entry)
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.places.scan_area_for_usable_nodes(pos1, pos2)
local result = {
bed_type = {},
sittable_type = {},
furnace_type = {},
storage_type = {},
openable_type = {}
}
local start_pos, end_pos = vector.sort(pos1, pos2)
result.bed_type = npc.places.get_nodes_by_type(start_pos, end_pos, npc.places.nodes.BED_TYPE)
result.sittable_type = npc.places.get_nodes_by_type(start_pos, end_pos, npc.places.nodes.SITTABLE_TYPE)
result.furnace_type = npc.places.get_nodes_by_type(start_pos, end_pos, npc.places.nodes.FURNACE_TYPE)
result.storage_type = npc.places.get_nodes_by_type(start_pos, end_pos, npc.places.nodes.STORAGE_TYPE)
result.openable_type = npc.places.get_nodes_by_type(start_pos, end_pos, npc.places.nodes.OPENABLE_TYPE)
return result
end
-- Specialized function to find doors that are an entrance to a building.
-- The definition of an entrance is:
-- The openable node with the shortest path to the plotmarker node
@ -231,18 +290,8 @@ function npc.places.find_entrance_from_openable_nodes(all_openable_nodes, marker
local start_pos = {x=open_pos.x, y=open_pos.y, z=open_pos.z}
local end_pos = {x=marker_pos.x, y=marker_pos.y, z=marker_pos.z}
-- Check if there's any difference in vertical position
-- minetest.log("Openable node pos: "..minetest.pos_to_string(open_pos))
-- minetest.log("Plotmarker node pos: "..minetest.pos_to_string(marker_pos))
-- NOTE: Commented out while testing MarkBu's pathfinder
--if start_pos.y ~= end_pos.y then
-- Adjust to make pathfinder find nodes one node above
-- end_pos.y = start_pos.y
--end
-- This adjustment allows the map to be created correctly
--start_pos.y = start_pos.y + 1
--end_pos.y = end_pos.y + 1
-- Find path from the openable node to the plotmarker
--local path = pathfinder.find_path(start_pos, end_pos, 20, {})

View File

@ -37,9 +37,9 @@ dofile(path .. "/actions/node_registry.lua")
dofile(path .. "/occupations/occupations.lua")
-- Load random data definitions
dofile(path .. "/random_data.lua")
dofile(path .. "/random_data/dialogues_data.lua")
dofile(path .. "/random_data/gift_items_data.lua")
dofile(path .. "/random_data/names_data.lua")
dofile(path .. "/random_data/occupations_data.lua")
dofile(path .. "/data/dialogues_data.lua")
dofile(path .. "/data/gift_items_data.lua")
dofile(path .. "/data/names_data.lua")
dofile(path .. "/data/occupations_data.lua")
print (S("[Mod] Advanced NPC loaded"))

39
npc.lua
View File

@ -474,7 +474,8 @@ function npc.initialize(entity, pos, is_lua_entity, npc_stats, occupation_name)
-- If occupation name given, override properties with
-- occupation values and initialize schedules
minetest.log("Entity age: "..dump(ent.age)..", afult? "..dump(ent.age==npc.age.adult))
--minetest.log("Entity age: "..dump(ent.age)..", afult? "..dump(ent.age==npc.age.adult))
npc.log("INFO", "Overriding NPC values with occupation: "..dump(occupation_name))
if occupation_name and occupation_name ~= "" and ent.age == npc.age.adult then
npc.occupations.initialize_occupation_values(ent, occupation_name)
end
@ -508,7 +509,7 @@ function npc.initialize(entity, pos, is_lua_entity, npc_stats, occupation_name)
-- Set initialized flag on
ent.initialized = true
npc.log("WARNING", "Spawned entity: "..dump(ent))
--npc.log("WARNING", "Spawned entity: "..dump(ent))
npc.log("INFO", "Successfully initialized NPC with name "..dump(ent.npc_name)
..", sex: "..ent.sex..", is child: "..dump(ent.is_child)
..", texture: "..dump(ent.textures))
@ -1156,7 +1157,26 @@ mobs:register_mob("advanced_npc:npc", {
{"npc_male4.png"},
{"npc_male5.png"},
{"npc_male6.png"},
{"npc_male7.png"},
{"npc_male8.png"},
{"npc_male9.png"},
{"npc_male10.png"},
{"npc_male11.png"},
{"npc_male12.png"},
{"npc_male13.png"},
{"npc_male14.png"},
{"npc_male15.png"},
{"npc_female1.png"}, -- female by nuttmeg20
{"npc_female2.png"},
{"npc_female3.png"},
{"npc_female4.png"},
{"npc_female5.png"},
{"npc_female6.png"},
{"npc_female7.png"},
{"npc_female8.png"},
{"npc_female9.png"},
{"npc_female10.png"},
{"npc_female11.png"},
},
child_texture = {
{"npc_child_male1.png"},
@ -1206,7 +1226,7 @@ mobs:register_mob("advanced_npc:npc", {
local item = clicker:get_wielded_item()
local name = clicker:get_player_name()
npc.log("DEBUG", "Right-clicked NPC: "..dump(self))
npc.log("INFO", "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
@ -1235,7 +1255,6 @@ mobs:register_mob("advanced_npc:npc", {
else
npc.start_dialogue(self, clicker, true)
end
end,
do_custom = function(self, dtime)
if self.initialized == nil then
@ -1266,6 +1285,7 @@ mobs:register_mob("advanced_npc:npc", {
-- Set interval to large interval so this code isn't called frequently
npc.texture_check.interval = 60
end
end
end
-- Timer function for casual traders to reset their trade offers
@ -1341,25 +1361,24 @@ mobs:register_mob("advanced_npc:npc", {
-- Get time of day
local time = get_time_in_hours()
-- Check if time is an hour
if time % 1 < 0.1 and self.schedules.lock == false then
if ((time % 1) < dtime) and self.schedules.lock == false then
-- Activate lock to avoid more than one entry to this code
self.schedules.lock = true
-- Get integer part of time
time = (time) - (time % 1)
-- Check if there is a schedule entry for this time
-- Note: Currently only one schedule is supported, for day 0
--minetest.log("Time: "..dump(time))
minetest.log("Time: "..dump(time))
local schedule = self.schedules.generic[0]
if schedule ~= nil then
-- Check if schedule for this time exists
--minetest.log("Found default schedule")
if schedule[time] ~= nil then
npc.log("WARNING", "Found schedule for time "..dump(time)..": "..dump(schedule[time]))
npc.log("DEBUG", "Adding actions to action queue")
-- Add to action queue all actions on schedule
for i = 1, #schedule[time] do
-- Check if schedule has a check function
if not schedule[time][i].check then
if schedule[time][i].check then
-- Add parameters for check function and run for first time
npc.log("INFO", "NPC "..dump(self.npc_name).." is starting check on "..minetest.pos_to_string(self.object:getpos()))
local check_params = schedule[time][i]
@ -1380,6 +1399,7 @@ mobs:register_mob("advanced_npc:npc", {
-- Execute check for the first time
npc.schedule_check(self)
else
npc.log("INFO", "Executing schedule entry: "..dump(schedule[time][i]))
-- Run usual schedule entry
-- Check chance
local execution_chance = math.random(1, 100)
@ -1423,10 +1443,9 @@ mobs:register_mob("advanced_npc:npc", {
npc.log("WARNING", "New action queue: "..dump(self.actions))
end
end
end
else
-- Check if lock can be released
if time % 1 > 0.1 then
if (time % 1) > dtime then
-- Release lock
self.schedules.lock = false
end

View File

@ -95,10 +95,7 @@ npc.occupations.registered_occupations = {}
-- don't have a specific occupation. It serves as an example.
npc.occupations.basic_def = {
-- Use random textures
textures = {
{"npc_male1.png"},
{"npc_child_male1.png"}
},
textures = {},
-- Use random dialogues
dialogues = {},
-- Initialize inventory with random items
@ -240,6 +237,7 @@ npc.occupations.basic_def = {
-- This function registers an occupation
function npc.occupations.register_occupation(name, def)
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

View File

@ -1,8 +1,12 @@
-- Advanced NPC spawner by Zorman2000
-- The advanced spawner will contain functionality to spawn NPC correctly on
-- mg_villages building. The spawn node will be the mg_villages:plotmarker.
-- This node will be replaced with one that will perform the following functions:
--
-- custom places, as well as in mg_villages building.
-- This works by using a special node to spawn NPCs on either a custom building or
-- on mg_villages building.
-- mg_villages functionality:
-- The spawn node for mg_villages will be the mg_villages:plotmarker.
-- Based on this node, the following things will be performed
-- - Scan the current building, check if it is of type:
-- - House
-- - Farm
@ -67,173 +71,252 @@ npc.spawner.spawn_data = {
}
}
local function get_basic_schedule()
return {
-- Create schedule entries
-- Morning actions: get out of bed, walk to outside of house
-- This will be executed around 8 AM MTG time
morning_actions = {
-- Get out of bed
[1] = {task = npc.actions.cmd.USE_BED, args = {
pos = npc.places.PLACE_TYPE.BED.PRIMARY,
action = npc.actions.const.beds.GET_UP
}
},
-- Walk outside
[2] = {task = npc.actions.cmd.WALK_TO_POS, args = {
end_pos = npc.places.PLACE_TYPE.OTHER.HOME_OUTSIDE,
walkable = {}
}
},
-- 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
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
[3] = {task = npc.actions.cmd.WALK_TO_POS, args = {
end_pos = npc.places.PLACE_TYPE.OTHER.HOME_INSIDE,
walkable = {}}
},
-- Allow mobs_redo wandering
[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
evening_actions = {
[1] = {task = npc.actions.cmd.WALK_TO_POS, args = {
end_pos = {place_type=npc.places.PLACE_TYPE.BED.PRIMARY, use_access_node=true},
walkable = {}
}
},
-- Use bed
[2] = {task = npc.actions.cmd.USE_BED, args = {
pos = npc.places.PLACE_TYPE.BED.PRIMARY,
action = npc.actions.const.beds.LAY
}
},
-- Stay put on bed
[3] = {action = npc.actions.cmd.FREEZE, args = {freeze = true}}
}
}
end
-- Array of nodes that serve as plotmarker of a plot, and therefore
-- as auto-spawners
spawner.plotmarker_nodes = {}
-- Array of items that are used to spawn NPCs
spawner.spawn_eggs = {}
---------------------------------------------------------------------------------------
-- Scanning functions
---------------------------------------------------------------------------------------
function spawner.filter_first_floor_nodes(nodes, ground_pos)
local result = {}
for _,node in pairs(nodes) do
if node.node_pos.y <= ground_pos.y + 2 then
table.insert(result, node)
end
end
return result
end
-- Creates an array of {pos=<node_pos>, owner=''} for managing
-- which NPC owns what
function spawner.get_nodes_by_type(start_pos, end_pos, type)
local result = {}
local nodes = npc.places.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"] = ''
table.insert(result, entry)
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 spawner.scan_area(pos1, pos2)
-- This function scans a 3D area that encloses a building and tries to identify:
-- - Entrance door
-- - Beds
-- - Storage nodes (chests, etc.)
-- - Furnace nodes
-- - Sittable nodes
-- It will return a table with all information gathered
-- Playername should be provided if manual spawning
function npc.spawner.scan_area_for_spawn(start_pos, end_pos, player_name)
local result = {
bed_type = {},
sittable_type = {},
furnace_type = {},
storage_type = {},
openable_type = {}
building_type = "",
building_plot_info = {},
building_entrance = {},
building_usable_nodes = {},
building_npcs = {},
building_npc_stats = {}
}
local start_pos, end_pos = vector.sort(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)
-- Set building_type
result.building_type = "custom"
-- Get min pos and max pos
local minp, maxp = vector.sort(start_pos, end_pos)
-- Set plot info
result.building_plot_info = {
-- TODO: Check this and see if it is accurate!
xsize = maxp.x - minp.x,
ysize = maxp.y - minp.y,
zsize = maxp.z - minp.z,
start_pos = start_pos,
end_pos = end_pos
}
-- Scan building nodes
-- Scan building for nodes
local usable_nodes = npc.places.scan_area_for_usable_nodes(start_pos, end_pos)
-- Get all doors
local doors = usable_nodes.openable_type
-- Find entrance node - this is very tricky when no outside position
-- is given. So to this end, three things will happen:
-- - First, we will check for plotmarker nodes. A plotmarker node should
-- be set at the left of the front door of the building. If this node is
-- found, it will assume it is at that location and use it.
-- - Second, we are going to search for an entrance marker. The entrance marker
-- will be directly in the posiition of the entrance node, so no search
-- is needed.
-- - Third, will assume that the start_pos is always at the left side of
-- the front of the building, where the entrance is
local outside_pos = start_pos
-- Check if there is a plotmarker or spawner node
local candidate_nodes = minetest.find_nodes_in_area_under_air(start_pos, end_pos,
{"mg_villages:plotmarker", "advanced_npc:auto_spawner"})
if table.getn(candidate_nodes) > 0 then
-- Found plotmarker, use it as outside_pos. Ideally should be only one
outside_pos = candidate_nodes[1]
elseif npc.spawner_marker and player_name then
-- Get entrance from spawner marker1
if npc.spawner_marker.entrance_markers[player_name] then
outside_pos = npc.spawner_marker.entrance_markers[player_name]
end
end
-- Try to find entrance
local entrance = npc.places.find_entrance_from_openable_nodes(doors, outside_pos)
if entrance then
npc.log("INFO", "Found building entrance at: "..minetest.pos_to_string(entrance.node_pos))
-- Set building entrance
result.building_entrance = entrance
else
npc.log("ERROR", "Unable to find building entrance!")
end
-- Set node_data
result.building_usable_nodes = usable_nodes
-- Initialize NPC stats
-- Initialize NPC stats
local npc_stats = {
male = {
total = 0,
adult = 0,
child = 0
},
female = {
total = 0,
adult = 0,
child = 0
},
adult_total = 0,
child_total = 0
}
result.building_npc_stats = npc_stats
--minetest.log("Found nodes inside area: "..dump(result))
return result
end
---------------------------------------------------------------------------------------
-- Spawning functions
---------------------------------------------------------------------------------------
-- This function is called when the node timer for spawning NPC
-- is expired. Can be called manually by supplying either:
-- - Position of mg_villages plotmarker, or,
-- - position of custom building spawner
-- Prerequisite for calling this function is:
-- - In case of mg_villages, spawner.adapt_mg_villages_plotmarker(), or,
-- - in case of custom buildings, npc.spawner.scan_area_for_spawn()
function npc.spawner.spawn_npc_on_plotmarker(pos)
-- Get timer
local timer = minetest.get_node_timer(pos)
-- Get metadata
local meta = minetest.get_meta(pos)
-- Get current NPC info
local area_info = {}
area_info["npcs"] = minetest.deserialize(meta:get_string("npcs"))
-- Get NPC stats
area_info["npc_stats"] = minetest.deserialize(meta:get_string("npc_stats"))
-- Get node data
area_info["entrance"] = minetest.deserialize(meta:get_string("entrance"))
area_info["node_data"] = minetest.deserialize(meta:get_string("node_data"))
-- Check amount of NPCs that should be spawned
area_info["npc_count"] = meta:get_int("npc_count")
area_info["spawned_npc_count"] = meta:get_int("spawned_npc_count")
-- TODO: Get occupation name
local metadata = npc.spawner.spawn_npc(pos, area_info)
-- Set all metadata back into the node
-- Increase NPC spawned count
area_info.spawned_npc_count = area_info.spawned_npc_count + 1
-- Store count into node
meta:set_int("spawned_npc_count", area_info.spawned_npc_count)
-- Store spawned NPC info
meta:set_string("npcs", minetest.serialize(area_info.npcs))
-- Store NPC stats
meta:set_string("npc_stats", minetest.serialize(area_info.npc_stats))
-- Check if there are more NPCs to spawn
if area_info.spawned_npc_count >= area_info.npc_count then
-- Stop timer
npc.log("INFO", "No more NPCs to spawn at this location")
timer:stop()
else
-- Start another timer to spawn more NPC
local new_delay = math.random(npc.spawner.spawn_delay)
npc.log("INFO", "Spawning one more NPC in "..dump(npc.spawner.spawn_delay).."s")
timer:start(new_delay)
end
end
-- This function spawns a NPC into the given pos.
-- If area_info is given, updated area_info is returned at end
function npc.spawner.spawn_npc(pos, area_info, occupation_name)
-- Get current NPC info
local npc_table = area_info.npcs
-- Get NPC stats
local npc_stats = area_info.npc_stats
-- Get building entrance
local entrance = area_info.entrance
-- Get node data
local node_data = area_info.node_data
-- Check amount of NPCs that should be spawned
local npc_count = area_info.npc_count
local spawned_npc_count = area_info.spawned_npc_count
-- Check if we actually have these variables - if we don't, it is because
-- this is a manually spawned NPC
local can_spawn = false
if npc_count and spawned_npc_count then
npc.log("INFO", "Currently spawned "..dump(spawned_npc_count).." of "..dump(npc_count).." NPCs")
if spawned_npc_count < npc_count then
can_spawn = true
end
else
-- Manually spawned
can_spawn = true
end
if can_spawn then
npc.log("INFO", "Spawning NPC at "..minetest.pos_to_string(pos))
-- Spawn a NPC
local ent = minetest.add_entity({x=pos.x, y=pos.y+1, z=pos.z}, "advanced_npc:npc")
if ent and ent:get_luaentity() then
ent:get_luaentity().initialized = false
-- Determine NPC occupation - use given or default
local occupation = occupation_name or "default_basic"
-- Initialize NPC
-- Call with stats if there are NPCs
if npc_table and #npc_table > 0 then
npc.initialize(ent, pos, false, npc_stats, occupation)
else
npc.initialize(ent, pos, nil, nil, occupation)
end
-- If entrance and node_data are present, assign nodes
if entrance and node_data then
npc.spawner.assign_places(ent:get_luaentity(), entrance, node_data, pos)
end
-- Store spawned NPC data and stats into node
local age = npc.age.adult
if ent:get_luaentity().child then
age = npc.age.child
end
-- TODO: Add more information here at some time...
local entry = {
status = npc.spawner.spawn_data.status.alive,
name = ent:get_luaentity().name,
id = ent:get_luaentity().npc_id,
sex = ent:get_luaentity().sex,
age = age,
born_day = minetest.get_day_count()
}
minetest.log("Area info: "..dump(area_info))
table.insert(area_info.npcs, entry)
-- Update and store stats
-- Increase total of NPCs for specific sex
npc_stats[ent:get_luaentity().sex].total =
npc_stats[ent:get_luaentity().sex].total + 1
-- Increase total number of NPCs by age
npc_stats[age.."_total"] = npc_stats[age.."_total"] + 1
-- Increase number of NPCs by age and sex
npc_stats[ent:get_luaentity().sex][age] =
npc_stats[ent:get_luaentity().sex][age] + 1
area_info.npc_stats = npc_stats
-- Return
npc.log("INFO", "Spawning successful!")
return true
else
npc.log("ERROR", "Spawning failed!")
ent:remove()
return false
end
end
end
-- This function will assign places to every NPC that belongs to a specific
-- house/building. It will use the resources of the house and give them
-- house/building. It will use the resources of the building and give them
-- until there's no more. Call this function after NPCs are initialized
-- The basic assumption:
-- - Use only items that are up to y+3 (first floor of building) for now
-- - Tell the NPC where the furnaces are
-- - Assign a unique bed to the NPC
-- - If there are as many chests as beds, assign one to a NPC
@ -241,14 +324,23 @@ end
-- - If there are as many benches as beds, assign one to a NPC
-- - Else, just let the NPC know one of the benches, but not own them
-- - Let the NPC know all doors to the house. Identify the front one as the entrance
function spawner.assign_places(self, pos)
local meta = minetest.get_meta(pos)
local entrance = minetest.deserialize(meta:get_string("entrance"))
local node_data = minetest.deserialize(meta:get_string("node_data"))
-- Self is the NPC lua entity object, pos is the position of the NPC spawner.
-- Prerequisite for using this function is to have called either
-- - In case of mg_villages, spawner.adapt_mg_villages_plotmarker(), or,
-- - in case of custom buildings, npc.spawner.scan_area_for_spawn()
-- Both function set the required metadata for this function
-- For mg_villages, this will be the position of the plotmarker node.
function npc.spawner.assign_places(self, entrance, node_data, pos)
minetest.log("Received node_data: "..dump(node_data))
--local meta = minetest.get_meta(pos)
--local entrance = minetest.deserialize(meta:get_string("entrance"))
--local node_data = minetest.deserialize(meta:get_string("node_data"))
-- Assign plotmarker
if pos then
npc.places.add_shared(self, npc.places.PLACE_TYPE.OTHER.HOME_PLOTMARKER,
npc.places.PLACE_TYPE.OTHER.HOME_PLOTMARKER, pos)
end
-- Assign entrance door and related locations
if entrance ~= nil and entrance.node_pos ~= nil then
@ -267,7 +359,7 @@ function spawner.assign_places(self, pos)
npc.places.add_owned_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))
--meta:set_string("node_data", minetest.serialize(node_data))
end
-- Assign sits
@ -278,7 +370,7 @@ function spawner.assign_places(self, pos)
npc.places.add_owned_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))
--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
@ -294,7 +386,7 @@ function spawner.assign_places(self, pos)
npc.places.add_owned_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))
--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
@ -310,7 +402,7 @@ function spawner.assign_places(self, pos)
npc.places.add_owned_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))
--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.
@ -321,126 +413,23 @@ function spawner.assign_places(self, pos)
npc.log("DEBUG", "Places for NPC "..self.npc_name..": "..dump(self.places_map))
-- Make NPC go into their house
-- If entrance is available let NPC
if entrance then
npc.add_task(self,
npc.actions.cmd.WALK_TO_POS,
{end_pos=npc.places.PLACE_TYPE.OTHER.HOME_INSIDE,
walkable={}})
npc.add_action(self, npc.actions.cmd.FREEZE, {freeze = false})
end
function spawner.assign_schedules(self, pos)
-- TODO: In the future, this needs to actually take into account
-- type of building and different schedules, e.g. farmers, traders, etc.
local basic_schedule = get_basic_schedule()
-- Add a simple schedule for testing
npc.create_schedule(self, npc.schedule_types.generic, 0)
-- 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, 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)
end
-- This function is called when the node timer for spawning NPC
-- is expired
function npc.spawner.spawn_npc(pos)
-- Get timer
local timer = minetest.get_node_timer(pos)
-- Get metadata
local meta = minetest.get_meta(pos)
-- Get current NPC info
local npc_table = minetest.deserialize(meta:get_string("npcs"))
-- Get NPC stats
local npc_stats = minetest.deserialize(meta:get_string("npc_stats"))
-- Check amount of NPCs that should be spawned
local npc_count = meta:get_int("npc_count")
local spawned_npc_count = meta:get_int("spawned_npc_count")
npc.log("INFO", "Currently spawned "..dump(spawned_npc_count).." of "..dump(npc_count).." NPCs")
if spawned_npc_count < npc_count then
npc.log("INFO", "Spawning NPC at "..minetest.pos_to_string(pos))
-- Spawn a NPC
local ent = minetest.add_entity({x=pos.x, y=pos.y+1, z=pos.z}, "advanced_npc:npc")
if ent and ent:get_luaentity() then
ent:get_luaentity().initialized = false
-- Determine NPC occupation
local occupation_name = "default_basic"
-- Initialize NPC
-- Call with stats if there are NPCs
if npc_table and #npc_table > 0 then
npc.initialize(ent, pos, false, npc_stats, occupation_name)
else
npc.initialize(ent, pos, nil, nil, occupation_name)
end
-- Assign nodes
spawner.assign_places(ent:get_luaentity(), pos)
-- Assign schedules
--spawner.assign_schedules(ent:get_luaentity(), pos)
-- Increase NPC spawned count
spawned_npc_count = spawned_npc_count + 1
-- Store count into node
meta:set_int("spawned_npc_count", spawned_npc_count)
-- Store spawned NPC data and stats into node
local age = npc.age.adult
if ent:get_luaentity().child then
age = npc.age.child
end
-- TODO: Add more information here at some time...
local entry = {
status = npc.spawner.spawn_data.status.alive,
name = ent:get_luaentity().name,
id = ent:get_luaentity().npc_id,
sex = ent:get_luaentity().sex,
age = age,
born_day = minetest.get_day_count()
}
table.insert(npc_table, entry)
meta:set_string("npcs", minetest.serialize(npc_table))
-- Update and store stats
-- Increase total of NPCs for specific sex
npc_stats[ent:get_luaentity().sex].total =
npc_stats[ent:get_luaentity().sex].total + 1
-- Increase total number of NPCs by age
npc_stats[age.."_total"] = npc_stats[age.."_total"] + 1
-- Increase number of NPCs by age and sex
npc_stats[ent:get_luaentity().sex][age] =
npc_stats[ent:get_luaentity().sex][age] + 1
meta:set_string("npc_stats", minetest.serialize(npc_stats))
-- Temp
--meta:set_string("infotext", meta:get_string("infotext")..", "..spawned_npc_count)
npc.log("INFO", "Spawning successful!")
-- Check if there are more NPCs to spawn
if spawned_npc_count >= npc_count then
-- Stop timer
npc.log("INFO", "No more NPCs to spawn at this location")
timer:stop()
else
-- Start another timer to spawn more NPC
local new_delay = math.random(npc.spawner.spawn_delay)
npc.log("INFO", "Spawning one more NPC in "..dump(npc.spawner.spawn_delay).."s")
timer:start(new_delay)
end
return true
else
npc.log("ERROR", "Spawning failed!")
ent:remove()
return false
end
end
return node_data
end
-- This function takes care of calculating how many NPCs will be spawn
function spawner.calculate_npc_spawning(pos)
-- Prerequisite for calling this function is:
-- - In case of mg_villages, spawner.adapt_mg_villages_plotmarker(), or,
-- - in case of custom buildings, npc.spawner.scan_area_for_spawn()
function npc.spawner.calculate_npc_spawning_on_plotmarker(pos)
-- Check node metadata
local meta = minetest.get_meta(pos)
if meta:get_string("replaced") ~= "true" then
@ -449,7 +438,7 @@ function spawner.calculate_npc_spawning(pos)
-- 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 mg_villages:plotmarker at position: "..minetest.pos_to_string(pos))
npc.log("ERROR", "Mis-configured spawner at position: "..minetest.pos_to_string(pos))
return
end
-- Check number of beds
@ -477,15 +466,28 @@ function spawner.calculate_npc_spawning(pos)
timer:start(delay)
end
---------------------------------------------------------------------------------------
-- Spawner nodes and items
---------------------------------------------------------------------------------------
-- The following are included:
-- - Auto-spawner: Basically a custom mg_villages:plotmarker that can be used
-- for custom buildings
-- - Manual spawner: This custom spawn item (egg) will show a formspec when used.
-- The formspec will allow the player the name of the NPC, the occupation and
-- the plot, entrance and workplace of the NPC. All of these are optional and
-- default values will be chosen whenever no input is provided.
---------------------------------------------------------------------------------------
-- Support code for mg_villages mods
---------------------------------------------------------------------------------------
if minetest.get_modpath("mg_villages") ~= nil then
-- This function creates a table of the scannable nodes inside
-- a mg_villages building. It needs the plotmarker position for a start
-- point and the building_data to get the x, y and z-coordinate size
-- of the building schematic
function spawner.scan_mg_villages_building(pos, building_data)
-- This function creates a table of the scannable nodes inside
-- a mg_villages building. It needs the plotmarker position for a start
-- point and the building_data to get the x, y and z-coordinate size
-- of the building schematic
function spawner.scan_mg_villages_building(pos, building_data)
--minetest.log("--------------------------------------------")
--minetest.log("Building data: "..dump(building_data))
--minetest.log("--------------------------------------------")
@ -544,26 +546,26 @@ function spawner.scan_mg_villages_building(pos, building_data)
npc.log("DEBUG", "Calculated end pos: "..minetest.pos_to_string(end_pos))
return spawner.scan_area(start_pos, end_pos)
end
return npc.places.scan_area_for_usable_nodes(start_pos, end_pos)
end
-- This function replaces an existent mg_villages:plotmarker with
-- and advanced_npc:auto_spawner. The existing metadata will be kept,
-- to allow compatibility. A new formspec will appear on right-click,
-- however it will as well allow to buy or manage the plot.
-- Also, the building is scanned for NPC-usable nodes and the amount
-- of NPCs to spawn and the interval is calculated.
function spawner.replace_mg_villages_plotmarker(pos)
-- This function "adapts" an existent mg_villages:plotmarker for NPC spawning.
-- The existing metadata will be kept, to allow compatibility. A new formspec
-- will appear on right-click, however it will as well allow to buy or manage
-- the plot. Also, the building is scanned for NPC-usable nodes and the amount
-- of NPCs to spawn and the interval is calculated.
function spawner.adapt_mg_villages_plotmarker(pos)
-- Get the meta at the current position
local meta = minetest.get_meta(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 == ""))
if (not village_id or (village_id and village_id == ""))
or (not plot_nr or (plot_nr and plot_nr == 0)) then
return
end
-- TODO: This should be replaced with new mg_villages API call
-- 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]
@ -574,12 +576,11 @@ function spawner.replace_mg_villages_plotmarker(pos)
if building_type == value then
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"})
-- Store old plotmarker metadata again
-- Store plotmarker metadata again
meta:set_string("village_id", village_id)
meta:set_int("plot_nr", plot_nr)
meta:set_string("infotext", infotext)
-- Store building type in metadata
meta:set_string("building_type", building_type)
-- Store plot information
@ -624,16 +625,12 @@ function spawner.replace_mg_villages_plotmarker(pos)
-- Set replaced
meta:set_string("replaced", "true")
-- Calculate how many NPCs will spawn
spawner.calculate_npc_spawning(pos)
npc.spawner.calculate_npc_spawning_on_plotmarker(pos)
-- Stop searching for building type
break
end
end
end
-- Only register the node, the ABM and the LBM if mg_villages mod
-- is present
if minetest.get_modpath("mg_villages") ~= nil then
end
-- Node registration
-- This node is currently a slightly modified mg_villages:plotmarker
@ -678,7 +675,7 @@ if minetest.get_modpath("mg_villages") ~= nil then
-- end,
on_timer = function(pos, elapsed)
npc.spawner.spawn_npc(pos)
npc.spawner.spawn_npc_on_plotmarker(pos)
end,
-- protect against digging
@ -718,13 +715,21 @@ if minetest.get_modpath("mg_villages") ~= nil then
catch_up = true,
action = function(pos, node, active_object_count, active_object_count_wider)
-- Check if replacement is needed
local meta = minetest.get_meta(pos)
if meta then
-- minetest.log("------ Plotmarker metadata -------")
-- local plot_nr = meta:get_int("plot_nr")
-- local village_id = meta:get_string("village_id")
-- minetest.log("Plot nr: "..dump(plot_nr)..", village ID: "..dump(village_id))
-- minetest.log(dump(mg_villages.get_plot_and_building_data( village_id, plot_nr )))
end
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)
spawner.adapt_mg_villages_plotmarker(pos)
end
end
})
@ -739,8 +744,9 @@ minetest.register_chatcommand("restore_plotmarkers", {
privs = {server=true},
func = function(name, param)
-- Check if radius is null
if param == nil then
if param == nil and type(param) ~= "number" then
minetest.chat_send_player(name, "Need to enter a radius as an integer number. Ex. /restore_plotmarkers 10 for a radius of 10")
return
end
-- Get player position
local pos = {}

3
spawner_marker.lua Normal file
View File

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

BIN
textures/npc_female10.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
textures/npc_female11.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
textures/npc_female2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
textures/npc_female3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
textures/npc_female4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
textures/npc_female5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
textures/npc_female6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
textures/npc_female7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
textures/npc_female8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
textures/npc_female9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
textures/npc_male10.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
textures/npc_male11.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
textures/npc_male12.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
textures/npc_male13.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
textures/npc_male14.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
textures/npc_male15.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
textures/npc_male8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
textures/npc_male9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB