From 4c102a70a451298ca91884ba998a362716b0849c Mon Sep 17 00:00:00 2001 From: Hector Franqui Date: Fri, 25 Aug 2017 09:31:45 -0400 Subject: [PATCH] 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. --- actions/places.lua | 83 +- {random_data => data}/dialogues_data.lua | 0 {random_data => data}/gift_items_data.lua | 0 {random_data => data}/names_data.lua | 0 {random_data => data}/occupations_data.lua | 0 init.lua | 8 +- npc.lua | 327 +++--- occupations/occupations.lua | 62 +- spawner.lua | 1074 ++++++++++---------- spawner_marker.lua | 3 + textures/npc_female10.png | Bin 0 -> 1915 bytes textures/npc_female11.png | Bin 0 -> 1898 bytes textures/npc_female2.png | Bin 0 -> 1587 bytes textures/npc_female3.png | Bin 0 -> 1522 bytes textures/npc_female4.png | Bin 0 -> 1422 bytes textures/npc_female5.png | Bin 0 -> 1932 bytes textures/npc_female6.png | Bin 0 -> 2025 bytes textures/npc_female7.png | Bin 0 -> 1907 bytes textures/npc_female8.png | Bin 0 -> 1883 bytes textures/npc_female9.png | Bin 0 -> 1986 bytes textures/npc_male10.png | Bin 0 -> 1734 bytes textures/npc_male11.png | Bin 0 -> 1671 bytes textures/npc_male12.png | Bin 0 -> 1667 bytes textures/npc_male13.png | Bin 0 -> 1715 bytes textures/npc_male14.png | Bin 0 -> 1739 bytes textures/npc_male15.png | Bin 0 -> 1654 bytes textures/npc_male8.png | Bin 0 -> 1503 bytes textures/npc_male9.png | Bin 0 -> 2419 bytes 28 files changed, 816 insertions(+), 741 deletions(-) rename {random_data => data}/dialogues_data.lua (100%) rename {random_data => data}/gift_items_data.lua (100%) rename {random_data => data}/names_data.lua (100%) rename {random_data => data}/occupations_data.lua (100%) create mode 100644 spawner_marker.lua create mode 100644 textures/npc_female10.png create mode 100644 textures/npc_female11.png create mode 100644 textures/npc_female2.png create mode 100644 textures/npc_female3.png create mode 100644 textures/npc_female4.png create mode 100644 textures/npc_female5.png create mode 100644 textures/npc_female6.png create mode 100644 textures/npc_female7.png create mode 100644 textures/npc_female8.png create mode 100644 textures/npc_female9.png create mode 100644 textures/npc_male10.png create mode 100644 textures/npc_male11.png create mode 100644 textures/npc_male12.png create mode 100644 textures/npc_male13.png create mode 100644 textures/npc_male14.png create mode 100644 textures/npc_male15.png create mode 100644 textures/npc_male8.png create mode 100644 textures/npc_male9.png diff --git a/actions/places.lua b/actions/places.lua index af1b076..8c83294 100644 --- a/actions/places.lua +++ b/actions/places.lua @@ -102,7 +102,7 @@ function npc.places.add_owned_accessible_place(self, nodes, place_type) -- Set owner to this NPC nodes[i].owner = self.npc_id -- Assign node to NPC - npc.places.add_owned(self, place_type, place_type, + 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)) @@ -126,7 +126,7 @@ function npc.places.add_shared_accessible_place(self, nodes, place_type, overrid -- Check if node is accessible if #empty_nodes > 0 then -- Assign node to NPC - npc.places.add_shared(self, place_type..dump(i), + npc.places.add_shared(self, place_type..dump(i), place_type, nodes[i].node_pos, empty_nodes[1].pos) end end @@ -139,7 +139,7 @@ function npc.places.add_shared_accessible_place(self, nodes, place_type, overrid -- Check if node is accessible if #empty_nodes > 0 then -- Nodes is only one node - npc.places.add_shared(self, place_type, place_type, + npc.places.add_shared(self, place_type, place_type, nodes.node_pos, empty_nodes[1].pos) end end @@ -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) @@ -169,7 +175,7 @@ end -- TODO: This function can be improved to support a radius greater than 1. function npc.places.find_node_orthogonally(pos, nodes, y_adjustment) - -- Calculate orthogonal points + -- Calculate orthogonal points local points = {} table.insert(points, {x=pos.x+1,y=pos.y+y_adjustment,z=pos.z}) table.insert(points, {x=pos.x-1,y=pos.y+y_adjustment,z=pos.z}) @@ -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=, 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 @@ -205,7 +264,7 @@ function npc.places.find_entrance_from_openable_nodes(all_openable_nodes, marker -- Filter out all other openable nodes except MTG doors. -- Why? For supported village types (which are: medieval, nore - -- and logcabin) all buildings use, as the main entrance, + -- and logcabin) all buildings use, as the main entrance, -- a MTG door. Some medieval building have "half_doors" (like farms) -- which NPCs love to confuse with the right building entrance. for i = 1, #all_openable_nodes do @@ -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, {}) @@ -258,7 +307,7 @@ function npc.places.find_entrance_from_openable_nodes(all_openable_nodes, marker if #path < min then -- Set min to path length and the result to the currently found node min = #path - result = openable_nodes[i] + result = openable_nodes[i] else -- Specific check to prefer mtg's doors to cottages' doors. -- The reason? Sometimes a cottages' door could be closer to the @@ -419,4 +468,4 @@ function npc.places.find_node_in_front_of_door(door_pos) -- Looking west return {x=door_pos.x + 1, y=door_pos.y, z=door_pos.z} end -end \ No newline at end of file +end diff --git a/random_data/dialogues_data.lua b/data/dialogues_data.lua similarity index 100% rename from random_data/dialogues_data.lua rename to data/dialogues_data.lua diff --git a/random_data/gift_items_data.lua b/data/gift_items_data.lua similarity index 100% rename from random_data/gift_items_data.lua rename to data/gift_items_data.lua diff --git a/random_data/names_data.lua b/data/names_data.lua similarity index 100% rename from random_data/names_data.lua rename to data/names_data.lua diff --git a/random_data/occupations_data.lua b/data/occupations_data.lua similarity index 100% rename from random_data/occupations_data.lua rename to data/occupations_data.lua diff --git a/init.lua b/init.lua index 01c8fe2..23efd33 100755 --- a/init.lua +++ b/init.lua @@ -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")) diff --git a/npc.lua b/npc.lua index ba5a47a..210b1f9 100755 --- a/npc.lua +++ b/npc.lua @@ -1,5 +1,5 @@ -- Advanced NPC by Zorman2000 --- Based on original NPC by Tenplus1 +-- Based on original NPC by Tenplus1 local S = mobs.intllib @@ -133,10 +133,10 @@ local function get_random_texture(sex, age) for i = 1, #textures do local current_texture = textures[i][1] - if (sex == npc.MALE - and string.find(current_texture, sex) + if (sex == npc.MALE + and string.find(current_texture, sex) and not string.find(current_texture, npc.FEMALE)) - or (sex == npc.FEMALE + or (sex == npc.FEMALE and string.find(current_texture, sex)) then table.insert(filtered_textures, current_texture) end @@ -156,18 +156,18 @@ function npc.get_random_texture_from_array(age, sex, textures) for i = 1, #textures do local current_texture = textures[i] -- Filter by age - if (sex == npc.MALE - and string.find(current_texture, sex) + if (sex == npc.MALE + and string.find(current_texture, sex) and not string.find(current_texture, npc.FEMALE) - and ((age == npc.age.adult + and ((age == npc.age.adult and not string.find(current_texture, npc.age.child)) or (age == npc.age.child and string.find(current_texture, npc.age.child)) ) ) - or (sex == npc.FEMALE + or (sex == npc.FEMALE and string.find(current_texture, sex) - and ((age == npc.age.adult + and ((age == npc.age.adult and not string.find(current_texture, npc.age.child)) or (age == npc.age.child and string.find(current_texture, npc.age.child)) @@ -185,7 +185,7 @@ function npc.get_random_texture_from_array(age, sex, textures) return filtered_textures[math.random(1, #filtered_textures)] end --- Choose whether NPC can have relationships. Only 30% of NPCs +-- Choose whether NPC can have relationships. Only 30% of NPCs -- cannot have relationships local function can_have_relationships(is_child) -- Children can't have relationships @@ -201,11 +201,11 @@ end local function choose_spawn_items(self) local number_of_items_to_add = math.random(1, 2) local number_of_items = #npc.FAVORITE_ITEMS[self.sex].phase1 - + for i = 1, number_of_items_to_add do npc.add_item_to_inventory( self, - npc.FAVORITE_ITEMS[self.sex].phase1[math.random(1, number_of_items)].item, + npc.FAVORITE_ITEMS[self.sex].phase1[math.random(1, number_of_items)].item, math.random(1,5) ) end @@ -244,7 +244,7 @@ function npc.initialize(entity, pos, is_lua_entity, npc_stats, occupation_name) -- Avoid NPC to be removed by mobs_redo API ent.remove_ok = false - + -- Determine sex and age -- If there's no previous NPC data, sex and age will be randomly chosen. -- - Sex: Female or male will have each 50% of spawning @@ -256,7 +256,7 @@ function npc.initialize(entity, pos, is_lua_entity, npc_stats, occupation_name) -- - Age: For each two adults, the chance of spawning a child next will be 50% -- If there's a child for two adults, the chance of spawning a child goes to -- 40% and keeps decreasing unless two adults have no child. - -- Use NPC stats if provided + -- Use NPC stats if provided if npc_stats then -- Default chances local male_s, male_e = 0, 50 @@ -273,7 +273,7 @@ function npc.initialize(entity, pos, is_lua_entity, npc_stats, occupation_name) end -- Determine age probabilities if npc_stats["adult_total"] >= 2 then - if npc_stats["adult_total"] % 2 == 0 + if npc_stats["adult_total"] % 2 == 0 and (npc_stats["adult_total"] / 2 > npc_stats["child_total"]) then child_s,child_e = 26, 100 adult_e = 25 @@ -307,7 +307,7 @@ function npc.initialize(entity, pos, is_lua_entity, npc_stats, occupation_name) ent.collisionbox = {-0.10,-0.50,-0.10, 0.10,0.40,0.10} ent.is_child = true -- For mobs_redo - ent.child = true + ent.child = true end -- Store the selected age ent.age = selected_age @@ -340,7 +340,7 @@ function npc.initialize(entity, pos, is_lua_entity, npc_stats, occupation_name) -- Set ID ent.npc_id = tostring(math.random(1000, 9999))..":"..ent.npc_name - + -- Initialize all gift data ent.gift_data = { -- Choose favorite items. Choose phase1 per default @@ -348,7 +348,7 @@ function npc.initialize(entity, pos, is_lua_entity, npc_stats, occupation_name) -- Choose disliked items. Choose phase1 per default disliked_items = npc.relationships.select_random_disliked_items(ent.sex), } - + -- Flag that determines if NPC can have a relationship ent.can_have_relationship = can_have_relationships(ent.is_child) @@ -365,7 +365,7 @@ function npc.initialize(entity, pos, is_lua_entity, npc_stats, occupation_name) -- Initialize dialogues ent.dialogues = npc.dialogue.select_random_dialogues_for_npc(ent, "phase1") - + -- Declare NPC inventory ent.inventory = initialize_inventory() @@ -435,14 +435,14 @@ function npc.initialize(entity, pos, is_lua_entity, npc_stats, occupation_name) is_walking = false, -- Path that the NPC is following path = {}, - -- Target position the NPC is supposed to walk to in this step. NOTE: + -- Target position the NPC is supposed to walk to in this step. NOTE: -- This is NOT the end of the path, but the next position in the path -- relative to the last position target_pos = {} } } - -- This flag is checked on every step. If it is true, the rest of + -- This flag is checked on every step. If it is true, the rest of -- Mobs Redo API is not executed ent.freeze = nil @@ -453,7 +453,7 @@ function npc.initialize(entity, pos, is_lua_entity, npc_stats, occupation_name) -- Schedule data ent.schedules = { -- Flag to enable or disable the schedules functionality - enabled = true, + enabled = true, -- Lock for when executing a schedule lock = false, -- Queue of schedules executed @@ -463,7 +463,7 @@ function npc.initialize(entity, pos, is_lua_entity, npc_stats, occupation_name) --- when calendars are implemented. Allows for only 7 schedules, -- one for each day of the week generic = {}, - -- An array of schedules, meant to be for specific dates in the + -- An array of schedules, meant to be for specific dates in the -- year. Can contain as many as possible. The keys will be strings -- in the format MM:DD date_based = {}, @@ -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)) @@ -561,7 +562,7 @@ function npc.add_item_to_inventory(self, item_name, count) local existing_count = npc.get_item_count(existing_item.item_string) if (existing_count + count) < npc.INVENTORY_ITEM_MAX_STACK then -- Set item here - self.inventory[existing_item.slot] = + self.inventory[existing_item.slot] = npc.get_item_name(existing_item.item_string).." "..tostring(existing_count + count) return true else @@ -569,7 +570,7 @@ function npc.add_item_to_inventory(self, item_name, count) for i = 1, #self.inventory do if self.inventory[i] == "" then -- Found slot, set item - self.inventory[i] = + self.inventory[i] = item_name.." "..tostring((existing_count + count) - npc.INVENTORY_ITEM_MAX_STACK) return true end @@ -789,7 +790,7 @@ function npc.lock_actions(self) -- Reset timer so that it has some time after interaction is done self.actions.action_timer = 0 -- Check if there are is an action executing - if self.actions.current_action_state == npc.action_state.executing + if self.actions.current_action_state == npc.action_state.executing and self.freeze == false then -- Store the current action state self.actions.state_before_lock.action_state = self.actions.current_action_state @@ -809,7 +810,7 @@ function npc.unlock_actions(self) self.actions.action_timer_lock = false -- Restore the value of self.freeze self.freeze = self.actions.state_before_lock.freeze - + if table.getn(self.actions.queue) == 0 then -- Allow mobs_redo API to execute since action queue is empty self.freeze = true @@ -822,7 +823,7 @@ end -- Schedule functionality --------------------------------------------------------------------------------------- -- Schedules allow the NPC to do different things depending on the time of the day. --- The time of the day is in 24 hours and is consistent with the Minetest Game +-- The time of the day is in 24 hours and is consistent with the Minetest Game -- /time command. Hours will be written as numbers: 1 for 1:00, 13 for 13:00 or 1:00 PM -- The API is as following: a schedule can be created for a specific date or for a -- day of the week. A date is a string in the format MM:DD @@ -839,7 +840,7 @@ npc.schedule_properties = { can_receive_gifts = "can_receive_gifts" } -local function get_time_in_hours() +local function get_time_in_hours() return minetest.get_timeofday() * 24 end @@ -848,7 +849,7 @@ end -- - Generic: Returns nil if there are already -- seven schedules, one for each day of the -- week or if the schedule attempting to add --- already exists. The date parameter is the +-- already exists. The date parameter is the -- day of the week it represents as follows: -- - 1: Monday -- - 2: Tuesday @@ -879,7 +880,7 @@ function npc.create_schedule(self, schedule_type, date) elseif schedule_type == npc.schedule_types.date then -- Check schedule doesn't exists already if self.schedules.date_based[date] == nil then - -- Add schedule + -- Add schedule self.schedules.date_based[date] = {} else -- Schedule already present @@ -895,7 +896,7 @@ end -- Schedule entries API -- Allows to add, get, update and delete entries from each --- schedule. Attempts to be as safe-fail as possible to avoid crashes. +-- schedule. Attempts to be as safe-fail as possible to avoid crashes. -- Actions is an array of actions and tasks that the NPC -- will perform at the scheduled time on the scheduled date @@ -996,11 +997,11 @@ function npc.add_schedule_check(self) table.insert(self.actions.queue, {action="schedule_check", args={}, is_task=false}) end --- Range: integer, radius in which nodes will be searched. Recommended radius is --- between 1-3 +-- Range: integer, radius in which nodes will be searched. Recommended radius is +-- between 1-3 -- Nodes: array of node names --- Actions: map of node names to entries {action=, args={}}. --- Arguments can be empty - the check function will try to determine most +-- Actions: map of node names to entries {action=, args={}}. +-- Arguments can be empty - the check function will try to determine most -- arguments anyways (like pos and dir). -- Special node "any" will execute those actions on any node except the -- already specified ones. @@ -1049,7 +1050,7 @@ function npc.schedule_check(self) -- behavior for placing nodes is replacing digged nodes. A NPC farmer, -- for instance, might dig a plant node and plant another one on the -- same position. - -- Defaults: items will be taken from inventory if existing, + -- Defaults: items will be taken from inventory if existing, -- if not will be force-placed (item comes from thin air) -- Protection will be respected args = { @@ -1076,7 +1077,7 @@ function npc.schedule_check(self) elseif actions[node.name][i].action == npc.actions.cmd.WALK_TO_POS then -- Optimize walking -- since distances can be really short, -- a simple walk_step() action can do most of the times. For - -- this, however, we need to calculate direction + -- this, however, we need to calculate direction -- First of all, check distance if vector.distance(start_pos, node_pos) < 3 then -- Will do walk_step based instead @@ -1105,7 +1106,7 @@ function npc.schedule_check(self) npc.add_action(self, action or actions[node.name][i].action, args or actions[node.name][i].args) end -- Enqueue next schedule check - if self.schedules.current_check_params.execution_count + if self.schedules.current_check_params.execution_count < self.schedules.current_check_params.execution_times then npc.add_schedule_check() end @@ -1118,7 +1119,7 @@ function npc.schedule_check(self) npc.add_action(self, none_actions[i].action, none_actions[i].args) end -- Enqueue next schedule check - if self.schedules.current_check_params.execution_count + if self.schedules.current_check_params.execution_count < self.schedules.current_check_params.execution_times then npc.add_schedule_check() end @@ -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,13 +1226,13 @@ 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 --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 + 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()] @@ -1232,16 +1252,15 @@ mobs:register_mob("advanced_npc:npc", { end, name ) - else - npc.start_dialogue(self, clicker, true) - end - - end, + else + npc.start_dialogue(self, clicker, true) + end + end, do_custom = function(self, dtime) if self.initialized == nil then -- Initialize NPC if spawned using the spawn egg built in from -- mobs_redo. This functionality will be removed in the future in - -- favor of a better manual spawning method with customization + -- favor of a better manual spawning method with customization npc.log("WARNING", "Initializing NPC from entity step. This message should only be appearing if an NPC is being spawned from inventory with egg!") npc.initialize(self, self.object:getpos(), true) self.tamed = false @@ -1250,7 +1269,7 @@ mobs:register_mob("advanced_npc:npc", { -- NPC is initialized, check other variables -- Check child texture issues if self.is_child then - -- Check texture + -- Check texture npc.texture_check.timer = npc.texture_check.timer + dtime if npc.texture_check.timer > npc.texture_check.interval then -- Reset timer @@ -1266,19 +1285,20 @@ 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 + end - -- Timer function for casual traders to reset their trade offers + -- Timer function for casual traders to reset their trade offers self.trader_data.change_offers_timer = self.trader_data.change_offers_timer + dtime -- Check if time has come to change offers - if self.trader_data.trader_status == npc.trade.CASUAL and + if self.trader_data.trader_status == npc.trade.CASUAL and self.trader_data.change_offers_timer >= self.trader_data.change_offers_timer_interval then -- Reset timer self.trader_data.change_offers_timer = 0 -- Re-select casual trade offers npc.trade.generate_trade_offers_by_status(self) end - + -- Timer function for gifts for i = 1, #self.relationships do local relationship = self.relationships[i] @@ -1291,15 +1311,15 @@ mobs:register_mob("advanced_npc:npc", { relationship.talk_timer_value = relationship.talk_timer_value + dtime else -- Relationship decrease timer - if relationship.relationship_decrease_timer_value + if relationship.relationship_decrease_timer_value < relationship.relationship_decrease_interval then - relationship.relationship_decrease_timer_value = + relationship.relationship_decrease_timer_value = relationship.relationship_decrease_timer_value + dtime else -- Check if married to decrease half if relationship.phase == "phase6" then -- Avoid going below the marriage phase limit - if (relationship.points - 0.5) >= + if (relationship.points - 0.5) >= npc.relationships.RELATIONSHIP_PHASE["phase5"].limit then relationship.points = relationship.points - 0.5 end @@ -1335,104 +1355,103 @@ mobs:register_mob("advanced_npc:npc", { end end - -- Schedule timer - -- Check if schedules are enabled - if self.schedules.enabled == true then - -- Get time of day - local time = get_time_in_hours() - -- Check if time is an hour - if time % 1 < 0.1 and self.schedules.lock == false then - -- Activate lock to avoid more than one entry to this code - self.schedules.lock = true - -- Get integer part of time - time = (time) - (time % 1) - -- Check if there is a schedule entry for this time - -- Note: Currently only one schedule is supported, for day 0 - --minetest.log("Time: "..dump(time)) - local schedule = self.schedules.generic[0] - if schedule ~= nil then - -- Check if schedule for this time exists - --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 - -- 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] - -- Calculates how many times check will be executed - local execution_times = check_params.count - if check_params.random_execution_times then - execution_times = math.random(check_params.min_count, check_params.max_count) - end - -- Set current parameters - self.schedules.current_check_params = { - range = check_params.range, - nodes = check_params.nodes, - actions = check_params.actions, - none_actions = check_params.none_actions, - execution_count = 0, - execution_times = execution_times - } - -- Execute check for the first time - npc.schedule_check(self) - else - -- Run usual schedule entry - -- Check chance - local execution_chance = math.random(1, 100) - if not schedule[time][i].chance or - (schedule[time][i].chance and execution_chance <= schedule[time][i].chance) then - -- Check if entry has dependency on other entry - local dependencies_met = nil - if schedule[time][i].depends then - dependencies_met = npc.utils.array_is_subset_of_array( - self.schedules.temp_executed_queue, - schedule[time][i].depends) - end + -- Schedule timer + -- Check if schedules are enabled + if self.schedules.enabled == true then + -- Get time of day + local time = get_time_in_hours() + -- Check if time is an hour + if ((time % 1) < dtime) and self.schedules.lock == false then + -- Activate lock to avoid more than one entry to this code + self.schedules.lock = true + -- Get integer part of time + time = (time) - (time % 1) + -- Check if there is a schedule entry for this time + -- Note: Currently only one schedule is supported, for day 0 + minetest.log("Time: "..dump(time)) + local schedule = self.schedules.generic[0] + if schedule ~= nil then + -- Check if schedule for this time exists + if schedule[time] ~= nil then + npc.log("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 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] + -- Calculates how many times check will be executed + local execution_times = check_params.count + if check_params.random_execution_times then + execution_times = math.random(check_params.min_count, check_params.max_count) + end + -- Set current parameters + self.schedules.current_check_params = { + range = check_params.range, + nodes = check_params.nodes, + actions = check_params.actions, + none_actions = check_params.none_actions, + execution_count = 0, + execution_times = execution_times + } + -- Execute check for the first time + npc.schedule_check(self) + else + npc.log("INFO", "Executing schedule entry: "..dump(schedule[time][i])) + -- Run usual schedule entry + -- Check chance + local execution_chance = math.random(1, 100) + if not schedule[time][i].chance or + (schedule[time][i].chance and execution_chance <= schedule[time][i].chance) then + -- Check if entry has dependency on other entry + local dependencies_met = nil + if schedule[time][i].depends then + dependencies_met = npc.utils.array_is_subset_of_array( + self.schedules.temp_executed_queue, + schedule[time][i].depends) + end - -- Check for dependencies being met - if dependencies_met == nil or dependencies_met == true then - -- Add tasks - if schedule[time][i].task ~= nil then - -- Add task - npc.add_task(self, schedule[time][i].task, schedule[time][i].args) - elseif schedule[time][i].action ~= nil then - -- Add action - npc.add_action(self, schedule[time][i].action, schedule[time][i].args) - elseif schedule[time][i].property ~= nil then - -- Change NPC property - npc.schedule_change_property(self, schedule[time][i].property, schedule[time][i].args) - end - -- Backward compatibility check - if self.schedules.temp_executed_queue then - -- Add into execution queue to meet dependency - table.insert(self.schedules.temp_executed_queue, i) - end + -- Check for dependencies being met + if dependencies_met == nil or dependencies_met == true then + -- Add tasks + if schedule[time][i].task ~= nil then + -- Add task + npc.add_task(self, schedule[time][i].task, schedule[time][i].args) + elseif schedule[time][i].action ~= nil then + -- Add action + npc.add_action(self, schedule[time][i].action, schedule[time][i].args) + elseif schedule[time][i].property ~= nil then + -- Change NPC property + npc.schedule_change_property(self, schedule[time][i].property, schedule[time][i].args) + end + -- Backward compatibility check + if self.schedules.temp_executed_queue then + -- Add into execution queue to meet dependency + table.insert(self.schedules.temp_executed_queue, i) end - else - -- TODO: Change to debug - npc.log("WARNING", "Skipping schedule entry for time "..dump(time)..": "..dump(schedule[time][i])) end - end - end - -- Clear execution queue - self.schedules.temp_executed_queue = {} - 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 - -- Release lock - self.schedules.lock = false - end - end - end - + else + -- TODO: Change to debug + npc.log("WARNING", "Skipping schedule entry for time "..dump(time)..": "..dump(schedule[time][i])) + end + end + end + -- Clear execution queue + self.schedules.temp_executed_queue = {} + npc.log("WARNING", "New action queue: "..dump(self.actions)) + end + end + else + -- Check if lock can be released + if (time % 1) > dtime then + -- Release lock + self.schedules.lock = false + end + end + end + return self.freeze end }) diff --git a/occupations/occupations.lua b/occupations/occupations.lua index 08dea73..2a27b7f 100644 --- a/occupations/occupations.lua +++ b/occupations/occupations.lua @@ -6,7 +6,7 @@ -- Occupations are essentially specific schedules, that can have slight -- random variations to provide diversity and make specific occupations -- less predictable. Occupations are associated with textures, dialogues, --- specific initial items, type of building (and surroundings) where NPC +-- specific initial items, type of building (and surroundings) where NPC -- lives, etc. -- Example of an occupation: farmer -- The farmer will have to live in a farm, or just beside a field. @@ -38,10 +38,10 @@ -- tags = {}, -- -- Array of tags to search for. This will have tags -- -- if the type is either "mix" or "tags" --- +-- -- }, -- textures = {}, --- -- Textures are an array of textures, as usually given on +-- -- Textures are an array of textures, as usually given on -- -- an entity definition. If given, the NPC will be guaranteed -- -- to have one of the given textures. Also, ensure they have sex -- -- as well in the filename so they can be chosen appropriately. @@ -76,10 +76,10 @@ -- [23] = {[1] = action=npc.action.cmd.freeze, args={freeze=false}} -- -- } -- -- The numbers, [1], [13] and [23] are the times when the entries --- -- corresponding to each are supposed to happen. The tables with +-- -- corresponding to each are supposed to happen. The tables with -- -- [1], [1],[2] and [1] actions respectively are the entries that -- -- will happen at time 1, 13 and 23. --- } +-- } -- Public API npc.occupations = {} @@ -91,14 +91,11 @@ local occupations = {} -- The key is the name of the occupation. npc.occupations.registered_occupations = {} --- This is the basic occupation definition, this is for all NPCs that +-- This is the basic occupation definition, this is for all NPCs that -- don't have a specific occupation. It serves as an example. npc.occupations.basic_def = { -- Use random textures - textures = { - {"npc_male1.png"}, - {"npc_child_male1.png"} - }, + textures = {}, -- Use random dialogues dialogues = {}, -- Initialize inventory with random items @@ -109,13 +106,13 @@ npc.occupations.basic_def = { [7] = { -- Get out of bed [1] = {task = npc.actions.cmd.USE_BED, args = { - pos = npc.places.PLACE_TYPE.BED.PRIMARY, + pos = npc.places.PLACE_TYPE.BED.PRIMARY, action = npc.actions.const.beds.GET_UP - } - }, + } + }, -- Walk to home inside [2] = {task = npc.actions.cmd.WALK_TO_POS, args = { - end_pos = npc.places.PLACE_TYPE.OTHER.HOME_INSIDE, + end_pos = npc.places.PLACE_TYPE.OTHER.HOME_INSIDE, walkable = {} }, chance = 75 @@ -127,7 +124,7 @@ npc.occupations.basic_def = { [8] = { -- Walk to outside of home [1] = {task = npc.actions.cmd.WALK_TO_POS, args = { - end_pos = npc.places.PLACE_TYPE.OTHER.HOME_OUTSIDE, + end_pos = npc.places.PLACE_TYPE.OTHER.HOME_OUTSIDE, walkable = {} }, chance = 75 @@ -142,7 +139,7 @@ npc.occupations.basic_def = { end_pos = {place_type=npc.places.PLACE_TYPE.SITTABLE.PRIMARY, use_access_node=true}, walkable = {"cottages:bench"} }, - chance = 75 + chance = 75 }, -- Sit on the node [2] = {task = npc.actions.cmd.USE_SITTABLE, args = { @@ -155,13 +152,13 @@ npc.occupations.basic_def = { [3] = {action = npc.actions.cmd.SET_INTERVAL, args = { freeze = true, interval = 35 - }, + }, depends = {2} }, [4] = {action = npc.actions.cmd.SET_INTERVAL, args = { freeze = true, interval = npc.actions.default_interval - }, + }, depends = {3} }, -- Get up from sit @@ -210,9 +207,9 @@ npc.occupations.basic_def = { }, -- Get inside home [3] = {task = npc.actions.cmd.WALK_TO_POS, args = { - end_pos = npc.places.PLACE_TYPE.OTHER.HOME_INSIDE, + end_pos = npc.places.PLACE_TYPE.OTHER.HOME_INSIDE, walkable = {} - } + } }, -- Allow mobs_redo wandering [4] = {action = npc.actions.cmd.FREEZE, args = {freeze = false}} @@ -220,16 +217,16 @@ npc.occupations.basic_def = { -- Schedule entry for 10 in the evening [22] = { [1] = {task = npc.actions.cmd.WALK_TO_POS, args = { - end_pos = {place_type=npc.places.PLACE_TYPE.BED.PRIMARY, use_access_node=true}, + 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, + 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}} } @@ -239,7 +236,8 @@ npc.occupations.basic_def = { -- This function registers an occupation function npc.occupations.register_occupation(name, def) - npc.occupations.registered_occupations[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 @@ -256,13 +254,13 @@ function npc.occupations.get_for_building(building_type, surrounding_building_ty -- Check if building type is contained in the def's building types if npc.utils.array_contains(def.building_types, building_type) then -- Check for empty or nil surrounding building types - if def.surrounding_building_types == nil + if def.surrounding_building_types == nil or def.surrounding_building_types == {} then -- Add this occupation table.insert(result, name) else -- Check if surround type is included in the def - if npc.utils.array_is_subset_of_array(def.surrounding_building_types, + if npc.utils.array_is_subset_of_array(def.surrounding_building_types, surrounding_building_types) then -- Add this occupation table.insert(result, name) @@ -291,7 +289,7 @@ function npc.occupations.initialize_occupation_values(self, occupation_name) -- Initialize textures, else it will leave the current textures minetest.log("Texture entries: "..dump(table.getn(def.textures))) if def.textures and table.getn(def.textures) > 0 then - self.selected_texture = + self.selected_texture = npc.get_random_texture_from_array(self.sex, self.age, def.textures) -- Set texture if it found for sex and age minetest.log("No textures found for sex "..dump(self.sex).." and age "..dump(self.age)) @@ -351,7 +349,7 @@ function npc.occupations.initialize_occupation_values(self, occupation_name) -- Add keys to set of dialogue keys for _, key in npc.utils.get_map_keys(dialogues) do table.insert(dialogue_keys, key) - end + end elseif def.dialogues.type == "tags" then -- We need to find the dialogues from tags. def.dialogues.tags contains -- an array of tags that we will use to search. @@ -361,7 +359,7 @@ function npc.occupations.initialize_occupation_values(self, occupation_name) end -- Add dialogues to NPC -- Check if there is a max of dialogues to be added - local max_dialogue_count = npc.dialogue.MAX_DIALOGUES + local max_dialogue_count = npc.dialogue.MAX_DIALOGUES if def.dialogues.max_count and def.max_dialogue_count > 0 then max_dialogue_count = def.dialogues.max_count end @@ -385,4 +383,4 @@ function npc.occupations.initialize_occupation_values(self, occupation_name) npc.log("INFO", "Successfully initialized NPC with occupation values") -end \ No newline at end of file +end diff --git a/spawner.lua b/spawner.lua index a72d4ca..c5ac0b9 100644 --- a/spawner.lua +++ b/spawner.lua @@ -1,15 +1,19 @@ -- 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 -- - Hut -- - NOTE: All other types are unsupported as-of now -- - If it's from any of the above types, the spawner will proceed to scan the --- the building and find out: +-- the building and find out: -- - Number and positions of beds -- - Number and positions of benches -- - Number and positions of chests @@ -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) +-- 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 = { + building_type = "", + building_plot_info = {}, + building_entrance = {}, + building_usable_nodes = {}, + building_npcs = {}, + building_npc_stats = {} + } + + -- 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 - end - return result + -- 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 + + return result end --- Creates an array of {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 +--------------------------------------------------------------------------------------- +-- 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 --- 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 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 - local result = { - bed_type = {}, - sittable_type = {}, - furnace_type = {}, - storage_type = {}, - openable_type = {} - } - 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) - - --minetest.log("Found nodes inside area: "..dump(result)) - return result + 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,400 +324,314 @@ 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 - npc.places.add_shared(self, npc.places.PLACE_TYPE.OTHER.HOME_PLOTMARKER, - npc.places.PLACE_TYPE.OTHER.HOME_PLOTMARKER, pos) + -- 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 - 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_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 entrance door and related locations + if entrance ~= nil and entrance.node_pos ~= nil then + 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_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 - -- Assign a specific sittable node to a NPC. - 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)) - end + -- Assign beds + if #node_data.bed_type > 0 then + -- Assign a specific sittable node to a NPC. + 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)) + end - -- 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_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)) - 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 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_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)) + 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_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)) - 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 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_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)) + 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_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)) - 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 + -- 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_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)) + 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)) + npc.log("DEBUG", "Places for NPC "..self.npc_name..": "..dump(self.places_map)) -- Make NPC go into their house - 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 + -- 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 - end - + + return node_data end -- This function takes care of calculating how many NPCs will be spawn -function spawner.calculate_npc_spawning(pos) - -- 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 mg_villages:plotmarker at position: "..minetest.pos_to_string(pos)) - return - end - -- Check number of beds - local beds_count = #node_data.bed_type--#spawner.filter_first_floor_nodes(node_data.bed_type, pos) - - npc.log("DEBUG", "Found "..dump(beds_count).." beds in the building at "..minetest.pos_to_string(pos)) - local npc_count = 0 - -- If number of beds is zero or beds/2 is less than one, spawn - -- a single NPC. - if beds_count == 0 or (beds_count > 0 and beds_count / 2 < 1) then - -- Spawn a single NPC - npc_count = 1 - else - -- Spawn (beds_count/2) NPCs - npc_count = ((beds_count / 2) - ((beds_count / 2) % 1)) - end - npc.log("INFO", "Will spawn "..dump(npc_count).." NPCs at "..minetest.pos_to_string(pos)) - -- Store amount of NPCs to spawn - meta:set_int("npc_count", npc_count) - -- Store amount of NPCs spawned - meta:set_int("spawned_npc_count", 0) - -- Start timer - local timer = minetest.get_node_timer(pos) - local delay = math.random(npc.spawner.spawn_delay) - timer:start(delay) +-- 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 + 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 spawner at position: "..minetest.pos_to_string(pos)) + return + end + -- Check number of beds + local beds_count = #node_data.bed_type--#spawner.filter_first_floor_nodes(node_data.bed_type, pos) + + npc.log("DEBUG", "Found "..dump(beds_count).." beds in the building at "..minetest.pos_to_string(pos)) + local npc_count = 0 + -- If number of beds is zero or beds/2 is less than one, spawn + -- a single NPC. + if beds_count == 0 or (beds_count > 0 and beds_count / 2 < 1) then + -- Spawn a single NPC + npc_count = 1 + else + -- Spawn (beds_count/2) NPCs + npc_count = ((beds_count / 2) - ((beds_count / 2) % 1)) + end + npc.log("INFO", "Will spawn "..dump(npc_count).." NPCs at "..minetest.pos_to_string(pos)) + -- Store amount of NPCs to spawn + meta:set_int("npc_count", npc_count) + -- Store amount of NPCs spawned + meta:set_int("spawned_npc_count", 0) + -- Start timer + local timer = minetest.get_node_timer(pos) + local delay = math.random(npc.spawner.spawn_delay) + 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 --------------------------------------------------------------------------------------- - --- 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("--------------------------------------------") - -- Get area of the building - local x_size = building_data.bsizex - local y_size = building_data.ysize - local z_size = building_data.bsizez - local brotate = building_data.brotate - local start_pos = {x=pos.x, y=pos.y, z=pos.z} - local x_sign, z_sign = 1, 1 - - -- Check plot direction - -- NOTE: Below values may be wrong, very wrong! - -- 0 - facing West, -X - -- 1 - facing North, +Z - -- 2 - facing East, +X - -- 3 - facing South -Z - if brotate == 0 then - x_sign, z_sign = 1, -1 - elseif brotate == 1 then - x_sign, z_sign = -1, -1 - local temp = z_size - z_size = x_size - x_size = temp - elseif brotate == 2 then - x_sign, z_sign = -1, 1 - elseif brotate == 3 then - x_sign, z_sign = 1, 1 - end - - ------------------------ - -- For debug: - ------------------------ - -- Red is x marker - --minetest.set_node({x=pos.x + (x_sign * x_size),y=pos.y,z=pos.z}, {name = "wool:red"}) - --minetest.get_meta({x=pos.x + (x_sign * x_size),y=pos.y,z=pos.z}):set_string("infotext", minetest.get_meta(pos):get_string("infotext")..", Axis: x, Sign: "..dump(x_sign)) - -- Blue is z marker - --minetest.set_node({x=pos.x,y=pos.y,z=pos.z + (z_sign * z_size)}, {name = "wool:blue"}) - --minetest.get_meta({x=pos.x,y=pos.y,z=pos.z + (z_sign * z_size)}):set_string("infotext", minetest.get_meta(pos):get_string("infotext")..", Axis: z, Sign: "..dump(z_sign)) - - npc.log("DEBUG", "Start pos: "..minetest.pos_to_string(start_pos)) - npc.log("DEBUG", "Plot: "..dump(minetest.get_meta(start_pos):get_string("infotext"))) - - npc.log("DEBUG", "Brotate: "..dump(brotate)) - npc.log("DEBUG", "X_sign: "..dump(x_sign)) - npc.log("DEBUG", "X_adj: "..dump(x_sign*x_size)) - npc.log("DEBUG", "Z_sign: "..dump(z_sign)) - npc.log("DEBUG", "Z_adj: "..dump(z_sign*z_size)) - - local end_pos = {x=pos.x + (x_sign * x_size), y=pos.y + y_size, z=pos.z + (z_sign * z_size)} - - -- For debug: - --minetest.set_node(start_pos, {name="default:mese_block"}) - --minetest.set_node(end_pos, {name="default:mese_block"}) - --minetest.get_meta(end_pos):set_string("infotext", minetest.get_meta(start_pos):get_string("infotext")) - - npc.log("DEBUG", "Calculated end pos: "..minetest.pos_to_string(end_pos)) - - return spawner.scan_area(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) - -- 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 == "")) - 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] - local building_type = building_data.typ - -- Check if the building is of the support types - for _,value in pairs(npc.spawner.mg_villages_supported_building_types) do - - 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 - 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 - local plot_info = mg_villages.all_villages[village_id].to_add_data.bpos[plot_nr] - plot_info["ysize"] = building_data.ysize - -- minetest.log("Plot info at replacement time: "..dump(plot_info)) - meta:set_string("plot_info", minetest.serialize(plot_info)) - -- Scan building for nodes - local nodedata = spawner.scan_mg_villages_building(pos, plot_info) - -- Find building entrance - local doors = nodedata.openable_type - --minetest.log("Found "..dump(#doors).." openable nodes") - local entrance = npc.places.find_entrance_from_openable_nodes(doors, pos) - if entrance then - npc.log("INFO", "Found building entrance at: "..minetest.pos_to_string(entrance.node_pos)) - else - npc.log("ERROR", "Unable to find building entrance!") - end - -- Store building entrance - meta:set_string("entrance", minetest.serialize(entrance)) - -- Store nodedata into the spawner's metadata - meta:set_string("node_data", minetest.serialize(nodedata)) - -- Initialize NPCs - local npcs = {} - meta:set_string("npcs", minetest.serialize(npcs)) - -- 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 - } - 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 - --- Only register the node, the ABM and the LBM if mg_villages mod --- is present 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) + --minetest.log("--------------------------------------------") + --minetest.log("Building data: "..dump(building_data)) + --minetest.log("--------------------------------------------") + -- Get area of the building + local x_size = building_data.bsizex + local y_size = building_data.ysize + local z_size = building_data.bsizez + local brotate = building_data.brotate + local start_pos = {x=pos.x, y=pos.y, z=pos.z} + local x_sign, z_sign = 1, 1 + + -- Check plot direction + -- NOTE: Below values may be wrong, very wrong! + -- 0 - facing West, -X + -- 1 - facing North, +Z + -- 2 - facing East, +X + -- 3 - facing South -Z + if brotate == 0 then + x_sign, z_sign = 1, -1 + elseif brotate == 1 then + x_sign, z_sign = -1, -1 + local temp = z_size + z_size = x_size + x_size = temp + elseif brotate == 2 then + x_sign, z_sign = -1, 1 + elseif brotate == 3 then + x_sign, z_sign = 1, 1 + end + + ------------------------ + -- For debug: + ------------------------ + -- Red is x marker + --minetest.set_node({x=pos.x + (x_sign * x_size),y=pos.y,z=pos.z}, {name = "wool:red"}) + --minetest.get_meta({x=pos.x + (x_sign * x_size),y=pos.y,z=pos.z}):set_string("infotext", minetest.get_meta(pos):get_string("infotext")..", Axis: x, Sign: "..dump(x_sign)) + -- Blue is z marker + --minetest.set_node({x=pos.x,y=pos.y,z=pos.z + (z_sign * z_size)}, {name = "wool:blue"}) + --minetest.get_meta({x=pos.x,y=pos.y,z=pos.z + (z_sign * z_size)}):set_string("infotext", minetest.get_meta(pos):get_string("infotext")..", Axis: z, Sign: "..dump(z_sign)) + + npc.log("DEBUG", "Start pos: "..minetest.pos_to_string(start_pos)) + npc.log("DEBUG", "Plot: "..dump(minetest.get_meta(start_pos):get_string("infotext"))) + + npc.log("DEBUG", "Brotate: "..dump(brotate)) + npc.log("DEBUG", "X_sign: "..dump(x_sign)) + npc.log("DEBUG", "X_adj: "..dump(x_sign*x_size)) + npc.log("DEBUG", "Z_sign: "..dump(z_sign)) + npc.log("DEBUG", "Z_adj: "..dump(z_sign*z_size)) + + local end_pos = {x=pos.x + (x_sign * x_size), y=pos.y + y_size, z=pos.z + (z_sign * z_size)} + + -- For debug: + --minetest.set_node(start_pos, {name="default:mese_block"}) + --minetest.set_node(end_pos, {name="default:mese_block"}) + --minetest.get_meta(end_pos):set_string("infotext", minetest.get_meta(start_pos):get_string("infotext")) + + npc.log("DEBUG", "Calculated end pos: "..minetest.pos_to_string(end_pos)) + + return npc.places.scan_area_for_usable_nodes(start_pos, end_pos) + end + + -- 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_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] + local building_type = building_data.typ + -- Check if the building is of the support types + for _,value in pairs(npc.spawner.mg_villages_supported_building_types) do + + if building_type == value then + + npc.log("INFO", "Replacing mg_villages:plotmarker at "..minetest.pos_to_string(pos)) + -- 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 + local plot_info = mg_villages.all_villages[village_id].to_add_data.bpos[plot_nr] + plot_info["ysize"] = building_data.ysize + -- minetest.log("Plot info at replacement time: "..dump(plot_info)) + meta:set_string("plot_info", minetest.serialize(plot_info)) + -- Scan building for nodes + local nodedata = spawner.scan_mg_villages_building(pos, plot_info) + -- Find building entrance + local doors = nodedata.openable_type + --minetest.log("Found "..dump(#doors).." openable nodes") + local entrance = npc.places.find_entrance_from_openable_nodes(doors, pos) + if entrance then + npc.log("INFO", "Found building entrance at: "..minetest.pos_to_string(entrance.node_pos)) + else + npc.log("ERROR", "Unable to find building entrance!") + end + -- Store building entrance + meta:set_string("entrance", minetest.serialize(entrance)) + -- Store nodedata into the spawner's metadata + meta:set_string("node_data", minetest.serialize(nodedata)) + -- Initialize NPCs + local npcs = {} + meta:set_string("npcs", minetest.serialize(npcs)) + -- 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 + } + meta:set_string("npc_stats", minetest.serialize(npc_stats)) + -- Set replaced + meta:set_string("replaced", "true") + -- Calculate how many NPCs will spawn + npc.spawner.calculate_npc_spawning_on_plotmarker(pos) + -- Stop searching for building type + break + end + end + end + -- Node registration -- This node is currently a slightly modified mg_villages:plotmarker -- TODO: Change formspec to a more detailed one. @@ -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 = {} @@ -754,7 +760,7 @@ minetest.register_chatcommand("restore_plotmarkers", { local radius = tonumber(param) local start_pos = {x=pos.x - radius, y=pos.y - 5, z=pos.z - radius} local end_pos = {x=pos.x + radius, y=pos.y + 5, z=pos.z + radius} - local nodes = minetest.find_nodes_in_area_under_air(start_pos, end_pos, + local nodes = minetest.find_nodes_in_area_under_air(start_pos, end_pos, {"mg_villages:plotmarker"}) -- Check if we have nodes to replace minetest.chat_send_player(name, "Found "..dump(#nodes).." nodes to replace...") @@ -780,4 +786,4 @@ minetest.register_chatcommand("restore_plotmarkers", { end minetest.chat_send_player(name, "Finished replacement of "..dump(#nodes).." auto-spawners successfully") end -}) \ No newline at end of file +}) diff --git a/spawner_marker.lua b/spawner_marker.lua new file mode 100644 index 0000000..533007d --- /dev/null +++ b/spawner_marker.lua @@ -0,0 +1,3 @@ +-- Spawner markers +-- Specialized functionality to allow players do NPC spawning +-- on their own custom buildings. diff --git a/textures/npc_female10.png b/textures/npc_female10.png new file mode 100644 index 0000000000000000000000000000000000000000..66316972f21c677b0652bb31ca6284e756e900e4 GIT binary patch literal 1915 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 literal 0 HcmV?d00001 diff --git a/textures/npc_female2.png b/textures/npc_female2.png new file mode 100644 index 0000000000000000000000000000000000000000..5f8060bfabd7e55a8ca3190e108847399ca23152 GIT binary patch literal 1587 zcmV-32F&@1P)E--okznzu6h>)b0e7PLTSgn zQp>m8ncIPe2yh3_`jlE8uRc4)`%X;##M>`FzNQ84wI51TqoV`#l0Xdp|AgiUxC7C! z1^@tzT$|jueW0c{VJa`^4s(i1xgHXHfe|5*r2pD0-%4=L8l7&-iWE{arVC%U^*(6l%zl`6FR zFY|dk3s{g07AX!w8+4j$kdQUpFW{CnhL%Ai#|?T`K01KECTD-UO_I&_=k<(4vZd}uF22;SV7a+qZdVkTX^Mz*B#HNfBh}ckz-re)qb3jpBWxno`O2EC(xnSwr>fv zKl|Z}H2~=7?T1-eKx*xf<9T#&q$#32C&BFWnBC4EZpPMsf^f16s+NRVS@1loUTpqs z(cH^W6WFnHi{t)_<7Zl8mj$nC$(GpjybceodK1rV{RjYX-}>Dx`IB}*mIcisR6KF8 z#*B%nWo$W!N?FHOV=p--j6U)xkmv-;*8u?27k_a)AADyuZMcPV1y%El?~Xgizc<3$ zG9fKbq#eGAKB`(06PYQ;_>GS}$lGd9AG$`~4%CDQ3!dTTLNRE(1JV_+N>)C-0qOiX z06<~+CUir-KiLZtU>}0o=ov@-SGA-g_#3l-2F1i-&RwlL#-jnuxM}MCvl5-$F*mAb?RKmK zFbvb~kh}qwC#?4e$vZ{$+dp`pEO;3a&59!@fTc6b09B2_2;hK`5Cr`Oz3QQQh7!X_ zP-=NpzwhLJx%YlJ*%g=t=G@hW`~!S4K->l%2!?%C*jnj`dK34gKH+_0cF5p4A`4rM zkq}OH0iYyw^!8)r!+Sk;R4oZ}?rKBX;2|KK?1EWYz+#m^QB?V?@^#1lx-?(JrFklz z@y%9ebN~S1`?z}6ChT&dh|2|AY!grA`8NcmmPcuPfv;le$pZjrRT3){3obi7hAYW1 zzX0IxwQq3J>LDUiRaHwOqMBGTB?MTj!sc9>FJkU4>!av=5rbC1*NW?)sw(o84M?jw z#5V(_aT+{x6on#IS!P)YES&D(f5k{HY}^l}N>JIjA6kMc^Zd{*Xo(p14xV$kcST#z zNm{HD07nA}PylQ%$syYQX@$_c)-zi2hXr9 zu0T;#EQ>4LW~HoiB4l7=8-e1tPr{$?lb69nr+o#$&v!G@ikI*I@Q002ovPDHLkV1k8{;@bcK literal 0 HcmV?d00001 diff --git a/textures/npc_female3.png b/textures/npc_female3.png new file mode 100644 index 0000000000000000000000000000000000000000..321a1f4a79727f6bfbe94e4e917e13b50c4b892b GIT binary patch literal 1522 zcmVfZPr*NQBRZ_VU zDoBYcdM1MZ0F*0-3JJ+-q(CDjBBfN~l1r*0O4X<&P&GiB)U`pS-o&xJlFi28H3xep zJG-;%b$+O{pJaJ<=FQvpzIpTB%(_qm|6Uv_*OIYA!|0rAH}m%!4Ne8MKs)y=<&t}D z%;SI~0<=Iou%(*%q+Ajo#8Ry9;*USG_UO@<_Q=Hjg;bZYDnn9b;h5g}Ckb^C@HE14 z4gdg+ME1*$$A+32npOe3B=|jE6V5$`8&6Lacv`Z)5iWh5ry))g9SiwJVo88TB3xpu z^=yRG2zj`&Qa&Jm1^_5M_c2=L&+kceF#<%+n}{$#Qe{ihte%Z%7@c!HipR?UP$(4O z^?IRi7YYRcfYY6RRN#}M%Q(R?rfpU;bY zzJR8cDO;!YGUWNpuIOgR7k-MeKcB$#r5DP$W7{%q_+ zTmKMx4YEF;mpP`92%D>)ZzO@eM@NDkxQNurdxm4e#iMW<0UQC`%G1+d*AQ+^PvRDU zch0;I0JuCo3Fw}kfFmWBWQ5f-e?KY;I|zxAm88nJJUxl%_!TS<46y#?fdNFvuP`F0 zEY%}2ezy=gzZZl^fZ1q$c)oSwGyve28{0=++CKeakymDOR5t2IBdPoh*vaj4H#b(> zeMF)4pA+^=i?O<1#vP`rmn+Y^U)5a2;?&u)6S6;zjDH{m!ApXK=eafgU+8hWvidsmnc=D=aumb+_KsiF;p%TdnDo-QtKK z0f6fBn-pxO#V0yrIErBWdIUhY`V`brJt#58Rj*YB_1nY=Oc6j81VcOj{ztE?l2k(T z%4(&p%0fr=j?fDd-2x=9faDbB3TgxmLY^;`WOkg<~>c?E6bqev-5q?97- zBY6eMCO7v>DMco0+}a{E02QWN{T}9gqD0VblLL&-r-IdrA!pG~4gmmi?S2FStgWZf zQzhok#C80d$s*ZQmCt0+soQ=uN~qxppC1PSw2hBqGcBWSe3UH=ZI8V$%-NI69|i!B zv6Gej^Ty}4_H$)Wp9?nD)0R;{Ln~1t+s*?>1>M-Vbk&sX?lm_90QyD-x19%&Y;v<` zl2jRYCayE*Ri7UnZq<~GTf!lPoO*_Im6 z5)MHl5n&E6qHko7*^?*{Y^G&wrey>}5p1ld*)t`=3c*kWl2oesK8fTNmRM-=ppLG7 z^0nneyCfse-UPs}*2bl)mbxf0eItWdm|5Zo4u&FJLZ)r2Dy8ptlE@okhx;q7u!Y3m zd?NWY%zqhtUN9w5BIvFfqiJrmghTkpLOpdZO#lD@07*qoM6N<$f@qx9Z~y=R literal 0 HcmV?d00001 diff --git a/textures/npc_female4.png b/textures/npc_female4.png new file mode 100644 index 0000000000000000000000000000000000000000..7dd3e7f2e7ab436ba777300ec205cd9eda0bc5d1 GIT binary patch literal 1422 zcmV;91#$X`P)f=KGc6up$~>sh}sfL4{6C+nk<)`ciFh{ zVQ=PkZ#R2;`O|8@aBws8o89?-=QrP(*%~Tf_E5)MrI;W@dQR6YrvGvy5;oA%n_Xqa zNH6(dZU#aLVCl_J$b0G(BhAbG0Kn1`%QcRhhKdd?c7Y%h{E0_>`>x~ct+NZo za2aEzayi0e$b4s!rN}O6f_~(s>lgA^TEg=y?;EyU4T270i#?=)V+qVRYnGOf8StyN z4!ypf#s;0bx)|B%9PtZBq#!~tXW93e#WO@0I44HK( z2M?CT*44#IgDVq3K(u+@6H)|MMmpkayZUREGy~*I9J53r5BGF%Zg7Bez~(J40&rq* z0378oaEW51J8LCIC-0kaKP=#=T>gBbn*A%oLehFMOCi)cvf8%$)+@vP zRlUq*T*b>(?ba_U&f?6G=jSTL%?ux(1E_85t|}`$!B?KDp92_ZUJJly-(7OG|MB~U zz!!Yn_4wQ`R}J5`hwso)yWa>L(2gyeLJ{4%YZpiQ`aJE`3gFoV1o8hmRH zWvCjwuzi!O{CWRZ%k`>7!E+r})*rYD!>Artu8da>3W1sfgCiajjMl15Werxwb^bo> zUbFwKeV3JSU6vm?^R}zKZGD5i&zBjeGTe`)H#7YGq<_56Hf(7Lxc_P2f%m+*k3$DO z@NIMYLnTj;8DJK2fK7f;DBc^&VQC4^XEh+0{0%?^O5_2ivbKXeXW5CBab0GIAH>mD z8SaN<%C1Z$eZMP${{hT(F>C3>$mP&uF$~j0SH%SFLuu&*z%WhOS65X&O^u+$VwJet z@9-m72|@%v7#jv$gs$aE;`$wul#QQw>xyy4Ge-ez^0^$ks$%4FF2Z1w7QzUz!ov5P zpb()jV|v=Avc|mR0d^!?<$-5gS1a$o)?qjJ%)W%k6bG_Td?YkRC9VH82Y~7x_wtXclrM@*w{!`3%=5vNScuxjvCCrG6?| zA`hhFD`cAbk-rg$2*QGFkij`I-%c=g>^E8>@Yb`(08n)e)5v3KeO#K_1-K7R*S@}t zUObQ+Ka8VK?{1b_Epd(|c*2-pj$dUb{iJzZJZ`5LQ(na{1d=s_Sv`C0FsT+v;6qA>ygHO6Ol$K!_3y cN&E)mU$e4qA>6X8Qvd(}07*qoM6N<$f+CfxPXGV_ literal 0 HcmV?d00001 diff --git a/textures/npc_female5.png b/textures/npc_female5.png new file mode 100644 index 0000000000000000000000000000000000000000..992d3fca81bde7f8ceec135e8511072fb3dd2208 GIT binary patch literal 1932 zcmV;72Xpv|P)iDFN`5!gbwsQ|jGBR|YT)12dJPNBRKNR5)#=5zfALDQ=JK-Mv;it}D7S zfG#CH(nn7lVMo=)lafaIjpW}?5bw1D?=`ye08gpV?W^IL*Ob6hwRKhO-9lFeh+^G~ zlVB|o|BpU?kVhXs*fj6wFaCNhbX-?-eG9m*%l}8-PeNuqHKzHnMMb zAf+T_7%T+}<+xzzq*E!}ii1)LE1f2t5>%H$Ql3(Ho`>)INYg+{fm?RM6%8FJM7Rzh zO{1;h7E3l~FI>{zo}3H-DwPVBWzpEKR4M>imWBVd-4tz?p_krB^tY0~d)Kb6jH3et zK|rNi1waUa-lHeT5BB#mJ$sWUpZEfOxf~le^pVL}l*<*qas2yW8X-}~4T(w#h7PWW z@_nQbq#Ha#ng&(JiIw=k_U&zzJ@VOo0f1VnAyfB)1w3^q9zXs23oYmV^YZ1O8zbmI z?5;uxG(*R$x^dq}K6D44e&|8AZW(HtoXLdNSB{G`4TKQ*zK>E05eR$;4N@np89LNG zL)F(`4NK)JzVBnD(=63$7(G3>wHjys_-CzCnSNUy5J59^0ylJR44q!f;;T=531iPN z0Pe+K0yKRC1g?jsE$`_!5R5`VN{OuBBr8Lr9U@(pT$eNFFRuB!V!049bd(dGybuB@ z1l3z*j6K8nzQ=(R&%~k!PCSF}d$A!RMS~zfh!(X~ovc!dAP5M$BoAQ@kt%3Oel3!q zfY9|DPbBgLr4$S2e+EGI&8zWv)LvQd`@Wwz`NT#-Af-gtbu>dKoz2j@A=e}~vj2f5 zx0Ys4M-oTAviHA!v38832tPXgotFJi9eJ*+O@t6=nud}ZQUxffA-vF7-JIgnpZjF+ z{O``M%d@&^(YGGT1pqJIxfg(+zC4*2zcPNcqarUn@~PnB^v#xIdxrYx8UA`lg`WK4 z!H(-k`W2K3zyl{=NQ@iz58sxdnkvDF`2Q**;@hONiez`_x)i=$tb7}+<-5S$@o@^* zUR@I-yf2@_o3kd{q!W9?JAoF(R3nPEi}lUVdvNlv3CsWM?$0E~|K|Dp zNcC2kbT*TCGYBEDa=m=nzY_0PP-av6m5m=u zthayRPl@q6ertEk39&Yf*4&&k@O{~7URzB4dzOr4F)?*6?wgo8N5-;v`$qGlXt7d( z9!b@4kfss$r!r}(juW4BEAs6;V72i^h-gA93=HO*`c_+b!FEWeG{(lpnV34q#MC*) z#>PpfG#2a@G3$EEg;7lbVe#YEYVm`raoTt1KZjwKon-1R(j!O_MM`OzD<#28~vEMf7tV*gI!!$wwIGN>&ELvcG zZl>)9t8L;eEuoZ(zco$cR=L);N{om`=Qnk= zKr1Z2E*0N8{we@Vt{W3=r7(nlBf!wntt^pVX4$5oOp3M}dqB~46UJDHNVe-7;AC>K z5sG#gcQ2xVkGls{5`QyVq&2b?{~%?TK*e>>+zTv z2f}1*9(x$GY_s*`2u0haXuENrS+=>Jm5HCC?Z&bzL%S7p2ra!iXT;J$4}e`!04!CY;2q}_7F$q72a4VaXnj?UnsFXlY_dR$_c$BQoNAK zu>sh6a)kN08MdAri4P`fAG_<3gg!UR_W)oXdpOKvpZH-%{S((mzb+8CFDm(KMcSrwR4#~a6KzyZwg$Gqw-2j-pS_$>H2|!odCcO^JjYwcFiNjs4vua{a}7?hjjoZ6vD6MDpC=uEh5T zTTCe^lO1(sd|xs!n5W(Uks_ks&0R}`R&)@3nfNmOEv(M}A`+IwS&xDu*Tl?;^=9<$E$A{kx-Ol=jhT7>w zP18_?0H7u%x~?ynP#-2bQh=suL?aQx!UVvO_OjVZIR5uH-S*ad?wT(d7Ze>SKox?p zFi~Tg-BHo034xcsm;C<|l#g0~j~X3$Kut>9?JKs;4XIF*#)1m=VWA@hII&)Hlc3>? z|1H}#vSr)Grgbm>=8y9sV_W>`N2ja}Ki>R-Z<>y80bSSmU(EZrLmnXHz31bx7)m6} z^wbT65X8C@07xm(4FhSKpx1mGbS>_od}A8ZGy{K6Pu&O9+oU#L?RKV zR4ViRJ;gXi7iBJ?`9?uX_%~nhuA6vOHP}rRh^;rON#y~{E0Q})x zWBlk}hFh-v_oYi#CrVHO*IgAwA*89)D!SYExlb?SfiK_3isijcixUZ3`?9Jdq8f^# zV45aUN)$`M1Y03>!Xb}m$ol?sVWw2UG)>~M7&C@}77n8u2FL#Pk5IcZ{gynyK}b`v zblbJjRJ!7EzPbJDXzTg`&@Y?@X!;Fc=`}*3#+jyvpp!d<5QzFsGCL%K;K(wg>l{0O zVcy>rjY3FMkyYFBilQJCh04tm+PZ#Bv&M#f2i&O}_8q`9Ypx<3K`qNdQCieib+S@Q zEX%^`kUW5hY6xkyBtIWXPyyTZy%upiK}t#S{0RWM-@EFLJMG!^zG<31%li&e6oe3{ zs!B*xiFGIFTHMn#Z|nVcH@UTlhTD?ZIdIPhezCTVBL}}a`h%AJ-`@FbM~5hiLMRkM ziV#9tND)G**~;p~icdZAMeBug=NIHz-L&az&!4jZhOZo0ax?#PybA>v|Uv?*Cp}fu4M9W83{-d-+uhAl;kc`r!w;`_PNNaqaW{x1^}1N^tnf zkw$y?@U86ip&;Fx0U$q-<4#eXWA@zWxD>us%zhip=er<3G7hQsd7)D@rR=`Xe3wTz z{d`_FUue7|qqn_B$cZTz1%vdOWr)&2OjGdZzfSpv`}zjmc>s~oHkmr1$$w>`U~u*PB|8*az~SQM6a)oq!mTz-XHv~8>Xr|R!Qzc7 zu}Fxaq05YpondtB3`0YgiA6#bZ?s5SdaY3y84|FIAKftQw}pecB9cp%Fj*{;ymKj& z#bPkCRG9okE_l2S+XL1t^B+V2c?X5yuR)JL@l$S=47cP>XHsr;I9)0=%^y5)(tbZw zbu6n9jge>7u3BaKQi7R4_r$M z9{$o^3gsE!^xeBZ3&5#Y_BY6buWjiy2Ko;*&Y!T2GMhv?lOml-we`@+LKa1l?Y+1T z#d`&MV<>;`Cky zb{uYmW5FPqTIx&4PCMw9^7eO<$kn62-7)7RFJ8I$6W{NT$C^KCI0~ICWHC)aI+G&z zPMb{&23s~AY=l;wJiwyF{qD3y3IG3ASow+mSv|jKb1?iDxmJuHxW*9}00000NkvXX Hu0mjf3ZdWb literal 0 HcmV?d00001 diff --git a/textures/npc_female7.png b/textures/npc_female7.png new file mode 100644 index 0000000000000000000000000000000000000000..51eb352c6c7e7bf33a2c197aa09681f068c068b8 GIT binary patch literal 1907 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 UkJ>IcasU7T07*qoM6N<$f^u-&1ONa4 literal 0 HcmV?d00001 diff --git a/textures/npc_male10.png b/textures/npc_male10.png new file mode 100644 index 0000000000000000000000000000000000000000..c3e25480c3c795f0994001bf200db2467edccbb6 GIT binary patch literal 1734 zcmV;%208hOP)KScM-{Pxe-uQMVERw1rbfIpXSls%_jdN) z%e-rBP7Xhbq`?MWdLRu7g=3i-T~A8KQP7- zs!Z7`^4Ux%6bg#GU*Ev5ZvX>_>~)OgfNaDoQ?}}pL>*@u>=M|eqzyPudb#XHZ0i6? zG_M9!rffXV+j4H9u&0+lFu@YguX;uZzl~WeB199j(}SRc5KRC!H^oldyW`l_0W#g? zHJb_jejmTR$>M_tY5D7PBA=@35_e;Y#p2zyeg`ntE&wPeWc#dAfsfetP`_2DR&m)3 zBI0gLr`z3eWA6qE#}6;`0eVK_3#VK{gdG5k-sB{t9;4T#XP7wU5`faFj{#hH_dmN@ zDAW>=WJTCX5$_qMGByu#TqmyU6k#VtUe1w@*=?A4aceciP@nJS)vq*RD zZliGg@Iqf2^V>_$17O=$O2~cWtC$x|8pkdJP`&bXx=(iTuPrH=Ac$1^^QT`LB3X8v zR5t9D8laYd>$zJB`PY_c92?v>j$OvTmX@(?OF5x3Wz*@#h_IuG=8@kEFyuM^=jWfs zwymM)>$tS^7MGUZO7-cHhr|)DU3gXzmc=ex^o(A2Z!Cev)t~l92}puj#{@w%(peH; zICXYt7l1)g{Odn&?dlR3diW2Y{$~H$^k*@OCY#MVwYM)4gdwdcMuZ{1``bGs4gT^o z7yFgj1p>bTfKfD68=&J(jWnp_xB1n%(_22{_~C{AT+PE5Nnn0*{<{Q0fNfiJyDZJM z>n+6EDupSJFzjHOlOMfvt5MM2VD|ox5_o*r^w2iBLsPVw^tLNr*|(bG76fgscNuNC$aNnK!S$H!`D) zj=zrlWl3iHWWRph3Xh$7VW=$)*OAvJ$+tw3MZSm>S~5+liEA&E$1^#Z3CJYPh3PFd;6W{v)wt;X_T%5=^W)6+bQQDZuhU!Fg! z2BEN}0U7;^=g!a)VQ$1^WF=YGu~Y`= zj*&8YCgep~FM#7@LE+@b5B4o9F)WIb36^Ey?JLpR9Gu&tD54q0OqYvfYSL8GUp&b+VF}w@<#omDSFQZlU^C$vPE+NWhT&m$_0 zlvgP$qv(rh=sb}{-rJ~->d|kEoNodu(ovKU{P&&Y@2y$SVYMZOqHbB1n&UW5N?|jM zdB2e?p+CR*1OT5r`Qv2JFmW7*I39d5k3ZBA1NYRb>N<+B{Pr-;+vs*MC7~#aI5dAR zS8v|t#6yd?r7~|^zroQ5=DB+FHq#_a0N}*I{Qz8lZ}>$?uL)oL+~uJ@Atw8?q+W00 zx(>E&(T|fFq(?mnB22SLsaQmWgMQn#2*;=a=E6*sTWbvt&EHE9h8&rnp)F$CVv{5D zGX!C%oN#1*2D_Z3a9hMI%v5Q9>N^t=njEVHL4@xMyeS7eQ%JkrBq-7;zAxCnf0{~>;4Y`Y|q+7`7rs7QN~dMN5aq#}s) zRuyUMNzm${#V#I1TSUEx9;6~-kcuFJ3L;`@@nBnQ5vo|NG;ylzreuzti^YPA1QuIKnRB8ZNmKB>X7K<2$!6TDSI5)9%3+0&^#wI41 z`TY_A+xNbn>UEDB3AtZGL0G^rjAcPi7UWu`$Y2j}%PNh;I zd9$*BSy=#@g52!rTL8l_&~?3KUJ%XYG*C03W`Z%0%c-m6=GvjJ5)jcAh?YX3P+(zU zq3ya<>ZUG#picpGU1xA`5QvMRX&Sbh@`7Mel&5x%ESrzS7Qg-FpE0{AF%f0U zvchrnJ~xX9ax`j!c#^On`uwA}x4i~%9FOE6jzWvk z;UkC8FPsdo^$RCCeB@Az-Tnv1dEv1;uq-Pq1F^Rdrk~z%J&^`F*1UjuZknTq_J-F- z5ADS~HyyK4RV5sY#+M|YM5he7o=A(--?O8Dswy!eNgSU(&GG5eu{p=_V%MswL`odOo)6^7T&_yE^>D%O5n>jg8}&^8lnJIUEB_F8$iop}*31 za^j6=I^N^ScYcfo?9F?A!Eqc^RiR$@ncVf5;JP(rdF7AZZ6AdlYk{%b?!YaK zvUA%6mhFdGaHh|VU%B^6I8LY2y!x}r(1uBv`MK-5ZqbiIh#)Hp8CAi!dKGQs#y-7k zqOAZ^`7$$=f0xZf=g@ zY!0thBb)8rlafdsJb18c6YPa6u?VM))J33~ESnCMJfI9l)>G zVs=T0ecB0>MBg7pR|3CYLy|Lm^;>iMUd}|L2F)~=O9?>oWfe@XDH>1=fBS=I^ z$7;WBgxS#6haMtFvFZW{N9&%`k^%iVmb6JWL*K1--6z&B`naF`{g)?V6PZkoTCMeC zW-67U(P$nR8;0`yGjFo_KePk}xfUlg*I zl~9!6{Pa8z@K6dl3IiDyt4*go&% za-E05D?)LLt*SC8c zjpEa;tCxF#h_vSg1poXu_(#WBAxC-9CYdFQqJ(R#>1Sgb{xffe;~&1;3%~=rJ`CqJ z4CV3t{yv#I0NY=R&n>@&_WL&43$h^Oc^+Ge>p6Q_XXmyF@|i4Ooxj9=n~I#htfLW> z0Km@8qX3-$Lud5qk7FBdj*OrFY?z|1h@i0xB!e9Kehk> literal 0 HcmV?d00001 diff --git a/textures/npc_male12.png b/textures/npc_male12.png new file mode 100644 index 0000000000000000000000000000000000000000..19b4fc41aed299b817966957cf2cfaca7c013cab GIT binary patch literal 1667 zcmV-}27LL6P)-sAN;J$?VQ;C+oxPIXg3% zy#%AlAC@`io9|ry-{o9pQ}nCz{)I>Gkmd44+nJr7X1H8t`sdRC zTy@{`sa|8OCDeYD1o=*N0K>=tFkCJ(J3YM$Q~Q6A?nh}LZ%EI_BO#SaN%Cf80kg6I zv<cM}%e8P!$Ess-k*JV0&0r6@Yr(TJ8C+I(iyFgu8ggBca)BV%F-EFWSbUc@EDt z87vg=8$ODn{I|1T1@yHG0MZC?nXaWc&p4mT*$Nv53e;^Ezv1IG8mm6p{{pG_KA+8K zSdJ&lnyN%XK5hiB;YaG>wwB|O&1eyMe2inKep}OpA`JnF@I*x!Iv;>UI#`Y;e?X+I zAQ~p0G{#y&O*+(w35tn-`u0Z=$%x@JRguPwlT0GK9*iK@fbiZPC%pT?0{~3C zwl7i->)E!;-H*N$@pjmdVYwJkA07M?e(&5UVA-9kA1&P z@6fAzWc#@HlgP$XV=a)F0&$qf4|DY1Yh?ZCz1LtKKOE693?18cfxyFJag3A9X0*h+ z#9ARO&iRL4-GgE17>3?a`SrtJ^ZMbhBQil+z%XUXekUodjo61JtUs2JHX>*jq3$cLY*uA><`)j$8Fbm4`4&aRC_1B?}BnXdfJmZeiHgveWt z)3d{KpBzBfb?i&V<+lRMvRU#QEcyTbevoSsulqhZMPY7kjt!YCuIG`-^j?$VNbTLb zx9cL<3%k#piPUq7Lf!YdbjA7-H+Zy1VoDL zdiUL~5!LlNoX_X`k|p89bMMRYk1u|RrYOq@LO|MU5v{r|b>FAnDto>!jaGHt2zgPD z!|Py>PU!nO+%LkD`sk;i`>g~MtA+@L^~0oS=|+gQ+IcPGBpEUZ;YjI(RX2bbxazuW zeeXRf>lXgP2%?>kh5HKkA@qBNe?RE!lh{h2DGF8BMN<@}#>bf&9|zi@9)})*lOTf@aHvx^q-DA9FYwt4pVlaUVpnm1+n%g^ou;JO>%kg`pK z1$@8%Pv&oNOQbGtq5a-QdqEY1T-Rm$$OTMJ&9LjzG6gNeN8g=h=hhJ>r)DS-qyWIK zZ5sjj?w1*!*>Pp0LvOV0y~)RZ0N~}z+B)$F=(S~4Dpd-FEQX=e^n*1h>~-6AQPp7A zu$;EsFmxQN^I|CtFgi5I*|~YPk6eK5IE;-9QL}t%R-LhtA#BHyMi?6z!pH>Y`ICn{)4vGJ2nzVFGiC5YR>?cRRUmia+0~3nRRgO&Oq@4Dvf0s70MZ#B9I)axNz}H7z$$`OjP3#3PA-?th&2Ns ziDuP+!2t`$aaJ9xX`6ca1AQz3^{OgDxDAX#0U>G_%{F*Vgs1_qye!t+-*v~D0g!T+ z*R&EkoepkenaN!{sJja^1DD})nXna7C=_n5_v?V3b^#y`Lb}Z?YW%?3M)g{i%3zsg zFCc7%G+V88clLHb+js9o2cWG-v9L=;gx>@}Z}(3^&ZDf!R0N3URQmufeEFXZ zEfnz*kYt75Ob~DDM(k`BWWP#Wom2SD1bI0|24UzR7p>kZ@NGuywJOUG|9(7S^!Tr%0K9$Xg~YRJ=Xn879X_4hA#SNDts*tJ z8x37`;@#<^ygPlg%jo=t%N)FaU;KRV{(YRkaJkFw?9`EXe0nKlNj`6B-6?24y>EL5 zqHXQ$)DZwI%lyA4^Sl6nrw*T{*$TVXJ|vX}o1h5FbN<<>BUqN1Oa7H}pYqDNPZMpD zw1j1uynW_{n7qtws`Tcoje0cFdZ1r3wlz@BZp3`ssw<~zrMDi3g~+I zpN>4+`Nz=1=mmr2T9wMbrty5AdJrOfpZCA`cdo-vJ~!PN9G$>(7Xi=aq5{>-?8uBiOyKX!4*+rUY&X#&-1V>lU6Ha`+?u|70|9KbVD1YQJZ5+WgM?w z<>J*F{mciz(%iN9UN0C}EhM2T8tco6IiM5%Y6s0}j65#4!3ldS{kl zud&4FJwL*Cwz7ZsBn=V9D!9<r8i`}khhb&mhjW%%yxh}vxKxV-EG%r4Y%?C2AG ztm0spCPmAnHapAE*jSI=tswI(VvgTKt$LH~?JpDLh3~I9;lc+eaX&td=dIWZ;n5CT zw8Ql&dNTU}^YilrL5rZ-q*UsE_tG9a@mg*X%tF&$pjfT|P=E8qOq;j<{QJc76Ng@= zF8nxq61}vQ?TDBpJywZ+gOGI~4H+QEF7eT&Z*vu;H{Au~Uz%jPO}4Ao)$qijm%Hwz z<0`V+B>B3CROE^vChORx#B&xTc^SAo7&4P-Ld9|=lccoo1@C>eQeq^TG$aw}tXkmI zg6E0=$1cV5q*bH7XSKd87*ft@Vyev3$m>&yygqdzCa7**X?s@$vAiY5Do&!{sX@qt zYM-7n3%bm7mIAVnM_xi^?H7lS@yv54;4Z@YQnT)P0fteaSSTR; zm3GTA@q4HN#>B`l*X9@5Ier(O@3UuogoX%dh-LPSkKp-!9E3gNBUq(K!VM8JF)~c; z=fCTVQ2&_2^8#E~;0)MUsY2RnMM;rCab3aItwSs>x^Z6~{s&Aybk5g2KH>lX002ov JPDHLkV1o9nL+}6q literal 0 HcmV?d00001 diff --git a/textures/npc_male14.png b/textures/npc_male14.png new file mode 100644 index 0000000000000000000000000000000000000000..73c2b39d079fdd7bde44a3a65cb52bc886cce0aa GIT binary patch literal 1739 zcmV;+1~mDJP)>!m`i&p5OD^-}m?Dd1hu6w!q}wN16bYstrJ)&j`)G`um51 z@z@=QSe~C}cx;UM|6T#$r;oj&Z1o$v39T0;LB88HfMH|+7#EjL_M%ki zGlI-VD?w3|fV{aolD*)KZu&Yw9ItdF)rT*)W3){h0{o+IN; z@Q=0gXHEfN82S$~X4@_Rk34yrTHWjDeF#???14>C+~=P^a|*-I6Um>O{+M&qABSXu zw18pgy#3m<0eO-6Xa%T^`krV3W&P6Y%_IduFd7)!c6+Ln6Tg0`Yb5~uYi?mzE1=`Y z|9I+m&CmLOj+)ZgEUz&5@3YvB!-nf&IS%iC`kzFHAAaU+voKu5HrD}AQ(7{#bn11F@duvRRzN#;p+%)YwYSi}D$5JOSWRgdxd95pMb@rep|cjY35h%-6IiRuD1CVxr-r6&|JFNp8@8(z zhHpEJli$zryT+(mUQh+IJsN)gC;tz|s;cth=OzOO#<9vviR+TGAHv84lxEQGJW1Nn zDK9P2KQhvzcWuN5Kp(x0^2&9_8(&h9$XkxHrNOm-yo|2v*ms@`-W6DuO~b3x@OEI+ zJ3Q%EL1h6BPqMnY%3vmoTdR@DZ2$C99(w7G#7!^`HFJ%0ZV-UK-TY2?va$e$BISF2 z5fb;$z8}nW=z}jGnSiCaPQ7mHqA~%=j`H+Ji4~>R%r(R(N-`=Fa{0Rhg(6fIe8MdR z$ldhG}o4mOti^JJrA^l)(xeZww*1YiA+WbrE`O^B!zw=T7=#!`5iVT$P0o| z$izf2qH9D}Wn!{SK(-MV@CZ~vGD=J?^C-GzUnWy1GIi}wOl`dDUnh|-&9v7++wrBj zwp?Ttk}E)vFBHRTx6pI8Nw)6@AA08EXP+VW^1#Id$C*Uq?n)X957wC}qG z6_WP|30joAd>u!&rxs|Do!WS}?MNJXXwOBnC6{n?ZIV4mZnp@u>+k#_BuJ;T)M}kC zGZjUl*=!#e2L^I{vu?su_jWqo!gT%wD76iLjm^BFn^@ZhJt)kHi4< zXHw|8e}1~I3%Ka{EctzDHa6S$ZLaH5c07c>=lSDUv@$fc)aw6ht!&0QB?2Q~cOx$o z?p6WfJ|Q?sK1vRNuIm9=5s3Ug@dTF%ZhmxKryn<@vk_-eEnUzvXwGkyzzzeG+l@f6zY)96;v1R>XTIW&3;m#!{y{H`%_ z=?w2(zQWzNk8*01K<@92&g^+i^HLIz-j-s9KvG9Ua1U zoFE8CM~5&n{{4N`@+b}s@XK#c##`?E1NjHx^!PHi?P8i1`MxYhHk|c(-A{@LifLNx h-`~&rx*5zR;d{j)KSgb4PWAu*002ovPDHLkV1muhSb_im literal 0 HcmV?d00001 diff --git a/textures/npc_male15.png b/textures/npc_male15.png new file mode 100644 index 0000000000000000000000000000000000000000..d55800ec8973a2bbe3f7c9320676a4fd0126abbc GIT binary patch literal 1654 zcmV-+28sEJP)7%!8OMJU&y2^dC(hapX{`i;+GhM$z|g{LAd@i$YV5N>{}~{Wj+{^{ zVVY*gC5p16%OBWo4-n*~d)wMTO{K8yHB^5K93R_W17LI09<;p!M}H+Ch$nZ0T8T!Z zfwi&8#KHZnTPygU#Yi?wP!C9@QqS1@{~u~!vt;5BD+66qc**z_l~RTLaF$KSBd7=X z_4+e1d9Y9#?@c#=c$b%IJeKy!hA5-mF40a`VoZ7G#tUr}YOJQANhB#3vUUVJ%}ZuZ zKaPCPP&-Cks{6Iq-e|ixcm4g|5$|w*h{;|cnms@=I7v8n{e6H)p1cQe9FMX2Yt2RN zdWMWn+4G#C%EX^cod6(tdd7JefLyVdxl!-f{A!%Qv}n z`DRQOk$wup(8JPbSO4Nn0XrrGl-1eaG`dX?1e1<&9Ivah1ah~ob}ok_Wj=uQ2N5p&@?wcZ-p|lh|vIx~}UMeSvB@bi(4ihWce4&CpqU z^oZQPeSKQjM2iE)wkq5!zRFtV36on3)Fkq@>-JQ*^2Z;e>pIToz8ZZiux*E}pw3n> zxc37y?_=hnTCI{#XYhQVbh>v>N<#VB+u!Qi1bgA}Uw+jhD-_}Mx!=ZQGZ#)VehB8S z$LCH?g+;MAuQ560vpBCsa`}3FCA=2VH}j#anyAo1kw`W<<#T36@0wBle-P;t!ho3z zry|+Rg;P|^P+DleMi3QF5aea&RLf2B@|q+B`9hJ%Y2DD@>+m*EEjOcg<&GN33dQd0 zgyT99lh+H9$>>8obNa!+GeC~s%8iq(+&Bq@#+v!oJz#NOi^z$H%aNC1=KY?|6l*#? zFNuL?fNaPsr^RvkWX!lEni17v^MoTrt(*DQ8gHkwS2oEQcm~LIe0ye^;LbFQ^IEK? z+ZhbR#*>Xt=oT5!DGMg2{I;dh3F`0u@TZtS(=zye>&Hw*QD{hqu)QN$9LGa{bnvE~Md z`9Tm~w|lVM0|dcN2FpD|j0@xCdJlI2Ep`eN&XFPafPS2y>bf406^<}>g-SB%baeR9 zb)6huY|eTIrLq%5lk1cLBF&TAo-jUno=BqI+o+81eb9Az*aStSJuf8q?RdjObwQ0D0H{g}c- zQcpPl&G7g5==u0l``@OiRDMA=o53)28bR2D9ILBVG}aCO1%E*cH0O*DkpKVy07*qoM6N<$f~ILV AjsO4v literal 0 HcmV?d00001 diff --git a/textures/npc_male8.png b/textures/npc_male8.png new file mode 100644 index 0000000000000000000000000000000000000000..a1c0847b7520176a45727e4e129b5ecfad1e6837 GIT binary patch literal 1503 zcmV<51t9u~P)f?ZfA`Xbs13i>u1p*`vJ%R!fAcPPXAXSSf5Yehh)25|%Dm$^WFK^6Y zyPNTPy|yE#$^Wu5JG0+>|L?UkBhduqTVvIjI_&qC%xiu-rVjJx*H`@hz>X1eGc)uK z3^4QWRRH!L{y=KB##$oQbb>%W>Yb*k0Q3$Fkeiw5fNBp0ax0TOuZzUJwj@cYsscbM zKktugnu=kV9aD6*&@=)>h~e+GJ=W>eygLSOt&(-h3(#j&k>8uEEe;0{c#?{G+D45bkpRU-~NtN z_@z_ltDAcJF-iphVsY7T0}L(w6RFTt>@hB!exuna#+i>tv`_sJW6I~I%a-lY=R ztwS(#De}80?|YcCr+q`nib6tDDBiwJX4|$FwOdB00Y+0fX7c|rwDb@20(sqRe!+3? z+xJlv1!LC>{#}8t8!R~$mYmL=52oyC0OscA=u(qdw(Vccv;*nHi4&1Uuo>)=;RnjG z8>6-T?N>tQi6QMl^sv|tj(bK3$`i-W_MVQ6sJ4#7?LZ_vw*5*dQ4v%2bSNQjEDX(z z*9umA9Bu^BbK{e)_@UwE(Br}1Ulh4^(xBr85IuPJP@!oQh~$D{--tvMxuBt%LH_Zp z3pFnii6pjNf0-#s64mPR4`XXK#hpTleJ5WIRbtAX=Be+Va1}Y&dISW?wOi=gjW#)> z#!(da|7lqkV4E98)STdc0&4r-Va-GEfOfXw$}Xhm_wMrJ z+Ok}Nxdk@`decdA3%c)hH$w1@0MuQ5-zk**_D???2H=?kpZf2%W>Yv$>sRJqe0;K| z@0~)$r-+~dvU^^O+z81YY0I)0>D$7@^;!1s9w3!a8NYm$J&*J;aebByZVCYGAMOF* z^0iqW|Ng}qfg?iu9b7~1`?kYdu`4xK8nmQYz{6^kgU$O8!_B{s)y%%U(sSOgR7m002ovPDHLk FV1nU7+o1pe literal 0 HcmV?d00001 diff --git a/textures/npc_male9.png b/textures/npc_male9.png new file mode 100644 index 0000000000000000000000000000000000000000..7c1a218666f22a1dae23c0cba329a9ced4e26c47 GIT binary patch literal 2419 zcmV-(35@oMP)$LFaQn9vt!0$}5F$`*mtJ>wVX-yNCbY!}TCEmQ6k!+!wr%4$4nhbtt#fG3{M_=n zjzt9kohR=5KDw?0U>F94LP0TX+uhi9H}L->ge3_ej$@J}Aq+#3Bq2>x3WWk`nszXc zRRpW3gkam&C%5x6ge3_exBZ-PUDuIP62~#sYW0-k3Wamp{DCD3AZI-P+TnXm3|+_f zr!k@gM1=281JG*uXMV?L7)ufWfN7eTriql2BuOv~1IUEqHCq78dQ(K9hwC`RGFHCg zT!yg}BHOm{eV<~nNU>NXj$;)C%wmCM)_ECSF{UL#Vi^-j`N{4245Dz^#`US=IJ8>X zDJDrmp-@nEpCk#c>*D*q$^{I=KuU?GX(UNP9LJ|#pioeBx%uRC1HAV3*jaTHMjXe? z%*>FcDY~wAIJ$DVOb`Sr3bg-179C8}MAvmClr&9&%rxW)Er{dH95hWsN{L|@9f{T1 zgaJ^kR#lwWGz}pH0MpZ%GuwC5766tl%T)cNkNyL|5AS}GFbn|*!;ng)f|Qal3@H=} zIF3WJ*+kPcG)+_U=K?ur1Q3QHVHm2@dvfnjlqUzce(2Gx{oY%EjB$WRhIV9==sHEy z;k&=MpEOM|3m>WJe) z1PBo@a&R}huD(q5@4EUjMh@;){g!FcZFh#eFbpvagEUP^)3n36>bkDJvkeVE2*D#m zJMjJK4(4;@<(Ch=!^?->nb%*RJWhRTif&s+C*;7MoysTVs$MRasaC5>Bbv>oa*}5g zg;#%ndzx2HcAaggrMs_H2j2d5foqNxygCSv1|CH9g7l zLk}Z@fY~s{4+36%b!5Q}U;Xok)2`mt2(JNvrW>lfR6ME%T8-@p zcbM_U<@ERWFQ`#kzNxu*aNp5vktE^iy@SY5AiM@vW4qccWX`C4j!Gpv*-JLvt=cwx z*;2xqE3hOOB?(5=9PRU)#UcPwN<7cQ^E@oeLb$_fp8o!REXz`NjP|{Gp0^P1nE=|E zXxW)#$(aXK!t(UqK@2l{;>u4=E3Kc$&>tT=M6p=pj@?^{2fo3wO*<9i!W~wOwhJ!r zZx`eZ*W9PdmSwSS-8!WOA9`QpePax*v|cf}a^*@z|4;cg>+-3!%Kuwi&eGd z0P1u-&%?IGwt zAGl%1pU`!khlanZ+Jz8!o`;lDnSo_lG#U-0lvtL9>$<8hr(cQ*6fPN9*AWGZMVly^ z|CpISgU`jQ&yRI5l{3_`huFI1Ak}h_*;d302Sz#spknz6`fY<3VHgI}UcmEDyrpR0 z5^r{#y6d`FmPNf@$1n^W$H8@7CMPG6QsTNU^?IFJt;Xc!Bo|(I;lfd%yQI@wwdt+e zbeD9>T1aoz<})k0RC{mLrpwHfWiI{6kUPmc9~f4ekhd*_nP%9*e9Io9R;w{EFn|z( zTCJ8vffMC+jmX>T^*R8Plaq_C2cVgzNwu7XoBZWBuiK@M|J^Z7lkSp^X=cerwOmx? z_S0`qRdXQ#eDf=N=9xl1cH7`Z%(fy9KD%`uO}$=cY-~)0!koc+y^fTUv9U46#>Nmr zsJ7hxm*NKuK%G(;hM1cdU}IkoO+TjTx7gU%g9rj8gpGYY zSSQS)>Bp?@aTVh^+A}fBvY4KprW@vIL5}vwBHZD5nI?b`A*1h)Q?1wlcuilGXPdsF l0q{JZTCE!)Le;kr{tIbt+P0)`47mUR002ovPDHLkV1m+vuV(-N literal 0 HcmV?d00001