123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532 |
- pm = pm or {}
- pm.modpath = minetest.get_modpath("pm")
- pm.sight_range = 30
- pm.nest_range = 15
- -- 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
- pm.run_velocity = 4.5
- pm.walk_velocity = 2
- -- Localize for performance.
- local vector_distance = vector.distance
- local vector_round = vector.round
- local math_floor = math.floor
- local math_max = math.max
- local math_random = math.random
- dofile(pm.modpath .. "/seek.lua")
- dofile(pm.modpath .. "/action.lua")
- dofile(pm.modpath .. "/spawner.lua")
- dofile(pm.modpath .. "/entity.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)
- --minetest.chat_send_player("MustTest", 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
- local overrides = {
- nest_guard = {
- -- Wisp is persistent and will not alter behavior.
- _no_autochose_behavior = true,
- _no_lifespan_limit = true,
- },
- nest_worker = {
- -- Wisp is persistent and will not alter behavior.
- _no_autochose_behavior = true,
- _no_lifespan_limit = true,
- },
- }
- -- Create entity at position, if possible.
- function pm.spawn_wisp(pos, behavior)
- pos = vector_round(pos)
- local node = minetest.get_node(pos)
- local pos_ok = false
- if node.name == "air" then
- pos_ok = true
- else
- local ndef = minetest.registered_nodes[node.name]
- if ndef and not ndef.walkable then
- pos_ok = true
- end
- end
- if pos_ok 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)
- -- Wisp has a chance to be completely silent.
- if math_random(1, 10) == 1 then
- luaent._no_sound = true
- end
- -- Apply overrides if wanted.
- if overrides[behavior] then
- local o = overrides[behavior]
- for k, v in pairs(o) do
- luaent[k] = v
- end
- end
- return ent, luaent
- else
- ent:remove()
- end
- end
- end
- end
- local behaviors = {
- "follower",
- "pest",
- "thief",
- "healer",
- "explorer",
- "boom", -- Never chosen by chance.
- "communal",
- "solitary",
- "guard",
- "nest_guard",
- "arsonist",
- "porter",
- "pusher",
- "nest_worker",
- }
- 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"
- elseif self._behavior == "nest_guard" then
- self._behavior = "guard"
- elseif self._behavior == "nest_worker" then
- self._behavior = "explorer"
- 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"
- elseif act == "nest_guard" then
- act = "guard"
- elseif act == "nest_worker" then
- act = "explorer"
- 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_not_wisp(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,
- -- Never chosen at random, can only be deliberately created.
- nest_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.nest_range then
- return origin, nil
- else
- -- Within sight range of spawn origin, seek target.
- return pm.seek_player(self, pos)
- end
- end
- end
- return nil, nil
- end,
- -- Never chosen at random, can only be deliberately created.
- nest_worker = 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_flora(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,
- pusher = 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,
- nest_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.nest_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,
- pusher = function(self, pos, target)
- pm.shove_player(self, 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)
- minetest.register_node("pm:spawner", {
- drawtype = "airlike",
- description = "Wisp Spawner (Please Report to Admin)",
- paramtype = "light",
- sunlight_propagates = true,
- walkable = false,
- pointable = false,
- groups = {immovable = 1},
- climbable = false,
- buildable_to = true,
- floodable = true,
- drop = "",
- on_construct = function(...)
- return pm.on_nodespawner_construct(...)
- end,
- on_destruct = function(...)
- return pm.on_nodespawner_destruct(...)
- end,
- on_timer = function(...)
- return pm.on_nodespawner_timer(...)
- end,
- on_finish_collapse = function(pos, node)
- minetest.remove_node(pos)
- end,
- on_collapse_to_entity = function(pos, node)
- -- Do nothing.
- end,
- })
- minetest.register_node("pm:quartz_ore", {
- description = "Quartz Crystals In Sand",
- tiles = {"default_desert_sand.png^quartz_ore.png"},
- groups = utility.dig_groups("mineral"),
- drop = 'quartz:quartz_crystal',
- sounds = default.node_sound_stone_defaults(),
- silverpick_drop = true,
- movement_speed_multiplier = default.SLOW_SPEED,
- after_destruct = function(pos, oldnode)
- if math_random(1, 500) == 1 then
- pm.spawn_random_wisp(pos)
- end
- end,
- })
- local c = "pm:core"
- local f = pm.modpath .. "/init.lua"
- reload.register_file(c, f, false)
- pm.registered = true
- end
|