--local dprint = print local dprint = function() return end local mapping = schemlib.mapping local npc_ai = {} local npc_ai_class = {} local mpc_ai_class_mt = {__index = npc_ai_class} -------------------------------------- -- Create NPC-AI object -------------------------------------- function npc_ai.new(plan, build_distance) local self = setmetatable({}, mpc_ai_class_mt) self.plan = plan self.build_distance = build_distance or 3 return self end -------------------------------------- -- Check if the node from plan already built in the world -------------------------------------- function npc_ai_class:get_if_buildable(node) if not node then return end --revert the final mapping if happens alrady --The NPC does bild the "name" so it is replaced by air in case the place needs to be cleared first if node.final_node_name then node.name = node.final_node_name node.nodeinfo = self.plan.data.nodeinfos[node.name] node.final_node_name = nil end -- check if already built local mapped = node:get_mapped() if not mapped then -- not buildable node:remove_from_plan() return nil end -- get the original node from loaded area. Load a chunk if not given local world_pos = node:get_world_pos() if not self.plan.vm_area or not self.plan.vm_area:contains(world_pos.x, world_pos.y, world_pos.z) then self.plan:load_region(world_pos) end local node_index = self.plan.vm_area:indexp(world_pos) local world_content_id = self.plan.vm_data[node_index] node.world_node_name = minetest.get_name_from_content_id(world_content_id) -- place attached nodes after the node under local attached_pos = node:get_attached_to() if attached_pos and self.plan:get_node(attached_pos) then return -- attaching not possible end if world_content_id == mapped.content_id then -- right node is at the place. there are no costs to touch them. Check if a touch needed if mapped.node_def.paramtype2 and (mapped.param2 ~= self.plan.vm_param2_data[node_index]) then --param2 adjustment return node elseif not mapped.meta or mapping.is_equal_meta(minetest.get_meta(world_pos):to_table(), mapped.meta) then --same item without metadata. nothing to do node:remove_from_plan() return nil else return node end else -- no right node at place -- Check if the previous content needs to be replaced if not mapping._volatile_contend_ids[world_content_id] then node.final_node_name = node.name node.name = "air" node.nodeinfo = self.plan.data.nodeinfos["air"] elseif mapping._airlike_contend_ids[mapped.content_id] and mapping._airlike_contend_ids[world_content_id] then -- removal of this type of node not decessary old/new are _not_removal_nodes node:remove_from_plan() return nil end return node end end -------------------------------------- -- Get rating for node which one should be built at next -------------------------------------- function npc_ai_class:get_node_rating(node, npcpos) local world_pos = node:get_world_pos() local mapped = node:get_mapped() local distance_pos = { x = world_pos.x, y = world_pos.y, z = world_pos.z} local prefer = 0 -- build water at the later time if mapped.node_def.groups.liquid then prefer = prefer - self.build_distance end --prefer same items in building order if self.lasttarget_name then if self.lasttarget_name == mapped.name then prefer = prefer + 1 end if world_pos.x == self.lasttarget_pos.x and world_pos.y == self.lasttarget_pos.y and world_pos.z == self.lasttarget_pos.z then prefer = prefer + self.build_distance end end -- prefer air in general, adjust prefered high for non-air, if mapped.name == "air" then prefer = prefer + self.build_distance + 1 else if node:get_under() then prefer = prefer - 2 end distance_pos.y = distance_pos.y + self.build_distance end -- penalty for air under the walking line and for non air above local walking_high = npcpos.y-1 + math.abs(npcpos.x-world_pos.x) + math.abs(npcpos.z-world_pos.z) if ( mapped.name ~= "air" and world_pos.y > walking_high) or ( mapped.name == "air" and world_pos.y < walking_high) then prefer = prefer - self.build_distance end -- avoid build directly under or in the npc if mapped.name ~= "air" and math.abs(npcpos.x - world_pos.x) < 0.5 and math.abs(npcpos.y - world_pos.y) <= self.build_distance and math.abs(npcpos.z - world_pos.z) < 0.5 then prefer = prefer - self.build_distance end -- compare return prefer - vector.distance(npcpos, distance_pos) end -------------------------------------- -- Select the best rated node from list -------------------------------------- function npc_ai_class:prefer_target(npcpos, nodeslist) local selected_node local selected_node_rating for _, node in ipairs(nodeslist) do if self:get_if_buildable(node) then local current_rating = self:get_node_rating(node, npcpos) if not selected_node or current_rating > selected_node_rating then selected_node = node selected_node_rating = current_rating end end end return selected_node end -------------------------------------- -- Select the best rated node from list -------------------------------------- function npc_ai_class:plan_target_get(npcpos) local npcpos_round = vector.round(npcpos) local npcpos_plan = self.plan:get_plan_pos(npcpos_round) local selectednode local first_distance = 5 local prefer_list = {} -- first try: look for nearly buildable nodes dprint("search for nearly node") for x=npcpos_plan.x-first_distance, npcpos_plan.x+first_distance do for y=npcpos_plan.y-first_distance, npcpos_plan.y+first_distance do for z=npcpos_plan.z-first_distance, npcpos_plan.z+first_distance do local node = self.plan:get_node({x=x,y=y,z=z}) if node then table.insert(prefer_list, node) end end end end self.plan:load_region(vector.subtract(npcpos_round, first_distance), vector.add(npcpos_round, first_distance)) selectednode = self:prefer_target(npcpos, prefer_list) if selectednode then dprint("nearly found: NPC: "..minetest.pos_to_string(npcpos).." Block "..minetest.pos_to_string(selectednode:get_world_pos())) self.lasttarget_name = selectednode:get_mapped().name self.lasttarget_pos = selectednode:get_world_pos() return selectednode else dprint("nearly nothing found") end -- second try. Check the current chunk dprint("search for node in current chunk") local chunk_nodes, min_world_pos, max_world_pos = self.plan:get_chunk_nodes(npcpos_plan) -- add last selection to the current chunk to compare if self.lasttarget_pos then local last_target_node = self.plan:get_node(self.lasttarget_pos) if last_target_node then table.insert(chunk_nodes, last_target_node) end end dprint("Chunk loaeded: nodes:", #chunk_nodes) self.plan:load_region(min_world_pos, max_world_pos) selectednode = self:prefer_target(npcpos, chunk_nodes) if selectednode then dprint("found in current chunk: NPC: "..minetest.pos_to_string(npcpos).." Block "..minetest.pos_to_string(selectednode:get_world_pos())) self.lasttarget_name = selectednode:get_mapped().name self.lasttarget_pos = selectednode:get_world_pos() return selectednode else dprint("current chunk nothing found") end dprint("get random node") local random_pos = self.plan:get_random_plan_pos() if random_pos then dprint("---check chunk", minetest.pos_to_string(random_pos)) selectednode = self:get_if_buildable(self.plan:get_node(random_pos)) if selectednode then dprint("random node: Block "..minetest.pos_to_string(random_pos)) else dprint("random node not buildable, check the whole chunk", minetest.pos_to_string(random_pos)) local chunk_nodes, min_world_pos, max_world_pos = self.plan:get_chunk_nodes(random_pos) dprint("Chunk loaeded: nodes:", #chunk_nodes) selectednode = self:prefer_target(npcpos, chunk_nodes) if selectednode then dprint("found in current chunk: Block "..minetest.pos_to_string(selectednode:get_world_pos())) end end else dprint("something wrong with random node") end if selectednode then self.lasttarget_name = selectednode:get_mapped().name self.lasttarget_pos = selectednode:get_world_pos() return selectednode else dprint("no next node found", self.plan.data.nodecount) end end function npc_ai_class:place_node(targetnode) local mapped = targetnode:get_mapped() local pos = targetnode:get_world_pos() dprint("target reached - build", mapped.name, targetnode.world_node_name, targetnode.final_node_name, minetest.pos_to_string(pos)) local soundspec local check_fall = false if mapped.name == "air" and targetnode.world_node_name then if minetest.registered_items[targetnode.world_node_name].sounds then soundspec = minetest.registered_items[targetnode.world_node_name].sounds.dug end check_fall = true elseif minetest.registered_items[mapped.name].sounds then soundspec = minetest.registered_items[mapped.name].sounds.place end targetnode:place() if soundspec then soundspec.pos = pos minetest.sound_play(soundspec.name, soundspec) end if check_fall then minetest.check_for_falling(pos) end end return npc_ai