123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904 |
- pm = pm or {}
- pm.modpath = minetest.get_modpath("pm")
- pm.sight_range = 30
- -- Pathfinder cooldown min/max time.
- pm.pf_cooldown_min = 5
- pm.pf_cooldown_max = 15
- -- Cooldown timer to acquire a new target.
- pm.aq_cooldown_min = 1
- pm.aq_cooldown_max = 10
- -- Range at which entity is considered to have found its target.
- pm.range = 2
- pm.velocity = 3
- dofile(pm.modpath .. "/seek.lua")
- dofile(pm.modpath .. "/action.lua")
- function pm.target_is_player_or_mob(target)
- if target:is_player() then
- return true
- end
- local ent = target:get_luaentity()
- if ent.mob then
- return true
- end
- end
- function pm.debug_chat(text)
- -- Comment or uncomment as needed for debugging.
- --minetest.chat_send_all(text)
- end
- function pm.debug_path(path)
- -- Ditto.
- --for k, v in ipairs(path) do pm.spawn_path_particle(v) end
- end
- function pm.debug_goal(pos)
- -- Ditto.
- --pm.spawn_path_particle(pos)
- --pm.death_particle_effect(pos)
- end
- function pm.death_particle_effect(pos)
- local particles = {
- amount = 100,
- time = 1.1,
- minpos = vector.add(pos, {x=-0.1, y=-0.1, z=-0.1}),
- maxpos = vector.add(pos, {x=0.1, y=0.1, z=0.1}),
- minvel = vector.new(-3.5, -3.5, -3.5),
- maxvel = vector.new(3.5, 3.5, 3.5),
- minacc = {x=0, y=0, z=0},
- maxacc = {x=0, y=0, z=0},
- minexptime = 1.5,
- maxexptime = 2.0,
- minsize = 0.5,
- maxsize = 1.0,
- collisiondetection = false,
- collision_removal = false,
- vertical = false,
- texture = "quartz_crystal_piece.png",
- glow = 14,
- --attached = entity,
- }
- minetest.add_particlespawner(particles)
- end
- -- Get objects inside radius, but remove self from the returned list.
- function pm.get_nearby_objects(self, pos, radius)
- local objects = minetest.get_objects_inside_radius(pos, radius)
- if self._identity then
- for i=1, #objects, 1 do
- local ent = objects[i]:get_luaentity()
- if ent and ent._identity then
- if ent._identity == self._identity then
- table.remove(objects, i)
- break
- end
- end
- end
- end
- return objects
- end
- function pm.spawn_path_particle(pos)
- local particles = {
- amount = 1,
- time = 0.1,
- minpos = pos,
- maxpos = pos,
- minvel = {x=0, y=0, z=0},
- maxvel = {x=0, y=0, z=0},
- minacc = {x=0, y=0, z=0},
- maxacc = {x=0, y=0, z=0},
- minexptime = 5,
- maxexptime = 5,
- minsize = 2.0,
- maxsize = 2.0,
- collisiondetection = false,
- collision_removal = false,
- vertical = false,
- texture = "default_mese_crystal.png",
- glow = 14,
- --attached = entity,
- }
- minetest.add_particlespawner(particles)
- end
- function pm.follower_spawn_particles(pos, entity)
- local particles = {
- amount = 10,
- time = 1,
- minpos = vector.add(pos, {x=-0.1, y=-0.1, z=-0.1}),
- maxpos = vector.add(pos, {x=0.1, y=0.1, z=0.1}),
- minvel = vector.new(-0.5, -0.5, -0.5),
- maxvel = vector.new(0.5, 0.5, 0.5),
- minacc = {x=0, y=0, z=0},
- maxacc = {x=0, y=0, z=0},
- minexptime = 0.5,
- maxexptime = 2.0,
- minsize = 0.5,
- maxsize = 1.0,
- collisiondetection = false,
- collision_removal = false,
- vertical = false,
- texture = "quartz_crystal_piece.png",
- glow = 14,
- --attached = entity,
- }
- minetest.add_particlespawner(particles)
- end
- function pm.follower_on_activate(self, staticdata, dtime_s)
- if staticdata and staticdata ~= "" then
- local data = minetest.deserialize(staticdata)
- if type(data) == "table" then
- for k, v in pairs(data) do
- pm.debug_chat("on_activate(): self["..k.."]="..tostring(v))
- self[k] = v
- end
- return
- end
- end
- -- Otherwise, set up default data.
- self._timer = self._timer or 0
- self._lifetime = self._lifetime or 60*60*24
- self._sound_time = self._sound_time or 0
- end
- function pm.follower_get_staticdata(self)
- local data = {}
- for k, v in pairs(self) do
- local t = type(v)
- if t == "number" or t == "string" or t == "boolean" then
- if k:find("_") == 1 then
- pm.debug_chat("get_staticdata(): data["..k.."]="..tostring(v))
- data[k] = v
- end
- end
- end
- return minetest.serialize(data) or ""
- end
- function pm.follower_on_step(self, dtime, moveresult)
- -- Remove object once we're old enough.
- if not self._lifetime then
- self.object:remove()
- return
- end
- self._lifetime = self._lifetime - dtime
- if self._lifetime < 0 then
- self.object:remove()
- return
- end
- -- Cooldown timer for the pathfinder, since using it is intensive.
- if self._failed_pathfind_cooldown then
- self._failed_pathfind_cooldown = self._failed_pathfind_cooldown - dtime
- if self._failed_pathfind_cooldown < 0 then
- self._failed_pathfind_cooldown = nil
- -- Also reset path/target info so we have a chance to acquire fresh data.
- self._goto = nil
- self._path = nil
- end
- end
- if self._acquire_target_cooldown then
- self._acquire_target_cooldown = self._acquire_target_cooldown - dtime
- if self._acquire_target_cooldown < 0 then
- self._acquire_target_cooldown = nil
- end
- end
- if self._wander_cooldown then
- self._wander_cooldown = self._wander_cooldown - dtime
- if self._wander_cooldown < 0 then
- self._wander_cooldown = nil
- end
- end
- -- Entity changes its behavior every so often.
- if not self._behavior_timer or self._behavior_timer < 0 then
- self._behavior_timer = math.random(1, 20)*60
- pm.choose_random_behavior(self)
- end
- self._behavior_timer = self._behavior_timer - dtime
- -- Entities sometimes get stuck against objects.
- -- Unstick them by rounding their positions to the nearest air node.
- if not self._unstick_timer or self._unstick_timer < 0 then
- self._unstick_timer = math.random(1, 30)
- local air = minetest.find_node_near(self.object:get_pos(), 1, "air", true)
- if air then
- self.object:set_pos(air)
- end
- end
- self._unstick_timer = self._unstick_timer - dtime
- -- Sound timer.
- if not self._sound_time then self._sound_time = 0 end
- self._sound_time = self._sound_time - dtime
- if self._sound_time < 0 then
- self._sound_time = math.random(100, 300)/100
- ambiance.sound_play("wisp", self.object:get_pos(), 0.2, 32)
- end
- -- If currently following a path, remove waypoints as we reach them.
- if self._path and #(self._path) > 0 then
- local p = self._path[1]
- -- Remove waypoint from path if we've reached it.
- if vector.distance(p, self.object:get_pos()) < 0.5 then
- pm.debug_chat('hit waypoint')
- self._stuck_timer = 2
- table.remove(self._path, 1)
- end
- -- Remove path when all waypoints exhausted.
- if #(self._path) < 1 then
- pm.debug_chat('finished following path: ' .. minetest.pos_to_string(self._goto))
- pm.debug_chat('node at terminus: ' .. minetest.get_node(self._goto).name)
- self._path = nil
- self._path_is_los = nil
- self._stuck_timer = nil
- self._failed_pathfind_cooldown = nil
- self.object:set_velocity({x=0, y=0, z=0})
- end
- if self._stuck_timer then
- self._stuck_timer = self._stuck_timer - dtime
- if self._stuck_timer < 0 then
- pm.debug_chat('got stuck following path')
- -- Got stuck trying to follow path.
- -- This is caused because the entity is physical and may collide with
- -- the environment. Blocks may have been added in the entity's path, or
- -- (more usually) the entity did not properly navigate a corner.
- -- We should seek a new target.
- self._goto = nil
- self._path = nil
- self._target = nil
- self._failed_pathfind_cooldown = math.random(pm.pf_cooldown_min, pm.pf_cooldown_max)
- self._stuck_timer = nil
- self._wander_cooldown = nil
- self.object:set_velocity({x=0, y=0, z=0})
- end
- end
- end
- -- Main logic timer.
- -- This controls how often the main "AI" logic runs.
- self._timer = self._timer + dtime
- if self._timer < 0.3 then return end
- self._timer = 0
- -- Spawn particles to indicate our location.
- local pos = self.object:get_pos()
- pm.follower_spawn_particles(pos, self.object)
- -- Find new target/goal-waypoint if we don't have one.
- if not self._acquire_target_cooldown and not self._failed_pathfind_cooldown then
- if not self._goto and not self._path then
- local tp, target = self._interest_point(self, pos)
- if target then
- -- Target is another entity.
- pm.debug_chat('acquired moving target')
- --if target:is_player() then pm.debug_chat('targeting player') end
- local s = tp
- if not s then s = target:get_pos() end
- if s then
- s = vector.round(s)
- if pm.target_is_player_or_mob(target) then
- s.y = s.y + 1 -- For players or mobs, seek above them, not at their feet.
- end
- s = minetest.find_node_near(s, 1, "air", true)
- -- Target must be standing in air.
- -- Otherwise it would never be reachable.
- if s then
- -- Don't reacquire target if we're already sitting on it.
- if vector.distance(pos, s) > pm.range then
- pm.debug_chat('set moving target goal')
- self._goto = vector.round(s)
- self._target = target -- Userdata object.
- end
- end
- end
- self._acquire_target_cooldown = math.random(pm.aq_cooldown_min, pm.aq_cooldown_max)
- elseif tp then
- -- Target is a static location.
- pm.debug_chat('acquired static target')
- -- Don't reacquire target if we're already sitting on it.
- if vector.distance(pos, tp) > pm.range then
- pm.debug_chat('set static target goal')
- self._goto = vector.round(tp)
- self._target = nil
- end
- self._acquire_target_cooldown = math.random(pm.aq_cooldown_min, pm.aq_cooldown_max)
- else
- -- No target acquired. Wait awhile before calling function again.
- pm.debug_chat('no target acquired')
- self._acquire_target_cooldown = math.random(pm.aq_cooldown_min, pm.aq_cooldown_max)
- end
- end
- end
- -- Get a path to our target if we don't have a path yet, and target is not nearby.
- if not self._failed_pathfind_cooldown then
- if self._goto and not self._path and vector.distance(self._goto, pos) > pm.range then
- pm.debug_chat('want path to target')
- local los, obstruction = minetest.line_of_sight(vector.round(pos), vector.round(self._goto))
- if los then
- -- We have LOS (line of sight) direct to target.
- pm.debug_chat('LOS confirmed')
- local dir = vector.subtract(vector.round(self._goto), vector.round(pos))
- local dst = vector.length(dir)
- dir = vector.normalize(dir) -- Returns 0,0,0 for zero-length vector.
- -- Assemble a straight-line path.
- local path = {}
- for i=1, dst, 1 do
- path[#path+1] = vector.add(pos, vector.multiply(dir, i))
- end
- if #path > 0 then
- self._path = path
- self._path_is_los = true
- self._stuck_timer = nil
- end
- else
- -- No line of sight to target. Use pathfinder!
- pm.debug_chat('will try pathfinder')
- local rp1 = vector.round(pos)
- local rp2 = vector.round(self._goto)
- local a1 = rp1
- local a2 = rp2
- local d1 = minetest.registered_nodes[minetest.get_node(rp1).name]
- local d2 = minetest.registered_nodes[minetest.get_node(rp2).name]
- -- If either start or end are non-walkable, we don't need to look for air.
- if d1.walkable then
- a1 = minetest.find_node_near(rp1, 2, "air", true)
- end
- if d2.walkable then
- a2 = minetest.find_node_near(rp2, 2, "air", true)
- end
- if a1 and a2 then
- pm.debug_chat('start and end position are both in air')
- local prepath = {table.copy(a1)}
- local postpath = {table.copy(a2)}
- -- Find air directly above ground for the wisp's start position.
- -- This is necessary because the wisp usually flies a little bit above
- -- the ground. Pathfinding will fail if we don't start at ground level.
- while minetest.get_node(vector.add(a1, {x=0, y=-1, z=0})).name == "air" do
- a1.y = a1.y - 1
- table.insert(prepath, table.copy(a1))
- end
- -- Find air directly above ground for the target position.
- local target_y = a2.y
- while minetest.get_node(vector.add(a2, {x=0, y=-1, z=0})).name == "air" do
- a2.y = a2.y - 1
- table.insert(postpath, 1, table.copy(a2))
- end
- -- If this triggers then the target is flying, or hanging over a high ledge.
- if (target_y - a2.y) > 2 then
- end
- -- The shorter the apparent distance between these 2 points, the farther
- -- we can afford to look around.
- local d = vector.distance(a1, a2)
- local r = math.max(1, math.floor(pm.sight_range - d))
- pm.debug_chat("trying to find path")
- self._path = minetest.find_path(a1, a2, r, 1, 1, "A*_noprefetch")
- if not self._path then
- pm.debug_chat('no path found')
- -- If we couldn't find a path to this location, we should remove this
- -- goal. Also set the pathfinder cooldown timer.
- self._goto = nil
- self._failed_pathfind_cooldown = math.random(pm.pf_cooldown_min, pm.pf_cooldown_max)
- else
- if #(self._path) >= 1 then
- pm.debug_chat("got path")
- self._stuck_timer = nil
- pm.debug_chat('welding pre and post paths')
- local path = {}
- for i=1, #prepath, 1 do
- path[#path+1] = prepath[i]
- end
- for i=1, #(self._path), 1 do
- path[#path+1] = self._path[i]
- end
- for i=1, #postpath, 1 do
- path[#path+1] = postpath[i]
- end
- self._path = path
- self._stuck_timer = nil
- -- Debug render path.
- pm.debug_path(self._path)
- -- If start and end points are equal, toss this path out.
- -- Also set the pathfinder cooldown timer.
- if vector.equals(self._path[1], self._path[#(self._path)]) then
- pm.debug_chat('tossing path because start and end are equal')
- self._path = nil
- self._goto = nil
- self._failed_pathfind_cooldown = math.random(pm.pf_cooldown_min, pm.pf_cooldown_max)
- end
- -- If path's start position is too far away, we can't use the path.
- if self._path then
- if vector.distance(self._path[1], pos) > pm.range then
- pm.debug_chat('tossing path because start is too far away')
- self._path = nil
- self._goto = nil
- self._failed_pathfind_cooldown = math.random(pm.pf_cooldown_min, pm.pf_cooldown_max)
- end
- end
- else
- -- Not a real path!
- -- Must have at least one position.
- pm.debug_chat('tossing path because it is bogus')
- self._goto = nil
- self._path = nil
- self._failed_pathfind_cooldown = math.random(pm.pf_cooldown_min, pm.pf_cooldown_max)
- end
- end
- else
- -- One or both positions not accessible (no nearby air).
- -- Thus we must give up this target.
- self._goto = nil
- self._path = nil
- self._failed_pathfind_cooldown = math.random(pm.pf_cooldown_min, pm.pf_cooldown_max)
- end
- end
- end
- end
- -- Follow current path.
- if self._path and #(self._path) > 0 then
- -- For paths of longer than trivial length, try to optimize with LOS.
- -- We can do this because this mob can fly over gaps and such. This also
- -- makes the movement look better.
- if #(self._path) > 5 then
- -- Don't do LOS optimization if the current waypoint is already so marked.
- local p = self._path[2]
- if not p.los then
- while #(self._path) > 1 and minetest.line_of_sight(pos, p) do
- table.remove(self._path, 1)
- p = self._path[2]
- end
- local dir = vector.subtract(self._path[1], pos)
- local dst = vector.length(dir)
- dir = vector.normalize(dir) -- Returns 0,0,0 for zero-length vector.
- -- Assemble a straight-line path.
- local path = {}
- for i=1, dst, 1 do
- path[#path+1] = vector.add(pos, vector.multiply(dir, i))
- path[#path].los = true -- Mark waypoint as a LOS point.
- end
- -- Append the remainder of the real path.
- for i=1, #(self._path), 1 do
- path[#path+1] = self._path[i]
- end
- -- Set new path.
- self._path = path
- self._stuck_timer = nil
- -- Debug render path.
- pm.debug_path(self._path)
- end
- end
- pm.debug_chat('following path')
- local waypoint = self._path[1]
- local waynode = minetest.get_node(waypoint)
- -- Check if path runs through an obstruction.
- -- Nodes must be 'air' or non-walkable (like plants).
- local obstructed = false
- if waynode.name ~= "air" then
- local ndef = minetest.registered_nodes[waynode.name]
- if not ndef or ndef.walkable then
- obstructed = true
- end
- end
- if not obstructed then
- if self._path_is_los or waypoint.los then
- -- Follow line-of-sight paths directly.
- --self.object:move_to(waypoint, true)
- --table.remove(self._path, 1)
- -- Smooth movement.
- local dir = vector.subtract(waypoint, pos)
- if vector.length(dir) > 0.4 then
- dir = vector.normalize(dir)
- dir = vector.multiply(dir, pm.velocity)
- self.object:set_velocity(dir)
- end
- else
- -- Cause entity to float 1.5 meters above ground when following path,
- -- if there's enough head room. But not for the last waypoint in the path.
- waypoint.y = waypoint.y + 1
- local n = minetest.get_node(waypoint)
- if n.name == "air" and #(self._path) > 1 then
- waypoint.y = waypoint.y - 0.5
- --self.object:move_to(waypoint, true)
- --table.remove(self._path, 1)
- -- Smooth movement.
- local dir = vector.subtract(waypoint, pos)
- if vector.length(dir) > 0.4 then
- dir = vector.normalize(dir)
- dir = vector.multiply(dir, pm.velocity)
- self.object:set_velocity(dir)
- end
- else
- waypoint.y = waypoint.y - 1
- --self.object:move_to(waypoint, true)
- --table.remove(self._path, 1)
- -- Smooth movement.
- local dir = vector.subtract(waypoint, pos)
- if vector.length(dir) > 0.4 then
- dir = vector.normalize(dir)
- dir = vector.multiply(dir, pm.velocity)
- self.object:set_velocity(dir)
- end
- end
- end
- else
- -- Path obstructed. Need new path, this one is bad.
- pm.debug_chat('path obstructed: ' .. waynode.name)
- self._path = nil
- self._path_is_los = nil
- self._goto = nil
- self.object:set_velocity({x=0, y=0, z=0})
- end
- end
- -- Dynamic targets can move while we're trying to path to them.
- -- Update path as long as we have LOS to the target.
- if self._target and self._path and self._goto then
- local target_pos = self._target:get_pos()
- if target_pos then
- if #(self._path) > 0 then
- local end_path = self._path[#(self._path)]
- if vector.distance(target_pos, end_path) > 3 then
- local los, obstruction = minetest.line_of_sight(vector.round(pos), vector.round(target_pos))
- if los then
- pm.debug_chat('target moved, repathing via LOS')
- self._goto = vector.round(target_pos)
- local dir = vector.subtract(self._goto, vector.round(pos))
- local dst = vector.length(dir)
- dir = vector.normalize(dir) -- Returns 0,0,0 for zero-length vector.
- -- Assemble a straight-line path.
- local path = {}
- for i=1, dst, 1 do
- path[#path+1] = vector.add(pos, vector.multiply(dir, i))
- end
- if #path > 0 then
- self._path = path
- self._path_is_los = true
- self._stuck_timer = nil
- end
- end
- end
- end
- end
- end
- -- Remove target waypoint once we're close enough to it.
- -- Only if done following path.
- if self._goto and not self._path then
- pm.debug_chat('distance to goal: ' .. vector.distance(self._goto, pos))
- pm.debug_goal(self._goto)
- if vector.distance(self._goto, pos) < pm.range then
- pm.debug_chat('reached goal')
- --self.object:move_to(self._goto, true)
- -- Have we arrived at the target (if we did indeed have a target)?
- if self._target then
- local s = self._target:get_pos()
- if s then
- s = vector.round(s)
- s.y = s.y + 1 -- For entities, we seek above them, not at their feet.
- if vector.distance(pos, s) < pm.range then
- pm.debug_chat('reached dynamic target')
- -- We have reached our moveable target.
- -- We can clear this and set a timer to delay acquiring the next target.
- self._on_arrival(self, self._goto, self._target)
- self._goto = nil
- self._target = nil
- self._acquire_target_cooldown = math.random(pm.aq_cooldown_min, pm.aq_cooldown_max)
- else
- -- Our moveable target has moved. We must move toward it again.
- -- Do so right away, without delay.
- pm.debug_chat('target has moved, reacquiring')
- self._goto = s
- self._acquire_target_cooldown = nil
- self._failed_pathfind_cooldown = nil
- end
- else
- -- Moving target no longer available. We must clear this.
- self._target = nil
- self._goto = nil
- end
- else
- pm.debug_chat('reached static target')
- self._on_arrival(self, self._goto, nil)
- -- No moving target, so we can clear this.
- self._goto = nil
- self._acquire_target_cooldown = math.random(pm.aq_cooldown_min, pm.aq_cooldown_max)
- end
- end
- end
- -- Drift behavior, as long as we don't have a target to go to.
- if not self._wander_cooldown then
- if not self._goto then
- local dir = {
- x = math.random(-1, 1)/10,
- y = math.random(-1, 1)/10,
- z = math.random(-1, 1)/10
- }
- self.object:set_velocity(dir)
- self._wander_cooldown = math.random(1, 5)
- end
- end
- end
- function pm.follower_on_punch(self, puncher, time_from_last_punch, tool_capabilities, dir)
- local pos = self.object:get_pos()
- pm.death_particle_effect(pos)
- minetest.add_item(pos, "glowstone:glowing_dust " .. math.random(1, 3))
- self.object:remove()
- end
- -- Create entity at position, if possible.
- function pm.spawn_wisp(pos, behavior)
- pos = vector.round(pos)
- local node = minetest.get_node(pos)
- if node.name == "air" then
- local ent = minetest.add_entity(pos, "pm:follower")
- if ent then
- local luaent = ent:get_luaentity()
- if luaent then
- luaent._behavior = behavior
- luaent._behavior_timer = math.random(1, 20)*60
- -- Allows to uniquely identify the wisp to other wisps, with little chance of collision.
- -- In particular this allows the wisp to ignore itself in any object queries.
- luaent._identity = math.random(1, 32000)
- -- This is so the wisp knows where it spawned at.
- -- We format it as a string so that this data is saved statically.
- luaent._spawn_origin = minetest.pos_to_string(pos)
- return ent
- else
- ent:remove()
- end
- end
- end
- end
- local behaviors = {
- "follower",
- "pest",
- "thief",
- "healer",
- "explorer",
- "boom",
- "communal",
- "solitary",
- "guard",
- "arsonist",
- "porter",
- }
- function pm.choose_random_behavior(self)
- self._behavior = behaviors[math.random(1, #behaviors)]
- -- Don't chose a self-destructive behavior by chance.
- if self._behavior == "boom" then
- self._behavior = "follower"
- end
- end
- -- Create entity at position, if possible.
- function pm.spawn_random_wisp(pos)
- local act = behaviors[math.random(1, #behaviors)]
- if act == "boom" then
- act = "follower"
- end
- return pm.spawn_wisp(pos, act)
- end
- -- Table of functions for obtaining interest points.
- local interests = {
- follower = function(self, pos)
- return pm.seek_player_or_mob_or_item(self, pos)
- end,
- thief = function(self, pos)
- return pm.seek_player_or_item(self, pos)
- end,
- pest = function(self, pos)
- return pm.seek_player(self, pos)
- end,
- healer = function(self, pos)
- return pm.seek_player(self, pos)
- end,
- explorer = function(self, pos)
- return pm.seek_node_with_meta(self, pos)
- end,
- -- Suicide, never chosen at random.
- boom = function(self, pos)
- return pm.seek_player_or_mob(self, pos)
- end,
- communal = function(self, pos)
- return pm.seek_wisp(self, pos)
- end,
- solitary = function(self, pos)
- return pm.seek_solitude(self, pos)
- end,
- guard = function(self, pos)
- -- Seek target in sight-range of spawn origin, otherwise return to origin.
- if self._spawn_origin then
- local origin = minetest.string_to_pos(self._spawn_origin)
- if origin then
- if vector.distance(origin, self.object:get_pos()) > pm.sight_range then
- return origin, nil
- else
- -- Within sight range of spawn origin, seek target.
- return pm.seek_player_or_mob_not_wisp(self, pos)
- end
- end
- end
- return nil, nil
- end,
- arsonist = function(self, pos)
- local target = pm.seek_flammable_node(self, pos)
- if not target then
- return pm.seek_player_or_mob(self, pos)
- end
- return target, nil
- end,
- porter = function(self, pos)
- return pm.seek_player(self, pos)
- end,
- }
- -- Table of possible action functions to take on arriving at a target.
- local actions = {
- pest = function(self, pos, target)
- pm.hurt_nearby_players(self)
- end,
- healer = function(self, pos, target)
- pm.heal_nearby_players(self)
- end,
- thief = function(self, pos, target)
- pm.steal_nearby_item(self, target)
- end,
- boom = function(self, pos, target)
- pm.explode_nearby_target(self, target)
- end,
- guard = function(self, pos, target)
- -- Attack target, but only if in sight-range of spawn origin.
- if self._spawn_origin then
- local origin = minetest.string_to_pos(self._spawn_origin)
- if origin then
- if vector.distance(origin, self.object:get_pos()) < pm.sight_range then
- pm.hurt_nearby_player_or_mob_not_wisp(self)
- end
- end
- end
- end,
- arsonist = function(self, pos, target)
- pm.commit_arson_at_target(pos)
- end,
- porter = function(self, pos, target)
- pm.teleport_player_to_prior_location(target)
- end,
- }
- function pm.interest_point(self, pos)
- if self._behavior then
- if interests[self._behavior] then
- return interests[self._behavior](self, pos)
- end
- end
- return nil, nil
- end
- function pm.on_arrival(self, pos, other)
- pm.debug_chat('arrived at target')
- if self._behavior then
- pm.debug_chat('have behavior: ' .. self._behavior)
- if actions[self._behavior] then
- actions[self._behavior](self, pos, other)
- end
- end
- end
- if not pm.registered then
- local entity = {
- initial_properties = {
- visual = "cube",
- textures = {
- "quartz_crystal_piece.png",
- "quartz_crystal_piece.png",
- "quartz_crystal_piece.png",
- "quartz_crystal_piece.png",
- "quartz_crystal_piece.png",
- "quartz_crystal_piece.png",
- },
- visual_size = {x=0.2, y=0.2, z=0.2},
- collide_with_objects = false,
- pointable = false,
- is_visible = true,
- makes_footstep_sound = false,
- glow = 14,
- automatic_rotate = 0.5,
- -- This is so that wandering/drifting wisps don't bury themselves.
- physical = true,
- collisionbox = {-0.2, -0.2, -0.2, 0.2, 0.2, 0.2},
- },
- -- So other game code can tell what this entity is.
- _name = "pm:follower",
- description = "Seon",
- mob = true,
- on_step = function(...) return pm.follower_on_step(...) end,
- on_punch = function(...) return pm.follower_on_punch(...) end,
- on_activate = function(...) return pm.follower_on_activate(...) end,
- get_staticdata = function(...) return pm.follower_get_staticdata(...) end,
- _interest_point = function(...) return pm.interest_point(...) end,
- _on_arrival = function(...) return pm.on_arrival(...) end,
- }
- minetest.register_entity("pm:follower", entity)
- local c = "pm:core"
- local f = pm.modpath .. "/init.lua"
- reload.register_file(c, f, false)
- pm.registered = true
- end
|