123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788 |
- local get_relevant_players = limbo_respawning.get_living_players
- local vector_mul = vector.multiply
- local vector_add = vector.add
- local vector_dist = vector.distance
- local raycast = minetest.raycast
- local get_node = minetest.get_node
- local mobs = defense_mob_api
- mobs.gravity = -9.81
- mobs.default_prototype = {
- -- minetest properties & defaults
- physical = true,
- collide_with_objects = true,
- makes_footstep_sound = true,
- visual = "mesh",
- automatic_face_movement_dir = true,
- stepheight = 0.6,
- -- custom properties
- id = 0, -- Automatically set
- smart_path = true, -- Use the pathfinder
- --organize in a swarm
- swarm = false,
- --for switching pathfinding on and off depending on line of line_of_sight
- --if negative (default) it's always on, if >= 0 it'll switch
- smart_path_for = -1,
- mass = 1,
- movement = "ground", -- "ground"/"air"
- move_speed = 1,
- jump_height = 1,
- armor = 0,
- attack_range = 1,
- attack_damage = 1,
- attack_interval = 1,
- current_animation = nil,
- current_animation_end = 0,
- destination = nil, -- position
- last_attack_time = 0,
- life_timer = 75,
- pause_timer = 0,
- timer = 0,
- media_prefix = "modname_mobname_", --set properly in register_mob
- idle_sound_cd = 5,
- time_since_last_idle_sound = 0,
- view_range = 20,
- -- cache
- cache_is_standing = nil,
- cache_find_nearest_player = nil,
- }
- local reg_nodes = minetest.registered_nodes
- local function vec_zero() return {x=0, y=0, z=0} end
- do --look
- function mobs.default_prototype:look(dir)
- local pos = self.object:get_pos()
- local dest = vector_add(pos, vector_mul(dir, self.view_range))
- local ray = raycast(pos, dest, true, true)
- local b = true
- while b
- do
- b = false
- local pt = ray:next()
- if pt
- then
- if pt.type == node
- then
- local node = get_node(pt.under)
- if node and node.name ~= "ignore"
- then
- if not(reg_nodes[node.name] or {walkable = true}).walkable
- then
- b = true
- end
- end
- if not b
- then
- return false, pt.under
- end
- end
- --[[elseif pt.type == object and
- vector_dist(pt.intersection_point, pos) > 2
- then
- return false
- end]]
- end
- end
- return true
- end
- end
- function mobs.default_prototype:make_sound(event_name, detached, gain, pitch, max_hear_dist)
- local def =
- {
- object = self.object,
- gain = gain or 0.5 + math.random() * 0.5,
- pitch = pitch or 1 + (math.random() - 0.5) * 0.5,
- max_hear_distance = max_hear_dist,
- }
- if detached then def.object = nil end
- return minetest.sound_play(self.media_prefix .. event_name, def)
- end
- --on_activate
- do--by using local scopes we can re-use the names for observer storage tables
- local observers, ocount = {}, 0
- --for registering functions as callback
- mobs.register_on_default_activate = function (func)
- ocount = ocount + 1
- observers[ocount] = func
- end
- function mobs.default_prototype:on_activate(staticdata)
- for i = 1, ocount
- do
- observers[i](self, staticdata)
- end
- self.object:set_armor_groups({fleshy = 100 - self.armor})
- if self.movement ~= "air" then
- self.object:setacceleration({x=0, y=mobs.gravity, z=0})
- end
- self.time_since_last_idle_sound = math.random() * self.idle_sound_cd
- self.id = math.random(0, 100000)
- end
- end
- do --safe remove
- local observers, ocount = {}, 0
- --for registering functions as callback
- mobs.register_on_default_remove = function(func)
- ocount = ocount + 1
- observers[ocount] = func
- end
- function mobs.default_prototype:remove()
- for i = 1, ocount
- do
- observers[i](self)
- end
- self.removed = true
- self.object:remove()
- end
- end
- --on_step
- do
- local observers, ocount = {}, 0
- --for registering functions as callback
- mobs.register_on_default_step = function(func)
- ocount = ocount + 1
- observers[ocount] = func
- end
- function mobs.default_prototype:on_step(dtime)
- local ceas = os.clock()
- for i = 1, ocount
- do
- observers[i](self, dtime)
- end
- self.cache_is_standing = nil
- self.cache_find_nearest_player = nil
- self.time_since_last_idle_sound = self.time_since_last_idle_sound + dtime
- if self.idle_sound_cd < self.time_since_last_idle_sound and
- math.random() < 0.3
- then
- self:make_sound("idle")
- self.time_since_last_idle_sound = 0
- end
- if self.pause_timer <= 0 then
- if self.destination then
- self:move(dtime, self.destination)
- if vector.distance(self.object:get_pos(), self.destination) < 0.5 then
- self.destination = nil
- end
- else
- self:move(dtime, self.object:get_pos())
- end
- else
- self.pause_timer = self.pause_timer - dtime
- end
- if self.movement ~= "air" and not self:is_standing() then
- self:set_animation("fall", {"jump", "attack", "move_attack"})
- end
- -- Remove when far enough and may not reach the player at all
- local nearest = self:find_target()
- if self.life_timer <= 0 then
- if nearest.distance > 12 then
- self:remove()
- end
- else
- self.life_timer = self.life_timer - dtime
- end
- -- Disable collision when far enough
- if self.collide_with_objects then
- if nearest.distance > 6 then
- self.collide_with_objects = false
- self.object:set_properties({collide_with_objects = self.collide_with_objects})
- end
- else
- if nearest.distance < 1.5 then
- self.collide_with_objects = true
- self.object:set_properties({collide_with_objects = self.collide_with_objects})
- end
- end
- self.timer = self.timer + dtime
- defense_mob_api:track_time("prototype on_step", os.clock() - ceas)
- end
- end
- --on_punch
- do
- local observers, ocount = {}, 0
- --for registering functions as callback
- mobs.register_on_default_punch = function(func)
- ocount = ocount + 1
- observers[ocount] = func
- end
- function mobs.default_prototype:on_punch(puncher, time_from_last_punch, tool_capabilities, dir, damage)
- for i = 1, ocount
- do
- observers[i](self, puncher, time_from_last_punch, tool_capabilities, dir, damage)
- end
- -- Weapon wear code adapted from TenPlus1's mobs redo (https://github.com/tenplus1/mobs)
- if puncher
- then
- local weapon = puncher:get_wielded_item()
- if tool_capabilities
- then
- local wear = (0.01) * (self.armor / 100) * 65534 + 1
- weapon:add_wear(wear)
- puncher:set_wielded_item(weapon)
- end
- end
- self:make_sound("hurt")
- dir.y = dir.y + 1
- local m = self.mass or 1
- local knockback = vector.multiply(vector.normalize(dir), 10 / (1 + m))
- self.object:setvelocity(vector.add(self.object:getvelocity(), knockback))
- self.pause_timer = 0.3
- if self.object:get_hp() - damage <= 0
- then
- self:die(puncher)
- end
- end
- end
- do --get_staticdata
- local observers, ocount = {}, 0
- --for registering functions as callback
- mobs.register_on_default_get_staticdata = function(func)
- ocount = ocount + 1
- observers[ocount] = func
- end
- function mobs.default_prototype:get_staticdata()
- local data = {}
- for i = 1, ocount
- do
- local key, val = observers[i](self)
- data[key] = val
- end
- return minetest.serialize(data)
- end
- end
- function mobs.default_prototype:damage(amount)
- local hp = self.object:get_hp()
- if hp <= amount then
- self:die()
- else
- self.object:set_hp(hp - amount)
- end
- end
- do --attack
- local observers, ocount = {}, 0
- --for registering functions as callback
- mobs.register_on_default_attack = function(func)
- ocount = ocount + 1
- observers[ocount] = func
- end
- function mobs.default_prototype:attack(obj, dir)
- for i = 1, ocount
- do
- observers[i](self, obj, dir)
- end
- self:make_sound("attack")
- obj:punch(self.object, self.timer - self.last_attack_time, {
- full_punch_interval=self.attack_interval,
- damage_groups = {fleshy=self.attack_damage}
- }, dir)
- end
- end
- function mobs.default_prototype:move(dtime, destination)
- mobs.move_method[self.movement](self, dtime, destination)
- end
- -- Attack the player
- function mobs.default_prototype:hunt()
- local nearest = self:find_target()
- if nearest.player then
- if nearest.distance <= self.attack_range then
- self:do_attack(nearest.player)
- end
- if nearest.distance > self.attack_range or nearest.distance < self.attack_range/2-1 then
- local pos = self.object:get_pos()
- local smart_dest = nil
- if self.smart_path then --Do pathfinding
- if self.smart_path_for > 0
- then
- self.smart_path_for = self.smart_path_for - 1
- end
- --add a little smarts if needed
- if self.smart_path_for >= 0 --this way it stays on if it's negative
- then
- local eyepos = {x = pos.x, y = pos.y + 0.5, z = pos.z}
- local los, obstacle = minetest.line_of_sight(nearest.position, eyepos)
- if obstacle and vector.distance(eyepos, obstacle) < 10
- then
- self.smart_path_for = self.smart_path_for + 2
- end
- end
- if smart_path_for ~= 0
- then
- smart_dest = defense_mob_api.pathfinder:get_waypoint(self.name, pos.x, pos.y, pos.z)
- end
- end
- if smart_dest then
- self.destination = smart_dest
- else
- local r = math.max(0, self.attack_range - 2)
- local dir = vector.aim(nearest.position, pos)
- self.destination = vector.add(nearest.position, vector.multiply(dir, r))
- end
- end
- end
- end
- function mobs.default_prototype:do_attack(obj)
- if self.last_attack_time + self.attack_interval < self.timer then
- local dir = vector.aim(self.object:get_pos(), obj:get_pos())
- self:attack(obj, dir)
- self.last_attack_time = self.timer
- if self.current_animation == "move" then
- self:set_animation("move_attack")
- else
- self:set_animation("attack")
- end
- end
- self.life_timer = math.min(300, self.life_timer + 60)
- end
- function mobs.default_prototype:jump(direction)
- if self:is_standing() then
- if direction then
- direction.y = 0.1
- direction = vector.normalize(direction)
- else
- direction = vec_zero()
- end
- local v = self.object:getvelocity()
- v.y = math.sqrt(2 * -mobs.gravity * (self.jump_height + 0.2))
- v.x = direction.x * self.jump_height
- v.z = direction.z * self.jump_height
- self.object:setvelocity(vector.add(self.object:getvelocity(), v))
- self:set_animation("jump")
- end
- end
- do --die
- local observers, ocount = {}, 0
- --for registering functions as callback
- mobs.register_on_default_die = function(func)
- ocount = ocount + 1
- observers[ocount] = func
- end
- function mobs.default_prototype:die(killer)
- for i = 1, ocount
- do
- observers[i](self, killer)
- end
- self.dead = true
- self:make_sound("die", true)
- self:on_death(killer)
- self:remove()
- end
- end
- function mobs.default_prototype:on_death(killer)
- end
- function mobs.default_prototype:is_standing()
- if self.cache_is_standing ~= nil then
- return self.cache_is_standing
- end
- if self.movement == "air" then
- self.cache_is_standing = false
- return false
- end
- if self.object:getvelocity().y ~= 0 then
- self.cache_is_standing = false
- return false
- end
- -- Check the four bottom corners for collision
- local p = self.object:get_pos()
- p.y = p.y + self.collisionbox[2] - 0.25
- local corners = {
- vector.add(p, {x=self.collisionbox[1], y=0, z=self.collisionbox[3]}),
- vector.add(p, {x=self.collisionbox[1], y=0, z=self.collisionbox[6]}),
- vector.add(p, {x=self.collisionbox[4], y=0, z=self.collisionbox[3]}),
- vector.add(p, {x=self.collisionbox[4], y=0, z=self.collisionbox[6]}),
- }
- for _,c in ipairs(corners) do
- local node = minetest.get_node_or_nil(c)
- if not node or
- (reg_nodes[node.name] or {walkable = true}).walkable
- then
- self.cache_is_standing = true
- return true
- end
- end
- self.cache_is_standing = false
- return false
- end
- function mobs.default_prototype:set_animation(name, inhibit)
- if self.current_animation == name then
- return
- end
- if inhibit then
- for _,p in ipairs(inhibit) do
- if self.current_animation == p and self.timer < self.current_animation_end then
- return
- end
- end
- end
- local anim_prop = self.animation[name]
- if anim_prop then
- self.current_animation = name
- self.current_animation_end = self.timer + (anim_prop.b - anim_prop.a - 1) / anim_prop.rate
- self.object:set_animation({x=anim_prop.a, y=anim_prop.b}, anim_prop.rate, 0)
- end
- end
- function mobs.default_prototype:find_target()
- if self.cache_find_nearest_player ~= nil then
- return self.cache_find_nearest_player
- end
- local p = self.object:get_pos()
- local nearest_player = nil
- local nearest_pos = p
- local nearest_dist = math.huge
- for _,obj in ipairs(get_relevant_players()) do --TODO change this
- local pos = obj:get_pos()
- pos.y = pos.y + 1
- local d = vector.distance(pos, p)
- if d < nearest_dist then
- nearest_player = obj
- nearest_pos = pos
- nearest_dist = d
- end
- end
- local ret = {player=nearest_player or self.object, position=nearest_pos, distance=nearest_dist}
- self.cache_find_nearest_player = ret
- return ret
- end
- -- Movement implementations for the default movement types
- mobs.move_method = {}
- function mobs.move_method:air(dtime, destination)
- local delta = vector.subtract(destination, self.object:get_pos())
- local dist = vector.length(delta)
- if dist ~= dist --nan check
- then
- return
- end
- -- Add random variation
- if dist > 3 then
- local r_angle = (self.id/100000) * 2 * math.pi
- local r_radius = (self.id/100000) * (dist - 3)/3
- delta = vector.add(delta, {
- x=math.cos(r_angle)*r_radius,
- y=r_radius,
- z=math.sin(r_angle)*r_radius
- })
- end
- -- Compute speed and smoothing factor
- local speed = self.move_speed * math.max(0, math.min(1, 1.2 * dist))
- local t
- local v = self.object:getvelocity()
- if vector.length(v) < self.move_speed * 1.5 then
- t = math.pow(0.1, dtime)
- else
- t = math.pow(0.4, dtime)
- speed = speed * 0.9
- end
- --[[Note:
- in lua
- x = a and b or c
- means
- if a then x = b else x = c
- ]]
- -- Compute and set resulting velocity
- self.object:setvelocity(vector.add(
- vector.multiply(v, t),
- vector.multiply(dist > 0 and vector.normalize(delta) or vec_zero(), speed * (1-t))
- ))
- if speed > self.move_speed * 0.04 then
- self:set_animation("move", {"attack", "move_attack"})
- else
- self:set_animation("idle", {"attack", "move_attack"})
- end
- end
- function mobs.move_method:ground(dtime, destination)
- local delta = vector.subtract(destination, self.object:get_pos())
- delta.y = 0
- local dist = vector.length(delta)
- if dist ~= dist --nan check
- then
- return
- end
- -- Add random variation
- if dist > 4 then
- local r_angle = (self.id/100000) * 2 * math.pi
- local r_radius = (dist - 4)/4
- delta = vector.add(delta, {
- x=math.cos(r_angle)*r_radius,
- y=0,
- z=math.sin(r_angle)*r_radius
- })
- end
- -- Compute speed and smoothing factor
- local speed = self.move_speed * math.max(0, math.min(1, 1.2 * dist))
- local t
- local v = self.object:getvelocity()
- if self:is_standing() and vector.length(v) < self.move_speed * 4 then
- t = math.pow(0.001, dtime)
- else
- t = math.pow(0.4, dtime)
- speed = speed * 0.9
- end
- -- Compute and set resulting velocity
- local dir = dist > 0 and vector.normalize(delta) or vec_zero()
- local v2 = vector.add(
- vector.multiply(v, t),
- vector.multiply(dir, speed * (1-t))
- )
- v2.y = v.y
- self.object:setvelocity(v2)
- local jump = nil
- local pos = self.object:get_pos()
- -- Check for jump
- if dist > 1 then
- --Jump over obstacles
- local p = vector.new(pos)
- p.y = p.y + self.collisionbox[2] + 0.5
- local sx = self.collisionbox[4] - self.collisionbox[1]
- local sz = self.collisionbox[6] - self.collisionbox[3]
- local r = math.sqrt(sx*sx + sz*sz)/2 + 0.5
- local fronts = {} --might be worth caching those
- do
- local xedge, zedge
- local offset = 0.2
- if math.sign(delta.x) < 0
- then
- xedge = self.collisionbox[1] - offset
- else
- xedge = self.collisionbox[4] + offset
- end
- if math.sign(delta.z) < 0
- then
- zedge = self.collisionbox[3] - offset
- else
- zedge = self.collisionbox[6] + offset
- end
- local index = 1
- --point to each node on the sides
- for i = 0, sz
- do
- fronts[index] = {x = self.collisionbox[1] + i, y = 0, z = zedge}
- index = index + 1
- end
- for i = 0, sx
- do
- fronts[index] = {x = xedge, y = 0, z = self.collisionbox[3] + i}
- index = index + 1
- end
- --also check the corner
- fronts[index] = {x = xedge, y = 0, z = zedge}
- end
- for _,f in ipairs(fronts) do
- local node = minetest.get_node_or_nil(vector.add(p, f))
- -- or {walkable = true} snippet to avoid indexing nil values with unknown nodes
- if not node or
- (reg_nodes[node.name] or {walkable = true}).walkable
- then
- jump = vector.aim(pos, destination)
- break
- end
- end
- elseif destination.y > pos.y + 1
- then
- jump = vector.aim(pos, destination)
- end
- if jump then
- self:jump(jump)
- elseif self:is_standing() then
- if speed > self.move_speed * 0.06 then
- self:set_animation("move", {"move_attack"})
- else
- self:set_animation("idle", {"attack", "move_attack"})
- end
- end
- end
- function mobs.register_mob(name, def)
- local prototype = {}
- --copy default prototype
- for k,v in pairs(mobs.default_prototype) do
- prototype[k] = v
- end
- --overwrite fields of default prototype that def uses
- for k,v in pairs(def) do
- prototype[k] = v
- end
- prototype.media_prefix = (string.gsub(name, ":", "_") .. "_")
- prototype.move = def.move or mobs.move_method[prototype.movement]
- -- Register for pathfinding
- if defense_mob_api.pathfinder and prototype.smart_path then
- defense_mob_api.pathfinder:register_class(name, {
- collisionbox = prototype.collisionbox,
- jump_height = math.floor(prototype.jump_height),
- path_check = def.pathfinder_check or defense_mob_api.pathfinder.default_path_check[prototype.movement],
- cost_method = def.pathfinder_cost or defense_mob_api.pathfinder.default_cost_method[prototype.movement],
- })
- end
- --Register for swarming
- if prototype.swarm
- then
- prototype.swarm = defense_mob_api.Swarm()
- end
- minetest.register_entity(name, prototype)
- end
|