diff --git a/actions/actions.lua b/actions/actions.lua index 8bede43..8c375c4 100644 --- a/actions/actions.lua +++ b/actions/actions.lua @@ -844,7 +844,7 @@ function npc.actions.walk_to_pos(self, args) -- Check if movement is enforced if enforce_move then -- Move to end pos - self.object:moveto(end_pos) + self.object:moveto({x=end_pos.x, y=end_pos.y+1, z=end_pos.z}) end end end \ No newline at end of file diff --git a/dialogue.lua b/dialogue.lua index 5d703a3..bc60dcf 100644 --- a/dialogue.lua +++ b/dialogue.lua @@ -296,7 +296,7 @@ function npc.dialogue.process_dialogue(self, dialogue, player_name) -- Send dialogue line if dialogue.text then - minetest.chat_send_player(player_name, self.name..": "..dialogue.text) + minetest.chat_send_player(player_name, self.npc_name..": "..dialogue.text) end -- Check if dialogue has responses. If it doesn't, unlock the actions diff --git a/npc.lua b/npc.lua index f4c59fc..b5ad09b 100755 --- a/npc.lua +++ b/npc.lua @@ -9,6 +9,11 @@ npc = {} npc.FEMALE = "female" npc.MALE = "male" +npc.age = { + adult = "adult", + child = "child" +} + npc.INVENTORY_ITEM_MAX_STACK = 99 npc.ANIMATION_STAND_START = 0 @@ -89,8 +94,46 @@ local function is_female_texture(textures) return false end +local function get_random_texture(sex, age) + local textures = {} + local filtered_textures = {} + -- Find textures by sex and age + if age == npc.age.adult then + --minetest.log("Registered: "..dump(minetest.registered_entities["advanced_npc:npc"])) + textures = minetest.registered_entities["advanced_npc:npc"].texture_list + elseif age == npc.age.child then + textures = minetest.registered_entities["advanced_npc:npc"].child_texture + end + + minetest.log("Textures: "..dump(textures)) + minetest.log("Sex: "..sex) + minetest.log("Age: "..age) + + for i = 1, #textures do + local current_texture = textures[i][1] + if (sex == npc.MALE + and string.find(current_texture, sex) + and not string.find(current_texture, npc.FEMALE)) + or (sex == npc.FEMALE + and string.find(current_texture, sex)) then + table.insert(filtered_textures, current_texture) + end + end + + -- Check if filtered textures is empty + if filtered_textures == {} then + return textures[1][1] + end + + return filtered_textures[math.random(1,#filtered_textures)] +end + -- Choose whether NPC can have relationships. Only 30% of NPCs cannot have relationships -local function can_have_relationships() +local function can_have_relationships(age) + -- Children can't have relationships + if not age then + return false + end local chance = math.random(1,10) return chance > 3 end @@ -132,7 +175,7 @@ end -- Spawn function. Initializes all variables that the -- NPC will have and choose random, starting values -function npc.initialize(entity, pos, is_lua_entity) +function npc.initialize(entity, pos, is_lua_entity, npc_stats) minetest.log("[advanced_npc] INFO: Initializing NPC at "..minetest.pos_to_string(pos)) -- Get variables @@ -146,21 +189,93 @@ function npc.initialize(entity, pos, is_lua_entity) -- Avoid NPC to be removed by mobs_redo API ent.remove_ok = false - -- Determine sex based on textures - if (is_female_texture(ent.base_texture)) then - ent.sex = npc.FEMALE + -- Determine sex and age + -- If there's no previous NPC data, sex and age will be randomly chosen. + -- - Sex: Female or male will have each 50% of spawning + -- - Age: 90% chance of spawning adults, 10% chance of spawning children. + -- If there is previous data then: + -- - Sex: The unbalanced sex will get a 75% chance of spawning + -- - Example: If there's one male, then female will have 75% spawn chance. + -- - If there's male and female, then each have 50% spawn chance. + -- - Age: For each two adults, the chance of spawning a child next will be 50% + -- If there's a child for two adults, the chance of spawning a child goes to + -- 40% and keeps decreasing unless two adults have no child. + if npc_stats then + -- Default chances + local male_s, male_e = 0, 50 + local female_s, female_e = 51, 100 + local adult_s, adult_e = 0, 90 + local child_s, child_e = 91, 100 + -- Determine sex probabilities + if npc_stats[npc.FEMALE].total > npc_stats[npc.MALE].total then + male_e = 75 + female_s, female_e = 76, 100 + elseif npc_stats[npc.FEMALE].total < npc_stats[npc.MALE].total then + male_e = 25 + female_s, female_e = 26, 100 + end + -- Determine age probabilities + if npc_stats["adult_total"] >= 2 then + if npc_stats["adult_total"] % 2 == 0 + and (npc_stats["adult_total"] / 2 > npc_stats["child_total"]) then + child_s,child_e = 51, 100 + adult_e = 50 + else + child_s, child_e = 61, 100 + adult_e = 60 + end + end + -- Get sex and age based on the probabilities + local sex_chance = math.random(1, 100) + local age_chance = math.random(1, 100) + local selected_sex = "" + local selected_age = "" + -- Select sex + if male_s <= sex_chance and sex_chance <= male_e then + selected_sex = npc.MALE + elseif female_s <= sex_chance and sex_chance <= female_e then + selected_sex = npc.FEMALE + end + -- Set sex for NPC + ent.sex = selected_sex + -- Select age + if adult_s <= age_chance and age_chance <= adult_e then + selected_age = npc.age.adult + elseif child_s <= age_chance and age_chance <= child_e then + selected_age = npc.age.child + ent.visual_size = { + x = 0.5, + y = 0.5 + } + ent.collisionbox = {-0.10,-0.50,-0.10, 0.10,0.40,0.10} + ent.is_child = true + end + -- Set texture accordingly + local selected_texture = get_random_texture(selected_sex, selected_age) + --minetest.log("Selected texture: "..dump(selected_texture)) + ent.textures = {selected_texture} + if selected_age == npc.age.child then + ent.base_texture = selected_texture + end else - ent.sex = npc.MALE + -- Get sex based on texture. This is a 50% chance for + -- each sex as there's same amount of textures for male and female. + -- Do not spawn child as first NPC + if (is_female_texture(ent.base_texture)) then + ent.sex = npc.FEMALE + else + ent.sex = npc.MALE + end end -- Nametag is initialized to blank ent.nametag = "" -- Set name - ent.name = get_random_name(ent.sex) + ent.npc_name = get_random_name(ent.sex) -- Set ID - ent.npc_id = tostring(math.random(1000, 9999))..":"..ent.name + ent.npc_id = tostring(math.random(1000, 9999))..":"..ent.npc_name -- Initialize all gift data ent.gift_data = { @@ -171,7 +286,7 @@ function npc.initialize(entity, pos, is_lua_entity) } -- Flag that determines if NPC can have a relationship - ent.can_have_relationship = can_have_relationships() + ent.can_have_relationship = can_have_relationships(ent.is_child) -- Initialize relationships object ent.relationships = {} @@ -285,28 +400,6 @@ function npc.initialize(entity, pos, is_lua_entity) date_based = {} } - -- Temporary initialization of actions for testing - local nodes = npc.places.find_node_nearby(ent.object:getpos(), {"cottages:bench"}, 20) - - --minetest.log("Self destination: "..minetest.pos_to_string(nodes[1])) - - --local path = pathfinder.find_path(ent.object:getpos(), nodes[1], 20, {}) - --minetest.log("Path to node: "..dump(path)) - --npc.add_action(ent, npc.actions.use_door, {self = ent, pos = nodes[1], action = npc.actions.door_action.OPEN}) - --npc.add_action(ent, npc.actions.stand, {self = ent}) - --npc.add_action(ent, npc.actions.stand, {self = ent}) - -- if nodes[1] ~= nil then - -- npc.add_task(ent, npc.actions.walk_to_pos, {end_pos=nodes[1], walkable={}}) - -- npc.actions.use_furnace(ent, nodes[1], "default:cobble 5", false) - -- --npc.add_action(ent, npc.actions.sit, {self = ent}) - -- -- npc.add_action(ent, npc.actions.lay, {self = ent}) - -- -- npc.add_action(ent, npc.actions.lay, {self = ent}) - -- -- npc.add_action(ent, npc.actions.lay, {self = ent}) - -- --npc.actions.use_sittable(ent, nodes[1], npc.actions.const.sittable.GET_UP) - -- --npc.add_action(ent, npc.actions.set_interval, {self=ent, interval=10, freeze=true}) - -- npc.add_action(ent, npc.actions.freeze, {freeze = false}) - --end - -- Dedicated trade test ent.trader_data.trade_list.both = { ["default:tree"] = {}, @@ -332,15 +425,8 @@ function npc.initialize(entity, pos, is_lua_entity) local offer2 = npc.trade.create_custom_sell_trade_offer("Do you want me to fix your mese sword?", "Fix mese sword", "Fix mese sword", "default:sword_mese", {"default:sword_mese", "default:copper_lump 10"}) table.insert(ent.trader_data.custom_trades, offer2) - -- Temporary initialization of places - -- local bed_nodes = npc.places.find_new_nearby(ent, npc.places.nodes.BEDS, 8) - -- minetest.log("Number of bed nodes: "..dump(#bed_nodes)) - -- if #bed_nodes > 0 then - -- npc.places.add_owned(ent, "bed1", npc.places.PLACE_TYPE.OWN_BED, bed_nodes[1]) - -- end - --minetest.log(dump(ent)) - minetest.log("[advanced_npc] INFO Successfully initialized NPC with name "..dump(ent.name)) + minetest.log("[advanced_npc] INFO Successfully initialized NPC with name "..dump(ent.npc_name)) -- Refreshes entity ent.object:set_properties(ent) end @@ -510,7 +596,7 @@ end function npc.execute_action(self) -- Check if an action was interrupted if self.actions.current_action_state == npc.action_state.interrupted then - minetest.log("[advanced_npc] DEBUG Re-inserting interrupted action for NPC: '"..dump(self.name).."': "..dump(self.actions.state_before_lock.interrupted_action)) + minetest.log("[advanced_npc] DEBUG Re-inserting interrupted action for NPC: '"..dump(self.npc_name).."': "..dump(self.actions.state_before_lock.interrupted_action)) -- Insert into queue the interrupted action table.insert(self.actions.queue, 1, self.actions.state_before_lock.interrupted_action) -- Clear the action @@ -532,7 +618,7 @@ function npc.execute_action(self) -- If the entry is a task, then push all this new operations in -- stack fashion if action_obj.is_task == true then - minetest.log("[advanced_npc] DEBUG Executing task for NPC '"..dump(self.name).."': "..dump(action_obj)) + minetest.log("[advanced_npc] DEBUG Executing task for NPC '"..dump(self.npc_name).."': "..dump(action_obj)) -- Backup current queue local backup_queue = self.actions.queue -- Remove this "task" action from queue @@ -548,7 +634,7 @@ function npc.execute_action(self) table.insert(self.actions.queue, backup_queue[i]) end else - minetest.log("[advanced_npc] DEBUG Executing action for NPC '"..dump(self.name).."': "..dump(action_obj)) + minetest.log("[advanced_npc] DEBUG Executing action for NPC '"..dump(self.npc_name).."': "..dump(action_obj)) -- Store the action that is being executed self.actions.state_before_lock.interrupted_action = action_obj -- Store current position @@ -773,12 +859,13 @@ mobs:register_mob("advanced_npc:npc", { mesh = "character.b3d", drawtype = "front", textures = { - {"mobs_npc_male1.png"}, - {"mobs_npc_female1.png"}, -- female by nuttmeg20 + {"npc_male1.png"}, + {"npc_female1.png"}, -- female by nuttmeg20 }, child_texture = { - {"mobs_npc_baby_male1.png"}, -- derpy baby by AmirDerAssassine - }, + {"npc_baby_male1.png"}, -- derpy baby by AmirDerAssassine + {"npc_baby_female1.png"}, + }, makes_footstep_sound = true, sounds = {}, -- Added walk chance @@ -822,8 +909,17 @@ mobs:register_mob("advanced_npc:npc", { -- Get information from clicker local item = clicker:get_wielded_item() local name = clicker:get_player_name() - + + --self.child = true + --self.textures = {"mobs_npc_child_male1.png"} + --self.base_texture = "mobs_npc_child_male1.png" + --self.object:set_properties(self) + minetest.log(dump(self)) + + minetest.log("Child: "..dump(self.is_child)) + minetest.log("Sex: "..dump(self.sex)) + minetest.log("Textures: "..dump(self.textures)) -- Receive gift or start chat. If player has no item in hand -- then it is going to start chat directly @@ -835,7 +931,7 @@ mobs:register_mob("advanced_npc:npc", { -- Show dialogue to confirm that player is giving item as gift npc.dialogue.show_yes_no_dialogue( self, - "Do you want to give "..item_name.." to "..self.name.."?", + "Do you want to give "..item_name.." to "..self.npc_name.."?", npc.dialogue.POSITIVE_GIFT_ANSWER_PREFIX..item_name, function() npc.relationships.receive_gift(self, clicker) diff --git a/spawner.lua b/spawner.lua index f38da63..c860f06 100644 --- a/spawner.lua +++ b/spawner.lua @@ -57,10 +57,14 @@ npc.spawner.replacement_interval = 60 npc.spawner.spawn_delay = 10 npc.spawner.spawn_data = { - status = { - ["dead"] = 0, - ["alive"] = 1 - } + status = { + dead = 0, + alive = 1 + }, + age = { + adult = "adult", + child = "child" + } } local function get_basic_schedule() @@ -220,7 +224,7 @@ function spawner.assign_places(self, pos) -- Store changes to node_data meta:set_string("node_data", minetest.serialize(node_data)) minetest.log("Added bed at "..minetest.pos_to_string(node_data.bed_type[i].node_pos) - .." to NPC "..dump(self.name)) + .." to NPC "..dump(self.npc_name)) break end end @@ -241,6 +245,8 @@ 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) @@ -252,11 +258,6 @@ function spawner.assign_schedules(self, pos) -- Add schedule entry for evening actions npc.add_schedule_entry(self, npc.schedule_types.generic, 0, 22, nil, basic_schedule.evening_actions) - - minetest.log("Schedules: "..dump(self.schedules)) - --local afternoon_actions = { [1] = {action = npc.actions.stand, args = {}} } - --local afternoon_actions = {[1] = {task = npc.actions.cmd.USE_SITTABLE, args = {pos=nodes[1], action=npc.actions.const.sittable.GET_UP} } } - end -- This function is called when the node timer for spawning NPC @@ -266,17 +267,27 @@ function npc.spawner.spawn_npc(pos) 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") - minetest.log("Currently spawned "..dump(spawned_npc_count).." of "..dump(npc_count).." NPCs") + minetest.log("[advanced_npc] INFO Currently spawned "..dump(spawned_npc_count).." of "..dump(npc_count).." NPCs") if spawned_npc_count < npc_count then - minetest.log("[advanced_npc] Spawning NPC at "..minetest.pos_to_string(pos)) + minetest.log("[advanced_npc] 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 - npc.initialize(ent, pos) + -- Initialize NPC + -- Call with stats if there are NPCs + if #npc_table > 0 then + npc.initialize(ent, pos, false, npc_stats) + else + npc.initialize(ent, pos) + end -- Assign nodes spawner.assign_places(ent:get_luaentity(), pos) -- Assign schedules @@ -285,22 +296,34 @@ function npc.spawner.spawn_npc(pos) spawned_npc_count = spawned_npc_count + 1 -- Store count into node meta:set_int("spawned_npc_count", spawned_npc_count) - -- Store spawned NPC data into node - local npc_table = minetest.deserialize(meta:get_string("npcs")) + -- 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 = ent:get_luaentity(). + age = age, born_day = minetest.get_day_count() } table.insert(npc_table, entry) - -- Store into metadata 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) + --meta:set_string("infotext", meta:get_string("infotext")..", "..spawned_npc_count) minetest.log("[advanced_npc] INFO Spawning successful!") -- Check if there are more NPCs to spawn if spawned_npc_count >= npc_count then @@ -486,6 +509,22 @@ function spawner.replace_mg_villages_plotmarker(pos) -- 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)) -- Stop searching for building type break @@ -525,11 +564,12 @@ if minetest.get_modpath("mg_villages") ~= nil then --local entrance = npc.places.find_entrance_from_openable_nodes(nodedata.openable_type, pos) --minetest.log("Found entrance: "..dump(entrance)) - for i = 1, #nodedata.bed_type do - nodedata.bed_type[i].owner = "" - end - minetest.get_meta(pos):set_string("node_data", minetest.serialize(nodedata)) - minetest.log("Cleared bed owners") + -- for i = 1, #nodedata.bed_type do + -- nodedata.bed_type[i].owner = "" + -- end + -- minetest.get_meta(pos):set_string("node_data", minetest.serialize(nodedata)) + -- minetest.log("Cleared bed owners") + minetest.log("NPC stats: "..dump(minetest.deserialize(minetest.get_meta(pos):get_string("npc_stats")))) return mg_villages.plotmarker_formspec( pos, nil, {}, clicker ) end, @@ -632,6 +672,10 @@ minetest.register_chatcommand("restore_plotmarkers", { meta:set_string("village_id", village_id) meta:set_int("plot_nr", plot_nr) meta:set_string("infotext", infotext) + -- Clear NPC stats, NPC data and node data + meta:set_string("node_data", nil) + meta:set_string("npcs", nil) + meta:set_string("npc_stats", nil) end minetest.chat_send_player(name, "Finished replacement of "..dump(#nodes).." auto-spawners successfully") end diff --git a/textures/mobs_npc_baby_male1.png b/textures/mobs_npc_baby_male1.png deleted file mode 100755 index e26e450..0000000 Binary files a/textures/mobs_npc_baby_male1.png and /dev/null differ diff --git a/textures/npc_baby_female1.png b/textures/npc_baby_female1.png new file mode 100755 index 0000000..12d17a4 Binary files /dev/null and b/textures/npc_baby_female1.png differ diff --git a/textures/mobs_npc_male1.png b/textures/npc_baby_male1.png similarity index 100% rename from textures/mobs_npc_male1.png rename to textures/npc_baby_male1.png diff --git a/textures/mobs_npc_female1.png b/textures/npc_female1.png similarity index 100% rename from textures/mobs_npc_female1.png rename to textures/npc_female1.png diff --git a/textures/npc_male1.png b/textures/npc_male1.png new file mode 100755 index 0000000..9356398 Binary files /dev/null and b/textures/npc_male1.png differ diff --git a/trade/trade.lua b/trade/trade.lua index 9234d1c..6008eed 100644 --- a/trade/trade.lua +++ b/trade/trade.lua @@ -112,7 +112,7 @@ function npc.trade.show_trade_offer_formspec(self, player, offer_type) default.gui_bg.. default.gui_bg_img.. default.gui_slots.. - "label[2,0.1;"..self.name..prompt_string.."]".. + "label[2,0.1;"..self.npc_name..prompt_string.."]".. "item_image_button[2,1.3;1.2,1.2;"..trade_offer.item..";item;]".. "label[3.75,1.75;"..for_string.."]".. "item_image_button[4.8,1.3;1.2,1.2;"..trade_offer.price[1]..";price;]".. @@ -221,7 +221,7 @@ function npc.trade.show_custom_trade_offer(self, player, offer) default.gui_bg.. default.gui_bg_img.. default.gui_slots.. - "label[2,0.1;"..self.name..": "..offer.dialogue_prompt.."]".. + "label[2,0.1;"..self.npc_name..": "..offer.dialogue_prompt.."]".. price_grid.. "label["..(margin_x + 3.75)..",1.75;"..for_string.."]".. "item_image_button["..(margin_x + 4.8)..",1.3;1.2,1.2;"..offer.item..";item;]".. diff --git a/trader.lua b/trader.lua index 169f445..f7ed2e8 100755 --- a/trader.lua +++ b/trader.lua @@ -243,16 +243,16 @@ function mobs_trader(self, clicker, entity, race) if not self.id then self.id = (math.random(1, 1000) * math.random(1, 10000)) - .. self.name .. (math.random(1, 1000) ^ 2) + .. self.npc_name .. (math.random(1, 1000) ^ 2) end if not self.game_name then self.game_name = tostring(race.names[math.random(1, #race.names)]) - self.nametag = S("Trader @1", self.game_name) + self.npc_nametag = S("Trader @1", self.game_name) self.object:set_properties({ - nametag = self.nametag, + nametag = self.npc_nametag, nametag_color = "#00FF00" }) diff --git a/trader_test.lua b/trader_test.lua index 41e734c..9ab48db 100755 --- a/trader_test.lua +++ b/trader_test.lua @@ -123,14 +123,14 @@ function mobs_trader(self, clicker, race) if not self.id then self.id = (math.random(1, 1000) * math.random(1, 10000)) - .. self.name .. (math.random(1, 1000) ^ 2) + .. self.npc_name .. (math.random(1, 1000) ^ 2) end if not self.game_name then self.game_name = tostring(race.names[math.random(1, #race.names)]) - self.nametag = S("Trader @1", self.game_name) + self.npc_nametag = S("Trader @1", self.game_name) self.object:set_properties({ - nametag = self.nametag, + nametag = self.npc_nametag, nametag_color = "#00FF00" }) end