diff --git a/README.md b/README.md index b7a82c9..c01bc70 100644 --- a/README.md +++ b/README.md @@ -373,40 +373,52 @@ Submitted data can then be captured in the NPC's own 'on_receive_fields' callbac Note that form text fields, dropdown, list and checkbox selections are automatically stored in the NPC's metadata table. Image/Button clicks, however, are not. -Control Framework +Movement Framework ---- ## Methods -### npcf.control_framework.getControl(npc_ref) -Constructor for the control object. Returns the reference. +### mvobj = npcf.movement.getControl(npc_ref) + +Constructor for the movement control object. Returns the reference. Note, the framework will be activated for NPC on first usage. -### control:stay() +### mvobj:stay() Stop walking, stand up -### control:look_to(pos) +### mvobj:look_to(pos) Look (set yaw) to direction of position pos -### control:sit() +### mvobj:sit() Stop walking and sit down -### control:lay() +### mvobj:lay() Stop walking and lay down -### control:mine() +### mvobj:mine() Begin the mining / digging / attacking animation -### control:mine_stop() +### mvobj:mine_stop() Stop the mining / digging / attacking animation -### control:walk(pos, speed, parameter) +### mvobj:teleport(pos) +Teleport the NPC to given position + +### mvobj:walk(pos, speed, parameter) Find the way and walk to position pos with given speed. For parameter check the set_walk_parameter documentation -###control_proto:stop() +###mvobj:stop() Stay and forgot about the destination -### control:set_walk_parameter(parameter) + +###mvobj:get_path(pos) +Calculate the path. Is used internally in mvobj:walk + +###mvobj:check_for_stuck() +Check if the NPC is stuck. Teleport or stay in this case. +This method is called in framework each step so there is no need to call it by self + +### mvobj:set_walk_parameter(parameter) key-value table to change the walking path determination parameter - find_path @@ -431,7 +443,7 @@ key-value table to change the walking path determination parameter false: forgot about the destination in case of stuck ## Attributes (should be used read-only) -control.is_mining - mining animation is active -control.speed - walking speed -control.real_speed - The "real" speed calculated on velocity -target_pos - Position vector that the NPC try to reach +mvobj.is_mining - mining animation is active +mvobj.speed - walking speed +mvobj.real_speed - The "real" speed calculated on velocity +mvobj.target_pos - Position vector that the NPC try to reach diff --git a/npcf/movement.lua b/npcf/movement.lua new file mode 100644 index 0000000..645da8c --- /dev/null +++ b/npcf/movement.lua @@ -0,0 +1,332 @@ +-- NPC framework navigation control object prototype +local mvobj_proto = { + is_mining = false, + speed = 0, + target_pos = nil, + _path = nil, + _npc = nil, + _state = NPCF_ANIM_STAND, + _step_timer = 0, + walk_param = { + find_path = true, + find_path_fallback = true, + find_path_max_distance = 20, + fuzzy_destination = true, + fuzzy_destination_distance = 5, + teleport_on_stuck = false, + } +} + +-- navigation control framework +local movement = { + mvobj_proto = mvobj_proto, + functions = {}, + getControl = function(npc) + local mvobj + if npc._mvobj then + mvobj = npc._mvobj + else + mvobj = npcf.deepcopy(mvobj_proto) + mvobj._npc = npc + npc._mvobj = mvobj + end + if npc.object and mvobj._step_init_done ~= true then + mvobj.pos = npc.object:getpos() + mvobj.yaw = npc.object:getyaw() + mvobj.velocity = npc.object:getvelocity() + mvobj.acceleration = npc.object:getacceleration() + mvobj._step_init_done = true + end + return mvobj + end +} +local functions = movement.functions + + +-- Stop walking and stand up +function mvobj_proto:stay() + self.speed = 0 + self._state = NPCF_ANIM_STAND +end + +-- Stay and forgot about the way +function mvobj_proto:stop() + self:stay() + self._path = nil + self._target_pos_bak = nil + self.target_pos = nil + self._last_distance = nil +end + +-- look to position +function mvobj_proto:look_to(pos) + self.yaw = npcf:get_face_direction(self.pos, pos) +end + +-- Stop walking and sitting down +function mvobj_proto:sit() + self.speed = 0 + self.is_mining = false + self._state = NPCF_ANIM_SIT +end + +-- Stop walking and lay +function mvobj_proto:lay() + self.speed = 0 + self.is_mining = false + self._state = NPCF_ANIM_LAY +end + +-- Start mining +function mvobj_proto:mine() + self.is_mining = true +end + +-- Stop mining +function mvobj_proto:mine_stop() + self.is_mining = false +end + +-- teleport to position +function mvobj_proto:teleport(pos) + self.pos = pos + self._npc.object:setpos(pos) + self:stay() +end + +-- Change default parameters for walking +function mvobj_proto:set_walk_parameter(param) + for k,v in pairs(param) do + self.walk_param[k] = v + end +end + +-- start walking to pos +function mvobj_proto:walk(pos, speed, param) + if param then + self:set_walk_parameter(param) + end + self._target_pos_bak = self.target_pos + self.target_pos = pos + self.speed = speed + if self.walk_param.find_path == true then + self._path = self:get_path(pos) + else + self._path = { pos } + self._path_used = false + end + + if self._path == nil then + self:stop() + self:look_to(pos) + else + self._walk_started = true + end +end + +-- do a walking step +function mvobj_proto:_do_movement_step(dtime) + -- step timing / initialization check + self._step_timer = self._step_timer + dtime + if self._step_timer < 0.1 then + return + end + self._step_timer = 0 + movement.getControl(self._npc) + self._step_init_done = false + + self:check_for_stuck() + + -- check path + if self.speed > 0 then + if not self._path or not self._path[1] then + self:stop() + else + local a = table.copy(self.pos) + a.y = 0 + local b = {x=self._path[1].x, y=0 ,z=self._path[1].z} + --print(minetest.pos_to_string(self.pos), minetest.pos_to_string(self._path[1]), vector.distance(a, b),minetest.pos_to_string(self._npc.object:getpos())) + --if self._path[2] then print(minetest.pos_to_string(self._path[2])) end + + if vector.distance(a, b) < 0.4 + or (self._path[2] and vector.distance(self.pos, self._path[2]) < vector.distance(self._path[1], self._path[2])) then + if self._path[2] then + table.remove(self._path, 1) + self._walk_started = true + else + self:stop() + end + end + end + end + -- check/set yaw + if self._path and self._path[1] then + self.yaw = npcf:get_face_direction(self.pos, self._path[1]) + end + self._npc.object:setyaw(self.yaw) + + -- check/set animation + if self.is_mining then + if self.speed == 0 then + self._state = NPCF_ANIM_MINE + else + self._state = NPCF_ANIM_WALK_MINE + end + else + if self.speed == 0 then + if self._state ~= NPCF_ANIM_SIT and + self._state ~= NPCF_ANIM_LAY then + self._state = NPCF_ANIM_STAND + end + else + self._state = NPCF_ANIM_WALK + end + end + npcf:set_animation(self._npc, self._state) + + -- check for current environment + local nodepos = table.copy(self.pos) + local node = {} + nodepos.y = nodepos.y - 0.5 + for i = -1, 1 do + node[i] = minetest.get_node(nodepos) + nodepos.y = nodepos.y + 1 + end + if string.find(node[-1].name, "^default:water") then + self.acceleration = {x=0, y=-4, z=0} + self._npc.object:setacceleration(self.acceleration) + -- we are walking in water + if string.find(node[0].name, "^default:water") or + string.find(node[1].name, "^default:water") then + -- we are under water. sink if target bellow the current position. otherwise swim up + if not self._path[1] or self._path[1].y > self.pos.y then + self.velocity.y = 3 + end + end + elseif minetest.find_node_near(self.pos, 2, {"group:water"}) then + -- Light-footed near water + self.acceleration = {x=0, y=-1, z=0} + self._npc.object:setacceleration(self.acceleration) + elseif minetest.registered_nodes[node[-1].name].walkable ~= false and + minetest.registered_nodes[node[0].name].walkable ~= false then + -- jump if in catched in walkable node + self.velocity.y = 3 + else + -- walking + self.acceleration = {x=0, y=-10, z=0} + self._npc.object:setacceleration(self.acceleration) + end + + --check/set velocity + self.velocity = npcf:get_walk_velocity(self.speed, self.velocity.y, self.yaw) + self._npc.object:setvelocity(self.velocity) +end + + +function mvobj_proto:get_path(pos) + local startpos = vector.round(self.pos) + startpos.y = startpos.y - 1 -- NPC is to high + local refpos + if vector.distance(self.pos, pos) > self.walk_param.find_path_max_distance then + refpos = vector.add(self.pos, vector.multiply(vector.direction(self.pos, pos), self.walk_param.find_path_max_distance)) + else + refpos = pos + end + + local destpos + if self.walk_param.fuzzy_destination == true then + destpos = functions.get_walkable_pos(refpos, self.walk_param.fuzzy_destination_distance) + end + if not destpos then + destpos = self.pos + end + local path = minetest.find_path(startpos, destpos, 10, 1, 5, "Dijkstra") + + if not path and self.walk_param.find_path_fallback == true then + path = { destpos, pos } + self._path_used = false + --print("fallback path to "..minetest.pos_to_string(pos)) + elseif path then + --print("calculated path to "..minetest.pos_to_string(destpos).."for destination"..minetest.pos_to_string(pos)) + self._path_used = true + table.insert(path, pos) + end + return path +end + +function mvobj_proto:check_for_stuck() + +-- high difference stuck + if self.walk_param.teleport_on_stuck == true and self.target_pos then + local teleport_dest + -- Big jump / teleport up- or downsite + if math.abs(self.pos.x - self.target_pos.x) <= 1 and + math.abs(self.pos.z - self.target_pos.z) <= 1 and + vector.distance(self.pos, self.target_pos) > 3 then + teleport_dest = table.copy(self.target_pos) + teleport_dest.y = teleport_dest.y + 1.5 -- teleport over the destination + --print("big-jump teleport to "..minetest.pos_to_string(teleport_dest).." for target "..minetest.pos_to_string(self.target_pos)) + self:teleport(teleport_dest) + end + end + + -- stuck check by distance and speed + if (self._target_pos_bak and self.target_pos and self.speed > 0 and + self._path_used ~= true and self._last_distance and + self._target_pos_bak.x == self.target_pos.x and + self._target_pos_bak.y == self.target_pos.y and + self._target_pos_bak.z == self.target_pos.z and + self._last_distance -0.01 <= vector.distance(self.pos, self.target_pos)) or + ( self._walk_started ~= true and self.speed > 0 and + math.sqrt( math.pow(self.velocity.x,2) + math.pow(self.velocity.z,2)) < (self.speed/3)) then + --print("Stuck") + if self.walk_param.teleport_on_stuck == true then + local teleport_dest + if vector.distance(self.pos, self.target_pos) > 5 then + teleport_dest = vector.add(self.pos, vector.multiply(vector.direction(self.pos, self.target_pos), 5)) -- 5 nodes teleport step + else + teleport_dest = table.copy(self.target_pos) + teleport_dest.y = teleport_dest.y + 1.5 -- teleport over the destination + end + self:teleport(teleport_dest) + else + self:stay() + end + elseif self.target_pos then + self._last_distance = vector.distance(self.pos, self.target_pos) + end + self._walk_started = false +end + +--------------------------------------------------------------- +-- define framework functions internally used +--------------------------------------------------------------- +function functions.get_walkable_pos(pos, dist) + local destpos + local rpos = vector.round(pos) + for y = rpos.y+dist-1, rpos.y-dist-1, -1 do + for x = rpos.x-dist, rpos.x+dist do + for z = rpos.z-dist, rpos.z+dist do + local p = {x=x, y=y, z=z} + local node = minetest.get_node(p) + local nodedef = minetest.registered_nodes[node.name] + if not (node.name == "air" or nodedef and (nodedef.walkable == false or nodedef.drawtype == "airlike")) then + p.y = p.y +1 + local node = minetest.get_node(p) + local nodedef = minetest.registered_nodes[node.name] + if node.name == "air" or nodedef and (nodedef.walkable == false or nodedef.drawtype == "airlike") then + if destpos == nil or vector.distance(p, pos) < vector.distance(destpos, pos) then + destpos = p + end + end + end + end + end + end + return destpos +end + +--------------------------------------------------------------- +-- Return the framework to calling function +--------------------------------------------------------------- +return movement diff --git a/npcf/npcf.lua b/npcf/npcf.lua index 4c8829c..f80bc1c 100644 --- a/npcf/npcf.lua +++ b/npcf/npcf.lua @@ -73,8 +73,8 @@ npcf = { animation_state = 0, animation_speed = 30, }, - -- control functions - control_framework = dofile(NPCF_MODPATH.."/control.lua"), + -- movement control functions + movement = dofile(NPCF_MODPATH.."/movement.lua"), deepcopy = deepcopy } @@ -242,8 +242,8 @@ function npcf:register_npc(name, def) if type(def.on_step) == "function" then self.timer = self.timer + dtime def.on_step(self, dtime) - if self._control then - self._control:_do_control_step(dtime) + if self._mvobj then + self._mvobj:_do_movement_step(dtime) end end else diff --git a/npcf_guard/init.lua b/npcf_guard/init.lua index 400812f..46852d1 100644 --- a/npcf_guard/init.lua +++ b/npcf_guard/init.lua @@ -102,11 +102,11 @@ npcf:register_npc("npcf_guard:npc", { end, on_step = function(self, dtime) if self.timer > 1 then - local control = npcf.control_framework.getControl(self) - local pos = control.pos + local move_obj = npcf.movement.getControl(self) + local pos = move_obj.pos local target = {object=nil, distance=0} local min_dist = 1000 - control:mine_stop() + move_obj:mine_stop() for _,object in ipairs(minetest.get_objects_inside_radius(pos, TARGET_RADIUS)) do local to_target = false if object:is_player() then @@ -144,9 +144,9 @@ npcf:register_npc("npcf_guard:npc", { end if target.object then if target.distance < 3 then - control:mine() - control:stay() - control:look_to(target.object:getpos()) + move_obj:mine() + move_obj:stay() + move_obj:look_to(target.object:getpos()) local tool_caps = {full_punch_interval=1.0, damage_groups={fleshy=1}} local item = self.metadata.wielditem if item ~= "" and minetest.registered_items[item] then @@ -158,7 +158,7 @@ npcf:register_npc("npcf_guard:npc", { end if target.distance > 2 then local speed = get_speed(target.distance) * 1.1 - control:walk(target.object:getpos(), speed) + move_obj:walk(target.object:getpos(), speed) end elseif self.metadata.follow_owner == "true" then local player = minetest.get_player_by_name(self.owner) @@ -166,11 +166,11 @@ npcf:register_npc("npcf_guard:npc", { local p = player:getpos() local distance = vector.distance(pos, {x=p.x, y=pos.y, z=p.z}) if distance > 3 then - control:walk(p, get_speed(distance)) + move_obj:walk(p, get_speed(distance)) else - control:stay() + move_obj:stay() end - control:mine_stop() + move_obj:mine_stop() end elseif self.metadata.patrol == "true" then self.var.rest_timer = self.var.rest_timer + self.timer @@ -183,10 +183,9 @@ npcf:register_npc("npcf_guard:npc", { if patrol_pos then local distance = vector.distance(pos, patrol_pos) if distance > 1 then - control:walk(patrol_pos, PATROL_SPEED) + move_obj:walk(patrol_pos, PATROL_SPEED) else - self.object:setpos(patrol_pos) - control:stay() + move_obj:teleport(patrol_pos) self.metadata.patrol_index = index self.var.rest_timer = 0 end @@ -195,11 +194,11 @@ npcf:register_npc("npcf_guard:npc", { elseif vector.equals(pos, self.origin.pos) == false then local distance = vector.distance(pos, self.origin.pos) if distance > 1 then - control:walk(self.origin.pos, get_speed(distance)) + move_obj:walk(self.origin.pos, get_speed(distance)) else - self.object:setpos(self.origin.pos) - control.look_to(self.origin.pos) - control:stay() + move_obj:teleport(self.origin.pos) + move_obj:stay() + move_obj.yaw = self.origin.yaw end end self.timer = 0