mirror of
https://github.com/ElCeejo/creatura.git
synced 2025-05-21 15:23:17 -04:00
Pathfinding movement functions for A* and theta have been combined into one generic function. Ensure next goal is always set if there is a valid path. Checking if mob has reached the next goal improved - now works no matter the mob height, and the height of the node below the goal. mob:pos_in_box now can take a size param that is a table. Using this to add stepheight to the box height, to fix issues with mobs stepping up onto next node before hitting the goal. Mobs now won't overshoot their goal, if they're very close to it (less than mob width) their speed reduces. The more a mob is turning, the less forward velocity it has. This should fix spinning a lot. mob:pos_in_box now can take a size param that is a table, containing the separate width and height. pathfinding.moveable no longer pads an extra 0.2 around the width of mob when checking if there is sufficient space for the mob. pathfinding.get_neighbors now uses max_fall for the maximum distance we can go down, instead of stepheight.
1165 lines
33 KiB
Lua
1165 lines
33 KiB
Lua
--------------
|
|
-- Mob Meta --
|
|
--------------
|
|
|
|
-- Math --
|
|
|
|
local pi = math.pi
|
|
local pi2 = pi * 2
|
|
local abs = math.abs
|
|
local floor = math.floor
|
|
local random = math.random
|
|
|
|
local sin = math.sin
|
|
local cos = math.cos
|
|
local atan2 = math.atan2
|
|
|
|
local function diff(a, b) -- Get difference between 2 angles
|
|
return math.atan2(math.sin(b - a), math.cos(b - a))
|
|
end
|
|
|
|
local function round(n, dec)
|
|
local x = 10^(dec or 0)
|
|
return math.floor(n * x + 0.5) / x
|
|
end
|
|
|
|
local vec_dir = vector.direction
|
|
local vec_dist = vector.distance
|
|
local vec_multi = vector.multiply
|
|
local vec_sub = vector.subtract
|
|
local vec_add = vector.add
|
|
local vec_normal = vector.normalize
|
|
|
|
local function vec_center(v)
|
|
return {x = floor(v.x + 0.5), y = floor(v.y + 0.5), z = floor(v.z + 0.5)}
|
|
end
|
|
|
|
local function vec_raise(v, n)
|
|
return {x = v.x, y = v.y + n, z = v.z}
|
|
end
|
|
|
|
local function vec_compress(v)
|
|
return {x = round(v.x, 2), y = round(v.y, 2), z = round(v.z, 2)}
|
|
end
|
|
|
|
local function dist_2d(pos1, pos2)
|
|
local a = vector.new(pos1.x, 0, pos1.z)
|
|
local b = vector.new(pos2.x, 0, pos2.z)
|
|
return vec_dist(a, b)
|
|
end
|
|
|
|
local function fast_ray_sight(pos1, pos2)
|
|
local ray = minetest.raycast(pos1, pos2, false, false)
|
|
for pointed_thing in ray do
|
|
if pointed_thing.type == "node" then
|
|
return false
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
-- Local Utilities --
|
|
|
|
local default_node_def = {walkable = true} -- both ignore and unknown nodes are walkable
|
|
|
|
local function get_node_def(name)
|
|
local def = minetest.registered_nodes[name] or default_node_def
|
|
if def.walkable
|
|
and creatura.get_node_height(name) < 0.26 then
|
|
def.walkable = false -- workaround for nodes like snow
|
|
end
|
|
return def
|
|
end
|
|
|
|
|
|
-------------------------
|
|
-- Physics/Vitals Tick --
|
|
-------------------------
|
|
|
|
local step_tick = 0.15
|
|
|
|
minetest.register_globalstep(function(dtime)
|
|
if step_tick <= 0 then
|
|
step_tick = 0.15
|
|
end
|
|
step_tick = step_tick - dtime
|
|
end)
|
|
|
|
-- A metatable is used to avoid issues
|
|
-- With mobs performing functions outside
|
|
-- their own scope
|
|
|
|
local mob = {
|
|
-- Stats
|
|
max_health = 20,
|
|
armor_groups = {fleshy = 100},
|
|
damage = 2,
|
|
speed = 4,
|
|
tracking_range = 16,
|
|
despawn_after = nil,
|
|
-- Physics
|
|
max_fall = 3,
|
|
stepheight = 1.1,
|
|
hitbox = {
|
|
width = 0.5,
|
|
height = 1
|
|
},
|
|
}
|
|
|
|
local mob_meta = {__index = mob}
|
|
|
|
local function index_box_border(self)
|
|
local width = self.width
|
|
local pos = self.object:get_pos()
|
|
pos.y = pos.y + 0.5
|
|
local pos1 = {
|
|
x = pos.x - (width + 0.7),
|
|
y = pos.y,
|
|
z = pos.z - (width + 0.7),
|
|
}
|
|
local pos2 = {
|
|
x = pos.x + (width + 0.7),
|
|
y = pos.y,
|
|
z = pos.z + (width + 0.7),
|
|
}
|
|
local border = {}
|
|
for z = pos1.z, pos2.z do
|
|
for x = pos1.x, pos2.x do
|
|
local vec = {
|
|
x = x,
|
|
y = pos.y,
|
|
z = z
|
|
}
|
|
if not self:pos_in_box(vec, width) then
|
|
table.insert(border, vec_sub(vec, pos))
|
|
end
|
|
end
|
|
end
|
|
return border
|
|
end
|
|
|
|
local function indicate_damage(self)
|
|
local texture_mod = self.object:get_texture_mod()
|
|
self.object:set_texture_mod(texture_mod .. "^[colorize:#FF000040")
|
|
core.after(0.2, function()
|
|
if creatura.is_alive(self) then
|
|
self.object:set_texture_mod(texture_mod)
|
|
end
|
|
end)
|
|
end
|
|
|
|
-- Set Movement Data
|
|
|
|
function mob:move(pos, method, speed_factor, anim)
|
|
self._movement_data.goal = pos
|
|
self._movement_data.method = method
|
|
self._movement_data.last_neighbor = nil
|
|
self._movement_data.gravity = self._movement_data.gravity or -9.8
|
|
self._movement_data.speed = (self.speed or 2) * (speed_factor or 1)
|
|
if anim then
|
|
self._movement_data.anim = anim
|
|
end
|
|
end
|
|
|
|
-- Clear Movement Data
|
|
|
|
function mob:halt()
|
|
self._movement_data = {
|
|
goal = nil,
|
|
method = nil,
|
|
last_neighbor = nil,
|
|
gravity = self._movement_data.gravity or -9.8,
|
|
speed = 0
|
|
}
|
|
self._path_data = {}
|
|
end
|
|
|
|
-- Turn to specified yaw
|
|
|
|
function mob:turn_to(tyaw, rate)
|
|
self._tyaw = tyaw
|
|
local weight = rate or 10
|
|
local yaw = self.object:get_yaw()
|
|
|
|
yaw = yaw + pi
|
|
tyaw = (tyaw + pi) % pi2
|
|
|
|
local step = math.min(self.dtime * weight, abs(tyaw - yaw) % pi2)
|
|
|
|
local dir = abs(tyaw - yaw) > pi and -1 or 1
|
|
dir = tyaw > yaw and dir * 1 or dir * -1
|
|
|
|
local nyaw = (yaw + step * dir) % pi2
|
|
self.object:set_yaw(nyaw - pi)
|
|
self.last_yaw = self.object:get_yaw()
|
|
end
|
|
|
|
-- Set Gravity (default of -9.8)
|
|
|
|
function mob:set_gravity(gravity)
|
|
self._movement_data.gravity = gravity or -9.8
|
|
end
|
|
|
|
-- Sets Velocity to desired speed in mobs current look direction
|
|
|
|
function mob:set_forward_velocity(speed)
|
|
local speed = speed or self._movement_data.speed
|
|
local dir = minetest.yaw_to_dir(self.object:get_yaw())
|
|
local vel = vec_multi(dir, speed)
|
|
vel.y = self.object:get_velocity().y
|
|
self.object:set_velocity(vel)
|
|
end
|
|
|
|
-- Sets Velocity on y axis
|
|
|
|
function mob:set_vertical_velocity(speed)
|
|
local vel = self.object:get_velocity() or {x = 0, y = 0, z = 0}
|
|
vel.y = speed
|
|
self.object:set_velocity(vel)
|
|
end
|
|
|
|
-- Applies knockback in 'dir'
|
|
|
|
function mob:apply_knockback(dir, power)
|
|
if not dir then return end
|
|
power = power or 6
|
|
if not self.touching_ground then
|
|
power = power * 0.8
|
|
end
|
|
local knockback = vec_multi(dir, power)
|
|
knockback.y = abs(power * 0.22)
|
|
self.object:add_velocity(knockback)
|
|
end
|
|
|
|
-- Punch 'target'
|
|
|
|
function mob:punch_target(target) --
|
|
target:punch(self.object, 1.0, {
|
|
full_punch_interval = 1.0,
|
|
damage_groups = {fleshy = self.damage or 5},
|
|
})
|
|
end
|
|
|
|
-- Apply damage to mob
|
|
|
|
function mob:hurt(health)
|
|
if self.protected then return end
|
|
self.hp = self.hp - math.ceil(health)
|
|
end
|
|
|
|
-- Add HP to mob
|
|
|
|
function mob:heal(health)
|
|
if self.protected then return end
|
|
self.hp = self.hp + math.ceil(health)
|
|
if self.hp > self.max_health then
|
|
self.hp = self.max_health
|
|
end
|
|
end
|
|
|
|
-- Return position at center of mobs hitbox
|
|
|
|
function mob:get_center_pos()
|
|
return vec_raise(self.object:get_pos(), self.height * 0.5 or 0.5)
|
|
end
|
|
|
|
-- Return true if position is within box
|
|
|
|
function mob:pos_in_box(pos, size)
|
|
if not pos then return false end
|
|
local center = self:get_center_pos()
|
|
|
|
local width = math.max(self.width, 0.5)
|
|
local height = (self.height * 0.5)
|
|
if size then
|
|
if type(size) == "table" then
|
|
width = size[1]
|
|
height = size[2]
|
|
else
|
|
width = size
|
|
height = size
|
|
end
|
|
end
|
|
|
|
local edge_a = {
|
|
x = center.x - width,
|
|
y = center.y - height,
|
|
z = center.z - width
|
|
}
|
|
local edge_b = {
|
|
x = center.x + width,
|
|
y = center.y + height,
|
|
z = center.z + width
|
|
}
|
|
local minp, maxp = vector.sort(edge_a, edge_b)
|
|
if pos.x >= minp.x
|
|
and pos.y >= minp.y
|
|
and pos.z >= minp.z
|
|
and pos.x <= maxp.x
|
|
and pos.y <= maxp.y
|
|
and pos.z <= maxp.z then
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Terrain Navigation --
|
|
|
|
function mob:get_wander_pos(min_range, max_range, dir)
|
|
local pos = vec_center(self.object:get_pos())
|
|
pos.y = floor(pos.y + 0.5)
|
|
local node = minetest.get_node(pos)
|
|
local us = minetest.get_us_time()
|
|
if get_node_def(node.name).walkable then -- Occurs if small mob is touching a fence
|
|
local offset = vector.add(pos, vec_multi(vec_dir(pos, self.object:get_pos()), 1.5))
|
|
pos.x = floor(offset.x + 0.5)
|
|
pos.z = floor(offset.z + 0.5)
|
|
pos = creatura.get_ground_level(pos, 1, 1, 0)
|
|
end
|
|
local width = self.width
|
|
local outset = random(min_range, max_range)
|
|
if width < 0.6 then width = 0.6 end
|
|
local move_dir = vec_normal({
|
|
x = random(-10, 10) * 0.1,
|
|
y = 0,
|
|
z = random(-10, 10) * 0.1
|
|
})
|
|
local pos2 = vec_add(pos, vec_multi(move_dir, width))
|
|
if get_node_def(minetest.get_node(pos2).name).walkable
|
|
and not dir then
|
|
for i = 1, 3 do
|
|
move_dir = {
|
|
x = move_dir.z,
|
|
y = 0,
|
|
z = move_dir.x * -1
|
|
}
|
|
pos2 = vec_add(pos, vec_multi(move_dir, width))
|
|
if not get_node_def(minetest.get_node(pos2).name).walkable then
|
|
break
|
|
end
|
|
end
|
|
elseif dir then
|
|
move_dir = dir
|
|
end
|
|
for i = 1, outset do
|
|
local a_pos = vec_add(pos2, vec_multi(move_dir, i))
|
|
local a_node = minetest.get_node(a_pos)
|
|
local b_pos = {x = a_pos.x, y = a_pos.y - 1, z = a_pos.z}
|
|
local b_node = minetest.get_node(b_pos)
|
|
if get_node_def(a_node.name).walkable
|
|
or not get_node_def(b_node.name).walkable then
|
|
a_pos = get_ground_level(a_pos, floor(self.stepheight or 1))
|
|
end
|
|
if not get_node_def(a_node.name).walkable then
|
|
pos2 = a_pos
|
|
else
|
|
break
|
|
end
|
|
end
|
|
return pos2
|
|
end
|
|
|
|
function mob:get_wander_pos_3d(min_range, max_range)
|
|
local pos = self.object:get_pos()
|
|
local outset = random(min_range, max_range)
|
|
local move_dir = {
|
|
x = random(-10, 10) * 0.1,
|
|
z = random(-10, 10) * 0.1,
|
|
y = random(-10, 10) * 0.1
|
|
}
|
|
local pos2 = vec_add(pos, vec_multi(vec_normal(move_dir), 1))
|
|
local fail_safe = 0
|
|
while fail_safe < 4
|
|
and minetest.registered_nodes[minetest.get_node(pos2).name].walkable do
|
|
move_dir = {
|
|
x = random(-10, 10) * 0.1,
|
|
z = random(-10, 10) * 0.1,
|
|
y = random(-10, 10) * 0.1
|
|
}
|
|
pos2 = vec_add(pos, vec_multi(vec_normal(move_dir), 1))
|
|
end
|
|
for i = 2, outset do
|
|
local out_pos = vec_add(pos, vec_multi(vec_normal(move_dir), i))
|
|
if minetest.registered_nodes[minetest.get_node(out_pos).name].walkable then
|
|
break
|
|
end
|
|
pos2 = out_pos
|
|
end
|
|
return pos2
|
|
end
|
|
|
|
function mob:is_pos_safe(pos)
|
|
local mob_pos = self.object:get_pos()
|
|
local node = minetest.get_node(pos)
|
|
if not node then return false end
|
|
if minetest.get_item_group(node.name, "igniter") > 0
|
|
or get_node_def(node.name).drawtype == "liquid" then return false end
|
|
local fall_safe = false
|
|
if self.max_fall ~= 0 then
|
|
for i = 1, self.max_fall or 3 do
|
|
local fall_pos = {
|
|
x = pos.x,
|
|
y = floor(mob_pos.y + 0.5) - i,
|
|
z = pos.z
|
|
}
|
|
if get_node_def(minetest.get_node(fall_pos).name).walkable then
|
|
fall_safe = true
|
|
break
|
|
end
|
|
end
|
|
else
|
|
fall_safe = true
|
|
end
|
|
return fall_safe
|
|
end
|
|
|
|
-- Set mobs animation (if specified animation isn't already playing)
|
|
|
|
function mob:animate(animation)
|
|
if not animation
|
|
or not self.animations[animation] then return end
|
|
if not self._anim
|
|
or self._anim ~= animation then
|
|
local anim = self.animations[animation]
|
|
self.object:set_animation(anim.range, anim.speed, anim.frame_blend, anim.loop)
|
|
self._anim = animation
|
|
end
|
|
end
|
|
|
|
-- Set texture to variable at 'id' index in 'tbl' or 'textures'
|
|
|
|
function mob:set_texture(id, tbl)
|
|
local _table = self.textures
|
|
if tbl then
|
|
_table = tbl
|
|
end
|
|
if not _table
|
|
or not _table[id] then
|
|
return
|
|
end
|
|
self.object:set_properties({
|
|
textures = {_table[id]}
|
|
})
|
|
return _table[id]
|
|
end
|
|
|
|
-- Set scale to base scale times 'x' and update bordering positions
|
|
|
|
function mob:set_scale(x)
|
|
local def = minetest.registered_entities[self.name]
|
|
local scale = def.visual_size
|
|
local box = def.collisionbox
|
|
local new_box = {}
|
|
for k, v in ipairs(box) do
|
|
new_box[k] = v * x
|
|
end
|
|
self.object:set_properties({
|
|
visual_size = {
|
|
x = scale.x * x,
|
|
y = scale.y * x
|
|
},
|
|
collisionbox = new_box
|
|
})
|
|
self._border = index_box_border(self)
|
|
end
|
|
|
|
-- Fixes mob scale being changed when attached to a parent
|
|
|
|
function mob:fix_attached_scale(parent)
|
|
local scale = self:get_visual_size()
|
|
local parent_size = parent:get_properties().visual_size
|
|
self.object:set_properties({
|
|
visual_size = {
|
|
x = scale.x / parent_size.x,
|
|
y = scale.y / parent_size.y
|
|
},
|
|
})
|
|
end
|
|
|
|
-- Add sets 'id' to 'val' in permanent data
|
|
|
|
function mob:memorize(id, val)
|
|
self.perm_data[id] = val
|
|
return self.perm_data[id]
|
|
end
|
|
|
|
-- Remove 'id' from permanent data
|
|
|
|
function mob:forget(id)
|
|
self.perm_data[id] = nil
|
|
end
|
|
|
|
-- Return value from 'id' in permanent data
|
|
|
|
function mob:recall(id)
|
|
return self.perm_data[id]
|
|
end
|
|
|
|
-- Return true on interval specified by 'n'
|
|
|
|
function mob:timer(n)
|
|
local t1 = floor(self.active_time)
|
|
local t2 = floor(self.active_time + self.dtime)
|
|
if t2 > t1 and t2%n == 0 then return true end
|
|
end
|
|
|
|
-- Play 'sound' from self.sounds
|
|
|
|
function mob:play_sound(sound)
|
|
local spec = self.sounds and self.sounds[sound]
|
|
local parameters = {object = self.object}
|
|
|
|
if type(spec) == "table" then
|
|
local name = spec.name
|
|
if spec.variations then
|
|
name = name .. "_" .. random(spec.variations)
|
|
end
|
|
local pitch = 1.0
|
|
|
|
pitch = pitch - (random(-10, 10) * 0.005)
|
|
|
|
parameters.gain = spec.gain or 1
|
|
parameters.max_hear_distance = spec.distance or 8
|
|
parameters.fade = spec.fade or 1
|
|
parameters.pitch = pitch
|
|
return minetest.sound_play(name, parameters)
|
|
end
|
|
return minetest.sound_play(spec, parameters)
|
|
end
|
|
|
|
-- Return current collisionbox
|
|
|
|
function mob:get_hitbox()
|
|
if not self.object:get_properties() then return self.collisionbox end
|
|
return self.object:get_properties().collisionbox
|
|
end
|
|
|
|
-- Return height of current collisionbox
|
|
|
|
function mob:get_height()
|
|
local hitbox = self:get_hitbox()
|
|
return hitbox[5] - hitbox[2]
|
|
end
|
|
|
|
-- Return current visual size
|
|
|
|
function mob:get_visual_size()
|
|
if not self.object:get_properties() then return end
|
|
return self.object:get_properties().visual_size
|
|
end
|
|
|
|
local function is_group_in_table(tbl, name)
|
|
for _, v in pairs(tbl) do
|
|
if minetest.get_item_group(name, v:split(":")[2]) > 0 then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
function mob:follow_wielded_item(player)
|
|
if not player
|
|
or not self.follow then return end
|
|
local item = player:get_wielded_item()
|
|
local name = item:get_name()
|
|
if type(self.follow) == "string"
|
|
and (name == self.follow
|
|
or minetest.get_item_group(name, self.follow:split(":")[2]) > 0) then
|
|
return item, name
|
|
end
|
|
if type(self.follow) == "table"
|
|
and (is_value_in_table(self.follow, name)
|
|
or is_group_in_table(self.follow, name)) then
|
|
return item, name
|
|
end
|
|
end
|
|
|
|
function mob:get_target(target)
|
|
local alive = creatura.is_alive(target)
|
|
if not alive then
|
|
return false, false, nil
|
|
end
|
|
if type(target) == "table" then
|
|
target = target.object
|
|
end
|
|
local pos = self:get_center_pos()
|
|
local tpos = target:get_pos()
|
|
tpos.y = floor(tpos.y + 0.5)
|
|
local line_of_sight = fast_ray_sight(pos, tpos)
|
|
return true, line_of_sight, tpos
|
|
end
|
|
|
|
-- Actions
|
|
|
|
function mob:set_action(func)
|
|
self._action = func
|
|
end
|
|
|
|
function mob:get_action()
|
|
if type(self._action) ~= "table" then
|
|
return self._action
|
|
end
|
|
return nil
|
|
end
|
|
|
|
function mob:clear_action()
|
|
self._action = {}
|
|
end
|
|
|
|
function mob:set_utility(func)
|
|
self._utility_data.func = func
|
|
end
|
|
|
|
function mob:get_utility()
|
|
return self._utility_data.utility
|
|
end
|
|
|
|
function mob:initiate_utility(utility, ...)
|
|
local func = creatura.registered_utilities[utility]
|
|
if not func then return end
|
|
self._utility_data.utility = utility
|
|
self:clear_action()
|
|
func(...)
|
|
end
|
|
|
|
function mob:set_utility_score(n)
|
|
self._utility_data.score = n or 0
|
|
end
|
|
|
|
-- Functions
|
|
|
|
function mob:activate(staticdata, dtime)
|
|
self.width = self:get_hitbox()[4] or 0.5
|
|
self.height = self:get_height() or 1
|
|
self._tyaw = self.object:get_yaw()
|
|
self.last_yaw = self.object:get_yaw()
|
|
self.active_time = 0
|
|
self.in_liquid = false
|
|
self.is_falling = false
|
|
self.touching_ground = false
|
|
|
|
-- Backend Data (Should not be modified unless modder knows what they're doing)
|
|
self._movement_data = {
|
|
goal = nil,
|
|
method = nil,
|
|
last_neighbor = nil,
|
|
gravity = -9.8,
|
|
speed = 0
|
|
}
|
|
self._path_data = {}
|
|
self._path = {}
|
|
self._task = {}
|
|
self._action = {}
|
|
|
|
local pos = self.object:get_pos()
|
|
local node = minetest.get_node(pos)
|
|
|
|
if node
|
|
and minetest.get_item_group(node.name, "liquid") > 0 then
|
|
self.in_liquid = node.name
|
|
end
|
|
|
|
-- Staticdata
|
|
if staticdata then
|
|
local data = minetest.deserialize(staticdata)
|
|
if data then
|
|
for k, v in pairs(data) do
|
|
self[k] = v
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Initialize Stats and Visuals
|
|
if not self.textures then
|
|
local textures = self.object:get_properties().textures
|
|
if textures then self.textures = textures end
|
|
end
|
|
|
|
if not self.perm_data then
|
|
if self.memory then
|
|
self.perm_data = self.memory
|
|
else
|
|
self.perm_data = {}
|
|
end
|
|
if #self.textures > 1 then self.texture_no = random(#self.textures) end
|
|
end
|
|
|
|
if self:recall("despawn_after") ~= nil then
|
|
self.despawn_after = self:recall("despawn_after")
|
|
end
|
|
self._despawn = self:recall("_despawn") or false
|
|
|
|
if self._despawn
|
|
and self.despawn_after then
|
|
self.object:remove()
|
|
return
|
|
end
|
|
|
|
self._breath = self:recall("_breath") or (self.max_breath or 30)
|
|
self._border = index_box_border(self)
|
|
|
|
if self.textures
|
|
and self.texture_no then
|
|
self:set_texture(self.texture_no, self.textures)
|
|
end
|
|
|
|
self.max_health = self.max_health or 10
|
|
self.hp = self.hp or self.max_health
|
|
|
|
if type(self.armor_groups) ~= "table" then
|
|
self.armor_groups = {}
|
|
end
|
|
self.armor_groups.immortal = 1
|
|
self.object:set_armor_groups(self.armor_groups)
|
|
|
|
if self.timer
|
|
and type(self.timer) == "number" then -- fix crash for converted mobs_redo mobs
|
|
self.timer = function(self, n)
|
|
local t1 = floor(self.active_time)
|
|
local t2 = floor(self.active_time + self.dtime)
|
|
if t2 > t1 and t2%n == 0 then return true end
|
|
end
|
|
end
|
|
|
|
if self.activate_func then
|
|
self:activate_func(self, staticdata, dtime)
|
|
end
|
|
end
|
|
|
|
function mob:staticdata()
|
|
local data = {}
|
|
data.perm_data = self.perm_data
|
|
data.hp = self.hp or self.max_health
|
|
data.texture_no = self.texture_no or random(#self.textures)
|
|
return minetest.serialize(data)
|
|
end
|
|
|
|
function mob:on_step(dtime, moveresult)
|
|
--local us_time = minetest.get_us_time()
|
|
if not self.hp then return end
|
|
self.dtime = dtime or 0.09
|
|
self.moveresult = moveresult or {}
|
|
self.touching_ground = false
|
|
if moveresult then
|
|
self.touching_ground = moveresult.touching_ground
|
|
end
|
|
if step_tick <= 0 then
|
|
-- Physics and Vitals
|
|
if self._physics then
|
|
self:_physics(moveresult)
|
|
end
|
|
if self._vitals then
|
|
self:_vitals()
|
|
end
|
|
-- Cached Geometry
|
|
self.width = self:get_hitbox()[4] or 0.5
|
|
self.height = self:get_height() or 1
|
|
end
|
|
self:_light_physics()
|
|
-- Movement Control
|
|
if self._move then
|
|
self:_move()
|
|
end
|
|
if self.utility_stack
|
|
and self._execute_utilities then
|
|
self:_execute_utilities()
|
|
self:_execute_actions()
|
|
end
|
|
-- Die
|
|
if self.hp <= 0
|
|
and self.death_func then
|
|
self:death_func()
|
|
self:halt()
|
|
return
|
|
end
|
|
if self.step_func
|
|
and self.perm_data then
|
|
self:step_func(dtime, moveresult)
|
|
end
|
|
self.active_time = self.active_time + dtime
|
|
if self.despawn_after
|
|
and self.active_time >= self.despawn_after then
|
|
self._despawn = self:memorize("_despawn", true)
|
|
end
|
|
end
|
|
|
|
function mob:on_deactivate()
|
|
self._task = {}
|
|
self._action = {}
|
|
if self.deactivate_func then
|
|
self:deactivate_func(self)
|
|
end
|
|
end
|
|
|
|
----------------
|
|
-- Object API --
|
|
----------------
|
|
|
|
local fancy_step = false
|
|
|
|
local step_type = minetest.settings:get("creatura_step_type")
|
|
|
|
if step_type == "fancy" then
|
|
fancy_step = true
|
|
end
|
|
|
|
-- Physics
|
|
|
|
local function do_step(self)
|
|
if not fancy_step then return end
|
|
local pos = self.object:get_pos()
|
|
local vel = self.object:get_velocity()
|
|
if not self._step then
|
|
if self.touching_ground
|
|
and abs(vel.x + vel.z) > 0 then
|
|
local border = self._border
|
|
local yaw_offset = vec_add(pos, vec_multi(minetest.yaw_to_dir(self.object:get_yaw()), self.width + 0.7))
|
|
table.sort(border, function(a, b) return vec_dist(vec_add(pos, a), yaw_offset) < vec_dist(vec_add(pos, b), yaw_offset) end)
|
|
local step_pos = vec_center(vec_add(pos, border[1]))
|
|
local halfway = vec_add(pos, vec_multi(vec_dir(pos, step_pos), 0.5))
|
|
halfway.y = step_pos.y
|
|
if creatura.get_node_def(step_pos).walkable
|
|
and abs(diff(self.object:get_yaw(), minetest.dir_to_yaw(vec_dir(pos, step_pos)))) < 1.5
|
|
and moveable(halfway, self.width, self.height) then
|
|
self._step = vec_center(step_pos)
|
|
end
|
|
end
|
|
else
|
|
local vel = self.object:get_velocity()
|
|
self.object:set_velocity(vector.new(vel.x, 7, vel.z))
|
|
if self._step.y < pos.y - 0.5 then
|
|
self.object:set_velocity(vector.new(vel.x, 0.5, vel.z))
|
|
self._step = nil
|
|
local step_pos = self.object:get_pos()
|
|
local dir = minetest.yaw_to_dir(self.object:get_yaw())
|
|
step_pos = vec_add(step_pos, vec_multi(dir, 0.1))
|
|
self.object:set_pos(step_pos)
|
|
end
|
|
end
|
|
end
|
|
|
|
local function collision_detection(self)
|
|
if not creatura.is_alive(self) then return end
|
|
local pos = self.object:get_pos()
|
|
local width = self.width + 0.25
|
|
local objects = minetest.get_objects_in_area(vec_sub(pos, width), vec_add(pos, width))
|
|
if #objects < 2 then return end
|
|
local col_no = 0
|
|
for i = 2, #objects do
|
|
local object = objects[i]
|
|
if creatura.is_alive(object)
|
|
and (not object:get_attach()
|
|
or object:get_attach() ~= self.object) then
|
|
if i > 5 then break end
|
|
local pos2 = object:get_pos()
|
|
local dir = vec_dir(pos, pos2)
|
|
dir.y = 0
|
|
if dir.x == 0 and dir.z == 0 then
|
|
dir = vector.new(random(-1, 1) * random(), 0,
|
|
random(-1, 1) * random())
|
|
end
|
|
local velocity = vec_multi(dir, 1.1)
|
|
local vel1 = vec_multi(velocity, -1)
|
|
local vel2 = velocity
|
|
self.object:add_velocity(vel1)
|
|
object:add_velocity(vel2)
|
|
end
|
|
end
|
|
end
|
|
|
|
local function water_physics(self)
|
|
-- Props
|
|
local gravity = self._movement_data.gravity
|
|
local height = self.height
|
|
-- Vectors
|
|
local floor_pos = self.object:get_pos()
|
|
floor_pos.y = floor_pos.y + 0.01
|
|
local surface_pos = floor_pos
|
|
local floor_node = minetest.get_node(floor_pos)
|
|
local surface_node = minetest.get_node(surface_pos)
|
|
if minetest.get_item_group(floor_node.name, "liquid") < 1 then
|
|
self.object:set_acceleration({
|
|
x = 0,
|
|
y = gravity,
|
|
z = 0
|
|
})
|
|
if self.in_liquid then
|
|
self.in_liquid = false
|
|
end
|
|
return
|
|
end
|
|
self.in_liquid = floor_node.name
|
|
-- Get submergence (Not the most accurate, but reduces lag)
|
|
for i = 1, math.ceil(height * 3) do
|
|
local step_pos = {
|
|
x = floor_pos.x,
|
|
y = floor_pos.y + 0.5 * i,
|
|
z = floor_pos.z
|
|
}
|
|
if minetest.get_item_group(minetest.get_node(step_pos).name, "liquid") > 0 then
|
|
surface_pos = step_pos
|
|
else
|
|
break
|
|
end
|
|
end
|
|
-- Apply Physics
|
|
local submergence = surface_pos.y - floor_pos.y
|
|
local vel = self.object:get_velocity()
|
|
local bouyancy = self.bouyancy_multiplier or 1
|
|
self.object:set_acceleration({
|
|
x = 0,
|
|
y = (submergence - vel.y * abs(vel.y) * 0.4) * bouyancy,
|
|
z = 0
|
|
})
|
|
local hydrodynamics = self.hydrodynamics_multiplier or 0.7
|
|
local vel_y = vel.y
|
|
if self.bouyancy_multiplier == 0 then -- if bouyancy is disabled drag will be applied to keep awuatic mobs from drifting
|
|
vel_y = vel.y * hydrodynamics
|
|
end
|
|
self.object:set_velocity({
|
|
x = vel.x * hydrodynamics,
|
|
y = vel_y,
|
|
z = vel.z * hydrodynamics
|
|
})
|
|
end
|
|
|
|
function mob:_physics(moveresult)
|
|
if not self.object then return end
|
|
water_physics(self)
|
|
-- Step up nodes
|
|
do_step(self, moveresult)
|
|
-- Object collision
|
|
collision_detection(self)
|
|
if not self.in_liquid
|
|
and not self.touching_ground then
|
|
self.is_falling = true
|
|
else
|
|
self.is_falling = false
|
|
end
|
|
if not self.in_liquid
|
|
and self._movement_data.gravity ~= 0 then
|
|
local vel = self.object:get_velocity()
|
|
if self.touching_ground then
|
|
local nvel = vector.multiply(vel, 0.2)
|
|
if nvel.x < 0.2
|
|
and nvel.z < 0.2 then
|
|
nvel.x = 0
|
|
nvel.z = 0
|
|
end
|
|
nvel.y = vel.y
|
|
self.object:set_velocity(nvel)
|
|
else
|
|
local nvel = vector.multiply(vel, 0.1)
|
|
if nvel.x < 0.2
|
|
and nvel.z < 0.2 then
|
|
nvel.x = 0
|
|
nvel.z = 0
|
|
end
|
|
nvel.y = vel.y
|
|
self.object:set_velocity(nvel)
|
|
end
|
|
end
|
|
end
|
|
|
|
function mob:_light_physics() -- physics that are lightweight enough to be called each step
|
|
end
|
|
|
|
-- Movement Control
|
|
|
|
function mob:_move()
|
|
if not self.object then return end
|
|
local data = self._movement_data
|
|
local speed = data.speed
|
|
if data.goal then
|
|
local pos = data.goal
|
|
local method = data.method
|
|
local anim = data.anim
|
|
if creatura.registered_movement_methods[method] then
|
|
local func = creatura.registered_movement_methods[method]
|
|
func(self, pos, speed, anim)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Execute Actions
|
|
|
|
function mob:_execute_actions()
|
|
if not self.object then return end
|
|
local task = self._task
|
|
if #self._task > 0 then
|
|
local func = self._task[#self._task].func
|
|
if func(self) then
|
|
self._task[#self._task] = nil
|
|
self:clear_action()
|
|
return
|
|
end
|
|
end
|
|
local action = self._action
|
|
if type(action) ~= "table" then
|
|
local func = action
|
|
if func(self) then
|
|
self:clear_action()
|
|
end
|
|
end
|
|
end
|
|
|
|
local function tbl_equals(tbl1, tbl2)
|
|
local match = true
|
|
for k, v in pairs(tbl1) do
|
|
if not tbl2[k]
|
|
and tbl2[k] ~= v then
|
|
match = false
|
|
break
|
|
end
|
|
end
|
|
return match
|
|
end
|
|
|
|
function mob:_execute_utilities()
|
|
local is_alive = self.hp > 0
|
|
if not self._utility_data then
|
|
self._utility_data = {
|
|
utility = nil,
|
|
func = nil,
|
|
score = 0
|
|
}
|
|
end
|
|
local loop_data = {
|
|
utility = nil,
|
|
func = nil,
|
|
score = 0
|
|
}
|
|
if (self:timer(self.task_timer or 1)
|
|
or not self._utility_data.func)
|
|
and is_alive then
|
|
for i = 1, #self.utility_stack do
|
|
local utility = self.utility_stack[i].utility
|
|
local get_score = self.utility_stack[i].get_score
|
|
local score, args = get_score(self)
|
|
if score > 0
|
|
and score >= self._utility_data.score
|
|
and score >= loop_data.score then
|
|
loop_data = {
|
|
utility = utility,
|
|
score = score,
|
|
args = args
|
|
}
|
|
end
|
|
end
|
|
end
|
|
if loop_data.utility
|
|
and loop_data.args then
|
|
local no_data = not self._utility_data.utility and not self._utility_data.args
|
|
local new_util = self._utility_data.utility ~= loop_data.utility or not tbl_equals(self._utility_data.args, loop_data.args)
|
|
if no_data
|
|
or new_util then -- if utilities are different or utilities are the same and args are different set new data
|
|
self._utility_data = loop_data
|
|
end
|
|
end
|
|
if self._utility_data.utility then
|
|
if not self._utility_data.func then
|
|
self:initiate_utility(self._utility_data.utility, unpack(self._utility_data.args))
|
|
end
|
|
local func = self._utility_data.func
|
|
if not func then return end
|
|
if func(self) then
|
|
self._utility_data = {
|
|
utility = nil,
|
|
func = nil,
|
|
score = 0
|
|
}
|
|
self:clear_action()
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Vitals
|
|
|
|
function mob:_vitals()
|
|
local stand_pos = self.object:get_pos()
|
|
local fall_start = self._fall_start
|
|
if self.is_falling
|
|
and not fall_start
|
|
and self.max_fall > 0 then
|
|
self._fall_start = stand_pos.y
|
|
elseif fall_start
|
|
and self.max_fall > 0 then
|
|
if self.touching_ground
|
|
and not self.in_liquid then
|
|
local damage = fall_start - stand_pos.y
|
|
if damage < (self.max_fall or 3) then
|
|
self._fall_start = nil
|
|
return
|
|
end
|
|
local resist = self.fall_resistance or 0
|
|
self:hurt(damage - (damage * (resist * 0.1)))
|
|
indicate_damage(self)
|
|
if random(4) < 2 then
|
|
self:play_sound("hurt")
|
|
end
|
|
self._fall_start = nil
|
|
elseif self.in_liquid then
|
|
self._fall_start = nil
|
|
end
|
|
end
|
|
if self:timer(1) then
|
|
local head_pos = vec_raise(stand_pos, self.height)
|
|
local head_def = get_node_def(minetest.get_node(head_pos).name)
|
|
if head_def.drawtype == "liquid"
|
|
and minetest.get_item_group(minetest.get_node(head_pos).name, "water") > 0 then
|
|
if self._breath <= 0 then
|
|
self:hurt(1)
|
|
indicate_damage(self)
|
|
if random(4) < 2 then
|
|
self:play_sound("hurt")
|
|
end
|
|
else
|
|
self._breath = self._breath - 1
|
|
self:memorize("_breath", self._breath)
|
|
end
|
|
end
|
|
local stand_def = get_node_def(minetest.get_node(stand_pos).name)
|
|
if minetest.get_item_group(minetest.get_node(stand_pos).name, "fire") > 0
|
|
and stand_def.damage_per_second then
|
|
local damage = stand_def.damage_per_second
|
|
local resist = self.fire_resistance or 0
|
|
self:hurt(damage - (damage * (resist * 0.1)))
|
|
indicate_damage(self)
|
|
if random(4) < 2 then
|
|
self:play_sound("hurt")
|
|
end
|
|
end
|
|
end
|
|
if self:timer(5) then
|
|
local objects = minetest.get_objects_inside_radius(stand_pos, 0.2)
|
|
if #objects > 10 then
|
|
indicate_damage(self)
|
|
self.hp = self:memorize("hp", -1)
|
|
self:death_func()
|
|
end
|
|
end
|
|
end
|
|
|
|
function creatura.register_mob(name, def)
|
|
local box_width = def.hitbox and def.hitbox.width or 0.5
|
|
local box_height = def.hitbox and def.hitbox.height or 1
|
|
local hitbox = {-box_width, 0, -box_width, box_width, box_height, box_width}
|
|
|
|
def.physical = def.physical or true
|
|
def.collide_with_objects = def.collide_with_objects or false
|
|
def.visual = "mesh"
|
|
def.makes_footstep_sound = def.makes_footstep_sound or false
|
|
def.static_save = true
|
|
def.collisionbox = hitbox
|
|
def._creatura_mob = true
|
|
|
|
def.on_activate = function(self, staticdata, dtime)
|
|
return self:activate(staticdata, dtime)
|
|
end
|
|
|
|
def.get_staticdata = function(self)
|
|
return self:staticdata(self)
|
|
end
|
|
|
|
minetest.register_entity(name, setmetatable(def, mob_meta))
|
|
end
|