From 67fd8c8b405fa18d0e317bbd0ee6875001c203aa Mon Sep 17 00:00:00 2001 From: cora Date: Wed, 9 Nov 2022 02:59:46 +0100 Subject: [PATCH] split off physics functions into separate file + use metatable --- mods/ENTITIES/mcl_mobs/api.lua | 1016 +++------------------------- mods/ENTITIES/mcl_mobs/effects.lua | 43 ++ mods/ENTITIES/mcl_mobs/init.lua | 9 +- mods/ENTITIES/mcl_mobs/physics.lua | 799 ++++++++++++++++++++++ 4 files changed, 946 insertions(+), 921 deletions(-) create mode 100644 mods/ENTITIES/mcl_mobs/effects.lua create mode 100644 mods/ENTITIES/mcl_mobs/physics.lua diff --git a/mods/ENTITIES/mcl_mobs/api.lua b/mods/ENTITIES/mcl_mobs/api.lua index 613381695..44f5e4348 100644 --- a/mods/ENTITIES/mcl_mobs/api.lua +++ b/mods/ENTITIES/mcl_mobs/api.lua @@ -1,7 +1,6 @@ +local mob_class = mcl_mobs.mob_class +local mob_class_meta = {__index = mcl_mobs.mob_class} -- API for Mobs Redo: MineClone 2 Edition (MRM) - -mcl_mobs = {} - local MAX_MOB_NAME_LENGTH = 30 local HORNY_TIME = 30 local HORNY_AGAIN_TIME = 300 @@ -10,8 +9,6 @@ local DEATH_DELAY = 0.5 local DEFAULT_FALL_SPEED = -9.81*1.5 local FLOP_HEIGHT = 6 local FLOP_HOR_SPEED = 1.5 -local ENTITY_CRAMMING_MAX = 24 -local CRAMMING_DAMAGE = 3 local PATHFINDING = "gowp" @@ -26,30 +23,6 @@ local function mcl_log (message) mcl_util.mcl_log (message, "[Mobs]", true) end end -local mob_class = {} -local mob_class_meta = {__index = mob_class} - -local function shortest_term_of_yaw_rotatoin(self, rot_origin, rot_target, nums) - - if not rot_origin or not rot_target then - return - end - - rot_origin = math.deg(rot_origin) - rot_target = math.deg(rot_target) - - - - if math.abs(rot_target - rot_origin) < 180 then - return rot_target - rot_origin - else - if (rot_target - rot_origin) > 0 then - return 360-(rot_target - rot_origin) - else - return (rot_target - rot_origin)+360 - end - end -end -- Invisibility mod check @@ -200,48 +173,33 @@ function mob_class:player_in_active_range() end --- play sound -local mob_sound = function(self, soundname, is_opinion, fixed_pitch) +-- blast damage to entities nearby +local function entity_physics(pos,radius) - local soundinfo - if self.sounds_child and self.child then - soundinfo = self.sounds_child - elseif self.sounds then - soundinfo = self.sounds - end - if not soundinfo then - return - end - local sound = soundinfo[soundname] - if sound then - if is_opinion and self.opinion_sound_cooloff > 0 then - return - end - local pitch - if not fixed_pitch then - local base_pitch = soundinfo.base_pitch - if not base_pitch then - base_pitch = 1 - end - if self.child and (not self.sounds_child) then - -- Children have higher pitch - pitch = base_pitch * 1.5 - else - pitch = base_pitch - end - -- randomize the pitch a bit - pitch = pitch + random(-10, 10) * 0.005 - end - minetest.sound_play(sound, { - object = self.object, - gain = 1.0, - max_hear_distance = self.sounds.distance, - pitch = pitch, - }, true) - self.opinion_sound_cooloff = 1 + radius = radius * 2 + + local objs = minetest.get_objects_inside_radius(pos, radius) + local obj_pos, dist + + for n = 1, #objs do + + obj_pos = objs[n]:get_pos() + + dist = vector.distance(pos, obj_pos) + if dist < 1 then dist = 1 end + + local damage = floor((4 / dist) * radius) + local ent = objs[n]:get_luaentity() + + -- punches work on entities AND players + objs[n]:punch(objs[n], 1.0, { + full_punch_interval = 1.0, + damage_groups = {fleshy = damage}, + }, pos) end end + -- Return true if object is in view_range local function object_in_range(self, object) if not object then @@ -279,190 +237,10 @@ local do_attack = function(self, player) -- TODO: Implement war_cry sound without being annoying --if random(0, 100) < 90 then - --mob_sound(self, "war_cry", true) + --self:mob_sound("war_cry", true) --end end - --- collision function borrowed amended from jordan4ibanez open_ai mod -local collision = function(self) - - local pos = self.object:get_pos() - if not pos then return {0,0} end - local vel = self.object:get_velocity() - local x = 0 - local z = 0 - local width = -self.collisionbox[1] + self.collisionbox[4] + 0.5 - for _,object in pairs(minetest.get_objects_inside_radius(pos, width)) do - - local ent = object:get_luaentity() - if object:is_player() or (ent and ent.is_mob and object ~= self.object) then - - if object:is_player() and mcl_burning.is_burning(self.object) then - mcl_burning.set_on_fire(object, 4) - end - - local pos2 = object:get_pos() - local vec = {x = pos.x - pos2.x, z = pos.z - pos2.z} - local force = (width + 0.5) - vector.distance( - {x = pos.x, y = 0, z = pos.z}, - {x = pos2.x, y = 0, z = pos2.z}) - - x = x + (vec.x * force) - z = z + (vec.z * force) - end - end - - return({x,z}) -end - --- move mob in facing direction -local set_velocity = function(self, v) - - local c_x, c_y = 0, 0 - - -- can mob be pushed, if so calculate direction - if self.pushable then - c_x, c_y = unpack(collision(self)) - end - - -- halt mob if it has been ordered to stay - if self.order == "stand" or self.order == "sit" then - self.acc=vector.new(0,0,0) - return - end - - local yaw = (self.object:get_yaw() or 0) + self.rotate - local vv = self.object:get_velocity() - if vv then - self.acc={ - x = ((sin(yaw) * -v) + c_x)*.27, - y = 0, - z = ((cos(yaw) * v) + c_y)*.27, - } - end -end - - - --- calculate mob velocity -local get_velocity = function(self) - - local v = self.object:get_velocity() - if v then - return (v.x * v.x + v.z * v.z) ^ 0.5 - end - - return 0 -end - -local function update_roll(self) - local is_Fleckenstein = self.nametag == "Fleckenstein" - local was_Fleckenstein = false - - local rot = self.object:get_rotation() - rot.z = is_Fleckenstein and pi or 0 - self.object:set_rotation(rot) - - local cbox = table.copy(self.collisionbox) - local acbox = self.object:get_properties().collisionbox - - if abs(cbox[2] - acbox[2]) > 0.1 then - was_Fleckenstein = true - end - - if is_Fleckenstein ~= was_Fleckenstein then - local pos = self.object:get_pos() - pos.y = pos.y + (acbox[2] + acbox[5]) - self.object:set_pos(pos) - end - - if is_Fleckenstein then - cbox[2], cbox[5] = -cbox[5], -cbox[2] - self.object:set_properties({collisionbox = cbox}) - -- This leads to child mobs having the wrong collisionbox - -- and seeing as it seems to be nothing but an easter egg - -- i've put it inside the if. Which just makes it be upside - -- down lol. - end - -end - --- set and return valid yaw - - -local set_yaw = function(self, yaw, delay, dtime) - if self.noyaw then return end - - if self.state ~= PATHFINDING then - self._turn_to = yaw - end - - --mcl_log("Yaw is: \t\t" .. tostring(math.deg(yaw))) - --mcl_log("self.object:get_yaw() is: \t" .. tostring(math.deg(self.object:get_yaw()))) - - --clamp our yaw to a 360 range - if math.deg(self.object:get_yaw()) > 360 then - self.object:set_yaw(math.rad(1)) - elseif math.deg(self.object:get_yaw()) < 0 then - self.object:set_yaw(math.rad(359)) - end - - --calculate the shortest way to turn to find our target - local target_shortest_path = shortest_term_of_yaw_rotatoin(self, self.object:get_yaw(), yaw, true) - - --turn in the shortest path possible toward our target. if we are attacking, don't dance. - if (math.abs(target_shortest_path) > 50 and not self._kb_turn) and (self.attack and self.attack:get_pos() or self.following and self.following:get_pos()) then - if self.following then - target_shortest_path = shortest_term_of_yaw_rotatoin(self, self.object:get_yaw(), minetest.dir_to_yaw(vector.direction(self.object:get_pos(), self.following:get_pos())), true) - else - target_shortest_path = shortest_term_of_yaw_rotatoin(self, self.object:get_yaw(), minetest.dir_to_yaw(vector.direction(self.object:get_pos(), self.attack:get_pos())), true) - end - end - - local ddtime = 0.05 --set_tick_rate - - if dtime then - ddtime = dtime - end - - if math.abs(target_shortest_path) > 280*ddtime then - if target_shortest_path > 0 then - self.object:set_yaw(self.object:get_yaw()+3.6*ddtime) - if self.acc then - self.acc=vector.rotate_around_axis(self.acc,vector.new(0,1,0), 3.6*ddtime) - end - else - self.object:set_yaw(self.object:get_yaw()-3.6*ddtime) - if self.acc then - self.acc=vector.rotate_around_axis(self.acc,vector.new(0,1,0), -3.6*ddtime) - end - end - end - - delay = delay or 0 - - yaw = self.object:get_yaw() - - if delay == 0 then - if self.shaking and dtime then - yaw = yaw + (random() * 2 - 1) * 5 * dtime - end - update_roll(self) - return yaw - end - - self.target_yaw = yaw - self.delay = delay - - return self.target_yaw -end - --- global function to set mob yaw -function mcl_mobs:yaw(self, yaw, delay, dtime) - set_yaw(self, yaw, delay, dtime) -end - local add_texture_mod = function(self, mod) local full_mod = "" local already_added = false @@ -494,32 +272,6 @@ local remove_texture_mod = function(self, mod) self.object:set_texture_mod(full_mod) end --- are we flying in what we are suppose to? (taikedz) -local flight_check = function(self) - - local nod = self.standing_in - local def = minetest.registered_nodes[nod] - - if not def then return false end -- nil check - - local fly_in - if type(self.fly_in) == "string" then - fly_in = { self.fly_in } - elseif type(self.fly_in) == "table" then - fly_in = self.fly_in - else - return false - end - - for _,checknode in pairs(fly_in) do - if nod == checknode or nod == "ignore" then - return true - end - end - - return false -end - -- set defined animation local set_animation = function(self, anim, fixed_frame) if not self.animation or not anim then @@ -534,7 +286,7 @@ local set_animation = function(self, anim, fixed_frame) end - if flight_check(self) and self.fly and anim == "walk" then anim = "fly" end + if self:flight_check() and self.fly and anim == "walk" then anim = "fly" end self._current_animation = self._current_animation or "" @@ -562,7 +314,6 @@ local set_animation = function(self, anim, fixed_frame) end end - -- above function exported for mount.lua function mcl_mobs:set_animation(self, anim) set_animation(self, anim) @@ -777,31 +528,6 @@ mcl_mobs.death_effect = function(pos, yaw, collisionbox, rotate) }, true) end -local update_tag = function(self) - local tag - if mobs_debug then - tag = "nametag = '"..tostring(self.nametag).."'\n".. - "state = '"..tostring(self.state).."'\n".. - "order = '"..tostring(self.order).."'\n".. - "attack = "..tostring(self.attack).."\n".. - "health = "..tostring(self.health).."\n".. - "breath = "..tostring(self.breath).."\n".. - "gotten = "..tostring(self.gotten).."\n".. - "tamed = "..tostring(self.tamed).."\n".. - "horny = "..tostring(self.horny).."\n".. - "hornytimer = "..tostring(self.hornytimer).."\n".. - "runaway_timer = "..tostring(self.runaway_timer).."\n".. - "following = "..tostring(self.following) - else - tag = self.nametag - end - self.object:set_properties({ - nametag = tag, - }) - - update_roll(self) -end - -- drop items local item_drop = function(self, cooked, looting_level) @@ -881,166 +607,6 @@ local item_drop = function(self, cooked, looting_level) self.drops = {} end - --- check if mob is dead or only hurt -local check_for_death = function(self, cause, cmi_cause) - - if self.state == "die" then - return true - end - - -- has health actually changed? - if self.health == self.old_health and self.health > 0 then - return false - end - - local damaged = self.health < self.old_health - self.old_health = self.health - - -- still got some health? - if self.health > 0 then - - -- make sure health isn't higher than max - if self.health > self.hp_max then - self.health = self.hp_max - end - - -- play damage sound if health was reduced and make mob flash red. - if damaged then - add_texture_mod(self, "^[colorize:#d42222:175") - minetest.after(1, function(self) - if self and self.object then - remove_texture_mod(self, "^[colorize:#d42222:175") - end - end, self) - mob_sound(self, "damage") - end - - -- backup nametag so we can show health stats - if not self.nametag2 then - self.nametag2 = self.nametag or "" - end - - if show_health - and (cmi_cause and cmi_cause.type == "punch") then - - self.htimer = 2 - self.nametag = "♥ " .. self.health .. " / " .. self.hp_max - - update_tag(self) - end - - return false - end - - mob_sound(self, "death") - - local function death_handle(self) - -- dropped cooked item if mob died in fire or lava - if cause == "lava" or cause == "fire" then - item_drop(self, true, 0) - else - local wielditem = ItemStack() - if cause == "hit" then - local puncher = cmi_cause.puncher - if puncher then - wielditem = puncher:get_wielded_item() - end - end - local cooked = mcl_burning.is_burning(self.object) or mcl_enchanting.has_enchantment(wielditem, "fire_aspect") - local looting = mcl_enchanting.get_enchantment(wielditem, "looting") - item_drop(self, cooked, looting) - - if ((not self.child) or self.type ~= "animal") and (minetest.get_us_time() - self.xp_timestamp <= 5000000) then - mcl_experience.throw_xp(self.object:get_pos(), random(self.xp_min, self.xp_max)) - end - end - end - - -- execute custom death function - if self.on_die then - - local pos = self.object:get_pos() - local on_die_exit = self.on_die(self, pos, cmi_cause) - if on_die_exit ~= true then - death_handle(self) - end - - if on_die_exit == true then - self.state = "die" - mcl_burning.extinguish(self.object) - self.object:remove() - return true - end - end - - local collisionbox - if self.collisionbox then - collisionbox = table.copy(self.collisionbox) - end - - self.state = "die" - self.attack = nil - self.v_start = false - self.fall_speed = DEFAULT_FALL_SPEED - self.timer = 0 - self.blinktimer = 0 - remove_texture_mod(self, "^[colorize:#FF000040") - remove_texture_mod(self, "^[brighten") - self.passive = true - - self.object:set_properties({ - pointable = false, - collide_with_objects = false, - }) - - set_velocity(self, 0) - local acc = self.object:get_acceleration() - acc.x, acc.y, acc.z = 0, DEFAULT_FALL_SPEED, 0 - self.object:set_acceleration(acc) - - local length - -- default death function and die animation (if defined) - if self.instant_death then - length = 0 - elseif self.animation - and self.animation.die_start - and self.animation.die_end then - - local frames = self.animation.die_end - self.animation.die_start - local speed = self.animation.die_speed or 15 - length = max(frames / speed, 0) + DEATH_DELAY - set_animation(self, "die") - else - length = 1 + DEATH_DELAY - set_animation(self, "stand", true) - end - - - -- Remove body after a few seconds and drop stuff - local kill = function(self) - if not self.object:get_luaentity() then - return - end - - death_handle(self) - local dpos = self.object:get_pos() - local cbox = self.collisionbox - local yaw = self.object:get_rotation().y - mcl_burning.extinguish(self.object) - self.object:remove() - mcl_mobs.death_effect(dpos, yaw, cbox, not self.instant_death) - end - if length <= 0 then - kill(self) - else - minetest.after(length, kill, self) - end - - return true -end - - -- check if within physical map limits (-30911 to 30927) local function within_limits(pos, radius) local wmin, wmax = -30912, 30928 @@ -1205,237 +771,6 @@ local is_at_water_danger = function(self) return false end - --- environmental damage (water, lava, fire, light etc.) -local do_env_damage = function(self) - - -- feed/tame text timer (so mob 'full' messages dont spam chat) - if self.htimer > 0 then - self.htimer = self.htimer - 1 - end - - -- reset nametag after showing health stats - if self.htimer < 1 and self.nametag2 then - - self.nametag = self.nametag2 - self.nametag2 = nil - - update_tag(self) - end - - local pos = self.object:get_pos() - - self.time_of_day = minetest.get_timeofday() - - -- remove mob if beyond map limits - if not within_limits(pos, 0) then - mcl_burning.extinguish(self.object) - self.object:remove() - return true - end - - - -- Deal light damage to mob, returns true if mob died - local deal_light_damage = function(self, pos, damage) - if not ((mcl_weather.rain.raining or mcl_weather.state == "snow") and mcl_weather.is_outdoor(pos)) then - self.health = self.health - damage - - effect(pos, 5, "mcl_particles_smoke.png") - - if check_for_death(self, "light", {type = "light"}) then - return true - end - end - end - - local sunlight = 10 - if within_limits(pos,0) then - sunlight = minetest.get_natural_light(pos, self.time_of_day) - end - - -- bright light harms mob - if self.light_damage ~= 0 and (sunlight or 0) > 12 then - if deal_light_damage(self, pos, self.light_damage) then - return true - end - end - local _, dim = mcl_worlds.y_to_layer(pos.y) - if (self.sunlight_damage ~= 0 or self.ignited_by_sunlight) and (sunlight or 0) >= minetest.LIGHT_MAX and dim == "overworld" then - if self.armor_list and not self.armor_list.helmet or not self.armor_list or self.armor_list and self.armor_list.helmet and self.armor_list.helmet == "" then - if self.ignited_by_sunlight then - mcl_burning.set_on_fire(self.object, 10) - else - deal_light_damage(self, pos, self.sunlight_damage) - return true - end - end - end - - local y_level = self.collisionbox[2] - - if self.child then - y_level = self.collisionbox[2] * 0.5 - end - - -- what is mob standing in? - pos.y = pos.y + y_level + 0.25 -- foot level - local pos2 = {x=pos.x, y=pos.y-1, z=pos.z} - self.standing_in = node_ok(pos, "air").name - self.standing_on = node_ok(pos2, "air").name - - -- don't fall when on ignore, just stand still - if self.standing_in == "ignore" then - self.object:set_velocity({x = 0, y = 0, z = 0}) - end - - local nodef = minetest.registered_nodes[self.standing_in] - - -- rain - if self.rain_damage > 0 then - if mcl_weather.rain.raining and mcl_weather.is_outdoor(pos) then - - self.health = self.health - self.rain_damage - - if check_for_death(self, "rain", {type = "environment", - pos = pos, node = self.standing_in}) then - return true - end - end - end - - pos.y = pos.y + 1 -- for particle effect position - - -- water damage - if self.water_damage > 0 - and nodef.groups.water then - - if self.water_damage ~= 0 then - - self.health = self.health - self.water_damage - - effect(pos, 5, "mcl_particles_smoke.png", nil, nil, 1, nil) - - if check_for_death(self, "water", {type = "environment", - pos = pos, node = self.standing_in}) then - return true - end - end - - -- lava damage - elseif self.lava_damage > 0 - and (nodef.groups.lava) then - - if self.lava_damage ~= 0 then - - self.health = self.health - self.lava_damage - - effect(pos, 5, "fire_basic_flame.png", nil, nil, 1, nil) - mcl_burning.set_on_fire(self.object, 10) - - if check_for_death(self, "lava", {type = "environment", - pos = pos, node = self.standing_in}) then - return true - end - end - - -- fire damage - elseif self.fire_damage > 0 - and (nodef.groups.fire) then - - if self.fire_damage ~= 0 then - - self.health = self.health - self.fire_damage - - effect(pos, 5, "fire_basic_flame.png", nil, nil, 1, nil) - mcl_burning.set_on_fire(self.object, 5) - - if check_for_death(self, "fire", {type = "environment", - pos = pos, node = self.standing_in}) then - return true - end - end - - -- damage_per_second node check - elseif nodef.damage_per_second ~= 0 and not nodef.groups.lava and not nodef.groups.fire then - - self.health = self.health - nodef.damage_per_second - - effect(pos, 5, "mcl_particles_smoke.png") - - if check_for_death(self, "dps", {type = "environment", - pos = pos, node = self.standing_in}) then - return true - end - end - - -- Drowning damage - if self.breath_max ~= -1 then - local drowning = false - if self.breathes_in_water then - if minetest.get_item_group(self.standing_in, "water") == 0 then - drowning = true - end - elseif nodef.drowning > 0 then - drowning = true - end - if drowning then - - self.breath = max(0, self.breath - 1) - - effect(pos, 2, "bubble.png", nil, nil, 1, nil) - if self.breath <= 0 then - local dmg - if nodef.drowning > 0 then - dmg = nodef.drowning - else - dmg = 4 - end - damage_effect(self, dmg) - self.health = self.health - dmg - end - if check_for_death(self, "drowning", {type = "environment", - pos = pos, node = self.standing_in}) then - return true - end - else - self.breath = min(self.breath_max, self.breath + 1) - end - end - - --- suffocation inside solid node - -- FIXME: Redundant with mcl_playerplus - if (self.suffocation == true) - and (nodef.walkable == nil or nodef.walkable == true) - and (nodef.collision_box == nil or nodef.collision_box.type == "regular") - and (nodef.node_box == nil or nodef.node_box.type == "regular") - and (nodef.groups.disable_suffocation ~= 1) - and (nodef.groups.opaque == 1) then - - -- Short grace period before starting to take suffocation damage. - -- This is different from players, who take damage instantly. - -- This has been done because mobs might briefly be inside solid nodes - -- when e.g. climbing up stairs. - -- This is a bit hacky because it assumes that do_env_damage - -- is called roughly every second only. - self.suffocation_timer = self.suffocation_timer + 1 - if self.suffocation_timer >= 3 then - -- 2 damage per second - -- TODO: Deal this damage once every 1/2 second - self.health = self.health - 2 - - if check_for_death(self, "suffocation", {type = "environment", - pos = pos, node = self.standing_in}) then - return true - end - end - else - self.suffocation_timer = 0 - end - - return check_for_death(self, "", {type = "unknown"}) -end - - -- jump if facing a solid node (not fences or gates) local do_jump = function(self) if not self.jump @@ -1450,7 +785,7 @@ local do_jump = function(self) -- something stopping us while moving? if self.state ~= "stand" - and get_velocity(self) > 0.5 + and self:get_velocity() > 0.5 and self.object:get_velocity().y ~= 0 then return false end @@ -1536,7 +871,7 @@ local do_jump = function(self) end, self, v) if self.jump_sound_cooloff <= 0 then - mob_sound(self, "jump") + self:mob_sound("jump") self.jump_sound_cooloff = 0.5 end else @@ -1553,7 +888,7 @@ local do_jump = function(self) local yaw = self.object:get_yaw() or 0 - yaw = set_yaw(self, yaw + 1.35, 8) + yaw = self:set_yaw( yaw + 1.35, 8) self.jump_count = 0 end @@ -1565,34 +900,6 @@ local do_jump = function(self) return false end - --- blast damage to entities nearby -local entity_physics = function(pos, radius) - - radius = radius * 2 - - local objs = minetest.get_objects_inside_radius(pos, radius) - local obj_pos, dist - - for n = 1, #objs do - - obj_pos = objs[n]:get_pos() - - dist = vector.distance(pos, obj_pos) - if dist < 1 then dist = 1 end - - local damage = floor((4 / dist) * radius) - local ent = objs[n]:get_luaentity() - - -- punches work on entities AND players - objs[n]:punch(objs[n], 1.0, { - full_punch_interval = 1.0, - damage_groups = {fleshy = damage}, - }, pos) - end -end - - -- should mob follow what I'm holding ? local follow_holding = function(self, clicker) if self.nofollow then return false end @@ -2075,9 +1382,9 @@ local smart_mobs = function(self, s, p, dist, dtime) self.path.following = true -- Yay, I found path! -- TODO: Implement war_cry sound without being annoying - --mob_sound(self, "war_cry", true) + --self:mob_sound("war_cry", true) else - set_velocity(self, self.walk_velocity) + self:set_velocity(self.walk_velocity) -- follow path now that it has it self.path.following = true @@ -2335,7 +1642,7 @@ local runaway_from = function(self) yaw = yaw + pi end - yaw = set_yaw(self, yaw, 4) + yaw = self:set_yaw( yaw, 4) self.state = "runaway" self.runaway_timer = 3 self.following = nil @@ -2425,19 +1732,19 @@ local follow_flop = function(self) if p.x > s.x then yaw = yaw + pi end - set_yaw(self, yaw, 2.35) + self:set_yaw( yaw, 2.35) -- anyone but standing npc's can move along if dist > 3 and self.order ~= "stand" then - set_velocity(self, self.follow_velocity) + self:set_velocity(self.follow_velocity) if self.walk_chance ~= 0 then set_animation(self, "run") end else - set_velocity(self, 0) + self:set_velocity(0) set_animation(self, "stand") end @@ -2449,7 +1756,7 @@ local follow_flop = function(self) -- swimmers flop when out of their element, and swim again when back in if self.fly then local s = self.object:get_pos() - if flight_check(self, s) == false then + if self:flight_check( s) == false then self.state = "flop" self.object:set_acceleration({x = 0, y = DEFAULT_FALL_SPEED, z = 0}) @@ -2459,7 +1766,7 @@ local follow_flop = function(self) -- Flop on ground if sdef and sdef.walkable then if self.object:get_velocity().y < 0.1 then - mob_sound(self, "flop") + self:mob_sound("flop") self.object:set_velocity({ x = random(-FLOP_HOR_SPEED, FLOP_HOR_SPEED), y = FLOP_HEIGHT, @@ -2474,7 +1781,7 @@ local follow_flop = function(self) elseif self.state == "flop" then self.state = "stand" self.object:set_acceleration({x = 0, y = 0, z = 0}) - set_velocity(self, 0) + self:set_velocity(0) end end end @@ -2515,14 +1822,14 @@ local function go_to_pos(entity,b) --self.state = "stand" return end if vector.distance(b,s) < 1 then - --set_velocity(entity,0) + --entity:set_velocity(0) return true end local v = { x = b.x - s.x, z = b.z - s.z } local yaw = (atann(v.z / v.x) + pi / 2) - entity.rotate if b.x > s.x then yaw = yaw + pi end entity.object:set_yaw(yaw) - set_velocity(entity,entity.follow_velocity) + entity:set_velocity(entity.follow_velocity) mcl_mobs:set_animation(entity, "walk") end @@ -2721,14 +2028,14 @@ local do_states = function(self, dtime) yaw = yaw + random(-0.5, 0.5) end - yaw = set_yaw(self, yaw, 8) + yaw = self:set_yaw( yaw, 8) end if self.order == "sit" then set_animation(self, "sit") - set_velocity(self, 0) + self:set_velocity(0) else set_animation(self, "stand") - set_velocity(self, 0) + self:set_velocity(0) end -- npc's ordered to stand stay standing @@ -2740,7 +2047,7 @@ local do_states = function(self, dtime) and random(1, 100) <= self.walk_chance and is_at_cliff_or_danger(self) == false then - set_velocity(self, self.walk_velocity) + self:set_velocity(self.walk_velocity) self.state = "walk" set_animation(self, "walk") end @@ -2805,8 +2112,8 @@ local do_states = function(self, dtime) if lp.x > s.x then yaw = yaw + pi end -- look towards land and move in that direction - yaw = set_yaw(self, yaw, 6) - set_velocity(self, self.walk_velocity) + yaw = self:set_yaw( yaw, 6) + self:set_velocity(self.walk_velocity) end end @@ -2817,17 +2124,17 @@ local do_states = function(self, dtime) -- Randomly turn if random(1, 100) <= 30 then yaw = yaw + random(-0.5, 0.5) - yaw = set_yaw(self, yaw, 8) + yaw = self:set_yaw( yaw, 8) end end - yaw = set_yaw(self, yaw, 8) + yaw = self:set_yaw( yaw, 8) -- otherwise randomly turn elseif random(1, 100) <= 30 then yaw = yaw + random(-0.5, 0.5) - yaw = set_yaw(self, yaw, 8) + yaw = self:set_yaw( yaw, 8) end -- stand for great fall or danger or fence in front @@ -2839,16 +2146,16 @@ local do_states = function(self, dtime) or cliff_or_danger or random(1, 100) <= 30 then - set_velocity(self, 0) + self:set_velocity(0) self.state = "stand" set_animation(self, "stand") local yaw = self.object:get_yaw() or 0 - yaw = set_yaw(self, yaw + 0.78, 8) + yaw = self:set_yaw( yaw + 0.78, 8) else - set_velocity(self, self.walk_velocity) + self:set_velocity(self.walk_velocity) - if flight_check(self) + if self:flight_check() and self.animation and self.animation.fly_start and self.animation.fly_end then @@ -2867,13 +2174,13 @@ local do_states = function(self, dtime) if self.runaway_timer > 5 or is_at_cliff_or_danger(self) then self.runaway_timer = 0 - set_velocity(self, 0) + self:set_velocity(0) self.state = "stand" set_animation(self, "stand") local yaw = self.object:get_yaw() or 0 - yaw = set_yaw(self, yaw + 0.78, 8) + yaw = self:set_yaw( yaw + 0.78, 8) else - set_velocity(self, self.run_velocity) + self:set_velocity( self.run_velocity) set_animation(self, "run") end @@ -2891,7 +2198,7 @@ local do_states = function(self, dtime) or (self.attack:is_player() and mcl_mobs.invis[ self.attack:get_player_name() ]) then self.state = "stand" - set_velocity(self, 0) + self:set_velocity( 0) set_animation(self, "stand") self.attack = nil self.v_start = false @@ -2916,7 +2223,7 @@ local do_states = function(self, dtime) if p.x > s.x then yaw = yaw + pi end - yaw = set_yaw(self, yaw, 0, dtime) + yaw = self:set_yaw( yaw, 0, dtime) local node_break_radius = self.explosion_radius or 1 local entity_damage_radius = self.explosion_damage_radius @@ -2930,7 +2237,7 @@ local do_states = function(self, dtime) self.v_start = true self.timer = 0 self.blinktimer = 0 - mob_sound(self, "fuse", nil, false) + self:mob_sound("fuse", nil, false) -- stop timer if out of reach or direct line of sight elseif self.allow_fuse_reset @@ -2946,9 +2253,9 @@ local do_states = function(self, dtime) -- walk right up to player unless the timer is active if self.v_start and (self.stop_to_explode or dist < self.reach) then - set_velocity(self, 0) + self:set_velocity( 0) else - set_velocity(self, self.run_velocity) + self:set_velocity( self.run_velocity) end if self.animation and self.animation.run_start then @@ -3011,7 +2318,7 @@ local do_states = function(self, dtime) local p_y = floor(p2.y + 1) local v = self.object:get_velocity() - if flight_check(self, s) then + if self:flight_check( s) then if me_y < p_y then @@ -3087,7 +2394,7 @@ local do_states = function(self, dtime) if p.x > s.x then yaw = yaw + pi end - yaw = set_yaw(self, yaw, 0, dtime) + yaw = self:set_yaw( yaw, 0, dtime) -- move towards enemy if beyond mob reach if dist > self.reach then @@ -3101,16 +2408,16 @@ local do_states = function(self, dtime) if is_at_cliff_or_danger(self) then - set_velocity(self, 0) + self:set_velocity( 0) set_animation(self, "stand") local yaw = self.object:get_yaw() or 0 - yaw = set_yaw(self, yaw + 0.78, 8) + yaw = self:set_yaw( yaw + 0.78, 8) else if self.path.stuck then - set_velocity(self, self.walk_velocity) + self:set_velocity( self.walk_velocity) else - set_velocity(self, self.run_velocity) + self:set_velocity( self.run_velocity) end if self.animation and self.animation.run_start then @@ -3126,7 +2433,7 @@ local do_states = function(self, dtime) self.path.stuck_timer = 0 self.path.following = false -- not stuck anymore - set_velocity(self, 0) + self:set_velocity( 0) if not self.custom_attack then @@ -3150,7 +2457,7 @@ local do_states = function(self, dtime) if line_of_sight(self, p2, s2) == true then -- play attack sound - mob_sound(self, "attack") + self:mob_sound("attack") -- punch player (or what player is attached to) local attached = self.attack:get_attach() @@ -3192,7 +2499,7 @@ local do_states = function(self, dtime) if p.x > s.x then yaw = yaw + pi end - yaw = set_yaw(self, yaw, 0, dtime) + yaw = self:set_yaw( yaw, 0, dtime) local stay_away_from_player = vector.new(0,0,0) @@ -3213,7 +2520,7 @@ local do_states = function(self, dtime) end self.acc = vector.add(vector.multiply(vector.rotate_around_axis(vector.direction(s, p), vector.new(0,1,0), self.strafe_direction), 0.3*self.walk_velocity), stay_away_from_player) else - set_velocity(self, 0) + self:set_velocity( 0) end local p = self.object:get_pos() @@ -3228,7 +2535,7 @@ local do_states = function(self, dtime) set_animation(self, "shoot") -- play shoot attack sound - mob_sound(self, "shoot_attack") + self:mob_sound("shoot_attack") -- Shoot arrow if minetest.registered_entities[self.arrow] then @@ -3600,141 +2907,12 @@ local function check_herd(self,dtime) if p then go_to_pos(self,p) elseif y then - set_yaw(self,y) + self:set_yaw(y) end end end end -local function damage_mob(self,reason,damage) - if not self.health then return end - damage = floor(damage) - if damage > 0 then - self.health = self.health - damage - - effect(self.object:get_pos(), 5, "mcl_particles_smoke.png", 1, 2, 2, nil) - - if check_for_death(self, reason, {type = reason}) then - return true - end - end -end - -local function check_entity_cramming(self) - local p = self.object:get_pos() - if not p then return end - local oo = minetest.get_objects_inside_radius(p,1) - local mobs = {} - for _,o in pairs(oo) do - local l = o:get_luaentity() - if l and l.is_mob and l.health > 0 then table.insert(mobs,l) end - end - local clear = #mobs < ENTITY_CRAMMING_MAX - local ncram = {} - for _,l in pairs(mobs) do - if l then - if clear then - l.cram = nil - elseif l.cram == nil and not self.child then - table.insert(ncram,l) - elseif l.cram then - damage_mob(l,"cramming",CRAMMING_DAMAGE) - end - end - end - for i,l in pairs(ncram) do - if i > ENTITY_CRAMMING_MAX then - l.cram = true - else - l.cram = nil - end - end -end - --- falling and fall damage --- returns true if mob died -local falling = function(self, pos) - - if self.fly and self.state ~= "die" then - return - end - - if mcl_portals ~= nil then - if mcl_portals.nether_portal_cooloff(self.object) then - return false -- mob has teleported through Nether portal - it's 99% not falling - end - end - - -- floating in water (or falling) - local v = self.object:get_velocity() - - if v.y > 0 then - - -- apply gravity when moving up - self.object:set_acceleration({ - x = 0, - y = DEFAULT_FALL_SPEED, - z = 0 - }) - - elseif v.y <= 0 and v.y > self.fall_speed then - - -- fall downwards at set speed - self.object:set_acceleration({ - x = 0, - y = self.fall_speed, - z = 0 - }) - else - -- stop accelerating once max fall speed hit - self.object:set_acceleration({x = 0, y = 0, z = 0}) - end - - if minetest.registered_nodes[node_ok(pos).name].groups.lava then - - if self.floats_on_lava == 1 then - - self.object:set_acceleration({ - x = 0, - y = -self.fall_speed / (max(1, v.y) ^ 2), - z = 0 - }) - end - end - - -- in water then float up - if minetest.registered_nodes[node_ok(pos).name].groups.water then - - if self.floats == 1 then - - self.object:set_acceleration({ - x = 0, - y = -self.fall_speed / (max(1, v.y) ^ 2), - z = 0 - }) - end - else - - -- fall damage onto solid ground - if self.fall_damage == 1 - and self.object:get_velocity().y == 0 then - local n = node_ok(vector.offset(pos,0,-1,0)).name - local d = (self.old_y or 0) - self.object:get_pos().y - - if d > 5 and n ~= "air" and n ~= "ignore" then - local add = minetest.get_item_group(self.standing_on, "fall_damage_add_percent") - local damage = d - 5 - if add ~= 0 then - damage = damage + damage * (add/100) - end - damage_mob(self,"fall",damage) - end - - self.old_y = self.object:get_pos().y - end - end -end - local teleport = function(self, target) if self.do_teleport then if self.do_teleport(self, target) == false then @@ -3888,7 +3066,7 @@ local mob_punch = function(self, hitter, tflp, tool_capabilities, dir) self.health = self.health - damage -- skip future functions if dead, except alerting others - if check_for_death(self, "hit", {type = "punch", puncher = hitter}) then + if self:check_for_death( "hit", {type = "punch", puncher = hitter}) then die = true end end @@ -3960,11 +3138,11 @@ local mob_punch = function(self, hitter, tflp, tool_capabilities, dir) -- if skittish then run away if hitter and is_player and hitter:get_pos() and not die and self.runaway == true and self.state ~= "flop" then - local yaw = set_yaw(self, minetest.dir_to_yaw(vector.direction(hitter:get_pos(), self.object:get_pos()))) + local yaw = self:set_yaw( minetest.dir_to_yaw(vector.direction(hitter:get_pos(), self.object:get_pos()))) minetest.after(0.2,function() if self and self.object and self.object:get_pos() and hitter and is_player and hitter:get_pos() then - yaw = set_yaw(self, minetest.dir_to_yaw(vector.direction(hitter:get_pos(), self.object:get_pos()))) - set_velocity(self, self.run_velocity) + yaw = self:set_yaw( minetest.dir_to_yaw(vector.direction(hitter:get_pos(), self.object:get_pos()))) + self:set_velocity( self.run_velocity) end end) self.state = "runaway" @@ -4238,8 +3416,8 @@ local mob_activate = function(self, staticdata, def, dtime) -- set anything changed above self.object:set_properties(self) - set_yaw(self, (random(0, 360) - 180) / 180 * pi, 6) - update_tag(self) + self:set_yaw( (random(0, 360) - 180) / 180 * pi, 6) + self:update_tag() self._current_animation = nil set_animation(self, "stand") @@ -4311,7 +3489,7 @@ local mob_step = function(self, dtime) local v = self.object:get_velocity() local d = 0.85 - if (self.state and self.state=="die" or check_for_death(self)) and not self.animation.die_end then + if (self.state and self.state=="die" or self:check_for_death()) and not self.animation.die_end then d = 0.92 local rot = self.object:get_rotation() rot.z = ((pi/2-rot.z)*.2)+rot.z @@ -4327,7 +3505,7 @@ local mob_step = function(self, dtime) self.object:set_velocity(vector.new(0,0,0)) end if acc.y == 0 and node_under == "air" then - falling(self, pos) + self:falling(pos) end return end @@ -4349,7 +3527,7 @@ local mob_step = function(self, dtime) local yaw = 0 if mobs_debug then - update_tag(self) + self:update_tag() end if self.state == "die" then @@ -4362,7 +3540,7 @@ local mob_step = function(self, dtime) if self.opinion_sound_cooloff > 0 then self.opinion_sound_cooloff = self.opinion_sound_cooloff - dtime end - if falling(self, pos) then + if self:falling(pos) then -- Return if mob died after falling return end @@ -4394,7 +3572,7 @@ local mob_step = function(self, dtime) -- smooth rotation by ThomasMonroe314 if self._turn_to then - set_yaw(self, self._turn_to, .1) + self:set_yaw( self._turn_to, .1) end if self.delay and self.delay > 0 then @@ -4434,7 +3612,7 @@ local mob_step = function(self, dtime) yaw = yaw + (random() * 2 - 1) * 5 * dtime end self.object:set_yaw(yaw) - update_roll(self) + self:update_roll() end -- end rotation @@ -4545,7 +3723,7 @@ local mob_step = function(self, dtime) -- mob plays random sound at times if random(1, 70) == 1 then - mob_sound(self, "random", true) + self:mob_sound("random", true) end -- environmental damage timer (every 1 second) @@ -4553,11 +3731,11 @@ local mob_step = function(self, dtime) if (self.state == "attack" and self.env_damage_timer > 1) or self.state ~= "attack" then - check_entity_cramming(self) + self:check_entity_cramming() self.env_damage_timer = 0 -- check for environmental damage (water, fire, lava etc.) - if do_env_damage(self) then + if self:do_env_damage() then return end @@ -4587,11 +3765,11 @@ local mob_step = function(self, dtime) if is_at_water_danger(self) and self.state ~= "attack" then if random(1, 10) <= 6 then - set_velocity(self, 0) + self:set_velocity(0) self.state = "stand" set_animation(self, "stand") yaw = yaw + random(-0.5, 0.5) - yaw = set_yaw(self, yaw, 8) + yaw = self:set_yaw( yaw, 8) end else if self.move_in_group ~= false then @@ -4638,11 +3816,11 @@ local mob_step = function(self, dtime) end if is_at_cliff_or_danger(self) then - set_velocity(self, 0) + self:set_velocity(0) self.state = "stand" set_animation(self, "stand") local yaw = self.object:get_yaw() or 0 - yaw = set_yaw(self, yaw + 0.78, 8) + yaw = self:set_yaw( yaw + 0.78, 8) end end @@ -4675,7 +3853,7 @@ local on_rightclick_prefix = function(self, clicker) end self.nametag = tag - update_tag(self) + self:update_tag() if not minetest.is_creative_enabled(clicker:get_player_name()) then item:take_item() @@ -5260,7 +4438,7 @@ function mcl_mobs:feed_tame(self, clicker, feed_count, breed, tame, notake) end end - update_tag(self) + self:update_tag() -- play a sound if the animal used the item and take the item if not in creative if consume_food then -- don't consume food if clicker is in creative @@ -5270,11 +4448,11 @@ function mcl_mobs:feed_tame(self, clicker, feed_count, breed, tame, notake) clicker:set_wielded_item(item) end -- always play the eat sound if food is used, even in creative - mob_sound(self, "eat", nil, true) + self:mob_sound("eat", nil, true) else -- make sound when the mob doesn't want food - mob_sound(self, "random", true) + self:mob_sound("random", true) end return true end diff --git a/mods/ENTITIES/mcl_mobs/effects.lua b/mods/ENTITIES/mcl_mobs/effects.lua new file mode 100644 index 000000000..239f2baa3 --- /dev/null +++ b/mods/ENTITIES/mcl_mobs/effects.lua @@ -0,0 +1,43 @@ +local math, vector, minetest, mcl_mobs = math, vector, minetest, mcl_mobs +local mob_class = mcl_mobs.mob_class +-- play sound +function mob_class:mob_sound(soundname, is_opinion, fixed_pitch) + + local soundinfo + if self.sounds_child and self.child then + soundinfo = self.sounds_child + elseif self.sounds then + soundinfo = self.sounds + end + if not soundinfo then + return + end + local sound = soundinfo[soundname] + if sound then + if is_opinion and self.opinion_sound_cooloff > 0 then + return + end + local pitch + if not fixed_pitch then + local base_pitch = soundinfo.base_pitch + if not base_pitch then + base_pitch = 1 + end + if self.child and (not self.sounds_child) then + -- Children have higher pitch + pitch = base_pitch * 1.5 + else + pitch = base_pitch + end + -- randomize the pitch a bit + pitch = pitch + math.random(-10, 10) * 0.005 + end + minetest.sound_play(sound, { + object = self.object, + gain = 1.0, + max_hear_distance = self.sounds.distance, + pitch = pitch, + }, true) + self.opinion_sound_cooloff = 1 + end +end diff --git a/mods/ENTITIES/mcl_mobs/init.lua b/mods/ENTITIES/mcl_mobs/init.lua index 69246b470..74b07efd9 100644 --- a/mods/ENTITIES/mcl_mobs/init.lua +++ b/mods/ENTITIES/mcl_mobs/init.lua @@ -1,6 +1,11 @@ - +mcl_mobs = {} +mcl_mobs.mob_class = {} +mcl_mobs.mob_class_meta = {__index = mcl_mobs.mob_class} local path = minetest.get_modpath(minetest.get_current_modname()) +dofile(path .. "/effects.lua") +dofile(path .. "/physics.lua") + -- Mob API dofile(path .. "/api.lua") @@ -11,4 +16,4 @@ dofile(path .. "/spawning.lua") dofile(path .. "/mount.lua") -- Mob Items -dofile(path .. "/crafts.lua") \ No newline at end of file +dofile(path .. "/crafts.lua") diff --git a/mods/ENTITIES/mcl_mobs/physics.lua b/mods/ENTITIES/mcl_mobs/physics.lua new file mode 100644 index 000000000..2f2402974 --- /dev/null +++ b/mods/ENTITIES/mcl_mobs/physics.lua @@ -0,0 +1,799 @@ +local math, vector, minetest, mcl_mobs = math, vector, minetest, mcl_mobs +local mob_class = mcl_mobs.mob_class + +local ENTITY_CRAMMING_MAX = 24 +local CRAMMING_DAMAGE = 3 +local DEFAULT_FALL_SPEED = -9.81*1.5 + + +-- get node but use fallback for nil or unknown +local node_ok = function(pos, fallback) + + fallback = fallback or mcl_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 + +-- check if within physical map limits (-30911 to 30927) +local function within_limits(pos, radius) + local wmin, wmax = -30912, 30928 + if mcl_vars then + if mcl_vars.mapgen_edge_min and mcl_vars.mapgen_edge_max then + wmin, wmax = mcl_vars.mapgen_edge_min, mcl_vars.mapgen_edge_max + end + end + if radius then + wmin = wmin - radius + wmax = wmax + radius + end + for _,v in pairs(pos) do + if v < wmin or v > wmax then return false end + end + return true +end + +-- collision function borrowed amended from jordan4ibanez open_ai mod +function mob_class:collision() + local pos = self.object:get_pos() + if not pos then return {0,0} end + local vel = self.object:get_velocity() + local x = 0 + local z = 0 + local width = -self.collisionbox[1] + self.collisionbox[4] + 0.5 + for _,object in pairs(minetest.get_objects_inside_radius(pos, width)) do + + local ent = object:get_luaentity() + if object:is_player() or (ent and ent.is_mob and object ~= self.object) then + + if object:is_player() and mcl_burning.is_burning(self.object) then + mcl_burning.set_on_fire(object, 4) + end + + local pos2 = object:get_pos() + local vec = {x = pos.x - pos2.x, z = pos.z - pos2.z} + local force = (width + 0.5) - vector.distance( + {x = pos.x, y = 0, z = pos.z}, + {x = pos2.x, y = 0, z = pos2.z}) + + x = x + (vec.x * force) + z = z + (vec.z * force) + end + end + + return({x,z}) +end + +-- move mob in facing direction +function mob_class:set_velocity( v) + + local c_x, c_y = 0, 0 + + -- can mob be pushed, if so calculate direction + if self.pushable then + c_x, c_y = unpack(self:collision()) + end + + -- halt mob if it has been ordered to stay + if self.order == "stand" or self.order == "sit" then + self.acc=vector.new(0,0,0) + return + end + + local yaw = (self.object:get_yaw() or 0) + self.rotate + local vv = self.object:get_velocity() + if vv then + self.acc={ + x = ((math.sin(yaw) * -v) + c_x)*.27, + y = 0, + z = ((math.cos(yaw) * v) + c_y)*.27, + } + end +end + +-- calculate mob velocity +function mob_class:get_velocity() + + local v = self.object:get_velocity() + if v then + return (v.x * v.x + v.z * v.z) ^ 0.5 + end + + return 0 +end + +function mob_class:update_roll() + local is_Fleckenstein = self.nametag == "Fleckenstein" + local was_Fleckenstein = false + + local rot = self.object:get_rotation() + rot.z = is_Fleckenstein and pi or 0 + self.object:set_rotation(rot) + + local cbox = table.copy(self.collisionbox) + local acbox = self.object:get_properties().collisionbox + + if math.abs(cbox[2] - acbox[2]) > 0.1 then + was_Fleckenstein = true + end + + if is_Fleckenstein ~= was_Fleckenstein then + local pos = self.object:get_pos() + pos.y = pos.y + (acbox[2] + acbox[5]) + self.object:set_pos(pos) + end + + if is_Fleckenstein then + cbox[2], cbox[5] = -cbox[5], -cbox[2] + self.object:set_properties({collisionbox = cbox}) + -- This leads to child mobs having the wrong collisionbox + -- and seeing as it seems to be nothing but an easter egg + -- i've put it inside the if. Which just makes it be upside + -- down lol. + end + +end + +function mob_class:update_tag() + local tag + if mobs_debug then + tag = "nametag = '"..tostring(self.nametag).."'\n".. + "state = '"..tostring(self.state).."'\n".. + "order = '"..tostring(self.order).."'\n".. + "attack = "..tostring(self.attack).."\n".. + "health = "..tostring(self.health).."\n".. + "breath = "..tostring(self.breath).."\n".. + "gotten = "..tostring(self.gotten).."\n".. + "tamed = "..tostring(self.tamed).."\n".. + "horny = "..tostring(self.horny).."\n".. + "hornytimer = "..tostring(self.hornytimer).."\n".. + "runaway_timer = "..tostring(self.runaway_timer).."\n".. + "following = "..tostring(self.following) + else + tag = self.nametag + end + self.object:set_properties({ + nametag = tag, + }) + + self:update_roll() +end + +local function shortest_term_of_yaw_rotatoin(self, rot_origin, rot_target, nums) + if not rot_origin or not rot_target then + return + end + + rot_origin = math.deg(rot_origin) + rot_target = math.deg(rot_target) + + if math.abs(rot_target - rot_origin) < 180 then + return rot_target - rot_origin + else + if (rot_target - rot_origin) > 0 then + return 360-(rot_target - rot_origin) + else + return (rot_target - rot_origin)+360 + end + end +end + + +-- set and return valid yaw +function mob_class:set_yaw(yaw, delay, dtime) + if self.noyaw then return end + + if self.state ~= PATHFINDING then + self._turn_to = yaw + end + + --mcl_log("Yaw is: \t\t" .. tostring(math.deg(yaw))) + --mcl_log("self.object:get_yaw() is: \t" .. tostring(math.deg(self.object:get_yaw()))) + + --clamp our yaw to a 360 range + if math.deg(self.object:get_yaw()) > 360 then + self.object:set_yaw(math.rad(1)) + elseif math.deg(self.object:get_yaw()) < 0 then + self.object:set_yaw(math.rad(359)) + end + + --calculate the shortest way to turn to find our target + local target_shortest_path = shortest_term_of_yaw_rotatoin(self, self.object:get_yaw(), yaw, true) + + --turn in the shortest path possible toward our target. if we are attacking, don't dance. + if (math.abs(target_shortest_path) > 50 and not self._kb_turn) and (self.attack and self.attack:get_pos() or self.following and self.following:get_pos()) then + if self.following then + target_shortest_path = shortest_term_of_yaw_rotatoin(self, self.object:get_yaw(), minetest.dir_to_yaw(vector.direction(self.object:get_pos(), self.following:get_pos())), true) + else + target_shortest_path = shortest_term_of_yaw_rotatoin(self, self.object:get_yaw(), minetest.dir_to_yaw(vector.direction(self.object:get_pos(), self.attack:get_pos())), true) + end + end + + local ddtime = 0.05 --set_tick_rate + + if dtime then + ddtime = dtime + end + + if math.abs(target_shortest_path) > 280*ddtime then + if target_shortest_path > 0 then + self.object:set_yaw(self.object:get_yaw()+3.6*ddtime) + if self.acc then + self.acc=vector.rotate_around_axis(self.acc,vector.new(0,1,0), 3.6*ddtime) + end + else + self.object:set_yaw(self.object:get_yaw()-3.6*ddtime) + if self.acc then + self.acc=vector.rotate_around_axis(self.acc,vector.new(0,1,0), -3.6*ddtime) + end + end + end + + delay = delay or 0 + + yaw = self.object:get_yaw() + + if delay == 0 then + if self.shaking and dtime then + yaw = yaw + (random() * 2 - 1) * 5 * dtime + end + self:update_roll() + return yaw + end + + self.target_yaw = yaw + self.delay = delay + + return self.target_yaw +end + +-- global function to set mob yaw +function mcl_mobs.yaw(self, yaw, delay, dtime) + return mob_class.set_yaw(self, yaw, delay, dtime) +end + +-- are we flying in what we are suppose to? (taikedz) +function mob_class:flight_check() + + local nod = self.standing_in + local def = minetest.registered_nodes[nod] + + if not def then return false end -- nil check + + local fly_in + if type(self.fly_in) == "string" then + fly_in = { self.fly_in } + elseif type(self.fly_in) == "table" then + fly_in = self.fly_in + else + return false + end + + for _,checknode in pairs(fly_in) do + if nod == checknode or nod == "ignore" then + return true + end + end + + return false +end + +-- check if mob is dead or only hurt +function mob_class:check_for_death(cause, cmi_cause) + + if self.state == "die" then + return true + end + + -- has health actually changed? + if self.health == self.old_health and self.health > 0 then + return false + end + + local damaged = self.health < self.old_health + self.old_health = self.health + + -- still got some health? + if self.health > 0 then + + -- make sure health isn't higher than max + if self.health > self.hp_max then + self.health = self.hp_max + end + + -- play damage sound if health was reduced and make mob flash red. + if damaged then + add_texture_mod(self, "^[colorize:#d42222:175") + minetest.after(1, function(self) + if self and self.object then + remove_texture_mod(self, "^[colorize:#d42222:175") + end + end, self) + self:mob_sound("damage") + end + + -- backup nametag so we can show health stats + if not self.nametag2 then + self.nametag2 = self.nametag or "" + end + + if show_health + and (cmi_cause and cmi_cause.type == "punch") then + + self.htimer = 2 + self.nametag = "♥ " .. self.health .. " / " .. self.hp_max + + self:update_tag() + end + + return false + end + + self:mob_sound("death") + + local function death_handle(self) + -- dropped cooked item if mob died in fire or lava + if cause == "lava" or cause == "fire" then + item_drop(self, true, 0) + else + local wielditem = ItemStack() + if cause == "hit" then + local puncher = cmi_cause.puncher + if puncher then + wielditem = puncher:get_wielded_item() + end + end + local cooked = mcl_burning.is_burning(self.object) or mcl_enchanting.has_enchantment(wielditem, "fire_aspect") + local looting = mcl_enchanting.get_enchantment(wielditem, "looting") + item_drop(self, cooked, looting) + + if ((not self.child) or self.type ~= "animal") and (minetest.get_us_time() - self.xp_timestamp <= 5000000) then + mcl_experience.throw_xp(self.object:get_pos(), random(self.xp_min, self.xp_max)) + end + end + end + + -- execute custom death function + if self.on_die then + + local pos = self.object:get_pos() + local on_die_exit = self.on_die(self, pos, cmi_cause) + if on_die_exit ~= true then + death_handle(self) + end + + if on_die_exit == true then + self.state = "die" + mcl_burning.extinguish(self.object) + self.object:remove() + return true + end + end + + local collisionbox + if self.collisionbox then + collisionbox = table.copy(self.collisionbox) + end + + self.state = "die" + self.attack = nil + self.v_start = false + self.fall_speed = DEFAULT_FALL_SPEED + self.timer = 0 + self.blinktimer = 0 + remove_texture_mod(self, "^[colorize:#FF000040") + remove_texture_mod(self, "^[brighten") + self.passive = true + + self.object:set_properties({ + pointable = false, + collide_with_objects = false, + }) + + set_velocity(self, 0) + local acc = self.object:get_acceleration() + acc.x, acc.y, acc.z = 0, DEFAULT_FALL_SPEED, 0 + self.object:set_acceleration(acc) + + local length + -- default death function and die animation (if defined) + if self.instant_death then + length = 0 + elseif self.animation + and self.animation.die_start + and self.animation.die_end then + + local frames = self.animation.die_end - self.animation.die_start + local speed = self.animation.die_speed or 15 + length = math.max(frames / speed, 0) + DEATH_DELAY + set_animation(self, "die") + else + length = 1 + DEATH_DELAY + set_animation(self, "stand", true) + end + + + -- Remove body after a few seconds and drop stuff + local kill = function(self) + if not self.object:get_luaentity() then + return + end + + death_handle(self) + local dpos = self.object:get_pos() + local cbox = self.collisionbox + local yaw = self.object:get_rotation().y + mcl_burning.extinguish(self.object) + self.object:remove() + mcl_mobs.death_effect(dpos, yaw, cbox, not self.instant_death) + end + if length <= 0 then + kill(self) + else + minetest.after(length, kill, self) + end + + return true +end + +-- environmental damage (water, lava, fire, light etc.) +function mob_class:do_env_damage() + + -- feed/tame text timer (so mob 'full' messages dont spam chat) + if self.htimer > 0 then + self.htimer = self.htimer - 1 + end + + -- reset nametag after showing health stats + if self.htimer < 1 and self.nametag2 then + + self.nametag = self.nametag2 + self.nametag2 = nil + + self:update_tag() + end + + local pos = self.object:get_pos() + + self.time_of_day = minetest.get_timeofday() + + -- remove mob if beyond map limits + if not within_limits(pos, 0) then + mcl_burning.extinguish(self.object) + self.object:remove() + return true + end + + -- Deal light damage to mob, returns true if mob died + local function deal_light_damage(self, pos, damage) + if not ((mcl_weather.rain.raining or mcl_weather.state == "snow") and mcl_weather.is_outdoor(pos)) then + self.health = self.health - damage + + effect(pos, 5, "mcl_particles_smoke.png") + + if self:check_for_death("light", {type = "light"}) then + return true + end + end + end + + local sunlight = 10 + if within_limits(pos,0) then + sunlight = minetest.get_natural_light(pos, self.time_of_day) + end + + -- bright light harms mob + if self.light_damage ~= 0 and (sunlight or 0) > 12 then + if deal_light_damage(self, pos, self.light_damage) then + return true + end + end + local _, dim = mcl_worlds.y_to_layer(pos.y) + if (self.sunlight_damage ~= 0 or self.ignited_by_sunlight) and (sunlight or 0) >= minetest.LIGHT_MAX and dim == "overworld" then + if self.armor_list and not self.armor_list.helmet or not self.armor_list or self.armor_list and self.armor_list.helmet and self.armor_list.helmet == "" then + if self.ignited_by_sunlight then + mcl_burning.set_on_fire(self.object, 10) + else + deal_light_damage(self, pos, self.sunlight_damage) + return true + end + end + end + + local y_level = self.collisionbox[2] + + if self.child then + y_level = self.collisionbox[2] * 0.5 + end + + -- what is mob standing in? + pos.y = pos.y + y_level + 0.25 -- foot level + local pos2 = {x=pos.x, y=pos.y-1, z=pos.z} + self.standing_in = node_ok(pos, "air").name + self.standing_on = node_ok(pos2, "air").name + + -- don't fall when on ignore, just stand still + if self.standing_in == "ignore" then + self.object:set_velocity({x = 0, y = 0, z = 0}) + end + + local nodef = minetest.registered_nodes[self.standing_in] + + -- rain + if self.rain_damage > 0 then + if mcl_weather.rain.raining and mcl_weather.is_outdoor(pos) then + + self.health = self.health - self.rain_damage + + if self:check_for_death("rain", {type = "environment", + pos = pos, node = self.standing_in}) then + return true + end + end + end + + pos.y = pos.y + 1 -- for particle effect position + + -- water damage + if self.water_damage > 0 + and nodef.groups.water then + + if self.water_damage ~= 0 then + + self.health = self.health - self.water_damage + + effect(pos, 5, "mcl_particles_smoke.png", nil, nil, 1, nil) + + if self:check_for_death("water", {type = "environment", + pos = pos, node = self.standing_in}) then + return true + end + end + + -- lava damage + elseif self.lava_damage > 0 + and (nodef.groups.lava) then + + if self.lava_damage ~= 0 then + + self.health = self.health - self.lava_damage + + effect(pos, 5, "fire_basic_flame.png", nil, nil, 1, nil) + mcl_burning.set_on_fire(self.object, 10) + + if self:check_for_death("lava", {type = "environment", + pos = pos, node = self.standing_in}) then + return true + end + end + + -- fire damage + elseif self.fire_damage > 0 + and (nodef.groups.fire) then + + if self.fire_damage ~= 0 then + + self.health = self.health - self.fire_damage + + effect(pos, 5, "fire_basic_flame.png", nil, nil, 1, nil) + mcl_burning.set_on_fire(self.object, 5) + + if self:check_for_death("fire", {type = "environment", + pos = pos, node = self.standing_in}) then + return true + end + end + + -- damage_per_second node check + elseif nodef.damage_per_second ~= 0 and not nodef.groups.lava and not nodef.groups.fire then + + self.health = self.health - nodef.damage_per_second + + effect(pos, 5, "mcl_particles_smoke.png") + + if self:check_for_death("dps", {type = "environment", + pos = pos, node = self.standing_in}) then + return true + end + end + + -- Drowning damage + if self.breath_max ~= -1 then + local drowning = false + if self.breathes_in_water then + if minetest.get_item_group(self.standing_in, "water") == 0 then + drowning = true + end + elseif nodef.drowning > 0 then + drowning = true + end + if drowning then + + self.breath = math.max(0, self.breath - 1) + + effect(pos, 2, "bubble.png", nil, nil, 1, nil) + if self.breath <= 0 then + local dmg + if nodef.drowning > 0 then + dmg = nodef.drowning + else + dmg = 4 + end + damage_effect(self, dmg) + self.health = self.health - dmg + end + if self:check_for_death("drowning", {type = "environment", + pos = pos, node = self.standing_in}) then + return true + end + else + self.breath = math.min(self.breath_max, self.breath + 1) + end + end + + --- suffocation inside solid node + -- FIXME: Redundant with mcl_playerplus + if (self.suffocation == true) + and (nodef.walkable == nil or nodef.walkable == true) + and (nodef.collision_box == nil or nodef.collision_box.type == "regular") + and (nodef.node_box == nil or nodef.node_box.type == "regular") + and (nodef.groups.disable_suffocation ~= 1) + and (nodef.groups.opaque == 1) then + + -- Short grace period before starting to take suffocation damage. + -- This is different from players, who take damage instantly. + -- This has been done because mobs might briefly be inside solid nodes + -- when e.g. climbing up stairs. + -- This is a bit hacky because it assumes that do_env_damage + -- is called roughly every second only. + self.suffocation_timer = self.suffocation_timer + 1 + if self.suffocation_timer >= 3 then + -- 2 damage per second + -- TODO: Deal this damage once every 1/2 second + self.health = self.health - 2 + + if self:check_for_death("suffocation", {type = "environment", + pos = pos, node = self.standing_in}) then + return true + end + end + else + self.suffocation_timer = 0 + end + + return self:check_for_death("", {type = "unknown"}) +end + +function mob_class:damage_mob(reason,damage) + if not self.health then return end + damage = floor(damage) + if damage > 0 then + self.health = self.health - damage + + effect(self.object:get_pos(), 5, "mcl_particles_smoke.png", 1, 2, 2, nil) + + if self:check_for_death(reason, {type = reason}) then + return true + end + end +end + +function mob_class:check_entity_cramming() + local p = self.object:get_pos() + if not p then return end + local oo = minetest.get_objects_inside_radius(p,1) + local mobs = {} + for _,o in pairs(oo) do + local l = o:get_luaentity() + if l and l.is_mob and l.health > 0 then table.insert(mobs,l) end + end + local clear = #mobs < ENTITY_CRAMMING_MAX + local ncram = {} + for _,l in pairs(mobs) do + if l then + if clear then + l.cram = nil + elseif l.cram == nil and not self.child then + table.insert(ncram,l) + elseif l.cram then + l:damage_mob("cramming",CRAMMING_DAMAGE) + end + end + end + for i,l in pairs(ncram) do + if i > ENTITY_CRAMMING_MAX then + l.cram = true + else + l.cram = nil + end + end +end + +-- falling and fall damage +-- returns true if mob died +function mob_class:falling(pos) + + if self.fly and self.state ~= "die" then + return + end + + if mcl_portals ~= nil then + if mcl_portals.nether_portal_cooloff(self.object) then + return false -- mob has teleported through Nether portal - it's 99% not falling + end + end + + -- floating in water (or falling) + local v = self.object:get_velocity() + + if v.y > 0 then + + -- apply gravity when moving up + self.object:set_acceleration({ + x = 0, + y = DEFAULT_FALL_SPEED, + z = 0 + }) + + elseif v.y <= 0 and v.y > self.fall_speed then + + -- fall downwards at set speed + self.object:set_acceleration({ + x = 0, + y = self.fall_speed, + z = 0 + }) + else + -- stop accelerating once max fall speed hit + self.object:set_acceleration({x = 0, y = 0, z = 0}) + end + + if minetest.registered_nodes[node_ok(pos).name].groups.lava then + + if self.floats_on_lava == 1 then + + self.object:set_acceleration({ + x = 0, + y = -self.fall_speed / (math.max(1, v.y) ^ 2), + z = 0 + }) + end + end + + -- in water then float up + if minetest.registered_nodes[node_ok(pos).name].groups.water then + + if self.floats == 1 then + + self.object:set_acceleration({ + x = 0, + y = -self.fall_speed / (math.max(1, v.y) ^ 2), + z = 0 + }) + end + else + + -- fall damage onto solid ground + if self.fall_damage == 1 + and self.object:get_velocity().y == 0 then + local n = node_ok(vector.offset(pos,0,-1,0)).name + local d = (self.old_y or 0) - self.object:get_pos().y + + if d > 5 and n ~= "air" and n ~= "ignore" then + local add = minetest.get_item_group(self.standing_on, "fall_damage_add_percent") + local damage = d - 5 + if add ~= 0 then + damage = damage + damage * (add/100) + end + self:damage_mob("fall",damage) + end + + self.old_y = self.object:get_pos().y + end + end +end