adaptive-ai/helper_mobs.lua

955 lines
23 KiB
Lua

local helper = adaptive_ai.helper
local chat_log = function(s)
--minetest.chat_send_player("singleplayer", s)
end
adaptive_ai.chat_log = chat_log
local function get_node_group(name, group)
if type(group) == "table" then
local check = false
for _,g in ipairs(group) do
check = check or get_node_group(name, g)
end
return check
else
if not minetest.registered_nodes[name] or not minetest.registered_nodes[name].groups[group] then
return nil
end
return minetest.registered_nodes[name].groups[group]
end
end
local function is_airlike(name, airlike_groups)
airlike_groups = airlike_groups or {}
if name == "air" then return true end
return get_node_group(name, airlike_groups)
end
local function search_surface(pos, airlike_groups)
local top = minetest.get_node_or_nil(pos)
if not top then return nil end
local top_pos = {x=pos.x, y=pos.y, z=pos.z}
top = minetest.get_node_or_nil(top_pos)
--chat_log("Found "..top.name.."...")
if not is_airlike(top.name) then
repeat
top_pos.y = top_pos.y + 1
top = minetest.get_node_or_nil(top_pos)
--chat_log("Going up - found "..top.name.."...")
until not top or is_airlike(top.name)
top_pos = not top and nil or top_pos
else
repeat
top_pos.y = top_pos.y - 1
top = minetest.get_node(top_pos)
--chat_log("Going down - found "..top.name.."...")
until not top or not is_airlike(top.name)
top_pos = not top and nil or top_pos
top_pos.y = top_pos.y + 1
end
--chat_log("Found!")
return top_pos
end
helper.get_node_group = get_node_group
helper.search_surface = search_surface
----------------------------------------------
-- Auxiliar functions copied from mobs_redo --
----------------------------------------------
-- Localize math functions
local pi = math.pi
local square = math.sqrt
local sin = math.sin
local cos = math.cos
local abs = math.abs
local min = math.min
local max = math.max
local atann = math.atan
local floor = math.floor
local random = math.random
local atan = function(x)
if not x or x ~= x then
--error("atan bassed NaN")
return 0
else
return atann(x)
end
end
-- config values
local show_health = minetest.settings:get_bool("mob_show_health") ~= false
local use_cmi = minetest.global_exists("cmi")
local mobs_drop_items = minetest.settings:get_bool("mobs_drop_items") ~= false
local mobs_griefing = minetest.settings:get_bool("mobs_griefing") ~= false
-- default nodes
local node_fire = "fire:basic_flame"
local node_permanent_flame = "fire:permanent_flame"
local node_ice = "default:ice"
local node_snowblock = "default:snowblock"
local node_snow = "default:snow"
helper.mobs = {}
local get_distance = function(a, b)
local x, y, z = a.x - b.x, a.y - b.y, a.z - b.z
return square(x * x + y * y + z * z)
end
local set_animation = function(ent, anim)
mobs:set_animation(ent,anim)
end
local mob_sound = function(ent, sound)
if sound then
minetest.sound_play(sound, {
object = ent.object,
gain = 1.0,
max_hear_distance = ent.sounds.distance
})
end
end
local set_velocity = function(ent, v)
-- do not move if mob has stopped
if ent.state == "stand" then
ent.object:setvelocity({x = 0, y = 0, z = 0})
return
end
local yaw = (ent.object:get_yaw() or 0) + ent.rotate
ent.object:setvelocity({
x = sin(yaw) * -v,
y = ent.object:getvelocity().y,
z = cos(yaw) * v
})
end
local get_velocity = function(ent)
local v = ent.object:getvelocity()
return (v.x * v.x + v.z * v.z) ^ 0.5
end
local node_ok = function(pos, fallback)
fallback = fallback or mobs.fallback_node
local node = minetest.get_node_or_nil(pos)
if node and minetest.registered_nodes[node.name] then
return node
end
return minetest.registered_nodes[fallback]
end
local set_yaw = function(ent, yaw, delay)
mobs:yaw(ent, yaw, delay)
return ent.target_yaw
end
local do_jump = function(self)
if not self.jump
or self.jump_height == 0
or self.fly
or self.child
or self.order == "stand" then
return false
end
self.facing_fence = false
-- something stopping us while moving?
if self.state ~= "stand"
and get_velocity(self) > 0.5
and self.object:getvelocity().y ~= 0 then
return false
end
local pos = self.object:get_pos()
local yaw = self.object:get_yaw()
-- what is mob standing on?
pos.y = pos.y + self.collisionbox[2] - 0.2
local nod = node_ok(pos)
if minetest.registered_nodes[nod.name].walkable == false then
return false
end
-- where is front
local dir_x = -sin(yaw) * (self.collisionbox[4] + 0.5)
local dir_z = cos(yaw) * (self.collisionbox[4] + 0.5)
-- what is in front of mob?
local nod = node_ok({
x = pos.x + dir_x,
y = pos.y + 0.5,
z = pos.z + dir_z
})
-- thin blocks that do not need to be jumped
if nod.name == node_snow then
return false
end
if minetest.registered_items[nod.name].walkable then
if not nod.name:find("fence")
and not nod.name:find("gate") then
local v = self.object:getvelocity()
v.y = self.jump_height
set_animation(self, "jump") -- only when defined
self.object:setvelocity(v)
-- when in air move forward
minetest.after(0.3, function(self, v)
if self.object:get_luaentity() then
self.object:set_acceleration({
x = v.x * 2,--1.5,
y = 0,
z = v.z * 2,--1.5
})
end
end, self, v)
if get_velocity(self) > 0 then
mob_sound(self, self.sounds.jump)
end
else
self.facing_fence = true
end
return true
end
return false
end
local is_at_cliff = function(ent)
if ent.fear_height == 0 then -- 0 for no falling protection!
return false
end
local yaw = ent.object:get_yaw()
local dir_x = -sin(yaw) * (ent.collisionbox[4] + 0.5)
local dir_z = cos(yaw) * (ent.collisionbox[4] + 0.5)
local pos = ent.object:get_pos()
local ypos = pos.y + ent.collisionbox[2] -- just above floor
if minetest.line_of_sight(
{x = pos.x + dir_x, y = ypos, z = pos.z + dir_z},
{x = pos.x + dir_x, y = ypos - ent.fear_height, z = pos.z + dir_z}
, 1) then
return true
end
return false
end
local within_limits = function(pos, radius)
if (pos.x - radius) > -30913
and (pos.x + radius) < 30928
and (pos.y - radius) > -30913
and (pos.y + radius) < 30928
and (pos.z - radius) > -30913
and (pos.z + radius) < 30928 then
return true -- within limits
end
return false -- beyond limits
end
-- update nametag colour
local update_tag = function(ent)
local col = "#00FF00"
local qua = ent.hp_max / 4
if ent.health <= floor(qua) then
col = "#FF0000"
elseif ent.health <= floor(qua * 2) then
col = "#FF6600"
elseif ent.health <= floor(qua * 3) then
col = "#FFFF00"
end
ent.object:set_properties({nametag = ent.nametag, nametag_color = col})
end
-- check line of sight (BrunoMine)
local line_of_sight = function(ent, pos1, pos2, stepsize)
stepsize = stepsize or 1
local s, pos = minetest.line_of_sight(pos1, pos2, stepsize)
-- normal walking and flying mobs can see you through air
if s == true then
return true
end
-- New pos1 to be analyzed
local npos1 = {x = pos1.x, y = pos1.y, z = pos1.z}
local r, pos = minetest.line_of_sight(npos1, pos2, stepsize)
-- Checks the return
if r == true then return true end
-- Nodename found
local nn = minetest.get_node(pos).name
-- Target Distance (td) to travel
local td = get_distance(pos1, pos2)
-- Actual Distance (ad) traveled
local ad = 0
-- It continues to advance in the line of sight in search of a real
-- obstruction which counts as 'normal' nodebox.
while minetest.registered_nodes[nn]
and (minetest.registered_nodes[nn].walkable == false
or minetest.registered_nodes[nn].drawtype == "nodebox") do
-- Check if you can still move forward
if td < ad + stepsize then
return true -- Reached the target
end
-- Moves the analyzed pos
local d = get_distance(pos1, pos2)
npos1.x = ((pos2.x - pos1.x) / d * stepsize) + pos1.x
npos1.y = ((pos2.y - pos1.y) / d * stepsize) + pos1.y
npos1.z = ((pos2.z - pos1.z) / d * stepsize) + pos1.z
-- NaN checks
if d == 0
or npos1.x ~= npos1.x
or npos1.y ~= npos1.y
or npos1.z ~= npos1.z then
return false
end
ad = ad + stepsize
-- scan again
r, pos = minetest.line_of_sight(npos1, pos2, stepsize)
if r == true then return true end
-- New Nodename found
nn = minetest.get_node(pos).name
end
return false
end
-- are we flying in what we are suppose to? (taikedz)
local flight_check = function(ent, pos_w)
local nod = ent.standing_in
local def = minetest.registered_nodes[nod]
if not def then return false end -- nil check
if type(ent.fly_in) == "string"
and nod == ent.fly_in then
return true
elseif type(ent.fly_in) == "table" then
for _,fly_in in pairs(ent.fly_in) do
if nod == fly_in then
return true
end
end
end
-- stops mobs getting stuck inside stairs and plantlike nodes
if def.drawtype ~= "airlike"
and def.drawtype ~= "liquid"
and def.drawtype ~= "flowingliquid" then
return true
end
return false
end
local effect = function(pos, amount, texture, min_size, max_size, radius, gravity, glow)
radius = radius or 2
min_size = min_size or 0.5
max_size = max_size or 1
gravity = gravity or -10
glow = glow or 0
minetest.add_particlespawner({
amount = amount,
time = 0.25,
minpos = pos,
maxpos = pos,
minvel = {x = -radius, y = -radius, z = -radius},
maxvel = {x = radius, y = radius, z = radius},
minacc = {x = 0, y = gravity, z = 0},
maxacc = {x = 0, y = gravity, z = 0},
minexptime = 0.1,
maxexptime = 1,
minsize = min_size,
maxsize = max_size,
texture = texture,
glow = glow,
})
end
-- drop items
local item_drop = function(ent, cooked)
-- no drops if disabled by setting
if not mobs_drop_items then return end
-- no drops for child mobs
if ent.child then return end
local obj, item, num
local pos = ent.object:get_pos()
ent.drops = ent.drops or {} -- nil check
for n = 1, #ent.drops do
if random(1, ent.drops[n].chance) == 1 then
num = random(ent.drops[n].min or 1, ent.drops[n].max or 1)
item = ent.drops[n].name
-- cook items when true
if cooked then
local output = minetest.get_craft_result({
method = "cooking", width = 1, items = {item}})
if output and output.item and not output.item:is_empty() then
item = output.item:get_name()
end
end
-- add item if it exists
obj = minetest.add_item(pos, ItemStack(item .. " " .. num))
if obj and obj:get_luaentity() then
obj:setvelocity({
x = random(-10, 10) / 9,
y = 6,
z = random(-10, 10) / 9,
})
elseif obj then
obj:remove() -- item does not exist
end
end
end
ent.drops = {}
end
-- check if mob is dead or only hurt
local check_for_death = function(ent, cause, cmi_cause)
-- has health actually changed?
if ent.health == ent.old_health and ent.health > 0 then
return
end
ent.old_health = ent.health
-- still got some health? play hurt sound
if ent.health > 0 then
mob_sound(ent, ent.sounds.damage)
-- make sure health isn't higher than max
if ent.health > ent.hp_max then
ent.health = ent.hp_max
end
-- backup nametag so we can show health stats
if not ent.nametag2 then
ent.nametag2 = ent.nametag or ""
end
if show_health and (cmi_cause and cmi_cause.type == "punch") then
ent.htimer = 2
ent.nametag = "" .. ent.health .. " / " .. ent.hp_max
update_tag(ent)
end
return false
end
-- dropped cooked item if mob died in lava
if cause == "lava" then
item_drop(ent, true)
else
item_drop(ent, nil)
end
mob_sound(ent, ent.sounds.death)
local pos = ent.object:get_pos()
-- execute custom death function
if ent.on_die then
ent.on_die(ent, pos)
if use_cmi then
cmi.notify_die(ent.object, cmi_cause)
end
ent.object:remove()
return true
end
-- default death function and die animation (if defined)
if ent.animation and ent.animation.die_start and ent.animation.die_end then
local frames = ent.animation.die_end - ent.animation.die_start
local speed = ent.animation.die_speed or 15
local length = max(frames / speed, 0)
ent.attack = nil
ent.v_start = false
ent.timer = 0
ent.blinktimer = 0
ent.passive = true
ent.state = "die"
set_velocity(ent, 0)
set_animation(ent, "die")
minetest.after(length, function(ent)
if use_cmi then
cmi.notify_die(ent.object, cmi_cause)
end
ent.object:remove()
end, ent)
else
if use_cmi then
cmi.notify_die(ent.object, cmi_cause)
end
ent.object:remove()
end
effect(pos, 20, "tnt_smoke.png")
return true
end
local do_env_damage = function(ent)
-- feed/tame text timer (so mob 'full' messages dont spam chat)
if ent.htimer > 0 then
ent.htimer = ent.htimer - 1
end
-- reset nametag after showing health stats
if ent.htimer < 1 and ent.nametag2 then
ent.nametag = ent.nametag2
ent.nametag2 = nil
update_tag(ent)
end
local pos = ent.object:get_pos()
ent.time_of_day = minetest.get_timeofday()
-- remove mob if beyond map limits
if not within_limits(pos, 0) then
ent.object:remove()
return
end
-- bright light harms mob
if ent.light_damage ~= 0
-- and pos.y > 0
-- and ent.time_of_day > 0.2
-- and ent.time_of_day < 0.8
and (minetest.get_node_light(pos) or 0) > 12 then
ent.health = ent.health - ent.light_damage
effect(pos, 5, "tnt_smoke.png")
if check_for_death(ent, "light", {type = "light"}) then return end
end
local y_level = ent.collisionbox[2]
if ent.child then
y_level = ent.collisionbox[2] * 0.5
end
-- what is mob standing in?
pos.y = pos.y + y_level + 0.25 -- foot level
ent.standing_in = node_ok(pos, "air").name
-- print ("standing in " .. ent.standing_in)
-- don't fall when on ignore, just stand still
if ent.standing_in == "ignore" then
ent.object:setvelocity({x = 0, y = 0, z = 0})
end
local nodef = minetest.registered_nodes[ent.standing_in]
pos.y = pos.y + 1 -- for particle effect position
-- water
if ent.water_damage and nodef.groups.water then
if ent.water_damage ~= 0 then
ent.health = ent.health - ent.water_damage
effect(pos, 5, "bubble.png", nil, nil, 1, nil)
if check_for_death(ent, "water", {type = "environment",
pos = pos, node = ent.standing_in}) then return end
end
-- lava or fire
elseif ent.lava_damage
and (nodef.groups.lava
or ent.standing_in == node_fire
or ent.standing_in == node_permanent_flame) then
if ent.lava_damage ~= 0 then
ent.health = ent.health - ent.lava_damage
effect(pos, 5, "fire_basic_flame.png", nil, nil, 1, nil)
if check_for_death(ent, "lava", {type = "environment",
pos = pos, node = ent.standing_in}) then return end
end
-- damage_per_second node check
elseif nodef.damage_per_second ~= 0 then
ent.health = ent.health - nodef.damage_per_second
effect(pos, 5, "tnt_smoke.png")
if check_for_death(ent, "dps", {type = "environment",
pos = pos, node = ent.standing_in}) then return end
end
check_for_death(ent, "", {type = "unknown"})
end
-- find and replace what mob is looking for (grass, wheat etc.)
local replace = function(ent, pos)
if not mobs_griefing
or not ent.replace_rate
or not ent.replace_what
or ent.child == true
or ent.object:getvelocity().y ~= 0
or random(1, ent.replace_rate) > 1 then
return
end
local what, with, y_offset
if type(ent.replace_what[1]) == "table" then
local num = random(#ent.replace_what)
what = ent.replace_what[num][1] or ""
with = ent.replace_what[num][2] or ""
y_offset = ent.replace_what[num][3] or 0
else
what = ent.replace_what
with = ent.replace_with or ""
y_offset = ent.replace_offset or 0
end
pos.y = pos.y + y_offset
if #minetest.find_nodes_in_area(pos, pos, what) > 0 then
local oldnode = {name = what}
local newnode = {name = with}
local on_replace_return
if ent.on_replace then
on_replace_return = ent.on_replace(ent, pos, oldnode, newnode)
end
if on_replace_return ~= false then
minetest.set_node(pos, {name = with})
-- when cow/sheep eats grass, replace wool and milk
if ent.gotten == true then
ent.gotten = false
ent.object:set_properties(ent)
end
end
end
end
-- specific runaway
local specific_runaway = function(list, what)
-- no list so do not run
if list == nil then
return false
end
-- found entity on list to attack?
for no = 1, #list do
if list[no] == what then
return true
end
end
return false
end
-- find someone to runaway from
local runaway_from = function(ent)
if not ent.runaway_from then
return
end
local s = ent.object:get_pos()
local p, sp, dist
local player, obj, min_player
local type, name = "", ""
local min_dist = ent.view_range + 1
local objs = minetest.get_objects_inside_radius(s, ent.view_range)
for n = 1, #objs do
if objs[n]:is_player() then
if mobs.invis[ objs[n]:get_player_name() ]
or ent.owner == objs[n]:get_player_name() then
type = ""
else
player = objs[n]
type = "player"
name = "player"
end
else
obj = objs[n]:get_luaentity()
if obj then
player = obj.object
type = obj.type
name = obj.name or ""
end
end
-- find specific mob to runaway from
if name ~= "" and name ~= ent.name
and specific_runaway(ent.runaway_from, name) then
p = player:get_pos()
sp = s
-- aim higher to make looking up hills more realistic
p.y = p.y + 1
sp.y = sp.y + 1
dist = get_distance(p, s)
-- choose closest player/mpb to runaway from
if dist < min_dist
and line_of_sight(ent, sp, p, 2) == true then
min_dist = dist
min_player = player
end
end
end
if min_player then
local lp = player:get_pos()
local vec = {
x = lp.x - s.x,
y = lp.y - s.y,
z = lp.z - s.z
}
local yaw = (atan(vec.z / vec.x) + 3 * pi / 2) - ent.rotate
if lp.x > s.x then
yaw = yaw + pi
end
yaw = set_yaw(ent, yaw, 4)
ent.state = "runaway"
ent.runaway_timer = 3
end
end
local flop = function(ent)
-- swimmers flop when out of their element, and swim again when back in
if ent.fly then
local s = ent.object:get_pos()
if not flight_check(ent, s) then
ent.state = "flop"
ent.object:setvelocity({x = 0, y = -5, z = 0})
set_animation(ent, "stand")
return
elseif ent.state == "flop" then
ent.state = "stand"
end
end
end
local do_stand = function(ent, dtime)
local yaw = self.object:get_yaw() or 0
if random(1, 4) == 1 then
local lp = nil
local s = ent.object:get_pos()
local objs = minetest.get_objects_inside_radius(s, 3)
for n = 1, #objs do
if objs[n]:is_player() then
lp = objs[n]:get_pos()
break
end
end
-- look at any players nearby, otherwise turn randomly
if lp then
local vec = {
x = lp.x - s.x,
z = lp.z - s.z
}
yaw = (atan(vec.z / vec.x) + pi / 2) - ent.rotate
if lp.x > s.x then yaw = yaw + pi end
else
yaw = yaw + random(-0.5, 0.5)
end
yaw = set_yaw(ent, yaw, 8)
end
set_velocity(ent, 0)
set_animation(ent, "stand")
-- npc's ordered to stand stay standing
if ent.type ~= "npc"
or ent.order ~= "stand" then
if ent.walk_chance ~= 0
and ent.facing_fence ~= true
and random(1, 100) <= ent.walk_chance
and is_at_cliff(ent) == false then
set_velocity(ent, ent.walk_velocity)
ent.state = "walk"
set_animation(ent, "walk")
--[[ fly up/down randomly for flying mobs
if ent.fly and random(1, 100) <= ent.walk_chance then
local v = ent.object:getvelocity()
local ud = random(-1, 2) / 9
ent.object:setvelocity({x = v.x, y = ud, z = v.z})
end--]]
end
end
end
local do_walk = function(ent, dtime)
local yaw = self.object:get_yaw() or 0
local s = self.object:get_pos()
local lp = nil
-- is there something I need to avoid?
if self.water_damage > 0
and self.lava_damage > 0 then
lp = minetest.find_node_near(s, 1, {"group:water", "group:lava"})
elseif self.water_damage > 0 then
lp = minetest.find_node_near(s, 1, {"group:water"})
elseif self.lava_damage > 0 then
lp = minetest.find_node_near(s, 1, {"group:lava"})
end
if lp then
-- if mob in water or lava then look for land
if (self.lava_damage
and minetest.registered_nodes[self.standing_in].groups.lava)
or (self.water_damage
and minetest.registered_nodes[self.standing_in].groups.water) then
lp = minetest.find_node_near(s, 5, {"group:soil", "group:stone",
"group:sand", node_ice, node_snowblock})
-- did we find land?
if lp then
local vec = {
x = lp.x - s.x,
z = lp.z - s.z
}
yaw = (atan(vec.z / vec.x) + pi / 2) - self.rotate
if lp.x > s.x then yaw = yaw + pi end
-- look towards land and jump/move in that direction
yaw = set_yaw(self, yaw, 6)
do_jump(self)
set_velocity(self, self.walk_velocity)
else
yaw = yaw + random(-0.5, 0.5)
end
else
local vec = {
x = lp.x - s.x,
z = lp.z - s.z
}
yaw = (atan(vec.z / vec.x) + pi / 2) - self.rotate
if lp.x > s.x then yaw = yaw + pi end
end
yaw = set_yaw(self, yaw, 8)
-- otherwise randomly turn
elseif random(1, 100) <= 30 then
yaw = yaw + random(-0.5, 0.5)
yaw = set_yaw(self, yaw, 8)
end
-- stand for great fall in front
local temp_is_cliff = is_at_cliff(self)
if self.facing_fence == true
or temp_is_cliff
or random(1, 100) <= 30 then
set_velocity(self, 0)
self.state = "stand"
set_animation(self, "stand")
else
set_velocity(self, self.walk_velocity)
if flight_check(self)
and self.animation
and self.animation.fly_start
and self.animation.fly_end then
set_animation(self, "fly")
else
set_animation(self, "walk")
end
end
end
-- Config values
helper.mobs.show_health = show_health
helper.mobs.use_cmi = use_cmi
helper.mobs.mobs_griefing = mobs_griefing
-- Auxiliar functions
helper.mobs.get_distance = get_distance
helper.mobs.set_animation = set_animation
helper.mobs.mob_sound = mob_sound
helper.mobs.node_ok = node_ok
helper.mobs.set_velocity = set_velocity
helper.mobs.get_velocity = get_velocity
helper.mobs.is_at_cliff = is_at_cliff
helper.mobs.within_limits = within_limits
helper.mobs.update_tag = update_tag
helper.mobs.flight_check = flight_check
helper.mobs.effect = effect
helper.mobs.item_drop = item_drop
helper.mobs.check_for_death = check_for_death
helper.mobs.do_env_damage = do_env_damage
helper.mobs.replace = replace
helper.mobs.flop = flop
helper.mobs.runaway_from = runaway_from
helper.mobs.do_jump = do_jump
helper.mobs.set_yaw = set_yaw
helper.mobs.do_state_stand = do_state_stand
helper.mobs.update_tag = update_tag
adaptive_ai.helper = helper